From b8c756ecdd7cced1db4300935484e8c83701c82e Mon Sep 17 00:00:00 2001 From: WuKong Date: Tue, 30 Jun 2015 18:47:29 +0200 Subject: migrate moon code from github to opnfv Change-Id: Ice53e368fd1114d56a75271aa9f2e598e3eba604 Signed-off-by: WuKong --- keystone-moon/keystone/__init__.py | 0 keystone-moon/keystone/assignment/__init__.py | 17 + .../keystone/assignment/backends/__init__.py | 0 keystone-moon/keystone/assignment/backends/ldap.py | 531 ++ keystone-moon/keystone/assignment/backends/sql.py | 415 ++ keystone-moon/keystone/assignment/controllers.py | 816 +++ keystone-moon/keystone/assignment/core.py | 1019 ++++ .../keystone/assignment/role_backends/__init__.py | 0 .../keystone/assignment/role_backends/ldap.py | 125 + .../keystone/assignment/role_backends/sql.py | 80 + keystone-moon/keystone/assignment/routers.py | 246 + keystone-moon/keystone/assignment/schema.py | 32 + keystone-moon/keystone/auth/__init__.py | 17 + keystone-moon/keystone/auth/controllers.py | 647 +++ keystone-moon/keystone/auth/core.py | 94 + keystone-moon/keystone/auth/plugins/__init__.py | 15 + keystone-moon/keystone/auth/plugins/core.py | 186 + keystone-moon/keystone/auth/plugins/external.py | 186 + keystone-moon/keystone/auth/plugins/mapped.py | 252 + keystone-moon/keystone/auth/plugins/oauth1.py | 75 + keystone-moon/keystone/auth/plugins/password.py | 49 + keystone-moon/keystone/auth/plugins/saml2.py | 27 + keystone-moon/keystone/auth/plugins/token.py | 99 + keystone-moon/keystone/auth/routers.py | 57 + keystone-moon/keystone/backends.py | 66 + keystone-moon/keystone/catalog/__init__.py | 17 + .../keystone/catalog/backends/__init__.py | 0 keystone-moon/keystone/catalog/backends/kvs.py | 154 + keystone-moon/keystone/catalog/backends/sql.py | 337 ++ .../keystone/catalog/backends/templated.py | 127 + keystone-moon/keystone/catalog/controllers.py | 336 ++ keystone-moon/keystone/catalog/core.py | 506 ++ keystone-moon/keystone/catalog/routers.py | 40 + keystone-moon/keystone/catalog/schema.py | 96 + keystone-moon/keystone/clean.py | 87 + keystone-moon/keystone/cli.py | 596 ++ keystone-moon/keystone/common/__init__.py | 0 keystone-moon/keystone/common/authorization.py | 87 + keystone-moon/keystone/common/base64utils.py | 396 ++ keystone-moon/keystone/common/cache/__init__.py | 15 + .../keystone/common/cache/_memcache_pool.py | 233 + .../keystone/common/cache/backends/__init__.py | 0 .../common/cache/backends/memcache_pool.py | 61 + .../keystone/common/cache/backends/mongo.py | 557 ++ .../keystone/common/cache/backends/noop.py | 49 + keystone-moon/keystone/common/cache/core.py | 308 ++ keystone-moon/keystone/common/config.py | 1118 ++++ keystone-moon/keystone/common/controller.py | 800 +++ keystone-moon/keystone/common/dependency.py | 311 ++ keystone-moon/keystone/common/driver_hints.py | 65 + .../keystone/common/environment/__init__.py | 100 + .../keystone/common/environment/eventlet_server.py | 194 + keystone-moon/keystone/common/extension.py | 45 + keystone-moon/keystone/common/json_home.py | 76 + keystone-moon/keystone/common/kvs/__init__.py | 33 + .../keystone/common/kvs/backends/__init__.py | 0 .../keystone/common/kvs/backends/inmemdb.py | 69 + .../keystone/common/kvs/backends/memcached.py | 188 + keystone-moon/keystone/common/kvs/core.py | 423 ++ keystone-moon/keystone/common/kvs/legacy.py | 60 + keystone-moon/keystone/common/ldap/__init__.py | 15 + keystone-moon/keystone/common/ldap/core.py | 1910 +++++++ keystone-moon/keystone/common/manager.py | 76 + keystone-moon/keystone/common/models.py | 182 + keystone-moon/keystone/common/openssl.py | 347 ++ keystone-moon/keystone/common/pemutils.py | 509 ++ keystone-moon/keystone/common/router.py | 80 + keystone-moon/keystone/common/sql/__init__.py | 15 + keystone-moon/keystone/common/sql/core.py | 431 ++ .../keystone/common/sql/migrate_repo/README | 4 + .../keystone/common/sql/migrate_repo/__init__.py | 17 + .../keystone/common/sql/migrate_repo/manage.py | 5 + .../keystone/common/sql/migrate_repo/migrate.cfg | 25 + .../sql/migrate_repo/versions/044_icehouse.py | 279 + .../sql/migrate_repo/versions/045_placeholder.py | 25 + .../sql/migrate_repo/versions/046_placeholder.py | 25 + .../sql/migrate_repo/versions/047_placeholder.py | 25 + .../sql/migrate_repo/versions/048_placeholder.py | 25 + .../sql/migrate_repo/versions/049_placeholder.py | 25 + .../versions/050_fk_consistent_indexes.py | 49 + .../migrate_repo/versions/051_add_id_mapping.py | 49 + .../versions/052_add_auth_url_to_region.py | 34 + .../versions/053_endpoint_to_region_association.py | 156 + .../versions/054_add_actor_id_index.py | 35 + .../versions/055_add_indexes_to_token_table.py | 35 + .../sql/migrate_repo/versions/056_placeholder.py | 22 + .../sql/migrate_repo/versions/057_placeholder.py | 22 + .../sql/migrate_repo/versions/058_placeholder.py | 22 + .../sql/migrate_repo/versions/059_placeholder.py | 22 + .../sql/migrate_repo/versions/060_placeholder.py | 22 + .../versions/061_add_parent_project.py | 54 + .../versions/062_drop_assignment_role_fk.py | 41 + .../versions/063_drop_region_auth_url.py | 32 + .../versions/064_drop_user_and_group_fk.py | 45 + .../migrate_repo/versions/065_add_domain_config.py | 55 + .../versions/066_fixup_service_name_value.py | 43 + .../common/sql/migrate_repo/versions/__init__.py | 0 .../keystone/common/sql/migration_helpers.py | 258 + keystone-moon/keystone/common/utils.py | 471 ++ .../keystone/common/validation/__init__.py | 62 + .../keystone/common/validation/parameter_types.py | 57 + .../keystone/common/validation/validators.py | 59 + keystone-moon/keystone/common/wsgi.py | 830 +++ keystone-moon/keystone/config.py | 91 + keystone-moon/keystone/contrib/__init__.py | 0 .../keystone/contrib/admin_crud/__init__.py | 15 + keystone-moon/keystone/contrib/admin_crud/core.py | 241 + keystone-moon/keystone/contrib/ec2/__init__.py | 18 + keystone-moon/keystone/contrib/ec2/controllers.py | 415 ++ keystone-moon/keystone/contrib/ec2/core.py | 34 + keystone-moon/keystone/contrib/ec2/routers.py | 95 + .../keystone/contrib/endpoint_filter/__init__.py | 15 + .../contrib/endpoint_filter/backends/__init__.py | 0 .../endpoint_filter/backends/catalog_sql.py | 76 + .../contrib/endpoint_filter/backends/sql.py | 224 + .../contrib/endpoint_filter/controllers.py | 300 + .../keystone/contrib/endpoint_filter/core.py | 289 + .../endpoint_filter/migrate_repo/__init__.py | 0 .../endpoint_filter/migrate_repo/migrate.cfg | 25 + .../versions/001_add_endpoint_filtering_table.py | 47 + .../versions/002_add_endpoint_groups.py | 51 + .../migrate_repo/versions/__init__.py | 0 .../keystone/contrib/endpoint_filter/routers.py | 149 + .../keystone/contrib/endpoint_filter/schema.py | 35 + .../keystone/contrib/endpoint_policy/__init__.py | 15 + .../contrib/endpoint_policy/backends/__init__.py | 0 .../contrib/endpoint_policy/backends/sql.py | 140 + .../contrib/endpoint_policy/controllers.py | 166 + .../keystone/contrib/endpoint_policy/core.py | 430 ++ .../endpoint_policy/migrate_repo/__init__.py | 0 .../endpoint_policy/migrate_repo/migrate.cfg | 25 + .../versions/001_add_endpoint_policy_table.py | 48 + .../migrate_repo/versions/__init__.py | 0 .../keystone/contrib/endpoint_policy/routers.py | 85 + keystone-moon/keystone/contrib/example/__init__.py | 0 .../keystone/contrib/example/configuration.rst | 31 + .../keystone/contrib/example/controllers.py | 26 + keystone-moon/keystone/contrib/example/core.py | 92 + .../contrib/example/migrate_repo/__init__.py | 0 .../contrib/example/migrate_repo/migrate.cfg | 25 + .../migrate_repo/versions/001_example_table.py | 43 + .../example/migrate_repo/versions/__init__.py | 0 keystone-moon/keystone/contrib/example/routers.py | 38 + .../keystone/contrib/federation/__init__.py | 15 + .../contrib/federation/backends/__init__.py | 0 .../keystone/contrib/federation/backends/sql.py | 315 ++ .../keystone/contrib/federation/controllers.py | 457 ++ keystone-moon/keystone/contrib/federation/core.py | 346 ++ keystone-moon/keystone/contrib/federation/idp.py | 558 ++ .../contrib/federation/migrate_repo/__init__.py | 0 .../contrib/federation/migrate_repo/migrate.cfg | 25 + .../versions/001_add_identity_provider_table.py | 51 + .../versions/002_add_mapping_tables.py | 37 + .../versions/003_mapping_id_nullable_false.py | 35 + .../versions/004_add_remote_id_column.py | 30 + .../versions/005_add_service_provider_table.py | 38 + .../006_fixup_service_provider_attributes.py | 48 + .../federation/migrate_repo/versions/__init__.py | 0 .../keystone/contrib/federation/routers.py | 226 + .../keystone/contrib/federation/schema.py | 78 + keystone-moon/keystone/contrib/federation/utils.py | 763 +++ keystone-moon/keystone/contrib/moon/__init__.py | 8 + .../keystone/contrib/moon/backends/__init__.py | 0 .../keystone/contrib/moon/backends/flat.py | 123 + .../keystone/contrib/moon/backends/sql.py | 1537 ++++++ keystone-moon/keystone/contrib/moon/controllers.py | 611 +++ keystone-moon/keystone/contrib/moon/core.py | 2375 ++++++++ keystone-moon/keystone/contrib/moon/exception.py | 112 + keystone-moon/keystone/contrib/moon/extension.py | 740 +++ .../keystone/contrib/moon/migrate_repo/__init__.py | 0 .../keystone/contrib/moon/migrate_repo/migrate.cfg | 25 + .../contrib/moon/migrate_repo/versions/001_moon.py | 194 + .../contrib/moon/migrate_repo/versions/002_moon.py | 34 + .../contrib/moon/migrate_repo/versions/003_moon.py | 32 + keystone-moon/keystone/contrib/moon/routers.py | 443 ++ keystone-moon/keystone/contrib/oauth1/__init__.py | 15 + .../keystone/contrib/oauth1/backends/__init__.py | 0 .../keystone/contrib/oauth1/backends/sql.py | 272 + .../keystone/contrib/oauth1/controllers.py | 417 ++ keystone-moon/keystone/contrib/oauth1/core.py | 361 ++ .../contrib/oauth1/migrate_repo/__init__.py | 0 .../contrib/oauth1/migrate_repo/migrate.cfg | 25 + .../migrate_repo/versions/001_add_oauth_tables.py | 67 + .../versions/002_fix_oauth_tables_fk.py | 54 + .../versions/003_consumer_description_nullalbe.py | 29 + .../versions/004_request_token_roles_nullable.py | 35 + .../migrate_repo/versions/005_consumer_id_index.py | 42 + .../oauth1/migrate_repo/versions/__init__.py | 0 keystone-moon/keystone/contrib/oauth1/routers.py | 154 + keystone-moon/keystone/contrib/oauth1/validator.py | 179 + keystone-moon/keystone/contrib/revoke/__init__.py | 13 + .../keystone/contrib/revoke/backends/__init__.py | 0 .../keystone/contrib/revoke/backends/kvs.py | 73 + .../keystone/contrib/revoke/backends/sql.py | 104 + .../keystone/contrib/revoke/controllers.py | 44 + keystone-moon/keystone/contrib/revoke/core.py | 250 + .../contrib/revoke/migrate_repo/__init__.py | 0 .../contrib/revoke/migrate_repo/migrate.cfg | 25 + .../migrate_repo/versions/001_revoke_table.py | 47 + .../002_add_audit_id_and_chain_to_revoke_table.py | 37 + .../revoke/migrate_repo/versions/__init__.py | 0 keystone-moon/keystone/contrib/revoke/model.py | 365 ++ keystone-moon/keystone/contrib/revoke/routers.py | 29 + keystone-moon/keystone/contrib/s3/__init__.py | 15 + keystone-moon/keystone/contrib/s3/core.py | 73 + .../keystone/contrib/simple_cert/__init__.py | 14 + .../keystone/contrib/simple_cert/controllers.py | 42 + keystone-moon/keystone/contrib/simple_cert/core.py | 32 + .../keystone/contrib/simple_cert/routers.py | 41 + .../keystone/contrib/user_crud/__init__.py | 15 + keystone-moon/keystone/contrib/user_crud/core.py | 134 + keystone-moon/keystone/controllers.py | 218 + keystone-moon/keystone/credential/__init__.py | 17 + .../keystone/credential/backends/__init__.py | 0 keystone-moon/keystone/credential/backends/sql.py | 104 + keystone-moon/keystone/credential/controllers.py | 108 + keystone-moon/keystone/credential/core.py | 140 + keystone-moon/keystone/credential/routers.py | 28 + keystone-moon/keystone/credential/schema.py | 62 + keystone-moon/keystone/exception.py | 469 ++ keystone-moon/keystone/hacking/__init__.py | 0 keystone-moon/keystone/hacking/checks.py | 446 ++ keystone-moon/keystone/i18n.py | 37 + keystone-moon/keystone/identity/__init__.py | 18 + .../keystone/identity/backends/__init__.py | 0 keystone-moon/keystone/identity/backends/ldap.py | 402 ++ keystone-moon/keystone/identity/backends/sql.py | 314 ++ keystone-moon/keystone/identity/controllers.py | 335 ++ keystone-moon/keystone/identity/core.py | 1259 +++++ keystone-moon/keystone/identity/generator.py | 52 + .../keystone/identity/id_generators/__init__.py | 0 .../keystone/identity/id_generators/sha256.py | 28 + .../keystone/identity/mapping_backends/__init__.py | 0 .../keystone/identity/mapping_backends/mapping.py | 18 + .../keystone/identity/mapping_backends/sql.py | 97 + keystone-moon/keystone/identity/routers.py | 84 + .../locale/de/LC_MESSAGES/keystone-log-critical.po | 25 + .../locale/de/LC_MESSAGES/keystone-log-info.po | 212 + .../en_AU/LC_MESSAGES/keystone-log-critical.po | 25 + .../locale/en_AU/LC_MESSAGES/keystone-log-error.po | 179 + .../keystone/locale/en_AU/LC_MESSAGES/keystone.po | 1542 ++++++ .../locale/en_GB/LC_MESSAGES/keystone-log-info.po | 214 + .../locale/es/LC_MESSAGES/keystone-log-critical.po | 25 + .../locale/es/LC_MESSAGES/keystone-log-error.po | 177 + .../locale/fr/LC_MESSAGES/keystone-log-critical.po | 25 + .../locale/fr/LC_MESSAGES/keystone-log-error.po | 184 + .../locale/fr/LC_MESSAGES/keystone-log-info.po | 223 + .../locale/fr/LC_MESSAGES/keystone-log-warning.po | 303 ++ .../locale/hu/LC_MESSAGES/keystone-log-critical.po | 25 + .../locale/it/LC_MESSAGES/keystone-log-critical.po | 25 + .../locale/it/LC_MESSAGES/keystone-log-error.po | 173 + .../locale/it/LC_MESSAGES/keystone-log-info.po | 211 + .../locale/ja/LC_MESSAGES/keystone-log-critical.po | 25 + .../locale/ja/LC_MESSAGES/keystone-log-error.po | 177 + .../keystone/locale/keystone-log-critical.pot | 24 + .../keystone/locale/keystone-log-error.pot | 174 + .../keystone/locale/keystone-log-info.pot | 210 + .../keystone/locale/keystone-log-warning.pot | 290 + keystone-moon/keystone/locale/keystone.pot | 1522 ++++++ .../ko_KR/LC_MESSAGES/keystone-log-critical.po | 25 + .../pl_PL/LC_MESSAGES/keystone-log-critical.po | 26 + .../pt_BR/LC_MESSAGES/keystone-log-critical.po | 25 + .../locale/pt_BR/LC_MESSAGES/keystone-log-error.po | 179 + .../keystone/locale/pt_BR/LC_MESSAGES/keystone.po | 1546 ++++++ .../locale/ru/LC_MESSAGES/keystone-log-critical.po | 26 + .../locale/vi_VN/LC_MESSAGES/keystone-log-info.po | 211 + .../zh_CN/LC_MESSAGES/keystone-log-critical.po | 25 + .../locale/zh_CN/LC_MESSAGES/keystone-log-error.po | 177 + .../locale/zh_CN/LC_MESSAGES/keystone-log-info.po | 215 + .../zh_TW/LC_MESSAGES/keystone-log-critical.po | 25 + keystone-moon/keystone/middleware/__init__.py | 15 + keystone-moon/keystone/middleware/core.py | 240 + keystone-moon/keystone/middleware/ec2_token.py | 44 + keystone-moon/keystone/models/__init__.py | 0 keystone-moon/keystone/models/token_model.py | 335 ++ keystone-moon/keystone/notifications.py | 686 +++ keystone-moon/keystone/openstack/__init__.py | 0 keystone-moon/keystone/openstack/common/README | 13 + .../keystone/openstack/common/__init__.py | 0 keystone-moon/keystone/openstack/common/_i18n.py | 45 + .../keystone/openstack/common/eventlet_backdoor.py | 151 + .../keystone/openstack/common/fileutils.py | 149 + .../keystone/openstack/common/loopingcall.py | 147 + keystone-moon/keystone/openstack/common/service.py | 495 ++ keystone-moon/keystone/openstack/common/systemd.py | 105 + .../keystone/openstack/common/threadgroup.py | 149 + .../keystone/openstack/common/versionutils.py | 262 + keystone-moon/keystone/policy/__init__.py | 17 + keystone-moon/keystone/policy/backends/__init__.py | 0 keystone-moon/keystone/policy/backends/rules.py | 92 + keystone-moon/keystone/policy/backends/sql.py | 79 + keystone-moon/keystone/policy/controllers.py | 56 + keystone-moon/keystone/policy/core.py | 135 + keystone-moon/keystone/policy/routers.py | 24 + keystone-moon/keystone/policy/schema.py | 36 + keystone-moon/keystone/resource/__init__.py | 15 + .../keystone/resource/backends/__init__.py | 0 keystone-moon/keystone/resource/backends/ldap.py | 196 + keystone-moon/keystone/resource/backends/sql.py | 260 + .../keystone/resource/config_backends/__init__.py | 0 .../keystone/resource/config_backends/sql.py | 119 + keystone-moon/keystone/resource/controllers.py | 281 + keystone-moon/keystone/resource/core.py | 1324 +++++ keystone-moon/keystone/resource/routers.py | 94 + keystone-moon/keystone/resource/schema.py | 75 + keystone-moon/keystone/routers.py | 80 + keystone-moon/keystone/server/__init__.py | 0 keystone-moon/keystone/server/common.py | 45 + keystone-moon/keystone/server/eventlet.py | 156 + keystone-moon/keystone/server/wsgi.py | 52 + keystone-moon/keystone/service.py | 118 + keystone-moon/keystone/tests/__init__.py | 0 keystone-moon/keystone/tests/moon/__init__.py | 4 + keystone-moon/keystone/tests/moon/func/__init__.py | 4 + .../tests/moon/func/test_func_api_authz.py | 129 + .../func/test_func_api_intra_extension_admin.py | 1011 ++++ .../keystone/tests/moon/func/test_func_api_log.py | 148 + .../tests/moon/func/test_func_api_tenant.py | 154 + keystone-moon/keystone/tests/moon/unit/__init__.py | 4 + .../unit/test_unit_core_intra_extension_admin.py | 1229 +++++ .../unit/test_unit_core_intra_extension_authz.py | 861 +++ .../keystone/tests/moon/unit/test_unit_core_log.py | 4 + .../tests/moon/unit/test_unit_core_tenant.py | 162 + keystone-moon/keystone/tests/unit/__init__.py | 41 + .../keystone/tests/unit/backend/__init__.py | 0 .../keystone/tests/unit/backend/core_ldap.py | 161 + .../keystone/tests/unit/backend/core_sql.py | 53 + .../tests/unit/backend/domain_config/__init__.py | 0 .../tests/unit/backend/domain_config/core.py | 523 ++ .../tests/unit/backend/domain_config/test_sql.py | 41 + .../keystone/tests/unit/backend/role/__init__.py | 0 .../keystone/tests/unit/backend/role/core.py | 130 + .../keystone/tests/unit/backend/role/test_ldap.py | 161 + .../keystone/tests/unit/backend/role/test_sql.py | 40 + .../keystone/tests/unit/catalog/__init__.py | 0 .../keystone/tests/unit/catalog/test_core.py | 74 + .../keystone/tests/unit/common/__init__.py | 0 .../keystone/tests/unit/common/test_base64utils.py | 208 + .../tests/unit/common/test_connection_pool.py | 119 + .../keystone/tests/unit/common/test_injection.py | 293 + .../keystone/tests/unit/common/test_json_home.py | 91 + .../keystone/tests/unit/common/test_ldap.py | 502 ++ .../tests/unit/common/test_notifications.py | 974 ++++ .../keystone/tests/unit/common/test_pemutils.py | 337 ++ .../keystone/tests/unit/common/test_sql_core.py | 52 + .../keystone/tests/unit/common/test_utils.py | 164 + .../tests/unit/config_files/backend_db2.conf | 4 + .../tests/unit/config_files/backend_ldap.conf | 5 + .../tests/unit/config_files/backend_ldap_pool.conf | 41 + .../tests/unit/config_files/backend_ldap_sql.conf | 14 + .../tests/unit/config_files/backend_liveldap.conf | 14 + .../unit/config_files/backend_multi_ldap_sql.conf | 9 + .../tests/unit/config_files/backend_mysql.conf | 4 + .../unit/config_files/backend_pool_liveldap.conf | 35 + .../unit/config_files/backend_postgresql.conf | 4 + .../tests/unit/config_files/backend_sql.conf | 8 + .../unit/config_files/backend_tls_liveldap.conf | 17 + .../tests/unit/config_files/deprecated.conf | 8 + .../unit/config_files/deprecated_override.conf | 15 + .../keystone.domain1.conf | 5 + .../keystone.Default.conf | 14 + .../keystone.domain1.conf | 11 + .../keystone.domain2.conf | 13 + .../keystone.domain2.conf | 5 + .../keystone.Default.conf | 14 + .../keystone.domain1.conf | 5 + .../tests/unit/config_files/test_auth_plugin.conf | 7 + keystone-moon/keystone/tests/unit/core.py | 660 +++ .../keystone/tests/unit/default_catalog.templates | 14 + .../keystone/tests/unit/default_fixtures.py | 121 + keystone-moon/keystone/tests/unit/fakeldap.py | 602 ++ .../keystone/tests/unit/federation_fixtures.py | 28 + keystone-moon/keystone/tests/unit/filtering.py | 96 + .../keystone/tests/unit/identity/__init__.py | 0 .../keystone/tests/unit/identity/test_core.py | 125 + .../keystone/tests/unit/identity_mapping.py | 23 + .../keystone/tests/unit/ksfixtures/__init__.py | 15 + .../keystone/tests/unit/ksfixtures/appserver.py | 79 + .../keystone/tests/unit/ksfixtures/cache.py | 36 + .../keystone/tests/unit/ksfixtures/database.py | 124 + .../keystone/tests/unit/ksfixtures/hacking.py | 489 ++ .../tests/unit/ksfixtures/key_repository.py | 34 + .../tests/unit/ksfixtures/temporaryfile.py | 29 + .../keystone/tests/unit/mapping_fixtures.py | 1023 ++++ keystone-moon/keystone/tests/unit/rest.py | 245 + .../tests/unit/saml2/idp_saml2_metadata.xml | 25 + .../tests/unit/saml2/signed_saml2_assertion.xml | 63 + .../test_associate_project_endpoint_extension.py | 1129 ++++ keystone-moon/keystone/tests/unit/test_auth.py | 1328 +++++ .../keystone/tests/unit/test_auth_plugin.py | 220 + keystone-moon/keystone/tests/unit/test_backend.py | 5741 ++++++++++++++++++++ .../tests/unit/test_backend_endpoint_policy.py | 247 + .../tests/unit/test_backend_endpoint_policy_sql.py | 37 + .../tests/unit/test_backend_federation_sql.py | 46 + .../tests/unit/test_backend_id_mapping_sql.py | 197 + .../keystone/tests/unit/test_backend_kvs.py | 172 + .../keystone/tests/unit/test_backend_ldap.py | 3049 +++++++++++ .../keystone/tests/unit/test_backend_ldap_pool.py | 244 + .../keystone/tests/unit/test_backend_rules.py | 62 + .../keystone/tests/unit/test_backend_sql.py | 948 ++++ .../keystone/tests/unit/test_backend_templated.py | 127 + keystone-moon/keystone/tests/unit/test_cache.py | 322 ++ .../tests/unit/test_cache_backend_mongo.py | 727 +++ keystone-moon/keystone/tests/unit/test_catalog.py | 219 + .../keystone/tests/unit/test_cert_setup.py | 246 + keystone-moon/keystone/tests/unit/test_cli.py | 252 + keystone-moon/keystone/tests/unit/test_config.py | 84 + .../keystone/tests/unit/test_contrib_s3_core.py | 55 + .../tests/unit/test_contrib_simple_cert.py | 57 + .../keystone/tests/unit/test_driver_hints.py | 60 + .../tests/unit/test_ec2_token_middleware.py | 34 + .../keystone/tests/unit/test_exception.py | 227 + .../keystone/tests/unit/test_hacking_checks.py | 143 + keystone-moon/keystone/tests/unit/test_ipv6.py | 51 + keystone-moon/keystone/tests/unit/test_kvs.py | 581 ++ .../keystone/tests/unit/test_ldap_livetest.py | 229 + .../keystone/tests/unit/test_ldap_pool_livetest.py | 208 + .../keystone/tests/unit/test_ldap_tls_livetest.py | 122 + .../keystone/tests/unit/test_middleware.py | 119 + .../tests/unit/test_no_admin_token_auth.py | 59 + keystone-moon/keystone/tests/unit/test_policy.py | 228 + keystone-moon/keystone/tests/unit/test_revoke.py | 637 +++ .../keystone/tests/unit/test_singular_plural.py | 48 + .../keystone/tests/unit/test_sql_livetest.py | 73 + .../tests/unit/test_sql_migrate_extensions.py | 380 ++ .../keystone/tests/unit/test_sql_upgrade.py | 957 ++++ keystone-moon/keystone/tests/unit/test_ssl.py | 176 + .../keystone/tests/unit/test_token_bind.py | 198 + .../keystone/tests/unit/test_token_provider.py | 836 +++ .../keystone/tests/unit/test_url_middleware.py | 53 + keystone-moon/keystone/tests/unit/test_v2.py | 1500 +++++ .../keystone/tests/unit/test_v2_controller.py | 95 + .../keystone/tests/unit/test_v2_keystoneclient.py | 1045 ++++ .../tests/unit/test_v2_keystoneclient_sql.py | 344 ++ keystone-moon/keystone/tests/unit/test_v3.py | 1283 +++++ .../keystone/tests/unit/test_v3_assignment.py | 2943 ++++++++++ keystone-moon/keystone/tests/unit/test_v3_auth.py | 4494 +++++++++++++++ .../keystone/tests/unit/test_v3_catalog.py | 746 +++ .../keystone/tests/unit/test_v3_controller.py | 52 + .../keystone/tests/unit/test_v3_credential.py | 406 ++ .../keystone/tests/unit/test_v3_domain_config.py | 210 + .../keystone/tests/unit/test_v3_endpoint_policy.py | 251 + .../keystone/tests/unit/test_v3_federation.py | 3296 +++++++++++ .../keystone/tests/unit/test_v3_filters.py | 452 ++ .../keystone/tests/unit/test_v3_identity.py | 584 ++ .../keystone/tests/unit/test_v3_oauth1.py | 891 +++ .../keystone/tests/unit/test_v3_os_revoke.py | 135 + .../keystone/tests/unit/test_v3_policy.py | 68 + .../keystone/tests/unit/test_v3_protection.py | 1170 ++++ .../keystone/tests/unit/test_validation.py | 1563 ++++++ keystone-moon/keystone/tests/unit/test_versions.py | 1051 ++++ keystone-moon/keystone/tests/unit/test_wsgi.py | 427 ++ .../keystone/tests/unit/tests/__init__.py | 0 .../keystone/tests/unit/tests/test_core.py | 62 + .../keystone/tests/unit/tests/test_utils.py | 37 + .../keystone/tests/unit/token/__init__.py | 0 .../tests/unit/token/test_fernet_provider.py | 183 + .../keystone/tests/unit/token/test_provider.py | 29 + .../tests/unit/token/test_token_data_helper.py | 55 + .../keystone/tests/unit/token/test_token_model.py | 262 + keystone-moon/keystone/tests/unit/utils.py | 89 + keystone-moon/keystone/token/__init__.py | 18 + keystone-moon/keystone/token/controllers.py | 523 ++ .../keystone/token/persistence/__init__.py | 16 + .../token/persistence/backends/__init__.py | 0 .../keystone/token/persistence/backends/kvs.py | 357 ++ .../token/persistence/backends/memcache.py | 33 + .../token/persistence/backends/memcache_pool.py | 28 + .../keystone/token/persistence/backends/sql.py | 279 + keystone-moon/keystone/token/persistence/core.py | 361 ++ keystone-moon/keystone/token/provider.py | 584 ++ keystone-moon/keystone/token/providers/__init__.py | 0 keystone-moon/keystone/token/providers/common.py | 709 +++ .../keystone/token/providers/fernet/__init__.py | 13 + .../keystone/token/providers/fernet/core.py | 267 + .../token/providers/fernet/token_formatters.py | 545 ++ .../keystone/token/providers/fernet/utils.py | 243 + keystone-moon/keystone/token/providers/pki.py | 53 + keystone-moon/keystone/token/providers/pkiz.py | 51 + keystone-moon/keystone/token/providers/uuid.py | 33 + keystone-moon/keystone/token/routers.py | 59 + keystone-moon/keystone/trust/__init__.py | 17 + keystone-moon/keystone/trust/backends/__init__.py | 0 keystone-moon/keystone/trust/backends/sql.py | 180 + keystone-moon/keystone/trust/controllers.py | 287 + keystone-moon/keystone/trust/core.py | 251 + keystone-moon/keystone/trust/routers.py | 67 + keystone-moon/keystone/trust/schema.py | 46 + 488 files changed, 114797 insertions(+) create mode 100644 keystone-moon/keystone/__init__.py create mode 100644 keystone-moon/keystone/assignment/__init__.py create mode 100644 keystone-moon/keystone/assignment/backends/__init__.py create mode 100644 keystone-moon/keystone/assignment/backends/ldap.py create mode 100644 keystone-moon/keystone/assignment/backends/sql.py create mode 100644 keystone-moon/keystone/assignment/controllers.py create mode 100644 keystone-moon/keystone/assignment/core.py create mode 100644 keystone-moon/keystone/assignment/role_backends/__init__.py create mode 100644 keystone-moon/keystone/assignment/role_backends/ldap.py create mode 100644 keystone-moon/keystone/assignment/role_backends/sql.py create mode 100644 keystone-moon/keystone/assignment/routers.py create mode 100644 keystone-moon/keystone/assignment/schema.py create mode 100644 keystone-moon/keystone/auth/__init__.py create mode 100644 keystone-moon/keystone/auth/controllers.py create mode 100644 keystone-moon/keystone/auth/core.py create mode 100644 keystone-moon/keystone/auth/plugins/__init__.py create mode 100644 keystone-moon/keystone/auth/plugins/core.py create mode 100644 keystone-moon/keystone/auth/plugins/external.py create mode 100644 keystone-moon/keystone/auth/plugins/mapped.py create mode 100644 keystone-moon/keystone/auth/plugins/oauth1.py create mode 100644 keystone-moon/keystone/auth/plugins/password.py create mode 100644 keystone-moon/keystone/auth/plugins/saml2.py create mode 100644 keystone-moon/keystone/auth/plugins/token.py create mode 100644 keystone-moon/keystone/auth/routers.py create mode 100644 keystone-moon/keystone/backends.py create mode 100644 keystone-moon/keystone/catalog/__init__.py create mode 100644 keystone-moon/keystone/catalog/backends/__init__.py create mode 100644 keystone-moon/keystone/catalog/backends/kvs.py create mode 100644 keystone-moon/keystone/catalog/backends/sql.py create mode 100644 keystone-moon/keystone/catalog/backends/templated.py create mode 100644 keystone-moon/keystone/catalog/controllers.py create mode 100644 keystone-moon/keystone/catalog/core.py create mode 100644 keystone-moon/keystone/catalog/routers.py create mode 100644 keystone-moon/keystone/catalog/schema.py create mode 100644 keystone-moon/keystone/clean.py create mode 100644 keystone-moon/keystone/cli.py create mode 100644 keystone-moon/keystone/common/__init__.py create mode 100644 keystone-moon/keystone/common/authorization.py create mode 100644 keystone-moon/keystone/common/base64utils.py create mode 100644 keystone-moon/keystone/common/cache/__init__.py create mode 100644 keystone-moon/keystone/common/cache/_memcache_pool.py create mode 100644 keystone-moon/keystone/common/cache/backends/__init__.py create mode 100644 keystone-moon/keystone/common/cache/backends/memcache_pool.py create mode 100644 keystone-moon/keystone/common/cache/backends/mongo.py create mode 100644 keystone-moon/keystone/common/cache/backends/noop.py create mode 100644 keystone-moon/keystone/common/cache/core.py create mode 100644 keystone-moon/keystone/common/config.py create mode 100644 keystone-moon/keystone/common/controller.py create mode 100644 keystone-moon/keystone/common/dependency.py create mode 100644 keystone-moon/keystone/common/driver_hints.py create mode 100644 keystone-moon/keystone/common/environment/__init__.py create mode 100644 keystone-moon/keystone/common/environment/eventlet_server.py create mode 100644 keystone-moon/keystone/common/extension.py create mode 100644 keystone-moon/keystone/common/json_home.py create mode 100644 keystone-moon/keystone/common/kvs/__init__.py create mode 100644 keystone-moon/keystone/common/kvs/backends/__init__.py create mode 100644 keystone-moon/keystone/common/kvs/backends/inmemdb.py create mode 100644 keystone-moon/keystone/common/kvs/backends/memcached.py create mode 100644 keystone-moon/keystone/common/kvs/core.py create mode 100644 keystone-moon/keystone/common/kvs/legacy.py create mode 100644 keystone-moon/keystone/common/ldap/__init__.py create mode 100644 keystone-moon/keystone/common/ldap/core.py create mode 100644 keystone-moon/keystone/common/manager.py create mode 100644 keystone-moon/keystone/common/models.py create mode 100644 keystone-moon/keystone/common/openssl.py create mode 100755 keystone-moon/keystone/common/pemutils.py create mode 100644 keystone-moon/keystone/common/router.py create mode 100644 keystone-moon/keystone/common/sql/__init__.py create mode 100644 keystone-moon/keystone/common/sql/core.py create mode 100644 keystone-moon/keystone/common/sql/migrate_repo/README create mode 100644 keystone-moon/keystone/common/sql/migrate_repo/__init__.py create mode 100644 keystone-moon/keystone/common/sql/migrate_repo/manage.py create mode 100644 keystone-moon/keystone/common/sql/migrate_repo/migrate.cfg create mode 100644 keystone-moon/keystone/common/sql/migrate_repo/versions/044_icehouse.py create mode 100644 keystone-moon/keystone/common/sql/migrate_repo/versions/045_placeholder.py create mode 100644 keystone-moon/keystone/common/sql/migrate_repo/versions/046_placeholder.py create mode 100644 keystone-moon/keystone/common/sql/migrate_repo/versions/047_placeholder.py create mode 100644 keystone-moon/keystone/common/sql/migrate_repo/versions/048_placeholder.py create mode 100644 keystone-moon/keystone/common/sql/migrate_repo/versions/049_placeholder.py create mode 100644 keystone-moon/keystone/common/sql/migrate_repo/versions/050_fk_consistent_indexes.py create mode 100644 keystone-moon/keystone/common/sql/migrate_repo/versions/051_add_id_mapping.py create mode 100644 keystone-moon/keystone/common/sql/migrate_repo/versions/052_add_auth_url_to_region.py create mode 100644 keystone-moon/keystone/common/sql/migrate_repo/versions/053_endpoint_to_region_association.py create mode 100644 keystone-moon/keystone/common/sql/migrate_repo/versions/054_add_actor_id_index.py create mode 100644 keystone-moon/keystone/common/sql/migrate_repo/versions/055_add_indexes_to_token_table.py create mode 100644 keystone-moon/keystone/common/sql/migrate_repo/versions/056_placeholder.py create mode 100644 keystone-moon/keystone/common/sql/migrate_repo/versions/057_placeholder.py create mode 100644 keystone-moon/keystone/common/sql/migrate_repo/versions/058_placeholder.py create mode 100644 keystone-moon/keystone/common/sql/migrate_repo/versions/059_placeholder.py create mode 100644 keystone-moon/keystone/common/sql/migrate_repo/versions/060_placeholder.py create mode 100644 keystone-moon/keystone/common/sql/migrate_repo/versions/061_add_parent_project.py create mode 100644 keystone-moon/keystone/common/sql/migrate_repo/versions/062_drop_assignment_role_fk.py create mode 100644 keystone-moon/keystone/common/sql/migrate_repo/versions/063_drop_region_auth_url.py create mode 100644 keystone-moon/keystone/common/sql/migrate_repo/versions/064_drop_user_and_group_fk.py create mode 100644 keystone-moon/keystone/common/sql/migrate_repo/versions/065_add_domain_config.py create mode 100644 keystone-moon/keystone/common/sql/migrate_repo/versions/066_fixup_service_name_value.py create mode 100644 keystone-moon/keystone/common/sql/migrate_repo/versions/__init__.py create mode 100644 keystone-moon/keystone/common/sql/migration_helpers.py create mode 100644 keystone-moon/keystone/common/utils.py create mode 100644 keystone-moon/keystone/common/validation/__init__.py create mode 100644 keystone-moon/keystone/common/validation/parameter_types.py create mode 100644 keystone-moon/keystone/common/validation/validators.py create mode 100644 keystone-moon/keystone/common/wsgi.py create mode 100644 keystone-moon/keystone/config.py create mode 100644 keystone-moon/keystone/contrib/__init__.py create mode 100644 keystone-moon/keystone/contrib/admin_crud/__init__.py create mode 100644 keystone-moon/keystone/contrib/admin_crud/core.py create mode 100644 keystone-moon/keystone/contrib/ec2/__init__.py create mode 100644 keystone-moon/keystone/contrib/ec2/controllers.py create mode 100644 keystone-moon/keystone/contrib/ec2/core.py create mode 100644 keystone-moon/keystone/contrib/ec2/routers.py create mode 100644 keystone-moon/keystone/contrib/endpoint_filter/__init__.py create mode 100644 keystone-moon/keystone/contrib/endpoint_filter/backends/__init__.py create mode 100644 keystone-moon/keystone/contrib/endpoint_filter/backends/catalog_sql.py create mode 100644 keystone-moon/keystone/contrib/endpoint_filter/backends/sql.py create mode 100644 keystone-moon/keystone/contrib/endpoint_filter/controllers.py create mode 100644 keystone-moon/keystone/contrib/endpoint_filter/core.py create mode 100644 keystone-moon/keystone/contrib/endpoint_filter/migrate_repo/__init__.py create mode 100644 keystone-moon/keystone/contrib/endpoint_filter/migrate_repo/migrate.cfg create mode 100644 keystone-moon/keystone/contrib/endpoint_filter/migrate_repo/versions/001_add_endpoint_filtering_table.py create mode 100644 keystone-moon/keystone/contrib/endpoint_filter/migrate_repo/versions/002_add_endpoint_groups.py create mode 100644 keystone-moon/keystone/contrib/endpoint_filter/migrate_repo/versions/__init__.py create mode 100644 keystone-moon/keystone/contrib/endpoint_filter/routers.py create mode 100644 keystone-moon/keystone/contrib/endpoint_filter/schema.py create mode 100644 keystone-moon/keystone/contrib/endpoint_policy/__init__.py create mode 100644 keystone-moon/keystone/contrib/endpoint_policy/backends/__init__.py create mode 100644 keystone-moon/keystone/contrib/endpoint_policy/backends/sql.py create mode 100644 keystone-moon/keystone/contrib/endpoint_policy/controllers.py create mode 100644 keystone-moon/keystone/contrib/endpoint_policy/core.py create mode 100644 keystone-moon/keystone/contrib/endpoint_policy/migrate_repo/__init__.py create mode 100644 keystone-moon/keystone/contrib/endpoint_policy/migrate_repo/migrate.cfg create mode 100644 keystone-moon/keystone/contrib/endpoint_policy/migrate_repo/versions/001_add_endpoint_policy_table.py create mode 100644 keystone-moon/keystone/contrib/endpoint_policy/migrate_repo/versions/__init__.py create mode 100644 keystone-moon/keystone/contrib/endpoint_policy/routers.py create mode 100644 keystone-moon/keystone/contrib/example/__init__.py create mode 100644 keystone-moon/keystone/contrib/example/configuration.rst create mode 100644 keystone-moon/keystone/contrib/example/controllers.py create mode 100644 keystone-moon/keystone/contrib/example/core.py create mode 100644 keystone-moon/keystone/contrib/example/migrate_repo/__init__.py create mode 100644 keystone-moon/keystone/contrib/example/migrate_repo/migrate.cfg create mode 100644 keystone-moon/keystone/contrib/example/migrate_repo/versions/001_example_table.py create mode 100644 keystone-moon/keystone/contrib/example/migrate_repo/versions/__init__.py create mode 100644 keystone-moon/keystone/contrib/example/routers.py create mode 100644 keystone-moon/keystone/contrib/federation/__init__.py create mode 100644 keystone-moon/keystone/contrib/federation/backends/__init__.py create mode 100644 keystone-moon/keystone/contrib/federation/backends/sql.py create mode 100644 keystone-moon/keystone/contrib/federation/controllers.py create mode 100644 keystone-moon/keystone/contrib/federation/core.py create mode 100644 keystone-moon/keystone/contrib/federation/idp.py create mode 100644 keystone-moon/keystone/contrib/federation/migrate_repo/__init__.py create mode 100644 keystone-moon/keystone/contrib/federation/migrate_repo/migrate.cfg create mode 100644 keystone-moon/keystone/contrib/federation/migrate_repo/versions/001_add_identity_provider_table.py create mode 100644 keystone-moon/keystone/contrib/federation/migrate_repo/versions/002_add_mapping_tables.py create mode 100644 keystone-moon/keystone/contrib/federation/migrate_repo/versions/003_mapping_id_nullable_false.py create mode 100644 keystone-moon/keystone/contrib/federation/migrate_repo/versions/004_add_remote_id_column.py create mode 100644 keystone-moon/keystone/contrib/federation/migrate_repo/versions/005_add_service_provider_table.py create mode 100644 keystone-moon/keystone/contrib/federation/migrate_repo/versions/006_fixup_service_provider_attributes.py create mode 100644 keystone-moon/keystone/contrib/federation/migrate_repo/versions/__init__.py create mode 100644 keystone-moon/keystone/contrib/federation/routers.py create mode 100644 keystone-moon/keystone/contrib/federation/schema.py create mode 100644 keystone-moon/keystone/contrib/federation/utils.py create mode 100644 keystone-moon/keystone/contrib/moon/__init__.py create mode 100644 keystone-moon/keystone/contrib/moon/backends/__init__.py create mode 100644 keystone-moon/keystone/contrib/moon/backends/flat.py create mode 100644 keystone-moon/keystone/contrib/moon/backends/sql.py create mode 100644 keystone-moon/keystone/contrib/moon/controllers.py create mode 100644 keystone-moon/keystone/contrib/moon/core.py create mode 100644 keystone-moon/keystone/contrib/moon/exception.py create mode 100644 keystone-moon/keystone/contrib/moon/extension.py create mode 100644 keystone-moon/keystone/contrib/moon/migrate_repo/__init__.py create mode 100644 keystone-moon/keystone/contrib/moon/migrate_repo/migrate.cfg create mode 100644 keystone-moon/keystone/contrib/moon/migrate_repo/versions/001_moon.py create mode 100644 keystone-moon/keystone/contrib/moon/migrate_repo/versions/002_moon.py create mode 100644 keystone-moon/keystone/contrib/moon/migrate_repo/versions/003_moon.py create mode 100644 keystone-moon/keystone/contrib/moon/routers.py create mode 100644 keystone-moon/keystone/contrib/oauth1/__init__.py create mode 100644 keystone-moon/keystone/contrib/oauth1/backends/__init__.py create mode 100644 keystone-moon/keystone/contrib/oauth1/backends/sql.py create mode 100644 keystone-moon/keystone/contrib/oauth1/controllers.py create mode 100644 keystone-moon/keystone/contrib/oauth1/core.py create mode 100644 keystone-moon/keystone/contrib/oauth1/migrate_repo/__init__.py create mode 100644 keystone-moon/keystone/contrib/oauth1/migrate_repo/migrate.cfg create mode 100644 keystone-moon/keystone/contrib/oauth1/migrate_repo/versions/001_add_oauth_tables.py create mode 100644 keystone-moon/keystone/contrib/oauth1/migrate_repo/versions/002_fix_oauth_tables_fk.py create mode 100644 keystone-moon/keystone/contrib/oauth1/migrate_repo/versions/003_consumer_description_nullalbe.py create mode 100644 keystone-moon/keystone/contrib/oauth1/migrate_repo/versions/004_request_token_roles_nullable.py create mode 100644 keystone-moon/keystone/contrib/oauth1/migrate_repo/versions/005_consumer_id_index.py create mode 100644 keystone-moon/keystone/contrib/oauth1/migrate_repo/versions/__init__.py create mode 100644 keystone-moon/keystone/contrib/oauth1/routers.py create mode 100644 keystone-moon/keystone/contrib/oauth1/validator.py create mode 100644 keystone-moon/keystone/contrib/revoke/__init__.py create mode 100644 keystone-moon/keystone/contrib/revoke/backends/__init__.py create mode 100644 keystone-moon/keystone/contrib/revoke/backends/kvs.py create mode 100644 keystone-moon/keystone/contrib/revoke/backends/sql.py create mode 100644 keystone-moon/keystone/contrib/revoke/controllers.py create mode 100644 keystone-moon/keystone/contrib/revoke/core.py create mode 100644 keystone-moon/keystone/contrib/revoke/migrate_repo/__init__.py create mode 100644 keystone-moon/keystone/contrib/revoke/migrate_repo/migrate.cfg create mode 100644 keystone-moon/keystone/contrib/revoke/migrate_repo/versions/001_revoke_table.py create mode 100644 keystone-moon/keystone/contrib/revoke/migrate_repo/versions/002_add_audit_id_and_chain_to_revoke_table.py create mode 100644 keystone-moon/keystone/contrib/revoke/migrate_repo/versions/__init__.py create mode 100644 keystone-moon/keystone/contrib/revoke/model.py create mode 100644 keystone-moon/keystone/contrib/revoke/routers.py create mode 100644 keystone-moon/keystone/contrib/s3/__init__.py create mode 100644 keystone-moon/keystone/contrib/s3/core.py create mode 100644 keystone-moon/keystone/contrib/simple_cert/__init__.py create mode 100644 keystone-moon/keystone/contrib/simple_cert/controllers.py create mode 100644 keystone-moon/keystone/contrib/simple_cert/core.py create mode 100644 keystone-moon/keystone/contrib/simple_cert/routers.py create mode 100644 keystone-moon/keystone/contrib/user_crud/__init__.py create mode 100644 keystone-moon/keystone/contrib/user_crud/core.py create mode 100644 keystone-moon/keystone/controllers.py create mode 100644 keystone-moon/keystone/credential/__init__.py create mode 100644 keystone-moon/keystone/credential/backends/__init__.py create mode 100644 keystone-moon/keystone/credential/backends/sql.py create mode 100644 keystone-moon/keystone/credential/controllers.py create mode 100644 keystone-moon/keystone/credential/core.py create mode 100644 keystone-moon/keystone/credential/routers.py create mode 100644 keystone-moon/keystone/credential/schema.py create mode 100644 keystone-moon/keystone/exception.py create mode 100644 keystone-moon/keystone/hacking/__init__.py create mode 100644 keystone-moon/keystone/hacking/checks.py create mode 100644 keystone-moon/keystone/i18n.py create mode 100644 keystone-moon/keystone/identity/__init__.py create mode 100644 keystone-moon/keystone/identity/backends/__init__.py create mode 100644 keystone-moon/keystone/identity/backends/ldap.py create mode 100644 keystone-moon/keystone/identity/backends/sql.py create mode 100644 keystone-moon/keystone/identity/controllers.py create mode 100644 keystone-moon/keystone/identity/core.py create mode 100644 keystone-moon/keystone/identity/generator.py create mode 100644 keystone-moon/keystone/identity/id_generators/__init__.py create mode 100644 keystone-moon/keystone/identity/id_generators/sha256.py create mode 100644 keystone-moon/keystone/identity/mapping_backends/__init__.py create mode 100644 keystone-moon/keystone/identity/mapping_backends/mapping.py create mode 100644 keystone-moon/keystone/identity/mapping_backends/sql.py create mode 100644 keystone-moon/keystone/identity/routers.py create mode 100644 keystone-moon/keystone/locale/de/LC_MESSAGES/keystone-log-critical.po create mode 100644 keystone-moon/keystone/locale/de/LC_MESSAGES/keystone-log-info.po create mode 100644 keystone-moon/keystone/locale/en_AU/LC_MESSAGES/keystone-log-critical.po create mode 100644 keystone-moon/keystone/locale/en_AU/LC_MESSAGES/keystone-log-error.po create mode 100644 keystone-moon/keystone/locale/en_AU/LC_MESSAGES/keystone.po create mode 100644 keystone-moon/keystone/locale/en_GB/LC_MESSAGES/keystone-log-info.po create mode 100644 keystone-moon/keystone/locale/es/LC_MESSAGES/keystone-log-critical.po create mode 100644 keystone-moon/keystone/locale/es/LC_MESSAGES/keystone-log-error.po create mode 100644 keystone-moon/keystone/locale/fr/LC_MESSAGES/keystone-log-critical.po create mode 100644 keystone-moon/keystone/locale/fr/LC_MESSAGES/keystone-log-error.po create mode 100644 keystone-moon/keystone/locale/fr/LC_MESSAGES/keystone-log-info.po create mode 100644 keystone-moon/keystone/locale/fr/LC_MESSAGES/keystone-log-warning.po create mode 100644 keystone-moon/keystone/locale/hu/LC_MESSAGES/keystone-log-critical.po create mode 100644 keystone-moon/keystone/locale/it/LC_MESSAGES/keystone-log-critical.po create mode 100644 keystone-moon/keystone/locale/it/LC_MESSAGES/keystone-log-error.po create mode 100644 keystone-moon/keystone/locale/it/LC_MESSAGES/keystone-log-info.po create mode 100644 keystone-moon/keystone/locale/ja/LC_MESSAGES/keystone-log-critical.po create mode 100644 keystone-moon/keystone/locale/ja/LC_MESSAGES/keystone-log-error.po create mode 100644 keystone-moon/keystone/locale/keystone-log-critical.pot create mode 100644 keystone-moon/keystone/locale/keystone-log-error.pot create mode 100644 keystone-moon/keystone/locale/keystone-log-info.pot create mode 100644 keystone-moon/keystone/locale/keystone-log-warning.pot create mode 100644 keystone-moon/keystone/locale/keystone.pot create mode 100644 keystone-moon/keystone/locale/ko_KR/LC_MESSAGES/keystone-log-critical.po create mode 100644 keystone-moon/keystone/locale/pl_PL/LC_MESSAGES/keystone-log-critical.po create mode 100644 keystone-moon/keystone/locale/pt_BR/LC_MESSAGES/keystone-log-critical.po create mode 100644 keystone-moon/keystone/locale/pt_BR/LC_MESSAGES/keystone-log-error.po create mode 100644 keystone-moon/keystone/locale/pt_BR/LC_MESSAGES/keystone.po create mode 100644 keystone-moon/keystone/locale/ru/LC_MESSAGES/keystone-log-critical.po create mode 100644 keystone-moon/keystone/locale/vi_VN/LC_MESSAGES/keystone-log-info.po create mode 100644 keystone-moon/keystone/locale/zh_CN/LC_MESSAGES/keystone-log-critical.po create mode 100644 keystone-moon/keystone/locale/zh_CN/LC_MESSAGES/keystone-log-error.po create mode 100644 keystone-moon/keystone/locale/zh_CN/LC_MESSAGES/keystone-log-info.po create mode 100644 keystone-moon/keystone/locale/zh_TW/LC_MESSAGES/keystone-log-critical.po create mode 100644 keystone-moon/keystone/middleware/__init__.py create mode 100644 keystone-moon/keystone/middleware/core.py create mode 100644 keystone-moon/keystone/middleware/ec2_token.py create mode 100644 keystone-moon/keystone/models/__init__.py create mode 100644 keystone-moon/keystone/models/token_model.py create mode 100644 keystone-moon/keystone/notifications.py create mode 100644 keystone-moon/keystone/openstack/__init__.py create mode 100644 keystone-moon/keystone/openstack/common/README create mode 100644 keystone-moon/keystone/openstack/common/__init__.py create mode 100644 keystone-moon/keystone/openstack/common/_i18n.py create mode 100644 keystone-moon/keystone/openstack/common/eventlet_backdoor.py create mode 100644 keystone-moon/keystone/openstack/common/fileutils.py create mode 100644 keystone-moon/keystone/openstack/common/loopingcall.py create mode 100644 keystone-moon/keystone/openstack/common/service.py create mode 100644 keystone-moon/keystone/openstack/common/systemd.py create mode 100644 keystone-moon/keystone/openstack/common/threadgroup.py create mode 100644 keystone-moon/keystone/openstack/common/versionutils.py create mode 100644 keystone-moon/keystone/policy/__init__.py create mode 100644 keystone-moon/keystone/policy/backends/__init__.py create mode 100644 keystone-moon/keystone/policy/backends/rules.py create mode 100644 keystone-moon/keystone/policy/backends/sql.py create mode 100644 keystone-moon/keystone/policy/controllers.py create mode 100644 keystone-moon/keystone/policy/core.py create mode 100644 keystone-moon/keystone/policy/routers.py create mode 100644 keystone-moon/keystone/policy/schema.py create mode 100644 keystone-moon/keystone/resource/__init__.py create mode 100644 keystone-moon/keystone/resource/backends/__init__.py create mode 100644 keystone-moon/keystone/resource/backends/ldap.py create mode 100644 keystone-moon/keystone/resource/backends/sql.py create mode 100644 keystone-moon/keystone/resource/config_backends/__init__.py create mode 100644 keystone-moon/keystone/resource/config_backends/sql.py create mode 100644 keystone-moon/keystone/resource/controllers.py create mode 100644 keystone-moon/keystone/resource/core.py create mode 100644 keystone-moon/keystone/resource/routers.py create mode 100644 keystone-moon/keystone/resource/schema.py create mode 100644 keystone-moon/keystone/routers.py create mode 100644 keystone-moon/keystone/server/__init__.py create mode 100644 keystone-moon/keystone/server/common.py create mode 100644 keystone-moon/keystone/server/eventlet.py create mode 100644 keystone-moon/keystone/server/wsgi.py create mode 100644 keystone-moon/keystone/service.py create mode 100644 keystone-moon/keystone/tests/__init__.py create mode 100644 keystone-moon/keystone/tests/moon/__init__.py create mode 100644 keystone-moon/keystone/tests/moon/func/__init__.py create mode 100644 keystone-moon/keystone/tests/moon/func/test_func_api_authz.py create mode 100644 keystone-moon/keystone/tests/moon/func/test_func_api_intra_extension_admin.py create mode 100644 keystone-moon/keystone/tests/moon/func/test_func_api_log.py create mode 100644 keystone-moon/keystone/tests/moon/func/test_func_api_tenant.py create mode 100644 keystone-moon/keystone/tests/moon/unit/__init__.py create mode 100644 keystone-moon/keystone/tests/moon/unit/test_unit_core_intra_extension_admin.py create mode 100644 keystone-moon/keystone/tests/moon/unit/test_unit_core_intra_extension_authz.py create mode 100644 keystone-moon/keystone/tests/moon/unit/test_unit_core_log.py create mode 100644 keystone-moon/keystone/tests/moon/unit/test_unit_core_tenant.py create mode 100644 keystone-moon/keystone/tests/unit/__init__.py create mode 100644 keystone-moon/keystone/tests/unit/backend/__init__.py create mode 100644 keystone-moon/keystone/tests/unit/backend/core_ldap.py create mode 100644 keystone-moon/keystone/tests/unit/backend/core_sql.py create mode 100644 keystone-moon/keystone/tests/unit/backend/domain_config/__init__.py create mode 100644 keystone-moon/keystone/tests/unit/backend/domain_config/core.py create mode 100644 keystone-moon/keystone/tests/unit/backend/domain_config/test_sql.py create mode 100644 keystone-moon/keystone/tests/unit/backend/role/__init__.py create mode 100644 keystone-moon/keystone/tests/unit/backend/role/core.py create mode 100644 keystone-moon/keystone/tests/unit/backend/role/test_ldap.py create mode 100644 keystone-moon/keystone/tests/unit/backend/role/test_sql.py create mode 100644 keystone-moon/keystone/tests/unit/catalog/__init__.py create mode 100644 keystone-moon/keystone/tests/unit/catalog/test_core.py create mode 100644 keystone-moon/keystone/tests/unit/common/__init__.py create mode 100644 keystone-moon/keystone/tests/unit/common/test_base64utils.py create mode 100644 keystone-moon/keystone/tests/unit/common/test_connection_pool.py create mode 100644 keystone-moon/keystone/tests/unit/common/test_injection.py create mode 100644 keystone-moon/keystone/tests/unit/common/test_json_home.py create mode 100644 keystone-moon/keystone/tests/unit/common/test_ldap.py create mode 100644 keystone-moon/keystone/tests/unit/common/test_notifications.py create mode 100644 keystone-moon/keystone/tests/unit/common/test_pemutils.py create mode 100644 keystone-moon/keystone/tests/unit/common/test_sql_core.py create mode 100644 keystone-moon/keystone/tests/unit/common/test_utils.py create mode 100644 keystone-moon/keystone/tests/unit/config_files/backend_db2.conf create mode 100644 keystone-moon/keystone/tests/unit/config_files/backend_ldap.conf create mode 100644 keystone-moon/keystone/tests/unit/config_files/backend_ldap_pool.conf create mode 100644 keystone-moon/keystone/tests/unit/config_files/backend_ldap_sql.conf create mode 100644 keystone-moon/keystone/tests/unit/config_files/backend_liveldap.conf create mode 100644 keystone-moon/keystone/tests/unit/config_files/backend_multi_ldap_sql.conf create mode 100644 keystone-moon/keystone/tests/unit/config_files/backend_mysql.conf create mode 100644 keystone-moon/keystone/tests/unit/config_files/backend_pool_liveldap.conf create mode 100644 keystone-moon/keystone/tests/unit/config_files/backend_postgresql.conf create mode 100644 keystone-moon/keystone/tests/unit/config_files/backend_sql.conf create mode 100644 keystone-moon/keystone/tests/unit/config_files/backend_tls_liveldap.conf create mode 100644 keystone-moon/keystone/tests/unit/config_files/deprecated.conf create mode 100644 keystone-moon/keystone/tests/unit/config_files/deprecated_override.conf create mode 100644 keystone-moon/keystone/tests/unit/config_files/domain_configs_default_ldap_one_sql/keystone.domain1.conf create mode 100644 keystone-moon/keystone/tests/unit/config_files/domain_configs_multi_ldap/keystone.Default.conf create mode 100644 keystone-moon/keystone/tests/unit/config_files/domain_configs_multi_ldap/keystone.domain1.conf create mode 100644 keystone-moon/keystone/tests/unit/config_files/domain_configs_multi_ldap/keystone.domain2.conf create mode 100644 keystone-moon/keystone/tests/unit/config_files/domain_configs_one_extra_sql/keystone.domain2.conf create mode 100644 keystone-moon/keystone/tests/unit/config_files/domain_configs_one_sql_one_ldap/keystone.Default.conf create mode 100644 keystone-moon/keystone/tests/unit/config_files/domain_configs_one_sql_one_ldap/keystone.domain1.conf create mode 100644 keystone-moon/keystone/tests/unit/config_files/test_auth_plugin.conf create mode 100644 keystone-moon/keystone/tests/unit/core.py create mode 100644 keystone-moon/keystone/tests/unit/default_catalog.templates create mode 100644 keystone-moon/keystone/tests/unit/default_fixtures.py create mode 100644 keystone-moon/keystone/tests/unit/fakeldap.py create mode 100644 keystone-moon/keystone/tests/unit/federation_fixtures.py create mode 100644 keystone-moon/keystone/tests/unit/filtering.py create mode 100644 keystone-moon/keystone/tests/unit/identity/__init__.py create mode 100644 keystone-moon/keystone/tests/unit/identity/test_core.py create mode 100644 keystone-moon/keystone/tests/unit/identity_mapping.py create mode 100644 keystone-moon/keystone/tests/unit/ksfixtures/__init__.py create mode 100644 keystone-moon/keystone/tests/unit/ksfixtures/appserver.py create mode 100644 keystone-moon/keystone/tests/unit/ksfixtures/cache.py create mode 100644 keystone-moon/keystone/tests/unit/ksfixtures/database.py create mode 100644 keystone-moon/keystone/tests/unit/ksfixtures/hacking.py create mode 100644 keystone-moon/keystone/tests/unit/ksfixtures/key_repository.py create mode 100644 keystone-moon/keystone/tests/unit/ksfixtures/temporaryfile.py create mode 100644 keystone-moon/keystone/tests/unit/mapping_fixtures.py create mode 100644 keystone-moon/keystone/tests/unit/rest.py create mode 100644 keystone-moon/keystone/tests/unit/saml2/idp_saml2_metadata.xml create mode 100644 keystone-moon/keystone/tests/unit/saml2/signed_saml2_assertion.xml create mode 100644 keystone-moon/keystone/tests/unit/test_associate_project_endpoint_extension.py create mode 100644 keystone-moon/keystone/tests/unit/test_auth.py create mode 100644 keystone-moon/keystone/tests/unit/test_auth_plugin.py create mode 100644 keystone-moon/keystone/tests/unit/test_backend.py create mode 100644 keystone-moon/keystone/tests/unit/test_backend_endpoint_policy.py create mode 100644 keystone-moon/keystone/tests/unit/test_backend_endpoint_policy_sql.py create mode 100644 keystone-moon/keystone/tests/unit/test_backend_federation_sql.py create mode 100644 keystone-moon/keystone/tests/unit/test_backend_id_mapping_sql.py create mode 100644 keystone-moon/keystone/tests/unit/test_backend_kvs.py create mode 100644 keystone-moon/keystone/tests/unit/test_backend_ldap.py create mode 100644 keystone-moon/keystone/tests/unit/test_backend_ldap_pool.py create mode 100644 keystone-moon/keystone/tests/unit/test_backend_rules.py create mode 100644 keystone-moon/keystone/tests/unit/test_backend_sql.py create mode 100644 keystone-moon/keystone/tests/unit/test_backend_templated.py create mode 100644 keystone-moon/keystone/tests/unit/test_cache.py create mode 100644 keystone-moon/keystone/tests/unit/test_cache_backend_mongo.py create mode 100644 keystone-moon/keystone/tests/unit/test_catalog.py create mode 100644 keystone-moon/keystone/tests/unit/test_cert_setup.py create mode 100644 keystone-moon/keystone/tests/unit/test_cli.py create mode 100644 keystone-moon/keystone/tests/unit/test_config.py create mode 100644 keystone-moon/keystone/tests/unit/test_contrib_s3_core.py create mode 100644 keystone-moon/keystone/tests/unit/test_contrib_simple_cert.py create mode 100644 keystone-moon/keystone/tests/unit/test_driver_hints.py create mode 100644 keystone-moon/keystone/tests/unit/test_ec2_token_middleware.py create mode 100644 keystone-moon/keystone/tests/unit/test_exception.py create mode 100644 keystone-moon/keystone/tests/unit/test_hacking_checks.py create mode 100644 keystone-moon/keystone/tests/unit/test_ipv6.py create mode 100644 keystone-moon/keystone/tests/unit/test_kvs.py create mode 100644 keystone-moon/keystone/tests/unit/test_ldap_livetest.py create mode 100644 keystone-moon/keystone/tests/unit/test_ldap_pool_livetest.py create mode 100644 keystone-moon/keystone/tests/unit/test_ldap_tls_livetest.py create mode 100644 keystone-moon/keystone/tests/unit/test_middleware.py create mode 100644 keystone-moon/keystone/tests/unit/test_no_admin_token_auth.py create mode 100644 keystone-moon/keystone/tests/unit/test_policy.py create mode 100644 keystone-moon/keystone/tests/unit/test_revoke.py create mode 100644 keystone-moon/keystone/tests/unit/test_singular_plural.py create mode 100644 keystone-moon/keystone/tests/unit/test_sql_livetest.py create mode 100644 keystone-moon/keystone/tests/unit/test_sql_migrate_extensions.py create mode 100644 keystone-moon/keystone/tests/unit/test_sql_upgrade.py create mode 100644 keystone-moon/keystone/tests/unit/test_ssl.py create mode 100644 keystone-moon/keystone/tests/unit/test_token_bind.py create mode 100644 keystone-moon/keystone/tests/unit/test_token_provider.py create mode 100644 keystone-moon/keystone/tests/unit/test_url_middleware.py create mode 100644 keystone-moon/keystone/tests/unit/test_v2.py create mode 100644 keystone-moon/keystone/tests/unit/test_v2_controller.py create mode 100644 keystone-moon/keystone/tests/unit/test_v2_keystoneclient.py create mode 100644 keystone-moon/keystone/tests/unit/test_v2_keystoneclient_sql.py create mode 100644 keystone-moon/keystone/tests/unit/test_v3.py create mode 100644 keystone-moon/keystone/tests/unit/test_v3_assignment.py create mode 100644 keystone-moon/keystone/tests/unit/test_v3_auth.py create mode 100644 keystone-moon/keystone/tests/unit/test_v3_catalog.py create mode 100644 keystone-moon/keystone/tests/unit/test_v3_controller.py create mode 100644 keystone-moon/keystone/tests/unit/test_v3_credential.py create mode 100644 keystone-moon/keystone/tests/unit/test_v3_domain_config.py create mode 100644 keystone-moon/keystone/tests/unit/test_v3_endpoint_policy.py create mode 100644 keystone-moon/keystone/tests/unit/test_v3_federation.py create mode 100644 keystone-moon/keystone/tests/unit/test_v3_filters.py create mode 100644 keystone-moon/keystone/tests/unit/test_v3_identity.py create mode 100644 keystone-moon/keystone/tests/unit/test_v3_oauth1.py create mode 100644 keystone-moon/keystone/tests/unit/test_v3_os_revoke.py create mode 100644 keystone-moon/keystone/tests/unit/test_v3_policy.py create mode 100644 keystone-moon/keystone/tests/unit/test_v3_protection.py create mode 100644 keystone-moon/keystone/tests/unit/test_validation.py create mode 100644 keystone-moon/keystone/tests/unit/test_versions.py create mode 100644 keystone-moon/keystone/tests/unit/test_wsgi.py create mode 100644 keystone-moon/keystone/tests/unit/tests/__init__.py create mode 100644 keystone-moon/keystone/tests/unit/tests/test_core.py create mode 100644 keystone-moon/keystone/tests/unit/tests/test_utils.py create mode 100644 keystone-moon/keystone/tests/unit/token/__init__.py create mode 100644 keystone-moon/keystone/tests/unit/token/test_fernet_provider.py create mode 100644 keystone-moon/keystone/tests/unit/token/test_provider.py create mode 100644 keystone-moon/keystone/tests/unit/token/test_token_data_helper.py create mode 100644 keystone-moon/keystone/tests/unit/token/test_token_model.py create mode 100644 keystone-moon/keystone/tests/unit/utils.py create mode 100644 keystone-moon/keystone/token/__init__.py create mode 100644 keystone-moon/keystone/token/controllers.py create mode 100644 keystone-moon/keystone/token/persistence/__init__.py create mode 100644 keystone-moon/keystone/token/persistence/backends/__init__.py create mode 100644 keystone-moon/keystone/token/persistence/backends/kvs.py create mode 100644 keystone-moon/keystone/token/persistence/backends/memcache.py create mode 100644 keystone-moon/keystone/token/persistence/backends/memcache_pool.py create mode 100644 keystone-moon/keystone/token/persistence/backends/sql.py create mode 100644 keystone-moon/keystone/token/persistence/core.py create mode 100644 keystone-moon/keystone/token/provider.py create mode 100644 keystone-moon/keystone/token/providers/__init__.py create mode 100644 keystone-moon/keystone/token/providers/common.py create mode 100644 keystone-moon/keystone/token/providers/fernet/__init__.py create mode 100644 keystone-moon/keystone/token/providers/fernet/core.py create mode 100644 keystone-moon/keystone/token/providers/fernet/token_formatters.py create mode 100644 keystone-moon/keystone/token/providers/fernet/utils.py create mode 100644 keystone-moon/keystone/token/providers/pki.py create mode 100644 keystone-moon/keystone/token/providers/pkiz.py create mode 100644 keystone-moon/keystone/token/providers/uuid.py create mode 100644 keystone-moon/keystone/token/routers.py create mode 100644 keystone-moon/keystone/trust/__init__.py create mode 100644 keystone-moon/keystone/trust/backends/__init__.py create mode 100644 keystone-moon/keystone/trust/backends/sql.py create mode 100644 keystone-moon/keystone/trust/controllers.py create mode 100644 keystone-moon/keystone/trust/core.py create mode 100644 keystone-moon/keystone/trust/routers.py create mode 100644 keystone-moon/keystone/trust/schema.py (limited to 'keystone-moon/keystone') diff --git a/keystone-moon/keystone/__init__.py b/keystone-moon/keystone/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/assignment/__init__.py b/keystone-moon/keystone/assignment/__init__.py new file mode 100644 index 00000000..49ad7594 --- /dev/null +++ b/keystone-moon/keystone/assignment/__init__.py @@ -0,0 +1,17 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 keystone.assignment import controllers # noqa +from keystone.assignment.core import * # noqa +from keystone.assignment import routers # noqa diff --git a/keystone-moon/keystone/assignment/backends/__init__.py b/keystone-moon/keystone/assignment/backends/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/assignment/backends/ldap.py b/keystone-moon/keystone/assignment/backends/ldap.py new file mode 100644 index 00000000..f93e989f --- /dev/null +++ b/keystone-moon/keystone/assignment/backends/ldap.py @@ -0,0 +1,531 @@ +# Copyright 2012-2013 OpenStack Foundation +# +# 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 __future__ import absolute_import + +import ldap as ldap +import ldap.filter +from oslo_config import cfg +from oslo_log import log + +from keystone import assignment +from keystone.assignment.role_backends import ldap as ldap_role +from keystone.common import ldap as common_ldap +from keystone.common import models +from keystone import exception +from keystone.i18n import _ +from keystone.identity.backends import ldap as ldap_identity +from keystone.openstack.common import versionutils + + +CONF = cfg.CONF +LOG = log.getLogger(__name__) + + +class Assignment(assignment.Driver): + @versionutils.deprecated( + versionutils.deprecated.KILO, + remove_in=+2, + what='keystone.assignment.backends.ldap.Assignment') + def __init__(self): + super(Assignment, self).__init__() + self.LDAP_URL = CONF.ldap.url + self.LDAP_USER = CONF.ldap.user + self.LDAP_PASSWORD = CONF.ldap.password + self.suffix = CONF.ldap.suffix + + # This is the only deep dependency from assignment back to identity. + # This is safe to do since if you are using LDAP for assignment, it is + # required that you are using it for identity as well. + self.user = ldap_identity.UserApi(CONF) + self.group = ldap_identity.GroupApi(CONF) + + self.project = ProjectApi(CONF) + self.role = RoleApi(CONF, self.user) + + def default_role_driver(self): + return 'keystone.assignment.role_backends.ldap.Role' + + def default_resource_driver(self): + return 'keystone.resource.backends.ldap.Resource' + + def list_role_ids_for_groups_on_project( + self, groups, project_id, project_domain_id, project_parents): + group_dns = [self.group._id_to_dn(group_id) for group_id in groups] + role_list = [self.role._dn_to_id(role_assignment.role_dn) + for role_assignment in self.role.get_role_assignments + (self.project._id_to_dn(project_id)) + if role_assignment.user_dn.upper() in group_dns] + # NOTE(morganfainberg): Does not support OS-INHERIT as domain + # metadata/roles are not supported by LDAP backend. Skip OS-INHERIT + # logic. + return role_list + + def _get_metadata(self, user_id=None, tenant_id=None, + domain_id=None, group_id=None): + + def _get_roles_for_just_user_and_project(user_id, tenant_id): + user_dn = self.user._id_to_dn(user_id) + return [self.role._dn_to_id(a.role_dn) + for a in self.role.get_role_assignments + (self.project._id_to_dn(tenant_id)) + if common_ldap.is_dn_equal(a.user_dn, user_dn)] + + def _get_roles_for_group_and_project(group_id, project_id): + group_dn = self.group._id_to_dn(group_id) + return [self.role._dn_to_id(a.role_dn) + for a in self.role.get_role_assignments + (self.project._id_to_dn(project_id)) + if common_ldap.is_dn_equal(a.user_dn, group_dn)] + + if domain_id is not None: + msg = _('Domain metadata not supported by LDAP') + raise exception.NotImplemented(message=msg) + if group_id is None and user_id is None: + return {} + + if tenant_id is None: + return {} + if user_id is None: + metadata_ref = _get_roles_for_group_and_project(group_id, + tenant_id) + else: + metadata_ref = _get_roles_for_just_user_and_project(user_id, + tenant_id) + if not metadata_ref: + return {} + return {'roles': [self._role_to_dict(r, False) for r in metadata_ref]} + + def list_project_ids_for_user(self, user_id, group_ids, hints, + inherited=False): + # TODO(henry-nash): The ldap driver does not support inherited + # assignments, so the inherited parameter is unused. + # See bug #1404273. + user_dn = self.user._id_to_dn(user_id) + associations = (self.role.list_project_roles_for_user + (user_dn, self.project.tree_dn)) + + for group_id in group_ids: + group_dn = self.group._id_to_dn(group_id) + for group_role in self.role.list_project_roles_for_group( + group_dn, self.project.tree_dn): + associations.append(group_role) + + return list(set( + [self.project._dn_to_id(x.project_dn) for x in associations])) + + def list_role_ids_for_groups_on_domain(self, group_ids, domain_id): + raise exception.NotImplemented() + + def list_project_ids_for_groups(self, group_ids, hints, + inherited=False): + raise exception.NotImplemented() + + def list_domain_ids_for_user(self, user_id, group_ids, hints): + raise exception.NotImplemented() + + def list_domain_ids_for_groups(self, group_ids, inherited=False): + raise exception.NotImplemented() + + def list_user_ids_for_project(self, tenant_id): + tenant_dn = self.project._id_to_dn(tenant_id) + rolegrants = self.role.get_role_assignments(tenant_dn) + return [self.user._dn_to_id(user_dn) for user_dn in + self.project.get_user_dns(tenant_id, rolegrants)] + + def _subrole_id_to_dn(self, role_id, tenant_id): + if tenant_id is None: + return self.role._id_to_dn(role_id) + else: + return '%s=%s,%s' % (self.role.id_attr, + ldap.dn.escape_dn_chars(role_id), + self.project._id_to_dn(tenant_id)) + + def add_role_to_user_and_project(self, user_id, tenant_id, role_id): + user_dn = self.user._id_to_dn(user_id) + role_dn = self._subrole_id_to_dn(role_id, tenant_id) + self.role.add_user(role_id, role_dn, user_dn, user_id, tenant_id) + tenant_dn = self.project._id_to_dn(tenant_id) + return UserRoleAssociation(role_dn=role_dn, + user_dn=user_dn, + tenant_dn=tenant_dn) + + def _add_role_to_group_and_project(self, group_id, tenant_id, role_id): + group_dn = self.group._id_to_dn(group_id) + role_dn = self._subrole_id_to_dn(role_id, tenant_id) + self.role.add_user(role_id, role_dn, group_dn, group_id, tenant_id) + tenant_dn = self.project._id_to_dn(tenant_id) + return GroupRoleAssociation(group_dn=group_dn, + role_dn=role_dn, + tenant_dn=tenant_dn) + + def remove_role_from_user_and_project(self, user_id, tenant_id, role_id): + role_dn = self._subrole_id_to_dn(role_id, tenant_id) + return self.role.delete_user(role_dn, + self.user._id_to_dn(user_id), role_id) + + def _remove_role_from_group_and_project(self, group_id, tenant_id, + role_id): + role_dn = self._subrole_id_to_dn(role_id, tenant_id) + return self.role.delete_user(role_dn, + self.group._id_to_dn(group_id), role_id) + +# Bulk actions on User From identity + def delete_user(self, user_id): + user_dn = self.user._id_to_dn(user_id) + for ref in self.role.list_global_roles_for_user(user_dn): + self.role.delete_user(ref.role_dn, ref.user_dn, + self.role._dn_to_id(ref.role_dn)) + for ref in self.role.list_project_roles_for_user(user_dn, + self.project.tree_dn): + self.role.delete_user(ref.role_dn, ref.user_dn, + self.role._dn_to_id(ref.role_dn)) + + def delete_group(self, group_id): + """Called when the group was deleted. + + Any role assignments for the group should be cleaned up. + + """ + group_dn = self.group._id_to_dn(group_id) + group_role_assignments = self.role.list_project_roles_for_group( + group_dn, self.project.tree_dn) + for ref in group_role_assignments: + self.role.delete_user(ref.role_dn, ref.group_dn, + self.role._dn_to_id(ref.role_dn)) + + def create_grant(self, role_id, user_id=None, group_id=None, + domain_id=None, project_id=None, + inherited_to_projects=False): + + try: + metadata_ref = self._get_metadata(user_id, project_id, + domain_id, group_id) + except exception.MetadataNotFound: + metadata_ref = {} + + if user_id is None: + metadata_ref['roles'] = self._add_role_to_group_and_project( + group_id, project_id, role_id) + else: + metadata_ref['roles'] = self.add_role_to_user_and_project( + user_id, project_id, role_id) + + def check_grant_role_id(self, role_id, user_id=None, group_id=None, + domain_id=None, project_id=None, + inherited_to_projects=False): + + try: + metadata_ref = self._get_metadata(user_id, project_id, + domain_id, group_id) + except exception.MetadataNotFound: + metadata_ref = {} + role_ids = set(self._roles_from_role_dicts( + metadata_ref.get('roles', []), inherited_to_projects)) + if role_id not in role_ids: + actor_id = user_id or group_id + target_id = domain_id or project_id + raise exception.RoleAssignmentNotFound(role_id=role_id, + actor_id=actor_id, + target_id=target_id) + + def delete_grant(self, role_id, user_id=None, group_id=None, + domain_id=None, project_id=None, + inherited_to_projects=False): + + try: + metadata_ref = self._get_metadata(user_id, project_id, + domain_id, group_id) + except exception.MetadataNotFound: + metadata_ref = {} + + try: + if user_id is None: + metadata_ref['roles'] = ( + self._remove_role_from_group_and_project( + group_id, project_id, role_id)) + else: + metadata_ref['roles'] = self.remove_role_from_user_and_project( + user_id, project_id, role_id) + except (exception.RoleNotFound, KeyError): + actor_id = user_id or group_id + target_id = domain_id or project_id + raise exception.RoleAssignmentNotFound(role_id=role_id, + actor_id=actor_id, + target_id=target_id) + + def list_grant_role_ids(self, user_id=None, group_id=None, + domain_id=None, project_id=None, + inherited_to_projects=False): + + try: + metadata_ref = self._get_metadata(user_id, project_id, + domain_id, group_id) + except exception.MetadataNotFound: + metadata_ref = {} + + return self._roles_from_role_dicts(metadata_ref.get('roles', []), + inherited_to_projects) + + def list_role_assignments(self): + role_assignments = [] + for a in self.role.list_role_assignments(self.project.tree_dn): + if isinstance(a, UserRoleAssociation): + assignment = { + 'role_id': self.role._dn_to_id(a.role_dn), + 'user_id': self.user._dn_to_id(a.user_dn), + 'project_id': self.project._dn_to_id(a.project_dn)} + else: + assignment = { + 'role_id': self.role._dn_to_id(a.role_dn), + 'group_id': self.group._dn_to_id(a.group_dn), + 'project_id': self.project._dn_to_id(a.project_dn)} + role_assignments.append(assignment) + return role_assignments + + def delete_project_assignments(self, project_id): + tenant_dn = self.project._id_to_dn(project_id) + self.role.roles_delete_subtree_by_project(tenant_dn) + + def delete_role_assignments(self, role_id): + self.role.roles_delete_subtree_by_role(role_id, self.project.tree_dn) + + +# TODO(termie): turn this into a data object and move logic to driver +class ProjectApi(common_ldap.ProjectLdapStructureMixin, + common_ldap.EnabledEmuMixIn, common_ldap.BaseLdap): + + model = models.Project + + def __init__(self, conf): + super(ProjectApi, self).__init__(conf) + self.member_attribute = (conf.ldap.project_member_attribute + or self.DEFAULT_MEMBER_ATTRIBUTE) + + def get_user_projects(self, user_dn, associations): + """Returns list of tenants a user has access to + """ + + project_ids = set() + for assoc in associations: + project_ids.add(self._dn_to_id(assoc.project_dn)) + projects = [] + for project_id in project_ids: + # slower to get them one at a time, but a huge list could blow out + # the connection. This is the safer way + projects.append(self.get(project_id)) + return projects + + def get_user_dns(self, tenant_id, rolegrants, role_dn=None): + tenant = self._ldap_get(tenant_id) + res = set() + if not role_dn: + # Get users who have default tenant mapping + for user_dn in tenant[1].get(self.member_attribute, []): + if self._is_dumb_member(user_dn): + continue + res.add(user_dn) + + # Get users who are explicitly mapped via a tenant + for rolegrant in rolegrants: + if role_dn is None or rolegrant.role_dn == role_dn: + res.add(rolegrant.user_dn) + return list(res) + + +class UserRoleAssociation(object): + """Role Grant model.""" + + def __init__(self, user_dn=None, role_dn=None, tenant_dn=None, + *args, **kw): + self.user_dn = user_dn + self.role_dn = role_dn + self.project_dn = tenant_dn + + +class GroupRoleAssociation(object): + """Role Grant model.""" + + def __init__(self, group_dn=None, role_dn=None, tenant_dn=None, + *args, **kw): + self.group_dn = group_dn + self.role_dn = role_dn + self.project_dn = tenant_dn + + +# TODO(termie): turn this into a data object and move logic to driver +# NOTE(heny-nash): The RoleLdapStructureMixin class enables the sharing of the +# LDAP structure between here and the role backend LDAP, no methods are shared. +class RoleApi(ldap_role.RoleLdapStructureMixin, common_ldap.BaseLdap): + + def __init__(self, conf, user_api): + super(RoleApi, self).__init__(conf) + self.member_attribute = (conf.ldap.role_member_attribute + or self.DEFAULT_MEMBER_ATTRIBUTE) + self._user_api = user_api + + def add_user(self, role_id, role_dn, user_dn, user_id, tenant_id=None): + try: + super(RoleApi, self).add_member(user_dn, role_dn) + except exception.Conflict: + msg = (_('User %(user_id)s already has role %(role_id)s in ' + 'tenant %(tenant_id)s') % + dict(user_id=user_id, role_id=role_id, tenant_id=tenant_id)) + raise exception.Conflict(type='role grant', details=msg) + except self.NotFound: + if tenant_id is None or self.get(role_id) is None: + raise Exception(_("Role %s not found") % (role_id,)) + + attrs = [('objectClass', [self.object_class]), + (self.member_attribute, [user_dn]), + (self.id_attr, [role_id])] + + if self.use_dumb_member: + attrs[1][1].append(self.dumb_member) + with self.get_connection() as conn: + conn.add_s(role_dn, attrs) + + def delete_user(self, role_dn, user_dn, role_id): + try: + super(RoleApi, self).remove_member(user_dn, role_dn) + except (self.NotFound, ldap.NO_SUCH_ATTRIBUTE): + raise exception.RoleNotFound(message=_( + 'Cannot remove role that has not been granted, %s') % + role_id) + + def get_role_assignments(self, tenant_dn): + try: + roles = self._ldap_get_list(tenant_dn, ldap.SCOPE_ONELEVEL, + attrlist=[self.member_attribute]) + except ldap.NO_SUCH_OBJECT: + roles = [] + res = [] + for role_dn, attrs in roles: + try: + user_dns = attrs[self.member_attribute] + except KeyError: + continue + for user_dn in user_dns: + if self._is_dumb_member(user_dn): + continue + res.append(UserRoleAssociation( + user_dn=user_dn, + role_dn=role_dn, + tenant_dn=tenant_dn)) + + return res + + def list_global_roles_for_user(self, user_dn): + user_dn_esc = ldap.filter.escape_filter_chars(user_dn) + roles = self.get_all('(%s=%s)' % (self.member_attribute, user_dn_esc)) + return [UserRoleAssociation( + role_dn=role.dn, + user_dn=user_dn) for role in roles] + + def list_project_roles_for_user(self, user_dn, project_subtree): + try: + roles = self._ldap_get_list(project_subtree, ldap.SCOPE_SUBTREE, + query_params={ + self.member_attribute: user_dn}, + attrlist=common_ldap.DN_ONLY) + except ldap.NO_SUCH_OBJECT: + roles = [] + res = [] + for role_dn, _role_attrs in roles: + # ldap.dn.dn2str returns an array, where the first + # element is the first segment. + # For a role assignment, this contains the role ID, + # The remainder is the DN of the tenant. + # role_dn is already utf8 encoded since it came from LDAP. + tenant = ldap.dn.str2dn(role_dn) + tenant.pop(0) + tenant_dn = ldap.dn.dn2str(tenant) + res.append(UserRoleAssociation( + user_dn=user_dn, + role_dn=role_dn, + tenant_dn=tenant_dn)) + return res + + def list_project_roles_for_group(self, group_dn, project_subtree): + group_dn_esc = ldap.filter.escape_filter_chars(group_dn) + query = '(&(objectClass=%s)(%s=%s))' % (self.object_class, + self.member_attribute, + group_dn_esc) + with self.get_connection() as conn: + try: + roles = conn.search_s(project_subtree, + ldap.SCOPE_SUBTREE, + query, + attrlist=common_ldap.DN_ONLY) + except ldap.NO_SUCH_OBJECT: + # Return no roles rather than raise an exception if the project + # subtree entry doesn't exist because an empty subtree is not + # an error. + return [] + + res = [] + for role_dn, _role_attrs in roles: + # ldap.dn.str2dn returns a list, where the first + # element is the first RDN. + # For a role assignment, this contains the role ID, + # the remainder is the DN of the project. + # role_dn is already utf8 encoded since it came from LDAP. + project = ldap.dn.str2dn(role_dn) + project.pop(0) + project_dn = ldap.dn.dn2str(project) + res.append(GroupRoleAssociation( + group_dn=group_dn, + role_dn=role_dn, + tenant_dn=project_dn)) + return res + + def roles_delete_subtree_by_project(self, tenant_dn): + self._delete_tree_nodes(tenant_dn, ldap.SCOPE_ONELEVEL) + + def roles_delete_subtree_by_role(self, role_id, tree_dn): + self._delete_tree_nodes(tree_dn, ldap.SCOPE_SUBTREE, query_params={ + self.id_attr: role_id}) + + def list_role_assignments(self, project_tree_dn): + """Returns a list of all the role assignments linked to project_tree_dn + attribute. + """ + try: + roles = self._ldap_get_list(project_tree_dn, ldap.SCOPE_SUBTREE, + attrlist=[self.member_attribute]) + except ldap.NO_SUCH_OBJECT: + roles = [] + res = [] + for role_dn, role in roles: + # role_dn is already utf8 encoded since it came from LDAP. + tenant = ldap.dn.str2dn(role_dn) + tenant.pop(0) + # It obtains the tenant DN to construct the UserRoleAssociation + # object. + tenant_dn = ldap.dn.dn2str(tenant) + for occupant_dn in role[self.member_attribute]: + if self._is_dumb_member(occupant_dn): + continue + if self._user_api.is_user(occupant_dn): + association = UserRoleAssociation( + user_dn=occupant_dn, + role_dn=role_dn, + tenant_dn=tenant_dn) + else: + # occupant_dn is a group. + association = GroupRoleAssociation( + group_dn=occupant_dn, + role_dn=role_dn, + tenant_dn=tenant_dn) + res.append(association) + return res diff --git a/keystone-moon/keystone/assignment/backends/sql.py b/keystone-moon/keystone/assignment/backends/sql.py new file mode 100644 index 00000000..2de6ca60 --- /dev/null +++ b/keystone-moon/keystone/assignment/backends/sql.py @@ -0,0 +1,415 @@ +# Copyright 2012-13 OpenStack Foundation +# +# 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 oslo_config import cfg +from oslo_log import log +import six +import sqlalchemy +from sqlalchemy.sql.expression import false + +from keystone import assignment as keystone_assignment +from keystone.common import sql +from keystone import exception +from keystone.i18n import _ + + +CONF = cfg.CONF +LOG = log.getLogger(__name__) + + +class AssignmentType(object): + USER_PROJECT = 'UserProject' + GROUP_PROJECT = 'GroupProject' + USER_DOMAIN = 'UserDomain' + GROUP_DOMAIN = 'GroupDomain' + + @classmethod + def calculate_type(cls, user_id, group_id, project_id, domain_id): + if user_id: + if project_id: + return cls.USER_PROJECT + if domain_id: + return cls.USER_DOMAIN + if group_id: + if project_id: + return cls.GROUP_PROJECT + if domain_id: + return cls.GROUP_DOMAIN + # Invalid parameters combination + raise exception.AssignmentTypeCalculationError(**locals()) + + +class Assignment(keystone_assignment.Driver): + + def default_role_driver(self): + return "keystone.assignment.role_backends.sql.Role" + + def default_resource_driver(self): + return 'keystone.resource.backends.sql.Resource' + + def list_user_ids_for_project(self, tenant_id): + with sql.transaction() as session: + query = session.query(RoleAssignment.actor_id) + query = query.filter_by(type=AssignmentType.USER_PROJECT) + query = query.filter_by(target_id=tenant_id) + query = query.distinct('actor_id') + assignments = query.all() + return [assignment.actor_id for assignment in assignments] + + def _get_metadata(self, user_id=None, tenant_id=None, + domain_id=None, group_id=None, session=None): + # TODO(henry-nash): This method represents the last vestiges of the old + # metadata concept in this driver. Although we no longer need it here, + # since the Manager layer uses the metadata concept across all + # assignment drivers, we need to remove it from all of them in order to + # finally remove this method. + + # We aren't given a session when called by the manager directly. + if session is None: + session = sql.get_session() + + q = session.query(RoleAssignment) + + def _calc_assignment_type(): + # Figure out the assignment type we're checking for from the args. + if user_id: + if tenant_id: + return AssignmentType.USER_PROJECT + else: + return AssignmentType.USER_DOMAIN + else: + if tenant_id: + return AssignmentType.GROUP_PROJECT + else: + return AssignmentType.GROUP_DOMAIN + + q = q.filter_by(type=_calc_assignment_type()) + q = q.filter_by(actor_id=user_id or group_id) + q = q.filter_by(target_id=tenant_id or domain_id) + refs = q.all() + if not refs: + raise exception.MetadataNotFound() + + metadata_ref = {} + metadata_ref['roles'] = [] + for assignment in refs: + role_ref = {} + role_ref['id'] = assignment.role_id + if assignment.inherited: + role_ref['inherited_to'] = 'projects' + metadata_ref['roles'].append(role_ref) + + return metadata_ref + + def create_grant(self, role_id, user_id=None, group_id=None, + domain_id=None, project_id=None, + inherited_to_projects=False): + + assignment_type = AssignmentType.calculate_type( + user_id, group_id, project_id, domain_id) + try: + with sql.transaction() as session: + session.add(RoleAssignment( + type=assignment_type, + actor_id=user_id or group_id, + target_id=project_id or domain_id, + role_id=role_id, + inherited=inherited_to_projects)) + except sql.DBDuplicateEntry: + # The v3 grant APIs are silent if the assignment already exists + pass + + def list_grant_role_ids(self, user_id=None, group_id=None, + domain_id=None, project_id=None, + inherited_to_projects=False): + with sql.transaction() as session: + q = session.query(RoleAssignment.role_id) + q = q.filter(RoleAssignment.actor_id == (user_id or group_id)) + q = q.filter(RoleAssignment.target_id == (project_id or domain_id)) + q = q.filter(RoleAssignment.inherited == inherited_to_projects) + return [x.role_id for x in q.all()] + + def _build_grant_filter(self, session, role_id, user_id, group_id, + domain_id, project_id, inherited_to_projects): + q = session.query(RoleAssignment) + q = q.filter_by(actor_id=user_id or group_id) + q = q.filter_by(target_id=project_id or domain_id) + q = q.filter_by(role_id=role_id) + q = q.filter_by(inherited=inherited_to_projects) + return q + + def check_grant_role_id(self, role_id, user_id=None, group_id=None, + domain_id=None, project_id=None, + inherited_to_projects=False): + with sql.transaction() as session: + try: + q = self._build_grant_filter( + session, role_id, user_id, group_id, domain_id, project_id, + inherited_to_projects) + q.one() + except sql.NotFound: + actor_id = user_id or group_id + target_id = domain_id or project_id + raise exception.RoleAssignmentNotFound(role_id=role_id, + actor_id=actor_id, + target_id=target_id) + + def delete_grant(self, role_id, user_id=None, group_id=None, + domain_id=None, project_id=None, + inherited_to_projects=False): + with sql.transaction() as session: + q = self._build_grant_filter( + session, role_id, user_id, group_id, domain_id, project_id, + inherited_to_projects) + if not q.delete(False): + actor_id = user_id or group_id + target_id = domain_id or project_id + raise exception.RoleAssignmentNotFound(role_id=role_id, + actor_id=actor_id, + target_id=target_id) + + def _list_project_ids_for_actor(self, actors, hints, inherited, + group_only=False): + # TODO(henry-nash): Now that we have a single assignment table, we + # should be able to honor the hints list that is provided. + + assignment_type = [AssignmentType.GROUP_PROJECT] + if not group_only: + assignment_type.append(AssignmentType.USER_PROJECT) + + sql_constraints = sqlalchemy.and_( + RoleAssignment.type.in_(assignment_type), + RoleAssignment.inherited == inherited, + RoleAssignment.actor_id.in_(actors)) + + with sql.transaction() as session: + query = session.query(RoleAssignment.target_id).filter( + sql_constraints).distinct() + + return [x.target_id for x in query.all()] + + def list_project_ids_for_user(self, user_id, group_ids, hints, + inherited=False): + actor_list = [user_id] + if group_ids: + actor_list = actor_list + group_ids + + return self._list_project_ids_for_actor(actor_list, hints, inherited) + + def list_domain_ids_for_user(self, user_id, group_ids, hints, + inherited=False): + with sql.transaction() as session: + query = session.query(RoleAssignment.target_id) + filters = [] + + if user_id: + sql_constraints = sqlalchemy.and_( + RoleAssignment.actor_id == user_id, + RoleAssignment.inherited == inherited, + RoleAssignment.type == AssignmentType.USER_DOMAIN) + filters.append(sql_constraints) + + if group_ids: + sql_constraints = sqlalchemy.and_( + RoleAssignment.actor_id.in_(group_ids), + RoleAssignment.inherited == inherited, + RoleAssignment.type == AssignmentType.GROUP_DOMAIN) + filters.append(sql_constraints) + + if not filters: + return [] + + query = query.filter(sqlalchemy.or_(*filters)).distinct() + + return [assignment.target_id for assignment in query.all()] + + def list_role_ids_for_groups_on_domain(self, group_ids, domain_id): + if not group_ids: + # If there's no groups then there will be no domain roles. + return [] + + sql_constraints = sqlalchemy.and_( + RoleAssignment.type == AssignmentType.GROUP_DOMAIN, + RoleAssignment.target_id == domain_id, + RoleAssignment.inherited == false(), + RoleAssignment.actor_id.in_(group_ids)) + + with sql.transaction() as session: + query = session.query(RoleAssignment.role_id).filter( + sql_constraints).distinct() + return [role.role_id for role in query.all()] + + def list_role_ids_for_groups_on_project( + self, group_ids, project_id, project_domain_id, project_parents): + + if not group_ids: + # If there's no groups then there will be no project roles. + return [] + + # NOTE(rodrigods): First, we always include projects with + # non-inherited assignments + sql_constraints = sqlalchemy.and_( + RoleAssignment.type == AssignmentType.GROUP_PROJECT, + RoleAssignment.inherited == false(), + RoleAssignment.target_id == project_id) + + if CONF.os_inherit.enabled: + # Inherited roles from domains + sql_constraints = sqlalchemy.or_( + sql_constraints, + sqlalchemy.and_( + RoleAssignment.type == AssignmentType.GROUP_DOMAIN, + RoleAssignment.inherited, + RoleAssignment.target_id == project_domain_id)) + + # Inherited roles from projects + if project_parents: + sql_constraints = sqlalchemy.or_( + sql_constraints, + sqlalchemy.and_( + RoleAssignment.type == AssignmentType.GROUP_PROJECT, + RoleAssignment.inherited, + RoleAssignment.target_id.in_(project_parents))) + + sql_constraints = sqlalchemy.and_( + sql_constraints, RoleAssignment.actor_id.in_(group_ids)) + + with sql.transaction() as session: + # NOTE(morganfainberg): Only select the columns we actually care + # about here, in this case role_id. + query = session.query(RoleAssignment.role_id).filter( + sql_constraints).distinct() + + return [result.role_id for result in query.all()] + + def list_project_ids_for_groups(self, group_ids, hints, + inherited=False): + return self._list_project_ids_for_actor( + group_ids, hints, inherited, group_only=True) + + def list_domain_ids_for_groups(self, group_ids, inherited=False): + if not group_ids: + # If there's no groups then there will be no domains. + return [] + + group_sql_conditions = sqlalchemy.and_( + RoleAssignment.type == AssignmentType.GROUP_DOMAIN, + RoleAssignment.inherited == inherited, + RoleAssignment.actor_id.in_(group_ids)) + + with sql.transaction() as session: + query = session.query(RoleAssignment.target_id).filter( + group_sql_conditions).distinct() + return [x.target_id for x in query.all()] + + def add_role_to_user_and_project(self, user_id, tenant_id, role_id): + try: + with sql.transaction() as session: + session.add(RoleAssignment( + type=AssignmentType.USER_PROJECT, + actor_id=user_id, target_id=tenant_id, + role_id=role_id, inherited=False)) + except sql.DBDuplicateEntry: + msg = ('User %s already has role %s in tenant %s' + % (user_id, role_id, tenant_id)) + raise exception.Conflict(type='role grant', details=msg) + + def remove_role_from_user_and_project(self, user_id, tenant_id, role_id): + with sql.transaction() as session: + q = session.query(RoleAssignment) + q = q.filter_by(actor_id=user_id) + q = q.filter_by(target_id=tenant_id) + q = q.filter_by(role_id=role_id) + if q.delete() == 0: + raise exception.RoleNotFound(message=_( + 'Cannot remove role that has not been granted, %s') % + role_id) + + def list_role_assignments(self): + + def denormalize_role(ref): + assignment = {} + if ref.type == AssignmentType.USER_PROJECT: + assignment['user_id'] = ref.actor_id + assignment['project_id'] = ref.target_id + elif ref.type == AssignmentType.USER_DOMAIN: + assignment['user_id'] = ref.actor_id + assignment['domain_id'] = ref.target_id + elif ref.type == AssignmentType.GROUP_PROJECT: + assignment['group_id'] = ref.actor_id + assignment['project_id'] = ref.target_id + elif ref.type == AssignmentType.GROUP_DOMAIN: + assignment['group_id'] = ref.actor_id + assignment['domain_id'] = ref.target_id + else: + raise exception.Error(message=_( + 'Unexpected assignment type encountered, %s') % + ref.type) + assignment['role_id'] = ref.role_id + if ref.inherited: + assignment['inherited_to_projects'] = 'projects' + return assignment + + with sql.transaction() as session: + refs = session.query(RoleAssignment).all() + return [denormalize_role(ref) for ref in refs] + + def delete_project_assignments(self, project_id): + with sql.transaction() as session: + q = session.query(RoleAssignment) + q = q.filter_by(target_id=project_id) + q.delete(False) + + def delete_role_assignments(self, role_id): + with sql.transaction() as session: + q = session.query(RoleAssignment) + q = q.filter_by(role_id=role_id) + q.delete(False) + + def delete_user(self, user_id): + with sql.transaction() as session: + q = session.query(RoleAssignment) + q = q.filter_by(actor_id=user_id) + q.delete(False) + + def delete_group(self, group_id): + with sql.transaction() as session: + q = session.query(RoleAssignment) + q = q.filter_by(actor_id=group_id) + q.delete(False) + + +class RoleAssignment(sql.ModelBase, sql.DictBase): + __tablename__ = 'assignment' + attributes = ['type', 'actor_id', 'target_id', 'role_id', 'inherited'] + # NOTE(henry-nash); Postgres requires a name to be defined for an Enum + type = sql.Column( + sql.Enum(AssignmentType.USER_PROJECT, AssignmentType.GROUP_PROJECT, + AssignmentType.USER_DOMAIN, AssignmentType.GROUP_DOMAIN, + name='type'), + nullable=False) + actor_id = sql.Column(sql.String(64), nullable=False, index=True) + target_id = sql.Column(sql.String(64), nullable=False) + role_id = sql.Column(sql.String(64), nullable=False) + inherited = sql.Column(sql.Boolean, default=False, nullable=False) + __table_args__ = (sql.PrimaryKeyConstraint('type', 'actor_id', 'target_id', + 'role_id'), {}) + + def to_dict(self): + """Override parent to_dict() method with a simpler implementation. + + RoleAssignment doesn't have non-indexed 'extra' attributes, so the + parent implementation is not applicable. + """ + return dict(six.iteritems(self)) diff --git a/keystone-moon/keystone/assignment/controllers.py b/keystone-moon/keystone/assignment/controllers.py new file mode 100644 index 00000000..ff27fd36 --- /dev/null +++ b/keystone-moon/keystone/assignment/controllers.py @@ -0,0 +1,816 @@ +# Copyright 2013 Metacloud, Inc. +# Copyright 2012 OpenStack Foundation +# +# 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. + +"""Workflow Logic the Assignment service.""" + +import copy +import functools +import uuid + +from oslo_config import cfg +from oslo_log import log +from six.moves import urllib + +from keystone.assignment import schema +from keystone.common import controller +from keystone.common import dependency +from keystone.common import validation +from keystone import exception +from keystone.i18n import _, _LW +from keystone.models import token_model +from keystone import notifications + + +CONF = cfg.CONF +LOG = log.getLogger(__name__) + + +@dependency.requires('assignment_api', 'identity_api', 'token_provider_api') +class TenantAssignment(controller.V2Controller): + """The V2 Project APIs that are processing assignments.""" + + @controller.v2_deprecated + def get_projects_for_token(self, context, **kw): + """Get valid tenants for token based on token used to authenticate. + + Pulls the token from the context, validates it and gets the valid + tenants for the user in the token. + + Doesn't care about token scopedness. + + """ + try: + token_data = self.token_provider_api.validate_token( + context['token_id']) + token_ref = token_model.KeystoneToken(token_id=context['token_id'], + token_data=token_data) + except exception.NotFound as e: + LOG.warning(_LW('Authentication failed: %s'), e) + raise exception.Unauthorized(e) + + tenant_refs = ( + self.assignment_api.list_projects_for_user(token_ref.user_id)) + tenant_refs = [self.filter_domain_id(ref) for ref in tenant_refs + if ref['domain_id'] == CONF.identity.default_domain_id] + params = { + 'limit': context['query_string'].get('limit'), + 'marker': context['query_string'].get('marker'), + } + return self.format_project_list(tenant_refs, **params) + + @controller.v2_deprecated + def get_project_users(self, context, tenant_id, **kw): + self.assert_admin(context) + user_refs = [] + user_ids = self.assignment_api.list_user_ids_for_project(tenant_id) + for user_id in user_ids: + try: + user_ref = self.identity_api.get_user(user_id) + except exception.UserNotFound: + # Log that user is missing and continue on. + message = ("User %(user_id)s in project %(project_id)s " + "doesn't exist.") + LOG.debug(message, + {'user_id': user_id, 'project_id': tenant_id}) + else: + user_refs.append(self.v3_to_v2_user(user_ref)) + return {'users': user_refs} + + +@dependency.requires('assignment_api', 'role_api') +class Role(controller.V2Controller): + """The Role management APIs.""" + + @controller.v2_deprecated + def get_role(self, context, role_id): + self.assert_admin(context) + return {'role': self.role_api.get_role(role_id)} + + @controller.v2_deprecated + def create_role(self, context, role): + role = self._normalize_dict(role) + self.assert_admin(context) + + if 'name' not in role or not role['name']: + msg = _('Name field is required and cannot be empty') + raise exception.ValidationError(message=msg) + + role_id = uuid.uuid4().hex + role['id'] = role_id + role_ref = self.role_api.create_role(role_id, role) + return {'role': role_ref} + + @controller.v2_deprecated + def delete_role(self, context, role_id): + self.assert_admin(context) + self.role_api.delete_role(role_id) + + @controller.v2_deprecated + def get_roles(self, context): + self.assert_admin(context) + return {'roles': self.role_api.list_roles()} + + +@dependency.requires('assignment_api', 'resource_api', 'role_api') +class RoleAssignmentV2(controller.V2Controller): + """The V2 Role APIs that are processing assignments.""" + + # COMPAT(essex-3) + @controller.v2_deprecated + def get_user_roles(self, context, user_id, tenant_id=None): + """Get the roles for a user and tenant pair. + + Since we're trying to ignore the idea of user-only roles we're + not implementing them in hopes that the idea will die off. + + """ + self.assert_admin(context) + roles = self.assignment_api.get_roles_for_user_and_project( + user_id, tenant_id) + return {'roles': [self.role_api.get_role(x) + for x in roles]} + + @controller.v2_deprecated + def add_role_to_user(self, context, user_id, role_id, tenant_id=None): + """Add a role to a user and tenant pair. + + Since we're trying to ignore the idea of user-only roles we're + not implementing them in hopes that the idea will die off. + + """ + self.assert_admin(context) + if tenant_id is None: + raise exception.NotImplemented(message='User roles not supported: ' + 'tenant_id required') + + self.assignment_api.add_role_to_user_and_project( + user_id, tenant_id, role_id) + + role_ref = self.role_api.get_role(role_id) + return {'role': role_ref} + + @controller.v2_deprecated + def remove_role_from_user(self, context, user_id, role_id, tenant_id=None): + """Remove a role from a user and tenant pair. + + Since we're trying to ignore the idea of user-only roles we're + not implementing them in hopes that the idea will die off. + + """ + self.assert_admin(context) + if tenant_id is None: + raise exception.NotImplemented(message='User roles not supported: ' + 'tenant_id required') + + # This still has the weird legacy semantics that adding a role to + # a user also adds them to a tenant, so we must follow up on that + self.assignment_api.remove_role_from_user_and_project( + user_id, tenant_id, role_id) + + # COMPAT(diablo): CRUD extension + @controller.v2_deprecated + def get_role_refs(self, context, user_id): + """Ultimate hack to get around having to make role_refs first-class. + + This will basically iterate over the various roles the user has in + all tenants the user is a member of and create fake role_refs where + the id encodes the user-tenant-role information so we can look + up the appropriate data when we need to delete them. + + """ + self.assert_admin(context) + tenants = self.assignment_api.list_projects_for_user(user_id) + o = [] + for tenant in tenants: + # As a v2 call, we should limit the response to those projects in + # the default domain. + if tenant['domain_id'] != CONF.identity.default_domain_id: + continue + role_ids = self.assignment_api.get_roles_for_user_and_project( + user_id, tenant['id']) + for role_id in role_ids: + ref = {'roleId': role_id, + 'tenantId': tenant['id'], + 'userId': user_id} + ref['id'] = urllib.parse.urlencode(ref) + o.append(ref) + return {'roles': o} + + # COMPAT(diablo): CRUD extension + @controller.v2_deprecated + def create_role_ref(self, context, user_id, role): + """This is actually used for adding a user to a tenant. + + In the legacy data model adding a user to a tenant required setting + a role. + + """ + self.assert_admin(context) + # TODO(termie): for now we're ignoring the actual role + tenant_id = role.get('tenantId') + role_id = role.get('roleId') + self.assignment_api.add_role_to_user_and_project( + user_id, tenant_id, role_id) + + role_ref = self.role_api.get_role(role_id) + return {'role': role_ref} + + # COMPAT(diablo): CRUD extension + @controller.v2_deprecated + def delete_role_ref(self, context, user_id, role_ref_id): + """This is actually used for deleting a user from a tenant. + + In the legacy data model removing a user from a tenant required + deleting a role. + + To emulate this, we encode the tenant and role in the role_ref_id, + and if this happens to be the last role for the user-tenant pair, + we remove the user from the tenant. + + """ + self.assert_admin(context) + # TODO(termie): for now we're ignoring the actual role + role_ref_ref = urllib.parse.parse_qs(role_ref_id) + tenant_id = role_ref_ref.get('tenantId')[0] + role_id = role_ref_ref.get('roleId')[0] + self.assignment_api.remove_role_from_user_and_project( + user_id, tenant_id, role_id) + + +@dependency.requires('assignment_api', 'resource_api') +class ProjectAssignmentV3(controller.V3Controller): + """The V3 Project APIs that are processing assignments.""" + + collection_name = 'projects' + member_name = 'project' + + def __init__(self): + super(ProjectAssignmentV3, self).__init__() + self.get_member_from_driver = self.resource_api.get_project + + @controller.filterprotected('enabled', 'name') + def list_user_projects(self, context, filters, user_id): + hints = ProjectAssignmentV3.build_driver_hints(context, filters) + refs = self.assignment_api.list_projects_for_user(user_id, + hints=hints) + return ProjectAssignmentV3.wrap_collection(context, refs, hints=hints) + + +@dependency.requires('role_api') +class RoleV3(controller.V3Controller): + """The V3 Role CRUD APIs.""" + + collection_name = 'roles' + member_name = 'role' + + def __init__(self): + super(RoleV3, self).__init__() + self.get_member_from_driver = self.role_api.get_role + + @controller.protected() + @validation.validated(schema.role_create, 'role') + def create_role(self, context, role): + ref = self._assign_unique_id(self._normalize_dict(role)) + initiator = notifications._get_request_audit_info(context) + ref = self.role_api.create_role(ref['id'], ref, initiator) + return RoleV3.wrap_member(context, ref) + + @controller.filterprotected('name') + def list_roles(self, context, filters): + hints = RoleV3.build_driver_hints(context, filters) + refs = self.role_api.list_roles( + hints=hints) + return RoleV3.wrap_collection(context, refs, hints=hints) + + @controller.protected() + def get_role(self, context, role_id): + ref = self.role_api.get_role(role_id) + return RoleV3.wrap_member(context, ref) + + @controller.protected() + @validation.validated(schema.role_update, 'role') + def update_role(self, context, role_id, role): + self._require_matching_id(role_id, role) + initiator = notifications._get_request_audit_info(context) + ref = self.role_api.update_role(role_id, role, initiator) + return RoleV3.wrap_member(context, ref) + + @controller.protected() + def delete_role(self, context, role_id): + initiator = notifications._get_request_audit_info(context) + self.role_api.delete_role(role_id, initiator) + + +@dependency.requires('assignment_api', 'identity_api', 'resource_api', + 'role_api') +class GrantAssignmentV3(controller.V3Controller): + """The V3 Grant Assignment APIs.""" + + collection_name = 'roles' + member_name = 'role' + + def __init__(self): + super(GrantAssignmentV3, self).__init__() + self.get_member_from_driver = self.role_api.get_role + + def _require_domain_xor_project(self, domain_id, project_id): + if domain_id and project_id: + msg = _('Specify a domain or project, not both') + raise exception.ValidationError(msg) + if not domain_id and not project_id: + msg = _('Specify one of domain or project') + raise exception.ValidationError(msg) + + def _require_user_xor_group(self, user_id, group_id): + if user_id and group_id: + msg = _('Specify a user or group, not both') + raise exception.ValidationError(msg) + if not user_id and not group_id: + msg = _('Specify one of user or group') + raise exception.ValidationError(msg) + + def _check_if_inherited(self, context): + return (CONF.os_inherit.enabled and + context['path'].startswith('/OS-INHERIT') and + context['path'].endswith('/inherited_to_projects')) + + def _check_grant_protection(self, context, protection, role_id=None, + user_id=None, group_id=None, + domain_id=None, project_id=None, + allow_no_user=False): + """Check protection for role grant APIs. + + The policy rule might want to inspect attributes of any of the entities + involved in the grant. So we get these and pass them to the + check_protection() handler in the controller. + + """ + ref = {} + if role_id: + ref['role'] = self.role_api.get_role(role_id) + if user_id: + try: + ref['user'] = self.identity_api.get_user(user_id) + except exception.UserNotFound: + if not allow_no_user: + raise + else: + ref['group'] = self.identity_api.get_group(group_id) + + if domain_id: + ref['domain'] = self.resource_api.get_domain(domain_id) + else: + ref['project'] = self.resource_api.get_project(project_id) + + self.check_protection(context, protection, ref) + + @controller.protected(callback=_check_grant_protection) + def create_grant(self, context, role_id, user_id=None, + group_id=None, domain_id=None, project_id=None): + """Grants a role to a user or group on either a domain or project.""" + self._require_domain_xor_project(domain_id, project_id) + self._require_user_xor_group(user_id, group_id) + + self.assignment_api.create_grant( + role_id, user_id, group_id, domain_id, project_id, + self._check_if_inherited(context), context) + + @controller.protected(callback=_check_grant_protection) + def list_grants(self, context, user_id=None, + group_id=None, domain_id=None, project_id=None): + """Lists roles granted to user/group on either a domain or project.""" + self._require_domain_xor_project(domain_id, project_id) + self._require_user_xor_group(user_id, group_id) + + refs = self.assignment_api.list_grants( + user_id, group_id, domain_id, project_id, + self._check_if_inherited(context)) + return GrantAssignmentV3.wrap_collection(context, refs) + + @controller.protected(callback=_check_grant_protection) + def check_grant(self, context, role_id, user_id=None, + group_id=None, domain_id=None, project_id=None): + """Checks if a role has been granted on either a domain or project.""" + self._require_domain_xor_project(domain_id, project_id) + self._require_user_xor_group(user_id, group_id) + + self.assignment_api.get_grant( + role_id, user_id, group_id, domain_id, project_id, + self._check_if_inherited(context)) + + # NOTE(lbragstad): This will allow users to clean up role assignments + # from the backend in the event the user was removed prior to the role + # assignment being removed. + @controller.protected(callback=functools.partial( + _check_grant_protection, allow_no_user=True)) + def revoke_grant(self, context, role_id, user_id=None, + group_id=None, domain_id=None, project_id=None): + """Revokes a role from user/group on either a domain or project.""" + self._require_domain_xor_project(domain_id, project_id) + self._require_user_xor_group(user_id, group_id) + + self.assignment_api.delete_grant( + role_id, user_id, group_id, domain_id, project_id, + self._check_if_inherited(context), context) + + +@dependency.requires('assignment_api', 'identity_api', 'resource_api') +class RoleAssignmentV3(controller.V3Controller): + """The V3 Role Assignment APIs, really just list_role_assignment().""" + + # TODO(henry-nash): The current implementation does not provide a full + # first class entity for role-assignment. There is no role_assignment_id + # and only the list_role_assignment call is supported. Further, since it + # is not a first class entity, the links for the individual entities + # reference the individual role grant APIs. + + collection_name = 'role_assignments' + member_name = 'role_assignment' + + @classmethod + def wrap_member(cls, context, ref): + # NOTE(henry-nash): Since we are not yet a true collection, we override + # the wrapper as have already included the links in the entities + pass + + def _format_entity(self, context, entity): + """Format an assignment entity for API response. + + The driver layer returns entities as dicts containing the ids of the + actor (e.g. user or group), target (e.g. domain or project) and role. + If it is an inherited role, then this is also indicated. Examples: + + {'user_id': user_id, + 'project_id': domain_id, + 'role_id': role_id} + + or, for an inherited role: + + {'user_id': user_id, + 'domain_id': domain_id, + 'role_id': role_id, + 'inherited_to_projects': true} + + This function maps this into the format to be returned via the API, + e.g. for the second example above: + + { + 'user': { + {'id': user_id} + }, + 'scope': { + 'domain': { + {'id': domain_id} + }, + 'OS-INHERIT:inherited_to': 'projects + }, + 'role': { + {'id': role_id} + }, + 'links': { + 'assignment': '/domains/domain_id/users/user_id/roles/' + 'role_id/inherited_to_projects' + } + } + + """ + + formatted_entity = {} + suffix = "" + if 'user_id' in entity: + formatted_entity['user'] = {'id': entity['user_id']} + actor_link = 'users/%s' % entity['user_id'] + if 'group_id' in entity: + formatted_entity['group'] = {'id': entity['group_id']} + actor_link = 'groups/%s' % entity['group_id'] + if 'role_id' in entity: + formatted_entity['role'] = {'id': entity['role_id']} + if 'project_id' in entity: + formatted_entity['scope'] = ( + {'project': {'id': entity['project_id']}}) + if 'inherited_to_projects' in entity: + formatted_entity['scope']['OS-INHERIT:inherited_to'] = ( + 'projects') + target_link = '/OS-INHERIT/projects/%s' % entity['project_id'] + suffix = '/inherited_to_projects' + else: + target_link = '/projects/%s' % entity['project_id'] + if 'domain_id' in entity: + formatted_entity['scope'] = ( + {'domain': {'id': entity['domain_id']}}) + if 'inherited_to_projects' in entity: + formatted_entity['scope']['OS-INHERIT:inherited_to'] = ( + 'projects') + target_link = '/OS-INHERIT/domains/%s' % entity['domain_id'] + suffix = '/inherited_to_projects' + else: + target_link = '/domains/%s' % entity['domain_id'] + formatted_entity.setdefault('links', {}) + + path = '%(target)s/%(actor)s/roles/%(role)s%(suffix)s' % { + 'target': target_link, + 'actor': actor_link, + 'role': entity['role_id'], + 'suffix': suffix} + formatted_entity['links']['assignment'] = self.base_url(context, path) + + return formatted_entity + + def _expand_indirect_assignments(self, context, refs): + """Processes entity list into all-direct assignments. + + For any group role assignments in the list, create a role assignment + entity for each member of that group, and then remove the group + assignment entity itself from the list. + + If the OS-INHERIT extension is enabled, then honor any inherited + roles on the domain by creating the equivalent on all projects + owned by the domain. + + For any new entity created by virtue of group membership, add in an + additional link to that membership. + + """ + def _get_group_members(ref): + """Get a list of group members. + + Get the list of group members. If this fails with + GroupNotFound, then log this as a warning, but allow + overall processing to continue. + + """ + try: + members = self.identity_api.list_users_in_group( + ref['group']['id']) + except exception.GroupNotFound: + members = [] + # The group is missing, which should not happen since + # group deletion should remove any related assignments, so + # log a warning + target = 'Unknown' + # Should always be a domain or project, but since to get + # here things have gone astray, let's be cautious. + if 'scope' in ref: + if 'domain' in ref['scope']: + dom_id = ref['scope']['domain'].get('id', 'Unknown') + target = 'Domain: %s' % dom_id + elif 'project' in ref['scope']: + proj_id = ref['scope']['project'].get('id', 'Unknown') + target = 'Project: %s' % proj_id + role_id = 'Unknown' + if 'role' in ref and 'id' in ref['role']: + role_id = ref['role']['id'] + LOG.warning( + _LW('Group %(group)s not found for role-assignment - ' + '%(target)s with Role: %(role)s'), { + 'group': ref['group']['id'], 'target': target, + 'role': role_id}) + return members + + def _build_user_assignment_equivalent_of_group( + user, group_id, template): + """Create a user assignment equivalent to the group one. + + The template has had the 'group' entity removed, so + substitute a 'user' one. The 'assignment' link stays as it is, + referring to the group assignment that led to this role. + A 'membership' link is added that refers to this particular + user's membership of this group. + + """ + user_entry = copy.deepcopy(template) + user_entry['user'] = {'id': user['id']} + user_entry['links']['membership'] = ( + self.base_url(context, '/groups/%s/users/%s' % + (group_id, user['id']))) + return user_entry + + def _build_project_equivalent_of_user_target_role( + project_id, target_id, target_type, template): + """Create a user project assignment equivalent to the domain one. + + The template has had the 'domain' entity removed, so + substitute a 'project' one, modifying the 'assignment' link + to match. + + """ + project_entry = copy.deepcopy(template) + project_entry['scope']['project'] = {'id': project_id} + project_entry['links']['assignment'] = ( + self.base_url( + context, + '/OS-INHERIT/%s/%s/users/%s/roles/%s' + '/inherited_to_projects' % ( + target_type, target_id, project_entry['user']['id'], + project_entry['role']['id']))) + return project_entry + + def _build_project_equivalent_of_group_target_role( + user_id, group_id, project_id, + target_id, target_type, template): + """Create a user project equivalent to the domain group one. + + The template has had the 'domain' and 'group' entities removed, so + substitute a 'user-project' one, modifying the 'assignment' link + to match. + + """ + project_entry = copy.deepcopy(template) + project_entry['user'] = {'id': user_id} + project_entry['scope']['project'] = {'id': project_id} + project_entry['links']['assignment'] = ( + self.base_url(context, + '/OS-INHERIT/%s/%s/groups/%s/roles/%s' + '/inherited_to_projects' % ( + target_type, target_id, group_id, + project_entry['role']['id']))) + project_entry['links']['membership'] = ( + self.base_url(context, '/groups/%s/users/%s' % + (group_id, user_id))) + return project_entry + + # Scan the list of entities for any assignments that need to be + # expanded. + # + # If the OS-INERIT extension is enabled, the refs lists may + # contain roles to be inherited from domain to project, so expand + # these as well into project equivalents + # + # For any regular group entries, expand these into user entries based + # on membership of that group. + # + # Due to the potentially large expansions, rather than modify the + # list we are enumerating, we build a new one as we go. + # + + new_refs = [] + for r in refs: + if 'OS-INHERIT:inherited_to' in r['scope']: + if 'domain' in r['scope']: + # It's an inherited domain role - so get the list of + # projects owned by this domain. + project_ids = ( + [x['id'] for x in + self.resource_api.list_projects_in_domain( + r['scope']['domain']['id'])]) + base_entry = copy.deepcopy(r) + target_type = 'domains' + target_id = base_entry['scope']['domain']['id'] + base_entry['scope'].pop('domain') + else: + # It's an inherited project role - so get the list of + # projects in this project subtree. + project_id = r['scope']['project']['id'] + project_ids = ( + [x['id'] for x in + self.resource_api.list_projects_in_subtree( + project_id)]) + base_entry = copy.deepcopy(r) + target_type = 'projects' + target_id = base_entry['scope']['project']['id'] + base_entry['scope'].pop('project') + + # For each project, create an equivalent role assignment + for p in project_ids: + # If it's a group assignment, then create equivalent user + # roles based on membership of the group + if 'group' in base_entry: + members = _get_group_members(base_entry) + sub_entry = copy.deepcopy(base_entry) + group_id = sub_entry['group']['id'] + sub_entry.pop('group') + for m in members: + new_entry = ( + _build_project_equivalent_of_group_target_role( + m['id'], group_id, p, + target_id, target_type, sub_entry)) + new_refs.append(new_entry) + else: + new_entry = ( + _build_project_equivalent_of_user_target_role( + p, target_id, target_type, base_entry)) + new_refs.append(new_entry) + elif 'group' in r: + # It's a non-inherited group role assignment, so get the list + # of members. + members = _get_group_members(r) + + # Now replace that group role assignment entry with an + # equivalent user role assignment for each of the group members + base_entry = copy.deepcopy(r) + group_id = base_entry['group']['id'] + base_entry.pop('group') + for m in members: + user_entry = _build_user_assignment_equivalent_of_group( + m, group_id, base_entry) + new_refs.append(user_entry) + else: + new_refs.append(r) + + return new_refs + + def _filter_inherited(self, entry): + if ('inherited_to_projects' in entry and + not CONF.os_inherit.enabled): + return False + else: + return True + + def _assert_effective_filters(self, inherited, group, domain): + """Assert that useless filter combinations are avoided. + + In effective mode, the following filter combinations are useless, since + they would always return an empty list of role assignments: + - group id, since no group assignment is returned in effective mode; + - domain id and inherited, since no domain inherited assignment is + returned in effective mode. + + """ + if group: + msg = _('Combining effective and group filter will always ' + 'result in an empty list.') + raise exception.ValidationError(msg) + + if inherited and domain: + msg = _('Combining effective, domain and inherited filters will ' + 'always result in an empty list.') + raise exception.ValidationError(msg) + + def _assert_domain_nand_project(self, domain_id, project_id): + if domain_id and project_id: + msg = _('Specify a domain or project, not both') + raise exception.ValidationError(msg) + + def _assert_user_nand_group(self, user_id, group_id): + if user_id and group_id: + msg = _('Specify a user or group, not both') + raise exception.ValidationError(msg) + + @controller.filterprotected('group.id', 'role.id', + 'scope.domain.id', 'scope.project.id', + 'scope.OS-INHERIT:inherited_to', 'user.id') + def list_role_assignments(self, context, filters): + + # TODO(henry-nash): This implementation uses the standard filtering + # in the V3.wrap_collection. Given the large number of individual + # assignments, this is pretty inefficient. An alternative would be + # to pass the filters into the driver call, so that the list size is + # kept a minimum. + + params = context['query_string'] + effective = 'effective' in params and ( + self.query_filter_is_true(params['effective'])) + + if 'scope.OS-INHERIT:inherited_to' in params: + inherited = ( + params['scope.OS-INHERIT:inherited_to'] == 'projects') + else: + # None means querying both inherited and direct assignments + inherited = None + + self._assert_domain_nand_project(params.get('scope.domain.id'), + params.get('scope.project.id')) + self._assert_user_nand_group(params.get('user.id'), + params.get('group.id')) + + if effective: + self._assert_effective_filters(inherited=inherited, + group=params.get('group.id'), + domain=params.get( + 'scope.domain.id')) + + hints = self.build_driver_hints(context, filters) + refs = self.assignment_api.list_role_assignments() + formatted_refs = ( + [self._format_entity(context, x) for x in refs + if self._filter_inherited(x)]) + + if effective: + formatted_refs = self._expand_indirect_assignments(context, + formatted_refs) + + return self.wrap_collection(context, formatted_refs, hints=hints) + + @controller.protected() + def get_role_assignment(self, context): + raise exception.NotImplemented() + + @controller.protected() + def update_role_assignment(self, context): + raise exception.NotImplemented() + + @controller.protected() + def delete_role_assignment(self, context): + raise exception.NotImplemented() diff --git a/keystone-moon/keystone/assignment/core.py b/keystone-moon/keystone/assignment/core.py new file mode 100644 index 00000000..0f9c03e9 --- /dev/null +++ b/keystone-moon/keystone/assignment/core.py @@ -0,0 +1,1019 @@ +# Copyright 2012 OpenStack Foundation +# +# 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. + +"""Main entry point into the assignment service.""" + +import abc + +from oslo_config import cfg +from oslo_log import log +import six + +from keystone.common import cache +from keystone.common import dependency +from keystone.common import driver_hints +from keystone.common import manager +from keystone import exception +from keystone.i18n import _ +from keystone.i18n import _LI +from keystone import notifications +from keystone.openstack.common import versionutils + + +CONF = cfg.CONF +LOG = log.getLogger(__name__) +MEMOIZE = cache.get_memoization_decorator(section='role') + + +def deprecated_to_role_api(f): + """Specialized deprecation wrapper for assignment to role api. + + This wraps the standard deprecation wrapper and fills in the method + names automatically. + + """ + @six.wraps(f) + def wrapper(*args, **kwargs): + x = versionutils.deprecated( + what='assignment.' + f.__name__ + '()', + as_of=versionutils.deprecated.KILO, + in_favor_of='role.' + f.__name__ + '()') + return x(f) + return wrapper() + + +def deprecated_to_resource_api(f): + """Specialized deprecation wrapper for assignment to resource api. + + This wraps the standard deprecation wrapper and fills in the method + names automatically. + + """ + @six.wraps(f) + def wrapper(*args, **kwargs): + x = versionutils.deprecated( + what='assignment.' + f.__name__ + '()', + as_of=versionutils.deprecated.KILO, + in_favor_of='resource.' + f.__name__ + '()') + return x(f) + return wrapper() + + +@dependency.provider('assignment_api') +@dependency.requires('credential_api', 'identity_api', 'resource_api', + 'revoke_api', 'role_api') +class Manager(manager.Manager): + """Default pivot point for the Assignment backend. + + See :mod:`keystone.common.manager.Manager` for more details on how this + dynamically calls the backend. + + """ + _PROJECT = 'project' + _ROLE_REMOVED_FROM_USER = 'role_removed_from_user' + _INVALIDATION_USER_PROJECT_TOKENS = 'invalidate_user_project_tokens' + + def __init__(self): + assignment_driver = CONF.assignment.driver + + # If there is no explicit assignment driver specified, we let the + # identity driver tell us what to use. This is for backward + # compatibility reasons from the time when identity, resource and + # assignment were all part of identity. + if assignment_driver is None: + identity_driver = dependency.get_provider('identity_api').driver + assignment_driver = identity_driver.default_assignment_driver() + + super(Manager, self).__init__(assignment_driver) + + def _get_group_ids_for_user_id(self, user_id): + # TODO(morganfainberg): Implement a way to get only group_ids + # instead of the more expensive to_dict() call for each record. + return [x['id'] for + x in self.identity_api.list_groups_for_user(user_id)] + + def list_user_ids_for_project(self, tenant_id): + self.resource_api.get_project(tenant_id) + return self.driver.list_user_ids_for_project(tenant_id) + + def _list_parent_ids_of_project(self, project_id): + if CONF.os_inherit.enabled: + return [x['id'] for x in ( + self.resource_api.list_project_parents(project_id))] + else: + return [] + + def get_roles_for_user_and_project(self, user_id, tenant_id): + """Get the roles associated with a user within given project. + + This includes roles directly assigned to the user on the + project, as well as those by virtue of group membership. If + the OS-INHERIT extension is enabled, then this will also + include roles inherited from the domain. + + :returns: a list of role ids. + :raises: keystone.exception.UserNotFound, + keystone.exception.ProjectNotFound + + """ + def _get_group_project_roles(user_id, project_ref): + group_ids = self._get_group_ids_for_user_id(user_id) + return self.driver.list_role_ids_for_groups_on_project( + group_ids, + project_ref['id'], + project_ref['domain_id'], + self._list_parent_ids_of_project(project_ref['id'])) + + def _get_user_project_roles(user_id, project_ref): + role_list = [] + try: + metadata_ref = self._get_metadata(user_id=user_id, + tenant_id=project_ref['id']) + role_list = self._roles_from_role_dicts( + metadata_ref.get('roles', {}), False) + except exception.MetadataNotFound: + pass + + if CONF.os_inherit.enabled: + # Now get any inherited roles for the owning domain + try: + metadata_ref = self._get_metadata( + user_id=user_id, domain_id=project_ref['domain_id']) + role_list += self._roles_from_role_dicts( + metadata_ref.get('roles', {}), True) + except (exception.MetadataNotFound, exception.NotImplemented): + pass + # As well inherited roles from parent projects + for p in self.list_project_parents(project_ref['id']): + p_roles = self.list_grants( + user_id=user_id, project_id=p['id'], + inherited_to_projects=True) + role_list += [x['id'] for x in p_roles] + + return role_list + + project_ref = self.resource_api.get_project(tenant_id) + user_role_list = _get_user_project_roles(user_id, project_ref) + group_role_list = _get_group_project_roles(user_id, project_ref) + # Use set() to process the list to remove any duplicates + return list(set(user_role_list + group_role_list)) + + def get_roles_for_user_and_domain(self, user_id, domain_id): + """Get the roles associated with a user within given domain. + + :returns: a list of role ids. + :raises: keystone.exception.UserNotFound, + keystone.exception.DomainNotFound + + """ + + def _get_group_domain_roles(user_id, domain_id): + role_list = [] + group_ids = self._get_group_ids_for_user_id(user_id) + for group_id in group_ids: + try: + metadata_ref = self._get_metadata(group_id=group_id, + domain_id=domain_id) + role_list += self._roles_from_role_dicts( + metadata_ref.get('roles', {}), False) + except (exception.MetadataNotFound, exception.NotImplemented): + # MetadataNotFound implies no group grant, so skip. + # Ignore NotImplemented since not all backends support + # domains. + pass + return role_list + + def _get_user_domain_roles(user_id, domain_id): + metadata_ref = {} + try: + metadata_ref = self._get_metadata(user_id=user_id, + domain_id=domain_id) + except (exception.MetadataNotFound, exception.NotImplemented): + # MetadataNotFound implies no user grants. + # Ignore NotImplemented since not all backends support + # domains + pass + return self._roles_from_role_dicts( + metadata_ref.get('roles', {}), False) + + self.get_domain(domain_id) + user_role_list = _get_user_domain_roles(user_id, domain_id) + group_role_list = _get_group_domain_roles(user_id, domain_id) + # Use set() to process the list to remove any duplicates + return list(set(user_role_list + group_role_list)) + + def get_roles_for_groups(self, group_ids, project_id=None, domain_id=None): + """Get a list of roles for this group on domain and/or project.""" + + if project_id is not None: + project = self.resource_api.get_project(project_id) + role_ids = self.driver.list_role_ids_for_groups_on_project( + group_ids, project_id, project['domain_id'], + self._list_parent_ids_of_project(project_id)) + elif domain_id is not None: + role_ids = self.driver.list_role_ids_for_groups_on_domain( + group_ids, domain_id) + else: + raise AttributeError(_("Must specify either domain or project")) + + return self.role_api.list_roles_from_ids(role_ids) + + def add_user_to_project(self, tenant_id, user_id): + """Add user to a tenant by creating a default role relationship. + + :raises: keystone.exception.ProjectNotFound, + keystone.exception.UserNotFound + + """ + self.resource_api.get_project(tenant_id) + try: + self.role_api.get_role(CONF.member_role_id) + self.driver.add_role_to_user_and_project( + user_id, + tenant_id, + CONF.member_role_id) + except exception.RoleNotFound: + LOG.info(_LI("Creating the default role %s " + "because it does not exist."), + CONF.member_role_id) + role = {'id': CONF.member_role_id, + 'name': CONF.member_role_name} + try: + self.role_api.create_role(CONF.member_role_id, role) + except exception.Conflict: + LOG.info(_LI("Creating the default role %s failed because it " + "was already created"), + CONF.member_role_id) + # now that default role exists, the add should succeed + self.driver.add_role_to_user_and_project( + user_id, + tenant_id, + CONF.member_role_id) + + def add_role_to_user_and_project(self, user_id, tenant_id, role_id): + self.resource_api.get_project(tenant_id) + self.role_api.get_role(role_id) + self.driver.add_role_to_user_and_project(user_id, tenant_id, role_id) + + def remove_user_from_project(self, tenant_id, user_id): + """Remove user from a tenant + + :raises: keystone.exception.ProjectNotFound, + keystone.exception.UserNotFound + + """ + roles = self.get_roles_for_user_and_project(user_id, tenant_id) + if not roles: + raise exception.NotFound(tenant_id) + for role_id in roles: + try: + self.driver.remove_role_from_user_and_project(user_id, + tenant_id, + role_id) + self.revoke_api.revoke_by_grant(role_id, user_id=user_id, + project_id=tenant_id) + + except exception.RoleNotFound: + LOG.debug("Removing role %s failed because it does not exist.", + role_id) + + # TODO(henry-nash): We might want to consider list limiting this at some + # point in the future. + def list_projects_for_user(self, user_id, hints=None): + # NOTE(henry-nash): In order to get a complete list of user projects, + # the driver will need to look at group assignments. To avoid cross + # calling between the assignment and identity driver we get the group + # list here and pass it in. The rest of the detailed logic of listing + # projects for a user is pushed down into the driver to enable + # optimization with the various backend technologies (SQL, LDAP etc.). + + group_ids = self._get_group_ids_for_user_id(user_id) + project_ids = self.driver.list_project_ids_for_user( + user_id, group_ids, hints or driver_hints.Hints()) + + if not CONF.os_inherit.enabled: + return self.resource_api.list_projects_from_ids(project_ids) + + # Inherited roles are enabled, so check to see if this user has any + # inherited role (direct or group) on any parent project, in which + # case we must add in all the projects in that parent's subtree. + project_ids = set(project_ids) + project_ids_inherited = self.driver.list_project_ids_for_user( + user_id, group_ids, hints or driver_hints.Hints(), inherited=True) + for proj_id in project_ids_inherited: + project_ids.update( + (x['id'] for x in + self.resource_api.list_projects_in_subtree(proj_id))) + + # Now do the same for any domain inherited roles + domain_ids = self.driver.list_domain_ids_for_user( + user_id, group_ids, hints or driver_hints.Hints(), + inherited=True) + project_ids.update( + self.resource_api.list_project_ids_from_domain_ids(domain_ids)) + + return self.resource_api.list_projects_from_ids(list(project_ids)) + + # TODO(henry-nash): We might want to consider list limiting this at some + # point in the future. + def list_domains_for_user(self, user_id, hints=None): + # NOTE(henry-nash): In order to get a complete list of user domains, + # the driver will need to look at group assignments. To avoid cross + # calling between the assignment and identity driver we get the group + # list here and pass it in. The rest of the detailed logic of listing + # projects for a user is pushed down into the driver to enable + # optimization with the various backend technologies (SQL, LDAP etc.). + group_ids = self._get_group_ids_for_user_id(user_id) + domain_ids = self.driver.list_domain_ids_for_user( + user_id, group_ids, hints or driver_hints.Hints()) + return self.resource_api.list_domains_from_ids(domain_ids) + + def list_domains_for_groups(self, group_ids): + domain_ids = self.driver.list_domain_ids_for_groups(group_ids) + return self.resource_api.list_domains_from_ids(domain_ids) + + def list_projects_for_groups(self, group_ids): + project_ids = ( + self.driver.list_project_ids_for_groups(group_ids, + driver_hints.Hints())) + if not CONF.os_inherit.enabled: + return self.resource_api.list_projects_from_ids(project_ids) + + # Inherited roles are enabled, so check to see if these groups have any + # roles on any domain, in which case we must add in all the projects + # in that domain. + + domain_ids = self.driver.list_domain_ids_for_groups( + group_ids, inherited=True) + + project_ids_from_domains = ( + self.resource_api.list_project_ids_from_domain_ids(domain_ids)) + + return self.resource_api.list_projects_from_ids( + list(set(project_ids + project_ids_from_domains))) + + def list_role_assignments_for_role(self, role_id=None): + # NOTE(henry-nash): Currently the efficiency of the key driver + # implementation (SQL) of list_role_assignments is severely hampered by + # the existence of the multiple grant tables - hence there is little + # advantage in pushing the logic of this method down into the driver. + # Once the single assignment table is implemented, then this situation + # will be different, and this method should have its own driver + # implementation. + return [r for r in self.driver.list_role_assignments() + if r['role_id'] == role_id] + + def remove_role_from_user_and_project(self, user_id, tenant_id, role_id): + self.driver.remove_role_from_user_and_project(user_id, tenant_id, + role_id) + self.identity_api.emit_invalidate_user_token_persistence(user_id) + self.revoke_api.revoke_by_grant(role_id, user_id=user_id, + project_id=tenant_id) + + @notifications.internal(notifications.INVALIDATE_USER_TOKEN_PERSISTENCE) + def _emit_invalidate_user_token_persistence(self, user_id): + self.identity_api.emit_invalidate_user_token_persistence(user_id) + + @notifications.role_assignment('created') + def create_grant(self, role_id, user_id=None, group_id=None, + domain_id=None, project_id=None, + inherited_to_projects=False, context=None): + self.role_api.get_role(role_id) + if domain_id: + self.resource_api.get_domain(domain_id) + if project_id: + self.resource_api.get_project(project_id) + self.driver.create_grant(role_id, user_id, group_id, domain_id, + project_id, inherited_to_projects) + + def get_grant(self, role_id, user_id=None, group_id=None, + domain_id=None, project_id=None, + inherited_to_projects=False): + role_ref = self.role_api.get_role(role_id) + if domain_id: + self.resource_api.get_domain(domain_id) + if project_id: + self.resource_api.get_project(project_id) + self.driver.check_grant_role_id( + role_id, user_id, group_id, domain_id, project_id, + inherited_to_projects) + return role_ref + + def list_grants(self, user_id=None, group_id=None, + domain_id=None, project_id=None, + inherited_to_projects=False): + if domain_id: + self.resource_api.get_domain(domain_id) + if project_id: + self.resource_api.get_project(project_id) + grant_ids = self.driver.list_grant_role_ids( + user_id, group_id, domain_id, project_id, inherited_to_projects) + return self.role_api.list_roles_from_ids(grant_ids) + + @notifications.role_assignment('deleted') + def delete_grant(self, role_id, user_id=None, group_id=None, + domain_id=None, project_id=None, + inherited_to_projects=False, context=None): + if group_id is None: + self.revoke_api.revoke_by_grant(user_id=user_id, + role_id=role_id, + domain_id=domain_id, + project_id=project_id) + else: + try: + # NOTE(morganfainberg): The user ids are the important part + # for invalidating tokens below, so extract them here. + for user in self.identity_api.list_users_in_group(group_id): + if user['id'] != user_id: + self._emit_invalidate_user_token_persistence( + user['id']) + self.revoke_api.revoke_by_grant( + user_id=user['id'], role_id=role_id, + domain_id=domain_id, project_id=project_id) + except exception.GroupNotFound: + LOG.debug('Group %s not found, no tokens to invalidate.', + group_id) + + # TODO(henry-nash): While having the call to get_role here mimics the + # previous behavior (when it was buried inside the driver delete call), + # this seems an odd place to have this check, given what we have + # already done so far in this method. See Bug #1406776. + self.role_api.get_role(role_id) + + if domain_id: + self.resource_api.get_domain(domain_id) + if project_id: + self.resource_api.get_project(project_id) + self.driver.delete_grant(role_id, user_id, group_id, domain_id, + project_id, inherited_to_projects) + if user_id is not None: + self._emit_invalidate_user_token_persistence(user_id) + + def delete_tokens_for_role_assignments(self, role_id): + assignments = self.list_role_assignments_for_role(role_id=role_id) + + # Iterate over the assignments for this role and build the list of + # user or user+project IDs for the tokens we need to delete + user_ids = set() + user_and_project_ids = list() + for assignment in assignments: + # If we have a project assignment, then record both the user and + # project IDs so we can target the right token to delete. If it is + # a domain assignment, we might as well kill all the tokens for + # the user, since in the vast majority of cases all the tokens + # for a user will be within one domain anyway, so not worth + # trying to delete tokens for each project in the domain. + if 'user_id' in assignment: + if 'project_id' in assignment: + user_and_project_ids.append( + (assignment['user_id'], assignment['project_id'])) + elif 'domain_id' in assignment: + self._emit_invalidate_user_token_persistence( + assignment['user_id']) + elif 'group_id' in assignment: + # Add in any users for this group, being tolerant of any + # cross-driver database integrity errors. + try: + users = self.identity_api.list_users_in_group( + assignment['group_id']) + except exception.GroupNotFound: + # Ignore it, but log a debug message + if 'project_id' in assignment: + target = _('Project (%s)') % assignment['project_id'] + elif 'domain_id' in assignment: + target = _('Domain (%s)') % assignment['domain_id'] + else: + target = _('Unknown Target') + msg = ('Group (%(group)s), referenced in assignment ' + 'for %(target)s, not found - ignoring.') + LOG.debug(msg, {'group': assignment['group_id'], + 'target': target}) + continue + + if 'project_id' in assignment: + for user in users: + user_and_project_ids.append( + (user['id'], assignment['project_id'])) + elif 'domain_id' in assignment: + for user in users: + self._emit_invalidate_user_token_persistence( + user['id']) + + # Now process the built up lists. Before issuing calls to delete any + # tokens, let's try and minimize the number of calls by pruning out + # any user+project deletions where a general token deletion for that + # same user is also planned. + user_and_project_ids_to_action = [] + for user_and_project_id in user_and_project_ids: + if user_and_project_id[0] not in user_ids: + user_and_project_ids_to_action.append(user_and_project_id) + + for user_id, project_id in user_and_project_ids_to_action: + self._emit_invalidate_user_project_tokens_notification( + {'user_id': user_id, + 'project_id': project_id}) + + @notifications.internal( + notifications.INVALIDATE_USER_PROJECT_TOKEN_PERSISTENCE) + def _emit_invalidate_user_project_tokens_notification(self, payload): + # This notification's payload is a dict of user_id and + # project_id so the token provider can invalidate the tokens + # from persistence if persistence is enabled. + pass + + @deprecated_to_role_api + def create_role(self, role_id, role): + return self.role_api.create_role(role_id, role) + + @deprecated_to_role_api + def get_role(self, role_id): + return self.role_api.get_role(role_id) + + @deprecated_to_role_api + def update_role(self, role_id, role): + return self.role_api.update_role(role_id, role) + + @deprecated_to_role_api + def delete_role(self, role_id): + return self.role_api.delete_role(role_id) + + @deprecated_to_role_api + def list_roles(self, hints=None): + return self.role_api.list_roles(hints=hints) + + @deprecated_to_resource_api + def create_project(self, project_id, project): + return self.resource_api.create_project(project_id, project) + + @deprecated_to_resource_api + def get_project_by_name(self, tenant_name, domain_id): + return self.resource_api.get_project_by_name(tenant_name, domain_id) + + @deprecated_to_resource_api + def get_project(self, project_id): + return self.resource_api.get_project(project_id) + + @deprecated_to_resource_api + def update_project(self, project_id, project): + return self.resource_api.update_project(project_id, project) + + @deprecated_to_resource_api + def delete_project(self, project_id): + return self.resource_api.delete_project(project_id) + + @deprecated_to_resource_api + def list_projects(self, hints=None): + return self.resource_api.list_projects(hints=hints) + + @deprecated_to_resource_api + def list_projects_in_domain(self, domain_id): + return self.resource_api.list_projects_in_domain(domain_id) + + @deprecated_to_resource_api + def create_domain(self, domain_id, domain): + return self.resource_api.create_domain(domain_id, domain) + + @deprecated_to_resource_api + def get_domain_by_name(self, domain_name): + return self.resource_api.get_domain_by_name(domain_name) + + @deprecated_to_resource_api + def get_domain(self, domain_id): + return self.resource_api.get_domain(domain_id) + + @deprecated_to_resource_api + def update_domain(self, domain_id, domain): + return self.resource_api.update_domain(domain_id, domain) + + @deprecated_to_resource_api + def delete_domain(self, domain_id): + return self.resource_api.delete_domain(domain_id) + + @deprecated_to_resource_api + def list_domains(self, hints=None): + return self.resource_api.list_domains(hints=hints) + + @deprecated_to_resource_api + def assert_domain_enabled(self, domain_id, domain=None): + return self.resource_api.assert_domain_enabled(domain_id, domain) + + @deprecated_to_resource_api + def assert_project_enabled(self, project_id, project=None): + return self.resource_api.assert_project_enabled(project_id, project) + + @deprecated_to_resource_api + def is_leaf_project(self, project_id): + return self.resource_api.is_leaf_project(project_id) + + @deprecated_to_resource_api + def list_project_parents(self, project_id, user_id=None): + return self.resource_api.list_project_parents(project_id, user_id) + + @deprecated_to_resource_api + def list_projects_in_subtree(self, project_id, user_id=None): + return self.resource_api.list_projects_in_subtree(project_id, user_id) + + +@six.add_metaclass(abc.ABCMeta) +class Driver(object): + + def _role_to_dict(self, role_id, inherited): + role_dict = {'id': role_id} + if inherited: + role_dict['inherited_to'] = 'projects' + return role_dict + + def _roles_from_role_dicts(self, dict_list, inherited): + role_list = [] + for d in dict_list: + if ((not d.get('inherited_to') and not inherited) or + (d.get('inherited_to') == 'projects' and inherited)): + role_list.append(d['id']) + return role_list + + def _add_role_to_role_dicts(self, role_id, inherited, dict_list, + allow_existing=True): + # There is a difference in error semantics when trying to + # assign a role that already exists between the coded v2 and v3 + # API calls. v2 will error if the assignment already exists, + # while v3 is silent. Setting the 'allow_existing' parameter + # appropriately lets this call be used for both. + role_set = set([frozenset(r.items()) for r in dict_list]) + key = frozenset(self._role_to_dict(role_id, inherited).items()) + if not allow_existing and key in role_set: + raise KeyError + role_set.add(key) + return [dict(r) for r in role_set] + + def _remove_role_from_role_dicts(self, role_id, inherited, dict_list): + role_set = set([frozenset(r.items()) for r in dict_list]) + role_set.remove(frozenset(self._role_to_dict(role_id, + inherited).items())) + return [dict(r) for r in role_set] + + def _get_list_limit(self): + return CONF.assignment.list_limit or CONF.list_limit + + @abc.abstractmethod + def list_user_ids_for_project(self, tenant_id): + """Lists all user IDs with a role assignment in the specified project. + + :returns: a list of user_ids or an empty set. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def add_role_to_user_and_project(self, user_id, tenant_id, role_id): + """Add a role to a user within given tenant. + + :raises: keystone.exception.Conflict + + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def remove_role_from_user_and_project(self, user_id, tenant_id, role_id): + """Remove a role from a user within given tenant. + + :raises: keystone.exception.RoleNotFound + + """ + raise exception.NotImplemented() # pragma: no cover + + # assignment/grant crud + + @abc.abstractmethod + def create_grant(self, role_id, user_id=None, group_id=None, + domain_id=None, project_id=None, + inherited_to_projects=False): + """Creates a new assignment/grant. + + If the assignment is to a domain, then optionally it may be + specified as inherited to owned projects (this requires + the OS-INHERIT extension to be enabled). + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_grant_role_ids(self, user_id=None, group_id=None, + domain_id=None, project_id=None, + inherited_to_projects=False): + """Lists role ids for assignments/grants.""" + + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def check_grant_role_id(self, role_id, user_id=None, group_id=None, + domain_id=None, project_id=None, + inherited_to_projects=False): + """Checks an assignment/grant role id. + + :raises: keystone.exception.RoleAssignmentNotFound + :returns: None or raises an exception if grant not found + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def delete_grant(self, role_id, user_id=None, group_id=None, + domain_id=None, project_id=None, + inherited_to_projects=False): + """Deletes assignments/grants. + + :raises: keystone.exception.RoleAssignmentNotFound + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_role_assignments(self): + + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_project_ids_for_user(self, user_id, group_ids, hints, + inherited=False): + """List all project ids associated with a given user. + + :param user_id: the user in question + :param group_ids: the groups this user is a member of. This list is + built in the Manager, so that the driver itself + does not have to call across to identity. + :param hints: filter hints which the driver should + implement if at all possible. + :param inherited: whether assignments marked as inherited should + be included. + + :returns: a list of project ids or an empty list. + + This method should not try and expand any inherited assignments, + just report the projects that have the role for this user. The manager + method is responsible for expanding out inherited assignments. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_project_ids_for_groups(self, group_ids, hints, + inherited=False): + """List project ids accessible to specified groups. + + :param group_ids: List of group ids. + :param hints: filter hints which the driver should + implement if at all possible. + :param inherited: whether assignments marked as inherited should + be included. + :returns: List of project ids accessible to specified groups. + + This method should not try and expand any inherited assignments, + just report the projects that have the role for this group. The manager + method is responsible for expanding out inherited assignments. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_domain_ids_for_user(self, user_id, group_ids, hints, + inherited=False): + """List all domain ids associated with a given user. + + :param user_id: the user in question + :param group_ids: the groups this user is a member of. This list is + built in the Manager, so that the driver itself + does not have to call across to identity. + :param hints: filter hints which the driver should + implement if at all possible. + :param inherited: whether to return domain_ids that have inherited + assignments or not. + + :returns: a list of domain ids or an empty list. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_domain_ids_for_groups(self, group_ids, inherited=False): + """List domain ids accessible to specified groups. + + :param group_ids: List of group ids. + :param inherited: whether to return domain_ids that have inherited + assignments or not. + :returns: List of domain ids accessible to specified groups. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_role_ids_for_groups_on_project( + self, group_ids, project_id, project_domain_id, project_parents): + """List the group role ids for a specific project. + + Supports the ``OS-INHERIT`` role inheritance from the project's domain + if supported by the assignment driver. + + :param group_ids: list of group ids + :type group_ids: list + :param project_id: project identifier + :type project_id: str + :param project_domain_id: project's domain identifier + :type project_domain_id: str + :param project_parents: list of parent ids of this project + :type project_parents: list + :returns: list of role ids for the project + :rtype: list + """ + raise exception.NotImplemented() + + @abc.abstractmethod + def list_role_ids_for_groups_on_domain(self, group_ids, domain_id): + """List the group role ids for a specific domain. + + :param group_ids: list of group ids + :type group_ids: list + :param domain_id: domain identifier + :type domain_id: str + :returns: list of role ids for the project + :rtype: list + """ + raise exception.NotImplemented() + + @abc.abstractmethod + def delete_project_assignments(self, project_id): + """Deletes all assignments for a project. + + :raises: keystone.exception.ProjectNotFound + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def delete_role_assignments(self, role_id): + """Deletes all assignments for a role.""" + + raise exception.NotImplemented() # pragma: no cover + + # TODO(henry-nash): Rename the following two methods to match the more + # meaningfully named ones above. + +# TODO(ayoung): determine what else these two functions raise + @abc.abstractmethod + def delete_user(self, user_id): + """Deletes all assignments for a user. + + :raises: keystone.exception.RoleNotFound + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def delete_group(self, group_id): + """Deletes all assignments for a group. + + :raises: keystone.exception.RoleNotFound + + """ + raise exception.NotImplemented() # pragma: no cover + + +@dependency.provider('role_api') +@dependency.requires('assignment_api') +class RoleManager(manager.Manager): + """Default pivot point for the Role backend.""" + + _ROLE = 'role' + + def __init__(self): + # If there is a specific driver specified for role, then use it. + # Otherwise retrieve the driver type from the assignment driver. + role_driver = CONF.role.driver + + if role_driver is None: + assignment_driver = ( + dependency.get_provider('assignment_api').driver) + role_driver = assignment_driver.default_role_driver() + + super(RoleManager, self).__init__(role_driver) + + @MEMOIZE + def get_role(self, role_id): + return self.driver.get_role(role_id) + + def create_role(self, role_id, role, initiator=None): + ret = self.driver.create_role(role_id, role) + notifications.Audit.created(self._ROLE, role_id, initiator) + if MEMOIZE.should_cache(ret): + self.get_role.set(ret, self, role_id) + return ret + + @manager.response_truncated + def list_roles(self, hints=None): + return self.driver.list_roles(hints or driver_hints.Hints()) + + def update_role(self, role_id, role, initiator=None): + ret = self.driver.update_role(role_id, role) + notifications.Audit.updated(self._ROLE, role_id, initiator) + self.get_role.invalidate(self, role_id) + return ret + + def delete_role(self, role_id, initiator=None): + try: + self.assignment_api.delete_tokens_for_role_assignments(role_id) + except exception.NotImplemented: + # FIXME(morganfainberg): Not all backends (ldap) implement + # `list_role_assignments_for_role` which would have previously + # caused a NotImplmented error to be raised when called through + # the controller. Now error or proper action will always come from + # the `delete_role` method logic. Work needs to be done to make + # the behavior between drivers consistent (capable of revoking + # tokens for the same circumstances). This is related to the bug + # https://bugs.launchpad.net/keystone/+bug/1221805 + pass + self.assignment_api.delete_role_assignments(role_id) + self.driver.delete_role(role_id) + notifications.Audit.deleted(self._ROLE, role_id, initiator) + self.get_role.invalidate(self, role_id) + + +@six.add_metaclass(abc.ABCMeta) +class RoleDriver(object): + + def _get_list_limit(self): + return CONF.role.list_limit or CONF.list_limit + + @abc.abstractmethod + def create_role(self, role_id, role): + """Creates a new role. + + :raises: keystone.exception.Conflict + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_roles(self, hints): + """List roles in the system. + + :param hints: filter hints which the driver should + implement if at all possible. + + :returns: a list of role_refs or an empty list. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_roles_from_ids(self, role_ids): + """List roles for the provided list of ids. + + :param role_ids: list of ids + + :returns: a list of role_refs. + + This method is used internally by the assignment manager to bulk read + a set of roles given their ids. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def get_role(self, role_id): + """Get a role by ID. + + :returns: role_ref + :raises: keystone.exception.RoleNotFound + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def update_role(self, role_id, role): + """Updates an existing role. + + :raises: keystone.exception.RoleNotFound, + keystone.exception.Conflict + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def delete_role(self, role_id): + """Deletes an existing role. + + :raises: keystone.exception.RoleNotFound + + """ + raise exception.NotImplemented() # pragma: no cover diff --git a/keystone-moon/keystone/assignment/role_backends/__init__.py b/keystone-moon/keystone/assignment/role_backends/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/assignment/role_backends/ldap.py b/keystone-moon/keystone/assignment/role_backends/ldap.py new file mode 100644 index 00000000..d5a06a4c --- /dev/null +++ b/keystone-moon/keystone/assignment/role_backends/ldap.py @@ -0,0 +1,125 @@ +# 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 __future__ import absolute_import + +from oslo_config import cfg +from oslo_log import log + +from keystone import assignment +from keystone.common import ldap as common_ldap +from keystone.common import models +from keystone import exception +from keystone.i18n import _ +from keystone.identity.backends import ldap as ldap_identity + + +CONF = cfg.CONF +LOG = log.getLogger(__name__) + + +class Role(assignment.RoleDriver): + + def __init__(self): + super(Role, self).__init__() + self.LDAP_URL = CONF.ldap.url + self.LDAP_USER = CONF.ldap.user + self.LDAP_PASSWORD = CONF.ldap.password + self.suffix = CONF.ldap.suffix + + # This is the only deep dependency from resource back + # to identity. The assumption is that if you are using + # LDAP for resource, you are using it for identity as well. + self.user = ldap_identity.UserApi(CONF) + self.role = RoleApi(CONF, self.user) + + def get_role(self, role_id): + return self.role.get(role_id) + + def list_roles(self, hints): + return self.role.get_all() + + def list_roles_from_ids(self, ids): + return [self.get_role(id) for id in ids] + + def create_role(self, role_id, role): + self.role.check_allow_create() + try: + self.get_role(role_id) + except exception.NotFound: + pass + else: + msg = _('Duplicate ID, %s.') % role_id + raise exception.Conflict(type='role', details=msg) + + try: + self.role.get_by_name(role['name']) + except exception.NotFound: + pass + else: + msg = _('Duplicate name, %s.') % role['name'] + raise exception.Conflict(type='role', details=msg) + + return self.role.create(role) + + def delete_role(self, role_id): + self.role.check_allow_delete() + return self.role.delete(role_id) + + def update_role(self, role_id, role): + self.role.check_allow_update() + self.get_role(role_id) + return self.role.update(role_id, role) + + +# NOTE(heny-nash): A mixin class to enable the sharing of the LDAP structure +# between here and the assignment LDAP. +class RoleLdapStructureMixin(object): + DEFAULT_OU = 'ou=Roles' + DEFAULT_STRUCTURAL_CLASSES = [] + DEFAULT_OBJECTCLASS = 'organizationalRole' + DEFAULT_MEMBER_ATTRIBUTE = 'roleOccupant' + NotFound = exception.RoleNotFound + options_name = 'role' + attribute_options_names = {'name': 'name'} + immutable_attrs = ['id'] + model = models.Role + + +# TODO(termie): turn this into a data object and move logic to driver +class RoleApi(RoleLdapStructureMixin, common_ldap.BaseLdap): + + def __init__(self, conf, user_api): + super(RoleApi, self).__init__(conf) + self._user_api = user_api + + def get(self, role_id, role_filter=None): + model = super(RoleApi, self).get(role_id, role_filter) + return model + + def create(self, values): + return super(RoleApi, self).create(values) + + def update(self, role_id, role): + new_name = role.get('name') + if new_name is not None: + try: + old_role = self.get_by_name(new_name) + if old_role['id'] != role_id: + raise exception.Conflict( + _('Cannot duplicate name %s') % old_role) + except exception.NotFound: + pass + return super(RoleApi, self).update(role_id, role) + + def delete(self, role_id): + super(RoleApi, self).delete(role_id) diff --git a/keystone-moon/keystone/assignment/role_backends/sql.py b/keystone-moon/keystone/assignment/role_backends/sql.py new file mode 100644 index 00000000..f19d1827 --- /dev/null +++ b/keystone-moon/keystone/assignment/role_backends/sql.py @@ -0,0 +1,80 @@ +# 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 keystone import assignment +from keystone.common import sql +from keystone import exception + + +class Role(assignment.RoleDriver): + + @sql.handle_conflicts(conflict_type='role') + def create_role(self, role_id, role): + with sql.transaction() as session: + ref = RoleTable.from_dict(role) + session.add(ref) + return ref.to_dict() + + @sql.truncated + def list_roles(self, hints): + with sql.transaction() as session: + query = session.query(RoleTable) + refs = sql.filter_limit_query(RoleTable, query, hints) + return [ref.to_dict() for ref in refs] + + def list_roles_from_ids(self, ids): + if not ids: + return [] + else: + with sql.transaction() as session: + query = session.query(RoleTable) + query = query.filter(RoleTable.id.in_(ids)) + role_refs = query.all() + return [role_ref.to_dict() for role_ref in role_refs] + + def _get_role(self, session, role_id): + ref = session.query(RoleTable).get(role_id) + if ref is None: + raise exception.RoleNotFound(role_id=role_id) + return ref + + def get_role(self, role_id): + with sql.transaction() as session: + return self._get_role(session, role_id).to_dict() + + @sql.handle_conflicts(conflict_type='role') + def update_role(self, role_id, role): + with sql.transaction() as session: + ref = self._get_role(session, role_id) + old_dict = ref.to_dict() + for k in role: + old_dict[k] = role[k] + new_role = RoleTable.from_dict(old_dict) + for attr in RoleTable.attributes: + if attr != 'id': + setattr(ref, attr, getattr(new_role, attr)) + ref.extra = new_role.extra + return ref.to_dict() + + def delete_role(self, role_id): + with sql.transaction() as session: + ref = self._get_role(session, role_id) + session.delete(ref) + + +class RoleTable(sql.ModelBase, sql.DictBase): + __tablename__ = 'role' + attributes = ['id', 'name'] + id = sql.Column(sql.String(64), primary_key=True) + name = sql.Column(sql.String(255), unique=True, nullable=False) + extra = sql.Column(sql.JsonBlob()) + __table_args__ = (sql.UniqueConstraint('name'), {}) diff --git a/keystone-moon/keystone/assignment/routers.py b/keystone-moon/keystone/assignment/routers.py new file mode 100644 index 00000000..49549a0b --- /dev/null +++ b/keystone-moon/keystone/assignment/routers.py @@ -0,0 +1,246 @@ +# Copyright 2013 Metacloud, Inc. +# Copyright 2012 OpenStack Foundation +# +# 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. + +"""WSGI Routers for the Assignment service.""" + +import functools + +from oslo_config import cfg + +from keystone.assignment import controllers +from keystone.common import json_home +from keystone.common import router +from keystone.common import wsgi + + +CONF = cfg.CONF + +build_os_inherit_relation = functools.partial( + json_home.build_v3_extension_resource_relation, + extension_name='OS-INHERIT', extension_version='1.0') + + +class Public(wsgi.ComposableRouter): + def add_routes(self, mapper): + tenant_controller = controllers.TenantAssignment() + mapper.connect('/tenants', + controller=tenant_controller, + action='get_projects_for_token', + conditions=dict(method=['GET'])) + + +class Admin(wsgi.ComposableRouter): + def add_routes(self, mapper): + # Role Operations + roles_controller = controllers.RoleAssignmentV2() + mapper.connect('/tenants/{tenant_id}/users/{user_id}/roles', + controller=roles_controller, + action='get_user_roles', + conditions=dict(method=['GET'])) + mapper.connect('/users/{user_id}/roles', + controller=roles_controller, + action='get_user_roles', + conditions=dict(method=['GET'])) + + +class Routers(wsgi.RoutersBase): + + def append_v3_routers(self, mapper, routers): + + project_controller = controllers.ProjectAssignmentV3() + self._add_resource( + mapper, project_controller, + path='/users/{user_id}/projects', + get_action='list_user_projects', + rel=json_home.build_v3_resource_relation('user_projects'), + path_vars={ + 'user_id': json_home.Parameters.USER_ID, + }) + + routers.append( + router.Router(controllers.RoleV3(), 'roles', 'role', + resource_descriptions=self.v3_resources)) + + grant_controller = controllers.GrantAssignmentV3() + self._add_resource( + mapper, grant_controller, + path='/projects/{project_id}/users/{user_id}/roles/{role_id}', + get_head_action='check_grant', + put_action='create_grant', + delete_action='revoke_grant', + rel=json_home.build_v3_resource_relation('project_user_role'), + path_vars={ + 'project_id': json_home.Parameters.PROJECT_ID, + 'role_id': json_home.Parameters.ROLE_ID, + 'user_id': json_home.Parameters.USER_ID, + }) + self._add_resource( + mapper, grant_controller, + path='/projects/{project_id}/groups/{group_id}/roles/{role_id}', + get_head_action='check_grant', + put_action='create_grant', + delete_action='revoke_grant', + rel=json_home.build_v3_resource_relation('project_group_role'), + path_vars={ + 'group_id': json_home.Parameters.GROUP_ID, + 'project_id': json_home.Parameters.PROJECT_ID, + 'role_id': json_home.Parameters.ROLE_ID, + }) + self._add_resource( + mapper, grant_controller, + path='/projects/{project_id}/users/{user_id}/roles', + get_action='list_grants', + rel=json_home.build_v3_resource_relation('project_user_roles'), + path_vars={ + 'project_id': json_home.Parameters.PROJECT_ID, + 'user_id': json_home.Parameters.USER_ID, + }) + self._add_resource( + mapper, grant_controller, + path='/projects/{project_id}/groups/{group_id}/roles', + get_action='list_grants', + rel=json_home.build_v3_resource_relation('project_group_roles'), + path_vars={ + 'group_id': json_home.Parameters.GROUP_ID, + 'project_id': json_home.Parameters.PROJECT_ID, + }) + self._add_resource( + mapper, grant_controller, + path='/domains/{domain_id}/users/{user_id}/roles/{role_id}', + get_head_action='check_grant', + put_action='create_grant', + delete_action='revoke_grant', + rel=json_home.build_v3_resource_relation('domain_user_role'), + path_vars={ + 'domain_id': json_home.Parameters.DOMAIN_ID, + 'role_id': json_home.Parameters.ROLE_ID, + 'user_id': json_home.Parameters.USER_ID, + }) + self._add_resource( + mapper, grant_controller, + path='/domains/{domain_id}/groups/{group_id}/roles/{role_id}', + get_head_action='check_grant', + put_action='create_grant', + delete_action='revoke_grant', + rel=json_home.build_v3_resource_relation('domain_group_role'), + path_vars={ + 'domain_id': json_home.Parameters.DOMAIN_ID, + 'group_id': json_home.Parameters.GROUP_ID, + 'role_id': json_home.Parameters.ROLE_ID, + }) + self._add_resource( + mapper, grant_controller, + path='/domains/{domain_id}/users/{user_id}/roles', + get_action='list_grants', + rel=json_home.build_v3_resource_relation('domain_user_roles'), + path_vars={ + 'domain_id': json_home.Parameters.DOMAIN_ID, + 'user_id': json_home.Parameters.USER_ID, + }) + self._add_resource( + mapper, grant_controller, + path='/domains/{domain_id}/groups/{group_id}/roles', + get_action='list_grants', + rel=json_home.build_v3_resource_relation('domain_group_roles'), + path_vars={ + 'domain_id': json_home.Parameters.DOMAIN_ID, + 'group_id': json_home.Parameters.GROUP_ID, + }) + + routers.append( + router.Router(controllers.RoleAssignmentV3(), + 'role_assignments', 'role_assignment', + resource_descriptions=self.v3_resources, + is_entity_implemented=False)) + + if CONF.os_inherit.enabled: + self._add_resource( + mapper, grant_controller, + path='/OS-INHERIT/domains/{domain_id}/users/{user_id}/roles/' + '{role_id}/inherited_to_projects', + get_head_action='check_grant', + put_action='create_grant', + delete_action='revoke_grant', + rel=build_os_inherit_relation( + resource_name='domain_user_role_inherited_to_projects'), + path_vars={ + 'domain_id': json_home.Parameters.DOMAIN_ID, + 'role_id': json_home.Parameters.ROLE_ID, + 'user_id': json_home.Parameters.USER_ID, + }) + self._add_resource( + mapper, grant_controller, + path='/OS-INHERIT/domains/{domain_id}/groups/{group_id}/roles/' + '{role_id}/inherited_to_projects', + get_head_action='check_grant', + put_action='create_grant', + delete_action='revoke_grant', + rel=build_os_inherit_relation( + resource_name='domain_group_role_inherited_to_projects'), + path_vars={ + 'domain_id': json_home.Parameters.DOMAIN_ID, + 'group_id': json_home.Parameters.GROUP_ID, + 'role_id': json_home.Parameters.ROLE_ID, + }) + self._add_resource( + mapper, grant_controller, + path='/OS-INHERIT/domains/{domain_id}/groups/{group_id}/roles/' + 'inherited_to_projects', + get_action='list_grants', + rel=build_os_inherit_relation( + resource_name='domain_group_roles_inherited_to_projects'), + path_vars={ + 'domain_id': json_home.Parameters.DOMAIN_ID, + 'group_id': json_home.Parameters.GROUP_ID, + }) + self._add_resource( + mapper, grant_controller, + path='/OS-INHERIT/domains/{domain_id}/users/{user_id}/roles/' + 'inherited_to_projects', + get_action='list_grants', + rel=build_os_inherit_relation( + resource_name='domain_user_roles_inherited_to_projects'), + path_vars={ + 'domain_id': json_home.Parameters.DOMAIN_ID, + 'user_id': json_home.Parameters.USER_ID, + }) + self._add_resource( + mapper, grant_controller, + path='/OS-INHERIT/projects/{project_id}/users/{user_id}/roles/' + '{role_id}/inherited_to_projects', + get_head_action='check_grant', + put_action='create_grant', + delete_action='revoke_grant', + rel=build_os_inherit_relation( + resource_name='project_user_role_inherited_to_projects'), + path_vars={ + 'project_id': json_home.Parameters.PROJECT_ID, + 'user_id': json_home.Parameters.USER_ID, + 'role_id': json_home.Parameters.ROLE_ID, + }) + self._add_resource( + mapper, grant_controller, + path='/OS-INHERIT/projects/{project_id}/groups/{group_id}/' + 'roles/{role_id}/inherited_to_projects', + get_head_action='check_grant', + put_action='create_grant', + delete_action='revoke_grant', + rel=build_os_inherit_relation( + resource_name='project_group_role_inherited_to_projects'), + path_vars={ + 'project_id': json_home.Parameters.PROJECT_ID, + 'group_id': json_home.Parameters.GROUP_ID, + 'role_id': json_home.Parameters.ROLE_ID, + }) diff --git a/keystone-moon/keystone/assignment/schema.py b/keystone-moon/keystone/assignment/schema.py new file mode 100644 index 00000000..f4d1b08a --- /dev/null +++ b/keystone-moon/keystone/assignment/schema.py @@ -0,0 +1,32 @@ +# 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 keystone.common.validation import parameter_types + + +_role_properties = { + 'name': parameter_types.name +} + +role_create = { + 'type': 'object', + 'properties': _role_properties, + 'required': ['name'], + 'additionalProperties': True +} + +role_update = { + 'type': 'object', + 'properties': _role_properties, + 'minProperties': 1, + 'additionalProperties': True +} diff --git a/keystone-moon/keystone/auth/__init__.py b/keystone-moon/keystone/auth/__init__.py new file mode 100644 index 00000000..b1e4203e --- /dev/null +++ b/keystone-moon/keystone/auth/__init__.py @@ -0,0 +1,17 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 keystone.auth import controllers # noqa +from keystone.auth.core import * # noqa +from keystone.auth import routers # noqa diff --git a/keystone-moon/keystone/auth/controllers.py b/keystone-moon/keystone/auth/controllers.py new file mode 100644 index 00000000..065f1f01 --- /dev/null +++ b/keystone-moon/keystone/auth/controllers.py @@ -0,0 +1,647 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 sys + +from keystoneclient.common import cms +from oslo_config import cfg +from oslo_log import log +from oslo_serialization import jsonutils +from oslo_utils import importutils +from oslo_utils import timeutils +import six + +from keystone.common import controller +from keystone.common import dependency +from keystone.common import wsgi +from keystone import config +from keystone.contrib import federation +from keystone import exception +from keystone.i18n import _, _LI, _LW +from keystone.resource import controllers as resource_controllers + + +LOG = log.getLogger(__name__) + +CONF = cfg.CONF + +# registry of authentication methods +AUTH_METHODS = {} +AUTH_PLUGINS_LOADED = False + + +def load_auth_methods(): + global AUTH_PLUGINS_LOADED + + if AUTH_PLUGINS_LOADED: + # Only try and load methods a single time. + return + # config.setup_authentication should be idempotent, call it to ensure we + # have setup all the appropriate configuration options we may need. + config.setup_authentication() + for plugin in CONF.auth.methods: + if '.' in plugin: + # NOTE(morganfainberg): if '.' is in the plugin name, it should be + # imported rather than used as a plugin identifier. + plugin_class = plugin + driver = importutils.import_object(plugin) + if not hasattr(driver, 'method'): + raise ValueError(_('Cannot load an auth-plugin by class-name ' + 'without a "method" attribute defined: %s'), + plugin_class) + + LOG.info(_LI('Loading auth-plugins by class-name is deprecated.')) + plugin_name = driver.method + else: + plugin_name = plugin + plugin_class = CONF.auth.get(plugin) + driver = importutils.import_object(plugin_class) + if plugin_name in AUTH_METHODS: + raise ValueError(_('Auth plugin %(plugin)s is requesting ' + 'previously registered method %(method)s') % + {'plugin': plugin_class, 'method': driver.method}) + AUTH_METHODS[plugin_name] = driver + AUTH_PLUGINS_LOADED = True + + +def get_auth_method(method_name): + global AUTH_METHODS + if method_name not in AUTH_METHODS: + raise exception.AuthMethodNotSupported() + return AUTH_METHODS[method_name] + + +class AuthContext(dict): + """Retrofitting auth_context to reconcile identity attributes. + + The identity attributes must not have conflicting values among the + auth plug-ins. The only exception is `expires_at`, which is set to its + earliest value. + + """ + + # identity attributes need to be reconciled among the auth plugins + IDENTITY_ATTRIBUTES = frozenset(['user_id', 'project_id', + 'access_token_id', 'domain_id', + 'expires_at']) + + def __setitem__(self, key, val): + if key in self.IDENTITY_ATTRIBUTES and key in self: + existing_val = self[key] + if key == 'expires_at': + # special treatment for 'expires_at', we are going to take + # the earliest expiration instead. + if existing_val != val: + LOG.info(_LI('"expires_at" has conflicting values ' + '%(existing)s and %(new)s. Will use the ' + 'earliest value.'), + {'existing': existing_val, 'new': val}) + if existing_val is None or val is None: + val = existing_val or val + else: + val = min(existing_val, val) + elif existing_val != val: + msg = _('Unable to reconcile identity attribute %(attribute)s ' + 'as it has conflicting values %(new)s and %(old)s') % ( + {'attribute': key, + 'new': val, + 'old': existing_val}) + raise exception.Unauthorized(msg) + return super(AuthContext, self).__setitem__(key, val) + + +# TODO(blk-u): this class doesn't use identity_api directly, but makes it +# available for consumers. Consumers should probably not be getting +# identity_api from this since it's available in global registry, then +# identity_api should be removed from this list. +@dependency.requires('identity_api', 'resource_api', 'trust_api') +class AuthInfo(object): + """Encapsulation of "auth" request.""" + + @staticmethod + def create(context, auth=None): + auth_info = AuthInfo(context, auth=auth) + auth_info._validate_and_normalize_auth_data() + return auth_info + + def __init__(self, context, auth=None): + self.context = context + self.auth = auth + self._scope_data = (None, None, None, None) + # self._scope_data is (domain_id, project_id, trust_ref, unscoped) + # project scope: (None, project_id, None, None) + # domain scope: (domain_id, None, None, None) + # trust scope: (None, None, trust_ref, None) + # unscoped: (None, None, None, 'unscoped') + + def _assert_project_is_enabled(self, project_ref): + # ensure the project is enabled + try: + self.resource_api.assert_project_enabled( + project_id=project_ref['id'], + project=project_ref) + except AssertionError as e: + LOG.warning(six.text_type(e)) + six.reraise(exception.Unauthorized, exception.Unauthorized(e), + sys.exc_info()[2]) + + def _assert_domain_is_enabled(self, domain_ref): + try: + self.resource_api.assert_domain_enabled( + domain_id=domain_ref['id'], + domain=domain_ref) + except AssertionError as e: + LOG.warning(six.text_type(e)) + six.reraise(exception.Unauthorized, exception.Unauthorized(e), + sys.exc_info()[2]) + + def _lookup_domain(self, domain_info): + domain_id = domain_info.get('id') + domain_name = domain_info.get('name') + domain_ref = None + if not domain_id and not domain_name: + raise exception.ValidationError(attribute='id or name', + target='domain') + try: + if domain_name: + domain_ref = self.resource_api.get_domain_by_name( + domain_name) + else: + domain_ref = self.resource_api.get_domain(domain_id) + except exception.DomainNotFound as e: + LOG.exception(six.text_type(e)) + raise exception.Unauthorized(e) + self._assert_domain_is_enabled(domain_ref) + return domain_ref + + def _lookup_project(self, project_info): + project_id = project_info.get('id') + project_name = project_info.get('name') + project_ref = None + if not project_id and not project_name: + raise exception.ValidationError(attribute='id or name', + target='project') + try: + if project_name: + if 'domain' not in project_info: + raise exception.ValidationError(attribute='domain', + target='project') + domain_ref = self._lookup_domain(project_info['domain']) + project_ref = self.resource_api.get_project_by_name( + project_name, domain_ref['id']) + else: + project_ref = self.resource_api.get_project(project_id) + # NOTE(morganfainberg): The _lookup_domain method will raise + # exception.Unauthorized if the domain isn't found or is + # disabled. + self._lookup_domain({'id': project_ref['domain_id']}) + except exception.ProjectNotFound as e: + raise exception.Unauthorized(e) + self._assert_project_is_enabled(project_ref) + return project_ref + + def _lookup_trust(self, trust_info): + trust_id = trust_info.get('id') + if not trust_id: + raise exception.ValidationError(attribute='trust_id', + target='trust') + trust = self.trust_api.get_trust(trust_id) + if not trust: + raise exception.TrustNotFound(trust_id=trust_id) + return trust + + def _validate_and_normalize_scope_data(self): + """Validate and normalize scope data.""" + if 'scope' not in self.auth: + return + if sum(['project' in self.auth['scope'], + 'domain' in self.auth['scope'], + 'unscoped' in self.auth['scope'], + 'OS-TRUST:trust' in self.auth['scope']]) != 1: + raise exception.ValidationError( + attribute='project, domain, OS-TRUST:trust or unscoped', + target='scope') + if 'unscoped' in self.auth['scope']: + self._scope_data = (None, None, None, 'unscoped') + return + if 'project' in self.auth['scope']: + project_ref = self._lookup_project(self.auth['scope']['project']) + self._scope_data = (None, project_ref['id'], None, None) + elif 'domain' in self.auth['scope']: + domain_ref = self._lookup_domain(self.auth['scope']['domain']) + self._scope_data = (domain_ref['id'], None, None, None) + elif 'OS-TRUST:trust' in self.auth['scope']: + if not CONF.trust.enabled: + raise exception.Forbidden('Trusts are disabled.') + trust_ref = self._lookup_trust( + self.auth['scope']['OS-TRUST:trust']) + # TODO(ayoung): when trusts support domains, fill in domain data + if trust_ref.get('project_id') is not None: + project_ref = self._lookup_project( + {'id': trust_ref['project_id']}) + self._scope_data = (None, project_ref['id'], trust_ref, None) + else: + self._scope_data = (None, None, trust_ref, None) + + def _validate_auth_methods(self): + if 'identity' not in self.auth: + raise exception.ValidationError(attribute='identity', + target='auth') + + # make sure auth methods are provided + if 'methods' not in self.auth['identity']: + raise exception.ValidationError(attribute='methods', + target='identity') + + # make sure all the method data/payload are provided + for method_name in self.get_method_names(): + if method_name not in self.auth['identity']: + raise exception.ValidationError(attribute=method_name, + target='identity') + + # make sure auth method is supported + for method_name in self.get_method_names(): + if method_name not in AUTH_METHODS: + raise exception.AuthMethodNotSupported() + + def _validate_and_normalize_auth_data(self): + """Make sure "auth" is valid.""" + # make sure "auth" exist + if not self.auth: + raise exception.ValidationError(attribute='auth', + target='request body') + + self._validate_auth_methods() + self._validate_and_normalize_scope_data() + + def get_method_names(self): + """Returns the identity method names. + + :returns: list of auth method names + + """ + # Sanitizes methods received in request's body + # Filters out duplicates, while keeping elements' order. + method_names = [] + for method in self.auth['identity']['methods']: + if method not in method_names: + method_names.append(method) + return method_names + + def get_method_data(self, method): + """Get the auth method payload. + + :returns: auth method payload + + """ + if method not in self.auth['identity']['methods']: + raise exception.ValidationError(attribute=method, + target='identity') + return self.auth['identity'][method] + + def get_scope(self): + """Get scope information. + + Verify and return the scoping information. + + :returns: (domain_id, project_id, trust_ref, unscoped). + If scope to a project, (None, project_id, None, None) + will be returned. + If scoped to a domain, (domain_id, None, None, None) + will be returned. + If scoped to a trust, (None, project_id, trust_ref, None), + Will be returned, where the project_id comes from the + trust definition. + If unscoped, (None, None, None, 'unscoped') will be + returned. + + """ + return self._scope_data + + def set_scope(self, domain_id=None, project_id=None, trust=None, + unscoped=None): + """Set scope information.""" + if domain_id and project_id: + msg = _('Scoping to both domain and project is not allowed') + raise ValueError(msg) + if domain_id and trust: + msg = _('Scoping to both domain and trust is not allowed') + raise ValueError(msg) + if project_id and trust: + msg = _('Scoping to both project and trust is not allowed') + raise ValueError(msg) + self._scope_data = (domain_id, project_id, trust, unscoped) + + +@dependency.requires('assignment_api', 'catalog_api', 'identity_api', + 'resource_api', 'token_provider_api', 'trust_api') +class Auth(controller.V3Controller): + + # Note(atiwari): From V3 auth controller code we are + # calling protection() wrappers, so we need to setup + # the member_name and collection_name attributes of + # auth controller code. + # In the absence of these attributes, default 'entity' + # string will be used to represent the target which is + # generic. Policy can be defined using 'entity' but it + # would not reflect the exact entity that is in context. + # We are defining collection_name = 'tokens' and + # member_name = 'token' to facilitate policy decisions. + collection_name = 'tokens' + member_name = 'token' + + def __init__(self, *args, **kw): + super(Auth, self).__init__(*args, **kw) + config.setup_authentication() + + def authenticate_for_token(self, context, auth=None): + """Authenticate user and issue a token.""" + include_catalog = 'nocatalog' not in context['query_string'] + + try: + auth_info = AuthInfo.create(context, auth=auth) + auth_context = AuthContext(extras={}, + method_names=[], + bind={}) + self.authenticate(context, auth_info, auth_context) + if auth_context.get('access_token_id'): + auth_info.set_scope(None, auth_context['project_id'], None) + self._check_and_set_default_scoping(auth_info, auth_context) + (domain_id, project_id, trust, unscoped) = auth_info.get_scope() + + method_names = auth_info.get_method_names() + method_names += auth_context.get('method_names', []) + # make sure the list is unique + method_names = list(set(method_names)) + expires_at = auth_context.get('expires_at') + # NOTE(morganfainberg): define this here so it is clear what the + # argument is during the issue_v3_token provider call. + metadata_ref = None + + token_audit_id = auth_context.get('audit_id') + + (token_id, token_data) = self.token_provider_api.issue_v3_token( + auth_context['user_id'], method_names, expires_at, project_id, + domain_id, auth_context, trust, metadata_ref, include_catalog, + parent_audit_id=token_audit_id) + + # NOTE(wanghong): We consume a trust use only when we are using + # trusts and have successfully issued a token. + if trust: + self.trust_api.consume_use(trust['id']) + + return render_token_data_response(token_id, token_data, + created=True) + except exception.TrustNotFound as e: + raise exception.Unauthorized(e) + + def _check_and_set_default_scoping(self, auth_info, auth_context): + (domain_id, project_id, trust, unscoped) = auth_info.get_scope() + if trust: + project_id = trust['project_id'] + if domain_id or project_id or trust: + # scope is specified + return + + # Skip scoping when unscoped federated token is being issued + if federation.IDENTITY_PROVIDER in auth_context: + return + + # Do not scope if request is for explicitly unscoped token + if unscoped is not None: + return + + # fill in default_project_id if it is available + try: + user_ref = self.identity_api.get_user(auth_context['user_id']) + except exception.UserNotFound as e: + LOG.exception(six.text_type(e)) + raise exception.Unauthorized(e) + + default_project_id = user_ref.get('default_project_id') + if not default_project_id: + # User has no default project. He shall get an unscoped token. + return + + # make sure user's default project is legit before scoping to it + try: + default_project_ref = self.resource_api.get_project( + default_project_id) + default_project_domain_ref = self.resource_api.get_domain( + default_project_ref['domain_id']) + if (default_project_ref.get('enabled', True) and + default_project_domain_ref.get('enabled', True)): + if self.assignment_api.get_roles_for_user_and_project( + user_ref['id'], default_project_id): + auth_info.set_scope(project_id=default_project_id) + else: + msg = _LW("User %(user_id)s doesn't have access to" + " default project %(project_id)s. The token" + " will be unscoped rather than scoped to the" + " project.") + LOG.warning(msg, + {'user_id': user_ref['id'], + 'project_id': default_project_id}) + else: + msg = _LW("User %(user_id)s's default project %(project_id)s" + " is disabled. The token will be unscoped rather" + " than scoped to the project.") + LOG.warning(msg, + {'user_id': user_ref['id'], + 'project_id': default_project_id}) + except (exception.ProjectNotFound, exception.DomainNotFound): + # default project or default project domain doesn't exist, + # will issue unscoped token instead + msg = _LW("User %(user_id)s's default project %(project_id)s not" + " found. The token will be unscoped rather than" + " scoped to the project.") + LOG.warning(msg, {'user_id': user_ref['id'], + 'project_id': default_project_id}) + + def authenticate(self, context, auth_info, auth_context): + """Authenticate user.""" + + # The 'external' method allows any 'REMOTE_USER' based authentication + # In some cases the server can set REMOTE_USER as '' instead of + # dropping it, so this must be filtered out + if context['environment'].get('REMOTE_USER'): + try: + external = get_auth_method('external') + external.authenticate(context, auth_info, auth_context) + except exception.AuthMethodNotSupported: + # This will happen there is no 'external' plugin registered + # and the container is performing authentication. + # The 'kerberos' and 'saml' methods will be used this way. + # In those cases, it is correct to not register an + # 'external' plugin; if there is both an 'external' and a + # 'kerberos' plugin, it would run the check on identity twice. + LOG.debug("No 'external' plugin is registered.") + except exception.Unauthorized: + # If external fails then continue and attempt to determine + # user identity using remaining auth methods + LOG.debug("Authorization failed for 'external' auth method.") + + # need to aggregate the results in case two or more methods + # are specified + auth_response = {'methods': []} + for method_name in auth_info.get_method_names(): + method = get_auth_method(method_name) + resp = method.authenticate(context, + auth_info.get_method_data(method_name), + auth_context) + if resp: + auth_response['methods'].append(method_name) + auth_response[method_name] = resp + + if auth_response["methods"]: + # authentication continuation required + raise exception.AdditionalAuthRequired(auth_response) + + if 'user_id' not in auth_context: + msg = _('User not found') + raise exception.Unauthorized(msg) + + @controller.protected() + def check_token(self, context): + token_id = context.get('subject_token_id') + token_data = self.token_provider_api.validate_v3_token( + token_id) + # NOTE(morganfainberg): The code in + # ``keystone.common.wsgi.render_response`` will remove the content + # body. + return render_token_data_response(token_id, token_data) + + @controller.protected() + def revoke_token(self, context): + token_id = context.get('subject_token_id') + return self.token_provider_api.revoke_token(token_id) + + @controller.protected() + def validate_token(self, context): + token_id = context.get('subject_token_id') + include_catalog = 'nocatalog' not in context['query_string'] + token_data = self.token_provider_api.validate_v3_token( + token_id) + if not include_catalog and 'catalog' in token_data['token']: + del token_data['token']['catalog'] + return render_token_data_response(token_id, token_data) + + @controller.protected() + def revocation_list(self, context, auth=None): + if not CONF.token.revoke_by_id: + raise exception.Gone() + tokens = self.token_provider_api.list_revoked_tokens() + + for t in tokens: + expires = t['expires'] + if not (expires and isinstance(expires, six.text_type)): + t['expires'] = timeutils.isotime(expires) + data = {'revoked': tokens} + json_data = jsonutils.dumps(data) + signed_text = cms.cms_sign_text(json_data, + CONF.signing.certfile, + CONF.signing.keyfile) + + return {'signed': signed_text} + + def _combine_lists_uniquely(self, a, b): + # it's most likely that only one of these will be filled so avoid + # the combination if possible. + if a and b: + return {x['id']: x for x in a + b}.values() + else: + return a or b + + @controller.protected() + def get_auth_projects(self, context): + auth_context = self.get_auth_context(context) + + user_id = auth_context.get('user_id') + user_refs = [] + if user_id: + try: + user_refs = self.assignment_api.list_projects_for_user(user_id) + except exception.UserNotFound: + # federated users have an id but they don't link to anything + pass + + group_ids = auth_context.get('group_ids') + grp_refs = [] + if group_ids: + grp_refs = self.assignment_api.list_projects_for_groups(group_ids) + + refs = self._combine_lists_uniquely(user_refs, grp_refs) + return resource_controllers.ProjectV3.wrap_collection(context, refs) + + @controller.protected() + def get_auth_domains(self, context): + auth_context = self.get_auth_context(context) + + user_id = auth_context.get('user_id') + user_refs = [] + if user_id: + try: + user_refs = self.assignment_api.list_domains_for_user(user_id) + except exception.UserNotFound: + # federated users have an id but they don't link to anything + pass + + group_ids = auth_context.get('group_ids') + grp_refs = [] + if group_ids: + grp_refs = self.assignment_api.list_domains_for_groups(group_ids) + + refs = self._combine_lists_uniquely(user_refs, grp_refs) + return resource_controllers.DomainV3.wrap_collection(context, refs) + + @controller.protected() + def get_auth_catalog(self, context): + auth_context = self.get_auth_context(context) + user_id = auth_context.get('user_id') + project_id = auth_context.get('project_id') + + if not project_id: + raise exception.Forbidden( + _('A project-scoped token is required to produce a service ' + 'catalog.')) + + # The V3Controller base methods mostly assume that you're returning + # either a collection or a single element from a collection, neither of + # which apply to the catalog. Because this is a special case, this + # re-implements a tiny bit of work done by the base controller (such as + # self-referential link building) to avoid overriding or refactoring + # several private methods. + return { + 'catalog': self.catalog_api.get_v3_catalog(user_id, project_id), + 'links': {'self': self.base_url(context, path='auth/catalog')} + } + + +# FIXME(gyee): not sure if it belongs here or keystone.common. Park it here +# for now. +def render_token_data_response(token_id, token_data, created=False): + """Render token data HTTP response. + + Stash token ID into the X-Subject-Token header. + + """ + headers = [('X-Subject-Token', token_id)] + + if created: + status = (201, 'Created') + else: + status = (200, 'OK') + + return wsgi.render_response(body=token_data, + status=status, headers=headers) diff --git a/keystone-moon/keystone/auth/core.py b/keystone-moon/keystone/auth/core.py new file mode 100644 index 00000000..9da2c123 --- /dev/null +++ b/keystone-moon/keystone/auth/core.py @@ -0,0 +1,94 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 abc + +import six + +from keystone import exception + + +@six.add_metaclass(abc.ABCMeta) +class AuthMethodHandler(object): + """Abstract base class for an authentication plugin.""" + + def __init__(self): + pass + + @abc.abstractmethod + def authenticate(self, context, auth_payload, auth_context): + """Authenticate user and return an authentication context. + + :param context: keystone's request context + :param auth_payload: the content of the authentication for a given + method + :param auth_context: user authentication context, a dictionary shared + by all plugins. It contains "method_names" and + "extras" by default. "method_names" is a list and + "extras" is a dictionary. + + If successful, plugin must set ``user_id`` in ``auth_context``. + ``method_name`` is used to convey any additional authentication methods + in case authentication is for re-scoping. For example, if the + authentication is for re-scoping, plugin must append the previous + method names into ``method_names``. Also, plugin may add any additional + information into ``extras``. Anything in ``extras`` will be conveyed in + the token's ``extras`` attribute. Here's an example of ``auth_context`` + on successful authentication:: + + { + "extras": {}, + "methods": [ + "password", + "token" + ], + "user_id": "abc123" + } + + Plugins are invoked in the order in which they are specified in the + ``methods`` attribute of the ``identity`` object. For example, + ``custom-plugin`` is invoked before ``password``, which is invoked + before ``token`` in the following authentication request:: + + { + "auth": { + "identity": { + "custom-plugin": { + "custom-data": "sdfdfsfsfsdfsf" + }, + "methods": [ + "custom-plugin", + "password", + "token" + ], + "password": { + "user": { + "id": "s23sfad1", + "password": "secrete" + } + }, + "token": { + "id": "sdfafasdfsfasfasdfds" + } + } + } + } + + :returns: None if authentication is successful. + Authentication payload in the form of a dictionary for the + next authentication step if this is a multi step + authentication. + :raises: exception.Unauthorized for authentication failure + """ + raise exception.Unauthorized() diff --git a/keystone-moon/keystone/auth/plugins/__init__.py b/keystone-moon/keystone/auth/plugins/__init__.py new file mode 100644 index 00000000..5da54703 --- /dev/null +++ b/keystone-moon/keystone/auth/plugins/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2015 CERN +# +# 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 keystone.auth.plugins.core import * # noqa diff --git a/keystone-moon/keystone/auth/plugins/core.py b/keystone-moon/keystone/auth/plugins/core.py new file mode 100644 index 00000000..96a5ecf8 --- /dev/null +++ b/keystone-moon/keystone/auth/plugins/core.py @@ -0,0 +1,186 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 sys + +from oslo_config import cfg +from oslo_log import log +import six + +from keystone.common import dependency +from keystone import exception + +CONF = cfg.CONF +LOG = log.getLogger(__name__) + + +def construct_method_map_from_config(): + """Determine authentication method types for deployment. + + :returns: a dictionary containing the methods and their indexes + + """ + method_map = dict() + method_index = 1 + for method in CONF.auth.methods: + method_map[method_index] = method + method_index = method_index * 2 + + return method_map + + +def convert_method_list_to_integer(methods): + """Convert the method type(s) to an integer. + + :param methods: a list of method names + :returns: an integer representing the methods + + """ + method_map = construct_method_map_from_config() + + method_ints = [] + for method in methods: + for k, v in six.iteritems(method_map): + if v == method: + method_ints.append(k) + return sum(method_ints) + + +def convert_integer_to_method_list(method_int): + """Convert an integer to a list of methods. + + :param method_int: an integer representing methods + :returns: a corresponding list of methods + + """ + # If the method_int is 0 then no methods were used so return an empty + # method list + if method_int == 0: + return [] + + method_map = construct_method_map_from_config() + method_ints = [] + for k, v in six.iteritems(method_map): + method_ints.append(k) + method_ints.sort(reverse=True) + + confirmed_methods = [] + for m_int in method_ints: + # (lbragstad): By dividing the method_int by each key in the + # method_map, we know if the division results in an integer of 1, that + # key was used in the construction of the total sum of the method_int. + # In that case, we should confirm the key value and store it so we can + # look it up later. Then we should take the remainder of what is + # confirmed and the method_int and continue the process. In the end, we + # should have a list of integers that correspond to indexes in our + # method_map and we can reinflate the methods that the original + # method_int represents. + if (method_int / m_int) == 1: + confirmed_methods.append(m_int) + method_int = method_int - m_int + + methods = [] + for method in confirmed_methods: + methods.append(method_map[method]) + + return methods + + +@dependency.requires('identity_api', 'resource_api') +class UserAuthInfo(object): + + @staticmethod + def create(auth_payload, method_name): + user_auth_info = UserAuthInfo() + user_auth_info._validate_and_normalize_auth_data(auth_payload) + user_auth_info.METHOD_NAME = method_name + return user_auth_info + + def __init__(self): + self.user_id = None + self.password = None + self.user_ref = None + self.METHOD_NAME = None + + def _assert_domain_is_enabled(self, domain_ref): + try: + self.resource_api.assert_domain_enabled( + domain_id=domain_ref['id'], + domain=domain_ref) + except AssertionError as e: + LOG.warning(six.text_type(e)) + six.reraise(exception.Unauthorized, exception.Unauthorized(e), + sys.exc_info()[2]) + + def _assert_user_is_enabled(self, user_ref): + try: + self.identity_api.assert_user_enabled( + user_id=user_ref['id'], + user=user_ref) + except AssertionError as e: + LOG.warning(six.text_type(e)) + six.reraise(exception.Unauthorized, exception.Unauthorized(e), + sys.exc_info()[2]) + + def _lookup_domain(self, domain_info): + domain_id = domain_info.get('id') + domain_name = domain_info.get('name') + domain_ref = None + if not domain_id and not domain_name: + raise exception.ValidationError(attribute='id or name', + target='domain') + try: + if domain_name: + domain_ref = self.resource_api.get_domain_by_name( + domain_name) + else: + domain_ref = self.resource_api.get_domain(domain_id) + except exception.DomainNotFound as e: + LOG.exception(six.text_type(e)) + raise exception.Unauthorized(e) + self._assert_domain_is_enabled(domain_ref) + return domain_ref + + def _validate_and_normalize_auth_data(self, auth_payload): + if 'user' not in auth_payload: + raise exception.ValidationError(attribute='user', + target=self.METHOD_NAME) + user_info = auth_payload['user'] + user_id = user_info.get('id') + user_name = user_info.get('name') + user_ref = None + if not user_id and not user_name: + raise exception.ValidationError(attribute='id or name', + target='user') + self.password = user_info.get('password') + try: + if user_name: + if 'domain' not in user_info: + raise exception.ValidationError(attribute='domain', + target='user') + domain_ref = self._lookup_domain(user_info['domain']) + user_ref = self.identity_api.get_user_by_name( + user_name, domain_ref['id']) + else: + user_ref = self.identity_api.get_user(user_id) + domain_ref = self.resource_api.get_domain( + user_ref['domain_id']) + self._assert_domain_is_enabled(domain_ref) + except exception.UserNotFound as e: + LOG.exception(six.text_type(e)) + raise exception.Unauthorized(e) + self._assert_user_is_enabled(user_ref) + self.user_ref = user_ref + self.user_id = user_ref['id'] + self.domain_id = domain_ref['id'] diff --git a/keystone-moon/keystone/auth/plugins/external.py b/keystone-moon/keystone/auth/plugins/external.py new file mode 100644 index 00000000..2322649f --- /dev/null +++ b/keystone-moon/keystone/auth/plugins/external.py @@ -0,0 +1,186 @@ +# Copyright 2013 OpenStack Foundation +# +# 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. + +"""Keystone External Authentication Plugins""" + +import abc + +from oslo_config import cfg +import six + +from keystone import auth +from keystone.common import dependency +from keystone import exception +from keystone.i18n import _ +from keystone.openstack.common import versionutils + + +CONF = cfg.CONF + + +@six.add_metaclass(abc.ABCMeta) +class Base(auth.AuthMethodHandler): + + method = 'external' + + def authenticate(self, context, auth_info, auth_context): + """Use REMOTE_USER to look up the user in the identity backend. + + auth_context is an in-out variable that will be updated with the + user_id from the actual user from the REMOTE_USER env variable. + """ + try: + REMOTE_USER = context['environment']['REMOTE_USER'] + except KeyError: + msg = _('No authenticated user') + raise exception.Unauthorized(msg) + try: + user_ref = self._authenticate(REMOTE_USER, context) + auth_context['user_id'] = user_ref['id'] + if ('kerberos' in CONF.token.bind and + (context['environment'].get('AUTH_TYPE', '').lower() + == 'negotiate')): + auth_context['bind']['kerberos'] = user_ref['name'] + except Exception: + msg = _('Unable to lookup user %s') % (REMOTE_USER) + raise exception.Unauthorized(msg) + + @abc.abstractmethod + def _authenticate(self, remote_user, context): + """Look up the user in the identity backend. + + Return user_ref + """ + pass + + +@dependency.requires('identity_api') +class DefaultDomain(Base): + def _authenticate(self, remote_user, context): + """Use remote_user to look up the user in the identity backend.""" + domain_id = CONF.identity.default_domain_id + user_ref = self.identity_api.get_user_by_name(remote_user, domain_id) + return user_ref + + +@dependency.requires('identity_api', 'resource_api') +class Domain(Base): + def _authenticate(self, remote_user, context): + """Use remote_user to look up the user in the identity backend. + + The domain will be extracted from the REMOTE_DOMAIN environment + variable if present. If not, the default domain will be used. + """ + + username = remote_user + try: + domain_name = context['environment']['REMOTE_DOMAIN'] + except KeyError: + domain_id = CONF.identity.default_domain_id + else: + domain_ref = self.resource_api.get_domain_by_name(domain_name) + domain_id = domain_ref['id'] + + user_ref = self.identity_api.get_user_by_name(username, domain_id) + return user_ref + + +@dependency.requires('assignment_api', 'identity_api') +class KerberosDomain(Domain): + """Allows `kerberos` as a method.""" + method = 'kerberos' + + def _authenticate(self, remote_user, context): + auth_type = context['environment'].get('AUTH_TYPE') + if auth_type != 'Negotiate': + raise exception.Unauthorized(_("auth_type is not Negotiate")) + return super(KerberosDomain, self)._authenticate(remote_user, context) + + +class ExternalDefault(DefaultDomain): + """Deprecated. Please use keystone.auth.external.DefaultDomain instead.""" + + @versionutils.deprecated( + as_of=versionutils.deprecated.ICEHOUSE, + in_favor_of='keystone.auth.external.DefaultDomain', + remove_in=+1) + def __init__(self): + super(ExternalDefault, self).__init__() + + +class ExternalDomain(Domain): + """Deprecated. Please use keystone.auth.external.Domain instead.""" + + @versionutils.deprecated( + as_of=versionutils.deprecated.ICEHOUSE, + in_favor_of='keystone.auth.external.Domain', + remove_in=+1) + def __init__(self): + super(ExternalDomain, self).__init__() + + +@dependency.requires('identity_api') +class LegacyDefaultDomain(Base): + """Deprecated. Please use keystone.auth.external.DefaultDomain instead. + + This plugin exists to provide compatibility for the unintended behavior + described here: https://bugs.launchpad.net/keystone/+bug/1253484 + + """ + + @versionutils.deprecated( + as_of=versionutils.deprecated.ICEHOUSE, + in_favor_of='keystone.auth.external.DefaultDomain', + remove_in=+1) + def __init__(self): + super(LegacyDefaultDomain, self).__init__() + + def _authenticate(self, remote_user, context): + """Use remote_user to look up the user in the identity backend.""" + # NOTE(dolph): this unintentionally discards half the REMOTE_USER value + names = remote_user.split('@') + username = names.pop(0) + domain_id = CONF.identity.default_domain_id + user_ref = self.identity_api.get_user_by_name(username, domain_id) + return user_ref + + +@dependency.requires('identity_api', 'resource_api') +class LegacyDomain(Base): + """Deprecated. Please use keystone.auth.external.Domain instead.""" + + @versionutils.deprecated( + as_of=versionutils.deprecated.ICEHOUSE, + in_favor_of='keystone.auth.external.Domain', + remove_in=+1) + def __init__(self): + super(LegacyDomain, self).__init__() + + def _authenticate(self, remote_user, context): + """Use remote_user to look up the user in the identity backend. + + If remote_user contains an `@` assume that the substring before the + rightmost `@` is the username, and the substring after the @ is the + domain name. + """ + names = remote_user.rsplit('@', 1) + username = names.pop(0) + if names: + domain_name = names[0] + domain_ref = self.resource_api.get_domain_by_name(domain_name) + domain_id = domain_ref['id'] + else: + domain_id = CONF.identity.default_domain_id + user_ref = self.identity_api.get_user_by_name(username, domain_id) + return user_ref diff --git a/keystone-moon/keystone/auth/plugins/mapped.py b/keystone-moon/keystone/auth/plugins/mapped.py new file mode 100644 index 00000000..abf44481 --- /dev/null +++ b/keystone-moon/keystone/auth/plugins/mapped.py @@ -0,0 +1,252 @@ +# 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 functools + +from oslo_log import log +from oslo_serialization import jsonutils +from pycadf import cadftaxonomy as taxonomy +from six.moves.urllib import parse + +from keystone import auth +from keystone.auth import plugins as auth_plugins +from keystone.common import dependency +from keystone.contrib import federation +from keystone.contrib.federation import utils +from keystone import exception +from keystone.i18n import _ +from keystone.models import token_model +from keystone import notifications + + +LOG = log.getLogger(__name__) + +METHOD_NAME = 'mapped' + + +@dependency.requires('assignment_api', 'federation_api', 'identity_api', + 'token_provider_api') +class Mapped(auth.AuthMethodHandler): + + def _get_token_ref(self, auth_payload): + token_id = auth_payload['id'] + response = self.token_provider_api.validate_token(token_id) + return token_model.KeystoneToken(token_id=token_id, + token_data=response) + + def authenticate(self, context, auth_payload, auth_context): + """Authenticate mapped user and return an authentication context. + + :param context: keystone's request context + :param auth_payload: the content of the authentication for a + given method + :param auth_context: user authentication context, a dictionary + shared by all plugins. + + In addition to ``user_id`` in ``auth_context``, this plugin sets + ``group_ids``, ``OS-FEDERATION:identity_provider`` and + ``OS-FEDERATION:protocol`` + + """ + + if 'id' in auth_payload: + token_ref = self._get_token_ref(auth_payload) + handle_scoped_token(context, auth_payload, auth_context, token_ref, + self.federation_api, + self.identity_api, + self.token_provider_api) + else: + handle_unscoped_token(context, auth_payload, auth_context, + self.assignment_api, self.federation_api, + self.identity_api) + + +def handle_scoped_token(context, auth_payload, auth_context, token_ref, + federation_api, identity_api, token_provider_api): + utils.validate_expiration(token_ref) + token_audit_id = token_ref.audit_id + identity_provider = token_ref.federation_idp_id + protocol = token_ref.federation_protocol_id + user_id = token_ref.user_id + group_ids = token_ref.federation_group_ids + send_notification = functools.partial( + notifications.send_saml_audit_notification, 'authenticate', + context, user_id, group_ids, identity_provider, protocol, + token_audit_id) + + utils.assert_enabled_identity_provider(federation_api, identity_provider) + + try: + mapping = federation_api.get_mapping_from_idp_and_protocol( + identity_provider, protocol) + utils.validate_groups(group_ids, mapping['id'], identity_api) + + except Exception: + # NOTE(topol): Diaper defense to catch any exception, so we can + # send off failed authentication notification, raise the exception + # after sending the notification + send_notification(taxonomy.OUTCOME_FAILURE) + raise + else: + send_notification(taxonomy.OUTCOME_SUCCESS) + + auth_context['user_id'] = user_id + auth_context['group_ids'] = group_ids + auth_context[federation.IDENTITY_PROVIDER] = identity_provider + auth_context[federation.PROTOCOL] = protocol + + +def handle_unscoped_token(context, auth_payload, auth_context, + assignment_api, federation_api, identity_api): + + def is_ephemeral_user(mapped_properties): + return mapped_properties['user']['type'] == utils.UserType.EPHEMERAL + + def build_ephemeral_user_context(auth_context, user, mapped_properties, + identity_provider, protocol): + auth_context['user_id'] = user['id'] + auth_context['group_ids'] = mapped_properties['group_ids'] + auth_context[federation.IDENTITY_PROVIDER] = identity_provider + auth_context[federation.PROTOCOL] = protocol + + def build_local_user_context(auth_context, mapped_properties): + user_info = auth_plugins.UserAuthInfo.create(mapped_properties, + METHOD_NAME) + auth_context['user_id'] = user_info.user_id + + assertion = extract_assertion_data(context) + identity_provider = auth_payload['identity_provider'] + protocol = auth_payload['protocol'] + + utils.assert_enabled_identity_provider(federation_api, identity_provider) + + group_ids = None + # NOTE(topol): The user is coming in from an IdP with a SAML assertion + # instead of from a token, so we set token_id to None + token_id = None + # NOTE(marek-denis): This variable is set to None and there is a + # possibility that it will be used in the CADF notification. This means + # operation will not be mapped to any user (even ephemeral). + user_id = None + + try: + mapped_properties = apply_mapping_filter( + identity_provider, protocol, assertion, assignment_api, + federation_api, identity_api) + + if is_ephemeral_user(mapped_properties): + user = setup_username(context, mapped_properties) + user_id = user['id'] + group_ids = mapped_properties['group_ids'] + mapping = federation_api.get_mapping_from_idp_and_protocol( + identity_provider, protocol) + utils.validate_groups_cardinality(group_ids, mapping['id']) + build_ephemeral_user_context(auth_context, user, + mapped_properties, + identity_provider, protocol) + else: + build_local_user_context(auth_context, mapped_properties) + + except Exception: + # NOTE(topol): Diaper defense to catch any exception, so we can + # send off failed authentication notification, raise the exception + # after sending the notification + outcome = taxonomy.OUTCOME_FAILURE + notifications.send_saml_audit_notification('authenticate', context, + user_id, group_ids, + identity_provider, + protocol, token_id, + outcome) + raise + else: + outcome = taxonomy.OUTCOME_SUCCESS + notifications.send_saml_audit_notification('authenticate', context, + user_id, group_ids, + identity_provider, + protocol, token_id, + outcome) + + +def extract_assertion_data(context): + assertion = dict(utils.get_assertion_params_from_env(context)) + return assertion + + +def apply_mapping_filter(identity_provider, protocol, assertion, + assignment_api, federation_api, identity_api): + idp = federation_api.get_idp(identity_provider) + utils.validate_idp(idp, assertion) + mapping = federation_api.get_mapping_from_idp_and_protocol( + identity_provider, protocol) + rules = jsonutils.loads(mapping['rules']) + LOG.debug('using the following rules: %s', rules) + rule_processor = utils.RuleProcessor(rules) + mapped_properties = rule_processor.process(assertion) + + # NOTE(marek-denis): We update group_ids only here to avoid fetching + # groups identified by name/domain twice. + # NOTE(marek-denis): Groups are translated from name/domain to their + # corresponding ids in the auth plugin, as we need information what + # ``mapping_id`` was used as well as idenity_api and assignment_api + # objects. + group_ids = mapped_properties['group_ids'] + utils.validate_groups_in_backend(group_ids, + mapping['id'], + identity_api) + group_ids.extend( + utils.transform_to_group_ids( + mapped_properties['group_names'], mapping['id'], + identity_api, assignment_api)) + mapped_properties['group_ids'] = list(set(group_ids)) + return mapped_properties + + +def setup_username(context, mapped_properties): + """Setup federated username. + + Function covers all the cases for properly setting user id, a primary + identifier for identity objects. Initial version of the mapping engine + assumed user is identified by ``name`` and his ``id`` is built from the + name. We, however need to be able to accept local rules that identify user + by either id or name/domain. + + The following use-cases are covered: + + 1) If neither user_name nor user_id is set raise exception.Unauthorized + 2) If user_id is set and user_name not, set user_name equal to user_id + 3) If user_id is not set and user_name is, set user_id as url safe version + of user_name. + + :param context: authentication context + :param mapped_properties: Properties issued by a RuleProcessor. + :type: dictionary + + :raises: exception.Unauthorized + :returns: dictionary with user identification + :rtype: dict + + """ + user = mapped_properties['user'] + + user_id = user.get('id') + user_name = user.get('name') or context['environment'].get('REMOTE_USER') + + if not any([user_id, user_name]): + raise exception.Unauthorized(_("Could not map user")) + + elif not user_name: + user['name'] = user_id + + elif not user_id: + user['id'] = parse.quote(user_name) + + return user diff --git a/keystone-moon/keystone/auth/plugins/oauth1.py b/keystone-moon/keystone/auth/plugins/oauth1.py new file mode 100644 index 00000000..2f1cc2fa --- /dev/null +++ b/keystone-moon/keystone/auth/plugins/oauth1.py @@ -0,0 +1,75 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 oslo_log import log +from oslo_utils import timeutils + +from keystone import auth +from keystone.common import controller +from keystone.common import dependency +from keystone.contrib.oauth1 import core as oauth +from keystone.contrib.oauth1 import validator +from keystone import exception +from keystone.i18n import _ + + +LOG = log.getLogger(__name__) + + +@dependency.requires('oauth_api') +class OAuth(auth.AuthMethodHandler): + + method = 'oauth1' + + def authenticate(self, context, auth_info, auth_context): + """Turn a signed request with an access key into a keystone token.""" + + if not self.oauth_api: + raise exception.Unauthorized(_('%s not supported') % self.method) + + headers = context['headers'] + oauth_headers = oauth.get_oauth_headers(headers) + access_token_id = oauth_headers.get('oauth_token') + + if not access_token_id: + raise exception.ValidationError( + attribute='oauth_token', target='request') + + acc_token = self.oauth_api.get_access_token(access_token_id) + + expires_at = acc_token['expires_at'] + if expires_at: + now = timeutils.utcnow() + expires = timeutils.normalize_time( + timeutils.parse_isotime(expires_at)) + if now > expires: + raise exception.Unauthorized(_('Access token is expired')) + + url = controller.V3Controller.base_url(context, context['path']) + access_verifier = oauth.ResourceEndpoint( + request_validator=validator.OAuthValidator(), + token_generator=oauth.token_generator) + result, request = access_verifier.validate_protected_resource_request( + url, + http_method='POST', + body=context['query_string'], + headers=headers, + realms=None + ) + if not result: + msg = _('Could not validate the access token') + raise exception.Unauthorized(msg) + auth_context['user_id'] = acc_token['authorizing_user_id'] + auth_context['access_token_id'] = access_token_id + auth_context['project_id'] = acc_token['project_id'] diff --git a/keystone-moon/keystone/auth/plugins/password.py b/keystone-moon/keystone/auth/plugins/password.py new file mode 100644 index 00000000..c5770445 --- /dev/null +++ b/keystone-moon/keystone/auth/plugins/password.py @@ -0,0 +1,49 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 oslo_log import log + +from keystone import auth +from keystone.auth import plugins as auth_plugins +from keystone.common import dependency +from keystone import exception +from keystone.i18n import _ + +METHOD_NAME = 'password' + +LOG = log.getLogger(__name__) + + +@dependency.requires('identity_api') +class Password(auth.AuthMethodHandler): + + method = METHOD_NAME + + def authenticate(self, context, auth_payload, auth_context): + """Try to authenticate against the identity backend.""" + user_info = auth_plugins.UserAuthInfo.create(auth_payload, self.method) + + # FIXME(gyee): identity.authenticate() can use some refactoring since + # all we care is password matches + try: + self.identity_api.authenticate( + context, + user_id=user_info.user_id, + password=user_info.password) + except AssertionError: + # authentication failed because of invalid username or password + msg = _('Invalid username or password') + raise exception.Unauthorized(msg) + + auth_context['user_id'] = user_info.user_id diff --git a/keystone-moon/keystone/auth/plugins/saml2.py b/keystone-moon/keystone/auth/plugins/saml2.py new file mode 100644 index 00000000..744f26a9 --- /dev/null +++ b/keystone-moon/keystone/auth/plugins/saml2.py @@ -0,0 +1,27 @@ +# 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 keystone.auth.plugins import mapped + +""" Provide an entry point to authenticate with SAML2 + +This plugin subclasses mapped.Mapped, and may be specified in keystone.conf: + + [auth] + methods = external,password,token,saml2 + saml2 = keystone.auth.plugins.mapped.Mapped +""" + + +class Saml2(mapped.Mapped): + + method = 'saml2' diff --git a/keystone-moon/keystone/auth/plugins/token.py b/keystone-moon/keystone/auth/plugins/token.py new file mode 100644 index 00000000..5ca0b257 --- /dev/null +++ b/keystone-moon/keystone/auth/plugins/token.py @@ -0,0 +1,99 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 oslo_config import cfg +from oslo_log import log +import six + +from keystone import auth +from keystone.auth.plugins import mapped +from keystone.common import dependency +from keystone.common import wsgi +from keystone import exception +from keystone.i18n import _ +from keystone.models import token_model + + +LOG = log.getLogger(__name__) + +CONF = cfg.CONF + + +@dependency.requires('federation_api', 'identity_api', 'token_provider_api') +class Token(auth.AuthMethodHandler): + + method = 'token' + + def _get_token_ref(self, auth_payload): + token_id = auth_payload['id'] + response = self.token_provider_api.validate_token(token_id) + return token_model.KeystoneToken(token_id=token_id, + token_data=response) + + def authenticate(self, context, auth_payload, user_context): + if 'id' not in auth_payload: + raise exception.ValidationError(attribute='id', + target=self.method) + token_ref = self._get_token_ref(auth_payload) + if token_ref.is_federated_user and self.federation_api: + mapped.handle_scoped_token( + context, auth_payload, user_context, token_ref, + self.federation_api, self.identity_api, + self.token_provider_api) + else: + token_authenticate(context, auth_payload, user_context, token_ref) + + +def token_authenticate(context, auth_payload, user_context, token_ref): + try: + + # Do not allow tokens used for delegation to + # create another token, or perform any changes of + # state in Keystone. To do so is to invite elevation of + # privilege attacks + + if token_ref.oauth_scoped or token_ref.trust_scoped: + raise exception.Forbidden() + + if not CONF.token.allow_rescope_scoped_token: + # Do not allow conversion from scoped tokens. + if token_ref.project_scoped or token_ref.domain_scoped: + raise exception.Forbidden(action=_("rescope a scoped token")) + + wsgi.validate_token_bind(context, token_ref) + + # New tokens maintain the audit_id of the original token in the + # chain (if possible) as the second element in the audit data + # structure. Look for the last element in the audit data structure + # which will be either the audit_id of the token (in the case of + # a token that has not been rescoped) or the audit_chain id (in + # the case of a token that has been rescoped). + try: + token_audit_id = token_ref.get('audit_ids', [])[-1] + except IndexError: + # NOTE(morganfainberg): In the case this is a token that was + # issued prior to audit id existing, the chain is not tracked. + token_audit_id = None + + user_context.setdefault('expires_at', token_ref.expires) + user_context['audit_id'] = token_audit_id + user_context.setdefault('user_id', token_ref.user_id) + # TODO(morganfainberg: determine if token 'extras' can be removed + # from the user_context + user_context['extras'].update(token_ref.get('extras', {})) + user_context['method_names'].extend(token_ref.methods) + + except AssertionError as e: + LOG.error(six.text_type(e)) + raise exception.Unauthorized(e) diff --git a/keystone-moon/keystone/auth/routers.py b/keystone-moon/keystone/auth/routers.py new file mode 100644 index 00000000..c7a525c3 --- /dev/null +++ b/keystone-moon/keystone/auth/routers.py @@ -0,0 +1,57 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 keystone.auth import controllers +from keystone.common import json_home +from keystone.common import wsgi + + +class Routers(wsgi.RoutersBase): + + def append_v3_routers(self, mapper, routers): + auth_controller = controllers.Auth() + + self._add_resource( + mapper, auth_controller, + path='/auth/tokens', + get_action='validate_token', + head_action='check_token', + post_action='authenticate_for_token', + delete_action='revoke_token', + rel=json_home.build_v3_resource_relation('auth_tokens')) + + self._add_resource( + mapper, auth_controller, + path='/auth/tokens/OS-PKI/revoked', + get_action='revocation_list', + rel=json_home.build_v3_extension_resource_relation( + 'OS-PKI', '1.0', 'revocations')) + + self._add_resource( + mapper, auth_controller, + path='/auth/catalog', + get_action='get_auth_catalog', + rel=json_home.build_v3_resource_relation('auth_catalog')) + + self._add_resource( + mapper, auth_controller, + path='/auth/projects', + get_action='get_auth_projects', + rel=json_home.build_v3_resource_relation('auth_projects')) + + self._add_resource( + mapper, auth_controller, + path='/auth/domains', + get_action='get_auth_domains', + rel=json_home.build_v3_resource_relation('auth_domains')) diff --git a/keystone-moon/keystone/backends.py b/keystone-moon/keystone/backends.py new file mode 100644 index 00000000..3a10675e --- /dev/null +++ b/keystone-moon/keystone/backends.py @@ -0,0 +1,66 @@ +# 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 keystone import assignment +from keystone import auth +from keystone import catalog +from keystone.common import cache +from keystone.contrib import endpoint_filter +from keystone.contrib import endpoint_policy +from keystone.contrib import federation +from keystone.contrib import oauth1 +from keystone.contrib import revoke +from keystone import credential +from keystone import identity +from keystone import policy +from keystone import resource +from keystone import token +from keystone import trust +# from keystone.contrib import moon + + +def load_backends(): + + # Configure and build the cache + cache.configure_cache_region(cache.REGION) + + # Ensure that the identity driver is created before the assignment manager. + # The default assignment driver is determined by the identity driver, so + # the identity driver must be available to the assignment manager. + _IDENTITY_API = identity.Manager() + + DRIVERS = dict( + assignment_api=assignment.Manager(), + catalog_api=catalog.Manager(), + credential_api=credential.Manager(), + domain_config_api=resource.DomainConfigManager(), + endpoint_filter_api=endpoint_filter.Manager(), + endpoint_policy_api=endpoint_policy.Manager(), + federation_api=federation.Manager(), + id_generator_api=identity.generator.Manager(), + id_mapping_api=identity.MappingManager(), + identity_api=_IDENTITY_API, + oauth_api=oauth1.Manager(), + policy_api=policy.Manager(), + resource_api=resource.Manager(), + revoke_api=revoke.Manager(), + role_api=assignment.RoleManager(), + token_api=token.persistence.Manager(), + trust_api=trust.Manager(), + token_provider_api=token.provider.Manager(), + # admin_api=moon.AdminManager(), + # authz_api=moon.AuthzManager() + ) + + auth.controllers.load_auth_methods() + + return DRIVERS diff --git a/keystone-moon/keystone/catalog/__init__.py b/keystone-moon/keystone/catalog/__init__.py new file mode 100644 index 00000000..8d4d1567 --- /dev/null +++ b/keystone-moon/keystone/catalog/__init__.py @@ -0,0 +1,17 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 keystone.catalog import controllers # noqa +from keystone.catalog.core import * # noqa +from keystone.catalog import routers # noqa diff --git a/keystone-moon/keystone/catalog/backends/__init__.py b/keystone-moon/keystone/catalog/backends/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/catalog/backends/kvs.py b/keystone-moon/keystone/catalog/backends/kvs.py new file mode 100644 index 00000000..30a121d8 --- /dev/null +++ b/keystone-moon/keystone/catalog/backends/kvs.py @@ -0,0 +1,154 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 keystone import catalog +from keystone.common import driver_hints +from keystone.common import kvs + + +class Catalog(kvs.Base, catalog.Driver): + # Public interface + def get_catalog(self, user_id, tenant_id): + return self.db.get('catalog-%s-%s' % (tenant_id, user_id)) + + # region crud + + def _delete_child_regions(self, region_id, root_region_id): + """Delete all child regions. + + Recursively delete any region that has the supplied region + as its parent. + """ + children = [r for r in self.list_regions(driver_hints.Hints()) + if r['parent_region_id'] == region_id] + for child in children: + if child['id'] == root_region_id: + # Hit a circular region hierarchy + return + self._delete_child_regions(child['id'], root_region_id) + self._delete_region(child['id']) + + def _check_parent_region(self, region_ref): + """Raise a NotFound if the parent region does not exist. + + If the region_ref has a specified parent_region_id, check that + the parent exists, otherwise, raise a NotFound. + """ + parent_region_id = region_ref.get('parent_region_id') + if parent_region_id is not None: + # This will raise NotFound if the parent doesn't exist, + # which is the behavior we want. + self.get_region(parent_region_id) + + def create_region(self, region): + region_id = region['id'] + region.setdefault('parent_region_id') + self._check_parent_region(region) + self.db.set('region-%s' % region_id, region) + region_list = set(self.db.get('region_list', [])) + region_list.add(region_id) + self.db.set('region_list', list(region_list)) + return region + + def list_regions(self, hints): + return [self.get_region(x) for x in self.db.get('region_list', [])] + + def get_region(self, region_id): + return self.db.get('region-%s' % region_id) + + def update_region(self, region_id, region): + self._check_parent_region(region) + old_region = self.get_region(region_id) + old_region.update(region) + self._ensure_no_circle_in_hierarchical_regions(old_region) + self.db.set('region-%s' % region_id, old_region) + return old_region + + def _delete_region(self, region_id): + self.db.delete('region-%s' % region_id) + region_list = set(self.db.get('region_list', [])) + region_list.remove(region_id) + self.db.set('region_list', list(region_list)) + + def delete_region(self, region_id): + self._delete_child_regions(region_id, region_id) + self._delete_region(region_id) + + # service crud + + def create_service(self, service_id, service): + self.db.set('service-%s' % service_id, service) + service_list = set(self.db.get('service_list', [])) + service_list.add(service_id) + self.db.set('service_list', list(service_list)) + return service + + def list_services(self, hints): + return [self.get_service(x) for x in self.db.get('service_list', [])] + + def get_service(self, service_id): + return self.db.get('service-%s' % service_id) + + def update_service(self, service_id, service): + old_service = self.get_service(service_id) + old_service.update(service) + self.db.set('service-%s' % service_id, old_service) + return old_service + + def delete_service(self, service_id): + # delete referencing endpoints + for endpoint_id in self.db.get('endpoint_list', []): + if self.get_endpoint(endpoint_id)['service_id'] == service_id: + self.delete_endpoint(endpoint_id) + + self.db.delete('service-%s' % service_id) + service_list = set(self.db.get('service_list', [])) + service_list.remove(service_id) + self.db.set('service_list', list(service_list)) + + # endpoint crud + + def create_endpoint(self, endpoint_id, endpoint): + self.db.set('endpoint-%s' % endpoint_id, endpoint) + endpoint_list = set(self.db.get('endpoint_list', [])) + endpoint_list.add(endpoint_id) + self.db.set('endpoint_list', list(endpoint_list)) + return endpoint + + def list_endpoints(self, hints): + return [self.get_endpoint(x) for x in self.db.get('endpoint_list', [])] + + def get_endpoint(self, endpoint_id): + return self.db.get('endpoint-%s' % endpoint_id) + + def update_endpoint(self, endpoint_id, endpoint): + if endpoint.get('region_id') is not None: + self.get_region(endpoint['region_id']) + + old_endpoint = self.get_endpoint(endpoint_id) + old_endpoint.update(endpoint) + self.db.set('endpoint-%s' % endpoint_id, old_endpoint) + return old_endpoint + + def delete_endpoint(self, endpoint_id): + self.db.delete('endpoint-%s' % endpoint_id) + endpoint_list = set(self.db.get('endpoint_list', [])) + endpoint_list.remove(endpoint_id) + self.db.set('endpoint_list', list(endpoint_list)) + + # Private interface + def _create_catalog(self, user_id, tenant_id, data): + self.db.set('catalog-%s-%s' % (tenant_id, user_id), data) + return data diff --git a/keystone-moon/keystone/catalog/backends/sql.py b/keystone-moon/keystone/catalog/backends/sql.py new file mode 100644 index 00000000..8ab82305 --- /dev/null +++ b/keystone-moon/keystone/catalog/backends/sql.py @@ -0,0 +1,337 @@ +# Copyright 2012 OpenStack Foundation +# Copyright 2012 Canonical Ltd. +# +# 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 itertools + +from oslo_config import cfg +import six +import sqlalchemy +from sqlalchemy.sql import true + +from keystone import catalog +from keystone.catalog import core +from keystone.common import sql +from keystone import exception + + +CONF = cfg.CONF + + +class Region(sql.ModelBase, sql.DictBase): + __tablename__ = 'region' + attributes = ['id', 'description', 'parent_region_id'] + id = sql.Column(sql.String(255), primary_key=True) + description = sql.Column(sql.String(255), nullable=False) + # NOTE(jaypipes): Right now, using an adjacency list model for + # storing the hierarchy of regions is fine, since + # the API does not support any kind of querying for + # more complex hierarchical queries such as "get me only + # the regions that are subchildren of this region", etc. + # If, in the future, such queries are needed, then it + # would be possible to add in columns to this model for + # "left" and "right" and provide support for a nested set + # model. + parent_region_id = sql.Column(sql.String(255), nullable=True) + + # TODO(jaypipes): I think it's absolutely stupid that every single model + # is required to have an "extra" column because of the + # DictBase in the keystone.common.sql.core module. Forcing + # tables to have pointless columns in the database is just + # bad. Remove all of this extra JSON blob stuff. + # See: https://bugs.launchpad.net/keystone/+bug/1265071 + extra = sql.Column(sql.JsonBlob()) + endpoints = sqlalchemy.orm.relationship("Endpoint", backref="region") + + +class Service(sql.ModelBase, sql.DictBase): + __tablename__ = 'service' + attributes = ['id', 'type', 'enabled'] + id = sql.Column(sql.String(64), primary_key=True) + type = sql.Column(sql.String(255)) + enabled = sql.Column(sql.Boolean, nullable=False, default=True, + server_default=sqlalchemy.sql.expression.true()) + extra = sql.Column(sql.JsonBlob()) + endpoints = sqlalchemy.orm.relationship("Endpoint", backref="service") + + +class Endpoint(sql.ModelBase, sql.DictBase): + __tablename__ = 'endpoint' + attributes = ['id', 'interface', 'region_id', 'service_id', 'url', + 'legacy_endpoint_id', 'enabled'] + id = sql.Column(sql.String(64), primary_key=True) + legacy_endpoint_id = sql.Column(sql.String(64)) + interface = sql.Column(sql.String(8), nullable=False) + region_id = sql.Column(sql.String(255), + sql.ForeignKey('region.id', + ondelete='RESTRICT'), + nullable=True, + default=None) + service_id = sql.Column(sql.String(64), + sql.ForeignKey('service.id'), + nullable=False) + url = sql.Column(sql.Text(), nullable=False) + enabled = sql.Column(sql.Boolean, nullable=False, default=True, + server_default=sqlalchemy.sql.expression.true()) + extra = sql.Column(sql.JsonBlob()) + + +class Catalog(catalog.Driver): + # Regions + def list_regions(self, hints): + session = sql.get_session() + regions = session.query(Region) + regions = sql.filter_limit_query(Region, regions, hints) + return [s.to_dict() for s in list(regions)] + + def _get_region(self, session, region_id): + ref = session.query(Region).get(region_id) + if not ref: + raise exception.RegionNotFound(region_id=region_id) + return ref + + def _delete_child_regions(self, session, region_id, root_region_id): + """Delete all child regions. + + Recursively delete any region that has the supplied region + as its parent. + """ + children = session.query(Region).filter_by(parent_region_id=region_id) + for child in children: + if child.id == root_region_id: + # Hit a circular region hierarchy + return + self._delete_child_regions(session, child.id, root_region_id) + session.delete(child) + + def _check_parent_region(self, session, region_ref): + """Raise a NotFound if the parent region does not exist. + + If the region_ref has a specified parent_region_id, check that + the parent exists, otherwise, raise a NotFound. + """ + parent_region_id = region_ref.get('parent_region_id') + if parent_region_id is not None: + # This will raise NotFound if the parent doesn't exist, + # which is the behavior we want. + self._get_region(session, parent_region_id) + + def _has_endpoints(self, session, region, root_region): + if region.endpoints is not None and len(region.endpoints) > 0: + return True + + q = session.query(Region) + q = q.filter_by(parent_region_id=region.id) + for child in q.all(): + if child.id == root_region.id: + # Hit a circular region hierarchy + return False + if self._has_endpoints(session, child, root_region): + return True + return False + + def get_region(self, region_id): + session = sql.get_session() + return self._get_region(session, region_id).to_dict() + + def delete_region(self, region_id): + session = sql.get_session() + with session.begin(): + ref = self._get_region(session, region_id) + if self._has_endpoints(session, ref, ref): + raise exception.RegionDeletionError(region_id=region_id) + self._delete_child_regions(session, region_id, region_id) + session.delete(ref) + + @sql.handle_conflicts(conflict_type='region') + def create_region(self, region_ref): + session = sql.get_session() + with session.begin(): + self._check_parent_region(session, region_ref) + region = Region.from_dict(region_ref) + session.add(region) + return region.to_dict() + + def update_region(self, region_id, region_ref): + session = sql.get_session() + with session.begin(): + self._check_parent_region(session, region_ref) + ref = self._get_region(session, region_id) + old_dict = ref.to_dict() + old_dict.update(region_ref) + self._ensure_no_circle_in_hierarchical_regions(old_dict) + new_region = Region.from_dict(old_dict) + for attr in Region.attributes: + if attr != 'id': + setattr(ref, attr, getattr(new_region, attr)) + return ref.to_dict() + + # Services + @sql.truncated + def list_services(self, hints): + session = sql.get_session() + services = session.query(Service) + services = sql.filter_limit_query(Service, services, hints) + return [s.to_dict() for s in list(services)] + + def _get_service(self, session, service_id): + ref = session.query(Service).get(service_id) + if not ref: + raise exception.ServiceNotFound(service_id=service_id) + return ref + + def get_service(self, service_id): + session = sql.get_session() + return self._get_service(session, service_id).to_dict() + + def delete_service(self, service_id): + session = sql.get_session() + with session.begin(): + ref = self._get_service(session, service_id) + session.query(Endpoint).filter_by(service_id=service_id).delete() + session.delete(ref) + + def create_service(self, service_id, service_ref): + session = sql.get_session() + with session.begin(): + service = Service.from_dict(service_ref) + session.add(service) + return service.to_dict() + + def update_service(self, service_id, service_ref): + session = sql.get_session() + with session.begin(): + ref = self._get_service(session, service_id) + old_dict = ref.to_dict() + old_dict.update(service_ref) + new_service = Service.from_dict(old_dict) + for attr in Service.attributes: + if attr != 'id': + setattr(ref, attr, getattr(new_service, attr)) + ref.extra = new_service.extra + return ref.to_dict() + + # Endpoints + def create_endpoint(self, endpoint_id, endpoint_ref): + session = sql.get_session() + new_endpoint = Endpoint.from_dict(endpoint_ref) + + with session.begin(): + session.add(new_endpoint) + return new_endpoint.to_dict() + + def delete_endpoint(self, endpoint_id): + session = sql.get_session() + with session.begin(): + ref = self._get_endpoint(session, endpoint_id) + session.delete(ref) + + def _get_endpoint(self, session, endpoint_id): + try: + return session.query(Endpoint).filter_by(id=endpoint_id).one() + except sql.NotFound: + raise exception.EndpointNotFound(endpoint_id=endpoint_id) + + def get_endpoint(self, endpoint_id): + session = sql.get_session() + return self._get_endpoint(session, endpoint_id).to_dict() + + @sql.truncated + def list_endpoints(self, hints): + session = sql.get_session() + endpoints = session.query(Endpoint) + endpoints = sql.filter_limit_query(Endpoint, endpoints, hints) + return [e.to_dict() for e in list(endpoints)] + + def update_endpoint(self, endpoint_id, endpoint_ref): + session = sql.get_session() + + with session.begin(): + ref = self._get_endpoint(session, endpoint_id) + old_dict = ref.to_dict() + old_dict.update(endpoint_ref) + new_endpoint = Endpoint.from_dict(old_dict) + for attr in Endpoint.attributes: + if attr != 'id': + setattr(ref, attr, getattr(new_endpoint, attr)) + ref.extra = new_endpoint.extra + return ref.to_dict() + + def get_catalog(self, user_id, tenant_id): + substitutions = dict( + itertools.chain(six.iteritems(CONF), + six.iteritems(CONF.eventlet_server))) + substitutions.update({'tenant_id': tenant_id, 'user_id': user_id}) + + session = sql.get_session() + endpoints = (session.query(Endpoint). + options(sql.joinedload(Endpoint.service)). + filter(Endpoint.enabled == true()).all()) + + catalog = {} + + for endpoint in endpoints: + if not endpoint.service['enabled']: + continue + try: + url = core.format_url(endpoint['url'], substitutions) + except exception.MalformedEndpoint: + continue # this failure is already logged in format_url() + + region = endpoint['region_id'] + service_type = endpoint.service['type'] + default_service = { + 'id': endpoint['id'], + 'name': endpoint.service.extra.get('name', ''), + 'publicURL': '' + } + catalog.setdefault(region, {}) + catalog[region].setdefault(service_type, default_service) + interface_url = '%sURL' % endpoint['interface'] + catalog[region][service_type][interface_url] = url + + return catalog + + def get_v3_catalog(self, user_id, tenant_id): + d = dict( + itertools.chain(six.iteritems(CONF), + six.iteritems(CONF.eventlet_server))) + d.update({'tenant_id': tenant_id, + 'user_id': user_id}) + + session = sql.get_session() + services = (session.query(Service).filter(Service.enabled == true()). + options(sql.joinedload(Service.endpoints)). + all()) + + def make_v3_endpoints(endpoints): + for endpoint in (ep.to_dict() for ep in endpoints if ep.enabled): + del endpoint['service_id'] + del endpoint['legacy_endpoint_id'] + del endpoint['enabled'] + endpoint['region'] = endpoint['region_id'] + try: + endpoint['url'] = core.format_url(endpoint['url'], d) + except exception.MalformedEndpoint: + continue # this failure is already logged in format_url() + + yield endpoint + + def make_v3_service(svc): + eps = list(make_v3_endpoints(svc.endpoints)) + service = {'endpoints': eps, 'id': svc.id, 'type': svc.type} + service['name'] = svc.extra.get('name', '') + return service + + return [make_v3_service(svc) for svc in services] diff --git a/keystone-moon/keystone/catalog/backends/templated.py b/keystone-moon/keystone/catalog/backends/templated.py new file mode 100644 index 00000000..d3ee105d --- /dev/null +++ b/keystone-moon/keystone/catalog/backends/templated.py @@ -0,0 +1,127 @@ +# Copyright 2012 OpenStack Foundationc +# +# 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 itertools +import os.path + +from oslo_config import cfg +from oslo_log import log +import six + +from keystone.catalog.backends import kvs +from keystone.catalog import core +from keystone import exception +from keystone.i18n import _LC + + +LOG = log.getLogger(__name__) + +CONF = cfg.CONF + + +def parse_templates(template_lines): + o = {} + for line in template_lines: + if ' = ' not in line: + continue + + k, v = line.strip().split(' = ') + if not k.startswith('catalog.'): + continue + + parts = k.split('.') + + region = parts[1] + # NOTE(termie): object-store insists on having a dash + service = parts[2].replace('_', '-') + key = parts[3] + + region_ref = o.get(region, {}) + service_ref = region_ref.get(service, {}) + service_ref[key] = v + + region_ref[service] = service_ref + o[region] = region_ref + + return o + + +class Catalog(kvs.Catalog): + """A backend that generates endpoints for the Catalog based on templates. + + It is usually configured via config entries that look like: + + catalog.$REGION.$SERVICE.$key = $value + + and is stored in a similar looking hierarchy. Where a value can contain + values to be interpolated by standard python string interpolation that look + like (the % is replaced by a $ due to paste attempting to interpolate on + its own: + + http://localhost:$(public_port)s/ + + When expanding the template it will pass in a dict made up of the conf + instance plus a few additional key-values, notably tenant_id and user_id. + + It does not care what the keys and values are but it is worth noting that + keystone_compat will expect certain keys to be there so that it can munge + them into the output format keystone expects. These keys are: + + name - the name of the service, most likely repeated for all services of + the same type, across regions. + + adminURL - the url of the admin endpoint + + publicURL - the url of the public endpoint + + internalURL - the url of the internal endpoint + + """ + + def __init__(self, templates=None): + super(Catalog, self).__init__() + if templates: + self.templates = templates + else: + template_file = CONF.catalog.template_file + if not os.path.exists(template_file): + template_file = CONF.find_file(template_file) + self._load_templates(template_file) + + def _load_templates(self, template_file): + try: + self.templates = parse_templates(open(template_file)) + except IOError: + LOG.critical(_LC('Unable to open template file %s'), template_file) + raise + + def get_catalog(self, user_id, tenant_id): + substitutions = dict( + itertools.chain(six.iteritems(CONF), + six.iteritems(CONF.eventlet_server))) + substitutions.update({'tenant_id': tenant_id, 'user_id': user_id}) + + catalog = {} + for region, region_ref in six.iteritems(self.templates): + catalog[region] = {} + for service, service_ref in six.iteritems(region_ref): + service_data = {} + try: + for k, v in six.iteritems(service_ref): + service_data[k] = core.format_url(v, substitutions) + except exception.MalformedEndpoint: + continue # this failure is already logged in format_url() + catalog[region][service] = service_data + + return catalog diff --git a/keystone-moon/keystone/catalog/controllers.py b/keystone-moon/keystone/catalog/controllers.py new file mode 100644 index 00000000..3518c4bf --- /dev/null +++ b/keystone-moon/keystone/catalog/controllers.py @@ -0,0 +1,336 @@ +# Copyright 2012 OpenStack Foundation +# Copyright 2012 Canonical Ltd. +# +# 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 + +import six + +from keystone.catalog import schema +from keystone.common import controller +from keystone.common import dependency +from keystone.common import validation +from keystone.common import wsgi +from keystone import exception +from keystone.i18n import _ +from keystone import notifications + + +INTERFACES = ['public', 'internal', 'admin'] + + +@dependency.requires('catalog_api') +class Service(controller.V2Controller): + + @controller.v2_deprecated + def get_services(self, context): + self.assert_admin(context) + service_list = self.catalog_api.list_services() + return {'OS-KSADM:services': service_list} + + @controller.v2_deprecated + def get_service(self, context, service_id): + self.assert_admin(context) + service_ref = self.catalog_api.get_service(service_id) + return {'OS-KSADM:service': service_ref} + + @controller.v2_deprecated + def delete_service(self, context, service_id): + self.assert_admin(context) + self.catalog_api.delete_service(service_id) + + @controller.v2_deprecated + def create_service(self, context, OS_KSADM_service): + self.assert_admin(context) + service_id = uuid.uuid4().hex + service_ref = OS_KSADM_service.copy() + service_ref['id'] = service_id + new_service_ref = self.catalog_api.create_service( + service_id, service_ref) + return {'OS-KSADM:service': new_service_ref} + + +@dependency.requires('catalog_api') +class Endpoint(controller.V2Controller): + + @controller.v2_deprecated + def get_endpoints(self, context): + """Merge matching v3 endpoint refs into legacy refs.""" + self.assert_admin(context) + legacy_endpoints = {} + for endpoint in self.catalog_api.list_endpoints(): + if not endpoint.get('legacy_endpoint_id'): + # endpoints created in v3 should not appear on the v2 API + continue + + # is this is a legacy endpoint we haven't indexed yet? + if endpoint['legacy_endpoint_id'] not in legacy_endpoints: + legacy_ep = endpoint.copy() + legacy_ep['id'] = legacy_ep.pop('legacy_endpoint_id') + legacy_ep.pop('interface') + legacy_ep.pop('url') + legacy_ep['region'] = legacy_ep.pop('region_id') + + legacy_endpoints[endpoint['legacy_endpoint_id']] = legacy_ep + else: + legacy_ep = legacy_endpoints[endpoint['legacy_endpoint_id']] + + # add the legacy endpoint with an interface url + legacy_ep['%surl' % endpoint['interface']] = endpoint['url'] + return {'endpoints': legacy_endpoints.values()} + + @controller.v2_deprecated + def create_endpoint(self, context, endpoint): + """Create three v3 endpoint refs based on a legacy ref.""" + self.assert_admin(context) + + # according to the v2 spec publicurl is mandatory + self._require_attribute(endpoint, 'publicurl') + # service_id is necessary + self._require_attribute(endpoint, 'service_id') + + initiator = notifications._get_request_audit_info(context) + + if endpoint.get('region') is not None: + try: + self.catalog_api.get_region(endpoint['region']) + except exception.RegionNotFound: + region = dict(id=endpoint['region']) + self.catalog_api.create_region(region, initiator) + + legacy_endpoint_ref = endpoint.copy() + + urls = {} + for i in INTERFACES: + # remove all urls so they aren't persisted them more than once + url = '%surl' % i + if endpoint.get(url): + # valid urls need to be persisted + urls[i] = endpoint.pop(url) + elif url in endpoint: + # null or empty urls can be discarded + endpoint.pop(url) + legacy_endpoint_ref.pop(url) + + legacy_endpoint_id = uuid.uuid4().hex + for interface, url in six.iteritems(urls): + endpoint_ref = endpoint.copy() + endpoint_ref['id'] = uuid.uuid4().hex + endpoint_ref['legacy_endpoint_id'] = legacy_endpoint_id + endpoint_ref['interface'] = interface + endpoint_ref['url'] = url + endpoint_ref['region_id'] = endpoint_ref.pop('region') + self.catalog_api.create_endpoint(endpoint_ref['id'], endpoint_ref, + initiator) + + legacy_endpoint_ref['id'] = legacy_endpoint_id + return {'endpoint': legacy_endpoint_ref} + + @controller.v2_deprecated + def delete_endpoint(self, context, endpoint_id): + """Delete up to three v3 endpoint refs based on a legacy ref ID.""" + self.assert_admin(context) + + deleted_at_least_one = False + for endpoint in self.catalog_api.list_endpoints(): + if endpoint['legacy_endpoint_id'] == endpoint_id: + self.catalog_api.delete_endpoint(endpoint['id']) + deleted_at_least_one = True + + if not deleted_at_least_one: + raise exception.EndpointNotFound(endpoint_id=endpoint_id) + + +@dependency.requires('catalog_api') +class RegionV3(controller.V3Controller): + collection_name = 'regions' + member_name = 'region' + + def create_region_with_id(self, context, region_id, region): + """Create a region with a user-specified ID. + + This method is unprotected because it depends on ``self.create_region`` + to enforce policy. + """ + if 'id' in region and region_id != region['id']: + raise exception.ValidationError( + _('Conflicting region IDs specified: ' + '"%(url_id)s" != "%(ref_id)s"') % { + 'url_id': region_id, + 'ref_id': region['id']}) + region['id'] = region_id + return self.create_region(context, region) + + @controller.protected() + @validation.validated(schema.region_create, 'region') + def create_region(self, context, region): + ref = self._normalize_dict(region) + + if not ref.get('id'): + ref = self._assign_unique_id(ref) + + initiator = notifications._get_request_audit_info(context) + ref = self.catalog_api.create_region(ref, initiator) + return wsgi.render_response( + RegionV3.wrap_member(context, ref), + status=(201, 'Created')) + + @controller.filterprotected('parent_region_id') + def list_regions(self, context, filters): + hints = RegionV3.build_driver_hints(context, filters) + refs = self.catalog_api.list_regions(hints) + return RegionV3.wrap_collection(context, refs, hints=hints) + + @controller.protected() + def get_region(self, context, region_id): + ref = self.catalog_api.get_region(region_id) + return RegionV3.wrap_member(context, ref) + + @controller.protected() + @validation.validated(schema.region_update, 'region') + def update_region(self, context, region_id, region): + self._require_matching_id(region_id, region) + initiator = notifications._get_request_audit_info(context) + ref = self.catalog_api.update_region(region_id, region, initiator) + return RegionV3.wrap_member(context, ref) + + @controller.protected() + def delete_region(self, context, region_id): + initiator = notifications._get_request_audit_info(context) + return self.catalog_api.delete_region(region_id, initiator) + + +@dependency.requires('catalog_api') +class ServiceV3(controller.V3Controller): + collection_name = 'services' + member_name = 'service' + + def __init__(self): + super(ServiceV3, self).__init__() + self.get_member_from_driver = self.catalog_api.get_service + + @controller.protected() + @validation.validated(schema.service_create, 'service') + def create_service(self, context, service): + ref = self._assign_unique_id(self._normalize_dict(service)) + initiator = notifications._get_request_audit_info(context) + ref = self.catalog_api.create_service(ref['id'], ref, initiator) + return ServiceV3.wrap_member(context, ref) + + @controller.filterprotected('type', 'name') + def list_services(self, context, filters): + hints = ServiceV3.build_driver_hints(context, filters) + refs = self.catalog_api.list_services(hints=hints) + return ServiceV3.wrap_collection(context, refs, hints=hints) + + @controller.protected() + def get_service(self, context, service_id): + ref = self.catalog_api.get_service(service_id) + return ServiceV3.wrap_member(context, ref) + + @controller.protected() + @validation.validated(schema.service_update, 'service') + def update_service(self, context, service_id, service): + self._require_matching_id(service_id, service) + initiator = notifications._get_request_audit_info(context) + ref = self.catalog_api.update_service(service_id, service, initiator) + return ServiceV3.wrap_member(context, ref) + + @controller.protected() + def delete_service(self, context, service_id): + initiator = notifications._get_request_audit_info(context) + return self.catalog_api.delete_service(service_id, initiator) + + +@dependency.requires('catalog_api') +class EndpointV3(controller.V3Controller): + collection_name = 'endpoints' + member_name = 'endpoint' + + def __init__(self): + super(EndpointV3, self).__init__() + self.get_member_from_driver = self.catalog_api.get_endpoint + + @classmethod + def filter_endpoint(cls, ref): + if 'legacy_endpoint_id' in ref: + ref.pop('legacy_endpoint_id') + ref['region'] = ref['region_id'] + return ref + + @classmethod + def wrap_member(cls, context, ref): + ref = cls.filter_endpoint(ref) + return super(EndpointV3, cls).wrap_member(context, ref) + + def _validate_endpoint_region(self, endpoint, context=None): + """Ensure the region for the endpoint exists. + + If 'region_id' is used to specify the region, then we will let the + manager/driver take care of this. If, however, 'region' is used, + then for backward compatibility, we will auto-create the region. + + """ + if (endpoint.get('region_id') is None and + endpoint.get('region') is not None): + # To maintain backward compatibility with clients that are + # using the v3 API in the same way as they used the v2 API, + # create the endpoint region, if that region does not exist + # in keystone. + endpoint['region_id'] = endpoint.pop('region') + try: + self.catalog_api.get_region(endpoint['region_id']) + except exception.RegionNotFound: + region = dict(id=endpoint['region_id']) + initiator = notifications._get_request_audit_info(context) + self.catalog_api.create_region(region, initiator) + + return endpoint + + @controller.protected() + @validation.validated(schema.endpoint_create, 'endpoint') + def create_endpoint(self, context, endpoint): + ref = self._assign_unique_id(self._normalize_dict(endpoint)) + ref = self._validate_endpoint_region(ref, context) + initiator = notifications._get_request_audit_info(context) + ref = self.catalog_api.create_endpoint(ref['id'], ref, initiator) + return EndpointV3.wrap_member(context, ref) + + @controller.filterprotected('interface', 'service_id') + def list_endpoints(self, context, filters): + hints = EndpointV3.build_driver_hints(context, filters) + refs = self.catalog_api.list_endpoints(hints=hints) + return EndpointV3.wrap_collection(context, refs, hints=hints) + + @controller.protected() + def get_endpoint(self, context, endpoint_id): + ref = self.catalog_api.get_endpoint(endpoint_id) + return EndpointV3.wrap_member(context, ref) + + @controller.protected() + @validation.validated(schema.endpoint_update, 'endpoint') + def update_endpoint(self, context, endpoint_id, endpoint): + self._require_matching_id(endpoint_id, endpoint) + + endpoint = self._validate_endpoint_region(endpoint.copy(), context) + + initiator = notifications._get_request_audit_info(context) + ref = self.catalog_api.update_endpoint(endpoint_id, endpoint, + initiator) + return EndpointV3.wrap_member(context, ref) + + @controller.protected() + def delete_endpoint(self, context, endpoint_id): + initiator = notifications._get_request_audit_info(context) + return self.catalog_api.delete_endpoint(endpoint_id, initiator) diff --git a/keystone-moon/keystone/catalog/core.py b/keystone-moon/keystone/catalog/core.py new file mode 100644 index 00000000..fba26b89 --- /dev/null +++ b/keystone-moon/keystone/catalog/core.py @@ -0,0 +1,506 @@ +# Copyright 2012 OpenStack Foundation +# Copyright 2012 Canonical Ltd. +# +# 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. + +"""Main entry point into the Catalog service.""" + +import abc + +from oslo_config import cfg +from oslo_log import log +import six + +from keystone.common import cache +from keystone.common import dependency +from keystone.common import driver_hints +from keystone.common import manager +from keystone.common import utils +from keystone import exception +from keystone.i18n import _ +from keystone.i18n import _LE +from keystone import notifications + + +CONF = cfg.CONF +LOG = log.getLogger(__name__) +MEMOIZE = cache.get_memoization_decorator(section='catalog') + + +def format_url(url, substitutions): + """Formats a user-defined URL with the given substitutions. + + :param string url: the URL to be formatted + :param dict substitutions: the dictionary used for substitution + :returns: a formatted URL + + """ + + WHITELISTED_PROPERTIES = [ + 'tenant_id', 'user_id', 'public_bind_host', 'admin_bind_host', + 'compute_host', 'compute_port', 'admin_port', 'public_port', + 'public_endpoint', 'admin_endpoint', ] + + substitutions = utils.WhiteListedItemFilter( + WHITELISTED_PROPERTIES, + substitutions) + try: + result = url.replace('$(', '%(') % substitutions + except AttributeError: + LOG.error(_LE('Malformed endpoint - %(url)r is not a string'), + {"url": url}) + raise exception.MalformedEndpoint(endpoint=url) + except KeyError as e: + LOG.error(_LE("Malformed endpoint %(url)s - unknown key %(keyerror)s"), + {"url": url, + "keyerror": e}) + raise exception.MalformedEndpoint(endpoint=url) + except TypeError as e: + LOG.error(_LE("Malformed endpoint '%(url)s'. The following type error " + "occurred during string substitution: %(typeerror)s"), + {"url": url, + "typeerror": e}) + raise exception.MalformedEndpoint(endpoint=url) + except ValueError as e: + LOG.error(_LE("Malformed endpoint %s - incomplete format " + "(are you missing a type notifier ?)"), url) + raise exception.MalformedEndpoint(endpoint=url) + return result + + +@dependency.provider('catalog_api') +class Manager(manager.Manager): + """Default pivot point for the Catalog backend. + + See :mod:`keystone.common.manager.Manager` for more details on how this + dynamically calls the backend. + + """ + _ENDPOINT = 'endpoint' + _SERVICE = 'service' + _REGION = 'region' + + def __init__(self): + super(Manager, self).__init__(CONF.catalog.driver) + + def create_region(self, region_ref, initiator=None): + # Check duplicate ID + try: + self.get_region(region_ref['id']) + except exception.RegionNotFound: + pass + else: + msg = _('Duplicate ID, %s.') % region_ref['id'] + raise exception.Conflict(type='region', details=msg) + + # NOTE(lbragstad): The description column of the region database + # can not be null. So if the user doesn't pass in a description then + # set it to an empty string. + region_ref.setdefault('description', '') + try: + ret = self.driver.create_region(region_ref) + except exception.NotFound: + parent_region_id = region_ref.get('parent_region_id') + raise exception.RegionNotFound(region_id=parent_region_id) + + notifications.Audit.created(self._REGION, ret['id'], initiator) + return ret + + @MEMOIZE + def get_region(self, region_id): + try: + return self.driver.get_region(region_id) + except exception.NotFound: + raise exception.RegionNotFound(region_id=region_id) + + def update_region(self, region_id, region_ref, initiator=None): + ref = self.driver.update_region(region_id, region_ref) + notifications.Audit.updated(self._REGION, region_id, initiator) + self.get_region.invalidate(self, region_id) + return ref + + def delete_region(self, region_id, initiator=None): + try: + ret = self.driver.delete_region(region_id) + notifications.Audit.deleted(self._REGION, region_id, initiator) + self.get_region.invalidate(self, region_id) + return ret + except exception.NotFound: + raise exception.RegionNotFound(region_id=region_id) + + @manager.response_truncated + def list_regions(self, hints=None): + return self.driver.list_regions(hints or driver_hints.Hints()) + + def create_service(self, service_id, service_ref, initiator=None): + service_ref.setdefault('enabled', True) + service_ref.setdefault('name', '') + ref = self.driver.create_service(service_id, service_ref) + notifications.Audit.created(self._SERVICE, service_id, initiator) + return ref + + @MEMOIZE + def get_service(self, service_id): + try: + return self.driver.get_service(service_id) + except exception.NotFound: + raise exception.ServiceNotFound(service_id=service_id) + + def update_service(self, service_id, service_ref, initiator=None): + ref = self.driver.update_service(service_id, service_ref) + notifications.Audit.updated(self._SERVICE, service_id, initiator) + self.get_service.invalidate(self, service_id) + return ref + + def delete_service(self, service_id, initiator=None): + try: + endpoints = self.list_endpoints() + ret = self.driver.delete_service(service_id) + notifications.Audit.deleted(self._SERVICE, service_id, initiator) + self.get_service.invalidate(self, service_id) + for endpoint in endpoints: + if endpoint['service_id'] == service_id: + self.get_endpoint.invalidate(self, endpoint['id']) + return ret + except exception.NotFound: + raise exception.ServiceNotFound(service_id=service_id) + + @manager.response_truncated + def list_services(self, hints=None): + return self.driver.list_services(hints or driver_hints.Hints()) + + def _assert_region_exists(self, region_id): + try: + if region_id is not None: + self.get_region(region_id) + except exception.RegionNotFound: + raise exception.ValidationError(attribute='endpoint region_id', + target='region table') + + def _assert_service_exists(self, service_id): + try: + if service_id is not None: + self.get_service(service_id) + except exception.ServiceNotFound: + raise exception.ValidationError(attribute='endpoint service_id', + target='service table') + + def create_endpoint(self, endpoint_id, endpoint_ref, initiator=None): + self._assert_region_exists(endpoint_ref.get('region_id')) + self._assert_service_exists(endpoint_ref['service_id']) + ref = self.driver.create_endpoint(endpoint_id, endpoint_ref) + + notifications.Audit.created(self._ENDPOINT, endpoint_id, initiator) + return ref + + def update_endpoint(self, endpoint_id, endpoint_ref, initiator=None): + self._assert_region_exists(endpoint_ref.get('region_id')) + self._assert_service_exists(endpoint_ref.get('service_id')) + ref = self.driver.update_endpoint(endpoint_id, endpoint_ref) + notifications.Audit.updated(self._ENDPOINT, endpoint_id, initiator) + self.get_endpoint.invalidate(self, endpoint_id) + return ref + + def delete_endpoint(self, endpoint_id, initiator=None): + try: + ret = self.driver.delete_endpoint(endpoint_id) + notifications.Audit.deleted(self._ENDPOINT, endpoint_id, initiator) + self.get_endpoint.invalidate(self, endpoint_id) + return ret + except exception.NotFound: + raise exception.EndpointNotFound(endpoint_id=endpoint_id) + + @MEMOIZE + def get_endpoint(self, endpoint_id): + try: + return self.driver.get_endpoint(endpoint_id) + except exception.NotFound: + raise exception.EndpointNotFound(endpoint_id=endpoint_id) + + @manager.response_truncated + def list_endpoints(self, hints=None): + return self.driver.list_endpoints(hints or driver_hints.Hints()) + + def get_catalog(self, user_id, tenant_id): + try: + return self.driver.get_catalog(user_id, tenant_id) + except exception.NotFound: + raise exception.NotFound('Catalog not found for user and tenant') + + +@six.add_metaclass(abc.ABCMeta) +class Driver(object): + """Interface description for an Catalog driver.""" + + def _get_list_limit(self): + return CONF.catalog.list_limit or CONF.list_limit + + def _ensure_no_circle_in_hierarchical_regions(self, region_ref): + if region_ref.get('parent_region_id') is None: + return + + root_region_id = region_ref['id'] + parent_region_id = region_ref['parent_region_id'] + + while parent_region_id: + # NOTE(wanghong): check before getting parent region can ensure no + # self circle + if parent_region_id == root_region_id: + raise exception.CircularRegionHierarchyError( + parent_region_id=parent_region_id) + parent_region = self.get_region(parent_region_id) + parent_region_id = parent_region.get('parent_region_id') + + @abc.abstractmethod + def create_region(self, region_ref): + """Creates a new region. + + :raises: keystone.exception.Conflict + :raises: keystone.exception.RegionNotFound (if parent region invalid) + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_regions(self, hints): + """List all regions. + + :param hints: contains the list of filters yet to be satisfied. + Any filters satisfied here will be removed so that + the caller will know if any filters remain. + + :returns: list of region_refs or an empty list. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def get_region(self, region_id): + """Get region by id. + + :returns: region_ref dict + :raises: keystone.exception.RegionNotFound + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def update_region(self, region_id, region_ref): + """Update region by id. + + :returns: region_ref dict + :raises: keystone.exception.RegionNotFound + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def delete_region(self, region_id): + """Deletes an existing region. + + :raises: keystone.exception.RegionNotFound + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def create_service(self, service_id, service_ref): + """Creates a new service. + + :raises: keystone.exception.Conflict + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_services(self, hints): + """List all services. + + :param hints: contains the list of filters yet to be satisfied. + Any filters satisfied here will be removed so that + the caller will know if any filters remain. + + :returns: list of service_refs or an empty list. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def get_service(self, service_id): + """Get service by id. + + :returns: service_ref dict + :raises: keystone.exception.ServiceNotFound + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def update_service(self, service_id, service_ref): + """Update service by id. + + :returns: service_ref dict + :raises: keystone.exception.ServiceNotFound + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def delete_service(self, service_id): + """Deletes an existing service. + + :raises: keystone.exception.ServiceNotFound + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def create_endpoint(self, endpoint_id, endpoint_ref): + """Creates a new endpoint for a service. + + :raises: keystone.exception.Conflict, + keystone.exception.ServiceNotFound + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def get_endpoint(self, endpoint_id): + """Get endpoint by id. + + :returns: endpoint_ref dict + :raises: keystone.exception.EndpointNotFound + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_endpoints(self, hints): + """List all endpoints. + + :param hints: contains the list of filters yet to be satisfied. + Any filters satisfied here will be removed so that + the caller will know if any filters remain. + + :returns: list of endpoint_refs or an empty list. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def update_endpoint(self, endpoint_id, endpoint_ref): + """Get endpoint by id. + + :returns: endpoint_ref dict + :raises: keystone.exception.EndpointNotFound + keystone.exception.ServiceNotFound + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def delete_endpoint(self, endpoint_id): + """Deletes an endpoint for a service. + + :raises: keystone.exception.EndpointNotFound + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def get_catalog(self, user_id, tenant_id): + """Retrieve and format the current service catalog. + + Example:: + + { 'RegionOne': + {'compute': { + 'adminURL': u'http://host:8774/v1.1/tenantid', + 'internalURL': u'http://host:8774/v1.1/tenant_id', + 'name': 'Compute Service', + 'publicURL': u'http://host:8774/v1.1/tenantid'}, + 'ec2': { + 'adminURL': 'http://host:8773/services/Admin', + 'internalURL': 'http://host:8773/services/Cloud', + 'name': 'EC2 Service', + 'publicURL': 'http://host:8773/services/Cloud'}} + + :returns: A nested dict representing the service catalog or an + empty dict. + :raises: keystone.exception.NotFound + + """ + raise exception.NotImplemented() # pragma: no cover + + def get_v3_catalog(self, user_id, tenant_id): + """Retrieve and format the current V3 service catalog. + + The default implementation builds the V3 catalog from the V2 catalog. + + Example:: + + [ + { + "endpoints": [ + { + "interface": "public", + "id": "--endpoint-id--", + "region": "RegionOne", + "url": "http://external:8776/v1/--project-id--" + }, + { + "interface": "internal", + "id": "--endpoint-id--", + "region": "RegionOne", + "url": "http://internal:8776/v1/--project-id--" + }], + "id": "--service-id--", + "type": "volume" + }] + + :returns: A list representing the service catalog or an empty list + :raises: keystone.exception.NotFound + + """ + v2_catalog = self.get_catalog(user_id, tenant_id) + v3_catalog = [] + + for region_name, region in six.iteritems(v2_catalog): + for service_type, service in six.iteritems(region): + service_v3 = { + 'type': service_type, + 'endpoints': [] + } + + for attr, value in six.iteritems(service): + # Attributes that end in URL are interfaces. In the V2 + # catalog, these are internalURL, publicURL, and adminURL. + # For example, .publicURL= in the V2 + # catalog becomes the V3 interface for the service: + # { 'interface': 'public', 'url': '', 'region': + # 'region: '' } + if attr.endswith('URL'): + v3_interface = attr[:-len('URL')] + service_v3['endpoints'].append({ + 'interface': v3_interface, + 'region': region_name, + 'url': value, + }) + continue + + # Other attributes are copied to the service. + service_v3[attr] = value + + v3_catalog.append(service_v3) + + return v3_catalog diff --git a/keystone-moon/keystone/catalog/routers.py b/keystone-moon/keystone/catalog/routers.py new file mode 100644 index 00000000..f3bd988b --- /dev/null +++ b/keystone-moon/keystone/catalog/routers.py @@ -0,0 +1,40 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 keystone.catalog import controllers +from keystone.common import router +from keystone.common import wsgi + + +class Routers(wsgi.RoutersBase): + + def append_v3_routers(self, mapper, routers): + regions_controller = controllers.RegionV3() + routers.append(router.Router(regions_controller, + 'regions', 'region', + resource_descriptions=self.v3_resources)) + + # Need to add an additional route to support PUT /regions/{region_id} + mapper.connect( + '/regions/{region_id}', + controller=regions_controller, + action='create_region_with_id', + conditions=dict(method=['PUT'])) + + routers.append(router.Router(controllers.ServiceV3(), + 'services', 'service', + resource_descriptions=self.v3_resources)) + routers.append(router.Router(controllers.EndpointV3(), + 'endpoints', 'endpoint', + resource_descriptions=self.v3_resources)) diff --git a/keystone-moon/keystone/catalog/schema.py b/keystone-moon/keystone/catalog/schema.py new file mode 100644 index 00000000..a779ad02 --- /dev/null +++ b/keystone-moon/keystone/catalog/schema.py @@ -0,0 +1,96 @@ +# 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 keystone.common.validation import parameter_types + + +_region_properties = { + 'description': parameter_types.description, + # NOTE(lbragstad): Regions use ID differently. The user can specify the ID + # or it will be generated automatically. + 'id': { + 'type': 'string' + }, + 'parent_region_id': { + 'type': ['string', 'null'] + } +} + +region_create = { + 'type': 'object', + 'properties': _region_properties, + 'additionalProperties': True + # NOTE(lbragstad): No parameters are required for creating regions. +} + +region_update = { + 'type': 'object', + 'properties': _region_properties, + 'minProperties': 1, + 'additionalProperties': True +} + +_service_properties = { + 'enabled': parameter_types.boolean, + 'name': parameter_types.name, + 'type': { + 'type': 'string', + 'minLength': 1, + 'maxLength': 255 + } +} + +service_create = { + 'type': 'object', + 'properties': _service_properties, + 'required': ['type'], + 'additionalProperties': True, +} + +service_update = { + 'type': 'object', + 'properties': _service_properties, + 'minProperties': 1, + 'additionalProperties': True +} + +_endpoint_properties = { + 'enabled': parameter_types.boolean, + 'interface': { + 'type': 'string', + 'enum': ['admin', 'internal', 'public'] + }, + 'region_id': { + 'type': 'string' + }, + 'region': { + 'type': 'string' + }, + 'service_id': { + 'type': 'string' + }, + 'url': parameter_types.url +} + +endpoint_create = { + 'type': 'object', + 'properties': _endpoint_properties, + 'required': ['interface', 'service_id', 'url'], + 'additionalProperties': True +} + +endpoint_update = { + 'type': 'object', + 'properties': _endpoint_properties, + 'minProperties': 1, + 'additionalProperties': True +} diff --git a/keystone-moon/keystone/clean.py b/keystone-moon/keystone/clean.py new file mode 100644 index 00000000..38564e0b --- /dev/null +++ b/keystone-moon/keystone/clean.py @@ -0,0 +1,87 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 six + +from keystone import exception +from keystone.i18n import _ + + +def check_length(property_name, value, min_length=1, max_length=64): + if len(value) < min_length: + if min_length == 1: + msg = _("%s cannot be empty.") % property_name + else: + msg = (_("%(property_name)s cannot be less than " + "%(min_length)s characters.") % dict( + property_name=property_name, min_length=min_length)) + raise exception.ValidationError(msg) + if len(value) > max_length: + msg = (_("%(property_name)s should not be greater than " + "%(max_length)s characters.") % dict( + property_name=property_name, max_length=max_length)) + + raise exception.ValidationError(msg) + + +def check_type(property_name, value, expected_type, display_expected_type): + if not isinstance(value, expected_type): + msg = (_("%(property_name)s is not a " + "%(display_expected_type)s") % dict( + property_name=property_name, + display_expected_type=display_expected_type)) + raise exception.ValidationError(msg) + + +def check_enabled(property_name, enabled): + # Allow int and it's subclass bool + check_type('%s enabled' % property_name, enabled, int, 'boolean') + return bool(enabled) + + +def check_name(property_name, name, min_length=1, max_length=64): + check_type('%s name' % property_name, name, six.string_types, + 'str or unicode') + name = name.strip() + check_length('%s name' % property_name, name, + min_length=min_length, max_length=max_length) + return name + + +def domain_name(name): + return check_name('Domain', name) + + +def domain_enabled(enabled): + return check_enabled('Domain', enabled) + + +def project_name(name): + return check_name('Project', name) + + +def project_enabled(enabled): + return check_enabled('Project', enabled) + + +def user_name(name): + return check_name('User', name, max_length=255) + + +def user_enabled(enabled): + return check_enabled('User', enabled) + + +def group_name(name): + return check_name('Group', name) diff --git a/keystone-moon/keystone/cli.py b/keystone-moon/keystone/cli.py new file mode 100644 index 00000000..b5fff136 --- /dev/null +++ b/keystone-moon/keystone/cli.py @@ -0,0 +1,596 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 __future__ import absolute_import +from __future__ import print_function + +import os + +from oslo_config import cfg +from oslo_log import log +import pbr.version + +from keystone import assignment +from keystone.common import driver_hints +from keystone.common import openssl +from keystone.common import sql +from keystone.common.sql import migration_helpers +from keystone.common import utils +from keystone import config +from keystone import exception +from keystone.i18n import _, _LW +from keystone import identity +from keystone import resource +from keystone import token +from keystone.token.providers.fernet import utils as fernet + + +CONF = cfg.CONF +LOG = log.getLogger(__name__) + + +class BaseApp(object): + + name = None + + @classmethod + def add_argument_parser(cls, subparsers): + parser = subparsers.add_parser(cls.name, help=cls.__doc__) + parser.set_defaults(cmd_class=cls) + return parser + + +class DbSync(BaseApp): + """Sync the database.""" + + name = 'db_sync' + + @classmethod + def add_argument_parser(cls, subparsers): + parser = super(DbSync, cls).add_argument_parser(subparsers) + parser.add_argument('version', default=None, nargs='?', + help=('Migrate the database up to a specified ' + 'version. If not provided, db_sync will ' + 'migrate the database to the latest known ' + 'version.')) + parser.add_argument('--extension', default=None, + help=('Migrate the database for the specified ' + 'extension. If not provided, db_sync will ' + 'migrate the common repository.')) + + return parser + + @staticmethod + def main(): + version = CONF.command.version + extension = CONF.command.extension + migration_helpers.sync_database_to_version(extension, version) + + +class DbVersion(BaseApp): + """Print the current migration version of the database.""" + + name = 'db_version' + + @classmethod + def add_argument_parser(cls, subparsers): + parser = super(DbVersion, cls).add_argument_parser(subparsers) + parser.add_argument('--extension', default=None, + help=('Print the migration version of the ' + 'database for the specified extension. If ' + 'not provided, print it for the common ' + 'repository.')) + + @staticmethod + def main(): + extension = CONF.command.extension + migration_helpers.print_db_version(extension) + + +class BasePermissionsSetup(BaseApp): + """Common user/group setup for file permissions.""" + + @classmethod + def add_argument_parser(cls, subparsers): + parser = super(BasePermissionsSetup, + cls).add_argument_parser(subparsers) + running_as_root = (os.geteuid() == 0) + parser.add_argument('--keystone-user', required=running_as_root) + parser.add_argument('--keystone-group', required=running_as_root) + return parser + + @staticmethod + def get_user_group(): + keystone_user_id = None + keystone_group_id = None + + try: + a = CONF.command.keystone_user + if a: + keystone_user_id = utils.get_unix_user(a)[0] + except KeyError: + raise ValueError("Unknown user '%s' in --keystone-user" % a) + + try: + a = CONF.command.keystone_group + if a: + keystone_group_id = utils.get_unix_group(a)[0] + except KeyError: + raise ValueError("Unknown group '%s' in --keystone-group" % a) + + return keystone_user_id, keystone_group_id + + +class BaseCertificateSetup(BasePermissionsSetup): + """Provides common options for certificate setup.""" + + @classmethod + def add_argument_parser(cls, subparsers): + parser = super(BaseCertificateSetup, + cls).add_argument_parser(subparsers) + parser.add_argument('--rebuild', default=False, action='store_true', + help=('Rebuild certificate files: erase previous ' + 'files and regenerate them.')) + return parser + + +class PKISetup(BaseCertificateSetup): + """Set up Key pairs and certificates for token signing and verification. + + This is NOT intended for production use, see Keystone Configuration + documentation for details. + """ + + name = 'pki_setup' + + @classmethod + def main(cls): + LOG.warn(_LW('keystone-manage pki_setup is not recommended for ' + 'production use.')) + keystone_user_id, keystone_group_id = cls.get_user_group() + conf_pki = openssl.ConfigurePKI(keystone_user_id, keystone_group_id, + rebuild=CONF.command.rebuild) + conf_pki.run() + + +class SSLSetup(BaseCertificateSetup): + """Create key pairs and certificates for HTTPS connections. + + This is NOT intended for production use, see Keystone Configuration + documentation for details. + """ + + name = 'ssl_setup' + + @classmethod + def main(cls): + LOG.warn(_LW('keystone-manage ssl_setup is not recommended for ' + 'production use.')) + keystone_user_id, keystone_group_id = cls.get_user_group() + conf_ssl = openssl.ConfigureSSL(keystone_user_id, keystone_group_id, + rebuild=CONF.command.rebuild) + conf_ssl.run() + + +class FernetSetup(BasePermissionsSetup): + """Setup a key repository for Fernet tokens. + + This also creates a primary key used for both creating and validating + Keystone Lightweight tokens. To improve security, you should rotate your + keys (using keystone-manage fernet_rotate, for example). + + """ + + name = 'fernet_setup' + + @classmethod + def main(cls): + keystone_user_id, keystone_group_id = cls.get_user_group() + fernet.create_key_directory(keystone_user_id, keystone_group_id) + if fernet.validate_key_repository(): + fernet.initialize_key_repository( + keystone_user_id, keystone_group_id) + + +class FernetRotate(BasePermissionsSetup): + """Rotate Fernet encryption keys. + + This assumes you have already run keystone-manage fernet_setup. + + A new primary key is placed into rotation, which is used for new tokens. + The old primary key is demoted to secondary, which can then still be used + for validating tokens. Excess secondary keys (beyond [fernet_tokens] + max_active_keys) are revoked. Revoked keys are permanently deleted. A new + staged key will be created and used to validate tokens. The next time key + rotation takes place, the staged key will be put into rotation as the + primary key. + + Rotating keys too frequently, or with [fernet_tokens] max_active_keys set + too low, will cause tokens to become invalid prior to their expiration. + + """ + + name = 'fernet_rotate' + + @classmethod + def main(cls): + keystone_user_id, keystone_group_id = cls.get_user_group() + if fernet.validate_key_repository(): + fernet.rotate_keys(keystone_user_id, keystone_group_id) + + +class TokenFlush(BaseApp): + """Flush expired tokens from the backend.""" + + name = 'token_flush' + + @classmethod + def main(cls): + token_manager = token.persistence.PersistenceManager() + token_manager.driver.flush_expired_tokens() + + +class MappingPurge(BaseApp): + """Purge the mapping table.""" + + name = 'mapping_purge' + + @classmethod + def add_argument_parser(cls, subparsers): + parser = super(MappingPurge, cls).add_argument_parser(subparsers) + parser.add_argument('--all', default=False, action='store_true', + help=('Purge all mappings.')) + parser.add_argument('--domain-name', default=None, + help=('Purge any mappings for the domain ' + 'specified.')) + parser.add_argument('--public-id', default=None, + help=('Purge the mapping for the Public ID ' + 'specified.')) + parser.add_argument('--local-id', default=None, + help=('Purge the mappings for the Local ID ' + 'specified.')) + parser.add_argument('--type', default=None, choices=['user', 'group'], + help=('Purge any mappings for the type ' + 'specified.')) + return parser + + @staticmethod + def main(): + def validate_options(): + # NOTE(henry-nash); It would be nice to use the argparse automated + # checking for this validation, but the only way I can see doing + # that is to make the default (i.e. if no optional parameters + # are specified) to purge all mappings - and that sounds too + # dangerous as a default. So we use it in a slightly + # unconventional way, where all parameters are optional, but you + # must specify at least one. + if (CONF.command.all is False and + CONF.command.domain_name is None and + CONF.command.public_id is None and + CONF.command.local_id is None and + CONF.command.type is None): + raise ValueError(_('At least one option must be provided')) + + if (CONF.command.all is True and + (CONF.command.domain_name is not None or + CONF.command.public_id is not None or + CONF.command.local_id is not None or + CONF.command.type is not None)): + raise ValueError(_('--all option cannot be mixed with ' + 'other options')) + + def get_domain_id(name): + try: + identity.Manager() + # init assignment manager to avoid KeyError in resource.core + assignment.Manager() + resource_manager = resource.Manager() + return resource_manager.driver.get_domain_by_name(name)['id'] + except KeyError: + raise ValueError(_("Unknown domain '%(name)s' specified by " + "--domain-name") % {'name': name}) + + validate_options() + # Now that we have validated the options, we know that at least one + # option has been specified, and if it was the --all option then this + # was the only option specified. + # + # The mapping dict is used to filter which mappings are purged, so + # leaving it empty means purge them all + mapping = {} + if CONF.command.domain_name is not None: + mapping['domain_id'] = get_domain_id(CONF.command.domain_name) + if CONF.command.public_id is not None: + mapping['public_id'] = CONF.command.public_id + if CONF.command.local_id is not None: + mapping['local_id'] = CONF.command.local_id + if CONF.command.type is not None: + mapping['type'] = CONF.command.type + + mapping_manager = identity.MappingManager() + mapping_manager.driver.purge_mappings(mapping) + + +DOMAIN_CONF_FHEAD = 'keystone.' +DOMAIN_CONF_FTAIL = '.conf' + + +class DomainConfigUploadFiles(object): + + def __init__(self): + super(DomainConfigUploadFiles, self).__init__() + self.load_backends() + + def load_backends(self): + """Load the backends needed for uploading domain configs. + + We only need the resource and domain_config managers, but there are + some dependencies which mean we have to load the assignment and + identity managers as well. + + The order of loading the backends is important, since the resource + manager depends on the assignment manager, which in turn depends on + the identity manager. + + """ + identity.Manager() + assignment.Manager() + self.resource_manager = resource.Manager() + self.domain_config_manager = resource.DomainConfigManager() + + def valid_options(self): + """Validate the options, returning True if they are indeed valid. + + It would be nice to use the argparse automated checking for this + validation, but the only way I can see doing that is to make the + default (i.e. if no optional parameters are specified) to upload + all configuration files - and that sounds too dangerous as a + default. So we use it in a slightly unconventional way, where all + parameters are optional, but you must specify at least one. + + """ + if (CONF.command.all is False and + CONF.command.domain_name is None): + print(_('At least one option must be provided, use either ' + '--all or --domain-name')) + raise ValueError + + if (CONF.command.all is True and + CONF.command.domain_name is not None): + print(_('The --all option cannot be used with ' + 'the --domain-name option')) + raise ValueError + + def upload_config_to_database(self, file_name, domain_name): + """Upload a single config file to the database. + + :param file_name: the file containing the config options + :param domain_name: the domain name + + :raises: ValueError: the domain does not exist or already has domain + specific configurations defined + :raises: Exceptions from oslo config: there is an issue with options + defined in the config file or its + format + + The caller of this method should catch the errors raised and handle + appropriately in order that the best UX experience can be provided for + both the case of when a user has asked for a specific config file to + be uploaded, as well as all config files in a directory. + + """ + try: + domain_ref = ( + self.resource_manager.driver.get_domain_by_name(domain_name)) + except exception.DomainNotFound: + print(_('Invalid domain name: %(domain)s found in config file ' + 'name: %(file)s - ignoring this file.') % { + 'domain': domain_name, + 'file': file_name}) + raise ValueError + + if self.domain_config_manager.get_config_with_sensitive_info( + domain_ref['id']): + print(_('Domain: %(domain)s already has a configuration ' + 'defined - ignoring file: %(file)s.') % { + 'domain': domain_name, + 'file': file_name}) + raise ValueError + + sections = {} + try: + parser = cfg.ConfigParser(file_name, sections) + parser.parse() + except Exception: + # We explicitly don't try and differentiate the error cases, in + # order to keep the code in this tool more robust as oslo.config + # changes. + print(_('Error parsing configuration file for domain: %(domain)s, ' + 'file: %(file)s.') % { + 'domain': domain_name, + 'file': file_name}) + raise + + for group in sections: + for option in sections[group]: + sections[group][option] = sections[group][option][0] + self.domain_config_manager.create_config(domain_ref['id'], sections) + + def upload_configs_to_database(self, file_name, domain_name): + """Upload configs from file and load into database. + + This method will be called repeatedly for all the config files in the + config directory. To provide a better UX, we differentiate the error + handling in this case (versus when the user has asked for a single + config file to be uploaded). + + """ + try: + self.upload_config_to_database(file_name, domain_name) + except ValueError: + # We've already given all the info we can in a message, so carry + # on to the next one + pass + except Exception: + # Some other error occurred relating to this specific config file + # or domain. Since we are trying to upload all the config files, + # we'll continue and hide this exception. However, we tell the + # user how to get more info about this error by re-running with + # just the domain at fault. When we run in single-domain mode we + # will NOT hide the exception. + print(_('To get a more detailed information on this error, re-run ' + 'this command for the specific domain, i.e.: ' + 'keystone-manage domain_config_upload --domain-name %s') % + domain_name) + pass + + def read_domain_configs_from_files(self): + """Read configs from file(s) and load into database. + + The command line parameters have already been parsed and the CONF + command option will have been set. It is either set to the name of an + explicit domain, or it's None to indicate that we want all domain + config files. + + """ + domain_name = CONF.command.domain_name + conf_dir = CONF.identity.domain_config_dir + if not os.path.exists(conf_dir): + print(_('Unable to locate domain config directory: %s') % conf_dir) + raise ValueError + + if domain_name: + # Request is to upload the configs for just one domain + fname = DOMAIN_CONF_FHEAD + domain_name + DOMAIN_CONF_FTAIL + self.upload_config_to_database( + os.path.join(conf_dir, fname), domain_name) + return + + # Request is to transfer all config files, so let's read all the + # files in the config directory, and transfer those that match the + # filename pattern of 'keystone..conf' + for r, d, f in os.walk(conf_dir): + for fname in f: + if (fname.startswith(DOMAIN_CONF_FHEAD) and + fname.endswith(DOMAIN_CONF_FTAIL)): + if fname.count('.') >= 2: + self.upload_configs_to_database( + os.path.join(r, fname), + fname[len(DOMAIN_CONF_FHEAD): + -len(DOMAIN_CONF_FTAIL)]) + else: + LOG.warn(_LW('Ignoring file (%s) while scanning ' + 'domain config directory'), fname) + + def run(self): + # First off, let's just check we can talk to the domain database + try: + self.resource_manager.driver.list_domains(driver_hints.Hints()) + except Exception: + # It is likely that there is some SQL or other backend error + # related to set up + print(_('Unable to access the keystone database, please check it ' + 'is configured correctly.')) + raise + + try: + self.valid_options() + self.read_domain_configs_from_files() + except ValueError: + # We will already have printed out a nice message, so indicate + # to caller the non-success error code to be used. + return 1 + + +class DomainConfigUpload(BaseApp): + """Upload the domain specific configuration files to the database.""" + + name = 'domain_config_upload' + + @classmethod + def add_argument_parser(cls, subparsers): + parser = super(DomainConfigUpload, cls).add_argument_parser(subparsers) + parser.add_argument('--all', default=False, action='store_true', + help='Upload contents of all domain specific ' + 'configuration files. Either use this option ' + 'or use the --domain-name option to choose a ' + 'specific domain.') + parser.add_argument('--domain-name', default=None, + help='Upload contents of the specific ' + 'configuration file for the given domain. ' + 'Either use this option or use the --all ' + 'option to upload contents for all domains.') + return parser + + @staticmethod + def main(): + dcu = DomainConfigUploadFiles() + status = dcu.run() + if status is not None: + exit(status) + + +class SamlIdentityProviderMetadata(BaseApp): + """Generate Identity Provider metadata.""" + + name = 'saml_idp_metadata' + + @staticmethod + def main(): + # NOTE(marek-denis): Since federation is currently an extension import + # corresponding modules only when they are really going to be used. + from keystone.contrib.federation import idp + metadata = idp.MetadataGenerator().generate_metadata() + print(metadata.to_string()) + + +CMDS = [ + DbSync, + DbVersion, + DomainConfigUpload, + FernetRotate, + FernetSetup, + MappingPurge, + PKISetup, + SamlIdentityProviderMetadata, + SSLSetup, + TokenFlush, +] + + +def add_command_parsers(subparsers): + for cmd in CMDS: + cmd.add_argument_parser(subparsers) + + +command_opt = cfg.SubCommandOpt('command', + title='Commands', + help='Available commands', + handler=add_command_parsers) + + +def main(argv=None, config_files=None): + CONF.register_cli_opt(command_opt) + + config.configure() + sql.initialize() + config.set_default_for_default_log_levels() + + CONF(args=argv[1:], + project='keystone', + version=pbr.version.VersionInfo('keystone').version_string(), + usage='%(prog)s [' + '|'.join([cmd.name for cmd in CMDS]) + ']', + default_config_files=config_files) + config.setup_logging() + CONF.command.cmd_class.main() diff --git a/keystone-moon/keystone/common/__init__.py b/keystone-moon/keystone/common/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/common/authorization.py b/keystone-moon/keystone/common/authorization.py new file mode 100644 index 00000000..5cb1e630 --- /dev/null +++ b/keystone-moon/keystone/common/authorization.py @@ -0,0 +1,87 @@ +# Copyright 2012 OpenStack Foundation +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# Copyright 2011 - 2012 Justin Santa Barbara +# All Rights Reserved. +# +# 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 oslo_log import log + +from keystone import exception +from keystone.i18n import _, _LW +from keystone.models import token_model + + +AUTH_CONTEXT_ENV = 'KEYSTONE_AUTH_CONTEXT' +"""Environment variable used to convey the Keystone auth context. + +Auth context is essentially the user credential used for policy enforcement. +It is a dictionary with the following attributes: + +* ``user_id``: user ID of the principal +* ``project_id`` (optional): project ID of the scoped project if auth is + project-scoped +* ``domain_id`` (optional): domain ID of the scoped domain if auth is + domain-scoped +* ``roles`` (optional): list of role names for the given scope +* ``group_ids``: list of group IDs for which the API user has membership + +""" + +LOG = log.getLogger(__name__) + + +def token_to_auth_context(token): + if not isinstance(token, token_model.KeystoneToken): + raise exception.UnexpectedError(_('token reference must be a ' + 'KeystoneToken type, got: %s') % + type(token)) + auth_context = {'token': token, + 'is_delegated_auth': False} + try: + auth_context['user_id'] = token.user_id + except KeyError: + LOG.warning(_LW('RBAC: Invalid user data in token')) + raise exception.Unauthorized() + + if token.project_scoped: + auth_context['project_id'] = token.project_id + elif token.domain_scoped: + auth_context['domain_id'] = token.domain_id + else: + LOG.debug('RBAC: Proceeding without project or domain scope') + + if token.trust_scoped: + auth_context['is_delegated_auth'] = True + auth_context['trust_id'] = token.trust_id + auth_context['trustor_id'] = token.trustor_user_id + auth_context['trustee_id'] = token.trustee_user_id + else: + auth_context['trust_id'] = None + auth_context['trustor_id'] = None + auth_context['trustee_id'] = None + + roles = token.role_names + if roles: + auth_context['roles'] = roles + + if token.oauth_scoped: + auth_context['is_delegated_auth'] = True + auth_context['consumer_id'] = token.oauth_consumer_id + auth_context['access_token_id'] = token.oauth_access_token_id + + if token.is_federated_user: + auth_context['group_ids'] = token.federation_group_ids + + return auth_context diff --git a/keystone-moon/keystone/common/base64utils.py b/keystone-moon/keystone/common/base64utils.py new file mode 100644 index 00000000..1a636f9b --- /dev/null +++ b/keystone-moon/keystone/common/base64utils.py @@ -0,0 +1,396 @@ +# Copyright 2013 Red Hat, Inc. +# +# 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. + +""" + +Python provides the base64 module as a core module but this is mostly +limited to encoding and decoding base64 and it's variants. It is often +useful to be able to perform other operations on base64 text. This +module is meant to be used in conjunction with the core base64 module. + +Standardized base64 is defined in +RFC-4648 "The Base16, Base32, and Base64 Data Encodings". + +This module provides the following base64 utility functionality: + + * tests if text is valid base64 + * filter formatting from base64 + * convert base64 between different alphabets + * Handle padding issues + - test if base64 is padded + - removes padding + - restores padding + * wraps base64 text into formatted blocks + - via iterator + - return formatted string + +""" + +import re +import string + +import six +from six.moves import urllib + +from keystone.i18n import _ + + +class InvalidBase64Error(ValueError): + pass + +base64_alphabet_re = re.compile(r'^[^A-Za-z0-9+/=]+$') +base64url_alphabet_re = re.compile(r'^[^A-Za-z0-9---_=]+$') + +base64_non_alphabet_re = re.compile(r'[^A-Za-z0-9+/=]+') +base64url_non_alphabet_re = re.compile(r'[^A-Za-z0-9---_=]+') + +_strip_formatting_re = re.compile(r'\s+') + +_base64_to_base64url_trans = string.maketrans('+/', '-_') +_base64url_to_base64_trans = string.maketrans('-_', '+/') + + +def _check_padding_length(pad): + if len(pad) != 1: + raise ValueError(_('pad must be single character')) + + +def is_valid_base64(text): + """Test if input text can be base64 decoded. + + :param text: input base64 text + :type text: string + :returns: bool -- True if text can be decoded as base64, False otherwise + """ + + text = filter_formatting(text) + + if base64_non_alphabet_re.search(text): + return False + + try: + return base64_is_padded(text) + except InvalidBase64Error: + return False + + +def is_valid_base64url(text): + """Test if input text can be base64url decoded. + + :param text: input base64 text + :type text: string + :returns: bool -- True if text can be decoded as base64url, + False otherwise + """ + + text = filter_formatting(text) + + if base64url_non_alphabet_re.search(text): + return False + + try: + return base64_is_padded(text) + except InvalidBase64Error: + return False + + +def filter_formatting(text): + """Return base64 text without any formatting, just the base64. + + Base64 text is often formatted with whitespace, line endings, + etc. This function strips out any formatting, the result will + contain only base64 characters. + + Note, this function does not filter out all non-base64 alphabet + characters, it only removes characters used for formatting. + + :param text: input text to filter + :type text: string + :returns: string -- filtered text without formatting + """ + return _strip_formatting_re.sub('', text) + + +def base64_to_base64url(text): + """Convert base64 text to base64url text. + + base64url text is designed to be safe for use in file names and + URL's. It is defined in RFC-4648 Section 5. + + base64url differs from base64 in the last two alphabet characters + at index 62 and 63, these are sometimes referred as the + altchars. The '+' character at index 62 is replaced by '-' + (hyphen) and the '/' character at index 63 is replaced by '_' + (underscore). + + This function only translates the altchars, non-alphabet + characters are not filtered out. + + WARNING:: + + base64url continues to use the '=' pad character which is NOT URL + safe. RFC-4648 suggests two alternate methods to deal with this: + + percent-encode + percent-encode the pad character (e.g. '=' becomes + '%3D'). This makes the base64url text fully safe. But + percent-encoding has the downside of requiring + percent-decoding prior to feeding the base64url text into a + base64url decoder since most base64url decoders do not + recognize %3D as a pad character and most decoders require + correct padding. + + no-padding + padding is not strictly necessary to decode base64 or + base64url text, the pad can be computed from the input text + length. However many decoders demand padding and will consider + non-padded text to be malformed. If one wants to omit the + trailing pad character(s) for use in URL's it can be added back + using the base64_assure_padding() function. + + This function makes no decisions about which padding methodology to + use. One can either call base64_strip_padding() to remove any pad + characters (restoring later with base64_assure_padding()) or call + base64url_percent_encode() to percent-encode the pad characters. + + :param text: input base64 text + :type text: string + :returns: string -- base64url text + """ + return text.translate(_base64_to_base64url_trans) + + +def base64url_to_base64(text): + """Convert base64url text to base64 text. + + See base64_to_base64url() for a description of base64url text and + it's issues. + + This function does NOT handle percent-encoded pad characters, they + will be left intact. If the input base64url text is + percent-encoded you should call + + :param text: text in base64url alphabet + :type text: string + :returns: string -- text in base64 alphabet + + """ + return text.translate(_base64url_to_base64_trans) + + +def base64_is_padded(text, pad='='): + """Test if the text is base64 padded. + + The input text must be in a base64 alphabet. The pad must be a + single character. If the text has been percent-encoded (e.g. pad + is the string '%3D') you must convert the text back to a base64 + alphabet (e.g. if percent-encoded use the function + base64url_percent_decode()). + + :param text: text containing ONLY characters in a base64 alphabet + :type text: string + :param pad: pad character (must be single character) (default: '=') + :type pad: string + :returns: bool -- True if padded, False otherwise + :raises: ValueError, InvalidBase64Error + """ + + _check_padding_length(pad) + + text_len = len(text) + if text_len > 0 and text_len % 4 == 0: + pad_index = text.find(pad) + if pad_index >= 0 and pad_index < text_len - 2: + raise InvalidBase64Error(_('text is multiple of 4, ' + 'but pad "%s" occurs before ' + '2nd to last char') % pad) + if pad_index == text_len - 2 and text[-1] != pad: + raise InvalidBase64Error(_('text is multiple of 4, ' + 'but pad "%s" occurs before ' + 'non-pad last char') % pad) + return True + + if text.find(pad) >= 0: + raise InvalidBase64Error(_('text is not a multiple of 4, ' + 'but contains pad "%s"') % pad) + return False + + +def base64url_percent_encode(text): + """Percent-encode base64url padding. + + The input text should only contain base64url alphabet + characters. Any non-base64url alphabet characters will also be + subject to percent-encoding. + + :param text: text containing ONLY characters in the base64url alphabet + :type text: string + :returns: string -- percent-encoded base64url text + :raises: InvalidBase64Error + """ + + if len(text) % 4 != 0: + raise InvalidBase64Error(_('padded base64url text must be ' + 'multiple of 4 characters')) + + return urllib.parse.quote(text) + + +def base64url_percent_decode(text): + """Percent-decode base64url padding. + + The input text should only contain base64url alphabet + characters and the percent-encoded pad character. Any other + percent-encoded characters will be subject to percent-decoding. + + :param text: base64url alphabet text + :type text: string + :returns: string -- percent-decoded base64url text + """ + + decoded_text = urllib.parse.unquote(text) + + if len(decoded_text) % 4 != 0: + raise InvalidBase64Error(_('padded base64url text must be ' + 'multiple of 4 characters')) + + return decoded_text + + +def base64_strip_padding(text, pad='='): + """Remove padding from input base64 text. + + :param text: text containing ONLY characters in a base64 alphabet + :type text: string + :param pad: pad character (must be single character) (default: '=') + :type pad: string + :returns: string -- base64 text without padding + :raises: ValueError + """ + _check_padding_length(pad) + + # Can't be padded if text is less than 4 characters. + if len(text) < 4: + return text + + if text[-1] == pad: + if text[-2] == pad: + return text[0:-2] + else: + return text[0:-1] + else: + return text + + +def base64_assure_padding(text, pad='='): + """Assure the input text ends with padding. + + Base64 text is normally expected to be a multiple of 4 + characters. Each 4 character base64 sequence produces 3 octets of + binary data. If the binary data is not a multiple of 3 the base64 + text is padded at the end with a pad character such that it is + always a multiple of 4. Padding is ignored and does not alter the + binary data nor it's length. + + In some circumstances it is desirable to omit the padding + character due to transport encoding conflicts. Base64 text can + still be correctly decoded if the length of the base64 text + (consisting only of characters in the desired base64 alphabet) is + known, padding is not absolutely necessary. + + Some base64 decoders demand correct padding or one may wish to + format RFC compliant base64, this function performs this action. + + Input is assumed to consist only of members of a base64 + alphabet (i.e no whitespace). Iteration yields a sequence of lines. + The line does NOT terminate with a line ending. + + Use the filter_formatting() function to assure the input text + contains only the members of the alphabet. + + If the text ends with the pad it is assumed to already be + padded. Otherwise the binary length is computed from the input + text length and correct number of pad characters are appended. + + :param text: text containing ONLY characters in a base64 alphabet + :type text: string + :param pad: pad character (must be single character) (default: '=') + :type pad: string + :returns: string -- input base64 text with padding + :raises: ValueError + """ + _check_padding_length(pad) + + if text.endswith(pad): + return text + + n = len(text) % 4 + if n == 0: + return text + + n = 4 - n + padding = pad * n + return text + padding + + +def base64_wrap_iter(text, width=64): + """Fold text into lines of text with max line length. + + Input is assumed to consist only of members of a base64 + alphabet (i.e no whitespace). Iteration yields a sequence of lines. + The line does NOT terminate with a line ending. + + Use the filter_formatting() function to assure the input text + contains only the members of the alphabet. + + :param text: text containing ONLY characters in a base64 alphabet + :type text: string + :param width: number of characters in each wrapped line (default: 64) + :type width: int + :returns: generator -- sequence of lines of base64 text. + """ + + text = six.text_type(text) + for x in six.moves.range(0, len(text), width): + yield text[x:x + width] + + +def base64_wrap(text, width=64): + """Fold text into lines of text with max line length. + + Input is assumed to consist only of members of a base64 + alphabet (i.e no whitespace). Fold the text into lines whose + line length is width chars long, terminate each line with line + ending (default is '\\n'). Return the wrapped text as a single + string. + + Use the filter_formatting() function to assure the input text + contains only the members of the alphabet. + + :param text: text containing ONLY characters in a base64 alphabet + :type text: string + :param width: number of characters in each wrapped line (default: 64) + :type width: int + :returns: string -- wrapped text. + """ + + buf = six.StringIO() + + for line in base64_wrap_iter(text, width): + buf.write(line) + buf.write(u'\n') + + text = buf.getvalue() + buf.close() + return text diff --git a/keystone-moon/keystone/common/cache/__init__.py b/keystone-moon/keystone/common/cache/__init__.py new file mode 100644 index 00000000..49502399 --- /dev/null +++ b/keystone-moon/keystone/common/cache/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2013 Metacloud +# +# 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 keystone.common.cache.core import * # noqa diff --git a/keystone-moon/keystone/common/cache/_memcache_pool.py b/keystone-moon/keystone/common/cache/_memcache_pool.py new file mode 100644 index 00000000..b15332db --- /dev/null +++ b/keystone-moon/keystone/common/cache/_memcache_pool.py @@ -0,0 +1,233 @@ +# Copyright 2014 Mirantis Inc +# All Rights Reserved. +# +# 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. + +"""Thread-safe connection pool for python-memcached.""" + +# NOTE(yorik-sar): this file is copied between keystone and keystonemiddleware +# and should be kept in sync until we can use external library for this. + +import collections +import contextlib +import itertools +import logging +import threading +import time + +import memcache +from oslo_log import log +from six.moves import queue + +from keystone import exception +from keystone.i18n import _ + + +LOG = log.getLogger(__name__) + +# This 'class' is taken from http://stackoverflow.com/a/22520633/238308 +# Don't inherit client from threading.local so that we can reuse clients in +# different threads +_MemcacheClient = type('_MemcacheClient', (object,), + dict(memcache.Client.__dict__)) + +_PoolItem = collections.namedtuple('_PoolItem', ['ttl', 'connection']) + + +class ConnectionPool(queue.Queue): + """Base connection pool class + + This class implements the basic connection pool logic as an abstract base + class. + """ + def __init__(self, maxsize, unused_timeout, conn_get_timeout=None): + """Initialize the connection pool. + + :param maxsize: maximum number of client connections for the pool + :type maxsize: int + :param unused_timeout: idle time to live for unused clients (in + seconds). If a client connection object has been + in the pool and idle for longer than the + unused_timeout, it will be reaped. This is to + ensure resources are released as utilization + goes down. + :type unused_timeout: int + :param conn_get_timeout: maximum time in seconds to wait for a + connection. If set to `None` timeout is + indefinite. + :type conn_get_timeout: int + """ + # super() cannot be used here because Queue in stdlib is an + # old-style class + queue.Queue.__init__(self, maxsize) + self._unused_timeout = unused_timeout + self._connection_get_timeout = conn_get_timeout + self._acquired = 0 + + def _create_connection(self): + """Returns a connection instance. + + This is called when the pool needs another instance created. + + :returns: a new connection instance + + """ + raise NotImplementedError + + def _destroy_connection(self, conn): + """Destroy and cleanup a connection instance. + + This is called when the pool wishes to get rid of an existing + connection. This is the opportunity for a subclass to free up + resources and cleaup after itself. + + :param conn: the connection object to destroy + + """ + raise NotImplementedError + + def _debug_logger(self, msg, *args, **kwargs): + if LOG.isEnabledFor(logging.DEBUG): + thread_id = threading.current_thread().ident + args = (id(self), thread_id) + args + prefix = 'Memcached pool %s, thread %s: ' + LOG.debug(prefix + msg, *args, **kwargs) + + @contextlib.contextmanager + def acquire(self): + self._debug_logger('Acquiring connection') + try: + conn = self.get(timeout=self._connection_get_timeout) + except queue.Empty: + raise exception.UnexpectedError( + _('Unable to get a connection from pool id %(id)s after ' + '%(seconds)s seconds.') % + {'id': id(self), 'seconds': self._connection_get_timeout}) + self._debug_logger('Acquired connection %s', id(conn)) + try: + yield conn + finally: + self._debug_logger('Releasing connection %s', id(conn)) + self._drop_expired_connections() + try: + # super() cannot be used here because Queue in stdlib is an + # old-style class + queue.Queue.put(self, conn, block=False) + except queue.Full: + self._debug_logger('Reaping exceeding connection %s', id(conn)) + self._destroy_connection(conn) + + def _qsize(self): + if self.maxsize: + return self.maxsize - self._acquired + else: + # A value indicating there is always a free connection + # if maxsize is None or 0 + return 1 + + # NOTE(dstanek): stdlib and eventlet Queue implementations + # have different names for the qsize method. This ensures + # that we override both of them. + if not hasattr(queue.Queue, '_qsize'): + qsize = _qsize + + def _get(self): + if self.queue: + conn = self.queue.pop().connection + else: + conn = self._create_connection() + self._acquired += 1 + return conn + + def _drop_expired_connections(self): + """Drop all expired connections from the right end of the queue.""" + now = time.time() + while self.queue and self.queue[0].ttl < now: + conn = self.queue.popleft().connection + self._debug_logger('Reaping connection %s', id(conn)) + self._destroy_connection(conn) + + def _put(self, conn): + self.queue.append(_PoolItem( + ttl=time.time() + self._unused_timeout, + connection=conn, + )) + self._acquired -= 1 + + +class MemcacheClientPool(ConnectionPool): + def __init__(self, urls, arguments, **kwargs): + # super() cannot be used here because Queue in stdlib is an + # old-style class + ConnectionPool.__init__(self, **kwargs) + self.urls = urls + self._arguments = arguments + # NOTE(morganfainberg): The host objects expect an int for the + # deaduntil value. Initialize this at 0 for each host with 0 indicating + # the host is not dead. + self._hosts_deaduntil = [0] * len(urls) + + def _create_connection(self): + return _MemcacheClient(self.urls, **self._arguments) + + def _destroy_connection(self, conn): + conn.disconnect_all() + + def _get(self): + # super() cannot be used here because Queue in stdlib is an + # old-style class + conn = ConnectionPool._get(self) + try: + # Propagate host state known to us to this client's list + now = time.time() + for deaduntil, host in zip(self._hosts_deaduntil, conn.servers): + if deaduntil > now and host.deaduntil <= now: + host.mark_dead('propagating death mark from the pool') + host.deaduntil = deaduntil + except Exception: + # We need to be sure that connection doesn't leak from the pool. + # This code runs before we enter context manager's try-finally + # block, so we need to explicitly release it here. + # super() cannot be used here because Queue in stdlib is an + # old-style class + ConnectionPool._put(self, conn) + raise + return conn + + def _put(self, conn): + try: + # If this client found that one of the hosts is dead, mark it as + # such in our internal list + now = time.time() + for i, host in zip(itertools.count(), conn.servers): + deaduntil = self._hosts_deaduntil[i] + # Do nothing if we already know this host is dead + if deaduntil <= now: + if host.deaduntil > now: + self._hosts_deaduntil[i] = host.deaduntil + self._debug_logger( + 'Marked host %s dead until %s', + self.urls[i], host.deaduntil) + else: + self._hosts_deaduntil[i] = 0 + # If all hosts are dead we should forget that they're dead. This + # way we won't get completely shut off until dead_retry seconds + # pass, but will be checking servers as frequent as we can (over + # way smaller socket_timeout) + if all(deaduntil > now for deaduntil in self._hosts_deaduntil): + self._debug_logger('All hosts are dead. Marking them as live.') + self._hosts_deaduntil[:] = [0] * len(self._hosts_deaduntil) + finally: + # super() cannot be used here because Queue in stdlib is an + # old-style class + ConnectionPool._put(self, conn) diff --git a/keystone-moon/keystone/common/cache/backends/__init__.py b/keystone-moon/keystone/common/cache/backends/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/common/cache/backends/memcache_pool.py b/keystone-moon/keystone/common/cache/backends/memcache_pool.py new file mode 100644 index 00000000..f3990b12 --- /dev/null +++ b/keystone-moon/keystone/common/cache/backends/memcache_pool.py @@ -0,0 +1,61 @@ +# Copyright 2014 Mirantis Inc +# All Rights Reserved. +# +# 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. + +"""dogpile.cache backend that uses Memcached connection pool""" + +import functools +import logging + +from dogpile.cache.backends import memcached as memcached_backend + +from keystone.common.cache import _memcache_pool + + +LOG = logging.getLogger(__name__) + + +# Helper to ease backend refactoring +class ClientProxy(object): + def __init__(self, client_pool): + self.client_pool = client_pool + + def _run_method(self, __name, *args, **kwargs): + with self.client_pool.acquire() as client: + return getattr(client, __name)(*args, **kwargs) + + def __getattr__(self, name): + return functools.partial(self._run_method, name) + + +class PooledMemcachedBackend(memcached_backend.MemcachedBackend): + # Composed from GenericMemcachedBackend's and MemcacheArgs's __init__ + def __init__(self, arguments): + super(PooledMemcachedBackend, self).__init__(arguments) + self.client_pool = _memcache_pool.MemcacheClientPool( + self.url, + arguments={ + 'dead_retry': arguments.get('dead_retry', 5 * 60), + 'socket_timeout': arguments.get('socket_timeout', 3), + }, + maxsize=arguments.get('pool_maxsize', 10), + unused_timeout=arguments.get('pool_unused_timeout', 60), + conn_get_timeout=arguments.get('pool_connection_get_timeout', 10), + ) + + # Since all methods in backend just call one of methods of client, this + # lets us avoid need to hack it too much + @property + def client(self): + return ClientProxy(self.client_pool) diff --git a/keystone-moon/keystone/common/cache/backends/mongo.py b/keystone-moon/keystone/common/cache/backends/mongo.py new file mode 100644 index 00000000..b5de9bc4 --- /dev/null +++ b/keystone-moon/keystone/common/cache/backends/mongo.py @@ -0,0 +1,557 @@ +# Copyright 2014 Hewlett-Packard Development Company, L.P. +# +# 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 abc +import datetime + +from dogpile.cache import api +from dogpile.cache import util as dp_util +from oslo_log import log +from oslo_utils import importutils +from oslo_utils import timeutils +import six + +from keystone import exception +from keystone.i18n import _, _LW + + +NO_VALUE = api.NO_VALUE +LOG = log.getLogger(__name__) + + +class MongoCacheBackend(api.CacheBackend): + """A MongoDB based caching backend implementing dogpile backend APIs. + + Arguments accepted in the arguments dictionary: + + :param db_hosts: string (required), hostname or IP address of the + MongoDB server instance. This can be a single MongoDB connection URI, + or a list of MongoDB connection URIs. + + :param db_name: string (required), the name of the database to be used. + + :param cache_collection: string (required), the name of collection to store + cached data. + *Note:* Different collection name can be provided if there is need to + create separate container (i.e. collection) for cache data. So region + configuration is done per collection. + + Following are optional parameters for MongoDB backend configuration, + + :param username: string, the name of the user to authenticate. + + :param password: string, the password of the user to authenticate. + + :param max_pool_size: integer, the maximum number of connections that the + pool will open simultaneously. By default the pool size is 10. + + :param w: integer, write acknowledgement for MongoDB client + + If not provided, then no default is set on MongoDB and then write + acknowledgement behavior occurs as per MongoDB default. This parameter + name is same as what is used in MongoDB docs. This value is specified + at collection level so its applicable to `cache_collection` db write + operations. + + If this is a replica set, write operations will block until they have + been replicated to the specified number or tagged set of servers. + Setting w=0 disables write acknowledgement and all other write concern + options. + + :param read_preference: string, the read preference mode for MongoDB client + Expected value is ``primary``, ``primaryPreferred``, ``secondary``, + ``secondaryPreferred``, or ``nearest``. This read_preference is + specified at collection level so its applicable to `cache_collection` + db read operations. + + :param use_replica: boolean, flag to indicate if replica client to be + used. Default is `False`. `replicaset_name` value is required if + `True`. + + :param replicaset_name: string, name of replica set. + Becomes required if `use_replica` is `True` + + :param son_manipulator: string, name of class with module name which + implements MongoDB SONManipulator. + Default manipulator used is :class:`.BaseTransform`. + + This manipulator is added per database. In multiple cache + configurations, the manipulator name should be same if same + database name ``db_name`` is used in those configurations. + + SONManipulator is used to manipulate custom data types as they are + saved or retrieved from MongoDB. Custom impl is only needed if cached + data is custom class and needs transformations when saving or reading + from db. If dogpile cached value contains built-in data types, then + BaseTransform class is sufficient as it already handles dogpile + CachedValue class transformation. + + :param mongo_ttl_seconds: integer, interval in seconds to indicate maximum + time-to-live value. + If value is greater than 0, then its assumed that cache_collection + needs to be TTL type (has index at 'doc_date' field). + By default, the value is -1 and its disabled. + Reference: + + .. NOTE:: + + This parameter is different from Dogpile own + expiration_time, which is the number of seconds after which Dogpile + will consider the value to be expired. When Dogpile considers a + value to be expired, it continues to use the value until generation + of a new value is complete, when using CacheRegion.get_or_create(). + Therefore, if you are setting `mongo_ttl_seconds`, you will want to + make sure it is greater than expiration_time by at least enough + seconds for new values to be generated, else the value would not + be available during a regeneration, forcing all threads to wait for + a regeneration each time a value expires. + + :param ssl: boolean, If True, create the connection to the server + using SSL. Default is `False`. Client SSL connection parameters depends + on server side SSL setup. For further reference on SSL configuration: + + + :param ssl_keyfile: string, the private keyfile used to identify the + local connection against mongod. If included with the certfile then + only the `ssl_certfile` is needed. Used only when `ssl` is `True`. + + :param ssl_certfile: string, the certificate file used to identify the + local connection against mongod. Used only when `ssl` is `True`. + + :param ssl_ca_certs: string, the ca_certs file contains a set of + concatenated 'certification authority' certificates, which are used to + validate certificates passed from the other end of the connection. + Used only when `ssl` is `True`. + + :param ssl_cert_reqs: string, the parameter cert_reqs specifies whether + a certificate is required from the other side of the connection, and + whether it will be validated if provided. It must be one of the three + values ``ssl.CERT_NONE`` (certificates ignored), ``ssl.CERT_OPTIONAL`` + (not required, but validated if provided), or + ``ssl.CERT_REQUIRED`` (required and validated). If the value of this + parameter is not ``ssl.CERT_NONE``, then the ssl_ca_certs parameter + must point to a file of CA certificates. Used only when `ssl` + is `True`. + + Rest of arguments are passed to mongo calls for read, write and remove. + So related options can be specified to pass to these operations. + + Further details of various supported arguments can be referred from + + + """ + + def __init__(self, arguments): + self.api = MongoApi(arguments) + + @dp_util.memoized_property + def client(self): + """Initializes MongoDB connection and collection defaults. + + This initialization is done only once and performed as part of lazy + inclusion of MongoDB dependency i.e. add imports only if related + backend is used. + + :return: :class:`.MongoApi` instance + """ + self.api.get_cache_collection() + return self.api + + def get(self, key): + value = self.client.get(key) + if value is None: + return NO_VALUE + else: + return value + + def get_multi(self, keys): + values = self.client.get_multi(keys) + return [ + NO_VALUE if key not in values + else values[key] for key in keys + ] + + def set(self, key, value): + self.client.set(key, value) + + def set_multi(self, mapping): + self.client.set_multi(mapping) + + def delete(self, key): + self.client.delete(key) + + def delete_multi(self, keys): + self.client.delete_multi(keys) + + +class MongoApi(object): + """Class handling MongoDB specific functionality. + + This class uses PyMongo APIs internally to create database connection + with configured pool size, ensures unique index on key, does database + authentication and ensure TTL collection index if configured so. + This class also serves as handle to cache collection for dogpile cache + APIs. + + In a single deployment, multiple cache configuration can be defined. In + that case of multiple cache collections usage, db client connection pool + is shared when cache collections are within same database. + """ + + # class level attributes for re-use of db client connection and collection + _DB = {} # dict of db_name: db connection reference + _MONGO_COLLS = {} # dict of cache_collection : db collection reference + + def __init__(self, arguments): + self._init_args(arguments) + self._data_manipulator = None + + def _init_args(self, arguments): + """Helper logic for collecting and parsing MongoDB specific arguments. + + The arguments passed in are separated out in connection specific + setting and rest of arguments are passed to create/update/delete + db operations. + """ + self.conn_kwargs = {} # connection specific arguments + + self.hosts = arguments.pop('db_hosts', None) + if self.hosts is None: + msg = _('db_hosts value is required') + raise exception.ValidationError(message=msg) + + self.db_name = arguments.pop('db_name', None) + if self.db_name is None: + msg = _('database db_name is required') + raise exception.ValidationError(message=msg) + + self.cache_collection = arguments.pop('cache_collection', None) + if self.cache_collection is None: + msg = _('cache_collection name is required') + raise exception.ValidationError(message=msg) + + self.username = arguments.pop('username', None) + self.password = arguments.pop('password', None) + self.max_pool_size = arguments.pop('max_pool_size', 10) + + self.w = arguments.pop('w', -1) + try: + self.w = int(self.w) + except ValueError: + msg = _('integer value expected for w (write concern attribute)') + raise exception.ValidationError(message=msg) + + self.read_preference = arguments.pop('read_preference', None) + + self.use_replica = arguments.pop('use_replica', False) + if self.use_replica: + if arguments.get('replicaset_name') is None: + msg = _('replicaset_name required when use_replica is True') + raise exception.ValidationError(message=msg) + self.replicaset_name = arguments.get('replicaset_name') + + self.son_manipulator = arguments.pop('son_manipulator', None) + + # set if mongo collection needs to be TTL type. + # This needs to be max ttl for any cache entry. + # By default, -1 means don't use TTL collection. + # With ttl set, it creates related index and have doc_date field with + # needed expiration interval + self.ttl_seconds = arguments.pop('mongo_ttl_seconds', -1) + try: + self.ttl_seconds = int(self.ttl_seconds) + except ValueError: + msg = _('integer value expected for mongo_ttl_seconds') + raise exception.ValidationError(message=msg) + + self.conn_kwargs['ssl'] = arguments.pop('ssl', False) + if self.conn_kwargs['ssl']: + ssl_keyfile = arguments.pop('ssl_keyfile', None) + ssl_certfile = arguments.pop('ssl_certfile', None) + ssl_ca_certs = arguments.pop('ssl_ca_certs', None) + ssl_cert_reqs = arguments.pop('ssl_cert_reqs', None) + if ssl_keyfile: + self.conn_kwargs['ssl_keyfile'] = ssl_keyfile + if ssl_certfile: + self.conn_kwargs['ssl_certfile'] = ssl_certfile + if ssl_ca_certs: + self.conn_kwargs['ssl_ca_certs'] = ssl_ca_certs + if ssl_cert_reqs: + self.conn_kwargs['ssl_cert_reqs'] = ( + self._ssl_cert_req_type(ssl_cert_reqs)) + + # rest of arguments are passed to mongo crud calls + self.meth_kwargs = arguments + + def _ssl_cert_req_type(self, req_type): + try: + import ssl + except ImportError: + raise exception.ValidationError(_('no ssl support available')) + req_type = req_type.upper() + try: + return { + 'NONE': ssl.CERT_NONE, + 'OPTIONAL': ssl.CERT_OPTIONAL, + 'REQUIRED': ssl.CERT_REQUIRED + }[req_type] + except KeyError: + msg = _('Invalid ssl_cert_reqs value of %s, must be one of ' + '"NONE", "OPTIONAL", "REQUIRED"') % (req_type) + raise exception.ValidationError(message=msg) + + def _get_db(self): + # defer imports until backend is used + global pymongo + import pymongo + if self.use_replica: + connection = pymongo.MongoReplicaSetClient( + host=self.hosts, replicaSet=self.replicaset_name, + max_pool_size=self.max_pool_size, **self.conn_kwargs) + else: # used for standalone node or mongos in sharded setup + connection = pymongo.MongoClient( + host=self.hosts, max_pool_size=self.max_pool_size, + **self.conn_kwargs) + + database = getattr(connection, self.db_name) + + self._assign_data_mainpulator() + database.add_son_manipulator(self._data_manipulator) + if self.username and self.password: + database.authenticate(self.username, self.password) + return database + + def _assign_data_mainpulator(self): + if self._data_manipulator is None: + if self.son_manipulator: + self._data_manipulator = importutils.import_object( + self.son_manipulator) + else: + self._data_manipulator = BaseTransform() + + def _get_doc_date(self): + if self.ttl_seconds > 0: + expire_delta = datetime.timedelta(seconds=self.ttl_seconds) + doc_date = timeutils.utcnow() + expire_delta + else: + doc_date = timeutils.utcnow() + return doc_date + + def get_cache_collection(self): + if self.cache_collection not in self._MONGO_COLLS: + global pymongo + import pymongo + # re-use db client connection if already defined as part of + # earlier dogpile cache configuration + if self.db_name not in self._DB: + self._DB[self.db_name] = self._get_db() + coll = getattr(self._DB[self.db_name], self.cache_collection) + + self._assign_data_mainpulator() + if self.read_preference: + self.read_preference = pymongo.read_preferences.mongos_enum( + self.read_preference) + coll.read_preference = self.read_preference + if self.w > -1: + coll.write_concern['w'] = self.w + if self.ttl_seconds > 0: + kwargs = {'expireAfterSeconds': self.ttl_seconds} + coll.ensure_index('doc_date', cache_for=5, **kwargs) + else: + self._validate_ttl_index(coll, self.cache_collection, + self.ttl_seconds) + self._MONGO_COLLS[self.cache_collection] = coll + + return self._MONGO_COLLS[self.cache_collection] + + def _get_cache_entry(self, key, value, meta, doc_date): + """MongoDB cache data representation. + + Storing cache key as ``_id`` field as MongoDB by default creates + unique index on this field. So no need to create separate field and + index for storing cache key. Cache data has additional ``doc_date`` + field for MongoDB TTL collection support. + """ + return dict(_id=key, value=value, meta=meta, doc_date=doc_date) + + def _validate_ttl_index(self, collection, coll_name, ttl_seconds): + """Checks if existing TTL index is removed on a collection. + + This logs warning when existing collection has TTL index defined and + new cache configuration tries to disable index with + ``mongo_ttl_seconds < 0``. In that case, existing index needs + to be addressed first to make new configuration effective. + Refer to MongoDB documentation around TTL index for further details. + """ + indexes = collection.index_information() + for indx_name, index_data in six.iteritems(indexes): + if all(k in index_data for k in ('key', 'expireAfterSeconds')): + existing_value = index_data['expireAfterSeconds'] + fld_present = 'doc_date' in index_data['key'][0] + if fld_present and existing_value > -1 and ttl_seconds < 1: + msg = _LW('TTL index already exists on db collection ' + '<%(c_name)s>, remove index <%(indx_name)s> ' + 'first to make updated mongo_ttl_seconds value ' + 'to be effective') + LOG.warn(msg, {'c_name': coll_name, + 'indx_name': indx_name}) + + def get(self, key): + critieria = {'_id': key} + result = self.get_cache_collection().find_one(spec_or_id=critieria, + **self.meth_kwargs) + if result: + return result['value'] + else: + return None + + def get_multi(self, keys): + db_results = self._get_results_as_dict(keys) + return {doc['_id']: doc['value'] for doc in six.itervalues(db_results)} + + def _get_results_as_dict(self, keys): + critieria = {'_id': {'$in': keys}} + db_results = self.get_cache_collection().find(spec=critieria, + **self.meth_kwargs) + return {doc['_id']: doc for doc in db_results} + + def set(self, key, value): + doc_date = self._get_doc_date() + ref = self._get_cache_entry(key, value.payload, value.metadata, + doc_date) + spec = {'_id': key} + # find and modify does not have manipulator support + # so need to do conversion as part of input document + ref = self._data_manipulator.transform_incoming(ref, self) + self.get_cache_collection().find_and_modify(spec, ref, upsert=True, + **self.meth_kwargs) + + def set_multi(self, mapping): + """Insert multiple documents specified as key, value pairs. + + In this case, multiple documents can be added via insert provided they + do not exist. + Update of multiple existing documents is done one by one + """ + doc_date = self._get_doc_date() + insert_refs = [] + update_refs = [] + existing_docs = self._get_results_as_dict(mapping.keys()) + for key, value in mapping.items(): + ref = self._get_cache_entry(key, value.payload, value.metadata, + doc_date) + if key in existing_docs: + ref['_id'] = existing_docs[key]['_id'] + update_refs.append(ref) + else: + insert_refs.append(ref) + if insert_refs: + self.get_cache_collection().insert(insert_refs, manipulate=True, + **self.meth_kwargs) + for upd_doc in update_refs: + self.get_cache_collection().save(upd_doc, manipulate=True, + **self.meth_kwargs) + + def delete(self, key): + critieria = {'_id': key} + self.get_cache_collection().remove(spec_or_id=critieria, + **self.meth_kwargs) + + def delete_multi(self, keys): + critieria = {'_id': {'$in': keys}} + self.get_cache_collection().remove(spec_or_id=critieria, + **self.meth_kwargs) + + +@six.add_metaclass(abc.ABCMeta) +class AbstractManipulator(object): + """Abstract class with methods which need to be implemented for custom + manipulation. + + Adding this as a base class for :class:`.BaseTransform` instead of adding + import dependency of pymongo specific class i.e. + `pymongo.son_manipulator.SONManipulator` and using that as base class. + This is done to avoid pymongo dependency if MongoDB backend is not used. + """ + @abc.abstractmethod + def transform_incoming(self, son, collection): + """Used while saving data to MongoDB. + + :param son: the SON object to be inserted into the database + :param collection: the collection the object is being inserted into + + :returns: transformed SON object + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def transform_outgoing(self, son, collection): + """Used while reading data from MongoDB. + + :param son: the SON object being retrieved from the database + :param collection: the collection this object was stored in + + :returns: transformed SON object + """ + raise exception.NotImplemented() # pragma: no cover + + def will_copy(self): + """Will this SON manipulator make a copy of the incoming document? + + Derived classes that do need to make a copy should override this + method, returning `True` instead of `False`. + + :returns: boolean + """ + return False + + +class BaseTransform(AbstractManipulator): + """Base transformation class to store and read dogpile cached data + from MongoDB. + + This is needed as dogpile internally stores data as a custom class + i.e. dogpile.cache.api.CachedValue + + Note: Custom manipulator needs to always override ``transform_incoming`` + and ``transform_outgoing`` methods. MongoDB manipulator logic specifically + checks that overridden method in instance and its super are different. + """ + + def transform_incoming(self, son, collection): + """Used while saving data to MongoDB.""" + for (key, value) in son.items(): + if isinstance(value, api.CachedValue): + son[key] = value.payload # key is 'value' field here + son['meta'] = value.metadata + elif isinstance(value, dict): # Make sure we recurse into sub-docs + son[key] = self.transform_incoming(value, collection) + return son + + def transform_outgoing(self, son, collection): + """Used while reading data from MongoDB.""" + metadata = None + # make sure its top level dictionary with all expected fields names + # present + if isinstance(son, dict) and all(k in son for k in + ('_id', 'value', 'meta', 'doc_date')): + payload = son.pop('value', None) + metadata = son.pop('meta', None) + for (key, value) in son.items(): + if isinstance(value, dict): + son[key] = self.transform_outgoing(value, collection) + if metadata is not None: + son['value'] = api.CachedValue(payload, metadata) + return son diff --git a/keystone-moon/keystone/common/cache/backends/noop.py b/keystone-moon/keystone/common/cache/backends/noop.py new file mode 100644 index 00000000..38329c94 --- /dev/null +++ b/keystone-moon/keystone/common/cache/backends/noop.py @@ -0,0 +1,49 @@ +# Copyright 2013 Metacloud +# +# 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 dogpile.cache import api + + +NO_VALUE = api.NO_VALUE + + +class NoopCacheBackend(api.CacheBackend): + """A no op backend as a default caching backend. + + The no op backend is provided as the default caching backend for keystone + to ensure that ``dogpile.cache.memory`` is not used in any real-world + circumstances unintentionally. ``dogpile.cache.memory`` does not have a + mechanism to cleanup it's internal dict and therefore could cause run-away + memory utilization. + """ + def __init__(self, *args): + return + + def get(self, key): + return NO_VALUE + + def get_multi(self, keys): + return [NO_VALUE for x in keys] + + def set(self, key, value): + return + + def set_multi(self, mapping): + return + + def delete(self, key): + return + + def delete_multi(self, keys): + return diff --git a/keystone-moon/keystone/common/cache/core.py b/keystone-moon/keystone/common/cache/core.py new file mode 100644 index 00000000..306587b3 --- /dev/null +++ b/keystone-moon/keystone/common/cache/core.py @@ -0,0 +1,308 @@ +# Copyright 2013 Metacloud +# +# 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. + +"""Keystone Caching Layer Implementation.""" + +import dogpile.cache +from dogpile.cache import proxy +from dogpile.cache import util +from oslo_config import cfg +from oslo_log import log +from oslo_utils import importutils + +from keystone import exception +from keystone.i18n import _, _LE + + +CONF = cfg.CONF +LOG = log.getLogger(__name__) + +make_region = dogpile.cache.make_region + +dogpile.cache.register_backend( + 'keystone.common.cache.noop', + 'keystone.common.cache.backends.noop', + 'NoopCacheBackend') + +dogpile.cache.register_backend( + 'keystone.cache.mongo', + 'keystone.common.cache.backends.mongo', + 'MongoCacheBackend') + +dogpile.cache.register_backend( + 'keystone.cache.memcache_pool', + 'keystone.common.cache.backends.memcache_pool', + 'PooledMemcachedBackend') + + +class DebugProxy(proxy.ProxyBackend): + """Extra Logging ProxyBackend.""" + # NOTE(morganfainberg): Pass all key/values through repr to ensure we have + # a clean description of the information. Without use of repr, it might + # be possible to run into encode/decode error(s). For logging/debugging + # purposes encode/decode is irrelevant and we should be looking at the + # data exactly as it stands. + + def get(self, key): + value = self.proxied.get(key) + LOG.debug('CACHE_GET: Key: "%(key)r" Value: "%(value)r"', + {'key': key, 'value': value}) + return value + + def get_multi(self, keys): + values = self.proxied.get_multi(keys) + LOG.debug('CACHE_GET_MULTI: "%(keys)r" Values: "%(values)r"', + {'keys': keys, 'values': values}) + return values + + def set(self, key, value): + LOG.debug('CACHE_SET: Key: "%(key)r" Value: "%(value)r"', + {'key': key, 'value': value}) + return self.proxied.set(key, value) + + def set_multi(self, keys): + LOG.debug('CACHE_SET_MULTI: "%r"', keys) + self.proxied.set_multi(keys) + + def delete(self, key): + self.proxied.delete(key) + LOG.debug('CACHE_DELETE: "%r"', key) + + def delete_multi(self, keys): + LOG.debug('CACHE_DELETE_MULTI: "%r"', keys) + self.proxied.delete_multi(keys) + + +def build_cache_config(): + """Build the cache region dictionary configuration. + + :returns: dict + """ + prefix = CONF.cache.config_prefix + conf_dict = {} + conf_dict['%s.backend' % prefix] = CONF.cache.backend + conf_dict['%s.expiration_time' % prefix] = CONF.cache.expiration_time + for argument in CONF.cache.backend_argument: + try: + (argname, argvalue) = argument.split(':', 1) + except ValueError: + msg = _LE('Unable to build cache config-key. Expected format ' + '":". Skipping unknown format: %s') + LOG.error(msg, argument) + continue + + arg_key = '.'.join([prefix, 'arguments', argname]) + conf_dict[arg_key] = argvalue + + LOG.debug('Keystone Cache Config: %s', conf_dict) + # NOTE(yorik-sar): these arguments will be used for memcache-related + # backends. Use setdefault for url to support old-style setting through + # backend_argument=url:127.0.0.1:11211 + conf_dict.setdefault('%s.arguments.url' % prefix, + CONF.cache.memcache_servers) + for arg in ('dead_retry', 'socket_timeout', 'pool_maxsize', + 'pool_unused_timeout', 'pool_connection_get_timeout'): + value = getattr(CONF.cache, 'memcache_' + arg) + conf_dict['%s.arguments.%s' % (prefix, arg)] = value + + return conf_dict + + +def configure_cache_region(region): + """Configure a cache region. + + :param region: optional CacheRegion object, if not provided a new region + will be instantiated + :raises: exception.ValidationError + :returns: dogpile.cache.CacheRegion + """ + if not isinstance(region, dogpile.cache.CacheRegion): + raise exception.ValidationError( + _('region not type dogpile.cache.CacheRegion')) + + if not region.is_configured: + # NOTE(morganfainberg): this is how you tell if a region is configured. + # There is a request logged with dogpile.cache upstream to make this + # easier / less ugly. + + config_dict = build_cache_config() + region.configure_from_config(config_dict, + '%s.' % CONF.cache.config_prefix) + + if CONF.cache.debug_cache_backend: + region.wrap(DebugProxy) + + # NOTE(morganfainberg): if the backend requests the use of a + # key_mangler, we should respect that key_mangler function. If a + # key_mangler is not defined by the backend, use the sha1_mangle_key + # mangler provided by dogpile.cache. This ensures we always use a fixed + # size cache-key. + if region.key_mangler is None: + region.key_mangler = util.sha1_mangle_key + + for class_path in CONF.cache.proxies: + # NOTE(morganfainberg): if we have any proxy wrappers, we should + # ensure they are added to the cache region's backend. Since + # configure_from_config doesn't handle the wrap argument, we need + # to manually add the Proxies. For information on how the + # ProxyBackends work, see the dogpile.cache documents on + # "changing-backend-behavior" + cls = importutils.import_class(class_path) + LOG.debug("Adding cache-proxy '%s' to backend.", class_path) + region.wrap(cls) + + return region + + +def get_should_cache_fn(section): + """Build a function that returns a config section's caching status. + + For any given driver in keystone that has caching capabilities, a boolean + config option for that driver's section (e.g. ``token``) should exist and + default to ``True``. This function will use that value to tell the caching + decorator if caching for that driver is enabled. To properly use this + with the decorator, pass this function the configuration section and assign + the result to a variable. Pass the new variable to the caching decorator + as the named argument ``should_cache_fn``. e.g.:: + + from keystone.common import cache + + SHOULD_CACHE = cache.get_should_cache_fn('token') + + @cache.on_arguments(should_cache_fn=SHOULD_CACHE) + def function(arg1, arg2): + ... + + :param section: name of the configuration section to examine + :type section: string + :returns: function reference + """ + def should_cache(value): + if not CONF.cache.enabled: + return False + conf_group = getattr(CONF, section) + return getattr(conf_group, 'caching', True) + return should_cache + + +def get_expiration_time_fn(section): + """Build a function that returns a config section's expiration time status. + + For any given driver in keystone that has caching capabilities, an int + config option called ``cache_time`` for that driver's section + (e.g. ``token``) should exist and typically default to ``None``. This + function will use that value to tell the caching decorator of the TTL + override for caching the resulting objects. If the value of the config + option is ``None`` the default value provided in the + ``[cache] expiration_time`` option will be used by the decorator. The + default may be set to something other than ``None`` in cases where the + caching TTL should not be tied to the global default(s) (e.g. + revocation_list changes very infrequently and can be cached for >1h by + default). + + To properly use this with the decorator, pass this function the + configuration section and assign the result to a variable. Pass the new + variable to the caching decorator as the named argument + ``expiration_time``. e.g.:: + + from keystone.common import cache + + EXPIRATION_TIME = cache.get_expiration_time_fn('token') + + @cache.on_arguments(expiration_time=EXPIRATION_TIME) + def function(arg1, arg2): + ... + + :param section: name of the configuration section to examine + :type section: string + :rtype: function reference + """ + def get_expiration_time(): + conf_group = getattr(CONF, section) + return getattr(conf_group, 'cache_time', None) + return get_expiration_time + + +def key_generate_to_str(s): + # NOTE(morganfainberg): Since we need to stringify all arguments, attempt + # to stringify and handle the Unicode error explicitly as needed. + try: + return str(s) + except UnicodeEncodeError: + return s.encode('utf-8') + + +def function_key_generator(namespace, fn, to_str=key_generate_to_str): + # NOTE(morganfainberg): This wraps dogpile.cache's default + # function_key_generator to change the default to_str mechanism. + return util.function_key_generator(namespace, fn, to_str=to_str) + + +REGION = dogpile.cache.make_region( + function_key_generator=function_key_generator) +on_arguments = REGION.cache_on_arguments + + +def get_memoization_decorator(section, expiration_section=None): + """Build a function based on the `on_arguments` decorator for the section. + + For any given driver in Keystone that has caching capabilities, a + pair of functions is required to properly determine the status of the + caching capabilities (a toggle to indicate caching is enabled and any + override of the default TTL for cached data). This function will return + an object that has the memoization decorator ``on_arguments`` + pre-configured for the driver. + + Example usage:: + + from keystone.common import cache + + MEMOIZE = cache.get_memoization_decorator(section='token') + + @MEMOIZE + def function(arg1, arg2): + ... + + + ALTERNATE_MEMOIZE = cache.get_memoization_decorator( + section='token', expiration_section='revoke') + + @ALTERNATE_MEMOIZE + def function2(arg1, arg2): + ... + + :param section: name of the configuration section to examine + :type section: string + :param expiration_section: name of the configuration section to examine + for the expiration option. This will fall back + to using ``section`` if the value is unspecified + or ``None`` + :type expiration_section: string + :rtype: function reference + """ + if expiration_section is None: + expiration_section = section + should_cache = get_should_cache_fn(section) + expiration_time = get_expiration_time_fn(expiration_section) + + memoize = REGION.cache_on_arguments(should_cache_fn=should_cache, + expiration_time=expiration_time) + + # Make sure the actual "should_cache" and "expiration_time" methods are + # available. This is potentially interesting/useful to pre-seed cache + # values. + memoize.should_cache = should_cache + memoize.get_expiration_time = expiration_time + + return memoize diff --git a/keystone-moon/keystone/common/config.py b/keystone-moon/keystone/common/config.py new file mode 100644 index 00000000..bcaedeef --- /dev/null +++ b/keystone-moon/keystone/common/config.py @@ -0,0 +1,1118 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 oslo_config import cfg +import oslo_messaging + + +_DEFAULT_AUTH_METHODS = ['external', 'password', 'token', 'oauth1'] +_CERTFILE = '/etc/keystone/ssl/certs/signing_cert.pem' +_KEYFILE = '/etc/keystone/ssl/private/signing_key.pem' +_SSO_CALLBACK = '/etc/keystone/sso_callback_template.html' + + +FILE_OPTIONS = { + None: [ + cfg.StrOpt('admin_token', secret=True, default='ADMIN', + help='A "shared secret" that can be used to bootstrap ' + 'Keystone. This "token" does not represent a user, ' + 'and carries no explicit authorization. To disable ' + 'in production (highly recommended), remove ' + 'AdminTokenAuthMiddleware from your paste ' + 'application pipelines (for example, in ' + 'keystone-paste.ini).'), + cfg.IntOpt('compute_port', default=8774, + help='(Deprecated) The port which the OpenStack Compute ' + 'service listens on. This option was only used for ' + 'string replacement in the templated catalog backend. ' + 'Templated catalogs should replace the ' + '"$(compute_port)s" substitution with the static port ' + 'of the compute service. As of Juno, this option is ' + 'deprecated and will be removed in the L release.'), + cfg.StrOpt('public_endpoint', + help='The base public endpoint URL for Keystone that is ' + 'advertised to clients (NOTE: this does NOT affect ' + 'how Keystone listens for connections). ' + 'Defaults to the base host URL of the request. E.g. a ' + 'request to http://server:5000/v3/users will ' + 'default to http://server:5000. You should only need ' + 'to set this value if the base URL contains a path ' + '(e.g. /prefix/v3) or the endpoint should be found ' + 'on a different server.'), + cfg.StrOpt('admin_endpoint', + help='The base admin endpoint URL for Keystone that is ' + 'advertised to clients (NOTE: this does NOT affect ' + 'how Keystone listens for connections). ' + 'Defaults to the base host URL of the request. E.g. a ' + 'request to http://server:35357/v3/users will ' + 'default to http://server:35357. You should only need ' + 'to set this value if the base URL contains a path ' + '(e.g. /prefix/v3) or the endpoint should be found ' + 'on a different server.'), + cfg.IntOpt('max_project_tree_depth', default=5, + help='Maximum depth of the project hierarchy. WARNING: ' + 'setting it to a large value may adversely impact ' + 'performance.'), + cfg.IntOpt('max_param_size', default=64, + help='Limit the sizes of user & project ID/names.'), + # we allow tokens to be a bit larger to accommodate PKI + cfg.IntOpt('max_token_size', default=8192, + help='Similar to max_param_size, but provides an ' + 'exception for token values.'), + cfg.StrOpt('member_role_id', + default='9fe2ff9ee4384b1894a90878d3e92bab', + help='Similar to the member_role_name option, this ' + 'represents the default role ID used to associate ' + 'users with their default projects in the v2 API. ' + 'This will be used as the explicit role where one is ' + 'not specified by the v2 API.'), + cfg.StrOpt('member_role_name', default='_member_', + help='This is the role name used in combination with the ' + 'member_role_id option; see that option for more ' + 'detail.'), + cfg.IntOpt('crypt_strength', default=40000, + help='The value passed as the keyword "rounds" to ' + 'passlib\'s encrypt method.'), + cfg.IntOpt('list_limit', + help='The maximum number of entities that will be ' + 'returned in a collection, with no limit set by ' + 'default. This global limit may be then overridden ' + 'for a specific driver, by specifying a list_limit ' + 'in the appropriate section (e.g. [assignment]).'), + cfg.BoolOpt('domain_id_immutable', default=True, + help='Set this to false if you want to enable the ' + 'ability for user, group and project entities ' + 'to be moved between domains by updating their ' + 'domain_id. Allowing such movement is not ' + 'recommended if the scope of a domain admin is being ' + 'restricted by use of an appropriate policy file ' + '(see policy.v3cloudsample as an example).'), + cfg.BoolOpt('strict_password_check', default=False, + help='If set to true, strict password length checking is ' + 'performed for password manipulation. If a password ' + 'exceeds the maximum length, the operation will fail ' + 'with an HTTP 403 Forbidden error. If set to false, ' + 'passwords are automatically truncated to the ' + 'maximum length.'), + cfg.StrOpt('secure_proxy_ssl_header', + help='The HTTP header used to determine the scheme for the ' + 'original request, even if it was removed by an SSL ' + 'terminating proxy. Typical value is ' + '"HTTP_X_FORWARDED_PROTO".'), + ], + 'identity': [ + cfg.StrOpt('default_domain_id', default='default', + help='This references the domain to use for all ' + 'Identity API v2 requests (which are not aware of ' + 'domains). A domain with this ID will be created ' + 'for you by keystone-manage db_sync in migration ' + '008. The domain referenced by this ID cannot be ' + 'deleted on the v3 API, to prevent accidentally ' + 'breaking the v2 API. There is nothing special about ' + 'this domain, other than the fact that it must ' + 'exist to order to maintain support for your v2 ' + 'clients.'), + cfg.BoolOpt('domain_specific_drivers_enabled', + default=False, + help='A subset (or all) of domains can have their own ' + 'identity driver, each with their own partial ' + 'configuration options, stored in either the ' + 'resource backend or in a file in a domain ' + 'configuration directory (depending on the setting ' + 'of domain_configurations_from_database). Only ' + 'values specific to the domain need to be specified ' + 'in this manner. This feature is disabled by ' + 'default; set to true to enable.'), + cfg.BoolOpt('domain_configurations_from_database', + default=False, + help='Extract the domain specific configuration options ' + 'from the resource backend where they have been ' + 'stored with the domain data. This feature is ' + 'disabled by default (in which case the domain ' + 'specific options will be loaded from files in the ' + 'domain configuration directory); set to true to ' + 'enable.'), + cfg.StrOpt('domain_config_dir', + default='/etc/keystone/domains', + help='Path for Keystone to locate the domain specific ' + 'identity configuration files if ' + 'domain_specific_drivers_enabled is set to true.'), + cfg.StrOpt('driver', + default=('keystone.identity.backends' + '.sql.Identity'), + help='Identity backend driver.'), + cfg.BoolOpt('caching', default=True, + help='Toggle for identity caching. This has no ' + 'effect unless global caching is enabled.'), + cfg.IntOpt('cache_time', default=600, + help='Time to cache identity data (in seconds). This has ' + 'no effect unless global and identity caching are ' + 'enabled.'), + cfg.IntOpt('max_password_length', default=4096, + help='Maximum supported length for user passwords; ' + 'decrease to improve performance.'), + cfg.IntOpt('list_limit', + help='Maximum number of entities that will be returned in ' + 'an identity collection.'), + ], + 'identity_mapping': [ + cfg.StrOpt('driver', + default=('keystone.identity.mapping_backends' + '.sql.Mapping'), + help='Keystone Identity Mapping backend driver.'), + cfg.StrOpt('generator', + default=('keystone.identity.id_generators' + '.sha256.Generator'), + help='Public ID generator for user and group entities. ' + 'The Keystone identity mapper only supports ' + 'generators that produce no more than 64 characters.'), + cfg.BoolOpt('backward_compatible_ids', + default=True, + help='The format of user and group IDs changed ' + 'in Juno for backends that do not generate UUIDs ' + '(e.g. LDAP), with keystone providing a hash mapping ' + 'to the underlying attribute in LDAP. By default ' + 'this mapping is disabled, which ensures that ' + 'existing IDs will not change. Even when the ' + 'mapping is enabled by using domain specific ' + 'drivers, any users and groups from the default ' + 'domain being handled by LDAP will still not be ' + 'mapped to ensure their IDs remain backward ' + 'compatible. Setting this value to False will ' + 'enable the mapping for even the default LDAP ' + 'driver. It is only safe to do this if you do not ' + 'already have assignments for users and ' + 'groups from the default LDAP domain, and it is ' + 'acceptable for Keystone to provide the different ' + 'IDs to clients than it did previously. Typically ' + 'this means that the only time you can set this ' + 'value to False is when configuring a fresh ' + 'installation.'), + ], + 'trust': [ + cfg.BoolOpt('enabled', default=True, + help='Delegation and impersonation features can be ' + 'optionally disabled.'), + cfg.BoolOpt('allow_redelegation', default=False, + help='Enable redelegation feature.'), + cfg.IntOpt('max_redelegation_count', default=3, + help='Maximum depth of trust redelegation.'), + cfg.StrOpt('driver', + default='keystone.trust.backends.sql.Trust', + help='Trust backend driver.')], + 'os_inherit': [ + cfg.BoolOpt('enabled', default=False, + help='role-assignment inheritance to projects from ' + 'owning domain or from projects higher in the ' + 'hierarchy can be optionally enabled.'), + ], + 'fernet_tokens': [ + cfg.StrOpt('key_repository', + default='/etc/keystone/fernet-keys/', + help='Directory containing Fernet token keys.'), + cfg.IntOpt('max_active_keys', + default=3, + help='This controls how many keys are held in rotation by ' + 'keystone-manage fernet_rotate before they are ' + 'discarded. The default value of 3 means that ' + 'keystone will maintain one staged key, one primary ' + 'key, and one secondary key. Increasing this value ' + 'means that additional secondary keys will be kept in ' + 'the rotation.'), + ], + 'token': [ + cfg.ListOpt('bind', default=[], + help='External auth mechanisms that should add bind ' + 'information to token, e.g., kerberos,x509.'), + cfg.StrOpt('enforce_token_bind', default='permissive', + help='Enforcement policy on tokens presented to Keystone ' + 'with bind information. One of disabled, permissive, ' + 'strict, required or a specifically required bind ' + 'mode, e.g., kerberos or x509 to require binding to ' + 'that authentication.'), + cfg.IntOpt('expiration', default=3600, + help='Amount of time a token should remain valid ' + '(in seconds).'), + cfg.StrOpt('provider', + default='keystone.token.providers.uuid.Provider', + help='Controls the token construction, validation, and ' + 'revocation operations. Core providers are ' + '"keystone.token.providers.[fernet|pkiz|pki|uuid].' + 'Provider".'), + cfg.StrOpt('driver', + default='keystone.token.persistence.backends.sql.Token', + help='Token persistence backend driver.'), + cfg.BoolOpt('caching', default=True, + help='Toggle for token system caching. This has no ' + 'effect unless global caching is enabled.'), + cfg.IntOpt('cache_time', + help='Time to cache tokens (in seconds). This has no ' + 'effect unless global and token caching are ' + 'enabled.'), + cfg.BoolOpt('revoke_by_id', default=True, + help='Revoke token by token identifier. Setting ' + 'revoke_by_id to true enables various forms of ' + 'enumerating tokens, e.g. `list tokens for user`. ' + 'These enumerations are processed to determine the ' + 'list of tokens to revoke. Only disable if you are ' + 'switching to using the Revoke extension with a ' + 'backend other than KVS, which stores events in memory.'), + cfg.BoolOpt('allow_rescope_scoped_token', default=True, + help='Allow rescoping of scoped token. Setting ' + 'allow_rescoped_scoped_token to false prevents a user ' + 'from exchanging a scoped token for any other token.'), + cfg.StrOpt('hash_algorithm', default='md5', + help="The hash algorithm to use for PKI tokens. This can " + "be set to any algorithm that hashlib supports. " + "WARNING: Before changing this value, the auth_token " + "middleware must be configured with the " + "hash_algorithms, otherwise token revocation will " + "not be processed correctly."), + ], + 'revoke': [ + cfg.StrOpt('driver', + default='keystone.contrib.revoke.backends.sql.Revoke', + help='An implementation of the backend for persisting ' + 'revocation events.'), + cfg.IntOpt('expiration_buffer', default=1800, + help='This value (calculated in seconds) is added to token ' + 'expiration before a revocation event may be removed ' + 'from the backend.'), + cfg.BoolOpt('caching', default=True, + help='Toggle for revocation event caching. This has no ' + 'effect unless global caching is enabled.'), + cfg.IntOpt('cache_time', default=3600, + help='Time to cache the revocation list and the revocation ' + 'events (in seconds). This has no effect unless ' + 'global and token caching are enabled.', + deprecated_opts=[cfg.DeprecatedOpt( + 'revocation_cache_time', group='token')]), + ], + 'cache': [ + cfg.StrOpt('config_prefix', default='cache.keystone', + help='Prefix for building the configuration dictionary ' + 'for the cache region. This should not need to be ' + 'changed unless there is another dogpile.cache ' + 'region with the same configuration name.'), + cfg.IntOpt('expiration_time', default=600, + help='Default TTL, in seconds, for any cached item in ' + 'the dogpile.cache region. This applies to any ' + 'cached method that doesn\'t have an explicit ' + 'cache expiration time defined for it.'), + # NOTE(morganfainberg): the dogpile.cache.memory acceptable in devstack + # and other such single-process/thread deployments. Running + # dogpile.cache.memory in any other configuration has the same pitfalls + # as the KVS token backend. It is recommended that either Redis or + # Memcached are used as the dogpile backend for real workloads. To + # prevent issues with the memory cache ending up in "production" + # unintentionally, we register a no-op as the keystone default caching + # backend. + cfg.StrOpt('backend', default='keystone.common.cache.noop', + help='Dogpile.cache backend module. It is recommended ' + 'that Memcache with pooling ' + '(keystone.cache.memcache_pool) or Redis ' + '(dogpile.cache.redis) be used in production ' + 'deployments. Small workloads (single process) ' + 'like devstack can use the dogpile.cache.memory ' + 'backend.'), + cfg.MultiStrOpt('backend_argument', default=[], + help='Arguments supplied to the backend module. ' + 'Specify this option once per argument to be ' + 'passed to the dogpile.cache backend. Example ' + 'format: ":".'), + cfg.ListOpt('proxies', default=[], + help='Proxy classes to import that will affect the way ' + 'the dogpile.cache backend functions. See the ' + 'dogpile.cache documentation on ' + 'changing-backend-behavior.'), + cfg.BoolOpt('enabled', default=False, + help='Global toggle for all caching using the ' + 'should_cache_fn mechanism.'), + cfg.BoolOpt('debug_cache_backend', default=False, + help='Extra debugging from the cache backend (cache ' + 'keys, get/set/delete/etc calls). This is only ' + 'really useful if you need to see the specific ' + 'cache-backend get/set/delete calls with the ' + 'keys/values. Typically this should be left set ' + 'to false.'), + cfg.ListOpt('memcache_servers', default=['localhost:11211'], + help='Memcache servers in the format of "host:port".' + ' (dogpile.cache.memcache and keystone.cache.memcache_pool' + ' backends only).'), + cfg.IntOpt('memcache_dead_retry', + default=5 * 60, + help='Number of seconds memcached server is considered dead' + ' before it is tried again. (dogpile.cache.memcache and' + ' keystone.cache.memcache_pool backends only).'), + cfg.IntOpt('memcache_socket_timeout', + default=3, + help='Timeout in seconds for every call to a server.' + ' (dogpile.cache.memcache and keystone.cache.memcache_pool' + ' backends only).'), + cfg.IntOpt('memcache_pool_maxsize', + default=10, + help='Max total number of open connections to every' + ' memcached server. (keystone.cache.memcache_pool backend' + ' only).'), + cfg.IntOpt('memcache_pool_unused_timeout', + default=60, + help='Number of seconds a connection to memcached is held' + ' unused in the pool before it is closed.' + ' (keystone.cache.memcache_pool backend only).'), + cfg.IntOpt('memcache_pool_connection_get_timeout', + default=10, + help='Number of seconds that an operation will wait to get ' + 'a memcache client connection.'), + ], + 'ssl': [ + cfg.StrOpt('ca_key', + default='/etc/keystone/ssl/private/cakey.pem', + help='Path of the CA key file for SSL.'), + cfg.IntOpt('key_size', default=1024, + help='SSL key length (in bits) (auto generated ' + 'certificate).'), + cfg.IntOpt('valid_days', default=3650, + help='Days the certificate is valid for once signed ' + '(auto generated certificate).'), + cfg.StrOpt('cert_subject', + default='/C=US/ST=Unset/L=Unset/O=Unset/CN=localhost', + help='SSL certificate subject (auto generated ' + 'certificate).'), + ], + 'signing': [ + cfg.StrOpt('certfile', + default=_CERTFILE, + help='Path of the certfile for token signing. For ' + 'non-production environments, you may be interested ' + 'in using `keystone-manage pki_setup` to generate ' + 'self-signed certificates.'), + cfg.StrOpt('keyfile', + default=_KEYFILE, + help='Path of the keyfile for token signing.'), + cfg.StrOpt('ca_certs', + default='/etc/keystone/ssl/certs/ca.pem', + help='Path of the CA for token signing.'), + cfg.StrOpt('ca_key', + default='/etc/keystone/ssl/private/cakey.pem', + help='Path of the CA key for token signing.'), + cfg.IntOpt('key_size', default=2048, + help='Key size (in bits) for token signing cert ' + '(auto generated certificate).'), + cfg.IntOpt('valid_days', default=3650, + help='Days the token signing cert is valid for ' + '(auto generated certificate).'), + cfg.StrOpt('cert_subject', + default=('/C=US/ST=Unset/L=Unset/O=Unset/' + 'CN=www.example.com'), + help='Certificate subject (auto generated certificate) for ' + 'token signing.'), + ], + 'assignment': [ + # assignment has no default for backward compatibility reasons. + # If assignment driver is not specified, the identity driver chooses + # the backend + cfg.StrOpt('driver', + help='Assignment backend driver.'), + ], + 'resource': [ + cfg.StrOpt('driver', + help='Resource backend driver. If a resource driver is ' + 'not specified, the assignment driver will choose ' + 'the resource driver.'), + cfg.BoolOpt('caching', default=True, + deprecated_opts=[cfg.DeprecatedOpt('caching', + group='assignment')], + help='Toggle for resource caching. This has no effect ' + 'unless global caching is enabled.'), + cfg.IntOpt('cache_time', + deprecated_opts=[cfg.DeprecatedOpt('cache_time', + group='assignment')], + help='TTL (in seconds) to cache resource data. This has ' + 'no effect unless global caching is enabled.'), + cfg.IntOpt('list_limit', + deprecated_opts=[cfg.DeprecatedOpt('list_limit', + group='assignment')], + help='Maximum number of entities that will be returned ' + 'in a resource collection.'), + ], + 'domain_config': [ + cfg.StrOpt('driver', + default='keystone.resource.config_backends.sql.' + 'DomainConfig', + help='Domain config backend driver.'), + ], + 'role': [ + # The role driver has no default for backward compatibility reasons. + # If role driver is not specified, the assignment driver chooses + # the backend + cfg.StrOpt('driver', + help='Role backend driver.'), + cfg.BoolOpt('caching', default=True, + help='Toggle for role caching. This has no effect ' + 'unless global caching is enabled.'), + cfg.IntOpt('cache_time', + help='TTL (in seconds) to cache role data. This has ' + 'no effect unless global caching is enabled.'), + cfg.IntOpt('list_limit', + help='Maximum number of entities that will be returned ' + 'in a role collection.'), + ], + 'credential': [ + cfg.StrOpt('driver', + default=('keystone.credential.backends' + '.sql.Credential'), + help='Credential backend driver.'), + ], + 'oauth1': [ + cfg.StrOpt('driver', + default='keystone.contrib.oauth1.backends.sql.OAuth1', + help='Credential backend driver.'), + cfg.IntOpt('request_token_duration', default=28800, + help='Duration (in seconds) for the OAuth Request Token.'), + cfg.IntOpt('access_token_duration', default=86400, + help='Duration (in seconds) for the OAuth Access Token.'), + ], + 'federation': [ + cfg.StrOpt('driver', + default='keystone.contrib.federation.' + 'backends.sql.Federation', + help='Federation backend driver.'), + cfg.StrOpt('assertion_prefix', default='', + help='Value to be used when filtering assertion parameters ' + 'from the environment.'), + cfg.StrOpt('remote_id_attribute', + help='Value to be used to obtain the entity ID of the ' + 'Identity Provider from the environment (e.g. if ' + 'using the mod_shib plugin this value is ' + '`Shib-Identity-Provider`).'), + cfg.StrOpt('federated_domain_name', default='Federated', + help='A domain name that is reserved to allow federated ' + 'ephemeral users to have a domain concept. Note that ' + 'an admin will not be able to create a domain with ' + 'this name or update an existing domain to this ' + 'name. You are not advised to change this value ' + 'unless you really have to. Changing this option ' + 'to empty string or None will not have any impact and ' + 'default name will be used.'), + cfg.MultiStrOpt('trusted_dashboard', default=[], + help='A list of trusted dashboard hosts. Before ' + 'accepting a Single Sign-On request to return a ' + 'token, the origin host must be a member of the ' + 'trusted_dashboard list. This configuration ' + 'option may be repeated for multiple values. ' + 'For example: trusted_dashboard=http://acme.com ' + 'trusted_dashboard=http://beta.com'), + cfg.StrOpt('sso_callback_template', default=_SSO_CALLBACK, + help='Location of Single Sign-On callback handler, will ' + 'return a token to a trusted dashboard host.'), + ], + 'policy': [ + cfg.StrOpt('driver', + default='keystone.policy.backends.sql.Policy', + help='Policy backend driver.'), + cfg.IntOpt('list_limit', + help='Maximum number of entities that will be returned ' + 'in a policy collection.'), + ], + 'endpoint_filter': [ + cfg.StrOpt('driver', + default='keystone.contrib.endpoint_filter.backends' + '.sql.EndpointFilter', + help='Endpoint Filter backend driver'), + cfg.BoolOpt('return_all_endpoints_if_no_filter', default=True, + help='Toggle to return all active endpoints if no filter ' + 'exists.'), + ], + 'endpoint_policy': [ + cfg.StrOpt('driver', + default='keystone.contrib.endpoint_policy.backends' + '.sql.EndpointPolicy', + help='Endpoint policy backend driver'), + ], + 'ldap': [ + cfg.StrOpt('url', default='ldap://localhost', + help='URL for connecting to the LDAP server.'), + cfg.StrOpt('user', + help='User BindDN to query the LDAP server.'), + cfg.StrOpt('password', secret=True, + help='Password for the BindDN to query the LDAP server.'), + cfg.StrOpt('suffix', default='cn=example,cn=com', + help='LDAP server suffix'), + cfg.BoolOpt('use_dumb_member', default=False, + help='If true, will add a dummy member to groups. This is ' + 'required if the objectclass for groups requires the ' + '"member" attribute.'), + cfg.StrOpt('dumb_member', default='cn=dumb,dc=nonexistent', + help='DN of the "dummy member" to use when ' + '"use_dumb_member" is enabled.'), + cfg.BoolOpt('allow_subtree_delete', default=False, + help='Delete subtrees using the subtree delete control. ' + 'Only enable this option if your LDAP server ' + 'supports subtree deletion.'), + cfg.StrOpt('query_scope', default='one', + help='The LDAP scope for queries, this can be either ' + '"one" (onelevel/singleLevel) or "sub" ' + '(subtree/wholeSubtree).'), + cfg.IntOpt('page_size', default=0, + help='Maximum results per page; a value of zero ("0") ' + 'disables paging.'), + cfg.StrOpt('alias_dereferencing', default='default', + help='The LDAP dereferencing option for queries. This ' + 'can be either "never", "searching", "always", ' + '"finding" or "default". The "default" option falls ' + 'back to using default dereferencing configured by ' + 'your ldap.conf.'), + cfg.IntOpt('debug_level', + help='Sets the LDAP debugging level for LDAP calls. ' + 'A value of 0 means that debugging is not enabled. ' + 'This value is a bitmask, consult your LDAP ' + 'documentation for possible values.'), + cfg.BoolOpt('chase_referrals', + help='Override the system\'s default referral chasing ' + 'behavior for queries.'), + cfg.StrOpt('user_tree_dn', + help='Search base for users.'), + cfg.StrOpt('user_filter', + help='LDAP search filter for users.'), + cfg.StrOpt('user_objectclass', default='inetOrgPerson', + help='LDAP objectclass for users.'), + cfg.StrOpt('user_id_attribute', default='cn', + help='LDAP attribute mapped to user id. ' + 'WARNING: must not be a multivalued attribute.'), + cfg.StrOpt('user_name_attribute', default='sn', + help='LDAP attribute mapped to user name.'), + cfg.StrOpt('user_mail_attribute', default='mail', + help='LDAP attribute mapped to user email.'), + cfg.StrOpt('user_pass_attribute', default='userPassword', + help='LDAP attribute mapped to password.'), + cfg.StrOpt('user_enabled_attribute', default='enabled', + help='LDAP attribute mapped to user enabled flag.'), + cfg.BoolOpt('user_enabled_invert', default=False, + help='Invert the meaning of the boolean enabled values. ' + 'Some LDAP servers use a boolean lock attribute ' + 'where "true" means an account is disabled. Setting ' + '"user_enabled_invert = true" will allow these lock ' + 'attributes to be used. This setting will have no ' + 'effect if "user_enabled_mask" or ' + '"user_enabled_emulation" settings are in use.'), + cfg.IntOpt('user_enabled_mask', default=0, + help='Bitmask integer to indicate the bit that the enabled ' + 'value is stored in if the LDAP server represents ' + '"enabled" as a bit on an integer rather than a ' + 'boolean. A value of "0" indicates the mask is not ' + 'used. If this is not set to "0" the typical value ' + 'is "2". This is typically used when ' + '"user_enabled_attribute = userAccountControl".'), + cfg.StrOpt('user_enabled_default', default='True', + help='Default value to enable users. This should match an ' + 'appropriate int value if the LDAP server uses ' + 'non-boolean (bitmask) values to indicate if a user ' + 'is enabled or disabled. If this is not set to "True" ' + 'the typical value is "512". This is typically used ' + 'when "user_enabled_attribute = userAccountControl".'), + cfg.ListOpt('user_attribute_ignore', + default=['default_project_id', 'tenants'], + help='List of attributes stripped off the user on ' + 'update.'), + cfg.StrOpt('user_default_project_id_attribute', + help='LDAP attribute mapped to default_project_id for ' + 'users.'), + cfg.BoolOpt('user_allow_create', default=True, + help='Allow user creation in LDAP backend.'), + cfg.BoolOpt('user_allow_update', default=True, + help='Allow user updates in LDAP backend.'), + cfg.BoolOpt('user_allow_delete', default=True, + help='Allow user deletion in LDAP backend.'), + cfg.BoolOpt('user_enabled_emulation', default=False, + help='If true, Keystone uses an alternative method to ' + 'determine if a user is enabled or not by checking ' + 'if they are a member of the ' + '"user_enabled_emulation_dn" group.'), + cfg.StrOpt('user_enabled_emulation_dn', + help='DN of the group entry to hold enabled users when ' + 'using enabled emulation.'), + cfg.ListOpt('user_additional_attribute_mapping', + default=[], + help='List of additional LDAP attributes used for mapping ' + 'additional attribute mappings for users. Attribute ' + 'mapping format is :, where ' + 'ldap_attr is the attribute in the LDAP entry and ' + 'user_attr is the Identity API attribute.'), + + cfg.StrOpt('project_tree_dn', + deprecated_opts=[cfg.DeprecatedOpt( + 'tenant_tree_dn', group='ldap')], + help='Search base for projects'), + cfg.StrOpt('project_filter', + deprecated_opts=[cfg.DeprecatedOpt( + 'tenant_filter', group='ldap')], + help='LDAP search filter for projects.'), + cfg.StrOpt('project_objectclass', default='groupOfNames', + deprecated_opts=[cfg.DeprecatedOpt( + 'tenant_objectclass', group='ldap')], + help='LDAP objectclass for projects.'), + cfg.StrOpt('project_id_attribute', default='cn', + deprecated_opts=[cfg.DeprecatedOpt( + 'tenant_id_attribute', group='ldap')], + help='LDAP attribute mapped to project id.'), + cfg.StrOpt('project_member_attribute', default='member', + deprecated_opts=[cfg.DeprecatedOpt( + 'tenant_member_attribute', group='ldap')], + help='LDAP attribute mapped to project membership for ' + 'user.'), + cfg.StrOpt('project_name_attribute', default='ou', + deprecated_opts=[cfg.DeprecatedOpt( + 'tenant_name_attribute', group='ldap')], + help='LDAP attribute mapped to project name.'), + cfg.StrOpt('project_desc_attribute', default='description', + deprecated_opts=[cfg.DeprecatedOpt( + 'tenant_desc_attribute', group='ldap')], + help='LDAP attribute mapped to project description.'), + cfg.StrOpt('project_enabled_attribute', default='enabled', + deprecated_opts=[cfg.DeprecatedOpt( + 'tenant_enabled_attribute', group='ldap')], + help='LDAP attribute mapped to project enabled.'), + cfg.StrOpt('project_domain_id_attribute', + deprecated_opts=[cfg.DeprecatedOpt( + 'tenant_domain_id_attribute', group='ldap')], + default='businessCategory', + help='LDAP attribute mapped to project domain_id.'), + cfg.ListOpt('project_attribute_ignore', default=[], + deprecated_opts=[cfg.DeprecatedOpt( + 'tenant_attribute_ignore', group='ldap')], + help='List of attributes stripped off the project on ' + 'update.'), + cfg.BoolOpt('project_allow_create', default=True, + deprecated_opts=[cfg.DeprecatedOpt( + 'tenant_allow_create', group='ldap')], + help='Allow project creation in LDAP backend.'), + cfg.BoolOpt('project_allow_update', default=True, + deprecated_opts=[cfg.DeprecatedOpt( + 'tenant_allow_update', group='ldap')], + help='Allow project update in LDAP backend.'), + cfg.BoolOpt('project_allow_delete', default=True, + deprecated_opts=[cfg.DeprecatedOpt( + 'tenant_allow_delete', group='ldap')], + help='Allow project deletion in LDAP backend.'), + cfg.BoolOpt('project_enabled_emulation', default=False, + deprecated_opts=[cfg.DeprecatedOpt( + 'tenant_enabled_emulation', group='ldap')], + help='If true, Keystone uses an alternative method to ' + 'determine if a project is enabled or not by ' + 'checking if they are a member of the ' + '"project_enabled_emulation_dn" group.'), + cfg.StrOpt('project_enabled_emulation_dn', + deprecated_opts=[cfg.DeprecatedOpt( + 'tenant_enabled_emulation_dn', group='ldap')], + help='DN of the group entry to hold enabled projects when ' + 'using enabled emulation.'), + cfg.ListOpt('project_additional_attribute_mapping', + deprecated_opts=[cfg.DeprecatedOpt( + 'tenant_additional_attribute_mapping', group='ldap')], + default=[], + help='Additional attribute mappings for projects. ' + 'Attribute mapping format is ' + ':, where ldap_attr is the ' + 'attribute in the LDAP entry and user_attr is the ' + 'Identity API attribute.'), + + cfg.StrOpt('role_tree_dn', + help='Search base for roles.'), + cfg.StrOpt('role_filter', + help='LDAP search filter for roles.'), + cfg.StrOpt('role_objectclass', default='organizationalRole', + help='LDAP objectclass for roles.'), + cfg.StrOpt('role_id_attribute', default='cn', + help='LDAP attribute mapped to role id.'), + cfg.StrOpt('role_name_attribute', default='ou', + help='LDAP attribute mapped to role name.'), + cfg.StrOpt('role_member_attribute', default='roleOccupant', + help='LDAP attribute mapped to role membership.'), + cfg.ListOpt('role_attribute_ignore', default=[], + help='List of attributes stripped off the role on ' + 'update.'), + cfg.BoolOpt('role_allow_create', default=True, + help='Allow role creation in LDAP backend.'), + cfg.BoolOpt('role_allow_update', default=True, + help='Allow role update in LDAP backend.'), + cfg.BoolOpt('role_allow_delete', default=True, + help='Allow role deletion in LDAP backend.'), + cfg.ListOpt('role_additional_attribute_mapping', + default=[], + help='Additional attribute mappings for roles. Attribute ' + 'mapping format is :, where ' + 'ldap_attr is the attribute in the LDAP entry and ' + 'user_attr is the Identity API attribute.'), + + cfg.StrOpt('group_tree_dn', + help='Search base for groups.'), + cfg.StrOpt('group_filter', + help='LDAP search filter for groups.'), + cfg.StrOpt('group_objectclass', default='groupOfNames', + help='LDAP objectclass for groups.'), + cfg.StrOpt('group_id_attribute', default='cn', + help='LDAP attribute mapped to group id.'), + cfg.StrOpt('group_name_attribute', default='ou', + help='LDAP attribute mapped to group name.'), + cfg.StrOpt('group_member_attribute', default='member', + help='LDAP attribute mapped to show group membership.'), + cfg.StrOpt('group_desc_attribute', default='description', + help='LDAP attribute mapped to group description.'), + cfg.ListOpt('group_attribute_ignore', default=[], + help='List of attributes stripped off the group on ' + 'update.'), + cfg.BoolOpt('group_allow_create', default=True, + help='Allow group creation in LDAP backend.'), + cfg.BoolOpt('group_allow_update', default=True, + help='Allow group update in LDAP backend.'), + cfg.BoolOpt('group_allow_delete', default=True, + help='Allow group deletion in LDAP backend.'), + cfg.ListOpt('group_additional_attribute_mapping', + default=[], + help='Additional attribute mappings for groups. Attribute ' + 'mapping format is :, where ' + 'ldap_attr is the attribute in the LDAP entry and ' + 'user_attr is the Identity API attribute.'), + + cfg.StrOpt('tls_cacertfile', + help='CA certificate file path for communicating with ' + 'LDAP servers.'), + cfg.StrOpt('tls_cacertdir', + help='CA certificate directory path for communicating with ' + 'LDAP servers.'), + cfg.BoolOpt('use_tls', default=False, + help='Enable TLS for communicating with LDAP servers.'), + cfg.StrOpt('tls_req_cert', default='demand', + help='Valid options for tls_req_cert are demand, never, ' + 'and allow.'), + cfg.BoolOpt('use_pool', default=False, + help='Enable LDAP connection pooling.'), + cfg.IntOpt('pool_size', default=10, + help='Connection pool size.'), + cfg.IntOpt('pool_retry_max', default=3, + help='Maximum count of reconnect trials.'), + cfg.FloatOpt('pool_retry_delay', default=0.1, + help='Time span in seconds to wait between two ' + 'reconnect trials.'), + cfg.IntOpt('pool_connection_timeout', default=-1, + help='Connector timeout in seconds. Value -1 indicates ' + 'indefinite wait for response.'), + cfg.IntOpt('pool_connection_lifetime', default=600, + help='Connection lifetime in seconds.'), + cfg.BoolOpt('use_auth_pool', default=False, + help='Enable LDAP connection pooling for end user ' + 'authentication. If use_pool is disabled, then this ' + 'setting is meaningless and is not used at all.'), + cfg.IntOpt('auth_pool_size', default=100, + help='End user auth connection pool size.'), + cfg.IntOpt('auth_pool_connection_lifetime', default=60, + help='End user auth connection lifetime in seconds.'), + ], + 'auth': [ + cfg.ListOpt('methods', default=_DEFAULT_AUTH_METHODS, + help='Default auth methods.'), + cfg.StrOpt('password', + default='keystone.auth.plugins.password.Password', + help='The password auth plugin module.'), + cfg.StrOpt('token', + default='keystone.auth.plugins.token.Token', + help='The token auth plugin module.'), + # deals with REMOTE_USER authentication + cfg.StrOpt('external', + default='keystone.auth.plugins.external.DefaultDomain', + help='The external (REMOTE_USER) auth plugin module.'), + cfg.StrOpt('oauth1', + default='keystone.auth.plugins.oauth1.OAuth', + help='The oAuth1.0 auth plugin module.'), + ], + 'paste_deploy': [ + cfg.StrOpt('config_file', default='keystone-paste.ini', + help='Name of the paste configuration file that defines ' + 'the available pipelines.'), + ], + 'memcache': [ + cfg.ListOpt('servers', default=['localhost:11211'], + help='Memcache servers in the format of "host:port".'), + cfg.IntOpt('dead_retry', + default=5 * 60, + help='Number of seconds memcached server is considered dead' + ' before it is tried again. This is used by the key ' + 'value store system (e.g. token ' + 'pooled memcached persistence backend).'), + cfg.IntOpt('socket_timeout', + default=3, + help='Timeout in seconds for every call to a server. This ' + 'is used by the key value store system (e.g. token ' + 'pooled memcached persistence backend).'), + cfg.IntOpt('pool_maxsize', + default=10, + help='Max total number of open connections to every' + ' memcached server. This is used by the key value ' + 'store system (e.g. token pooled memcached ' + 'persistence backend).'), + cfg.IntOpt('pool_unused_timeout', + default=60, + help='Number of seconds a connection to memcached is held' + ' unused in the pool before it is closed. This is used' + ' by the key value store system (e.g. token pooled ' + 'memcached persistence backend).'), + cfg.IntOpt('pool_connection_get_timeout', + default=10, + help='Number of seconds that an operation will wait to get ' + 'a memcache client connection. This is used by the ' + 'key value store system (e.g. token pooled memcached ' + 'persistence backend).'), + ], + 'catalog': [ + cfg.StrOpt('template_file', + default='default_catalog.templates', + help='Catalog template file name for use with the ' + 'template catalog backend.'), + cfg.StrOpt('driver', + default='keystone.catalog.backends.sql.Catalog', + help='Catalog backend driver.'), + cfg.BoolOpt('caching', default=True, + help='Toggle for catalog caching. This has no ' + 'effect unless global caching is enabled.'), + cfg.IntOpt('cache_time', + help='Time to cache catalog data (in seconds). This has no ' + 'effect unless global and catalog caching are ' + 'enabled.'), + cfg.IntOpt('list_limit', + help='Maximum number of entities that will be returned ' + 'in a catalog collection.'), + ], + 'kvs': [ + cfg.ListOpt('backends', default=[], + help='Extra dogpile.cache backend modules to register ' + 'with the dogpile.cache library.'), + cfg.StrOpt('config_prefix', default='keystone.kvs', + help='Prefix for building the configuration dictionary ' + 'for the KVS region. This should not need to be ' + 'changed unless there is another dogpile.cache ' + 'region with the same configuration name.'), + cfg.BoolOpt('enable_key_mangler', default=True, + help='Toggle to disable using a key-mangling function ' + 'to ensure fixed length keys. This is toggle-able ' + 'for debugging purposes, it is highly recommended ' + 'to always leave this set to true.'), + cfg.IntOpt('default_lock_timeout', default=5, + help='Default lock timeout (in seconds) for distributed ' + 'locking.'), + ], + 'saml': [ + cfg.IntOpt('assertion_expiration_time', default=3600, + help='Default TTL, in seconds, for any generated SAML ' + 'assertion created by Keystone.'), + cfg.StrOpt('xmlsec1_binary', + default='xmlsec1', + help='Binary to be called for XML signing. Install the ' + 'appropriate package, specify absolute path or adjust ' + 'your PATH environment variable if the binary cannot ' + 'be found.'), + cfg.StrOpt('certfile', + default=_CERTFILE, + help='Path of the certfile for SAML signing. For ' + 'non-production environments, you may be interested ' + 'in using `keystone-manage pki_setup` to generate ' + 'self-signed certificates. Note, the path cannot ' + 'contain a comma.'), + cfg.StrOpt('keyfile', + default=_KEYFILE, + help='Path of the keyfile for SAML signing. Note, the path ' + 'cannot contain a comma.'), + cfg.StrOpt('idp_entity_id', + help='Entity ID value for unique Identity Provider ' + 'identification. Usually FQDN is set with a suffix. ' + 'A value is required to generate IDP Metadata. ' + 'For example: https://keystone.example.com/v3/' + 'OS-FEDERATION/saml2/idp'), + cfg.StrOpt('idp_sso_endpoint', + help='Identity Provider Single-Sign-On service value, ' + 'required in the Identity Provider\'s metadata. ' + 'A value is required to generate IDP Metadata. ' + 'For example: https://keystone.example.com/v3/' + 'OS-FEDERATION/saml2/sso'), + cfg.StrOpt('idp_lang', default='en', + help='Language used by the organization.'), + cfg.StrOpt('idp_organization_name', + help='Organization name the installation belongs to.'), + cfg.StrOpt('idp_organization_display_name', + help='Organization name to be displayed.'), + cfg.StrOpt('idp_organization_url', + help='URL of the organization.'), + cfg.StrOpt('idp_contact_company', + help='Company of contact person.'), + cfg.StrOpt('idp_contact_name', + help='Given name of contact person'), + cfg.StrOpt('idp_contact_surname', + help='Surname of contact person.'), + cfg.StrOpt('idp_contact_email', + help='Email address of contact person.'), + cfg.StrOpt('idp_contact_telephone', + help='Telephone number of contact person.'), + cfg.StrOpt('idp_contact_type', default='other', + help='Contact type. Allowed values are: ' + 'technical, support, administrative ' + 'billing, and other'), + cfg.StrOpt('idp_metadata_path', + default='/etc/keystone/saml2_idp_metadata.xml', + help='Path to the Identity Provider Metadata file. ' + 'This file should be generated with the ' + 'keystone-manage saml_idp_metadata command.'), + ], + 'eventlet_server': [ + cfg.IntOpt('public_workers', + deprecated_name='public_workers', + deprecated_group='DEFAULT', + help='The number of worker processes to serve the public ' + 'eventlet application. Defaults to number of CPUs ' + '(minimum of 2).'), + cfg.IntOpt('admin_workers', + deprecated_name='admin_workers', + deprecated_group='DEFAULT', + help='The number of worker processes to serve the admin ' + 'eventlet application. Defaults to number of CPUs ' + '(minimum of 2).'), + cfg.StrOpt('public_bind_host', + default='0.0.0.0', + deprecated_opts=[cfg.DeprecatedOpt('bind_host', + group='DEFAULT'), + cfg.DeprecatedOpt('public_bind_host', + group='DEFAULT'), ], + help='The IP address of the network interface for the ' + 'public service to listen on.'), + cfg.IntOpt('public_port', default=5000, deprecated_name='public_port', + deprecated_group='DEFAULT', + help='The port number which the public service listens ' + 'on.'), + cfg.StrOpt('admin_bind_host', + default='0.0.0.0', + deprecated_opts=[cfg.DeprecatedOpt('bind_host', + group='DEFAULT'), + cfg.DeprecatedOpt('admin_bind_host', + group='DEFAULT')], + help='The IP address of the network interface for the ' + 'admin service to listen on.'), + cfg.IntOpt('admin_port', default=35357, deprecated_name='admin_port', + deprecated_group='DEFAULT', + help='The port number which the admin service listens ' + 'on.'), + cfg.BoolOpt('tcp_keepalive', default=False, + deprecated_name='tcp_keepalive', + deprecated_group='DEFAULT', + help='Set this to true if you want to enable ' + 'TCP_KEEPALIVE on server sockets, i.e. sockets used ' + 'by the Keystone wsgi server for client ' + 'connections.'), + cfg.IntOpt('tcp_keepidle', + default=600, + deprecated_name='tcp_keepidle', + deprecated_group='DEFAULT', + help='Sets the value of TCP_KEEPIDLE in seconds for each ' + 'server socket. Only applies if tcp_keepalive is ' + 'true.'), + ], + 'eventlet_server_ssl': [ + cfg.BoolOpt('enable', default=False, deprecated_name='enable', + deprecated_group='ssl', + help='Toggle for SSL support on the Keystone ' + 'eventlet servers.'), + cfg.StrOpt('certfile', + default="/etc/keystone/ssl/certs/keystone.pem", + deprecated_name='certfile', deprecated_group='ssl', + help='Path of the certfile for SSL. For non-production ' + 'environments, you may be interested in using ' + '`keystone-manage ssl_setup` to generate self-signed ' + 'certificates.'), + cfg.StrOpt('keyfile', + default='/etc/keystone/ssl/private/keystonekey.pem', + deprecated_name='keyfile', deprecated_group='ssl', + help='Path of the keyfile for SSL.'), + cfg.StrOpt('ca_certs', + default='/etc/keystone/ssl/certs/ca.pem', + deprecated_name='ca_certs', deprecated_group='ssl', + help='Path of the CA cert file for SSL.'), + cfg.BoolOpt('cert_required', default=False, + deprecated_name='cert_required', deprecated_group='ssl', + help='Require client certificate.'), + ], +} + + +CONF = cfg.CONF +oslo_messaging.set_transport_defaults(control_exchange='keystone') + + +def _register_auth_plugin_opt(conf, option): + conf.register_opt(option, group='auth') + + +def setup_authentication(conf=None): + # register any non-default auth methods here (used by extensions, etc) + if conf is None: + conf = CONF + for method_name in conf.auth.methods: + if method_name not in _DEFAULT_AUTH_METHODS: + option = cfg.StrOpt(method_name) + _register_auth_plugin_opt(conf, option) + + +def configure(conf=None): + if conf is None: + conf = CONF + + conf.register_cli_opt( + cfg.BoolOpt('standard-threads', default=False, + help='Do not monkey-patch threading system modules.')) + conf.register_cli_opt( + cfg.StrOpt('pydev-debug-host', + help='Host to connect to for remote debugger.')) + conf.register_cli_opt( + cfg.IntOpt('pydev-debug-port', + help='Port to connect to for remote debugger.')) + + for section in FILE_OPTIONS: + for option in FILE_OPTIONS[section]: + if section: + conf.register_opt(option, group=section) + else: + conf.register_opt(option) + + # register any non-default auth methods here (used by extensions, etc) + setup_authentication(conf) + + +def list_opts(): + """Return a list of oslo_config options available in Keystone. + + The returned list includes all oslo_config options which are registered as + the "FILE_OPTIONS" in keystone.common.config. This list will not include + the options from the oslo-incubator library or any options registered + dynamically at run time. + + Each object in the list is a two element tuple. The first element of + each tuple is the name of the group under which the list of options in the + second element will be registered. A group name of None corresponds to the + [DEFAULT] group in config files. + + This function is also discoverable via the 'oslo_config.opts' entry point + under the 'keystone.config.opts' namespace. + + The purpose of this is to allow tools like the Oslo sample config file + generator to discover the options exposed to users by this library. + + :returns: a list of (group_name, opts) tuples + """ + return FILE_OPTIONS.items() diff --git a/keystone-moon/keystone/common/controller.py b/keystone-moon/keystone/common/controller.py new file mode 100644 index 00000000..bd26b7c4 --- /dev/null +++ b/keystone-moon/keystone/common/controller.py @@ -0,0 +1,800 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 functools +import uuid + +from oslo_config import cfg +from oslo_log import log +import six + +from keystone.common import authorization +from keystone.common import dependency +from keystone.common import driver_hints +from keystone.common import utils +from keystone.common import wsgi +from keystone import exception +from keystone.i18n import _, _LW +from keystone.models import token_model + + +LOG = log.getLogger(__name__) +CONF = cfg.CONF + + +def v2_deprecated(f): + """No-op decorator in preparation for deprecating Identity API v2. + + This is a placeholder for the pending deprecation of v2. The implementation + of this decorator can be replaced with:: + + from keystone.openstack.common import versionutils + + + v2_deprecated = versionutils.deprecated( + what='v2 API', + as_of=versionutils.deprecated.JUNO, + in_favor_of='v3 API') + + """ + return f + + +def _build_policy_check_credentials(self, action, context, kwargs): + LOG.debug('RBAC: Authorizing %(action)s(%(kwargs)s)', { + 'action': action, + 'kwargs': ', '.join(['%s=%s' % (k, kwargs[k]) for k in kwargs])}) + + # see if auth context has already been created. If so use it. + if ('environment' in context and + authorization.AUTH_CONTEXT_ENV in context['environment']): + LOG.debug('RBAC: using auth context from the request environment') + return context['environment'].get(authorization.AUTH_CONTEXT_ENV) + + # There is no current auth context, build it from the incoming token. + # TODO(morganfainberg): Collapse this logic with AuthContextMiddleware + # in a sane manner as this just mirrors the logic in AuthContextMiddleware + try: + LOG.debug('RBAC: building auth context from the incoming auth token') + token_ref = token_model.KeystoneToken( + token_id=context['token_id'], + token_data=self.token_provider_api.validate_token( + context['token_id'])) + # NOTE(jamielennox): whilst this maybe shouldn't be within this + # function it would otherwise need to reload the token_ref from + # backing store. + wsgi.validate_token_bind(context, token_ref) + except exception.TokenNotFound: + LOG.warning(_LW('RBAC: Invalid token')) + raise exception.Unauthorized() + + auth_context = authorization.token_to_auth_context(token_ref) + + return auth_context + + +def protected(callback=None): + """Wraps API calls with role based access controls (RBAC). + + This handles both the protection of the API parameters as well as any + target entities for single-entity API calls. + + More complex API calls (for example that deal with several different + entities) should pass in a callback function, that will be subsequently + called to check protection for these multiple entities. This callback + function should gather the appropriate entities needed and then call + check_protection() in the V3Controller class. + + """ + def wrapper(f): + @functools.wraps(f) + def inner(self, context, *args, **kwargs): + if 'is_admin' in context and context['is_admin']: + LOG.warning(_LW('RBAC: Bypassing authorization')) + elif callback is not None: + prep_info = {'f_name': f.__name__, + 'input_attr': kwargs} + callback(self, context, prep_info, *args, **kwargs) + else: + action = 'identity:%s' % f.__name__ + creds = _build_policy_check_credentials(self, action, + context, kwargs) + + policy_dict = {} + + # Check to see if we need to include the target entity in our + # policy checks. We deduce this by seeing if the class has + # specified a get_member() method and that kwargs contains the + # appropriate entity id. + if (hasattr(self, 'get_member_from_driver') and + self.get_member_from_driver is not None): + key = '%s_id' % self.member_name + if key in kwargs: + ref = self.get_member_from_driver(kwargs[key]) + policy_dict['target'] = {self.member_name: ref} + + # TODO(henry-nash): Move this entire code to a member + # method inside v3 Auth + if context.get('subject_token_id') is not None: + token_ref = token_model.KeystoneToken( + token_id=context['subject_token_id'], + token_data=self.token_provider_api.validate_token( + context['subject_token_id'])) + policy_dict.setdefault('target', {}) + policy_dict['target'].setdefault(self.member_name, {}) + policy_dict['target'][self.member_name]['user_id'] = ( + token_ref.user_id) + try: + user_domain_id = token_ref.user_domain_id + except exception.UnexpectedError: + user_domain_id = None + if user_domain_id: + policy_dict['target'][self.member_name].setdefault( + 'user', {}) + policy_dict['target'][self.member_name][ + 'user'].setdefault('domain', {}) + policy_dict['target'][self.member_name]['user'][ + 'domain']['id'] = ( + user_domain_id) + + # Add in the kwargs, which means that any entity provided as a + # parameter for calls like create and update will be included. + policy_dict.update(kwargs) + self.policy_api.enforce(creds, + action, + utils.flatten_dict(policy_dict)) + LOG.debug('RBAC: Authorization granted') + return f(self, context, *args, **kwargs) + return inner + return wrapper + + +def filterprotected(*filters): + """Wraps filtered API calls with role based access controls (RBAC).""" + + def _filterprotected(f): + @functools.wraps(f) + def wrapper(self, context, **kwargs): + if not context['is_admin']: + action = 'identity:%s' % f.__name__ + creds = _build_policy_check_credentials(self, action, + context, kwargs) + # Now, build the target dict for policy check. We include: + # + # - Any query filter parameters + # - Data from the main url (which will be in the kwargs + # parameter) and would typically include the prime key + # of a get/update/delete call + # + # First any query filter parameters + target = dict() + if filters: + for item in filters: + if item in context['query_string']: + target[item] = context['query_string'][item] + + LOG.debug('RBAC: Adding query filter params (%s)', ( + ', '.join(['%s=%s' % (item, target[item]) + for item in target]))) + + # Now any formal url parameters + for key in kwargs: + target[key] = kwargs[key] + + self.policy_api.enforce(creds, + action, + utils.flatten_dict(target)) + + LOG.debug('RBAC: Authorization granted') + else: + LOG.warning(_LW('RBAC: Bypassing authorization')) + return f(self, context, filters, **kwargs) + return wrapper + return _filterprotected + + +class V2Controller(wsgi.Application): + """Base controller class for Identity API v2.""" + def _normalize_domain_id(self, context, ref): + """Fill in domain_id since v2 calls are not domain-aware. + + This will overwrite any domain_id that was inadvertently + specified in the v2 call. + + """ + ref['domain_id'] = CONF.identity.default_domain_id + return ref + + @staticmethod + def filter_domain_id(ref): + """Remove domain_id since v2 calls are not domain-aware.""" + ref.pop('domain_id', None) + return ref + + @staticmethod + def filter_domain(ref): + """Remove domain since v2 calls are not domain-aware. + + V3 Fernet tokens builds the users with a domain in the token data. + This method will ensure that users create in v3 belong to the default + domain. + + """ + if 'domain' in ref: + if ref['domain'].get('id') != CONF.identity.default_domain_id: + raise exception.Unauthorized( + _('Non-default domain is not supported')) + del ref['domain'] + return ref + + @staticmethod + def normalize_username_in_response(ref): + """Adds username to outgoing user refs to match the v2 spec. + + Internally we use `name` to represent a user's name. The v2 spec + requires the use of `username` instead. + + """ + if 'username' not in ref and 'name' in ref: + ref['username'] = ref['name'] + return ref + + @staticmethod + def normalize_username_in_request(ref): + """Adds name in incoming user refs to match the v2 spec. + + Internally we use `name` to represent a user's name. The v2 spec + requires the use of `username` instead. + + """ + if 'name' not in ref and 'username' in ref: + ref['name'] = ref.pop('username') + return ref + + @staticmethod + def v3_to_v2_user(ref): + """Convert a user_ref from v3 to v2 compatible. + + * v2.0 users are not domain aware, and should have domain_id removed + * v2.0 users expect the use of tenantId instead of default_project_id + * v2.0 users have a username attribute + + This method should only be applied to user_refs being returned from the + v2.0 controller(s). + + If ref is a list type, we will iterate through each element and do the + conversion. + """ + + def _format_default_project_id(ref): + """Convert default_project_id to tenantId for v2 calls.""" + default_project_id = ref.pop('default_project_id', None) + if default_project_id is not None: + ref['tenantId'] = default_project_id + elif 'tenantId' in ref: + # NOTE(morganfainberg): To avoid v2.0 confusion if somehow a + # tenantId property sneaks its way into the extra blob on the + # user, we remove it here. If default_project_id is set, we + # would override it in either case. + del ref['tenantId'] + + def _normalize_and_filter_user_properties(ref): + """Run through the various filter/normalization methods.""" + _format_default_project_id(ref) + V2Controller.filter_domain(ref) + V2Controller.filter_domain_id(ref) + V2Controller.normalize_username_in_response(ref) + return ref + + if isinstance(ref, dict): + return _normalize_and_filter_user_properties(ref) + elif isinstance(ref, list): + return [_normalize_and_filter_user_properties(x) for x in ref] + else: + raise ValueError(_('Expected dict or list: %s') % type(ref)) + + def format_project_list(self, tenant_refs, **kwargs): + """Format a v2 style project list, including marker/limits.""" + marker = kwargs.get('marker') + first_index = 0 + if marker is not None: + for (marker_index, tenant) in enumerate(tenant_refs): + if tenant['id'] == marker: + # we start pagination after the marker + first_index = marker_index + 1 + break + else: + msg = _('Marker could not be found') + raise exception.ValidationError(message=msg) + + limit = kwargs.get('limit') + last_index = None + if limit is not None: + try: + limit = int(limit) + if limit < 0: + raise AssertionError() + except (ValueError, AssertionError): + msg = _('Invalid limit value') + raise exception.ValidationError(message=msg) + last_index = first_index + limit + + tenant_refs = tenant_refs[first_index:last_index] + + for x in tenant_refs: + if 'enabled' not in x: + x['enabled'] = True + o = {'tenants': tenant_refs, + 'tenants_links': []} + return o + + +@dependency.requires('policy_api', 'token_provider_api') +class V3Controller(wsgi.Application): + """Base controller class for Identity API v3. + + Child classes should set the ``collection_name`` and ``member_name`` class + attributes, representing the collection of entities they are exposing to + the API. This is required for supporting self-referential links, + pagination, etc. + + Class parameters: + + * `_mutable_parameters` - set of parameters that can be changed by users. + Usually used by cls.check_immutable_params() + * `_public_parameters` - set of parameters that are exposed to the user. + Usually used by cls.filter_params() + + """ + + collection_name = 'entities' + member_name = 'entity' + get_member_from_driver = None + + @classmethod + def base_url(cls, context, path=None): + endpoint = super(V3Controller, cls).base_url(context, 'public') + if not path: + path = cls.collection_name + + return '%s/%s/%s' % (endpoint, 'v3', path.lstrip('/')) + + def get_auth_context(self, context): + # TODO(dolphm): this method of accessing the auth context is terrible, + # but context needs to be refactored to always have reasonable values. + env_context = context.get('environment', {}) + return env_context.get(authorization.AUTH_CONTEXT_ENV, {}) + + @classmethod + def full_url(cls, context, path=None): + url = cls.base_url(context, path) + if context['environment'].get('QUERY_STRING'): + url = '%s?%s' % (url, context['environment']['QUERY_STRING']) + + return url + + @classmethod + def query_filter_is_true(cls, filter_value): + """Determine if bool query param is 'True'. + + We treat this the same way as we do for policy + enforcement: + + {bool_param}=0 is treated as False + + Any other value is considered to be equivalent to + True, including the absence of a value + + """ + + if (isinstance(filter_value, six.string_types) and + filter_value == '0'): + val = False + else: + val = True + return val + + @classmethod + def _add_self_referential_link(cls, context, ref): + ref.setdefault('links', {}) + ref['links']['self'] = cls.base_url(context) + '/' + ref['id'] + + @classmethod + def wrap_member(cls, context, ref): + cls._add_self_referential_link(context, ref) + return {cls.member_name: ref} + + @classmethod + def wrap_collection(cls, context, refs, hints=None): + """Wrap a collection, checking for filtering and pagination. + + Returns the wrapped collection, which includes: + - Executing any filtering not already carried out + - Truncate to a set limit if necessary + - Adds 'self' links in every member + - Adds 'next', 'self' and 'prev' links for the whole collection. + + :param context: the current context, containing the original url path + and query string + :param refs: the list of members of the collection + :param hints: list hints, containing any relevant filters and limit. + Any filters already satisfied by managers will have been + removed + """ + # Check if there are any filters in hints that were not + # handled by the drivers. The driver will not have paginated or + # limited the output if it found there were filters it was unable to + # handle. + + if hints is not None: + refs = cls.filter_by_attributes(refs, hints) + + list_limited, refs = cls.limit(refs, hints) + + for ref in refs: + cls.wrap_member(context, ref) + + container = {cls.collection_name: refs} + container['links'] = { + 'next': None, + 'self': cls.full_url(context, path=context['path']), + 'previous': None} + + if list_limited: + container['truncated'] = True + + return container + + @classmethod + def limit(cls, refs, hints): + """Limits a list of entities. + + The underlying driver layer may have already truncated the collection + for us, but in case it was unable to handle truncation we check here. + + :param refs: the list of members of the collection + :param hints: hints, containing, among other things, the limit + requested + + :returns: boolean indicating whether the list was truncated, as well + as the list of (truncated if necessary) entities. + + """ + NOT_LIMITED = False + LIMITED = True + + if hints is None or hints.limit is None: + # No truncation was requested + return NOT_LIMITED, refs + + if hints.limit.get('truncated', False): + # The driver did truncate the list + return LIMITED, refs + + if len(refs) > hints.limit['limit']: + # The driver layer wasn't able to truncate it for us, so we must + # do it here + return LIMITED, refs[:hints.limit['limit']] + + return NOT_LIMITED, refs + + @classmethod + def filter_by_attributes(cls, refs, hints): + """Filters a list of references by filter values.""" + + def _attr_match(ref_attr, val_attr): + """Matches attributes allowing for booleans as strings. + + We test explicitly for a value that defines it as 'False', + which also means that the existence of the attribute with + no value implies 'True' + + """ + if type(ref_attr) is bool: + return ref_attr == utils.attr_as_boolean(val_attr) + else: + return ref_attr == val_attr + + def _inexact_attr_match(filter, ref): + """Applies an inexact filter to a result dict. + + :param filter: the filter in question + :param ref: the dict to check + + :returns True if there is a match + + """ + comparator = filter['comparator'] + key = filter['name'] + + if key in ref: + filter_value = filter['value'] + target_value = ref[key] + if not filter['case_sensitive']: + # We only support inexact filters on strings so + # it's OK to use lower() + filter_value = filter_value.lower() + target_value = target_value.lower() + + if comparator == 'contains': + return (filter_value in target_value) + elif comparator == 'startswith': + return target_value.startswith(filter_value) + elif comparator == 'endswith': + return target_value.endswith(filter_value) + else: + # We silently ignore unsupported filters + return True + + return False + + for filter in hints.filters: + if filter['comparator'] == 'equals': + attr = filter['name'] + value = filter['value'] + refs = [r for r in refs if _attr_match( + utils.flatten_dict(r).get(attr), value)] + else: + # It might be an inexact filter + refs = [r for r in refs if _inexact_attr_match( + filter, r)] + + return refs + + @classmethod + def build_driver_hints(cls, context, supported_filters): + """Build list hints based on the context query string. + + :param context: contains the query_string from which any list hints can + be extracted + :param supported_filters: list of filters supported, so ignore any + keys in query_dict that are not in this list. + + """ + query_dict = context['query_string'] + hints = driver_hints.Hints() + + if query_dict is None: + return hints + + for key in query_dict: + # Check if this is an exact filter + if supported_filters is None or key in supported_filters: + hints.add_filter(key, query_dict[key]) + continue + + # Check if it is an inexact filter + for valid_key in supported_filters: + # See if this entry in query_dict matches a known key with an + # inexact suffix added. If it doesn't match, then that just + # means that there is no inexact filter for that key in this + # query. + if not key.startswith(valid_key + '__'): + continue + + base_key, comparator = key.split('__', 1) + + # We map the query-style inexact of, for example: + # + # {'email__contains', 'myISP'} + # + # into a list directive add filter call parameters of: + # + # name = 'email' + # value = 'myISP' + # comparator = 'contains' + # case_sensitive = True + + case_sensitive = True + if comparator.startswith('i'): + case_sensitive = False + comparator = comparator[1:] + hints.add_filter(base_key, query_dict[key], + comparator=comparator, + case_sensitive=case_sensitive) + + # NOTE(henry-nash): If we were to support pagination, we would pull any + # pagination directives out of the query_dict here, and add them into + # the hints list. + return hints + + def _require_matching_id(self, value, ref): + """Ensures the value matches the reference's ID, if any.""" + if 'id' in ref and ref['id'] != value: + raise exception.ValidationError('Cannot change ID') + + def _require_matching_domain_id(self, ref_id, ref, get_member): + """Ensure the current domain ID matches the reference one, if any. + + Provided we want domain IDs to be immutable, check whether any + domain_id specified in the ref dictionary matches the existing + domain_id for this entity. + + :param ref_id: the ID of the entity + :param ref: the dictionary of new values proposed for this entity + :param get_member: The member function to call to get the current + entity + :raises: :class:`keystone.exception.ValidationError` + + """ + # TODO(henry-nash): It might be safer and more efficient to do this + # check in the managers affected, so look to migrate this check to + # there in the future. + if CONF.domain_id_immutable and 'domain_id' in ref: + existing_ref = get_member(ref_id) + if ref['domain_id'] != existing_ref['domain_id']: + raise exception.ValidationError(_('Cannot change Domain ID')) + + def _assign_unique_id(self, ref): + """Generates and assigns a unique identifier to a reference.""" + ref = ref.copy() + ref['id'] = uuid.uuid4().hex + return ref + + def _get_domain_id_for_list_request(self, context): + """Get the domain_id for a v3 list call. + + If we running with multiple domain drivers, then the caller must + specify a domain_id either as a filter or as part of the token scope. + + """ + if not CONF.identity.domain_specific_drivers_enabled: + # We don't need to specify a domain ID in this case + return + + if context['query_string'].get('domain_id') is not None: + return context['query_string'].get('domain_id') + + try: + token_ref = token_model.KeystoneToken( + token_id=context['token_id'], + token_data=self.token_provider_api.validate_token( + context['token_id'])) + except KeyError: + raise exception.ValidationError( + _('domain_id is required as part of entity')) + except (exception.TokenNotFound, + exception.UnsupportedTokenVersionException): + LOG.warning(_LW('Invalid token found while getting domain ID ' + 'for list request')) + raise exception.Unauthorized() + + if token_ref.domain_scoped: + return token_ref.domain_id + else: + LOG.warning( + _LW('No domain information specified as part of list request')) + raise exception.Unauthorized() + + def _get_domain_id_from_token(self, context): + """Get the domain_id for a v3 create call. + + In the case of a v3 create entity call that does not specify a domain + ID, the spec says that we should use the domain scoping from the token + being used. + + """ + # We could make this more efficient by loading the domain_id + # into the context in the wrapper function above (since + # this version of normalize_domain will only be called inside + # a v3 protected call). However, this optimization is probably not + # worth the duplication of state + try: + token_ref = token_model.KeystoneToken( + token_id=context['token_id'], + token_data=self.token_provider_api.validate_token( + context['token_id'])) + except KeyError: + # This might happen if we use the Admin token, for instance + raise exception.ValidationError( + _('A domain-scoped token must be used')) + except (exception.TokenNotFound, + exception.UnsupportedTokenVersionException): + LOG.warning(_LW('Invalid token found while getting domain ID ' + 'for list request')) + raise exception.Unauthorized() + + if token_ref.domain_scoped: + return token_ref.domain_id + else: + # TODO(henry-nash): We should issue an exception here since if + # a v3 call does not explicitly specify the domain_id in the + # entity, it should be using a domain scoped token. However, + # the current tempest heat tests issue a v3 call without this. + # This is raised as bug #1283539. Once this is fixed, we + # should remove the line below and replace it with an error. + return CONF.identity.default_domain_id + + def _normalize_domain_id(self, context, ref): + """Fill in domain_id if not specified in a v3 call.""" + if 'domain_id' not in ref: + ref['domain_id'] = self._get_domain_id_from_token(context) + return ref + + @staticmethod + def filter_domain_id(ref): + """Override v2 filter to let domain_id out for v3 calls.""" + return ref + + def check_protection(self, context, prep_info, target_attr=None): + """Provide call protection for complex target attributes. + + As well as including the standard parameters from the original API + call (which is passed in prep_info), this call will add in any + additional entities or attributes (passed in target_attr), so that + they can be referenced by policy rules. + + """ + if 'is_admin' in context and context['is_admin']: + LOG.warning(_LW('RBAC: Bypassing authorization')) + else: + action = 'identity:%s' % prep_info['f_name'] + # TODO(henry-nash) need to log the target attributes as well + creds = _build_policy_check_credentials(self, action, + context, + prep_info['input_attr']) + # Build the dict the policy engine will check against from both the + # parameters passed into the call we are protecting (which was + # stored in the prep_info by protected()), plus the target + # attributes provided. + policy_dict = {} + if target_attr: + policy_dict = {'target': target_attr} + policy_dict.update(prep_info['input_attr']) + self.policy_api.enforce(creds, + action, + utils.flatten_dict(policy_dict)) + LOG.debug('RBAC: Authorization granted') + + @classmethod + def check_immutable_params(cls, ref): + """Raise exception when disallowed parameter is in ref. + + Check whether the ref dictionary representing a request has only + mutable parameters included. If not, raise an exception. This method + checks only root-level keys from a ref dictionary. + + :param ref: a dictionary representing deserialized request to be + stored + :raises: :class:`keystone.exception.ImmutableAttributeError` + + """ + ref_keys = set(ref.keys()) + blocked_keys = ref_keys.difference(cls._mutable_parameters) + + if not blocked_keys: + # No immutable parameters changed + return + + exception_args = {'target': cls.__name__, + 'attributes': ', '.join(blocked_keys)} + raise exception.ImmutableAttributeError(**exception_args) + + @classmethod + def filter_params(cls, ref): + """Remove unspecified parameters from the dictionary. + + This function removes unspecified parameters from the dictionary. See + check_immutable_parameters for corresponding function that raises + exceptions. This method checks only root-level keys from a ref + dictionary. + + :param ref: a dictionary representing deserialized response to be + serialized + """ + ref_keys = set(ref.keys()) + blocked_keys = ref_keys - cls._public_parameters + for blocked_param in blocked_keys: + del ref[blocked_param] + return ref diff --git a/keystone-moon/keystone/common/dependency.py b/keystone-moon/keystone/common/dependency.py new file mode 100644 index 00000000..14a68f19 --- /dev/null +++ b/keystone-moon/keystone/common/dependency.py @@ -0,0 +1,311 @@ +# Copyright 2012 OpenStack Foundation +# +# 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. + +"""This module provides support for dependency injection. + +Providers are registered via the ``@provider()`` decorator, and dependencies on +them are registered with ``@requires()`` or ``@optional()``. Providers are +available to their consumers via an attribute. See the documentation for the +individual functions for more detail. + +See also: + + https://en.wikipedia.org/wiki/Dependency_injection + +""" + +import traceback + +import six + +from keystone.i18n import _ +from keystone import notifications + + +_REGISTRY = {} + +_future_dependencies = {} +_future_optionals = {} +_factories = {} + + +def _set_provider(name, provider): + _original_provider, where_registered = _REGISTRY.get(name, (None, None)) + if where_registered: + raise Exception('%s already has a registered provider, at\n%s' % + (name, ''.join(where_registered))) + _REGISTRY[name] = (provider, traceback.format_stack()) + + +GET_REQUIRED = object() +GET_OPTIONAL = object() + + +def get_provider(name, optional=GET_REQUIRED): + if optional is GET_REQUIRED: + return _REGISTRY[name][0] + return _REGISTRY.get(name, (None, None))[0] + + +class UnresolvableDependencyException(Exception): + """Raised when a required dependency is not resolvable. + + See ``resolve_future_dependencies()`` for more details. + + """ + def __init__(self, name, targets): + msg = _('Unregistered dependency: %(name)s for %(targets)s') % { + 'name': name, 'targets': targets} + super(UnresolvableDependencyException, self).__init__(msg) + + +def provider(name): + """A class decorator used to register providers. + + When ``@provider()`` is used to decorate a class, members of that class + will register themselves as providers for the named dependency. As an + example, In the code fragment:: + + @dependency.provider('foo_api') + class Foo: + def __init__(self): + ... + + ... + + foo = Foo() + + The object ``foo`` will be registered as a provider for ``foo_api``. No + more than one such instance should be created; additional instances will + replace the previous ones, possibly resulting in different instances being + used by different consumers. + + """ + def wrapper(cls): + def wrapped(init): + def register_event_callbacks(self): + # NOTE(morganfainberg): A provider who has an implicit + # dependency on other providers may utilize the event callback + # mechanism to react to any changes in those providers. This is + # performed at the .provider() mechanism so that we can ensure + # that the callback is only ever called once and guaranteed + # to be on the properly configured and instantiated backend. + if not hasattr(self, 'event_callbacks'): + return + + if not isinstance(self.event_callbacks, dict): + msg = _('event_callbacks must be a dict') + raise ValueError(msg) + + for event in self.event_callbacks: + if not isinstance(self.event_callbacks[event], dict): + msg = _('event_callbacks[%s] must be a dict') % event + raise ValueError(msg) + for resource_type in self.event_callbacks[event]: + # Make sure we register the provider for each event it + # cares to call back. + callbacks = self.event_callbacks[event][resource_type] + if not callbacks: + continue + if not hasattr(callbacks, '__iter__'): + # ensure the callback information is a list + # allowing multiple callbacks to exist + callbacks = [callbacks] + notifications.register_event_callback(event, + resource_type, + callbacks) + + def __wrapped_init__(self, *args, **kwargs): + """Initialize the wrapped object and add it to the registry.""" + init(self, *args, **kwargs) + _set_provider(name, self) + register_event_callbacks(self) + + resolve_future_dependencies(__provider_name=name) + + return __wrapped_init__ + + cls.__init__ = wrapped(cls.__init__) + _factories[name] = cls + return cls + return wrapper + + +def _process_dependencies(obj): + # Any dependencies that can be resolved immediately are resolved. + # Dependencies that cannot be resolved immediately are stored for + # resolution in resolve_future_dependencies. + + def process(obj, attr_name, unresolved_in_out): + for dependency in getattr(obj, attr_name, []): + if dependency not in _REGISTRY: + # We don't know about this dependency, so save it for later. + unresolved_in_out.setdefault(dependency, []).append(obj) + continue + + setattr(obj, dependency, get_provider(dependency)) + + process(obj, '_dependencies', _future_dependencies) + process(obj, '_optionals', _future_optionals) + + +def requires(*dependencies): + """A class decorator used to inject providers into consumers. + + The required providers will be made available to instances of the decorated + class via an attribute with the same name as the provider. For example, in + the code fragment:: + + @dependency.requires('foo_api', 'bar_api') + class FooBarClient: + def __init__(self): + ... + + ... + + client = FooBarClient() + + The object ``client`` will have attributes named ``foo_api`` and + ``bar_api``, which are instances of the named providers. + + Objects must not rely on the existence of these attributes until after + ``resolve_future_dependencies()`` has been called; they may not exist + beforehand. + + Dependencies registered via ``@required()`` must have providers; if not, + an ``UnresolvableDependencyException`` will be raised when + ``resolve_future_dependencies()`` is called. + + """ + def wrapper(self, *args, **kwargs): + """Inject each dependency from the registry.""" + self.__wrapped_init__(*args, **kwargs) + _process_dependencies(self) + + def wrapped(cls): + """Note the required dependencies on the object for later injection. + + The dependencies of the parent class are combined with that of the + child class to create a new set of dependencies. + + """ + existing_dependencies = getattr(cls, '_dependencies', set()) + cls._dependencies = existing_dependencies.union(dependencies) + if not hasattr(cls, '__wrapped_init__'): + cls.__wrapped_init__ = cls.__init__ + cls.__init__ = wrapper + return cls + + return wrapped + + +def optional(*dependencies): + """Similar to ``@requires()``, except that the dependencies are optional. + + If no provider is available, the attributes will be set to ``None``. + + """ + def wrapper(self, *args, **kwargs): + """Inject each dependency from the registry.""" + self.__wrapped_init__(*args, **kwargs) + _process_dependencies(self) + + def wrapped(cls): + """Note the optional dependencies on the object for later injection. + + The dependencies of the parent class are combined with that of the + child class to create a new set of dependencies. + + """ + existing_optionals = getattr(cls, '_optionals', set()) + cls._optionals = existing_optionals.union(dependencies) + if not hasattr(cls, '__wrapped_init__'): + cls.__wrapped_init__ = cls.__init__ + cls.__init__ = wrapper + return cls + + return wrapped + + +def resolve_future_dependencies(__provider_name=None): + """Forces injection of all dependencies. + + Before this function is called, circular dependencies may not have been + injected. This function should be called only once, after all global + providers are registered. If an object needs to be created after this + call, it must not have circular dependencies. + + If any required dependencies are unresolvable, this function will raise an + ``UnresolvableDependencyException``. + + Outside of this module, this function should be called with no arguments; + the optional argument, ``__provider_name`` is used internally, and should + be treated as an implementation detail. + + """ + new_providers = dict() + if __provider_name: + # A provider was registered, so take care of any objects depending on + # it. + targets = _future_dependencies.pop(__provider_name, []) + targets.extend(_future_optionals.pop(__provider_name, [])) + + for target in targets: + setattr(target, __provider_name, get_provider(__provider_name)) + + return + + # Resolve optional dependencies, sets the attribute to None if there's no + # provider registered. + for dependency, targets in six.iteritems(_future_optionals.copy()): + provider = get_provider(dependency, optional=GET_OPTIONAL) + if provider is None: + factory = _factories.get(dependency) + if factory: + provider = factory() + new_providers[dependency] = provider + for target in targets: + setattr(target, dependency, provider) + + # Resolve future dependencies, raises UnresolvableDependencyException if + # there's no provider registered. + try: + for dependency, targets in six.iteritems(_future_dependencies.copy()): + if dependency not in _REGISTRY: + # a Class was registered that could fulfill the dependency, but + # it has not yet been initialized. + factory = _factories.get(dependency) + if factory: + provider = factory() + new_providers[dependency] = provider + else: + raise UnresolvableDependencyException(dependency, targets) + + for target in targets: + setattr(target, dependency, get_provider(dependency)) + finally: + _future_dependencies.clear() + return new_providers + + +def reset(): + """Reset the registry of providers. + + This is useful for unit testing to ensure that tests don't use providers + from previous tests. + """ + + _REGISTRY.clear() + _future_dependencies.clear() + _future_optionals.clear() diff --git a/keystone-moon/keystone/common/driver_hints.py b/keystone-moon/keystone/common/driver_hints.py new file mode 100644 index 00000000..0361e314 --- /dev/null +++ b/keystone-moon/keystone/common/driver_hints.py @@ -0,0 +1,65 @@ +# Copyright 2013 OpenStack Foundation +# Copyright 2013 IBM Corp. +# +# 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. + + +class Hints(object): + """Encapsulate driver hints for listing entities. + + Hints are modifiers that affect the return of entities from a + list_ operation. They are typically passed to a driver to give + direction as to what filtering, pagination or list limiting actions are + being requested. + + It is optional for a driver to action some or all of the list hints, + but any filters that it does satisfy must be marked as such by calling + removing the filter from the list. + + A Hint object contains filters, which is a list of dicts that can be + accessed publicly. Also it contains a dict called limit, which will + indicate the amount of data we want to limit our listing to. + + Each filter term consists of: + + * ``name``: the name of the attribute being matched + * ``value``: the value against which it is being matched + * ``comparator``: the operation, which can be one of ``equals``, + ``startswith`` or ``endswith`` + * ``case_sensitive``: whether any comparison should take account of + case + * ``type``: will always be 'filter' + + """ + def __init__(self): + self.limit = None + self.filters = list() + + def add_filter(self, name, value, comparator='equals', + case_sensitive=False): + """Adds a filter to the filters list, which is publicly accessible.""" + self.filters.append({'name': name, 'value': value, + 'comparator': comparator, + 'case_sensitive': case_sensitive, + 'type': 'filter'}) + + def get_exact_filter_by_name(self, name): + """Return a filter key and value if exact filter exists for name.""" + for entry in self.filters: + if (entry['type'] == 'filter' and entry['name'] == name and + entry['comparator'] == 'equals'): + return entry + + def set_limit(self, limit, truncated=False): + """Set a limit to indicate the list should be truncated.""" + self.limit = {'limit': limit, 'type': 'limit', 'truncated': truncated} diff --git a/keystone-moon/keystone/common/environment/__init__.py b/keystone-moon/keystone/common/environment/__init__.py new file mode 100644 index 00000000..da1de890 --- /dev/null +++ b/keystone-moon/keystone/common/environment/__init__.py @@ -0,0 +1,100 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 functools +import os + +from oslo_log import log + +LOG = log.getLogger(__name__) + + +__all__ = ['Server', 'httplib', 'subprocess'] + +_configured = False + +Server = None +httplib = None +subprocess = None + + +def configure_once(name): + """Ensure that environment configuration is only run once. + + If environment is reconfigured in the same way then it is ignored. + It is an error to attempt to reconfigure environment in a different way. + """ + def decorator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + global _configured + if _configured: + if _configured == name: + return + else: + raise SystemError("Environment has already been " + "configured as %s" % _configured) + + LOG.debug("Environment configured as: %s", name) + _configured = name + return func(*args, **kwargs) + + return wrapper + return decorator + + +@configure_once('eventlet') +def use_eventlet(monkeypatch_thread=None): + global httplib, subprocess, Server + + # This must be set before the initial import of eventlet because if + # dnspython is present in your environment then eventlet monkeypatches + # socket.getaddrinfo() with an implementation which doesn't work for IPv6. + os.environ['EVENTLET_NO_GREENDNS'] = 'yes' + + import eventlet + from eventlet.green import httplib as _httplib + from eventlet.green import subprocess as _subprocess + + from keystone.common.environment import eventlet_server + + if monkeypatch_thread is None: + monkeypatch_thread = not os.getenv('STANDARD_THREADS') + + # Raise the default from 8192 to accommodate large tokens + eventlet.wsgi.MAX_HEADER_LINE = 16384 + + # NOTE(ldbragst): Explicitly declare what should be monkey patched and + # what shouldn't. Doing this allows for more readable code when + # understanding Eventlet in Keystone. The following is a complete list + # of what is monkey patched instead of passing all=False and then passing + # module=True to monkey patch a specific module. + eventlet.patcher.monkey_patch(os=False, select=True, socket=True, + thread=monkeypatch_thread, time=True, + psycopg=False, MySQLdb=False) + + Server = eventlet_server.Server + httplib = _httplib + subprocess = _subprocess + + +@configure_once('stdlib') +def use_stdlib(): + global httplib, subprocess + + import httplib as _httplib + import subprocess as _subprocess + + httplib = _httplib + subprocess = _subprocess diff --git a/keystone-moon/keystone/common/environment/eventlet_server.py b/keystone-moon/keystone/common/environment/eventlet_server.py new file mode 100644 index 00000000..639e074a --- /dev/null +++ b/keystone-moon/keystone/common/environment/eventlet_server.py @@ -0,0 +1,194 @@ +# Copyright 2012 OpenStack Foundation +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# Copyright 2010 OpenStack Foundation +# All Rights Reserved. +# +# 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 errno +import re +import socket +import ssl +import sys + +import eventlet +import eventlet.wsgi +import greenlet +from oslo_log import log +from oslo_log import loggers + +from keystone.i18n import _LE, _LI + + +LOG = log.getLogger(__name__) + +# The size of a pool that is used to spawn a single green thread in which +# a wsgi server is then started. The size of one is enough, because in case +# of several workers the parent process forks and each child gets a copy +# of a pool, which does not include any greenthread object as the spawn is +# done after the fork. +POOL_SIZE = 1 + + +class EventletFilteringLogger(loggers.WritableLogger): + # NOTE(morganfainberg): This logger is designed to filter out specific + # Tracebacks to limit the amount of data that eventlet can log. In the + # case of broken sockets (EPIPE and ECONNRESET), we are seeing a huge + # volume of data being written to the logs due to ~14 lines+ per traceback. + # The traceback in these cases are, at best, useful for limited debugging + # cases. + def __init__(self, *args, **kwargs): + super(EventletFilteringLogger, self).__init__(*args, **kwargs) + self.regex = re.compile(r'errno (%d|%d)' % + (errno.EPIPE, errno.ECONNRESET), re.IGNORECASE) + + def write(self, msg): + m = self.regex.search(msg) + if m: + self.logger.log(log.logging.DEBUG, 'Error(%s) writing to socket.', + m.group(1)) + else: + self.logger.log(self.level, msg.rstrip()) + + +class Server(object): + """Server class to manage multiple WSGI sockets and applications.""" + + def __init__(self, application, host=None, port=None, keepalive=False, + keepidle=None): + self.application = application + self.host = host or '0.0.0.0' + self.port = port or 0 + # Pool for a green thread in which wsgi server will be running + self.pool = eventlet.GreenPool(POOL_SIZE) + self.socket_info = {} + self.greenthread = None + self.do_ssl = False + self.cert_required = False + self.keepalive = keepalive + self.keepidle = keepidle + self.socket = None + + def listen(self, key=None, backlog=128): + """Create and start listening on socket. + + Call before forking worker processes. + + Raises Exception if this has already been called. + """ + + # TODO(dims): eventlet's green dns/socket module does not actually + # support IPv6 in getaddrinfo(). We need to get around this in the + # future or monitor upstream for a fix. + # Please refer below link + # (https://bitbucket.org/eventlet/eventlet/ + # src/e0f578180d7d82d2ed3d8a96d520103503c524ec/eventlet/support/ + # greendns.py?at=0.12#cl-163) + info = socket.getaddrinfo(self.host, + self.port, + socket.AF_UNSPEC, + socket.SOCK_STREAM)[0] + + try: + self.socket = eventlet.listen(info[-1], family=info[0], + backlog=backlog) + except EnvironmentError: + LOG.error(_LE("Could not bind to %(host)s:%(port)s"), + {'host': self.host, 'port': self.port}) + raise + + LOG.info(_LI('Starting %(arg0)s on %(host)s:%(port)s'), + {'arg0': sys.argv[0], + 'host': self.host, + 'port': self.port}) + + def start(self, key=None, backlog=128): + """Run a WSGI server with the given application.""" + + if self.socket is None: + self.listen(key=key, backlog=backlog) + + dup_socket = self.socket.dup() + if key: + self.socket_info[key] = self.socket.getsockname() + # SSL is enabled + if self.do_ssl: + if self.cert_required: + cert_reqs = ssl.CERT_REQUIRED + else: + cert_reqs = ssl.CERT_NONE + + dup_socket = eventlet.wrap_ssl(dup_socket, certfile=self.certfile, + keyfile=self.keyfile, + server_side=True, + cert_reqs=cert_reqs, + ca_certs=self.ca_certs) + + # Optionally enable keepalive on the wsgi socket. + if self.keepalive: + dup_socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) + + if self.keepidle is not None: + dup_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, + self.keepidle) + + self.greenthread = self.pool.spawn(self._run, + self.application, + dup_socket) + + def set_ssl(self, certfile, keyfile=None, ca_certs=None, + cert_required=True): + self.certfile = certfile + self.keyfile = keyfile + self.ca_certs = ca_certs + self.cert_required = cert_required + self.do_ssl = True + + def stop(self): + if self.greenthread is not None: + self.greenthread.kill() + + def wait(self): + """Wait until all servers have completed running.""" + try: + self.pool.waitall() + except KeyboardInterrupt: + pass + except greenlet.GreenletExit: + pass + + def reset(self): + """Required by the service interface. + + The service interface is used by the launcher when receiving a + SIGHUP. The service interface is defined in + keystone.openstack.common.service.Service. + + Keystone does not need to do anything here. + """ + pass + + def _run(self, application, socket): + """Start a WSGI server with a new green thread pool.""" + logger = log.getLogger('eventlet.wsgi.server') + try: + eventlet.wsgi.server(socket, application, + log=EventletFilteringLogger(logger), + debug=False) + except greenlet.GreenletExit: + # Wait until all servers have completed running + pass + except Exception: + LOG.exception(_LE('Server error')) + raise diff --git a/keystone-moon/keystone/common/extension.py b/keystone-moon/keystone/common/extension.py new file mode 100644 index 00000000..b2ea80bc --- /dev/null +++ b/keystone-moon/keystone/common/extension.py @@ -0,0 +1,45 @@ +# Copyright 2013 OpenStack Foundation +# +# 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. + + +ADMIN_EXTENSIONS = {} +PUBLIC_EXTENSIONS = {} + + +def register_admin_extension(url_prefix, extension_data): + """Register extension with collection of admin extensions. + + Extensions register the information here that will show + up in the /extensions page as a way to indicate that the extension is + active. + + url_prefix: unique key for the extension that will appear in the + urls generated by the extension. + + extension_data is a dictionary. The expected fields are: + 'name': short, human readable name of the extension + 'namespace': xml namespace + 'alias': identifier for the extension + 'updated': date the extension was last updated + 'description': text description of the extension + 'links': hyperlinks to documents describing the extension + + """ + ADMIN_EXTENSIONS[url_prefix] = extension_data + + +def register_public_extension(url_prefix, extension_data): + """Same as register_admin_extension but for public extensions.""" + + PUBLIC_EXTENSIONS[url_prefix] = extension_data diff --git a/keystone-moon/keystone/common/json_home.py b/keystone-moon/keystone/common/json_home.py new file mode 100644 index 00000000..215d596a --- /dev/null +++ b/keystone-moon/keystone/common/json_home.py @@ -0,0 +1,76 @@ +# Copyright 2014 IBM Corp. +# +# 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 six + + +def build_v3_resource_relation(resource_name): + return ('http://docs.openstack.org/api/openstack-identity/3/rel/%s' % + resource_name) + + +def build_v3_extension_resource_relation(extension_name, extension_version, + resource_name): + return ( + 'http://docs.openstack.org/api/openstack-identity/3/ext/%s/%s/rel/%s' % + (extension_name, extension_version, resource_name)) + + +def build_v3_parameter_relation(parameter_name): + return ('http://docs.openstack.org/api/openstack-identity/3/param/%s' % + parameter_name) + + +def build_v3_extension_parameter_relation(extension_name, extension_version, + parameter_name): + return ( + 'http://docs.openstack.org/api/openstack-identity/3/ext/%s/%s/param/' + '%s' % (extension_name, extension_version, parameter_name)) + + +class Parameters(object): + """Relationships for Common parameters.""" + + DOMAIN_ID = build_v3_parameter_relation('domain_id') + ENDPOINT_ID = build_v3_parameter_relation('endpoint_id') + GROUP_ID = build_v3_parameter_relation('group_id') + POLICY_ID = build_v3_parameter_relation('policy_id') + PROJECT_ID = build_v3_parameter_relation('project_id') + REGION_ID = build_v3_parameter_relation('region_id') + ROLE_ID = build_v3_parameter_relation('role_id') + SERVICE_ID = build_v3_parameter_relation('service_id') + USER_ID = build_v3_parameter_relation('user_id') + + +class Status(object): + """Status values supported.""" + + DEPRECATED = 'deprecated' + EXPERIMENTAL = 'experimental' + STABLE = 'stable' + + @classmethod + def is_supported(cls, status): + return status in [cls.DEPRECATED, cls.EXPERIMENTAL, cls.STABLE] + + +def translate_urls(json_home, new_prefix): + """Given a JSON Home document, sticks new_prefix on each of the urls.""" + + for dummy_rel, resource in six.iteritems(json_home['resources']): + if 'href' in resource: + resource['href'] = new_prefix + resource['href'] + elif 'href-template' in resource: + resource['href-template'] = new_prefix + resource['href-template'] diff --git a/keystone-moon/keystone/common/kvs/__init__.py b/keystone-moon/keystone/common/kvs/__init__.py new file mode 100644 index 00000000..9a406a85 --- /dev/null +++ b/keystone-moon/keystone/common/kvs/__init__.py @@ -0,0 +1,33 @@ +# Copyright 2013 Metacloud, Inc. +# +# 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 dogpile.cache import region + +from keystone.common.kvs.core import * # noqa +from keystone.common.kvs.legacy import Base, DictKvs, INMEMDB # noqa + + +# NOTE(morganfainberg): Provided backends are registered here in the __init__ +# for the kvs system. Any out-of-tree backends should be registered via the +# ``backends`` option in the ``[kvs]`` section of the Keystone configuration +# file. +region.register_backend( + 'openstack.kvs.Memory', + 'keystone.common.kvs.backends.inmemdb', + 'MemoryBackend') + +region.register_backend( + 'openstack.kvs.Memcached', + 'keystone.common.kvs.backends.memcached', + 'MemcachedBackend') diff --git a/keystone-moon/keystone/common/kvs/backends/__init__.py b/keystone-moon/keystone/common/kvs/backends/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/common/kvs/backends/inmemdb.py b/keystone-moon/keystone/common/kvs/backends/inmemdb.py new file mode 100644 index 00000000..68072ef4 --- /dev/null +++ b/keystone-moon/keystone/common/kvs/backends/inmemdb.py @@ -0,0 +1,69 @@ +# Copyright 2013 Metacloud, Inc. +# +# 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. + +""" +Keystone In-Memory Dogpile.cache backend implementation. +""" + +import copy + +from dogpile.cache import api + + +NO_VALUE = api.NO_VALUE + + +class MemoryBackend(api.CacheBackend): + """A backend that uses a plain dictionary. + + There is no size management, and values which are placed into the + dictionary will remain until explicitly removed. Note that Dogpile's + expiration of items is based on timestamps and does not remove them from + the cache. + + E.g.:: + + from dogpile.cache import make_region + + region = make_region().configure( + 'keystone.common.kvs.Memory' + ) + """ + def __init__(self, arguments): + self._db = {} + + def _isolate_value(self, value): + if value is not NO_VALUE: + return copy.deepcopy(value) + return value + + def get(self, key): + return self._isolate_value(self._db.get(key, NO_VALUE)) + + def get_multi(self, keys): + return [self.get(key) for key in keys] + + def set(self, key, value): + self._db[key] = self._isolate_value(value) + + def set_multi(self, mapping): + for key, value in mapping.items(): + self.set(key, value) + + def delete(self, key): + self._db.pop(key, None) + + def delete_multi(self, keys): + for key in keys: + self.delete(key) diff --git a/keystone-moon/keystone/common/kvs/backends/memcached.py b/keystone-moon/keystone/common/kvs/backends/memcached.py new file mode 100644 index 00000000..db453143 --- /dev/null +++ b/keystone-moon/keystone/common/kvs/backends/memcached.py @@ -0,0 +1,188 @@ +# Copyright 2013 Metacloud, Inc. +# +# 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. + +""" +Keystone Memcached dogpile.cache backend implementation. +""" + +import random as _random +import time + +from dogpile.cache import api +from dogpile.cache.backends import memcached +from oslo_config import cfg +from oslo_log import log + +from keystone.common.cache.backends import memcache_pool +from keystone.common import manager +from keystone import exception +from keystone.i18n import _ + + +CONF = cfg.CONF +LOG = log.getLogger(__name__) +NO_VALUE = api.NO_VALUE +random = _random.SystemRandom() + +VALID_DOGPILE_BACKENDS = dict( + pylibmc=memcached.PylibmcBackend, + bmemcached=memcached.BMemcachedBackend, + memcached=memcached.MemcachedBackend, + pooled_memcached=memcache_pool.PooledMemcachedBackend) + + +class MemcachedLock(object): + """Simple distributed lock using memcached. + + This is an adaptation of the lock featured at + http://amix.dk/blog/post/19386 + + """ + def __init__(self, client_fn, key, lock_timeout, max_lock_attempts): + self.client_fn = client_fn + self.key = "_lock" + key + self.lock_timeout = lock_timeout + self.max_lock_attempts = max_lock_attempts + + def acquire(self, wait=True): + client = self.client_fn() + for i in range(self.max_lock_attempts): + if client.add(self.key, 1, self.lock_timeout): + return True + elif not wait: + return False + else: + sleep_time = random.random() + time.sleep(sleep_time) + raise exception.UnexpectedError( + _('Maximum lock attempts on %s occurred.') % self.key) + + def release(self): + client = self.client_fn() + client.delete(self.key) + + +class MemcachedBackend(manager.Manager): + """Pivot point to leverage the various dogpile.cache memcached backends. + + To specify a specific dogpile.cache memcached driver, pass the argument + `memcached_driver` set to one of the provided memcached drivers (at this + time `memcached`, `bmemcached`, `pylibmc` are valid). + """ + def __init__(self, arguments): + self._key_mangler = None + self.raw_no_expiry_keys = set(arguments.pop('no_expiry_keys', set())) + self.no_expiry_hashed_keys = set() + + self.lock_timeout = arguments.pop('lock_timeout', None) + self.max_lock_attempts = arguments.pop('max_lock_attempts', 15) + # NOTE(morganfainberg): Remove distributed locking from the arguments + # passed to the "real" backend if it exists. + arguments.pop('distributed_lock', None) + backend = arguments.pop('memcached_backend', None) + if 'url' not in arguments: + # FIXME(morganfainberg): Log deprecation warning for old-style + # configuration once full dict_config style configuration for + # KVS backends is supported. For now use the current memcache + # section of the configuration. + arguments['url'] = CONF.memcache.servers + + if backend is None: + # NOTE(morganfainberg): Use the basic memcached backend if nothing + # else is supplied. + self.driver = VALID_DOGPILE_BACKENDS['memcached'](arguments) + else: + if backend not in VALID_DOGPILE_BACKENDS: + raise ValueError( + _('Backend `%(driver)s` is not a valid memcached ' + 'backend. Valid drivers: %(driver_list)s') % + {'driver': backend, + 'driver_list': ','.join(VALID_DOGPILE_BACKENDS.keys())}) + else: + self.driver = VALID_DOGPILE_BACKENDS[backend](arguments) + + def _get_set_arguments_driver_attr(self, exclude_expiry=False): + + # NOTE(morganfainberg): Shallow copy the .set_arguments dict to + # ensure no changes cause the values to change in the instance + # variable. + set_arguments = getattr(self.driver, 'set_arguments', {}).copy() + + if exclude_expiry: + # NOTE(morganfainberg): Explicitly strip out the 'time' key/value + # from the set_arguments in the case that this key isn't meant + # to expire + set_arguments.pop('time', None) + return set_arguments + + def set(self, key, value): + mapping = {key: value} + self.set_multi(mapping) + + def set_multi(self, mapping): + mapping_keys = set(mapping.keys()) + no_expiry_keys = mapping_keys.intersection(self.no_expiry_hashed_keys) + has_expiry_keys = mapping_keys.difference(self.no_expiry_hashed_keys) + + if no_expiry_keys: + # NOTE(morganfainberg): For keys that have expiry excluded, + # bypass the backend and directly call the client. Bypass directly + # to the client is required as the 'set_arguments' are applied to + # all ``set`` and ``set_multi`` calls by the driver, by calling + # the client directly it is possible to exclude the ``time`` + # argument to the memcached server. + new_mapping = {k: mapping[k] for k in no_expiry_keys} + set_arguments = self._get_set_arguments_driver_attr( + exclude_expiry=True) + self.driver.client.set_multi(new_mapping, **set_arguments) + + if has_expiry_keys: + new_mapping = {k: mapping[k] for k in has_expiry_keys} + self.driver.set_multi(new_mapping) + + @classmethod + def from_config_dict(cls, config_dict, prefix): + prefix_len = len(prefix) + return cls( + {key[prefix_len:]: config_dict[key] for key in config_dict + if key.startswith(prefix)}) + + @property + def key_mangler(self): + if self._key_mangler is None: + self._key_mangler = self.driver.key_mangler + return self._key_mangler + + @key_mangler.setter + def key_mangler(self, key_mangler): + if callable(key_mangler): + self._key_mangler = key_mangler + self._rehash_keys() + elif key_mangler is None: + # NOTE(morganfainberg): Set the hashed key map to the unhashed + # list since we no longer have a key_mangler. + self._key_mangler = None + self.no_expiry_hashed_keys = self.raw_no_expiry_keys + else: + raise TypeError(_('`key_mangler` functions must be callable.')) + + def _rehash_keys(self): + no_expire = set() + for key in self.raw_no_expiry_keys: + no_expire.add(self._key_mangler(key)) + self.no_expiry_hashed_keys = no_expire + + def get_mutex(self, key): + return MemcachedLock(lambda: self.driver.client, key, + self.lock_timeout, self.max_lock_attempts) diff --git a/keystone-moon/keystone/common/kvs/core.py b/keystone-moon/keystone/common/kvs/core.py new file mode 100644 index 00000000..cbbb7462 --- /dev/null +++ b/keystone-moon/keystone/common/kvs/core.py @@ -0,0 +1,423 @@ +# Copyright 2013 Metacloud, Inc. +# +# 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 contextlib +import threading +import time +import weakref + +from dogpile.cache import api +from dogpile.cache import proxy +from dogpile.cache import region +from dogpile.cache import util as dogpile_util +from dogpile.core import nameregistry +from oslo_config import cfg +from oslo_log import log +from oslo_utils import importutils +import six + +from keystone import exception +from keystone.i18n import _ +from keystone.i18n import _LI +from keystone.i18n import _LW + + +__all__ = ['KeyValueStore', 'KeyValueStoreLock', 'LockTimeout', + 'get_key_value_store'] + + +BACKENDS_REGISTERED = False +CONF = cfg.CONF +KEY_VALUE_STORE_REGISTRY = weakref.WeakValueDictionary() +LOCK_WINDOW = 1 +LOG = log.getLogger(__name__) +NO_VALUE = api.NO_VALUE + + +def _register_backends(): + # NOTE(morganfainberg): This function exists to ensure we do not try and + # register the backends prior to the configuration object being fully + # available. We also need to ensure we do not register a given backend + # more than one time. All backends will be prefixed with openstack.kvs + # as the "short" name to reference them for configuration purposes. This + # function is used in addition to the pre-registered backends in the + # __init__ file for the KVS system. + global BACKENDS_REGISTERED + + if not BACKENDS_REGISTERED: + prefix = 'openstack.kvs.%s' + for backend in CONF.kvs.backends: + module, cls = backend.rsplit('.', 1) + backend_name = prefix % cls + LOG.debug(('Registering Dogpile Backend %(backend_path)s as ' + '%(backend_name)s'), + {'backend_path': backend, 'backend_name': backend_name}) + region.register_backend(backend_name, module, cls) + BACKENDS_REGISTERED = True + + +class LockTimeout(exception.UnexpectedError): + debug_message_format = _('Lock Timeout occurred for key, %(target)s') + + +class KeyValueStore(object): + """Basic KVS manager object to support Keystone Key-Value-Store systems. + + This manager also supports the concept of locking a given key resource to + allow for a guaranteed atomic transaction to the backend. + """ + def __init__(self, kvs_region): + self.locking = True + self._lock_timeout = 0 + self._region = kvs_region + self._security_strategy = None + self._secret_key = None + self._lock_registry = nameregistry.NameRegistry(self._create_mutex) + + def configure(self, backing_store, key_mangler=None, proxy_list=None, + locking=True, **region_config_args): + """Configure the KeyValueStore instance. + + :param backing_store: dogpile.cache short name of the region backend + :param key_mangler: key_mangler function + :param proxy_list: list of proxy classes to apply to the region + :param locking: boolean that allows disabling of locking mechanism for + this instantiation + :param region_config_args: key-word args passed to the dogpile.cache + backend for configuration + :return: + """ + if self.is_configured: + # NOTE(morganfainberg): It is a bad idea to reconfigure a backend, + # there are a lot of pitfalls and potential memory leaks that could + # occur. By far the best approach is to re-create the KVS object + # with the new configuration. + raise RuntimeError(_('KVS region %s is already configured. ' + 'Cannot reconfigure.') % self._region.name) + + self.locking = locking + self._lock_timeout = region_config_args.pop( + 'lock_timeout', CONF.kvs.default_lock_timeout) + self._configure_region(backing_store, **region_config_args) + self._set_key_mangler(key_mangler) + self._apply_region_proxy(proxy_list) + + @property + def is_configured(self): + return 'backend' in self._region.__dict__ + + def _apply_region_proxy(self, proxy_list): + if isinstance(proxy_list, list): + proxies = [] + + for item in proxy_list: + if isinstance(item, str): + LOG.debug('Importing class %s as KVS proxy.', item) + pxy = importutils.import_class(item) + else: + pxy = item + + if issubclass(pxy, proxy.ProxyBackend): + proxies.append(pxy) + else: + LOG.warning(_LW('%s is not a dogpile.proxy.ProxyBackend'), + pxy.__name__) + + for proxy_cls in reversed(proxies): + LOG.info(_LI('Adding proxy \'%(proxy)s\' to KVS %(name)s.'), + {'proxy': proxy_cls.__name__, + 'name': self._region.name}) + self._region.wrap(proxy_cls) + + def _assert_configured(self): + if'backend' not in self._region.__dict__: + raise exception.UnexpectedError(_('Key Value Store not ' + 'configured: %s'), + self._region.name) + + def _set_keymangler_on_backend(self, key_mangler): + try: + self._region.backend.key_mangler = key_mangler + except Exception as e: + # NOTE(morganfainberg): The setting of the key_mangler on the + # backend is used to allow the backend to + # calculate a hashed key value as needed. Not all backends + # require the ability to calculate hashed keys. If the + # backend does not support/require this feature log a + # debug line and move on otherwise raise the proper exception. + # Support of the feature is implied by the existence of the + # 'raw_no_expiry_keys' attribute. + if not hasattr(self._region.backend, 'raw_no_expiry_keys'): + LOG.debug(('Non-expiring keys not supported/required by ' + '%(region)s backend; unable to set ' + 'key_mangler for backend: %(err)s'), + {'region': self._region.name, 'err': e}) + else: + raise + + def _set_key_mangler(self, key_mangler): + # Set the key_mangler that is appropriate for the given region being + # configured here. The key_mangler function is called prior to storing + # the value(s) in the backend. This is to help prevent collisions and + # limit issues such as memcache's limited cache_key size. + use_backend_key_mangler = getattr(self._region.backend, + 'use_backend_key_mangler', False) + if ((key_mangler is None or use_backend_key_mangler) and + (self._region.backend.key_mangler is not None)): + # NOTE(morganfainberg): Use the configured key_mangler as a first + # choice. Second choice would be the key_mangler defined by the + # backend itself. Finally, fall back to the defaults. The one + # exception is if the backend defines `use_backend_key_mangler` + # as True, which indicates the backend's key_mangler should be + # the first choice. + key_mangler = self._region.backend.key_mangler + + if CONF.kvs.enable_key_mangler: + if key_mangler is not None: + msg = _LI('Using %(func)s as KVS region %(name)s key_mangler') + if callable(key_mangler): + self._region.key_mangler = key_mangler + LOG.info(msg, {'func': key_mangler.__name__, + 'name': self._region.name}) + else: + # NOTE(morganfainberg): We failed to set the key_mangler, + # we should error out here to ensure we aren't causing + # key-length or collision issues. + raise exception.ValidationError( + _('`key_mangler` option must be a function reference')) + else: + LOG.info(_LI('Using default dogpile sha1_mangle_key as KVS ' + 'region %s key_mangler'), self._region.name) + # NOTE(morganfainberg): Sane 'default' keymangler is the + # dogpile sha1_mangle_key function. This ensures that unless + # explicitly changed, we mangle keys. This helps to limit + # unintended cases of exceeding cache-key in backends such + # as memcache. + self._region.key_mangler = dogpile_util.sha1_mangle_key + self._set_keymangler_on_backend(self._region.key_mangler) + else: + LOG.info(_LI('KVS region %s key_mangler disabled.'), + self._region.name) + self._set_keymangler_on_backend(None) + + def _configure_region(self, backend, **config_args): + prefix = CONF.kvs.config_prefix + conf_dict = {} + conf_dict['%s.backend' % prefix] = backend + + if 'distributed_lock' not in config_args: + config_args['distributed_lock'] = True + + config_args['lock_timeout'] = self._lock_timeout + + # NOTE(morganfainberg): To mitigate race conditions on comparing + # the timeout and current time on the lock mutex, we are building + # in a static 1 second overlap where the lock will still be valid + # in the backend but not from the perspective of the context + # manager. Since we must develop to the lowest-common-denominator + # when it comes to the backends, memcache's cache store is not more + # refined than 1 second, therefore we must build in at least a 1 + # second overlap. `lock_timeout` of 0 means locks never expire. + if config_args['lock_timeout'] > 0: + config_args['lock_timeout'] += LOCK_WINDOW + + for argument, value in six.iteritems(config_args): + arg_key = '.'.join([prefix, 'arguments', argument]) + conf_dict[arg_key] = value + + LOG.debug('KVS region configuration for %(name)s: %(config)r', + {'name': self._region.name, 'config': conf_dict}) + self._region.configure_from_config(conf_dict, '%s.' % prefix) + + def _mutex(self, key): + return self._lock_registry.get(key) + + def _create_mutex(self, key): + mutex = self._region.backend.get_mutex(key) + if mutex is not None: + return mutex + else: + return self._LockWrapper(lock_timeout=self._lock_timeout) + + class _LockWrapper(object): + """weakref-capable threading.Lock wrapper.""" + def __init__(self, lock_timeout): + self.lock = threading.Lock() + self.lock_timeout = lock_timeout + + def acquire(self, wait=True): + return self.lock.acquire(wait) + + def release(self): + self.lock.release() + + def get(self, key): + """Get a single value from the KVS backend.""" + self._assert_configured() + value = self._region.get(key) + if value is NO_VALUE: + raise exception.NotFound(target=key) + return value + + def get_multi(self, keys): + """Get multiple values in a single call from the KVS backend.""" + self._assert_configured() + values = self._region.get_multi(keys) + not_found = [] + for index, key in enumerate(keys): + if values[index] is NO_VALUE: + not_found.append(key) + if not_found: + # NOTE(morganfainberg): If any of the multi-get values are non- + # existent, we should raise a NotFound error to mimic the .get() + # method's behavior. In all cases the internal dogpile NO_VALUE + # should be masked from the consumer of the KeyValueStore. + raise exception.NotFound(target=not_found) + return values + + def set(self, key, value, lock=None): + """Set a single value in the KVS backend.""" + self._assert_configured() + with self._action_with_lock(key, lock): + self._region.set(key, value) + + def set_multi(self, mapping): + """Set multiple key/value pairs in the KVS backend at once. + + Like delete_multi, this call does not serialize through the + KeyValueStoreLock mechanism (locking cannot occur on more than one + key in a given context without significant deadlock potential). + """ + self._assert_configured() + self._region.set_multi(mapping) + + def delete(self, key, lock=None): + """Delete a single key from the KVS backend. + + This method will raise NotFound if the key doesn't exist. The get and + delete are done in a single transaction (via KeyValueStoreLock + mechanism). + """ + self._assert_configured() + + with self._action_with_lock(key, lock): + self.get(key) + self._region.delete(key) + + def delete_multi(self, keys): + """Delete multiple keys from the KVS backend in a single call. + + Like set_multi, this call does not serialize through the + KeyValueStoreLock mechanism (locking cannot occur on more than one + key in a given context without significant deadlock potential). + """ + self._assert_configured() + self._region.delete_multi(keys) + + def get_lock(self, key): + """Get a write lock on the KVS value referenced by `key`. + + The ability to get a context manager to pass into the set/delete + methods allows for a single-transaction to occur while guaranteeing the + backing store will not change between the start of the 'lock' and the + end. Lock timeout is fixed to the KeyValueStore configured lock + timeout. + """ + self._assert_configured() + return KeyValueStoreLock(self._mutex(key), key, self.locking, + self._lock_timeout) + + @contextlib.contextmanager + def _action_with_lock(self, key, lock=None): + """Wrapper context manager to validate and handle the lock and lock + timeout if passed in. + """ + if not isinstance(lock, KeyValueStoreLock): + # NOTE(morganfainberg): Locking only matters if a lock is passed in + # to this method. If lock isn't a KeyValueStoreLock, treat this as + # if no locking needs to occur. + yield + else: + if not lock.key == key: + raise ValueError(_('Lock key must match target key: %(lock)s ' + '!= %(target)s') % + {'lock': lock.key, 'target': key}) + if not lock.active: + raise exception.ValidationError(_('Must be called within an ' + 'active lock context.')) + if not lock.expired: + yield + else: + raise LockTimeout(target=key) + + +class KeyValueStoreLock(object): + """Basic KeyValueStoreLock context manager that hooks into the + dogpile.cache backend mutex allowing for distributed locking on resources. + + This is only a write lock, and will not prevent reads from occurring. + """ + def __init__(self, mutex, key, locking_enabled=True, lock_timeout=0): + self.mutex = mutex + self.key = key + self.enabled = locking_enabled + self.lock_timeout = lock_timeout + self.active = False + self.acquire_time = 0 + + def acquire(self): + if self.enabled: + self.mutex.acquire() + LOG.debug('KVS lock acquired for: %s', self.key) + self.active = True + self.acquire_time = time.time() + return self + + __enter__ = acquire + + @property + def expired(self): + if self.lock_timeout: + calculated = time.time() - self.acquire_time + LOCK_WINDOW + return calculated > self.lock_timeout + else: + return False + + def release(self): + if self.enabled: + self.mutex.release() + if not self.expired: + LOG.debug('KVS lock released for: %s', self.key) + else: + LOG.warning(_LW('KVS lock released (timeout reached) for: %s'), + self.key) + + def __exit__(self, exc_type, exc_val, exc_tb): + self.release() + + +def get_key_value_store(name, kvs_region=None): + """Instantiate a new :class:`.KeyValueStore` or return a previous + instantiation that has the same name. + """ + global KEY_VALUE_STORE_REGISTRY + + _register_backends() + key_value_store = KEY_VALUE_STORE_REGISTRY.get(name) + if key_value_store is None: + if kvs_region is None: + kvs_region = region.make_region(name=name) + key_value_store = KeyValueStore(kvs_region) + KEY_VALUE_STORE_REGISTRY[name] = key_value_store + return key_value_store diff --git a/keystone-moon/keystone/common/kvs/legacy.py b/keystone-moon/keystone/common/kvs/legacy.py new file mode 100644 index 00000000..ba036016 --- /dev/null +++ b/keystone-moon/keystone/common/kvs/legacy.py @@ -0,0 +1,60 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 keystone import exception +from keystone.openstack.common import versionutils + + +class DictKvs(dict): + def get(self, key, default=None): + try: + if isinstance(self[key], dict): + return self[key].copy() + else: + return self[key][:] + except KeyError: + if default is not None: + return default + raise exception.NotFound(target=key) + + def set(self, key, value): + if isinstance(value, dict): + self[key] = value.copy() + else: + self[key] = value[:] + + def delete(self, key): + """Deletes an item, returning True on success, False otherwise.""" + try: + del self[key] + except KeyError: + raise exception.NotFound(target=key) + + +INMEMDB = DictKvs() + + +class Base(object): + @versionutils.deprecated(versionutils.deprecated.ICEHOUSE, + in_favor_of='keystone.common.kvs.KeyValueStore', + remove_in=+2, + what='keystone.common.kvs.Base') + def __init__(self, db=None): + if db is None: + db = INMEMDB + elif isinstance(db, DictKvs): + db = db + elif isinstance(db, dict): + db = DictKvs(db) + self.db = db diff --git a/keystone-moon/keystone/common/ldap/__init__.py b/keystone-moon/keystone/common/ldap/__init__.py new file mode 100644 index 00000000..ab5bf4d0 --- /dev/null +++ b/keystone-moon/keystone/common/ldap/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 keystone.common.ldap.core import * # noqa diff --git a/keystone-moon/keystone/common/ldap/core.py b/keystone-moon/keystone/common/ldap/core.py new file mode 100644 index 00000000..144c0cfd --- /dev/null +++ b/keystone-moon/keystone/common/ldap/core.py @@ -0,0 +1,1910 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 abc +import codecs +import functools +import os.path +import re +import sys +import weakref + +import ldap.filter +import ldappool +from oslo_log import log +import six + +from keystone import exception +from keystone.i18n import _ +from keystone.i18n import _LW + +LOG = log.getLogger(__name__) + +LDAP_VALUES = {'TRUE': True, 'FALSE': False} +CONTROL_TREEDELETE = '1.2.840.113556.1.4.805' +LDAP_SCOPES = {'one': ldap.SCOPE_ONELEVEL, + 'sub': ldap.SCOPE_SUBTREE} +LDAP_DEREF = {'always': ldap.DEREF_ALWAYS, + 'default': None, + 'finding': ldap.DEREF_FINDING, + 'never': ldap.DEREF_NEVER, + 'searching': ldap.DEREF_SEARCHING} +LDAP_TLS_CERTS = {'never': ldap.OPT_X_TLS_NEVER, + 'demand': ldap.OPT_X_TLS_DEMAND, + 'allow': ldap.OPT_X_TLS_ALLOW} + + +# RFC 4511 (The LDAP Protocol) defines a list containing only the OID '1.1' to +# indicate that no attributes should be returned besides the DN. +DN_ONLY = ['1.1'] + +_utf8_encoder = codecs.getencoder('utf-8') + + +def utf8_encode(value): + """Encode a basestring to UTF-8. + + If the string is unicode encode it to UTF-8, if the string is + str then assume it's already encoded. Otherwise raise a TypeError. + + :param value: A basestring + :returns: UTF-8 encoded version of value + :raises: TypeError if value is not basestring + """ + if isinstance(value, six.text_type): + return _utf8_encoder(value)[0] + elif isinstance(value, six.binary_type): + return value + else: + raise TypeError("value must be basestring, " + "not %s" % value.__class__.__name__) + +_utf8_decoder = codecs.getdecoder('utf-8') + + +def utf8_decode(value): + """Decode a from UTF-8 into unicode. + + If the value is a binary string assume it's UTF-8 encoded and decode + it into a unicode string. Otherwise convert the value from its + type into a unicode string. + + :param value: value to be returned as unicode + :returns: value as unicode + :raises: UnicodeDecodeError for invalid UTF-8 encoding + """ + if isinstance(value, six.binary_type): + return _utf8_decoder(value)[0] + return six.text_type(value) + + +def py2ldap(val): + """Type convert a Python value to a type accepted by LDAP (unicode). + + The LDAP API only accepts strings for values therefore convert + the value's type to a unicode string. A subsequent type conversion + will encode the unicode as UTF-8 as required by the python-ldap API, + but for now we just want a string representation of the value. + + :param val: The value to convert to a LDAP string representation + :returns: unicode string representation of value. + """ + if isinstance(val, bool): + return u'TRUE' if val else u'FALSE' + else: + return six.text_type(val) + + +def enabled2py(val): + """Similar to ldap2py, only useful for the enabled attribute.""" + + try: + return LDAP_VALUES[val] + except KeyError: + pass + try: + return int(val) + except ValueError: + pass + return utf8_decode(val) + + +def ldap2py(val): + """Convert an LDAP formatted value to Python type used by OpenStack. + + Virtually all LDAP values are stored as UTF-8 encoded strings. + OpenStack prefers values which are unicode friendly. + + :param val: LDAP formatted value + :returns: val converted to preferred Python type + """ + return utf8_decode(val) + + +def convert_ldap_result(ldap_result): + """Convert LDAP search result to Python types used by OpenStack. + + Each result tuple is of the form (dn, attrs), where dn is a string + containing the DN (distinguished name) of the entry, and attrs is + a dictionary containing the attributes associated with the + entry. The keys of attrs are strings, and the associated values + are lists of strings. + + OpenStack wants to use Python types of its choosing. Strings will + be unicode, truth values boolean, whole numbers int's, etc. DN's will + also be decoded from UTF-8 to unicode. + + :param ldap_result: LDAP search result + :returns: list of 2-tuples containing (dn, attrs) where dn is unicode + and attrs is a dict whose values are type converted to + OpenStack preferred types. + """ + py_result = [] + at_least_one_referral = False + for dn, attrs in ldap_result: + ldap_attrs = {} + if dn is None: + # this is a Referral object, rather than an Entry object + at_least_one_referral = True + continue + + for kind, values in six.iteritems(attrs): + try: + val2py = enabled2py if kind == 'enabled' else ldap2py + ldap_attrs[kind] = [val2py(x) for x in values] + except UnicodeDecodeError: + LOG.debug('Unable to decode value for attribute %s', kind) + + py_result.append((utf8_decode(dn), ldap_attrs)) + if at_least_one_referral: + LOG.debug(('Referrals were returned and ignored. Enable referral ' + 'chasing in keystone.conf via [ldap] chase_referrals')) + + return py_result + + +def safe_iter(attrs): + if attrs is None: + return + elif isinstance(attrs, list): + for e in attrs: + yield e + else: + yield attrs + + +def parse_deref(opt): + try: + return LDAP_DEREF[opt] + except KeyError: + raise ValueError(_('Invalid LDAP deref option: %(option)s. ' + 'Choose one of: %(options)s') % + {'option': opt, + 'options': ', '.join(LDAP_DEREF.keys()), }) + + +def parse_tls_cert(opt): + try: + return LDAP_TLS_CERTS[opt] + except KeyError: + raise ValueError(_( + 'Invalid LDAP TLS certs option: %(option)s. ' + 'Choose one of: %(options)s') % { + 'option': opt, + 'options': ', '.join(LDAP_TLS_CERTS.keys())}) + + +def ldap_scope(scope): + try: + return LDAP_SCOPES[scope] + except KeyError: + raise ValueError( + _('Invalid LDAP scope: %(scope)s. Choose one of: %(options)s') % { + 'scope': scope, + 'options': ', '.join(LDAP_SCOPES.keys())}) + + +def prep_case_insensitive(value): + """Prepare a string for case-insensitive comparison. + + This is defined in RFC4518. For simplicity, all this function does is + lowercase all the characters, strip leading and trailing whitespace, + and compress sequences of spaces to a single space. + """ + value = re.sub(r'\s+', ' ', value.strip().lower()) + return value + + +def is_ava_value_equal(attribute_type, val1, val2): + """Returns True if and only if the AVAs are equal. + + When comparing AVAs, the equality matching rule for the attribute type + should be taken into consideration. For simplicity, this implementation + does a case-insensitive comparison. + + Note that this function uses prep_case_insenstive so the limitations of + that function apply here. + + """ + + return prep_case_insensitive(val1) == prep_case_insensitive(val2) + + +def is_rdn_equal(rdn1, rdn2): + """Returns True if and only if the RDNs are equal. + + * RDNs must have the same number of AVAs. + * Each AVA of the RDNs must be the equal for the same attribute type. The + order isn't significant. Note that an attribute type will only be in one + AVA in an RDN, otherwise the DN wouldn't be valid. + * Attribute types aren't case sensitive. Note that attribute type + comparison is more complicated than implemented. This function only + compares case-insentive. The code should handle multiple names for an + attribute type (e.g., cn, commonName, and 2.5.4.3 are the same). + + Note that this function uses is_ava_value_equal to compare AVAs so the + limitations of that function apply here. + + """ + + if len(rdn1) != len(rdn2): + return False + + for attr_type_1, val1, dummy in rdn1: + found = False + for attr_type_2, val2, dummy in rdn2: + if attr_type_1.lower() != attr_type_2.lower(): + continue + + found = True + if not is_ava_value_equal(attr_type_1, val1, val2): + return False + break + if not found: + return False + + return True + + +def is_dn_equal(dn1, dn2): + """Returns True if and only if the DNs are equal. + + Two DNs are equal if they've got the same number of RDNs and if the RDNs + are the same at each position. See RFC4517. + + Note that this function uses is_rdn_equal to compare RDNs so the + limitations of that function apply here. + + :param dn1: Either a string DN or a DN parsed by ldap.dn.str2dn. + :param dn2: Either a string DN or a DN parsed by ldap.dn.str2dn. + + """ + + if not isinstance(dn1, list): + dn1 = ldap.dn.str2dn(utf8_encode(dn1)) + if not isinstance(dn2, list): + dn2 = ldap.dn.str2dn(utf8_encode(dn2)) + + if len(dn1) != len(dn2): + return False + + for rdn1, rdn2 in zip(dn1, dn2): + if not is_rdn_equal(rdn1, rdn2): + return False + return True + + +def dn_startswith(descendant_dn, dn): + """Returns True if and only if the descendant_dn is under the dn. + + :param descendant_dn: Either a string DN or a DN parsed by ldap.dn.str2dn. + :param dn: Either a string DN or a DN parsed by ldap.dn.str2dn. + + """ + + if not isinstance(descendant_dn, list): + descendant_dn = ldap.dn.str2dn(utf8_encode(descendant_dn)) + if not isinstance(dn, list): + dn = ldap.dn.str2dn(utf8_encode(dn)) + + if len(descendant_dn) <= len(dn): + return False + + # Use the last len(dn) RDNs. + return is_dn_equal(descendant_dn[-len(dn):], dn) + + +@six.add_metaclass(abc.ABCMeta) +class LDAPHandler(object): + '''Abstract class which defines methods for a LDAP API provider. + + Native Keystone values cannot be passed directly into and from the + python-ldap API. Type conversion must occur at the LDAP API + boudary, examples of type conversions are: + + * booleans map to the strings 'TRUE' and 'FALSE' + + * integer values map to their string representation. + + * unicode strings are encoded in UTF-8 + + In addition to handling type conversions at the API boundary we + have the requirement to support more than one LDAP API + provider. Currently we have: + + * python-ldap, this is the standard LDAP API for Python, it + requires access to a live LDAP server. + + * Fake LDAP which emulates python-ldap. This is used for + testing without requiring a live LDAP server. + + To support these requirements we need a layer that performs type + conversions and then calls another LDAP API which is configurable + (e.g. either python-ldap or the fake emulation). + + We have an additional constraint at the time of this writing due to + limitations in the logging module. The logging module is not + capable of accepting UTF-8 encoded strings, it will throw an + encoding exception. Therefore all logging MUST be performed prior + to UTF-8 conversion. This means no logging can be performed in the + ldap APIs that implement the python-ldap API because those APIs + are defined to accept only UTF-8 strings. Thus the layer which + performs type conversions must also do the logging. We do the type + conversions in two steps, once to convert all Python types to + unicode strings, then log, then convert the unicode strings to + UTF-8. + + There are a variety of ways one could accomplish this, we elect to + use a chaining technique whereby instances of this class simply + call the next member in the chain via the "conn" attribute. The + chain is constructed by passing in an existing instance of this + class as the conn attribute when the class is instantiated. + + Here is a brief explanation of why other possible approaches were + not used: + + subclassing + + To perform the wrapping operations in the correct order + the type convesion class would have to subclass each of + the API providers. This is awkward, doubles the number of + classes, and does not scale well. It requires the type + conversion class to be aware of all possible API + providers. + + decorators + + Decorators provide an elegant solution to wrap methods and + would be an ideal way to perform type conversions before + calling the wrapped function and then converting the + values returned from the wrapped function. However + decorators need to be aware of the method signature, it + has to know what input parameters need conversion and how + to convert the result. For an API like python-ldap which + has a large number of different method signatures it would + require a large number of specialized + decorators. Experience has shown it's very easy to apply + the wrong decorator due to the inherent complexity and + tendency to cut-n-paste code. Another option is to + parameterize the decorator to make it "smart". Experience + has shown such decorators become insanely complicated and + difficult to understand and debug. Also decorators tend to + hide what's really going on when a method is called, the + operations being performed are not visible when looking at + the implemation of a decorated method, this too experience + has shown leads to mistakes. + + Chaining simplifies both wrapping to perform type conversion as + well as the substitution of alternative API providers. One simply + creates a new instance of the API interface and insert it at the + front of the chain. Type conversions are explicit and obvious. + + If a new method needs to be added to the API interface one adds it + to the abstract class definition. Should one miss adding the new + method to any derivations of the abstract class the code will fail + to load and run making it impossible to forget updating all the + derived classes. + ''' + @abc.abstractmethod + def __init__(self, conn=None): + self.conn = conn + + @abc.abstractmethod + def connect(self, url, page_size=0, alias_dereferencing=None, + use_tls=False, tls_cacertfile=None, tls_cacertdir=None, + tls_req_cert='demand', chase_referrals=None, debug_level=None, + use_pool=None, pool_size=None, pool_retry_max=None, + pool_retry_delay=None, pool_conn_timeout=None, + pool_conn_lifetime=None): + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def set_option(self, option, invalue): + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def get_option(self, option): + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def simple_bind_s(self, who='', cred='', + serverctrls=None, clientctrls=None): + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def unbind_s(self): + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def add_s(self, dn, modlist): + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def search_s(self, base, scope, + filterstr='(objectClass=*)', attrlist=None, attrsonly=0): + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def search_ext(self, base, scope, + filterstr='(objectClass=*)', attrlist=None, attrsonly=0, + serverctrls=None, clientctrls=None, + timeout=-1, sizelimit=0): + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def result3(self, msgid=ldap.RES_ANY, all=1, timeout=None, + resp_ctrl_classes=None): + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def modify_s(self, dn, modlist): + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def delete_s(self, dn): + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def delete_ext_s(self, dn, serverctrls=None, clientctrls=None): + raise exception.NotImplemented() # pragma: no cover + + +class PythonLDAPHandler(LDAPHandler): + '''Implementation of the LDAPHandler interface which calls the + python-ldap API. + + Note, the python-ldap API requires all string values to be UTF-8 + encoded. The KeystoneLDAPHandler enforces this prior to invoking + the methods in this class. + ''' + + def __init__(self, conn=None): + super(PythonLDAPHandler, self).__init__(conn=conn) + + def connect(self, url, page_size=0, alias_dereferencing=None, + use_tls=False, tls_cacertfile=None, tls_cacertdir=None, + tls_req_cert='demand', chase_referrals=None, debug_level=None, + use_pool=None, pool_size=None, pool_retry_max=None, + pool_retry_delay=None, pool_conn_timeout=None, + pool_conn_lifetime=None): + + _common_ldap_initialization(url=url, + use_tls=use_tls, + tls_cacertfile=tls_cacertfile, + tls_cacertdir=tls_cacertdir, + tls_req_cert=tls_req_cert, + debug_level=debug_level) + + self.conn = ldap.initialize(url) + self.conn.protocol_version = ldap.VERSION3 + + if alias_dereferencing is not None: + self.conn.set_option(ldap.OPT_DEREF, alias_dereferencing) + self.page_size = page_size + + if use_tls: + self.conn.start_tls_s() + + if chase_referrals is not None: + self.conn.set_option(ldap.OPT_REFERRALS, int(chase_referrals)) + + def set_option(self, option, invalue): + return self.conn.set_option(option, invalue) + + def get_option(self, option): + return self.conn.get_option(option) + + def simple_bind_s(self, who='', cred='', + serverctrls=None, clientctrls=None): + return self.conn.simple_bind_s(who, cred, serverctrls, clientctrls) + + def unbind_s(self): + return self.conn.unbind_s() + + def add_s(self, dn, modlist): + return self.conn.add_s(dn, modlist) + + def search_s(self, base, scope, + filterstr='(objectClass=*)', attrlist=None, attrsonly=0): + return self.conn.search_s(base, scope, filterstr, + attrlist, attrsonly) + + def search_ext(self, base, scope, + filterstr='(objectClass=*)', attrlist=None, attrsonly=0, + serverctrls=None, clientctrls=None, + timeout=-1, sizelimit=0): + return self.conn.search_ext(base, scope, + filterstr, attrlist, attrsonly, + serverctrls, clientctrls, + timeout, sizelimit) + + def result3(self, msgid=ldap.RES_ANY, all=1, timeout=None, + resp_ctrl_classes=None): + # The resp_ctrl_classes parameter is a recent addition to the + # API. It defaults to None. We do not anticipate using it. + # To run with older versions of python-ldap we do not pass it. + return self.conn.result3(msgid, all, timeout) + + def modify_s(self, dn, modlist): + return self.conn.modify_s(dn, modlist) + + def delete_s(self, dn): + return self.conn.delete_s(dn) + + def delete_ext_s(self, dn, serverctrls=None, clientctrls=None): + return self.conn.delete_ext_s(dn, serverctrls, clientctrls) + + +def _common_ldap_initialization(url, use_tls=False, tls_cacertfile=None, + tls_cacertdir=None, tls_req_cert=None, + debug_level=None): + '''Method for common ldap initialization between PythonLDAPHandler and + PooledLDAPHandler. + ''' + + LOG.debug("LDAP init: url=%s", url) + LOG.debug('LDAP init: use_tls=%s tls_cacertfile=%s tls_cacertdir=%s ' + 'tls_req_cert=%s tls_avail=%s', + use_tls, tls_cacertfile, tls_cacertdir, + tls_req_cert, ldap.TLS_AVAIL) + + if debug_level is not None: + ldap.set_option(ldap.OPT_DEBUG_LEVEL, debug_level) + + using_ldaps = url.lower().startswith("ldaps") + + if use_tls and using_ldaps: + raise AssertionError(_('Invalid TLS / LDAPS combination')) + + # The certificate trust options apply for both LDAPS and TLS. + if use_tls or using_ldaps: + if not ldap.TLS_AVAIL: + raise ValueError(_('Invalid LDAP TLS_AVAIL option: %s. TLS ' + 'not available') % ldap.TLS_AVAIL) + if tls_cacertfile: + # NOTE(topol) + # python ldap TLS does not verify CACERTFILE or CACERTDIR + # so we add some extra simple sanity check verification + # Also, setting these values globally (i.e. on the ldap object) + # works but these values are ignored when setting them on the + # connection + if not os.path.isfile(tls_cacertfile): + raise IOError(_("tls_cacertfile %s not found " + "or is not a file") % + tls_cacertfile) + ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, tls_cacertfile) + elif tls_cacertdir: + # NOTE(topol) + # python ldap TLS does not verify CACERTFILE or CACERTDIR + # so we add some extra simple sanity check verification + # Also, setting these values globally (i.e. on the ldap object) + # works but these values are ignored when setting them on the + # connection + if not os.path.isdir(tls_cacertdir): + raise IOError(_("tls_cacertdir %s not found " + "or is not a directory") % + tls_cacertdir) + ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, tls_cacertdir) + if tls_req_cert in LDAP_TLS_CERTS.values(): + ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, tls_req_cert) + else: + LOG.debug("LDAP TLS: invalid TLS_REQUIRE_CERT Option=%s", + tls_req_cert) + + +class MsgId(list): + '''Wrapper class to hold connection and msgid.''' + pass + + +def use_conn_pool(func): + '''Use this only for connection pool specific ldap API. + + This adds connection object to decorated API as next argument after self. + ''' + def wrapper(self, *args, **kwargs): + # assert isinstance(self, PooledLDAPHandler) + with self._get_pool_connection() as conn: + self._apply_options(conn) + return func(self, conn, *args, **kwargs) + return wrapper + + +class PooledLDAPHandler(LDAPHandler): + '''Implementation of the LDAPHandler interface which uses pooled + connection manager. + + Pool specific configuration is defined in [ldap] section. + All other LDAP configuration is still used from [ldap] section + + Keystone LDAP authentication logic authenticates an end user using its DN + and password via LDAP bind to establish supplied password is correct. + This can fill up the pool quickly (as pool re-uses existing connection + based on its bind data) and would not leave space in pool for connection + re-use for other LDAP operations. + Now a separate pool can be established for those requests when related flag + 'use_auth_pool' is enabled. That pool can have its own size and + connection lifetime. Other pool attributes are shared between those pools. + If 'use_pool' is disabled, then 'use_auth_pool' does not matter. + If 'use_auth_pool' is not enabled, then connection pooling is not used for + those LDAP operations. + + Note, the python-ldap API requires all string values to be UTF-8 + encoded. The KeystoneLDAPHandler enforces this prior to invoking + the methods in this class. + ''' + + # Added here to allow override for testing + Connector = ldappool.StateConnector + auth_pool_prefix = 'auth_pool_' + + connection_pools = {} # static connector pool dict + + def __init__(self, conn=None, use_auth_pool=False): + super(PooledLDAPHandler, self).__init__(conn=conn) + self.who = '' + self.cred = '' + self.conn_options = {} # connection specific options + self.page_size = None + self.use_auth_pool = use_auth_pool + self.conn_pool = None + + def connect(self, url, page_size=0, alias_dereferencing=None, + use_tls=False, tls_cacertfile=None, tls_cacertdir=None, + tls_req_cert='demand', chase_referrals=None, debug_level=None, + use_pool=None, pool_size=None, pool_retry_max=None, + pool_retry_delay=None, pool_conn_timeout=None, + pool_conn_lifetime=None): + + _common_ldap_initialization(url=url, + use_tls=use_tls, + tls_cacertfile=tls_cacertfile, + tls_cacertdir=tls_cacertdir, + tls_req_cert=tls_req_cert, + debug_level=debug_level) + + self.page_size = page_size + + # Following two options are not added in common initialization as they + # need to follow a sequence in PythonLDAPHandler code. + if alias_dereferencing is not None: + self.set_option(ldap.OPT_DEREF, alias_dereferencing) + if chase_referrals is not None: + self.set_option(ldap.OPT_REFERRALS, int(chase_referrals)) + + if self.use_auth_pool: # separate pool when use_auth_pool enabled + pool_url = self.auth_pool_prefix + url + else: + pool_url = url + try: + self.conn_pool = self.connection_pools[pool_url] + except KeyError: + self.conn_pool = ldappool.ConnectionManager( + url, + size=pool_size, + retry_max=pool_retry_max, + retry_delay=pool_retry_delay, + timeout=pool_conn_timeout, + connector_cls=self.Connector, + use_tls=use_tls, + max_lifetime=pool_conn_lifetime) + self.connection_pools[pool_url] = self.conn_pool + + def set_option(self, option, invalue): + self.conn_options[option] = invalue + + def get_option(self, option): + value = self.conn_options.get(option) + # if option was not specified explicitly, then use connection default + # value for that option if there. + if value is None: + with self._get_pool_connection() as conn: + value = conn.get_option(option) + return value + + def _apply_options(self, conn): + # if connection has a lifetime, then it already has options specified + if conn.get_lifetime() > 30: + return + for option, invalue in six.iteritems(self.conn_options): + conn.set_option(option, invalue) + + def _get_pool_connection(self): + return self.conn_pool.connection(self.who, self.cred) + + def simple_bind_s(self, who='', cred='', + serverctrls=None, clientctrls=None): + '''Not using use_conn_pool decorator here as this API takes cred as + input. + ''' + self.who = who + self.cred = cred + with self._get_pool_connection() as conn: + self._apply_options(conn) + + def unbind_s(self): + # After connection generator is done `with` statement execution block + # connection is always released via finally block in ldappool. + # So this unbind is a no op. + pass + + @use_conn_pool + def add_s(self, conn, dn, modlist): + return conn.add_s(dn, modlist) + + @use_conn_pool + def search_s(self, conn, base, scope, + filterstr='(objectClass=*)', attrlist=None, attrsonly=0): + return conn.search_s(base, scope, filterstr, attrlist, + attrsonly) + + def search_ext(self, base, scope, + filterstr='(objectClass=*)', attrlist=None, attrsonly=0, + serverctrls=None, clientctrls=None, + timeout=-1, sizelimit=0): + '''This API is asynchoronus API which returns MsgId instance to be used + in result3 call. + + To work with result3 API in predicatable manner, same LDAP connection + is needed which provided msgid. So wrapping used connection and msgid + in MsgId class. The connection associated with search_ext is released + once last hard reference to MsgId object is freed. This will happen + when the method is done with returned MsgId usage. + ''' + + conn_ctxt = self._get_pool_connection() + conn = conn_ctxt.__enter__() + try: + msgid = conn.search_ext(base, scope, + filterstr, attrlist, attrsonly, + serverctrls, clientctrls, + timeout, sizelimit) + except Exception: + conn_ctxt.__exit__(*sys.exc_info()) + raise + res = MsgId((conn, msgid)) + weakref.ref(res, functools.partial(conn_ctxt.__exit__, + None, None, None)) + return res + + def result3(self, msgid, all=1, timeout=None, + resp_ctrl_classes=None): + '''This method is used to wait for and return the result of an + operation previously initiated by one of the LDAP asynchronous + operation routines (eg search_ext()) It returned an invocation + identifier (a message id) upon successful initiation of their + operation. + + Input msgid is expected to be instance of class MsgId which has LDAP + session/connection used to execute search_ext and message idenfier. + + The connection associated with search_ext is released once last hard + reference to MsgId object is freed. This will happen when function + which requested msgId and used it in result3 exits. + ''' + + conn, msg_id = msgid + return conn.result3(msg_id, all, timeout) + + @use_conn_pool + def modify_s(self, conn, dn, modlist): + return conn.modify_s(dn, modlist) + + @use_conn_pool + def delete_s(self, conn, dn): + return conn.delete_s(dn) + + @use_conn_pool + def delete_ext_s(self, conn, dn, serverctrls=None, clientctrls=None): + return conn.delete_ext_s(dn, serverctrls, clientctrls) + + +class KeystoneLDAPHandler(LDAPHandler): + '''Convert data types and perform logging. + + This LDAP inteface wraps the python-ldap based interfaces. The + python-ldap interfaces require string values encoded in UTF-8. The + OpenStack logging framework at the time of this writing is not + capable of accepting strings encoded in UTF-8, the log functions + will throw decoding errors if a non-ascii character appears in a + string. + + Prior to the call Python data types are converted to a string + representation as required by the LDAP APIs. + + Then logging is performed so we can track what is being + sent/received from LDAP. Also the logging filters security + sensitive items (i.e. passwords). + + Then the string values are encoded into UTF-8. + + Then the LDAP API entry point is invoked. + + Data returned from the LDAP call is converted back from UTF-8 + encoded strings into the Python data type used internally in + OpenStack. + ''' + + def __init__(self, conn=None): + super(KeystoneLDAPHandler, self).__init__(conn=conn) + self.page_size = 0 + + def __enter__(self): + return self + + def _disable_paging(self): + # Disable the pagination from now on + self.page_size = 0 + + def connect(self, url, page_size=0, alias_dereferencing=None, + use_tls=False, tls_cacertfile=None, tls_cacertdir=None, + tls_req_cert='demand', chase_referrals=None, debug_level=None, + use_pool=None, pool_size=None, + pool_retry_max=None, pool_retry_delay=None, + pool_conn_timeout=None, pool_conn_lifetime=None): + self.page_size = page_size + return self.conn.connect(url, page_size, alias_dereferencing, + use_tls, tls_cacertfile, tls_cacertdir, + tls_req_cert, chase_referrals, + debug_level=debug_level, + use_pool=use_pool, + pool_size=pool_size, + pool_retry_max=pool_retry_max, + pool_retry_delay=pool_retry_delay, + pool_conn_timeout=pool_conn_timeout, + pool_conn_lifetime=pool_conn_lifetime) + + def set_option(self, option, invalue): + return self.conn.set_option(option, invalue) + + def get_option(self, option): + return self.conn.get_option(option) + + def simple_bind_s(self, who='', cred='', + serverctrls=None, clientctrls=None): + LOG.debug("LDAP bind: who=%s", who) + who_utf8 = utf8_encode(who) + cred_utf8 = utf8_encode(cred) + return self.conn.simple_bind_s(who_utf8, cred_utf8, + serverctrls=serverctrls, + clientctrls=clientctrls) + + def unbind_s(self): + LOG.debug("LDAP unbind") + return self.conn.unbind_s() + + def add_s(self, dn, modlist): + ldap_attrs = [(kind, [py2ldap(x) for x in safe_iter(values)]) + for kind, values in modlist] + logging_attrs = [(kind, values + if kind != 'userPassword' + else ['****']) + for kind, values in ldap_attrs] + LOG.debug('LDAP add: dn=%s attrs=%s', + dn, logging_attrs) + dn_utf8 = utf8_encode(dn) + ldap_attrs_utf8 = [(kind, [utf8_encode(x) for x in safe_iter(values)]) + for kind, values in ldap_attrs] + return self.conn.add_s(dn_utf8, ldap_attrs_utf8) + + def search_s(self, base, scope, + filterstr='(objectClass=*)', attrlist=None, attrsonly=0): + # NOTE(morganfainberg): Remove "None" singletons from this list, which + # allows us to set mapped attributes to "None" as defaults in config. + # Without this filtering, the ldap query would raise a TypeError since + # attrlist is expected to be an iterable of strings. + if attrlist is not None: + attrlist = [attr for attr in attrlist if attr is not None] + LOG.debug('LDAP search: base=%s scope=%s filterstr=%s ' + 'attrs=%s attrsonly=%s', + base, scope, filterstr, attrlist, attrsonly) + if self.page_size: + ldap_result = self._paged_search_s(base, scope, + filterstr, attrlist) + else: + base_utf8 = utf8_encode(base) + filterstr_utf8 = utf8_encode(filterstr) + if attrlist is None: + attrlist_utf8 = None + else: + attrlist_utf8 = map(utf8_encode, attrlist) + ldap_result = self.conn.search_s(base_utf8, scope, + filterstr_utf8, + attrlist_utf8, attrsonly) + + py_result = convert_ldap_result(ldap_result) + + return py_result + + def search_ext(self, base, scope, + filterstr='(objectClass=*)', attrlist=None, attrsonly=0, + serverctrls=None, clientctrls=None, + timeout=-1, sizelimit=0): + if attrlist is not None: + attrlist = [attr for attr in attrlist if attr is not None] + LOG.debug('LDAP search_ext: base=%s scope=%s filterstr=%s ' + 'attrs=%s attrsonly=%s' + 'serverctrls=%s clientctrls=%s timeout=%s sizelimit=%s', + base, scope, filterstr, attrlist, attrsonly, + serverctrls, clientctrls, timeout, sizelimit) + return self.conn.search_ext(base, scope, + filterstr, attrlist, attrsonly, + serverctrls, clientctrls, + timeout, sizelimit) + + def _paged_search_s(self, base, scope, filterstr, attrlist=None): + res = [] + use_old_paging_api = False + # The API for the simple paged results control changed between + # python-ldap 2.3 and 2.4. We need to detect the capabilities + # of the python-ldap version we are using. + if hasattr(ldap, 'LDAP_CONTROL_PAGE_OID'): + use_old_paging_api = True + lc = ldap.controls.SimplePagedResultsControl( + controlType=ldap.LDAP_CONTROL_PAGE_OID, + criticality=True, + controlValue=(self.page_size, '')) + page_ctrl_oid = ldap.LDAP_CONTROL_PAGE_OID + else: + lc = ldap.controls.libldap.SimplePagedResultsControl( + criticality=True, + size=self.page_size, + cookie='') + page_ctrl_oid = ldap.controls.SimplePagedResultsControl.controlType + + base_utf8 = utf8_encode(base) + filterstr_utf8 = utf8_encode(filterstr) + if attrlist is None: + attrlist_utf8 = None + else: + attrlist = [attr for attr in attrlist if attr is not None] + attrlist_utf8 = map(utf8_encode, attrlist) + msgid = self.conn.search_ext(base_utf8, + scope, + filterstr_utf8, + attrlist_utf8, + serverctrls=[lc]) + # Endless loop request pages on ldap server until it has no data + while True: + # Request to the ldap server a page with 'page_size' entries + rtype, rdata, rmsgid, serverctrls = self.conn.result3(msgid) + # Receive the data + res.extend(rdata) + pctrls = [c for c in serverctrls + if c.controlType == page_ctrl_oid] + if pctrls: + # LDAP server supports pagination + if use_old_paging_api: + est, cookie = pctrls[0].controlValue + lc.controlValue = (self.page_size, cookie) + else: + cookie = lc.cookie = pctrls[0].cookie + + if cookie: + # There is more data still on the server + # so we request another page + msgid = self.conn.search_ext(base_utf8, + scope, + filterstr_utf8, + attrlist_utf8, + serverctrls=[lc]) + else: + # Exit condition no more data on server + break + else: + LOG.warning(_LW('LDAP Server does not support paging. ' + 'Disable paging in keystone.conf to ' + 'avoid this message.')) + self._disable_paging() + break + return res + + def result3(self, msgid=ldap.RES_ANY, all=1, timeout=None, + resp_ctrl_classes=None): + ldap_result = self.conn.result3(msgid, all, timeout, resp_ctrl_classes) + + LOG.debug('LDAP result3: msgid=%s all=%s timeout=%s ' + 'resp_ctrl_classes=%s ldap_result=%s', + msgid, all, timeout, resp_ctrl_classes, ldap_result) + + py_result = convert_ldap_result(ldap_result) + return py_result + + def modify_s(self, dn, modlist): + ldap_modlist = [ + (op, kind, (None if values is None + else [py2ldap(x) for x in safe_iter(values)])) + for op, kind, values in modlist] + + logging_modlist = [(op, kind, (values if kind != 'userPassword' + else ['****'])) + for op, kind, values in ldap_modlist] + LOG.debug('LDAP modify: dn=%s modlist=%s', + dn, logging_modlist) + + dn_utf8 = utf8_encode(dn) + ldap_modlist_utf8 = [ + (op, kind, (None if values is None + else [utf8_encode(x) for x in safe_iter(values)])) + for op, kind, values in ldap_modlist] + return self.conn.modify_s(dn_utf8, ldap_modlist_utf8) + + def delete_s(self, dn): + LOG.debug("LDAP delete: dn=%s", dn) + dn_utf8 = utf8_encode(dn) + return self.conn.delete_s(dn_utf8) + + def delete_ext_s(self, dn, serverctrls=None, clientctrls=None): + LOG.debug('LDAP delete_ext: dn=%s serverctrls=%s clientctrls=%s', + dn, serverctrls, clientctrls) + dn_utf8 = utf8_encode(dn) + return self.conn.delete_ext_s(dn_utf8, serverctrls, clientctrls) + + def __exit__(self, exc_type, exc_val, exc_tb): + self.unbind_s() + + +_HANDLERS = {} + + +def register_handler(prefix, handler): + _HANDLERS[prefix] = handler + + +def _get_connection(conn_url, use_pool=False, use_auth_pool=False): + for prefix, handler in six.iteritems(_HANDLERS): + if conn_url.startswith(prefix): + return handler() + + if use_pool: + return PooledLDAPHandler(use_auth_pool=use_auth_pool) + else: + return PythonLDAPHandler() + + +def filter_entity(entity_ref): + """Filter out private items in an entity dict. + + :param entity_ref: the entity dictionary. The 'dn' field will be removed. + 'dn' is used in LDAP, but should not be returned to the user. This + value may be modified. + + :returns: entity_ref + + """ + if entity_ref: + entity_ref.pop('dn', None) + return entity_ref + + +class BaseLdap(object): + DEFAULT_SUFFIX = "dc=example,dc=com" + DEFAULT_OU = None + DEFAULT_STRUCTURAL_CLASSES = None + DEFAULT_ID_ATTR = 'cn' + DEFAULT_OBJECTCLASS = None + DEFAULT_FILTER = None + DEFAULT_EXTRA_ATTR_MAPPING = [] + DUMB_MEMBER_DN = 'cn=dumb,dc=nonexistent' + NotFound = None + notfound_arg = None + options_name = None + model = None + attribute_options_names = {} + immutable_attrs = [] + attribute_ignore = [] + tree_dn = None + + def __init__(self, conf): + self.LDAP_URL = conf.ldap.url + self.LDAP_USER = conf.ldap.user + self.LDAP_PASSWORD = conf.ldap.password + self.LDAP_SCOPE = ldap_scope(conf.ldap.query_scope) + self.alias_dereferencing = parse_deref(conf.ldap.alias_dereferencing) + self.page_size = conf.ldap.page_size + self.use_tls = conf.ldap.use_tls + self.tls_cacertfile = conf.ldap.tls_cacertfile + self.tls_cacertdir = conf.ldap.tls_cacertdir + self.tls_req_cert = parse_tls_cert(conf.ldap.tls_req_cert) + self.attribute_mapping = {} + self.chase_referrals = conf.ldap.chase_referrals + self.debug_level = conf.ldap.debug_level + + # LDAP Pool specific attribute + self.use_pool = conf.ldap.use_pool + self.pool_size = conf.ldap.pool_size + self.pool_retry_max = conf.ldap.pool_retry_max + self.pool_retry_delay = conf.ldap.pool_retry_delay + self.pool_conn_timeout = conf.ldap.pool_connection_timeout + self.pool_conn_lifetime = conf.ldap.pool_connection_lifetime + + # End user authentication pool specific config attributes + self.use_auth_pool = self.use_pool and conf.ldap.use_auth_pool + self.auth_pool_size = conf.ldap.auth_pool_size + self.auth_pool_conn_lifetime = conf.ldap.auth_pool_connection_lifetime + + if self.options_name is not None: + self.suffix = conf.ldap.suffix + if self.suffix is None: + self.suffix = self.DEFAULT_SUFFIX + dn = '%s_tree_dn' % self.options_name + self.tree_dn = (getattr(conf.ldap, dn) + or '%s,%s' % (self.DEFAULT_OU, self.suffix)) + + idatt = '%s_id_attribute' % self.options_name + self.id_attr = getattr(conf.ldap, idatt) or self.DEFAULT_ID_ATTR + + objclass = '%s_objectclass' % self.options_name + self.object_class = (getattr(conf.ldap, objclass) + or self.DEFAULT_OBJECTCLASS) + + for k, v in six.iteritems(self.attribute_options_names): + v = '%s_%s_attribute' % (self.options_name, v) + self.attribute_mapping[k] = getattr(conf.ldap, v) + + attr_mapping_opt = ('%s_additional_attribute_mapping' % + self.options_name) + attr_mapping = (getattr(conf.ldap, attr_mapping_opt) + or self.DEFAULT_EXTRA_ATTR_MAPPING) + self.extra_attr_mapping = self._parse_extra_attrs(attr_mapping) + + ldap_filter = '%s_filter' % self.options_name + self.ldap_filter = getattr(conf.ldap, + ldap_filter) or self.DEFAULT_FILTER + + allow_create = '%s_allow_create' % self.options_name + self.allow_create = getattr(conf.ldap, allow_create) + + allow_update = '%s_allow_update' % self.options_name + self.allow_update = getattr(conf.ldap, allow_update) + + allow_delete = '%s_allow_delete' % self.options_name + self.allow_delete = getattr(conf.ldap, allow_delete) + + member_attribute = '%s_member_attribute' % self.options_name + self.member_attribute = getattr(conf.ldap, member_attribute, None) + + self.structural_classes = self.DEFAULT_STRUCTURAL_CLASSES + + if self.notfound_arg is None: + self.notfound_arg = self.options_name + '_id' + + attribute_ignore = '%s_attribute_ignore' % self.options_name + self.attribute_ignore = getattr(conf.ldap, attribute_ignore) + + self.use_dumb_member = conf.ldap.use_dumb_member + self.dumb_member = (conf.ldap.dumb_member or + self.DUMB_MEMBER_DN) + + self.subtree_delete_enabled = conf.ldap.allow_subtree_delete + + def _not_found(self, object_id): + if self.NotFound is None: + return exception.NotFound(target=object_id) + else: + return self.NotFound(**{self.notfound_arg: object_id}) + + def _parse_extra_attrs(self, option_list): + mapping = {} + for item in option_list: + try: + ldap_attr, attr_map = item.split(':') + except Exception: + LOG.warn(_LW( + 'Invalid additional attribute mapping: "%s". ' + 'Format must be :'), + item) + continue + mapping[ldap_attr] = attr_map + return mapping + + def _is_dumb_member(self, member_dn): + """Checks that member is a dumb member. + + :param member_dn: DN of member to be checked. + """ + return (self.use_dumb_member + and is_dn_equal(member_dn, self.dumb_member)) + + def get_connection(self, user=None, password=None, end_user_auth=False): + use_pool = self.use_pool + pool_size = self.pool_size + pool_conn_lifetime = self.pool_conn_lifetime + + if end_user_auth: + if not self.use_auth_pool: + use_pool = False + else: + pool_size = self.auth_pool_size + pool_conn_lifetime = self.auth_pool_conn_lifetime + + conn = _get_connection(self.LDAP_URL, use_pool, + use_auth_pool=end_user_auth) + + conn = KeystoneLDAPHandler(conn=conn) + + conn.connect(self.LDAP_URL, + page_size=self.page_size, + alias_dereferencing=self.alias_dereferencing, + use_tls=self.use_tls, + tls_cacertfile=self.tls_cacertfile, + tls_cacertdir=self.tls_cacertdir, + tls_req_cert=self.tls_req_cert, + chase_referrals=self.chase_referrals, + debug_level=self.debug_level, + use_pool=use_pool, + pool_size=pool_size, + pool_retry_max=self.pool_retry_max, + pool_retry_delay=self.pool_retry_delay, + pool_conn_timeout=self.pool_conn_timeout, + pool_conn_lifetime=pool_conn_lifetime + ) + + if user is None: + user = self.LDAP_USER + + if password is None: + password = self.LDAP_PASSWORD + + # not all LDAP servers require authentication, so we don't bind + # if we don't have any user/pass + if user and password: + conn.simple_bind_s(user, password) + + return conn + + def _id_to_dn_string(self, object_id): + return u'%s=%s,%s' % (self.id_attr, + ldap.dn.escape_dn_chars( + six.text_type(object_id)), + self.tree_dn) + + def _id_to_dn(self, object_id): + if self.LDAP_SCOPE == ldap.SCOPE_ONELEVEL: + return self._id_to_dn_string(object_id) + with self.get_connection() as conn: + search_result = conn.search_s( + self.tree_dn, self.LDAP_SCOPE, + u'(&(%(id_attr)s=%(id)s)(objectclass=%(objclass)s))' % + {'id_attr': self.id_attr, + 'id': ldap.filter.escape_filter_chars( + six.text_type(object_id)), + 'objclass': self.object_class}, + attrlist=DN_ONLY) + if search_result: + dn, attrs = search_result[0] + return dn + else: + return self._id_to_dn_string(object_id) + + @staticmethod + def _dn_to_id(dn): + return utf8_decode(ldap.dn.str2dn(utf8_encode(dn))[0][0][1]) + + def _ldap_res_to_model(self, res): + # LDAP attribute names may be returned in a different case than + # they are defined in the mapping, so we need to check for keys + # in a case-insensitive way. We use the case specified in the + # mapping for the model to ensure we have a predictable way of + # retrieving values later. + lower_res = {k.lower(): v for k, v in six.iteritems(res[1])} + + id_attrs = lower_res.get(self.id_attr.lower()) + if not id_attrs: + message = _('ID attribute %(id_attr)s not found in LDAP ' + 'object %(dn)s') % ({'id_attr': self.id_attr, + 'dn': res[0]}) + raise exception.NotFound(message=message) + if len(id_attrs) > 1: + # FIXME(gyee): if this is a multi-value attribute and it has + # multiple values, we can't use it as ID. Retain the dn_to_id + # logic here so it does not potentially break existing + # deployments. We need to fix our read-write LDAP logic so + # it does not get the ID from DN. + message = _LW('ID attribute %(id_attr)s for LDAP object %(dn)s ' + 'has multiple values and therefore cannot be used ' + 'as an ID. Will get the ID from DN instead') % ( + {'id_attr': self.id_attr, + 'dn': res[0]}) + LOG.warn(message) + id_val = self._dn_to_id(res[0]) + else: + id_val = id_attrs[0] + obj = self.model(id=id_val) + + for k in obj.known_keys: + if k in self.attribute_ignore: + continue + + try: + map_attr = self.attribute_mapping.get(k, k) + if map_attr is None: + # Ignore attributes that are mapped to None. + continue + + v = lower_res[map_attr.lower()] + except KeyError: + pass + else: + try: + obj[k] = v[0] + except IndexError: + obj[k] = None + + return obj + + def check_allow_create(self): + if not self.allow_create: + action = _('LDAP %s create') % self.options_name + raise exception.ForbiddenAction(action=action) + + def check_allow_update(self): + if not self.allow_update: + action = _('LDAP %s update') % self.options_name + raise exception.ForbiddenAction(action=action) + + def check_allow_delete(self): + if not self.allow_delete: + action = _('LDAP %s delete') % self.options_name + raise exception.ForbiddenAction(action=action) + + def affirm_unique(self, values): + if values.get('name') is not None: + try: + self.get_by_name(values['name']) + except exception.NotFound: + pass + else: + raise exception.Conflict(type=self.options_name, + details=_('Duplicate name, %s.') % + values['name']) + + if values.get('id') is not None: + try: + self.get(values['id']) + except exception.NotFound: + pass + else: + raise exception.Conflict(type=self.options_name, + details=_('Duplicate ID, %s.') % + values['id']) + + def create(self, values): + self.affirm_unique(values) + object_classes = self.structural_classes + [self.object_class] + attrs = [('objectClass', object_classes)] + for k, v in six.iteritems(values): + if k in self.attribute_ignore: + continue + if k == 'id': + # no need to check if v is None as 'id' will always have + # a value + attrs.append((self.id_attr, [v])) + elif v is not None: + attr_type = self.attribute_mapping.get(k, k) + if attr_type is not None: + attrs.append((attr_type, [v])) + extra_attrs = [attr for attr, name + in six.iteritems(self.extra_attr_mapping) + if name == k] + for attr in extra_attrs: + attrs.append((attr, [v])) + + if 'groupOfNames' in object_classes and self.use_dumb_member: + attrs.append(('member', [self.dumb_member])) + with self.get_connection() as conn: + conn.add_s(self._id_to_dn(values['id']), attrs) + return values + + def _ldap_get(self, object_id, ldap_filter=None): + query = (u'(&(%(id_attr)s=%(id)s)' + u'%(filter)s' + u'(objectClass=%(object_class)s))' + % {'id_attr': self.id_attr, + 'id': ldap.filter.escape_filter_chars( + six.text_type(object_id)), + 'filter': (ldap_filter or self.ldap_filter or ''), + 'object_class': self.object_class}) + with self.get_connection() as conn: + try: + attrs = list(set(([self.id_attr] + + self.attribute_mapping.values() + + self.extra_attr_mapping.keys()))) + res = conn.search_s(self.tree_dn, + self.LDAP_SCOPE, + query, + attrs) + except ldap.NO_SUCH_OBJECT: + return None + try: + return res[0] + except IndexError: + return None + + def _ldap_get_all(self, ldap_filter=None): + query = u'(&%s(objectClass=%s))' % (ldap_filter or + self.ldap_filter or + '', self.object_class) + with self.get_connection() as conn: + try: + attrs = list(set(([self.id_attr] + + self.attribute_mapping.values() + + self.extra_attr_mapping.keys()))) + return conn.search_s(self.tree_dn, + self.LDAP_SCOPE, + query, + attrs) + except ldap.NO_SUCH_OBJECT: + return [] + + def _ldap_get_list(self, search_base, scope, query_params=None, + attrlist=None): + query = u'(objectClass=%s)' % self.object_class + if query_params: + + def calc_filter(attrname, value): + val_esc = ldap.filter.escape_filter_chars(value) + return '(%s=%s)' % (attrname, val_esc) + + query = (u'(&%s%s)' % + (query, ''.join([calc_filter(k, v) for k, v in + six.iteritems(query_params)]))) + with self.get_connection() as conn: + return conn.search_s(search_base, scope, query, attrlist) + + def get(self, object_id, ldap_filter=None): + res = self._ldap_get(object_id, ldap_filter) + if res is None: + raise self._not_found(object_id) + else: + return self._ldap_res_to_model(res) + + def get_by_name(self, name, ldap_filter=None): + query = (u'(%s=%s)' % (self.attribute_mapping['name'], + ldap.filter.escape_filter_chars( + six.text_type(name)))) + res = self.get_all(query) + try: + return res[0] + except IndexError: + raise self._not_found(name) + + def get_all(self, ldap_filter=None): + return [self._ldap_res_to_model(x) + for x in self._ldap_get_all(ldap_filter)] + + def update(self, object_id, values, old_obj=None): + if old_obj is None: + old_obj = self.get(object_id) + + modlist = [] + for k, v in six.iteritems(values): + if k == 'id': + # id can't be modified. + continue + + if k in self.attribute_ignore: + + # Handle 'enabled' specially since can't disable if ignored. + if k == 'enabled' and (not v): + action = _("Disabling an entity where the 'enable' " + "attribute is ignored by configuration.") + raise exception.ForbiddenAction(action=action) + + continue + + # attribute value has not changed + if k in old_obj and old_obj[k] == v: + continue + + if k in self.immutable_attrs: + msg = (_("Cannot change %(option_name)s %(attr)s") % + {'option_name': self.options_name, 'attr': k}) + raise exception.ValidationError(msg) + + if v is None: + if old_obj.get(k) is not None: + modlist.append((ldap.MOD_DELETE, + self.attribute_mapping.get(k, k), + None)) + continue + + current_value = old_obj.get(k) + if current_value is None: + op = ldap.MOD_ADD + modlist.append((op, self.attribute_mapping.get(k, k), [v])) + elif current_value != v: + op = ldap.MOD_REPLACE + modlist.append((op, self.attribute_mapping.get(k, k), [v])) + + if modlist: + with self.get_connection() as conn: + try: + conn.modify_s(self._id_to_dn(object_id), modlist) + except ldap.NO_SUCH_OBJECT: + raise self._not_found(object_id) + + return self.get(object_id) + + def delete(self, object_id): + with self.get_connection() as conn: + try: + conn.delete_s(self._id_to_dn(object_id)) + except ldap.NO_SUCH_OBJECT: + raise self._not_found(object_id) + + def deleteTree(self, object_id): + tree_delete_control = ldap.controls.LDAPControl(CONTROL_TREEDELETE, + 0, + None) + with self.get_connection() as conn: + try: + conn.delete_ext_s(self._id_to_dn(object_id), + serverctrls=[tree_delete_control]) + except ldap.NO_SUCH_OBJECT: + raise self._not_found(object_id) + except ldap.NOT_ALLOWED_ON_NONLEAF: + # Most LDAP servers do not support the tree_delete_control. + # In these servers, the usual idiom is to first perform a + # search to get the entries to delete, then delete them in + # in order of child to parent, since LDAP forbids the + # deletion of a parent entry before deleting the children + # of that parent. The simplest way to do that is to delete + # the entries in order of the length of the DN, from longest + # to shortest DN. + dn = self._id_to_dn(object_id) + scope = ldap.SCOPE_SUBTREE + # With some directory servers, an entry with objectclass + # ldapsubentry will not be returned unless it is explicitly + # requested, by specifying the objectclass in the search + # filter. We must specify this, with objectclass=*, in an + # LDAP filter OR clause, in order to return all entries + filt = '(|(objectclass=*)(objectclass=ldapsubentry))' + # We only need the DNs of the entries. Since no attributes + # will be returned, we do not have to specify attrsonly=1. + entries = conn.search_s(dn, scope, filt, attrlist=DN_ONLY) + if entries: + for dn in sorted((e[0] for e in entries), + key=len, reverse=True): + conn.delete_s(dn) + else: + LOG.debug('No entries in LDAP subtree %s', dn) + + def add_member(self, member_dn, member_list_dn): + """Add member to the member list. + + :param member_dn: DN of member to be added. + :param member_list_dn: DN of group to which the + member will be added. + + :raises: exception.Conflict: If the user was already a member. + self.NotFound: If the group entry didn't exist. + """ + with self.get_connection() as conn: + try: + mod = (ldap.MOD_ADD, self.member_attribute, member_dn) + conn.modify_s(member_list_dn, [mod]) + except ldap.TYPE_OR_VALUE_EXISTS: + raise exception.Conflict(_('Member %(member)s ' + 'is already a member' + ' of group %(group)s') % { + 'member': member_dn, + 'group': member_list_dn}) + except ldap.NO_SUCH_OBJECT: + raise self._not_found(member_list_dn) + + def remove_member(self, member_dn, member_list_dn): + """Remove member from the member list. + + :param member_dn: DN of member to be removed. + :param member_list_dn: DN of group from which the + member will be removed. + + :raises: self.NotFound: If the group entry didn't exist. + ldap.NO_SUCH_ATTRIBUTE: If the user wasn't a member. + """ + with self.get_connection() as conn: + try: + mod = (ldap.MOD_DELETE, self.member_attribute, member_dn) + conn.modify_s(member_list_dn, [mod]) + except ldap.NO_SUCH_OBJECT: + raise self._not_found(member_list_dn) + + def _delete_tree_nodes(self, search_base, scope, query_params=None): + query = u'(objectClass=%s)' % self.object_class + if query_params: + query = (u'(&%s%s)' % + (query, ''.join(['(%s=%s)' + % (k, ldap.filter.escape_filter_chars(v)) + for k, v in + six.iteritems(query_params)]))) + not_deleted_nodes = [] + with self.get_connection() as conn: + try: + nodes = conn.search_s(search_base, scope, query, + attrlist=DN_ONLY) + except ldap.NO_SUCH_OBJECT: + LOG.debug('Could not find entry with dn=%s', search_base) + raise self._not_found(self._dn_to_id(search_base)) + else: + for node_dn, _t in nodes: + try: + conn.delete_s(node_dn) + except ldap.NO_SUCH_OBJECT: + not_deleted_nodes.append(node_dn) + + if not_deleted_nodes: + LOG.warn(_LW("When deleting entries for %(search_base)s, could not" + " delete nonexistent entries %(entries)s%(dots)s"), + {'search_base': search_base, + 'entries': not_deleted_nodes[:3], + 'dots': '...' if len(not_deleted_nodes) > 3 else ''}) + + def filter_query(self, hints, query=None): + """Applies filtering to a query. + + :param hints: contains the list of filters, which may be None, + indicating that there are no filters to be applied. + If it's not None, then any filters satisfied here will be + removed so that the caller will know if any filters + remain to be applied. + :param query: LDAP query into which to include filters + + :returns query: LDAP query, updated with any filters satisfied + + """ + def build_filter(filter_, hints): + """Build a filter for the query. + + :param filter_: the dict that describes this filter + :param hints: contains the list of filters yet to be satisfied. + + :returns query: LDAP query term to be added + + """ + ldap_attr = self.attribute_mapping[filter_['name']] + val_esc = ldap.filter.escape_filter_chars(filter_['value']) + + if filter_['case_sensitive']: + # NOTE(henry-nash): Although dependent on the schema being + # used, most LDAP attributes are configured with case + # insensitive matching rules, so we'll leave this to the + # controller to filter. + return + + if filter_['name'] == 'enabled': + # NOTE(henry-nash): Due to the different options for storing + # the enabled attribute (e,g, emulated or not), for now we + # don't try and filter this at the driver level - we simply + # leave the filter to be handled by the controller. It seems + # unlikley that this will cause a signifcant performance + # issue. + return + + # TODO(henry-nash): Currently there are no booleans (other than + # 'enabled' that is handled above) on which you can filter. If + # there were, we would need to add special handling here to + # convert the booleans values to 'TRUE' and 'FALSE'. To do that + # we would also need to know which filter keys were actually + # booleans (this is related to bug #1411478). + + if filter_['comparator'] == 'equals': + query_term = (u'(%(attr)s=%(val)s)' + % {'attr': ldap_attr, 'val': val_esc}) + elif filter_['comparator'] == 'contains': + query_term = (u'(%(attr)s=*%(val)s*)' + % {'attr': ldap_attr, 'val': val_esc}) + elif filter_['comparator'] == 'startswith': + query_term = (u'(%(attr)s=%(val)s*)' + % {'attr': ldap_attr, 'val': val_esc}) + elif filter_['comparator'] == 'endswith': + query_term = (u'(%(attr)s=*%(val)s)' + % {'attr': ldap_attr, 'val': val_esc}) + else: + # It's a filter we don't understand, so let the caller + # work out if they need to do something with it. + return + + return query_term + + if hints is None: + return query + + filter_list = [] + satisfied_filters = [] + + for filter_ in hints.filters: + if filter_['name'] not in self.attribute_mapping: + continue + new_filter = build_filter(filter_, hints) + if new_filter is not None: + filter_list.append(new_filter) + satisfied_filters.append(filter_) + + if filter_list: + query = u'(&%s%s)' % (query, ''.join(filter_list)) + + # Remove satisfied filters, then the caller will know remaining filters + for filter_ in satisfied_filters: + hints.filters.remove(filter_) + + return query + + +class EnabledEmuMixIn(BaseLdap): + """Emulates boolean 'enabled' attribute if turned on. + + Creates groupOfNames holding all enabled objects of this class, all missing + objects are considered disabled. + + Options: + + * $name_enabled_emulation - boolean, on/off + * $name_enabled_emulation_dn - DN of that groupOfNames, default is + cn=enabled_${name}s,${tree_dn} + + Where ${name}s is the plural of self.options_name ('users' or 'tenants'), + ${tree_dn} is self.tree_dn. + """ + + def __init__(self, conf): + super(EnabledEmuMixIn, self).__init__(conf) + enabled_emulation = '%s_enabled_emulation' % self.options_name + self.enabled_emulation = getattr(conf.ldap, enabled_emulation) + + enabled_emulation_dn = '%s_enabled_emulation_dn' % self.options_name + self.enabled_emulation_dn = getattr(conf.ldap, enabled_emulation_dn) + if not self.enabled_emulation_dn: + naming_attr_name = 'cn' + naming_attr_value = 'enabled_%ss' % self.options_name + sub_vals = (naming_attr_name, naming_attr_value, self.tree_dn) + self.enabled_emulation_dn = '%s=%s,%s' % sub_vals + naming_attr = (naming_attr_name, [naming_attr_value]) + else: + # Extract the attribute name and value from the configured DN. + naming_dn = ldap.dn.str2dn(utf8_encode(self.enabled_emulation_dn)) + naming_rdn = naming_dn[0][0] + naming_attr = (utf8_decode(naming_rdn[0]), + utf8_decode(naming_rdn[1])) + self.enabled_emulation_naming_attr = naming_attr + + def _get_enabled(self, object_id): + dn = self._id_to_dn(object_id) + query = '(member=%s)' % dn + with self.get_connection() as conn: + try: + enabled_value = conn.search_s(self.enabled_emulation_dn, + ldap.SCOPE_BASE, + query, ['cn']) + except ldap.NO_SUCH_OBJECT: + return False + else: + return bool(enabled_value) + + def _add_enabled(self, object_id): + if not self._get_enabled(object_id): + modlist = [(ldap.MOD_ADD, + 'member', + [self._id_to_dn(object_id)])] + with self.get_connection() as conn: + try: + conn.modify_s(self.enabled_emulation_dn, modlist) + except ldap.NO_SUCH_OBJECT: + attr_list = [('objectClass', ['groupOfNames']), + ('member', [self._id_to_dn(object_id)]), + self.enabled_emulation_naming_attr] + if self.use_dumb_member: + attr_list[1][1].append(self.dumb_member) + conn.add_s(self.enabled_emulation_dn, attr_list) + + def _remove_enabled(self, object_id): + modlist = [(ldap.MOD_DELETE, + 'member', + [self._id_to_dn(object_id)])] + with self.get_connection() as conn: + try: + conn.modify_s(self.enabled_emulation_dn, modlist) + except (ldap.NO_SUCH_OBJECT, ldap.NO_SUCH_ATTRIBUTE): + pass + + def create(self, values): + if self.enabled_emulation: + enabled_value = values.pop('enabled', True) + ref = super(EnabledEmuMixIn, self).create(values) + if 'enabled' not in self.attribute_ignore: + if enabled_value: + self._add_enabled(ref['id']) + ref['enabled'] = enabled_value + return ref + else: + return super(EnabledEmuMixIn, self).create(values) + + def get(self, object_id, ldap_filter=None): + ref = super(EnabledEmuMixIn, self).get(object_id, ldap_filter) + if 'enabled' not in self.attribute_ignore and self.enabled_emulation: + ref['enabled'] = self._get_enabled(object_id) + return ref + + def get_all(self, ldap_filter=None): + if 'enabled' not in self.attribute_ignore and self.enabled_emulation: + # had to copy BaseLdap.get_all here to ldap_filter by DN + tenant_list = [self._ldap_res_to_model(x) + for x in self._ldap_get_all(ldap_filter) + if x[0] != self.enabled_emulation_dn] + for tenant_ref in tenant_list: + tenant_ref['enabled'] = self._get_enabled(tenant_ref['id']) + return tenant_list + else: + return super(EnabledEmuMixIn, self).get_all(ldap_filter) + + def update(self, object_id, values, old_obj=None): + if 'enabled' not in self.attribute_ignore and self.enabled_emulation: + data = values.copy() + enabled_value = data.pop('enabled', None) + ref = super(EnabledEmuMixIn, self).update(object_id, data, old_obj) + if enabled_value is not None: + if enabled_value: + self._add_enabled(object_id) + else: + self._remove_enabled(object_id) + ref['enabled'] = enabled_value + return ref + else: + return super(EnabledEmuMixIn, self).update( + object_id, values, old_obj) + + def delete(self, object_id): + if self.enabled_emulation: + self._remove_enabled(object_id) + super(EnabledEmuMixIn, self).delete(object_id) + + +class ProjectLdapStructureMixin(object): + """Project LDAP Structure shared between LDAP backends. + + This is shared between the resource and assignment LDAP backends. + + """ + DEFAULT_OU = 'ou=Groups' + DEFAULT_STRUCTURAL_CLASSES = [] + DEFAULT_OBJECTCLASS = 'groupOfNames' + DEFAULT_ID_ATTR = 'cn' + NotFound = exception.ProjectNotFound + notfound_arg = 'project_id' # NOTE(yorik-sar): while options_name = tenant + options_name = 'project' + attribute_options_names = {'name': 'name', + 'description': 'desc', + 'enabled': 'enabled', + 'domain_id': 'domain_id'} + immutable_attrs = ['name'] diff --git a/keystone-moon/keystone/common/manager.py b/keystone-moon/keystone/common/manager.py new file mode 100644 index 00000000..28bf2efb --- /dev/null +++ b/keystone-moon/keystone/common/manager.py @@ -0,0 +1,76 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 functools + +from oslo_utils import importutils + + +def response_truncated(f): + """Truncate the list returned by the wrapped function. + + This is designed to wrap Manager list_{entity} methods to ensure that + any list limits that are defined are passed to the driver layer. If a + hints list is provided, the wrapper will insert the relevant limit into + the hints so that the underlying driver call can try and honor it. If the + driver does truncate the response, it will update the 'truncated' attribute + in the 'limit' entry in the hints list, which enables the caller of this + function to know if truncation has taken place. If, however, the driver + layer is unable to perform truncation, the 'limit' entry is simply left in + the hints list for the caller to handle. + + A _get_list_limit() method is required to be present in the object class + hierarchy, which returns the limit for this backend to which we will + truncate. + + If a hints list is not provided in the arguments of the wrapped call then + any limits set in the config file are ignored. This allows internal use + of such wrapped methods where the entire data set is needed as input for + the calculations of some other API (e.g. get role assignments for a given + project). + + """ + @functools.wraps(f) + def wrapper(self, *args, **kwargs): + if kwargs.get('hints') is None: + return f(self, *args, **kwargs) + + list_limit = self.driver._get_list_limit() + if list_limit: + kwargs['hints'].set_limit(list_limit) + return f(self, *args, **kwargs) + return wrapper + + +class Manager(object): + """Base class for intermediary request layer. + + The Manager layer exists to support additional logic that applies to all + or some of the methods exposed by a service that are not specific to the + HTTP interface. + + It also provides a stable entry point to dynamic backends. + + An example of a probable use case is logging all the calls. + + """ + + def __init__(self, driver_name): + self.driver = importutils.import_object(driver_name) + + def __getattr__(self, name): + """Forward calls to the underlying driver.""" + f = getattr(self.driver, name) + setattr(self, name, f) + return f diff --git a/keystone-moon/keystone/common/models.py b/keystone-moon/keystone/common/models.py new file mode 100644 index 00000000..3b3aabe1 --- /dev/null +++ b/keystone-moon/keystone/common/models.py @@ -0,0 +1,182 @@ +# Copyright (C) 2011 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Base model for keystone internal services + +Unless marked otherwise, all fields are strings. + +""" + + +class Model(dict): + """Base model class.""" + def __hash__(self): + return self['id'].__hash__() + + @property + def known_keys(cls): + return cls.required_keys + cls.optional_keys + + +class Token(Model): + """Token object. + + Required keys: + id + expires (datetime) + + Optional keys: + user + tenant + metadata + trust_id + """ + + required_keys = ('id', 'expires') + optional_keys = ('extra',) + + +class Service(Model): + """Service object. + + Required keys: + id + type + name + + Optional keys: + """ + + required_keys = ('id', 'type', 'name') + optional_keys = tuple() + + +class Endpoint(Model): + """Endpoint object + + Required keys: + id + region + service_id + + Optional keys: + internalurl + publicurl + adminurl + """ + + required_keys = ('id', 'region', 'service_id') + optional_keys = ('internalurl', 'publicurl', 'adminurl') + + +class User(Model): + """User object. + + Required keys: + id + name + domain_id + + Optional keys: + password + description + email + enabled (bool, default True) + default_project_id + """ + + required_keys = ('id', 'name', 'domain_id') + optional_keys = ('password', 'description', 'email', 'enabled', + 'default_project_id') + + +class Group(Model): + """Group object. + + Required keys: + id + name + domain_id + + Optional keys: + + description + + """ + + required_keys = ('id', 'name', 'domain_id') + optional_keys = ('description',) + + +class Project(Model): + """Project object. + + Required keys: + id + name + domain_id + + Optional Keys: + description + enabled (bool, default True) + + """ + + required_keys = ('id', 'name', 'domain_id') + optional_keys = ('description', 'enabled') + + +class Role(Model): + """Role object. + + Required keys: + id + name + + """ + + required_keys = ('id', 'name') + optional_keys = tuple() + + +class Trust(Model): + """Trust object. + + Required keys: + id + trustor_user_id + trustee_user_id + project_id + """ + + required_keys = ('id', 'trustor_user_id', 'trustee_user_id', 'project_id') + optional_keys = ('expires_at',) + + +class Domain(Model): + """Domain object. + + Required keys: + id + name + + Optional keys: + + description + enabled (bool, default True) + + """ + + required_keys = ('id', 'name') + optional_keys = ('description', 'enabled') diff --git a/keystone-moon/keystone/common/openssl.py b/keystone-moon/keystone/common/openssl.py new file mode 100644 index 00000000..4eb7d1d1 --- /dev/null +++ b/keystone-moon/keystone/common/openssl.py @@ -0,0 +1,347 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 os + +from oslo_config import cfg +from oslo_log import log + +from keystone.common import environment +from keystone.common import utils +from keystone.i18n import _LI, _LE + +LOG = log.getLogger(__name__) +CONF = cfg.CONF + +PUBLIC_DIR_PERMS = 0o755 # -rwxr-xr-x +PRIVATE_DIR_PERMS = 0o750 # -rwxr-x--- +PUBLIC_FILE_PERMS = 0o644 # -rw-r--r-- +PRIVATE_FILE_PERMS = 0o640 # -rw-r----- + + +def file_exists(file_path): + return os.path.exists(file_path) + + +class BaseCertificateConfigure(object): + """Create a certificate signing environment. + + This is based on a config section and reasonable OpenSSL defaults. + + """ + + def __init__(self, conf_obj, server_conf_obj, keystone_user, + keystone_group, rebuild, **kwargs): + self.conf_dir = os.path.dirname(server_conf_obj.ca_certs) + self.use_keystone_user = keystone_user + self.use_keystone_group = keystone_group + self.rebuild = rebuild + self.ssl_config_file_name = os.path.join(self.conf_dir, "openssl.conf") + self.request_file_name = os.path.join(self.conf_dir, "req.pem") + self.ssl_dictionary = {'conf_dir': self.conf_dir, + 'ca_cert': server_conf_obj.ca_certs, + 'default_md': 'default', + 'ssl_config': self.ssl_config_file_name, + 'ca_private_key': conf_obj.ca_key, + 'request_file': self.request_file_name, + 'signing_key': server_conf_obj.keyfile, + 'signing_cert': server_conf_obj.certfile, + 'key_size': int(conf_obj.key_size), + 'valid_days': int(conf_obj.valid_days), + 'cert_subject': conf_obj.cert_subject} + + try: + # OpenSSL 1.0 and newer support default_md = default, olders do not + openssl_ver = environment.subprocess.Popen( + ['openssl', 'version'], + stdout=environment.subprocess.PIPE).stdout.read() + if "OpenSSL 0." in openssl_ver: + self.ssl_dictionary['default_md'] = 'sha1' + except OSError: + LOG.warn('Failed to invoke ``openssl version``, ' + 'assuming is v1.0 or newer') + self.ssl_dictionary.update(kwargs) + + def exec_command(self, command): + to_exec = [] + for cmd_part in command: + to_exec.append(cmd_part % self.ssl_dictionary) + LOG.info(_LI('Running command - %s'), ' '.join(to_exec)) + # NOTE(Jeffrey4l): Redirect both stdout and stderr to pipe, so the + # output can be captured. + # NOTE(Jeffrey4l): check_output is not compatible with Python 2.6. + # So use Popen instead. + process = environment.subprocess.Popen( + to_exec, + stdout=environment.subprocess.PIPE, + stderr=environment.subprocess.STDOUT) + output = process.communicate()[0] + retcode = process.poll() + if retcode: + LOG.error(_LE('Command %(to_exec)s exited with %(retcode)s' + '- %(output)s'), + {'to_exec': to_exec, + 'retcode': retcode, + 'output': output}) + e = environment.subprocess.CalledProcessError(retcode, to_exec[0]) + # NOTE(Jeffrey4l): Python 2.6 compatibility: + # CalledProcessError did not have output keyword argument + e.output = output + raise e + + def clean_up_existing_files(self): + files_to_clean = [self.ssl_dictionary['ca_private_key'], + self.ssl_dictionary['ca_cert'], + self.ssl_dictionary['signing_key'], + self.ssl_dictionary['signing_cert'], + ] + + existing_files = [] + + for file_path in files_to_clean: + if file_exists(file_path): + if self.rebuild: + # The file exists but the user wants to rebuild it, so blow + # it away + try: + os.remove(file_path) + except OSError as exc: + LOG.error(_LE('Failed to remove file %(file_path)r: ' + '%(error)s'), + {'file_path': file_path, + 'error': exc.strerror}) + raise + else: + existing_files.append(file_path) + + return existing_files + + def build_ssl_config_file(self): + utils.make_dirs(os.path.dirname(self.ssl_config_file_name), + mode=PUBLIC_DIR_PERMS, + user=self.use_keystone_user, + group=self.use_keystone_group, log=LOG) + if not file_exists(self.ssl_config_file_name): + ssl_config_file = open(self.ssl_config_file_name, 'w') + ssl_config_file.write(self.sslconfig % self.ssl_dictionary) + ssl_config_file.close() + utils.set_permissions(self.ssl_config_file_name, + mode=PRIVATE_FILE_PERMS, + user=self.use_keystone_user, + group=self.use_keystone_group, log=LOG) + + index_file_name = os.path.join(self.conf_dir, 'index.txt') + if not file_exists(index_file_name): + index_file = open(index_file_name, 'w') + index_file.write('') + index_file.close() + utils.set_permissions(index_file_name, + mode=PRIVATE_FILE_PERMS, + user=self.use_keystone_user, + group=self.use_keystone_group, log=LOG) + + serial_file_name = os.path.join(self.conf_dir, 'serial') + if not file_exists(serial_file_name): + index_file = open(serial_file_name, 'w') + index_file.write('01') + index_file.close() + utils.set_permissions(serial_file_name, + mode=PRIVATE_FILE_PERMS, + user=self.use_keystone_user, + group=self.use_keystone_group, log=LOG) + + def build_ca_cert(self): + ca_key_file = self.ssl_dictionary['ca_private_key'] + utils.make_dirs(os.path.dirname(ca_key_file), + mode=PRIVATE_DIR_PERMS, + user=self.use_keystone_user, + group=self.use_keystone_group, log=LOG) + if not file_exists(ca_key_file): + self.exec_command(['openssl', 'genrsa', + '-out', '%(ca_private_key)s', + '%(key_size)d']) + utils.set_permissions(ca_key_file, + mode=PRIVATE_FILE_PERMS, + user=self.use_keystone_user, + group=self.use_keystone_group, log=LOG) + + ca_cert = self.ssl_dictionary['ca_cert'] + utils.make_dirs(os.path.dirname(ca_cert), + mode=PUBLIC_DIR_PERMS, + user=self.use_keystone_user, + group=self.use_keystone_group, log=LOG) + if not file_exists(ca_cert): + self.exec_command(['openssl', 'req', '-new', '-x509', + '-extensions', 'v3_ca', + '-key', '%(ca_private_key)s', + '-out', '%(ca_cert)s', + '-days', '%(valid_days)d', + '-config', '%(ssl_config)s', + '-subj', '%(cert_subject)s']) + utils.set_permissions(ca_cert, + mode=PUBLIC_FILE_PERMS, + user=self.use_keystone_user, + group=self.use_keystone_group, log=LOG) + + def build_private_key(self): + signing_keyfile = self.ssl_dictionary['signing_key'] + utils.make_dirs(os.path.dirname(signing_keyfile), + mode=PRIVATE_DIR_PERMS, + user=self.use_keystone_user, + group=self.use_keystone_group, log=LOG) + if not file_exists(signing_keyfile): + self.exec_command(['openssl', 'genrsa', '-out', '%(signing_key)s', + '%(key_size)d']) + utils.set_permissions(signing_keyfile, + mode=PRIVATE_FILE_PERMS, + user=self.use_keystone_user, + group=self.use_keystone_group, log=LOG) + + def build_signing_cert(self): + signing_cert = self.ssl_dictionary['signing_cert'] + + utils.make_dirs(os.path.dirname(signing_cert), + mode=PUBLIC_DIR_PERMS, + user=self.use_keystone_user, + group=self.use_keystone_group, log=LOG) + if not file_exists(signing_cert): + self.exec_command(['openssl', 'req', '-key', '%(signing_key)s', + '-new', '-out', '%(request_file)s', + '-config', '%(ssl_config)s', + '-subj', '%(cert_subject)s']) + + self.exec_command(['openssl', 'ca', '-batch', + '-out', '%(signing_cert)s', + '-config', '%(ssl_config)s', + '-days', '%(valid_days)dd', + '-cert', '%(ca_cert)s', + '-keyfile', '%(ca_private_key)s', + '-infiles', '%(request_file)s']) + + def run(self): + try: + existing_files = self.clean_up_existing_files() + except OSError: + print('An error occurred when rebuilding cert files.') + return + if existing_files: + print('The following cert files already exist, use --rebuild to ' + 'remove the existing files before regenerating:') + for f in existing_files: + print('%s already exists' % f) + return + + self.build_ssl_config_file() + self.build_ca_cert() + self.build_private_key() + self.build_signing_cert() + + +class ConfigurePKI(BaseCertificateConfigure): + """Generate files for PKI signing using OpenSSL. + + Signed tokens require a private key and signing certificate which itself + must be signed by a CA. This class generates them with workable defaults + if each of the files are not present + + """ + + def __init__(self, keystone_user, keystone_group, rebuild=False): + super(ConfigurePKI, self).__init__(CONF.signing, CONF.signing, + keystone_user, keystone_group, + rebuild=rebuild) + + +class ConfigureSSL(BaseCertificateConfigure): + """Generate files for HTTPS using OpenSSL. + + Creates a public/private key and certificates. If a CA is not given + one will be generated using provided arguments. + """ + + def __init__(self, keystone_user, keystone_group, rebuild=False): + super(ConfigureSSL, self).__init__(CONF.ssl, CONF.eventlet_server_ssl, + keystone_user, keystone_group, + rebuild=rebuild) + + +BaseCertificateConfigure.sslconfig = """ +# OpenSSL configuration file. +# + +# Establish working directory. + +dir = %(conf_dir)s + +[ ca ] +default_ca = CA_default + +[ CA_default ] +new_certs_dir = $dir +serial = $dir/serial +database = $dir/index.txt +default_days = 365 +default_md = %(default_md)s +preserve = no +email_in_dn = no +nameopt = default_ca +certopt = default_ca +policy = policy_anything +x509_extensions = usr_cert +unique_subject = no + +[ policy_anything ] +countryName = optional +stateOrProvinceName = optional +organizationName = optional +organizationalUnitName = optional +commonName = supplied +emailAddress = optional + +[ req ] +default_bits = 2048 # Size of keys +default_keyfile = key.pem # name of generated keys +string_mask = utf8only # permitted characters +distinguished_name = req_distinguished_name +req_extensions = v3_req +x509_extensions = v3_ca + +[ req_distinguished_name ] +countryName = Country Name (2 letter code) +countryName_min = 2 +countryName_max = 2 +stateOrProvinceName = State or Province Name (full name) +localityName = Locality Name (city, district) +0.organizationName = Organization Name (company) +organizationalUnitName = Organizational Unit Name (department, division) +commonName = Common Name (hostname, IP, or your name) +commonName_max = 64 +emailAddress = Email Address +emailAddress_max = 64 + +[ v3_ca ] +basicConstraints = CA:TRUE +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid:always,issuer + +[ v3_req ] +basicConstraints = CA:FALSE +keyUsage = nonRepudiation, digitalSignature, keyEncipherment + +[ usr_cert ] +basicConstraints = CA:FALSE +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid:always +""" diff --git a/keystone-moon/keystone/common/pemutils.py b/keystone-moon/keystone/common/pemutils.py new file mode 100755 index 00000000..ddbe05cf --- /dev/null +++ b/keystone-moon/keystone/common/pemutils.py @@ -0,0 +1,509 @@ +# Copyright 2013 Red Hat, Inc. +# +# 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. + + +""" +PEM formatted data is used frequently in conjunction with X509 PKI as +a data exchange mechanism for binary data. The acronym PEM stands for +Privacy Enhanced Mail as defined in RFC-1421. Contrary to expectation +the PEM format in common use has little to do with RFC-1421. Instead +what we know as PEM format grew out of the need for a data exchange +mechanism largely by the influence of OpenSSL. Other X509 +implementations have adopted it. + +Unfortunately PEM format has never been officially standarized. It's +basic format is as follows: + +1) A header consisting of 5 hyphens followed by the word BEGIN and a +single space. Then an upper case string describing the contents of the +PEM block, this is followed by 5 hyphens and a newline. + +2) Binary data (typically in DER ASN.1 format) encoded in base64. The +base64 text is line wrapped so that each line of base64 is 64 +characters long and terminated with a newline. The last line of base64 +text may be less than 64 characters. The content and format of the +binary data is entirely dependent upon the type of data announced in +the header and footer. + +3) A footer in the exact same as the header except the word BEGIN is +replaced by END. The content name in both the header and footer should +exactly match. + +The above is called a PEM block. It is permissible for multiple PEM +blocks to appear in a single file or block of text. This is often used +when specifying multiple X509 certificates. + +An example PEM block for a certificate is: + +-----BEGIN CERTIFICATE----- +MIIC0TCCAjqgAwIBAgIJANsHKV73HYOwMA0GCSqGSIb3DQEBBQUAMIGeMQowCAYD +VQQFEwE1MQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExEjAQBgNVBAcTCVN1bm55 +dmFsZTESMBAGA1UEChMJT3BlblN0YWNrMREwDwYDVQQLEwhLZXlzdG9uZTElMCMG +CSqGSIb3DQEJARYWa2V5c3RvbmVAb3BlbnN0YWNrLm9yZzEUMBIGA1UEAxMLU2Vs +ZiBTaWduZWQwIBcNMTIxMTA1MTgxODI0WhgPMjA3MTA0MzAxODE4MjRaMIGeMQow +CAYDVQQFEwE1MQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExEjAQBgNVBAcTCVN1 +bm55dmFsZTESMBAGA1UEChMJT3BlblN0YWNrMREwDwYDVQQLEwhLZXlzdG9uZTEl +MCMGCSqGSIb3DQEJARYWa2V5c3RvbmVAb3BlbnN0YWNrLm9yZzEUMBIGA1UEAxML +U2VsZiBTaWduZWQwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBALzI17ExCaqd +r7xY2Q5CBZ1bW1lsrXxS8eNJRdQtskDuQVAluY03/OGZd8HQYiiY/ci2tYy7BNIC +bh5GaO95eqTDykJR3liOYE/tHbY6puQlj2ZivmhlSd2d5d7lF0/H28RQsLu9VktM +uw6q9DpDm35jfrr8LgSeA3MdVqcS/4OhAgMBAAGjEzARMA8GA1UdEwEB/wQFMAMB +Af8wDQYJKoZIhvcNAQEFBQADgYEAjSQND7i1dNZtLKpWgX+JqMr3BdVlM15mFeVr +C26ZspZjZVY5okdozO9gU3xcwRe4Cg30sKFOe6EBQKpkTZucFOXwBtD3h6dWJrdD +c+m/CL/rs0GatDavbaIT2vv405SQUQooCdVh72LYel+4/a6xmRd7fQx3iEXN9QYj +vmHJUcA= +-----END CERTIFICATE----- + +PEM format is safe for transmission in 7-bit ASCII systems +(i.e. standard email). Since 7-bit ASCII is a proper subset of UTF-8 +and Latin-1 it is not affected by transcoding between those +charsets. Nor is PEM format affected by the choice of line +endings. This makes PEM format particularity attractive for transport +and storage of binary data. + +This module provides a number of utilities supporting the generation +and consumption of PEM formatted data including: + + * parse text and find all PEM blocks contained in the + text. Information on the location of the block in the text, the + type of PEM block, and it's base64 and binary data contents. + + * parse text assumed to contain PEM data and return the binary + data. + + * test if a block of text is a PEM block + + * convert base64 text into a formatted PEM block + + * convert binary data into a formatted PEM block + + * access to the valid PEM types and their headers + +""" + +import base64 +import re + +import six + +from keystone.common import base64utils +from keystone.i18n import _ + + +PEM_TYPE_TO_HEADER = { + u'cms': u'CMS', + u'dsa-private': u'DSA PRIVATE KEY', + u'dsa-public': u'DSA PUBLIC KEY', + u'ecdsa-public': u'ECDSA PUBLIC KEY', + u'ec-private': u'EC PRIVATE KEY', + u'pkcs7': u'PKCS7', + u'pkcs7-signed': u'PKCS', + u'pkcs8': u'ENCRYPTED PRIVATE KEY', + u'private-key': u'PRIVATE KEY', + u'public-key': u'PUBLIC KEY', + u'rsa-private': u'RSA PRIVATE KEY', + u'rsa-public': u'RSA PUBLIC KEY', + u'cert': u'CERTIFICATE', + u'crl': u'X509 CRL', + u'cert-pair': u'CERTIFICATE PAIR', + u'csr': u'CERTIFICATE REQUEST', +} + +# This is not a 1-to-1 reverse map of PEM_TYPE_TO_HEADER +# because it includes deprecated headers that map to 1 pem_type. +PEM_HEADER_TO_TYPE = { + u'CMS': u'cms', + u'DSA PRIVATE KEY': u'dsa-private', + u'DSA PUBLIC KEY': u'dsa-public', + u'ECDSA PUBLIC KEY': u'ecdsa-public', + u'EC PRIVATE KEY': u'ec-private', + u'PKCS7': u'pkcs7', + u'PKCS': u'pkcs7-signed', + u'ENCRYPTED PRIVATE KEY': u'pkcs8', + u'PRIVATE KEY': u'private-key', + u'PUBLIC KEY': u'public-key', + u'RSA PRIVATE KEY': u'rsa-private', + u'RSA PUBLIC KEY': u'rsa-public', + u'CERTIFICATE': u'cert', + u'X509 CERTIFICATE': u'cert', + u'CERTIFICATE PAIR': u'cert-pair', + u'X509 CRL': u'crl', + u'CERTIFICATE REQUEST': u'csr', + u'NEW CERTIFICATE REQUEST': u'csr', +} + +# List of valid pem_types +pem_types = sorted(PEM_TYPE_TO_HEADER.keys()) + +# List of valid pem_headers +pem_headers = sorted(PEM_TYPE_TO_HEADER.values()) + +_pem_begin_re = re.compile(r'^-{5}BEGIN\s+([^-]+)-{5}\s*$', re.MULTILINE) +_pem_end_re = re.compile(r'^-{5}END\s+([^-]+)-{5}\s*$', re.MULTILINE) + + +class PEMParseResult(object): + """Information returned when a PEM block is found in text. + + PEMParseResult contains information about a PEM block discovered + while parsing text. The following properties are defined: + + pem_type + A short hand name for the type of the PEM data, e.g. cert, + csr, crl, cms, key. Valid pem_types are listed in pem_types. + When the pem_type is set the pem_header is updated to match it. + + pem_header + The text following '-----BEGIN ' in the PEM header. + Common examples are: + + -----BEGIN CERTIFICATE----- + -----BEGIN CMS----- + + Thus the pem_header would be CERTIFICATE and CMS respectively. + When the pem_header is set the pem_type is updated to match it. + + pem_start, pem_end + The beginning and ending positions of the PEM block + including the PEM header and footer. + + base64_start, base64_end + The beginning and ending positions of the base64 data + contained inside the PEM header and footer. Includes trailing + new line + + binary_data + The decoded base64 data. None if not decoded. + + """ + + def __init__(self, pem_type=None, pem_header=None, + pem_start=None, pem_end=None, + base64_start=None, base64_end=None, + binary_data=None): + + self._pem_type = None + self._pem_header = None + + if pem_type is not None: + self.pem_type = pem_type + + if pem_header is not None: + self.pem_header = pem_header + + self.pem_start = pem_start + self.pem_end = pem_end + self.base64_start = base64_start + self.base64_end = base64_end + self.binary_data = binary_data + + @property + def pem_type(self): + return self._pem_type + + @pem_type.setter + def pem_type(self, pem_type): + if pem_type is None: + self._pem_type = None + self._pem_header = None + else: + pem_header = PEM_TYPE_TO_HEADER.get(pem_type) + if pem_header is None: + raise ValueError(_('unknown pem_type "%(pem_type)s", ' + 'valid types are: %(valid_pem_types)s') % + {'pem_type': pem_type, + 'valid_pem_types': ', '.join(pem_types)}) + self._pem_type = pem_type + self._pem_header = pem_header + + @property + def pem_header(self): + return self._pem_header + + @pem_header.setter + def pem_header(self, pem_header): + if pem_header is None: + self._pem_type = None + self._pem_header = None + else: + pem_type = PEM_HEADER_TO_TYPE.get(pem_header) + if pem_type is None: + raise ValueError(_('unknown pem header "%(pem_header)s", ' + 'valid headers are: ' + '%(valid_pem_headers)s') % + {'pem_header': pem_header, + 'valid_pem_headers': + ', '.join("'%s'" % + [x for x in pem_headers])}) + + self._pem_type = pem_type + self._pem_header = pem_header + + +def pem_search(text, start=0): + """Search for a block of PEM formatted data + + Search for a PEM block in a text string. The search begins at + start. If a PEM block is found a PEMParseResult object is + returned, otherwise if no PEM block is found None is returned. + + If the pem_type is not the same in both the header and footer + a ValueError is raised. + + The start and end positions are suitable for use as slices into + the text. To search for multiple PEM blocks pass pem_end as the + start position for the next iteration. Terminate the iteration + when None is returned. Example:: + + start = 0 + while True: + block = pem_search(text, start) + if block is None: + break + base64_data = text[block.base64_start : block.base64_end] + start = block.pem_end + + :param text: the text to search for PEM blocks + :type text: string + :param start: the position in text to start searching from (default: 0) + :type start: int + :returns: PEMParseResult or None if not found + :raises: ValueError + """ + + match = _pem_begin_re.search(text, pos=start) + if match: + pem_start = match.start() + begin_text = match.group(0) + base64_start = min(len(text), match.end() + 1) + begin_pem_header = match.group(1).strip() + + match = _pem_end_re.search(text, pos=base64_start) + if match: + pem_end = min(len(text), match.end() + 1) + base64_end = match.start() + end_pem_header = match.group(1).strip() + else: + raise ValueError(_('failed to find end matching "%s"') % + begin_text) + + if begin_pem_header != end_pem_header: + raise ValueError(_('beginning & end PEM headers do not match ' + '(%(begin_pem_header)s' + '!= ' + '%(end_pem_header)s)') % + {'begin_pem_header': begin_pem_header, + 'end_pem_header': end_pem_header}) + else: + return None + + result = PEMParseResult(pem_header=begin_pem_header, + pem_start=pem_start, pem_end=pem_end, + base64_start=base64_start, base64_end=base64_end) + + return result + + +def parse_pem(text, pem_type=None, max_items=None): + """Scan text for PEM data, return list of PEM items + + The input text is scanned for PEM blocks, for each one found a + PEMParseResult is constructed and added to the return list. + + pem_type operates as a filter on the type of PEM desired. If + pem_type is specified only those PEM blocks which match will be + included. The pem_type is a logical name, not the actual text in + the pem header (e.g. 'cert'). If the pem_type is None all PEM + blocks are returned. + + If max_items is specified the result is limited to that number of + items. + + The return value is a list of PEMParseResult objects. The + PEMParseResult provides complete information about the PEM block + including the decoded binary data for the PEM block. The list is + ordered in the same order as found in the text. + + Examples:: + + # Get all certs + certs = parse_pem(text, 'cert') + + # Get the first cert + try: + binary_cert = parse_pem(text, 'cert', 1)[0].binary_data + except IndexError: + raise ValueError('no cert found') + + :param text: The text to search for PEM blocks + :type text: string + :param pem_type: Only return data for this pem_type. + Valid types are: csr, cert, crl, cms, key. + If pem_type is None no filtering is performed. + (default: None) + :type pem_type: string or None + :param max_items: Limit the number of blocks returned. (default: None) + :type max_items: int or None + :return: List of PEMParseResult, one for each PEM block found + :raises: ValueError, InvalidBase64Error + """ + + pem_blocks = [] + start = 0 + + while True: + block = pem_search(text, start) + if block is None: + break + start = block.pem_end + if pem_type is None: + pem_blocks.append(block) + else: + try: + if block.pem_type == pem_type: + pem_blocks.append(block) + except KeyError: + raise ValueError(_('unknown pem_type: "%s"') % (pem_type)) + + if max_items is not None and len(pem_blocks) >= max_items: + break + + for block in pem_blocks: + base64_data = text[block.base64_start:block.base64_end] + try: + binary_data = base64.b64decode(base64_data) + except Exception as e: + block.binary_data = None + raise base64utils.InvalidBase64Error( + _('failed to base64 decode %(pem_type)s PEM at position' + '%(position)d: %(err_msg)s') % + {'pem_type': block.pem_type, + 'position': block.pem_start, + 'err_msg': six.text_type(e)}) + else: + block.binary_data = binary_data + + return pem_blocks + + +def get_pem_data(text, pem_type='cert'): + """Scan text for PEM data, return binary contents + + The input text is scanned for a PEM block which matches the pem_type. + If found the binary data contained in the PEM block is returned. + If no PEM block is found or it does not match the specified pem type + None is returned. + + :param text: The text to search for the PEM block + :type text: string + :param pem_type: Only return data for this pem_type. + Valid types are: csr, cert, crl, cms, key. + (default: 'cert') + :type pem_type: string + :return: binary data or None if not found. + """ + + blocks = parse_pem(text, pem_type, 1) + if not blocks: + return None + return blocks[0].binary_data + + +def is_pem(text, pem_type='cert'): + """Does this text contain a PEM block. + + Check for the existence of a PEM formatted block in the + text, if one is found verify it's contents can be base64 + decoded, if so return True. Return False otherwise. + + :param text: The text to search for PEM blocks + :type text: string + :param pem_type: Only return data for this pem_type. + Valid types are: csr, cert, crl, cms, key. + (default: 'cert') + :type pem_type: string + :returns: bool -- True if text contains PEM matching the pem_type, + False otherwise. + """ + + try: + pem_blocks = parse_pem(text, pem_type, max_items=1) + except base64utils.InvalidBase64Error: + return False + + if pem_blocks: + return True + else: + return False + + +def base64_to_pem(base64_text, pem_type='cert'): + """Format string of base64 text into PEM format + + Input is assumed to consist only of members of the base64 alphabet + (i.e no whitepace). Use one of the filter functions from + base64utils to assure the input is clean + (i.e. strip_whitespace()). + + :param base64_text: text containing ONLY base64 alphabet + characters to be inserted into PEM output. + :type base64_text: string + :param pem_type: Produce a PEM block for this type. + Valid types are: csr, cert, crl, cms, key. + (default: 'cert') + :type pem_type: string + :returns: string -- PEM formatted text + + + """ + pem_header = PEM_TYPE_TO_HEADER[pem_type] + buf = six.StringIO() + + buf.write(u'-----BEGIN %s-----' % pem_header) + buf.write(u'\n') + + for line in base64utils.base64_wrap_iter(base64_text, width=64): + buf.write(line) + buf.write(u'\n') + + buf.write(u'-----END %s-----' % pem_header) + buf.write(u'\n') + + text = buf.getvalue() + buf.close() + return text + + +def binary_to_pem(binary_data, pem_type='cert'): + """Format binary data into PEM format + + Example: + + # get the certificate binary data in DER format + der_data = certificate.der + # convert the DER binary data into a PEM + pem = binary_to_pem(der_data, 'cert') + + + :param binary_data: binary data to encapsulate into PEM + :type binary_data: buffer + :param pem_type: Produce a PEM block for this type. + Valid types are: csr, cert, crl, cms, key. + (default: 'cert') + :type pem_type: string + :returns: string -- PEM formatted text + + """ + base64_text = base64.b64encode(binary_data) + return base64_to_pem(base64_text, pem_type) diff --git a/keystone-moon/keystone/common/router.py b/keystone-moon/keystone/common/router.py new file mode 100644 index 00000000..ce4e834d --- /dev/null +++ b/keystone-moon/keystone/common/router.py @@ -0,0 +1,80 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 keystone.common import json_home +from keystone.common import wsgi + + +class Router(wsgi.ComposableRouter): + def __init__(self, controller, collection_key, key, + resource_descriptions=None, + is_entity_implemented=True): + self.controller = controller + self.key = key + self.collection_key = collection_key + self._resource_descriptions = resource_descriptions + self._is_entity_implemented = is_entity_implemented + + def add_routes(self, mapper): + collection_path = '/%(collection_key)s' % { + 'collection_key': self.collection_key} + entity_path = '/%(collection_key)s/{%(key)s_id}' % { + 'collection_key': self.collection_key, + 'key': self.key} + + mapper.connect( + collection_path, + controller=self.controller, + action='create_%s' % self.key, + conditions=dict(method=['POST'])) + mapper.connect( + collection_path, + controller=self.controller, + action='list_%s' % self.collection_key, + conditions=dict(method=['GET'])) + mapper.connect( + entity_path, + controller=self.controller, + action='get_%s' % self.key, + conditions=dict(method=['GET'])) + mapper.connect( + entity_path, + controller=self.controller, + action='update_%s' % self.key, + conditions=dict(method=['PATCH'])) + mapper.connect( + entity_path, + controller=self.controller, + action='delete_%s' % self.key, + conditions=dict(method=['DELETE'])) + + # Add the collection resource and entity resource to the resource + # descriptions. + + collection_rel = json_home.build_v3_resource_relation( + self.collection_key) + rel_data = {'href': collection_path, } + self._resource_descriptions.append((collection_rel, rel_data)) + + if self._is_entity_implemented: + entity_rel = json_home.build_v3_resource_relation(self.key) + id_str = '%s_id' % self.key + id_param_rel = json_home.build_v3_parameter_relation(id_str) + entity_rel_data = { + 'href-template': entity_path, + 'href-vars': { + id_str: id_param_rel, + }, + } + self._resource_descriptions.append((entity_rel, entity_rel_data)) diff --git a/keystone-moon/keystone/common/sql/__init__.py b/keystone-moon/keystone/common/sql/__init__.py new file mode 100644 index 00000000..84e0fb83 --- /dev/null +++ b/keystone-moon/keystone/common/sql/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 keystone.common.sql.core import * # noqa diff --git a/keystone-moon/keystone/common/sql/core.py b/keystone-moon/keystone/common/sql/core.py new file mode 100644 index 00000000..bf168701 --- /dev/null +++ b/keystone-moon/keystone/common/sql/core.py @@ -0,0 +1,431 @@ +# Copyright 2012 OpenStack Foundation +# +# 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. + +"""SQL backends for the various services. + +Before using this module, call initialize(). This has to be done before +CONF() because it sets up configuration options. + +""" +import contextlib +import functools + +from oslo_config import cfg +from oslo_db import exception as db_exception +from oslo_db import options as db_options +from oslo_db.sqlalchemy import models +from oslo_db.sqlalchemy import session as db_session +from oslo_log import log +from oslo_serialization import jsonutils +import six +import sqlalchemy as sql +from sqlalchemy.ext import declarative +from sqlalchemy.orm.attributes import flag_modified, InstrumentedAttribute +from sqlalchemy import types as sql_types + +from keystone.common import utils +from keystone import exception +from keystone.i18n import _ + + +CONF = cfg.CONF +LOG = log.getLogger(__name__) + +ModelBase = declarative.declarative_base() + + +# For exporting to other modules +Column = sql.Column +Index = sql.Index +String = sql.String +Integer = sql.Integer +Enum = sql.Enum +ForeignKey = sql.ForeignKey +DateTime = sql.DateTime +IntegrityError = sql.exc.IntegrityError +DBDuplicateEntry = db_exception.DBDuplicateEntry +OperationalError = sql.exc.OperationalError +NotFound = sql.orm.exc.NoResultFound +Boolean = sql.Boolean +Text = sql.Text +UniqueConstraint = sql.UniqueConstraint +PrimaryKeyConstraint = sql.PrimaryKeyConstraint +joinedload = sql.orm.joinedload +# Suppress flake8's unused import warning for flag_modified: +flag_modified = flag_modified + + +def initialize(): + """Initialize the module.""" + + db_options.set_defaults( + CONF, + connection="sqlite:///keystone.db") + + +def initialize_decorator(init): + """Ensure that the length of string field do not exceed the limit. + + This decorator check the initialize arguments, to make sure the + length of string field do not exceed the length limit, or raise a + 'StringLengthExceeded' exception. + + Use decorator instead of inheritance, because the metaclass will + check the __tablename__, primary key columns, etc. at the class + definition. + + """ + def initialize(self, *args, **kwargs): + cls = type(self) + for k, v in kwargs.items(): + if hasattr(cls, k): + attr = getattr(cls, k) + if isinstance(attr, InstrumentedAttribute): + column = attr.property.columns[0] + if isinstance(column.type, String): + if not isinstance(v, six.text_type): + v = six.text_type(v) + if column.type.length and column.type.length < len(v): + raise exception.StringLengthExceeded( + string=v, type=k, length=column.type.length) + + init(self, *args, **kwargs) + return initialize + +ModelBase.__init__ = initialize_decorator(ModelBase.__init__) + + +# Special Fields +class JsonBlob(sql_types.TypeDecorator): + + impl = sql.Text + + def process_bind_param(self, value, dialect): + return jsonutils.dumps(value) + + def process_result_value(self, value, dialect): + return jsonutils.loads(value) + + +class DictBase(models.ModelBase): + attributes = [] + + @classmethod + def from_dict(cls, d): + new_d = d.copy() + + new_d['extra'] = {k: new_d.pop(k) for k in six.iterkeys(d) + if k not in cls.attributes and k != 'extra'} + + return cls(**new_d) + + def to_dict(self, include_extra_dict=False): + """Returns the model's attributes as a dictionary. + + If include_extra_dict is True, 'extra' attributes are literally + included in the resulting dictionary twice, for backwards-compatibility + with a broken implementation. + + """ + d = self.extra.copy() + for attr in self.__class__.attributes: + d[attr] = getattr(self, attr) + + if include_extra_dict: + d['extra'] = self.extra.copy() + + return d + + def __getitem__(self, key): + if key in self.extra: + return self.extra[key] + return getattr(self, key) + + +class ModelDictMixin(object): + + @classmethod + def from_dict(cls, d): + """Returns a model instance from a dictionary.""" + return cls(**d) + + def to_dict(self): + """Returns the model's attributes as a dictionary.""" + names = (column.name for column in self.__table__.columns) + return {name: getattr(self, name) for name in names} + + +_engine_facade = None + + +def _get_engine_facade(): + global _engine_facade + + if not _engine_facade: + _engine_facade = db_session.EngineFacade.from_config(CONF) + + return _engine_facade + + +def cleanup(): + global _engine_facade + + _engine_facade = None + + +def get_engine(): + return _get_engine_facade().get_engine() + + +def get_session(expire_on_commit=False): + return _get_engine_facade().get_session(expire_on_commit=expire_on_commit) + + +@contextlib.contextmanager +def transaction(expire_on_commit=False): + """Return a SQLAlchemy session in a scoped transaction.""" + session = get_session(expire_on_commit=expire_on_commit) + with session.begin(): + yield session + + +def truncated(f): + """Ensure list truncation is detected in Driver list entity methods. + + This is designed to wrap and sql Driver list_{entity} methods in order to + calculate if the resultant list has been truncated. Provided a limit dict + is found in the hints list, we increment the limit by one so as to ask the + wrapped function for one more entity than the limit, and then once the list + has been generated, we check to see if the original limit has been + exceeded, in which case we truncate back to that limit and set the + 'truncated' boolean to 'true' in the hints limit dict. + + """ + @functools.wraps(f) + def wrapper(self, hints, *args, **kwargs): + if not hasattr(hints, 'limit'): + raise exception.UnexpectedError( + _('Cannot truncate a driver call without hints list as ' + 'first parameter after self ')) + + if hints.limit is None: + return f(self, hints, *args, **kwargs) + + # A limit is set, so ask for one more entry than we need + list_limit = hints.limit['limit'] + hints.set_limit(list_limit + 1) + ref_list = f(self, hints, *args, **kwargs) + + # If we got more than the original limit then trim back the list and + # mark it truncated. In both cases, make sure we set the limit back + # to its original value. + if len(ref_list) > list_limit: + hints.set_limit(list_limit, truncated=True) + return ref_list[:list_limit] + else: + hints.set_limit(list_limit) + return ref_list + return wrapper + + +def _filter(model, query, hints): + """Applies filtering to a query. + + :param model: the table model in question + :param query: query to apply filters to + :param hints: contains the list of filters yet to be satisfied. + Any filters satisfied here will be removed so that + the caller will know if any filters remain. + + :returns query: query, updated with any filters satisfied + + """ + def inexact_filter(model, query, filter_, satisfied_filters, hints): + """Applies an inexact filter to a query. + + :param model: the table model in question + :param query: query to apply filters to + :param filter_: the dict that describes this filter + :param satisfied_filters: a cumulative list of satisfied filters, to + which filter_ will be added if it is + satisfied. + :param hints: contains the list of filters yet to be satisfied. + + :returns query: query updated to add any inexact filters we could + satisfy + + """ + column_attr = getattr(model, filter_['name']) + + # TODO(henry-nash): Sqlalchemy 0.7 defaults to case insensitivity + # so once we find a way of changing that (maybe on a call-by-call + # basis), we can add support for the case sensitive versions of + # the filters below. For now, these case sensitive versions will + # be handled at the controller level. + + if filter_['case_sensitive']: + return query + + if filter_['comparator'] == 'contains': + query_term = column_attr.ilike('%%%s%%' % filter_['value']) + elif filter_['comparator'] == 'startswith': + query_term = column_attr.ilike('%s%%' % filter_['value']) + elif filter_['comparator'] == 'endswith': + query_term = column_attr.ilike('%%%s' % filter_['value']) + else: + # It's a filter we don't understand, so let the caller + # work out if they need to do something with it. + return query + + satisfied_filters.append(filter_) + return query.filter(query_term) + + def exact_filter( + model, filter_, satisfied_filters, cumulative_filter_dict, hints): + """Applies an exact filter to a query. + + :param model: the table model in question + :param filter_: the dict that describes this filter + :param satisfied_filters: a cumulative list of satisfied filters, to + which filter_ will be added if it is + satisfied. + :param cumulative_filter_dict: a dict that describes the set of + exact filters built up so far + :param hints: contains the list of filters yet to be satisfied. + + :returns: updated cumulative dict + + """ + key = filter_['name'] + if isinstance(getattr(model, key).property.columns[0].type, + sql.types.Boolean): + cumulative_filter_dict[key] = ( + utils.attr_as_boolean(filter_['value'])) + else: + cumulative_filter_dict[key] = filter_['value'] + satisfied_filters.append(filter_) + return cumulative_filter_dict + + filter_dict = {} + satisfied_filters = [] + for filter_ in hints.filters: + if filter_['name'] not in model.attributes: + continue + if filter_['comparator'] == 'equals': + filter_dict = exact_filter( + model, filter_, satisfied_filters, filter_dict, hints) + else: + query = inexact_filter( + model, query, filter_, satisfied_filters, hints) + + # Apply any exact filters we built up + if filter_dict: + query = query.filter_by(**filter_dict) + + # Remove satisfied filters, then the caller will know remaining filters + for filter_ in satisfied_filters: + hints.filters.remove(filter_) + + return query + + +def _limit(query, hints): + """Applies a limit to a query. + + :param query: query to apply filters to + :param hints: contains the list of filters and limit details. + + :returns updated query + + """ + # NOTE(henry-nash): If we were to implement pagination, then we + # we would expand this method to support pagination and limiting. + + # If we satisfied all the filters, set an upper limit if supplied + if hints.limit: + query = query.limit(hints.limit['limit']) + return query + + +def filter_limit_query(model, query, hints): + """Applies filtering and limit to a query. + + :param model: table model + :param query: query to apply filters to + :param hints: contains the list of filters and limit details. This may + be None, indicating that there are no filters or limits + to be applied. If it's not None, then any filters + satisfied here will be removed so that the caller will + know if any filters remain. + + :returns: updated query + + """ + if hints is None: + return query + + # First try and satisfy any filters + query = _filter(model, query, hints) + + # NOTE(henry-nash): Any unsatisfied filters will have been left in + # the hints list for the controller to handle. We can only try and + # limit here if all the filters are already satisfied since, if not, + # doing so might mess up the final results. If there are still + # unsatisfied filters, we have to leave any limiting to the controller + # as well. + + if not hints.filters: + return _limit(query, hints) + else: + return query + + +def handle_conflicts(conflict_type='object'): + """Converts select sqlalchemy exceptions into HTTP 409 Conflict.""" + _conflict_msg = 'Conflict %(conflict_type)s: %(details)s' + + def decorator(method): + @functools.wraps(method) + def wrapper(*args, **kwargs): + try: + return method(*args, **kwargs) + except db_exception.DBDuplicateEntry as e: + # LOG the exception for debug purposes, do not send the + # exception details out with the raised Conflict exception + # as it can contain raw SQL. + LOG.debug(_conflict_msg, {'conflict_type': conflict_type, + 'details': six.text_type(e)}) + raise exception.Conflict(type=conflict_type, + details=_('Duplicate Entry')) + except db_exception.DBError as e: + # TODO(blk-u): inspecting inner_exception breaks encapsulation; + # oslo_db should provide exception we need. + if isinstance(e.inner_exception, IntegrityError): + # LOG the exception for debug purposes, do not send the + # exception details out with the raised Conflict exception + # as it can contain raw SQL. + LOG.debug(_conflict_msg, {'conflict_type': conflict_type, + 'details': six.text_type(e)}) + # NOTE(morganfainberg): This is really a case where the SQL + # failed to store the data. This is not something that the + # user has done wrong. Example would be a ForeignKey is + # missing; the code that is executed before reaching the + # SQL writing to the DB should catch the issue. + raise exception.UnexpectedError( + _('An unexpected error occurred when trying to ' + 'store %s') % conflict_type) + raise + + return wrapper + return decorator diff --git a/keystone-moon/keystone/common/sql/migrate_repo/README b/keystone-moon/keystone/common/sql/migrate_repo/README new file mode 100644 index 00000000..6218f8ca --- /dev/null +++ b/keystone-moon/keystone/common/sql/migrate_repo/README @@ -0,0 +1,4 @@ +This is a database migration repository. + +More information at +http://code.google.com/p/sqlalchemy-migrate/ diff --git a/keystone-moon/keystone/common/sql/migrate_repo/__init__.py b/keystone-moon/keystone/common/sql/migrate_repo/__init__.py new file mode 100644 index 00000000..f73dfc12 --- /dev/null +++ b/keystone-moon/keystone/common/sql/migrate_repo/__init__.py @@ -0,0 +1,17 @@ +# Copyright 2014 Mirantis.inc +# All Rights Reserved. +# +# 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. + + +DB_INIT_VERSION = 43 diff --git a/keystone-moon/keystone/common/sql/migrate_repo/manage.py b/keystone-moon/keystone/common/sql/migrate_repo/manage.py new file mode 100644 index 00000000..39fa3892 --- /dev/null +++ b/keystone-moon/keystone/common/sql/migrate_repo/manage.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python +from migrate.versioning.shell import main + +if __name__ == '__main__': + main(debug='False') diff --git a/keystone-moon/keystone/common/sql/migrate_repo/migrate.cfg b/keystone-moon/keystone/common/sql/migrate_repo/migrate.cfg new file mode 100644 index 00000000..db531bb4 --- /dev/null +++ b/keystone-moon/keystone/common/sql/migrate_repo/migrate.cfg @@ -0,0 +1,25 @@ +[db_settings] +# Used to identify which repository this database is versioned under. +# You can use the name of your project. +repository_id=keystone + +# The name of the database table used to track the schema version. +# This name shouldn't already be used by your project. +# If this is changed once a database is under version control, you'll need to +# change the table name in each database too. +version_table=migrate_version + +# When committing a change script, Migrate will attempt to generate the +# sql for all supported databases; normally, if one of them fails - probably +# because you don't have that database installed - it is ignored and the +# commit continues, perhaps ending successfully. +# Databases in this list MUST compile successfully during a commit, or the +# entire commit will fail. List the databases your application will actually +# be using to ensure your updates to that database work properly. +# This must be a list; example: ['postgres','sqlite'] +required_dbs=[] + +# When creating new change scripts, Migrate will stamp the new script with +# a version number. By default this is latest_version + 1. You can set this +# to 'true' to tell Migrate to use the UTC timestamp instead. +use_timestamp_numbering=False diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/044_icehouse.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/044_icehouse.py new file mode 100644 index 00000000..6f326ecf --- /dev/null +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/044_icehouse.py @@ -0,0 +1,279 @@ +# 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 migrate +from oslo_config import cfg +from oslo_log import log +import sqlalchemy as sql +from sqlalchemy import orm + +from keystone.assignment.backends import sql as assignment_sql +from keystone.common import sql as ks_sql +from keystone.common.sql import migration_helpers + + +LOG = log.getLogger(__name__) +CONF = cfg.CONF + + +def upgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + + if migrate_engine.name == 'mysql': + # In Folsom we explicitly converted migrate_version to UTF8. + migrate_engine.execute( + 'ALTER TABLE migrate_version CONVERT TO CHARACTER SET utf8') + # Set default DB charset to UTF8. + migrate_engine.execute( + 'ALTER DATABASE %s DEFAULT CHARACTER SET utf8' % + migrate_engine.url.database) + + credential = sql.Table( + 'credential', meta, + sql.Column('id', sql.String(length=64), primary_key=True), + sql.Column('user_id', sql.String(length=64), nullable=False), + sql.Column('project_id', sql.String(length=64)), + sql.Column('blob', ks_sql.JsonBlob, nullable=False), + sql.Column('type', sql.String(length=255), nullable=False), + sql.Column('extra', ks_sql.JsonBlob.impl), + mysql_engine='InnoDB', + mysql_charset='utf8') + + domain = sql.Table( + 'domain', meta, + sql.Column('id', sql.String(length=64), primary_key=True), + sql.Column('name', sql.String(length=64), nullable=False), + sql.Column('enabled', sql.Boolean, default=True, nullable=False), + sql.Column('extra', ks_sql.JsonBlob.impl), + mysql_engine='InnoDB', + mysql_charset='utf8') + + endpoint = sql.Table( + 'endpoint', meta, + sql.Column('id', sql.String(length=64), primary_key=True), + sql.Column('legacy_endpoint_id', sql.String(length=64)), + sql.Column('interface', sql.String(length=8), nullable=False), + sql.Column('region', sql.String(length=255)), + sql.Column('service_id', sql.String(length=64), nullable=False), + sql.Column('url', sql.Text, nullable=False), + sql.Column('extra', ks_sql.JsonBlob.impl), + sql.Column('enabled', sql.Boolean, nullable=False, default=True, + server_default='1'), + mysql_engine='InnoDB', + mysql_charset='utf8') + + group = sql.Table( + 'group', meta, + sql.Column('id', sql.String(length=64), primary_key=True), + sql.Column('domain_id', sql.String(length=64), nullable=False), + sql.Column('name', sql.String(length=64), nullable=False), + sql.Column('description', sql.Text), + sql.Column('extra', ks_sql.JsonBlob.impl), + mysql_engine='InnoDB', + mysql_charset='utf8') + + policy = sql.Table( + 'policy', meta, + sql.Column('id', sql.String(length=64), primary_key=True), + sql.Column('type', sql.String(length=255), nullable=False), + sql.Column('blob', ks_sql.JsonBlob, nullable=False), + sql.Column('extra', ks_sql.JsonBlob.impl), + mysql_engine='InnoDB', + mysql_charset='utf8') + + project = sql.Table( + 'project', meta, + sql.Column('id', sql.String(length=64), primary_key=True), + sql.Column('name', sql.String(length=64), nullable=False), + sql.Column('extra', ks_sql.JsonBlob.impl), + sql.Column('description', sql.Text), + sql.Column('enabled', sql.Boolean), + sql.Column('domain_id', sql.String(length=64), nullable=False), + mysql_engine='InnoDB', + mysql_charset='utf8') + + role = sql.Table( + 'role', meta, + sql.Column('id', sql.String(length=64), primary_key=True), + sql.Column('name', sql.String(length=255), nullable=False), + sql.Column('extra', ks_sql.JsonBlob.impl), + mysql_engine='InnoDB', + mysql_charset='utf8') + + service = sql.Table( + 'service', meta, + sql.Column('id', sql.String(length=64), primary_key=True), + sql.Column('type', sql.String(length=255)), + sql.Column('enabled', sql.Boolean, nullable=False, default=True, + server_default='1'), + sql.Column('extra', ks_sql.JsonBlob.impl), + mysql_engine='InnoDB', + mysql_charset='utf8') + + token = sql.Table( + 'token', meta, + sql.Column('id', sql.String(length=64), primary_key=True), + sql.Column('expires', sql.DateTime, default=None), + sql.Column('extra', ks_sql.JsonBlob.impl), + sql.Column('valid', sql.Boolean, default=True, nullable=False), + sql.Column('trust_id', sql.String(length=64)), + sql.Column('user_id', sql.String(length=64)), + mysql_engine='InnoDB', + mysql_charset='utf8') + + trust = sql.Table( + 'trust', meta, + sql.Column('id', sql.String(length=64), primary_key=True), + sql.Column('trustor_user_id', sql.String(length=64), nullable=False), + sql.Column('trustee_user_id', sql.String(length=64), nullable=False), + sql.Column('project_id', sql.String(length=64)), + sql.Column('impersonation', sql.Boolean, nullable=False), + sql.Column('deleted_at', sql.DateTime), + sql.Column('expires_at', sql.DateTime), + sql.Column('remaining_uses', sql.Integer, nullable=True), + sql.Column('extra', ks_sql.JsonBlob.impl), + mysql_engine='InnoDB', + mysql_charset='utf8') + + trust_role = sql.Table( + 'trust_role', meta, + sql.Column('trust_id', sql.String(length=64), primary_key=True, + nullable=False), + sql.Column('role_id', sql.String(length=64), primary_key=True, + nullable=False), + mysql_engine='InnoDB', + mysql_charset='utf8') + + user = sql.Table( + 'user', meta, + sql.Column('id', sql.String(length=64), primary_key=True), + sql.Column('name', sql.String(length=255), nullable=False), + sql.Column('extra', ks_sql.JsonBlob.impl), + sql.Column('password', sql.String(length=128)), + sql.Column('enabled', sql.Boolean), + sql.Column('domain_id', sql.String(length=64), nullable=False), + sql.Column('default_project_id', sql.String(length=64)), + mysql_engine='InnoDB', + mysql_charset='utf8') + + user_group_membership = sql.Table( + 'user_group_membership', meta, + sql.Column('user_id', sql.String(length=64), primary_key=True), + sql.Column('group_id', sql.String(length=64), primary_key=True), + mysql_engine='InnoDB', + mysql_charset='utf8') + + region = sql.Table( + 'region', + meta, + sql.Column('id', sql.String(64), primary_key=True), + sql.Column('description', sql.String(255), nullable=False), + sql.Column('parent_region_id', sql.String(64), nullable=True), + sql.Column('extra', sql.Text()), + mysql_engine='InnoDB', + mysql_charset='utf8') + + assignment = sql.Table( + 'assignment', + meta, + sql.Column('type', sql.Enum( + assignment_sql.AssignmentType.USER_PROJECT, + assignment_sql.AssignmentType.GROUP_PROJECT, + assignment_sql.AssignmentType.USER_DOMAIN, + assignment_sql.AssignmentType.GROUP_DOMAIN, + name='type'), + nullable=False), + sql.Column('actor_id', sql.String(64), nullable=False), + sql.Column('target_id', sql.String(64), nullable=False), + sql.Column('role_id', sql.String(64), nullable=False), + sql.Column('inherited', sql.Boolean, default=False, nullable=False), + sql.PrimaryKeyConstraint('type', 'actor_id', 'target_id', 'role_id'), + mysql_engine='InnoDB', + mysql_charset='utf8') + + # create all tables + tables = [credential, domain, endpoint, group, + policy, project, role, service, + token, trust, trust_role, user, + user_group_membership, region, assignment] + + for table in tables: + try: + table.create() + except Exception: + LOG.exception('Exception while creating table: %r', table) + raise + + # Unique Constraints + migrate.UniqueConstraint(user.c.domain_id, + user.c.name, + name='ixu_user_name_domain_id').create() + migrate.UniqueConstraint(group.c.domain_id, + group.c.name, + name='ixu_group_name_domain_id').create() + migrate.UniqueConstraint(role.c.name, + name='ixu_role_name').create() + migrate.UniqueConstraint(project.c.domain_id, + project.c.name, + name='ixu_project_name_domain_id').create() + migrate.UniqueConstraint(domain.c.name, + name='ixu_domain_name').create() + + # Indexes + sql.Index('ix_token_expires', token.c.expires).create() + sql.Index('ix_token_expires_valid', token.c.expires, + token.c.valid).create() + + fkeys = [ + {'columns': [endpoint.c.service_id], + 'references': [service.c.id]}, + + {'columns': [user_group_membership.c.group_id], + 'references': [group.c.id], + 'name': 'fk_user_group_membership_group_id'}, + + {'columns': [user_group_membership.c.user_id], + 'references':[user.c.id], + 'name': 'fk_user_group_membership_user_id'}, + + {'columns': [user.c.domain_id], + 'references': [domain.c.id], + 'name': 'fk_user_domain_id'}, + + {'columns': [group.c.domain_id], + 'references': [domain.c.id], + 'name': 'fk_group_domain_id'}, + + {'columns': [project.c.domain_id], + 'references': [domain.c.id], + 'name': 'fk_project_domain_id'}, + + {'columns': [assignment.c.role_id], + 'references': [role.c.id]} + ] + + for fkey in fkeys: + migrate.ForeignKeyConstraint(columns=fkey['columns'], + refcolumns=fkey['references'], + name=fkey.get('name')).create() + + # Create the default domain. + session = orm.sessionmaker(bind=migrate_engine)() + domain.insert(migration_helpers.get_default_domain()).execute() + session.commit() + + +def downgrade(migrate_engine): + raise NotImplementedError('Downgrade to pre-Icehouse release db schema is ' + 'unsupported.') diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/045_placeholder.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/045_placeholder.py new file mode 100644 index 00000000..b6f40719 --- /dev/null +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/045_placeholder.py @@ -0,0 +1,25 @@ +# 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. + +# This is a placeholder for Icehouse backports. Do not use this number for new +# Juno work. New Juno work starts after all the placeholders. +# +# See blueprint reserved-db-migrations-icehouse and the related discussion: +# http://lists.openstack.org/pipermail/openstack-dev/2013-March/006827.html + + +def upgrade(migrate_engine): + pass + + +def downgrade(migration_engine): + pass diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/046_placeholder.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/046_placeholder.py new file mode 100644 index 00000000..b6f40719 --- /dev/null +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/046_placeholder.py @@ -0,0 +1,25 @@ +# 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. + +# This is a placeholder for Icehouse backports. Do not use this number for new +# Juno work. New Juno work starts after all the placeholders. +# +# See blueprint reserved-db-migrations-icehouse and the related discussion: +# http://lists.openstack.org/pipermail/openstack-dev/2013-March/006827.html + + +def upgrade(migrate_engine): + pass + + +def downgrade(migration_engine): + pass diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/047_placeholder.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/047_placeholder.py new file mode 100644 index 00000000..b6f40719 --- /dev/null +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/047_placeholder.py @@ -0,0 +1,25 @@ +# 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. + +# This is a placeholder for Icehouse backports. Do not use this number for new +# Juno work. New Juno work starts after all the placeholders. +# +# See blueprint reserved-db-migrations-icehouse and the related discussion: +# http://lists.openstack.org/pipermail/openstack-dev/2013-March/006827.html + + +def upgrade(migrate_engine): + pass + + +def downgrade(migration_engine): + pass diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/048_placeholder.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/048_placeholder.py new file mode 100644 index 00000000..b6f40719 --- /dev/null +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/048_placeholder.py @@ -0,0 +1,25 @@ +# 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. + +# This is a placeholder for Icehouse backports. Do not use this number for new +# Juno work. New Juno work starts after all the placeholders. +# +# See blueprint reserved-db-migrations-icehouse and the related discussion: +# http://lists.openstack.org/pipermail/openstack-dev/2013-March/006827.html + + +def upgrade(migrate_engine): + pass + + +def downgrade(migration_engine): + pass diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/049_placeholder.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/049_placeholder.py new file mode 100644 index 00000000..b6f40719 --- /dev/null +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/049_placeholder.py @@ -0,0 +1,25 @@ +# 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. + +# This is a placeholder for Icehouse backports. Do not use this number for new +# Juno work. New Juno work starts after all the placeholders. +# +# See blueprint reserved-db-migrations-icehouse and the related discussion: +# http://lists.openstack.org/pipermail/openstack-dev/2013-March/006827.html + + +def upgrade(migrate_engine): + pass + + +def downgrade(migration_engine): + pass diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/050_fk_consistent_indexes.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/050_fk_consistent_indexes.py new file mode 100644 index 00000000..535a0944 --- /dev/null +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/050_fk_consistent_indexes.py @@ -0,0 +1,49 @@ +# Copyright 2014 Mirantis.inc +# All Rights Reserved. +# +# 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 sqlalchemy as sa + + +def upgrade(migrate_engine): + + if migrate_engine.name == 'mysql': + meta = sa.MetaData(bind=migrate_engine) + endpoint = sa.Table('endpoint', meta, autoload=True) + + # NOTE(i159): MySQL requires indexes on referencing columns, and those + # indexes create automatically. That those indexes will have different + # names, depending on version of MySQL used. We shoud make this naming + # consistent, by reverting index name to a consistent condition. + if any(i for i in endpoint.indexes if + i.columns.keys() == ['service_id'] and i.name != 'service_id'): + # NOTE(i159): by this action will be made re-creation of an index + # with the new name. This can be considered as renaming under the + # MySQL rules. + sa.Index('service_id', endpoint.c.service_id).create() + + user_group_membership = sa.Table('user_group_membership', + meta, autoload=True) + + if any(i for i in user_group_membership.indexes if + i.columns.keys() == ['group_id'] and i.name != 'group_id'): + sa.Index('group_id', user_group_membership.c.group_id).create() + + +def downgrade(migrate_engine): + # NOTE(i159): index exists only in MySQL schemas, and got an inconsistent + # name only when MySQL 5.5 renamed it after re-creation + # (during migrations). So we just fixed inconsistency, there is no + # necessity to revert it. + pass diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/051_add_id_mapping.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/051_add_id_mapping.py new file mode 100644 index 00000000..074fbb63 --- /dev/null +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/051_add_id_mapping.py @@ -0,0 +1,49 @@ +# Copyright 2014 IBM Corp. +# +# 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 sqlalchemy as sql + +from keystone.identity.mapping_backends import mapping + + +MAPPING_TABLE = 'id_mapping' + + +def upgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + + mapping_table = sql.Table( + MAPPING_TABLE, + meta, + sql.Column('public_id', sql.String(64), primary_key=True), + sql.Column('domain_id', sql.String(64), nullable=False), + sql.Column('local_id', sql.String(64), nullable=False), + sql.Column('entity_type', sql.Enum( + mapping.EntityType.USER, + mapping.EntityType.GROUP, + name='entity_type'), + nullable=False), + sql.UniqueConstraint('domain_id', 'local_id', 'entity_type'), + mysql_engine='InnoDB', + mysql_charset='utf8') + mapping_table.create(migrate_engine, checkfirst=True) + + +def downgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + + assignment = sql.Table(MAPPING_TABLE, meta, autoload=True) + assignment.drop(migrate_engine, checkfirst=True) diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/052_add_auth_url_to_region.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/052_add_auth_url_to_region.py new file mode 100644 index 00000000..9f1fd9f0 --- /dev/null +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/052_add_auth_url_to_region.py @@ -0,0 +1,34 @@ +# Copyright 2014 IBM Corp. +# +# 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 sqlalchemy as sql + +_REGION_TABLE_NAME = 'region' + + +def upgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + + region_table = sql.Table(_REGION_TABLE_NAME, meta, autoload=True) + url_column = sql.Column('url', sql.String(255), nullable=True) + region_table.create_column(url_column) + + +def downgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + + region_table = sql.Table(_REGION_TABLE_NAME, meta, autoload=True) + region_table.drop_column('url') diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/053_endpoint_to_region_association.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/053_endpoint_to_region_association.py new file mode 100644 index 00000000..6dc0004f --- /dev/null +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/053_endpoint_to_region_association.py @@ -0,0 +1,156 @@ +# Copyright (c) 2013 Hewlett-Packard Development Company, L.P +# +# 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. + + +"""Migrated the endpoint 'region' column to 'region_id. + +In addition to the rename, the new column is made a foreign key to the +respective 'region' in the region table, ensuring that we auto-create +any regions that are missing. Further, since the old region column +was 255 chars, and the id column in the region table is 64 chars, the size +of the id column in the region table is increased to match. + +To Upgrade: + + +Region Table + +Increase the size of the if column in the region table + +Endpoint Table + +a. Add the endpoint region_id column, that is a foreign key to the region table +b. For each endpoint + i. Ensure there is matching region in region table, and if not, create it + ii. Assign the id to the region_id column +c. Remove the column region + + +To Downgrade: + +Endpoint Table + +a. Add back in the region column +b. For each endpoint + i. Copy the region_id column to the region column +c. Remove the column region_id + +Region Table + +Decrease the size of the id column in the region table, making sure that +we don't get classing primary keys. + +""" + +import migrate +import six +import sqlalchemy as sql +from sqlalchemy.orm import sessionmaker + + +def _migrate_to_region_id(migrate_engine, region_table, endpoint_table): + endpoints = list(endpoint_table.select().execute()) + + for endpoint in endpoints: + if endpoint.region is None: + continue + + region = list(region_table.select( + whereclause=region_table.c.id == endpoint.region).execute()) + if len(region) == 1: + region_id = region[0].id + else: + region_id = endpoint.region + region = {'id': region_id, + 'description': '', + 'extra': '{}'} + session = sessionmaker(bind=migrate_engine)() + region_table.insert(region).execute() + session.commit() + + new_values = {'region_id': region_id} + f = endpoint_table.c.id == endpoint.id + update = endpoint_table.update().where(f).values(new_values) + migrate_engine.execute(update) + + migrate.ForeignKeyConstraint( + columns=[endpoint_table.c.region_id], + refcolumns=[region_table.c.id], + name='fk_endpoint_region_id').create() + + +def _migrate_to_region(migrate_engine, region_table, endpoint_table): + endpoints = list(endpoint_table.select().execute()) + + for endpoint in endpoints: + new_values = {'region': endpoint.region_id} + f = endpoint_table.c.id == endpoint.id + update = endpoint_table.update().where(f).values(new_values) + migrate_engine.execute(update) + + if 'sqlite' != migrate_engine.name: + migrate.ForeignKeyConstraint( + columns=[endpoint_table.c.region_id], + refcolumns=[region_table.c.id], + name='fk_endpoint_region_id').drop() + endpoint_table.c.region_id.drop() + + +def _prepare_regions_for_id_truncation(migrate_engine, region_table): + """Ensure there are no IDs that are bigger than 64 chars. + + The size of the id and parent_id fields where increased from 64 to 255 + during the upgrade. On downgrade we have to make sure that the ids can + fit in the new column size. For rows with ids greater than this, we have + no choice but to dump them. + + """ + for region in list(region_table.select().execute()): + if (len(six.text_type(region.id)) > 64 or + len(six.text_type(region.parent_region_id)) > 64): + delete = region_table.delete(region_table.c.id == region.id) + migrate_engine.execute(delete) + + +def upgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + + region_table = sql.Table('region', meta, autoload=True) + region_table.c.id.alter(type=sql.String(length=255)) + region_table.c.parent_region_id.alter(type=sql.String(length=255)) + endpoint_table = sql.Table('endpoint', meta, autoload=True) + region_id_column = sql.Column('region_id', + sql.String(length=255), nullable=True) + region_id_column.create(endpoint_table) + + _migrate_to_region_id(migrate_engine, region_table, endpoint_table) + + endpoint_table.c.region.drop() + + +def downgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + + region_table = sql.Table('region', meta, autoload=True) + endpoint_table = sql.Table('endpoint', meta, autoload=True) + region_column = sql.Column('region', sql.String(length=255)) + region_column.create(endpoint_table) + + _migrate_to_region(migrate_engine, region_table, endpoint_table) + _prepare_regions_for_id_truncation(migrate_engine, region_table) + + region_table.c.id.alter(type=sql.String(length=64)) + region_table.c.parent_region_id.alter(type=sql.String(length=64)) diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/054_add_actor_id_index.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/054_add_actor_id_index.py new file mode 100644 index 00000000..33b13b7d --- /dev/null +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/054_add_actor_id_index.py @@ -0,0 +1,35 @@ +# Copyright 2014 IBM Corp. +# +# 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 sqlalchemy as sql + +ASSIGNMENT_TABLE = 'assignment' + + +def upgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + + assignment = sql.Table(ASSIGNMENT_TABLE, meta, autoload=True) + idx = sql.Index('ix_actor_id', assignment.c.actor_id) + idx.create(migrate_engine) + + +def downgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + + assignment = sql.Table(ASSIGNMENT_TABLE, meta, autoload=True) + idx = sql.Index('ix_actor_id', assignment.c.actor_id) + idx.drop(migrate_engine) diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/055_add_indexes_to_token_table.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/055_add_indexes_to_token_table.py new file mode 100644 index 00000000..1cfddd3f --- /dev/null +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/055_add_indexes_to_token_table.py @@ -0,0 +1,35 @@ +# 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. + +"""Add indexes to `user_id` and `trust_id` columns for the `token` table.""" + +import sqlalchemy as sql + + +def upgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + + token = sql.Table('token', meta, autoload=True) + + sql.Index('ix_token_user_id', token.c.user_id).create() + sql.Index('ix_token_trust_id', token.c.trust_id).create() + + +def downgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + + token = sql.Table('token', meta, autoload=True) + + sql.Index('ix_token_user_id', token.c.user_id).drop() + sql.Index('ix_token_trust_id', token.c.trust_id).drop() diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/056_placeholder.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/056_placeholder.py new file mode 100644 index 00000000..5f82254f --- /dev/null +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/056_placeholder.py @@ -0,0 +1,22 @@ +# 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. + +# This is a placeholder for Juno backports. Do not use this number for new +# Kilo work. New Kilo work starts after all the placeholders. + + +def upgrade(migrate_engine): + pass + + +def downgrade(migration_engine): + pass diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/057_placeholder.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/057_placeholder.py new file mode 100644 index 00000000..5f82254f --- /dev/null +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/057_placeholder.py @@ -0,0 +1,22 @@ +# 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. + +# This is a placeholder for Juno backports. Do not use this number for new +# Kilo work. New Kilo work starts after all the placeholders. + + +def upgrade(migrate_engine): + pass + + +def downgrade(migration_engine): + pass diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/058_placeholder.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/058_placeholder.py new file mode 100644 index 00000000..5f82254f --- /dev/null +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/058_placeholder.py @@ -0,0 +1,22 @@ +# 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. + +# This is a placeholder for Juno backports. Do not use this number for new +# Kilo work. New Kilo work starts after all the placeholders. + + +def upgrade(migrate_engine): + pass + + +def downgrade(migration_engine): + pass diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/059_placeholder.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/059_placeholder.py new file mode 100644 index 00000000..5f82254f --- /dev/null +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/059_placeholder.py @@ -0,0 +1,22 @@ +# 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. + +# This is a placeholder for Juno backports. Do not use this number for new +# Kilo work. New Kilo work starts after all the placeholders. + + +def upgrade(migrate_engine): + pass + + +def downgrade(migration_engine): + pass diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/060_placeholder.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/060_placeholder.py new file mode 100644 index 00000000..5f82254f --- /dev/null +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/060_placeholder.py @@ -0,0 +1,22 @@ +# 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. + +# This is a placeholder for Juno backports. Do not use this number for new +# Kilo work. New Kilo work starts after all the placeholders. + + +def upgrade(migrate_engine): + pass + + +def downgrade(migration_engine): + pass diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/061_add_parent_project.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/061_add_parent_project.py new file mode 100644 index 00000000..bb8ef9f6 --- /dev/null +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/061_add_parent_project.py @@ -0,0 +1,54 @@ +# 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 sqlalchemy as sql + +from keystone.common.sql import migration_helpers + +_PROJECT_TABLE_NAME = 'project' +_PARENT_ID_COLUMN_NAME = 'parent_id' + + +def list_constraints(project_table): + constraints = [{'table': project_table, + 'fk_column': _PARENT_ID_COLUMN_NAME, + 'ref_column': project_table.c.id}] + + return constraints + + +def upgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + + project_table = sql.Table(_PROJECT_TABLE_NAME, meta, autoload=True) + parent_id = sql.Column(_PARENT_ID_COLUMN_NAME, sql.String(64), + nullable=True) + project_table.create_column(parent_id) + + if migrate_engine.name == 'sqlite': + return + migration_helpers.add_constraints(list_constraints(project_table)) + + +def downgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + + project_table = sql.Table(_PROJECT_TABLE_NAME, meta, autoload=True) + + # SQLite does not support constraints, and querying the constraints + # raises an exception + if migrate_engine.name != 'sqlite': + migration_helpers.remove_constraints(list_constraints(project_table)) + + project_table.drop_column(_PARENT_ID_COLUMN_NAME) diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/062_drop_assignment_role_fk.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/062_drop_assignment_role_fk.py new file mode 100644 index 00000000..5a33486c --- /dev/null +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/062_drop_assignment_role_fk.py @@ -0,0 +1,41 @@ +# 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 sqlalchemy + +from keystone.common.sql import migration_helpers + + +def list_constraints(migrate_engine): + meta = sqlalchemy.MetaData() + meta.bind = migrate_engine + assignment_table = sqlalchemy.Table('assignment', meta, autoload=True) + role_table = sqlalchemy.Table('role', meta, autoload=True) + + constraints = [{'table': assignment_table, + 'fk_column': 'role_id', + 'ref_column': role_table.c.id}] + return constraints + + +def upgrade(migrate_engine): + # SQLite does not support constraints, and querying the constraints + # raises an exception + if migrate_engine.name == 'sqlite': + return + migration_helpers.remove_constraints(list_constraints(migrate_engine)) + + +def downgrade(migrate_engine): + if migrate_engine.name == 'sqlite': + return + migration_helpers.add_constraints(list_constraints(migrate_engine)) diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/063_drop_region_auth_url.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/063_drop_region_auth_url.py new file mode 100644 index 00000000..109a8412 --- /dev/null +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/063_drop_region_auth_url.py @@ -0,0 +1,32 @@ +# 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 sqlalchemy as sql + +_REGION_TABLE_NAME = 'region' + + +def upgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + + region_table = sql.Table(_REGION_TABLE_NAME, meta, autoload=True) + region_table.drop_column('url') + + +def downgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + + region_table = sql.Table(_REGION_TABLE_NAME, meta, autoload=True) + url_column = sql.Column('url', sql.String(255), nullable=True) + region_table.create_column(url_column) diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/064_drop_user_and_group_fk.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/064_drop_user_and_group_fk.py new file mode 100644 index 00000000..bca00902 --- /dev/null +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/064_drop_user_and_group_fk.py @@ -0,0 +1,45 @@ +# 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 sqlalchemy + +from keystone.common.sql import migration_helpers + + +def list_constraints(migrate_engine): + meta = sqlalchemy.MetaData() + meta.bind = migrate_engine + user_table = sqlalchemy.Table('user', meta, autoload=True) + group_table = sqlalchemy.Table('group', meta, autoload=True) + domain_table = sqlalchemy.Table('domain', meta, autoload=True) + + constraints = [{'table': user_table, + 'fk_column': 'domain_id', + 'ref_column': domain_table.c.id}, + {'table': group_table, + 'fk_column': 'domain_id', + 'ref_column': domain_table.c.id}] + return constraints + + +def upgrade(migrate_engine): + # SQLite does not support constraints, and querying the constraints + # raises an exception + if migrate_engine.name == 'sqlite': + return + migration_helpers.remove_constraints(list_constraints(migrate_engine)) + + +def downgrade(migrate_engine): + if migrate_engine.name == 'sqlite': + return + migration_helpers.add_constraints(list_constraints(migrate_engine)) diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/065_add_domain_config.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/065_add_domain_config.py new file mode 100644 index 00000000..fd8717d2 --- /dev/null +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/065_add_domain_config.py @@ -0,0 +1,55 @@ +# 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 sqlalchemy as sql + +from keystone.common import sql as ks_sql + +WHITELIST_TABLE = 'whitelisted_config' +SENSITIVE_TABLE = 'sensitive_config' + + +def upgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + + whitelist_table = sql.Table( + WHITELIST_TABLE, + meta, + sql.Column('domain_id', sql.String(64), primary_key=True), + sql.Column('group', sql.String(255), primary_key=True), + sql.Column('option', sql.String(255), primary_key=True), + sql.Column('value', ks_sql.JsonBlob.impl, nullable=False), + mysql_engine='InnoDB', + mysql_charset='utf8') + whitelist_table.create(migrate_engine, checkfirst=True) + + sensitive_table = sql.Table( + SENSITIVE_TABLE, + meta, + sql.Column('domain_id', sql.String(64), primary_key=True), + sql.Column('group', sql.String(255), primary_key=True), + sql.Column('option', sql.String(255), primary_key=True), + sql.Column('value', ks_sql.JsonBlob.impl, nullable=False), + mysql_engine='InnoDB', + mysql_charset='utf8') + sensitive_table.create(migrate_engine, checkfirst=True) + + +def downgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + + table = sql.Table(WHITELIST_TABLE, meta, autoload=True) + table.drop(migrate_engine, checkfirst=True) + table = sql.Table(SENSITIVE_TABLE, meta, autoload=True) + table.drop(migrate_engine, checkfirst=True) diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/066_fixup_service_name_value.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/066_fixup_service_name_value.py new file mode 100644 index 00000000..3feadc53 --- /dev/null +++ b/keystone-moon/keystone/common/sql/migrate_repo/versions/066_fixup_service_name_value.py @@ -0,0 +1,43 @@ +# 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 oslo_serialization import jsonutils +import sqlalchemy as sql + + +def upgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + + service_table = sql.Table('service', meta, autoload=True) + services = list(service_table.select().execute()) + + for service in services: + extra_dict = jsonutils.loads(service.extra) + # Skip records where service is not null + if extra_dict.get('name') is not None: + continue + # Default the name to empty string + extra_dict['name'] = '' + new_values = { + 'extra': jsonutils.dumps(extra_dict), + } + f = service_table.c.id == service.id + update = service_table.update().where(f).values(new_values) + migrate_engine.execute(update) + + +def downgrade(migration_engine): + # The upgrade fixes the data inconsistency for the service name, + # it defaults the value to empty string. There is no necessity + # to revert it. + pass diff --git a/keystone-moon/keystone/common/sql/migrate_repo/versions/__init__.py b/keystone-moon/keystone/common/sql/migrate_repo/versions/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/common/sql/migration_helpers.py b/keystone-moon/keystone/common/sql/migration_helpers.py new file mode 100644 index 00000000..86932995 --- /dev/null +++ b/keystone-moon/keystone/common/sql/migration_helpers.py @@ -0,0 +1,258 @@ +# Copyright 2013 OpenStack Foundation +# Copyright 2013 Red Hat, Inc. +# All Rights Reserved. +# +# 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 os +import sys + +import migrate +from migrate import exceptions +from oslo_config import cfg +from oslo_db.sqlalchemy import migration +from oslo_serialization import jsonutils +from oslo_utils import importutils +import six +import sqlalchemy + +from keystone.common import sql +from keystone.common.sql import migrate_repo +from keystone import contrib +from keystone import exception +from keystone.i18n import _ + + +CONF = cfg.CONF +DEFAULT_EXTENSIONS = ['endpoint_filter', + 'endpoint_policy', + 'federation', + 'oauth1', + 'revoke', + ] + + +def get_default_domain(): + # Return the reference used for the default domain structure during + # sql migrations. + return { + 'id': CONF.identity.default_domain_id, + 'name': 'Default', + 'enabled': True, + 'extra': jsonutils.dumps({'description': 'Owns users and tenants ' + '(i.e. projects) available ' + 'on Identity API v2.'})} + + +# Different RDBMSs use different schemes for naming the Foreign Key +# Constraints. SQLAlchemy does not yet attempt to determine the name +# for the constraint, and instead attempts to deduce it from the column. +# This fails on MySQL. +def get_constraints_names(table, column_name): + fkeys = [fk.name for fk in table.constraints + if (isinstance(fk, sqlalchemy.ForeignKeyConstraint) and + column_name in fk.columns)] + return fkeys + + +# remove_constraints and add_constraints both accept a list of dictionaries +# that contain: +# {'table': a sqlalchemy table. The constraint is added to dropped from +# this table. +# 'fk_column': the name of a column on the above table, The constraint +# is added to or dropped from this column +# 'ref_column':a sqlalchemy column object. This is the reference column +# for the constraint. +def remove_constraints(constraints): + for constraint_def in constraints: + constraint_names = get_constraints_names(constraint_def['table'], + constraint_def['fk_column']) + for constraint_name in constraint_names: + migrate.ForeignKeyConstraint( + columns=[getattr(constraint_def['table'].c, + constraint_def['fk_column'])], + refcolumns=[constraint_def['ref_column']], + name=constraint_name).drop() + + +def add_constraints(constraints): + for constraint_def in constraints: + + if constraint_def['table'].kwargs.get('mysql_engine') == 'MyISAM': + # Don't try to create constraint when using MyISAM because it's + # not supported. + continue + + ref_col = constraint_def['ref_column'] + ref_engine = ref_col.table.kwargs.get('mysql_engine') + if ref_engine == 'MyISAM': + # Don't try to create constraint when using MyISAM because it's + # not supported. + continue + + migrate.ForeignKeyConstraint( + columns=[getattr(constraint_def['table'].c, + constraint_def['fk_column'])], + refcolumns=[constraint_def['ref_column']]).create() + + +def rename_tables_with_constraints(renames, constraints, engine): + """Renames tables with foreign key constraints. + + Tables are renamed after first removing constraints. The constraints are + replaced after the rename is complete. + + This works on databases that don't support renaming tables that have + constraints on them (DB2). + + `renames` is a dict, mapping {'to_table_name': from_table, ...} + """ + + if engine.name != 'sqlite': + # Sqlite doesn't support constraints, so nothing to remove. + remove_constraints(constraints) + + for to_table_name in renames: + from_table = renames[to_table_name] + from_table.rename(to_table_name) + + if engine != 'sqlite': + add_constraints(constraints) + + +def find_migrate_repo(package=None, repo_name='migrate_repo'): + package = package or sql + path = os.path.abspath(os.path.join( + os.path.dirname(package.__file__), repo_name)) + if os.path.isdir(path): + return path + raise exception.MigrationNotProvided(package.__name__, path) + + +def _sync_common_repo(version): + abs_path = find_migrate_repo() + init_version = migrate_repo.DB_INIT_VERSION + engine = sql.get_engine() + migration.db_sync(engine, abs_path, version=version, + init_version=init_version) + + +def _fix_federation_tables(engine): + """Fix the identity_provider, federation_protocol and mapping tables + to be InnoDB and Charset UTF8. + + This function is to work around bug #1426334. This has occurred because + the original migration did not specify InnoDB and charset utf8. Due + to the sanity_check, a deployer can get wedged here and require manual + database changes to fix. + """ + # NOTE(marco-fargetta) This is a workaround to "fix" that tables only + # if we're under MySQL + if engine.name == 'mysql': + # * Disable any check for the foreign keys because they prevent the + # alter table to execute + engine.execute("SET foreign_key_checks = 0") + # * Make the tables using InnoDB engine + engine.execute("ALTER TABLE identity_provider Engine=InnoDB") + engine.execute("ALTER TABLE federation_protocol Engine=InnoDB") + engine.execute("ALTER TABLE mapping Engine=InnoDB") + # * Make the tables using utf8 encoding + engine.execute("ALTER TABLE identity_provider " + "CONVERT TO CHARACTER SET utf8") + engine.execute("ALTER TABLE federation_protocol " + "CONVERT TO CHARACTER SET utf8") + engine.execute("ALTER TABLE mapping CONVERT TO CHARACTER SET utf8") + # * Revert the foreign keys check back + engine.execute("SET foreign_key_checks = 1") + + +def _sync_extension_repo(extension, version): + init_version = 0 + engine = sql.get_engine() + + try: + package_name = '.'.join((contrib.__name__, extension)) + package = importutils.import_module(package_name) + except ImportError: + raise ImportError(_("%s extension does not exist.") + % package_name) + try: + abs_path = find_migrate_repo(package) + try: + migration.db_version_control(sql.get_engine(), abs_path) + # Register the repo with the version control API + # If it already knows about the repo, it will throw + # an exception that we can safely ignore + except exceptions.DatabaseAlreadyControlledError: + pass + except exception.MigrationNotProvided as e: + print(e) + sys.exit(1) + try: + migration.db_sync(engine, abs_path, version=version, + init_version=init_version) + except ValueError: + # NOTE(marco-fargetta): ValueError is raised from the sanity check ( + # verifies that tables are utf8 under mysql). The federation_protocol, + # identity_provider and mapping tables were not initially built with + # InnoDB and utf8 as part of the table arguments when the migration + # was initially created. Bug #1426334 is a scenario where the deployer + # can get wedged, unable to upgrade or downgrade. + # This is a workaround to "fix" those tables if we're under MySQL and + # the version is before the 6 because before the tables were introduced + # before and patched when migration 5 was available + if engine.name == 'mysql' and \ + int(six.text_type(get_db_version(extension))) < 6: + _fix_federation_tables(engine) + # The migration is applied again after the fix + migration.db_sync(engine, abs_path, version=version, + init_version=init_version) + else: + raise + + +def sync_database_to_version(extension=None, version=None): + if not extension: + _sync_common_repo(version) + # If version is greater than 0, it is for the common + # repository only, and only that will be synchronized. + if version is None: + for default_extension in DEFAULT_EXTENSIONS: + _sync_extension_repo(default_extension, version) + else: + _sync_extension_repo(extension, version) + + +def get_db_version(extension=None): + if not extension: + return migration.db_version(sql.get_engine(), find_migrate_repo(), + migrate_repo.DB_INIT_VERSION) + + try: + package_name = '.'.join((contrib.__name__, extension)) + package = importutils.import_module(package_name) + except ImportError: + raise ImportError(_("%s extension does not exist.") + % package_name) + + return migration.db_version( + sql.get_engine(), find_migrate_repo(package), 0) + + +def print_db_version(extension=None): + try: + db_version = get_db_version(extension=extension) + print(db_version) + except exception.MigrationNotProvided as e: + print(e) + sys.exit(1) diff --git a/keystone-moon/keystone/common/utils.py b/keystone-moon/keystone/common/utils.py new file mode 100644 index 00000000..a4b03ffd --- /dev/null +++ b/keystone-moon/keystone/common/utils.py @@ -0,0 +1,471 @@ +# Copyright 2012 OpenStack Foundation +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# Copyright 2011 - 2012 Justin Santa Barbara +# All Rights Reserved. +# +# 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 calendar +import collections +import grp +import hashlib +import os +import pwd + +from oslo_config import cfg +from oslo_log import log +from oslo_serialization import jsonutils +from oslo_utils import strutils +import passlib.hash +import six +from six import moves + +from keystone import exception +from keystone.i18n import _, _LE, _LW + + +CONF = cfg.CONF + +LOG = log.getLogger(__name__) + + +def flatten_dict(d, parent_key=''): + """Flatten a nested dictionary + + Converts a dictionary with nested values to a single level flat + dictionary, with dotted notation for each key. + + """ + items = [] + for k, v in d.items(): + new_key = parent_key + '.' + k if parent_key else k + if isinstance(v, collections.MutableMapping): + items.extend(flatten_dict(v, new_key).items()) + else: + items.append((new_key, v)) + return dict(items) + + +def read_cached_file(filename, cache_info, reload_func=None): + """Read from a file if it has been modified. + + :param cache_info: dictionary to hold opaque cache. + :param reload_func: optional function to be called with data when + file is reloaded due to a modification. + + :returns: data from file. + + """ + mtime = os.path.getmtime(filename) + if not cache_info or mtime != cache_info.get('mtime'): + with open(filename) as fap: + cache_info['data'] = fap.read() + cache_info['mtime'] = mtime + if reload_func: + reload_func(cache_info['data']) + return cache_info['data'] + + +class SmarterEncoder(jsonutils.json.JSONEncoder): + """Help for JSON encoding dict-like objects.""" + def default(self, obj): + if not isinstance(obj, dict) and hasattr(obj, 'iteritems'): + return dict(obj.iteritems()) + return super(SmarterEncoder, self).default(obj) + + +class PKIEncoder(SmarterEncoder): + """Special encoder to make token JSON a bit shorter.""" + item_separator = ',' + key_separator = ':' + + +def verify_length_and_trunc_password(password): + """Verify and truncate the provided password to the max_password_length.""" + max_length = CONF.identity.max_password_length + try: + if len(password) > max_length: + if CONF.strict_password_check: + raise exception.PasswordVerificationError(size=max_length) + else: + LOG.warning( + _LW('Truncating user password to ' + '%d characters.'), max_length) + return password[:max_length] + else: + return password + except TypeError: + raise exception.ValidationError(attribute='string', target='password') + + +def hash_access_key(access): + hash_ = hashlib.sha256() + hash_.update(access) + return hash_.hexdigest() + + +def hash_user_password(user): + """Hash a user dict's password without modifying the passed-in dict.""" + password = user.get('password') + if password is None: + return user + + return dict(user, password=hash_password(password)) + + +def hash_password(password): + """Hash a password. Hard.""" + password_utf8 = verify_length_and_trunc_password(password).encode('utf-8') + return passlib.hash.sha512_crypt.encrypt( + password_utf8, rounds=CONF.crypt_strength) + + +def check_password(password, hashed): + """Check that a plaintext password matches hashed. + + hashpw returns the salt value concatenated with the actual hash value. + It extracts the actual salt if this value is then passed as the salt. + + """ + if password is None or hashed is None: + return False + password_utf8 = verify_length_and_trunc_password(password).encode('utf-8') + return passlib.hash.sha512_crypt.verify(password_utf8, hashed) + + +def attr_as_boolean(val_attr): + """Returns the boolean value, decoded from a string. + + We test explicitly for a value meaning False, which can be one of + several formats as specified in oslo strutils.FALSE_STRINGS. + All other string values (including an empty string) are treated as + meaning True. + + """ + return strutils.bool_from_string(val_attr, default=True) + + +def get_blob_from_credential(credential): + try: + blob = jsonutils.loads(credential.blob) + except (ValueError, TypeError): + raise exception.ValidationError( + message=_('Invalid blob in credential')) + if not blob or not isinstance(blob, dict): + raise exception.ValidationError(attribute='blob', + target='credential') + return blob + + +def convert_ec2_to_v3_credential(ec2credential): + blob = {'access': ec2credential.access, + 'secret': ec2credential.secret} + return {'id': hash_access_key(ec2credential.access), + 'user_id': ec2credential.user_id, + 'project_id': ec2credential.tenant_id, + 'blob': jsonutils.dumps(blob), + 'type': 'ec2', + 'extra': jsonutils.dumps({})} + + +def convert_v3_to_ec2_credential(credential): + blob = get_blob_from_credential(credential) + return {'access': blob.get('access'), + 'secret': blob.get('secret'), + 'user_id': credential.user_id, + 'tenant_id': credential.project_id, + } + + +def unixtime(dt_obj): + """Format datetime object as unix timestamp + + :param dt_obj: datetime.datetime object + :returns: float + + """ + return calendar.timegm(dt_obj.utctimetuple()) + + +def auth_str_equal(provided, known): + """Constant-time string comparison. + + :params provided: the first string + :params known: the second string + + :return: True if the strings are equal. + + This function takes two strings and compares them. It is intended to be + used when doing a comparison for authentication purposes to help guard + against timing attacks. When using the function for this purpose, always + provide the user-provided password as the first argument. The time this + function will take is always a factor of the length of this string. + """ + result = 0 + p_len = len(provided) + k_len = len(known) + for i in moves.range(p_len): + a = ord(provided[i]) if i < p_len else 0 + b = ord(known[i]) if i < k_len else 0 + result |= a ^ b + return (p_len == k_len) & (result == 0) + + +def setup_remote_pydev_debug(): + if CONF.pydev_debug_host and CONF.pydev_debug_port: + try: + try: + from pydev import pydevd + except ImportError: + import pydevd + + pydevd.settrace(CONF.pydev_debug_host, + port=CONF.pydev_debug_port, + stdoutToServer=True, + stderrToServer=True) + return True + except Exception: + LOG.exception(_LE( + 'Error setting up the debug environment. Verify that the ' + 'option --debug-url has the format : and that a ' + 'debugger processes is listening on that port.')) + raise + + +def get_unix_user(user=None): + '''Get the uid and user name. + + This is a convenience utility which accepts a variety of input + which might represent a unix user. If successful it returns the uid + and name. Valid input is: + + string + A string is first considered to be a user name and a lookup is + attempted under that name. If no name is found then an attempt + is made to convert the string to an integer and perform a + lookup as a uid. + + int + An integer is interpretted as a uid. + + None + None is interpreted to mean use the current process's + effective user. + + If the input is a valid type but no user is found a KeyError is + raised. If the input is not a valid type a TypeError is raised. + + :param object user: string, int or None specifying the user to + lookup. + + :return: tuple of (uid, name) + ''' + + if isinstance(user, six.string_types): + try: + user_info = pwd.getpwnam(user) + except KeyError: + try: + i = int(user) + except ValueError: + raise KeyError("user name '%s' not found" % user) + try: + user_info = pwd.getpwuid(i) + except KeyError: + raise KeyError("user id %d not found" % i) + elif isinstance(user, int): + try: + user_info = pwd.getpwuid(user) + except KeyError: + raise KeyError("user id %d not found" % user) + elif user is None: + user_info = pwd.getpwuid(os.geteuid()) + else: + raise TypeError('user must be string, int or None; not %s (%r)' % + (user.__class__.__name__, user)) + + return user_info.pw_uid, user_info.pw_name + + +def get_unix_group(group=None): + '''Get the gid and group name. + + This is a convenience utility which accepts a variety of input + which might represent a unix group. If successful it returns the gid + and name. Valid input is: + + string + A string is first considered to be a group name and a lookup is + attempted under that name. If no name is found then an attempt + is made to convert the string to an integer and perform a + lookup as a gid. + + int + An integer is interpretted as a gid. + + None + None is interpreted to mean use the current process's + effective group. + + If the input is a valid type but no group is found a KeyError is + raised. If the input is not a valid type a TypeError is raised. + + + :param object group: string, int or None specifying the group to + lookup. + + :return: tuple of (gid, name) + ''' + + if isinstance(group, six.string_types): + try: + group_info = grp.getgrnam(group) + except KeyError: + # Was an int passed as a string? + # Try converting to int and lookup by id instead. + try: + i = int(group) + except ValueError: + raise KeyError("group name '%s' not found" % group) + try: + group_info = grp.getgrgid(i) + except KeyError: + raise KeyError("group id %d not found" % i) + elif isinstance(group, int): + try: + group_info = grp.getgrgid(group) + except KeyError: + raise KeyError("group id %d not found" % group) + elif group is None: + group_info = grp.getgrgid(os.getegid()) + else: + raise TypeError('group must be string, int or None; not %s (%r)' % + (group.__class__.__name__, group)) + + return group_info.gr_gid, group_info.gr_name + + +def set_permissions(path, mode=None, user=None, group=None, log=None): + '''Set the ownership and permissions on the pathname. + + Each of the mode, user and group are optional, if None then + that aspect is not modified. + + Owner and group may be specified either with a symbolic name + or numeric id. + + :param string path: Pathname of directory whose existence is assured. + :param object mode: ownership permissions flags (int) i.e. chmod, + if None do not set. + :param object user: set user, name (string) or uid (integer), + if None do not set. + :param object group: set group, name (string) or gid (integer) + if None do not set. + :param logger log: logging.logger object, used to emit log messages, + if None no logging is performed. + ''' + + if user is None: + user_uid, user_name = None, None + else: + user_uid, user_name = get_unix_user(user) + + if group is None: + group_gid, group_name = None, None + else: + group_gid, group_name = get_unix_group(group) + + if log: + if mode is None: + mode_string = str(mode) + else: + mode_string = oct(mode) + log.debug("set_permissions: " + "path='%s' mode=%s user=%s(%s) group=%s(%s)", + path, mode_string, + user_name, user_uid, group_name, group_gid) + + # Change user and group if specified + if user_uid is not None or group_gid is not None: + if user_uid is None: + user_uid = -1 + if group_gid is None: + group_gid = -1 + try: + os.chown(path, user_uid, group_gid) + except OSError as exc: + raise EnvironmentError("chown('%s', %s, %s): %s" % + (path, + user_name, group_name, + exc.strerror)) + + # Change permission flags + if mode is not None: + try: + os.chmod(path, mode) + except OSError as exc: + raise EnvironmentError("chmod('%s', %#o): %s" % + (path, mode, exc.strerror)) + + +def make_dirs(path, mode=None, user=None, group=None, log=None): + '''Assure directory exists, set ownership and permissions. + + Assure the directory exists and optionally set its ownership + and permissions. + + Each of the mode, user and group are optional, if None then + that aspect is not modified. + + Owner and group may be specified either with a symbolic name + or numeric id. + + :param string path: Pathname of directory whose existence is assured. + :param object mode: ownership permissions flags (int) i.e. chmod, + if None do not set. + :param object user: set user, name (string) or uid (integer), + if None do not set. + :param object group: set group, name (string) or gid (integer) + if None do not set. + :param logger log: logging.logger object, used to emit log messages, + if None no logging is performed. + ''' + + if log: + if mode is None: + mode_string = str(mode) + else: + mode_string = oct(mode) + log.debug("make_dirs path='%s' mode=%s user=%s group=%s", + path, mode_string, user, group) + + if not os.path.exists(path): + try: + os.makedirs(path) + except OSError as exc: + raise EnvironmentError("makedirs('%s'): %s" % (path, exc.strerror)) + + set_permissions(path, mode, user, group, log) + + +class WhiteListedItemFilter(object): + + def __init__(self, whitelist, data): + self._whitelist = set(whitelist or []) + self._data = data + + def __getitem__(self, name): + if name not in self._whitelist: + raise KeyError + return self._data[name] diff --git a/keystone-moon/keystone/common/validation/__init__.py b/keystone-moon/keystone/common/validation/__init__.py new file mode 100644 index 00000000..f9c58eaf --- /dev/null +++ b/keystone-moon/keystone/common/validation/__init__.py @@ -0,0 +1,62 @@ +# 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. +"""Request body validating middleware for OpenStack Identity resources.""" + +import functools + +from keystone.common.validation import validators + + +def validated(request_body_schema, resource_to_validate): + """Register a schema to validate a resource reference. + + Registered schema will be used for validating a request body just before + API method execution. + + :param request_body_schema: a schema to validate the resource reference + :param resource_to_validate: the reference to validate + + """ + schema_validator = validators.SchemaValidator(request_body_schema) + + def add_validator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + if resource_to_validate in kwargs: + schema_validator.validate(kwargs[resource_to_validate]) + return func(*args, **kwargs) + return wrapper + return add_validator + + +def nullable(property_schema): + """Clone a property schema into one that is nullable. + + :param dict property_schema: schema to clone into a nullable schema + :returns: a new dict schema + """ + # TODO(dstanek): deal with the case where type is already a list; we don't + # do that yet so I'm not wasting time on it + new_schema = property_schema.copy() + new_schema['type'] = [property_schema['type'], 'null'] + return new_schema + + +def add_array_type(property_schema): + """Convert the parameter schema to be of type list. + + :param dict property_schema: schema to add array type to + :returns: a new dict schema + """ + new_schema = property_schema.copy() + new_schema['type'] = [property_schema['type'], 'array'] + return new_schema diff --git a/keystone-moon/keystone/common/validation/parameter_types.py b/keystone-moon/keystone/common/validation/parameter_types.py new file mode 100644 index 00000000..c5908836 --- /dev/null +++ b/keystone-moon/keystone/common/validation/parameter_types.py @@ -0,0 +1,57 @@ +# 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. +"""Common parameter types for validating a request reference.""" + +boolean = { + 'type': 'boolean', + 'enum': [True, False] +} + +# NOTE(lbragstad): Be mindful of this pattern as it might require changes +# once this is used on user names, LDAP-based user names specifically since +# commas aren't allowed in the following pattern. Here we are only going to +# check the length of the name and ensure that it's a string. Right now we are +# not going to validate on a naming pattern for issues with +# internationalization. +name = { + 'type': 'string', + 'minLength': 1, + 'maxLength': 255 +} + +id_string = { + 'type': 'string', + 'minLength': 1, + 'maxLength': 64, + # TODO(lbragstad): Find a way to make this configurable such that the end + # user chooses how much control they want over id_strings with a regex + 'pattern': '^[a-zA-Z0-9-]+$' +} + +description = { + 'type': 'string' +} + +url = { + 'type': 'string', + 'minLength': 0, + 'maxLength': 225, + # NOTE(edmondsw): we could do more to validate per various RFCs, but + # decision was made to err on the side of leniency. The following is based + # on rfc1738 section 2.1 + 'pattern': '[a-zA-Z0-9+.-]+:.+' +} + +email = { + 'type': 'string', + 'format': 'email' +} diff --git a/keystone-moon/keystone/common/validation/validators.py b/keystone-moon/keystone/common/validation/validators.py new file mode 100644 index 00000000..a4574176 --- /dev/null +++ b/keystone-moon/keystone/common/validation/validators.py @@ -0,0 +1,59 @@ +# 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. +"""Internal implementation of request body validating middleware.""" + +import jsonschema + +from keystone import exception +from keystone.i18n import _ + + +class SchemaValidator(object): + """Resource reference validator class.""" + + validator = None + validator_org = jsonschema.Draft4Validator + + def __init__(self, schema): + # NOTE(lbragstad): If at some point in the future we want to extend + # our validators to include something specific we need to check for, + # we can do it here. Nova's V3 API validators extend the validator to + # include `self._validate_minimum` and `self._validate_maximum`. This + # would be handy if we needed to check for something the jsonschema + # didn't by default. See the Nova V3 validator for details on how this + # is done. + validators = {} + validator_cls = jsonschema.validators.extend(self.validator_org, + validators) + fc = jsonschema.FormatChecker() + self.validator = validator_cls(schema, format_checker=fc) + + def validate(self, *args, **kwargs): + try: + self.validator.validate(*args, **kwargs) + except jsonschema.ValidationError as ex: + # NOTE: For whole OpenStack message consistency, this error + # message has been written in a format consistent with WSME. + if len(ex.path) > 0: + # NOTE(lbragstad): Here we could think about using iter_errors + # as a method of providing invalid parameters back to the + # user. + # TODO(lbragstad): If the value of a field is confidential or + # too long, then we should build the masking in here so that + # we don't expose sensitive user information in the event it + # fails validation. + detail = _("Invalid input for field '%(path)s'. The value is " + "'%(value)s'.") % {'path': ex.path.pop(), + 'value': ex.instance} + else: + detail = ex.message + raise exception.SchemaValidationError(detail=detail) diff --git a/keystone-moon/keystone/common/wsgi.py b/keystone-moon/keystone/common/wsgi.py new file mode 100644 index 00000000..6ee8150d --- /dev/null +++ b/keystone-moon/keystone/common/wsgi.py @@ -0,0 +1,830 @@ +# Copyright 2012 OpenStack Foundation +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# Copyright 2010 OpenStack Foundation +# All Rights Reserved. +# +# 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. + +"""Utility methods for working with WSGI servers.""" + +import copy +import itertools +import urllib + +from oslo_config import cfg +import oslo_i18n +from oslo_log import log +from oslo_serialization import jsonutils +from oslo_utils import importutils +from oslo_utils import strutils +import routes.middleware +import six +import webob.dec +import webob.exc + +from keystone.common import dependency +from keystone.common import json_home +from keystone.common import utils +from keystone import exception +from keystone.i18n import _ +from keystone.i18n import _LI +from keystone.i18n import _LW +from keystone.models import token_model + + +CONF = cfg.CONF +LOG = log.getLogger(__name__) + +# Environment variable used to pass the request context +CONTEXT_ENV = 'openstack.context' + + +# Environment variable used to pass the request params +PARAMS_ENV = 'openstack.params' + + +def validate_token_bind(context, token_ref): + bind_mode = CONF.token.enforce_token_bind + + if bind_mode == 'disabled': + return + + if not isinstance(token_ref, token_model.KeystoneToken): + raise exception.UnexpectedError(_('token reference must be a ' + 'KeystoneToken type, got: %s') % + type(token_ref)) + bind = token_ref.bind + + # permissive and strict modes don't require there to be a bind + permissive = bind_mode in ('permissive', 'strict') + + # get the named mode if bind_mode is not one of the known + name = None if permissive or bind_mode == 'required' else bind_mode + + if not bind: + if permissive: + # no bind provided and none required + return + else: + LOG.info(_LI("No bind information present in token")) + raise exception.Unauthorized() + + if name and name not in bind: + LOG.info(_LI("Named bind mode %s not in bind information"), name) + raise exception.Unauthorized() + + for bind_type, identifier in six.iteritems(bind): + if bind_type == 'kerberos': + if not (context['environment'].get('AUTH_TYPE', '').lower() + == 'negotiate'): + LOG.info(_LI("Kerberos credentials required and not present")) + raise exception.Unauthorized() + + if not context['environment'].get('REMOTE_USER') == identifier: + LOG.info(_LI("Kerberos credentials do not match " + "those in bind")) + raise exception.Unauthorized() + + LOG.info(_LI("Kerberos bind authentication successful")) + + elif bind_mode == 'permissive': + LOG.debug(("Ignoring unknown bind for permissive mode: " + "{%(bind_type)s: %(identifier)s}"), + {'bind_type': bind_type, 'identifier': identifier}) + else: + LOG.info(_LI("Couldn't verify unknown bind: " + "{%(bind_type)s: %(identifier)s}"), + {'bind_type': bind_type, 'identifier': identifier}) + raise exception.Unauthorized() + + +def best_match_language(req): + """Determines the best available locale from the Accept-Language + HTTP header passed in the request. + """ + + if not req.accept_language: + return None + return req.accept_language.best_match( + oslo_i18n.get_available_languages('keystone')) + + +class BaseApplication(object): + """Base WSGI application wrapper. Subclasses need to implement __call__.""" + + @classmethod + def factory(cls, global_config, **local_config): + """Used for paste app factories in paste.deploy config files. + + Any local configuration (that is, values under the [app:APPNAME] + section of the paste config) will be passed into the `__init__` method + as kwargs. + + A hypothetical configuration would look like: + + [app:wadl] + latest_version = 1.3 + paste.app_factory = keystone.fancy_api:Wadl.factory + + which would result in a call to the `Wadl` class as + + import keystone.fancy_api + keystone.fancy_api.Wadl(latest_version='1.3') + + You could of course re-implement the `factory` method in subclasses, + but using the kwarg passing it shouldn't be necessary. + + """ + return cls(**local_config) + + def __call__(self, environ, start_response): + r"""Subclasses will probably want to implement __call__ like this: + + @webob.dec.wsgify() + def __call__(self, req): + # Any of the following objects work as responses: + + # Option 1: simple string + res = 'message\n' + + # Option 2: a nicely formatted HTTP exception page + res = exc.HTTPForbidden(explanation='Nice try') + + # Option 3: a webob Response object (in case you need to play with + # headers, or you want to be treated like an iterable, or or or) + res = Response(); + res.app_iter = open('somefile') + + # Option 4: any wsgi app to be run next + res = self.application + + # Option 5: you can get a Response object for a wsgi app, too, to + # play with headers etc + res = req.get_response(self.application) + + # You can then just return your response... + return res + # ... or set req.response and return None. + req.response = res + + See the end of http://pythonpaste.org/webob/modules/dec.html + for more info. + + """ + raise NotImplementedError('You must implement __call__') + + +@dependency.requires('assignment_api', 'policy_api', 'token_provider_api') +class Application(BaseApplication): + @webob.dec.wsgify() + def __call__(self, req): + arg_dict = req.environ['wsgiorg.routing_args'][1] + action = arg_dict.pop('action') + del arg_dict['controller'] + + # allow middleware up the stack to provide context, params and headers. + context = req.environ.get(CONTEXT_ENV, {}) + context['query_string'] = dict(six.iteritems(req.params)) + context['headers'] = dict(six.iteritems(req.headers)) + context['path'] = req.environ['PATH_INFO'] + scheme = (None if not CONF.secure_proxy_ssl_header + else req.environ.get(CONF.secure_proxy_ssl_header)) + if scheme: + # NOTE(andrey-mp): "wsgi.url_scheme" contains the protocol used + # before the proxy removed it ('https' usually). So if + # the webob.Request instance is modified in order to use this + # scheme instead of the one defined by API, the call to + # webob.Request.relative_url() will return a URL with the correct + # scheme. + req.environ['wsgi.url_scheme'] = scheme + context['host_url'] = req.host_url + params = req.environ.get(PARAMS_ENV, {}) + # authentication and authorization attributes are set as environment + # values by the container and processed by the pipeline. the complete + # set is not yet know. + context['environment'] = req.environ + context['accept_header'] = req.accept + req.environ = None + + params.update(arg_dict) + + context.setdefault('is_admin', False) + + # TODO(termie): do some basic normalization on methods + method = getattr(self, action) + + # NOTE(morganfainberg): use the request method to normalize the + # response code between GET and HEAD requests. The HTTP status should + # be the same. + req_method = req.environ['REQUEST_METHOD'].upper() + LOG.info('%(req_method)s %(path)s?%(params)s', { + 'req_method': req_method, + 'path': context['path'], + 'params': urllib.urlencode(req.params)}) + + params = self._normalize_dict(params) + + try: + result = method(context, **params) + except exception.Unauthorized as e: + LOG.warning( + _LW("Authorization failed. %(exception)s from " + "%(remote_addr)s"), + {'exception': e, 'remote_addr': req.environ['REMOTE_ADDR']}) + return render_exception(e, context=context, + user_locale=best_match_language(req)) + except exception.Error as e: + LOG.warning(six.text_type(e)) + return render_exception(e, context=context, + user_locale=best_match_language(req)) + except TypeError as e: + LOG.exception(six.text_type(e)) + return render_exception(exception.ValidationError(e), + context=context, + user_locale=best_match_language(req)) + except Exception as e: + LOG.exception(six.text_type(e)) + return render_exception(exception.UnexpectedError(exception=e), + context=context, + user_locale=best_match_language(req)) + + if result is None: + return render_response(status=(204, 'No Content')) + elif isinstance(result, six.string_types): + return result + elif isinstance(result, webob.Response): + return result + elif isinstance(result, webob.exc.WSGIHTTPException): + return result + + response_code = self._get_response_code(req) + return render_response(body=result, status=response_code, + method=req_method) + + def _get_response_code(self, req): + req_method = req.environ['REQUEST_METHOD'] + controller = importutils.import_class('keystone.common.controller') + code = None + if isinstance(self, controller.V3Controller) and req_method == 'POST': + code = (201, 'Created') + return code + + def _normalize_arg(self, arg): + return arg.replace(':', '_').replace('-', '_') + + def _normalize_dict(self, d): + return {self._normalize_arg(k): v for (k, v) in six.iteritems(d)} + + def assert_admin(self, context): + if not context['is_admin']: + try: + user_token_ref = token_model.KeystoneToken( + token_id=context['token_id'], + token_data=self.token_provider_api.validate_token( + context['token_id'])) + except exception.TokenNotFound as e: + raise exception.Unauthorized(e) + + validate_token_bind(context, user_token_ref) + creds = copy.deepcopy(user_token_ref.metadata) + + try: + creds['user_id'] = user_token_ref.user_id + except exception.UnexpectedError: + LOG.debug('Invalid user') + raise exception.Unauthorized() + + if user_token_ref.project_scoped: + creds['tenant_id'] = user_token_ref.project_id + else: + LOG.debug('Invalid tenant') + raise exception.Unauthorized() + + creds['roles'] = user_token_ref.role_names + # Accept either is_admin or the admin role + self.policy_api.enforce(creds, 'admin_required', {}) + + def _attribute_is_empty(self, ref, attribute): + """Returns true if the attribute in the given ref (which is a + dict) is empty or None. + """ + return ref.get(attribute) is None or ref.get(attribute) == '' + + def _require_attribute(self, ref, attribute): + """Ensures the reference contains the specified attribute. + + Raise a ValidationError if the given attribute is not present + """ + if self._attribute_is_empty(ref, attribute): + msg = _('%s field is required and cannot be empty') % attribute + raise exception.ValidationError(message=msg) + + def _require_attributes(self, ref, attrs): + """Ensures the reference contains the specified attributes. + + Raise a ValidationError if any of the given attributes is not present + """ + missing_attrs = [attribute for attribute in attrs + if self._attribute_is_empty(ref, attribute)] + + if missing_attrs: + msg = _('%s field(s) cannot be empty') % ', '.join(missing_attrs) + raise exception.ValidationError(message=msg) + + def _get_trust_id_for_request(self, context): + """Get the trust_id for a call. + + Retrieve the trust_id from the token + Returns None if token is not trust scoped + """ + if ('token_id' not in context or + context.get('token_id') == CONF.admin_token): + LOG.debug(('will not lookup trust as the request auth token is ' + 'either absent or it is the system admin token')) + return None + + try: + token_data = self.token_provider_api.validate_token( + context['token_id']) + except exception.TokenNotFound: + LOG.warning(_LW('Invalid token in _get_trust_id_for_request')) + raise exception.Unauthorized() + + token_ref = token_model.KeystoneToken(token_id=context['token_id'], + token_data=token_data) + return token_ref.trust_id + + @classmethod + def base_url(cls, context, endpoint_type): + url = CONF['%s_endpoint' % endpoint_type] + + if url: + substitutions = dict( + itertools.chain(six.iteritems(CONF), + six.iteritems(CONF.eventlet_server))) + + url = url % substitutions + else: + # NOTE(jamielennox): if url is not set via the config file we + # should set it relative to the url that the user used to get here + # so as not to mess with version discovery. This is not perfect. + # host_url omits the path prefix, but there isn't another good + # solution that will work for all urls. + url = context['host_url'] + + return url.rstrip('/') + + +class Middleware(Application): + """Base WSGI middleware. + + These classes require an application to be + initialized that will be called next. By default the middleware will + simply call its wrapped app, or you can override __call__ to customize its + behavior. + + """ + + @classmethod + def factory(cls, global_config, **local_config): + """Used for paste app factories in paste.deploy config files. + + Any local configuration (that is, values under the [filter:APPNAME] + section of the paste config) will be passed into the `__init__` method + as kwargs. + + A hypothetical configuration would look like: + + [filter:analytics] + redis_host = 127.0.0.1 + paste.filter_factory = keystone.analytics:Analytics.factory + + which would result in a call to the `Analytics` class as + + import keystone.analytics + keystone.analytics.Analytics(app, redis_host='127.0.0.1') + + You could of course re-implement the `factory` method in subclasses, + but using the kwarg passing it shouldn't be necessary. + + """ + def _factory(app): + conf = global_config.copy() + conf.update(local_config) + return cls(app, **local_config) + return _factory + + def __init__(self, application): + super(Middleware, self).__init__() + self.application = application + + def process_request(self, request): + """Called on each request. + + If this returns None, the next application down the stack will be + executed. If it returns a response then that response will be returned + and execution will stop here. + + """ + return None + + def process_response(self, request, response): + """Do whatever you'd like to the response, based on the request.""" + return response + + @webob.dec.wsgify() + def __call__(self, request): + try: + response = self.process_request(request) + if response: + return response + response = request.get_response(self.application) + return self.process_response(request, response) + except exception.Error as e: + LOG.warning(six.text_type(e)) + return render_exception(e, request=request, + user_locale=best_match_language(request)) + except TypeError as e: + LOG.exception(six.text_type(e)) + return render_exception(exception.ValidationError(e), + request=request, + user_locale=best_match_language(request)) + except Exception as e: + LOG.exception(six.text_type(e)) + return render_exception(exception.UnexpectedError(exception=e), + request=request, + user_locale=best_match_language(request)) + + +class Debug(Middleware): + """Helper class for debugging a WSGI application. + + Can be inserted into any WSGI application chain to get information + about the request and response. + + """ + + @webob.dec.wsgify() + def __call__(self, req): + if not hasattr(LOG, 'isEnabledFor') or LOG.isEnabledFor(LOG.debug): + LOG.debug('%s %s %s', ('*' * 20), 'REQUEST ENVIRON', ('*' * 20)) + for key, value in req.environ.items(): + LOG.debug('%s = %s', key, + strutils.mask_password(value)) + LOG.debug('') + LOG.debug('%s %s %s', ('*' * 20), 'REQUEST BODY', ('*' * 20)) + for line in req.body_file: + LOG.debug('%s', strutils.mask_password(line)) + LOG.debug('') + + resp = req.get_response(self.application) + if not hasattr(LOG, 'isEnabledFor') or LOG.isEnabledFor(LOG.debug): + LOG.debug('%s %s %s', ('*' * 20), 'RESPONSE HEADERS', ('*' * 20)) + for (key, value) in six.iteritems(resp.headers): + LOG.debug('%s = %s', key, value) + LOG.debug('') + + resp.app_iter = self.print_generator(resp.app_iter) + + return resp + + @staticmethod + def print_generator(app_iter): + """Iterator that prints the contents of a wrapper string.""" + LOG.debug('%s %s %s', ('*' * 20), 'RESPONSE BODY', ('*' * 20)) + for part in app_iter: + LOG.debug(part) + yield part + + +class Router(object): + """WSGI middleware that maps incoming requests to WSGI apps.""" + + def __init__(self, mapper): + """Create a router for the given routes.Mapper. + + Each route in `mapper` must specify a 'controller', which is a + WSGI app to call. You'll probably want to specify an 'action' as + well and have your controller be an object that can route + the request to the action-specific method. + + Examples: + mapper = routes.Mapper() + sc = ServerController() + + # Explicit mapping of one route to a controller+action + mapper.connect(None, '/svrlist', controller=sc, action='list') + + # Actions are all implicitly defined + mapper.resource('server', 'servers', controller=sc) + + # Pointing to an arbitrary WSGI app. You can specify the + # {path_info:.*} parameter so the target app can be handed just that + # section of the URL. + mapper.connect(None, '/v1.0/{path_info:.*}', controller=BlogApp()) + + """ + self.map = mapper + self._router = routes.middleware.RoutesMiddleware(self._dispatch, + self.map) + + @webob.dec.wsgify() + def __call__(self, req): + """Route the incoming request to a controller based on self.map. + + If no match, return a 404. + + """ + return self._router + + @staticmethod + @webob.dec.wsgify() + def _dispatch(req): + """Dispatch the request to the appropriate controller. + + Called by self._router after matching the incoming request to a route + and putting the information into req.environ. Either returns 404 + or the routed WSGI app's response. + + """ + match = req.environ['wsgiorg.routing_args'][1] + if not match: + msg = _('The resource could not be found.') + return render_exception(exception.NotFound(msg), + request=req, + user_locale=best_match_language(req)) + app = match['controller'] + return app + + +class ComposingRouter(Router): + def __init__(self, mapper=None, routers=None): + if mapper is None: + mapper = routes.Mapper() + if routers is None: + routers = [] + for router in routers: + router.add_routes(mapper) + super(ComposingRouter, self).__init__(mapper) + + +class ComposableRouter(Router): + """Router that supports use by ComposingRouter.""" + + def __init__(self, mapper=None): + if mapper is None: + mapper = routes.Mapper() + self.add_routes(mapper) + super(ComposableRouter, self).__init__(mapper) + + def add_routes(self, mapper): + """Add routes to given mapper.""" + pass + + +class ExtensionRouter(Router): + """A router that allows extensions to supplement or overwrite routes. + + Expects to be subclassed. + """ + def __init__(self, application, mapper=None): + if mapper is None: + mapper = routes.Mapper() + self.application = application + self.add_routes(mapper) + mapper.connect('{path_info:.*}', controller=self.application) + super(ExtensionRouter, self).__init__(mapper) + + def add_routes(self, mapper): + pass + + @classmethod + def factory(cls, global_config, **local_config): + """Used for paste app factories in paste.deploy config files. + + Any local configuration (that is, values under the [filter:APPNAME] + section of the paste config) will be passed into the `__init__` method + as kwargs. + + A hypothetical configuration would look like: + + [filter:analytics] + redis_host = 127.0.0.1 + paste.filter_factory = keystone.analytics:Analytics.factory + + which would result in a call to the `Analytics` class as + + import keystone.analytics + keystone.analytics.Analytics(app, redis_host='127.0.0.1') + + You could of course re-implement the `factory` method in subclasses, + but using the kwarg passing it shouldn't be necessary. + + """ + def _factory(app): + conf = global_config.copy() + conf.update(local_config) + return cls(app, **local_config) + return _factory + + +class RoutersBase(object): + """Base class for Routers.""" + + def __init__(self): + self.v3_resources = [] + + def append_v3_routers(self, mapper, routers): + """Append v3 routers. + + Subclasses should override this method to map its routes. + + Use self._add_resource() to map routes for a resource. + """ + + def _add_resource(self, mapper, controller, path, rel, + get_action=None, head_action=None, get_head_action=None, + put_action=None, post_action=None, patch_action=None, + delete_action=None, get_post_action=None, + path_vars=None, status=None): + if get_head_action: + getattr(controller, get_head_action) # ensure the attribute exists + mapper.connect(path, controller=controller, action=get_head_action, + conditions=dict(method=['GET', 'HEAD'])) + if get_action: + getattr(controller, get_action) # ensure the attribute exists + mapper.connect(path, controller=controller, action=get_action, + conditions=dict(method=['GET'])) + if head_action: + getattr(controller, head_action) # ensure the attribute exists + mapper.connect(path, controller=controller, action=head_action, + conditions=dict(method=['HEAD'])) + if put_action: + getattr(controller, put_action) # ensure the attribute exists + mapper.connect(path, controller=controller, action=put_action, + conditions=dict(method=['PUT'])) + if post_action: + getattr(controller, post_action) # ensure the attribute exists + mapper.connect(path, controller=controller, action=post_action, + conditions=dict(method=['POST'])) + if patch_action: + getattr(controller, patch_action) # ensure the attribute exists + mapper.connect(path, controller=controller, action=patch_action, + conditions=dict(method=['PATCH'])) + if delete_action: + getattr(controller, delete_action) # ensure the attribute exists + mapper.connect(path, controller=controller, action=delete_action, + conditions=dict(method=['DELETE'])) + if get_post_action: + getattr(controller, get_post_action) # ensure the attribute exists + mapper.connect(path, controller=controller, action=get_post_action, + conditions=dict(method=['GET', 'POST'])) + + resource_data = dict() + + if path_vars: + resource_data['href-template'] = path + resource_data['href-vars'] = path_vars + else: + resource_data['href'] = path + + if status: + if not json_home.Status.is_supported(status): + raise exception.Error(message=_( + 'Unexpected status requested for JSON Home response, %s') % + status) + resource_data.setdefault('hints', {}) + resource_data['hints']['status'] = status + + self.v3_resources.append((rel, resource_data)) + + +class V3ExtensionRouter(ExtensionRouter, RoutersBase): + """Base class for V3 extension router.""" + + def __init__(self, application, mapper=None): + self.v3_resources = list() + super(V3ExtensionRouter, self).__init__(application, mapper) + + def _update_version_response(self, response_data): + response_data['resources'].update(self.v3_resources) + + @webob.dec.wsgify() + def __call__(self, request): + if request.path_info != '/': + # Not a request for version info so forward to super. + return super(V3ExtensionRouter, self).__call__(request) + + response = request.get_response(self.application) + + if response.status_code != 200: + # The request failed, so don't update the response. + return response + + if response.headers['Content-Type'] != 'application/json-home': + # Not a request for JSON Home document, so don't update the + # response. + return response + + response_data = jsonutils.loads(response.body) + self._update_version_response(response_data) + response.body = jsonutils.dumps(response_data, + cls=utils.SmarterEncoder) + return response + + +def render_response(body=None, status=None, headers=None, method=None): + """Forms a WSGI response.""" + if headers is None: + headers = [] + else: + headers = list(headers) + headers.append(('Vary', 'X-Auth-Token')) + + if body is None: + body = '' + status = status or (204, 'No Content') + else: + content_types = [v for h, v in headers if h == 'Content-Type'] + if content_types: + content_type = content_types[0] + else: + content_type = None + + JSON_ENCODE_CONTENT_TYPES = ('application/json', + 'application/json-home',) + if content_type is None or content_type in JSON_ENCODE_CONTENT_TYPES: + body = jsonutils.dumps(body, cls=utils.SmarterEncoder) + if content_type is None: + headers.append(('Content-Type', 'application/json')) + status = status or (200, 'OK') + + resp = webob.Response(body=body, + status='%s %s' % status, + headerlist=headers) + + if method == 'HEAD': + # NOTE(morganfainberg): HEAD requests should return the same status + # as a GET request and same headers (including content-type and + # content-length). The webob.Response object automatically changes + # content-length (and other headers) if the body is set to b''. Capture + # all headers and reset them on the response object after clearing the + # body. The body can only be set to a binary-type (not TextType or + # NoneType), so b'' is used here and should be compatible with + # both py2x and py3x. + stored_headers = resp.headers.copy() + resp.body = b'' + for header, value in six.iteritems(stored_headers): + resp.headers[header] = value + + return resp + + +def render_exception(error, context=None, request=None, user_locale=None): + """Forms a WSGI response based on the current error.""" + + error_message = error.args[0] + message = oslo_i18n.translate(error_message, desired_locale=user_locale) + if message is error_message: + # translate() didn't do anything because it wasn't a Message, + # convert to a string. + message = six.text_type(message) + + body = {'error': { + 'code': error.code, + 'title': error.title, + 'message': message, + }} + headers = [] + if isinstance(error, exception.AuthPluginException): + body['error']['identity'] = error.authentication + elif isinstance(error, exception.Unauthorized): + url = CONF.public_endpoint + if not url: + if request: + context = {'host_url': request.host_url} + if context: + url = Application.base_url(context, 'public') + else: + url = 'http://localhost:%d' % CONF.eventlet_server.public_port + else: + substitutions = dict( + itertools.chain(six.iteritems(CONF), + six.iteritems(CONF.eventlet_server))) + url = url % substitutions + + headers.append(('WWW-Authenticate', 'Keystone uri="%s"' % url)) + return render_response(status=(error.code, error.title), + body=body, + headers=headers) diff --git a/keystone-moon/keystone/config.py b/keystone-moon/keystone/config.py new file mode 100644 index 00000000..3d9a29fd --- /dev/null +++ b/keystone-moon/keystone/config.py @@ -0,0 +1,91 @@ +# Copyright 2012 OpenStack Foundation +# +# 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. +"""Wrapper for keystone.common.config that configures itself on import.""" + +import logging +import os + +from oslo_config import cfg +from oslo_log import log + +from keystone.common import config +from keystone import exception + + +CONF = cfg.CONF + +setup_authentication = config.setup_authentication +configure = config.configure + + +def set_default_for_default_log_levels(): + """Set the default for the default_log_levels option for keystone. + + Keystone uses some packages that other OpenStack services don't use that do + logging. This will set the default_log_levels default level for those + packages. + + This function needs to be called before CONF(). + + """ + + extra_log_level_defaults = [ + 'dogpile=INFO', + 'routes=INFO', + 'keystone.common._memcache_pool=INFO', + ] + + log.register_options(CONF) + CONF.default_log_levels.extend(extra_log_level_defaults) + + +def setup_logging(): + """Sets up logging for the keystone package.""" + log.setup(CONF, 'keystone') + logging.captureWarnings(True) + + +def find_paste_config(): + """Find Keystone's paste.deploy configuration file. + + Keystone's paste.deploy configuration file is specified in the + ``[paste_deploy]`` section of the main Keystone configuration file, + ``keystone.conf``. + + For example:: + + [paste_deploy] + config_file = keystone-paste.ini + + :returns: The selected configuration filename + :raises: exception.ConfigFileNotFound + + """ + if CONF.paste_deploy.config_file: + paste_config = CONF.paste_deploy.config_file + paste_config_value = paste_config + if not os.path.isabs(paste_config): + paste_config = CONF.find_file(paste_config) + elif CONF.config_file: + paste_config = CONF.config_file[0] + paste_config_value = paste_config + else: + # this provides backwards compatibility for keystone.conf files that + # still have the entire paste configuration included, rather than just + # a [paste_deploy] configuration section referring to an external file + paste_config = CONF.find_file('keystone.conf') + paste_config_value = 'keystone.conf' + if not paste_config or not os.path.exists(paste_config): + raise exception.ConfigFileNotFound(config_file=paste_config_value) + return paste_config diff --git a/keystone-moon/keystone/contrib/__init__.py b/keystone-moon/keystone/contrib/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/contrib/admin_crud/__init__.py b/keystone-moon/keystone/contrib/admin_crud/__init__.py new file mode 100644 index 00000000..d6020920 --- /dev/null +++ b/keystone-moon/keystone/contrib/admin_crud/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 keystone.contrib.admin_crud.core import * # noqa diff --git a/keystone-moon/keystone/contrib/admin_crud/core.py b/keystone-moon/keystone/contrib/admin_crud/core.py new file mode 100644 index 00000000..5d69d249 --- /dev/null +++ b/keystone-moon/keystone/contrib/admin_crud/core.py @@ -0,0 +1,241 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 keystone import assignment +from keystone import catalog +from keystone.common import extension +from keystone.common import wsgi +from keystone import identity +from keystone import resource + + +extension.register_admin_extension( + 'OS-KSADM', { + 'name': 'OpenStack Keystone Admin', + 'namespace': 'http://docs.openstack.org/identity/api/ext/' + 'OS-KSADM/v1.0', + 'alias': 'OS-KSADM', + 'updated': '2013-07-11T17:14:00-00:00', + 'description': 'OpenStack extensions to Keystone v2.0 API ' + 'enabling Administrative Operations.', + 'links': [ + { + 'rel': 'describedby', + # TODO(dolph): link needs to be revised after + # bug 928059 merges + 'type': 'text/html', + 'href': 'https://github.com/openstack/identity-api', + } + ]}) + + +class CrudExtension(wsgi.ExtensionRouter): + """Previously known as the OS-KSADM extension. + + Provides a bunch of CRUD operations for internal data types. + + """ + + def add_routes(self, mapper): + tenant_controller = resource.controllers.Tenant() + assignment_tenant_controller = ( + assignment.controllers.TenantAssignment()) + user_controller = identity.controllers.User() + role_controller = assignment.controllers.Role() + assignment_role_controller = assignment.controllers.RoleAssignmentV2() + service_controller = catalog.controllers.Service() + endpoint_controller = catalog.controllers.Endpoint() + + # Tenant Operations + mapper.connect( + '/tenants', + controller=tenant_controller, + action='create_project', + conditions=dict(method=['POST'])) + mapper.connect( + '/tenants/{tenant_id}', + controller=tenant_controller, + action='update_project', + conditions=dict(method=['PUT', 'POST'])) + mapper.connect( + '/tenants/{tenant_id}', + controller=tenant_controller, + action='delete_project', + conditions=dict(method=['DELETE'])) + mapper.connect( + '/tenants/{tenant_id}/users', + controller=assignment_tenant_controller, + action='get_project_users', + conditions=dict(method=['GET'])) + + # User Operations + mapper.connect( + '/users', + controller=user_controller, + action='get_users', + conditions=dict(method=['GET'])) + mapper.connect( + '/users', + controller=user_controller, + action='create_user', + conditions=dict(method=['POST'])) + # NOTE(termie): not in diablo + mapper.connect( + '/users/{user_id}', + controller=user_controller, + action='update_user', + conditions=dict(method=['PUT'])) + mapper.connect( + '/users/{user_id}', + controller=user_controller, + action='delete_user', + conditions=dict(method=['DELETE'])) + + # COMPAT(diablo): the copy with no OS-KSADM is from diablo + mapper.connect( + '/users/{user_id}/password', + controller=user_controller, + action='set_user_password', + conditions=dict(method=['PUT'])) + mapper.connect( + '/users/{user_id}/OS-KSADM/password', + controller=user_controller, + action='set_user_password', + conditions=dict(method=['PUT'])) + + # COMPAT(diablo): the copy with no OS-KSADM is from diablo + mapper.connect( + '/users/{user_id}/tenant', + controller=user_controller, + action='update_user', + conditions=dict(method=['PUT'])) + mapper.connect( + '/users/{user_id}/OS-KSADM/tenant', + controller=user_controller, + action='update_user', + conditions=dict(method=['PUT'])) + + # COMPAT(diablo): the copy with no OS-KSADM is from diablo + mapper.connect( + '/users/{user_id}/enabled', + controller=user_controller, + action='set_user_enabled', + conditions=dict(method=['PUT'])) + mapper.connect( + '/users/{user_id}/OS-KSADM/enabled', + controller=user_controller, + action='set_user_enabled', + conditions=dict(method=['PUT'])) + + # User Roles + mapper.connect( + '/users/{user_id}/roles/OS-KSADM/{role_id}', + controller=assignment_role_controller, + action='add_role_to_user', + conditions=dict(method=['PUT'])) + mapper.connect( + '/users/{user_id}/roles/OS-KSADM/{role_id}', + controller=assignment_role_controller, + action='remove_role_from_user', + conditions=dict(method=['DELETE'])) + + # COMPAT(diablo): User Roles + mapper.connect( + '/users/{user_id}/roleRefs', + controller=assignment_role_controller, + action='get_role_refs', + conditions=dict(method=['GET'])) + mapper.connect( + '/users/{user_id}/roleRefs', + controller=assignment_role_controller, + action='create_role_ref', + conditions=dict(method=['POST'])) + mapper.connect( + '/users/{user_id}/roleRefs/{role_ref_id}', + controller=assignment_role_controller, + action='delete_role_ref', + conditions=dict(method=['DELETE'])) + + # User-Tenant Roles + mapper.connect( + '/tenants/{tenant_id}/users/{user_id}/roles/OS-KSADM/{role_id}', + controller=assignment_role_controller, + action='add_role_to_user', + conditions=dict(method=['PUT'])) + mapper.connect( + '/tenants/{tenant_id}/users/{user_id}/roles/OS-KSADM/{role_id}', + controller=assignment_role_controller, + action='remove_role_from_user', + conditions=dict(method=['DELETE'])) + + # Service Operations + mapper.connect( + '/OS-KSADM/services', + controller=service_controller, + action='get_services', + conditions=dict(method=['GET'])) + mapper.connect( + '/OS-KSADM/services', + controller=service_controller, + action='create_service', + conditions=dict(method=['POST'])) + mapper.connect( + '/OS-KSADM/services/{service_id}', + controller=service_controller, + action='delete_service', + conditions=dict(method=['DELETE'])) + mapper.connect( + '/OS-KSADM/services/{service_id}', + controller=service_controller, + action='get_service', + conditions=dict(method=['GET'])) + + # Endpoint Templates + mapper.connect( + '/endpoints', + controller=endpoint_controller, + action='get_endpoints', + conditions=dict(method=['GET'])) + mapper.connect( + '/endpoints', + controller=endpoint_controller, + action='create_endpoint', + conditions=dict(method=['POST'])) + mapper.connect( + '/endpoints/{endpoint_id}', + controller=endpoint_controller, + action='delete_endpoint', + conditions=dict(method=['DELETE'])) + + # Role Operations + mapper.connect( + '/OS-KSADM/roles', + controller=role_controller, + action='create_role', + conditions=dict(method=['POST'])) + mapper.connect( + '/OS-KSADM/roles', + controller=role_controller, + action='get_roles', + conditions=dict(method=['GET'])) + mapper.connect( + '/OS-KSADM/roles/{role_id}', + controller=role_controller, + action='get_role', + conditions=dict(method=['GET'])) + mapper.connect( + '/OS-KSADM/roles/{role_id}', + controller=role_controller, + action='delete_role', + conditions=dict(method=['DELETE'])) diff --git a/keystone-moon/keystone/contrib/ec2/__init__.py b/keystone-moon/keystone/contrib/ec2/__init__.py new file mode 100644 index 00000000..88622e53 --- /dev/null +++ b/keystone-moon/keystone/contrib/ec2/__init__.py @@ -0,0 +1,18 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 keystone.contrib.ec2 import controllers # noqa +from keystone.contrib.ec2.core import * # noqa +from keystone.contrib.ec2.routers import Ec2Extension # noqa +from keystone.contrib.ec2.routers import Ec2ExtensionV3 # noqa diff --git a/keystone-moon/keystone/contrib/ec2/controllers.py b/keystone-moon/keystone/contrib/ec2/controllers.py new file mode 100644 index 00000000..6e6d3268 --- /dev/null +++ b/keystone-moon/keystone/contrib/ec2/controllers.py @@ -0,0 +1,415 @@ +# Copyright 2013 OpenStack Foundation +# +# 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. + +"""Main entry point into the EC2 Credentials service. + +This service allows the creation of access/secret credentials used for +the ec2 interop layer of OpenStack. + +A user can create as many access/secret pairs, each of which map to a +specific project. This is required because OpenStack supports a user +belonging to multiple projects, whereas the signatures created on ec2-style +requests don't allow specification of which project the user wishes to act +upon. + +To complete the cycle, we provide a method that OpenStack services can +use to validate a signature and get a corresponding OpenStack token. This +token allows method calls to other services within the context the +access/secret was created. As an example, Nova requests Keystone to validate +the signature of a request, receives a token, and then makes a request to +Glance to list images needed to perform the requested task. + +""" + +import abc +import sys +import uuid + +from keystoneclient.contrib.ec2 import utils as ec2_utils +from oslo_serialization import jsonutils +import six + +from keystone.common import controller +from keystone.common import dependency +from keystone.common import utils +from keystone.common import wsgi +from keystone import exception +from keystone.i18n import _ +from keystone.models import token_model + + +@dependency.requires('assignment_api', 'catalog_api', 'credential_api', + 'identity_api', 'resource_api', 'role_api', + 'token_provider_api') +@six.add_metaclass(abc.ABCMeta) +class Ec2ControllerCommon(object): + def check_signature(self, creds_ref, credentials): + signer = ec2_utils.Ec2Signer(creds_ref['secret']) + signature = signer.generate(credentials) + if utils.auth_str_equal(credentials['signature'], signature): + return + # NOTE(vish): Some libraries don't use the port when signing + # requests, so try again without port. + elif ':' in credentials['signature']: + hostname, _port = credentials['host'].split(':') + credentials['host'] = hostname + signature = signer.generate(credentials) + if not utils.auth_str_equal(credentials.signature, signature): + raise exception.Unauthorized(message='Invalid EC2 signature.') + else: + raise exception.Unauthorized(message='EC2 signature not supplied.') + + @abc.abstractmethod + def authenticate(self, context, credentials=None, ec2Credentials=None): + """Validate a signed EC2 request and provide a token. + + Other services (such as Nova) use this **admin** call to determine + if a request they signed received is from a valid user. + + If it is a valid signature, an OpenStack token that maps + to the user/tenant is returned to the caller, along with + all the other details returned from a normal token validation + call. + + The returned token is useful for making calls to other + OpenStack services within the context of the request. + + :param context: standard context + :param credentials: dict of ec2 signature + :param ec2Credentials: DEPRECATED dict of ec2 signature + :returns: token: OpenStack token equivalent to access key along + with the corresponding service catalog and roles + """ + raise exception.NotImplemented() + + def _authenticate(self, credentials=None, ec2credentials=None): + """Common code shared between the V2 and V3 authenticate methods. + + :returns: user_ref, tenant_ref, metadata_ref, roles_ref, catalog_ref + """ + + # FIXME(ja): validate that a service token was used! + + # NOTE(termie): backwards compat hack + if not credentials and ec2credentials: + credentials = ec2credentials + + if 'access' not in credentials: + raise exception.Unauthorized(message='EC2 signature not supplied.') + + creds_ref = self._get_credentials(credentials['access']) + self.check_signature(creds_ref, credentials) + + # TODO(termie): don't create new tokens every time + # TODO(termie): this is copied from TokenController.authenticate + tenant_ref = self.resource_api.get_project(creds_ref['tenant_id']) + user_ref = self.identity_api.get_user(creds_ref['user_id']) + metadata_ref = {} + metadata_ref['roles'] = ( + self.assignment_api.get_roles_for_user_and_project( + user_ref['id'], tenant_ref['id'])) + + trust_id = creds_ref.get('trust_id') + if trust_id: + metadata_ref['trust_id'] = trust_id + metadata_ref['trustee_user_id'] = user_ref['id'] + + # Validate that the auth info is valid and nothing is disabled + try: + self.identity_api.assert_user_enabled( + user_id=user_ref['id'], user=user_ref) + self.resource_api.assert_domain_enabled( + domain_id=user_ref['domain_id']) + self.resource_api.assert_project_enabled( + project_id=tenant_ref['id'], project=tenant_ref) + except AssertionError as e: + six.reraise(exception.Unauthorized, exception.Unauthorized(e), + sys.exc_info()[2]) + + roles = metadata_ref.get('roles', []) + if not roles: + raise exception.Unauthorized(message='User not valid for tenant.') + roles_ref = [self.role_api.get_role(role_id) for role_id in roles] + + catalog_ref = self.catalog_api.get_catalog( + user_ref['id'], tenant_ref['id']) + + return user_ref, tenant_ref, metadata_ref, roles_ref, catalog_ref + + def create_credential(self, context, user_id, tenant_id): + """Create a secret/access pair for use with ec2 style auth. + + Generates a new set of credentials that map the user/tenant + pair. + + :param context: standard context + :param user_id: id of user + :param tenant_id: id of tenant + :returns: credential: dict of ec2 credential + """ + + self.identity_api.get_user(user_id) + self.resource_api.get_project(tenant_id) + trust_id = self._get_trust_id_for_request(context) + blob = {'access': uuid.uuid4().hex, + 'secret': uuid.uuid4().hex, + 'trust_id': trust_id} + credential_id = utils.hash_access_key(blob['access']) + cred_ref = {'user_id': user_id, + 'project_id': tenant_id, + 'blob': jsonutils.dumps(blob), + 'id': credential_id, + 'type': 'ec2'} + self.credential_api.create_credential(credential_id, cred_ref) + return {'credential': self._convert_v3_to_ec2_credential(cred_ref)} + + def get_credentials(self, user_id): + """List all credentials for a user. + + :param user_id: id of user + :returns: credentials: list of ec2 credential dicts + """ + + self.identity_api.get_user(user_id) + credential_refs = self.credential_api.list_credentials_for_user( + user_id) + return {'credentials': + [self._convert_v3_to_ec2_credential(credential) + for credential in credential_refs]} + + def get_credential(self, user_id, credential_id): + """Retrieve a user's access/secret pair by the access key. + + Grab the full access/secret pair for a given access key. + + :param user_id: id of user + :param credential_id: access key for credentials + :returns: credential: dict of ec2 credential + """ + + self.identity_api.get_user(user_id) + return {'credential': self._get_credentials(credential_id)} + + def delete_credential(self, user_id, credential_id): + """Delete a user's access/secret pair. + + Used to revoke a user's access/secret pair + + :param user_id: id of user + :param credential_id: access key for credentials + :returns: bool: success + """ + + self.identity_api.get_user(user_id) + self._get_credentials(credential_id) + ec2_credential_id = utils.hash_access_key(credential_id) + return self.credential_api.delete_credential(ec2_credential_id) + + @staticmethod + def _convert_v3_to_ec2_credential(credential): + # Prior to bug #1259584 fix, blob was stored unserialized + # but it should be stored as a json string for compatibility + # with the v3 credentials API. Fall back to the old behavior + # for backwards compatibility with existing DB contents + try: + blob = jsonutils.loads(credential['blob']) + except TypeError: + blob = credential['blob'] + return {'user_id': credential.get('user_id'), + 'tenant_id': credential.get('project_id'), + 'access': blob.get('access'), + 'secret': blob.get('secret'), + 'trust_id': blob.get('trust_id')} + + def _get_credentials(self, credential_id): + """Return credentials from an ID. + + :param credential_id: id of credential + :raises exception.Unauthorized: when credential id is invalid + :returns: credential: dict of ec2 credential. + """ + ec2_credential_id = utils.hash_access_key(credential_id) + creds = self.credential_api.get_credential(ec2_credential_id) + if not creds: + raise exception.Unauthorized(message='EC2 access key not found.') + return self._convert_v3_to_ec2_credential(creds) + + +@dependency.requires('policy_api', 'token_provider_api') +class Ec2Controller(Ec2ControllerCommon, controller.V2Controller): + + @controller.v2_deprecated + def authenticate(self, context, credentials=None, ec2Credentials=None): + (user_ref, tenant_ref, metadata_ref, roles_ref, + catalog_ref) = self._authenticate(credentials=credentials, + ec2credentials=ec2Credentials) + + # NOTE(morganfainberg): Make sure the data is in correct form since it + # might be consumed external to Keystone and this is a v2.0 controller. + # The token provider does not explicitly care about user_ref version + # in this case, but the data is stored in the token itself and should + # match the version + user_ref = self.v3_to_v2_user(user_ref) + auth_token_data = dict(user=user_ref, + tenant=tenant_ref, + metadata=metadata_ref, + id='placeholder') + (token_id, token_data) = self.token_provider_api.issue_v2_token( + auth_token_data, roles_ref, catalog_ref) + return token_data + + @controller.v2_deprecated + def get_credential(self, context, user_id, credential_id): + if not self._is_admin(context): + self._assert_identity(context, user_id) + return super(Ec2Controller, self).get_credential(user_id, + credential_id) + + @controller.v2_deprecated + def get_credentials(self, context, user_id): + if not self._is_admin(context): + self._assert_identity(context, user_id) + return super(Ec2Controller, self).get_credentials(user_id) + + @controller.v2_deprecated + def create_credential(self, context, user_id, tenant_id): + if not self._is_admin(context): + self._assert_identity(context, user_id) + return super(Ec2Controller, self).create_credential(context, user_id, + tenant_id) + + @controller.v2_deprecated + def delete_credential(self, context, user_id, credential_id): + if not self._is_admin(context): + self._assert_identity(context, user_id) + self._assert_owner(user_id, credential_id) + return super(Ec2Controller, self).delete_credential(user_id, + credential_id) + + def _assert_identity(self, context, user_id): + """Check that the provided token belongs to the user. + + :param context: standard context + :param user_id: id of user + :raises exception.Forbidden: when token is invalid + + """ + try: + token_data = self.token_provider_api.validate_token( + context['token_id']) + except exception.TokenNotFound as e: + raise exception.Unauthorized(e) + + token_ref = token_model.KeystoneToken(token_id=context['token_id'], + token_data=token_data) + + if token_ref.user_id != user_id: + raise exception.Forbidden(_('Token belongs to another user')) + + def _is_admin(self, context): + """Wrap admin assertion error return statement. + + :param context: standard context + :returns: bool: success + + """ + try: + # NOTE(morganfainberg): policy_api is required for assert_admin + # to properly perform policy enforcement. + self.assert_admin(context) + return True + except exception.Forbidden: + return False + + def _assert_owner(self, user_id, credential_id): + """Ensure the provided user owns the credential. + + :param user_id: expected credential owner + :param credential_id: id of credential object + :raises exception.Forbidden: on failure + + """ + ec2_credential_id = utils.hash_access_key(credential_id) + cred_ref = self.credential_api.get_credential(ec2_credential_id) + if user_id != cred_ref['user_id']: + raise exception.Forbidden(_('Credential belongs to another user')) + + +@dependency.requires('policy_api', 'token_provider_api') +class Ec2ControllerV3(Ec2ControllerCommon, controller.V3Controller): + + member_name = 'project' + + def __init__(self): + super(Ec2ControllerV3, self).__init__() + self.get_member_from_driver = self.credential_api.get_credential + + def _check_credential_owner_and_user_id_match(self, context, prep_info, + user_id, credential_id): + # NOTE(morganfainberg): this method needs to capture the arguments of + # the method that is decorated with @controller.protected() (with + # exception of the first argument ('context') since the protected + # method passes in *args, **kwargs. In this case, it is easier to see + # the expected input if the argspec is `user_id` and `credential_id` + # explicitly (matching the :class:`.ec2_delete_credential()` method + # below). + ref = {} + credential_id = utils.hash_access_key(credential_id) + ref['credential'] = self.credential_api.get_credential(credential_id) + # NOTE(morganfainberg): policy_api is required for this + # check_protection to properly be able to perform policy enforcement. + self.check_protection(context, prep_info, ref) + + def authenticate(self, context, credentials=None, ec2Credentials=None): + (user_ref, project_ref, metadata_ref, roles_ref, + catalog_ref) = self._authenticate(credentials=credentials, + ec2credentials=ec2Credentials) + + method_names = ['ec2credential'] + + token_id, token_data = self.token_provider_api.issue_v3_token( + user_ref['id'], method_names, project_id=project_ref['id'], + metadata_ref=metadata_ref) + return render_token_data_response(token_id, token_data) + + @controller.protected(callback=_check_credential_owner_and_user_id_match) + def ec2_get_credential(self, context, user_id, credential_id): + return super(Ec2ControllerV3, self).get_credential(user_id, + credential_id) + + @controller.protected() + def ec2_list_credentials(self, context, user_id): + return super(Ec2ControllerV3, self).get_credentials(user_id) + + @controller.protected() + def ec2_create_credential(self, context, user_id, tenant_id): + return super(Ec2ControllerV3, self).create_credential(context, user_id, + tenant_id) + + @controller.protected(callback=_check_credential_owner_and_user_id_match) + def ec2_delete_credential(self, context, user_id, credential_id): + return super(Ec2ControllerV3, self).delete_credential(user_id, + credential_id) + + +def render_token_data_response(token_id, token_data): + """Render token data HTTP response. + + Stash token ID into the X-Subject-Token header. + + """ + headers = [('X-Subject-Token', token_id)] + + return wsgi.render_response(body=token_data, + status=(200, 'OK'), headers=headers) diff --git a/keystone-moon/keystone/contrib/ec2/core.py b/keystone-moon/keystone/contrib/ec2/core.py new file mode 100644 index 00000000..77857af8 --- /dev/null +++ b/keystone-moon/keystone/contrib/ec2/core.py @@ -0,0 +1,34 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 keystone.common import extension + + +EXTENSION_DATA = { + 'name': 'OpenStack EC2 API', + 'namespace': 'http://docs.openstack.org/identity/api/ext/' + 'OS-EC2/v1.0', + 'alias': 'OS-EC2', + 'updated': '2013-07-07T12:00:0-00:00', + 'description': 'OpenStack EC2 Credentials backend.', + 'links': [ + { + 'rel': 'describedby', + # TODO(ayoung): needs a description + 'type': 'text/html', + 'href': 'https://github.com/openstack/identity-api', + } + ]} +extension.register_admin_extension(EXTENSION_DATA['alias'], EXTENSION_DATA) +extension.register_public_extension(EXTENSION_DATA['alias'], EXTENSION_DATA) diff --git a/keystone-moon/keystone/contrib/ec2/routers.py b/keystone-moon/keystone/contrib/ec2/routers.py new file mode 100644 index 00000000..7b6bf115 --- /dev/null +++ b/keystone-moon/keystone/contrib/ec2/routers.py @@ -0,0 +1,95 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 functools + +from keystone.common import json_home +from keystone.common import wsgi +from keystone.contrib.ec2 import controllers + + +build_resource_relation = functools.partial( + json_home.build_v3_extension_resource_relation, extension_name='OS-EC2', + extension_version='1.0') + +build_parameter_relation = functools.partial( + json_home.build_v3_extension_parameter_relation, extension_name='OS-EC2', + extension_version='1.0') + + +class Ec2Extension(wsgi.ExtensionRouter): + def add_routes(self, mapper): + ec2_controller = controllers.Ec2Controller() + # validation + mapper.connect( + '/ec2tokens', + controller=ec2_controller, + action='authenticate', + conditions=dict(method=['POST'])) + + # crud + mapper.connect( + '/users/{user_id}/credentials/OS-EC2', + controller=ec2_controller, + action='create_credential', + conditions=dict(method=['POST'])) + mapper.connect( + '/users/{user_id}/credentials/OS-EC2', + controller=ec2_controller, + action='get_credentials', + conditions=dict(method=['GET'])) + mapper.connect( + '/users/{user_id}/credentials/OS-EC2/{credential_id}', + controller=ec2_controller, + action='get_credential', + conditions=dict(method=['GET'])) + mapper.connect( + '/users/{user_id}/credentials/OS-EC2/{credential_id}', + controller=ec2_controller, + action='delete_credential', + conditions=dict(method=['DELETE'])) + + +class Ec2ExtensionV3(wsgi.V3ExtensionRouter): + + def add_routes(self, mapper): + ec2_controller = controllers.Ec2ControllerV3() + # validation + self._add_resource( + mapper, ec2_controller, + path='/ec2tokens', + post_action='authenticate', + rel=build_resource_relation(resource_name='ec2tokens')) + + # crud + self._add_resource( + mapper, ec2_controller, + path='/users/{user_id}/credentials/OS-EC2', + get_action='ec2_list_credentials', + post_action='ec2_create_credential', + rel=build_resource_relation(resource_name='user_credentials'), + path_vars={ + 'user_id': json_home.Parameters.USER_ID, + }) + self._add_resource( + mapper, ec2_controller, + path='/users/{user_id}/credentials/OS-EC2/{credential_id}', + get_action='ec2_get_credential', + delete_action='ec2_delete_credential', + rel=build_resource_relation(resource_name='user_credential'), + path_vars={ + 'credential_id': + build_parameter_relation(parameter_name='credential_id'), + 'user_id': json_home.Parameters.USER_ID, + }) diff --git a/keystone-moon/keystone/contrib/endpoint_filter/__init__.py b/keystone-moon/keystone/contrib/endpoint_filter/__init__.py new file mode 100644 index 00000000..72508c3e --- /dev/null +++ b/keystone-moon/keystone/contrib/endpoint_filter/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 keystone.contrib.endpoint_filter.core import * # noqa diff --git a/keystone-moon/keystone/contrib/endpoint_filter/backends/__init__.py b/keystone-moon/keystone/contrib/endpoint_filter/backends/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/contrib/endpoint_filter/backends/catalog_sql.py b/keystone-moon/keystone/contrib/endpoint_filter/backends/catalog_sql.py new file mode 100644 index 00000000..6ac3c1ca --- /dev/null +++ b/keystone-moon/keystone/contrib/endpoint_filter/backends/catalog_sql.py @@ -0,0 +1,76 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 oslo_config import cfg +import six + +from keystone.catalog.backends import sql +from keystone.catalog import core as catalog_core +from keystone.common import dependency +from keystone import exception + +CONF = cfg.CONF + + +@dependency.requires('endpoint_filter_api') +class EndpointFilterCatalog(sql.Catalog): + def get_v3_catalog(self, user_id, project_id): + substitutions = dict(six.iteritems(CONF)) + substitutions.update({'tenant_id': project_id, 'user_id': user_id}) + + services = {} + + refs = self.endpoint_filter_api.list_endpoints_for_project(project_id) + + if (not refs and + CONF.endpoint_filter.return_all_endpoints_if_no_filter): + return super(EndpointFilterCatalog, self).get_v3_catalog( + user_id, project_id) + + for entry in refs: + try: + endpoint = self.get_endpoint(entry['endpoint_id']) + if not endpoint['enabled']: + # Skip disabled endpoints. + continue + service_id = endpoint['service_id'] + services.setdefault( + service_id, + self.get_service(service_id)) + service = services[service_id] + del endpoint['service_id'] + del endpoint['enabled'] + del endpoint['legacy_endpoint_id'] + endpoint['url'] = catalog_core.format_url( + endpoint['url'], substitutions) + # populate filtered endpoints + if 'endpoints' in services[service_id]: + service['endpoints'].append(endpoint) + else: + service['endpoints'] = [endpoint] + except exception.EndpointNotFound: + # remove bad reference from association + self.endpoint_filter_api.remove_endpoint_from_project( + entry['endpoint_id'], project_id) + + # format catalog + catalog = [] + for service_id, service in six.iteritems(services): + formatted_service = {} + formatted_service['id'] = service['id'] + formatted_service['type'] = service['type'] + formatted_service['endpoints'] = service['endpoints'] + catalog.append(formatted_service) + + return catalog diff --git a/keystone-moon/keystone/contrib/endpoint_filter/backends/sql.py b/keystone-moon/keystone/contrib/endpoint_filter/backends/sql.py new file mode 100644 index 00000000..a998423f --- /dev/null +++ b/keystone-moon/keystone/contrib/endpoint_filter/backends/sql.py @@ -0,0 +1,224 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 keystone.common import sql +from keystone import exception +from keystone.i18n import _ + + +class ProjectEndpoint(sql.ModelBase, sql.ModelDictMixin): + """project-endpoint relationship table.""" + __tablename__ = 'project_endpoint' + attributes = ['endpoint_id', 'project_id'] + endpoint_id = sql.Column(sql.String(64), + primary_key=True, + nullable=False) + project_id = sql.Column(sql.String(64), + primary_key=True, + nullable=False) + + +class EndpointGroup(sql.ModelBase, sql.ModelDictMixin): + """Endpoint Groups table.""" + __tablename__ = 'endpoint_group' + attributes = ['id', 'name', 'description', 'filters'] + mutable_attributes = frozenset(['name', 'description', 'filters']) + id = sql.Column(sql.String(64), primary_key=True) + name = sql.Column(sql.String(255), nullable=False) + description = sql.Column(sql.Text, nullable=True) + filters = sql.Column(sql.JsonBlob(), nullable=False) + + +class ProjectEndpointGroupMembership(sql.ModelBase, sql.ModelDictMixin): + """Project to Endpoint group relationship table.""" + __tablename__ = 'project_endpoint_group' + attributes = ['endpoint_group_id', 'project_id'] + endpoint_group_id = sql.Column(sql.String(64), + sql.ForeignKey('endpoint_group.id'), + nullable=False) + project_id = sql.Column(sql.String(64), nullable=False) + __table_args__ = (sql.PrimaryKeyConstraint('endpoint_group_id', + 'project_id'), {}) + + +class EndpointFilter(object): + + @sql.handle_conflicts(conflict_type='project_endpoint') + def add_endpoint_to_project(self, endpoint_id, project_id): + session = sql.get_session() + with session.begin(): + endpoint_filter_ref = ProjectEndpoint(endpoint_id=endpoint_id, + project_id=project_id) + session.add(endpoint_filter_ref) + + def _get_project_endpoint_ref(self, session, endpoint_id, project_id): + endpoint_filter_ref = session.query(ProjectEndpoint).get( + (endpoint_id, project_id)) + if endpoint_filter_ref is None: + msg = _('Endpoint %(endpoint_id)s not found in project ' + '%(project_id)s') % {'endpoint_id': endpoint_id, + 'project_id': project_id} + raise exception.NotFound(msg) + return endpoint_filter_ref + + def check_endpoint_in_project(self, endpoint_id, project_id): + session = sql.get_session() + self._get_project_endpoint_ref(session, endpoint_id, project_id) + + def remove_endpoint_from_project(self, endpoint_id, project_id): + session = sql.get_session() + endpoint_filter_ref = self._get_project_endpoint_ref( + session, endpoint_id, project_id) + with session.begin(): + session.delete(endpoint_filter_ref) + + def list_endpoints_for_project(self, project_id): + session = sql.get_session() + query = session.query(ProjectEndpoint) + query = query.filter_by(project_id=project_id) + endpoint_filter_refs = query.all() + return [ref.to_dict() for ref in endpoint_filter_refs] + + def list_projects_for_endpoint(self, endpoint_id): + session = sql.get_session() + query = session.query(ProjectEndpoint) + query = query.filter_by(endpoint_id=endpoint_id) + endpoint_filter_refs = query.all() + return [ref.to_dict() for ref in endpoint_filter_refs] + + def delete_association_by_endpoint(self, endpoint_id): + session = sql.get_session() + with session.begin(): + query = session.query(ProjectEndpoint) + query = query.filter_by(endpoint_id=endpoint_id) + query.delete(synchronize_session=False) + + def delete_association_by_project(self, project_id): + session = sql.get_session() + with session.begin(): + query = session.query(ProjectEndpoint) + query = query.filter_by(project_id=project_id) + query.delete(synchronize_session=False) + + def create_endpoint_group(self, endpoint_group_id, endpoint_group): + session = sql.get_session() + with session.begin(): + endpoint_group_ref = EndpointGroup.from_dict(endpoint_group) + session.add(endpoint_group_ref) + return endpoint_group_ref.to_dict() + + def _get_endpoint_group(self, session, endpoint_group_id): + endpoint_group_ref = session.query(EndpointGroup).get( + endpoint_group_id) + if endpoint_group_ref is None: + raise exception.EndpointGroupNotFound( + endpoint_group_id=endpoint_group_id) + return endpoint_group_ref + + def get_endpoint_group(self, endpoint_group_id): + session = sql.get_session() + endpoint_group_ref = self._get_endpoint_group(session, + endpoint_group_id) + return endpoint_group_ref.to_dict() + + def update_endpoint_group(self, endpoint_group_id, endpoint_group): + session = sql.get_session() + with session.begin(): + endpoint_group_ref = self._get_endpoint_group(session, + endpoint_group_id) + old_endpoint_group = endpoint_group_ref.to_dict() + old_endpoint_group.update(endpoint_group) + new_endpoint_group = EndpointGroup.from_dict(old_endpoint_group) + for attr in EndpointGroup.mutable_attributes: + setattr(endpoint_group_ref, attr, + getattr(new_endpoint_group, attr)) + return endpoint_group_ref.to_dict() + + def delete_endpoint_group(self, endpoint_group_id): + session = sql.get_session() + endpoint_group_ref = self._get_endpoint_group(session, + endpoint_group_id) + with session.begin(): + session.delete(endpoint_group_ref) + self._delete_endpoint_group_association_by_endpoint_group( + session, endpoint_group_id) + + def get_endpoint_group_in_project(self, endpoint_group_id, project_id): + session = sql.get_session() + ref = self._get_endpoint_group_in_project(session, + endpoint_group_id, + project_id) + return ref.to_dict() + + @sql.handle_conflicts(conflict_type='project_endpoint_group') + def add_endpoint_group_to_project(self, endpoint_group_id, project_id): + session = sql.get_session() + + with session.begin(): + # Create a new Project Endpoint group entity + endpoint_group_project_ref = ProjectEndpointGroupMembership( + endpoint_group_id=endpoint_group_id, project_id=project_id) + session.add(endpoint_group_project_ref) + + def _get_endpoint_group_in_project(self, session, + endpoint_group_id, project_id): + endpoint_group_project_ref = session.query( + ProjectEndpointGroupMembership).get((endpoint_group_id, + project_id)) + if endpoint_group_project_ref is None: + msg = _('Endpoint Group Project Association not found') + raise exception.NotFound(msg) + else: + return endpoint_group_project_ref + + def list_endpoint_groups(self): + session = sql.get_session() + query = session.query(EndpointGroup) + endpoint_group_refs = query.all() + return [e.to_dict() for e in endpoint_group_refs] + + def list_endpoint_groups_for_project(self, project_id): + session = sql.get_session() + query = session.query(ProjectEndpointGroupMembership) + query = query.filter_by(project_id=project_id) + endpoint_group_refs = query.all() + return [ref.to_dict() for ref in endpoint_group_refs] + + def remove_endpoint_group_from_project(self, endpoint_group_id, + project_id): + session = sql.get_session() + endpoint_group_project_ref = self._get_endpoint_group_in_project( + session, endpoint_group_id, project_id) + with session.begin(): + session.delete(endpoint_group_project_ref) + + def list_projects_associated_with_endpoint_group(self, endpoint_group_id): + session = sql.get_session() + query = session.query(ProjectEndpointGroupMembership) + query = query.filter_by(endpoint_group_id=endpoint_group_id) + endpoint_group_refs = query.all() + return [ref.to_dict() for ref in endpoint_group_refs] + + def _delete_endpoint_group_association_by_endpoint_group( + self, session, endpoint_group_id): + query = session.query(ProjectEndpointGroupMembership) + query = query.filter_by(endpoint_group_id=endpoint_group_id) + query.delete() + + def delete_endpoint_group_association_by_project(self, project_id): + session = sql.get_session() + with session.begin(): + query = session.query(ProjectEndpointGroupMembership) + query = query.filter_by(project_id=project_id) + query.delete() diff --git a/keystone-moon/keystone/contrib/endpoint_filter/controllers.py b/keystone-moon/keystone/contrib/endpoint_filter/controllers.py new file mode 100644 index 00000000..dc4ef7a3 --- /dev/null +++ b/keystone-moon/keystone/contrib/endpoint_filter/controllers.py @@ -0,0 +1,300 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 six + +from keystone.catalog import controllers as catalog_controllers +from keystone.common import controller +from keystone.common import dependency +from keystone.common import validation +from keystone.contrib.endpoint_filter import schema +from keystone import exception +from keystone import notifications +from keystone import resource + + +@dependency.requires('catalog_api', 'endpoint_filter_api', 'resource_api') +class _ControllerBase(controller.V3Controller): + """Base behaviors for endpoint filter controllers.""" + + def _get_endpoint_groups_for_project(self, project_id): + # recover the project endpoint group memberships and for each + # membership recover the endpoint group + self.resource_api.get_project(project_id) + try: + refs = self.endpoint_filter_api.list_endpoint_groups_for_project( + project_id) + endpoint_groups = [self.endpoint_filter_api.get_endpoint_group( + ref['endpoint_group_id']) for ref in refs] + return endpoint_groups + except exception.EndpointGroupNotFound: + return [] + + def _get_endpoints_filtered_by_endpoint_group(self, endpoint_group_id): + endpoints = self.catalog_api.list_endpoints() + filters = self.endpoint_filter_api.get_endpoint_group( + endpoint_group_id)['filters'] + filtered_endpoints = [] + + for endpoint in endpoints: + is_candidate = True + for key, value in six.iteritems(filters): + if endpoint[key] != value: + is_candidate = False + break + if is_candidate: + filtered_endpoints.append(endpoint) + return filtered_endpoints + + +class EndpointFilterV3Controller(_ControllerBase): + + def __init__(self): + super(EndpointFilterV3Controller, self).__init__() + notifications.register_event_callback( + notifications.ACTIONS.deleted, 'project', + self._on_project_or_endpoint_delete) + notifications.register_event_callback( + notifications.ACTIONS.deleted, 'endpoint', + self._on_project_or_endpoint_delete) + + def _on_project_or_endpoint_delete(self, service, resource_type, operation, + payload): + project_or_endpoint_id = payload['resource_info'] + if resource_type == 'project': + self.endpoint_filter_api.delete_association_by_project( + project_or_endpoint_id) + else: + self.endpoint_filter_api.delete_association_by_endpoint( + project_or_endpoint_id) + + @controller.protected() + def add_endpoint_to_project(self, context, project_id, endpoint_id): + """Establishes an association between an endpoint and a project.""" + # NOTE(gyee): we just need to make sure endpoint and project exist + # first. We don't really care whether if project is disabled. + # The relationship can still be established even with a disabled + # project as there are no security implications. + self.catalog_api.get_endpoint(endpoint_id) + self.resource_api.get_project(project_id) + self.endpoint_filter_api.add_endpoint_to_project(endpoint_id, + project_id) + + @controller.protected() + def check_endpoint_in_project(self, context, project_id, endpoint_id): + """Verifies endpoint is currently associated with given project.""" + self.catalog_api.get_endpoint(endpoint_id) + self.resource_api.get_project(project_id) + self.endpoint_filter_api.check_endpoint_in_project(endpoint_id, + project_id) + + @controller.protected() + def list_endpoints_for_project(self, context, project_id): + """List all endpoints currently associated with a given project.""" + self.resource_api.get_project(project_id) + refs = self.endpoint_filter_api.list_endpoints_for_project(project_id) + filtered_endpoints = {ref['endpoint_id']: + self.catalog_api.get_endpoint(ref['endpoint_id']) + for ref in refs} + + # need to recover endpoint_groups associated with project + # then for each endpoint group return the endpoints. + endpoint_groups = self._get_endpoint_groups_for_project(project_id) + for endpoint_group in endpoint_groups: + endpoint_refs = self._get_endpoints_filtered_by_endpoint_group( + endpoint_group['id']) + # now check if any endpoints for current endpoint group are not + # contained in the list of filtered endpoints + for endpoint_ref in endpoint_refs: + if endpoint_ref['id'] not in filtered_endpoints: + filtered_endpoints[endpoint_ref['id']] = endpoint_ref + + return catalog_controllers.EndpointV3.wrap_collection( + context, [v for v in six.itervalues(filtered_endpoints)]) + + @controller.protected() + def remove_endpoint_from_project(self, context, project_id, endpoint_id): + """Remove the endpoint from the association with given project.""" + self.endpoint_filter_api.remove_endpoint_from_project(endpoint_id, + project_id) + + @controller.protected() + def list_projects_for_endpoint(self, context, endpoint_id): + """Return a list of projects associated with the endpoint.""" + self.catalog_api.get_endpoint(endpoint_id) + refs = self.endpoint_filter_api.list_projects_for_endpoint(endpoint_id) + + projects = [self.resource_api.get_project( + ref['project_id']) for ref in refs] + return resource.controllers.ProjectV3.wrap_collection(context, + projects) + + +class EndpointGroupV3Controller(_ControllerBase): + collection_name = 'endpoint_groups' + member_name = 'endpoint_group' + + VALID_FILTER_KEYS = ['service_id', 'region_id', 'interface'] + + def __init__(self): + super(EndpointGroupV3Controller, self).__init__() + + @classmethod + def base_url(cls, context, path=None): + """Construct a path and pass it to V3Controller.base_url method.""" + + path = '/OS-EP-FILTER/' + cls.collection_name + return super(EndpointGroupV3Controller, cls).base_url(context, + path=path) + + @controller.protected() + @validation.validated(schema.endpoint_group_create, 'endpoint_group') + def create_endpoint_group(self, context, endpoint_group): + """Creates an Endpoint Group with the associated filters.""" + ref = self._assign_unique_id(self._normalize_dict(endpoint_group)) + self._require_attribute(ref, 'filters') + self._require_valid_filter(ref) + ref = self.endpoint_filter_api.create_endpoint_group(ref['id'], ref) + return EndpointGroupV3Controller.wrap_member(context, ref) + + def _require_valid_filter(self, endpoint_group): + filters = endpoint_group.get('filters') + for key in six.iterkeys(filters): + if key not in self.VALID_FILTER_KEYS: + raise exception.ValidationError( + attribute=self._valid_filter_keys(), + target='endpoint_group') + + def _valid_filter_keys(self): + return ' or '.join(self.VALID_FILTER_KEYS) + + @controller.protected() + def get_endpoint_group(self, context, endpoint_group_id): + """Retrieve the endpoint group associated with the id if exists.""" + ref = self.endpoint_filter_api.get_endpoint_group(endpoint_group_id) + return EndpointGroupV3Controller.wrap_member( + context, ref) + + @controller.protected() + @validation.validated(schema.endpoint_group_update, 'endpoint_group') + def update_endpoint_group(self, context, endpoint_group_id, + endpoint_group): + """Update fixed values and/or extend the filters.""" + if 'filters' in endpoint_group: + self._require_valid_filter(endpoint_group) + ref = self.endpoint_filter_api.update_endpoint_group(endpoint_group_id, + endpoint_group) + return EndpointGroupV3Controller.wrap_member( + context, ref) + + @controller.protected() + def delete_endpoint_group(self, context, endpoint_group_id): + """Delete endpoint_group.""" + self.endpoint_filter_api.delete_endpoint_group(endpoint_group_id) + + @controller.protected() + def list_endpoint_groups(self, context): + """List all endpoint groups.""" + refs = self.endpoint_filter_api.list_endpoint_groups() + return EndpointGroupV3Controller.wrap_collection( + context, refs) + + @controller.protected() + def list_endpoint_groups_for_project(self, context, project_id): + """List all endpoint groups associated with a given project.""" + return EndpointGroupV3Controller.wrap_collection( + context, self._get_endpoint_groups_for_project(project_id)) + + @controller.protected() + def list_projects_associated_with_endpoint_group(self, + context, + endpoint_group_id): + """List all projects associated with endpoint group.""" + endpoint_group_refs = (self.endpoint_filter_api. + list_projects_associated_with_endpoint_group( + endpoint_group_id)) + projects = [] + for endpoint_group_ref in endpoint_group_refs: + project = self.resource_api.get_project( + endpoint_group_ref['project_id']) + if project: + projects.append(project) + return resource.controllers.ProjectV3.wrap_collection(context, + projects) + + @controller.protected() + def list_endpoints_associated_with_endpoint_group(self, + context, + endpoint_group_id): + """List all the endpoints filtered by a specific endpoint group.""" + filtered_endpoints = self._get_endpoints_filtered_by_endpoint_group( + endpoint_group_id) + return catalog_controllers.EndpointV3.wrap_collection( + context, filtered_endpoints) + + +class ProjectEndpointGroupV3Controller(_ControllerBase): + collection_name = 'project_endpoint_groups' + member_name = 'project_endpoint_group' + + def __init__(self): + super(ProjectEndpointGroupV3Controller, self).__init__() + notifications.register_event_callback( + notifications.ACTIONS.deleted, 'project', + self._on_project_delete) + + def _on_project_delete(self, service, resource_type, + operation, payload): + project_id = payload['resource_info'] + (self.endpoint_filter_api. + delete_endpoint_group_association_by_project( + project_id)) + + @controller.protected() + def get_endpoint_group_in_project(self, context, endpoint_group_id, + project_id): + """Retrieve the endpoint group associated with the id if exists.""" + self.resource_api.get_project(project_id) + self.endpoint_filter_api.get_endpoint_group(endpoint_group_id) + ref = self.endpoint_filter_api.get_endpoint_group_in_project( + endpoint_group_id, project_id) + return ProjectEndpointGroupV3Controller.wrap_member( + context, ref) + + @controller.protected() + def add_endpoint_group_to_project(self, context, endpoint_group_id, + project_id): + """Creates an association between an endpoint group and project.""" + self.resource_api.get_project(project_id) + self.endpoint_filter_api.get_endpoint_group(endpoint_group_id) + self.endpoint_filter_api.add_endpoint_group_to_project( + endpoint_group_id, project_id) + + @controller.protected() + def remove_endpoint_group_from_project(self, context, endpoint_group_id, + project_id): + """Remove the endpoint group from associated project.""" + self.resource_api.get_project(project_id) + self.endpoint_filter_api.get_endpoint_group(endpoint_group_id) + self.endpoint_filter_api.remove_endpoint_group_from_project( + endpoint_group_id, project_id) + + @classmethod + def _add_self_referential_link(cls, context, ref): + url = ('/OS-EP-FILTER/endpoint_groups/%(endpoint_group_id)s' + '/projects/%(project_id)s' % { + 'endpoint_group_id': ref['endpoint_group_id'], + 'project_id': ref['project_id']}) + ref.setdefault('links', {}) + ref['links']['self'] = url diff --git a/keystone-moon/keystone/contrib/endpoint_filter/core.py b/keystone-moon/keystone/contrib/endpoint_filter/core.py new file mode 100644 index 00000000..972b65dd --- /dev/null +++ b/keystone-moon/keystone/contrib/endpoint_filter/core.py @@ -0,0 +1,289 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 abc + +from oslo_config import cfg +from oslo_log import log +import six + +from keystone.common import dependency +from keystone.common import extension +from keystone.common import manager +from keystone import exception + + +CONF = cfg.CONF +LOG = log.getLogger(__name__) + +extension_data = { + 'name': 'OpenStack Keystone Endpoint Filter API', + 'namespace': 'http://docs.openstack.org/identity/api/ext/' + 'OS-EP-FILTER/v1.0', + 'alias': 'OS-EP-FILTER', + 'updated': '2013-07-23T12:00:0-00:00', + 'description': 'OpenStack Keystone Endpoint Filter API.', + 'links': [ + { + 'rel': 'describedby', + # TODO(ayoung): needs a description + 'type': 'text/html', + 'href': 'https://github.com/openstack/identity-api/blob/master' + '/openstack-identity-api/v3/src/markdown/' + 'identity-api-v3-os-ep-filter-ext.md', + } + ]} +extension.register_admin_extension(extension_data['alias'], extension_data) + + +@dependency.provider('endpoint_filter_api') +class Manager(manager.Manager): + """Default pivot point for the Endpoint Filter backend. + + See :mod:`keystone.common.manager.Manager` for more details on how this + dynamically calls the backend. + + """ + + def __init__(self): + super(Manager, self).__init__(CONF.endpoint_filter.driver) + + +@six.add_metaclass(abc.ABCMeta) +class Driver(object): + """Interface description for an Endpoint Filter driver.""" + + @abc.abstractmethod + def add_endpoint_to_project(self, endpoint_id, project_id): + """Create an endpoint to project association. + + :param endpoint_id: identity of endpoint to associate + :type endpoint_id: string + :param project_id: identity of the project to be associated with + :type project_id: string + :raises: keystone.exception.Conflict, + :returns: None. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def remove_endpoint_from_project(self, endpoint_id, project_id): + """Removes an endpoint to project association. + + :param endpoint_id: identity of endpoint to remove + :type endpoint_id: string + :param project_id: identity of the project associated with + :type project_id: string + :raises: exception.NotFound + :returns: None. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def check_endpoint_in_project(self, endpoint_id, project_id): + """Checks if an endpoint is associated with a project. + + :param endpoint_id: identity of endpoint to check + :type endpoint_id: string + :param project_id: identity of the project associated with + :type project_id: string + :raises: exception.NotFound + :returns: None. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_endpoints_for_project(self, project_id): + """List all endpoints associated with a project. + + :param project_id: identity of the project to check + :type project_id: string + :returns: a list of identity endpoint ids or an empty list. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_projects_for_endpoint(self, endpoint_id): + """List all projects associated with an endpoint. + + :param endpoint_id: identity of endpoint to check + :type endpoint_id: string + :returns: a list of projects or an empty list. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def delete_association_by_endpoint(self, endpoint_id): + """Removes all the endpoints to project association with endpoint. + + :param endpoint_id: identity of endpoint to check + :type endpoint_id: string + :returns: None + + """ + raise exception.NotImplemented() + + @abc.abstractmethod + def delete_association_by_project(self, project_id): + """Removes all the endpoints to project association with project. + + :param project_id: identity of the project to check + :type project_id: string + :returns: None + + """ + raise exception.NotImplemented() + + @abc.abstractmethod + def create_endpoint_group(self, endpoint_group): + """Create an endpoint group. + + :param endpoint_group: endpoint group to create + :type endpoint_group: dictionary + :raises: keystone.exception.Conflict, + :returns: an endpoint group representation. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def get_endpoint_group(self, endpoint_group_id): + """Get an endpoint group. + + :param endpoint_group_id: identity of endpoint group to retrieve + :type endpoint_group_id: string + :raises: exception.NotFound + :returns: an endpoint group representation. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def update_endpoint_group(self, endpoint_group_id, endpoint_group): + """Update an endpoint group. + + :param endpoint_group_id: identity of endpoint group to retrieve + :type endpoint_group_id: string + :param endpoint_group: A full or partial endpoint_group + :type endpoint_group: dictionary + :raises: exception.NotFound + :returns: an endpoint group representation. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def delete_endpoint_group(self, endpoint_group_id): + """Delete an endpoint group. + + :param endpoint_group_id: identity of endpoint group to delete + :type endpoint_group_id: string + :raises: exception.NotFound + :returns: None. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def add_endpoint_group_to_project(self, endpoint_group_id, project_id): + """Adds an endpoint group to project association. + + :param endpoint_group_id: identity of endpoint to associate + :type endpoint_group_id: string + :param project_id: identity of project to associate + :type project_id: string + :raises: keystone.exception.Conflict, + :returns: None. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def get_endpoint_group_in_project(self, endpoint_group_id, project_id): + """Get endpoint group to project association. + + :param endpoint_group_id: identity of endpoint group to retrieve + :type endpoint_group_id: string + :param project_id: identity of project to associate + :type project_id: string + :raises: exception.NotFound + :returns: a project endpoint group representation. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_endpoint_groups(self): + """List all endpoint groups. + + :raises: exception.NotFound + :returns: None. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_endpoint_groups_for_project(self, project_id): + """List all endpoint group to project associations for a project. + + :param project_id: identity of project to associate + :type project_id: string + :raises: exception.NotFound + :returns: None. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_projects_associated_with_endpoint_group(self, endpoint_group_id): + """List all projects associated with endpoint group. + + :param endpoint_group_id: identity of endpoint to associate + :type endpoint_group_id: string + :raises: exception.NotFound + :returns: None. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def remove_endpoint_group_from_project(self, endpoint_group_id, + project_id): + """Remove an endpoint to project association. + + :param endpoint_group_id: identity of endpoint to associate + :type endpoint_group_id: string + :param project_id: identity of project to associate + :type project_id: string + :raises: exception.NotFound + :returns: None. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def delete_endpoint_group_association_by_project(self, project_id): + """Remove endpoint group to project associations. + + :param project_id: identity of the project to check + :type project_id: string + :returns: None + + """ + raise exception.NotImplemented() # pragma: no cover diff --git a/keystone-moon/keystone/contrib/endpoint_filter/migrate_repo/__init__.py b/keystone-moon/keystone/contrib/endpoint_filter/migrate_repo/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/contrib/endpoint_filter/migrate_repo/migrate.cfg b/keystone-moon/keystone/contrib/endpoint_filter/migrate_repo/migrate.cfg new file mode 100644 index 00000000..c7d34785 --- /dev/null +++ b/keystone-moon/keystone/contrib/endpoint_filter/migrate_repo/migrate.cfg @@ -0,0 +1,25 @@ +[db_settings] +# Used to identify which repository this database is versioned under. +# You can use the name of your project. +repository_id=endpoint_filter + +# The name of the database table used to track the schema version. +# This name shouldn't already be used by your project. +# If this is changed once a database is under version control, you'll need to +# change the table name in each database too. +version_table=migrate_version + +# When committing a change script, Migrate will attempt to generate the +# sql for all supported databases; normally, if one of them fails - probably +# because you don't have that database installed - it is ignored and the +# commit continues, perhaps ending successfully. +# Databases in this list MUST compile successfully during a commit, or the +# entire commit will fail. List the databases your application will actually +# be using to ensure your updates to that database work properly. +# This must be a list; example: ['postgres','sqlite'] +required_dbs=[] + +# When creating new change scripts, Migrate will stamp the new script with +# a version number. By default this is latest_version + 1. You can set this +# to 'true' to tell Migrate to use the UTC timestamp instead. +use_timestamp_numbering=False diff --git a/keystone-moon/keystone/contrib/endpoint_filter/migrate_repo/versions/001_add_endpoint_filtering_table.py b/keystone-moon/keystone/contrib/endpoint_filter/migrate_repo/versions/001_add_endpoint_filtering_table.py new file mode 100644 index 00000000..090e7f47 --- /dev/null +++ b/keystone-moon/keystone/contrib/endpoint_filter/migrate_repo/versions/001_add_endpoint_filtering_table.py @@ -0,0 +1,47 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 sqlalchemy as sql + + +def upgrade(migrate_engine): + # Upgrade operations go here. Don't create your own engine; bind + # migrate_engine to your metadata + meta = sql.MetaData() + meta.bind = migrate_engine + + endpoint_filtering_table = sql.Table( + 'project_endpoint', + meta, + sql.Column( + 'endpoint_id', + sql.String(64), + primary_key=True, + nullable=False), + sql.Column( + 'project_id', + sql.String(64), + primary_key=True, + nullable=False)) + + endpoint_filtering_table.create(migrate_engine, checkfirst=True) + + +def downgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + # Operations to reverse the above upgrade go here. + for table_name in ['project_endpoint']: + table = sql.Table(table_name, meta, autoload=True) + table.drop() diff --git a/keystone-moon/keystone/contrib/endpoint_filter/migrate_repo/versions/002_add_endpoint_groups.py b/keystone-moon/keystone/contrib/endpoint_filter/migrate_repo/versions/002_add_endpoint_groups.py new file mode 100644 index 00000000..5f80160a --- /dev/null +++ b/keystone-moon/keystone/contrib/endpoint_filter/migrate_repo/versions/002_add_endpoint_groups.py @@ -0,0 +1,51 @@ +# Copyright 2014 Hewlett-Packard Company +# +# 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 sqlalchemy as sql + + +def upgrade(migrate_engine): + # Upgrade operations go here. Don't create your own engine; bind + # migrate_engine to your metadata + meta = sql.MetaData() + meta.bind = migrate_engine + + endpoint_group_table = sql.Table( + 'endpoint_group', + meta, + sql.Column('id', sql.String(64), primary_key=True), + sql.Column('name', sql.String(255), nullable=False), + sql.Column('description', sql.Text, nullable=True), + sql.Column('filters', sql.Text(), nullable=False)) + endpoint_group_table.create(migrate_engine, checkfirst=True) + + project_endpoint_group_table = sql.Table( + 'project_endpoint_group', + meta, + sql.Column('endpoint_group_id', sql.String(64), + sql.ForeignKey('endpoint_group.id'), nullable=False), + sql.Column('project_id', sql.String(64), nullable=False), + sql.PrimaryKeyConstraint('endpoint_group_id', + 'project_id')) + project_endpoint_group_table.create(migrate_engine, checkfirst=True) + + +def downgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + # Operations to reverse the above upgrade go here. + for table_name in ['project_endpoint_group', + 'endpoint_group']: + table = sql.Table(table_name, meta, autoload=True) + table.drop() diff --git a/keystone-moon/keystone/contrib/endpoint_filter/migrate_repo/versions/__init__.py b/keystone-moon/keystone/contrib/endpoint_filter/migrate_repo/versions/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/contrib/endpoint_filter/routers.py b/keystone-moon/keystone/contrib/endpoint_filter/routers.py new file mode 100644 index 00000000..00c8cd72 --- /dev/null +++ b/keystone-moon/keystone/contrib/endpoint_filter/routers.py @@ -0,0 +1,149 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 functools + +from keystone.common import json_home +from keystone.common import wsgi +from keystone.contrib.endpoint_filter import controllers + + +build_resource_relation = functools.partial( + json_home.build_v3_extension_resource_relation, + extension_name='OS-EP-FILTER', extension_version='1.0') + +build_parameter_relation = functools.partial( + json_home.build_v3_extension_parameter_relation, + extension_name='OS-EP-FILTER', extension_version='1.0') + +ENDPOINT_GROUP_PARAMETER_RELATION = build_parameter_relation( + parameter_name='endpoint_group_id') + + +class EndpointFilterExtension(wsgi.V3ExtensionRouter): + """API Endpoints for the Endpoint Filter extension. + + The API looks like:: + + PUT /OS-EP-FILTER/projects/$project_id/endpoints/$endpoint_id + GET /OS-EP-FILTER/projects/$project_id/endpoints/$endpoint_id + HEAD /OS-EP-FILTER/projects/$project_id/endpoints/$endpoint_id + DELETE /OS-EP-FILTER/projects/$project_id/endpoints/$endpoint_id + GET /OS-EP-FILTER/endpoints/$endpoint_id/projects + GET /OS-EP-FILTER/projects/$project_id/endpoints + + GET /OS-EP-FILTER/endpoint_groups + POST /OS-EP-FILTER/endpoint_groups + GET /OS-EP-FILTER/endpoint_groups/$endpoint_group_id + HEAD /OS-EP-FILTER/endpoint_groups/$endpoint_group_id + PATCH /OS-EP-FILTER/endpoint_groups/$endpoint_group_id + DELETE /OS-EP-FILTER/endpoint_groups/$endpoint_group_id + + GET /OS-EP-FILTER/endpoint_groups/$endpoint_group_id/projects + GET /OS-EP-FILTER/endpoint_groups/$endpoint_group_id/endpoints + + PUT /OS-EP-FILTER/endpoint_groups/$endpoint_group/projects/$project_id + GET /OS-EP-FILTER/endpoint_groups/$endpoint_group/projects/$project_id + HEAD /OS-EP-FILTER/endpoint_groups/$endpoint_group/projects/$project_id + DELETE /OS-EP-FILTER/endpoint_groups/$endpoint_group/projects/ + $project_id + + """ + PATH_PREFIX = '/OS-EP-FILTER' + PATH_PROJECT_ENDPOINT = '/projects/{project_id}/endpoints/{endpoint_id}' + PATH_ENDPOINT_GROUPS = '/endpoint_groups/{endpoint_group_id}' + PATH_ENDPOINT_GROUP_PROJECTS = PATH_ENDPOINT_GROUPS + ( + '/projects/{project_id}') + + def add_routes(self, mapper): + endpoint_filter_controller = controllers.EndpointFilterV3Controller() + endpoint_group_controller = controllers.EndpointGroupV3Controller() + project_endpoint_group_controller = ( + controllers.ProjectEndpointGroupV3Controller()) + + self._add_resource( + mapper, endpoint_filter_controller, + path=self.PATH_PREFIX + '/endpoints/{endpoint_id}/projects', + get_action='list_projects_for_endpoint', + rel=build_resource_relation(resource_name='endpoint_projects'), + path_vars={ + 'endpoint_id': json_home.Parameters.ENDPOINT_ID, + }) + self._add_resource( + mapper, endpoint_filter_controller, + path=self.PATH_PREFIX + self.PATH_PROJECT_ENDPOINT, + get_head_action='check_endpoint_in_project', + put_action='add_endpoint_to_project', + delete_action='remove_endpoint_from_project', + rel=build_resource_relation(resource_name='project_endpoint'), + path_vars={ + 'endpoint_id': json_home.Parameters.ENDPOINT_ID, + 'project_id': json_home.Parameters.PROJECT_ID, + }) + self._add_resource( + mapper, endpoint_filter_controller, + path=self.PATH_PREFIX + '/projects/{project_id}/endpoints', + get_action='list_endpoints_for_project', + rel=build_resource_relation(resource_name='project_endpoints'), + path_vars={ + 'project_id': json_home.Parameters.PROJECT_ID, + }) + self._add_resource( + mapper, endpoint_group_controller, + path=self.PATH_PREFIX + '/endpoint_groups', + get_action='list_endpoint_groups', + post_action='create_endpoint_group', + rel=build_resource_relation(resource_name='endpoint_groups')) + self._add_resource( + mapper, endpoint_group_controller, + path=self.PATH_PREFIX + self.PATH_ENDPOINT_GROUPS, + get_head_action='get_endpoint_group', + patch_action='update_endpoint_group', + delete_action='delete_endpoint_group', + rel=build_resource_relation(resource_name='endpoint_group'), + path_vars={ + 'endpoint_group_id': ENDPOINT_GROUP_PARAMETER_RELATION + }) + self._add_resource( + mapper, project_endpoint_group_controller, + path=self.PATH_PREFIX + self.PATH_ENDPOINT_GROUP_PROJECTS, + get_head_action='get_endpoint_group_in_project', + put_action='add_endpoint_group_to_project', + delete_action='remove_endpoint_group_from_project', + rel=build_resource_relation( + resource_name='endpoint_group_to_project_association'), + path_vars={ + 'project_id': json_home.Parameters.PROJECT_ID, + 'endpoint_group_id': ENDPOINT_GROUP_PARAMETER_RELATION + }) + self._add_resource( + mapper, endpoint_group_controller, + path=self.PATH_PREFIX + self.PATH_ENDPOINT_GROUPS + ( + '/projects'), + get_action='list_projects_associated_with_endpoint_group', + rel=build_resource_relation( + resource_name='projects_associated_with_endpoint_group'), + path_vars={ + 'endpoint_group_id': ENDPOINT_GROUP_PARAMETER_RELATION + }) + self._add_resource( + mapper, endpoint_group_controller, + path=self.PATH_PREFIX + self.PATH_ENDPOINT_GROUPS + ( + '/endpoints'), + get_action='list_endpoints_associated_with_endpoint_group', + rel=build_resource_relation( + resource_name='endpoints_in_endpoint_group'), + path_vars={ + 'endpoint_group_id': ENDPOINT_GROUP_PARAMETER_RELATION + }) diff --git a/keystone-moon/keystone/contrib/endpoint_filter/schema.py b/keystone-moon/keystone/contrib/endpoint_filter/schema.py new file mode 100644 index 00000000..cbe54e36 --- /dev/null +++ b/keystone-moon/keystone/contrib/endpoint_filter/schema.py @@ -0,0 +1,35 @@ +# 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 keystone.common import validation +from keystone.common.validation import parameter_types + + +_endpoint_group_properties = { + 'description': validation.nullable(parameter_types.description), + 'filters': { + 'type': 'object' + }, + 'name': parameter_types.name +} + +endpoint_group_create = { + 'type': 'object', + 'properties': _endpoint_group_properties, + 'required': ['name', 'filters'] +} + +endpoint_group_update = { + 'type': 'object', + 'properties': _endpoint_group_properties, + 'minProperties': 1 +} diff --git a/keystone-moon/keystone/contrib/endpoint_policy/__init__.py b/keystone-moon/keystone/contrib/endpoint_policy/__init__.py new file mode 100644 index 00000000..12722dc5 --- /dev/null +++ b/keystone-moon/keystone/contrib/endpoint_policy/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2014 IBM Corp. +# +# 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 keystone.contrib.endpoint_policy.core import * # noqa diff --git a/keystone-moon/keystone/contrib/endpoint_policy/backends/__init__.py b/keystone-moon/keystone/contrib/endpoint_policy/backends/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/contrib/endpoint_policy/backends/sql.py b/keystone-moon/keystone/contrib/endpoint_policy/backends/sql.py new file mode 100644 index 00000000..484444f1 --- /dev/null +++ b/keystone-moon/keystone/contrib/endpoint_policy/backends/sql.py @@ -0,0 +1,140 @@ +# Copyright 2014 IBM Corp. +# +# 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 + +import sqlalchemy + +from keystone.common import sql +from keystone import exception + + +class PolicyAssociation(sql.ModelBase, sql.ModelDictMixin): + __tablename__ = 'policy_association' + attributes = ['policy_id', 'endpoint_id', 'region_id', 'service_id'] + # The id column is never exposed outside this module. It only exists to + # provide a primary key, given that the real columns we would like to use + # (endpoint_id, service_id, region_id) can be null + id = sql.Column(sql.String(64), primary_key=True) + policy_id = sql.Column(sql.String(64), nullable=False) + endpoint_id = sql.Column(sql.String(64), nullable=True) + service_id = sql.Column(sql.String(64), nullable=True) + region_id = sql.Column(sql.String(64), nullable=True) + __table_args__ = (sql.UniqueConstraint('endpoint_id', 'service_id', + 'region_id'), {}) + + def to_dict(self): + """Returns the model's attributes as a dictionary. + + We override the standard method in order to hide the id column, + since this only exists to provide the table with a primary key. + + """ + d = {} + for attr in self.__class__.attributes: + d[attr] = getattr(self, attr) + return d + + +class EndpointPolicy(object): + + def create_policy_association(self, policy_id, endpoint_id=None, + service_id=None, region_id=None): + with sql.transaction() as session: + try: + # See if there is already a row for this association, and if + # so, update it with the new policy_id + query = session.query(PolicyAssociation) + query = query.filter_by(endpoint_id=endpoint_id) + query = query.filter_by(service_id=service_id) + query = query.filter_by(region_id=region_id) + association = query.one() + association.policy_id = policy_id + except sql.NotFound: + association = PolicyAssociation(id=uuid.uuid4().hex, + policy_id=policy_id, + endpoint_id=endpoint_id, + service_id=service_id, + region_id=region_id) + session.add(association) + + def check_policy_association(self, policy_id, endpoint_id=None, + service_id=None, region_id=None): + sql_constraints = sqlalchemy.and_( + PolicyAssociation.policy_id == policy_id, + PolicyAssociation.endpoint_id == endpoint_id, + PolicyAssociation.service_id == service_id, + PolicyAssociation.region_id == region_id) + + # NOTE(henry-nash): Getting a single value to save object + # management overhead. + with sql.transaction() as session: + if session.query(PolicyAssociation.id).filter( + sql_constraints).distinct().count() == 0: + raise exception.PolicyAssociationNotFound() + + def delete_policy_association(self, policy_id, endpoint_id=None, + service_id=None, region_id=None): + with sql.transaction() as session: + query = session.query(PolicyAssociation) + query = query.filter_by(policy_id=policy_id) + query = query.filter_by(endpoint_id=endpoint_id) + query = query.filter_by(service_id=service_id) + query = query.filter_by(region_id=region_id) + query.delete() + + def get_policy_association(self, endpoint_id=None, + service_id=None, region_id=None): + sql_constraints = sqlalchemy.and_( + PolicyAssociation.endpoint_id == endpoint_id, + PolicyAssociation.service_id == service_id, + PolicyAssociation.region_id == region_id) + + try: + with sql.transaction() as session: + policy_id = session.query(PolicyAssociation.policy_id).filter( + sql_constraints).distinct().one() + return {'policy_id': policy_id} + except sql.NotFound: + raise exception.PolicyAssociationNotFound() + + def list_associations_for_policy(self, policy_id): + with sql.transaction() as session: + query = session.query(PolicyAssociation) + query = query.filter_by(policy_id=policy_id) + return [ref.to_dict() for ref in query.all()] + + def delete_association_by_endpoint(self, endpoint_id): + with sql.transaction() as session: + query = session.query(PolicyAssociation) + query = query.filter_by(endpoint_id=endpoint_id) + query.delete() + + def delete_association_by_service(self, service_id): + with sql.transaction() as session: + query = session.query(PolicyAssociation) + query = query.filter_by(service_id=service_id) + query.delete() + + def delete_association_by_region(self, region_id): + with sql.transaction() as session: + query = session.query(PolicyAssociation) + query = query.filter_by(region_id=region_id) + query.delete() + + def delete_association_by_policy(self, policy_id): + with sql.transaction() as session: + query = session.query(PolicyAssociation) + query = query.filter_by(policy_id=policy_id) + query.delete() diff --git a/keystone-moon/keystone/contrib/endpoint_policy/controllers.py b/keystone-moon/keystone/contrib/endpoint_policy/controllers.py new file mode 100644 index 00000000..b96834dc --- /dev/null +++ b/keystone-moon/keystone/contrib/endpoint_policy/controllers.py @@ -0,0 +1,166 @@ +# Copyright 2014 IBM Corp. +# +# 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 keystone.common import controller +from keystone.common import dependency +from keystone import notifications + + +@dependency.requires('policy_api', 'catalog_api', 'endpoint_policy_api') +class EndpointPolicyV3Controller(controller.V3Controller): + collection_name = 'endpoints' + member_name = 'endpoint' + + def __init__(self): + super(EndpointPolicyV3Controller, self).__init__() + notifications.register_event_callback( + 'deleted', 'endpoint', self._on_endpoint_delete) + notifications.register_event_callback( + 'deleted', 'service', self._on_service_delete) + notifications.register_event_callback( + 'deleted', 'region', self._on_region_delete) + notifications.register_event_callback( + 'deleted', 'policy', self._on_policy_delete) + + def _on_endpoint_delete(self, service, resource_type, operation, payload): + self.endpoint_policy_api.delete_association_by_endpoint( + payload['resource_info']) + + def _on_service_delete(self, service, resource_type, operation, payload): + self.endpoint_policy_api.delete_association_by_service( + payload['resource_info']) + + def _on_region_delete(self, service, resource_type, operation, payload): + self.endpoint_policy_api.delete_association_by_region( + payload['resource_info']) + + def _on_policy_delete(self, service, resource_type, operation, payload): + self.endpoint_policy_api.delete_association_by_policy( + payload['resource_info']) + + @controller.protected() + def create_policy_association_for_endpoint(self, context, + policy_id, endpoint_id): + """Create an association between a policy and an endpoint.""" + self.policy_api.get_policy(policy_id) + self.catalog_api.get_endpoint(endpoint_id) + self.endpoint_policy_api.create_policy_association( + policy_id, endpoint_id=endpoint_id) + + @controller.protected() + def check_policy_association_for_endpoint(self, context, + policy_id, endpoint_id): + """Check an association between a policy and an endpoint.""" + self.policy_api.get_policy(policy_id) + self.catalog_api.get_endpoint(endpoint_id) + self.endpoint_policy_api.check_policy_association( + policy_id, endpoint_id=endpoint_id) + + @controller.protected() + def delete_policy_association_for_endpoint(self, context, + policy_id, endpoint_id): + """Delete an association between a policy and an endpoint.""" + self.policy_api.get_policy(policy_id) + self.catalog_api.get_endpoint(endpoint_id) + self.endpoint_policy_api.delete_policy_association( + policy_id, endpoint_id=endpoint_id) + + @controller.protected() + def create_policy_association_for_service(self, context, + policy_id, service_id): + """Create an association between a policy and a service.""" + self.policy_api.get_policy(policy_id) + self.catalog_api.get_service(service_id) + self.endpoint_policy_api.create_policy_association( + policy_id, service_id=service_id) + + @controller.protected() + def check_policy_association_for_service(self, context, + policy_id, service_id): + """Check an association between a policy and a service.""" + self.policy_api.get_policy(policy_id) + self.catalog_api.get_service(service_id) + self.endpoint_policy_api.check_policy_association( + policy_id, service_id=service_id) + + @controller.protected() + def delete_policy_association_for_service(self, context, + policy_id, service_id): + """Delete an association between a policy and a service.""" + self.policy_api.get_policy(policy_id) + self.catalog_api.get_service(service_id) + self.endpoint_policy_api.delete_policy_association( + policy_id, service_id=service_id) + + @controller.protected() + def create_policy_association_for_region_and_service( + self, context, policy_id, service_id, region_id): + """Create an association between a policy and region+service.""" + self.policy_api.get_policy(policy_id) + self.catalog_api.get_service(service_id) + self.catalog_api.get_region(region_id) + self.endpoint_policy_api.create_policy_association( + policy_id, service_id=service_id, region_id=region_id) + + @controller.protected() + def check_policy_association_for_region_and_service( + self, context, policy_id, service_id, region_id): + """Check an association between a policy and region+service.""" + self.policy_api.get_policy(policy_id) + self.catalog_api.get_service(service_id) + self.catalog_api.get_region(region_id) + self.endpoint_policy_api.check_policy_association( + policy_id, service_id=service_id, region_id=region_id) + + @controller.protected() + def delete_policy_association_for_region_and_service( + self, context, policy_id, service_id, region_id): + """Delete an association between a policy and region+service.""" + self.policy_api.get_policy(policy_id) + self.catalog_api.get_service(service_id) + self.catalog_api.get_region(region_id) + self.endpoint_policy_api.delete_policy_association( + policy_id, service_id=service_id, region_id=region_id) + + @controller.protected() + def get_policy_for_endpoint(self, context, endpoint_id): + """Get the effective policy for an endpoint.""" + self.catalog_api.get_endpoint(endpoint_id) + ref = self.endpoint_policy_api.get_policy_for_endpoint(endpoint_id) + # NOTE(henry-nash): since the collection and member for this class is + # set to endpoints, we have to handle wrapping this policy entity + # ourselves. + self._add_self_referential_link(context, ref) + return {'policy': ref} + + # NOTE(henry-nash): As in the catalog controller, we must ensure that the + # legacy_endpoint_id does not escape. + + @classmethod + def filter_endpoint(cls, ref): + if 'legacy_endpoint_id' in ref: + ref.pop('legacy_endpoint_id') + return ref + + @classmethod + def wrap_member(cls, context, ref): + ref = cls.filter_endpoint(ref) + return super(EndpointPolicyV3Controller, cls).wrap_member(context, ref) + + @controller.protected() + def list_endpoints_for_policy(self, context, policy_id): + """List endpoints with the effective association to a policy.""" + self.policy_api.get_policy(policy_id) + refs = self.endpoint_policy_api.list_endpoints_for_policy(policy_id) + return EndpointPolicyV3Controller.wrap_collection(context, refs) diff --git a/keystone-moon/keystone/contrib/endpoint_policy/core.py b/keystone-moon/keystone/contrib/endpoint_policy/core.py new file mode 100644 index 00000000..1aa03267 --- /dev/null +++ b/keystone-moon/keystone/contrib/endpoint_policy/core.py @@ -0,0 +1,430 @@ +# Copyright 2014 IBM Corp. +# +# 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 abc + +from oslo_config import cfg +from oslo_log import log +import six + +from keystone.common import dependency +from keystone.common import manager +from keystone import exception +from keystone.i18n import _, _LE, _LW + +CONF = cfg.CONF +LOG = log.getLogger(__name__) + + +@dependency.provider('endpoint_policy_api') +@dependency.requires('catalog_api', 'policy_api') +class Manager(manager.Manager): + """Default pivot point for the Endpoint Policy backend. + + See :mod:`keystone.common.manager.Manager` for more details on how this + dynamically calls the backend. + + """ + + def __init__(self): + super(Manager, self).__init__(CONF.endpoint_policy.driver) + + def _assert_valid_association(self, endpoint_id, service_id, region_id): + """Assert that the association is supported. + + There are three types of association supported: + + - Endpoint (in which case service and region must be None) + - Service and region (in which endpoint must be None) + - Service (in which case endpoint and region must be None) + + """ + if (endpoint_id is not None and + service_id is None and region_id is None): + return + if (service_id is not None and region_id is not None and + endpoint_id is None): + return + if (service_id is not None and + endpoint_id is None and region_id is None): + return + + raise exception.InvalidPolicyAssociation(endpoint_id=endpoint_id, + service_id=service_id, + region_id=region_id) + + def create_policy_association(self, policy_id, endpoint_id=None, + service_id=None, region_id=None): + self._assert_valid_association(endpoint_id, service_id, region_id) + self.driver.create_policy_association(policy_id, endpoint_id, + service_id, region_id) + + def check_policy_association(self, policy_id, endpoint_id=None, + service_id=None, region_id=None): + self._assert_valid_association(endpoint_id, service_id, region_id) + self.driver.check_policy_association(policy_id, endpoint_id, + service_id, region_id) + + def delete_policy_association(self, policy_id, endpoint_id=None, + service_id=None, region_id=None): + self._assert_valid_association(endpoint_id, service_id, region_id) + self.driver.delete_policy_association(policy_id, endpoint_id, + service_id, region_id) + + def list_endpoints_for_policy(self, policy_id): + + def _get_endpoint(endpoint_id, policy_id): + try: + return self.catalog_api.get_endpoint(endpoint_id) + except exception.EndpointNotFound: + msg = _LW('Endpoint %(endpoint_id)s referenced in ' + 'association for policy %(policy_id)s not found.') + LOG.warning(msg, {'policy_id': policy_id, + 'endpoint_id': endpoint_id}) + raise + + def _get_endpoints_for_service(service_id, endpoints): + # TODO(henry-nash): Consider optimizing this in the future by + # adding an explicit list_endpoints_for_service to the catalog API. + return [ep for ep in endpoints if ep['service_id'] == service_id] + + def _get_endpoints_for_service_and_region( + service_id, region_id, endpoints, regions): + # TODO(henry-nash): Consider optimizing this in the future. + # The lack of a two-way pointer in the region tree structure + # makes this somewhat inefficient. + + def _recursively_get_endpoints_for_region( + region_id, service_id, endpoint_list, region_list, + endpoints_found, regions_examined): + """Recursively search down a region tree for endpoints. + + :param region_id: the point in the tree to examine + :param service_id: the service we are interested in + :param endpoint_list: list of all endpoints + :param region_list: list of all regions + :param endpoints_found: list of matching endpoints found so + far - which will be updated if more are + found in this iteration + :param regions_examined: list of regions we have already looked + at - used to spot illegal circular + references in the tree to avoid never + completing search + :returns: list of endpoints that match + + """ + + if region_id in regions_examined: + msg = _LE('Circular reference or a repeated entry found ' + 'in region tree - %(region_id)s.') + LOG.error(msg, {'region_id': ref.region_id}) + return + + regions_examined.append(region_id) + endpoints_found += ( + [ep for ep in endpoint_list if + ep['service_id'] == service_id and + ep['region_id'] == region_id]) + + for region in region_list: + if region['parent_region_id'] == region_id: + _recursively_get_endpoints_for_region( + region['id'], service_id, endpoints, regions, + endpoints_found, regions_examined) + + endpoints_found = [] + regions_examined = [] + + # Now walk down the region tree + _recursively_get_endpoints_for_region( + region_id, service_id, endpoints, regions, + endpoints_found, regions_examined) + + return endpoints_found + + matching_endpoints = [] + endpoints = self.catalog_api.list_endpoints() + regions = self.catalog_api.list_regions() + for ref in self.driver.list_associations_for_policy(policy_id): + if ref.get('endpoint_id') is not None: + matching_endpoints.append( + _get_endpoint(ref['endpoint_id'], policy_id)) + continue + + if (ref.get('service_id') is not None and + ref.get('region_id') is None): + matching_endpoints += _get_endpoints_for_service( + ref['service_id'], endpoints) + continue + + if (ref.get('service_id') is not None and + ref.get('region_id') is not None): + matching_endpoints += ( + _get_endpoints_for_service_and_region( + ref['service_id'], ref['region_id'], + endpoints, regions)) + continue + + msg = _LW('Unsupported policy association found - ' + 'Policy %(policy_id)s, Endpoint %(endpoint_id)s, ' + 'Service %(service_id)s, Region %(region_id)s, ') + LOG.warning(msg, {'policy_id': policy_id, + 'endpoint_id': ref['endpoint_id'], + 'service_id': ref['service_id'], + 'region_id': ref['region_id']}) + + return matching_endpoints + + def get_policy_for_endpoint(self, endpoint_id): + + def _get_policy(policy_id, endpoint_id): + try: + return self.policy_api.get_policy(policy_id) + except exception.PolicyNotFound: + msg = _LW('Policy %(policy_id)s referenced in association ' + 'for endpoint %(endpoint_id)s not found.') + LOG.warning(msg, {'policy_id': policy_id, + 'endpoint_id': endpoint_id}) + raise + + def _look_for_policy_for_region_and_service(endpoint): + """Look in the region and its parents for a policy. + + Examine the region of the endpoint for a policy appropriate for + the service of the endpoint. If there isn't a match, then chase up + the region tree to find one. + + """ + region_id = endpoint['region_id'] + regions_examined = [] + while region_id is not None: + try: + ref = self.driver.get_policy_association( + service_id=endpoint['service_id'], + region_id=region_id) + return ref['policy_id'] + except exception.PolicyAssociationNotFound: + pass + + # There wasn't one for that region & service, let's + # chase up the region tree + regions_examined.append(region_id) + region = self.catalog_api.get_region(region_id) + region_id = None + if region.get('parent_region_id') is not None: + region_id = region['parent_region_id'] + if region_id in regions_examined: + msg = _LE('Circular reference or a repeated entry ' + 'found in region tree - %(region_id)s.') + LOG.error(msg, {'region_id': region_id}) + break + + # First let's see if there is a policy explicitly defined for + # this endpoint. + + try: + ref = self.driver.get_policy_association(endpoint_id=endpoint_id) + return _get_policy(ref['policy_id'], endpoint_id) + except exception.PolicyAssociationNotFound: + pass + + # There wasn't a policy explicitly defined for this endpoint, so + # now let's see if there is one for the Region & Service. + + endpoint = self.catalog_api.get_endpoint(endpoint_id) + policy_id = _look_for_policy_for_region_and_service(endpoint) + if policy_id is not None: + return _get_policy(policy_id, endpoint_id) + + # Finally, just check if there is one for the service. + try: + ref = self.driver.get_policy_association( + service_id=endpoint['service_id']) + return _get_policy(ref['policy_id'], endpoint_id) + except exception.PolicyAssociationNotFound: + pass + + msg = _('No policy is associated with endpoint ' + '%(endpoint_id)s.') % {'endpoint_id': endpoint_id} + raise exception.NotFound(msg) + + +@six.add_metaclass(abc.ABCMeta) +class Driver(object): + """Interface description for an Endpoint Policy driver.""" + + @abc.abstractmethod + def create_policy_association(self, policy_id, endpoint_id=None, + service_id=None, region_id=None): + """Creates a policy association. + + :param policy_id: identity of policy that is being associated + :type policy_id: string + :param endpoint_id: identity of endpoint to associate + :type endpoint_id: string + :param service_id: identity of the service to associate + :type service_id: string + :param region_id: identity of the region to associate + :type region_id: string + :returns: None + + There are three types of association permitted: + + - Endpoint (in which case service and region must be None) + - Service and region (in which endpoint must be None) + - Service (in which case endpoint and region must be None) + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def check_policy_association(self, policy_id, endpoint_id=None, + service_id=None, region_id=None): + """Checks existence a policy association. + + :param policy_id: identity of policy that is being associated + :type policy_id: string + :param endpoint_id: identity of endpoint to associate + :type endpoint_id: string + :param service_id: identity of the service to associate + :type service_id: string + :param region_id: identity of the region to associate + :type region_id: string + :raises: keystone.exception.PolicyAssociationNotFound if there is no + match for the specified association + :returns: None + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def delete_policy_association(self, policy_id, endpoint_id=None, + service_id=None, region_id=None): + """Deletes a policy association. + + :param policy_id: identity of policy that is being associated + :type policy_id: string + :param endpoint_id: identity of endpoint to associate + :type endpoint_id: string + :param service_id: identity of the service to associate + :type service_id: string + :param region_id: identity of the region to associate + :type region_id: string + :returns: None + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def get_policy_association(self, endpoint_id=None, + service_id=None, region_id=None): + """Gets the policy for an explicit association. + + This method is not exposed as a public API, but is used by + get_policy_for_endpoint(). + + :param endpoint_id: identity of endpoint + :type endpoint_id: string + :param service_id: identity of the service + :type service_id: string + :param region_id: identity of the region + :type region_id: string + :raises: keystone.exception.PolicyAssociationNotFound if there is no + match for the specified association + :returns: dict containing policy_id + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_associations_for_policy(self, policy_id): + """List the associations for a policy. + + This method is not exposed as a public API, but is used by + list_endpoints_for_policy(). + + :param policy_id: identity of policy + :type policy_id: string + :returns: List of association dicts + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_endpoints_for_policy(self, policy_id): + """List all the endpoints using a given policy. + + :param policy_id: identity of policy that is being associated + :type policy_id: string + :returns: list of endpoints that have an effective association with + that policy + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def get_policy_for_endpoint(self, endpoint_id): + """Get the appropriate policy for a given endpoint. + + :param endpoint_id: identity of endpoint + :type endpoint_id: string + :returns: Policy entity for the endpoint + + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def delete_association_by_endpoint(self, endpoint_id): + """Removes all the policy associations with the specific endpoint. + + :param endpoint_id: identity of endpoint to check + :type endpoint_id: string + :returns: None + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def delete_association_by_service(self, service_id): + """Removes all the policy associations with the specific service. + + :param service_id: identity of endpoint to check + :type service_id: string + :returns: None + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def delete_association_by_region(self, region_id): + """Removes all the policy associations with the specific region. + + :param region_id: identity of endpoint to check + :type region_id: string + :returns: None + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def delete_association_by_policy(self, policy_id): + """Removes all the policy associations with the specific policy. + + :param policy_id: identity of endpoint to check + :type policy_id: string + :returns: None + + """ + raise exception.NotImplemented() # pragma: no cover diff --git a/keystone-moon/keystone/contrib/endpoint_policy/migrate_repo/__init__.py b/keystone-moon/keystone/contrib/endpoint_policy/migrate_repo/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/contrib/endpoint_policy/migrate_repo/migrate.cfg b/keystone-moon/keystone/contrib/endpoint_policy/migrate_repo/migrate.cfg new file mode 100644 index 00000000..62895d6f --- /dev/null +++ b/keystone-moon/keystone/contrib/endpoint_policy/migrate_repo/migrate.cfg @@ -0,0 +1,25 @@ +[db_settings] +# Used to identify which repository this database is versioned under. +# You can use the name of your project. +repository_id=endpoint_policy + +# The name of the database table used to track the schema version. +# This name shouldn't already be used by your project. +# If this is changed once a database is under version control, you'll need to +# change the table name in each database too. +version_table=migrate_version + +# When committing a change script, Migrate will attempt to generate the +# sql for all supported databases; normally, if one of them fails - probably +# because you don't have that database installed - it is ignored and the +# commit continues, perhaps ending successfully. +# Databases in this list MUST compile successfully during a commit, or the +# entire commit will fail. List the databases your application will actually +# be using to ensure your updates to that database work properly. +# This must be a list; example: ['postgres','sqlite'] +required_dbs=[] + +# When creating new change scripts, Migrate will stamp the new script with +# a version number. By default this is latest_version + 1. You can set this +# to 'true' to tell Migrate to use the UTC timestamp instead. +use_timestamp_numbering=False diff --git a/keystone-moon/keystone/contrib/endpoint_policy/migrate_repo/versions/001_add_endpoint_policy_table.py b/keystone-moon/keystone/contrib/endpoint_policy/migrate_repo/versions/001_add_endpoint_policy_table.py new file mode 100644 index 00000000..c77e4380 --- /dev/null +++ b/keystone-moon/keystone/contrib/endpoint_policy/migrate_repo/versions/001_add_endpoint_policy_table.py @@ -0,0 +1,48 @@ +# Copyright 2014 IBM Corp. +# +# 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 sqlalchemy as sql + + +def upgrade(migrate_engine): + # Upgrade operations go here. Don't create your own engine; bind + # migrate_engine to your metadata + meta = sql.MetaData() + meta.bind = migrate_engine + + endpoint_policy_table = sql.Table( + 'policy_association', + meta, + sql.Column('id', sql.String(64), primary_key=True), + sql.Column('policy_id', sql.String(64), + nullable=False), + sql.Column('endpoint_id', sql.String(64), + nullable=True), + sql.Column('service_id', sql.String(64), + nullable=True), + sql.Column('region_id', sql.String(64), + nullable=True), + sql.UniqueConstraint('endpoint_id', 'service_id', 'region_id'), + mysql_engine='InnoDB', + mysql_charset='utf8') + + endpoint_policy_table.create(migrate_engine, checkfirst=True) + + +def downgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + # Operations to reverse the above upgrade go here. + table = sql.Table('policy_association', meta, autoload=True) + table.drop() diff --git a/keystone-moon/keystone/contrib/endpoint_policy/migrate_repo/versions/__init__.py b/keystone-moon/keystone/contrib/endpoint_policy/migrate_repo/versions/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/contrib/endpoint_policy/routers.py b/keystone-moon/keystone/contrib/endpoint_policy/routers.py new file mode 100644 index 00000000..999d1eed --- /dev/null +++ b/keystone-moon/keystone/contrib/endpoint_policy/routers.py @@ -0,0 +1,85 @@ +# Copyright 2014 IBM Corp. +# +# 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 functools + +from keystone.common import json_home +from keystone.common import wsgi +from keystone.contrib.endpoint_policy import controllers + + +build_resource_relation = functools.partial( + json_home.build_v3_extension_resource_relation, + extension_name='OS-ENDPOINT-POLICY', extension_version='1.0') + + +class EndpointPolicyExtension(wsgi.V3ExtensionRouter): + + PATH_PREFIX = '/OS-ENDPOINT-POLICY' + + def add_routes(self, mapper): + endpoint_policy_controller = controllers.EndpointPolicyV3Controller() + + self._add_resource( + mapper, endpoint_policy_controller, + path='/endpoints/{endpoint_id}' + self.PATH_PREFIX + '/policy', + get_head_action='get_policy_for_endpoint', + rel=build_resource_relation(resource_name='endpoint_policy'), + path_vars={'endpoint_id': json_home.Parameters.ENDPOINT_ID}) + self._add_resource( + mapper, endpoint_policy_controller, + path='/policies/{policy_id}' + self.PATH_PREFIX + '/endpoints', + get_action='list_endpoints_for_policy', + rel=build_resource_relation(resource_name='policy_endpoints'), + path_vars={'policy_id': json_home.Parameters.POLICY_ID}) + self._add_resource( + mapper, endpoint_policy_controller, + path=('/policies/{policy_id}' + self.PATH_PREFIX + + '/endpoints/{endpoint_id}'), + get_head_action='check_policy_association_for_endpoint', + put_action='create_policy_association_for_endpoint', + delete_action='delete_policy_association_for_endpoint', + rel=build_resource_relation( + resource_name='endpoint_policy_association'), + path_vars={ + 'policy_id': json_home.Parameters.POLICY_ID, + 'endpoint_id': json_home.Parameters.ENDPOINT_ID, + }) + self._add_resource( + mapper, endpoint_policy_controller, + path=('/policies/{policy_id}' + self.PATH_PREFIX + + '/services/{service_id}'), + get_head_action='check_policy_association_for_service', + put_action='create_policy_association_for_service', + delete_action='delete_policy_association_for_service', + rel=build_resource_relation( + resource_name='service_policy_association'), + path_vars={ + 'policy_id': json_home.Parameters.POLICY_ID, + 'service_id': json_home.Parameters.SERVICE_ID, + }) + self._add_resource( + mapper, endpoint_policy_controller, + path=('/policies/{policy_id}' + self.PATH_PREFIX + + '/services/{service_id}/regions/{region_id}'), + get_head_action='check_policy_association_for_region_and_service', + put_action='create_policy_association_for_region_and_service', + delete_action='delete_policy_association_for_region_and_service', + rel=build_resource_relation( + resource_name='region_and_service_policy_association'), + path_vars={ + 'policy_id': json_home.Parameters.POLICY_ID, + 'service_id': json_home.Parameters.SERVICE_ID, + 'region_id': json_home.Parameters.REGION_ID, + }) diff --git a/keystone-moon/keystone/contrib/example/__init__.py b/keystone-moon/keystone/contrib/example/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/contrib/example/configuration.rst b/keystone-moon/keystone/contrib/example/configuration.rst new file mode 100644 index 00000000..979d3457 --- /dev/null +++ b/keystone-moon/keystone/contrib/example/configuration.rst @@ -0,0 +1,31 @@ +.. + Copyright 2013 OpenStack, Foundation + All Rights Reserved. + + 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. + +================= +Extension Example +================= + +Please describe here in details how to enable your extension: + +1. Add the required fields and values in the ``[example]`` section + in ``keystone.conf``. + +2. Optional: add the required ``filter`` to the ``pipeline`` in ``keystone-paste.ini`` + +3. Optional: create the extension tables if using the provided sql backend. Example:: + + + ./bin/keystone-manage db_sync --extension example \ No newline at end of file diff --git a/keystone-moon/keystone/contrib/example/controllers.py b/keystone-moon/keystone/contrib/example/controllers.py new file mode 100644 index 00000000..95b3e82f --- /dev/null +++ b/keystone-moon/keystone/contrib/example/controllers.py @@ -0,0 +1,26 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 keystone.common import controller +from keystone.common import dependency + + +@dependency.requires('example_api') +class ExampleV3Controller(controller.V3Controller): + + @controller.protected() + def example_get(self, context): + """Description of the controller logic.""" + self.example_api.do_something(context) diff --git a/keystone-moon/keystone/contrib/example/core.py b/keystone-moon/keystone/contrib/example/core.py new file mode 100644 index 00000000..6e85c7f7 --- /dev/null +++ b/keystone-moon/keystone/contrib/example/core.py @@ -0,0 +1,92 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 oslo_log import log + +from keystone.common import dependency +from keystone.common import manager +from keystone import exception +from keystone.i18n import _LI +from keystone import notifications + + +LOG = log.getLogger(__name__) + + +@dependency.provider('example_api') +class ExampleManager(manager.Manager): + """Example Manager. + + See :mod:`keystone.common.manager.Manager` for more details on + how this dynamically calls the backend. + + """ + + def __init__(self): + # The following is an example of event callbacks. In this setup, + # ExampleManager's data model is depended on project's data model. + # It must create additional aggregates when a new project is created, + # and it must cleanup data related to the project whenever a project + # has been deleted. + # + # In this example, the project_deleted_callback will be invoked + # whenever a project has been deleted. Similarly, the + # project_created_callback will be invoked whenever a new project is + # created. + + # This information is used when the @dependency.provider decorator acts + # on the class. + self.event_callbacks = { + notifications.ACTIONS.deleted: { + 'project': [self.project_deleted_callback], + }, + notifications.ACTIONS.created: { + 'project': [self.project_created_callback], + }, + } + super(ExampleManager, self).__init__( + 'keystone.contrib.example.core.ExampleDriver') + + def project_deleted_callback(self, service, resource_type, operation, + payload): + # The code below is merely an example. + msg = _LI('Received the following notification: service %(service)s, ' + 'resource_type: %(resource_type)s, operation %(operation)s ' + 'payload %(payload)s') + LOG.info(msg, {'service': service, 'resource_type': resource_type, + 'operation': operation, 'payload': payload}) + + def project_created_callback(self, service, resource_type, operation, + payload): + # The code below is merely an example. + msg = _LI('Received the following notification: service %(service)s, ' + 'resource_type: %(resource_type)s, operation %(operation)s ' + 'payload %(payload)s') + LOG.info(msg, {'service': service, 'resource_type': resource_type, + 'operation': operation, 'payload': payload}) + + +class ExampleDriver(object): + """Interface description for Example driver.""" + + def do_something(self, data): + """Do something + + :param data: example data + :type data: string + :raises: keystone.exception, + :returns: None. + + """ + raise exception.NotImplemented() diff --git a/keystone-moon/keystone/contrib/example/migrate_repo/__init__.py b/keystone-moon/keystone/contrib/example/migrate_repo/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/contrib/example/migrate_repo/migrate.cfg b/keystone-moon/keystone/contrib/example/migrate_repo/migrate.cfg new file mode 100644 index 00000000..5b1b1c0a --- /dev/null +++ b/keystone-moon/keystone/contrib/example/migrate_repo/migrate.cfg @@ -0,0 +1,25 @@ +[db_settings] +# Used to identify which repository this database is versioned under. +# You can use the name of your project. +repository_id=example + +# The name of the database table used to track the schema version. +# This name shouldn't already be used by your project. +# If this is changed once a database is under version control, you'll need to +# change the table name in each database too. +version_table=migrate_version + +# When committing a change script, Migrate will attempt to generate the +# sql for all supported databases; normally, if one of them fails - probably +# because you don't have that database installed - it is ignored and the +# commit continues, perhaps ending successfully. +# Databases in this list MUST compile successfully during a commit, or the +# entire commit will fail. List the databases your application will actually +# be using to ensure your updates to that database work properly. +# This must be a list; example: ['postgres','sqlite'] +required_dbs=[] + +# When creating new change scripts, Migrate will stamp the new script with +# a version number. By default this is latest_version + 1. You can set this +# to 'true' to tell Migrate to use the UTC timestamp instead. +use_timestamp_numbering=False diff --git a/keystone-moon/keystone/contrib/example/migrate_repo/versions/001_example_table.py b/keystone-moon/keystone/contrib/example/migrate_repo/versions/001_example_table.py new file mode 100644 index 00000000..10b7ccc7 --- /dev/null +++ b/keystone-moon/keystone/contrib/example/migrate_repo/versions/001_example_table.py @@ -0,0 +1,43 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 sqlalchemy as sql + + +def upgrade(migrate_engine): + # Upgrade operations go here. Don't create your own engine; bind + # migrate_engine to your metadata + meta = sql.MetaData() + meta.bind = migrate_engine + + # catalog + + service_table = sql.Table( + 'example', + meta, + sql.Column('id', sql.String(64), primary_key=True), + sql.Column('type', sql.String(255)), + sql.Column('extra', sql.Text())) + service_table.create(migrate_engine, checkfirst=True) + + +def downgrade(migrate_engine): + # Operations to reverse the above upgrade go here. + meta = sql.MetaData() + meta.bind = migrate_engine + + tables = ['example'] + for t in tables: + table = sql.Table(t, meta, autoload=True) + table.drop(migrate_engine, checkfirst=True) diff --git a/keystone-moon/keystone/contrib/example/migrate_repo/versions/__init__.py b/keystone-moon/keystone/contrib/example/migrate_repo/versions/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/contrib/example/routers.py b/keystone-moon/keystone/contrib/example/routers.py new file mode 100644 index 00000000..30cffe1b --- /dev/null +++ b/keystone-moon/keystone/contrib/example/routers.py @@ -0,0 +1,38 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 functools + +from keystone.common import json_home +from keystone.common import wsgi +from keystone.contrib.example import controllers + + +build_resource_relation = functools.partial( + json_home.build_v3_extension_resource_relation, + extension_name='OS-EXAMPLE', extension_version='1.0') + + +class ExampleRouter(wsgi.V3ExtensionRouter): + + PATH_PREFIX = '/OS-EXAMPLE' + + def add_routes(self, mapper): + example_controller = controllers.ExampleV3Controller() + + self._add_resource( + mapper, example_controller, + path=self.PATH_PREFIX + '/example', + get_action='do_something', + rel=build_resource_relation(resource_name='example')) diff --git a/keystone-moon/keystone/contrib/federation/__init__.py b/keystone-moon/keystone/contrib/federation/__init__.py new file mode 100644 index 00000000..57c9e42c --- /dev/null +++ b/keystone-moon/keystone/contrib/federation/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2014 OpenStack Foundation +# +# 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 keystone.contrib.federation.core import * # noqa diff --git a/keystone-moon/keystone/contrib/federation/backends/__init__.py b/keystone-moon/keystone/contrib/federation/backends/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/contrib/federation/backends/sql.py b/keystone-moon/keystone/contrib/federation/backends/sql.py new file mode 100644 index 00000000..f2c124d0 --- /dev/null +++ b/keystone-moon/keystone/contrib/federation/backends/sql.py @@ -0,0 +1,315 @@ +# Copyright 2014 OpenStack Foundation +# +# 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 oslo_serialization import jsonutils + +from keystone.common import sql +from keystone.contrib.federation import core +from keystone import exception + + +class FederationProtocolModel(sql.ModelBase, sql.DictBase): + __tablename__ = 'federation_protocol' + attributes = ['id', 'idp_id', 'mapping_id'] + mutable_attributes = frozenset(['mapping_id']) + + id = sql.Column(sql.String(64), primary_key=True) + idp_id = sql.Column(sql.String(64), sql.ForeignKey('identity_provider.id', + ondelete='CASCADE'), primary_key=True) + mapping_id = sql.Column(sql.String(64), nullable=False) + + @classmethod + def from_dict(cls, dictionary): + new_dictionary = dictionary.copy() + return cls(**new_dictionary) + + def to_dict(self): + """Return a dictionary with model's attributes.""" + d = dict() + for attr in self.__class__.attributes: + d[attr] = getattr(self, attr) + return d + + +class IdentityProviderModel(sql.ModelBase, sql.DictBase): + __tablename__ = 'identity_provider' + attributes = ['id', 'remote_id', 'enabled', 'description'] + mutable_attributes = frozenset(['description', 'enabled', 'remote_id']) + + id = sql.Column(sql.String(64), primary_key=True) + remote_id = sql.Column(sql.String(256), nullable=True) + enabled = sql.Column(sql.Boolean, nullable=False) + description = sql.Column(sql.Text(), nullable=True) + + @classmethod + def from_dict(cls, dictionary): + new_dictionary = dictionary.copy() + return cls(**new_dictionary) + + def to_dict(self): + """Return a dictionary with model's attributes.""" + d = dict() + for attr in self.__class__.attributes: + d[attr] = getattr(self, attr) + return d + + +class MappingModel(sql.ModelBase, sql.DictBase): + __tablename__ = 'mapping' + attributes = ['id', 'rules'] + + id = sql.Column(sql.String(64), primary_key=True) + rules = sql.Column(sql.JsonBlob(), nullable=False) + + @classmethod + def from_dict(cls, dictionary): + new_dictionary = dictionary.copy() + return cls(**new_dictionary) + + def to_dict(self): + """Return a dictionary with model's attributes.""" + d = dict() + for attr in self.__class__.attributes: + d[attr] = getattr(self, attr) + return d + + +class ServiceProviderModel(sql.ModelBase, sql.DictBase): + __tablename__ = 'service_provider' + attributes = ['auth_url', 'id', 'enabled', 'description', 'sp_url'] + mutable_attributes = frozenset(['auth_url', 'description', 'enabled', + 'sp_url']) + + id = sql.Column(sql.String(64), primary_key=True) + enabled = sql.Column(sql.Boolean, nullable=False) + description = sql.Column(sql.Text(), nullable=True) + auth_url = sql.Column(sql.String(256), nullable=False) + sp_url = sql.Column(sql.String(256), nullable=False) + + @classmethod + def from_dict(cls, dictionary): + new_dictionary = dictionary.copy() + return cls(**new_dictionary) + + def to_dict(self): + """Return a dictionary with model's attributes.""" + d = dict() + for attr in self.__class__.attributes: + d[attr] = getattr(self, attr) + return d + + +class Federation(core.Driver): + + # Identity Provider CRUD + @sql.handle_conflicts(conflict_type='identity_provider') + def create_idp(self, idp_id, idp): + idp['id'] = idp_id + with sql.transaction() as session: + idp_ref = IdentityProviderModel.from_dict(idp) + session.add(idp_ref) + return idp_ref.to_dict() + + def delete_idp(self, idp_id): + with sql.transaction() as session: + idp_ref = self._get_idp(session, idp_id) + session.delete(idp_ref) + + def _get_idp(self, session, idp_id): + idp_ref = session.query(IdentityProviderModel).get(idp_id) + if not idp_ref: + raise exception.IdentityProviderNotFound(idp_id=idp_id) + return idp_ref + + def _get_idp_from_remote_id(self, session, remote_id): + q = session.query(IdentityProviderModel) + q = q.filter_by(remote_id=remote_id) + try: + return q.one() + except sql.NotFound: + raise exception.IdentityProviderNotFound(idp_id=remote_id) + + def list_idps(self): + with sql.transaction() as session: + idps = session.query(IdentityProviderModel) + idps_list = [idp.to_dict() for idp in idps] + return idps_list + + def get_idp(self, idp_id): + with sql.transaction() as session: + idp_ref = self._get_idp(session, idp_id) + return idp_ref.to_dict() + + def get_idp_from_remote_id(self, remote_id): + with sql.transaction() as session: + idp_ref = self._get_idp_from_remote_id(session, remote_id) + return idp_ref.to_dict() + + def update_idp(self, idp_id, idp): + with sql.transaction() as session: + idp_ref = self._get_idp(session, idp_id) + old_idp = idp_ref.to_dict() + old_idp.update(idp) + new_idp = IdentityProviderModel.from_dict(old_idp) + for attr in IdentityProviderModel.mutable_attributes: + setattr(idp_ref, attr, getattr(new_idp, attr)) + return idp_ref.to_dict() + + # Protocol CRUD + def _get_protocol(self, session, idp_id, protocol_id): + q = session.query(FederationProtocolModel) + q = q.filter_by(id=protocol_id, idp_id=idp_id) + try: + return q.one() + except sql.NotFound: + kwargs = {'protocol_id': protocol_id, + 'idp_id': idp_id} + raise exception.FederatedProtocolNotFound(**kwargs) + + @sql.handle_conflicts(conflict_type='federation_protocol') + def create_protocol(self, idp_id, protocol_id, protocol): + protocol['id'] = protocol_id + protocol['idp_id'] = idp_id + with sql.transaction() as session: + self._get_idp(session, idp_id) + protocol_ref = FederationProtocolModel.from_dict(protocol) + session.add(protocol_ref) + return protocol_ref.to_dict() + + def update_protocol(self, idp_id, protocol_id, protocol): + with sql.transaction() as session: + proto_ref = self._get_protocol(session, idp_id, protocol_id) + old_proto = proto_ref.to_dict() + old_proto.update(protocol) + new_proto = FederationProtocolModel.from_dict(old_proto) + for attr in FederationProtocolModel.mutable_attributes: + setattr(proto_ref, attr, getattr(new_proto, attr)) + return proto_ref.to_dict() + + def get_protocol(self, idp_id, protocol_id): + with sql.transaction() as session: + protocol_ref = self._get_protocol(session, idp_id, protocol_id) + return protocol_ref.to_dict() + + def list_protocols(self, idp_id): + with sql.transaction() as session: + q = session.query(FederationProtocolModel) + q = q.filter_by(idp_id=idp_id) + protocols = [protocol.to_dict() for protocol in q] + return protocols + + def delete_protocol(self, idp_id, protocol_id): + with sql.transaction() as session: + key_ref = self._get_protocol(session, idp_id, protocol_id) + session.delete(key_ref) + + # Mapping CRUD + def _get_mapping(self, session, mapping_id): + mapping_ref = session.query(MappingModel).get(mapping_id) + if not mapping_ref: + raise exception.MappingNotFound(mapping_id=mapping_id) + return mapping_ref + + @sql.handle_conflicts(conflict_type='mapping') + def create_mapping(self, mapping_id, mapping): + ref = {} + ref['id'] = mapping_id + ref['rules'] = jsonutils.dumps(mapping.get('rules')) + with sql.transaction() as session: + mapping_ref = MappingModel.from_dict(ref) + session.add(mapping_ref) + return mapping_ref.to_dict() + + def delete_mapping(self, mapping_id): + with sql.transaction() as session: + mapping_ref = self._get_mapping(session, mapping_id) + session.delete(mapping_ref) + + def list_mappings(self): + with sql.transaction() as session: + mappings = session.query(MappingModel) + return [x.to_dict() for x in mappings] + + def get_mapping(self, mapping_id): + with sql.transaction() as session: + mapping_ref = self._get_mapping(session, mapping_id) + return mapping_ref.to_dict() + + @sql.handle_conflicts(conflict_type='mapping') + def update_mapping(self, mapping_id, mapping): + ref = {} + ref['id'] = mapping_id + ref['rules'] = jsonutils.dumps(mapping.get('rules')) + with sql.transaction() as session: + mapping_ref = self._get_mapping(session, mapping_id) + old_mapping = mapping_ref.to_dict() + old_mapping.update(ref) + new_mapping = MappingModel.from_dict(old_mapping) + for attr in MappingModel.attributes: + setattr(mapping_ref, attr, getattr(new_mapping, attr)) + return mapping_ref.to_dict() + + def get_mapping_from_idp_and_protocol(self, idp_id, protocol_id): + with sql.transaction() as session: + protocol_ref = self._get_protocol(session, idp_id, protocol_id) + mapping_id = protocol_ref.mapping_id + mapping_ref = self._get_mapping(session, mapping_id) + return mapping_ref.to_dict() + + # Service Provider CRUD + @sql.handle_conflicts(conflict_type='service_provider') + def create_sp(self, sp_id, sp): + sp['id'] = sp_id + with sql.transaction() as session: + sp_ref = ServiceProviderModel.from_dict(sp) + session.add(sp_ref) + return sp_ref.to_dict() + + def delete_sp(self, sp_id): + with sql.transaction() as session: + sp_ref = self._get_sp(session, sp_id) + session.delete(sp_ref) + + def _get_sp(self, session, sp_id): + sp_ref = session.query(ServiceProviderModel).get(sp_id) + if not sp_ref: + raise exception.ServiceProviderNotFound(sp_id=sp_id) + return sp_ref + + def list_sps(self): + with sql.transaction() as session: + sps = session.query(ServiceProviderModel) + sps_list = [sp.to_dict() for sp in sps] + return sps_list + + def get_sp(self, sp_id): + with sql.transaction() as session: + sp_ref = self._get_sp(session, sp_id) + return sp_ref.to_dict() + + def update_sp(self, sp_id, sp): + with sql.transaction() as session: + sp_ref = self._get_sp(session, sp_id) + old_sp = sp_ref.to_dict() + old_sp.update(sp) + new_sp = ServiceProviderModel.from_dict(old_sp) + for attr in ServiceProviderModel.mutable_attributes: + setattr(sp_ref, attr, getattr(new_sp, attr)) + return sp_ref.to_dict() + + def get_enabled_service_providers(self): + with sql.transaction() as session: + service_providers = session.query(ServiceProviderModel) + service_providers = service_providers.filter_by(enabled=True) + return service_providers diff --git a/keystone-moon/keystone/contrib/federation/controllers.py b/keystone-moon/keystone/contrib/federation/controllers.py new file mode 100644 index 00000000..6066a33f --- /dev/null +++ b/keystone-moon/keystone/contrib/federation/controllers.py @@ -0,0 +1,457 @@ +# 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. + +"""Extensions supporting Federation.""" + +import string + +from oslo_config import cfg +from oslo_log import log +import six +from six.moves import urllib +import webob + +from keystone.auth import controllers as auth_controllers +from keystone.common import authorization +from keystone.common import controller +from keystone.common import dependency +from keystone.common import validation +from keystone.common import wsgi +from keystone.contrib.federation import idp as keystone_idp +from keystone.contrib.federation import schema +from keystone.contrib.federation import utils +from keystone import exception +from keystone.i18n import _ +from keystone.models import token_model + + +CONF = cfg.CONF +LOG = log.getLogger(__name__) + + +class _ControllerBase(controller.V3Controller): + """Base behaviors for federation controllers.""" + + @classmethod + def base_url(cls, context, path=None): + """Construct a path and pass it to V3Controller.base_url method.""" + + path = '/OS-FEDERATION/' + cls.collection_name + return super(_ControllerBase, cls).base_url(context, path=path) + + +@dependency.requires('federation_api') +class IdentityProvider(_ControllerBase): + """Identity Provider representation.""" + collection_name = 'identity_providers' + member_name = 'identity_provider' + + _mutable_parameters = frozenset(['description', 'enabled', 'remote_id']) + _public_parameters = frozenset(['id', 'enabled', 'description', + 'remote_id', 'links' + ]) + + @classmethod + def _add_related_links(cls, context, ref): + """Add URLs for entities related with Identity Provider. + + Add URLs pointing to: + - protocols tied to the Identity Provider + + """ + ref.setdefault('links', {}) + base_path = ref['links'].get('self') + if base_path is None: + base_path = '/'.join([IdentityProvider.base_url(context), + ref['id']]) + for name in ['protocols']: + ref['links'][name] = '/'.join([base_path, name]) + + @classmethod + def _add_self_referential_link(cls, context, ref): + id = ref.get('id') + self_path = '/'.join([cls.base_url(context), id]) + ref.setdefault('links', {}) + ref['links']['self'] = self_path + + @classmethod + def wrap_member(cls, context, ref): + cls._add_self_referential_link(context, ref) + cls._add_related_links(context, ref) + ref = cls.filter_params(ref) + return {cls.member_name: ref} + + @controller.protected() + def create_identity_provider(self, context, idp_id, identity_provider): + identity_provider = self._normalize_dict(identity_provider) + identity_provider.setdefault('enabled', False) + IdentityProvider.check_immutable_params(identity_provider) + idp_ref = self.federation_api.create_idp(idp_id, identity_provider) + response = IdentityProvider.wrap_member(context, idp_ref) + return wsgi.render_response(body=response, status=('201', 'Created')) + + @controller.protected() + def list_identity_providers(self, context): + ref = self.federation_api.list_idps() + ref = [self.filter_params(x) for x in ref] + return IdentityProvider.wrap_collection(context, ref) + + @controller.protected() + def get_identity_provider(self, context, idp_id): + ref = self.federation_api.get_idp(idp_id) + return IdentityProvider.wrap_member(context, ref) + + @controller.protected() + def delete_identity_provider(self, context, idp_id): + self.federation_api.delete_idp(idp_id) + + @controller.protected() + def update_identity_provider(self, context, idp_id, identity_provider): + identity_provider = self._normalize_dict(identity_provider) + IdentityProvider.check_immutable_params(identity_provider) + idp_ref = self.federation_api.update_idp(idp_id, identity_provider) + return IdentityProvider.wrap_member(context, idp_ref) + + +@dependency.requires('federation_api') +class FederationProtocol(_ControllerBase): + """A federation protocol representation. + + See IdentityProvider docstring for explanation on _mutable_parameters + and _public_parameters class attributes. + + """ + collection_name = 'protocols' + member_name = 'protocol' + + _public_parameters = frozenset(['id', 'mapping_id', 'links']) + _mutable_parameters = frozenset(['mapping_id']) + + @classmethod + def _add_self_referential_link(cls, context, ref): + """Add 'links' entry to the response dictionary. + + Calls IdentityProvider.base_url() class method, as it constructs + proper URL along with the 'identity providers' part included. + + :param ref: response dictionary + + """ + ref.setdefault('links', {}) + base_path = ref['links'].get('identity_provider') + if base_path is None: + base_path = [IdentityProvider.base_url(context), ref['idp_id']] + base_path = '/'.join(base_path) + self_path = [base_path, 'protocols', ref['id']] + self_path = '/'.join(self_path) + ref['links']['self'] = self_path + + @classmethod + def _add_related_links(cls, context, ref): + """Add new entries to the 'links' subdictionary in the response. + + Adds 'identity_provider' key with URL pointing to related identity + provider as a value. + + :param ref: response dictionary + + """ + ref.setdefault('links', {}) + base_path = '/'.join([IdentityProvider.base_url(context), + ref['idp_id']]) + ref['links']['identity_provider'] = base_path + + @classmethod + def wrap_member(cls, context, ref): + cls._add_related_links(context, ref) + cls._add_self_referential_link(context, ref) + ref = cls.filter_params(ref) + return {cls.member_name: ref} + + @controller.protected() + def create_protocol(self, context, idp_id, protocol_id, protocol): + ref = self._normalize_dict(protocol) + FederationProtocol.check_immutable_params(ref) + ref = self.federation_api.create_protocol(idp_id, protocol_id, ref) + response = FederationProtocol.wrap_member(context, ref) + return wsgi.render_response(body=response, status=('201', 'Created')) + + @controller.protected() + def update_protocol(self, context, idp_id, protocol_id, protocol): + ref = self._normalize_dict(protocol) + FederationProtocol.check_immutable_params(ref) + ref = self.federation_api.update_protocol(idp_id, protocol_id, + protocol) + return FederationProtocol.wrap_member(context, ref) + + @controller.protected() + def get_protocol(self, context, idp_id, protocol_id): + ref = self.federation_api.get_protocol(idp_id, protocol_id) + return FederationProtocol.wrap_member(context, ref) + + @controller.protected() + def list_protocols(self, context, idp_id): + protocols_ref = self.federation_api.list_protocols(idp_id) + protocols = list(protocols_ref) + return FederationProtocol.wrap_collection(context, protocols) + + @controller.protected() + def delete_protocol(self, context, idp_id, protocol_id): + self.federation_api.delete_protocol(idp_id, protocol_id) + + +@dependency.requires('federation_api') +class MappingController(_ControllerBase): + collection_name = 'mappings' + member_name = 'mapping' + + @controller.protected() + def create_mapping(self, context, mapping_id, mapping): + ref = self._normalize_dict(mapping) + utils.validate_mapping_structure(ref) + mapping_ref = self.federation_api.create_mapping(mapping_id, ref) + response = MappingController.wrap_member(context, mapping_ref) + return wsgi.render_response(body=response, status=('201', 'Created')) + + @controller.protected() + def list_mappings(self, context): + ref = self.federation_api.list_mappings() + return MappingController.wrap_collection(context, ref) + + @controller.protected() + def get_mapping(self, context, mapping_id): + ref = self.federation_api.get_mapping(mapping_id) + return MappingController.wrap_member(context, ref) + + @controller.protected() + def delete_mapping(self, context, mapping_id): + self.federation_api.delete_mapping(mapping_id) + + @controller.protected() + def update_mapping(self, context, mapping_id, mapping): + mapping = self._normalize_dict(mapping) + utils.validate_mapping_structure(mapping) + mapping_ref = self.federation_api.update_mapping(mapping_id, mapping) + return MappingController.wrap_member(context, mapping_ref) + + +@dependency.requires('federation_api') +class Auth(auth_controllers.Auth): + + def federated_authentication(self, context, identity_provider, protocol): + """Authenticate from dedicated url endpoint. + + Build HTTP request body for federated authentication and inject + it into the ``authenticate_for_token`` function. + + """ + auth = { + 'identity': { + 'methods': [protocol], + protocol: { + 'identity_provider': identity_provider, + 'protocol': protocol + } + } + } + + return self.authenticate_for_token(context, auth=auth) + + def federated_sso_auth(self, context, protocol_id): + try: + remote_id_name = CONF.federation.remote_id_attribute + remote_id = context['environment'][remote_id_name] + except KeyError: + msg = _('Missing entity ID from environment') + LOG.error(msg) + raise exception.Unauthorized(msg) + + if 'origin' in context['query_string']: + origin = context['query_string'].get('origin') + host = urllib.parse.unquote_plus(origin) + else: + msg = _('Request must have an origin query parameter') + LOG.error(msg) + raise exception.ValidationError(msg) + + if host in CONF.federation.trusted_dashboard: + ref = self.federation_api.get_idp_from_remote_id(remote_id) + identity_provider = ref['id'] + res = self.federated_authentication(context, identity_provider, + protocol_id) + token_id = res.headers['X-Subject-Token'] + return self.render_html_response(host, token_id) + else: + msg = _('%(host)s is not a trusted dashboard host') + msg = msg % {'host': host} + LOG.error(msg) + raise exception.Unauthorized(msg) + + def render_html_response(self, host, token_id): + """Forms an HTML Form from a template with autosubmit.""" + + headers = [('Content-Type', 'text/html')] + + with open(CONF.federation.sso_callback_template) as template: + src = string.Template(template.read()) + + subs = {'host': host, 'token': token_id} + body = src.substitute(subs) + return webob.Response(body=body, status='200', + headerlist=headers) + + @validation.validated(schema.saml_create, 'auth') + def create_saml_assertion(self, context, auth): + """Exchange a scoped token for a SAML assertion. + + :param auth: Dictionary that contains a token and service provider id + :returns: SAML Assertion based on properties from the token + """ + + issuer = CONF.saml.idp_entity_id + sp_id = auth['scope']['service_provider']['id'] + service_provider = self.federation_api.get_sp(sp_id) + utils.assert_enabled_service_provider_object(service_provider) + + sp_url = service_provider.get('sp_url') + auth_url = service_provider.get('auth_url') + + token_id = auth['identity']['token']['id'] + token_data = self.token_provider_api.validate_token(token_id) + token_ref = token_model.KeystoneToken(token_id, token_data) + subject = token_ref.user_name + roles = token_ref.role_names + + if not token_ref.project_scoped: + action = _('Use a project scoped token when attempting to create ' + 'a SAML assertion') + raise exception.ForbiddenAction(action=action) + + project = token_ref.project_name + generator = keystone_idp.SAMLGenerator() + response = generator.samlize_token(issuer, sp_url, subject, roles, + project) + + return wsgi.render_response(body=response.to_string(), + status=('200', 'OK'), + headers=[('Content-Type', 'text/xml'), + ('X-sp-url', + six.binary_type(sp_url)), + ('X-auth-url', + six.binary_type(auth_url))]) + + +@dependency.requires('assignment_api', 'resource_api') +class DomainV3(controller.V3Controller): + collection_name = 'domains' + member_name = 'domain' + + def __init__(self): + super(DomainV3, self).__init__() + self.get_member_from_driver = self.resource_api.get_domain + + @controller.protected() + def list_domains_for_groups(self, context): + """List all domains available to an authenticated user's groups. + + :param context: request context + :returns: list of accessible domains + + """ + auth_context = context['environment'][authorization.AUTH_CONTEXT_ENV] + domains = self.assignment_api.list_domains_for_groups( + auth_context['group_ids']) + return DomainV3.wrap_collection(context, domains) + + +@dependency.requires('assignment_api', 'resource_api') +class ProjectAssignmentV3(controller.V3Controller): + collection_name = 'projects' + member_name = 'project' + + def __init__(self): + super(ProjectAssignmentV3, self).__init__() + self.get_member_from_driver = self.resource_api.get_project + + @controller.protected() + def list_projects_for_groups(self, context): + """List all projects available to an authenticated user's groups. + + :param context: request context + :returns: list of accessible projects + + """ + auth_context = context['environment'][authorization.AUTH_CONTEXT_ENV] + projects = self.assignment_api.list_projects_for_groups( + auth_context['group_ids']) + return ProjectAssignmentV3.wrap_collection(context, projects) + + +@dependency.requires('federation_api') +class ServiceProvider(_ControllerBase): + """Service Provider representation.""" + + collection_name = 'service_providers' + member_name = 'service_provider' + + _mutable_parameters = frozenset(['auth_url', 'description', 'enabled', + 'sp_url']) + _public_parameters = frozenset(['auth_url', 'id', 'enabled', 'description', + 'links', 'sp_url']) + + @controller.protected() + @validation.validated(schema.service_provider_create, 'service_provider') + def create_service_provider(self, context, sp_id, service_provider): + service_provider = self._normalize_dict(service_provider) + service_provider.setdefault('enabled', False) + ServiceProvider.check_immutable_params(service_provider) + sp_ref = self.federation_api.create_sp(sp_id, service_provider) + response = ServiceProvider.wrap_member(context, sp_ref) + return wsgi.render_response(body=response, status=('201', 'Created')) + + @controller.protected() + def list_service_providers(self, context): + ref = self.federation_api.list_sps() + ref = [self.filter_params(x) for x in ref] + return ServiceProvider.wrap_collection(context, ref) + + @controller.protected() + def get_service_provider(self, context, sp_id): + ref = self.federation_api.get_sp(sp_id) + return ServiceProvider.wrap_member(context, ref) + + @controller.protected() + def delete_service_provider(self, context, sp_id): + self.federation_api.delete_sp(sp_id) + + @controller.protected() + @validation.validated(schema.service_provider_update, 'service_provider') + def update_service_provider(self, context, sp_id, service_provider): + service_provider = self._normalize_dict(service_provider) + ServiceProvider.check_immutable_params(service_provider) + sp_ref = self.federation_api.update_sp(sp_id, service_provider) + return ServiceProvider.wrap_member(context, sp_ref) + + +class SAMLMetadataV3(_ControllerBase): + member_name = 'metadata' + + def get_metadata(self, context): + metadata_path = CONF.saml.idp_metadata_path + try: + with open(metadata_path, 'r') as metadata_handler: + metadata = metadata_handler.read() + except IOError as e: + # Raise HTTP 500 in case Metadata file cannot be read. + raise exception.MetadataFileError(reason=e) + return wsgi.render_response(body=metadata, status=('200', 'OK'), + headers=[('Content-Type', 'text/xml')]) diff --git a/keystone-moon/keystone/contrib/federation/core.py b/keystone-moon/keystone/contrib/federation/core.py new file mode 100644 index 00000000..b596cff7 --- /dev/null +++ b/keystone-moon/keystone/contrib/federation/core.py @@ -0,0 +1,346 @@ +# 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. + +"""Extension supporting Federation.""" + +import abc + +from oslo_config import cfg +from oslo_log import log as logging +import six + +from keystone.common import dependency +from keystone.common import extension +from keystone.common import manager +from keystone import exception + + +CONF = cfg.CONF +LOG = logging.getLogger(__name__) +EXTENSION_DATA = { + 'name': 'OpenStack Federation APIs', + 'namespace': 'http://docs.openstack.org/identity/api/ext/' + 'OS-FEDERATION/v1.0', + 'alias': 'OS-FEDERATION', + 'updated': '2013-12-17T12:00:0-00:00', + 'description': 'OpenStack Identity Providers Mechanism.', + 'links': [{ + 'rel': 'describedby', + 'type': 'text/html', + 'href': 'https://github.com/openstack/identity-api' + }]} +extension.register_admin_extension(EXTENSION_DATA['alias'], EXTENSION_DATA) +extension.register_public_extension(EXTENSION_DATA['alias'], EXTENSION_DATA) + +FEDERATION = 'OS-FEDERATION' +IDENTITY_PROVIDER = 'OS-FEDERATION:identity_provider' +PROTOCOL = 'OS-FEDERATION:protocol' +FEDERATED_DOMAIN_KEYWORD = 'Federated' + + +@dependency.provider('federation_api') +class Manager(manager.Manager): + """Default pivot point for the Federation backend. + + See :mod:`keystone.common.manager.Manager` for more details on how this + dynamically calls the backend. + + """ + def __init__(self): + super(Manager, self).__init__(CONF.federation.driver) + + def get_enabled_service_providers(self): + """List enabled service providers for Service Catalog + + Service Provider in a catalog contains three attributes: ``id``, + ``auth_url``, ``sp_url``, where: + + - id is an unique, user defined identifier for service provider object + - auth_url is a authentication URL of remote Keystone + - sp_url a URL accessible at the remote service provider where SAML + assertion is transmitted. + + :returns: list of dictionaries with enabled service providers + :rtype: list of dicts + + """ + def normalize(sp): + ref = { + 'auth_url': sp.auth_url, + 'id': sp.id, + 'sp_url': sp.sp_url + } + return ref + + service_providers = self.driver.get_enabled_service_providers() + return [normalize(sp) for sp in service_providers] + + +@six.add_metaclass(abc.ABCMeta) +class Driver(object): + + @abc.abstractmethod + def create_idp(self, idp_id, idp): + """Create an identity provider. + + :returns: idp_ref + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def delete_idp(self, idp_id): + """Delete an identity provider. + + :raises: keystone.exception.IdentityProviderNotFound + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_idps(self): + """List all identity providers. + + :raises: keystone.exception.IdentityProviderNotFound + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def get_idp(self, idp_id): + """Get an identity provider by ID. + + :raises: keystone.exception.IdentityProviderNotFound + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def get_idp_from_remote_id(self, remote_id): + """Get an identity provider by remote ID. + + :raises: keystone.exception.IdentityProviderNotFound + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def update_idp(self, idp_id, idp): + """Update an identity provider by ID. + + :raises: keystone.exception.IdentityProviderNotFound + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def create_protocol(self, idp_id, protocol_id, protocol): + """Add an IdP-Protocol configuration. + + :raises: keystone.exception.IdentityProviderNotFound + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def update_protocol(self, idp_id, protocol_id, protocol): + """Change an IdP-Protocol configuration. + + :raises: keystone.exception.IdentityProviderNotFound, + keystone.exception.FederatedProtocolNotFound + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def get_protocol(self, idp_id, protocol_id): + """Get an IdP-Protocol configuration. + + :raises: keystone.exception.IdentityProviderNotFound, + keystone.exception.FederatedProtocolNotFound + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_protocols(self, idp_id): + """List an IdP's supported protocols. + + :raises: keystone.exception.IdentityProviderNotFound, + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def delete_protocol(self, idp_id, protocol_id): + """Delete an IdP-Protocol configuration. + + :raises: keystone.exception.IdentityProviderNotFound, + keystone.exception.FederatedProtocolNotFound, + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def create_mapping(self, mapping_ref): + """Create a mapping. + + :param mapping_ref: mapping ref with mapping name + :type mapping_ref: dict + :returns: mapping_ref + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def delete_mapping(self, mapping_id): + """Delete a mapping. + + :param mapping_id: id of mapping to delete + :type mapping_ref: string + :returns: None + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def update_mapping(self, mapping_id, mapping_ref): + """Update a mapping. + + :param mapping_id: id of mapping to update + :type mapping_id: string + :param mapping_ref: new mapping ref + :type mapping_ref: dict + :returns: mapping_ref + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_mappings(self): + """List all mappings. + + returns: list of mappings + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def get_mapping(self, mapping_id): + """Get a mapping, returns the mapping based + on mapping_id. + + :param mapping_id: id of mapping to get + :type mapping_ref: string + :returns: mapping_ref + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def get_mapping_from_idp_and_protocol(self, idp_id, protocol_id): + """Get mapping based on idp_id and protocol_id. + + :param idp_id: id of the identity provider + :type idp_id: string + :param protocol_id: id of the protocol + :type protocol_id: string + :raises: keystone.exception.IdentityProviderNotFound, + keystone.exception.FederatedProtocolNotFound, + :returns: mapping_ref + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def create_sp(self, sp_id, sp): + """Create a service provider. + + :param sp_id: id of the service provider + :type sp_id: string + :param sp: service prvider object + :type sp: dict + + :returns: sp_ref + :rtype: dict + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def delete_sp(self, sp_id): + """Delete a service provider. + + :param sp_id: id of the service provider + :type sp_id: string + + :raises: keystone.exception.ServiceProviderNotFound + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_sps(self): + """List all service providers. + + :returns List of sp_ref objects + :rtype: list of dicts + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def get_sp(self, sp_id): + """Get a service provider. + + :param sp_id: id of the service provider + :type sp_id: string + + :returns: sp_ref + :raises: keystone.exception.ServiceProviderNotFound + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def update_sp(self, sp_id, sp): + """Update a service provider. + + :param sp_id: id of the service provider + :type sp_id: string + :param sp: service prvider object + :type sp: dict + + :returns: sp_ref + :rtype: dict + + :raises: keystone.exception.ServiceProviderNotFound + + """ + raise exception.NotImplemented() # pragma: no cover + + def get_enabled_service_providers(self): + """List enabled service providers for Service Catalog + + Service Provider in a catalog contains three attributes: ``id``, + ``auth_url``, ``sp_url``, where: + + - id is an unique, user defined identifier for service provider object + - auth_url is a authentication URL of remote Keystone + - sp_url a URL accessible at the remote service provider where SAML + assertion is transmitted. + + :returns: list of dictionaries with enabled service providers + :rtype: list of dicts + + """ + raise exception.NotImplemented() # pragma: no cover diff --git a/keystone-moon/keystone/contrib/federation/idp.py b/keystone-moon/keystone/contrib/federation/idp.py new file mode 100644 index 00000000..bf400135 --- /dev/null +++ b/keystone-moon/keystone/contrib/federation/idp.py @@ -0,0 +1,558 @@ +# 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 datetime +import os +import subprocess +import uuid + +from oslo_config import cfg +from oslo_log import log +from oslo_utils import timeutils +import saml2 +from saml2 import md +from saml2 import saml +from saml2 import samlp +from saml2 import sigver +import xmldsig + +from keystone import exception +from keystone.i18n import _, _LE +from keystone.openstack.common import fileutils + + +LOG = log.getLogger(__name__) +CONF = cfg.CONF + + +class SAMLGenerator(object): + """A class to generate SAML assertions.""" + + def __init__(self): + self.assertion_id = uuid.uuid4().hex + + def samlize_token(self, issuer, recipient, user, roles, project, + expires_in=None): + """Convert Keystone attributes to a SAML assertion. + + :param issuer: URL of the issuing party + :type issuer: string + :param recipient: URL of the recipient + :type recipient: string + :param user: User name + :type user: string + :param roles: List of role names + :type roles: list + :param project: Project name + :type project: string + :param expires_in: Sets how long the assertion is valid for, in seconds + :type expires_in: int + + :return: XML object + + """ + expiration_time = self._determine_expiration_time(expires_in) + status = self._create_status() + saml_issuer = self._create_issuer(issuer) + subject = self._create_subject(user, expiration_time, recipient) + attribute_statement = self._create_attribute_statement(user, roles, + project) + authn_statement = self._create_authn_statement(issuer, expiration_time) + signature = self._create_signature() + + assertion = self._create_assertion(saml_issuer, signature, + subject, authn_statement, + attribute_statement) + + assertion = _sign_assertion(assertion) + + response = self._create_response(saml_issuer, status, assertion, + recipient) + return response + + def _determine_expiration_time(self, expires_in): + if expires_in is None: + expires_in = CONF.saml.assertion_expiration_time + now = timeutils.utcnow() + future = now + datetime.timedelta(seconds=expires_in) + return timeutils.isotime(future, subsecond=True) + + def _create_status(self): + """Create an object that represents a SAML Status. + + + + + + :return: XML object + + """ + status = samlp.Status() + status_code = samlp.StatusCode() + status_code.value = samlp.STATUS_SUCCESS + status_code.set_text('') + status.status_code = status_code + return status + + def _create_issuer(self, issuer_url): + """Create an object that represents a SAML Issuer. + + + https://acme.com/FIM/sps/openstack/saml20 + + :return: XML object + + """ + issuer = saml.Issuer() + issuer.format = saml.NAMEID_FORMAT_ENTITY + issuer.set_text(issuer_url) + return issuer + + def _create_subject(self, user, expiration_time, recipient): + """Create an object that represents a SAML Subject. + + + + john@smith.com + + + + + + :return: XML object + + """ + name_id = saml.NameID() + name_id.set_text(user) + subject_conf_data = saml.SubjectConfirmationData() + subject_conf_data.recipient = recipient + subject_conf_data.not_on_or_after = expiration_time + subject_conf = saml.SubjectConfirmation() + subject_conf.method = saml.SCM_BEARER + subject_conf.subject_confirmation_data = subject_conf_data + subject = saml.Subject() + subject.subject_confirmation = subject_conf + subject.name_id = name_id + return subject + + def _create_attribute_statement(self, user, roles, project): + """Create an object that represents a SAML AttributeStatement. + + + + test_user + + + admin + member + + + development + + + + :return: XML object + + """ + openstack_user = 'openstack_user' + user_attribute = saml.Attribute() + user_attribute.name = openstack_user + user_value = saml.AttributeValue() + user_value.set_text(user) + user_attribute.attribute_value = user_value + + openstack_roles = 'openstack_roles' + roles_attribute = saml.Attribute() + roles_attribute.name = openstack_roles + + for role in roles: + role_value = saml.AttributeValue() + role_value.set_text(role) + roles_attribute.attribute_value.append(role_value) + + openstack_project = 'openstack_project' + project_attribute = saml.Attribute() + project_attribute.name = openstack_project + project_value = saml.AttributeValue() + project_value.set_text(project) + project_attribute.attribute_value = project_value + + attribute_statement = saml.AttributeStatement() + attribute_statement.attribute.append(user_attribute) + attribute_statement.attribute.append(roles_attribute) + attribute_statement.attribute.append(project_attribute) + return attribute_statement + + def _create_authn_statement(self, issuer, expiration_time): + """Create an object that represents a SAML AuthnStatement. + + + + + urn:oasis:names:tc:SAML:2.0:ac:classes:Password + + + https://acme.com/FIM/sps/openstack/saml20 + + + + + :return: XML object + + """ + authn_statement = saml.AuthnStatement() + authn_statement.authn_instant = timeutils.isotime() + authn_statement.session_index = uuid.uuid4().hex + authn_statement.session_not_on_or_after = expiration_time + + authn_context = saml.AuthnContext() + authn_context_class = saml.AuthnContextClassRef() + authn_context_class.set_text(saml.AUTHN_PASSWORD) + + authn_authority = saml.AuthenticatingAuthority() + authn_authority.set_text(issuer) + authn_context.authn_context_class_ref = authn_context_class + authn_context.authenticating_authority = authn_authority + + authn_statement.authn_context = authn_context + + return authn_statement + + def _create_assertion(self, issuer, signature, subject, authn_statement, + attribute_statement): + """Create an object that represents a SAML Assertion. + + + ... + ... + ... + ... + ... + + + :return: XML object + + """ + assertion = saml.Assertion() + assertion.id = self.assertion_id + assertion.issue_instant = timeutils.isotime() + assertion.version = '2.0' + assertion.issuer = issuer + assertion.signature = signature + assertion.subject = subject + assertion.authn_statement = authn_statement + assertion.attribute_statement = attribute_statement + return assertion + + def _create_response(self, issuer, status, assertion, recipient): + """Create an object that represents a SAML Response. + + + ... + ... + ... + + + :return: XML object + + """ + response = samlp.Response() + response.id = uuid.uuid4().hex + response.destination = recipient + response.issue_instant = timeutils.isotime() + response.version = '2.0' + response.issuer = issuer + response.status = status + response.assertion = assertion + return response + + def _create_signature(self): + """Create an object that represents a SAML . + + This must be filled with algorithms that the signing binary will apply + in order to sign the whole message. + Currently we enforce X509 signing. + Example of the template:: + + + + + + + + + + + + + + + + + + + + + :return: XML object + + """ + canonicalization_method = xmldsig.CanonicalizationMethod() + canonicalization_method.algorithm = xmldsig.ALG_EXC_C14N + signature_method = xmldsig.SignatureMethod( + algorithm=xmldsig.SIG_RSA_SHA1) + + transforms = xmldsig.Transforms() + envelope_transform = xmldsig.Transform( + algorithm=xmldsig.TRANSFORM_ENVELOPED) + + c14_transform = xmldsig.Transform(algorithm=xmldsig.ALG_EXC_C14N) + transforms.transform = [envelope_transform, c14_transform] + + digest_method = xmldsig.DigestMethod(algorithm=xmldsig.DIGEST_SHA1) + digest_value = xmldsig.DigestValue() + + reference = xmldsig.Reference() + reference.uri = '#' + self.assertion_id + reference.digest_method = digest_method + reference.digest_value = digest_value + reference.transforms = transforms + + signed_info = xmldsig.SignedInfo() + signed_info.canonicalization_method = canonicalization_method + signed_info.signature_method = signature_method + signed_info.reference = reference + + key_info = xmldsig.KeyInfo() + key_info.x509_data = xmldsig.X509Data() + + signature = xmldsig.Signature() + signature.signed_info = signed_info + signature.signature_value = xmldsig.SignatureValue() + signature.key_info = key_info + + return signature + + +def _sign_assertion(assertion): + """Sign a SAML assertion. + + This method utilizes ``xmlsec1`` binary and signs SAML assertions in a + separate process. ``xmlsec1`` cannot read input data from stdin so the + prepared assertion needs to be serialized and stored in a temporary + file. This file will be deleted immediately after ``xmlsec1`` returns. + The signed assertion is redirected to a standard output and read using + subprocess.PIPE redirection. A ``saml.Assertion`` class is created + from the signed string again and returned. + + Parameters that are required in the CONF:: + * xmlsec_binary + * private key file path + * public key file path + :return: XML object + + """ + xmlsec_binary = CONF.saml.xmlsec1_binary + idp_private_key = CONF.saml.keyfile + idp_public_key = CONF.saml.certfile + + # xmlsec1 --sign --privkey-pem privkey,cert --id-attr:ID + certificates = '%(idp_private_key)s,%(idp_public_key)s' % { + 'idp_public_key': idp_public_key, + 'idp_private_key': idp_private_key + } + + command_list = [xmlsec_binary, '--sign', '--privkey-pem', certificates, + '--id-attr:ID', 'Assertion'] + + try: + # NOTE(gyee): need to make the namespace prefixes explicit so + # they won't get reassigned when we wrap the assertion into + # SAML2 response + file_path = fileutils.write_to_tempfile(assertion.to_string( + nspair={'saml': saml2.NAMESPACE, + 'xmldsig': xmldsig.NAMESPACE})) + command_list.append(file_path) + stdout = subprocess.check_output(command_list) + except Exception as e: + msg = _LE('Error when signing assertion, reason: %(reason)s') + msg = msg % {'reason': e} + LOG.error(msg) + raise exception.SAMLSigningError(reason=e) + finally: + try: + os.remove(file_path) + except OSError: + pass + + return saml2.create_class_from_xml_string(saml.Assertion, stdout) + + +class MetadataGenerator(object): + """A class for generating SAML IdP Metadata.""" + + def generate_metadata(self): + """Generate Identity Provider Metadata. + + Generate and format metadata into XML that can be exposed and + consumed by a federated Service Provider. + + :return: XML object. + :raises: keystone.exception.ValidationError: Raises if the required + config options aren't set. + + """ + self._ensure_required_values_present() + entity_descriptor = self._create_entity_descriptor() + entity_descriptor.idpsso_descriptor = ( + self._create_idp_sso_descriptor()) + return entity_descriptor + + def _create_entity_descriptor(self): + ed = md.EntityDescriptor() + ed.entity_id = CONF.saml.idp_entity_id + return ed + + def _create_idp_sso_descriptor(self): + + def get_cert(): + try: + return sigver.read_cert_from_file(CONF.saml.certfile, 'pem') + except (IOError, sigver.CertificateError) as e: + msg = _('Cannot open certificate %(cert_file)s. ' + 'Reason: %(reason)s') + msg = msg % {'cert_file': CONF.saml.certfile, 'reason': e} + LOG.error(msg) + raise IOError(msg) + + def key_descriptor(): + cert = get_cert() + return md.KeyDescriptor( + key_info=xmldsig.KeyInfo( + x509_data=xmldsig.X509Data( + x509_certificate=xmldsig.X509Certificate(text=cert) + ) + ), use='signing' + ) + + def single_sign_on_service(): + idp_sso_endpoint = CONF.saml.idp_sso_endpoint + return md.SingleSignOnService( + binding=saml2.BINDING_URI, + location=idp_sso_endpoint) + + def organization(): + name = md.OrganizationName(lang=CONF.saml.idp_lang, + text=CONF.saml.idp_organization_name) + display_name = md.OrganizationDisplayName( + lang=CONF.saml.idp_lang, + text=CONF.saml.idp_organization_display_name) + url = md.OrganizationURL(lang=CONF.saml.idp_lang, + text=CONF.saml.idp_organization_url) + + return md.Organization( + organization_display_name=display_name, + organization_url=url, organization_name=name) + + def contact_person(): + company = md.Company(text=CONF.saml.idp_contact_company) + given_name = md.GivenName(text=CONF.saml.idp_contact_name) + surname = md.SurName(text=CONF.saml.idp_contact_surname) + email = md.EmailAddress(text=CONF.saml.idp_contact_email) + telephone = md.TelephoneNumber( + text=CONF.saml.idp_contact_telephone) + contact_type = CONF.saml.idp_contact_type + + return md.ContactPerson( + company=company, given_name=given_name, sur_name=surname, + email_address=email, telephone_number=telephone, + contact_type=contact_type) + + def name_id_format(): + return md.NameIDFormat(text=saml.NAMEID_FORMAT_TRANSIENT) + + idpsso = md.IDPSSODescriptor() + idpsso.protocol_support_enumeration = samlp.NAMESPACE + idpsso.key_descriptor = key_descriptor() + idpsso.single_sign_on_service = single_sign_on_service() + idpsso.name_id_format = name_id_format() + if self._check_organization_values(): + idpsso.organization = organization() + if self._check_contact_person_values(): + idpsso.contact_person = contact_person() + return idpsso + + def _ensure_required_values_present(self): + """Ensure idp_sso_endpoint and idp_entity_id have values.""" + + if CONF.saml.idp_entity_id is None: + msg = _('Ensure configuration option idp_entity_id is set.') + raise exception.ValidationError(msg) + if CONF.saml.idp_sso_endpoint is None: + msg = _('Ensure configuration option idp_sso_endpoint is set.') + raise exception.ValidationError(msg) + + def _check_contact_person_values(self): + """Determine if contact information is included in metadata.""" + + # Check if we should include contact information + params = [CONF.saml.idp_contact_company, + CONF.saml.idp_contact_name, + CONF.saml.idp_contact_surname, + CONF.saml.idp_contact_email, + CONF.saml.idp_contact_telephone] + for value in params: + if value is None: + return False + + # Check if contact type is an invalid value + valid_type_values = ['technical', 'other', 'support', 'administrative', + 'billing'] + if CONF.saml.idp_contact_type not in valid_type_values: + msg = _('idp_contact_type must be one of: [technical, other, ' + 'support, administrative or billing.') + raise exception.ValidationError(msg) + return True + + def _check_organization_values(self): + """Determine if organization information is included in metadata.""" + + params = [CONF.saml.idp_organization_name, + CONF.saml.idp_organization_display_name, + CONF.saml.idp_organization_url] + for value in params: + if value is None: + return False + return True diff --git a/keystone-moon/keystone/contrib/federation/migrate_repo/__init__.py b/keystone-moon/keystone/contrib/federation/migrate_repo/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/contrib/federation/migrate_repo/migrate.cfg b/keystone-moon/keystone/contrib/federation/migrate_repo/migrate.cfg new file mode 100644 index 00000000..464ab62b --- /dev/null +++ b/keystone-moon/keystone/contrib/federation/migrate_repo/migrate.cfg @@ -0,0 +1,25 @@ +[db_settings] +# Used to identify which repository this database is versioned under. +# You can use the name of your project. +repository_id=federation + +# The name of the database table used to track the schema version. +# This name shouldn't already be used by your project. +# If this is changed once a database is under version control, you'll need to +# change the table name in each database too. +version_table=migrate_version + +# When committing a change script, Migrate will attempt to generate the +# sql for all supported databases; normally, if one of them fails - probably +# because you don't have that database installed - it is ignored and the +# commit continues, perhaps ending successfully. +# Databases in this list MUST compile successfully during a commit, or the +# entire commit will fail. List the databases your application will actually +# be using to ensure your updates to that database work properly. +# This must be a list; example: ['postgres','sqlite'] +required_dbs=[] + +# When creating new change scripts, Migrate will stamp the new script with +# a version number. By default this is latest_version + 1. You can set this +# to 'true' to tell Migrate to use the UTC timestamp instead. +use_timestamp_numbering=False diff --git a/keystone-moon/keystone/contrib/federation/migrate_repo/versions/001_add_identity_provider_table.py b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/001_add_identity_provider_table.py new file mode 100644 index 00000000..cfb6f2c4 --- /dev/null +++ b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/001_add_identity_provider_table.py @@ -0,0 +1,51 @@ +# 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 sqlalchemy as sql + + +def upgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + + idp_table = sql.Table( + 'identity_provider', + meta, + sql.Column('id', sql.String(64), primary_key=True), + sql.Column('enabled', sql.Boolean, nullable=False), + sql.Column('description', sql.Text(), nullable=True), + mysql_engine='InnoDB', + mysql_charset='utf8') + + idp_table.create(migrate_engine, checkfirst=True) + + federation_protocol_table = sql.Table( + 'federation_protocol', + meta, + sql.Column('id', sql.String(64), primary_key=True), + sql.Column('idp_id', sql.String(64), + sql.ForeignKey('identity_provider.id', ondelete='CASCADE'), + primary_key=True), + sql.Column('mapping_id', sql.String(64), nullable=True), + mysql_engine='InnoDB', + mysql_charset='utf8') + + federation_protocol_table.create(migrate_engine, checkfirst=True) + + +def downgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + tables = ['federation_protocol', 'identity_provider'] + for table_name in tables: + table = sql.Table(table_name, meta, autoload=True) + table.drop() diff --git a/keystone-moon/keystone/contrib/federation/migrate_repo/versions/002_add_mapping_tables.py b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/002_add_mapping_tables.py new file mode 100644 index 00000000..f827f9a9 --- /dev/null +++ b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/002_add_mapping_tables.py @@ -0,0 +1,37 @@ +# 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 sqlalchemy as sql + + +def upgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + + mapping_table = sql.Table( + 'mapping', + meta, + sql.Column('id', sql.String(64), primary_key=True), + sql.Column('rules', sql.Text(), nullable=False), + mysql_engine='InnoDB', + mysql_charset='utf8') + mapping_table.create(migrate_engine, checkfirst=True) + + +def downgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + # Drop previously created tables + tables = ['mapping'] + for table_name in tables: + table = sql.Table(table_name, meta, autoload=True) + table.drop() diff --git a/keystone-moon/keystone/contrib/federation/migrate_repo/versions/003_mapping_id_nullable_false.py b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/003_mapping_id_nullable_false.py new file mode 100644 index 00000000..eb8b2378 --- /dev/null +++ b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/003_mapping_id_nullable_false.py @@ -0,0 +1,35 @@ +# Copyright 2014 Mirantis.inc +# All Rights Reserved. +# +# 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 sqlalchemy as sa + + +def upgrade(migrate_engine): + meta = sa.MetaData(bind=migrate_engine) + federation_protocol = sa.Table('federation_protocol', meta, autoload=True) + # NOTE(i159): The column is changed to non-nullable. To prevent + # database errors when the column will be altered, all the existing + # null-records should be filled with not null values. + stmt = (federation_protocol.update(). + where(federation_protocol.c.mapping_id.is_(None)). + values(mapping_id='')) + migrate_engine.execute(stmt) + federation_protocol.c.mapping_id.alter(nullable=False) + + +def downgrade(migrate_engine): + meta = sa.MetaData(bind=migrate_engine) + federation_protocol = sa.Table('federation_protocol', meta, autoload=True) + federation_protocol.c.mapping_id.alter(nullable=True) diff --git a/keystone-moon/keystone/contrib/federation/migrate_repo/versions/004_add_remote_id_column.py b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/004_add_remote_id_column.py new file mode 100644 index 00000000..dbe5d1f1 --- /dev/null +++ b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/004_add_remote_id_column.py @@ -0,0 +1,30 @@ +# 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 oslo_db.sqlalchemy import utils +import sqlalchemy as sql + + +def upgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + + idp_table = utils.get_table(migrate_engine, 'identity_provider') + remote_id = sql.Column('remote_id', sql.String(256), nullable=True) + idp_table.create_column(remote_id) + + +def downgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + idp_table = utils.get_table(migrate_engine, 'identity_provider') + idp_table.drop_column('remote_id') diff --git a/keystone-moon/keystone/contrib/federation/migrate_repo/versions/005_add_service_provider_table.py b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/005_add_service_provider_table.py new file mode 100644 index 00000000..bff6a252 --- /dev/null +++ b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/005_add_service_provider_table.py @@ -0,0 +1,38 @@ +# 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 sqlalchemy as sql + + +def upgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + + sp_table = sql.Table( + 'service_provider', + meta, + sql.Column('auth_url', sql.String(256), nullable=True), + sql.Column('id', sql.String(64), primary_key=True), + sql.Column('enabled', sql.Boolean, nullable=False), + sql.Column('description', sql.Text(), nullable=True), + sql.Column('sp_url', sql.String(256), nullable=True), + mysql_engine='InnoDB', + mysql_charset='utf8') + + sp_table.create(migrate_engine, checkfirst=True) + + +def downgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + table = sql.Table('service_provider', meta, autoload=True) + table.drop() diff --git a/keystone-moon/keystone/contrib/federation/migrate_repo/versions/006_fixup_service_provider_attributes.py b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/006_fixup_service_provider_attributes.py new file mode 100644 index 00000000..8a42ce3a --- /dev/null +++ b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/006_fixup_service_provider_attributes.py @@ -0,0 +1,48 @@ +# 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 sqlalchemy as sql + +_SP_TABLE_NAME = 'service_provider' + + +def _update_null_columns(migrate_engine, sp_table): + stmt = (sp_table.update(). + where(sp_table.c.auth_url.is_(None)). + values(auth_url='')) + migrate_engine.execute(stmt) + + stmt = (sp_table.update(). + where(sp_table.c.sp_url.is_(None)). + values(sp_url='')) + migrate_engine.execute(stmt) + + +def upgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + sp_table = sql.Table(_SP_TABLE_NAME, meta, autoload=True) + # The columns are being changed to non-nullable. To prevent + # database errors when both are altered, all the existing + # null-records should be filled with not null values. + _update_null_columns(migrate_engine, sp_table) + + sp_table.c.auth_url.alter(nullable=False) + sp_table.c.sp_url.alter(nullable=False) + + +def downgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + sp_table = sql.Table(_SP_TABLE_NAME, meta, autoload=True) + sp_table.c.auth_url.alter(nullable=True) + sp_table.c.sp_url.alter(nullable=True) diff --git a/keystone-moon/keystone/contrib/federation/migrate_repo/versions/__init__.py b/keystone-moon/keystone/contrib/federation/migrate_repo/versions/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/contrib/federation/routers.py b/keystone-moon/keystone/contrib/federation/routers.py new file mode 100644 index 00000000..9a6224b7 --- /dev/null +++ b/keystone-moon/keystone/contrib/federation/routers.py @@ -0,0 +1,226 @@ +# 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 functools + +from keystone.common import json_home +from keystone.common import wsgi +from keystone.contrib.federation import controllers + + +build_resource_relation = functools.partial( + json_home.build_v3_extension_resource_relation, + extension_name='OS-FEDERATION', extension_version='1.0') + +build_parameter_relation = functools.partial( + json_home.build_v3_extension_parameter_relation, + extension_name='OS-FEDERATION', extension_version='1.0') + +IDP_ID_PARAMETER_RELATION = build_parameter_relation(parameter_name='idp_id') +PROTOCOL_ID_PARAMETER_RELATION = build_parameter_relation( + parameter_name='protocol_id') +SP_ID_PARAMETER_RELATION = build_parameter_relation(parameter_name='sp_id') + + +class FederationExtension(wsgi.V3ExtensionRouter): + """API Endpoints for the Federation extension. + + The API looks like:: + + PUT /OS-FEDERATION/identity_providers/$identity_provider + GET /OS-FEDERATION/identity_providers + GET /OS-FEDERATION/identity_providers/$identity_provider + DELETE /OS-FEDERATION/identity_providers/$identity_provider + PATCH /OS-FEDERATION/identity_providers/$identity_provider + + PUT /OS-FEDERATION/identity_providers/ + $identity_provider/protocols/$protocol + GET /OS-FEDERATION/identity_providers/ + $identity_provider/protocols + GET /OS-FEDERATION/identity_providers/ + $identity_provider/protocols/$protocol + PATCH /OS-FEDERATION/identity_providers/ + $identity_provider/protocols/$protocol + DELETE /OS-FEDERATION/identity_providers/ + $identity_provider/protocols/$protocol + + PUT /OS-FEDERATION/mappings + GET /OS-FEDERATION/mappings + PATCH /OS-FEDERATION/mappings/$mapping_id + GET /OS-FEDERATION/mappings/$mapping_id + DELETE /OS-FEDERATION/mappings/$mapping_id + + GET /OS-FEDERATION/projects + GET /OS-FEDERATION/domains + + PUT /OS-FEDERATION/service_providers/$service_provider + GET /OS-FEDERATION/service_providers + GET /OS-FEDERATION/service_providers/$service_provider + DELETE /OS-FEDERATION/service_providers/$service_provider + PATCH /OS-FEDERATION/service_providers/$service_provider + + GET /OS-FEDERATION/identity_providers/$identity_provider/ + protocols/$protocol/auth + POST /OS-FEDERATION/identity_providers/$identity_provider/ + protocols/$protocol/auth + + POST /auth/OS-FEDERATION/saml2 + GET /OS-FEDERATION/saml2/metadata + + GET /auth/OS-FEDERATION/websso/{protocol_id} + ?origin=https%3A//horizon.example.com + + POST /auth/OS-FEDERATION/websso/{protocol_id} + ?origin=https%3A//horizon.example.com + + """ + def _construct_url(self, suffix): + return "/OS-FEDERATION/%s" % suffix + + def add_routes(self, mapper): + auth_controller = controllers.Auth() + idp_controller = controllers.IdentityProvider() + protocol_controller = controllers.FederationProtocol() + mapping_controller = controllers.MappingController() + project_controller = controllers.ProjectAssignmentV3() + domain_controller = controllers.DomainV3() + saml_metadata_controller = controllers.SAMLMetadataV3() + sp_controller = controllers.ServiceProvider() + + # Identity Provider CRUD operations + + self._add_resource( + mapper, idp_controller, + path=self._construct_url('identity_providers/{idp_id}'), + get_action='get_identity_provider', + put_action='create_identity_provider', + patch_action='update_identity_provider', + delete_action='delete_identity_provider', + rel=build_resource_relation(resource_name='identity_provider'), + path_vars={ + 'idp_id': IDP_ID_PARAMETER_RELATION, + }) + self._add_resource( + mapper, idp_controller, + path=self._construct_url('identity_providers'), + get_action='list_identity_providers', + rel=build_resource_relation(resource_name='identity_providers')) + + # Protocol CRUD operations + + self._add_resource( + mapper, protocol_controller, + path=self._construct_url('identity_providers/{idp_id}/protocols/' + '{protocol_id}'), + get_action='get_protocol', + put_action='create_protocol', + patch_action='update_protocol', + delete_action='delete_protocol', + rel=build_resource_relation( + resource_name='identity_provider_protocol'), + path_vars={ + 'idp_id': IDP_ID_PARAMETER_RELATION, + 'protocol_id': PROTOCOL_ID_PARAMETER_RELATION, + }) + self._add_resource( + mapper, protocol_controller, + path=self._construct_url('identity_providers/{idp_id}/protocols'), + get_action='list_protocols', + rel=build_resource_relation( + resource_name='identity_provider_protocols'), + path_vars={ + 'idp_id': IDP_ID_PARAMETER_RELATION, + }) + + # Mapping CRUD operations + + self._add_resource( + mapper, mapping_controller, + path=self._construct_url('mappings/{mapping_id}'), + get_action='get_mapping', + put_action='create_mapping', + patch_action='update_mapping', + delete_action='delete_mapping', + rel=build_resource_relation(resource_name='mapping'), + path_vars={ + 'mapping_id': build_parameter_relation( + parameter_name='mapping_id'), + }) + self._add_resource( + mapper, mapping_controller, + path=self._construct_url('mappings'), + get_action='list_mappings', + rel=build_resource_relation(resource_name='mappings')) + + # Service Providers CRUD operations + + self._add_resource( + mapper, sp_controller, + path=self._construct_url('service_providers/{sp_id}'), + get_action='get_service_provider', + put_action='create_service_provider', + patch_action='update_service_provider', + delete_action='delete_service_provider', + rel=build_resource_relation(resource_name='service_provider'), + path_vars={ + 'sp_id': SP_ID_PARAMETER_RELATION, + }) + + self._add_resource( + mapper, sp_controller, + path=self._construct_url('service_providers'), + get_action='list_service_providers', + rel=build_resource_relation(resource_name='service_providers')) + + self._add_resource( + mapper, domain_controller, + path=self._construct_url('domains'), + get_action='list_domains_for_groups', + rel=build_resource_relation(resource_name='domains')) + self._add_resource( + mapper, project_controller, + path=self._construct_url('projects'), + get_action='list_projects_for_groups', + rel=build_resource_relation(resource_name='projects')) + self._add_resource( + mapper, auth_controller, + path=self._construct_url('identity_providers/{identity_provider}/' + 'protocols/{protocol}/auth'), + get_post_action='federated_authentication', + rel=build_resource_relation( + resource_name='identity_provider_protocol_auth'), + path_vars={ + 'identity_provider': IDP_ID_PARAMETER_RELATION, + 'protocol': PROTOCOL_ID_PARAMETER_RELATION, + }) + + # Auth operations + self._add_resource( + mapper, auth_controller, + path='/auth' + self._construct_url('saml2'), + post_action='create_saml_assertion', + rel=build_resource_relation(resource_name='saml2')) + self._add_resource( + mapper, auth_controller, + path='/auth' + self._construct_url('websso/{protocol_id}'), + get_post_action='federated_sso_auth', + rel=build_resource_relation(resource_name='websso'), + path_vars={ + 'protocol_id': PROTOCOL_ID_PARAMETER_RELATION, + }) + + # Keystone-Identity-Provider metadata endpoint + self._add_resource( + mapper, saml_metadata_controller, + path=self._construct_url('saml2/metadata'), + get_action='get_metadata', + rel=build_resource_relation(resource_name='metadata')) diff --git a/keystone-moon/keystone/contrib/federation/schema.py b/keystone-moon/keystone/contrib/federation/schema.py new file mode 100644 index 00000000..645e1129 --- /dev/null +++ b/keystone-moon/keystone/contrib/federation/schema.py @@ -0,0 +1,78 @@ +# 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 keystone.common import validation +from keystone.common.validation import parameter_types + + +basic_property_id = { + 'type': 'object', + 'properties': { + 'id': { + 'type': 'string' + } + }, + 'required': ['id'], + 'additionalProperties': False +} + +saml_create = { + 'type': 'object', + 'properties': { + 'identity': { + 'type': 'object', + 'properties': { + 'token': basic_property_id, + 'methods': { + 'type': 'array' + } + }, + 'required': ['token'], + 'additionalProperties': False + }, + 'scope': { + 'type': 'object', + 'properties': { + 'service_provider': basic_property_id + }, + 'required': ['service_provider'], + 'additionalProperties': False + }, + }, + 'required': ['identity', 'scope'], + 'additionalProperties': False +} + +_service_provider_properties = { + # NOTE(rodrigods): The database accepts URLs with 256 as max length, + # but parameter_types.url uses 225 as max length. + 'auth_url': parameter_types.url, + 'sp_url': parameter_types.url, + 'description': validation.nullable(parameter_types.description), + 'enabled': parameter_types.boolean +} + +service_provider_create = { + 'type': 'object', + 'properties': _service_provider_properties, + # NOTE(rodrigods): 'id' is not required since it is passed in the URL + 'required': ['auth_url', 'sp_url'], + 'additionalProperties': False +} + +service_provider_update = { + 'type': 'object', + 'properties': _service_provider_properties, + # Make sure at least one property is being updated + 'minProperties': 1, + 'additionalProperties': False +} diff --git a/keystone-moon/keystone/contrib/federation/utils.py b/keystone-moon/keystone/contrib/federation/utils.py new file mode 100644 index 00000000..939fe9a0 --- /dev/null +++ b/keystone-moon/keystone/contrib/federation/utils.py @@ -0,0 +1,763 @@ +# 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. + +"""Utilities for Federation Extension.""" + +import ast +import re + +import jsonschema +from oslo_config import cfg +from oslo_log import log +from oslo_utils import timeutils +import six + +from keystone.contrib import federation +from keystone import exception +from keystone.i18n import _, _LW + + +CONF = cfg.CONF +LOG = log.getLogger(__name__) + + +MAPPING_SCHEMA = { + "type": "object", + "required": ['rules'], + "properties": { + "rules": { + "minItems": 1, + "type": "array", + "items": { + "type": "object", + "required": ['local', 'remote'], + "additionalProperties": False, + "properties": { + "local": { + "type": "array" + }, + "remote": { + "minItems": 1, + "type": "array", + "items": { + "type": "object", + "oneOf": [ + {"$ref": "#/definitions/empty"}, + {"$ref": "#/definitions/any_one_of"}, + {"$ref": "#/definitions/not_any_of"}, + {"$ref": "#/definitions/blacklist"}, + {"$ref": "#/definitions/whitelist"} + ], + } + } + } + } + } + }, + "definitions": { + "empty": { + "type": "object", + "required": ['type'], + "properties": { + "type": { + "type": "string" + }, + }, + "additionalProperties": False, + }, + "any_one_of": { + "type": "object", + "additionalProperties": False, + "required": ['type', 'any_one_of'], + "properties": { + "type": { + "type": "string" + }, + "any_one_of": { + "type": "array" + }, + "regex": { + "type": "boolean" + } + } + }, + "not_any_of": { + "type": "object", + "additionalProperties": False, + "required": ['type', 'not_any_of'], + "properties": { + "type": { + "type": "string" + }, + "not_any_of": { + "type": "array" + }, + "regex": { + "type": "boolean" + } + } + }, + "blacklist": { + "type": "object", + "additionalProperties": False, + "required": ['type', 'blacklist'], + "properties": { + "type": { + "type": "string" + }, + "blacklist": { + "type": "array" + } + } + }, + "whitelist": { + "type": "object", + "additionalProperties": False, + "required": ['type', 'whitelist'], + "properties": { + "type": { + "type": "string" + }, + "whitelist": { + "type": "array" + } + } + } + } +} + + +class DirectMaps(object): + """An abstraction around the remote matches. + + Each match is treated internally as a list. + """ + + def __init__(self): + self._matches = [] + + def add(self, values): + """Adds a matched value to the list of matches. + + :param list value: the match to save + + """ + self._matches.append(values) + + def __getitem__(self, idx): + """Used by Python when executing ``''.format(*DirectMaps())``.""" + value = self._matches[idx] + if isinstance(value, list) and len(value) == 1: + return value[0] + else: + return value + + +def validate_mapping_structure(ref): + v = jsonschema.Draft4Validator(MAPPING_SCHEMA) + + messages = '' + for error in sorted(v.iter_errors(ref), key=str): + messages = messages + error.message + "\n" + + if messages: + raise exception.ValidationError(messages) + + +def validate_expiration(token_ref): + if timeutils.utcnow() > token_ref.expires: + raise exception.Unauthorized(_('Federation token is expired')) + + +def validate_groups_cardinality(group_ids, mapping_id): + """Check if groups list is non-empty. + + :param group_ids: list of group ids + :type group_ids: list of str + + :raises exception.MissingGroups: if ``group_ids`` cardinality is 0 + + """ + if not group_ids: + raise exception.MissingGroups(mapping_id=mapping_id) + + +def validate_idp(idp, assertion): + """Check if the IdP providing the assertion is the one registered for + the mapping + """ + remote_id_parameter = CONF.federation.remote_id_attribute + if not remote_id_parameter or not idp['remote_id']: + LOG.warning(_LW('Impossible to identify the IdP %s '), + idp['id']) + # If nothing is defined, the administrator may want to + # allow the mapping of every IdP + return + try: + idp_remote_identifier = assertion[remote_id_parameter] + except KeyError: + msg = _('Could not find Identity Provider identifier in ' + 'environment, check [federation] remote_id_attribute ' + 'for details.') + raise exception.ValidationError(msg) + if idp_remote_identifier != idp['remote_id']: + msg = _('Incoming identity provider identifier not included ' + 'among the accepted identifiers.') + raise exception.Forbidden(msg) + + +def validate_groups_in_backend(group_ids, mapping_id, identity_api): + """Iterate over group ids and make sure they are present in the backend/ + + This call is not transactional. + :param group_ids: IDs of the groups to be checked + :type group_ids: list of str + + :param mapping_id: id of the mapping used for this operation + :type mapping_id: str + + :param identity_api: Identity Manager object used for communication with + backend + :type identity_api: identity.Manager + + :raises: exception.MappedGroupNotFound + + """ + for group_id in group_ids: + try: + identity_api.get_group(group_id) + except exception.GroupNotFound: + raise exception.MappedGroupNotFound( + group_id=group_id, mapping_id=mapping_id) + + +def validate_groups(group_ids, mapping_id, identity_api): + """Check group ids cardinality and check their existence in the backend. + + This call is not transactional. + :param group_ids: IDs of the groups to be checked + :type group_ids: list of str + + :param mapping_id: id of the mapping used for this operation + :type mapping_id: str + + :param identity_api: Identity Manager object used for communication with + backend + :type identity_api: identity.Manager + + :raises: exception.MappedGroupNotFound + :raises: exception.MissingGroups + + """ + validate_groups_cardinality(group_ids, mapping_id) + validate_groups_in_backend(group_ids, mapping_id, identity_api) + + +# TODO(marek-denis): Optimize this function, so the number of calls to the +# backend are minimized. +def transform_to_group_ids(group_names, mapping_id, + identity_api, assignment_api): + """Transform groups identitified by name/domain to their ids + + Function accepts list of groups identified by a name and domain giving + a list of group ids in return. + + Example of group_names parameter:: + + [ + { + "name": "group_name", + "domain": { + "id": "domain_id" + }, + }, + { + "name": "group_name_2", + "domain": { + "name": "domain_name" + } + } + ] + + :param group_names: list of group identified by name and its domain. + :type group_names: list + + :param mapping_id: id of the mapping used for mapping assertion into + local credentials + :type mapping_id: str + + :param identity_api: identity_api object + :param assignment_api: assignment_api object + + :returns: generator object with group ids + + :raises: excepton.MappedGroupNotFound: in case asked group doesn't + exist in the backend. + + """ + + def resolve_domain(domain): + """Return domain id. + + Input is a dictionary with a domain identified either by a ``id`` or a + ``name``. In the latter case system will attempt to fetch domain object + from the backend. + + :returns: domain's id + :rtype: str + + """ + domain_id = (domain.get('id') or + assignment_api.get_domain_by_name( + domain.get('name')).get('id')) + return domain_id + + for group in group_names: + try: + group_dict = identity_api.get_group_by_name( + group['name'], resolve_domain(group['domain'])) + yield group_dict['id'] + except exception.GroupNotFound: + LOG.debug('Skip mapping group %s; has no entry in the backend', + group['name']) + + +def get_assertion_params_from_env(context): + LOG.debug('Environment variables: %s', context['environment']) + prefix = CONF.federation.assertion_prefix + for k, v in context['environment'].items(): + if k.startswith(prefix): + yield (k, v) + + +class UserType(object): + """User mapping type.""" + EPHEMERAL = 'ephemeral' + LOCAL = 'local' + + +class RuleProcessor(object): + """A class to process assertions and mapping rules.""" + + class _EvalType(object): + """Mapping rule evaluation types.""" + ANY_ONE_OF = 'any_one_of' + NOT_ANY_OF = 'not_any_of' + BLACKLIST = 'blacklist' + WHITELIST = 'whitelist' + + def __init__(self, rules): + """Initialize RuleProcessor. + + Example rules can be found at: + :class:`keystone.tests.mapping_fixtures` + + :param rules: rules from a mapping + :type rules: dict + + """ + + self.rules = rules + + def process(self, assertion_data): + """Transform assertion to a dictionary of user name and group ids + based on mapping rules. + + This function will iterate through the mapping rules to find + assertions that are valid. + + :param assertion_data: an assertion containing values from an IdP + :type assertion_data: dict + + Example assertion_data:: + + { + 'Email': 'testacct@example.com', + 'UserName': 'testacct', + 'FirstName': 'Test', + 'LastName': 'Account', + 'orgPersonType': 'Tester' + } + + :returns: dictionary with user and group_ids + + The expected return structure is:: + + { + 'name': 'foobar', + 'group_ids': ['abc123', 'def456'], + 'group_names': [ + { + 'name': 'group_name_1', + 'domain': { + 'name': 'domain1' + } + }, + { + 'name': 'group_name_1_1', + 'domain': { + 'name': 'domain1' + } + }, + { + 'name': 'group_name_2', + 'domain': { + 'id': 'xyz132' + } + } + ] + } + + """ + + # Assertions will come in as string key-value pairs, and will use a + # semi-colon to indicate multiple values, i.e. groups. + # This will create a new dictionary where the values are arrays, and + # any multiple values are stored in the arrays. + LOG.debug('assertion data: %s', assertion_data) + assertion = {n: v.split(';') for n, v in assertion_data.items() + if isinstance(v, six.string_types)} + LOG.debug('assertion: %s', assertion) + identity_values = [] + + LOG.debug('rules: %s', self.rules) + for rule in self.rules: + direct_maps = self._verify_all_requirements(rule['remote'], + assertion) + + # If the compare comes back as None, then the rule did not apply + # to the assertion data, go on to the next rule + if direct_maps is None: + continue + + # If there are no direct mappings, then add the local mapping + # directly to the array of saved values. However, if there is + # a direct mapping, then perform variable replacement. + if not direct_maps: + identity_values += rule['local'] + else: + for local in rule['local']: + new_local = self._update_local_mapping(local, direct_maps) + identity_values.append(new_local) + + LOG.debug('identity_values: %s', identity_values) + mapped_properties = self._transform(identity_values) + LOG.debug('mapped_properties: %s', mapped_properties) + return mapped_properties + + def _transform(self, identity_values): + """Transform local mappings, to an easier to understand format. + + Transform the incoming array to generate the return value for + the process function. Generating content for Keystone tokens will + be easier if some pre-processing is done at this level. + + :param identity_values: local mapping from valid evaluations + :type identity_values: array of dict + + Example identity_values:: + + [ + { + 'group': {'id': '0cd5e9'}, + 'user': { + 'email': 'bob@example.com' + }, + }, + { + 'groups': ['member', 'admin', tester'], + 'domain': { + 'name': 'default_domain' + } + } + ] + + :returns: dictionary with user name, group_ids and group_names. + :rtype: dict + + """ + + def extract_groups(groups_by_domain): + for groups in groups_by_domain.values(): + for group in {g['name']: g for g in groups}.values(): + yield group + + def normalize_user(user): + """Parse and validate user mapping.""" + + user_type = user.get('type') + + if user_type and user_type not in (UserType.EPHEMERAL, + UserType.LOCAL): + msg = _("User type %s not supported") % user_type + raise exception.ValidationError(msg) + + if user_type is None: + user_type = user['type'] = UserType.EPHEMERAL + + if user_type == UserType.EPHEMERAL: + user['domain'] = { + 'id': (CONF.federation.federated_domain_name or + federation.FEDERATED_DOMAIN_KEYWORD) + } + + # initialize the group_ids as a set to eliminate duplicates + user = {} + group_ids = set() + group_names = list() + groups_by_domain = dict() + + for identity_value in identity_values: + if 'user' in identity_value: + # if a mapping outputs more than one user name, log it + if user: + LOG.warning(_LW('Ignoring user name')) + else: + user = identity_value.get('user') + if 'group' in identity_value: + group = identity_value['group'] + if 'id' in group: + group_ids.add(group['id']) + elif 'name' in group: + domain = (group['domain'].get('name') or + group['domain'].get('id')) + groups_by_domain.setdefault(domain, list()).append(group) + group_names.extend(extract_groups(groups_by_domain)) + if 'groups' in identity_value: + if 'domain' not in identity_value: + msg = _("Invalid rule: %(identity_value)s. Both 'groups' " + "and 'domain' keywords must be specified.") + msg = msg % {'identity_value': identity_value} + raise exception.ValidationError(msg) + # In this case, identity_value['groups'] is a string + # representation of a list, and we want a real list. This is + # due to the way we do direct mapping substitutions today (see + # function _update_local_mapping() ) + try: + group_names_list = ast.literal_eval( + identity_value['groups']) + except ValueError: + group_names_list = [identity_value['groups']] + domain = identity_value['domain'] + group_dicts = [{'name': name, 'domain': domain} for name in + group_names_list] + + group_names.extend(group_dicts) + + normalize_user(user) + + return {'user': user, + 'group_ids': list(group_ids), + 'group_names': group_names} + + def _update_local_mapping(self, local, direct_maps): + """Replace any {0}, {1} ... values with data from the assertion. + + :param local: local mapping reference that needs to be updated + :type local: dict + :param direct_maps: identity values used to update local + :type direct_maps: keystone.contrib.federation.utils.DirectMaps + + Example local:: + + {'user': {'name': '{0} {1}', 'email': '{2}'}} + + Example direct_maps:: + + ['Bob', 'Thompson', 'bob@example.com'] + + :returns: new local mapping reference with replaced values. + + The expected return structure is:: + + {'user': {'name': 'Bob Thompson', 'email': 'bob@example.org'}} + + """ + + LOG.debug('direct_maps: %s', direct_maps) + LOG.debug('local: %s', local) + new = {} + for k, v in six.iteritems(local): + if isinstance(v, dict): + new_value = self._update_local_mapping(v, direct_maps) + else: + new_value = v.format(*direct_maps) + new[k] = new_value + return new + + def _verify_all_requirements(self, requirements, assertion): + """Go through the remote requirements of a rule, and compare against + the assertion. + + If a value of ``None`` is returned, the rule with this assertion + doesn't apply. + If an array of zero length is returned, then there are no direct + mappings to be performed, but the rule is valid. + Otherwise, then it will first attempt to filter the values according + to blacklist or whitelist rules and finally return the values in + order, to be directly mapped. + + :param requirements: list of remote requirements from rules + :type requirements: list + + Example requirements:: + + [ + { + "type": "UserName" + }, + { + "type": "orgPersonType", + "any_one_of": [ + "Customer" + ] + }, + { + "type": "ADFS_GROUPS", + "whitelist": [ + "g1", "g2", "g3", "g4" + ] + } + ] + + :param assertion: dict of attributes from an IdP + :type assertion: dict + + Example assertion:: + + { + 'UserName': ['testacct'], + 'LastName': ['Account'], + 'orgPersonType': ['Tester'], + 'Email': ['testacct@example.com'], + 'FirstName': ['Test'], + 'ADFS_GROUPS': ['g1', 'g2'] + } + + :returns: identity values used to update local + :rtype: keystone.contrib.federation.utils.DirectMaps + + """ + + direct_maps = DirectMaps() + + for requirement in requirements: + requirement_type = requirement['type'] + regex = requirement.get('regex', False) + + any_one_values = requirement.get(self._EvalType.ANY_ONE_OF) + if any_one_values is not None: + if self._evaluate_requirement(any_one_values, + requirement_type, + self._EvalType.ANY_ONE_OF, + regex, + assertion): + continue + else: + return None + + not_any_values = requirement.get(self._EvalType.NOT_ANY_OF) + if not_any_values is not None: + if self._evaluate_requirement(not_any_values, + requirement_type, + self._EvalType.NOT_ANY_OF, + regex, + assertion): + continue + else: + return None + + # If 'any_one_of' or 'not_any_of' are not found, then values are + # within 'type'. Attempt to find that 'type' within the assertion, + # and filter these values if 'whitelist' or 'blacklist' is set. + direct_map_values = assertion.get(requirement_type) + if direct_map_values: + blacklisted_values = requirement.get(self._EvalType.BLACKLIST) + whitelisted_values = requirement.get(self._EvalType.WHITELIST) + + # If a blacklist or whitelist is used, we want to map to the + # whole list instead of just its values separately. + if blacklisted_values: + direct_map_values = [v for v in direct_map_values + if v not in blacklisted_values] + elif whitelisted_values: + direct_map_values = [v for v in direct_map_values + if v in whitelisted_values] + + direct_maps.add(direct_map_values) + + LOG.debug('updating a direct mapping: %s', direct_map_values) + + return direct_maps + + def _evaluate_values_by_regex(self, values, assertion_values): + for value in values: + for assertion_value in assertion_values: + if re.search(value, assertion_value): + return True + return False + + def _evaluate_requirement(self, values, requirement_type, + eval_type, regex, assertion): + """Evaluate the incoming requirement and assertion. + + If the requirement type does not exist in the assertion data, then + return False. If regex is specified, then compare the values and + assertion values. Otherwise, grab the intersection of the values + and use that to compare against the evaluation type. + + :param values: list of allowed values, defined in the requirement + :type values: list + :param requirement_type: key to look for in the assertion + :type requirement_type: string + :param eval_type: determine how to evaluate requirements + :type eval_type: string + :param regex: perform evaluation with regex + :type regex: boolean + :param assertion: dict of attributes from the IdP + :type assertion: dict + + :returns: boolean, whether requirement is valid or not. + + """ + + assertion_values = assertion.get(requirement_type) + if not assertion_values: + return False + + if regex: + any_match = self._evaluate_values_by_regex(values, + assertion_values) + else: + any_match = bool(set(values).intersection(set(assertion_values))) + if any_match and eval_type == self._EvalType.ANY_ONE_OF: + return True + if not any_match and eval_type == self._EvalType.NOT_ANY_OF: + return True + + return False + + +def assert_enabled_identity_provider(federation_api, idp_id): + identity_provider = federation_api.get_idp(idp_id) + if identity_provider.get('enabled') is not True: + msg = _('Identity Provider %(idp)s is disabled') % {'idp': idp_id} + LOG.debug(msg) + raise exception.Forbidden(msg) + + +def assert_enabled_service_provider_object(service_provider): + if service_provider.get('enabled') is not True: + sp_id = service_provider['id'] + msg = _('Service Provider %(sp)s is disabled') % {'sp': sp_id} + LOG.debug(msg) + raise exception.Forbidden(msg) diff --git a/keystone-moon/keystone/contrib/moon/__init__.py b/keystone-moon/keystone/contrib/moon/__init__.py new file mode 100644 index 00000000..6a96782e --- /dev/null +++ b/keystone-moon/keystone/contrib/moon/__init__.py @@ -0,0 +1,8 @@ +# Copyright 2015 Open Platform for NFV Project, Inc. and its contributors +# This software is distributed under the terms and conditions of the 'Apache-2.0' +# license which can be found in the file 'LICENSE' in this package distribution +# or at 'http://www.apache.org/licenses/LICENSE-2.0'. + +from keystone.contrib.moon.core import * # noqa +from keystone.contrib.moon import controllers # noqa +from keystone.contrib.moon import routers # noqa \ No newline at end of file diff --git a/keystone-moon/keystone/contrib/moon/backends/__init__.py b/keystone-moon/keystone/contrib/moon/backends/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/contrib/moon/backends/flat.py b/keystone-moon/keystone/contrib/moon/backends/flat.py new file mode 100644 index 00000000..6d18d3ea --- /dev/null +++ b/keystone-moon/keystone/contrib/moon/backends/flat.py @@ -0,0 +1,123 @@ +# Copyright 2015 Open Platform for NFV Project, Inc. and its contributors +# This software is distributed under the terms and conditions of the 'Apache-2.0' +# license which can be found in the file 'LICENSE' in this package distribution +# or at 'http://www.apache.org/licenses/LICENSE-2.0'. + +from uuid import uuid4 +import os +import logging +import re +import time +from keystone import config +from oslo_log import log +# from keystone.contrib.moon.core import SuperExtensionDriver +from keystone.contrib.moon.core import LogDriver + + +CONF = config.CONF + + +class LogConnector(LogDriver): + + AUTHZ_FILE = '/var/log/moon/authz.log' + TIME_FORMAT = '%Y-%m-%d-%H:%M:%S' + + def __init__(self): + # Fixme (dthom): when logging from an other class, the %appname% in the event + # is always keystone.contrib.moon.backends.flat + super(LogConnector, self).__init__() + # Configure Log to add new files in /var/log/moon/authz.log and /var/log/moon/system.log + self.LOG = log.getLogger(__name__) + self.AUTHZ_LOG = logging.getLogger("authz") + self.AUTHZ_LOG.setLevel(logging.WARNING) + fh = logging.FileHandler(self.AUTHZ_FILE) + fh.setLevel(logging.WARNING) + formatter = logging.Formatter('%(asctime)s ------ %(message)s', self.TIME_FORMAT) + fh.setFormatter(formatter) + self.AUTHZ_LOG.addHandler(fh) + + def authz(self, message): + self.AUTHZ_LOG.warn(message) + + def debug(self, message): + self.LOG.debug(message) + + def info(self, message): + self.LOG.info(message) + + def warning(self, message): + self.LOG.warning(message) + + def error(self, message): + self.LOG.error(message) + + def critical(self, message): + self.LOG.critical(message) + + def get_logs(self, options): + options = options.split(",") + self.info("Options of logs check : {}".format(options)) + event_number = None + time_from = None + time_to = None + filter_str = None + for opt in options: + if "event_number" in opt: + event_number = "".join(re.findall("\d*", opt.split("=")[-1])) + try: + event_number = int(event_number) + except ValueError: + event_number = None + elif "from" in opt: + time_from = "".join(re.findall("[\w\-:]*", opt.split("=")[-1])) + try: + time_from = time.strptime(time_from, self.TIME_FORMAT) + except ValueError: + time_from = None + elif "to" in opt: + time_to = "".join(re.findall("[\w\-:] *", opt.split("=")[-1])) + try: + time_to = time.strptime(time_to, self.TIME_FORMAT) + except ValueError: + time_to = None + elif "filter" in opt: + filter_str = "".join(re.findall("\w*", opt.split("=")[-1])) + _logs = open(self.AUTHZ_FILE).readlines() + if filter_str: + _logs = filter(lambda x: filter_str in x, _logs) + self.info("Options of logs check : {} {} {} {}".format(event_number, time_from, time_to, filter_str)) + if time_from: + try: + for log in _logs: + __logs = filter(lambda x: time_from <= time.strptime(x.split(" ")[0], self.TIME_FORMAT), _logs) + _logs = __logs + except ValueError: + self.error("Time format error") + if time_to: + try: + for log in _logs: + __logs = filter(lambda x: time_to >= time.strptime(x.split(" ")[0], self.TIME_FORMAT), _logs) + _logs = __logs + except ValueError: + self.error("Time format error") + if event_number: + _logs = _logs[-event_number:] + return list(_logs) + + +# class SuperExtensionConnector(SuperExtensionDriver): +# +# def __init__(self): +# super(SuperExtensionConnector, self).__init__() +# # Super_Extension is loaded every time the server is started +# self.__uuid = uuid4().hex +# # self.__super_extension = Extension() +# _policy_abs_dir = os.path.join(CONF.moon.super_extension_directory, 'policy') +# # self.__super_extension.load_from_json(_policy_abs_dir) +# +# def get_super_extensions(self): +# return None +# +# def admin(self, sub, obj, act): +# # return self.__super_extension.authz(sub, obj, act) +# return True \ No newline at end of file diff --git a/keystone-moon/keystone/contrib/moon/backends/sql.py b/keystone-moon/keystone/contrib/moon/backends/sql.py new file mode 100644 index 00000000..5f76e235 --- /dev/null +++ b/keystone-moon/keystone/contrib/moon/backends/sql.py @@ -0,0 +1,1537 @@ +# Copyright 2015 Open Platform for NFV Project, Inc. and its contributors +# This software is distributed under the terms and conditions of the 'Apache-2.0' +# license which can be found in the file 'LICENSE' in this package distribution +# or at 'http://www.apache.org/licenses/LICENSE-2.0'. + +import six +from uuid import uuid4 +import copy + +from keystone import config +from oslo_log import log +from keystone.common import sql +from keystone import exception +from keystone.contrib.moon.exception import * +from oslo_serialization import jsonutils +from keystone.contrib.moon import IntraExtensionDriver +from keystone.contrib.moon import TenantDriver +# from keystone.contrib.moon import InterExtensionDriver + +from keystone.contrib.moon.exception import TenantError, TenantListEmptyError + +CONF = config.CONF +LOG = log.getLogger(__name__) + + +class IntraExtension(sql.ModelBase, sql.DictBase): + __tablename__ = 'intra_extension' + attributes = ['id', 'name', 'model', 'description'] + id = sql.Column(sql.String(64), primary_key=True) + name = sql.Column(sql.String(64), nullable=False) + model = sql.Column(sql.String(64), nullable=True) + description = sql.Column(sql.Text()) + + @classmethod + def from_dict(cls, d): + new_d = d.copy() + return cls(**new_d) + + def to_dict(self): + return dict(six.iteritems(self)) + + +class Subject(sql.ModelBase, sql.DictBase): + __tablename__ = 'subject' + attributes = ['id', 'subjects', 'intra_extension_uuid'] + id = sql.Column(sql.String(64), primary_key=True) + subjects = sql.Column(sql.JsonBlob(), nullable=True) + intra_extension_uuid = sql.Column(sql.ForeignKey("intra_extension.id"), nullable=False) + + @classmethod + def from_dict(cls, d): + new_d = d.copy() + return cls(**new_d) + + def to_dict(self): + return dict(six.iteritems(self)) + + +class Object(sql.ModelBase, sql.DictBase): + __tablename__ = 'object' + attributes = ['id', 'objects', 'intra_extension_uuid'] + id = sql.Column(sql.String(64), primary_key=True) + objects = sql.Column(sql.JsonBlob(), nullable=True) + intra_extension_uuid = sql.Column(sql.ForeignKey("intra_extension.id"), nullable=False) + + @classmethod + def from_dict(cls, d): + new_d = d.copy() + return cls(**new_d) + + def to_dict(self): + return dict(six.iteritems(self)) + + +class Action(sql.ModelBase, sql.DictBase): + __tablename__ = 'action' + attributes = ['id', 'actions', 'intra_extension_uuid'] + id = sql.Column(sql.String(64), primary_key=True) + actions = sql.Column(sql.JsonBlob(), nullable=True) + intra_extension_uuid = sql.Column(sql.ForeignKey("intra_extension.id"), nullable=False) + + @classmethod + def from_dict(cls, d): + new_d = d.copy() + return cls(**new_d) + + def to_dict(self): + return dict(six.iteritems(self)) + + +class SubjectCategory(sql.ModelBase, sql.DictBase): + __tablename__ = 'subject_category' + attributes = ['id', 'subject_categories', 'intra_extension_uuid'] + id = sql.Column(sql.String(64), primary_key=True) + subject_categories = sql.Column(sql.JsonBlob(), nullable=True) + intra_extension_uuid = sql.Column(sql.ForeignKey("intra_extension.id"), nullable=False) + + @classmethod + def from_dict(cls, d): + new_d = d.copy() + return cls(**new_d) + + def to_dict(self): + return dict(six.iteritems(self)) + + +class ObjectCategory(sql.ModelBase, sql.DictBase): + __tablename__ = 'object_category' + attributes = ['id', 'object_categories', 'intra_extension_uuid'] + id = sql.Column(sql.String(64), primary_key=True) + object_categories = sql.Column(sql.JsonBlob(), nullable=True) + intra_extension_uuid = sql.Column(sql.ForeignKey("intra_extension.id"), nullable=False) + + @classmethod + def from_dict(cls, d): + new_d = d.copy() + return cls(**new_d) + + def to_dict(self): + return dict(six.iteritems(self)) + + +class ActionCategory(sql.ModelBase, sql.DictBase): + __tablename__ = 'action_category' + attributes = ['id', 'action_categories', 'intra_extension_uuid'] + id = sql.Column(sql.String(64), primary_key=True) + action_categories = sql.Column(sql.JsonBlob(), nullable=True) + intra_extension_uuid = sql.Column(sql.ForeignKey("intra_extension.id"), nullable=False) + + @classmethod + def from_dict(cls, d): + new_d = d.copy() + return cls(**new_d) + + def to_dict(self): + return dict(six.iteritems(self)) + + +class SubjectCategoryScope(sql.ModelBase, sql.DictBase): + __tablename__ = 'subject_category_scope' + attributes = ['id', 'subject_category_scope', 'intra_extension_uuid'] + id = sql.Column(sql.String(64), primary_key=True) + subject_category_scope = sql.Column(sql.JsonBlob(), nullable=True) + intra_extension_uuid = sql.Column(sql.ForeignKey("intra_extension.id"), nullable=False) + + @classmethod + def from_dict(cls, d): + new_d = d.copy() + return cls(**new_d) + + def to_dict(self): + return dict(six.iteritems(self)) + + +class ObjectCategoryScope(sql.ModelBase, sql.DictBase): + __tablename__ = 'object_category_scope' + attributes = ['id', 'object_category_scope', 'intra_extension_uuid'] + id = sql.Column(sql.String(64), primary_key=True) + object_category_scope = sql.Column(sql.JsonBlob(), nullable=True) + intra_extension_uuid = sql.Column(sql.ForeignKey("intra_extension.id"), nullable=False) + + @classmethod + def from_dict(cls, d): + new_d = d.copy() + return cls(**new_d) + + def to_dict(self): + return dict(six.iteritems(self)) + + +class ActionCategoryScope(sql.ModelBase, sql.DictBase): + __tablename__ = 'action_category_scope' + attributes = ['id', 'action_category_scope', 'intra_extension_uuid'] + id = sql.Column(sql.String(64), primary_key=True) + action_category_scope = sql.Column(sql.JsonBlob(), nullable=True) + intra_extension_uuid = sql.Column(sql.ForeignKey("intra_extension.id"), nullable=False) + + @classmethod + def from_dict(cls, d): + new_d = d.copy() + return cls(**new_d) + + def to_dict(self): + return dict(six.iteritems(self)) + + +class SubjectCategoryAssignment(sql.ModelBase, sql.DictBase): + __tablename__ = 'subject_category_assignment' + attributes = ['id', 'subject_category_assignments', 'intra_extension_uuid'] + id = sql.Column(sql.String(64), primary_key=True) + subject_category_assignments = sql.Column(sql.JsonBlob(), nullable=True) + intra_extension_uuid = sql.Column(sql.ForeignKey("intra_extension.id"), nullable=False) + + @classmethod + def from_dict(cls, d): + new_d = d.copy() + return cls(**new_d) + + def to_dict(self): + return dict(six.iteritems(self)) + + +class ObjectCategoryAssignment(sql.ModelBase, sql.DictBase): + __tablename__ = 'object_category_assignment' + attributes = ['id', 'object_category_assignments', 'intra_extension_uuid'] + id = sql.Column(sql.String(64), primary_key=True) + object_category_assignments = sql.Column(sql.JsonBlob(), nullable=True) + intra_extension_uuid = sql.Column(sql.ForeignKey("intra_extension.id"), nullable=False) + + @classmethod + def from_dict(cls, d): + new_d = d.copy() + return cls(**new_d) + + def to_dict(self): + return dict(six.iteritems(self)) + + +class ActionCategoryAssignment(sql.ModelBase, sql.DictBase): + __tablename__ = 'action_category_assignment' + attributes = ['id', 'action_category_assignments', 'intra_extension_uuid'] + id = sql.Column(sql.String(64), primary_key=True) + action_category_assignments = sql.Column(sql.JsonBlob(), nullable=True) + intra_extension_uuid = sql.Column(sql.ForeignKey("intra_extension.id"), nullable=False) + + @classmethod + def from_dict(cls, d): + new_d = d.copy() + return cls(**new_d) + + def to_dict(self): + return dict(six.iteritems(self)) + + +class MetaRule(sql.ModelBase, sql.DictBase): + __tablename__ = 'metarule' + attributes = ['id', 'sub_meta_rules', 'aggregation', 'intra_extension_uuid'] + id = sql.Column(sql.String(64), primary_key=True) + sub_meta_rules = sql.Column(sql.JsonBlob(), nullable=True) + aggregation = sql.Column(sql.Text(), nullable=True) + intra_extension_uuid = sql.Column(sql.ForeignKey("intra_extension.id"), nullable=False) + + @classmethod + def from_dict(cls, d): + new_d = d.copy() + return cls(**new_d) + + def to_dict(self): + return dict(six.iteritems(self)) + + +class Rule(sql.ModelBase, sql.DictBase): + __tablename__ = 'rule' + attributes = ['id', 'rules', 'intra_extension_uuid'] + id = sql.Column(sql.String(64), primary_key=True) + rules = sql.Column(sql.JsonBlob(), nullable=True) + intra_extension_uuid = sql.Column(sql.ForeignKey("intra_extension.id"), nullable=False) + + @classmethod + def from_dict(cls, d): + new_d = d.copy() + return cls(**new_d) + + def to_dict(self): + return dict(six.iteritems(self)) + + +class Tenant(sql.ModelBase, sql.DictBase): + __tablename__ = 'tenants' + attributes = [ + 'id', 'name', 'authz', 'admin' + ] + id = sql.Column(sql.String(64), primary_key=True, nullable=False) + name = sql.Column(sql.String(128), nullable=True) + authz = sql.Column(sql.String(64), nullable=True) + admin = sql.Column(sql.String(64), nullable=True) + + @classmethod + def from_dict(cls, d): + """Override parent from_dict() method with a different implementation. + """ + new_d = d.copy() + uuid = new_d.keys()[0] + return cls(id=uuid, **new_d[uuid]) + + def to_dict(self): + """ + """ + tenant_dict = {} + for key in ("id", "name", "authz", "admin"): + tenant_dict[key] = getattr(self, key) + return tenant_dict + +__all_objects__ = ( + Subject, + Object, + Action, + SubjectCategory, + ObjectCategory, + ActionCategory, + SubjectCategoryScope, + ObjectCategoryScope, + ActionCategoryScope, + SubjectCategoryAssignment, + ObjectCategoryAssignment, + ActionCategoryAssignment, + MetaRule, + Rule, +) + +class IntraExtensionConnector(IntraExtensionDriver): + + def get_intra_extension_list(self): + with sql.transaction() as session: + query = session.query(IntraExtension.id) + intraextensions = query.all() + # return intraextensions + return [intraextension[0] for intraextension in intraextensions] + + def set_intra_extension(self, intra_id, intra_extension): + with sql.transaction() as session: + # intra_extension["admin"] = jsonutils.dumps(intra_extension["admin"]) + # intra_extension["authz"] = jsonutils.dumps(intra_extension["authz"]) + ie_ref = IntraExtension.from_dict(intra_extension) + session.add(ie_ref) + return IntraExtension.to_dict(ie_ref) + + def get_intra_extension(self, uuid): + with sql.transaction() as session: + query = session.query(IntraExtension) + query = query.filter_by(id=uuid) + ref = query.first() + if not ref: + raise exception.NotFound + return ref.to_dict() + + def delete_intra_extension(self, intra_extension_id): + with sql.transaction() as session: + ref = session.query(IntraExtension).get(intra_extension_id) + # Must delete all references to that IntraExtension + for _object in __all_objects__: + query = session.query(_object) + query = query.filter_by(intra_extension_uuid=intra_extension_id) + _ref = query.first() + if _ref: + session.delete(_ref) + session.flush() + session.delete(ref) + + # Getter and setter for name + + def get_name(self, uuid): + intra_extension = self.get_intra_extension(uuid) + return intra_extension["name"] + + def set_name(self, uuid, name): + raise exception.NotImplemented() # pragma: no cover + + # Getter and setter for model + + def get_model(self, uuid): + intra_extension = self.get_intra_extension(uuid) + return intra_extension["model"] + + def set_model(self, uuid, model): + raise exception.NotImplemented() # pragma: no cover + + # Getter and setter for description + + def get_description(self, uuid): + intra_extension = self.get_intra_extension(uuid) + return intra_extension["description"] + + def set_description(self, uuid, args): + raise exception.NotImplemented() # pragma: no cover + + def get_subject_dict(self, extension_uuid): + with sql.transaction() as session: + query = session.query(Subject) + query = query.filter_by(intra_extension_uuid=extension_uuid) + ref = query.first() + if not ref: + raise IntraExtensionNotFound() + return ref.to_dict() + + def set_subject_dict(self, extension_uuid, subject_uuid): + with sql.transaction() as session: + query = session.query(Subject) + query = query.filter_by(intra_extension_uuid=extension_uuid) + ref = query.first() + new_ref = Subject.from_dict( + { + "id": uuid4().hex, + 'subjects': subject_uuid, + 'intra_extension_uuid': extension_uuid + } + ) + if not ref: + session.add(new_ref) + ref = new_ref + else: + for attr in Subject.attributes: + if attr != 'id': + setattr(ref, attr, getattr(new_ref, attr)) + return ref.to_dict() + + def add_subject(self, extension_uuid, subject_uuid, subject_name): + with sql.transaction() as session: + query = session.query(Subject) + query = query.filter_by(intra_extension_uuid=extension_uuid) + ref = query.first() + if not ref: + raise IntraExtensionNotFound() + old_ref = ref.to_dict() + subjects = dict(old_ref["subjects"]) + subjects[subject_uuid] = subject_name + new_ref = Subject.from_dict( + { + "id": old_ref["id"], + 'subjects': subjects, + 'intra_extension_uuid': old_ref["intra_extension_uuid"] + } + ) + for attr in Subject.attributes: + if attr != 'id': + setattr(ref, attr, getattr(new_ref, attr)) + return {"subject": {"uuid": subject_uuid, "name": subject_name}} + + def remove_subject(self, extension_uuid, subject_uuid): + with sql.transaction() as session: + query = session.query(Subject) + query = query.filter_by(intra_extension_uuid=extension_uuid) + ref = query.first() + if not ref: + raise IntraExtensionNotFound() + else: + old_ref = ref.to_dict() + subjects = dict(old_ref["subjects"]) + try: + subjects.pop(subject_uuid) + except KeyError: + LOG.error("KeyError in remove_subject {} | {}".format(subject_uuid, subjects)) + else: + new_ref = Subject.from_dict( + { + "id": old_ref["id"], + 'subjects': subjects, + 'intra_extension_uuid': old_ref["intra_extension_uuid"] + } + ) + for attr in Subject.attributes: + if attr != 'id': + setattr(ref, attr, getattr(new_ref, attr)) + + def get_object_dict(self, extension_uuid): + with sql.transaction() as session: + query = session.query(Object) + query = query.filter_by(intra_extension_uuid=extension_uuid) + ref = query.first() + if not ref: + raise IntraExtensionNotFound() + return ref.to_dict() + + def set_object_dict(self, extension_uuid, object_uuid): + with sql.transaction() as session: + query = session.query(Object) + query = query.filter_by(intra_extension_uuid=extension_uuid) + ref = query.first() + new_ref = Object.from_dict( + { + "id": uuid4().hex, + 'objects': object_uuid, + 'intra_extension_uuid': extension_uuid + } + ) + if not ref: + session.add(new_ref) + ref = new_ref + else: + for attr in Object.attributes: + if attr != 'id': + setattr(ref, attr, getattr(new_ref, attr)) + return ref.to_dict() + + def add_object(self, extension_uuid, object_uuid, object_name): + with sql.transaction() as session: + query = session.query(Object) + query = query.filter_by(intra_extension_uuid=extension_uuid) + ref = query.first() + if not ref: + raise IntraExtensionNotFound() + old_ref = ref.to_dict() + objects = dict(old_ref["objects"]) + objects[object_uuid] = object_name + new_ref = Object.from_dict( + { + "id": old_ref["id"], + 'objects': objects, + 'intra_extension_uuid': old_ref["intra_extension_uuid"] + } + ) + for attr in Object.attributes: + if attr != 'id': + setattr(ref, attr, getattr(new_ref, attr)) + return {"object": {"uuid": object_uuid, "name": object_name}} + + def remove_object(self, extension_uuid, object_uuid): + with sql.transaction() as session: + query = session.query(Object) + query = query.filter_by(intra_extension_uuid=extension_uuid) + ref = query.first() + if not ref: + raise IntraExtensionNotFound() + else: + old_ref = ref.to_dict() + objects = dict(old_ref["objects"]) + try: + objects.pop(object_uuid) + except KeyError: + LOG.error("KeyError in remove_object {} | {}".format(object_uuid, objects)) + else: + new_ref = Object.from_dict( + { + "id": old_ref["id"], + 'objects': objects, + 'intra_extension_uuid': old_ref["intra_extension_uuid"] + } + ) + for attr in Object.attributes: + if attr != 'id': + setattr(ref, attr, getattr(new_ref, attr)) + + def get_action_dict(self, extension_uuid): + with sql.transaction() as session: + query = session.query(Action) + query = query.filter_by(intra_extension_uuid=extension_uuid) + ref = query.first() + if not ref: + raise IntraExtensionNotFound() + return ref.to_dict() + + def set_action_dict(self, extension_uuid, action_uuid): + with sql.transaction() as session: + query = session.query(Action) + query = query.filter_by(intra_extension_uuid=extension_uuid) + ref = query.first() + new_ref = Action.from_dict( + { + "id": uuid4().hex, + 'actions': action_uuid, + 'intra_extension_uuid': extension_uuid + } + ) + if not ref: + session.add(new_ref) + ref = new_ref + else: + for attr in Action.attributes: + if attr != 'id': + setattr(ref, attr, getattr(new_ref, attr)) + return ref.to_dict() + + def add_action(self, extension_uuid, action_uuid, action_name): + with sql.transaction() as session: + query = session.query(Action) + query = query.filter_by(intra_extension_uuid=extension_uuid) + ref = query.first() + if not ref: + raise IntraExtensionNotFound() + old_ref = ref.to_dict() + actions = dict(old_ref["actions"]) + actions[action_uuid] = action_name + new_ref = Action.from_dict( + { + "id": old_ref["id"], + 'actions': actions, + 'intra_extension_uuid': old_ref["intra_extension_uuid"] + } + ) + for attr in Action.attributes: + if attr != 'id': + setattr(ref, attr, getattr(new_ref, attr)) + return {"action": {"uuid": action_uuid, "name": action_name}} + + def remove_action(self, extension_uuid, action_uuid): + with sql.transaction() as session: + query = session.query(Action) + query = query.filter_by(intra_extension_uuid=extension_uuid) + ref = query.first() + if not ref: + raise IntraExtensionNotFound() + else: + old_ref = ref.to_dict() + actions = dict(old_ref["actions"]) + try: + actions.pop(action_uuid) + except KeyError: + LOG.error("KeyError in remove_action {} | {}".format(action_uuid, actions)) + else: + new_ref = Action.from_dict( + { + "id": old_ref["id"], + 'actions': actions, + 'intra_extension_uuid': old_ref["intra_extension_uuid"] + } + ) + for attr in Action.attributes: + if attr != 'id': + setattr(ref, attr, getattr(new_ref, attr)) + + # Getter and Setter for subject_category + + def get_subject_category_dict(self, extension_uuid): + with sql.transaction() as session: + query = session.query(SubjectCategory) + query = query.filter_by(intra_extension_uuid=extension_uuid) + ref = query.first() + if not ref: + raise IntraExtensionNotFound() + return ref.to_dict() + + def set_subject_category_dict(self, extension_uuid, subject_categories): + with sql.transaction() as session: + query = session.query(SubjectCategory) + query = query.filter_by(intra_extension_uuid=extension_uuid) + ref = query.first() + new_ref = SubjectCategory.from_dict( + { + "id": uuid4().hex, + 'subject_categories': subject_categories, + 'intra_extension_uuid': extension_uuid + } + ) + if not ref: + session.add(new_ref) + ref = new_ref + else: + for attr in SubjectCategory.attributes: + if attr != 'id': + setattr(ref, attr, getattr(new_ref, attr)) + return ref.to_dict() + + def add_subject_category_dict(self, extension_uuid, subject_category_uuid, subject_category_name): + with sql.transaction() as session: + query = session.query(SubjectCategory) + query = query.filter_by(intra_extension_uuid=extension_uuid) + ref = query.first() + if not ref: + raise IntraExtensionNotFound() + old_ref = ref.to_dict() + subject_categories = dict(old_ref["subject_categories"]) + subject_categories[subject_category_uuid] = subject_category_name + new_ref = SubjectCategory.from_dict( + { + "id": old_ref["id"], + 'subject_categories': subject_categories, + 'intra_extension_uuid': old_ref["intra_extension_uuid"] + } + ) + for attr in SubjectCategory.attributes: + if attr != 'id': + setattr(ref, attr, getattr(new_ref, attr)) + return {"subject_category": {"uuid": subject_category_uuid, "name": subject_category_name}} + + def remove_subject_category(self, extension_uuid, subject_category_uuid): + with sql.transaction() as session: + query = session.query(SubjectCategory) + query = query.filter_by(intra_extension_uuid=extension_uuid) + ref = query.first() + if not ref: + raise IntraExtensionNotFound() + else: + old_ref = ref.to_dict() + subject_categories = dict(old_ref["subject_categories"]) + try: + subject_categories.pop(subject_category_uuid) + except KeyError: + pass + else: + new_ref = SubjectCategory.from_dict( + { + "id": old_ref["id"], + 'subject_categories': subject_categories, + 'intra_extension_uuid': old_ref["intra_extension_uuid"] + } + ) + for attr in SubjectCategory.attributes: + if attr != 'id': + setattr(ref, attr, getattr(new_ref, attr)) + return ref.to_dict() + + # Getter and Setter for object_category + + def get_object_category_dict(self, extension_uuid): + with sql.transaction() as session: + query = session.query(ObjectCategory) + query = query.filter_by(intra_extension_uuid=extension_uuid) + ref = query.first() + if not ref: + raise IntraExtensionNotFound() + return ref.to_dict() + + def set_object_category_dict(self, extension_uuid, object_categories): + with sql.transaction() as session: + query = session.query(ObjectCategory) + query = query.filter_by(intra_extension_uuid=extension_uuid) + ref = query.first() + new_ref = ObjectCategory.from_dict( + { + "id": uuid4().hex, + 'object_categories': object_categories, + 'intra_extension_uuid': extension_uuid + } + ) + if not ref: + session.add(new_ref) + ref = new_ref + else: + for attr in ObjectCategory.attributes: + if attr != 'id': + setattr(ref, attr, getattr(new_ref, attr)) + return ref.to_dict() + + def add_object_category_dict(self, extension_uuid, object_category_uuid, object_category_name): + with sql.transaction() as session: + query = session.query(ObjectCategory) + query = query.filter_by(intra_extension_uuid=extension_uuid) + ref = query.first() + if not ref: + raise IntraExtensionNotFound() + old_ref = ref.to_dict() + object_categories = dict(old_ref["object_categories"]) + object_categories[object_category_uuid] = object_category_name + new_ref = ObjectCategory.from_dict( + { + "id": old_ref["id"], + 'object_categories': object_categories, + 'intra_extension_uuid': old_ref["intra_extension_uuid"] + } + ) + for attr in ObjectCategory.attributes: + if attr != 'id': + setattr(ref, attr, getattr(new_ref, attr)) + return {"object_category": {"uuid": object_category_uuid, "name": object_category_name}} + + def remove_object_category(self, extension_uuid, object_category_uuid): + with sql.transaction() as session: + query = session.query(ObjectCategory) + query = query.filter_by(intra_extension_uuid=extension_uuid) + ref = query.first() + if not ref: + raise IntraExtensionNotFound() + else: + old_ref = ref.to_dict() + object_categories = dict(old_ref["object_categories"]) + try: + object_categories.pop(object_category_uuid) + except KeyError: + pass + else: + new_ref = ObjectCategory.from_dict( + { + "id": old_ref["id"], + 'object_categories': object_categories, + 'intra_extension_uuid': old_ref["intra_extension_uuid"] + } + ) + for attr in ObjectCategory.attributes: + if attr != 'id': + setattr(ref, attr, getattr(new_ref, attr)) + return ref.to_dict() + + # Getter and Setter for action_category + + def get_action_category_dict(self, extension_uuid): + with sql.transaction() as session: + query = session.query(ActionCategory) + query = query.filter_by(intra_extension_uuid=extension_uuid) + ref = query.first() + if not ref: + raise IntraExtensionNotFound() + return ref.to_dict() + + def set_action_category_dict(self, extension_uuid, action_categories): + with sql.transaction() as session: + query = session.query(ActionCategory) + query = query.filter_by(intra_extension_uuid=extension_uuid) + ref = query.first() + new_ref = ActionCategory.from_dict( + { + "id": uuid4().hex, + 'action_categories': action_categories, + 'intra_extension_uuid': extension_uuid + } + ) + if not ref: + session.add(new_ref) + ref = new_ref + else: + for attr in ActionCategory.attributes: + if attr != 'id': + setattr(ref, attr, getattr(new_ref, attr)) + return ref.to_dict() + + def add_action_category_dict(self, extension_uuid, action_category_uuid, action_category_name): + with sql.transaction() as session: + query = session.query(ActionCategory) + query = query.filter_by(intra_extension_uuid=extension_uuid) + ref = query.first() + if not ref: + raise IntraExtensionNotFound() + old_ref = ref.to_dict() + action_categories = dict(old_ref["action_categories"]) + action_categories[action_category_uuid] = action_category_name + new_ref = ActionCategory.from_dict( + { + "id": old_ref["id"], + 'action_categories': action_categories, + 'intra_extension_uuid': old_ref["intra_extension_uuid"] + } + ) + for attr in ActionCategory.attributes: + if attr != 'id': + setattr(ref, attr, getattr(new_ref, attr)) + return {"action_category": {"uuid": action_category_uuid, "name": action_category_name}} + + def remove_action_category(self, extension_uuid, action_category_uuid): + with sql.transaction() as session: + query = session.query(ActionCategory) + query = query.filter_by(intra_extension_uuid=extension_uuid) + ref = query.first() + if not ref: + raise IntraExtensionNotFound() + else: + old_ref = ref.to_dict() + action_categories = dict(old_ref["action_categories"]) + try: + action_categories.pop(action_category_uuid) + except KeyError: + pass + else: + new_ref = ActionCategory.from_dict( + { + "id": old_ref["id"], + 'action_categories': action_categories, + 'intra_extension_uuid': old_ref["intra_extension_uuid"] + } + ) + for attr in ActionCategory.attributes: + if attr != 'id': + setattr(ref, attr, getattr(new_ref, attr)) + return ref.to_dict() + + # Getter and Setter for subject_category_value_scope + + def get_subject_category_scope_dict(self, extension_uuid, subject_category): + with sql.transaction() as session: + query = session.query(SubjectCategoryScope) + query = query.filter_by(intra_extension_uuid=extension_uuid) + ref = query.first() + if not ref: + raise IntraExtensionNotFound() + result = copy.deepcopy(ref.to_dict()) + if subject_category not in result["subject_category_scope"].keys(): + raise CategoryNotFound() + result["subject_category_scope"] = {subject_category: result["subject_category_scope"][subject_category]} + return result + + def set_subject_category_scope_dict(self, extension_uuid, subject_category, scope): + with sql.transaction() as session: + query = session.query(SubjectCategoryScope) + query = query.filter_by(intra_extension_uuid=extension_uuid) + ref = query.first() + if not ref: + new_ref = SubjectCategoryScope.from_dict( + { + "id": uuid4().hex, + 'subject_category_scope': {subject_category: scope}, + 'intra_extension_uuid': extension_uuid + } + ) + session.add(new_ref) + ref = new_ref + else: + tmp_ref = ref.to_dict() + tmp_ref['subject_category_scope'].update({subject_category: scope}) + session.delete(ref) + new_ref = SubjectCategoryScope.from_dict(tmp_ref) + session.add(new_ref) + return ref.to_dict() + + def add_subject_category_scope_dict(self, extension_uuid, subject_category, scope_uuid, scope_name): + with sql.transaction() as session: + query = session.query(SubjectCategoryScope) + query = query.filter_by(intra_extension_uuid=extension_uuid) + ref = query.first() + if not ref: + raise IntraExtensionNotFound() + old_ref = ref.to_dict() + scope = copy.deepcopy(old_ref["subject_category_scope"]) + if subject_category not in scope.keys(): + scope[subject_category] = dict() + scope[subject_category][scope_uuid] = scope_name + self.set_subject_category_scope_dict(extension_uuid, subject_category, scope[subject_category]) + return {"subject_category_scope": {"uuid": scope_uuid, "name": scope_name}} + + def remove_subject_category_scope_dict(self, extension_uuid, subject_category, scope_uuid): + with sql.transaction() as session: + query = session.query(SubjectCategoryScope) + query = query.filter_by(intra_extension_uuid=extension_uuid) + ref = query.first() + if not ref: + raise IntraExtensionNotFound() + old_ref = ref.to_dict() + scope = dict(old_ref["subject_category_scope"]) + if subject_category not in scope: + return + try: + scope[subject_category].pop(scope_uuid) + except KeyError: + return + new_ref = SubjectCategoryScope.from_dict( + { + "id": old_ref["id"], + 'subject_category_scope': scope, + 'intra_extension_uuid': old_ref["intra_extension_uuid"] + } + ) + for attr in SubjectCategoryScope.attributes: + if attr != 'id': + setattr(ref, attr, getattr(new_ref, attr)) + return ref.to_dict() + + # Getter and Setter for object_category_scope + + def get_object_category_scope_dict(self, extension_uuid, object_category): + with sql.transaction() as session: + query = session.query(ObjectCategoryScope) + query = query.filter_by(intra_extension_uuid=extension_uuid) + ref = query.first() + if not ref: + raise IntraExtensionNotFound() + result = copy.deepcopy(ref.to_dict()) + if object_category not in result["object_category_scope"].keys(): + raise CategoryNotFound() + result["object_category_scope"] = {object_category: result["object_category_scope"][object_category]} + return result + + def set_object_category_scope_dict(self, extension_uuid, object_category, scope): + with sql.transaction() as session: + query = session.query(ObjectCategoryScope) + query = query.filter_by(intra_extension_uuid=extension_uuid) + ref = query.first() + if not ref: + new_ref = ObjectCategoryScope.from_dict( + { + "id": uuid4().hex, + 'object_category_scope': {object_category: scope}, + 'intra_extension_uuid': extension_uuid + } + ) + session.add(new_ref) + ref = new_ref + else: + tmp_ref = ref.to_dict() + tmp_ref['object_category_scope'].update({object_category: scope}) + session.delete(ref) + new_ref = ObjectCategoryScope.from_dict(tmp_ref) + session.add(new_ref) + return ref.to_dict() + + def add_object_category_scope_dict(self, extension_uuid, object_category, scope_uuid, scope_name): + with sql.transaction() as session: + query = session.query(ObjectCategoryScope) + query = query.filter_by(intra_extension_uuid=extension_uuid) + ref = query.first() + if not ref: + raise IntraExtensionNotFound() + old_ref = ref.to_dict() + scope = dict(old_ref["object_category_scope"]) + if object_category not in scope: + scope[object_category] = dict() + scope[object_category][scope_uuid] = scope_name + self.set_object_category_scope_dict(extension_uuid, object_category, scope[object_category]) + return {"object_category_scope": {"uuid": scope_uuid, "name": scope_name}} + + def remove_object_category_scope_dict(self, extension_uuid, object_category, scope_uuid): + with sql.transaction() as session: + query = session.query(ObjectCategoryScope) + query = query.filter_by(intra_extension_uuid=extension_uuid) + ref = query.first() + if not ref: + raise IntraExtensionNotFound() + old_ref = ref.to_dict() + scope = dict(old_ref["object_category_scope"]) + if object_category not in scope: + return + try: + scope[object_category].pop(scope_uuid) + except KeyError: + return + new_ref = ObjectCategoryScope.from_dict( + { + "id": old_ref["id"], + 'object_category_scope': scope, + 'intra_extension_uuid': old_ref["intra_extension_uuid"] + } + ) + for attr in ObjectCategoryScope.attributes: + if attr != 'id': + setattr(ref, attr, getattr(new_ref, attr)) + return ref.to_dict() + + # Getter and Setter for action_category_scope + + def get_action_category_scope_dict(self, extension_uuid, action_category): + with sql.transaction() as session: + query = session.query(ActionCategoryScope) + query = query.filter_by(intra_extension_uuid=extension_uuid) + ref = query.first() + if not ref: + raise IntraExtensionNotFound() + result = copy.deepcopy(ref.to_dict()) + if action_category not in result["action_category_scope"].keys(): + raise CategoryNotFound("Unknown category id {}/{}".format(action_category, result["action_category_scope"].keys())) + result["action_category_scope"] = {action_category: result["action_category_scope"][action_category]} + return result + + def set_action_category_scope_dict(self, extension_uuid, action_category, scope): + with sql.transaction() as session: + query = session.query(ActionCategoryScope) + query = query.filter_by(intra_extension_uuid=extension_uuid) + ref = query.first() + if not ref: + new_ref = ActionCategoryScope.from_dict( + { + "id": uuid4().hex, + 'action_category_scope': {action_category: scope}, + 'intra_extension_uuid': extension_uuid + } + ) + session.add(new_ref) + ref = new_ref + else: + tmp_ref = ref.to_dict() + tmp_ref['action_category_scope'].update({action_category: scope}) + session.delete(ref) + new_ref = ActionCategoryScope.from_dict(tmp_ref) + session.add(new_ref) + return ref.to_dict() + + def add_action_category_scope_dict(self, extension_uuid, action_category, scope_uuid, scope_name): + with sql.transaction() as session: + query = session.query(ActionCategoryScope) + query = query.filter_by(intra_extension_uuid=extension_uuid) + ref = query.first() + if not ref: + raise IntraExtensionNotFound() + old_ref = ref.to_dict() + scope = dict(old_ref["action_category_scope"]) + if action_category not in scope: + scope[action_category] = dict() + scope[action_category][scope_uuid] = scope_name + self.set_action_category_scope_dict(extension_uuid, action_category, scope[action_category]) + return {"action_category_scope": {"uuid": scope_uuid, "name": scope_name}} + + def remove_action_category_scope_dict(self, extension_uuid, action_category, scope_uuid): + with sql.transaction() as session: + query = session.query(ActionCategoryScope) + query = query.filter_by(intra_extension_uuid=extension_uuid) + ref = query.first() + if not ref: + raise IntraExtensionNotFound() + old_ref = ref.to_dict() + scope = dict(old_ref["action_category_scope"]) + if action_category not in scope: + return + try: + scope[action_category].pop(scope_uuid) + except KeyError: + return + new_ref = ActionCategoryScope.from_dict( + { + "id": old_ref["id"], + 'action_category_scope': scope, + 'intra_extension_uuid': old_ref["intra_extension_uuid"] + } + ) + for attr in ActionCategoryScope.attributes: + if attr != 'id': + setattr(ref, attr, getattr(new_ref, attr)) + return ref.to_dict() + + # Getter and Setter for subject_category_assignment + + def get_subject_category_assignment_dict(self, extension_uuid, subject_uuid): + """ From a subject_uuid, return a dictionary of (category: scope for that subject) + + :param extension_uuid: intra extension UUID + :param subject_uuid: subject UUID + :return: a dictionary of (keys are category nd values are scope for that subject) + """ + with sql.transaction() as session: + query = session.query(SubjectCategoryAssignment) + query = query.filter_by(intra_extension_uuid=extension_uuid) + ref = query.first() + if not ref: + raise IntraExtensionNotFound("get_subject_category_assignment_dict") + _ref = ref.to_dict() + if subject_uuid in _ref["subject_category_assignments"]: + _backup_dict = _ref["subject_category_assignments"][subject_uuid] + _ref["subject_category_assignments"] = dict() + _ref["subject_category_assignments"][subject_uuid] = _backup_dict + else: + _ref["subject_category_assignments"] = dict() + _ref["subject_category_assignments"][subject_uuid] = dict() + return _ref + + def set_subject_category_assignment_dict(self, extension_uuid, subject_uuid=None, assignment_dict={}): + with sql.transaction() as session: + query = session.query(SubjectCategoryAssignment) + query = query.filter_by(intra_extension_uuid=extension_uuid) + ref = query.first() + if type(assignment_dict) is not dict: + raise IntraExtensionError() + for value in assignment_dict.values(): + if type(value) is not list: + raise IntraExtensionError(str(value)) + if not subject_uuid: + subject_category_assignments = {} + else: + subject_category_assignments = {subject_uuid: assignment_dict} + new_ref = SubjectCategoryAssignment.from_dict( + { + "id": uuid4().hex, + 'subject_category_assignments': subject_category_assignments, + 'intra_extension_uuid': extension_uuid + } + ) + if not ref: + session.add(new_ref) + ref = new_ref + else: + new_ref.subject_category_assignments[subject_uuid] = assignment_dict + for attr in SubjectCategoryAssignment.attributes: + if attr != 'id': + setattr(ref, attr, getattr(new_ref, attr)) + return ref.to_dict() + + def add_subject_category_assignment_dict(self, extension_uuid, subject_uuid, category_uuid, scope_uuid): + with sql.transaction() as session: + query = session.query(SubjectCategoryAssignment) + query = query.filter_by(intra_extension_uuid=extension_uuid) + ref = query.first() + if not ref: + raise IntraExtensionNotFound() + assignments = ref.to_dict()['subject_category_assignments'] + if subject_uuid not in assignments: + assignments[subject_uuid] = dict() + if category_uuid not in assignments[subject_uuid]: + assignments[subject_uuid][category_uuid] = list() + if scope_uuid not in assignments[subject_uuid][category_uuid]: + assignments[subject_uuid][category_uuid].append(scope_uuid) + return self.set_subject_category_assignment_dict( + extension_uuid, + subject_uuid, + assignments[subject_uuid]) + + def remove_subject_category_assignment(self, extension_uuid, subject_uuid, category_uuid, scope_uuid): + with sql.transaction() as session: + query = session.query(SubjectCategoryAssignment) + query = query.filter_by(intra_extension_uuid=extension_uuid) + ref = query.first() + if not ref: + raise IntraExtensionNotFound() + old_ref = ref.to_dict() + if subject_uuid in old_ref["subject_category_assignments"]: + if category_uuid in old_ref["subject_category_assignments"][subject_uuid]: + old_ref["subject_category_assignments"][subject_uuid][category_uuid].remove(scope_uuid) + if not old_ref["subject_category_assignments"][subject_uuid][category_uuid]: + old_ref["subject_category_assignments"][subject_uuid].pop(category_uuid) + if not old_ref["subject_category_assignments"][subject_uuid]: + old_ref["subject_category_assignments"].pop(subject_uuid) + try: + self.set_subject_category_assignment_dict( + extension_uuid, + subject_uuid, + old_ref["subject_category_assignments"][subject_uuid]) + except KeyError: + self.set_subject_category_assignment_dict( + extension_uuid, + subject_uuid, + {}) + + # Getter and Setter for object_category_assignment + + def get_object_category_assignment_dict(self, extension_uuid, object_uuid): + """ From a object_uuid, return a dictionary of (category: scope for that object) + + :param extension_uuid: intra extension UUID + :param object_uuid: object UUID + :return: a dictionary of (keys are category nd values are scope for that object) + """ + with sql.transaction() as session: + query = session.query(ObjectCategoryAssignment) + query = query.filter_by(intra_extension_uuid=extension_uuid) + ref = query.first() + if not ref: + raise IntraExtensionNotFound() + _ref = ref.to_dict() + if object_uuid in _ref["object_category_assignments"]: + _backup_dict = _ref["object_category_assignments"][object_uuid] + _ref["object_category_assignments"] = dict() + _ref["object_category_assignments"][object_uuid] = _backup_dict + else: + _ref["object_category_assignments"] = dict() + _ref["object_category_assignments"][object_uuid] = dict() + return _ref + + def set_object_category_assignment_dict(self, extension_uuid, object_uuid=None, assignment_dict={}): + with sql.transaction() as session: + query = session.query(ObjectCategoryAssignment) + query = query.filter_by(intra_extension_uuid=extension_uuid) + ref = query.first() + if type(assignment_dict) is not dict: + raise IntraExtensionError() + for value in assignment_dict.values(): + if type(value) is not list: + raise IntraExtensionError(str(value)) + new_ref = ObjectCategoryAssignment.from_dict( + { + "id": uuid4().hex, + 'object_category_assignments': {object_uuid: assignment_dict}, + 'intra_extension_uuid': extension_uuid + } + ) + if not ref: + session.add(new_ref) + ref = new_ref + else: + new_ref.object_category_assignments[object_uuid] = assignment_dict + for attr in ObjectCategoryAssignment.attributes: + if attr != 'id': + setattr(ref, attr, getattr(new_ref, attr)) + return ref.to_dict() + + def add_object_category_assignment_dict(self, extension_uuid, object_uuid, category_uuid, scope_uuid): + with sql.transaction() as session: + query = session.query(ObjectCategoryAssignment) + query = query.filter_by(intra_extension_uuid=extension_uuid) + ref = query.first() + if not ref: + raise IntraExtensionNotFound() + assignments = ref.to_dict()['object_category_assignments'] + if object_uuid not in assignments: + assignments[object_uuid] = dict() + if category_uuid not in assignments[object_uuid]: + assignments[object_uuid][category_uuid] = list() + if scope_uuid not in assignments[object_uuid][category_uuid]: + assignments[object_uuid][category_uuid].append(scope_uuid) + return self.set_object_category_assignment_dict( + extension_uuid, + object_uuid, + assignments[object_uuid]) + + def remove_object_category_assignment(self, extension_uuid, object_uuid, category_uuid, scope_uuid): + with sql.transaction() as session: + query = session.query(ObjectCategoryAssignment) + query = query.filter_by(intra_extension_uuid=extension_uuid) + ref = query.first() + if not ref: + raise IntraExtensionNotFound() + old_ref = ref.to_dict() + if object_uuid in old_ref["object_category_assignments"]: + if category_uuid in old_ref["object_category_assignments"][object_uuid]: + old_ref["object_category_assignments"][object_uuid][category_uuid].remove(scope_uuid) + if not old_ref["object_category_assignments"][object_uuid][category_uuid]: + old_ref["object_category_assignments"][object_uuid].pop(category_uuid) + if not old_ref["object_category_assignments"][object_uuid]: + old_ref["object_category_assignments"].pop(object_uuid) + self.set_object_category_assignment_dict( + extension_uuid, + object_uuid, + old_ref["object_category_assignments"][object_uuid]) + + # Getter and Setter for action_category_assignment + + def get_action_category_assignment_dict(self, extension_uuid, action_uuid): + """ From a action_uuid, return a dictionary of (category: scope for that action) + + :param extension_uuid: intra extension UUID + :param action_uuid: action UUID + :return: a dictionary of (keys are category nd values are scope for that action) + """ + with sql.transaction() as session: + query = session.query(ActionCategoryAssignment) + query = query.filter_by(intra_extension_uuid=extension_uuid) + ref = query.first() + if not ref: + raise IntraExtensionNotFound() + _ref = ref.to_dict() + if action_uuid in _ref["action_category_assignments"]: + _backup_dict = _ref["action_category_assignments"][action_uuid] + _ref["action_category_assignments"] = dict() + _ref["action_category_assignments"][action_uuid] = _backup_dict + else: + _ref["action_category_assignments"] = dict() + _ref["action_category_assignments"][action_uuid] = dict() + return _ref + + def set_action_category_assignment_dict(self, extension_uuid, action_uuid=None, assignment_dict={}): + with sql.transaction() as session: + query = session.query(ActionCategoryAssignment) + query = query.filter_by(intra_extension_uuid=extension_uuid) + ref = query.first() + if type(assignment_dict) is not dict: + raise IntraExtensionError() + for value in assignment_dict.values(): + if type(value) is not list: + raise IntraExtensionError(str(value)) + new_ref = ActionCategoryAssignment.from_dict( + { + "id": uuid4().hex, + 'action_category_assignments': {action_uuid: assignment_dict}, + 'intra_extension_uuid': extension_uuid + } + ) + if not ref: + session.add(new_ref) + ref = new_ref + else: + new_ref.action_category_assignments[action_uuid] = assignment_dict + for attr in ActionCategoryAssignment.attributes: + if attr != 'id': + setattr(ref, attr, getattr(new_ref, attr)) + return ref.to_dict() + + def add_action_category_assignment_dict(self, extension_uuid, action_uuid, category_uuid, scope_uuid): + with sql.transaction() as session: + query = session.query(ActionCategoryAssignment) + query = query.filter_by(intra_extension_uuid=extension_uuid) + ref = query.first() + if not ref: + raise IntraExtensionNotFound() + assignments = ref.to_dict()['action_category_assignments'] + if action_uuid not in assignments: + assignments[action_uuid] = dict() + if category_uuid not in assignments[action_uuid]: + assignments[action_uuid][category_uuid] = list() + if scope_uuid not in assignments[action_uuid][category_uuid]: + assignments[action_uuid][category_uuid].append(scope_uuid) + return self.set_action_category_assignment_dict( + extension_uuid, + action_uuid, + assignments[action_uuid]) + + def remove_action_category_assignment(self, extension_uuid, action_uuid, category_uuid, scope_uuid): + with sql.transaction() as session: + query = session.query(ActionCategoryAssignment) + query = query.filter_by(intra_extension_uuid=extension_uuid) + ref = query.first() + if not ref: + raise IntraExtensionNotFound() + old_ref = ref.to_dict() + if action_uuid in old_ref["action_category_assignments"]: + if category_uuid in old_ref["action_category_assignments"][action_uuid]: + old_ref["action_category_assignments"][action_uuid][category_uuid].remove(scope_uuid) + if not old_ref["action_category_assignments"][action_uuid][category_uuid]: + old_ref["action_category_assignments"][action_uuid].pop(category_uuid) + if not old_ref["action_category_assignments"][action_uuid]: + old_ref["action_category_assignments"].pop(action_uuid) + self.set_action_category_assignment_dict( + extension_uuid, + action_uuid, + old_ref["action_category_assignments"][action_uuid]) + + # Getter and Setter for meta_rule + + def get_meta_rule_dict(self, extension_uuid): + with sql.transaction() as session: + query = session.query(MetaRule) + query = query.filter_by(intra_extension_uuid=extension_uuid) + ref = query.first() + if not ref: + raise IntraExtensionNotFound() + return ref.to_dict() + + def set_meta_rule_dict(self, extension_uuid, meta_rule): + with sql.transaction() as session: + query = session.query(MetaRule) + query = query.filter_by(intra_extension_uuid=extension_uuid) + ref = query.first() + meta_rule["id"] = uuid4().hex + meta_rule["intra_extension_uuid"] = extension_uuid + new_ref = MetaRule.from_dict(meta_rule) + if not ref: + session.add(new_ref) + ref = new_ref + else: + for attr in MetaRule.attributes: + if attr != 'id': + setattr(ref, attr, getattr(new_ref, attr)) + return ref.to_dict() + + # Getter and Setter for rules + + def get_rules(self, extension_uuid): + with sql.transaction() as session: + query = session.query(Rule) + query = query.filter_by(intra_extension_uuid=extension_uuid) + ref = query.first() + if not ref: + raise IntraExtensionNotFound() + return ref.to_dict() + + def set_rules(self, extension_uuid, subrules): + with sql.transaction() as session: + query = session.query(Rule) + query = query.filter_by(intra_extension_uuid=extension_uuid) + ref = query.first() + rules = dict() + rules["id"] = uuid4().hex + rules["intra_extension_uuid"] = extension_uuid + rules["rules"] = subrules + new_ref = Rule.from_dict(rules) + if not ref: + session.add(new_ref) + ref = new_ref + else: + for attr in Rule.attributes: + if attr != 'id': + setattr(ref, attr, getattr(new_ref, attr)) + return ref.to_dict() + + +class TenantConnector(TenantDriver): + + def get_tenant_dict(self): + with sql.transaction() as session: + query = session.query(Tenant) + # query = query.filter_by(uuid=tenant_uuid) + # ref = query.first().to_dict() + tenants = query.all() + if not tenants: + raise TenantListEmptyError() + return {tenant.id: Tenant.to_dict(tenant) for tenant in tenants} + # return [Tenant.to_dict(tenant) for tenant in tenants] + + def set_tenant_dict(self, tenant): + with sql.transaction() as session: + uuid = tenant.keys()[0] + query = session.query(Tenant) + query = query.filter_by(id=uuid) + ref = query.first() + if not ref: + # if not result, create the database line + ref = Tenant.from_dict(tenant) + session.add(ref) + return Tenant.to_dict(ref) + elif not tenant[uuid]["authz"] and not tenant[uuid]["admin"]: + # if admin and authz extensions are not set, delete the mapping + session.delete(ref) + return + elif tenant[uuid]["authz"] or tenant[uuid]["admin"]: + tenant_ref = ref.to_dict() + tenant_ref.update(tenant[uuid]) + new_tenant = Tenant( + id=uuid, + name=tenant[uuid]["name"], + authz=tenant[uuid]["authz"], + admin=tenant[uuid]["admin"], + ) + for attr in Tenant.attributes: + if attr != 'id': + setattr(ref, attr, getattr(new_tenant, attr)) + return Tenant.to_dict(ref) + raise TenantError() + + +# class InterExtension(sql.ModelBase, sql.DictBase): +# __tablename__ = 'inter_extension' +# attributes = [ +# 'id', +# 'requesting_intra_extension_uuid', +# 'requested_intra_extension_uuid', +# 'virtual_entity_uuid', +# 'genre', +# 'description', +# ] +# id = sql.Column(sql.String(64), primary_key=True) +# requesting_intra_extension_uuid = sql.Column(sql.String(64)) +# requested_intra_extension_uuid = sql.Column(sql.String(64)) +# virtual_entity_uuid = sql.Column(sql.String(64)) +# genre = sql.Column(sql.String(64)) +# description = sql.Column(sql.Text()) +# +# @classmethod +# def from_dict(cls, d): +# """Override parent from_dict() method with a simpler implementation. +# """ +# new_d = d.copy() +# return cls(**new_d) +# +# def to_dict(self): +# """Override parent to_dict() method with a simpler implementation. +# """ +# return dict(six.iteritems(self)) +# +# +# class InterExtensionConnector(InterExtensionDriver): +# +# def get_inter_extensions(self): +# with sql.transaction() as session: +# query = session.query(InterExtension.id) +# interextensions = query.all() +# return [interextension.id for interextension in interextensions] +# +# def create_inter_extensions(self, inter_id, inter_extension): +# with sql.transaction() as session: +# ie_ref = InterExtension.from_dict(inter_extension) +# session.add(ie_ref) +# return InterExtension.to_dict(ie_ref) +# +# def get_inter_extension(self, uuid): +# with sql.transaction() as session: +# query = session.query(InterExtension) +# query = query.filter_by(id=uuid) +# ref = query.first() +# if not ref: +# raise exception.NotFound +# return ref.to_dict() +# +# def delete_inter_extensions(self, inter_extension_id): +# with sql.transaction() as session: +# ref = session.query(InterExtension).get(inter_extension_id) +# session.delete(ref) + diff --git a/keystone-moon/keystone/contrib/moon/controllers.py b/keystone-moon/keystone/contrib/moon/controllers.py new file mode 100644 index 00000000..3c87da45 --- /dev/null +++ b/keystone-moon/keystone/contrib/moon/controllers.py @@ -0,0 +1,611 @@ +# Copyright 2015 Open Platform for NFV Project, Inc. and its contributors +# This software is distributed under the terms and conditions of the 'Apache-2.0' +# license which can be found in the file 'LICENSE' in this package distribution +# or at 'http://www.apache.org/licenses/LICENSE-2.0'. + +from keystone.common import controller +from keystone.common import dependency +from keystone import config +from keystone.models import token_model +from keystone import exception +import os +import glob +from oslo_log import log + +CONF = config.CONF +LOG = log.getLogger(__name__) + + +@dependency.requires('authz_api') +class Authz_v3(controller.V3Controller): + + def __init__(self): + super(Authz_v3, self).__init__() + + @controller.protected() + def get_authz(self, context, tenant_id, subject_id, object_id, action_id): + # TODO (dthom): build the authz functionality + try: + _authz = self.authz_api.authz(tenant_id, subject_id, object_id, action_id) + except exception.NotFound: + _authz = True + except: + _authz = False + return {"authz": _authz, + "tenant_id": tenant_id, + "subject_id": subject_id, + "object_id": object_id, + "action_id": action_id} + + +@dependency.requires('admin_api', 'authz_api') +class IntraExtensions(controller.V3Controller): + collection_name = 'intra_extensions' + member_name = 'intra_extension' + + def __init__(self): + super(IntraExtensions, self).__init__() + + def _get_user_from_token(self, token_id): + response = self.token_provider_api.validate_token(token_id) + token_ref = token_model.KeystoneToken(token_id=token_id, token_data=response) + return token_ref['user'] + + # IntraExtension functions + @controller.protected() + def get_intra_extensions(self, context, **kw): + user = self._get_user_from_token(context["token_id"]) + return { + "intra_extensions": + self.admin_api.get_intra_extension_list() + } + + @controller.protected() + def get_intra_extension(self, context, **kw): + user = self._get_user_from_token(context["token_id"]) + return { + "intra_extensions": + self.admin_api.get_intra_extension(uuid=kw['intra_extensions_id']) + } + + @controller.protected() + def create_intra_extension(self, context, **kw): + user = self._get_user_from_token(context["token_id"]) + return self.admin_api.load_intra_extension(kw) + + @controller.protected() + def delete_intra_extension(self, context, **kw): + user = self._get_user_from_token(context["token_id"]) + if "intra_extensions_id" not in kw: + raise exception.Error + return self.admin_api.delete_intra_extension(kw["intra_extensions_id"]) + + # Perimeter functions + @controller.protected() + def get_subjects(self, context, **kw): + user = self._get_user_from_token(context["token_id"]) + ie_uuid = kw["intra_extensions_id"] + return self.admin_api.get_subject_dict(user, ie_uuid) + + @controller.protected() + def add_subject(self, context, **kw): + user = self._get_user_from_token(context["token_id"]) + ie_uuid = kw["intra_extensions_id"] + subject = kw["subject_id"] + return self.admin_api.add_subject_dict(user, ie_uuid, subject) + + @controller.protected() + def del_subject(self, context, **kw): + user = self._get_user_from_token(context["token_id"]) + ie_uuid = kw["intra_extensions_id"] + subject = kw["subject_id"] + return self.admin_api.del_subject(user, ie_uuid, subject) + + @controller.protected() + def get_objects(self, context, **kw): + user = self._get_user_from_token(context["token_id"]) + ie_uuid = kw["intra_extensions_id"] + return self.admin_api.get_object_dict(user, ie_uuid) + + @controller.protected() + def add_object(self, context, **kw): + user = self._get_user_from_token(context["token_id"]) + ie_uuid = kw["intra_extensions_id"] + object_id = kw["object_id"] + return self.admin_api.add_object_dict(user, ie_uuid, object_id) + + @controller.protected() + def del_object(self, context, **kw): + user = self._get_user_from_token(context["token_id"]) + ie_uuid = kw["intra_extensions_id"] + object_id = kw["object_id"] + return self.admin_api.del_object(user, ie_uuid, object_id) + + @controller.protected() + def get_actions(self, context, **kw): + user = self._get_user_from_token(context["token_id"]) + ie_uuid = kw["intra_extensions_id"] + return self.admin_api.get_action_dict(user, ie_uuid) + + @controller.protected() + def add_action(self, context, **kw): + user = self._get_user_from_token(context["token_id"]) + ie_uuid = kw["intra_extensions_id"] + action = kw["action_id"] + return self.admin_api.add_action_dict(user, ie_uuid, action) + + @controller.protected() + def del_action(self, context, **kw): + user = self._get_user_from_token(context["token_id"]) + ie_uuid = kw["intra_extensions_id"] + action = kw["action_id"] + return self.admin_api.del_action(user, ie_uuid, action) + + # Metadata functions + @controller.protected() + def get_subject_categories(self, context, **kw): + user = self._get_user_from_token(context["token_id"]) + ie_uuid = kw["intra_extensions_id"] + return self.admin_api.get_subject_category_dict(user, ie_uuid) + + @controller.protected() + def add_subject_category(self, context, **kw): + user = self._get_user_from_token(context["token_id"]) + ie_uuid = kw["intra_extensions_id"] + subject_category = kw["subject_category_id"] + return self.admin_api.add_subject_category_dict(user, ie_uuid, subject_category) + + @controller.protected() + def del_subject_category(self, context, **kw): + user = self._get_user_from_token(context["token_id"]) + ie_uuid = kw["intra_extensions_id"] + subject_category = kw["subject_category_id"] + return self.admin_api.del_subject_category(user, ie_uuid, subject_category) + + @controller.protected() + def get_object_categories(self, context, **kw): + user = self._get_user_from_token(context["token_id"]) + ie_uuid = kw["intra_extensions_id"] + return self.admin_api.get_object_category_dict(user, ie_uuid) + + @controller.protected() + def add_object_category(self, context, **kw): + user = self._get_user_from_token(context["token_id"]) + ie_uuid = kw["intra_extensions_id"] + object_category = kw["object_category_id"] + return self.admin_api.add_object_category_dict(user, ie_uuid, object_category) + + @controller.protected() + def del_object_category(self, context, **kw): + user = self._get_user_from_token(context["token_id"]) + ie_uuid = kw["intra_extensions_id"] + object_category = kw["object_category_id"] + return self.admin_api.del_object_category(user, ie_uuid, object_category) + + @controller.protected() + def get_action_categories(self, context, **kw): + user = self._get_user_from_token(context["token_id"]) + ie_uuid = kw["intra_extensions_id"] + return self.admin_api.get_action_category_dict(user, ie_uuid) + + @controller.protected() + def add_action_category(self, context, **kw): + user = self._get_user_from_token(context["token_id"]) + ie_uuid = kw["intra_extensions_id"] + action_category = kw["action_category_id"] + return self.admin_api.add_action_category_dict(user, ie_uuid, action_category) + + @controller.protected() + def del_action_category(self, context, **kw): + user = self._get_user_from_token(context["token_id"]) + ie_uuid = kw["intra_extensions_id"] + action_category = kw["action_category_id"] + return self.admin_api.del_action_category(user, ie_uuid, action_category) + + # Scope functions + @controller.protected() + def get_subject_category_scope(self, context, **kw): + user = self._get_user_from_token(context["token_id"]) + ie_uuid = kw["intra_extensions_id"] + category_id = kw["subject_category_id"] + return self.admin_api.get_subject_category_scope_dict(user, ie_uuid, category_id) + + @controller.protected() + def add_subject_category_scope(self, context, **kw): + user = self._get_user_from_token(context["token_id"]) + ie_uuid = kw["intra_extensions_id"] + subject_category = kw["subject_category_id"] + subject_category_scope = kw["subject_category_scope_id"] + return self.admin_api.add_subject_category_scope_dict( + user, + ie_uuid, + subject_category, + subject_category_scope) + + @controller.protected() + def del_subject_category_scope(self, context, **kw): + user = self._get_user_from_token(context["token_id"]) + ie_uuid = kw["intra_extensions_id"] + subject_category = kw["subject_category_id"] + subject_category_scope = kw["subject_category_scope_id"] + return self.admin_api.del_subject_category_scope( + user, + ie_uuid, + subject_category, + subject_category_scope) + + @controller.protected() + def get_object_category_scope(self, context, **kw): + user = self._get_user_from_token(context["token_id"]) + ie_uuid = kw["intra_extensions_id"] + category_id = kw["object_category_id"] + return self.admin_api.get_object_category_scope_dict(user, ie_uuid, category_id) + + @controller.protected() + def add_object_category_scope(self, context, **kw): + user = self._get_user_from_token(context["token_id"]) + ie_uuid = kw["intra_extensions_id"] + object_category = kw["object_category_id"] + object_category_scope = kw["object_category_scope_id"] + return self.admin_api.add_object_category_scope_dict( + user, + ie_uuid, + object_category, + object_category_scope) + + @controller.protected() + def del_object_category_scope(self, context, **kw): + user = self._get_user_from_token(context["token_id"]) + ie_uuid = kw["intra_extensions_id"] + object_category = kw["object_category_id"] + object_category_scope = kw["object_category_scope_id"] + return self.admin_api.del_object_category_scope( + user, + ie_uuid, + object_category, + object_category_scope) + + @controller.protected() + def get_action_category_scope(self, context, **kw): + user = self._get_user_from_token(context["token_id"]) + ie_uuid = kw["intra_extensions_id"] + category_id = kw["action_category_id"] + return self.admin_api.get_action_category_scope_dict(user, ie_uuid, category_id) + + @controller.protected() + def add_action_category_scope(self, context, **kw): + user = self._get_user_from_token(context["token_id"]) + ie_uuid = kw["intra_extensions_id"] + action_category = kw["action_category_id"] + action_category_scope = kw["action_category_scope_id"] + return self.admin_api.add_action_category_scope_dict( + user, + ie_uuid, + action_category, + action_category_scope) + + @controller.protected() + def del_action_category_scope(self, context, **kw): + user = self._get_user_from_token(context["token_id"]) + ie_uuid = kw["intra_extensions_id"] + action_category = kw["action_category_id"] + action_category_scope = kw["action_category_scope_id"] + return self.admin_api.del_action_category_scope( + user, + ie_uuid, + action_category, + action_category_scope) + + # Assignment functions + @controller.protected() + def get_subject_assignments(self, context, **kw): + user = self._get_user_from_token(context["token_id"]) + ie_uuid = kw["intra_extensions_id"] + subject_id = kw["subject_id"] + return self.admin_api.get_subject_category_assignment_dict(user, ie_uuid, subject_id) + + @controller.protected() + def add_subject_assignment(self, context, **kw): + user = self._get_user_from_token(context["token_id"]) + ie_uuid = kw["intra_extensions_id"] + subject_id = kw["subject_id"] + subject_category = kw["subject_category"] + subject_category_scope = kw["subject_category_scope"] + return self.admin_api.add_subject_category_assignment_dict( + user, + ie_uuid, + subject_id, + subject_category, + subject_category_scope) + + @controller.protected() + def del_subject_assignment(self, context, **kw): + user = self._get_user_from_token(context["token_id"]) + ie_uuid = kw["intra_extensions_id"] + subject_id = kw["subject_id"] + subject_category = kw["subject_category"] + subject_category_scope = kw["subject_category_scope"] + return self.admin_api.del_subject_category_assignment( + user, + ie_uuid, + subject_id, + subject_category, + subject_category_scope) + + @controller.protected() + def get_object_assignments(self, context, **kw): + user = self._get_user_from_token(context["token_id"]) + ie_uuid = kw["intra_extensions_id"] + object_id = kw["object_id"] + return self.admin_api.get_object_category_assignment_dict(user, ie_uuid, object_id) + + @controller.protected() + def add_object_assignment(self, context, **kw): + user = self._get_user_from_token(context["token_id"]) + ie_uuid = kw["intra_extensions_id"] + object_id = kw["object_id"] + object_category = kw["object_category"] + object_category_scope = kw["object_category_scope"] + return self.admin_api.add_object_category_assignment_dict( + user, + ie_uuid, + object_id, + object_category, + object_category_scope) + + @controller.protected() + def del_object_assignment(self, context, **kw): + user = self._get_user_from_token(context["token_id"]) + ie_uuid = kw["intra_extensions_id"] + object_id = kw["object_id"] + object_category = kw["object_category"] + object_category_scope = kw["object_category_scope"] + return self.admin_api.del_object_category_assignment( + user, + ie_uuid, + object_id, + object_category, + object_category_scope) + + @controller.protected() + def get_action_assignments(self, context, **kw): + user = self._get_user_from_token(context["token_id"]) + ie_uuid = kw["intra_extensions_id"] + action_id = kw["action_id"] + return self.admin_api.get_action_category_assignment_dict(user, ie_uuid, action_id) + + @controller.protected() + def add_action_assignment(self, context, **kw): + user = self._get_user_from_token(context["token_id"]) + ie_uuid = kw["intra_extensions_id"] + action_id = kw["action_id"] + action_category = kw["action_category"] + action_category_scope = kw["action_category_scope"] + return self.admin_api.add_action_category_assignment_dict( + user, + ie_uuid, + action_id, + action_category, + action_category_scope) + + @controller.protected() + def del_action_assignment(self, context, **kw): + user = self._get_user_from_token(context["token_id"]) + ie_uuid = kw["intra_extensions_id"] + action_id = kw["action_id"] + action_category = kw["action_category"] + action_category_scope = kw["action_category_scope"] + return self.admin_api.del_object_category_assignment( + user, + ie_uuid, + action_id, + action_category, + action_category_scope) + + # Metarule functions + @controller.protected() + def get_aggregation_algorithms(self, context, **kw): + user = self._get_user_from_token(context["token_id"]) + ie_uuid = kw["intra_extensions_id"] + return self.admin_api.get_aggregation_algorithms(user, ie_uuid) + + @controller.protected() + def get_aggregation_algorithm(self, context, **kw): + user = self._get_user_from_token(context["token_id"]) + ie_uuid = kw["intra_extensions_id"] + return self.admin_api.get_aggregation_algorithm(user, ie_uuid) + + @controller.protected() + def set_aggregation_algorithm(self, context, **kw): + user = self._get_user_from_token(context["token_id"]) + ie_uuid = kw["intra_extensions_id"] + aggregation_algorithm = kw["aggregation_algorithm"] + return self.admin_api.set_aggregation_algorithm(user, ie_uuid, aggregation_algorithm) + + @controller.protected() + def get_sub_meta_rule(self, context, **kw): + user = self._get_user_from_token(context["token_id"]) + ie_uuid = kw["intra_extensions_id"] + return self.admin_api.get_sub_meta_rule(user, ie_uuid) + + @controller.protected() + def set_sub_meta_rule(self, context, **kw): + user = self._get_user_from_token(context["token_id"]) + ie_uuid = kw.pop("intra_extensions_id") + # subject_categories = kw["subject_categories"] + # action_categories = kw["action_categories"] + # object_categories = kw["object_categories"] + # relation = kw["relation"] + # aggregation_algorithm = kw["aggregation_algorithm"] + return self.admin_api.set_sub_meta_rule( + user, + ie_uuid, + kw) + + @controller.protected() + def get_sub_meta_rule_relations(self, context, **kw): + user = self._get_user_from_token(context["token_id"]) + ie_uuid = kw["intra_extensions_id"] + return self.admin_api.get_sub_meta_rule_relations(user, ie_uuid) + + # Rules functions + @controller.protected() + def get_sub_rules(self, context, **kw): + user = self._get_user_from_token(context["token_id"]) + ie_uuid = kw["intra_extensions_id"] + return self.admin_api.get_sub_rules(user, ie_uuid) + + @controller.protected() + def set_sub_rule(self, context, **kw): + user = self._get_user_from_token(context["token_id"]) + ie_uuid = kw["intra_extensions_id"] + sub_rule = kw["rule"] + relation = kw["relation"] + return self.admin_api.set_sub_rule(user, ie_uuid, relation, sub_rule) + + @controller.protected() + def del_sub_rule(self, context, **kw): + user = self._get_user_from_token(context["token_id"]) + ie_uuid = kw["intra_extensions_id"] + relation_name = kw["relation_name"] + rule = kw["rule"] + return self.admin_api.del_sub_rule( + user, + ie_uuid, + relation_name, + rule) + + +class AuthzPolicies(controller.V3Controller): + collection_name = 'authz_policies' + member_name = 'authz_policy' + + def __init__(self): + super(AuthzPolicies, self).__init__() + + @controller.protected() + def get_authz_policies(self, context, **kw): + nodes = glob.glob(os.path.join(CONF.moon.policy_directory, "*")) + return { + "authz_policies": + [os.path.basename(n) for n in nodes if os.path.isdir(n)] + } + + +@dependency.requires('tenant_api', 'resource_api') +class Tenants(controller.V3Controller): + + def __init__(self): + super(Tenants, self).__init__() + + def _get_user_from_token(self, token_id): + response = self.token_provider_api.validate_token(token_id) + token_ref = token_model.KeystoneToken(token_id=token_id, token_data=response) + return token_ref['user'] + + @controller.protected() + def get_tenants(self, context, **kw): + # user = self._get_user_from_token(context["token_id"]) + return { + "tenants": + self.tenant_api.get_tenant_dict() + } + + @controller.protected() + def get_tenant(self, context, **kw): + # user = self._get_user_from_token(context["token_id"]) + tenant_uuid = kw.get("tenant_uuid") + return { + "tenant": + self.tenant_api.get_tenant_dict()[tenant_uuid] + } + + @controller.protected() + def set_tenant(self, context, **kw): + # user = self._get_user_from_token(context["token_id"]) + tenant_uuid = kw.get("id") + name = self.resource_api.get_project(tenant_uuid)["name"] + authz = kw.get("authz") + admin = kw.get("admin") + self.tenant_api.set_tenant_dict(tenant_uuid, name, authz, admin) + return { + "tenant": + self.tenant_api.get_tenant_dict()[tenant_uuid] + } + + @controller.protected() + def delete_tenant(self, context, **kw): + # user = self._get_user_from_token(context["token_id"]) + tenant_uuid = kw.get("tenant_uuid") + self.tenant_api.set_tenant_dict(tenant_uuid, None, None, None) + + +@dependency.requires('authz_api') +class InterExtensions(controller.V3Controller): + + def __init__(self): + super(InterExtensions, self).__init__() + + def _get_user_from_token(self, token_id): + response = self.token_provider_api.validate_token(token_id) + token_ref = token_model.KeystoneToken(token_id=token_id, token_data=response) + return token_ref['user'] + + # @controller.protected() + # def get_inter_extensions(self, context, **kw): + # user = self._get_user_from_token(context["token_id"]) + # return { + # "inter_extensions": + # self.interextension_api.get_inter_extensions() + # } + + # @controller.protected() + # def get_inter_extension(self, context, **kw): + # user = self._get_user_from_token(context["token_id"]) + # return { + # "inter_extensions": + # self.interextension_api.get_inter_extension(uuid=kw['inter_extensions_id']) + # } + + # @controller.protected() + # def create_inter_extension(self, context, **kw): + # user = self._get_user_from_token(context["token_id"]) + # return self.interextension_api.create_inter_extension(kw) + + # @controller.protected() + # def delete_inter_extension(self, context, **kw): + # user = self._get_user_from_token(context["token_id"]) + # if "inter_extensions_id" not in kw: + # raise exception.Error + # return self.interextension_api.delete_inter_extension(kw["inter_extensions_id"]) + + +@dependency.requires('authz_api') +class SuperExtensions(controller.V3Controller): + + def __init__(self): + super(SuperExtensions, self).__init__() + + +@dependency.requires('moonlog_api', 'authz_api') +class Logs(controller.V3Controller): + + def __init__(self): + super(Logs, self).__init__() + + def _get_user_from_token(self, token_id): + response = self.token_provider_api.validate_token(token_id) + token_ref = token_model.KeystoneToken(token_id=token_id, token_data=response) + return token_ref['user'] + + @controller.protected() + def get_logs(self, context, **kw): + user = self._get_user_from_token(context["token_id"]) + options = kw.get("options", "") + # FIXME (dthom): the authorization for get_logs must be done with an intra_extension + #if self.authz_api.admin(user["name"], "logs", "read"): + return { + "logs": + self.moonlog_api.get_logs(options) + } + diff --git a/keystone-moon/keystone/contrib/moon/core.py b/keystone-moon/keystone/contrib/moon/core.py new file mode 100644 index 00000000..1dc23c4a --- /dev/null +++ b/keystone-moon/keystone/contrib/moon/core.py @@ -0,0 +1,2375 @@ +# Copyright 2015 Open Platform for NFV Project, Inc. and its contributors +# This software is distributed under the terms and conditions of the 'Apache-2.0' +# license which can be found in the file 'LICENSE' in this package distribution +# or at 'http://www.apache.org/licenses/LICENSE-2.0'. + +from uuid import uuid4 +import os +import json +import copy +import re +import six + +from keystone.common import manager +from keystone import config +from oslo_log import log +from keystone.common import dependency +from keystone import exception +from oslo_config import cfg +from keystone.i18n import _ + +from keystone.contrib.moon.exception import * + +CONF = config.CONF +LOG = log.getLogger(__name__) + +_OPTS = [ + cfg.StrOpt('authz_driver', + default='keystone.contrib.moon.backends.flat.SuperExtensionConnector', + help='Authorisation backend driver.'), + cfg.StrOpt('log_driver', + default='keystone.contrib.moon.backends.flat.LogConnector', + help='Logs backend driver.'), + cfg.StrOpt('superextension_driver', + default='keystone.contrib.moon.backends.flat.SuperExtensionConnector', + help='SuperExtension backend driver.'), + cfg.StrOpt('intraextension_driver', + default='keystone.contrib.moon.backends.sql.IntraExtensionConnector', + help='IntraExtension backend driver.'), + cfg.StrOpt('tenant_driver', + default='keystone.contrib.moon.backends.sql.TenantConnector', + help='Tenant backend driver.'), + cfg.StrOpt('interextension_driver', + default='keystone.contrib.moon.backends.sql.InterExtensionConnector', + help='InterExtension backend driver.'), + cfg.StrOpt('policy_directory', + default='/etc/keystone/policies', + help='Local directory where all policies are stored.'), + cfg.StrOpt('super_extension_directory', + default='/etc/keystone/super_extension', + help='Local directory where SuperExtension configuration is stored.'), +] +CONF.register_opts(_OPTS, group='moon') + + +def filter_args(func): + def wrapped(*args, **kwargs): + _args = [] + for arg in args: + if type(arg) in (unicode, str): + arg = "".join(re.findall("[\w\-+]*", arg)) + _args.append(arg) + for arg in kwargs: + if type(kwargs[arg]) in (unicode, str): + kwargs[arg] = "".join(re.findall("[\w\-+]*", kwargs[arg])) + return func(*_args, **kwargs) + return wrapped + + +def enforce(actions, object, **extra): + def wrap(func): + def wrapped(*args): + global actions + self = args[0] + user_name = args[1] + intra_extension_uuid = args[2] + _admin_extension_uuid = self.tenant_api.get_admin_extension_uuid(args[2]) + # func.func_globals["_admin_extension_uuid"] = _admin_extension_uuid + if not _admin_extension_uuid: + args[0].moonlog_api.warning("No admin IntraExtension found, authorization granted by default.") + return func(*args) + else: + _authz = False + if type(actions) in (str, unicode): + actions = (actions, ) + for action in actions: + if self.authz_api.authz( + intra_extension_uuid, + user_name, + object, + action): + _authz = True + else: + _authz = False + break + if _authz: + return func(*args) + return wrapped + return wrap + + +def filter_input(data): + if type(data) not in (str, unicode): + return data + try: + return "".join(re.findall("[\w\-+*]", data)) + except TypeError: + LOG.error("Error in filtering input data: {}".format(data)) + + +@dependency.provider('moonlog_api') +class LogManager(manager.Manager): + + def __init__(self): + driver = CONF.moon.log_driver + super(LogManager, self).__init__(driver) + + def get_logs(self, options): + return self.driver.get_logs(options) + + def authz(self, message): + return self.driver.authz(message) + + def debug(self, message): + return self.driver.debug(message) + + def info(self, message): + return self.driver.info(message) + + def warning(self, message): + return self.driver.warning(message) + + def error(self, message): + return self.driver.error(message) + + def critical(self, message): + return self.driver.critical(message) + + +@dependency.provider('tenant_api') +@dependency.requires('moonlog_api') +class TenantManager(manager.Manager): + + def __init__(self): + super(TenantManager, self).__init__(CONF.moon.tenant_driver) + + def get_tenant_dict(self): + """ + Return a dictionnary with all tenants + :return: dict + """ + try: + return self.driver.get_tenant_dict() + except TenantListEmptyError: + self.moonlog_api.error(_("Tenant Mapping list is empty.")) + return {} + + def get_tenant_name(self, tenant_uuid): + _tenant_dict = self.get_tenant_dict() + if tenant_uuid not in _tenant_dict: + raise TenantNotFoundError(_("Tenant UUID ({}) was not found.".format(tenant_uuid))) + return _tenant_dict[tenant_uuid]["name"] + + def set_tenant_name(self, tenant_uuid, tenant_name): + _tenant_dict = self.get_tenant_dict() + if tenant_uuid not in _tenant_dict: + raise TenantNotFoundError(_("Tenant UUID ({}) was not found.".format(tenant_uuid))) + _tenant_dict[tenant_uuid]['name'] = tenant_name + return self.driver.set_tenant_dict(_tenant_dict) + + def get_extension_uuid(self, tenant_uuid, scope="authz"): + """ + Return the UUID of the scoped extension for a particular tenant. + :param tenant_uuid: UUID of the tenant + :param scope: "admin" or "authz" + :return (str): the UUID of the scoped extension + """ + # 1 tenant only with 1 authz extension and 1 admin extension + _tenant_dict = self.get_tenant_dict() + if tenant_uuid not in _tenant_dict: + raise TenantNotFoundError(_("Tenant UUID ({}) was not found.".format(tenant_uuid))) + if not _tenant_dict[tenant_uuid][scope]: + raise IntraExtensionNotFound(_("No IntraExtension found for Tenant {}.".format(tenant_uuid))) + return _tenant_dict[tenant_uuid][scope] + + def get_tenant_uuid(self, extension_uuid): + for _tenant_uuid, _tenant_value in six.iteritems(self.get_tenant_dict()): + if extension_uuid == _tenant_value["authz"] or extension_uuid == _tenant_value["admin"]: + return _tenant_uuid + raise TenantNotFoundError() + + def get_admin_extension_uuid(self, authz_extension_uuid): + _tenants = self.get_tenant_dict() + for _tenant_uuid in _tenants: + if authz_extension_uuid == _tenants[_tenant_uuid]['authz']and _tenants[_tenant_uuid]['admin']: + return _tenants[_tenant_uuid]['admin'] + self.moonlog_api.error(_("No IntraExtension found mapping this Authz IntraExtension: {}.".format( + authz_extension_uuid))) + # FIXME (dthom): if AdminIntraExtensionNotFound, maybe we can add an option in configuration file + # to allow or not the fact that Admin IntraExtension can be None + # raise AdminIntraExtensionNotFound() + + def delete(self, authz_extension_uuid): + _tenants = self.get_tenant_dict() + for _tenant_uuid in _tenants: + if authz_extension_uuid == _tenants[_tenant_uuid]['authz']: + return self.set_tenant_dict(_tenant_uuid, "", "", "") + raise AuthzIntraExtensionNotFound(_("No IntraExtension found mapping this Authz IntraExtension: {}.".format( + authz_extension_uuid))) + + def set_tenant_dict(self, tenant_uuid, name, authz_extension_uuid, admin_extension_uuid): + tenant = { + tenant_uuid: { + "name": name, + "authz": authz_extension_uuid, + "admin": admin_extension_uuid + } + } + # TODO (dthom): Tenant must be checked against Keystone database. + return self.driver.set_tenant_dict(tenant) + + +class TenantDriver: + + def get_tenant_dict(self): + raise exception.NotImplemented() # pragma: no cover + + def set_tenant_dict(self, tenant): + raise exception.NotImplemented() # pragma: no cover + + +@dependency.requires('identity_api', 'moonlog_api', 'tenant_api', 'authz_api') +class IntraExtensionManager(manager.Manager): + + __genre__ = None + + def __init__(self): + driver = CONF.moon.intraextension_driver + super(IntraExtensionManager, self).__init__(driver) + + def authz(self, uuid, sub, obj, act): + """Check authorization for a particular action. + + :param uuid: UUID of an IntraExtension + :param sub: subject of the request + :param obj: object of the request + :param act: action of the request + :return: True or False or raise an exception + """ + if not self.driver.get_intra_extension(uuid): + raise IntraExtensionNotFound() + # self.moonlog_api.authz("Unknown: Authorization framework disabled ({} {} {} {})".format(uuid, sub, obj, act)) + # self.moonlog_api.warning("Unknown: Authorization framework disabled ({} {} {} {})".format(uuid, sub, obj, act)) + # return True + # #TODO (dthom): must raise IntraExtensionNotAuthorized + # try: + # _subject_category_dict = self.driver.get_subject_category_dict(extension_uuid) + # _object_category_dict = self.driver.get_object_category_dict(extension_uuid) + # _action_category_dict = self.driver.get_action_category_dict(extension_uuid) + # _subject_category_value_dict = self.driver.get_subject_category_value_dict(extension_uuid, subject_name) + # _object_category_value_dict = self.driver.get_object_category_value_dict(extension_uuid, object_name) + # _action_category_value_dict = self.driver.get_action_category_value_dict(extension_uuid, action_name) + # _meta_rule = self.driver.get_meta_rule(extension_uuid) + # _rules = self.driver.get_rules(extension_uuid) + # # TODO: algorithm to validate requests + # return True + # except exception: # TODO: exception.IntraExtension.NotAuthorized + # pass + sub_meta_rule = self.driver.get_meta_rule(uuid) + subject_assignments = self.driver.get_subject_category_assignment_dict(uuid) + action_assignments = self.driver.get_action_category_assignment_dict(uuid) + object_assignments = self.driver.get_object_category_assignment_dict(uuid) + # check if subject exists + if sub not in self.driver.get_subject_dict(uuid): + self.moonlog_api.authz("KO: Subject {} unknown".format(sub)) + return False + # check if object exists + if obj not in self.driver.get_object_dict(uuid): + self.moonlog_api.authz("KO: Object {} unknown".format(obj)) + return False + # check if action exists + if act not in self.driver.get_action_dict(uuid): + self.moonlog_api.authz("KO: Action {} unknown".format(act)) + return False + # check if subject is in subject_assignment + for cat in subject_assignments.keys(): + if sub in subject_assignments[cat]: + break + else: + self.moonlog_api.authz("KO: Subject no found in categories {}".format( + subject_assignments.keys())) + return False + # check if object is in object_assignment + for cat in object_assignments.keys(): + if obj in object_assignments[cat]: + break + else: + self.moonlog_api.authz("KO: Object no found in categories {}".format( + object_assignments)) + return False + # check if action is in action_assignment + for cat in action_assignments.keys(): + if act in action_assignments[cat]: + break + else: + self.moonlog_api.authz("KO: Action no found in categories {}".format( + action_assignments.keys())) + return False + # get all rules for intra_extension + rules = self.driver.get_rules(uuid) + # check if relation exists in rules + relation_to_check = None + relations = self.driver.get_sub_meta_rule_relations(uuid) + for relation in rules: + if relation in relations: + # hypothesis: only one relation to check + relation_to_check = relation + break + else: + self.moonlog_api.authz("KO: No relation can be used {}".format(rules.keys())) + return False + for sub_rule in rules[relation_to_check]: + for cat in sub_meta_rule[relation_to_check]["subject_categories"]: + rule_scope = sub_rule.pop(0) + if rule_scope in subject_assignments[cat][sub]: + break + else: + continue + for cat in sub_meta_rule[relation_to_check]["action_categories"]: + rule_scope = sub_rule.pop(0) + if rule_scope in action_assignments[cat][act]: + break + else: + continue + for cat in sub_meta_rule[relation_to_check]["object_categories"]: + rule_scope = sub_rule.pop(0) + if rule_scope in object_assignments[cat][obj]: + break + else: + continue + self.moonlog_api.authz("OK ({} {},{},{})".format(uuid, sub, act, obj)) + return True + self.moonlog_api.authz("KO ({} {},{},{})".format(uuid, sub, act, obj)) + return False + + def __get_key_from_value(self, value, values_dict): + return filter(lambda v: v[1] == value, values_dict.iteritems())[0][0] + + def get_intra_extension_list(self): + # TODO: check will be done through super_extension later + return self.driver.get_intra_extension_list() + + def get_intra_extension_id_for_tenant(self, tenant_id): + for intra_extension_id in self.driver.get_intra_extension_list(): + if self.driver.get_intra_extension(intra_extension_id)["tenant"] == tenant_id: + return intra_extension_id + LOG.error("IntraExtension not found for tenant {}".format(tenant_id)) + raise exception.NotFound + + def get_intra_extension(self, uuid): + return self.driver.get_intra_extension(uuid) + + def set_perimeter_values(self, ie, policy_dir): + + perimeter_path = os.path.join(policy_dir, 'perimeter.json') + f = open(perimeter_path) + json_perimeter = json.load(f) + + subject_dict = dict() + # We suppose that all subjects can be mapped to a true user in Keystone + for _subject in json_perimeter['subjects']: + user = self.identity_api.get_user_by_name(_subject, "default") + subject_dict[user["id"]] = user["name"] + self.driver.set_subject_dict(ie["id"], subject_dict) + ie["subjects"] = subject_dict + + # Copy all values for objects and subjects + object_dict = dict() + for _object in json_perimeter['objects']: + object_dict[uuid4().hex] = _object + self.driver.set_object_dict(ie["id"], object_dict) + ie["objects"] = object_dict + + action_dict = dict() + for _action in json_perimeter['actions']: + action_dict[uuid4().hex] = _action + self.driver.set_action_dict(ie["id"], action_dict) + ie["ations"] = action_dict + + def set_metadata_values(self, ie, policy_dir): + + metadata_path = os.path.join(policy_dir, 'metadata.json') + f = open(metadata_path) + json_perimeter = json.load(f) + + subject_categories_dict = dict() + for _cat in json_perimeter['subject_categories']: + subject_categories_dict[uuid4().hex] = _cat + self.driver.set_subject_category_dict(ie["id"], subject_categories_dict) + # Initialize scope categories + for _cat in subject_categories_dict.keys(): + self.driver.set_subject_category_scope_dict(ie["id"], _cat, {}) + ie['subject_categories'] = subject_categories_dict + + object_categories_dict = dict() + for _cat in json_perimeter['object_categories']: + object_categories_dict[uuid4().hex] = _cat + self.driver.set_object_category_dict(ie["id"], object_categories_dict) + # Initialize scope categories + for _cat in object_categories_dict.keys(): + self.driver.set_object_category_scope_dict(ie["id"], _cat, {}) + ie['object_categories'] = object_categories_dict + + action_categories_dict = dict() + for _cat in json_perimeter['action_categories']: + action_categories_dict[uuid4().hex] = _cat + self.driver.set_action_category_dict(ie["id"], action_categories_dict) + # Initialize scope categories + for _cat in action_categories_dict.keys(): + self.driver.set_action_category_scope_dict(ie["id"], _cat, {}) + ie['action_categories'] = action_categories_dict + + def set_scope_values(self, ie, policy_dir): + + metadata_path = os.path.join(policy_dir, 'scope.json') + f = open(metadata_path) + json_perimeter = json.load(f) + + ie['subject_category_scope'] = dict() + for category, scope in json_perimeter["subject_category_scope"].iteritems(): + category = self.__get_key_from_value( + category, + self.driver.get_subject_category_dict(ie["id"])["subject_categories"]) + _scope_dict = dict() + for _scope in scope: + _scope_dict[uuid4().hex] = _scope + self.driver.set_subject_category_scope_dict(ie["id"], category, _scope_dict) + ie['subject_category_scope'][category] = _scope_dict + + ie['object_category_scope'] = dict() + for category, scope in json_perimeter["object_category_scope"].iteritems(): + category = self.__get_key_from_value( + category, + self.driver.get_object_category_dict(ie["id"])["object_categories"]) + _scope_dict = dict() + for _scope in scope: + _scope_dict[uuid4().hex] = _scope + self.driver.set_object_category_scope_dict(ie["id"], category, _scope_dict) + ie['object_category_scope'][category] = _scope_dict + + ie['action_category_scope'] = dict() + for category, scope in json_perimeter["action_category_scope"].iteritems(): + category = self.__get_key_from_value( + category, + self.driver.get_action_category_dict(ie["id"])["action_categories"]) + _scope_dict = dict() + for _scope in scope: + _scope_dict[uuid4().hex] = _scope + self.driver.set_action_category_scope_dict(ie["id"], category, _scope_dict) + ie['action_category_scope'][category] = _scope_dict + + def set_assignments_values(self, ie, policy_dir): + + f = open(os.path.join(policy_dir, 'assignment.json')) + json_assignments = json.load(f) + + subject_assignments = dict() + for category, value in json_assignments['subject_assignments'].iteritems(): + category = self.__get_key_from_value( + category, + self.driver.get_subject_category_dict(ie["id"])["subject_categories"]) + for user in value: + if user not in subject_assignments: + subject_assignments[user] = dict() + subject_assignments[user][category] = \ + map(lambda x: self.__get_key_from_value(x, ie['subject_category_scope'][category]), value[user]) + else: + subject_assignments[user][category].extend( + map(lambda x: self.__get_key_from_value(x, ie['subject_category_scope'][category]), value[user]) + ) + # Note (dthom): subject_category_assignment must be initialized because when there is no data in json + # we will not go through the for loop + self.driver.set_subject_category_assignment_dict(ie["id"]) + for subject in subject_assignments: + self.driver.set_subject_category_assignment_dict(ie["id"], subject, subject_assignments[subject]) + + object_assignments = dict() + for category, value in json_assignments["object_assignments"].iteritems(): + category = self.__get_key_from_value( + category, + self.driver.get_object_category_dict(ie["id"])["object_categories"]) + for object_name in value: + if object_name not in object_assignments: + object_assignments[object_name] = dict() + object_assignments[object_name][category] = \ + map(lambda x: self.__get_key_from_value(x, ie['object_category_scope'][category]), + value[object_name]) + else: + object_assignments[object_name][category].extend( + map(lambda x: self.__get_key_from_value(x, ie['object_category_scope'][category]), + value[object_name]) + ) + # Note (dthom): object_category_assignment must be initialized because when there is no data in json + # we will not go through the for loop + self.driver.set_object_category_assignment_dict(ie["id"]) + for object in object_assignments: + self.driver.set_object_category_assignment_dict(ie["id"], object, object_assignments[object]) + + action_assignments = dict() + for category, value in json_assignments["action_assignments"].iteritems(): + category = self.__get_key_from_value( + category, + self.driver.get_action_category_dict(ie["id"])["action_categories"]) + for action_name in value: + if action_name not in action_assignments: + action_assignments[action_name] = dict() + action_assignments[action_name][category] = \ + map(lambda x: self.__get_key_from_value(x, ie['action_category_scope'][category]), + value[action_name]) + else: + action_assignments[action_name][category].extend( + map(lambda x: self.__get_key_from_value(x, ie['action_category_scope'][category]), + value[action_name]) + ) + # Note (dthom): action_category_assignment must be initialized because when there is no data in json + # we will not go through the for loop + self.driver.set_action_category_assignment_dict(ie["id"]) + for action in action_assignments: + self.driver.set_action_category_assignment_dict(ie["id"], action, action_assignments[action]) + + def set_metarule_values(self, ie, policy_dir): + + metadata_path = os.path.join(policy_dir, 'metarule.json') + f = open(metadata_path) + json_metarule = json.load(f) + # ie["meta_rules"] = copy.deepcopy(json_metarule) + metarule = dict() + categories = { + "subject_categories": self.driver.get_subject_category_dict(ie["id"]), + "object_categories": self.driver.get_object_category_dict(ie["id"]), + "action_categories": self.driver.get_action_category_dict(ie["id"]) + } + # Translate value from JSON file to UUID for Database + for relation in json_metarule["sub_meta_rules"]: + metarule[relation] = dict() + for item in ("subject_categories", "object_categories", "action_categories"): + metarule[relation][item] = list() + for element in json_metarule["sub_meta_rules"][relation][item]: + metarule[relation][item].append(self.__get_key_from_value( + element, + categories[item][item] + )) + submetarules = { + "aggregation": json_metarule["aggregation"], + "sub_meta_rules": metarule + } + self.driver.set_meta_rule_dict(ie["id"], submetarules) + + def set_subrules_values(self, ie, policy_dir): + + metadata_path = os.path.join(policy_dir, 'rules.json') + f = open(metadata_path) + json_rules = json.load(f) + ie["sub_rules"] = {"rules": copy.deepcopy(json_rules)} + # Translate value from JSON file to UUID for Database + rules = dict() + sub_meta_rules = self.driver.get_meta_rule_dict(ie["id"]) + for relation in json_rules: + if relation not in self.get_sub_meta_rule_relations("admin", ie["id"])["sub_meta_rule_relations"]: + raise IntraExtensionError("Bad relation name {} in rules".format(relation)) + rules[relation] = list() + for rule in json_rules[relation]: + subrule = list() + for cat, cat_func in ( + ("subject_categories", self.driver.get_subject_category_scope_dict), + ("action_categories", self.driver.get_action_category_scope_dict), + ("object_categories", self.driver.get_object_category_scope_dict), + ): + for cat_value in sub_meta_rules["sub_meta_rules"][relation][cat]: + scope = cat_func( + ie["id"], + cat_value + )[cat_func.__name__.replace("get_", "").replace("_dict", "")] + + _ = rule.pop(0) + a_scope = self.__get_key_from_value(_, scope[cat_value]) + subrule.append(a_scope) + # if a positive/negative value exists, all titem of rule have not be consumed + if len(rule) >= 1 and type(rule[0]) is bool: + subrule.append(rule[0]) + else: + # if value doesn't exist add a default value + subrule.append(True) + rules[relation].append(subrule) + self.driver.set_rules(ie["id"], rules) + + def load_intra_extension(self, intra_extension): + ie = dict() + # TODO: clean some values + ie['id'] = uuid4().hex + ie["name"] = filter_input(intra_extension["name"]) + ie["model"] = filter_input(intra_extension["policymodel"]) + ie["description"] = filter_input(intra_extension["description"]) + ref = self.driver.set_intra_extension(ie['id'], ie) + self.moonlog_api.debug("Creation of IE: {}".format(ref)) + # read the profile given by "policymodel" and populate default variables + policy_dir = os.path.join(CONF.moon.policy_directory, ie["model"]) + self.set_perimeter_values(ie, policy_dir) + self.set_metadata_values(ie, policy_dir) + self.set_scope_values(ie, policy_dir) + self.set_assignments_values(ie, policy_dir) + self.set_metarule_values(ie, policy_dir) + self.set_subrules_values(ie, policy_dir) + return ref + + def delete_intra_extension(self, intra_extension_id): + ref = self.driver.delete_intra_extension(intra_extension_id) + return ref + + # Perimeter functions + + @filter_args + @enforce("read", "subjects") + def get_subject_dict(self, user_name, intra_extension_uuid): + return self.driver.get_subject_dict(intra_extension_uuid) + + @filter_args + @enforce(("read", "write"), "subjects") + def set_subject_dict(self, user_name, intra_extension_uuid, subject_dict): + for uuid in subject_dict: + # Next line will raise an error if user is not present in Keystone database + self.identity_api.get_user(uuid) + return self.driver.set_subject_dict(intra_extension_uuid, subject_dict) + + @filter_args + @enforce(("read", "write"), "subjects") + def add_subject_dict(self, user_name, intra_extension_uuid, subject_uuid): + # Next line will raise an error if user is not present in Keystone database + user = self.identity_api.get_user(subject_uuid) + return self.driver.add_subject(intra_extension_uuid, subject_uuid, user["name"]) + + @filter_args + @enforce("write", "subjects") + def del_subject(self, user_name, intra_extension_uuid, subject_uuid): + self.driver.remove_subject(intra_extension_uuid, subject_uuid) + + @filter_args + @enforce("read", "objects") + def get_object_dict(self, user_name, intra_extension_uuid): + return self.driver.get_object_dict(intra_extension_uuid) + + @filter_args + @enforce(("read", "write"), "objects") + def set_object_dict(self, user_name, intra_extension_uuid, object_dict): + return self.driver.set_object_dict(intra_extension_uuid, object_dict) + + @filter_args + @enforce(("read", "write"), "objects") + def add_object_dict(self, user_name, intra_extension_uuid, object_name): + object_uuid = uuid4().hex + return self.driver.add_object(intra_extension_uuid, object_uuid, object_name) + + @filter_args + @enforce("write", "objects") + def del_object(self, user_name, intra_extension_uuid, object_uuid): + self.driver.remove_object(intra_extension_uuid, object_uuid) + + @filter_args + @enforce("read", "actions") + def get_action_dict(self, user_name, intra_extension_uuid): + return self.driver.get_action_dict(intra_extension_uuid) + + @filter_args + @enforce(("read", "write"), "actions") + def set_action_dict(self, user_name, intra_extension_uuid, action_dict): + return self.driver.set_action_dict(intra_extension_uuid, action_dict) + + @filter_args + @enforce(("read", "write"), "actions") + def add_action_dict(self, user_name, intra_extension_uuid, action_name): + action_uuid = uuid4().hex + return self.driver.add_action(intra_extension_uuid, action_uuid, action_name) + + @filter_args + @enforce("write", "actions") + def del_action(self, user_name, intra_extension_uuid, action_uuid): + self.driver.remove_action(intra_extension_uuid, action_uuid) + + # Metadata functions + + @filter_args + @enforce("read", "subject_categories") + def get_subject_category_dict(self, user_name, intra_extension_uuid): + return self.driver.get_subject_category_dict(intra_extension_uuid) + + @filter_args + @enforce("read", "subject_categories") + @enforce("read", "subject_category_scope") + @enforce("write", "subject_category_scope") + def set_subject_category_dict(self, user_name, intra_extension_uuid, subject_category): + subject_category_dict = self.driver.set_subject_category_dict(intra_extension_uuid, subject_category) + # if we add a new category, we must add it to the subject_category_scope + for _cat in subject_category.keys(): + try: + _ = self.driver.get_subject_category_scope_dict(intra_extension_uuid, _cat) + except CategoryNotFound: + self.driver.set_subject_category_scope_dict(intra_extension_uuid, _cat, {}) + return subject_category_dict + + @filter_args + @enforce("read", "subject_categories") + @enforce("write", "subject_categories") + def add_subject_category_dict(self, user_name, intra_extension_uuid, subject_category_name): + subject_category_uuid = uuid4().hex + return self.driver.add_subject_category_dict(intra_extension_uuid, subject_category_uuid, subject_category_name) + + @filter_args + @enforce("write", "subject_categories") + def del_subject_category(self, user_name, intra_extension_uuid, subject_uuid): + return self.driver.remove_subject_category(intra_extension_uuid, subject_uuid) + + @filter_args + @enforce("read", "object_categories") + def get_object_category_dict(self, user_name, intra_extension_uuid): + return self.driver.get_object_category_dict(intra_extension_uuid) + + @filter_args + @enforce("read", "object_categories") + @enforce("read", "object_category_scope") + @enforce("write", "object_category_scope") + def set_object_category_dict(self, user_name, intra_extension_uuid, object_category): + object_category_dict = self.driver.set_object_category_dict(intra_extension_uuid, object_category) + # if we add a new category, we must add it to the object_category_scope + for _cat in object_category.keys(): + try: + _ = self.driver.get_object_category_scope_dict(intra_extension_uuid, _cat) + except CategoryNotFound: + self.driver.set_object_category_scope_dict(intra_extension_uuid, _cat, {}) + return object_category_dict + + @filter_args + @enforce("read", "object_categories") + @enforce("write", "object_categories") + def add_object_category_dict(self, user_name, intra_extension_uuid, object_category_name): + object_category_uuid = uuid4().hex + return self.driver.add_object_category_dict(intra_extension_uuid, object_category_uuid, object_category_name) + + @filter_args + @enforce("write", "object_categories") + def del_object_category(self, user_name, intra_extension_uuid, object_uuid): + return self.driver.remove_object_category(intra_extension_uuid, object_uuid) + + @filter_args + @enforce("read", "action_categories") + def get_action_category_dict(self, user_name, intra_extension_uuid): + return self.driver.get_action_category_dict(intra_extension_uuid) + + @filter_args + @enforce("read", "action_categories") + @enforce("read", "action_category_scope") + @enforce("write", "action_category_scope") + def set_action_category_dict(self, user_name, intra_extension_uuid, action_category): + action_category_dict = self.driver.set_action_category_dict(intra_extension_uuid, action_category) + # if we add a new category, we must add it to the action_category_scope + for _cat in action_category.keys(): + try: + _ = self.driver.get_action_category_scope_dict(intra_extension_uuid, _cat) + except CategoryNotFound: + self.driver.set_action_category_scope_dict(intra_extension_uuid, _cat, {}) + return action_category_dict + + @filter_args + @enforce("read", "action_categories") + @enforce("write", "action_categories") + def add_action_category_dict(self, user_name, intra_extension_uuid, action_category_name): + action_category_uuid = uuid4().hex + return self.driver.add_action_category_dict(intra_extension_uuid, action_category_uuid, action_category_name) + + @filter_args + @enforce("write", "action_categories") + def del_action_category(self, user_name, intra_extension_uuid, action_uuid): + return self.driver.remove_action_category(intra_extension_uuid, action_uuid) + + # Scope functions + @filter_args + @enforce("read", "subject_category_scope") + @enforce("read", "subject_category") + def get_subject_category_scope_dict(self, user_name, intra_extension_uuid, category): + if category not in self.get_subject_category_dict(user_name, intra_extension_uuid)["subject_categories"]: + raise IntraExtensionError("Subject category {} is unknown.".format(category)) + return self.driver.get_subject_category_scope_dict(intra_extension_uuid, category) + + @filter_args + @enforce("read", "subject_category_scope") + @enforce("read", "subject_category") + def set_subject_category_scope_dict(self, user_name, intra_extension_uuid, category, scope): + if category not in self.get_subject_category_dict(user_name, intra_extension_uuid)["subject_categories"]: + raise IntraExtensionError("Subject category {} is unknown.".format(category)) + return self.driver.set_subject_category_scope_dict(intra_extension_uuid, category, scope) + + @filter_args + @enforce(("read", "write"), "subject_category_scope") + @enforce("read", "subject_category") + def add_subject_category_scope_dict(self, user_name, intra_extension_uuid, subject_category, scope_name): + subject_categories = self.get_subject_category_dict(user_name, intra_extension_uuid) + # check if subject_category exists in database + if subject_category not in subject_categories["subject_categories"]: + raise IntraExtensionError("Subject category {} is unknown.".format(subject_category)) + scope_uuid = uuid4().hex + return self.driver.add_subject_category_scope_dict( + intra_extension_uuid, + subject_category, + scope_uuid, + scope_name) + + @filter_args + @enforce("write", "subject_category_scope") + @enforce("read", "subject_category") + def del_subject_category_scope(self, user_name, intra_extension_uuid, subject_category, subject_category_scope): + subject_categories = self.get_subject_category_dict(user_name, intra_extension_uuid) + # check if subject_category exists in database + if subject_category not in subject_categories["subject_categories"]: + raise IntraExtensionError("Subject category {} is unknown.".format(subject_category)) + return self.driver.remove_subject_category_scope_dict( + intra_extension_uuid, + subject_category, + subject_category_scope) + + @filter_args + @enforce("read", "object_category_scope") + @enforce("read", "object_category") + def get_object_category_scope_dict(self, user_name, intra_extension_uuid, category): + if category not in self.get_object_category_dict(user_name, intra_extension_uuid)["object_categories"]: + raise IntraExtensionError("Object category {} is unknown.".format(category)) + return self.driver.get_object_category_scope_dict(intra_extension_uuid, category) + + @filter_args + @enforce("read", "object_category_scope") + @enforce("read", "object_category") + def set_object_category_scope_dict(self, user_name, intra_extension_uuid, category, scope): + if category not in self.get_object_category_dict(user_name, intra_extension_uuid)["object_categories"]: + raise IntraExtensionError("Object category {} is unknown.".format(category)) + return self.driver.set_object_category_scope_dict(intra_extension_uuid, category, scope) + + @filter_args + @enforce(("read", "write"), "object_category_scope") + @enforce("read", "object_category") + def add_object_category_scope_dict(self, user_name, intra_extension_uuid, object_category, scope_name): + object_categories = self.get_object_category_dict(user_name, intra_extension_uuid) + # check if object_category exists in database + if object_category not in object_categories["object_categories"]: + raise IntraExtensionError("Object category {} is unknown.".format(object_category)) + scope_uuid = uuid4().hex + return self.driver.add_object_category_scope_dict( + intra_extension_uuid, + object_category, + scope_uuid, + scope_name) + + @filter_args + @enforce("write", "object_category_scope") + @enforce("read", "object_category") + def del_object_category_scope(self, user_name, intra_extension_uuid, object_category, object_category_scope): + object_categories = self.get_object_category_dict(user_name, intra_extension_uuid) + # check if object_category exists in database + if object_category not in object_categories["object_categories"]: + raise IntraExtensionError("Object category {} is unknown.".format(object_category)) + return self.driver.remove_object_category_scope_dict( + intra_extension_uuid, + object_category, + object_category_scope) + + @filter_args + @enforce("read", "action_category_scope") + @enforce("read", "action_category") + def get_action_category_scope_dict(self, user_name, intra_extension_uuid, category): + if category not in self.get_action_category_dict(user_name, intra_extension_uuid)["action_categories"]: + raise IntraExtensionError("Action category {} is unknown.".format(category)) + return self.driver.get_action_category_scope_dict(intra_extension_uuid, category) + + @filter_args + @enforce(("read", "write"), "action_category_scope") + @enforce("read", "action_category") + def set_action_category_scope_dict(self, user_name, intra_extension_uuid, category, scope): + if category not in self.get_action_category_dict(user_name, intra_extension_uuid)["action_categories"]: + raise IntraExtensionError("Action category {} is unknown.".format(category)) + return self.driver.set_action_category_scope_dict(intra_extension_uuid, category, scope) + + @filter_args + @enforce(("read", "write"), "action_category_scope") + @enforce("read", "action_category") + def add_action_category_scope_dict(self, user_name, intra_extension_uuid, action_category, scope_name): + action_categories = self.get_action_category_dict(user_name, intra_extension_uuid) + # check if action_category exists in database + if action_category not in action_categories["action_categories"]: + raise IntraExtensionError("Action category {} is unknown.".format(action_category)) + scope_uuid = uuid4().hex + return self.driver.add_action_category_scope_dict( + intra_extension_uuid, + action_category, + scope_uuid, + scope_name) + + @filter_args + @enforce("write", "action_category_scope") + @enforce("read", "action_category") + def del_action_category_scope(self, user_name, intra_extension_uuid, action_category, action_category_scope): + action_categories = self.get_action_category_dict(user_name, intra_extension_uuid) + # check if action_category exists in database + if action_category not in action_categories["action_categories"]: + raise IntraExtensionError("Action category {} is unknown.".format(action_category)) + return self.driver.remove_action_category_scope_dict( + intra_extension_uuid, + action_category, + action_category_scope) + + # Assignment functions + + @filter_args + @enforce("read", "subject_category_assignment") + @enforce("read", "subjects") + def get_subject_category_assignment_dict(self, user_name, intra_extension_uuid, subject_uuid): + # check if subject exists in database + if subject_uuid not in self.get_subject_dict(user_name, intra_extension_uuid)["subjects"]: + LOG.error("add_subject_assignment: unknown subject_id {}".format(subject_uuid)) + raise IntraExtensionError("Bad input data") + return self.driver.get_subject_category_assignment_dict(intra_extension_uuid, subject_uuid) + + @filter_args + @enforce("read", "subject_category_assignment") + @enforce("write", "subject_category_assignment") + @enforce("read", "subjects") + def set_subject_category_assignment_dict(self, user_name, intra_extension_uuid, subject_uuid, assignment_dict): + # check if subject exists in database + if subject_uuid not in self.get_subject_dict(user_name, intra_extension_uuid)["subjects"]: + LOG.error("add_subject_assignment: unknown subject_id {}".format(subject_uuid)) + raise IntraExtensionError("Bad input data") + return self.driver.set_subject_category_assignment_dict(intra_extension_uuid, subject_uuid, assignment_dict) + + @filter_args + @enforce("read", "subject_category_assignment") + @enforce("write", "subject_category_assignment") + @enforce("read", "subjects") + @enforce("read", "subject_category") + def del_subject_category_assignment(self, user_name, intra_extension_uuid, subject_uuid, category_uuid, scope_uuid): + # check if category exists in database + if category_uuid not in self.get_subject_category_dict(user_name, intra_extension_uuid)["subject_categories"]: + LOG.error("add_subject_category_scope: unknown subject_category {}".format(category_uuid)) + raise IntraExtensionError("Bad input data") + # check if subject exists in database + if subject_uuid not in self.get_subject_dict(user_name, intra_extension_uuid)["subjects"]: + LOG.error("add_subject_assignment: unknown subject_id {}".format(subject_uuid)) + raise IntraExtensionError("Bad input data") + self.driver.remove_subject_category_assignment(intra_extension_uuid, subject_uuid, category_uuid, scope_uuid) + + @filter_args + @enforce("write", "subject_category_assignment") + @enforce("read", "subjects") + @enforce("read", "subject_category") + def add_subject_category_assignment_dict(self, user_name, intra_extension_uuid, subject_uuid, category_uuid, scope_uuid): + # check if category exists in database + if category_uuid not in self.get_subject_category_dict(user_name, intra_extension_uuid)["subject_categories"]: + LOG.error("add_subject_category_scope: unknown subject_category {}".format(category_uuid)) + raise IntraExtensionError("Bad input data") + # check if subject exists in database + if subject_uuid not in self.get_subject_dict(user_name, intra_extension_uuid)["subjects"]: + LOG.error("add_subject_assignment: unknown subject_id {}".format(subject_uuid)) + raise IntraExtensionError("Bad input data") + return self.driver.add_subject_category_assignment_dict(intra_extension_uuid, subject_uuid, category_uuid, scope_uuid) + + @filter_args + @enforce("read", "object_category_assignment") + @enforce("read", "objects") + def get_object_category_assignment_dict(self, user_name, intra_extension_uuid, object_uuid): + # check if object exists in database + if object_uuid not in self.get_object_dict(user_name, intra_extension_uuid)["objects"]: + LOG.error("add_object_assignment: unknown object_id {}".format(object_uuid)) + raise IntraExtensionError("Bad input data") + return self.driver.get_object_category_assignment_dict(intra_extension_uuid, object_uuid) + + @filter_args + @enforce("read", "object_category_assignment") + @enforce("write", "object_category_assignment") + @enforce("read", "objects") + def set_object_category_assignment_dict(self, user_name, intra_extension_uuid, object_uuid, assignment_dict): + # check if object exists in database + if object_uuid not in self.get_object_dict(user_name, intra_extension_uuid)["objects"]: + LOG.error("add_object_assignment: unknown object_id {}".format(object_uuid)) + raise IntraExtensionError("Bad input data") + return self.driver.set_object_category_assignment_dict(intra_extension_uuid, object_uuid, assignment_dict) + + @filter_args + @enforce("read", "object_category_assignment") + @enforce("write", "object_category_assignment") + @enforce("read", "objects") + @enforce("read", "object_category") + def del_object_category_assignment(self, user_name, intra_extension_uuid, object_uuid, category_uuid, scope_uuid): + # check if category exists in database + if category_uuid not in self.get_object_category_dict(user_name, intra_extension_uuid)["object_categories"]: + LOG.error("add_object_category_scope: unknown object_category {}".format(category_uuid)) + raise IntraExtensionError("Bad input data") + # check if object exists in database + if object_uuid not in self.get_object_dict(user_name, intra_extension_uuid)["objects"]: + LOG.error("add_object_assignment: unknown object_id {}".format(object_uuid)) + raise IntraExtensionError("Bad input data") + self.driver.remove_object_category_assignment(intra_extension_uuid, object_uuid, category_uuid, scope_uuid) + + @filter_args + @enforce("write", "object_category_assignment") + @enforce("read", "objects") + @enforce("read", "object_category") + def add_object_category_assignment_dict(self, user_name, intra_extension_uuid, object_uuid, category_uuid, scope_uuid): + # check if category exists in database + if category_uuid not in self.get_object_category_dict(user_name, intra_extension_uuid)["object_categories"]: + LOG.error("add_object_category_scope: unknown object_category {}".format(category_uuid)) + raise IntraExtensionError("Bad input data") + # check if object exists in database + if object_uuid not in self.get_object_dict(user_name, intra_extension_uuid)["objects"]: + LOG.error("add_object_assignment: unknown object_id {}".format(object_uuid)) + raise IntraExtensionError("Bad input data") + return self.driver.add_object_category_assignment_dict(intra_extension_uuid, object_uuid, category_uuid, scope_uuid) + + @filter_args + @enforce("read", "action_category_assignment") + @enforce("read", "actions") + def get_action_category_assignment_dict(self, user_name, intra_extension_uuid, action_uuid): + # check if action exists in database + if action_uuid not in self.get_action_dict(user_name, intra_extension_uuid)["actions"]: + LOG.error("add_action_assignment: unknown action_id {}".format(action_uuid)) + raise IntraExtensionError("Bad input data") + return self.driver.get_action_category_assignment_dict(intra_extension_uuid, action_uuid) + + @filter_args + @enforce("read", "action_category_assignment") + @enforce("write", "action_category_assignment") + @enforce("read", "actions") + def set_action_category_assignment_dict(self, user_name, intra_extension_uuid, action_uuid, assignment_dict): + # check if action exists in database + if action_uuid not in self.get_action_dict(user_name, intra_extension_uuid)["actions"]: + LOG.error("add_action_assignment: unknown action_id {}".format(action_uuid)) + raise IntraExtensionError("Bad input data") + return self.driver.set_action_category_assignment_dict(intra_extension_uuid, action_uuid, assignment_dict) + + @filter_args + @enforce("read", "action_category_assignment") + @enforce("write", "action_category_assignment") + @enforce("read", "actions") + @enforce("read", "action_category") + def del_action_category_assignment(self, user_name, intra_extension_uuid, action_uuid, category_uuid, scope_uuid): + # check if category exists in database + if category_uuid not in self.get_action_category_dict(user_name, intra_extension_uuid)["action_categories"]: + LOG.error("add_action_category_scope: unknown action_category {}".format(category_uuid)) + raise IntraExtensionError("Bad input data") + # check if action exists in database + if action_uuid not in self.get_action_dict(user_name, intra_extension_uuid)["actions"]: + LOG.error("add_action_assignment: unknown action_id {}".format(action_uuid)) + raise IntraExtensionError("Bad input data") + self.driver.remove_action_category_assignment(intra_extension_uuid, action_uuid, category_uuid, scope_uuid) + + @filter_args + @enforce("write", "action_category_assignment") + @enforce("read", "actions") + @enforce("read", "action_category") + def add_action_category_assignment_dict(self, user_name, intra_extension_uuid, action_uuid, category_uuid, scope_uuid): + # check if category exists in database + if category_uuid not in self.get_action_category_dict(user_name, intra_extension_uuid)["action_categories"]: + LOG.error("add_action_category_scope: unknown action_category {}".format(category_uuid)) + raise IntraExtensionError("Bad input data") + # check if action exists in database + if action_uuid not in self.get_action_dict(user_name, intra_extension_uuid)["actions"]: + LOG.error("add_action_assignment: unknown action_id {}".format(action_uuid)) + raise IntraExtensionError("Bad input data") + return self.driver.add_action_category_assignment_dict( + intra_extension_uuid, + action_uuid, + category_uuid, + scope_uuid + ) + + # Metarule functions + @filter_args + def get_aggregation_algorithms(self, user_name, intra_extension_uuid): + # TODO: check which algorithms are really usable + return {"aggregation_algorithms": ["and_true_aggregation", "test_aggregation"]} + + @filter_args + @enforce("read", "aggregation_algorithms") + def get_aggregation_algorithm(self, user_name, intra_extension_uuid): + return self.driver.get_meta_rule_dict(intra_extension_uuid) + + @filter_args + @enforce("read", "aggregation_algorithms") + @enforce("write", "aggregation_algorithms") + def set_aggregation_algorithm(self, user_name, intra_extension_uuid, aggregation_algorithm): + if aggregation_algorithm not in self.get_aggregation_algorithms( + user_name, intra_extension_uuid)["aggregation_algorithms"]: + raise IntraExtensionError("Unknown aggregation_algorithm: {}".format(aggregation_algorithm)) + meta_rule = self.driver.get_meta_rule_dict(intra_extension_uuid) + meta_rule["aggregation"] = aggregation_algorithm + return self.driver.set_meta_rule_dict(intra_extension_uuid, meta_rule) + + @filter_args + @enforce("read", "sub_meta_rule") + def get_sub_meta_rule(self, user_name, intra_extension_uuid): + return self.driver.get_meta_rule_dict(intra_extension_uuid) + + @filter_args + @enforce("read", "sub_meta_rule") + @enforce("write", "sub_meta_rule") + def set_sub_meta_rule(self, user_name, intra_extension_uuid, sub_meta_rules): + # TODO (dthom): When sub_meta_rule is set, all rules must be dropped + # because the previous rules cannot be mapped to the new sub_meta_rule. + for relation in sub_meta_rules.keys(): + if relation not in self.get_sub_meta_rule_relations(user_name, intra_extension_uuid)["sub_meta_rule_relations"]: + LOG.error("set_sub_meta_rule unknown MetaRule relation {}".format(relation)) + raise IntraExtensionError("Bad input data.") + for cat in ("subject_categories", "object_categories", "action_categories"): + if cat not in sub_meta_rules[relation]: + LOG.error("set_sub_meta_rule category {} missed".format(cat)) + raise IntraExtensionError("Bad input data.") + if type(sub_meta_rules[relation][cat]) is not list: + LOG.error("set_sub_meta_rule category {} is not a list".format(cat)) + raise IntraExtensionError("Bad input data.") + subject_categories = self.get_subject_category_dict(user_name, intra_extension_uuid) + for data in sub_meta_rules[relation]["subject_categories"]: + if data not in subject_categories["subject_categories"]: + LOG.error("set_sub_meta_rule category {} is not part of subject_categories {}".format( + data, subject_categories)) + raise IntraExtensionError("Bad input data.") + object_categories = self.get_object_category_dict(user_name, intra_extension_uuid) + for data in sub_meta_rules[relation]["object_categories"]: + if data not in object_categories["object_categories"]: + LOG.error("set_sub_meta_rule category {} is not part of object_categories {}".format( + data, object_categories)) + raise IntraExtensionError("Bad input data.") + action_categories = self.get_action_category_dict(user_name, intra_extension_uuid) + for data in sub_meta_rules[relation]["action_categories"]: + if data not in action_categories["action_categories"]: + LOG.error("set_sub_meta_rule category {} is not part of action_categories {}".format( + data, action_categories)) + raise IntraExtensionError("Bad input data.") + aggregation = self.driver.get_meta_rule_dict(intra_extension_uuid)["aggregation"] + return self.driver.set_meta_rule_dict( + intra_extension_uuid, + { + "aggregation": aggregation, + "sub_meta_rules": sub_meta_rules + }) + + # Sub-rules functions + @filter_args + @enforce("read", "sub_rules") + def get_sub_rules(self, user_name, intra_extension_uuid): + return self.driver.get_rules(intra_extension_uuid) + + @filter_args + @enforce("read", "sub_rules") + @enforce("write", "sub_rules") + def set_sub_rule(self, user_name, intra_extension_uuid, relation, sub_rule): + for item in sub_rule: + if type(item) not in (str, unicode, bool): + raise IntraExtensionError("Bad input data (sub_rule).") + ref_rules = self.driver.get_rules(intra_extension_uuid) + _sub_rule = list(sub_rule) + if relation not in self.get_sub_meta_rule_relations(user_name, intra_extension_uuid)["sub_meta_rule_relations"]: + raise IntraExtensionError("Bad input data (rules).") + # filter strings in sub_rule + sub_rule = [filter_input(x) for x in sub_rule] + # check if length of sub_rule is correct from metadata_sub_rule + metadata_sub_rule = self.get_sub_meta_rule(user_name, intra_extension_uuid) + metadata_sub_rule_length = len(metadata_sub_rule['sub_meta_rules'][relation]["subject_categories"]) + \ + len(metadata_sub_rule['sub_meta_rules'][relation]["action_categories"]) + \ + len(metadata_sub_rule['sub_meta_rules'][relation]["object_categories"]) + 1 + if metadata_sub_rule_length != len(sub_rule): + raise IntraExtensionError("Bad number of argument in sub_rule {}/{}".format(sub_rule, + metadata_sub_rule_length)) + # check if each item in sub_rule match a corresponding scope value + for category in metadata_sub_rule['sub_meta_rules'][relation]["subject_categories"]: + item = _sub_rule.pop(0) + if item not in self.get_subject_category_scope_dict( + user_name, + intra_extension_uuid, category)["subject_category_scope"][category].keys(): + raise IntraExtensionError("Bad subject value in sub_rule {}/{}".format(category, item)) + for category in metadata_sub_rule['sub_meta_rules'][relation]["action_categories"]: + action_categories = self.get_action_category_scope_dict( + user_name, + intra_extension_uuid, category)["action_category_scope"][category] + item = _sub_rule.pop(0) + if item not in action_categories.keys(): + self.moonlog_api.warning("set_sub_rule bad action value in sub_rule {}/{}".format(category, item)) + raise IntraExtensionError("Bad input data.") + for category in metadata_sub_rule['sub_meta_rules'][relation]["object_categories"]: + item = _sub_rule.pop(0) + if item not in self.get_object_category_scope_dict( + user_name, + intra_extension_uuid, category)["object_category_scope"][category].keys(): + raise IntraExtensionError("Bad object value in sub_rule {}/{}".format(category, item)) + # check if relation is already there + if relation not in ref_rules["rules"]: + ref_rules["rules"][relation] = list() + # add sub_rule + ref_rules["rules"][relation].append(sub_rule) + return self.driver.set_rules(intra_extension_uuid, ref_rules["rules"]) + + @filter_args + @enforce("read", "sub_rules") + @enforce("write", "sub_rules") + def del_sub_rule(self, user_name, intra_extension_uuid, relation_name, rule): + ref_rules = self.driver.get_rules(intra_extension_uuid) + rule = rule.split("+") + for index, _item in enumerate(rule): + if "True" in _item: + rule[index] = True + if "False" in _item: + rule[index] = False + if relation_name in ref_rules["rules"]: + if rule in ref_rules["rules"][relation_name]: + ref_rules["rules"][relation_name].remove(rule) + else: + self.moonlog_api.error("Unknown rule: {}".format(rule)) + else: + self.moonlog_api.error("Unknown relation name for rules: {}".format(relation_name)) + return self.driver.set_rules(intra_extension_uuid, ref_rules["rules"]) + + +@dependency.provider('authz_api') +@dependency.requires('identity_api', 'moonlog_api', 'tenant_api') +class IntraExtensionAuthzManager(IntraExtensionManager): + + __genre__ = "authz" + + def authz(self, uuid, sub, obj, act): + """Check authorization for a particular action. + + :param uuid: UUID of a tenant + :param sub: subject of the request + :param obj: object of the request + :param act: action of the request + :return: True or False or raise an exception + """ + _uuid = self.tenant_api.get_extension_uuid(uuid, "authz") + return super(IntraExtensionAuthzManager, self).authz(_uuid, sub, obj, act) + + def delete_intra_extension(self, intra_extension_id): + raise AuthIntraExtensionModificationNotAuthorized() + + def set_subject_dict(self, user_name, intra_extension_uuid, subject_dict): + raise AuthIntraExtensionModificationNotAuthorized() + + def add_subject_dict(self, user_name, intra_extension_uuid, subject_uuid): + raise AuthIntraExtensionModificationNotAuthorized() + + def del_subject(self, user_name, intra_extension_uuid, subject_uuid): + raise AuthIntraExtensionModificationNotAuthorized() + + def set_object_dict(self, user_name, intra_extension_uuid, object_dict): + raise AuthIntraExtensionModificationNotAuthorized() + + def add_object_dict(self, user_name, intra_extension_uuid, object_name): + raise AuthIntraExtensionModificationNotAuthorized() + + def del_object(self, user_name, intra_extension_uuid, object_uuid): + raise AuthIntraExtensionModificationNotAuthorized() + + def set_action_dict(self, user_name, intra_extension_uuid, action_dict): + raise AuthIntraExtensionModificationNotAuthorized() + + def add_action_dict(self, user_name, intra_extension_uuid, action_name): + raise AuthIntraExtensionModificationNotAuthorized() + + def del_action(self, user_name, intra_extension_uuid, action_uuid): + raise AuthIntraExtensionModificationNotAuthorized() + + def set_subject_category_dict(self, user_name, intra_extension_uuid, subject_category): + raise AuthIntraExtensionModificationNotAuthorized() + + def add_subject_category_dict(self, user_name, intra_extension_uuid, subject_category_name): + raise AuthIntraExtensionModificationNotAuthorized() + + def del_subject_category(self, user_name, intra_extension_uuid, subject_uuid): + raise AuthIntraExtensionModificationNotAuthorized() + + def set_object_category_dict(self, user_name, intra_extension_uuid, object_category): + raise AuthIntraExtensionModificationNotAuthorized() + + def add_object_category_dict(self, user_name, intra_extension_uuid, object_category_name): + raise AuthIntraExtensionModificationNotAuthorized() + + def del_object_category(self, user_name, intra_extension_uuid, object_uuid): + raise AuthIntraExtensionModificationNotAuthorized() + + def set_action_category_dict(self, user_name, intra_extension_uuid, action_category): + raise AuthIntraExtensionModificationNotAuthorized() + + def add_action_category_dict(self, user_name, intra_extension_uuid, action_category_name): + raise AuthIntraExtensionModificationNotAuthorized() + + def del_action_category(self, user_name, intra_extension_uuid, action_uuid): + raise AuthIntraExtensionModificationNotAuthorized() + + def set_subject_category_scope_dict(self, user_name, intra_extension_uuid, category, scope): + raise AuthIntraExtensionModificationNotAuthorized() + + def add_subject_category_scope_dict(self, user_name, intra_extension_uuid, subject_category, scope_name): + raise AuthIntraExtensionModificationNotAuthorized() + + def del_subject_category_scope(self, user_name, intra_extension_uuid, subject_category, subject_category_scope): + raise AuthIntraExtensionModificationNotAuthorized() + + def set_object_category_scope_dict(self, user_name, intra_extension_uuid, category, scope): + raise AuthIntraExtensionModificationNotAuthorized() + + def add_object_category_scope_dict(self, user_name, intra_extension_uuid, object_category, scope_name): + raise AuthIntraExtensionModificationNotAuthorized() + + def del_object_category_scope(self, user_name, intra_extension_uuid, object_category, object_category_scope): + raise AuthIntraExtensionModificationNotAuthorized() + + def set_action_category_scope_dict(self, user_name, intra_extension_uuid, category, scope): + raise AuthIntraExtensionModificationNotAuthorized() + + def add_action_category_scope_dict(self, user_name, intra_extension_uuid, action_category, scope_name): + raise AuthIntraExtensionModificationNotAuthorized() + + def del_action_category_scope(self, user_name, intra_extension_uuid, action_category, action_category_scope): + raise AuthIntraExtensionModificationNotAuthorized() + + def set_subject_category_assignment_dict(self, user_name, intra_extension_uuid, subject_uuid, assignment_dict): + raise AuthIntraExtensionModificationNotAuthorized() + + def del_subject_category_assignment(self, user_name, intra_extension_uuid, subject_uuid, category_uuid, scope_uuid): + raise AuthIntraExtensionModificationNotAuthorized() + + def add_subject_category_assignment_dict(self, user_name, intra_extension_uuid, subject_uuid, category_uuid, scope_uuid): + raise AuthIntraExtensionModificationNotAuthorized() + + def set_object_category_assignment_dict(self, user_name, intra_extension_uuid, object_uuid, assignment_dict): + raise AuthIntraExtensionModificationNotAuthorized() + + def del_object_category_assignment(self, user_name, intra_extension_uuid, object_uuid, category_uuid, scope_uuid): + raise AuthIntraExtensionModificationNotAuthorized() + + def add_object_category_assignment_dict(self, user_name, intra_extension_uuid, object_uuid, category_uuid, scope_uuid): + raise AuthIntraExtensionModificationNotAuthorized() + + def set_action_category_assignment_dict(self, user_name, intra_extension_uuid, action_uuid, assignment_dict): + raise AuthIntraExtensionModificationNotAuthorized() + + def del_action_category_assignment(self, user_name, intra_extension_uuid, action_uuid, category_uuid, scope_uuid): + raise AuthIntraExtensionModificationNotAuthorized() + + def add_action_category_assignment_dict(self, user_name, intra_extension_uuid, action_uuid, category_uuid, scope_uuid): + raise AuthIntraExtensionModificationNotAuthorized() + + def set_aggregation_algorithm(self, user_name, intra_extension_uuid, aggregation_algorithm): + raise AuthIntraExtensionModificationNotAuthorized() + + def set_sub_meta_rule(self, user_name, intra_extension_uuid, sub_meta_rules): + raise AuthIntraExtensionModificationNotAuthorized() + + def set_sub_rule(self, user_name, intra_extension_uuid, relation, sub_rule): + raise AuthIntraExtensionModificationNotAuthorized() + + def del_sub_rule(self, user_name, intra_extension_uuid, relation_name, rule): + raise AuthIntraExtensionModificationNotAuthorized() + +@dependency.provider('admin_api') +@dependency.requires('identity_api', 'moonlog_api', 'tenant_api') +class IntraExtensionAdminManager(IntraExtensionManager): + + __genre__ = "admin" + + # def set_perimeter_values(self, ie, policy_dir): + # + # # Check if object like "subjects", "objects", "actions" exist... + # perimeter_path = os.path.join(policy_dir, 'perimeter.json') + # f = open(perimeter_path) + # json_perimeter = json.load(f) + # for item in ("subjects", "objects", "actions"): + # if item not in json_perimeter["objects"]: + # raise AdminIntraExtensionCreationError() + # + # super(IntraExtensionAdminManager, self).set_perimeter_values(ie, policy_dir) + # + # @filter_args + # def add_subject_dict(self, user_name, uuid, subject_uuid): + # raise AdminIntraExtensionModificationNotAuthorized() + # + # @filter_args + # def del_subject(self, user_name, uuid, subject_uuid): + # raise AdminIntraExtensionModificationNotAuthorized() + + +class AuthzDriver(object): + + def get_subject_category_list(self, extension_uuid): + raise exception.NotImplemented() # pragma: no cover + + def get_object_category_list(self, extension_uuid): + raise exception.NotImplemented() # pragma: no cover + + def get_action_category_list(self, extension_uuid): + raise exception.NotImplemented() # pragma: no cover + + def get_subject_category_value_dict(self, extension_uuid, subject_uuid): + raise exception.NotImplemented() # pragma: no cover + + def get_object_category_value_dict(self, extension_uuid, object_uuid): + raise exception.NotImplemented() # pragma: no cover + + def get_action_category_value_dict(self, extension_uuid, action_uuid): + raise exception.NotImplemented() # pragma: no cover + + def get_meta_rule(self, extension_uuid): + raise exception.NotImplemented() # pragma: no cover + + def get_rules(self, extension_uuid): + raise exception.NotImplemented() # pragma: no cover + + +class UpdateDriver(object): + + def get_intra_extensions(self): + raise exception.NotImplemented() # pragma: no cover + + def get_intra_extension(self, extension_uuid): + raise exception.NotImplemented() # pragma: no cover + + def create_intra_extensions(self, extension_uuid, intra_extension): + raise exception.NotImplemented() # pragma: no cover + + def delete_intra_extensions(self, extension_uuid): + raise exception.NotImplemented() # pragma: no cover + + # Getter and setter for tenant + + def get_tenant(self, uuid): + raise exception.NotImplemented() # pragma: no cover + + def set_tenant(self, uuid, tenant_id): + raise exception.NotImplemented() # pragma: no cover + + # Getter and setter for name + + def get_name(self, uuid): + raise exception.NotImplemented() # pragma: no cover + + def set_name(self, uuid, name): + raise exception.NotImplemented() # pragma: no cover + + # Getter and setter for model + + def get_model(self, uuid): + raise exception.NotImplemented() # pragma: no cover + + def set_model(self, uuid, model): + raise exception.NotImplemented() # pragma: no cover + + # Getter and setter for genre + + def get_genre(self, uuid): + raise exception.NotImplemented() # pragma: no cover + + def set_genre(self, uuid, genre): + raise exception.NotImplemented() # pragma: no cover + + # Getter and setter for description + + def get_description(self, uuid): + raise exception.NotImplemented() # pragma: no cover + + def set_description(self, uuid, args): + raise exception.NotImplemented() # pragma: no cover + + +class IntraExtensionDriver(object): + + # Getter ad Setter for subjects + + def get_subject_dict(self, extension_uuid): + """Get the list of subject for that IntraExtension + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :return: a dictionary containing all subjects for that IntraExtension, eg. {"uuid1": "name1", "uuid2": "name2"} + """ + raise exception.NotImplemented() # pragma: no cover + + def set_subject_dict(self, extension_uuid, subject_dict): + """Set the list of subject for that IntraExtension + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :param subject_dict: dict of subject: {"uuid1": "name1", "uuid2": "name2"} + :type subject_dict: dict + :return: a dictionary containing all subjects for that IntraExtension, eg. {"uuid1": "name1", "uuid2": "name2"} + """ + raise exception.NotImplemented() # pragma: no cover + + def add_subject(self, extension_uuid, subject_uuid, subject_name): + """Add a subject + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :param subject_uuid: Subject UUID + :type subject_uuid: string + :param subject_name: Subject name + :type subject_name: string + :return: the added subject {"uuid1": "name1"} + """ + raise exception.NotImplemented() # pragma: no cover + + def remove_subject(self, extension_uuid, subject_uuid): + """Remove a subject + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :param subject_uuid: Subject UUID + :type subject_uuid: string + :return: None + """ + raise exception.NotImplemented() # pragma: no cover + + # Getter ad Setter for objects + + def get_object_dict(self, extension_uuid): + """Get the list of object for that IntraExtension + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :return: a dictionary containing all objects for that IntraExtension, eg. {"uuid1": "name1", "uuid2": "name2"} + """ + raise exception.NotImplemented() # pragma: no cover + + def set_object_dict(self, extension_uuid, object_dict): + """Set the list of object for that IntraExtension + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :param object_dict: dict of object: {"uuid1": "name1", "uuid2": "name2"} + :type object_dict: dict + :return: a dictionary containing all objects for that IntraExtension, eg. {"uuid1": "name1", "uuid2": "name2"} + """ + raise exception.NotImplemented() # pragma: no cover + + def add_object(self, extension_uuid, object_uuid, object_name): + """Ad an object + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :param object_uuid: Object UUID + :type object_uuid: string + :param object_name: Object name + :type object_name: string + :return: the added object {"uuid1": "name1"} + """ + raise exception.NotImplemented() # pragma: no cover + + def remove_object(self, extension_uuid, object_uuid): + """Remove an object + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :param object_uuid: Object UUID + :type object_uuid: string + :return: None + """ + raise exception.NotImplemented() # pragma: no cover + + # Getter ad Setter for actions + + def get_action_dict(self, extension_uuid): + """ Get the list of action for that IntraExtension + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :return: a dictionary containing all actions for that IntraExtension, eg. {"uuid1": "name1", "uuid2": "name2"} + """ + raise exception.NotImplemented() # pragma: no cover + + def set_action_dict(self, extension_uuid, action_dict): + """ Set the list of action for that IntraExtension + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :param action_dict: dict of actions: {"uuid1": "name1", "uuid2": "name2"} + :type action_dict: dict + :return: a dictionary containing all actions for that IntraExtension, eg. {"uuid1": "name1", "uuid2": "name2"} + """ + raise exception.NotImplemented() # pragma: no cover + + def add_action(self, extension_uuid, action_uuid, action_name): + """Ad an action + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :param action_uuid: Action UUID + :type action_uuid: string + :param action_name: Action name + :type action_name: string + :return: the added action {"uuid1": "name1"} + """ + raise exception.NotImplemented() # pragma: no cover + + def remove_action(self, extension_uuid, action_uuid): + """Remove an action + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :param action_uuid: Action UUID + :type action_uuid: string + :return: None + """ + raise exception.NotImplemented() # pragma: no cover + + # Getter ad Setter for subject_category + + def get_subject_category_dict(self, extension_uuid): + """Get a list of all subject categories + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :return: a dictionary containing all subject categories {"uuid1": "name1", "uuid2": "name2"} + """ + raise exception.NotImplemented() # pragma: no cover + + def set_subject_category_dict(self, extension_uuid, subject_categories): + """Set the list of all subject categories + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :param subject_categories: dict of subject categories {"uuid1": "name1", "uuid2": "name2"} + :type subject_categories: dict + :return: a dictionary containing all subject categories {"uuid1": "name1", "uuid2": "name2"} + """ + raise exception.NotImplemented() # pragma: no cover + + def add_subject_category_dict(self, extension_uuid, subject_category_uuid, subject_category_name): + """Add a subject category + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :param subject_category_uuid: the UUID of the subject category + :type subject_category_uuid: string + :param subject_category_name: the name of the subject category + :type subject_category_name: string + :return: a dictionnary with the subject catgory added {"uuid1": "name1"} + """ + raise exception.NotImplemented() # pragma: no cover + + def remove_subject_category(self, extension_uuid, subject_category_uuid): + """Remove one subject category + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :param subject_category_uuid: the UUID of subject category to remove + :type subject_category_uuid: string + :return: a dictionary containing all subject categories {"uuid1": "name1", "uuid2": "name2"} + """ + raise exception.NotImplemented() # pragma: no cover + + # Getter ad Setter for object_category + + def get_object_category_dict(self, extension_uuid): + """Get a list of all object categories + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :return: a dictionary containing all object categories {"uuid1": "name1", "uuid2": "name2"} + """ + raise exception.NotImplemented() # pragma: no cover + + def set_object_category_dict(self, extension_uuid, object_categories): + """Set the list of all object categories + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :param object_categories: dict of object categories {"uuid1": "name1", "uuid2": "name2"} + :type object_categories: dict + :return: a dictionary containing all object categories {"uuid1": "name1", "uuid2": "name2"} + """ + raise exception.NotImplemented() # pragma: no cover + + def add_object_category_dict(self, extension_uuid, object_category_uuid, object_category_name): + """Add a object category + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :param object_category_uuid: the UUID of the object category + :type object_category_uuid: string + :param object_category_name: the name of the object category + :type object_category_name: string + :return: a dictionnary with the object catgory added {"uuid1": "name1"} + """ + raise exception.NotImplemented() # pragma: no cover + + def remove_object_category(self, extension_uuid, object_category_uuid): + """Remove one object category + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :param object_category_uuid: the UUID of object category to remove + :type object_category_uuid: string + :return: a dictionary containing all object categories {"uuid1": "name1", "uuid2": "name2"} + """ + raise exception.NotImplemented() # pragma: no cover + + # Getter ad Setter for action_category + + def get_action_category_dict(self, extension_uuid): + """Get a list of all action categories + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :return: a dictionary containing all action categories {"uuid1": "name1", "uuid2": "name2"} + """ + raise exception.NotImplemented() # pragma: no cover + + def set_action_category_dict(self, extension_uuid, action_categories): + """Set the list of all action categories + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :param action_categories: dict of action categories {"uuid1": "name1", "uuid2": "name2"} + :type action_categories: dict + :return: a dictionary containing all action categories {"uuid1": "name1", "uuid2": "name2"} + """ + raise exception.NotImplemented() # pragma: no cover + + def add_action_category_dict(self, extension_uuid, action_category_uuid, action_category_name): + """Add a action category + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :param action_category_uuid: the UUID of the action category + :type action_category_uuid: string + :param action_category_name: the name of the action category + :type action_category_name: string + :return: a dictionnary with the action catgory added {"uuid1": "name1"} + """ + raise exception.NotImplemented() # pragma: no cover + + def remove_action_category(self, extension_uuid, action_category_uuid): + """Remove one action category + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :param action_category_uuid: the UUID of action category to remove + :type action_category_uuid: string + :return: a dictionary containing all action categories {"uuid1": "name1", "uuid2": "name2"} + """ + raise exception.NotImplemented() # pragma: no cover + + # Getter and Setter for subject_category_value_scope + + def get_subject_category_scope_dict(self, extension_uuid, category): + """Get a list of all subject category scope + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :param category: the category UUID where the scope values are + :type category: string + :return: a dictionary containing all subject category scope {"category1": {"scope_uuid1": "scope_name1}} + """ + raise exception.NotImplemented() # pragma: no cover + + def set_subject_category_scope_dict(self, extension_uuid, subject_category, scope): + """Set the list of all scope for that subject category + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :param subject_category: the UUID of the subject category where this scope will be set + :type subject_category: string + :return: a dictionary containing all scope {"scope_uuid1": "scope_name1, "scope_uuid2": "scope_name2} + """ + raise exception.NotImplemented() # pragma: no cover + + def add_subject_category_scope_dict(self, extension_uuid, subject_category, scope_uuid, scope_name): + """Add a subject category + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :param subject_category: the subject category UUID where the scope will be added + :type subject_category: string + :param scope_uuid: the UUID of the subject category + :type scope_uuid: string + :param scope_name: the name of the subject category + :type scope_name: string + :return: a dictionary containing the subject category scope added {"category1": {"scope_uuid1": "scope_name1}} + """ + raise exception.NotImplemented() # pragma: no cover + + def remove_subject_category_scope_dict(self, extension_uuid, subject_category, scope_uuid): + """Remove one scope belonging to a subject category + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :param subject_category: the UUID of subject categorywhere we can find the scope to remove + :type subject_category: string + :param scope_uuid: the UUID of the scope to remove + :type scope_uuid: string + :return: None + """ + raise exception.NotImplemented() # pragma: no cover + + # Getter and Setter for object_category_scope + + def get_object_category_scope_dict(self, extension_uuid, category): + """Get a list of all object category scope + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :param category: the category UUID where the scope values are + :type category: string + :return: a dictionary containing all object category scope {"category1": {"scope_uuid1": "scope_name1}} + """ + raise exception.NotImplemented() # pragma: no cover + + def set_object_category_scope_dict(self, extension_uuid, object_category, scope): + """Set the list of all scope for that object category + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :param object_category: the UUID of the object category where this scope will be set + :type object_category: string + :return: a dictionary containing all scope {"scope_uuid1": "scope_name1, "scope_uuid2": "scope_name2} + """ + raise exception.NotImplemented() # pragma: no cover + + def add_object_category_scope_dict(self, extension_uuid, object_category, scope_uuid, scope_name): + """Add a object category + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :param object_category: the object category UUID where the scope will be added + :type object_category: string + :param scope_uuid: the UUID of the object category + :type scope_uuid: string + :param scope_name: the name of the object category + :type scope_name: string + :return: a dictionary containing the object category scope added {"category1": {"scope_uuid1": "scope_name1}} + """ + raise exception.NotImplemented() # pragma: no cover + + def remove_object_category_scope_dict(self, extension_uuid, object_category, scope_uuid): + """Remove one scope belonging to a object category + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :param object_category: the UUID of object categorywhere we can find the scope to remove + :type object_category: string + :param scope_uuid: the UUID of the scope to remove + :type scope_uuid: string + :return: None + """ + raise exception.NotImplemented() # pragma: no cover + + # Getter and Setter for action_category_scope + + def get_action_category_scope_dict(self, extension_uuid, category): + """Get a list of all action category scope + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :param category: the category UUID where the scope values are + :type category: string + :return: a dictionary containing all action category scope {"category1": {"scope_uuid1": "scope_name1}} + """ + raise exception.NotImplemented() # pragma: no cover + + def set_action_category_scope_dict(self, extension_uuid, action_category, scope): + """Set the list of all scope for that action category + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :param action_category: the UUID of the action category where this scope will be set + :type action_category: string + :return: a dictionary containing all scope {"scope_uuid1": "scope_name1, "scope_uuid2": "scope_name2} + """ + raise exception.NotImplemented() # pragma: no cover + + def add_action_category_scope_dict(self, extension_uuid, action_category, scope_uuid, scope_name): + """Add a action category + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :param action_category: the action category UUID where the scope will be added + :type action_category: string + :param scope_uuid: the UUID of the action category + :type scope_uuid: string + :param scope_name: the name of the action category + :type scope_name: string + :return: a dictionary containing the action category scope added {"category1": {"scope_uuid1": "scope_name1}} + """ + raise exception.NotImplemented() # pragma: no cover + + def remove_action_category_scope_dict(self, extension_uuid, action_category, scope_uuid): + """Remove one scope belonging to a action category + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :param action_category: the UUID of action categorywhere we can find the scope to remove + :type action_category: string + :param scope_uuid: the UUID of the scope to remove + :type scope_uuid: string + :return: None + """ + raise exception.NotImplemented() # pragma: no cover + + # Getter and Setter for subject_category_assignment + + def get_subject_category_assignment_dict(self, extension_uuid, subject_uuid): + """Get the assignment for a given subject_uuid + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :param subject_uuid: subject UUID + :type subject_uuid: string + :return: a dictionary of assignment for the given subject {"cat1": ["scope_uuid1", "scope_uuid2"]} + """ + raise exception.NotImplemented() # pragma: no cover + + def set_subject_category_assignment_dict(self, extension_uuid, subject_uuid, assignment_dict): + """Set the assignment for a given subject_uuid + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :param subject_uuid: subject UUID + :type subject_uuid: string + :param assignment_dict: the assignment dictionary {"cat1": ["scope_uuid1", "scope_uuid2"]} + :type assignment_dict: dict + :return: a dictionary of assignment for the given subject {"cat1": ["scope_uuid1", "scope_uuid2"]} + """ + raise exception.NotImplemented() # pragma: no cover + + def add_subject_category_assignment_dict(self, extension_uuid, subject_uuid, category_uuid, scope_uuid): + """Add a scope to a category and to a subject + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :param subject_uuid: the subject UUID + :type subject_uuid: string + :param category_uuid: the category UUID + :type category_uuid: string + :param scope_uuid: the scope UUID + :type scope_uuid: string + :return: a dictionary of assignment for the given subject {"cat1": ["scope_uuid1", "scope_uuid2"]} + """ + raise exception.NotImplemented() # pragma: no cover + + def remove_subject_category_assignment(self, extension_uuid, subject_uuid, category_uuid, scope_uuid): + """Remove a scope from a category and from a subject + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :param subject_uuid: the subject UUID + :type subject_uuid: string + :param category_uuid: the category UUID + :type category_uuid: string + :param scope_uuid: the scope UUID + :type scope_uuid: string + :return: None + """ + raise exception.NotImplemented() # pragma: no cover + + # Getter and Setter for object_category_assignment + + def get_object_category_assignment_dict(self, extension_uuid, object_uuid): + """Get the assignment for a given object_uuid + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :param object_uuid: object UUID + :type object_uuid: string + :return: a dictionary of assignment for the given object {"cat1": ["scope_uuid1", "scope_uuid2"]} + """ + raise exception.NotImplemented() # pragma: no cover + + def set_object_category_assignment_dict(self, extension_uuid, object_uuid, assignment_dict): + """Set the assignment for a given object_uuid + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :param object_uuid: object UUID + :type object_uuid: string + :param assignment_dict: the assignment dictionary {"cat1": ["scope_uuid1", "scope_uuid2"]} + :type assignment_dict: dict + :return: a dictionary of assignment for the given object {"cat1": ["scope_uuid1", "scope_uuid2"]} + """ + raise exception.NotImplemented() # pragma: no cover + + def add_object_category_assignment_dict(self, extension_uuid, object_uuid, category_uuid, scope_uuid): + """Add a scope to a category and to a object + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :param object_uuid: the object UUID + :type object_uuid: string + :param category_uuid: the category UUID + :type category_uuid: string + :param scope_uuid: the scope UUID + :type scope_uuid: string + :return: a dictionary of assignment for the given object {"cat1": ["scope_uuid1", "scope_uuid2"]} + """ + raise exception.NotImplemented() # pragma: no cover + + def remove_object_category_assignment(self, extension_uuid, object_uuid, category_uuid, scope_uuid): + """Remove a scope from a category and from a object + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :param object_uuid: the object UUID + :type object_uuid: string + :param category_uuid: the category UUID + :type category_uuid: string + :param scope_uuid: the scope UUID + :type scope_uuid: string + :return: None + """ + raise exception.NotImplemented() # pragma: no cover + + # Getter and Setter for action_category_assignment + + def get_action_category_assignment_dict(self, extension_uuid, action_uuid): + """Get the assignment for a given action_uuid + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :param action_uuid: action UUID + :type action_uuid: string + :return: a dictionary of assignment for the given action {"cat1": ["scope_uuid1", "scope_uuid2"]} + """ + raise exception.NotImplemented() # pragma: no cover + + def set_action_category_assignment_dict(self, extension_uuid, action_uuid, assignment_dict): + """Set the assignment for a given action_uuid + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :param action_uuid: action UUID + :type action_uuid: string + :param assignment_dict: the assignment dictionary {"cat1": ["scope_uuid1", "scope_uuid2"]} + :type assignment_dict: dict + :return: a dictionary of assignment for the given action {"cat1": ["scope_uuid1", "scope_uuid2"]} + """ + raise exception.NotImplemented() # pragma: no cover + + def add_action_category_assignment_dict(self, extension_uuid, action_uuid, category_uuid, scope_uuid): + """Add a scope to a category and to a action + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :param action_uuid: the action UUID + :type action_uuid: string + :param category_uuid: the category UUID + :type category_uuid: string + :param scope_uuid: the scope UUID + :type scope_uuid: string + :return: a dictionary of assignment for the given action {"cat1": ["scope_uuid1", "scope_uuid2"]} + """ + raise exception.NotImplemented() # pragma: no cover + + def remove_action_category_assignment(self, extension_uuid, action_uuid, category_uuid, scope_uuid): + """Remove a scope from a category and from a action + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :param action_uuid: the action UUID + :type action_uuid: string + :param category_uuid: the category UUID + :type category_uuid: string + :param scope_uuid: the scope UUID + :type scope_uuid: string + :return: None + """ + raise exception.NotImplemented() # pragma: no cover + + # Getter and Setter for meta_rule + + def get_meta_rule_dict(self, extension_uuid): + """Get the Meta rule + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :return: a dictionary containing the meta_rule + + Here is an example of a meta_rule: + { + "sub_meta_rules": { + "relation_super": { + "subject_categories": ["role"], + "action_categories": ["computing_action"], + "object_categories": ["id"], + "relation": "relation_super" + } + }, + "aggregation": "and_true_aggregation" + } + """ + raise exception.NotImplemented() # pragma: no cover + + def set_meta_rule_dict(self, extension_uuid, meta_rule): + """Set the Meta rule + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :param meta_rule: a dictionary representing the meta_rule (see below) + :return:a dictionary containing the meta_rule + + Here is an example of a meta_rule: + { + "sub_meta_rules": { + "relation_super": { + "subject_categories": ["role"], + "action_categories": ["computing_action"], + "object_categories": ["id"], + "relation": "relation_super" + } + }, + "aggregation": "and_true_aggregation" + } + """ + raise exception.NotImplemented() # pragma: no cover + + # Getter and Setter for rules + + def get_rules(self, extension_uuid): + """Get all rules + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :return: a dictionary containing rules ie. + { + "relation_super":[ + ["admin", "vm_admin", "servers", True], + ["admin", "vm_access", "servers", True] + ] + } + All items will be UUID. + The last boolean item is the positive/negative value. If True, request that conforms to that rule + will be authorized, if false, request will be rejected. + """ + raise exception.NotImplemented() # pragma: no cover + + def set_rules(self, extension_uuid, rules): + """Set all rules + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :param rules: a dictionary containing rules (see below) + :type rules: dict + :return: a dictionary containing rules ie. + { + "relation_super":[ + ["admin", "vm_admin", "servers", True], + ["admin", "vm_access", "servers", True] + ] + } + All items will be UUID. + The last boolean item is the positive/negative value. If True, request that conforms to that rule + will be authorized, if false, request will be rejected. + """ + raise exception.NotImplemented() # pragma: no cover + + # Getter and Setter for intra_extension + + def get_intra_extension_list(self): + """Get a list of IntraExtension UUIDs + + :return: a list of IntraExtension UUIDs ["uuid1", "uuid2"] + """ + raise exception.NotImplemented() # pragma: no cover + + def get_intra_extension_dict(self, extension_uuid): + """Get a description of an IntraExtension + + :param extension_uuid: the UUID of the IntraExtension + :type extension_uuid: string + :return: + """ + raise exception.NotImplemented() # pragma: no cover + + def set_intra_extension(self, extension_uuid, extension_dict): + """Set a new IntraExtension + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :param extension_dict: a dictionary withe the description of the IntraExtension (see below) + :type extension_dict: dict + :return: the IntraExtension dictionary, example: + { + "id": "uuid1", + "name": "Name of the intra_extension", + "model": "Model of te intra_extension (admin or authz)" + "description": "a description of the intra_extension" + } + """ + raise exception.NotImplemented() # pragma: no cover + + def delete_intra_extension(self, extension_uuid): + """Delete an IntraExtension + + :param extension_uuid: IntraExtension UUID + :type extension_uuid: string + :return: None + """ + raise exception.NotImplemented() # pragma: no cover + + def get_sub_meta_rule_relations(self, username, uuid): + # TODO: check which relations are really usable + return {"sub_meta_rule_relations": ["relation_super", "relation_test"]} + + +class LogDriver(object): + + def authz(self, message): + """Log authorization message + + :param message: the message to log + :type message: string + :return: None + """ + raise exception.NotImplemented() # pragma: no cover + + def debug(self, message): + """Log debug message + + :param message: the message to log + :type message: string + :return: None + """ + raise exception.NotImplemented() # pragma: no cover + + def info(self, message): + """Log informational message + + :param message: the message to log + :type message: string + :return: None + """ + raise exception.NotImplemented() # pragma: no cover + + def warning(self, message): + """Log warning message + + :param message: the message to log + :type message: string + :return: None + """ + raise exception.NotImplemented() # pragma: no cover + + def error(self, message): + """Log error message + + :param message: the message to log + :type message: string + :return: None + """ + raise exception.NotImplemented() # pragma: no cover + + def critical(self, message): + """Log critical message + + :param message: the message to log + :type message: string + :return: None + """ + raise exception.NotImplemented() # pragma: no cover + + def get_logs(self, options): + """Get logs + + :param options: options to filter log events + :type options: string eg: "event_number=10,from=2014-01-01-10:10:10,to=2014-01-01-12:10:10,filter=expression" + :return: a list of log events + + TIME_FORMAT is '%Y-%m-%d-%H:%M:%S' + """ + raise exception.NotImplemented() # pragma: no cover + +# @dependency.provider('superextension_api') +# class SuperExtensionManager(manager.Manager): +# +# def __init__(self): +# driver = CONF.moon.superextension_driver +# super(SuperExtensionManager, self).__init__(driver) +# +# def authz(self, sub, obj, act): +# #return self.driver.admin(sub, obj, act) +# return True + + +# @dependency.provider('interextension_api') +# @dependency.requires('identity_api') +# class InterExtensionManager(manager.Manager): +# +# def __init__(self): +# driver = CONF.moon.interextension_driver +# super(InterExtensionManager, self).__init__(driver) +# +# def check_inter_extension(self, uuid): +# if uuid not in self.get_inter_extensions(): +# LOG.error("Unknown InterExtension {}".format(uuid)) +# raise exception.NotFound("InterExtension not found.") +# +# def get_inter_extensions(self): +# return self.driver.get_inter_extensions() +# +# def get_inter_extension(self, uuid): +# return self.driver.get_inter_extension(uuid) +# +# def create_inter_extension(self, inter_extension): +# ie = dict() +# ie['id'] = uuid4().hex +# ie["requesting_intra_extension_uuid"] = filter_input(inter_extension["requesting_intra_extension_uuid"]) +# ie["requested_intra_extension_uuid"] = filter_input(inter_extension["requested_intra_extension_uuid"]) +# ie["description"] = filter_input(inter_extension["description"]) +# ie["virtual_entity_uuid"] = filter_input(inter_extension["virtual_entity_uuid"]) +# ie["genre"] = filter_input(inter_extension["genre"]) +# +# ref = self.driver.create_inter_extensions(ie['id'], ie) +# return ref +# +# def delete_inter_extension(self, inter_extension_id): +# LOG.error("Deleting {}".format(inter_extension_id)) +# ref = self.driver.delete_inter_extensions(inter_extension_id) +# return ref +# +# +# class SuperExtensionDriver(object): +# +# def __init__(self): +# self.__super_extension = None +# +# def admin(self, sub, obj, act): +# return self.__super_extension.authz(sub, obj, act) +# +# def delegate(self, delegating_uuid, delegated_uuid, privilege): # TODO later +# pass +# +# # Getter and Setter for SuperExtensions +# +# def get_super_extensions(self): +# raise exception.NotImplemented() # pragma: no cover +# +# def create_super_extensions(self, super_id, super_extension): +# raise exception.NotImplemented() # pragma: no cover +# +# +# class InterExtensionDriver(object): +# +# # Getter and Setter for InterExtensions +# +# def get_inter_extensions(self): +# raise exception.NotImplemented() # pragma: no cover +# +# def get_inter_extension(self, uuid): +# raise exception.NotImplemented() # pragma: no cover +# +# def create_inter_extensions(self, intra_id, intra_extension): +# raise exception.NotImplemented() # pragma: no cover +# +# def delete_inter_extensions(self, intra_extension_id): +# raise exception.NotImplemented() # pragma: no cover +# +# +# class VirtualEntityDriver(object): +# +# # Getter and Setter for InterExtensions +# +# def get_virtual_entities(self): +# raise exception.NotImplemented() # pragma: no cover +# +# def create_virtual_entities(self, ve_id, virtual_entity): +# raise exception.NotImplemented() # pragma: no cover + diff --git a/keystone-moon/keystone/contrib/moon/exception.py b/keystone-moon/keystone/contrib/moon/exception.py new file mode 100644 index 00000000..20a7d737 --- /dev/null +++ b/keystone-moon/keystone/contrib/moon/exception.py @@ -0,0 +1,112 @@ +# Copyright 2015 Open Platform for NFV Project, Inc. and its contributors +# This software is distributed under the terms and conditions of the 'Apache-2.0' +# license which can be found in the file 'LICENSE' in this package distribution +# or at 'http://www.apache.org/licenses/LICENSE-2.0'. + +from keystone.common import dependency +from keystone.exception import Error +from keystone.i18n import _, _LW + +@dependency.requires('moonlog_api') +class TenantError(Error): + message_format = _("There is an error requesting this tenant" + " the server could not comply with the request" + " since it is either malformed or otherwise" + " incorrect. The client is assumed to be in error.") + code = 400 + title = 'Tenant Error' + logger = "ERROR" + + def __del__(self): + if self.logger == "ERROR": + self.moonlog_api.error(self.message_format) + elif self.logger == "WARNING": + self.moonlog_api.warning(self.message_format) + elif self.logger == "CRITICAL": + self.moonlog_api.critical(self.message_format) + elif self.logger == "AUTHZ": + self.moonlog_api.authz(self.message_format) + self.moonlog_api.error(self.message_format) + else: + self.moonlog_api.info(self.message_format) + + + +class TenantListEmptyError(TenantError): + message_format = _("The tenant list mapping is empty, you must set the mapping first.") + code = 400 + title = 'Tenant List Empty Error' + + +class TenantNotFoundError(TenantError): + message_format = _("The tenant UUID was not found.") + code = 400 + title = 'Tenant UUID Not Found Error' + + +class IntraExtensionError(TenantError): + message_format = _("There is an error requesting this IntraExtension.") + code = 400 + title = 'Extension Error' + + +class CategoryNotFound(IntraExtensionError): + message_format = _("The category is unknown.") + code = 400 + title = 'Extension Error' + logger = "WARNING" + + +class IntraExtensionUnMapped(TenantError): + message_format = _("The Extension is not mapped to a tenant.") + code = 400 + title = 'Extension UUID Not Found Error' + logger = "WARNING" + + +class IntraExtensionNotFound(IntraExtensionError): + message_format = _("The Extension for that tenant is unknown.") + code = 400 + title = 'Extension UUID Not Found Error' + logger = "WARNING" + + +class IntraExtensionNotAuthorized(IntraExtensionError): + message_format = _("User has no authorization for that action.") + code = 400 + title = 'Authorization Error' + logger = "AUTHZ" + + +class AdminIntraExtensionNotFound(IntraExtensionNotFound): + message_format = _("The admin Extension for that tenant is unknown.") + code = 400 + title = 'Admin Extension UUID Not Found Error' + logger = "WARNING" + + +class AdminIntraExtensionCreationError(IntraExtensionError): + message_format = _("The arguments for the creation of this admin Extension were malformed.") + code = 400 + title = 'Admin Extension Creation Error' + + +class AdminIntraExtensionModificationNotAuthorized(IntraExtensionError): + message_format = _("The modification of this admin Extension is not authorizaed.") + code = 400 + title = 'Admin Extension Creation Error' + logger = "AUTHZ" + +class AuthIntraExtensionModificationNotAuthorized(IntraExtensionError): + message_format = _("The modification of this authz Extension is not authorizaed.") + code = 400 + title = 'Authz Extension Creation Error' + logger = "AUTHZ" + + +class AuthzIntraExtensionNotFound(IntraExtensionNotFound): + message_format = _("The authz Extension for that tenant is unknown.") + code = 400 + title = 'Authz Extension UUID Not Found Error' + logger = "WARNING" + diff --git a/keystone-moon/keystone/contrib/moon/extension.py b/keystone-moon/keystone/contrib/moon/extension.py new file mode 100644 index 00000000..efee55c5 --- /dev/null +++ b/keystone-moon/keystone/contrib/moon/extension.py @@ -0,0 +1,740 @@ +# Copyright 2015 Open Platform for NFV Project, Inc. and its contributors +# This software is distributed under the terms and conditions of the 'Apache-2.0' +# license which can be found in the file 'LICENSE' in this package distribution +# or at 'http://www.apache.org/licenses/LICENSE-2.0'. + +import os.path +import copy +import json +import itertools +from uuid import uuid4 +import logging + +LOG = logging.getLogger("moon.authz") + + +class Metadata: + + def __init__(self): + self.__name = '' + self.__model = '' + self.__genre = '' + self.__description = '' + self.__subject_categories = list() + self.__object_categories = list() + self.__meta_rule = dict() + self.__meta_rule['sub_meta_rules'] = list() + self.__meta_rule['aggregation'] = '' + + def load_from_json(self, extension_setting_dir): + metadata_path = os.path.join(extension_setting_dir, 'metadata.json') + f = open(metadata_path) + json_metadata = json.load(f) + self.__name = json_metadata['name'] + self.__model = json_metadata['model'] + self.__genre = json_metadata['genre'] + self.__description = json_metadata['description'] + self.__subject_categories = copy.deepcopy(json_metadata['subject_categories']) + self.__object_categories = copy.deepcopy(json_metadata['object_categories']) + self.__meta_rule = copy.deepcopy(json_metadata['meta_rule']) + + def get_name(self): + return self.__name + + def get_genre(self): + return self.__genre + + def get_model(self): + return self.__model + + def get_subject_categories(self): + return self.__subject_categories + + def get_object_categories(self): + return self.__object_categories + + def get_meta_rule(self): + return self.__meta_rule + + def get_meta_rule_aggregation(self): + return self.__meta_rule['aggregation'] + + def get_data(self): + data = dict() + data["name"] = self.get_name() + data["model"] = self.__model + data["genre"] = self.__genre + data["description"] = self.__description + data["subject_categories"] = self.get_subject_categories() + data["object_categories"] = self.get_object_categories() + data["meta_rule"] = dict(self.get_meta_rule()) + return data + + def set_data(self, data): + self.__name = data["name"] + self.__model = data["model"] + self.__genre = data["genre"] + self.__description = data["description"] + self.__subject_categories = list(data["subject_categories"]) + self.__object_categories = list(data["object_categories"]) + self.__meta_rule = dict(data["meta_rule"]) + + +class Configuration: + def __init__(self): + self.__subject_category_values = dict() + # examples: { "role": {"admin", "dev", }, } + self.__object_category_values = dict() + self.__rules = list() + + def load_from_json(self, extension_setting_dir): + configuration_path = os.path.join(extension_setting_dir, 'configuration.json') + f = open(configuration_path) + json_configuration = json.load(f) + self.__subject_category_values = copy.deepcopy(json_configuration['subject_category_values']) + self.__object_category_values = copy.deepcopy(json_configuration['object_category_values']) + self.__rules = copy.deepcopy(json_configuration['rules']) # TODO: currently a list, will be a dict with sub-meta-rule as key + + def get_subject_category_values(self): + return self.__subject_category_values + + def get_object_category_values(self): + return self.__object_category_values + + def get_rules(self): + return self.__rules + + def get_data(self): + data = dict() + data["subject_category_values"] = self.get_subject_category_values() + data["object_category_values"] = self.get_object_category_values() + data["rules"] = self.get_rules() + return data + + def set_data(self, data): + self.__subject_category_values = list(data["subject_category_values"]) + self.__object_category_values = list(data["object_category_values"]) + self.__rules = list(data["rules"]) + + +class Perimeter: + def __init__(self): + self.__subjects = list() + self.__objects = list() + + def load_from_json(self, extension_setting_dir): + perimeter_path = os.path.join(extension_setting_dir, 'perimeter.json') + f = open(perimeter_path) + json_perimeter = json.load(f) + self.__subjects = copy.deepcopy(json_perimeter['subjects']) + self.__objects = copy.deepcopy(json_perimeter['objects']) + # print(self.__subjects) + # print(self.__objects) + + def get_subjects(self): + return self.__subjects + + def get_objects(self): + return self.__objects + + def get_data(self): + data = dict() + data["subjects"] = self.get_subjects() + data["objects"] = self.get_objects() + return data + + def set_data(self, data): + self.__subjects = list(data["subjects"]) + self.__objects = list(data["objects"]) + + +class Assignment: + def __init__(self): + self.__subject_category_assignments = dict() + # examples: { "role": {"user1": {"dev"}, "user2": {"admin",}}, } TODO: limit to one value for each attr + self.__object_category_assignments = dict() + + def load_from_json(self, extension_setting_dir): + assignment_path = os.path.join(extension_setting_dir, 'assignment.json') + f = open(assignment_path) + json_assignment = json.load(f) + + self.__subject_category_assignments = dict(copy.deepcopy(json_assignment['subject_category_assignments'])) + self.__object_category_assignments = dict(copy.deepcopy(json_assignment['object_category_assignments'])) + + def get_subject_category_assignments(self): + return self.__subject_category_assignments + + def get_object_category_assignments(self): + return self.__object_category_assignments + + def get_data(self): + data = dict() + data["subject_category_assignments"] = self.get_subject_category_assignments() + data["object_category_assignments"] = self.get_object_category_assignments() + return data + + def set_data(self, data): + self.__subject_category_assignments = list(data["subject_category_assignments"]) + self.__object_category_assignments = list(data["object_category_assignments"]) + + +class AuthzData: + def __init__(self, sub, obj, act): + self.validation = "False" # "OK, KO, Out of Scope" # "auth": False, + self.subject = sub + self.object = str(obj) + self.action = str(act) + self.type = "" # intra-tenant, inter-tenant, Out of Scope + self.subject_attrs = dict() + self.object_attrs = dict() + self.requesting_tenant = "" # "subject_tenant": subject_tenant, + self.requested_tenant = "" # "object_tenant": object_tenant, + + def __str__(self): + return """AuthzData: + validation={} + subject={} + object={} + action={} + """.format(self.validation, self.subject, self.object, self.action) + + +class Extension: + def __init__(self): + self.metadata = Metadata() + self.configuration = Configuration() + self.perimeter = Perimeter() + self.assignment = Assignment() + + def load_from_json(self, extension_setting_dir): + self.metadata.load_from_json(extension_setting_dir) + self.configuration.load_from_json(extension_setting_dir) + self.perimeter.load_from_json(extension_setting_dir) + self.assignment.load_from_json(extension_setting_dir) + + def get_name(self): + return self.metadata.get_name() + + def get_genre(self): + return self.metadata.get_genre() + + def authz(self, sub, obj, act): + authz_data = AuthzData(sub, obj, act) + # authz_logger.warning('extension/authz request: [sub {}, obj {}, act {}]'.format(sub, obj, act)) + + if authz_data.subject in self.perimeter.get_subjects() and authz_data.object in self.perimeter.get_objects(): + + for subject_category in self.metadata.get_subject_categories(): + authz_data.subject_attrs[subject_category] = copy.copy( + # self.assignment.get_subject_category_attr(subject_category, sub) + self.assignment.get_subject_category_assignments()[subject_category][sub] + ) + # authz_logger.warning('extension/authz subject attribute: [subject attr: {}]'.format( + # #self.assignment.get_subject_category_attr(subject_category, sub)) + # self.assignment.get_subject_category_assignments()[subject_category][sub]) + # ) + + for object_category in self.metadata.get_object_categories(): + if object_category == 'action': + authz_data.object_attrs[object_category] = [act] + # authz_logger.warning('extension/authz object attribute: [object attr: {}]'.format([act])) + else: + authz_data.object_attrs[object_category] = copy.copy( + self.assignment.get_object_category_assignments()[object_category][obj] + ) + # authz_logger.warning('extension/authz object attribute: [object attr: {}]'.format( + # self.assignment.get_object_category_assignments()[object_category][obj]) + # ) + + _aggregation_data = dict() + + for sub_meta_rule in self.metadata.get_meta_rule()["sub_meta_rules"].values(): + _tmp_relation_args = list() + + for sub_subject_category in sub_meta_rule["subject_categories"]: + _tmp_relation_args.append(authz_data.subject_attrs[sub_subject_category]) + + for sub_object_category in sub_meta_rule["object_categories"]: + _tmp_relation_args.append(authz_data.object_attrs[sub_object_category]) + + _relation_args = list(itertools.product(*_tmp_relation_args)) + + if sub_meta_rule['relation'] == 'relation_super': # TODO: replace by Prolog Engine + _aggregation_data['relation_super'] = dict() + _aggregation_data['relation_super']['result'] = False + for _relation_arg in _relation_args: + if list(_relation_arg) in self.configuration.get_rules()[sub_meta_rule['relation']]: + # authz_logger.warning( + # 'extension/authz relation super OK: [sub_sl: {}, obj_sl: {}, action: {}]'.format( + # _relation_arg[0], _relation_arg[1], _relation_arg[2] + # ) + # ) + _aggregation_data['relation_super']['result'] = True + break + _aggregation_data['relation_super']['status'] = 'finished' + + elif sub_meta_rule['relation'] == 'permission': + _aggregation_data['permission'] = dict() + _aggregation_data['permission']['result'] = False + for _relation_arg in _relation_args: + if list(_relation_arg) in self.configuration.get_rules()[sub_meta_rule['relation']]: + # authz_logger.warning( + # 'extension/authz relation permission OK: [role: {}, object: {}, action: {}]'.format( + # _relation_arg[0], _relation_arg[1], _relation_arg[2] + # ) + # ) + _aggregation_data['permission']['result'] = True + break + _aggregation_data['permission']['status'] = 'finished' + + if self.metadata.get_meta_rule_aggregation() == 'and_true_aggregation': + authz_data.validation = "OK" + for relation in _aggregation_data: + if _aggregation_data[relation]['status'] == 'finished' \ + and _aggregation_data[relation]['result'] == False: + authz_data.validation = "KO" + else: + authz_data.validation = 'Out of Scope' + + return authz_data.validation + + # ---------------- metadate api ---------------- + + def get_subject_categories(self): + return self.metadata.get_subject_categories() + + def add_subject_category(self, category_id): + if category_id in self.get_subject_categories(): + return "[ERROR] Add Subject Category: Subject Category Exists" + else: + self.get_subject_categories().append(category_id) + self.configuration.get_subject_category_values()[category_id] = list() + self.assignment.get_subject_category_assignments()[category_id] = dict() + return self.get_subject_categories() + + def del_subject_category(self, category_id): + if category_id in self.get_subject_categories(): + self.configuration.get_subject_category_values().pop(category_id) + self.assignment.get_subject_category_assignments().pop(category_id) + self.get_subject_categories().remove(category_id) + return self.get_subject_categories() + else: + return "[ERROR] Del Subject Category: Subject Category Unknown" + + def get_object_categories(self): + return self.metadata.get_object_categories() + + def add_object_category(self, category_id): + if category_id in self.get_object_categories(): + return "[ERROR] Add Object Category: Object Category Exists" + else: + self.get_object_categories().append(category_id) + self.configuration.get_object_category_values()[category_id] = list() + self.assignment.get_object_category_assignments()[category_id] = dict() + return self.get_object_categories() + + def del_object_category(self, category_id): + if category_id in self.get_object_categories(): + self.configuration.get_object_category_values().pop(category_id) + self.assignment.get_object_category_assignments().pop(category_id) + self.get_object_categories().remove(category_id) + return self.get_object_categories() + else: + return "[ERROR] Del Object Category: Object Category Unknown" + + def get_meta_rule(self): + return self.metadata.get_meta_rule() + + # ---------------- configuration api ---------------- + + def get_subject_category_values(self, category_id): + return self.configuration.get_subject_category_values()[category_id] + + def add_subject_category_value(self, category_id, category_value): + if category_value in self.configuration.get_subject_category_values()[category_id]: + return "[ERROR] Add Subject Category Value: Subject Category Value Exists" + else: + self.configuration.get_subject_category_values()[category_id].append(category_value) + return self.configuration.get_subject_category_values()[category_id] + + def del_subject_category_value(self, category_id, category_value): + if category_value in self.configuration.get_subject_category_values()[category_id]: + self.configuration.get_subject_category_values()[category_id].remove(category_value) + return self.configuration.get_subject_category_values()[category_id] + else: + return "[ERROR] Del Subject Category Value: Subject Category Value Unknown" + + def get_object_category_values(self, category_id): + return self.configuration.get_object_category_values()[category_id] + + def add_object_category_value(self, category_id, category_value): + if category_value in self.configuration.get_object_category_values()[category_id]: + return "[ERROR] Add Object Category Value: Object Category Value Exists" + else: + self.configuration.get_object_category_values()[category_id].append(category_value) + return self.configuration.get_object_category_values()[category_id] + + def del_object_category_value(self, category_id, category_value): + if category_value in self.configuration.get_object_category_values()[category_id]: + self.configuration.get_object_category_values()[category_id].remove(category_value) + return self.configuration.get_object_category_values()[category_id] + else: + return "[ERROR] Del Object Category Value: Object Category Value Unknown" + + def get_meta_rules(self): + return self.metadata.get_meta_rule() + + def _build_rule_from_list(self, relation, rule): + rule = list(rule) + _rule = dict() + _rule["sub_cat_value"] = dict() + _rule["obj_cat_value"] = dict() + if relation in self.metadata.get_meta_rule()["sub_meta_rules"]: + _rule["sub_cat_value"][relation] = dict() + _rule["obj_cat_value"][relation] = dict() + for s_category in self.metadata.get_meta_rule()["sub_meta_rules"][relation]["subject_categories"]: + _rule["sub_cat_value"][relation][s_category] = rule.pop(0) + for o_category in self.metadata.get_meta_rule()["sub_meta_rules"][relation]["object_categories"]: + _rule["obj_cat_value"][relation][o_category] = rule.pop(0) + return _rule + + def get_rules(self, full=False): + if not full: + return self.configuration.get_rules() + rules = dict() + for key in self.configuration.get_rules(): + rules[key] = map(lambda x: self._build_rule_from_list(key, x), self.configuration.get_rules()[key]) + return rules + + def add_rule(self, sub_cat_value_dict, obj_cat_value_dict): + for _relation in self.metadata.get_meta_rule()["sub_meta_rules"]: + _sub_rule = list() + for sub_subject_category in self.metadata.get_meta_rule()["sub_meta_rules"][_relation]["subject_categories"]: + try: + if sub_cat_value_dict[_relation][sub_subject_category] \ + in self.configuration.get_subject_category_values()[sub_subject_category]: + _sub_rule.append(sub_cat_value_dict[_relation][sub_subject_category]) + else: + return "[Error] Add Rule: Subject Category Value Unknown" + except KeyError as e: + # DThom: sometimes relation attribute is buggy, I don't know why... + print(e) + + #BUG: when adding a new category in rules despite it was previously adding + # data = { + # "sub_cat_value": + # {"relation_super": + # {"subject_security_level": "high", "AMH_CAT": "AMH_VAL"} + # }, + # "obj_cat_value": + # {"relation_super": + # {"object_security_level": "medium"} + # } + # } + # traceback = """ + # Traceback (most recent call last): + # File "/moon/gui/views_json.py", line 20, in wrapped + # result = function(*args, **kwargs) + # File "/moon/gui/views_json.py", line 429, in rules + # obj_cat_value=filter_input(data["obj_cat_value"])) + # File "/usr/local/lib/python2.7/dist-packages/moon/core/pap/core.py", line 380, in add_rule + # obj_cat_value) + # File "/usr/local/lib/python2.7/dist-packages/moon/core/pdp/extension.py", line 414, in add_rule + # if obj_cat_value_dict[_relation][sub_object_category] \ + # KeyError: u'action' + # """ + for sub_object_category in self.metadata.get_meta_rule()["sub_meta_rules"][_relation]["object_categories"]: + if obj_cat_value_dict[_relation][sub_object_category] \ + in self.configuration.get_object_category_values()[sub_object_category]: + _sub_rule.append(obj_cat_value_dict[_relation][sub_object_category]) + else: + return "[Error] Add Rule: Object Category Value Unknown" + + if _sub_rule in self.configuration.get_rules()[_relation]: + return "[Error] Add Rule: Rule Exists" + else: + self.configuration.get_rules()[_relation].append(_sub_rule) + return { + sub_cat_value_dict.keys()[0]: ({ + "sub_cat_value": copy.deepcopy(sub_cat_value_dict), + "obj_cat_value": copy.deepcopy(obj_cat_value_dict) + }, ) + } + return self.configuration.get_rules() + + def del_rule(self, sub_cat_value_dict, obj_cat_value_dict): + for _relation in self.metadata.get_meta_rule()["sub_meta_rules"]: + _sub_rule = list() + for sub_subject_category in self.metadata.get_meta_rule()["sub_meta_rules"][_relation]["subject_categories"]: + _sub_rule.append(sub_cat_value_dict[_relation][sub_subject_category]) + + for sub_object_category in self.metadata.get_meta_rule()["sub_meta_rules"][_relation]["object_categories"]: + _sub_rule.append(obj_cat_value_dict[_relation][sub_object_category]) + + if _sub_rule in self.configuration.get_rules()[_relation]: + self.configuration.get_rules()[_relation].remove(_sub_rule) + else: + return "[Error] Del Rule: Rule Unknown" + return self.configuration.get_rules() + + # ---------------- perimeter api ---------------- + + def get_subjects(self): + return self.perimeter.get_subjects() + + def get_objects(self): + return self.perimeter.get_objects() + + def add_subject(self, subject_id): + if subject_id in self.perimeter.get_subjects(): + return "[ERROR] Add Subject: Subject Exists" + else: + self.perimeter.get_subjects().append(subject_id) + return self.perimeter.get_subjects() + + def del_subject(self, subject_id): + if subject_id in self.perimeter.get_subjects(): + self.perimeter.get_subjects().remove(subject_id) + return self.perimeter.get_subjects() + else: + return "[ERROR] Del Subject: Subject Unknown" + + def add_object(self, object_id): + if object_id in self.perimeter.get_objects(): + return "[ERROR] Add Object: Object Exists" + else: + self.perimeter.get_objects().append(object_id) + return self.perimeter.get_objects() + + def del_object(self, object_id): + if object_id in self.perimeter.get_objects(): + self.perimeter.get_objects().remove(object_id) + return self.perimeter.get_objects() + else: + return "[ERROR] Del Object: Object Unknown" + + # ---------------- assignment api ---------------- + + def get_subject_assignments(self, category_id): + if category_id in self.metadata.get_subject_categories(): + return self.assignment.get_subject_category_assignments()[category_id] + else: + return "[ERROR] Get Subject Assignment: Subject Category Unknown" + + def add_subject_assignment(self, category_id, subject_id, category_value): + if category_id in self.metadata.get_subject_categories(): + if subject_id in self.perimeter.get_subjects(): + if category_value in self.configuration.get_subject_category_values()[category_id]: + if category_id in self.assignment.get_subject_category_assignments().keys(): + if subject_id in self.assignment.get_subject_category_assignments()[category_id].keys(): + if category_value in self.assignment.get_subject_category_assignments()[category_id][subject_id]: + return "[ERROR] Add Subject Assignment: Subject Assignment Exists" + else: + self.assignment.get_subject_category_assignments()[category_id][subject_id].extend([category_value]) + else: + self.assignment.get_subject_category_assignments()[category_id][subject_id] = [category_value] + else: + self.assignment.get_subject_category_assignments()[category_id] = {subject_id: [category_value]} + return self.assignment.get_subject_category_assignments() + else: + return "[ERROR] Add Subject Assignment: Subject Category Value Unknown" + else: + return "[ERROR] Add Subject Assignment: Subject Unknown" + else: + return "[ERROR] Add Subject Assignment: Subject Category Unknown" + + def del_subject_assignment(self, category_id, subject_id, category_value): + if category_id in self.metadata.get_subject_categories(): + if subject_id in self.perimeter.get_subjects(): + if category_value in self.configuration.get_subject_category_values()[category_id]: + if len(self.assignment.get_subject_category_assignments()[category_id][subject_id]) >= 2: + self.assignment.get_subject_category_assignments()[category_id][subject_id].remove(category_value) + else: + self.assignment.get_subject_category_assignments()[category_id].pop(subject_id) + return self.assignment.get_subject_category_assignments() + else: + return "[ERROR] Del Subject Assignment: Assignment Unknown" + else: + return "[ERROR] Del Subject Assignment: Subject Unknown" + else: + return "[ERROR] Del Subject Assignment: Subject Category Unknown" + + def get_object_assignments(self, category_id): + if category_id in self.metadata.get_object_categories(): + return self.assignment.get_object_category_assignments()[category_id] + else: + return "[ERROR] Get Object Assignment: Object Category Unknown" + + def add_object_assignment(self, category_id, object_id, category_value): + if category_id in self.metadata.get_object_categories(): + if object_id in self.perimeter.get_objects(): + if category_value in self.configuration.get_object_category_values()[category_id]: + if category_id in self.assignment.get_object_category_assignments().keys(): + if object_id in self.assignment.get_object_category_assignments()[category_id].keys(): + if category_value in self.assignment.get_object_category_assignments()[category_id][object_id]: + return "[ERROR] Add Object Assignment: Object Assignment Exists" + else: + self.assignment.get_object_category_assignments()[category_id][object_id].extend([category_value]) + else: + self.assignment.get_object_category_assignments()[category_id][object_id] = [category_value] + else: + self.assignment.get_object_category_assignments()[category_id] = {object_id: [category_value]} + return self.assignment.get_object_category_assignments() + else: + return "[ERROR] Add Object Assignment: Object Category Value Unknown" + else: + return "[ERROR] Add Object Assignment: Object Unknown" + else: + return "[ERROR] Add Object Assignment: Object Category Unknown" + + def del_object_assignment(self, category_id, object_id, category_value): + if category_id in self.metadata.get_object_categories(): + if object_id in self.perimeter.get_objects(): + if category_value in self.configuration.get_object_category_values()[category_id]: + if len(self.assignment.get_object_category_assignments()[category_id][object_id]) >= 2: + self.assignment.get_object_category_assignments()[category_id][object_id].remove(category_value) + else: + self.assignment.get_object_category_assignments()[category_id].pop(object_id) + return self.assignment.get_object_category_assignments() + else: + return "[ERROR] Del Object Assignment: Assignment Unknown" + else: + return "[ERROR] Del Object Assignment: Object Unknown" + else: + return "[ERROR] Del Object Assignment: Object Category Unknown" + + # ---------------- inter-extension API ---------------- + + def create_requesting_collaboration(self, sub_list, vent_uuid, act): + _sub_cat_values = dict() + _obj_cat_values = dict() + + if type(self.add_object(vent_uuid)) is not list: + return "[Error] Create Requesting Collaboration: No Success" + for _relation in self.get_meta_rule()["sub_meta_rules"]: + for _sub_cat_id in self.get_meta_rule()["sub_meta_rules"][_relation]["subject_categories"]: + _sub_cat_value = str(uuid4()) + if type(self.add_subject_category_value(_sub_cat_id, _sub_cat_value)) is not list: + return "[Error] Create Requesting Collaboration: No Success" + _sub_cat_values[_relation] = {_sub_cat_id: _sub_cat_value} + for _sub in sub_list: + if type(self.add_subject_assignment(_sub_cat_id, _sub, _sub_cat_value)) is not dict: + return "[Error] Create Requesting Collaboration: No Success" + + for _obj_cat_id in self.get_meta_rule()["sub_meta_rules"][_relation]["object_categories"]: + if _obj_cat_id == 'action': + _obj_cat_values[_relation][_obj_cat_id] = act + else: + _obj_cat_value = str(uuid4()) + if type(self.add_object_category_value(_obj_cat_id, _obj_cat_value)) is not list: + return "[Error] Create Requesting Collaboration: No Success" + if type(self.add_object_assignment(_obj_cat_id, vent_uuid, _obj_cat_value)) is not dict: + return "[Error] Create Requesting Collaboration: No Success" + _obj_cat_values[_relation] = {_obj_cat_id: _obj_cat_value} + + _rule = self.add_rule(_sub_cat_values, _obj_cat_values) + if type(_rule) is not dict: + return "[Error] Create Requesting Collaboration: No Success" + return {"subject_category_value_dict": _sub_cat_values, "object_category_value_dict": _obj_cat_values, + "rule": _rule} + + def destroy_requesting_collaboration(self, sub_list, vent_uuid, sub_cat_value_dict, obj_cat_value_dict): + for _relation in self.get_meta_rule()["sub_meta_rules"]: + for _sub_cat_id in self.get_meta_rule()["sub_meta_rules"][_relation]["subject_categories"]: + for _sub in sub_list: + if type(self.del_subject_assignment(_sub_cat_id, _sub, sub_cat_value_dict[_relation][_sub_cat_id]))\ + is not dict: + return "[Error] Destroy Requesting Collaboration: No Success" + if type(self.del_subject_category_value(_sub_cat_id, sub_cat_value_dict[_relation][_sub_cat_id])) \ + is not list: + return "[Error] Destroy Requesting Collaboration: No Success" + + for _obj_cat_id in self.get_meta_rule()["sub_meta_rules"][_relation]["object_categories"]: + if _obj_cat_id == "action": + pass # TODO: reconsidering the action as object attribute + else: + if type(self.del_object_assignment(_obj_cat_id, vent_uuid, obj_cat_value_dict[_relation][_obj_cat_id])) is not dict: + return "[Error] Destroy Requesting Collaboration: No Success" + if type(self.del_object_category_value(_obj_cat_id, obj_cat_value_dict[_relation][_obj_cat_id])) is not list: + return "[Error] Destroy Requesting Collaboration: No Success" + + if type(self.del_rule(sub_cat_value_dict, obj_cat_value_dict)) is not dict: + return "[Error] Destroy Requesting Collaboration: No Success" + if type(self.del_object(vent_uuid)) is not list: + return "[Error] Destroy Requesting Collaboration: No Success" + return "[Destroy Requesting Collaboration] OK" + + def create_requested_collaboration(self, vent_uuid, obj_list, act): + _sub_cat_values = dict() + _obj_cat_values = dict() + + if type(self.add_subject(vent_uuid)) is not list: + return "[Error] Create Requested Collaboration: No Success" + + for _relation in self.get_meta_rule()["sub_meta_rules"]: + for _sub_cat_id in self.get_meta_rule()["sub_meta_rules"][_relation]["subject_categories"]: + _sub_cat_value = str(uuid4()) + if type(self.add_subject_category_value(_sub_cat_id, _sub_cat_value)) is not list: + return "[Error] Create Requested Collaboration: No Success" + _sub_cat_values[_relation] = {_sub_cat_id: _sub_cat_value} + if type(self.add_subject_assignment(_sub_cat_id, vent_uuid, _sub_cat_value)) is not dict: + return "[Error] Create Requested Collaboration: No Success" + + for _obj_cat_id in self.get_meta_rule()["sub_meta_rules"][_relation]["object_categories"]: + if _obj_cat_id == 'action': + _obj_cat_values[_relation][_obj_cat_id] = act + else: + _obj_cat_value = str(uuid4()) + if type(self.add_object_category_value(_obj_cat_id, _obj_cat_value)) is not list: + return "[Error] Create Requested Collaboration: No Success" + _obj_cat_values[_relation] = {_obj_cat_id: _obj_cat_value} + for _obj in obj_list: + if type(self.add_object_assignment(_obj_cat_id, _obj, _obj_cat_value)) is not dict: + return "[Error] Create Requested Collaboration: No Success" + + _rule = self.add_rule(_sub_cat_values, _obj_cat_values) + if type(_rule) is not dict: + return "[Error] Create Requested Collaboration: No Success" + return {"subject_category_value_dict": _sub_cat_values, "object_category_value_dict": _obj_cat_values, + "rule": _rule} + + def destroy_requested_collaboration(self, vent_uuid, obj_list, sub_cat_value_dict, obj_cat_value_dict): + for _relation in self.get_meta_rule()["sub_meta_rules"]: + for _sub_cat_id in self.get_meta_rule()["sub_meta_rules"][_relation]["subject_categories"]: + if type(self.del_subject_assignment(_sub_cat_id, vent_uuid, sub_cat_value_dict[_relation][_sub_cat_id])) is not dict: + return "[Error] Destroy Requested Collaboration: No Success" + if type(self.del_subject_category_value(_sub_cat_id, sub_cat_value_dict[_relation][_sub_cat_id])) is not list: + return "[Error] Destroy Requested Collaboration: No Success" + + for _obj_cat_id in self.get_meta_rule()["sub_meta_rules"][_relation]["object_categories"]: + if _obj_cat_id == "action": + pass # TODO: reconsidering the action as object attribute + else: + for _obj in obj_list: + if type(self.del_object_assignment(_obj_cat_id, _obj, obj_cat_value_dict[_relation][_obj_cat_id])) is not dict: + return "[Error] Destroy Requested Collaboration: No Success" + if type(self.del_object_category_value(_obj_cat_id, obj_cat_value_dict[_relation][_obj_cat_id])) is not list: + return "[Error] Destroy Requested Collaboration: No Success" + + if type(self.del_rule(sub_cat_value_dict, obj_cat_value_dict)) is not dict: + return "[Error] Destroy Requested Collaboration: No Success" + if type(self.del_subject(vent_uuid)) is not list: + return "[Error] Destroy Requested Collaboration: No Success" + return "[Destroy Requested Collaboration] OK" + + # ---------------- sync_db api ---------------- + + def get_data(self): + data = dict() + data["metadata"] = self.metadata.get_data() + data["configuration"] = self.configuration.get_data() + data["perimeter"] = self.perimeter.get_data() + data["assignment"] = self.assignment.get_data() + return data + + def set_data(self, extension_data): + self.metadata.set_data(extension_data["metadata"]) + self.configuration.set_data(extension_data["configuration"]) + self.perimeter.set_data(extension_data["perimeter"]) + self.assignment.set_data(extension_data["assignment"]) diff --git a/keystone-moon/keystone/contrib/moon/migrate_repo/__init__.py b/keystone-moon/keystone/contrib/moon/migrate_repo/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/contrib/moon/migrate_repo/migrate.cfg b/keystone-moon/keystone/contrib/moon/migrate_repo/migrate.cfg new file mode 100644 index 00000000..7a7bd1f8 --- /dev/null +++ b/keystone-moon/keystone/contrib/moon/migrate_repo/migrate.cfg @@ -0,0 +1,25 @@ +[db_settings] +# Used to identify which repository this database is versioned under. +# You can use the name of your project. +repository_id=moon + +# The name of the database table used to track the schema version. +# This name shouldn't already be used by your project. +# If this is changed once a database is under version control, you'll need to +# change the table name in each database too. +version_table=migrate_version + +# When committing a change script, Migrate will attempt to generate the +# sql for all supported databases; normally, if one of them fails - probably +# because you don't have that database installed - it is ignored and the +# commit continues, perhaps ending successfully. +# Databases in this list MUST compile successfully during a commit, or the +# entire commit will fail. List the databases your application will actually +# be using to ensure your updates to that database work properly. +# This must be a list; example: ['postgres','sqlite'] +required_dbs=[] + +# When creating new change scripts, Migrate will stamp the new script with +# a version number. By default this is latest_version + 1. You can set this +# to 'true' to tell Migrate to use the UTC timestamp instead. +use_timestamp_numbering=False diff --git a/keystone-moon/keystone/contrib/moon/migrate_repo/versions/001_moon.py b/keystone-moon/keystone/contrib/moon/migrate_repo/versions/001_moon.py new file mode 100644 index 00000000..a49ca206 --- /dev/null +++ b/keystone-moon/keystone/contrib/moon/migrate_repo/versions/001_moon.py @@ -0,0 +1,194 @@ +# Copyright 2015 Open Platform for NFV Project, Inc. and its contributors +# This software is distributed under the terms and conditions of the 'Apache-2.0' +# license which can be found in the file 'LICENSE' in this package distribution +# or at 'http://www.apache.org/licenses/LICENSE-2.0'. + +import sqlalchemy as sql +from keystone.common import sql as k_sql + + +def upgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + + intra_extension_table = sql.Table( + 'intra_extension', + meta, + sql.Column('id', sql.String(64), primary_key=True), + sql.Column('name', sql.String(64), nullable=False), + sql.Column('model', sql.String(64), nullable=True), + sql.Column('description', sql.Text(), nullable=True), + mysql_engine='InnoDB', + mysql_charset='utf8') + intra_extension_table.create(migrate_engine, checkfirst=True) + + subjects_table = sql.Table( + 'subject', + meta, + sql.Column('id', sql.String(64), primary_key=True), + sql.Column('subjects', k_sql.JsonBlob(), nullable=True), + sql.Column('intra_extension_uuid', sql.ForeignKey("intra_extension.id"), nullable=False), + mysql_engine='InnoDB', + mysql_charset='utf8') + subjects_table.create(migrate_engine, checkfirst=True) + + objects_table = sql.Table( + 'object', + meta, + sql.Column('id', sql.String(64), primary_key=True), + sql.Column('objects', k_sql.JsonBlob(), nullable=True), + sql.Column('intra_extension_uuid', sql.ForeignKey("intra_extension.id"), nullable=False), + mysql_engine='InnoDB', + mysql_charset='utf8') + objects_table.create(migrate_engine, checkfirst=True) + + actions_table = sql.Table( + 'action', + meta, + sql.Column('id', sql.String(64), primary_key=True), + sql.Column('actions', k_sql.JsonBlob(), nullable=True), + sql.Column('intra_extension_uuid', sql.ForeignKey("intra_extension.id"), nullable=False), + mysql_engine='InnoDB', + mysql_charset='utf8') + actions_table.create(migrate_engine, checkfirst=True) + + subject_categories_table = sql.Table( + 'subject_category', + meta, + sql.Column('id', sql.String(64), primary_key=True), + sql.Column('subject_categories', k_sql.JsonBlob(), nullable=True), + sql.Column('intra_extension_uuid', sql.ForeignKey("intra_extension.id"), nullable=False), + mysql_engine='InnoDB', + mysql_charset='utf8') + subject_categories_table.create(migrate_engine, checkfirst=True) + + object_categories_table = sql.Table( + 'object_category', + meta, + sql.Column('id', sql.String(64), primary_key=True), + sql.Column('object_categories', k_sql.JsonBlob(), nullable=True), + sql.Column('intra_extension_uuid', sql.ForeignKey("intra_extension.id"), nullable=False), + mysql_engine='InnoDB', + mysql_charset='utf8') + object_categories_table.create(migrate_engine, checkfirst=True) + + action_categories_table = sql.Table( + 'action_category', + meta, + sql.Column('id', sql.String(64), primary_key=True), + sql.Column('action_categories', k_sql.JsonBlob(), nullable=True), + sql.Column('intra_extension_uuid', sql.ForeignKey("intra_extension.id"), nullable=False), + mysql_engine='InnoDB', + mysql_charset='utf8') + action_categories_table.create(migrate_engine, checkfirst=True) + + subject_category_values_table = sql.Table( + 'subject_category_scope', + meta, + sql.Column('id', sql.String(64), primary_key=True), + sql.Column('subject_category_scope', k_sql.JsonBlob(), nullable=True), + sql.Column('intra_extension_uuid', sql.ForeignKey("intra_extension.id"), nullable=False), + mysql_engine='InnoDB', + mysql_charset='utf8') + subject_category_values_table.create(migrate_engine, checkfirst=True) + + object_category_values_table = sql.Table( + 'object_category_scope', + meta, + sql.Column('id', sql.String(64), primary_key=True), + sql.Column('object_category_scope', k_sql.JsonBlob(), nullable=True), + sql.Column('intra_extension_uuid', sql.ForeignKey("intra_extension.id"), nullable=False), + mysql_engine='InnoDB', + mysql_charset='utf8') + object_category_values_table.create(migrate_engine, checkfirst=True) + + action_category_values_table = sql.Table( + 'action_category_scope', + meta, + sql.Column('id', sql.String(64), primary_key=True), + sql.Column('action_category_scope', k_sql.JsonBlob(), nullable=True), + sql.Column('intra_extension_uuid', sql.ForeignKey("intra_extension.id"), nullable=False), + mysql_engine='InnoDB', + mysql_charset='utf8') + action_category_values_table.create(migrate_engine, checkfirst=True) + + subject_category_assignments_table = sql.Table( + 'subject_category_assignment', + meta, + sql.Column('id', sql.String(64), primary_key=True), + sql.Column('subject_category_assignments', k_sql.JsonBlob(), nullable=True), + sql.Column('intra_extension_uuid', sql.ForeignKey("intra_extension.id"), nullable=False), + mysql_engine='InnoDB', + mysql_charset='utf8') + subject_category_assignments_table.create(migrate_engine, checkfirst=True) + + object_category_assignments_table = sql.Table( + 'object_category_assignment', + meta, + sql.Column('id', sql.String(64), primary_key=True), + sql.Column('object_category_assignments', k_sql.JsonBlob(), nullable=True), + sql.Column('intra_extension_uuid', sql.ForeignKey("intra_extension.id"), nullable=False), + mysql_engine='InnoDB', + mysql_charset='utf8') + object_category_assignments_table.create(migrate_engine, checkfirst=True) + + action_category_assignments_table = sql.Table( + 'action_category_assignment', + meta, + sql.Column('id', sql.String(64), primary_key=True), + sql.Column('action_category_assignments', k_sql.JsonBlob(), nullable=True), + sql.Column('intra_extension_uuid', sql.ForeignKey("intra_extension.id"), nullable=False), + mysql_engine='InnoDB', + mysql_charset='utf8') + action_category_assignments_table.create(migrate_engine, checkfirst=True) + + meta_rule_table = sql.Table( + 'metarule', + meta, + sql.Column('id', sql.String(64), primary_key=True), + sql.Column('sub_meta_rules', k_sql.JsonBlob(), nullable=True), + sql.Column('aggregation', sql.Text(), nullable=True), + sql.Column('intra_extension_uuid', sql.ForeignKey("intra_extension.id"), nullable=False), + mysql_engine='InnoDB', + mysql_charset='utf8') + meta_rule_table.create(migrate_engine, checkfirst=True) + + rule_table = sql.Table( + 'rule', + meta, + sql.Column('id', sql.String(64), primary_key=True), + sql.Column('rules', k_sql.JsonBlob(), nullable=True), + sql.Column('intra_extension_uuid', sql.ForeignKey("intra_extension.id"), nullable=False), + mysql_engine='InnoDB', + mysql_charset='utf8') + rule_table.create(migrate_engine, checkfirst=True) + + +def downgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + + for _table in ( + 'subject', + 'object', + 'action', + 'subject_category', + 'object_category', + 'action_category', + 'subject_category_scope', + 'object_category_scope', + 'action_category_scope', + 'subject_category_assignment', + 'object_category_assignment', + 'action_category_assignment', + 'metarule', + 'rule', + 'intra_extension', + ): + try: + table = sql.Table(_table, meta, autoload=True) + table.drop(migrate_engine, checkfirst=True) + except Exception as e: + print(e.message) + + diff --git a/keystone-moon/keystone/contrib/moon/migrate_repo/versions/002_moon.py b/keystone-moon/keystone/contrib/moon/migrate_repo/versions/002_moon.py new file mode 100644 index 00000000..a0f9095f --- /dev/null +++ b/keystone-moon/keystone/contrib/moon/migrate_repo/versions/002_moon.py @@ -0,0 +1,34 @@ +# Copyright 2015 Open Platform for NFV Project, Inc. and its contributors +# This software is distributed under the terms and conditions of the 'Apache-2.0' +# license which can be found in the file 'LICENSE' in this package distribution +# or at 'http://www.apache.org/licenses/LICENSE-2.0'. + +import sqlalchemy as sql +from keystone.common import sql as k_sql + + +def upgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + + region_table = sql.Table( + 'inter_extension', + meta, + sql.Column('id', sql.String(64), primary_key=True), + sql.Column('requesting_intra_extension_uuid', sql.String(64), nullable=False), + sql.Column('requested_intra_extension_uuid', sql.String(64), nullable=False), + sql.Column('virtual_entity_uuid', sql.String(64), nullable=False), + sql.Column('genre', sql.String(64), nullable=False), + sql.Column('description', sql.Text(), nullable=True), + + mysql_engine='InnoDB', + mysql_charset='utf8') + region_table.create(migrate_engine, checkfirst=True) + + +def downgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + + table = sql.Table('inter_extension', meta, autoload=True) + table.drop(migrate_engine, checkfirst=True) diff --git a/keystone-moon/keystone/contrib/moon/migrate_repo/versions/003_moon.py b/keystone-moon/keystone/contrib/moon/migrate_repo/versions/003_moon.py new file mode 100644 index 00000000..06932754 --- /dev/null +++ b/keystone-moon/keystone/contrib/moon/migrate_repo/versions/003_moon.py @@ -0,0 +1,32 @@ +# Copyright 2015 Open Platform for NFV Project, Inc. and its contributors +# This software is distributed under the terms and conditions of the 'Apache-2.0' +# license which can be found in the file 'LICENSE' in this package distribution +# or at 'http://www.apache.org/licenses/LICENSE-2.0'. + +import sqlalchemy as sql +from keystone.common import sql as k_sql + + +def upgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + + region_table = sql.Table( + 'tenants', + meta, + sql.Column('id', sql.String(64), primary_key=True), + sql.Column('name', sql.String(128), nullable=True), + sql.Column('authz', sql.String(64), nullable=True), + sql.Column('admin', sql.String(64), nullable=True), + + mysql_engine='InnoDB', + mysql_charset='utf8') + region_table.create(migrate_engine, checkfirst=True) + + +def downgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + + table = sql.Table('tenants', meta, autoload=True) + table.drop(migrate_engine, checkfirst=True) diff --git a/keystone-moon/keystone/contrib/moon/routers.py b/keystone-moon/keystone/contrib/moon/routers.py new file mode 100644 index 00000000..e1eb1130 --- /dev/null +++ b/keystone-moon/keystone/contrib/moon/routers.py @@ -0,0 +1,443 @@ +# Copyright 2015 Open Platform for NFV Project, Inc. and its contributors +# This software is distributed under the terms and conditions of the 'Apache-2.0' +# license which can be found in the file 'LICENSE' in this package distribution +# or at 'http://www.apache.org/licenses/LICENSE-2.0'. + +"""WSGI Routers for the Moon service.""" + +from keystone.contrib.moon import controllers +from keystone.common import wsgi + + +class Routers(wsgi.RoutersBase): + """API Endpoints for the Moon extension. + """ + + PATH_PREFIX = '/OS-MOON' + + @staticmethod + def _get_rel(component): + return 'http://docs.openstack.org/api/openstack-authz/3/param/{}'.format(component) + + @staticmethod + def _get_path(component): + return 'http://docs.openstack.org/api/openstack-authz/3/param/{}'.format(component) + + def append_v3_routers(self, mapper, routers): + # Controllers creation + authz_controller = controllers.Authz_v3() + intra_ext_controller = controllers.IntraExtensions() + authz_policies_controller = controllers.AuthzPolicies() + tenants_controller = controllers.Tenants() + logs_controller = controllers.Logs() + inter_ext_controller = controllers.InterExtensions() + + # Authz route + self._add_resource( + mapper, authz_controller, + path=self.PATH_PREFIX+'/authz/{tenant_id}/{subject_id}/{object_id}/{action_id}', + get_action='get_authz', + rel=self._get_rel('authz'), + path_vars={ + 'tenant_id': self._get_path('tenants'), + 'subject_id': self._get_path('subjects'), + 'object_id': self._get_path('objects'), + 'action_id': self._get_path('actions'), + }) + + # IntraExtensions route + self._add_resource( + mapper, intra_ext_controller, + path=self.PATH_PREFIX+'/intra_extensions', + get_action='get_intra_extensions', + post_action='create_intra_extension', + rel=self._get_rel('intra_extensions'), + path_vars={}) + self._add_resource( + mapper, intra_ext_controller, + path=self.PATH_PREFIX+'/intra_extensions/{intra_extensions_id}', + get_action='get_intra_extension', + delete_action='delete_intra_extension', + rel=self._get_rel('intra_extensions'), + path_vars={ + 'intra_extensions_id': self._get_path('intra_extensions'), + }) + + self._add_resource( + mapper, authz_policies_controller, + path=self.PATH_PREFIX+'/authz_policies', + get_action='get_authz_policies', + rel=self._get_rel('authz_policies'), + path_vars={}) + + # Perimeter route + self._add_resource( + mapper, intra_ext_controller, + path=self.PATH_PREFIX+'/intra_extensions/{intra_extensions_id}/subjects', + get_action='get_subjects', + post_action='add_subject', + rel=self._get_rel('subjects'), + path_vars={ + 'intra_extensions_id': self._get_path('intra_extensions'), + }) + self._add_resource( + mapper, intra_ext_controller, + path=self.PATH_PREFIX+'/intra_extensions/{intra_extensions_id}/subjects/{subject_id}', + delete_action='del_subject', + rel=self._get_rel('subjects'), + path_vars={ + 'intra_extensions_id': self._get_path('intra_extensions'), + }) + self._add_resource( + mapper, intra_ext_controller, + path=self.PATH_PREFIX+'/intra_extensions/{intra_extensions_id}/objects', + get_action='get_objects', + post_action='add_object', + rel=self._get_rel('subjects'), + path_vars={ + 'intra_extensions_id': self._get_path('intra_extensions'), + }) + self._add_resource( + mapper, intra_ext_controller, + path=self.PATH_PREFIX+'/intra_extensions/{intra_extensions_id}/objects/{object_id}', + delete_action='del_object', + rel=self._get_rel('objects'), + path_vars={ + 'intra_extensions_id': self._get_path('intra_extensions'), + }) + self._add_resource( + mapper, intra_ext_controller, + path=self.PATH_PREFIX+'/intra_extensions/{intra_extensions_id}/actions', + get_action='get_actions', + post_action='add_action', + rel=self._get_rel('actions'), + path_vars={ + 'intra_extensions_id': self._get_path('intra_extensions'), + }) + self._add_resource( + mapper, intra_ext_controller, + path=self.PATH_PREFIX+'/intra_extensions/{intra_extensions_id}/actions/{action_id}', + delete_action='del_action', + rel=self._get_rel('actions'), + path_vars={ + 'intra_extensions_id': self._get_path('intra_extensions'), + }) + + # Metadata route + self._add_resource( + mapper, intra_ext_controller, + path=self.PATH_PREFIX+'/intra_extensions/{intra_extensions_id}/subject_categories', + get_action='get_subject_categories', + post_action='add_subject_category', + rel=self._get_rel('subject_categories'), + path_vars={ + 'intra_extensions_id': self._get_path('intra_extensions'), + }) + self._add_resource( + mapper, intra_ext_controller, + path=self.PATH_PREFIX+'/intra_extensions/{intra_extensions_id}/subject_categories/{subject_category_id}', + delete_action='del_subject_category', + rel=self._get_rel('subject_categories'), + path_vars={ + 'intra_extensions_id': self._get_path('intra_extensions'), + }) + self._add_resource( + mapper, intra_ext_controller, + path=self.PATH_PREFIX+'/intra_extensions/{intra_extensions_id}/object_categories', + get_action='get_object_categories', + post_action='add_object_category', + rel=self._get_rel('object_categories'), + path_vars={ + 'intra_extensions_id': self._get_path('intra_extensions'), + }) + self._add_resource( + mapper, intra_ext_controller, + path=self.PATH_PREFIX+'/intra_extensions/{intra_extensions_id}/object_categories/{object_category_id}', + delete_action='del_object_category', + rel=self._get_rel('object_categories'), + path_vars={ + 'intra_extensions_id': self._get_path('intra_extensions'), + }) + self._add_resource( + mapper, intra_ext_controller, + path=self.PATH_PREFIX+'/intra_extensions/{intra_extensions_id}/action_categories', + get_action='get_action_categories', + post_action='add_action_category', + rel=self._get_rel('action_categories'), + path_vars={ + 'intra_extensions_id': self._get_path('intra_extensions'), + }) + self._add_resource( + mapper, intra_ext_controller, + path=self.PATH_PREFIX+'/intra_extensions/{intra_extensions_id}/action_categories/{action_category_id}', + delete_action='del_action_category', + rel=self._get_rel('action_categories'), + path_vars={ + 'intra_extensions_id': self._get_path('intra_extensions'), + }) + + # Scope route + self._add_resource( + mapper, intra_ext_controller, + path=self.PATH_PREFIX+'/intra_extensions/{intra_extensions_id}/subject_category_scope', + post_action='add_subject_category_scope', + rel=self._get_rel('subject_category_scope'), + path_vars={ + 'intra_extensions_id': self._get_path('intra_extensions'), + }) + self._add_resource( + mapper, intra_ext_controller, + path=self.PATH_PREFIX+'/intra_extensions/{intra_extensions_id}/subject_category_scope/{subject_category_id}', + get_action='get_subject_category_scope', + rel=self._get_rel('subject_category_scope'), + path_vars={ + 'intra_extensions_id': self._get_path('intra_extensions'), + }) + self._add_resource( + mapper, intra_ext_controller, + path=self.PATH_PREFIX+'/intra_extensions/{intra_extensions_id}/subject_category_scope/{subject_category_id}/{subject_category_scope_id}', + delete_action='del_subject_category_scope', + rel=self._get_rel('subject_category_scope'), + path_vars={ + 'intra_extensions_id': self._get_path('intra_extensions'), + }) + self._add_resource( + mapper, intra_ext_controller, + path=self.PATH_PREFIX+'/intra_extensions/{intra_extensions_id}/object_category_scope', + post_action='add_object_category_scope', + rel=self._get_rel('object_category_scope'), + path_vars={ + 'intra_extensions_id': self._get_path('intra_extensions'), + }) + self._add_resource( + mapper, intra_ext_controller, + path=self.PATH_PREFIX+'/intra_extensions/{intra_extensions_id}/object_category_scope/{object_category_id}', + get_action='get_object_category_scope', + rel=self._get_rel('object_category_scope'), + path_vars={ + 'intra_extensions_id': self._get_path('intra_extensions'), + }) + self._add_resource( + mapper, intra_ext_controller, + path=self.PATH_PREFIX+'/intra_extensions/{intra_extensions_id}/object_category_scope/{object_category_id}/{object_category_scope_id}', + delete_action='del_object_category_scope', + rel=self._get_rel('object_category_scope'), + path_vars={ + 'intra_extensions_id': self._get_path('intra_extensions'), + }) + self._add_resource( + mapper, intra_ext_controller, + path=self.PATH_PREFIX+'/intra_extensions/{intra_extensions_id}/action_category_scope', + post_action='add_action_category_scope', + rel=self._get_rel('action_category_scope'), + path_vars={ + 'intra_extensions_id': self._get_path('intra_extensions'), + }) + self._add_resource( + mapper, intra_ext_controller, + path=self.PATH_PREFIX+'/intra_extensions/{intra_extensions_id}/action_category_scope/{action_category_id}', + get_action='get_action_category_scope', + rel=self._get_rel('action_category_scope'), + path_vars={ + 'intra_extensions_id': self._get_path('intra_extensions'), + }) + self._add_resource( + mapper, intra_ext_controller, + path=self.PATH_PREFIX+'/intra_extensions/{intra_extensions_id}/action_category_scope/{action_category_id}/{action_category_scope_id}', + delete_action='del_action_category_scope', + rel=self._get_rel('action_category_scope'), + path_vars={ + 'intra_extensions_id': self._get_path('intra_extensions'), + }) + + # Assignment route + self._add_resource( + mapper, intra_ext_controller, + path=self.PATH_PREFIX+'/intra_extensions/{intra_extensions_id}/subject_assignments/{subject_id}', + get_action='get_subject_assignments', + rel=self._get_rel('subject_assignments'), + path_vars={ + 'intra_extensions_id': self._get_path('intra_extensions'), + }) + self._add_resource( + mapper, intra_ext_controller, + path=self.PATH_PREFIX+'/intra_extensions/{intra_extensions_id}/subject_assignments', + post_action='add_subject_assignment', + rel=self._get_rel('subject_assignments'), + path_vars={ + 'intra_extensions_id': self._get_path('intra_extensions'), + }) + self._add_resource( + mapper, intra_ext_controller, + path=self.PATH_PREFIX+'/intra_extensions/{intra_extensions_id}/' + 'subject_assignments/{subject_id}/{subject_category}/{subject_category_scope}', + delete_action='del_subject_assignment', + rel=self._get_rel('subject_assignments'), + path_vars={ + 'intra_extensions_id': self._get_path('intra_extensions'), + }) + self._add_resource( + mapper, intra_ext_controller, + path=self.PATH_PREFIX+'/intra_extensions/{intra_extensions_id}/object_assignments/{object_id}', + get_action='get_object_assignments', + rel=self._get_rel('object_assignments'), + path_vars={ + 'intra_extensions_id': self._get_path('intra_extensions'), + }) + self._add_resource( + mapper, intra_ext_controller, + path=self.PATH_PREFIX+'/intra_extensions/{intra_extensions_id}/object_assignments', + post_action='add_object_assignment', + rel=self._get_rel('object_assignments'), + path_vars={ + 'intra_extensions_id': self._get_path('intra_extensions'), + }) + self._add_resource( + mapper, intra_ext_controller, + path=self.PATH_PREFIX+'/intra_extensions/{intra_extensions_id}/' + 'object_assignments/{object_id}/{object_category}/{object_category_scope}', + delete_action='del_object_assignment', + rel=self._get_rel('object_assignments'), + path_vars={ + 'intra_extensions_id': self._get_path('intra_extensions'), + }) + self._add_resource( + mapper, intra_ext_controller, + path=self.PATH_PREFIX+'/intra_extensions/{intra_extensions_id}/action_assignments/{action_id}', + get_action='get_action_assignments', + rel=self._get_rel('action_assignments'), + path_vars={ + 'intra_extensions_id': self._get_path('intra_extensions'), + }) + self._add_resource( + mapper, intra_ext_controller, + path=self.PATH_PREFIX+'/intra_extensions/{intra_extensions_id}/action_assignments', + post_action='add_action_assignment', + rel=self._get_rel('action_assignments'), + path_vars={ + 'intra_extensions_id': self._get_path('intra_extensions'), + }) + self._add_resource( + mapper, intra_ext_controller, + path=self.PATH_PREFIX+'/intra_extensions/{intra_extensions_id}/' + 'action_assignments/{action_id}/{action_category}/{action_category_scope}', + delete_action='del_action_assignment', + rel=self._get_rel('action_assignments'), + path_vars={ + 'intra_extensions_id': self._get_path('intra_extensions'), + }) + + # Metarule route + self._add_resource( + mapper, intra_ext_controller, + path=self.PATH_PREFIX+'/intra_extensions/{intra_extensions_id}/aggregation_algorithms', + get_action='get_aggregation_algorithms', + rel=self._get_rel('aggregation_algorithms'), + path_vars={ + 'intra_extensions_id': self._get_path('intra_extensions'), + }) + + self._add_resource( + mapper, intra_ext_controller, + path=self.PATH_PREFIX+'/intra_extensions/{intra_extensions_id}/aggregation_algorithm', + get_action='get_aggregation_algorithm', + post_action='set_aggregation_algorithm', + rel=self._get_rel('aggregation_algorithms'), + path_vars={ + 'intra_extensions_id': self._get_path('intra_extensions'), + }) + + self._add_resource( + mapper, intra_ext_controller, + path=self.PATH_PREFIX+'/intra_extensions/{intra_extensions_id}/sub_meta_rule', + get_action='get_sub_meta_rule', + post_action='set_sub_meta_rule', + rel=self._get_rel('sub_meta_rule'), + path_vars={ + 'intra_extensions_id': self._get_path('intra_extensions'), + }) + + self._add_resource( + mapper, intra_ext_controller, + path=self.PATH_PREFIX+'/intra_extensions/{intra_extensions_id}/sub_meta_rule_relations', + get_action='get_sub_meta_rule_relations', + rel=self._get_rel('sub_meta_rule_relations'), + path_vars={ + 'intra_extensions_id': self._get_path('intra_extensions'), + }) + + # Rules route + self._add_resource( + mapper, intra_ext_controller, + path=self.PATH_PREFIX+'/intra_extensions/{intra_extensions_id}/sub_rules', + get_action='get_sub_rules', + post_action='set_sub_rule', + rel=self._get_rel('sub_rules'), + path_vars={ + 'intra_extensions_id': self._get_path('intra_extensions'), + }) + self._add_resource( + mapper, intra_ext_controller, + path=self.PATH_PREFIX+'/intra_extensions/{intra_extensions_id}/sub_rules/{relation_name}/{rule}', + delete_action='del_sub_rule', + rel=self._get_rel('sub_rules'), + path_vars={ + 'intra_extensions_id': self._get_path('intra_extensions'), + }) + + # Tenants route + self._add_resource( + mapper, tenants_controller, + path=self.PATH_PREFIX+'/tenants', + get_action='get_tenants', + rel=self._get_rel('tenants'), + path_vars={}) + self._add_resource( + mapper, tenants_controller, + path=self.PATH_PREFIX+'/tenant', + post_action='set_tenant', + rel=self._get_rel('tenants'), + path_vars={}) + self._add_resource( + mapper, tenants_controller, + path=self.PATH_PREFIX+'/tenant/{tenant_uuid}', + get_action='get_tenant', + delete_action='delete_tenant', + rel=self._get_rel('tenants'), + path_vars={ + 'tenant_uuid': self._get_path('tenants'), + }) + + # Logs route + self._add_resource( + mapper, logs_controller, + path=self.PATH_PREFIX+'/logs', + get_action='get_logs', + rel=self._get_rel('logs'), + path_vars={ + }) + self._add_resource( + mapper, logs_controller, + path=self.PATH_PREFIX+'/logs/{options}', + get_action='get_logs', + rel=self._get_rel('logs'), + path_vars={ + }) + + # InterExtensions route + # self._add_resource( + # mapper, inter_ext_controller, + # path=self.PATH_PREFIX+'/inter_extensions', + # get_action='get_inter_extensions', + # post_action='create_inter_extension', + # rel=self._get_rel('inter_extensions'), + # path_vars={}) + # self._add_resource( + # mapper, inter_ext_controller, + # path=self.PATH_PREFIX+'/inter_extensions/{inter_extensions_id}', + # get_action='get_inter_extension', + # delete_action='delete_inter_extension', + # rel=self._get_rel('inter_extensions'), + # path_vars={ + # 'inter_extensions_id': self._get_path('inter_extensions'), + # }) diff --git a/keystone-moon/keystone/contrib/oauth1/__init__.py b/keystone-moon/keystone/contrib/oauth1/__init__.py new file mode 100644 index 00000000..8cab2498 --- /dev/null +++ b/keystone-moon/keystone/contrib/oauth1/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 keystone.contrib.oauth1.core import * # noqa diff --git a/keystone-moon/keystone/contrib/oauth1/backends/__init__.py b/keystone-moon/keystone/contrib/oauth1/backends/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/contrib/oauth1/backends/sql.py b/keystone-moon/keystone/contrib/oauth1/backends/sql.py new file mode 100644 index 00000000..c6ab6e5a --- /dev/null +++ b/keystone-moon/keystone/contrib/oauth1/backends/sql.py @@ -0,0 +1,272 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 datetime +import random as _random +import uuid + +from oslo_serialization import jsonutils +from oslo_utils import timeutils +import six + +from keystone.common import sql +from keystone.contrib.oauth1 import core +from keystone import exception +from keystone.i18n import _ + + +random = _random.SystemRandom() + + +class Consumer(sql.ModelBase, sql.DictBase): + __tablename__ = 'consumer' + attributes = ['id', 'description', 'secret'] + id = sql.Column(sql.String(64), primary_key=True, nullable=False) + description = sql.Column(sql.String(64), nullable=True) + secret = sql.Column(sql.String(64), nullable=False) + extra = sql.Column(sql.JsonBlob(), nullable=False) + + +class RequestToken(sql.ModelBase, sql.DictBase): + __tablename__ = 'request_token' + attributes = ['id', 'request_secret', + 'verifier', 'authorizing_user_id', 'requested_project_id', + 'role_ids', 'consumer_id', 'expires_at'] + id = sql.Column(sql.String(64), primary_key=True, nullable=False) + request_secret = sql.Column(sql.String(64), nullable=False) + verifier = sql.Column(sql.String(64), nullable=True) + authorizing_user_id = sql.Column(sql.String(64), nullable=True) + requested_project_id = sql.Column(sql.String(64), nullable=False) + role_ids = sql.Column(sql.Text(), nullable=True) + consumer_id = sql.Column(sql.String(64), sql.ForeignKey('consumer.id'), + nullable=False, index=True) + expires_at = sql.Column(sql.String(64), nullable=True) + + @classmethod + def from_dict(cls, user_dict): + return cls(**user_dict) + + def to_dict(self): + return dict(six.iteritems(self)) + + +class AccessToken(sql.ModelBase, sql.DictBase): + __tablename__ = 'access_token' + attributes = ['id', 'access_secret', 'authorizing_user_id', + 'project_id', 'role_ids', 'consumer_id', + 'expires_at'] + id = sql.Column(sql.String(64), primary_key=True, nullable=False) + access_secret = sql.Column(sql.String(64), nullable=False) + authorizing_user_id = sql.Column(sql.String(64), nullable=False, + index=True) + project_id = sql.Column(sql.String(64), nullable=False) + role_ids = sql.Column(sql.Text(), nullable=False) + consumer_id = sql.Column(sql.String(64), sql.ForeignKey('consumer.id'), + nullable=False) + expires_at = sql.Column(sql.String(64), nullable=True) + + @classmethod + def from_dict(cls, user_dict): + return cls(**user_dict) + + def to_dict(self): + return dict(six.iteritems(self)) + + +class OAuth1(object): + def _get_consumer(self, session, consumer_id): + consumer_ref = session.query(Consumer).get(consumer_id) + if consumer_ref is None: + raise exception.NotFound(_('Consumer not found')) + return consumer_ref + + def get_consumer_with_secret(self, consumer_id): + session = sql.get_session() + consumer_ref = self._get_consumer(session, consumer_id) + return consumer_ref.to_dict() + + def get_consumer(self, consumer_id): + return core.filter_consumer( + self.get_consumer_with_secret(consumer_id)) + + def create_consumer(self, consumer): + consumer['secret'] = uuid.uuid4().hex + if not consumer.get('description'): + consumer['description'] = None + session = sql.get_session() + with session.begin(): + consumer_ref = Consumer.from_dict(consumer) + session.add(consumer_ref) + return consumer_ref.to_dict() + + def _delete_consumer(self, session, consumer_id): + consumer_ref = self._get_consumer(session, consumer_id) + session.delete(consumer_ref) + + def _delete_request_tokens(self, session, consumer_id): + q = session.query(RequestToken) + req_tokens = q.filter_by(consumer_id=consumer_id) + req_tokens_list = set([x.id for x in req_tokens]) + for token_id in req_tokens_list: + token_ref = self._get_request_token(session, token_id) + session.delete(token_ref) + + def _delete_access_tokens(self, session, consumer_id): + q = session.query(AccessToken) + acc_tokens = q.filter_by(consumer_id=consumer_id) + acc_tokens_list = set([x.id for x in acc_tokens]) + for token_id in acc_tokens_list: + token_ref = self._get_access_token(session, token_id) + session.delete(token_ref) + + def delete_consumer(self, consumer_id): + session = sql.get_session() + with session.begin(): + self._delete_request_tokens(session, consumer_id) + self._delete_access_tokens(session, consumer_id) + self._delete_consumer(session, consumer_id) + + def list_consumers(self): + session = sql.get_session() + cons = session.query(Consumer) + return [core.filter_consumer(x.to_dict()) for x in cons] + + def update_consumer(self, consumer_id, consumer): + session = sql.get_session() + with session.begin(): + consumer_ref = self._get_consumer(session, consumer_id) + old_consumer_dict = consumer_ref.to_dict() + old_consumer_dict.update(consumer) + new_consumer = Consumer.from_dict(old_consumer_dict) + consumer_ref.description = new_consumer.description + consumer_ref.extra = new_consumer.extra + return core.filter_consumer(consumer_ref.to_dict()) + + def create_request_token(self, consumer_id, project_id, token_duration, + request_token_id=None, request_token_secret=None): + if request_token_id is None: + request_token_id = uuid.uuid4().hex + if request_token_secret is None: + request_token_secret = uuid.uuid4().hex + expiry_date = None + if token_duration: + now = timeutils.utcnow() + future = now + datetime.timedelta(seconds=token_duration) + expiry_date = timeutils.isotime(future, subsecond=True) + + ref = {} + ref['id'] = request_token_id + ref['request_secret'] = request_token_secret + ref['verifier'] = None + ref['authorizing_user_id'] = None + ref['requested_project_id'] = project_id + ref['role_ids'] = None + ref['consumer_id'] = consumer_id + ref['expires_at'] = expiry_date + session = sql.get_session() + with session.begin(): + token_ref = RequestToken.from_dict(ref) + session.add(token_ref) + return token_ref.to_dict() + + def _get_request_token(self, session, request_token_id): + token_ref = session.query(RequestToken).get(request_token_id) + if token_ref is None: + raise exception.NotFound(_('Request token not found')) + return token_ref + + def get_request_token(self, request_token_id): + session = sql.get_session() + token_ref = self._get_request_token(session, request_token_id) + return token_ref.to_dict() + + def authorize_request_token(self, request_token_id, user_id, + role_ids): + session = sql.get_session() + with session.begin(): + token_ref = self._get_request_token(session, request_token_id) + token_dict = token_ref.to_dict() + token_dict['authorizing_user_id'] = user_id + token_dict['verifier'] = ''.join(random.sample(core.VERIFIER_CHARS, + 8)) + token_dict['role_ids'] = jsonutils.dumps(role_ids) + + new_token = RequestToken.from_dict(token_dict) + for attr in RequestToken.attributes: + if (attr == 'authorizing_user_id' or attr == 'verifier' + or attr == 'role_ids'): + setattr(token_ref, attr, getattr(new_token, attr)) + + return token_ref.to_dict() + + def create_access_token(self, request_token_id, token_duration, + access_token_id=None, access_token_secret=None): + if access_token_id is None: + access_token_id = uuid.uuid4().hex + if access_token_secret is None: + access_token_secret = uuid.uuid4().hex + session = sql.get_session() + with session.begin(): + req_token_ref = self._get_request_token(session, request_token_id) + token_dict = req_token_ref.to_dict() + + expiry_date = None + if token_duration: + now = timeutils.utcnow() + future = now + datetime.timedelta(seconds=token_duration) + expiry_date = timeutils.isotime(future, subsecond=True) + + # add Access Token + ref = {} + ref['id'] = access_token_id + ref['access_secret'] = access_token_secret + ref['authorizing_user_id'] = token_dict['authorizing_user_id'] + ref['project_id'] = token_dict['requested_project_id'] + ref['role_ids'] = token_dict['role_ids'] + ref['consumer_id'] = token_dict['consumer_id'] + ref['expires_at'] = expiry_date + token_ref = AccessToken.from_dict(ref) + session.add(token_ref) + + # remove request token, it's been used + session.delete(req_token_ref) + + return token_ref.to_dict() + + def _get_access_token(self, session, access_token_id): + token_ref = session.query(AccessToken).get(access_token_id) + if token_ref is None: + raise exception.NotFound(_('Access token not found')) + return token_ref + + def get_access_token(self, access_token_id): + session = sql.get_session() + token_ref = self._get_access_token(session, access_token_id) + return token_ref.to_dict() + + def list_access_tokens(self, user_id): + session = sql.get_session() + q = session.query(AccessToken) + user_auths = q.filter_by(authorizing_user_id=user_id) + return [core.filter_token(x.to_dict()) for x in user_auths] + + def delete_access_token(self, user_id, access_token_id): + session = sql.get_session() + with session.begin(): + token_ref = self._get_access_token(session, access_token_id) + token_dict = token_ref.to_dict() + if token_dict['authorizing_user_id'] != user_id: + raise exception.Unauthorized(_('User IDs do not match')) + + session.delete(token_ref) diff --git a/keystone-moon/keystone/contrib/oauth1/controllers.py b/keystone-moon/keystone/contrib/oauth1/controllers.py new file mode 100644 index 00000000..fb5d0bc2 --- /dev/null +++ b/keystone-moon/keystone/contrib/oauth1/controllers.py @@ -0,0 +1,417 @@ +# Copyright 2013 OpenStack Foundation +# +# 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. + +"""Extensions supporting OAuth1.""" + +from oslo_config import cfg +from oslo_serialization import jsonutils +from oslo_utils import timeutils + +from keystone.common import controller +from keystone.common import dependency +from keystone.common import wsgi +from keystone.contrib.oauth1 import core as oauth1 +from keystone.contrib.oauth1 import validator +from keystone import exception +from keystone.i18n import _ +from keystone.models import token_model +from keystone import notifications + + +CONF = cfg.CONF + + +@notifications.internal(notifications.INVALIDATE_USER_OAUTH_CONSUMER_TOKENS, + resource_id_arg_index=0) +def _emit_user_oauth_consumer_token_invalidate(payload): + # This is a special case notification that expect the payload to be a dict + # containing the user_id and the consumer_id. This is so that the token + # provider can invalidate any tokens in the token persistence if + # token persistence is enabled + pass + + +@dependency.requires('oauth_api', 'token_provider_api') +class ConsumerCrudV3(controller.V3Controller): + collection_name = 'consumers' + member_name = 'consumer' + + @classmethod + def base_url(cls, context, path=None): + """Construct a path and pass it to V3Controller.base_url method.""" + + # NOTE(stevemar): Overriding path to /OS-OAUTH1/consumers so that + # V3Controller.base_url handles setting the self link correctly. + path = '/OS-OAUTH1/' + cls.collection_name + return controller.V3Controller.base_url(context, path=path) + + @controller.protected() + def create_consumer(self, context, consumer): + ref = self._assign_unique_id(self._normalize_dict(consumer)) + initiator = notifications._get_request_audit_info(context) + consumer_ref = self.oauth_api.create_consumer(ref, initiator) + return ConsumerCrudV3.wrap_member(context, consumer_ref) + + @controller.protected() + def update_consumer(self, context, consumer_id, consumer): + self._require_matching_id(consumer_id, consumer) + ref = self._normalize_dict(consumer) + self._validate_consumer_ref(ref) + initiator = notifications._get_request_audit_info(context) + ref = self.oauth_api.update_consumer(consumer_id, ref, initiator) + return ConsumerCrudV3.wrap_member(context, ref) + + @controller.protected() + def list_consumers(self, context): + ref = self.oauth_api.list_consumers() + return ConsumerCrudV3.wrap_collection(context, ref) + + @controller.protected() + def get_consumer(self, context, consumer_id): + ref = self.oauth_api.get_consumer(consumer_id) + return ConsumerCrudV3.wrap_member(context, ref) + + @controller.protected() + def delete_consumer(self, context, consumer_id): + user_token_ref = token_model.KeystoneToken( + token_id=context['token_id'], + token_data=self.token_provider_api.validate_token( + context['token_id'])) + payload = {'user_id': user_token_ref.user_id, + 'consumer_id': consumer_id} + _emit_user_oauth_consumer_token_invalidate(payload) + initiator = notifications._get_request_audit_info(context) + self.oauth_api.delete_consumer(consumer_id, initiator) + + def _validate_consumer_ref(self, consumer): + if 'secret' in consumer: + msg = _('Cannot change consumer secret') + raise exception.ValidationError(message=msg) + + +@dependency.requires('oauth_api') +class AccessTokenCrudV3(controller.V3Controller): + collection_name = 'access_tokens' + member_name = 'access_token' + + @classmethod + def _add_self_referential_link(cls, context, ref): + # NOTE(lwolf): overriding method to add proper path to self link + ref.setdefault('links', {}) + path = '/users/%(user_id)s/OS-OAUTH1/access_tokens' % { + 'user_id': cls._get_user_id(ref) + } + ref['links']['self'] = cls.base_url(context, path) + '/' + ref['id'] + + @controller.protected() + def get_access_token(self, context, user_id, access_token_id): + access_token = self.oauth_api.get_access_token(access_token_id) + if access_token['authorizing_user_id'] != user_id: + raise exception.NotFound() + access_token = self._format_token_entity(context, access_token) + return AccessTokenCrudV3.wrap_member(context, access_token) + + @controller.protected() + def list_access_tokens(self, context, user_id): + auth_context = context.get('environment', + {}).get('KEYSTONE_AUTH_CONTEXT', {}) + if auth_context.get('is_delegated_auth'): + raise exception.Forbidden( + _('Cannot list request tokens' + ' with a token issued via delegation.')) + refs = self.oauth_api.list_access_tokens(user_id) + formatted_refs = ([self._format_token_entity(context, x) + for x in refs]) + return AccessTokenCrudV3.wrap_collection(context, formatted_refs) + + @controller.protected() + def delete_access_token(self, context, user_id, access_token_id): + access_token = self.oauth_api.get_access_token(access_token_id) + consumer_id = access_token['consumer_id'] + payload = {'user_id': user_id, 'consumer_id': consumer_id} + _emit_user_oauth_consumer_token_invalidate(payload) + initiator = notifications._get_request_audit_info(context) + return self.oauth_api.delete_access_token( + user_id, access_token_id, initiator) + + @staticmethod + def _get_user_id(entity): + return entity.get('authorizing_user_id', '') + + def _format_token_entity(self, context, entity): + + formatted_entity = entity.copy() + access_token_id = formatted_entity['id'] + user_id = self._get_user_id(formatted_entity) + if 'role_ids' in entity: + formatted_entity.pop('role_ids') + if 'access_secret' in entity: + formatted_entity.pop('access_secret') + + url = ('/users/%(user_id)s/OS-OAUTH1/access_tokens/%(access_token_id)s' + '/roles' % {'user_id': user_id, + 'access_token_id': access_token_id}) + + formatted_entity.setdefault('links', {}) + formatted_entity['links']['roles'] = (self.base_url(context, url)) + + return formatted_entity + + +@dependency.requires('oauth_api', 'role_api') +class AccessTokenRolesV3(controller.V3Controller): + collection_name = 'roles' + member_name = 'role' + + @controller.protected() + def list_access_token_roles(self, context, user_id, access_token_id): + access_token = self.oauth_api.get_access_token(access_token_id) + if access_token['authorizing_user_id'] != user_id: + raise exception.NotFound() + authed_role_ids = access_token['role_ids'] + authed_role_ids = jsonutils.loads(authed_role_ids) + refs = ([self._format_role_entity(x) for x in authed_role_ids]) + return AccessTokenRolesV3.wrap_collection(context, refs) + + @controller.protected() + def get_access_token_role(self, context, user_id, + access_token_id, role_id): + access_token = self.oauth_api.get_access_token(access_token_id) + if access_token['authorizing_user_id'] != user_id: + raise exception.Unauthorized(_('User IDs do not match')) + authed_role_ids = access_token['role_ids'] + authed_role_ids = jsonutils.loads(authed_role_ids) + for authed_role_id in authed_role_ids: + if authed_role_id == role_id: + role = self._format_role_entity(role_id) + return AccessTokenRolesV3.wrap_member(context, role) + raise exception.RoleNotFound(_('Could not find role')) + + def _format_role_entity(self, role_id): + role = self.role_api.get_role(role_id) + formatted_entity = role.copy() + if 'description' in role: + formatted_entity.pop('description') + if 'enabled' in role: + formatted_entity.pop('enabled') + return formatted_entity + + +@dependency.requires('assignment_api', 'oauth_api', + 'resource_api', 'token_provider_api') +class OAuthControllerV3(controller.V3Controller): + collection_name = 'not_used' + member_name = 'not_used' + + def create_request_token(self, context): + headers = context['headers'] + oauth_headers = oauth1.get_oauth_headers(headers) + consumer_id = oauth_headers.get('oauth_consumer_key') + requested_project_id = headers.get('Requested-Project-Id') + + if not consumer_id: + raise exception.ValidationError( + attribute='oauth_consumer_key', target='request') + if not requested_project_id: + raise exception.ValidationError( + attribute='requested_project_id', target='request') + + # NOTE(stevemar): Ensure consumer and requested project exist + self.resource_api.get_project(requested_project_id) + self.oauth_api.get_consumer(consumer_id) + + url = self.base_url(context, context['path']) + + req_headers = {'Requested-Project-Id': requested_project_id} + req_headers.update(headers) + request_verifier = oauth1.RequestTokenEndpoint( + request_validator=validator.OAuthValidator(), + token_generator=oauth1.token_generator) + h, b, s = request_verifier.create_request_token_response( + url, + http_method='POST', + body=context['query_string'], + headers=req_headers) + + if (not b) or int(s) > 399: + msg = _('Invalid signature') + raise exception.Unauthorized(message=msg) + + request_token_duration = CONF.oauth1.request_token_duration + initiator = notifications._get_request_audit_info(context) + token_ref = self.oauth_api.create_request_token(consumer_id, + requested_project_id, + request_token_duration, + initiator) + + result = ('oauth_token=%(key)s&oauth_token_secret=%(secret)s' + % {'key': token_ref['id'], + 'secret': token_ref['request_secret']}) + + if CONF.oauth1.request_token_duration: + expiry_bit = '&oauth_expires_at=%s' % token_ref['expires_at'] + result += expiry_bit + + headers = [('Content-Type', 'application/x-www-urlformencoded')] + response = wsgi.render_response(result, + status=(201, 'Created'), + headers=headers) + + return response + + def create_access_token(self, context): + headers = context['headers'] + oauth_headers = oauth1.get_oauth_headers(headers) + consumer_id = oauth_headers.get('oauth_consumer_key') + request_token_id = oauth_headers.get('oauth_token') + oauth_verifier = oauth_headers.get('oauth_verifier') + + if not consumer_id: + raise exception.ValidationError( + attribute='oauth_consumer_key', target='request') + if not request_token_id: + raise exception.ValidationError( + attribute='oauth_token', target='request') + if not oauth_verifier: + raise exception.ValidationError( + attribute='oauth_verifier', target='request') + + req_token = self.oauth_api.get_request_token( + request_token_id) + + expires_at = req_token['expires_at'] + if expires_at: + now = timeutils.utcnow() + expires = timeutils.normalize_time( + timeutils.parse_isotime(expires_at)) + if now > expires: + raise exception.Unauthorized(_('Request token is expired')) + + url = self.base_url(context, context['path']) + + access_verifier = oauth1.AccessTokenEndpoint( + request_validator=validator.OAuthValidator(), + token_generator=oauth1.token_generator) + h, b, s = access_verifier.create_access_token_response( + url, + http_method='POST', + body=context['query_string'], + headers=headers) + params = oauth1.extract_non_oauth_params(b) + if len(params) != 0: + msg = _('There should not be any non-oauth parameters') + raise exception.Unauthorized(message=msg) + + if req_token['consumer_id'] != consumer_id: + msg = _('provided consumer key does not match stored consumer key') + raise exception.Unauthorized(message=msg) + + if req_token['verifier'] != oauth_verifier: + msg = _('provided verifier does not match stored verifier') + raise exception.Unauthorized(message=msg) + + if req_token['id'] != request_token_id: + msg = _('provided request key does not match stored request key') + raise exception.Unauthorized(message=msg) + + if not req_token.get('authorizing_user_id'): + msg = _('Request Token does not have an authorizing user id') + raise exception.Unauthorized(message=msg) + + access_token_duration = CONF.oauth1.access_token_duration + initiator = notifications._get_request_audit_info(context) + token_ref = self.oauth_api.create_access_token(request_token_id, + access_token_duration, + initiator) + + result = ('oauth_token=%(key)s&oauth_token_secret=%(secret)s' + % {'key': token_ref['id'], + 'secret': token_ref['access_secret']}) + + if CONF.oauth1.access_token_duration: + expiry_bit = '&oauth_expires_at=%s' % (token_ref['expires_at']) + result += expiry_bit + + headers = [('Content-Type', 'application/x-www-urlformencoded')] + response = wsgi.render_response(result, + status=(201, 'Created'), + headers=headers) + + return response + + @controller.protected() + def authorize_request_token(self, context, request_token_id, roles): + """An authenticated user is going to authorize a request token. + + As a security precaution, the requested roles must match those in + the request token. Because this is in a CLI-only world at the moment, + there is not another easy way to make sure the user knows which roles + are being requested before authorizing. + """ + auth_context = context.get('environment', + {}).get('KEYSTONE_AUTH_CONTEXT', {}) + if auth_context.get('is_delegated_auth'): + raise exception.Forbidden( + _('Cannot authorize a request token' + ' with a token issued via delegation.')) + + req_token = self.oauth_api.get_request_token(request_token_id) + + expires_at = req_token['expires_at'] + if expires_at: + now = timeutils.utcnow() + expires = timeutils.normalize_time( + timeutils.parse_isotime(expires_at)) + if now > expires: + raise exception.Unauthorized(_('Request token is expired')) + + # put the roles in a set for easy comparison + authed_roles = set() + for role in roles: + authed_roles.add(role['id']) + + # verify the authorizing user has the roles + user_token = token_model.KeystoneToken( + token_id=context['token_id'], + token_data=self.token_provider_api.validate_token( + context['token_id'])) + user_id = user_token.user_id + project_id = req_token['requested_project_id'] + user_roles = self.assignment_api.get_roles_for_user_and_project( + user_id, project_id) + cred_set = set(user_roles) + + if not cred_set.issuperset(authed_roles): + msg = _('authorizing user does not have role required') + raise exception.Unauthorized(message=msg) + + # create list of just the id's for the backend + role_list = list(authed_roles) + + # verify the user has the project too + req_project_id = req_token['requested_project_id'] + user_projects = self.assignment_api.list_projects_for_user(user_id) + for user_project in user_projects: + if user_project['id'] == req_project_id: + break + else: + msg = _("User is not a member of the requested project") + raise exception.Unauthorized(message=msg) + + # finally authorize the token + authed_token = self.oauth_api.authorize_request_token( + request_token_id, user_id, role_list) + + to_return = {'token': {'oauth_verifier': authed_token['verifier']}} + return to_return diff --git a/keystone-moon/keystone/contrib/oauth1/core.py b/keystone-moon/keystone/contrib/oauth1/core.py new file mode 100644 index 00000000..eeb3e114 --- /dev/null +++ b/keystone-moon/keystone/contrib/oauth1/core.py @@ -0,0 +1,361 @@ +# Copyright 2013 OpenStack Foundation +# +# 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. + +"""Extensions supporting OAuth1.""" + +from __future__ import absolute_import + +import abc +import string +import uuid + +import oauthlib.common +from oauthlib import oauth1 +from oslo_config import cfg +from oslo_log import log +import six + +from keystone.common import dependency +from keystone.common import extension +from keystone.common import manager +from keystone import exception +from keystone.i18n import _LE +from keystone import notifications + + +RequestValidator = oauth1.RequestValidator +Client = oauth1.Client +AccessTokenEndpoint = oauth1.AccessTokenEndpoint +ResourceEndpoint = oauth1.ResourceEndpoint +AuthorizationEndpoint = oauth1.AuthorizationEndpoint +SIG_HMAC = oauth1.SIGNATURE_HMAC +RequestTokenEndpoint = oauth1.RequestTokenEndpoint +oRequest = oauthlib.common.Request +# The characters used to generate verifiers are limited to alphanumerical +# values for ease of manual entry. Commonly confused characters are omitted. +VERIFIER_CHARS = string.ascii_letters + string.digits +CONFUSED_CHARS = 'jiIl1oO0' +VERIFIER_CHARS = ''.join(c for c in VERIFIER_CHARS if c not in CONFUSED_CHARS) + + +class Token(object): + def __init__(self, key, secret): + self.key = key + self.secret = secret + self.verifier = None + + def set_verifier(self, verifier): + self.verifier = verifier + + +CONF = cfg.CONF +LOG = log.getLogger(__name__) + + +def token_generator(*args, **kwargs): + return uuid.uuid4().hex + + +EXTENSION_DATA = { + 'name': 'OpenStack OAUTH1 API', + 'namespace': 'http://docs.openstack.org/identity/api/ext/' + 'OS-OAUTH1/v1.0', + 'alias': 'OS-OAUTH1', + 'updated': '2013-07-07T12:00:0-00:00', + 'description': 'OpenStack OAuth 1.0a Delegated Auth Mechanism.', + 'links': [ + { + 'rel': 'describedby', + # TODO(dolph): link needs to be revised after + # bug 928059 merges + 'type': 'text/html', + 'href': 'https://github.com/openstack/identity-api', + } + ]} +extension.register_admin_extension(EXTENSION_DATA['alias'], EXTENSION_DATA) +extension.register_public_extension(EXTENSION_DATA['alias'], EXTENSION_DATA) + + +def filter_consumer(consumer_ref): + """Filter out private items in a consumer dict. + + 'secret' is never returned. + + :returns: consumer_ref + + """ + if consumer_ref: + consumer_ref = consumer_ref.copy() + consumer_ref.pop('secret', None) + return consumer_ref + + +def filter_token(access_token_ref): + """Filter out private items in an access token dict. + + 'access_secret' is never returned. + + :returns: access_token_ref + + """ + if access_token_ref: + access_token_ref = access_token_ref.copy() + access_token_ref.pop('access_secret', None) + return access_token_ref + + +def get_oauth_headers(headers): + parameters = {} + + # The incoming headers variable is your usual heading from context + # In an OAuth signed req, where the oauth variables are in the header, + # they with the key 'Authorization'. + + if headers and 'Authorization' in headers: + # A typical value for Authorization is seen below + # 'OAuth realm="", oauth_body_hash="2jm%3D", oauth_nonce="14475435" + # along with other oauth variables, the 'OAuth ' part is trimmed + # to split the rest of the headers. + + auth_header = headers['Authorization'] + params = oauth1.rfc5849.utils.parse_authorization_header(auth_header) + parameters.update(dict(params)) + return parameters + else: + msg = _LE('Cannot retrieve Authorization headers') + LOG.error(msg) + raise exception.OAuthHeadersMissingError() + + +def extract_non_oauth_params(query_string): + params = oauthlib.common.extract_params(query_string) + return {k: v for k, v in params if not k.startswith('oauth_')} + + +@dependency.provider('oauth_api') +class Manager(manager.Manager): + """Default pivot point for the OAuth1 backend. + + See :mod:`keystone.common.manager.Manager` for more details on how this + dynamically calls the backend. + + """ + _ACCESS_TOKEN = "OS-OAUTH1:access_token" + _REQUEST_TOKEN = "OS-OAUTH1:request_token" + _CONSUMER = "OS-OAUTH1:consumer" + + def __init__(self): + super(Manager, self).__init__(CONF.oauth1.driver) + + def create_consumer(self, consumer_ref, initiator=None): + ret = self.driver.create_consumer(consumer_ref) + notifications.Audit.created(self._CONSUMER, ret['id'], initiator) + return ret + + def update_consumer(self, consumer_id, consumer_ref, initiator=None): + ret = self.driver.update_consumer(consumer_id, consumer_ref) + notifications.Audit.updated(self._CONSUMER, consumer_id, initiator) + return ret + + def delete_consumer(self, consumer_id, initiator=None): + ret = self.driver.delete_consumer(consumer_id) + notifications.Audit.deleted(self._CONSUMER, consumer_id, initiator) + return ret + + def create_access_token(self, request_id, access_token_duration, + initiator=None): + ret = self.driver.create_access_token(request_id, + access_token_duration) + notifications.Audit.created(self._ACCESS_TOKEN, ret['id'], initiator) + return ret + + def delete_access_token(self, user_id, access_token_id, initiator=None): + ret = self.driver.delete_access_token(user_id, access_token_id) + notifications.Audit.deleted(self._ACCESS_TOKEN, access_token_id, + initiator) + return ret + + def create_request_token(self, consumer_id, requested_project, + request_token_duration, initiator=None): + ret = self.driver.create_request_token( + consumer_id, requested_project, request_token_duration) + notifications.Audit.created(self._REQUEST_TOKEN, ret['id'], + initiator) + return ret + + +@six.add_metaclass(abc.ABCMeta) +class Driver(object): + """Interface description for an OAuth1 driver.""" + + @abc.abstractmethod + def create_consumer(self, consumer_ref): + """Create consumer. + + :param consumer_ref: consumer ref with consumer name + :type consumer_ref: dict + :returns: consumer_ref + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def update_consumer(self, consumer_id, consumer_ref): + """Update consumer. + + :param consumer_id: id of consumer to update + :type consumer_id: string + :param consumer_ref: new consumer ref with consumer name + :type consumer_ref: dict + :returns: consumer_ref + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_consumers(self): + """List consumers. + + :returns: list of consumers + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def get_consumer(self, consumer_id): + """Get consumer, returns the consumer id (key) + and description. + + :param consumer_id: id of consumer to get + :type consumer_id: string + :returns: consumer_ref + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def get_consumer_with_secret(self, consumer_id): + """Like get_consumer() but returned consumer_ref includes + the consumer secret. + + Secrets should only be shared upon consumer creation; the + consumer secret is required to verify incoming OAuth requests. + + :param consumer_id: id of consumer to get + :type consumer_id: string + :returns: consumer_ref + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def delete_consumer(self, consumer_id): + """Delete consumer. + + :param consumer_id: id of consumer to get + :type consumer_id: string + :returns: None. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_access_tokens(self, user_id): + """List access tokens. + + :param user_id: search for access tokens authorized by given user id + :type user_id: string + :returns: list of access tokens the user has authorized + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def delete_access_token(self, user_id, access_token_id): + """Delete access token. + + :param user_id: authorizing user id + :type user_id: string + :param access_token_id: access token to delete + :type access_token_id: string + :returns: None + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def create_request_token(self, consumer_id, requested_project, + request_token_duration): + """Create request token. + + :param consumer_id: the id of the consumer + :type consumer_id: string + :param requested_project_id: requested project id + :type requested_project_id: string + :param request_token_duration: duration of request token + :type request_token_duration: string + :returns: request_token_ref + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def get_request_token(self, request_token_id): + """Get request token. + + :param request_token_id: the id of the request token + :type request_token_id: string + :returns: request_token_ref + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def get_access_token(self, access_token_id): + """Get access token. + + :param access_token_id: the id of the access token + :type access_token_id: string + :returns: access_token_ref + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def authorize_request_token(self, request_id, user_id, role_ids): + """Authorize request token. + + :param request_id: the id of the request token, to be authorized + :type request_id: string + :param user_id: the id of the authorizing user + :type user_id: string + :param role_ids: list of role ids to authorize + :type role_ids: list + :returns: verifier + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def create_access_token(self, request_id, access_token_duration): + """Create access token. + + :param request_id: the id of the request token, to be deleted + :type request_id: string + :param access_token_duration: duration of an access token + :type access_token_duration: string + :returns: access_token_ref + + """ + raise exception.NotImplemented() # pragma: no cover diff --git a/keystone-moon/keystone/contrib/oauth1/migrate_repo/__init__.py b/keystone-moon/keystone/contrib/oauth1/migrate_repo/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/contrib/oauth1/migrate_repo/migrate.cfg b/keystone-moon/keystone/contrib/oauth1/migrate_repo/migrate.cfg new file mode 100644 index 00000000..97ca7810 --- /dev/null +++ b/keystone-moon/keystone/contrib/oauth1/migrate_repo/migrate.cfg @@ -0,0 +1,25 @@ +[db_settings] +# Used to identify which repository this database is versioned under. +# You can use the name of your project. +repository_id=oauth1 + +# The name of the database table used to track the schema version. +# This name shouldn't already be used by your project. +# If this is changed once a database is under version control, you'll need to +# change the table name in each database too. +version_table=migrate_version + +# When committing a change script, Migrate will attempt to generate the +# sql for all supported databases; normally, if one of them fails - probably +# because you don't have that database installed - it is ignored and the +# commit continues, perhaps ending successfully. +# Databases in this list MUST compile successfully during a commit, or the +# entire commit will fail. List the databases your application will actually +# be using to ensure your updates to that database work properly. +# This must be a list; example: ['postgres','sqlite'] +required_dbs=[] + +# When creating new change scripts, Migrate will stamp the new script with +# a version number. By default this is latest_version + 1. You can set this +# to 'true' to tell Migrate to use the UTC timestamp instead. +use_timestamp_numbering=False diff --git a/keystone-moon/keystone/contrib/oauth1/migrate_repo/versions/001_add_oauth_tables.py b/keystone-moon/keystone/contrib/oauth1/migrate_repo/versions/001_add_oauth_tables.py new file mode 100644 index 00000000..a4fbf155 --- /dev/null +++ b/keystone-moon/keystone/contrib/oauth1/migrate_repo/versions/001_add_oauth_tables.py @@ -0,0 +1,67 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 sqlalchemy as sql + + +def upgrade(migrate_engine): + # Upgrade operations go here. Don't create your own engine; bind + # migrate_engine to your metadata + meta = sql.MetaData() + meta.bind = migrate_engine + + consumer_table = sql.Table( + 'consumer', + meta, + sql.Column('id', sql.String(64), primary_key=True, nullable=False), + sql.Column('description', sql.String(64), nullable=False), + sql.Column('secret', sql.String(64), nullable=False), + sql.Column('extra', sql.Text(), nullable=False)) + consumer_table.create(migrate_engine, checkfirst=True) + + request_token_table = sql.Table( + 'request_token', + meta, + sql.Column('id', sql.String(64), primary_key=True, nullable=False), + sql.Column('request_secret', sql.String(64), nullable=False), + sql.Column('verifier', sql.String(64), nullable=True), + sql.Column('authorizing_user_id', sql.String(64), nullable=True), + sql.Column('requested_project_id', sql.String(64), nullable=False), + sql.Column('requested_roles', sql.Text(), nullable=False), + sql.Column('consumer_id', sql.String(64), nullable=False, index=True), + sql.Column('expires_at', sql.String(64), nullable=True)) + request_token_table.create(migrate_engine, checkfirst=True) + + access_token_table = sql.Table( + 'access_token', + meta, + sql.Column('id', sql.String(64), primary_key=True, nullable=False), + sql.Column('access_secret', sql.String(64), nullable=False), + sql.Column('authorizing_user_id', sql.String(64), + nullable=False, index=True), + sql.Column('project_id', sql.String(64), nullable=False), + sql.Column('requested_roles', sql.Text(), nullable=False), + sql.Column('consumer_id', sql.String(64), nullable=False), + sql.Column('expires_at', sql.String(64), nullable=True)) + access_token_table.create(migrate_engine, checkfirst=True) + + +def downgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + # Operations to reverse the above upgrade go here. + tables = ['consumer', 'request_token', 'access_token'] + for table_name in tables: + table = sql.Table(table_name, meta, autoload=True) + table.drop() diff --git a/keystone-moon/keystone/contrib/oauth1/migrate_repo/versions/002_fix_oauth_tables_fk.py b/keystone-moon/keystone/contrib/oauth1/migrate_repo/versions/002_fix_oauth_tables_fk.py new file mode 100644 index 00000000..d39df8d5 --- /dev/null +++ b/keystone-moon/keystone/contrib/oauth1/migrate_repo/versions/002_fix_oauth_tables_fk.py @@ -0,0 +1,54 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 sqlalchemy as sql + +from keystone.common.sql import migration_helpers + + +def upgrade(migrate_engine): + # Upgrade operations go here. Don't create your own engine; bind + # migrate_engine to your metadata + meta = sql.MetaData() + meta.bind = migrate_engine + + consumer_table = sql.Table('consumer', meta, autoload=True) + request_token_table = sql.Table('request_token', meta, autoload=True) + access_token_table = sql.Table('access_token', meta, autoload=True) + + constraints = [{'table': request_token_table, + 'fk_column': 'consumer_id', + 'ref_column': consumer_table.c.id}, + {'table': access_token_table, + 'fk_column': 'consumer_id', + 'ref_column': consumer_table.c.id}] + if meta.bind != 'sqlite': + migration_helpers.add_constraints(constraints) + + +def downgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + consumer_table = sql.Table('consumer', meta, autoload=True) + request_token_table = sql.Table('request_token', meta, autoload=True) + access_token_table = sql.Table('access_token', meta, autoload=True) + + constraints = [{'table': request_token_table, + 'fk_column': 'consumer_id', + 'ref_column': consumer_table.c.id}, + {'table': access_token_table, + 'fk_column': 'consumer_id', + 'ref_column': consumer_table.c.id}] + if migrate_engine.name != 'sqlite': + migration_helpers.remove_constraints(constraints) diff --git a/keystone-moon/keystone/contrib/oauth1/migrate_repo/versions/003_consumer_description_nullalbe.py b/keystone-moon/keystone/contrib/oauth1/migrate_repo/versions/003_consumer_description_nullalbe.py new file mode 100644 index 00000000..e1cf8843 --- /dev/null +++ b/keystone-moon/keystone/contrib/oauth1/migrate_repo/versions/003_consumer_description_nullalbe.py @@ -0,0 +1,29 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 sqlalchemy as sql + + +def upgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + user_table = sql.Table('consumer', meta, autoload=True) + user_table.c.description.alter(nullable=True) + + +def downgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + user_table = sql.Table('consumer', meta, autoload=True) + user_table.c.description.alter(nullable=False) diff --git a/keystone-moon/keystone/contrib/oauth1/migrate_repo/versions/004_request_token_roles_nullable.py b/keystone-moon/keystone/contrib/oauth1/migrate_repo/versions/004_request_token_roles_nullable.py new file mode 100644 index 00000000..6f1e2e81 --- /dev/null +++ b/keystone-moon/keystone/contrib/oauth1/migrate_repo/versions/004_request_token_roles_nullable.py @@ -0,0 +1,35 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 sqlalchemy as sql + + +def upgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + request_token_table = sql.Table('request_token', meta, autoload=True) + request_token_table.c.requested_roles.alter(nullable=True) + request_token_table.c.requested_roles.alter(name="role_ids") + access_token_table = sql.Table('access_token', meta, autoload=True) + access_token_table.c.requested_roles.alter(name="role_ids") + + +def downgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + request_token_table = sql.Table('request_token', meta, autoload=True) + request_token_table.c.role_ids.alter(nullable=False) + request_token_table.c.role_ids.alter(name="requested_roles") + access_token_table = sql.Table('access_token', meta, autoload=True) + access_token_table.c.role_ids.alter(name="requested_roles") diff --git a/keystone-moon/keystone/contrib/oauth1/migrate_repo/versions/005_consumer_id_index.py b/keystone-moon/keystone/contrib/oauth1/migrate_repo/versions/005_consumer_id_index.py new file mode 100644 index 00000000..428971f8 --- /dev/null +++ b/keystone-moon/keystone/contrib/oauth1/migrate_repo/versions/005_consumer_id_index.py @@ -0,0 +1,42 @@ +# Copyright 2014 Mirantis.inc +# All Rights Reserved. +# +# 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 sqlalchemy as sa + + +def upgrade(migrate_engine): + + if migrate_engine.name == 'mysql': + meta = sa.MetaData(bind=migrate_engine) + table = sa.Table('access_token', meta, autoload=True) + + # NOTE(i159): MySQL requires indexes on referencing columns, and those + # indexes create automatically. That those indexes will have different + # names, depending on version of MySQL used. We shoud make this naming + # consistent, by reverting index name to a consistent condition. + if any(i for i in table.indexes if i.columns.keys() == ['consumer_id'] + and i.name != 'consumer_id'): + # NOTE(i159): by this action will be made re-creation of an index + # with the new name. This can be considered as renaming under the + # MySQL rules. + sa.Index('consumer_id', table.c.consumer_id).create() + + +def downgrade(migrate_engine): + # NOTE(i159): index exists only in MySQL schemas, and got an inconsistent + # name only when MySQL 5.5 renamed it after re-creation + # (during migrations). So we just fixed inconsistency, there is no + # necessity to revert it. + pass diff --git a/keystone-moon/keystone/contrib/oauth1/migrate_repo/versions/__init__.py b/keystone-moon/keystone/contrib/oauth1/migrate_repo/versions/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/contrib/oauth1/routers.py b/keystone-moon/keystone/contrib/oauth1/routers.py new file mode 100644 index 00000000..35619ede --- /dev/null +++ b/keystone-moon/keystone/contrib/oauth1/routers.py @@ -0,0 +1,154 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 functools + +from keystone.common import json_home +from keystone.common import wsgi +from keystone.contrib.oauth1 import controllers + + +build_resource_relation = functools.partial( + json_home.build_v3_extension_resource_relation, + extension_name='OS-OAUTH1', extension_version='1.0') + +build_parameter_relation = functools.partial( + json_home.build_v3_extension_parameter_relation, + extension_name='OS-OAUTH1', extension_version='1.0') + +ACCESS_TOKEN_ID_PARAMETER_RELATION = build_parameter_relation( + parameter_name='access_token_id') + + +class OAuth1Extension(wsgi.V3ExtensionRouter): + """API Endpoints for the OAuth1 extension. + + The goal of this extension is to allow third-party service providers + to acquire tokens with a limited subset of a user's roles for acting + on behalf of that user. This is done using an oauth-similar flow and + api. + + The API looks like:: + + # Basic admin-only consumer crud + POST /OS-OAUTH1/consumers + GET /OS-OAUTH1/consumers + PATCH /OS-OAUTH1/consumers/$consumer_id + GET /OS-OAUTH1/consumers/$consumer_id + DELETE /OS-OAUTH1/consumers/$consumer_id + + # User access token crud + GET /users/$user_id/OS-OAUTH1/access_tokens + GET /users/$user_id/OS-OAUTH1/access_tokens/$access_token_id + GET /users/{user_id}/OS-OAUTH1/access_tokens/{access_token_id}/roles + GET /users/{user_id}/OS-OAUTH1/access_tokens + /{access_token_id}/roles/{role_id} + DELETE /users/$user_id/OS-OAUTH1/access_tokens/$access_token_id + + # OAuth interfaces + POST /OS-OAUTH1/request_token # create a request token + PUT /OS-OAUTH1/authorize # authorize a request token + POST /OS-OAUTH1/access_token # create an access token + + """ + + def add_routes(self, mapper): + consumer_controller = controllers.ConsumerCrudV3() + access_token_controller = controllers.AccessTokenCrudV3() + access_token_roles_controller = controllers.AccessTokenRolesV3() + oauth_controller = controllers.OAuthControllerV3() + + # basic admin-only consumer crud + self._add_resource( + mapper, consumer_controller, + path='/OS-OAUTH1/consumers', + get_action='list_consumers', + post_action='create_consumer', + rel=build_resource_relation(resource_name='consumers')) + self._add_resource( + mapper, consumer_controller, + path='/OS-OAUTH1/consumers/{consumer_id}', + get_action='get_consumer', + patch_action='update_consumer', + delete_action='delete_consumer', + rel=build_resource_relation(resource_name='consumer'), + path_vars={ + 'consumer_id': + build_parameter_relation(parameter_name='consumer_id'), + }) + + # user access token crud + self._add_resource( + mapper, access_token_controller, + path='/users/{user_id}/OS-OAUTH1/access_tokens', + get_action='list_access_tokens', + rel=build_resource_relation(resource_name='user_access_tokens'), + path_vars={ + 'user_id': json_home.Parameters.USER_ID, + }) + self._add_resource( + mapper, access_token_controller, + path='/users/{user_id}/OS-OAUTH1/access_tokens/{access_token_id}', + get_action='get_access_token', + delete_action='delete_access_token', + rel=build_resource_relation(resource_name='user_access_token'), + path_vars={ + 'access_token_id': ACCESS_TOKEN_ID_PARAMETER_RELATION, + 'user_id': json_home.Parameters.USER_ID, + }) + self._add_resource( + mapper, access_token_roles_controller, + path='/users/{user_id}/OS-OAUTH1/access_tokens/{access_token_id}/' + 'roles', + get_action='list_access_token_roles', + rel=build_resource_relation( + resource_name='user_access_token_roles'), + path_vars={ + 'access_token_id': ACCESS_TOKEN_ID_PARAMETER_RELATION, + 'user_id': json_home.Parameters.USER_ID, + }) + self._add_resource( + mapper, access_token_roles_controller, + path='/users/{user_id}/OS-OAUTH1/access_tokens/{access_token_id}/' + 'roles/{role_id}', + get_action='get_access_token_role', + rel=build_resource_relation( + resource_name='user_access_token_role'), + path_vars={ + 'access_token_id': ACCESS_TOKEN_ID_PARAMETER_RELATION, + 'role_id': json_home.Parameters.ROLE_ID, + 'user_id': json_home.Parameters.USER_ID, + }) + + # oauth flow calls + self._add_resource( + mapper, oauth_controller, + path='/OS-OAUTH1/request_token', + post_action='create_request_token', + rel=build_resource_relation(resource_name='request_tokens')) + self._add_resource( + mapper, oauth_controller, + path='/OS-OAUTH1/access_token', + post_action='create_access_token', + rel=build_resource_relation(resource_name='access_tokens')) + self._add_resource( + mapper, oauth_controller, + path='/OS-OAUTH1/authorize/{request_token_id}', + path_vars={ + 'request_token_id': + build_parameter_relation(parameter_name='request_token_id') + }, + put_action='authorize_request_token', + rel=build_resource_relation( + resource_name='authorize_request_token')) diff --git a/keystone-moon/keystone/contrib/oauth1/validator.py b/keystone-moon/keystone/contrib/oauth1/validator.py new file mode 100644 index 00000000..8f44059e --- /dev/null +++ b/keystone-moon/keystone/contrib/oauth1/validator.py @@ -0,0 +1,179 @@ +# Copyright 2014 OpenStack Foundation +# +# 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. + +"""oAuthlib request validator.""" + +from oslo_log import log +import six + +from keystone.common import dependency +from keystone.contrib.oauth1 import core as oauth1 +from keystone import exception + + +METHOD_NAME = 'oauth_validator' +LOG = log.getLogger(__name__) + + +@dependency.requires('oauth_api') +class OAuthValidator(oauth1.RequestValidator): + + # TODO(mhu) set as option probably? + @property + def enforce_ssl(self): + return False + + @property + def safe_characters(self): + # oauth tokens are generated from a uuid hex value + return set("abcdef0123456789") + + def _check_token(self, token): + # generic token verification when they're obtained from a uuid hex + return (set(token) <= self.safe_characters and + len(token) == 32) + + def check_client_key(self, client_key): + return self._check_token(client_key) + + def check_request_token(self, request_token): + return self._check_token(request_token) + + def check_access_token(self, access_token): + return self._check_token(access_token) + + def check_nonce(self, nonce): + # Assuming length is not a concern + return set(nonce) <= self.safe_characters + + def check_verifier(self, verifier): + return (all(i in oauth1.VERIFIER_CHARS for i in verifier) and + len(verifier) == 8) + + def get_client_secret(self, client_key, request): + client = self.oauth_api.get_consumer_with_secret(client_key) + return client['secret'] + + def get_request_token_secret(self, client_key, token, request): + token_ref = self.oauth_api.get_request_token(token) + return token_ref['request_secret'] + + def get_access_token_secret(self, client_key, token, request): + access_token = self.oauth_api.get_access_token(token) + return access_token['access_secret'] + + def get_default_realms(self, client_key, request): + # realms weren't implemented with the previous library + return [] + + def get_realms(self, token, request): + return [] + + def get_redirect_uri(self, token, request): + # OOB (out of band) is supposed to be the default value to use + return 'oob' + + def get_rsa_key(self, client_key, request): + # HMAC signing is used, so return a dummy value + return '' + + def invalidate_request_token(self, client_key, request_token, request): + # this method is invoked when an access token is generated out of a + # request token, to make sure that request token cannot be consumed + # anymore. This is done in the backend, so we do nothing here. + pass + + def validate_client_key(self, client_key, request): + try: + return self.oauth_api.get_consumer(client_key) is not None + except exception.NotFound: + return False + + def validate_request_token(self, client_key, token, request): + try: + return self.oauth_api.get_request_token(token) is not None + except exception.NotFound: + return False + + def validate_access_token(self, client_key, token, request): + try: + return self.oauth_api.get_access_token(token) is not None + except exception.NotFound: + return False + + def validate_timestamp_and_nonce(self, + client_key, + timestamp, + nonce, + request, + request_token=None, + access_token=None): + return True + + def validate_redirect_uri(self, client_key, redirect_uri, request): + # we expect OOB, we don't really care + return True + + def validate_requested_realms(self, client_key, realms, request): + # realms are not used + return True + + def validate_realms(self, + client_key, + token, + request, + uri=None, + realms=None): + return True + + def validate_verifier(self, client_key, token, verifier, request): + try: + req_token = self.oauth_api.get_request_token(token) + return req_token['verifier'] == verifier + except exception.NotFound: + return False + + def verify_request_token(self, token, request): + # there aren't strong expectations on the request token format + return isinstance(token, six.string_types) + + def verify_realms(self, token, realms, request): + return True + + # The following save_XXX methods are called to create tokens. I chose to + # keep the original logic, but the comments below show how that could be + # implemented. The real implementation logic is in the backend. + def save_access_token(self, token, request): + pass +# token_duration = CONF.oauth1.request_token_duration +# request_token_id = request.client_key +# self.oauth_api.create_access_token(request_token_id, +# token_duration, +# token["oauth_token"], +# token["oauth_token_secret"]) + + def save_request_token(self, token, request): + pass +# project_id = request.headers.get('Requested-Project-Id') +# token_duration = CONF.oauth1.request_token_duration +# self.oauth_api.create_request_token(request.client_key, +# project_id, +# token_duration, +# token["oauth_token"], +# token["oauth_token_secret"]) + + def save_verifier(self, token, verifier, request): + # keep the old logic for this, as it is done in two steps and requires + # information that the request validator has no access to + pass diff --git a/keystone-moon/keystone/contrib/revoke/__init__.py b/keystone-moon/keystone/contrib/revoke/__init__.py new file mode 100644 index 00000000..58ba68db --- /dev/null +++ b/keystone-moon/keystone/contrib/revoke/__init__.py @@ -0,0 +1,13 @@ +# 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 keystone.contrib.revoke.core import * # noqa diff --git a/keystone-moon/keystone/contrib/revoke/backends/__init__.py b/keystone-moon/keystone/contrib/revoke/backends/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/contrib/revoke/backends/kvs.py b/keystone-moon/keystone/contrib/revoke/backends/kvs.py new file mode 100644 index 00000000..cc41fbee --- /dev/null +++ b/keystone-moon/keystone/contrib/revoke/backends/kvs.py @@ -0,0 +1,73 @@ +# 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 datetime + +from oslo_config import cfg +from oslo_utils import timeutils + +from keystone.common import kvs +from keystone.contrib import revoke +from keystone import exception +from keystone.openstack.common import versionutils + + +CONF = cfg.CONF + +_EVENT_KEY = 'os-revoke-events' +_KVS_BACKEND = 'openstack.kvs.Memory' + + +class Revoke(revoke.Driver): + + @versionutils.deprecated( + versionutils.deprecated.JUNO, + in_favor_of='keystone.contrib.revoke.backends.sql', + remove_in=+1, + what='keystone.contrib.revoke.backends.kvs') + def __init__(self, **kwargs): + super(Revoke, self).__init__() + self._store = kvs.get_key_value_store('os-revoke-driver') + self._store.configure(backing_store=_KVS_BACKEND, **kwargs) + + def _list_events(self): + try: + return self._store.get(_EVENT_KEY) + except exception.NotFound: + return [] + + def _prune_expired_events_and_get(self, last_fetch=None, new_event=None): + pruned = [] + results = [] + expire_delta = datetime.timedelta(seconds=CONF.token.expiration) + oldest = timeutils.utcnow() - expire_delta + # TODO(ayoung): Store the time of the oldest event so that the + # prune process can be skipped if none of the events have timed out. + with self._store.get_lock(_EVENT_KEY) as lock: + events = self._list_events() + if new_event is not None: + events.append(new_event) + + for event in events: + revoked_at = event.revoked_at + if revoked_at > oldest: + pruned.append(event) + if last_fetch is None or revoked_at > last_fetch: + results.append(event) + self._store.set(_EVENT_KEY, pruned, lock) + return results + + def list_events(self, last_fetch=None): + return self._prune_expired_events_and_get(last_fetch=last_fetch) + + def revoke(self, event): + self._prune_expired_events_and_get(new_event=event) diff --git a/keystone-moon/keystone/contrib/revoke/backends/sql.py b/keystone-moon/keystone/contrib/revoke/backends/sql.py new file mode 100644 index 00000000..1b0cde1e --- /dev/null +++ b/keystone-moon/keystone/contrib/revoke/backends/sql.py @@ -0,0 +1,104 @@ +# 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 keystone.common import sql +from keystone.contrib import revoke +from keystone.contrib.revoke import model + + +class RevocationEvent(sql.ModelBase, sql.ModelDictMixin): + __tablename__ = 'revocation_event' + attributes = model.REVOKE_KEYS + + # The id field is not going to be exposed to the outside world. + # It is, however, necessary for SQLAlchemy. + id = sql.Column(sql.String(64), primary_key=True) + domain_id = sql.Column(sql.String(64)) + project_id = sql.Column(sql.String(64)) + user_id = sql.Column(sql.String(64)) + role_id = sql.Column(sql.String(64)) + trust_id = sql.Column(sql.String(64)) + consumer_id = sql.Column(sql.String(64)) + access_token_id = sql.Column(sql.String(64)) + issued_before = sql.Column(sql.DateTime(), nullable=False) + expires_at = sql.Column(sql.DateTime()) + revoked_at = sql.Column(sql.DateTime(), nullable=False) + audit_id = sql.Column(sql.String(32)) + audit_chain_id = sql.Column(sql.String(32)) + + +class Revoke(revoke.Driver): + def _flush_batch_size(self, dialect): + batch_size = 0 + if dialect == 'ibm_db_sa': + # This functionality is limited to DB2, because + # it is necessary to prevent the transaction log + # from filling up, whereas at least some of the + # other supported databases do not support update + # queries with LIMIT subqueries nor do they appear + # to require the use of such queries when deleting + # large numbers of records at once. + batch_size = 100 + # Limit of 100 is known to not fill a transaction log + # of default maximum size while not significantly + # impacting the performance of large token purges on + # systems where the maximum transaction log size has + # been increased beyond the default. + return batch_size + + def _prune_expired_events(self): + oldest = revoke.revoked_before_cutoff_time() + + session = sql.get_session() + dialect = session.bind.dialect.name + batch_size = self._flush_batch_size(dialect) + if batch_size > 0: + query = session.query(RevocationEvent.id) + query = query.filter(RevocationEvent.revoked_at < oldest) + query = query.limit(batch_size).subquery() + delete_query = (session.query(RevocationEvent). + filter(RevocationEvent.id.in_(query))) + while True: + rowcount = delete_query.delete(synchronize_session=False) + if rowcount == 0: + break + else: + query = session.query(RevocationEvent) + query = query.filter(RevocationEvent.revoked_at < oldest) + query.delete(synchronize_session=False) + + session.flush() + + def list_events(self, last_fetch=None): + self._prune_expired_events() + session = sql.get_session() + query = session.query(RevocationEvent).order_by( + RevocationEvent.revoked_at) + + if last_fetch: + query = query.filter(RevocationEvent.revoked_at > last_fetch) + + events = [model.RevokeEvent(**e.to_dict()) for e in query] + + return events + + def revoke(self, event): + kwargs = dict() + for attr in model.REVOKE_KEYS: + kwargs[attr] = getattr(event, attr) + kwargs['id'] = uuid.uuid4().hex + record = RevocationEvent(**kwargs) + session = sql.get_session() + with session.begin(): + session.add(record) diff --git a/keystone-moon/keystone/contrib/revoke/controllers.py b/keystone-moon/keystone/contrib/revoke/controllers.py new file mode 100644 index 00000000..40151bae --- /dev/null +++ b/keystone-moon/keystone/contrib/revoke/controllers.py @@ -0,0 +1,44 @@ +# 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 oslo_utils import timeutils + +from keystone.common import controller +from keystone.common import dependency +from keystone import exception +from keystone.i18n import _ + + +@dependency.requires('revoke_api') +class RevokeController(controller.V3Controller): + @controller.protected() + def list_revoke_events(self, context): + since = context['query_string'].get('since') + last_fetch = None + if since: + try: + last_fetch = timeutils.normalize_time( + timeutils.parse_isotime(since)) + except ValueError: + raise exception.ValidationError( + message=_('invalid date format %s') % since) + events = self.revoke_api.list_events(last_fetch=last_fetch) + # Build the links by hand as the standard controller calls require ids + response = {'events': [event.to_dict() for event in events], + 'links': { + 'next': None, + 'self': RevokeController.base_url( + context, + path=context['path']), + 'previous': None} + } + return response diff --git a/keystone-moon/keystone/contrib/revoke/core.py b/keystone-moon/keystone/contrib/revoke/core.py new file mode 100644 index 00000000..c7335690 --- /dev/null +++ b/keystone-moon/keystone/contrib/revoke/core.py @@ -0,0 +1,250 @@ +# 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 abc +import datetime + +from oslo_config import cfg +from oslo_log import log +from oslo_utils import timeutils +import six + +from keystone.common import cache +from keystone.common import dependency +from keystone.common import extension +from keystone.common import manager +from keystone.contrib.revoke import model +from keystone import exception +from keystone.i18n import _ +from keystone import notifications +from keystone.openstack.common import versionutils + + +CONF = cfg.CONF +LOG = log.getLogger(__name__) + + +EXTENSION_DATA = { + 'name': 'OpenStack Revoke API', + 'namespace': 'http://docs.openstack.org/identity/api/ext/' + 'OS-REVOKE/v1.0', + 'alias': 'OS-REVOKE', + 'updated': '2014-02-24T20:51:0-00:00', + 'description': 'OpenStack revoked token reporting mechanism.', + 'links': [ + { + 'rel': 'describedby', + 'type': 'text/html', + 'href': ('https://github.com/openstack/identity-api/blob/master/' + 'openstack-identity-api/v3/src/markdown/' + 'identity-api-v3-os-revoke-ext.md'), + } + ]} +extension.register_admin_extension(EXTENSION_DATA['alias'], EXTENSION_DATA) +extension.register_public_extension(EXTENSION_DATA['alias'], EXTENSION_DATA) + +MEMOIZE = cache.get_memoization_decorator(section='revoke') + + +def revoked_before_cutoff_time(): + expire_delta = datetime.timedelta( + seconds=CONF.token.expiration + CONF.revoke.expiration_buffer) + oldest = timeutils.utcnow() - expire_delta + return oldest + + +@dependency.provider('revoke_api') +class Manager(manager.Manager): + """Revoke API Manager. + + Performs common logic for recording revocations. + + """ + + def __init__(self): + super(Manager, self).__init__(CONF.revoke.driver) + self._register_listeners() + self.model = model + + def _user_callback(self, service, resource_type, operation, + payload): + self.revoke_by_user(payload['resource_info']) + + def _role_callback(self, service, resource_type, operation, + payload): + self.revoke( + model.RevokeEvent(role_id=payload['resource_info'])) + + def _project_callback(self, service, resource_type, operation, + payload): + self.revoke( + model.RevokeEvent(project_id=payload['resource_info'])) + + def _domain_callback(self, service, resource_type, operation, + payload): + self.revoke( + model.RevokeEvent(domain_id=payload['resource_info'])) + + def _trust_callback(self, service, resource_type, operation, + payload): + self.revoke( + model.RevokeEvent(trust_id=payload['resource_info'])) + + def _consumer_callback(self, service, resource_type, operation, + payload): + self.revoke( + model.RevokeEvent(consumer_id=payload['resource_info'])) + + def _access_token_callback(self, service, resource_type, operation, + payload): + self.revoke( + model.RevokeEvent(access_token_id=payload['resource_info'])) + + def _group_callback(self, service, resource_type, operation, payload): + user_ids = (u['id'] for u in self.identity_api.list_users_in_group( + payload['resource_info'])) + for uid in user_ids: + self.revoke(model.RevokeEvent(user_id=uid)) + + def _register_listeners(self): + callbacks = { + notifications.ACTIONS.deleted: [ + ['OS-TRUST:trust', self._trust_callback], + ['OS-OAUTH1:consumer', self._consumer_callback], + ['OS-OAUTH1:access_token', self._access_token_callback], + ['role', self._role_callback], + ['user', self._user_callback], + ['project', self._project_callback], + ], + notifications.ACTIONS.disabled: [ + ['user', self._user_callback], + ['project', self._project_callback], + ['domain', self._domain_callback], + ], + notifications.ACTIONS.internal: [ + [notifications.INVALIDATE_USER_TOKEN_PERSISTENCE, + self._user_callback], + ] + } + + for event, cb_info in six.iteritems(callbacks): + for resource_type, callback_fns in cb_info: + notifications.register_event_callback(event, resource_type, + callback_fns) + + def revoke_by_user(self, user_id): + return self.revoke(model.RevokeEvent(user_id=user_id)) + + def _assert_not_domain_and_project_scoped(self, domain_id=None, + project_id=None): + if domain_id is not None and project_id is not None: + msg = _('The revoke call must not have both domain_id and ' + 'project_id. This is a bug in the Keystone server. The ' + 'current request is aborted.') + raise exception.UnexpectedError(exception=msg) + + @versionutils.deprecated(as_of=versionutils.deprecated.JUNO, + remove_in=0) + def revoke_by_expiration(self, user_id, expires_at, + domain_id=None, project_id=None): + + self._assert_not_domain_and_project_scoped(domain_id=domain_id, + project_id=project_id) + + self.revoke( + model.RevokeEvent(user_id=user_id, + expires_at=expires_at, + domain_id=domain_id, + project_id=project_id)) + + def revoke_by_audit_id(self, audit_id): + self.revoke(model.RevokeEvent(audit_id=audit_id)) + + def revoke_by_audit_chain_id(self, audit_chain_id, project_id=None, + domain_id=None): + + self._assert_not_domain_and_project_scoped(domain_id=domain_id, + project_id=project_id) + + self.revoke(model.RevokeEvent(audit_chain_id=audit_chain_id, + domain_id=domain_id, + project_id=project_id)) + + def revoke_by_grant(self, role_id, user_id=None, + domain_id=None, project_id=None): + self.revoke( + model.RevokeEvent(user_id=user_id, + role_id=role_id, + domain_id=domain_id, + project_id=project_id)) + + def revoke_by_user_and_project(self, user_id, project_id): + self.revoke( + model.RevokeEvent(project_id=project_id, user_id=user_id)) + + def revoke_by_project_role_assignment(self, project_id, role_id): + self.revoke(model.RevokeEvent(project_id=project_id, role_id=role_id)) + + def revoke_by_domain_role_assignment(self, domain_id, role_id): + self.revoke(model.RevokeEvent(domain_id=domain_id, role_id=role_id)) + + @MEMOIZE + def _get_revoke_tree(self): + events = self.driver.list_events() + revoke_tree = model.RevokeTree(revoke_events=events) + + return revoke_tree + + def check_token(self, token_values): + """Checks the values from a token against the revocation list + + :param token_values: dictionary of values from a token, + normalized for differences between v2 and v3. The checked values are a + subset of the attributes of model.TokenEvent + + :raises exception.TokenNotFound: if the token is invalid + + """ + if self._get_revoke_tree().is_revoked(token_values): + raise exception.TokenNotFound(_('Failed to validate token')) + + def revoke(self, event): + self.driver.revoke(event) + self._get_revoke_tree.invalidate(self) + + +@six.add_metaclass(abc.ABCMeta) +class Driver(object): + """Interface for recording and reporting revocation events.""" + + @abc.abstractmethod + def list_events(self, last_fetch=None): + """return the revocation events, as a list of objects + + :param last_fetch: Time of last fetch. Return all events newer. + :returns: A list of keystone.contrib.revoke.model.RevokeEvent + newer than `last_fetch.` + If no last_fetch is specified, returns all events + for tokens issued after the expiration cutoff. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def revoke(self, event): + """register a revocation event + + :param event: An instance of + keystone.contrib.revoke.model.RevocationEvent + + """ + raise exception.NotImplemented() # pragma: no cover diff --git a/keystone-moon/keystone/contrib/revoke/migrate_repo/__init__.py b/keystone-moon/keystone/contrib/revoke/migrate_repo/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/contrib/revoke/migrate_repo/migrate.cfg b/keystone-moon/keystone/contrib/revoke/migrate_repo/migrate.cfg new file mode 100644 index 00000000..0e61bcaa --- /dev/null +++ b/keystone-moon/keystone/contrib/revoke/migrate_repo/migrate.cfg @@ -0,0 +1,25 @@ +[db_settings] +# Used to identify which repository this database is versioned under. +# You can use the name of your project. +repository_id=revoke + +# The name of the database table used to track the schema version. +# This name shouldn't already be used by your project. +# If this is changed once a database is under version control, you'll need to +# change the table name in each database too. +version_table=migrate_version + +# When committing a change script, Migrate will attempt to generate the +# sql for all supported databases; normally, if one of them fails - probably +# because you don't have that database installed - it is ignored and the +# commit continues, perhaps ending successfully. +# Databases in this list MUST compile successfully during a commit, or the +# entire commit will fail. List the databases your application will actually +# be using to ensure your updates to that database work properly. +# This must be a list; example: ['postgres','sqlite'] +required_dbs=[] + +# When creating new change scripts, Migrate will stamp the new script with +# a version number. By default this is latest_version + 1. You can set this +# to 'true' to tell Migrate to use the UTC timestamp instead. +use_timestamp_numbering=False diff --git a/keystone-moon/keystone/contrib/revoke/migrate_repo/versions/001_revoke_table.py b/keystone-moon/keystone/contrib/revoke/migrate_repo/versions/001_revoke_table.py new file mode 100644 index 00000000..7927ce0c --- /dev/null +++ b/keystone-moon/keystone/contrib/revoke/migrate_repo/versions/001_revoke_table.py @@ -0,0 +1,47 @@ +# 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 sqlalchemy as sql + + +def upgrade(migrate_engine): + # Upgrade operations go here. Don't create your own engine; bind + # migrate_engine to your metadata + meta = sql.MetaData() + meta.bind = migrate_engine + + service_table = sql.Table( + 'revocation_event', + meta, + sql.Column('id', sql.String(64), primary_key=True), + sql.Column('domain_id', sql.String(64)), + sql.Column('project_id', sql.String(64)), + sql.Column('user_id', sql.String(64)), + sql.Column('role_id', sql.String(64)), + sql.Column('trust_id', sql.String(64)), + sql.Column('consumer_id', sql.String(64)), + sql.Column('access_token_id', sql.String(64)), + sql.Column('issued_before', sql.DateTime(), nullable=False), + sql.Column('expires_at', sql.DateTime()), + sql.Column('revoked_at', sql.DateTime(), index=True, nullable=False)) + service_table.create(migrate_engine, checkfirst=True) + + +def downgrade(migrate_engine): + # Operations to reverse the above upgrade go here. + meta = sql.MetaData() + meta.bind = migrate_engine + + tables = ['revocation_event'] + for t in tables: + table = sql.Table(t, meta, autoload=True) + table.drop(migrate_engine, checkfirst=True) diff --git a/keystone-moon/keystone/contrib/revoke/migrate_repo/versions/002_add_audit_id_and_chain_to_revoke_table.py b/keystone-moon/keystone/contrib/revoke/migrate_repo/versions/002_add_audit_id_and_chain_to_revoke_table.py new file mode 100644 index 00000000..bee6fb2a --- /dev/null +++ b/keystone-moon/keystone/contrib/revoke/migrate_repo/versions/002_add_audit_id_and_chain_to_revoke_table.py @@ -0,0 +1,37 @@ +# 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 sqlalchemy as sql + + +_TABLE_NAME = 'revocation_event' + + +def upgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + + event_table = sql.Table(_TABLE_NAME, meta, autoload=True) + audit_id_column = sql.Column('audit_id', sql.String(32), nullable=True) + audit_chain_column = sql.Column('audit_chain_id', sql.String(32), + nullable=True) + event_table.create_column(audit_id_column) + event_table.create_column(audit_chain_column) + + +def downgrade(migrate_engine): + meta = sql.MetaData() + meta.bind = migrate_engine + + event_table = sql.Table(_TABLE_NAME, meta, autoload=True) + event_table.drop_column('audit_id') + event_table.drop_column('audit_chain_id') diff --git a/keystone-moon/keystone/contrib/revoke/migrate_repo/versions/__init__.py b/keystone-moon/keystone/contrib/revoke/migrate_repo/versions/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/contrib/revoke/model.py b/keystone-moon/keystone/contrib/revoke/model.py new file mode 100644 index 00000000..5e92042d --- /dev/null +++ b/keystone-moon/keystone/contrib/revoke/model.py @@ -0,0 +1,365 @@ +# 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 oslo_utils import timeutils + + +# The set of attributes common between the RevokeEvent +# and the dictionaries created from the token Data. +_NAMES = ['trust_id', + 'consumer_id', + 'access_token_id', + 'audit_id', + 'audit_chain_id', + 'expires_at', + 'domain_id', + 'project_id', + 'user_id', + 'role_id'] + + +# Additional arguments for creating a RevokeEvent +_EVENT_ARGS = ['issued_before', 'revoked_at'] + +# Names of attributes in the RevocationEvent, including "virtual" attributes. +# Virtual attributes are those added based on other values. +_EVENT_NAMES = _NAMES + ['domain_scope_id'] + +# Values that will be in the token data but not in the event. +# These will compared with event values that have different names. +# For example: both trustor_id and trustee_id are compared against user_id +_TOKEN_KEYS = ['identity_domain_id', + 'assignment_domain_id', + 'issued_at', + 'trustor_id', + 'trustee_id'] + + +REVOKE_KEYS = _NAMES + _EVENT_ARGS + + +def blank_token_data(issued_at): + token_data = dict() + for name in _NAMES: + token_data[name] = None + for name in _TOKEN_KEYS: + token_data[name] = None + # required field + token_data['issued_at'] = issued_at + return token_data + + +class RevokeEvent(object): + def __init__(self, **kwargs): + for k in REVOKE_KEYS: + v = kwargs.get(k, None) + setattr(self, k, v) + + if self.domain_id and self.expires_at: + # This is revoking a domain-scoped token. + self.domain_scope_id = self.domain_id + self.domain_id = None + else: + # This is revoking all tokens for a domain. + self.domain_scope_id = None + + if self.expires_at is not None: + # Trim off the expiration time because MySQL timestamps are only + # accurate to the second. + self.expires_at = self.expires_at.replace(microsecond=0) + + if self.revoked_at is None: + self.revoked_at = timeutils.utcnow() + if self.issued_before is None: + self.issued_before = self.revoked_at + + def to_dict(self): + keys = ['user_id', + 'role_id', + 'domain_id', + 'domain_scope_id', + 'project_id', + 'audit_id', + 'audit_chain_id', + ] + event = {key: self.__dict__[key] for key in keys + if self.__dict__[key] is not None} + if self.trust_id is not None: + event['OS-TRUST:trust_id'] = self.trust_id + if self.consumer_id is not None: + event['OS-OAUTH1:consumer_id'] = self.consumer_id + if self.consumer_id is not None: + event['OS-OAUTH1:access_token_id'] = self.access_token_id + if self.expires_at is not None: + event['expires_at'] = timeutils.isotime(self.expires_at) + if self.issued_before is not None: + event['issued_before'] = timeutils.isotime(self.issued_before, + subsecond=True) + return event + + def key_for_name(self, name): + return "%s=%s" % (name, getattr(self, name) or '*') + + +def attr_keys(event): + return map(event.key_for_name, _EVENT_NAMES) + + +class RevokeTree(object): + """Fast Revocation Checking Tree Structure + + The Tree is an index to quickly match tokens against events. + Each node is a hashtable of key=value combinations from revocation events. + The + + """ + + def __init__(self, revoke_events=None): + self.revoke_map = dict() + self.add_events(revoke_events) + + def add_event(self, event): + """Updates the tree based on a revocation event. + + Creates any necessary internal nodes in the tree corresponding to the + fields of the revocation event. The leaf node will always be set to + the latest 'issued_before' for events that are otherwise identical. + + :param: Event to add to the tree + + :returns: the event that was passed in. + + """ + revoke_map = self.revoke_map + for key in attr_keys(event): + revoke_map = revoke_map.setdefault(key, {}) + revoke_map['issued_before'] = max( + event.issued_before, revoke_map.get( + 'issued_before', event.issued_before)) + return event + + def remove_event(self, event): + """Update the tree based on the removal of a Revocation Event + + Removes empty nodes from the tree from the leaf back to the root. + + If multiple events trace the same path, but have different + 'issued_before' values, only the last is ever stored in the tree. + So only an exact match on 'issued_before' ever triggers a removal + + :param: Event to remove from the tree + + """ + stack = [] + revoke_map = self.revoke_map + for name in _EVENT_NAMES: + key = event.key_for_name(name) + nxt = revoke_map.get(key) + if nxt is None: + break + stack.append((revoke_map, key, nxt)) + revoke_map = nxt + else: + if event.issued_before == revoke_map['issued_before']: + revoke_map.pop('issued_before') + for parent, key, child in reversed(stack): + if not any(child): + del parent[key] + + def add_events(self, revoke_events): + return map(self.add_event, revoke_events or []) + + def is_revoked(self, token_data): + """Check if a token matches the revocation event + + Compare the values for each level of the tree with the values from + the token, accounting for attributes that have alternative + keys, and for wildcard matches. + if there is a match, continue down the tree. + if there is no match, exit early. + + token_data is a map based on a flattened view of token. + The required fields are: + + 'expires_at','user_id', 'project_id', 'identity_domain_id', + 'assignment_domain_id', 'trust_id', 'trustor_id', 'trustee_id' + 'consumer_id', 'access_token_id' + + """ + # Alternative names to be checked in token for every field in + # revoke tree. + alternatives = { + 'user_id': ['user_id', 'trustor_id', 'trustee_id'], + 'domain_id': ['identity_domain_id', 'assignment_domain_id'], + # For a domain-scoped token, the domain is in assignment_domain_id. + 'domain_scope_id': ['assignment_domain_id', ], + } + # Contains current forest (collection of trees) to be checked. + partial_matches = [self.revoke_map] + # We iterate over every layer of our revoke tree (except the last one). + for name in _EVENT_NAMES: + # bundle is the set of partial matches for the next level down + # the tree + bundle = [] + wildcard = '%s=*' % (name,) + # For every tree in current forest. + for tree in partial_matches: + # If there is wildcard node on current level we take it. + bundle.append(tree.get(wildcard)) + if name == 'role_id': + # Roles are very special since a token has a list of them. + # If the revocation event matches any one of them, + # revoke the token. + for role_id in token_data.get('roles', []): + bundle.append(tree.get('role_id=%s' % role_id)) + else: + # For other fields we try to get any branch that concur + # with any alternative field in the token. + for alt_name in alternatives.get(name, [name]): + bundle.append( + tree.get('%s=%s' % (name, token_data[alt_name]))) + # tree.get returns `None` if there is no match, so `bundle.append` + # adds a 'None' entry. This call remoes the `None` entries. + partial_matches = [x for x in bundle if x is not None] + if not partial_matches: + # If we end up with no branches to follow means that the token + # is definitely not in the revoke tree and all further + # iterations will be for nothing. + return False + + # The last (leaf) level is checked in a special way because we verify + # issued_at field differently. + for leaf in partial_matches: + try: + if leaf['issued_before'] > token_data['issued_at']: + return True + except KeyError: + pass + # If we made it out of the loop then no element in revocation tree + # corresponds to our token and it is good. + return False + + +def build_token_values_v2(access, default_domain_id): + token_data = access['token'] + + token_expires_at = timeutils.parse_isotime(token_data['expires']) + + # Trim off the microseconds because the revocation event only has + # expirations accurate to the second. + token_expires_at = token_expires_at.replace(microsecond=0) + + token_values = { + 'expires_at': timeutils.normalize_time(token_expires_at), + 'issued_at': timeutils.normalize_time( + timeutils.parse_isotime(token_data['issued_at'])), + 'audit_id': token_data.get('audit_ids', [None])[0], + 'audit_chain_id': token_data.get('audit_ids', [None])[-1], + } + + token_values['user_id'] = access.get('user', {}).get('id') + + project = token_data.get('tenant') + if project is not None: + token_values['project_id'] = project['id'] + else: + token_values['project_id'] = None + + token_values['identity_domain_id'] = default_domain_id + token_values['assignment_domain_id'] = default_domain_id + + trust = token_data.get('trust') + if trust is None: + token_values['trust_id'] = None + token_values['trustor_id'] = None + token_values['trustee_id'] = None + else: + token_values['trust_id'] = trust['id'] + token_values['trustor_id'] = trust['trustor_id'] + token_values['trustee_id'] = trust['trustee_id'] + + token_values['consumer_id'] = None + token_values['access_token_id'] = None + + role_list = [] + # Roles are by ID in metadata and by name in the user section + roles = access.get('metadata', {}).get('roles', []) + for role in roles: + role_list.append(role) + token_values['roles'] = role_list + return token_values + + +def build_token_values(token_data): + + token_expires_at = timeutils.parse_isotime(token_data['expires_at']) + + # Trim off the microseconds because the revocation event only has + # expirations accurate to the second. + token_expires_at = token_expires_at.replace(microsecond=0) + + token_values = { + 'expires_at': timeutils.normalize_time(token_expires_at), + 'issued_at': timeutils.normalize_time( + timeutils.parse_isotime(token_data['issued_at'])), + 'audit_id': token_data.get('audit_ids', [None])[0], + 'audit_chain_id': token_data.get('audit_ids', [None])[-1], + } + + user = token_data.get('user') + if user is not None: + token_values['user_id'] = user['id'] + # Federated users do not have a domain, be defensive and get the user + # domain set to None in the federated user case. + token_values['identity_domain_id'] = user.get('domain', {}).get('id') + else: + token_values['user_id'] = None + token_values['identity_domain_id'] = None + + project = token_data.get('project', token_data.get('tenant')) + if project is not None: + token_values['project_id'] = project['id'] + token_values['assignment_domain_id'] = project['domain']['id'] + else: + token_values['project_id'] = None + + domain = token_data.get('domain') + if domain is not None: + token_values['assignment_domain_id'] = domain['id'] + else: + token_values['assignment_domain_id'] = None + + role_list = [] + roles = token_data.get('roles') + if roles is not None: + for role in roles: + role_list.append(role['id']) + token_values['roles'] = role_list + + trust = token_data.get('OS-TRUST:trust') + if trust is None: + token_values['trust_id'] = None + token_values['trustor_id'] = None + token_values['trustee_id'] = None + else: + token_values['trust_id'] = trust['id'] + token_values['trustor_id'] = trust['trustor_user']['id'] + token_values['trustee_id'] = trust['trustee_user']['id'] + + oauth1 = token_data.get('OS-OAUTH1') + if oauth1 is None: + token_values['consumer_id'] = None + token_values['access_token_id'] = None + else: + token_values['consumer_id'] = oauth1['consumer_id'] + token_values['access_token_id'] = oauth1['access_token_id'] + return token_values diff --git a/keystone-moon/keystone/contrib/revoke/routers.py b/keystone-moon/keystone/contrib/revoke/routers.py new file mode 100644 index 00000000..4d2edfc0 --- /dev/null +++ b/keystone-moon/keystone/contrib/revoke/routers.py @@ -0,0 +1,29 @@ +# 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 keystone.common import json_home +from keystone.common import wsgi +from keystone.contrib.revoke import controllers + + +class RevokeExtension(wsgi.V3ExtensionRouter): + + PATH_PREFIX = '/OS-REVOKE' + + def add_routes(self, mapper): + revoke_controller = controllers.RevokeController() + self._add_resource( + mapper, revoke_controller, + path=self.PATH_PREFIX + '/events', + get_action='list_revoke_events', + rel=json_home.build_v3_extension_resource_relation( + 'OS-REVOKE', '1.0', 'events')) diff --git a/keystone-moon/keystone/contrib/s3/__init__.py b/keystone-moon/keystone/contrib/s3/__init__.py new file mode 100644 index 00000000..eec77c72 --- /dev/null +++ b/keystone-moon/keystone/contrib/s3/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 keystone.contrib.s3.core import * # noqa diff --git a/keystone-moon/keystone/contrib/s3/core.py b/keystone-moon/keystone/contrib/s3/core.py new file mode 100644 index 00000000..34095bf4 --- /dev/null +++ b/keystone-moon/keystone/contrib/s3/core.py @@ -0,0 +1,73 @@ +# Copyright 2012 OpenStack Foundation +# +# 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. + +"""Main entry point into the S3 Credentials service. + +This service provides S3 token validation for services configured with the +s3_token middleware to authorize S3 requests. + +This service uses the same credentials used by EC2. Refer to the documentation +for the EC2 module for how to generate the required credentials. +""" + +import base64 +import hashlib +import hmac + +from keystone.common import extension +from keystone.common import json_home +from keystone.common import utils +from keystone.common import wsgi +from keystone.contrib.ec2 import controllers +from keystone import exception + +EXTENSION_DATA = { + 'name': 'OpenStack S3 API', + 'namespace': 'http://docs.openstack.org/identity/api/ext/' + 's3tokens/v1.0', + 'alias': 's3tokens', + 'updated': '2013-07-07T12:00:0-00:00', + 'description': 'OpenStack S3 API.', + 'links': [ + { + 'rel': 'describedby', + # TODO(ayoung): needs a description + 'type': 'text/html', + 'href': 'https://github.com/openstack/identity-api', + } + ]} +extension.register_admin_extension(EXTENSION_DATA['alias'], EXTENSION_DATA) + + +class S3Extension(wsgi.V3ExtensionRouter): + def add_routes(self, mapper): + controller = S3Controller() + # validation + self._add_resource( + mapper, controller, + path='/s3tokens', + post_action='authenticate', + rel=json_home.build_v3_extension_resource_relation( + 's3tokens', '1.0', 's3tokens')) + + +class S3Controller(controllers.Ec2Controller): + def check_signature(self, creds_ref, credentials): + msg = base64.urlsafe_b64decode(str(credentials['token'])) + key = str(creds_ref['secret']) + signed = base64.encodestring( + hmac.new(key, msg, hashlib.sha1).digest()).strip() + + if not utils.auth_str_equal(credentials['signature'], signed): + raise exception.Unauthorized('Credential signature mismatch') diff --git a/keystone-moon/keystone/contrib/simple_cert/__init__.py b/keystone-moon/keystone/contrib/simple_cert/__init__.py new file mode 100644 index 00000000..b213192e --- /dev/null +++ b/keystone-moon/keystone/contrib/simple_cert/__init__.py @@ -0,0 +1,14 @@ +# 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 keystone.contrib.simple_cert.core import * # noqa +from keystone.contrib.simple_cert.routers import SimpleCertExtension # noqa diff --git a/keystone-moon/keystone/contrib/simple_cert/controllers.py b/keystone-moon/keystone/contrib/simple_cert/controllers.py new file mode 100644 index 00000000..d34c03a6 --- /dev/null +++ b/keystone-moon/keystone/contrib/simple_cert/controllers.py @@ -0,0 +1,42 @@ +# 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 oslo_config import cfg +import webob + +from keystone.common import controller +from keystone.common import dependency +from keystone import exception + +CONF = cfg.CONF + + +@dependency.requires('token_provider_api') +class SimpleCert(controller.V3Controller): + + def _get_certificate(self, name): + try: + with open(name, 'r') as f: + body = f.read() + except IOError: + raise exception.CertificateFilesUnavailable() + + # NOTE(jamielennox): We construct the webob Response ourselves here so + # that we don't pass through the JSON encoding process. + headers = [('Content-Type', 'application/x-pem-file')] + return webob.Response(body=body, headerlist=headers, status="200 OK") + + def get_ca_certificate(self, context): + return self._get_certificate(CONF.signing.ca_certs) + + def list_certificates(self, context): + return self._get_certificate(CONF.signing.certfile) diff --git a/keystone-moon/keystone/contrib/simple_cert/core.py b/keystone-moon/keystone/contrib/simple_cert/core.py new file mode 100644 index 00000000..531c6aae --- /dev/null +++ b/keystone-moon/keystone/contrib/simple_cert/core.py @@ -0,0 +1,32 @@ +# 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 keystone.common import extension + +EXTENSION_DATA = { + 'name': 'OpenStack Simple Certificate API', + 'namespace': 'http://docs.openstack.org/identity/api/ext/' + 'OS-SIMPLE-CERT/v1.0', + 'alias': 'OS-SIMPLE-CERT', + 'updated': '2014-01-20T12:00:0-00:00', + 'description': 'OpenStack simple certificate retrieval extension', + 'links': [ + { + 'rel': 'describedby', + # TODO(dolph): link needs to be revised after + # bug 928059 merges + 'type': 'text/html', + 'href': 'https://github.com/openstack/identity-api', + } + ]} +extension.register_admin_extension(EXTENSION_DATA['alias'], EXTENSION_DATA) +extension.register_public_extension(EXTENSION_DATA['alias'], EXTENSION_DATA) diff --git a/keystone-moon/keystone/contrib/simple_cert/routers.py b/keystone-moon/keystone/contrib/simple_cert/routers.py new file mode 100644 index 00000000..8c36c2a4 --- /dev/null +++ b/keystone-moon/keystone/contrib/simple_cert/routers.py @@ -0,0 +1,41 @@ +# 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 functools + +from keystone.common import json_home +from keystone.common import wsgi +from keystone.contrib.simple_cert import controllers + + +build_resource_relation = functools.partial( + json_home.build_v3_extension_resource_relation, + extension_name='OS-SIMPLE-CERT', extension_version='1.0') + + +class SimpleCertExtension(wsgi.V3ExtensionRouter): + + PREFIX = 'OS-SIMPLE-CERT' + + def add_routes(self, mapper): + controller = controllers.SimpleCert() + + self._add_resource( + mapper, controller, + path='/%s/ca' % self.PREFIX, + get_action='get_ca_certificate', + rel=build_resource_relation(resource_name='ca_certificate')) + self._add_resource( + mapper, controller, + path='/%s/certificates' % self.PREFIX, + get_action='list_certificates', + rel=build_resource_relation(resource_name='certificates')) diff --git a/keystone-moon/keystone/contrib/user_crud/__init__.py b/keystone-moon/keystone/contrib/user_crud/__init__.py new file mode 100644 index 00000000..271ceee6 --- /dev/null +++ b/keystone-moon/keystone/contrib/user_crud/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2012 Red Hat, Inc +# +# 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 keystone.contrib.user_crud.core import * # noqa diff --git a/keystone-moon/keystone/contrib/user_crud/core.py b/keystone-moon/keystone/contrib/user_crud/core.py new file mode 100644 index 00000000..dd16d3a5 --- /dev/null +++ b/keystone-moon/keystone/contrib/user_crud/core.py @@ -0,0 +1,134 @@ +# Copyright 2012 Red Hat, Inc +# +# 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 +import uuid + +from oslo_log import log + +from keystone.common import dependency +from keystone.common import extension +from keystone.common import wsgi +from keystone import exception +from keystone import identity +from keystone.models import token_model + + +LOG = log.getLogger(__name__) + + +extension.register_public_extension( + 'OS-KSCRUD', { + 'name': 'OpenStack Keystone User CRUD', + 'namespace': 'http://docs.openstack.org/identity/api/ext/' + 'OS-KSCRUD/v1.0', + 'alias': 'OS-KSCRUD', + 'updated': '2013-07-07T12:00:0-00:00', + 'description': 'OpenStack extensions to Keystone v2.0 API ' + 'enabling User Operations.', + 'links': [ + { + 'rel': 'describedby', + # TODO(ayoung): needs a description + 'type': 'text/html', + 'href': 'https://github.com/openstack/identity-api', + } + ]}) + + +@dependency.requires('catalog_api', 'identity_api', 'resource_api', + 'token_provider_api') +class UserController(identity.controllers.User): + def set_user_password(self, context, user_id, user): + token_id = context.get('token_id') + original_password = user.get('original_password') + + token_data = self.token_provider_api.validate_token(token_id) + token_ref = token_model.KeystoneToken(token_id=token_id, + token_data=token_data) + + if token_ref.user_id != user_id: + raise exception.Forbidden('Token belongs to another user') + if original_password is None: + raise exception.ValidationError(target='user', + attribute='original password') + + try: + user_ref = self.identity_api.authenticate( + context, + user_id=token_ref.user_id, + password=original_password) + if not user_ref.get('enabled', True): + # NOTE(dolph): why can't you set a disabled user's password? + raise exception.Unauthorized('User is disabled') + except AssertionError: + raise exception.Unauthorized() + + update_dict = {'password': user['password'], 'id': user_id} + + admin_context = copy.copy(context) + admin_context['is_admin'] = True + super(UserController, self).set_user_password(admin_context, + user_id, + update_dict) + + # Issue a new token based upon the original token data. This will + # always be a V2.0 token. + + # TODO(morganfainberg): Add a mechanism to issue a new token directly + # from a token model so that this code can go away. This is likely + # not the norm as most cases do not need to yank apart a token to + # issue a new one. + new_token_ref = {} + metadata_ref = {} + roles_ref = None + + new_token_ref['user'] = user_ref + if token_ref.bind: + new_token_ref['bind'] = token_ref.bind + if token_ref.project_id: + new_token_ref['tenant'] = self.resource_api.get_project( + token_ref.project_id) + if token_ref.role_names: + roles_ref = [dict(name=value) + for value in token_ref.role_names] + if token_ref.role_ids: + metadata_ref['roles'] = token_ref.role_ids + if token_ref.trust_id: + metadata_ref['trust'] = { + 'id': token_ref.trust_id, + 'trustee_user_id': token_ref.trustee_user_id} + new_token_ref['metadata'] = metadata_ref + new_token_ref['id'] = uuid.uuid4().hex + + catalog_ref = self.catalog_api.get_catalog(user_id, + token_ref.project_id) + + new_token_id, new_token_data = self.token_provider_api.issue_v2_token( + token_ref=new_token_ref, roles_ref=roles_ref, + catalog_ref=catalog_ref) + LOG.debug('TOKEN_REF %s', new_token_data) + return new_token_data + + +class CrudExtension(wsgi.ExtensionRouter): + """Provides a subset of CRUD operations for internal data types.""" + + def add_routes(self, mapper): + user_controller = UserController() + + mapper.connect('/OS-KSCRUD/users/{user_id}', + controller=user_controller, + action='set_user_password', + conditions=dict(method=['PATCH'])) diff --git a/keystone-moon/keystone/controllers.py b/keystone-moon/keystone/controllers.py new file mode 100644 index 00000000..12f13c77 --- /dev/null +++ b/keystone-moon/keystone/controllers.py @@ -0,0 +1,218 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 oslo_log import log +from oslo_serialization import jsonutils +import webob + +from keystone.common import extension +from keystone.common import json_home +from keystone.common import wsgi +from keystone import exception + + +LOG = log.getLogger(__name__) + +MEDIA_TYPE_JSON = 'application/vnd.openstack.identity-%s+json' + +_VERSIONS = [] + +# NOTE(blk-u): latest_app will be set by keystone.service.loadapp(). It gets +# set to the application that was just loaded. In the case of keystone-all, +# loadapp() gets called twice, once for the public app and once for the admin +# app. In the case of httpd/keystone, loadapp() gets called once for the public +# app if this is the public instance or loadapp() gets called for the admin app +# if it's the admin instance. +# This is used to fetch the /v3 JSON Home response. The /v3 JSON Home response +# is the same whether it's the admin or public service so either admin or +# public works. +latest_app = None + + +def request_v3_json_home(new_prefix): + if 'v3' not in _VERSIONS: + # No V3 support, so return an empty JSON Home document. + return {'resources': {}} + + req = webob.Request.blank( + '/v3', headers={'Accept': 'application/json-home'}) + v3_json_home_str = req.get_response(latest_app).body + v3_json_home = jsonutils.loads(v3_json_home_str) + json_home.translate_urls(v3_json_home, new_prefix) + + return v3_json_home + + +class Extensions(wsgi.Application): + """Base extensions controller to be extended by public and admin API's.""" + + # extend in subclass to specify the set of extensions + @property + def extensions(self): + return None + + def get_extensions_info(self, context): + return {'extensions': {'values': self.extensions.values()}} + + def get_extension_info(self, context, extension_alias): + try: + return {'extension': self.extensions[extension_alias]} + except KeyError: + raise exception.NotFound(target=extension_alias) + + +class AdminExtensions(Extensions): + @property + def extensions(self): + return extension.ADMIN_EXTENSIONS + + +class PublicExtensions(Extensions): + @property + def extensions(self): + return extension.PUBLIC_EXTENSIONS + + +def register_version(version): + _VERSIONS.append(version) + + +class MimeTypes(object): + JSON = 'application/json' + JSON_HOME = 'application/json-home' + + +def v3_mime_type_best_match(context): + + # accept_header is a WebOb MIMEAccept object so supports best_match. + accept_header = context['accept_header'] + + if not accept_header: + return MimeTypes.JSON + + SUPPORTED_TYPES = [MimeTypes.JSON, MimeTypes.JSON_HOME] + return accept_header.best_match(SUPPORTED_TYPES) + + +class Version(wsgi.Application): + + def __init__(self, version_type, routers=None): + self.endpoint_url_type = version_type + self._routers = routers + + super(Version, self).__init__() + + def _get_identity_url(self, context, version): + """Returns a URL to keystone's own endpoint.""" + url = self.base_url(context, self.endpoint_url_type) + return '%s/%s/' % (url, version) + + def _get_versions_list(self, context): + """The list of versions is dependent on the context.""" + versions = {} + if 'v2.0' in _VERSIONS: + versions['v2.0'] = { + 'id': 'v2.0', + 'status': 'stable', + 'updated': '2014-04-17T00:00:00Z', + 'links': [ + { + 'rel': 'self', + 'href': self._get_identity_url(context, 'v2.0'), + }, { + 'rel': 'describedby', + 'type': 'text/html', + 'href': 'http://docs.openstack.org/' + } + ], + 'media-types': [ + { + 'base': 'application/json', + 'type': MEDIA_TYPE_JSON % 'v2.0' + } + ] + } + + if 'v3' in _VERSIONS: + versions['v3'] = { + 'id': 'v3.0', + 'status': 'stable', + 'updated': '2013-03-06T00:00:00Z', + 'links': [ + { + 'rel': 'self', + 'href': self._get_identity_url(context, 'v3'), + } + ], + 'media-types': [ + { + 'base': 'application/json', + 'type': MEDIA_TYPE_JSON % 'v3' + } + ] + } + + return versions + + def get_versions(self, context): + + req_mime_type = v3_mime_type_best_match(context) + if req_mime_type == MimeTypes.JSON_HOME: + v3_json_home = request_v3_json_home('/v3') + return wsgi.render_response( + body=v3_json_home, + headers=(('Content-Type', MimeTypes.JSON_HOME),)) + + versions = self._get_versions_list(context) + return wsgi.render_response(status=(300, 'Multiple Choices'), body={ + 'versions': { + 'values': versions.values() + } + }) + + def get_version_v2(self, context): + versions = self._get_versions_list(context) + if 'v2.0' in _VERSIONS: + return wsgi.render_response(body={ + 'version': versions['v2.0'] + }) + else: + raise exception.VersionNotFound(version='v2.0') + + def _get_json_home_v3(self): + + def all_resources(): + for router in self._routers: + for resource in router.v3_resources: + yield resource + + return { + 'resources': dict(all_resources()) + } + + def get_version_v3(self, context): + versions = self._get_versions_list(context) + if 'v3' in _VERSIONS: + req_mime_type = v3_mime_type_best_match(context) + + if req_mime_type == MimeTypes.JSON_HOME: + return wsgi.render_response( + body=self._get_json_home_v3(), + headers=(('Content-Type', MimeTypes.JSON_HOME),)) + + return wsgi.render_response(body={ + 'version': versions['v3'] + }) + else: + raise exception.VersionNotFound(version='v3') diff --git a/keystone-moon/keystone/credential/__init__.py b/keystone-moon/keystone/credential/__init__.py new file mode 100644 index 00000000..fc7b6317 --- /dev/null +++ b/keystone-moon/keystone/credential/__init__.py @@ -0,0 +1,17 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 keystone.credential import controllers # noqa +from keystone.credential.core import * # noqa +from keystone.credential import routers # noqa diff --git a/keystone-moon/keystone/credential/backends/__init__.py b/keystone-moon/keystone/credential/backends/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/credential/backends/sql.py b/keystone-moon/keystone/credential/backends/sql.py new file mode 100644 index 00000000..12daed3f --- /dev/null +++ b/keystone-moon/keystone/credential/backends/sql.py @@ -0,0 +1,104 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 keystone.common import sql +from keystone import credential +from keystone import exception + + +class CredentialModel(sql.ModelBase, sql.DictBase): + __tablename__ = 'credential' + attributes = ['id', 'user_id', 'project_id', 'blob', 'type'] + id = sql.Column(sql.String(64), primary_key=True) + user_id = sql.Column(sql.String(64), + nullable=False) + project_id = sql.Column(sql.String(64)) + blob = sql.Column(sql.JsonBlob(), nullable=False) + type = sql.Column(sql.String(255), nullable=False) + extra = sql.Column(sql.JsonBlob()) + + +class Credential(credential.Driver): + + # credential crud + + @sql.handle_conflicts(conflict_type='credential') + def create_credential(self, credential_id, credential): + session = sql.get_session() + with session.begin(): + ref = CredentialModel.from_dict(credential) + session.add(ref) + return ref.to_dict() + + @sql.truncated + def list_credentials(self, hints): + session = sql.get_session() + credentials = session.query(CredentialModel) + credentials = sql.filter_limit_query(CredentialModel, + credentials, hints) + return [s.to_dict() for s in credentials] + + def list_credentials_for_user(self, user_id): + session = sql.get_session() + query = session.query(CredentialModel) + refs = query.filter_by(user_id=user_id).all() + return [ref.to_dict() for ref in refs] + + def _get_credential(self, session, credential_id): + ref = session.query(CredentialModel).get(credential_id) + if ref is None: + raise exception.CredentialNotFound(credential_id=credential_id) + return ref + + def get_credential(self, credential_id): + session = sql.get_session() + return self._get_credential(session, credential_id).to_dict() + + @sql.handle_conflicts(conflict_type='credential') + def update_credential(self, credential_id, credential): + session = sql.get_session() + with session.begin(): + ref = self._get_credential(session, credential_id) + old_dict = ref.to_dict() + for k in credential: + old_dict[k] = credential[k] + new_credential = CredentialModel.from_dict(old_dict) + for attr in CredentialModel.attributes: + if attr != 'id': + setattr(ref, attr, getattr(new_credential, attr)) + ref.extra = new_credential.extra + return ref.to_dict() + + def delete_credential(self, credential_id): + session = sql.get_session() + + with session.begin(): + ref = self._get_credential(session, credential_id) + session.delete(ref) + + def delete_credentials_for_project(self, project_id): + session = sql.get_session() + + with session.begin(): + query = session.query(CredentialModel) + query = query.filter_by(project_id=project_id) + query.delete() + + def delete_credentials_for_user(self, user_id): + session = sql.get_session() + + with session.begin(): + query = session.query(CredentialModel) + query = query.filter_by(user_id=user_id) + query.delete() diff --git a/keystone-moon/keystone/credential/controllers.py b/keystone-moon/keystone/credential/controllers.py new file mode 100644 index 00000000..65c17278 --- /dev/null +++ b/keystone-moon/keystone/credential/controllers.py @@ -0,0 +1,108 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 hashlib + +from oslo_serialization import jsonutils + +from keystone.common import controller +from keystone.common import dependency +from keystone.common import validation +from keystone.credential import schema +from keystone import exception +from keystone.i18n import _ + + +@dependency.requires('credential_api') +class CredentialV3(controller.V3Controller): + collection_name = 'credentials' + member_name = 'credential' + + def __init__(self): + super(CredentialV3, self).__init__() + self.get_member_from_driver = self.credential_api.get_credential + + def _assign_unique_id(self, ref, trust_id=None): + # Generates and assigns a unique identifier to + # a credential reference. + if ref.get('type', '').lower() == 'ec2': + try: + blob = jsonutils.loads(ref.get('blob')) + except (ValueError, TypeError): + raise exception.ValidationError( + message=_('Invalid blob in credential')) + if not blob or not isinstance(blob, dict): + raise exception.ValidationError(attribute='blob', + target='credential') + if blob.get('access') is None: + raise exception.ValidationError(attribute='access', + target='blob') + ret_ref = ref.copy() + ret_ref['id'] = hashlib.sha256(blob['access']).hexdigest() + # Update the blob with the trust_id, so credentials created + # with a trust scoped token will result in trust scoped + # tokens when authentication via ec2tokens happens + if trust_id is not None: + blob['trust_id'] = trust_id + ret_ref['blob'] = jsonutils.dumps(blob) + return ret_ref + else: + return super(CredentialV3, self)._assign_unique_id(ref) + + @controller.protected() + @validation.validated(schema.credential_create, 'credential') + def create_credential(self, context, credential): + trust_id = self._get_trust_id_for_request(context) + ref = self._assign_unique_id(self._normalize_dict(credential), + trust_id) + ref = self.credential_api.create_credential(ref['id'], ref) + return CredentialV3.wrap_member(context, ref) + + @staticmethod + def _blob_to_json(ref): + # credentials stored via ec2tokens before the fix for #1259584 + # need json serializing, as that's the documented API format + blob = ref.get('blob') + if isinstance(blob, dict): + new_ref = ref.copy() + new_ref['blob'] = jsonutils.dumps(blob) + return new_ref + else: + return ref + + @controller.filterprotected('user_id') + def list_credentials(self, context, filters): + hints = CredentialV3.build_driver_hints(context, filters) + refs = self.credential_api.list_credentials(hints) + ret_refs = [self._blob_to_json(r) for r in refs] + return CredentialV3.wrap_collection(context, ret_refs, + hints=hints) + + @controller.protected() + def get_credential(self, context, credential_id): + ref = self.credential_api.get_credential(credential_id) + ret_ref = self._blob_to_json(ref) + return CredentialV3.wrap_member(context, ret_ref) + + @controller.protected() + @validation.validated(schema.credential_update, 'credential') + def update_credential(self, context, credential_id, credential): + self._require_matching_id(credential_id, credential) + + ref = self.credential_api.update_credential(credential_id, credential) + return CredentialV3.wrap_member(context, ref) + + @controller.protected() + def delete_credential(self, context, credential_id): + return self.credential_api.delete_credential(credential_id) diff --git a/keystone-moon/keystone/credential/core.py b/keystone-moon/keystone/credential/core.py new file mode 100644 index 00000000..d3354ea3 --- /dev/null +++ b/keystone-moon/keystone/credential/core.py @@ -0,0 +1,140 @@ +# Copyright 2013 OpenStack Foundation +# +# 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. + +"""Main entry point into the Credentials service.""" + +import abc + +from oslo_config import cfg +from oslo_log import log +import six + +from keystone.common import dependency +from keystone.common import driver_hints +from keystone.common import manager +from keystone import exception + + +CONF = cfg.CONF + +LOG = log.getLogger(__name__) + + +@dependency.provider('credential_api') +class Manager(manager.Manager): + """Default pivot point for the Credential backend. + + See :mod:`keystone.common.manager.Manager` for more details on how this + dynamically calls the backend. + + """ + + def __init__(self): + super(Manager, self).__init__(CONF.credential.driver) + + @manager.response_truncated + def list_credentials(self, hints=None): + return self.driver.list_credentials(hints or driver_hints.Hints()) + + +@six.add_metaclass(abc.ABCMeta) +class Driver(object): + # credential crud + + @abc.abstractmethod + def create_credential(self, credential_id, credential): + """Creates a new credential. + + :raises: keystone.exception.Conflict + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_credentials(self, hints): + """List all credentials. + + :param hints: contains the list of filters yet to be satisfied. + Any filters satisfied here will be removed so that + the caller will know if any filters remain. + + :returns: a list of credential_refs or an empty list. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_credentials_for_user(self, user_id): + """List credentials for a user. + + :param user_id: ID of a user to filter credentials by. + + :returns: a list of credential_refs or an empty list. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def get_credential(self, credential_id): + """Get a credential by ID. + + :returns: credential_ref + :raises: keystone.exception.CredentialNotFound + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def update_credential(self, credential_id, credential): + """Updates an existing credential. + + :raises: keystone.exception.CredentialNotFound, + keystone.exception.Conflict + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def delete_credential(self, credential_id): + """Deletes an existing credential. + + :raises: keystone.exception.CredentialNotFound + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def delete_credentials_for_project(self, project_id): + """Deletes all credentials for a project.""" + self._delete_credentials(lambda cr: cr['project_id'] == project_id) + + @abc.abstractmethod + def delete_credentials_for_user(self, user_id): + """Deletes all credentials for a user.""" + self._delete_credentials(lambda cr: cr['user_id'] == user_id) + + def _delete_credentials(self, match_fn): + """Do the actual credential deletion work (default implementation). + + :param match_fn: function that takes a credential dict as the + parameter and returns true or false if the + identifier matches the credential dict. + """ + for cr in self.list_credentials(): + if match_fn(cr): + try: + self.credential_api.delete_credential(cr['id']) + except exception.CredentialNotFound: + LOG.debug('Deletion of credential is not required: %s', + cr['id']) diff --git a/keystone-moon/keystone/credential/routers.py b/keystone-moon/keystone/credential/routers.py new file mode 100644 index 00000000..db3651f4 --- /dev/null +++ b/keystone-moon/keystone/credential/routers.py @@ -0,0 +1,28 @@ +# Copyright 2013 OpenStack Foundation +# +# 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. + +"""WSGI Routers for the Credentials service.""" + +from keystone.common import router +from keystone.common import wsgi +from keystone.credential import controllers + + +class Routers(wsgi.RoutersBase): + + def append_v3_routers(self, mapper, routers): + routers.append( + router.Router(controllers.CredentialV3(), + 'credentials', 'credential', + resource_descriptions=self.v3_resources)) diff --git a/keystone-moon/keystone/credential/schema.py b/keystone-moon/keystone/credential/schema.py new file mode 100644 index 00000000..749f0c0a --- /dev/null +++ b/keystone-moon/keystone/credential/schema.py @@ -0,0 +1,62 @@ +# 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. + + +_credential_properties = { + 'blob': { + 'type': 'string' + }, + 'project_id': { + 'type': 'string' + }, + 'type': { + 'type': 'string' + }, + 'user_id': { + 'type': 'string' + } +} + +credential_create = { + 'type': 'object', + 'properties': _credential_properties, + 'additionalProperties': True, + 'oneOf': [ + { + 'title': 'ec2 credential requires project_id', + 'required': ['blob', 'type', 'user_id', 'project_id'], + 'properties': { + 'type': { + 'enum': ['ec2'] + } + } + }, + { + 'title': 'non-ec2 credential does not require project_id', + 'required': ['blob', 'type', 'user_id'], + 'properties': { + 'type': { + 'not': { + 'enum': ['ec2'] + } + } + } + } + ] +} + +credential_update = { + 'type': 'object', + 'properties': _credential_properties, + 'minProperties': 1, + 'additionalProperties': True +} diff --git a/keystone-moon/keystone/exception.py b/keystone-moon/keystone/exception.py new file mode 100644 index 00000000..6749fdcd --- /dev/null +++ b/keystone-moon/keystone/exception.py @@ -0,0 +1,469 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 oslo_config import cfg +from oslo_log import log +from oslo_utils import encodeutils +import six + +from keystone.i18n import _, _LW + + +CONF = cfg.CONF +LOG = log.getLogger(__name__) + +# Tests use this to make exception message format errors fatal +_FATAL_EXCEPTION_FORMAT_ERRORS = False + + +class Error(Exception): + """Base error class. + + Child classes should define an HTTP status code, title, and a + message_format. + + """ + code = None + title = None + message_format = None + + def __init__(self, message=None, **kwargs): + try: + message = self._build_message(message, **kwargs) + except KeyError: + # if you see this warning in your logs, please raise a bug report + if _FATAL_EXCEPTION_FORMAT_ERRORS: + raise + else: + LOG.warning(_LW('missing exception kwargs (programmer error)')) + message = self.message_format + + super(Error, self).__init__(message) + + def _build_message(self, message, **kwargs): + """Builds and returns an exception message. + + :raises: KeyError given insufficient kwargs + + """ + if not message: + try: + message = self.message_format % kwargs + except UnicodeDecodeError: + try: + kwargs = {k: encodeutils.safe_decode(v) + for k, v in six.iteritems(kwargs)} + except UnicodeDecodeError: + # NOTE(jamielennox): This is the complete failure case + # at least by showing the template we have some idea + # of where the error is coming from + message = self.message_format + else: + message = self.message_format % kwargs + + return message + + +class ValidationError(Error): + message_format = _("Expecting to find %(attribute)s in %(target)s -" + " the server could not comply with the request" + " since it is either malformed or otherwise" + " incorrect. The client is assumed to be in error.") + code = 400 + title = 'Bad Request' + + +class SchemaValidationError(ValidationError): + # NOTE(lbragstad): For whole OpenStack message consistency, this error + # message has been written in a format consistent with WSME. + message_format = _("%(detail)s") + + +class ValidationTimeStampError(Error): + message_format = _("Timestamp not in expected format." + " The server could not comply with the request" + " since it is either malformed or otherwise" + " incorrect. The client is assumed to be in error.") + code = 400 + title = 'Bad Request' + + +class StringLengthExceeded(ValidationError): + message_format = _("String length exceeded.The length of" + " string '%(string)s' exceeded the limit" + " of column %(type)s(CHAR(%(length)d)).") + + +class ValidationSizeError(Error): + message_format = _("Request attribute %(attribute)s must be" + " less than or equal to %(size)i. The server" + " could not comply with the request because" + " the attribute size is invalid (too large)." + " The client is assumed to be in error.") + code = 400 + title = 'Bad Request' + + +class CircularRegionHierarchyError(Error): + message_format = _("The specified parent region %(parent_region_id)s " + "would create a circular region hierarchy.") + code = 400 + title = 'Bad Request' + + +class PasswordVerificationError(Error): + message_format = _("The password length must be less than or equal " + "to %(size)i. The server could not comply with the " + "request because the password is invalid.") + code = 403 + title = 'Forbidden' + + +class RegionDeletionError(Error): + message_format = _("Unable to delete region %(region_id)s because it or " + "its child regions have associated endpoints.") + code = 403 + title = 'Forbidden' + + +class PKITokenExpected(Error): + message_format = _('The certificates you requested are not available. ' + 'It is likely that this server does not use PKI tokens ' + 'otherwise this is the result of misconfiguration.') + code = 403 + title = 'Cannot retrieve certificates' + + +class SecurityError(Error): + """Avoids exposing details of security failures, unless in debug mode.""" + amendment = _('(Disable debug mode to suppress these details.)') + + def _build_message(self, message, **kwargs): + """Only returns detailed messages in debug mode.""" + if CONF.debug: + return _('%(message)s %(amendment)s') % { + 'message': message or self.message_format % kwargs, + 'amendment': self.amendment} + else: + return self.message_format % kwargs + + +class Unauthorized(SecurityError): + message_format = _("The request you have made requires authentication.") + code = 401 + title = 'Unauthorized' + + +class AuthPluginException(Unauthorized): + message_format = _("Authentication plugin error.") + + def __init__(self, *args, **kwargs): + super(AuthPluginException, self).__init__(*args, **kwargs) + self.authentication = {} + + +class MissingGroups(Unauthorized): + message_format = _("Unable to find valid groups while using " + "mapping %(mapping_id)s") + + +class AuthMethodNotSupported(AuthPluginException): + message_format = _("Attempted to authenticate with an unsupported method.") + + def __init__(self, *args, **kwargs): + super(AuthMethodNotSupported, self).__init__(*args, **kwargs) + self.authentication = {'methods': CONF.auth.methods} + + +class AdditionalAuthRequired(AuthPluginException): + message_format = _("Additional authentications steps required.") + + def __init__(self, auth_response=None, **kwargs): + super(AdditionalAuthRequired, self).__init__(message=None, **kwargs) + self.authentication = auth_response + + +class Forbidden(SecurityError): + message_format = _("You are not authorized to perform the" + " requested action.") + code = 403 + title = 'Forbidden' + + +class ForbiddenAction(Forbidden): + message_format = _("You are not authorized to perform the" + " requested action: %(action)s") + + +class ImmutableAttributeError(Forbidden): + message_format = _("Could not change immutable attribute(s) " + "'%(attributes)s' in target %(target)s") + + +class CrossBackendNotAllowed(Forbidden): + message_format = _("Group membership across backend boundaries is not " + "allowed, group in question is %(group_id)s, " + "user is %(user_id)s") + + +class InvalidPolicyAssociation(Forbidden): + message_format = _("Invalid mix of entities for policy association - " + "only Endpoint, Service or Region+Service allowed. " + "Request was - Endpoint: %(endpoint_id)s, " + "Service: %(service_id)s, Region: %(region_id)s") + + +class InvalidDomainConfig(Forbidden): + message_format = _("Invalid domain specific configuration: %(reason)s") + + +class NotFound(Error): + message_format = _("Could not find: %(target)s") + code = 404 + title = 'Not Found' + + +class EndpointNotFound(NotFound): + message_format = _("Could not find endpoint: %(endpoint_id)s") + + +class MetadataNotFound(NotFound): + """(dolph): metadata is not a user-facing concept, + so this exception should not be exposed + """ + message_format = _("An unhandled exception has occurred:" + " Could not find metadata.") + + +class PolicyNotFound(NotFound): + message_format = _("Could not find policy: %(policy_id)s") + + +class PolicyAssociationNotFound(NotFound): + message_format = _("Could not find policy association") + + +class RoleNotFound(NotFound): + message_format = _("Could not find role: %(role_id)s") + + +class RoleAssignmentNotFound(NotFound): + message_format = _("Could not find role assignment with role: " + "%(role_id)s, user or group: %(actor_id)s, " + "project or domain: %(target_id)s") + + +class RegionNotFound(NotFound): + message_format = _("Could not find region: %(region_id)s") + + +class ServiceNotFound(NotFound): + message_format = _("Could not find service: %(service_id)s") + + +class DomainNotFound(NotFound): + message_format = _("Could not find domain: %(domain_id)s") + + +class ProjectNotFound(NotFound): + message_format = _("Could not find project: %(project_id)s") + + +class InvalidParentProject(NotFound): + message_format = _("Cannot create project with parent: %(project_id)s") + + +class TokenNotFound(NotFound): + message_format = _("Could not find token: %(token_id)s") + + +class UserNotFound(NotFound): + message_format = _("Could not find user: %(user_id)s") + + +class GroupNotFound(NotFound): + message_format = _("Could not find group: %(group_id)s") + + +class MappingNotFound(NotFound): + message_format = _("Could not find mapping: %(mapping_id)s") + + +class TrustNotFound(NotFound): + message_format = _("Could not find trust: %(trust_id)s") + + +class TrustUseLimitReached(Forbidden): + message_format = _("No remaining uses for trust: %(trust_id)s") + + +class CredentialNotFound(NotFound): + message_format = _("Could not find credential: %(credential_id)s") + + +class VersionNotFound(NotFound): + message_format = _("Could not find version: %(version)s") + + +class EndpointGroupNotFound(NotFound): + message_format = _("Could not find Endpoint Group: %(endpoint_group_id)s") + + +class IdentityProviderNotFound(NotFound): + message_format = _("Could not find Identity Provider: %(idp_id)s") + + +class ServiceProviderNotFound(NotFound): + message_format = _("Could not find Service Provider: %(sp_id)s") + + +class FederatedProtocolNotFound(NotFound): + message_format = _("Could not find federated protocol %(protocol_id)s for" + " Identity Provider: %(idp_id)s") + + +class PublicIDNotFound(NotFound): + # This is used internally and mapped to either User/GroupNotFound or, + # Assertion before the exception leaves Keystone. + message_format = "%(id)s" + + +class DomainConfigNotFound(NotFound): + message_format = _('Could not find %(group_or_option)s in domain ' + 'configuration for domain %(domain_id)s') + + +class Conflict(Error): + message_format = _("Conflict occurred attempting to store %(type)s -" + " %(details)s") + code = 409 + title = 'Conflict' + + +class UnexpectedError(SecurityError): + """Avoids exposing details of failures, unless in debug mode.""" + _message_format = _("An unexpected error prevented the server " + "from fulfilling your request.") + + debug_message_format = _("An unexpected error prevented the server " + "from fulfilling your request: %(exception)s") + + @property + def message_format(self): + """Return the generic message format string unless debug is enabled.""" + if CONF.debug: + return self.debug_message_format + return self._message_format + + def _build_message(self, message, **kwargs): + if CONF.debug and 'exception' not in kwargs: + # Ensure that exception has a value to be extra defensive for + # substitutions and make sure the exception doesn't raise an + # exception. + kwargs['exception'] = '' + return super(UnexpectedError, self)._build_message(message, **kwargs) + + code = 500 + title = 'Internal Server Error' + + +class TrustConsumeMaximumAttempt(UnexpectedError): + debug_message_format = _("Unable to consume trust %(trust_id)s, unable to " + "acquire lock.") + + +class CertificateFilesUnavailable(UnexpectedError): + debug_message_format = _("Expected signing certificates are not available " + "on the server. Please check Keystone " + "configuration.") + + +class MalformedEndpoint(UnexpectedError): + debug_message_format = _("Malformed endpoint URL (%(endpoint)s)," + " see ERROR log for details.") + + +class MappedGroupNotFound(UnexpectedError): + debug_message_format = _("Group %(group_id)s returned by mapping " + "%(mapping_id)s was not found in the backend.") + + +class MetadataFileError(UnexpectedError): + message_format = _("Error while reading metadata file, %(reason)s") + + +class AssignmentTypeCalculationError(UnexpectedError): + message_format = _( + 'Unexpected combination of grant attributes - ' + 'User: %(user_id)s, Group: %(group_id)s, Project: %(project_id)s, ' + 'Domain: %(domain_id)s') + + +class NotImplemented(Error): + message_format = _("The action you have requested has not" + " been implemented.") + code = 501 + title = 'Not Implemented' + + +class Gone(Error): + message_format = _("The service you have requested is no" + " longer available on this server.") + code = 410 + title = 'Gone' + + +class ConfigFileNotFound(UnexpectedError): + debug_message_format = _("The Keystone configuration file %(config_file)s " + "could not be found.") + + +class KeysNotFound(UnexpectedError): + message_format = _('No encryption keys found; run keystone-manage ' + 'fernet_setup to bootstrap one.') + + +class MultipleSQLDriversInConfig(UnexpectedError): + message_format = _('The Keystone domain-specific configuration has ' + 'specified more than one SQL driver (only one is ' + 'permitted): %(source)s.') + + +class MigrationNotProvided(Exception): + def __init__(self, mod_name, path): + super(MigrationNotProvided, self).__init__(_( + "%(mod_name)s doesn't provide database migrations. The migration" + " repository path at %(path)s doesn't exist or isn't a directory." + ) % {'mod_name': mod_name, 'path': path}) + + +class UnsupportedTokenVersionException(Exception): + """Token version is unrecognizable or unsupported.""" + pass + + +class SAMLSigningError(UnexpectedError): + debug_message_format = _('Unable to sign SAML assertion. It is likely ' + 'that this server does not have xmlsec1 ' + 'installed, or this is the result of ' + 'misconfiguration. Reason %(reason)s') + title = 'Error signing SAML assertion' + + +class OAuthHeadersMissingError(UnexpectedError): + debug_message_format = _('No Authorization headers found, cannot proceed ' + 'with OAuth related calls, if running under ' + 'HTTPd or Apache, ensure WSGIPassAuthorization ' + 'is set to On.') + title = 'Error retrieving OAuth headers' diff --git a/keystone-moon/keystone/hacking/__init__.py b/keystone-moon/keystone/hacking/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/hacking/checks.py b/keystone-moon/keystone/hacking/checks.py new file mode 100644 index 00000000..5d715d91 --- /dev/null +++ b/keystone-moon/keystone/hacking/checks.py @@ -0,0 +1,446 @@ +# 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. + +"""Keystone's pep8 extensions. + +In order to make the review process faster and easier for core devs we are +adding some Keystone specific pep8 checks. This will catch common errors +so that core devs don't have to. + +There are two types of pep8 extensions. One is a function that takes either +a physical or logical line. The physical or logical line is the first param +in the function definition and can be followed by other parameters supported +by pep8. The second type is a class that parses AST trees. For more info +please see pep8.py. +""" + +import ast +import re + +import six + + +class BaseASTChecker(ast.NodeVisitor): + """Provides a simple framework for writing AST-based checks. + + Subclasses should implement visit_* methods like any other AST visitor + implementation. When they detect an error for a particular node the + method should call ``self.add_error(offending_node)``. Details about + where in the code the error occurred will be pulled from the node + object. + + Subclasses should also provide a class variable named CHECK_DESC to + be used for the human readable error message. + + """ + + def __init__(self, tree, filename): + """This object is created automatically by pep8. + + :param tree: an AST tree + :param filename: name of the file being analyzed + (ignored by our checks) + """ + self._tree = tree + self._errors = [] + + def run(self): + """Called automatically by pep8.""" + self.visit(self._tree) + return self._errors + + def add_error(self, node, message=None): + """Add an error caused by a node to the list of errors for pep8.""" + message = message or self.CHECK_DESC + error = (node.lineno, node.col_offset, message, self.__class__) + self._errors.append(error) + + +class CheckForMutableDefaultArgs(BaseASTChecker): + """Checks for the use of mutable objects as function/method defaults. + + We are only checking for list and dict literals at this time. This means + that a developer could specify an instance of their own and cause a bug. + The fix for this is probably more work than it's worth because it will + get caught during code review. + + """ + + CHECK_DESC = 'K001 Using mutable as a function/method default' + MUTABLES = ( + ast.List, ast.ListComp, + ast.Dict, ast.DictComp, + ast.Set, ast.SetComp, + ast.Call) + + def visit_FunctionDef(self, node): + for arg in node.args.defaults: + if isinstance(arg, self.MUTABLES): + self.add_error(arg) + + super(CheckForMutableDefaultArgs, self).generic_visit(node) + + +def block_comments_begin_with_a_space(physical_line, line_number): + """There should be a space after the # of block comments. + + There is already a check in pep8 that enforces this rule for + inline comments. + + Okay: # this is a comment + Okay: #!/usr/bin/python + Okay: # this is a comment + K002: #this is a comment + + """ + MESSAGE = "K002 block comments should start with '# '" + + # shebangs are OK + if line_number == 1 and physical_line.startswith('#!'): + return + + text = physical_line.strip() + if text.startswith('#'): # look for block comments + if len(text) > 1 and not text[1].isspace(): + return physical_line.index('#'), MESSAGE + + +class CheckForAssertingNoneEquality(BaseASTChecker): + """Ensures that code does not use a None with assert(Not*)Equal.""" + + CHECK_DESC_IS = ('K003 Use self.assertIsNone(...) when comparing ' + 'against None') + CHECK_DESC_ISNOT = ('K004 Use assertIsNotNone(...) when comparing ' + ' against None') + + def visit_Call(self, node): + # NOTE(dstanek): I wrote this in a verbose way to make it easier to + # read for those that have little experience with Python's AST. + + if isinstance(node.func, ast.Attribute): + if node.func.attr == 'assertEqual': + for arg in node.args: + if isinstance(arg, ast.Name) and arg.id == 'None': + self.add_error(node, message=self.CHECK_DESC_IS) + elif node.func.attr == 'assertNotEqual': + for arg in node.args: + if isinstance(arg, ast.Name) and arg.id == 'None': + self.add_error(node, message=self.CHECK_DESC_ISNOT) + + super(CheckForAssertingNoneEquality, self).generic_visit(node) + + +class CheckForLoggingIssues(BaseASTChecker): + + DEBUG_CHECK_DESC = 'K005 Using translated string in debug logging' + NONDEBUG_CHECK_DESC = 'K006 Not using translating helper for logging' + EXCESS_HELPER_CHECK_DESC = 'K007 Using hints when _ is necessary' + LOG_MODULES = ('logging', 'keystone.openstack.common.log') + I18N_MODULES = ( + 'keystone.i18n._', + 'keystone.i18n._LI', + 'keystone.i18n._LW', + 'keystone.i18n._LE', + 'keystone.i18n._LC', + ) + TRANS_HELPER_MAP = { + 'debug': None, + 'info': '_LI', + 'warn': '_LW', + 'warning': '_LW', + 'error': '_LE', + 'exception': '_LE', + 'critical': '_LC', + } + + def __init__(self, tree, filename): + super(CheckForLoggingIssues, self).__init__(tree, filename) + + self.logger_names = [] + self.logger_module_names = [] + self.i18n_names = {} + + # NOTE(dstanek): this kinda accounts for scopes when talking + # about only leaf node in the graph + self.assignments = {} + + def generic_visit(self, node): + """Called if no explicit visitor function exists for a node.""" + for field, value in ast.iter_fields(node): + if isinstance(value, list): + for item in value: + if isinstance(item, ast.AST): + item._parent = node + self.visit(item) + elif isinstance(value, ast.AST): + value._parent = node + self.visit(value) + + def _filter_imports(self, module_name, alias): + """Keeps lists of logging and i18n imports + + """ + if module_name in self.LOG_MODULES: + self.logger_module_names.append(alias.asname or alias.name) + elif module_name in self.I18N_MODULES: + self.i18n_names[alias.asname or alias.name] = alias.name + + def visit_Import(self, node): + for alias in node.names: + self._filter_imports(alias.name, alias) + return super(CheckForLoggingIssues, self).generic_visit(node) + + def visit_ImportFrom(self, node): + for alias in node.names: + full_name = '%s.%s' % (node.module, alias.name) + self._filter_imports(full_name, alias) + return super(CheckForLoggingIssues, self).generic_visit(node) + + def _find_name(self, node): + """Return the fully qualified name or a Name or Attribute.""" + if isinstance(node, ast.Name): + return node.id + elif (isinstance(node, ast.Attribute) + and isinstance(node.value, (ast.Name, ast.Attribute))): + method_name = node.attr + obj_name = self._find_name(node.value) + if obj_name is None: + return None + return obj_name + '.' + method_name + elif isinstance(node, six.string_types): + return node + else: # could be Subscript, Call or many more + return None + + def visit_Assign(self, node): + """Look for 'LOG = logging.getLogger' + + This handles the simple case: + name = [logging_module].getLogger(...) + + - or - + + name = [i18n_name](...) + + And some much more comple ones: + name = [i18n_name](...) % X + + - or - + + self.name = [i18n_name](...) % X + + """ + attr_node_types = (ast.Name, ast.Attribute) + + if (len(node.targets) != 1 + or not isinstance(node.targets[0], attr_node_types)): + # say no to: "x, y = ..." + return super(CheckForLoggingIssues, self).generic_visit(node) + + target_name = self._find_name(node.targets[0]) + + if (isinstance(node.value, ast.BinOp) and + isinstance(node.value.op, ast.Mod)): + if (isinstance(node.value.left, ast.Call) and + isinstance(node.value.left.func, ast.Name) and + node.value.left.func.id in self.i18n_names): + # NOTE(dstanek): this is done to match cases like: + # `msg = _('something %s') % x` + node = ast.Assign(value=node.value.left) + + if not isinstance(node.value, ast.Call): + # node.value must be a call to getLogger + self.assignments.pop(target_name, None) + return super(CheckForLoggingIssues, self).generic_visit(node) + + # is this a call to an i18n function? + if (isinstance(node.value.func, ast.Name) + and node.value.func.id in self.i18n_names): + self.assignments[target_name] = node.value.func.id + return super(CheckForLoggingIssues, self).generic_visit(node) + + if (not isinstance(node.value.func, ast.Attribute) + or not isinstance(node.value.func.value, attr_node_types)): + # function must be an attribute on an object like + # logging.getLogger + return super(CheckForLoggingIssues, self).generic_visit(node) + + object_name = self._find_name(node.value.func.value) + func_name = node.value.func.attr + + if (object_name in self.logger_module_names + and func_name == 'getLogger'): + self.logger_names.append(target_name) + + return super(CheckForLoggingIssues, self).generic_visit(node) + + def visit_Call(self, node): + """Look for the 'LOG.*' calls. + + """ + + # obj.method + if isinstance(node.func, ast.Attribute): + obj_name = self._find_name(node.func.value) + if isinstance(node.func.value, ast.Name): + method_name = node.func.attr + elif isinstance(node.func.value, ast.Attribute): + obj_name = self._find_name(node.func.value) + method_name = node.func.attr + else: # could be Subscript, Call or many more + return super(CheckForLoggingIssues, self).generic_visit(node) + + # must be a logger instance and one of the support logging methods + if (obj_name not in self.logger_names + or method_name not in self.TRANS_HELPER_MAP): + return super(CheckForLoggingIssues, self).generic_visit(node) + + # the call must have arguments + if not len(node.args): + return super(CheckForLoggingIssues, self).generic_visit(node) + + if method_name == 'debug': + self._process_debug(node) + elif method_name in self.TRANS_HELPER_MAP: + self._process_non_debug(node, method_name) + + return super(CheckForLoggingIssues, self).generic_visit(node) + + def _process_debug(self, node): + msg = node.args[0] # first arg to a logging method is the msg + + # if first arg is a call to a i18n name + if (isinstance(msg, ast.Call) + and isinstance(msg.func, ast.Name) + and msg.func.id in self.i18n_names): + self.add_error(msg, message=self.DEBUG_CHECK_DESC) + + # if the first arg is a reference to a i18n call + elif (isinstance(msg, ast.Name) + and msg.id in self.assignments + and not self._is_raised_later(node, msg.id)): + self.add_error(msg, message=self.DEBUG_CHECK_DESC) + + def _process_non_debug(self, node, method_name): + msg = node.args[0] # first arg to a logging method is the msg + + # if first arg is a call to a i18n name + if isinstance(msg, ast.Call): + try: + func_name = msg.func.id + except AttributeError: + # in the case of logging only an exception, the msg function + # will not have an id associated with it, for instance: + # LOG.warning(six.text_type(e)) + return + + # the function name is the correct translation helper + # for the logging method + if func_name == self.TRANS_HELPER_MAP[method_name]: + return + + # the function name is an alias for the correct translation + # helper for the loggine method + if (self.i18n_names[func_name] == + self.TRANS_HELPER_MAP[method_name]): + return + + self.add_error(msg, message=self.NONDEBUG_CHECK_DESC) + + # if the first arg is not a reference to the correct i18n hint + elif isinstance(msg, ast.Name): + + # FIXME(dstanek): to make sure more robust we should be checking + # all names passed into a logging method. we can't right now + # because: + # 1. We have code like this that we'll fix when dealing with the %: + # msg = _('....') % {} + # LOG.warn(msg) + # 2. We also do LOG.exception(e) in several places. I'm not sure + # exactly what we should be doing about that. + if msg.id not in self.assignments: + return + + helper_method_name = self.TRANS_HELPER_MAP[method_name] + if (self.assignments[msg.id] != helper_method_name + and not self._is_raised_later(node, msg.id)): + self.add_error(msg, message=self.NONDEBUG_CHECK_DESC) + elif (self.assignments[msg.id] == helper_method_name + and self._is_raised_later(node, msg.id)): + self.add_error(msg, message=self.EXCESS_HELPER_CHECK_DESC) + + def _is_raised_later(self, node, name): + + def find_peers(node): + node_for_line = node._parent + for _field, value in ast.iter_fields(node._parent._parent): + if isinstance(value, list) and node_for_line in value: + return value[value.index(node_for_line) + 1:] + continue + return [] + + peers = find_peers(node) + for peer in peers: + if isinstance(peer, ast.Raise): + if (isinstance(peer.type, ast.Call) and + len(peer.type.args) > 0 and + isinstance(peer.type.args[0], ast.Name) and + name in (a.id for a in peer.type.args)): + return True + else: + return False + elif isinstance(peer, ast.Assign): + if name in (t.id for t in peer.targets): + return False + + +def check_oslo_namespace_imports(logical_line, blank_before, filename): + oslo_namespace_imports = re.compile( + r"(((from)|(import))\s+oslo\.)|(from\s+oslo\s+import\s+)") + + if re.match(oslo_namespace_imports, logical_line): + msg = ("K333: '%s' must be used instead of '%s'.") % ( + logical_line.replace('oslo.', 'oslo_'), + logical_line) + yield(0, msg) + + +def dict_constructor_with_sequence_copy(logical_line): + """Should use a dict comprehension instead of a dict constructor. + + PEP-0274 introduced dict comprehension with performance enhancement + and it also makes code more readable. + + Okay: lower_res = {k.lower(): v for k, v in six.iteritems(res[1])} + Okay: fool = dict(a='a', b='b') + K008: lower_res = dict((k.lower(), v) for k, v in six.iteritems(res[1])) + K008: attrs = dict([(k, _from_json(v)) + K008: dict([[i,i] for i in range(3)]) + + """ + MESSAGE = ("K008 Must use a dict comprehension instead of a dict" + " constructor with a sequence of key-value pairs.") + + dict_constructor_with_sequence_re = ( + re.compile(r".*\bdict\((\[)?(\(|\[)(?!\{)")) + + if dict_constructor_with_sequence_re.match(logical_line): + yield (0, MESSAGE) + + +def factory(register): + register(CheckForMutableDefaultArgs) + register(block_comments_begin_with_a_space) + register(CheckForAssertingNoneEquality) + register(CheckForLoggingIssues) + register(check_oslo_namespace_imports) + register(dict_constructor_with_sequence_copy) diff --git a/keystone-moon/keystone/i18n.py b/keystone-moon/keystone/i18n.py new file mode 100644 index 00000000..2eb42d3a --- /dev/null +++ b/keystone-moon/keystone/i18n.py @@ -0,0 +1,37 @@ +# Copyright 2014 IBM Corp. +# +# 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. + +"""oslo.i18n integration module. + +See http://docs.openstack.org/developer/oslo.i18n/usage.html . + +""" + +import oslo_i18n + + +_translators = oslo_i18n.TranslatorFactory(domain='keystone') + +# The primary translation function using the well-known name "_" +_ = _translators.primary + +# Translators for log levels. +# +# The abbreviated names are meant to reflect the usual use of a short +# name like '_'. The "L" is for "log" and the other letter comes from +# the level. +_LI = _translators.log_info +_LW = _translators.log_warning +_LE = _translators.log_error +_LC = _translators.log_critical diff --git a/keystone-moon/keystone/identity/__init__.py b/keystone-moon/keystone/identity/__init__.py new file mode 100644 index 00000000..3063b5ca --- /dev/null +++ b/keystone-moon/keystone/identity/__init__.py @@ -0,0 +1,18 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 keystone.identity import controllers # noqa +from keystone.identity.core import * # noqa +from keystone.identity import generator # noqa +from keystone.identity import routers # noqa diff --git a/keystone-moon/keystone/identity/backends/__init__.py b/keystone-moon/keystone/identity/backends/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/identity/backends/ldap.py b/keystone-moon/keystone/identity/backends/ldap.py new file mode 100644 index 00000000..0f7ee450 --- /dev/null +++ b/keystone-moon/keystone/identity/backends/ldap.py @@ -0,0 +1,402 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 __future__ import absolute_import +import uuid + +import ldap +import ldap.filter +from oslo_config import cfg +from oslo_log import log +import six + +from keystone import clean +from keystone.common import driver_hints +from keystone.common import ldap as common_ldap +from keystone.common import models +from keystone import exception +from keystone.i18n import _ +from keystone import identity + + +CONF = cfg.CONF +LOG = log.getLogger(__name__) + + +class Identity(identity.Driver): + def __init__(self, conf=None): + super(Identity, self).__init__() + if conf is None: + conf = CONF + self.user = UserApi(conf) + self.group = GroupApi(conf) + + def default_assignment_driver(self): + return "keystone.assignment.backends.ldap.Assignment" + + def is_domain_aware(self): + return False + + def generates_uuids(self): + return False + + # Identity interface + + def authenticate(self, user_id, password): + try: + user_ref = self._get_user(user_id) + except exception.UserNotFound: + raise AssertionError(_('Invalid user / password')) + if not user_id or not password: + raise AssertionError(_('Invalid user / password')) + conn = None + try: + conn = self.user.get_connection(user_ref['dn'], + password, end_user_auth=True) + if not conn: + raise AssertionError(_('Invalid user / password')) + except Exception: + raise AssertionError(_('Invalid user / password')) + finally: + if conn: + conn.unbind_s() + return self.user.filter_attributes(user_ref) + + def _get_user(self, user_id): + return self.user.get(user_id) + + def get_user(self, user_id): + return self.user.get_filtered(user_id) + + def list_users(self, hints): + return self.user.get_all_filtered(hints) + + def get_user_by_name(self, user_name, domain_id): + # domain_id will already have been handled in the Manager layer, + # parameter left in so this matches the Driver specification + return self.user.filter_attributes(self.user.get_by_name(user_name)) + + # CRUD + def create_user(self, user_id, user): + self.user.check_allow_create() + user_ref = self.user.create(user) + return self.user.filter_attributes(user_ref) + + def update_user(self, user_id, user): + self.user.check_allow_update() + old_obj = self.user.get(user_id) + if 'name' in user and old_obj.get('name') != user['name']: + raise exception.Conflict(_('Cannot change user name')) + + if self.user.enabled_mask: + self.user.mask_enabled_attribute(user) + elif self.user.enabled_invert and not self.user.enabled_emulation: + # We need to invert the enabled value for the old model object + # to prevent the LDAP update code from thinking that the enabled + # values are already equal. + user['enabled'] = not user['enabled'] + old_obj['enabled'] = not old_obj['enabled'] + + self.user.update(user_id, user, old_obj) + return self.user.get_filtered(user_id) + + def delete_user(self, user_id): + self.user.check_allow_delete() + user = self.user.get(user_id) + user_dn = user['dn'] + groups = self.group.list_user_groups(user_dn) + for group in groups: + self.group.remove_user(user_dn, group['id'], user_id) + + if hasattr(user, 'tenant_id'): + self.project.remove_user(user.tenant_id, user_dn) + self.user.delete(user_id) + + def create_group(self, group_id, group): + self.group.check_allow_create() + group['name'] = clean.group_name(group['name']) + return common_ldap.filter_entity(self.group.create(group)) + + def get_group(self, group_id): + return self.group.get_filtered(group_id) + + def get_group_by_name(self, group_name, domain_id): + # domain_id will already have been handled in the Manager layer, + # parameter left in so this matches the Driver specification + return self.group.get_filtered_by_name(group_name) + + def update_group(self, group_id, group): + self.group.check_allow_update() + if 'name' in group: + group['name'] = clean.group_name(group['name']) + return common_ldap.filter_entity(self.group.update(group_id, group)) + + def delete_group(self, group_id): + self.group.check_allow_delete() + return self.group.delete(group_id) + + def add_user_to_group(self, user_id, group_id): + user_ref = self._get_user(user_id) + user_dn = user_ref['dn'] + self.group.add_user(user_dn, group_id, user_id) + + def remove_user_from_group(self, user_id, group_id): + user_ref = self._get_user(user_id) + user_dn = user_ref['dn'] + self.group.remove_user(user_dn, group_id, user_id) + + def list_groups_for_user(self, user_id, hints): + user_ref = self._get_user(user_id) + user_dn = user_ref['dn'] + return self.group.list_user_groups_filtered(user_dn, hints) + + def list_groups(self, hints): + return self.group.get_all_filtered(hints) + + def list_users_in_group(self, group_id, hints): + users = [] + for user_dn in self.group.list_group_users(group_id): + user_id = self.user._dn_to_id(user_dn) + try: + users.append(self.user.get_filtered(user_id)) + except exception.UserNotFound: + LOG.debug(("Group member '%(user_dn)s' not found in" + " '%(group_id)s'. The user should be removed" + " from the group. The user will be ignored."), + dict(user_dn=user_dn, group_id=group_id)) + return users + + def check_user_in_group(self, user_id, group_id): + user_refs = self.list_users_in_group(group_id, driver_hints.Hints()) + for x in user_refs: + if x['id'] == user_id: + break + else: + # Try to fetch the user to see if it even exists. This + # will raise a more accurate exception. + self.get_user(user_id) + raise exception.NotFound(_("User '%(user_id)s' not found in" + " group '%(group_id)s'") % + {'user_id': user_id, + 'group_id': group_id}) + + +# TODO(termie): turn this into a data object and move logic to driver +class UserApi(common_ldap.EnabledEmuMixIn, common_ldap.BaseLdap): + DEFAULT_OU = 'ou=Users' + DEFAULT_STRUCTURAL_CLASSES = ['person'] + DEFAULT_ID_ATTR = 'cn' + DEFAULT_OBJECTCLASS = 'inetOrgPerson' + NotFound = exception.UserNotFound + options_name = 'user' + attribute_options_names = {'password': 'pass', + 'email': 'mail', + 'name': 'name', + 'enabled': 'enabled', + 'default_project_id': 'default_project_id'} + immutable_attrs = ['id'] + + model = models.User + + def __init__(self, conf): + super(UserApi, self).__init__(conf) + self.enabled_mask = conf.ldap.user_enabled_mask + self.enabled_default = conf.ldap.user_enabled_default + self.enabled_invert = conf.ldap.user_enabled_invert + self.enabled_emulation = conf.ldap.user_enabled_emulation + + def _ldap_res_to_model(self, res): + obj = super(UserApi, self)._ldap_res_to_model(res) + if self.enabled_mask != 0: + enabled = int(obj.get('enabled', self.enabled_default)) + obj['enabled'] = ((enabled & self.enabled_mask) != + self.enabled_mask) + elif self.enabled_invert and not self.enabled_emulation: + # This could be a bool or a string. If it's a string, + # we need to convert it so we can invert it properly. + enabled = obj.get('enabled', self.enabled_default) + if isinstance(enabled, six.string_types): + if enabled.lower() == 'true': + enabled = True + else: + enabled = False + obj['enabled'] = not enabled + obj['dn'] = res[0] + + return obj + + def mask_enabled_attribute(self, values): + value = values['enabled'] + values.setdefault('enabled_nomask', int(self.enabled_default)) + if value != ((values['enabled_nomask'] & self.enabled_mask) != + self.enabled_mask): + values['enabled_nomask'] ^= self.enabled_mask + values['enabled'] = values['enabled_nomask'] + del values['enabled_nomask'] + + def create(self, values): + if self.enabled_mask: + orig_enabled = values['enabled'] + self.mask_enabled_attribute(values) + elif self.enabled_invert and not self.enabled_emulation: + orig_enabled = values['enabled'] + if orig_enabled is not None: + values['enabled'] = not orig_enabled + else: + values['enabled'] = self.enabled_default + values = super(UserApi, self).create(values) + if self.enabled_mask or (self.enabled_invert and + not self.enabled_emulation): + values['enabled'] = orig_enabled + return values + + def get_filtered(self, user_id): + user = self.get(user_id) + return self.filter_attributes(user) + + def get_all_filtered(self, hints): + query = self.filter_query(hints) + return [self.filter_attributes(user) for user in self.get_all(query)] + + def filter_attributes(self, user): + return identity.filter_user(common_ldap.filter_entity(user)) + + def is_user(self, dn): + """Returns True if the entry is a user.""" + + # NOTE(blk-u): It's easy to check if the DN is under the User tree, + # but may not be accurate. A more accurate test would be to fetch the + # entry to see if it's got the user objectclass, but this could be + # really expensive considering how this is used. + + return common_ldap.dn_startswith(dn, self.tree_dn) + + +class GroupApi(common_ldap.BaseLdap): + DEFAULT_OU = 'ou=UserGroups' + DEFAULT_STRUCTURAL_CLASSES = [] + DEFAULT_OBJECTCLASS = 'groupOfNames' + DEFAULT_ID_ATTR = 'cn' + DEFAULT_MEMBER_ATTRIBUTE = 'member' + NotFound = exception.GroupNotFound + options_name = 'group' + attribute_options_names = {'description': 'desc', + 'name': 'name'} + immutable_attrs = ['name'] + model = models.Group + + def _ldap_res_to_model(self, res): + model = super(GroupApi, self)._ldap_res_to_model(res) + model['dn'] = res[0] + return model + + def __init__(self, conf): + super(GroupApi, self).__init__(conf) + self.member_attribute = (conf.ldap.group_member_attribute + or self.DEFAULT_MEMBER_ATTRIBUTE) + + def create(self, values): + data = values.copy() + if data.get('id') is None: + data['id'] = uuid.uuid4().hex + if 'description' in data and data['description'] in ['', None]: + data.pop('description') + return super(GroupApi, self).create(data) + + def delete(self, group_id): + if self.subtree_delete_enabled: + super(GroupApi, self).deleteTree(group_id) + else: + # TODO(spzala): this is only placeholder for group and domain + # role support which will be added under bug 1101287 + + group_ref = self.get(group_id) + group_dn = group_ref['dn'] + if group_dn: + self._delete_tree_nodes(group_dn, ldap.SCOPE_ONELEVEL) + super(GroupApi, self).delete(group_id) + + def update(self, group_id, values): + old_obj = self.get(group_id) + return super(GroupApi, self).update(group_id, values, old_obj) + + def add_user(self, user_dn, group_id, user_id): + group_ref = self.get(group_id) + group_dn = group_ref['dn'] + try: + super(GroupApi, self).add_member(user_dn, group_dn) + except exception.Conflict: + raise exception.Conflict(_( + 'User %(user_id)s is already a member of group %(group_id)s') % + {'user_id': user_id, 'group_id': group_id}) + + def remove_user(self, user_dn, group_id, user_id): + group_ref = self.get(group_id) + group_dn = group_ref['dn'] + try: + super(GroupApi, self).remove_member(user_dn, group_dn) + except ldap.NO_SUCH_ATTRIBUTE: + raise exception.UserNotFound(user_id=user_id) + + def list_user_groups(self, user_dn): + """Return a list of groups for which the user is a member.""" + + user_dn_esc = ldap.filter.escape_filter_chars(user_dn) + query = '(&(objectClass=%s)(%s=%s)%s)' % (self.object_class, + self.member_attribute, + user_dn_esc, + self.ldap_filter or '') + return self.get_all(query) + + def list_user_groups_filtered(self, user_dn, hints): + """Return a filtered list of groups for which the user is a member.""" + + user_dn_esc = ldap.filter.escape_filter_chars(user_dn) + query = '(&(objectClass=%s)(%s=%s)%s)' % (self.object_class, + self.member_attribute, + user_dn_esc, + self.ldap_filter or '') + return self.get_all_filtered(hints, query) + + def list_group_users(self, group_id): + """Return a list of user dns which are members of a group.""" + group_ref = self.get(group_id) + group_dn = group_ref['dn'] + + try: + attrs = self._ldap_get_list(group_dn, ldap.SCOPE_BASE, + attrlist=[self.member_attribute]) + except ldap.NO_SUCH_OBJECT: + raise self.NotFound(group_id=group_id) + + users = [] + for dn, member in attrs: + user_dns = member.get(self.member_attribute, []) + for user_dn in user_dns: + if self._is_dumb_member(user_dn): + continue + users.append(user_dn) + return users + + def get_filtered(self, group_id): + group = self.get(group_id) + return common_ldap.filter_entity(group) + + def get_filtered_by_name(self, group_name): + group = self.get_by_name(group_name) + return common_ldap.filter_entity(group) + + def get_all_filtered(self, hints, query=None): + query = self.filter_query(hints, query) + return [common_ldap.filter_entity(group) + for group in self.get_all(query)] diff --git a/keystone-moon/keystone/identity/backends/sql.py b/keystone-moon/keystone/identity/backends/sql.py new file mode 100644 index 00000000..39868416 --- /dev/null +++ b/keystone-moon/keystone/identity/backends/sql.py @@ -0,0 +1,314 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 oslo_config import cfg + +from keystone.common import sql +from keystone.common import utils +from keystone import exception +from keystone.i18n import _ +from keystone import identity + + +CONF = cfg.CONF + + +class User(sql.ModelBase, sql.DictBase): + __tablename__ = 'user' + attributes = ['id', 'name', 'domain_id', 'password', 'enabled', + 'default_project_id'] + id = sql.Column(sql.String(64), primary_key=True) + name = sql.Column(sql.String(255), nullable=False) + domain_id = sql.Column(sql.String(64), nullable=False) + password = sql.Column(sql.String(128)) + enabled = sql.Column(sql.Boolean) + extra = sql.Column(sql.JsonBlob()) + default_project_id = sql.Column(sql.String(64)) + # Unique constraint across two columns to create the separation + # rather than just only 'name' being unique + __table_args__ = (sql.UniqueConstraint('domain_id', 'name'), {}) + + def to_dict(self, include_extra_dict=False): + d = super(User, self).to_dict(include_extra_dict=include_extra_dict) + if 'default_project_id' in d and d['default_project_id'] is None: + del d['default_project_id'] + return d + + +class Group(sql.ModelBase, sql.DictBase): + __tablename__ = 'group' + attributes = ['id', 'name', 'domain_id', 'description'] + id = sql.Column(sql.String(64), primary_key=True) + name = sql.Column(sql.String(64), nullable=False) + domain_id = sql.Column(sql.String(64), nullable=False) + description = sql.Column(sql.Text()) + extra = sql.Column(sql.JsonBlob()) + # Unique constraint across two columns to create the separation + # rather than just only 'name' being unique + __table_args__ = (sql.UniqueConstraint('domain_id', 'name'), {}) + + +class UserGroupMembership(sql.ModelBase, sql.DictBase): + """Group membership join table.""" + __tablename__ = 'user_group_membership' + user_id = sql.Column(sql.String(64), + sql.ForeignKey('user.id'), + primary_key=True) + group_id = sql.Column(sql.String(64), + sql.ForeignKey('group.id'), + primary_key=True) + + +class Identity(identity.Driver): + # NOTE(henry-nash): Override the __init__() method so as to take a + # config parameter to enable sql to be used as a domain-specific driver. + def __init__(self, conf=None): + super(Identity, self).__init__() + + def default_assignment_driver(self): + return "keystone.assignment.backends.sql.Assignment" + + @property + def is_sql(self): + return True + + def _check_password(self, password, user_ref): + """Check the specified password against the data store. + + Note that we'll pass in the entire user_ref in case the subclass + needs things like user_ref.get('name') + For further justification, please see the follow up suggestion at + https://blueprints.launchpad.net/keystone/+spec/sql-identiy-pam + + """ + return utils.check_password(password, user_ref.password) + + # Identity interface + def authenticate(self, user_id, password): + session = sql.get_session() + user_ref = None + try: + user_ref = self._get_user(session, user_id) + except exception.UserNotFound: + raise AssertionError(_('Invalid user / password')) + if not self._check_password(password, user_ref): + raise AssertionError(_('Invalid user / password')) + return identity.filter_user(user_ref.to_dict()) + + # user crud + + @sql.handle_conflicts(conflict_type='user') + def create_user(self, user_id, user): + user = utils.hash_user_password(user) + session = sql.get_session() + with session.begin(): + user_ref = User.from_dict(user) + session.add(user_ref) + return identity.filter_user(user_ref.to_dict()) + + @sql.truncated + def list_users(self, hints): + session = sql.get_session() + query = session.query(User) + user_refs = sql.filter_limit_query(User, query, hints) + return [identity.filter_user(x.to_dict()) for x in user_refs] + + def _get_user(self, session, user_id): + user_ref = session.query(User).get(user_id) + if not user_ref: + raise exception.UserNotFound(user_id=user_id) + return user_ref + + def get_user(self, user_id): + session = sql.get_session() + return identity.filter_user(self._get_user(session, user_id).to_dict()) + + def get_user_by_name(self, user_name, domain_id): + session = sql.get_session() + query = session.query(User) + query = query.filter_by(name=user_name) + query = query.filter_by(domain_id=domain_id) + try: + user_ref = query.one() + except sql.NotFound: + raise exception.UserNotFound(user_id=user_name) + return identity.filter_user(user_ref.to_dict()) + + @sql.handle_conflicts(conflict_type='user') + def update_user(self, user_id, user): + session = sql.get_session() + + with session.begin(): + user_ref = self._get_user(session, user_id) + old_user_dict = user_ref.to_dict() + user = utils.hash_user_password(user) + for k in user: + old_user_dict[k] = user[k] + new_user = User.from_dict(old_user_dict) + for attr in User.attributes: + if attr != 'id': + setattr(user_ref, attr, getattr(new_user, attr)) + user_ref.extra = new_user.extra + return identity.filter_user(user_ref.to_dict(include_extra_dict=True)) + + def add_user_to_group(self, user_id, group_id): + session = sql.get_session() + self.get_group(group_id) + self.get_user(user_id) + query = session.query(UserGroupMembership) + query = query.filter_by(user_id=user_id) + query = query.filter_by(group_id=group_id) + rv = query.first() + if rv: + return + + with session.begin(): + session.add(UserGroupMembership(user_id=user_id, + group_id=group_id)) + + def check_user_in_group(self, user_id, group_id): + session = sql.get_session() + self.get_group(group_id) + self.get_user(user_id) + query = session.query(UserGroupMembership) + query = query.filter_by(user_id=user_id) + query = query.filter_by(group_id=group_id) + if not query.first(): + raise exception.NotFound(_("User '%(user_id)s' not found in" + " group '%(group_id)s'") % + {'user_id': user_id, + 'group_id': group_id}) + + def remove_user_from_group(self, user_id, group_id): + session = sql.get_session() + # We don't check if user or group are still valid and let the remove + # be tried anyway - in case this is some kind of clean-up operation + query = session.query(UserGroupMembership) + query = query.filter_by(user_id=user_id) + query = query.filter_by(group_id=group_id) + membership_ref = query.first() + if membership_ref is None: + # Check if the group and user exist to return descriptive + # exceptions. + self.get_group(group_id) + self.get_user(user_id) + raise exception.NotFound(_("User '%(user_id)s' not found in" + " group '%(group_id)s'") % + {'user_id': user_id, + 'group_id': group_id}) + with session.begin(): + session.delete(membership_ref) + + def list_groups_for_user(self, user_id, hints): + # TODO(henry-nash) We could implement full filtering here by enhancing + # the join below. However, since it is likely to be a fairly rare + # occurrence to filter on more than the user_id already being used + # here, this is left as future enhancement and until then we leave + # it for the controller to do for us. + session = sql.get_session() + self.get_user(user_id) + query = session.query(Group).join(UserGroupMembership) + query = query.filter(UserGroupMembership.user_id == user_id) + return [g.to_dict() for g in query] + + def list_users_in_group(self, group_id, hints): + # TODO(henry-nash) We could implement full filtering here by enhancing + # the join below. However, since it is likely to be a fairly rare + # occurrence to filter on more than the group_id already being used + # here, this is left as future enhancement and until then we leave + # it for the controller to do for us. + session = sql.get_session() + self.get_group(group_id) + query = session.query(User).join(UserGroupMembership) + query = query.filter(UserGroupMembership.group_id == group_id) + + return [identity.filter_user(u.to_dict()) for u in query] + + def delete_user(self, user_id): + session = sql.get_session() + + with session.begin(): + ref = self._get_user(session, user_id) + + q = session.query(UserGroupMembership) + q = q.filter_by(user_id=user_id) + q.delete(False) + + session.delete(ref) + + # group crud + + @sql.handle_conflicts(conflict_type='group') + def create_group(self, group_id, group): + session = sql.get_session() + with session.begin(): + ref = Group.from_dict(group) + session.add(ref) + return ref.to_dict() + + @sql.truncated + def list_groups(self, hints): + session = sql.get_session() + query = session.query(Group) + refs = sql.filter_limit_query(Group, query, hints) + return [ref.to_dict() for ref in refs] + + def _get_group(self, session, group_id): + ref = session.query(Group).get(group_id) + if not ref: + raise exception.GroupNotFound(group_id=group_id) + return ref + + def get_group(self, group_id): + session = sql.get_session() + return self._get_group(session, group_id).to_dict() + + def get_group_by_name(self, group_name, domain_id): + session = sql.get_session() + query = session.query(Group) + query = query.filter_by(name=group_name) + query = query.filter_by(domain_id=domain_id) + try: + group_ref = query.one() + except sql.NotFound: + raise exception.GroupNotFound(group_id=group_name) + return group_ref.to_dict() + + @sql.handle_conflicts(conflict_type='group') + def update_group(self, group_id, group): + session = sql.get_session() + + with session.begin(): + ref = self._get_group(session, group_id) + old_dict = ref.to_dict() + for k in group: + old_dict[k] = group[k] + new_group = Group.from_dict(old_dict) + for attr in Group.attributes: + if attr != 'id': + setattr(ref, attr, getattr(new_group, attr)) + ref.extra = new_group.extra + return ref.to_dict() + + def delete_group(self, group_id): + session = sql.get_session() + + with session.begin(): + ref = self._get_group(session, group_id) + + q = session.query(UserGroupMembership) + q = q.filter_by(group_id=group_id) + q.delete(False) + + session.delete(ref) diff --git a/keystone-moon/keystone/identity/controllers.py b/keystone-moon/keystone/identity/controllers.py new file mode 100644 index 00000000..a2676c41 --- /dev/null +++ b/keystone-moon/keystone/identity/controllers.py @@ -0,0 +1,335 @@ +# Copyright 2012 OpenStack Foundation +# +# 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. + +"""Workflow Logic the Identity service.""" + +from oslo_config import cfg +from oslo_log import log + +from keystone.common import controller +from keystone.common import dependency +from keystone import exception +from keystone.i18n import _, _LW +from keystone import notifications + + +CONF = cfg.CONF +LOG = log.getLogger(__name__) + + +@dependency.requires('assignment_api', 'identity_api', 'resource_api') +class User(controller.V2Controller): + + @controller.v2_deprecated + def get_user(self, context, user_id): + self.assert_admin(context) + ref = self.identity_api.get_user(user_id) + return {'user': self.v3_to_v2_user(ref)} + + @controller.v2_deprecated + def get_users(self, context): + # NOTE(termie): i can't imagine that this really wants all the data + # about every single user in the system... + if 'name' in context['query_string']: + return self.get_user_by_name( + context, context['query_string'].get('name')) + + self.assert_admin(context) + user_list = self.identity_api.list_users( + CONF.identity.default_domain_id) + return {'users': self.v3_to_v2_user(user_list)} + + @controller.v2_deprecated + def get_user_by_name(self, context, user_name): + self.assert_admin(context) + ref = self.identity_api.get_user_by_name( + user_name, CONF.identity.default_domain_id) + return {'user': self.v3_to_v2_user(ref)} + + # CRUD extension + @controller.v2_deprecated + def create_user(self, context, user): + user = self._normalize_OSKSADM_password_on_request(user) + user = self.normalize_username_in_request(user) + user = self._normalize_dict(user) + self.assert_admin(context) + + if 'name' not in user or not user['name']: + msg = _('Name field is required and cannot be empty') + raise exception.ValidationError(message=msg) + if 'enabled' in user and not isinstance(user['enabled'], bool): + msg = _('Enabled field must be a boolean') + raise exception.ValidationError(message=msg) + + default_project_id = user.pop('tenantId', None) + if default_project_id is not None: + # Check to see if the project is valid before moving on. + self.resource_api.get_project(default_project_id) + user['default_project_id'] = default_project_id + + # The manager layer will generate the unique ID for users + user_ref = self._normalize_domain_id(context, user.copy()) + new_user_ref = self.v3_to_v2_user( + self.identity_api.create_user(user_ref)) + + if default_project_id is not None: + self.assignment_api.add_user_to_project(default_project_id, + new_user_ref['id']) + return {'user': new_user_ref} + + @controller.v2_deprecated + def update_user(self, context, user_id, user): + # NOTE(termie): this is really more of a patch than a put + user = self.normalize_username_in_request(user) + self.assert_admin(context) + + if 'enabled' in user and not isinstance(user['enabled'], bool): + msg = _('Enabled field should be a boolean') + raise exception.ValidationError(message=msg) + + default_project_id = user.pop('tenantId', None) + if default_project_id is not None: + user['default_project_id'] = default_project_id + + old_user_ref = self.v3_to_v2_user( + self.identity_api.get_user(user_id)) + + # Check whether a tenant is being added or changed for the user. + # Catch the case where the tenant is being changed for a user and also + # where a user previously had no tenant but a tenant is now being + # added for the user. + if (('tenantId' in old_user_ref and + old_user_ref['tenantId'] != default_project_id and + default_project_id is not None) or + ('tenantId' not in old_user_ref and + default_project_id is not None)): + # Make sure the new project actually exists before we perform the + # user update. + self.resource_api.get_project(default_project_id) + + user_ref = self.v3_to_v2_user( + self.identity_api.update_user(user_id, user)) + + # If 'tenantId' is in either ref, we might need to add or remove the + # user from a project. + if 'tenantId' in user_ref or 'tenantId' in old_user_ref: + if user_ref['tenantId'] != old_user_ref.get('tenantId'): + if old_user_ref.get('tenantId'): + try: + member_role_id = CONF.member_role_id + self.assignment_api.remove_role_from_user_and_project( + user_id, old_user_ref['tenantId'], member_role_id) + except exception.NotFound: + # NOTE(morganfainberg): This is not a critical error it + # just means that the user cannot be removed from the + # old tenant. This could occur if roles aren't found + # or if the project is invalid or if there are no roles + # for the user on that project. + msg = _LW('Unable to remove user %(user)s from ' + '%(tenant)s.') + LOG.warning(msg, {'user': user_id, + 'tenant': old_user_ref['tenantId']}) + + if user_ref['tenantId']: + try: + self.assignment_api.add_user_to_project( + user_ref['tenantId'], user_id) + except exception.Conflict: + # We are already a member of that tenant + pass + except exception.NotFound: + # NOTE(morganfainberg): Log this and move on. This is + # not the end of the world if we can't add the user to + # the appropriate tenant. Most of the time this means + # that the project is invalid or roles are some how + # incorrect. This shouldn't prevent the return of the + # new ref. + msg = _LW('Unable to add user %(user)s to %(tenant)s.') + LOG.warning(msg, {'user': user_id, + 'tenant': user_ref['tenantId']}) + + return {'user': user_ref} + + @controller.v2_deprecated + def delete_user(self, context, user_id): + self.assert_admin(context) + self.identity_api.delete_user(user_id) + + @controller.v2_deprecated + def set_user_enabled(self, context, user_id, user): + return self.update_user(context, user_id, user) + + @controller.v2_deprecated + def set_user_password(self, context, user_id, user): + user = self._normalize_OSKSADM_password_on_request(user) + return self.update_user(context, user_id, user) + + @staticmethod + def _normalize_OSKSADM_password_on_request(ref): + """Sets the password from the OS-KSADM Admin Extension. + + The OS-KSADM Admin Extension documentation says that + `OS-KSADM:password` can be used in place of `password`. + + """ + if 'OS-KSADM:password' in ref: + ref['password'] = ref.pop('OS-KSADM:password') + return ref + + +@dependency.requires('identity_api') +class UserV3(controller.V3Controller): + collection_name = 'users' + member_name = 'user' + + def __init__(self): + super(UserV3, self).__init__() + self.get_member_from_driver = self.identity_api.get_user + + def _check_user_and_group_protection(self, context, prep_info, + user_id, group_id): + ref = {} + ref['user'] = self.identity_api.get_user(user_id) + ref['group'] = self.identity_api.get_group(group_id) + self.check_protection(context, prep_info, ref) + + @controller.protected() + def create_user(self, context, user): + self._require_attribute(user, 'name') + + # The manager layer will generate the unique ID for users + ref = self._normalize_dict(user) + ref = self._normalize_domain_id(context, ref) + initiator = notifications._get_request_audit_info(context) + ref = self.identity_api.create_user(ref, initiator) + return UserV3.wrap_member(context, ref) + + @controller.filterprotected('domain_id', 'enabled', 'name') + def list_users(self, context, filters): + hints = UserV3.build_driver_hints(context, filters) + refs = self.identity_api.list_users( + domain_scope=self._get_domain_id_for_list_request(context), + hints=hints) + return UserV3.wrap_collection(context, refs, hints=hints) + + @controller.filterprotected('domain_id', 'enabled', 'name') + def list_users_in_group(self, context, filters, group_id): + hints = UserV3.build_driver_hints(context, filters) + refs = self.identity_api.list_users_in_group(group_id, hints=hints) + return UserV3.wrap_collection(context, refs, hints=hints) + + @controller.protected() + def get_user(self, context, user_id): + ref = self.identity_api.get_user(user_id) + return UserV3.wrap_member(context, ref) + + def _update_user(self, context, user_id, user): + self._require_matching_id(user_id, user) + self._require_matching_domain_id( + user_id, user, self.identity_api.get_user) + initiator = notifications._get_request_audit_info(context) + ref = self.identity_api.update_user(user_id, user, initiator) + return UserV3.wrap_member(context, ref) + + @controller.protected() + def update_user(self, context, user_id, user): + return self._update_user(context, user_id, user) + + @controller.protected(callback=_check_user_and_group_protection) + def add_user_to_group(self, context, user_id, group_id): + self.identity_api.add_user_to_group(user_id, group_id) + + @controller.protected(callback=_check_user_and_group_protection) + def check_user_in_group(self, context, user_id, group_id): + return self.identity_api.check_user_in_group(user_id, group_id) + + @controller.protected(callback=_check_user_and_group_protection) + def remove_user_from_group(self, context, user_id, group_id): + self.identity_api.remove_user_from_group(user_id, group_id) + + @controller.protected() + def delete_user(self, context, user_id): + initiator = notifications._get_request_audit_info(context) + return self.identity_api.delete_user(user_id, initiator) + + @controller.protected() + def change_password(self, context, user_id, user): + original_password = user.get('original_password') + if original_password is None: + raise exception.ValidationError(target='user', + attribute='original_password') + + password = user.get('password') + if password is None: + raise exception.ValidationError(target='user', + attribute='password') + try: + self.identity_api.change_password( + context, user_id, original_password, password) + except AssertionError: + raise exception.Unauthorized() + + +@dependency.requires('identity_api') +class GroupV3(controller.V3Controller): + collection_name = 'groups' + member_name = 'group' + + def __init__(self): + super(GroupV3, self).__init__() + self.get_member_from_driver = self.identity_api.get_group + + @controller.protected() + def create_group(self, context, group): + self._require_attribute(group, 'name') + + # The manager layer will generate the unique ID for groups + ref = self._normalize_dict(group) + ref = self._normalize_domain_id(context, ref) + initiator = notifications._get_request_audit_info(context) + ref = self.identity_api.create_group(ref, initiator) + return GroupV3.wrap_member(context, ref) + + @controller.filterprotected('domain_id', 'name') + def list_groups(self, context, filters): + hints = GroupV3.build_driver_hints(context, filters) + refs = self.identity_api.list_groups( + domain_scope=self._get_domain_id_for_list_request(context), + hints=hints) + return GroupV3.wrap_collection(context, refs, hints=hints) + + @controller.filterprotected('name') + def list_groups_for_user(self, context, filters, user_id): + hints = GroupV3.build_driver_hints(context, filters) + refs = self.identity_api.list_groups_for_user(user_id, hints=hints) + return GroupV3.wrap_collection(context, refs, hints=hints) + + @controller.protected() + def get_group(self, context, group_id): + ref = self.identity_api.get_group(group_id) + return GroupV3.wrap_member(context, ref) + + @controller.protected() + def update_group(self, context, group_id, group): + self._require_matching_id(group_id, group) + self._require_matching_domain_id( + group_id, group, self.identity_api.get_group) + initiator = notifications._get_request_audit_info(context) + ref = self.identity_api.update_group(group_id, group, initiator) + return GroupV3.wrap_member(context, ref) + + @controller.protected() + def delete_group(self, context, group_id): + initiator = notifications._get_request_audit_info(context) + self.identity_api.delete_group(group_id, initiator) diff --git a/keystone-moon/keystone/identity/core.py b/keystone-moon/keystone/identity/core.py new file mode 100644 index 00000000..988df78b --- /dev/null +++ b/keystone-moon/keystone/identity/core.py @@ -0,0 +1,1259 @@ +# Copyright 2012 OpenStack Foundation +# +# 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. + +"""Main entry point into the Identity service.""" + +import abc +import functools +import os +import uuid + +from oslo_config import cfg +from oslo_log import log +from oslo_utils import importutils +import six + +from keystone import clean +from keystone.common import cache +from keystone.common import dependency +from keystone.common import driver_hints +from keystone.common import manager +from keystone import config +from keystone import exception +from keystone.i18n import _, _LW +from keystone.identity.mapping_backends import mapping +from keystone import notifications + + +CONF = cfg.CONF + +LOG = log.getLogger(__name__) + +MEMOIZE = cache.get_memoization_decorator(section='identity') + +DOMAIN_CONF_FHEAD = 'keystone.' +DOMAIN_CONF_FTAIL = '.conf' + + +def filter_user(user_ref): + """Filter out private items in a user dict. + + 'password', 'tenants' and 'groups' are never returned. + + :returns: user_ref + + """ + if user_ref: + user_ref = user_ref.copy() + user_ref.pop('password', None) + user_ref.pop('tenants', None) + user_ref.pop('groups', None) + user_ref.pop('domains', None) + try: + user_ref['extra'].pop('password', None) + user_ref['extra'].pop('tenants', None) + except KeyError: + pass + return user_ref + + +@dependency.requires('domain_config_api') +class DomainConfigs(dict): + """Discover, store and provide access to domain specific configs. + + The setup_domain_drivers() call will be made via the wrapper from + the first call to any driver function handled by this manager. + + Domain specific configurations are only supported for the identity backend + and the individual configurations are either specified in the resource + database or in individual domain configuration files, depending on the + setting of the 'domain_configurations_from_database' config option. + + The result will be that for each domain with a specific configuration, + this class will hold a reference to a ConfigOpts and driver object that + the identity manager and driver can use. + + """ + configured = False + driver = None + _any_sql = False + + def _load_driver(self, domain_config): + return importutils.import_object( + domain_config['cfg'].identity.driver, domain_config['cfg']) + + def _assert_no_more_than_one_sql_driver(self, domain_id, new_config, + config_file=None): + """Ensure there is more than one sql driver. + + Check to see if the addition of the driver in this new config + would cause there to now be more than one sql driver. + + If we are loading from configuration files, the config_file will hold + the name of the file we have just loaded. + + """ + if (new_config['driver'].is_sql and + (self.driver.is_sql or self._any_sql)): + # The addition of this driver would cause us to have more than + # one sql driver, so raise an exception. + if not config_file: + config_file = _('Database at /domains/%s/config') % domain_id + raise exception.MultipleSQLDriversInConfig(source=config_file) + self._any_sql = new_config['driver'].is_sql + + def _load_config_from_file(self, resource_api, file_list, domain_name): + + try: + domain_ref = resource_api.get_domain_by_name(domain_name) + except exception.DomainNotFound: + LOG.warning( + _LW('Invalid domain name (%s) found in config file name'), + domain_name) + return + + # Create a new entry in the domain config dict, which contains + # a new instance of both the conf environment and driver using + # options defined in this set of config files. Later, when we + # service calls via this Manager, we'll index via this domain + # config dict to make sure we call the right driver + domain_config = {} + domain_config['cfg'] = cfg.ConfigOpts() + config.configure(conf=domain_config['cfg']) + domain_config['cfg'](args=[], project='keystone', + default_config_files=file_list) + domain_config['driver'] = self._load_driver(domain_config) + self._assert_no_more_than_one_sql_driver(domain_ref['id'], + domain_config, + config_file=file_list) + self[domain_ref['id']] = domain_config + + def _setup_domain_drivers_from_files(self, standard_driver, resource_api): + """Read the domain specific configuration files and load the drivers. + + Domain configuration files are stored in the domain config directory, + and must be named of the form: + + keystone..conf + + For each file, call the load config method where the domain_name + will be turned into a domain_id and then: + + - Create a new config structure, adding in the specific additional + options defined in this config file + - Initialise a new instance of the required driver with this new config + + """ + conf_dir = CONF.identity.domain_config_dir + if not os.path.exists(conf_dir): + LOG.warning(_LW('Unable to locate domain config directory: %s'), + conf_dir) + return + + for r, d, f in os.walk(conf_dir): + for fname in f: + if (fname.startswith(DOMAIN_CONF_FHEAD) and + fname.endswith(DOMAIN_CONF_FTAIL)): + if fname.count('.') >= 2: + self._load_config_from_file( + resource_api, [os.path.join(r, fname)], + fname[len(DOMAIN_CONF_FHEAD): + -len(DOMAIN_CONF_FTAIL)]) + else: + LOG.debug(('Ignoring file (%s) while scanning domain ' + 'config directory'), + fname) + + def _load_config_from_database(self, domain_id, specific_config): + domain_config = {} + domain_config['cfg'] = cfg.ConfigOpts() + config.configure(conf=domain_config['cfg']) + domain_config['cfg'](args=[], project='keystone') + + # Override any options that have been passed in as specified in the + # database. + for group in specific_config: + for option in specific_config[group]: + domain_config['cfg'].set_override( + option, specific_config[group][option], group) + + domain_config['driver'] = self._load_driver(domain_config) + self._assert_no_more_than_one_sql_driver(domain_id, domain_config) + self[domain_id] = domain_config + + def _setup_domain_drivers_from_database(self, standard_driver, + resource_api): + """Read domain specific configuration from database and load drivers. + + Domain configurations are stored in the domain-config backend, + so we go through each domain to find those that have a specific config + defined, and for those that do we: + + - Create a new config structure, overriding any specific options + defined in the resource backend + - Initialise a new instance of the required driver with this new config + + """ + for domain in resource_api.list_domains(): + domain_config_options = ( + self.domain_config_api. + get_config_with_sensitive_info(domain['id'])) + if domain_config_options: + self._load_config_from_database(domain['id'], + domain_config_options) + + def setup_domain_drivers(self, standard_driver, resource_api): + # This is called by the api call wrapper + self.configured = True + self.driver = standard_driver + + if CONF.identity.domain_configurations_from_database: + self._setup_domain_drivers_from_database(standard_driver, + resource_api) + else: + self._setup_domain_drivers_from_files(standard_driver, + resource_api) + + def get_domain_driver(self, domain_id): + if domain_id in self: + return self[domain_id]['driver'] + + def get_domain_conf(self, domain_id): + if domain_id in self: + return self[domain_id]['cfg'] + else: + return CONF + + def reload_domain_driver(self, domain_id): + # Only used to support unit tests that want to set + # new config values. This should only be called once + # the domains have been configured, since it relies on + # the fact that the configuration files/database have already been + # read. + if self.configured: + if domain_id in self: + self[domain_id]['driver'] = ( + self._load_driver(self[domain_id])) + else: + # The standard driver + self.driver = self.driver() + + +def domains_configured(f): + """Wraps API calls to lazy load domain configs after init. + + This is required since the assignment manager needs to be initialized + before this manager, and yet this manager's init wants to be + able to make assignment calls (to build the domain configs). So + instead, we check if the domains have been initialized on entry + to each call, and if requires load them, + + """ + @functools.wraps(f) + def wrapper(self, *args, **kwargs): + if (not self.domain_configs.configured and + CONF.identity.domain_specific_drivers_enabled): + self.domain_configs.setup_domain_drivers( + self.driver, self.resource_api) + return f(self, *args, **kwargs) + return wrapper + + +def exception_translated(exception_type): + """Wraps API calls to map to correct exception.""" + + def _exception_translated(f): + @functools.wraps(f) + def wrapper(self, *args, **kwargs): + try: + return f(self, *args, **kwargs) + except exception.PublicIDNotFound as e: + if exception_type == 'user': + raise exception.UserNotFound(user_id=str(e)) + elif exception_type == 'group': + raise exception.GroupNotFound(group_id=str(e)) + elif exception_type == 'assertion': + raise AssertionError(_('Invalid user / password')) + else: + raise + return wrapper + return _exception_translated + + +@dependency.provider('identity_api') +@dependency.requires('assignment_api', 'credential_api', 'id_mapping_api', + 'resource_api', 'revoke_api') +class Manager(manager.Manager): + """Default pivot point for the Identity backend. + + See :mod:`keystone.common.manager.Manager` for more details on how this + dynamically calls the backend. + + This class also handles the support of domain specific backends, by using + the DomainConfigs class. The setup call for DomainConfigs is called + from with the @domains_configured wrapper in a lazy loading fashion + to get around the fact that we can't satisfy the assignment api it needs + from within our __init__() function since the assignment driver is not + itself yet initialized. + + Each of the identity calls are pre-processed here to choose, based on + domain, which of the drivers should be called. The non-domain-specific + driver is still in place, and is used if there is no specific driver for + the domain in question (or we are not using multiple domain drivers). + + Starting with Juno, in order to be able to obtain the domain from + just an ID being presented as part of an API call, a public ID to domain + and local ID mapping is maintained. This mapping also allows for the local + ID of drivers that do not provide simple UUIDs (such as LDAP) to be + referenced via a public facing ID. The mapping itself is automatically + generated as entities are accessed via the driver. + + This mapping is only used when: + - the entity is being handled by anything other than the default driver, or + - the entity is being handled by the default LDAP driver and backward + compatible IDs are not required. + + This means that in the standard case of a single SQL backend or the default + settings of a single LDAP backend (since backward compatible IDs is set to + True by default), no mapping is used. An alternative approach would be to + always use the mapping table, but in the cases where we don't need it to + make the public and local IDs the same. It is felt that not using the + mapping by default is a more prudent way to introduce this functionality. + + """ + _USER = 'user' + _GROUP = 'group' + + def __init__(self): + super(Manager, self).__init__(CONF.identity.driver) + self.domain_configs = DomainConfigs() + + self.event_callbacks = { + notifications.ACTIONS.deleted: { + 'domain': [self._domain_deleted], + }, + } + + def _domain_deleted(self, service, resource_type, operation, + payload): + domain_id = payload['resource_info'] + + user_refs = self.list_users(domain_scope=domain_id) + group_refs = self.list_groups(domain_scope=domain_id) + + for group in group_refs: + # Cleanup any existing groups. + try: + self.delete_group(group['id']) + except exception.GroupNotFound: + LOG.debug(('Group %(groupid)s not found when deleting domain ' + 'contents for %(domainid)s, continuing with ' + 'cleanup.'), + {'groupid': group['id'], 'domainid': domain_id}) + + # And finally, delete the users themselves + for user in user_refs: + try: + self.delete_user(user['id']) + except exception.UserNotFound: + LOG.debug(('User %(userid)s not found when deleting domain ' + 'contents for %(domainid)s, continuing with ' + 'cleanup.'), + {'userid': user['id'], 'domainid': domain_id}) + + # Domain ID normalization methods + + def _set_domain_id_and_mapping(self, ref, domain_id, driver, + entity_type): + """Patch the domain_id/public_id into the resulting entity(ies). + + :param ref: the entity or list of entities to post process + :param domain_id: the domain scope used for the call + :param driver: the driver used to execute the call + :param entity_type: whether this is a user or group + + :returns: post processed entity or list or entities + + Called to post-process the entity being returned, using a mapping + to substitute a public facing ID as necessary. This method must + take into account: + + - If the driver is not domain aware, then we must set the domain + attribute of all entities irrespective of mapping. + - If the driver does not support UUIDs, then we always want to provide + a mapping, except for the special case of this being the default + driver and backward_compatible_ids is set to True. This is to ensure + that entity IDs do not change for an existing LDAP installation (only + single domain/driver LDAP configurations were previously supported). + - If the driver does support UUIDs, then we always create a mapping + entry, but use the local UUID as the public ID. The exception to + - this is that if we just have single driver (i.e. not using specific + multi-domain configs), then we don't both with the mapping at all. + + """ + conf = CONF.identity + + if not self._needs_post_processing(driver): + # a classic case would be when running with a single SQL driver + return ref + + LOG.debug('ID Mapping - Domain ID: %(domain)s, ' + 'Default Driver: %(driver)s, ' + 'Domains: %(aware)s, UUIDs: %(generate)s, ' + 'Compatible IDs: %(compat)s', + {'domain': domain_id, + 'driver': (driver == self.driver), + 'aware': driver.is_domain_aware(), + 'generate': driver.generates_uuids(), + 'compat': CONF.identity_mapping.backward_compatible_ids}) + + if isinstance(ref, dict): + return self._set_domain_id_and_mapping_for_single_ref( + ref, domain_id, driver, entity_type, conf) + elif isinstance(ref, list): + return [self._set_domain_id_and_mapping( + x, domain_id, driver, entity_type) for x in ref] + else: + raise ValueError(_('Expected dict or list: %s') % type(ref)) + + def _needs_post_processing(self, driver): + """Returns whether entity from driver needs domain added or mapping.""" + return (driver is not self.driver or not driver.generates_uuids() or + not driver.is_domain_aware()) + + def _set_domain_id_and_mapping_for_single_ref(self, ref, domain_id, + driver, entity_type, conf): + LOG.debug('Local ID: %s', ref['id']) + ref = ref.copy() + + self._insert_domain_id_if_needed(ref, driver, domain_id, conf) + + if self._is_mapping_needed(driver): + local_entity = {'domain_id': ref['domain_id'], + 'local_id': ref['id'], + 'entity_type': entity_type} + public_id = self.id_mapping_api.get_public_id(local_entity) + if public_id: + ref['id'] = public_id + LOG.debug('Found existing mapping to public ID: %s', + ref['id']) + else: + # Need to create a mapping. If the driver generates UUIDs + # then pass the local UUID in as the public ID to use. + if driver.generates_uuids(): + public_id = ref['id'] + ref['id'] = self.id_mapping_api.create_id_mapping( + local_entity, public_id) + LOG.debug('Created new mapping to public ID: %s', + ref['id']) + return ref + + def _insert_domain_id_if_needed(self, ref, driver, domain_id, conf): + """Inserts the domain ID into the ref, if required. + + If the driver can't handle domains, then we need to insert the + domain_id into the entity being returned. If the domain_id is + None that means we are running in a single backend mode, so to + remain backwardly compatible, we put in the default domain ID. + """ + if not driver.is_domain_aware(): + if domain_id is None: + domain_id = conf.default_domain_id + ref['domain_id'] = domain_id + + def _is_mapping_needed(self, driver): + """Returns whether mapping is needed. + + There are two situations where we must use the mapping: + - this isn't the default driver (i.e. multiple backends), or + - we have a single backend that doesn't use UUIDs + The exception to the above is that we must honor backward + compatibility if this is the default driver (e.g. to support + current LDAP) + """ + is_not_default_driver = driver is not self.driver + return (is_not_default_driver or ( + not driver.generates_uuids() and + not CONF.identity_mapping.backward_compatible_ids)) + + def _clear_domain_id_if_domain_unaware(self, driver, ref): + """Clear domain_id details if driver is not domain aware.""" + if not driver.is_domain_aware() and 'domain_id' in ref: + ref = ref.copy() + ref.pop('domain_id') + return ref + + def _select_identity_driver(self, domain_id): + """Choose a backend driver for the given domain_id. + + :param domain_id: The domain_id for which we want to find a driver. If + the domain_id is specified as None, then this means + we need a driver that handles multiple domains. + + :returns: chosen backend driver + + If there is a specific driver defined for this domain then choose it. + If the domain is None, or there no specific backend for the given + domain is found, then we chose the default driver. + + """ + if domain_id is None: + driver = self.driver + else: + driver = (self.domain_configs.get_domain_driver(domain_id) or + self.driver) + + # If the driver is not domain aware (e.g. LDAP) then check to + # ensure we are not mapping multiple domains onto it - the only way + # that would happen is that the default driver is LDAP and the + # domain is anything other than None or the default domain. + if (not driver.is_domain_aware() and driver == self.driver and + domain_id != CONF.identity.default_domain_id and + domain_id is not None): + LOG.warning('Found multiple domains being mapped to a ' + 'driver that does not support that (e.g. ' + 'LDAP) - Domain ID: %(domain)s, ' + 'Default Driver: %(driver)s', + {'domain': domain_id, + 'driver': (driver == self.driver)}) + raise exception.DomainNotFound(domain_id=domain_id) + return driver + + def _get_domain_driver_and_entity_id(self, public_id): + """Look up details using the public ID. + + :param public_id: the ID provided in the call + + :returns: domain_id, which can be None to indicate that the driver + in question supports multiple domains + driver selected based on this domain + entity_id which will is understood by the driver. + + Use the mapping table to look up the domain, driver and local entity + that is represented by the provided public ID. Handle the situations + were we do not use the mapping (e.g. single driver that understands + UUIDs etc.) + + """ + conf = CONF.identity + # First, since we don't know anything about the entity yet, we must + # assume it needs mapping, so long as we are using domain specific + # drivers. + if conf.domain_specific_drivers_enabled: + local_id_ref = self.id_mapping_api.get_id_mapping(public_id) + if local_id_ref: + return ( + local_id_ref['domain_id'], + self._select_identity_driver(local_id_ref['domain_id']), + local_id_ref['local_id']) + + # So either we are using multiple drivers but the public ID is invalid + # (and hence was not found in the mapping table), or the public ID is + # being handled by the default driver. Either way, the only place left + # to look is in that standard driver. However, we don't yet know if + # this driver also needs mapping (e.g. LDAP in non backward + # compatibility mode). + driver = self.driver + if driver.generates_uuids(): + if driver.is_domain_aware: + # No mapping required, and the driver can handle the domain + # information itself. The classic case of this is the + # current SQL driver. + return (None, driver, public_id) + else: + # Although we don't have any drivers of this type, i.e. that + # understand UUIDs but not domains, conceptually you could. + return (conf.default_domain_id, driver, public_id) + + # So the only place left to find the ID is in the default driver which + # we now know doesn't generate UUIDs + if not CONF.identity_mapping.backward_compatible_ids: + # We are not running in backward compatibility mode, so we + # must use a mapping. + local_id_ref = self.id_mapping_api.get_id_mapping(public_id) + if local_id_ref: + return ( + local_id_ref['domain_id'], + driver, + local_id_ref['local_id']) + else: + raise exception.PublicIDNotFound(id=public_id) + + # If we reach here, this means that the default driver + # requires no mapping - but also doesn't understand domains + # (e.g. the classic single LDAP driver situation). Hence we pass + # back the public_ID unmodified and use the default domain (to + # keep backwards compatibility with existing installations). + # + # It is still possible that the public ID is just invalid in + # which case we leave this to the caller to check. + return (conf.default_domain_id, driver, public_id) + + def _assert_user_and_group_in_same_backend( + self, user_entity_id, user_driver, group_entity_id, group_driver): + """Ensures that user and group IDs are backed by the same backend. + + Raise a CrossBackendNotAllowed exception if they are not from the same + backend, otherwise return None. + + """ + if user_driver is not group_driver: + # Determine first if either IDs don't exist by calling + # the driver.get methods (which will raise a NotFound + # exception). + user_driver.get_user(user_entity_id) + group_driver.get_group(group_entity_id) + # If we get here, then someone is attempting to create a cross + # backend membership, which is not allowed. + raise exception.CrossBackendNotAllowed(group_id=group_entity_id, + user_id=user_entity_id) + + def _mark_domain_id_filter_satisfied(self, hints): + if hints: + for filter in hints.filters: + if (filter['name'] == 'domain_id' and + filter['comparator'] == 'equals'): + hints.filters.remove(filter) + + def _ensure_domain_id_in_hints(self, hints, domain_id): + if (domain_id is not None and + not hints.get_exact_filter_by_name('domain_id')): + hints.add_filter('domain_id', domain_id) + + # The actual driver calls - these are pre/post processed here as + # part of the Manager layer to make sure we: + # + # - select the right driver for this domain + # - clear/set domain_ids for drivers that do not support domains + # - create any ID mapping that might be required + + @notifications.emit_event('authenticate') + @domains_configured + @exception_translated('assertion') + def authenticate(self, context, user_id, password): + domain_id, driver, entity_id = ( + self._get_domain_driver_and_entity_id(user_id)) + ref = driver.authenticate(entity_id, password) + return self._set_domain_id_and_mapping( + ref, domain_id, driver, mapping.EntityType.USER) + + @domains_configured + @exception_translated('user') + def create_user(self, user_ref, initiator=None): + user = user_ref.copy() + user['name'] = clean.user_name(user['name']) + user.setdefault('enabled', True) + user['enabled'] = clean.user_enabled(user['enabled']) + domain_id = user['domain_id'] + self.resource_api.get_domain(domain_id) + + # For creating a user, the domain is in the object itself + domain_id = user_ref['domain_id'] + driver = self._select_identity_driver(domain_id) + user = self._clear_domain_id_if_domain_unaware(driver, user) + # Generate a local ID - in the future this might become a function of + # the underlying driver so that it could conform to rules set down by + # that particular driver type. + user['id'] = uuid.uuid4().hex + ref = driver.create_user(user['id'], user) + notifications.Audit.created(self._USER, user['id'], initiator) + return self._set_domain_id_and_mapping( + ref, domain_id, driver, mapping.EntityType.USER) + + @domains_configured + @exception_translated('user') + @MEMOIZE + def get_user(self, user_id): + domain_id, driver, entity_id = ( + self._get_domain_driver_and_entity_id(user_id)) + ref = driver.get_user(entity_id) + return self._set_domain_id_and_mapping( + ref, domain_id, driver, mapping.EntityType.USER) + + def assert_user_enabled(self, user_id, user=None): + """Assert the user and the user's domain are enabled. + + :raise AssertionError if the user or the user's domain is disabled. + """ + if user is None: + user = self.get_user(user_id) + self.resource_api.assert_domain_enabled(user['domain_id']) + if not user.get('enabled', True): + raise AssertionError(_('User is disabled: %s') % user_id) + + @domains_configured + @exception_translated('user') + @MEMOIZE + def get_user_by_name(self, user_name, domain_id): + driver = self._select_identity_driver(domain_id) + ref = driver.get_user_by_name(user_name, domain_id) + return self._set_domain_id_and_mapping( + ref, domain_id, driver, mapping.EntityType.USER) + + @manager.response_truncated + @domains_configured + @exception_translated('user') + def list_users(self, domain_scope=None, hints=None): + driver = self._select_identity_driver(domain_scope) + hints = hints or driver_hints.Hints() + if driver.is_domain_aware(): + # Force the domain_scope into the hint to ensure that we only get + # back domains for that scope. + self._ensure_domain_id_in_hints(hints, domain_scope) + else: + # We are effectively satisfying any domain_id filter by the above + # driver selection, so remove any such filter. + self._mark_domain_id_filter_satisfied(hints) + ref_list = driver.list_users(hints) + return self._set_domain_id_and_mapping( + ref_list, domain_scope, driver, mapping.EntityType.USER) + + @domains_configured + @exception_translated('user') + def update_user(self, user_id, user_ref, initiator=None): + old_user_ref = self.get_user(user_id) + user = user_ref.copy() + if 'name' in user: + user['name'] = clean.user_name(user['name']) + if 'enabled' in user: + user['enabled'] = clean.user_enabled(user['enabled']) + if 'domain_id' in user: + self.resource_api.get_domain(user['domain_id']) + if 'id' in user: + if user_id != user['id']: + raise exception.ValidationError(_('Cannot change user ID')) + # Since any ID in the user dict is now irrelevant, remove its so as + # the driver layer won't be confused by the fact the this is the + # public ID not the local ID + user.pop('id') + + domain_id, driver, entity_id = ( + self._get_domain_driver_and_entity_id(user_id)) + user = self._clear_domain_id_if_domain_unaware(driver, user) + self.get_user.invalidate(self, old_user_ref['id']) + self.get_user_by_name.invalidate(self, old_user_ref['name'], + old_user_ref['domain_id']) + + ref = driver.update_user(entity_id, user) + + notifications.Audit.updated(self._USER, user_id, initiator) + + enabled_change = ((user.get('enabled') is False) and + user['enabled'] != old_user_ref.get('enabled')) + if enabled_change or user.get('password') is not None: + self.emit_invalidate_user_token_persistence(user_id) + + return self._set_domain_id_and_mapping( + ref, domain_id, driver, mapping.EntityType.USER) + + @domains_configured + @exception_translated('user') + def delete_user(self, user_id, initiator=None): + domain_id, driver, entity_id = ( + self._get_domain_driver_and_entity_id(user_id)) + # Get user details to invalidate the cache. + user_old = self.get_user(user_id) + driver.delete_user(entity_id) + self.assignment_api.delete_user(user_id) + self.get_user.invalidate(self, user_id) + self.get_user_by_name.invalidate(self, user_old['name'], + user_old['domain_id']) + self.credential_api.delete_credentials_for_user(user_id) + self.id_mapping_api.delete_id_mapping(user_id) + notifications.Audit.deleted(self._USER, user_id, initiator) + + @domains_configured + @exception_translated('group') + def create_group(self, group_ref, initiator=None): + group = group_ref.copy() + group.setdefault('description', '') + domain_id = group['domain_id'] + self.resource_api.get_domain(domain_id) + + # For creating a group, the domain is in the object itself + domain_id = group_ref['domain_id'] + driver = self._select_identity_driver(domain_id) + group = self._clear_domain_id_if_domain_unaware(driver, group) + # Generate a local ID - in the future this might become a function of + # the underlying driver so that it could conform to rules set down by + # that particular driver type. + group['id'] = uuid.uuid4().hex + ref = driver.create_group(group['id'], group) + + notifications.Audit.created(self._GROUP, group['id'], initiator) + + return self._set_domain_id_and_mapping( + ref, domain_id, driver, mapping.EntityType.GROUP) + + @domains_configured + @exception_translated('group') + @MEMOIZE + def get_group(self, group_id): + domain_id, driver, entity_id = ( + self._get_domain_driver_and_entity_id(group_id)) + ref = driver.get_group(entity_id) + return self._set_domain_id_and_mapping( + ref, domain_id, driver, mapping.EntityType.GROUP) + + @domains_configured + @exception_translated('group') + def get_group_by_name(self, group_name, domain_id): + driver = self._select_identity_driver(domain_id) + ref = driver.get_group_by_name(group_name, domain_id) + return self._set_domain_id_and_mapping( + ref, domain_id, driver, mapping.EntityType.GROUP) + + @domains_configured + @exception_translated('group') + def update_group(self, group_id, group, initiator=None): + if 'domain_id' in group: + self.resource_api.get_domain(group['domain_id']) + domain_id, driver, entity_id = ( + self._get_domain_driver_and_entity_id(group_id)) + group = self._clear_domain_id_if_domain_unaware(driver, group) + ref = driver.update_group(entity_id, group) + self.get_group.invalidate(self, group_id) + notifications.Audit.updated(self._GROUP, group_id, initiator) + return self._set_domain_id_and_mapping( + ref, domain_id, driver, mapping.EntityType.GROUP) + + @domains_configured + @exception_translated('group') + def delete_group(self, group_id, initiator=None): + domain_id, driver, entity_id = ( + self._get_domain_driver_and_entity_id(group_id)) + user_ids = (u['id'] for u in self.list_users_in_group(group_id)) + driver.delete_group(entity_id) + self.get_group.invalidate(self, group_id) + self.id_mapping_api.delete_id_mapping(group_id) + self.assignment_api.delete_group(group_id) + + notifications.Audit.deleted(self._GROUP, group_id, initiator) + + for uid in user_ids: + self.emit_invalidate_user_token_persistence(uid) + + @domains_configured + @exception_translated('group') + def add_user_to_group(self, user_id, group_id): + @exception_translated('user') + def get_entity_info_for_user(public_id): + return self._get_domain_driver_and_entity_id(public_id) + + _domain_id, group_driver, group_entity_id = ( + self._get_domain_driver_and_entity_id(group_id)) + # Get the same info for the user_id, taking care to map any + # exceptions correctly + _domain_id, user_driver, user_entity_id = ( + get_entity_info_for_user(user_id)) + + self._assert_user_and_group_in_same_backend( + user_entity_id, user_driver, group_entity_id, group_driver) + + group_driver.add_user_to_group(user_entity_id, group_entity_id) + + @domains_configured + @exception_translated('group') + def remove_user_from_group(self, user_id, group_id): + @exception_translated('user') + def get_entity_info_for_user(public_id): + return self._get_domain_driver_and_entity_id(public_id) + + _domain_id, group_driver, group_entity_id = ( + self._get_domain_driver_and_entity_id(group_id)) + # Get the same info for the user_id, taking care to map any + # exceptions correctly + _domain_id, user_driver, user_entity_id = ( + get_entity_info_for_user(user_id)) + + self._assert_user_and_group_in_same_backend( + user_entity_id, user_driver, group_entity_id, group_driver) + + group_driver.remove_user_from_group(user_entity_id, group_entity_id) + self.emit_invalidate_user_token_persistence(user_id) + + @notifications.internal(notifications.INVALIDATE_USER_TOKEN_PERSISTENCE) + def emit_invalidate_user_token_persistence(self, user_id): + """Emit a notification to the callback system to revoke user tokens. + + This method and associated callback listener removes the need for + making a direct call to another manager to delete and revoke tokens. + + :param user_id: user identifier + :type user_id: string + """ + pass + + @manager.response_truncated + @domains_configured + @exception_translated('user') + def list_groups_for_user(self, user_id, hints=None): + domain_id, driver, entity_id = ( + self._get_domain_driver_and_entity_id(user_id)) + hints = hints or driver_hints.Hints() + if not driver.is_domain_aware(): + # We are effectively satisfying any domain_id filter by the above + # driver selection, so remove any such filter + self._mark_domain_id_filter_satisfied(hints) + ref_list = driver.list_groups_for_user(entity_id, hints) + return self._set_domain_id_and_mapping( + ref_list, domain_id, driver, mapping.EntityType.GROUP) + + @manager.response_truncated + @domains_configured + @exception_translated('group') + def list_groups(self, domain_scope=None, hints=None): + driver = self._select_identity_driver(domain_scope) + hints = hints or driver_hints.Hints() + if driver.is_domain_aware(): + # Force the domain_scope into the hint to ensure that we only get + # back domains for that scope. + self._ensure_domain_id_in_hints(hints, domain_scope) + else: + # We are effectively satisfying any domain_id filter by the above + # driver selection, so remove any such filter. + self._mark_domain_id_filter_satisfied(hints) + ref_list = driver.list_groups(hints) + return self._set_domain_id_and_mapping( + ref_list, domain_scope, driver, mapping.EntityType.GROUP) + + @manager.response_truncated + @domains_configured + @exception_translated('group') + def list_users_in_group(self, group_id, hints=None): + domain_id, driver, entity_id = ( + self._get_domain_driver_and_entity_id(group_id)) + hints = hints or driver_hints.Hints() + if not driver.is_domain_aware(): + # We are effectively satisfying any domain_id filter by the above + # driver selection, so remove any such filter + self._mark_domain_id_filter_satisfied(hints) + ref_list = driver.list_users_in_group(entity_id, hints) + return self._set_domain_id_and_mapping( + ref_list, domain_id, driver, mapping.EntityType.USER) + + @domains_configured + @exception_translated('group') + def check_user_in_group(self, user_id, group_id): + @exception_translated('user') + def get_entity_info_for_user(public_id): + return self._get_domain_driver_and_entity_id(public_id) + + _domain_id, group_driver, group_entity_id = ( + self._get_domain_driver_and_entity_id(group_id)) + # Get the same info for the user_id, taking care to map any + # exceptions correctly + _domain_id, user_driver, user_entity_id = ( + get_entity_info_for_user(user_id)) + + self._assert_user_and_group_in_same_backend( + user_entity_id, user_driver, group_entity_id, group_driver) + + return group_driver.check_user_in_group(user_entity_id, + group_entity_id) + + @domains_configured + def change_password(self, context, user_id, original_password, + new_password): + + # authenticate() will raise an AssertionError if authentication fails + self.authenticate(context, user_id, original_password) + + update_dict = {'password': new_password} + self.update_user(user_id, update_dict) + + +@six.add_metaclass(abc.ABCMeta) +class Driver(object): + """Interface description for an Identity driver.""" + + def _get_list_limit(self): + return CONF.identity.list_limit or CONF.list_limit + + def is_domain_aware(self): + """Indicates if Driver supports domains.""" + return True + + @property + def is_sql(self): + """Indicates if this Driver uses SQL.""" + return False + + @property + def multiple_domains_supported(self): + return (self.is_domain_aware() or + CONF.identity.domain_specific_drivers_enabled) + + def generates_uuids(self): + """Indicates if Driver generates UUIDs as the local entity ID.""" + return True + + @abc.abstractmethod + def authenticate(self, user_id, password): + """Authenticate a given user and password. + :returns: user_ref + :raises: AssertionError + """ + raise exception.NotImplemented() # pragma: no cover + + # user crud + + @abc.abstractmethod + def create_user(self, user_id, user): + """Creates a new user. + + :raises: keystone.exception.Conflict + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_users(self, hints): + """List users in the system. + + :param hints: filter hints which the driver should + implement if at all possible. + + :returns: a list of user_refs or an empty list. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_users_in_group(self, group_id, hints): + """List users in a group. + + :param group_id: the group in question + :param hints: filter hints which the driver should + implement if at all possible. + + :returns: a list of user_refs or an empty list. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def get_user(self, user_id): + """Get a user by ID. + + :returns: user_ref + :raises: keystone.exception.UserNotFound + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def update_user(self, user_id, user): + """Updates an existing user. + + :raises: keystone.exception.UserNotFound, + keystone.exception.Conflict + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def add_user_to_group(self, user_id, group_id): + """Adds a user to a group. + + :raises: keystone.exception.UserNotFound, + keystone.exception.GroupNotFound + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def check_user_in_group(self, user_id, group_id): + """Checks if a user is a member of a group. + + :raises: keystone.exception.UserNotFound, + keystone.exception.GroupNotFound + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def remove_user_from_group(self, user_id, group_id): + """Removes a user from a group. + + :raises: keystone.exception.NotFound + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def delete_user(self, user_id): + """Deletes an existing user. + + :raises: keystone.exception.UserNotFound + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def get_user_by_name(self, user_name, domain_id): + """Get a user by name. + + :returns: user_ref + :raises: keystone.exception.UserNotFound + + """ + raise exception.NotImplemented() # pragma: no cover + + # group crud + + @abc.abstractmethod + def create_group(self, group_id, group): + """Creates a new group. + + :raises: keystone.exception.Conflict + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_groups(self, hints): + """List groups in the system. + + :param hints: filter hints which the driver should + implement if at all possible. + + :returns: a list of group_refs or an empty list. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_groups_for_user(self, user_id, hints): + """List groups a user is in + + :param user_id: the user in question + :param hints: filter hints which the driver should + implement if at all possible. + + :returns: a list of group_refs or an empty list. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def get_group(self, group_id): + """Get a group by ID. + + :returns: group_ref + :raises: keystone.exception.GroupNotFound + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def get_group_by_name(self, group_name, domain_id): + """Get a group by name. + + :returns: group_ref + :raises: keystone.exception.GroupNotFound + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def update_group(self, group_id, group): + """Updates an existing group. + + :raises: keystone.exceptionGroupNotFound, + keystone.exception.Conflict + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def delete_group(self, group_id): + """Deletes an existing group. + + :raises: keystone.exception.GroupNotFound + + """ + raise exception.NotImplemented() # pragma: no cover + + # end of identity + + +@dependency.provider('id_mapping_api') +class MappingManager(manager.Manager): + """Default pivot point for the ID Mapping backend.""" + + def __init__(self): + super(MappingManager, self).__init__(CONF.identity_mapping.driver) + + +@six.add_metaclass(abc.ABCMeta) +class MappingDriver(object): + """Interface description for an ID Mapping driver.""" + + @abc.abstractmethod + def get_public_id(self, local_entity): + """Returns the public ID for the given local entity. + + :param dict local_entity: Containing the entity domain, local ID and + type ('user' or 'group'). + :returns: public ID, or None if no mapping is found. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def get_id_mapping(self, public_id): + """Returns the local mapping. + + :param public_id: The public ID for the mapping required. + :returns dict: Containing the entity domain, local ID and type. If no + mapping is found, it returns None. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def create_id_mapping(self, local_entity, public_id=None): + """Create and store a mapping to a public_id. + + :param dict local_entity: Containing the entity domain, local ID and + type ('user' or 'group'). + :param public_id: If specified, this will be the public ID. If this + is not specified, a public ID will be generated. + :returns: public ID + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def delete_id_mapping(self, public_id): + """Deletes an entry for the given public_id. + + :param public_id: The public ID for the mapping to be deleted. + + The method is silent if no mapping is found. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def purge_mappings(self, purge_filter): + """Purge selected identity mappings. + + :param dict purge_filter: Containing the attributes of the filter that + defines which entries to purge. An empty + filter means purge all mappings. + + """ + raise exception.NotImplemented() # pragma: no cover diff --git a/keystone-moon/keystone/identity/generator.py b/keystone-moon/keystone/identity/generator.py new file mode 100644 index 00000000..d25426ce --- /dev/null +++ b/keystone-moon/keystone/identity/generator.py @@ -0,0 +1,52 @@ +# Copyright 2014 IBM Corp. +# +# 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. + +"""ID Generator provider interface.""" + +import abc + +from oslo_config import cfg +import six + +from keystone.common import dependency +from keystone.common import manager +from keystone import exception + +CONF = cfg.CONF + + +@dependency.provider('id_generator_api') +class Manager(manager.Manager): + """Default pivot point for the identifier generator backend.""" + + def __init__(self): + super(Manager, self).__init__(CONF.identity_mapping.generator) + + +@six.add_metaclass(abc.ABCMeta) +class IDGenerator(object): + """Interface description for an ID Generator provider.""" + + @abc.abstractmethod + def generate_public_ID(self, mapping): + """Return a Public ID for the given mapping dict. + + :param dict mapping: The items to be hashed. + + The ID must be reproducible and no more than 64 chars in length. + The ID generated should be independent of the order of the items + in the mapping dict. + + """ + raise exception.NotImplemented() # pragma: no cover diff --git a/keystone-moon/keystone/identity/id_generators/__init__.py b/keystone-moon/keystone/identity/id_generators/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/identity/id_generators/sha256.py b/keystone-moon/keystone/identity/id_generators/sha256.py new file mode 100644 index 00000000..e3a8b416 --- /dev/null +++ b/keystone-moon/keystone/identity/id_generators/sha256.py @@ -0,0 +1,28 @@ +# Copyright 2014 IBM Corp. +# +# 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 hashlib + +import six + +from keystone.identity import generator + + +class Generator(generator.IDGenerator): + + def generate_public_ID(self, mapping): + m = hashlib.sha256() + for key in sorted(six.iterkeys(mapping)): + m.update(mapping[key].encode('utf-8')) + return m.hexdigest() diff --git a/keystone-moon/keystone/identity/mapping_backends/__init__.py b/keystone-moon/keystone/identity/mapping_backends/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/identity/mapping_backends/mapping.py b/keystone-moon/keystone/identity/mapping_backends/mapping.py new file mode 100644 index 00000000..dddf36c1 --- /dev/null +++ b/keystone-moon/keystone/identity/mapping_backends/mapping.py @@ -0,0 +1,18 @@ +# Copyright 2014 IBM Corp. +# +# 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. + + +class EntityType(object): + USER = 'user' + GROUP = 'group' diff --git a/keystone-moon/keystone/identity/mapping_backends/sql.py b/keystone-moon/keystone/identity/mapping_backends/sql.py new file mode 100644 index 00000000..b2f9cb95 --- /dev/null +++ b/keystone-moon/keystone/identity/mapping_backends/sql.py @@ -0,0 +1,97 @@ +# Copyright 2014 IBM Corp. +# +# 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 keystone.common import dependency +from keystone.common import sql +from keystone import identity +from keystone.identity.mapping_backends import mapping as identity_mapping + + +class IDMapping(sql.ModelBase, sql.ModelDictMixin): + __tablename__ = 'id_mapping' + public_id = sql.Column(sql.String(64), primary_key=True) + domain_id = sql.Column(sql.String(64), nullable=False) + local_id = sql.Column(sql.String(64), nullable=False) + # NOTE(henry-nash); Postgres requires a name to be defined for an Enum + entity_type = sql.Column( + sql.Enum(identity_mapping.EntityType.USER, + identity_mapping.EntityType.GROUP, + name='entity_type'), + nullable=False) + # Unique constraint to ensure you can't store more than one mapping to the + # same underlying values + __table_args__ = ( + sql.UniqueConstraint('domain_id', 'local_id', 'entity_type'), {}) + + +@dependency.requires('id_generator_api') +class Mapping(identity.MappingDriver): + + def get_public_id(self, local_entity): + # NOTE(henry-nash): Since the Public ID is regeneratable, rather + # than search for the entry using the local entity values, we + # could create the hash and do a PK lookup. However this would only + # work if we hashed all the entries, even those that already generate + # UUIDs, like SQL. Further, this would only work if the generation + # algorithm was immutable (e.g. it had always been sha256). + session = sql.get_session() + query = session.query(IDMapping.public_id) + query = query.filter_by(domain_id=local_entity['domain_id']) + query = query.filter_by(local_id=local_entity['local_id']) + query = query.filter_by(entity_type=local_entity['entity_type']) + try: + public_ref = query.one() + public_id = public_ref.public_id + return public_id + except sql.NotFound: + return None + + def get_id_mapping(self, public_id): + session = sql.get_session() + mapping_ref = session.query(IDMapping).get(public_id) + if mapping_ref: + return mapping_ref.to_dict() + + def create_id_mapping(self, local_entity, public_id=None): + entity = local_entity.copy() + with sql.transaction() as session: + if public_id is None: + public_id = self.id_generator_api.generate_public_ID(entity) + entity['public_id'] = public_id + mapping_ref = IDMapping.from_dict(entity) + session.add(mapping_ref) + return public_id + + def delete_id_mapping(self, public_id): + with sql.transaction() as session: + try: + session.query(IDMapping).filter( + IDMapping.public_id == public_id).delete() + except sql.NotFound: + # NOTE(morganfainberg): There is nothing to delete and nothing + # to do. + pass + + def purge_mappings(self, purge_filter): + session = sql.get_session() + query = session.query(IDMapping) + if 'domain_id' in purge_filter: + query = query.filter_by(domain_id=purge_filter['domain_id']) + if 'public_id' in purge_filter: + query = query.filter_by(public_id=purge_filter['public_id']) + if 'local_id' in purge_filter: + query = query.filter_by(local_id=purge_filter['local_id']) + if 'entity_type' in purge_filter: + query = query.filter_by(entity_type=purge_filter['entity_type']) + query.delete() diff --git a/keystone-moon/keystone/identity/routers.py b/keystone-moon/keystone/identity/routers.py new file mode 100644 index 00000000..e274d6f4 --- /dev/null +++ b/keystone-moon/keystone/identity/routers.py @@ -0,0 +1,84 @@ +# Copyright 2012 OpenStack Foundation +# +# 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. +"""WSGI Routers for the Identity service.""" + +from keystone.common import json_home +from keystone.common import router +from keystone.common import wsgi +from keystone.identity import controllers + + +class Admin(wsgi.ComposableRouter): + def add_routes(self, mapper): + # User Operations + user_controller = controllers.User() + mapper.connect('/users/{user_id}', + controller=user_controller, + action='get_user', + conditions=dict(method=['GET'])) + + +class Routers(wsgi.RoutersBase): + + def append_v3_routers(self, mapper, routers): + user_controller = controllers.UserV3() + routers.append( + router.Router(user_controller, + 'users', 'user', + resource_descriptions=self.v3_resources)) + + self._add_resource( + mapper, user_controller, + path='/users/{user_id}/password', + post_action='change_password', + rel=json_home.build_v3_resource_relation('user_change_password'), + path_vars={ + 'user_id': json_home.Parameters.USER_ID, + }) + + self._add_resource( + mapper, user_controller, + path='/groups/{group_id}/users', + get_action='list_users_in_group', + rel=json_home.build_v3_resource_relation('group_users'), + path_vars={ + 'group_id': json_home.Parameters.GROUP_ID, + }) + + self._add_resource( + mapper, user_controller, + path='/groups/{group_id}/users/{user_id}', + put_action='add_user_to_group', + get_head_action='check_user_in_group', + delete_action='remove_user_from_group', + rel=json_home.build_v3_resource_relation('group_user'), + path_vars={ + 'group_id': json_home.Parameters.GROUP_ID, + 'user_id': json_home.Parameters.USER_ID, + }) + + group_controller = controllers.GroupV3() + routers.append( + router.Router(group_controller, + 'groups', 'group', + resource_descriptions=self.v3_resources)) + + self._add_resource( + mapper, group_controller, + path='/users/{user_id}/groups', + get_action='list_groups_for_user', + rel=json_home.build_v3_resource_relation('user_groups'), + path_vars={ + 'user_id': json_home.Parameters.USER_ID, + }) diff --git a/keystone-moon/keystone/locale/de/LC_MESSAGES/keystone-log-critical.po b/keystone-moon/keystone/locale/de/LC_MESSAGES/keystone-log-critical.po new file mode 100644 index 00000000..8e4b6773 --- /dev/null +++ b/keystone-moon/keystone/locale/de/LC_MESSAGES/keystone-log-critical.po @@ -0,0 +1,25 @@ +# Translations template for keystone. +# Copyright (C) 2014 OpenStack Foundation +# This file is distributed under the same license as the keystone project. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: Keystone\n" +"Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" +"POT-Creation-Date: 2014-09-07 06:06+0000\n" +"PO-Revision-Date: 2014-08-31 15:19+0000\n" +"Last-Translator: openstackjenkins \n" +"Language-Team: German (http://www.transifex.com/projects/p/keystone/language/" +"de/)\n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 1.3\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: keystone/catalog/backends/templated.py:106 +#, python-format +msgid "Unable to open template file %s" +msgstr "Vorlagendatei %s kann nicht geöffnet werden" diff --git a/keystone-moon/keystone/locale/de/LC_MESSAGES/keystone-log-info.po b/keystone-moon/keystone/locale/de/LC_MESSAGES/keystone-log-info.po new file mode 100644 index 00000000..fdf84ad9 --- /dev/null +++ b/keystone-moon/keystone/locale/de/LC_MESSAGES/keystone-log-info.po @@ -0,0 +1,212 @@ +# Translations template for keystone. +# Copyright (C) 2015 OpenStack Foundation +# This file is distributed under the same license as the keystone project. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: Keystone\n" +"Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" +"POT-Creation-Date: 2015-03-09 06:03+0000\n" +"PO-Revision-Date: 2015-03-07 04:31+0000\n" +"Last-Translator: openstackjenkins \n" +"Language-Team: German (http://www.transifex.com/projects/p/keystone/language/" +"de/)\n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 1.3\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: keystone/assignment/core.py:250 +#, python-format +msgid "Creating the default role %s because it does not exist." +msgstr "" + +#: keystone/assignment/core.py:258 +#, python-format +msgid "Creating the default role %s failed because it was already created" +msgstr "" + +#: keystone/auth/controllers.py:64 +msgid "Loading auth-plugins by class-name is deprecated." +msgstr "" + +#: keystone/auth/controllers.py:106 +#, python-format +msgid "" +"\"expires_at\" has conflicting values %(existing)s and %(new)s. Will use " +"the earliest value." +msgstr "" + +#: keystone/common/openssl.py:81 +#, python-format +msgid "Running command - %s" +msgstr "" + +#: keystone/common/wsgi.py:79 +msgid "No bind information present in token" +msgstr "" + +#: keystone/common/wsgi.py:83 +#, python-format +msgid "Named bind mode %s not in bind information" +msgstr "" + +#: keystone/common/wsgi.py:90 +msgid "Kerberos credentials required and not present" +msgstr "" + +#: keystone/common/wsgi.py:94 +msgid "Kerberos credentials do not match those in bind" +msgstr "" + +#: keystone/common/wsgi.py:98 +msgid "Kerberos bind authentication successful" +msgstr "" + +#: keystone/common/wsgi.py:105 +#, python-format +msgid "Couldn't verify unknown bind: {%(bind_type)s: %(identifier)s}" +msgstr "" + +#: keystone/common/environment/eventlet_server.py:103 +#, python-format +msgid "Starting %(arg0)s on %(host)s:%(port)s" +msgstr "Starten von %(arg0)s auf %(host)s:%(port)s" + +#: keystone/common/kvs/core.py:138 +#, python-format +msgid "Adding proxy '%(proxy)s' to KVS %(name)s." +msgstr "" + +#: keystone/common/kvs/core.py:188 +#, python-format +msgid "Using %(func)s as KVS region %(name)s key_mangler" +msgstr "" + +#: keystone/common/kvs/core.py:200 +#, python-format +msgid "Using default dogpile sha1_mangle_key as KVS region %s key_mangler" +msgstr "" + +#: keystone/common/kvs/core.py:210 +#, python-format +msgid "KVS region %s key_mangler disabled." +msgstr "" + +#: keystone/contrib/example/core.py:64 keystone/contrib/example/core.py:73 +#, python-format +msgid "" +"Received the following notification: service %(service)s, resource_type: " +"%(resource_type)s, operation %(operation)s payload %(payload)s" +msgstr "" + +#: keystone/openstack/common/eventlet_backdoor.py:146 +#, python-format +msgid "Eventlet backdoor listening on %(port)s for process %(pid)d" +msgstr "Eventlet backdoor hört auf %(port)s für Prozess %(pid)d" + +#: keystone/openstack/common/service.py:173 +#, python-format +msgid "Caught %s, exiting" +msgstr "%s abgefangen. Vorgang wird beendet" + +#: keystone/openstack/common/service.py:231 +msgid "Parent process has died unexpectedly, exiting" +msgstr "" +"Übergeordneter Prozess wurde unerwartet abgebrochen. Vorgang wird beendet" + +#: keystone/openstack/common/service.py:262 +#, python-format +msgid "Child caught %s, exiting" +msgstr "Untergeordnetes Element %s abgefangen; Vorgang wird beendet" + +#: keystone/openstack/common/service.py:301 +msgid "Forking too fast, sleeping" +msgstr "Verzweigung zu schnell; im Ruhemodus" + +#: keystone/openstack/common/service.py:320 +#, python-format +msgid "Started child %d" +msgstr "Untergeordnetes Element %d gestartet" + +#: keystone/openstack/common/service.py:330 +#, python-format +msgid "Starting %d workers" +msgstr "Starten von %d Workers" + +#: keystone/openstack/common/service.py:347 +#, python-format +msgid "Child %(pid)d killed by signal %(sig)d" +msgstr "Untergeordnetes Element %(pid)d durch Signal %(sig)d abgebrochen" + +#: keystone/openstack/common/service.py:351 +#, python-format +msgid "Child %(pid)s exited with status %(code)d" +msgstr "Untergeordnete %(pid)s mit Status %(code)d beendet" + +#: keystone/openstack/common/service.py:390 +#, python-format +msgid "Caught %s, stopping children" +msgstr "%s abgefangen, untergeordnete Elemente werden gestoppt" + +#: keystone/openstack/common/service.py:399 +msgid "Wait called after thread killed. Cleaning up." +msgstr "" + +#: keystone/openstack/common/service.py:415 +#, python-format +msgid "Waiting on %d children to exit" +msgstr "Warten auf Beenden von %d untergeordneten Elementen" + +#: keystone/token/persistence/backends/sql.py:279 +#, python-format +msgid "Total expired tokens removed: %d" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:72 +msgid "" +"[fernet_tokens] key_repository does not appear to exist; attempting to " +"create it" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:130 +#, python-format +msgid "Created a new key: %s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:143 +msgid "Key repository is already initialized; aborting." +msgstr "" + +#: keystone/token/providers/fernet/utils.py:179 +#, python-format +msgid "Starting key rotation with %(count)s key files: %(list)s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:185 +#, python-format +msgid "Current primary key is: %s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:187 +#, python-format +msgid "Next primary key will be: %s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:197 +#, python-format +msgid "Promoted key 0 to be the primary: %s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:213 +#, python-format +msgid "Excess keys to purge: %s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:237 +#, python-format +msgid "Loaded %(count)s encryption keys from: %(dir)s" +msgstr "" diff --git a/keystone-moon/keystone/locale/en_AU/LC_MESSAGES/keystone-log-critical.po b/keystone-moon/keystone/locale/en_AU/LC_MESSAGES/keystone-log-critical.po new file mode 100644 index 00000000..d2f5ebe6 --- /dev/null +++ b/keystone-moon/keystone/locale/en_AU/LC_MESSAGES/keystone-log-critical.po @@ -0,0 +1,25 @@ +# Translations template for keystone. +# Copyright (C) 2014 OpenStack Foundation +# This file is distributed under the same license as the keystone project. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: Keystone\n" +"Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" +"POT-Creation-Date: 2014-09-07 06:06+0000\n" +"PO-Revision-Date: 2014-08-31 15:19+0000\n" +"Last-Translator: openstackjenkins \n" +"Language-Team: English (Australia) (http://www.transifex.com/projects/p/" +"keystone/language/en_AU/)\n" +"Language: en_AU\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 1.3\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: keystone/catalog/backends/templated.py:106 +#, python-format +msgid "Unable to open template file %s" +msgstr "Unable to open template file %s" diff --git a/keystone-moon/keystone/locale/en_AU/LC_MESSAGES/keystone-log-error.po b/keystone-moon/keystone/locale/en_AU/LC_MESSAGES/keystone-log-error.po new file mode 100644 index 00000000..977af694 --- /dev/null +++ b/keystone-moon/keystone/locale/en_AU/LC_MESSAGES/keystone-log-error.po @@ -0,0 +1,179 @@ +# Translations template for keystone. +# Copyright (C) 2015 OpenStack Foundation +# This file is distributed under the same license as the keystone project. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: Keystone\n" +"Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" +"POT-Creation-Date: 2015-03-09 06:03+0000\n" +"PO-Revision-Date: 2015-03-07 04:31+0000\n" +"Last-Translator: openstackjenkins \n" +"Language-Team: English (Australia) (http://www.transifex.com/projects/p/" +"keystone/language/en_AU/)\n" +"Language: en_AU\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 1.3\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: keystone/notifications.py:304 +msgid "Failed to construct notifier" +msgstr "" + +#: keystone/notifications.py:389 +#, python-format +msgid "Failed to send %(res_id)s %(event_type)s notification" +msgstr "Failed to send %(res_id)s %(event_type)s notification" + +#: keystone/notifications.py:606 +#, python-format +msgid "Failed to send %(action)s %(event_type)s notification" +msgstr "" + +#: keystone/catalog/core.py:62 +#, python-format +msgid "Malformed endpoint - %(url)r is not a string" +msgstr "" + +#: keystone/catalog/core.py:66 +#, python-format +msgid "Malformed endpoint %(url)s - unknown key %(keyerror)s" +msgstr "Malformed endpoint %(url)s - unknown key %(keyerror)s" + +#: keystone/catalog/core.py:71 +#, python-format +msgid "" +"Malformed endpoint '%(url)s'. The following type error occurred during " +"string substitution: %(typeerror)s" +msgstr "" + +#: keystone/catalog/core.py:77 +#, python-format +msgid "" +"Malformed endpoint %s - incomplete format (are you missing a type notifier ?)" +msgstr "" +"Malformed endpoint %s - incomplete format (are you missing a type notifier ?)" + +#: keystone/common/openssl.py:93 +#, python-format +msgid "Command %(to_exec)s exited with %(retcode)s- %(output)s" +msgstr "" + +#: keystone/common/openssl.py:121 +#, python-format +msgid "Failed to remove file %(file_path)r: %(error)s" +msgstr "" + +#: keystone/common/utils.py:239 +msgid "" +"Error setting up the debug environment. Verify that the option --debug-url " +"has the format : and that a debugger processes is listening on " +"that port." +msgstr "" +"Error setting up the debug environment. Verify that the option --debug-url " +"has the format : and that a debugger processes is listening on " +"that port." + +#: keystone/common/cache/core.py:100 +#, python-format +msgid "" +"Unable to build cache config-key. Expected format \":\". " +"Skipping unknown format: %s" +msgstr "" +"Unable to build cache config-key. Expected format \":\". " +"Skipping unknown format: %s" + +#: keystone/common/environment/eventlet_server.py:99 +#, python-format +msgid "Could not bind to %(host)s:%(port)s" +msgstr "" + +#: keystone/common/environment/eventlet_server.py:185 +msgid "Server error" +msgstr "Server error" + +#: keystone/contrib/endpoint_policy/core.py:129 +#: keystone/contrib/endpoint_policy/core.py:228 +#, python-format +msgid "" +"Circular reference or a repeated entry found in region tree - %(region_id)s." +msgstr "" + +#: keystone/contrib/federation/idp.py:410 +#, python-format +msgid "Error when signing assertion, reason: %(reason)s" +msgstr "" + +#: keystone/contrib/oauth1/core.py:136 +msgid "Cannot retrieve Authorization headers" +msgstr "" + +#: keystone/openstack/common/loopingcall.py:95 +msgid "in fixed duration looping call" +msgstr "in fixed duration looping call" + +#: keystone/openstack/common/loopingcall.py:138 +msgid "in dynamic looping call" +msgstr "in dynamic looping call" + +#: keystone/openstack/common/service.py:268 +msgid "Unhandled exception" +msgstr "Unhandled exception" + +#: keystone/resource/core.py:477 +#, python-format +msgid "" +"Circular reference or a repeated entry found projects hierarchy - " +"%(project_id)s." +msgstr "" + +#: keystone/resource/core.py:939 +#, python-format +msgid "" +"Unexpected results in response for domain config - %(count)s responses, " +"first option is %(option)s, expected option %(expected)s" +msgstr "" + +#: keystone/resource/backends/sql.py:102 keystone/resource/backends/sql.py:121 +#, python-format +msgid "" +"Circular reference or a repeated entry found in projects hierarchy - " +"%(project_id)s." +msgstr "" + +#: keystone/token/provider.py:292 +#, python-format +msgid "Unexpected error or malformed token determining token expiry: %s" +msgstr "Unexpected error or malformed token determining token expiry: %s" + +#: keystone/token/persistence/backends/kvs.py:226 +#, python-format +msgid "" +"Reinitializing revocation list due to error in loading revocation list from " +"backend. Expected `list` type got `%(type)s`. Old revocation list data: " +"%(list)r" +msgstr "" + +#: keystone/token/providers/common.py:611 +msgid "Failed to validate token" +msgstr "Failed to validate token" + +#: keystone/token/providers/pki.py:47 +msgid "Unable to sign token" +msgstr "Unable to sign token" + +#: keystone/token/providers/fernet/utils.py:38 +#, python-format +msgid "" +"Either [fernet_tokens] key_repository does not exist or Keystone does not " +"have sufficient permission to access it: %s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:79 +msgid "" +"Failed to create [fernet_tokens] key_repository: either it already exists or " +"you don't have sufficient permissions to create it" +msgstr "" diff --git a/keystone-moon/keystone/locale/en_AU/LC_MESSAGES/keystone.po b/keystone-moon/keystone/locale/en_AU/LC_MESSAGES/keystone.po new file mode 100644 index 00000000..e3dea47d --- /dev/null +++ b/keystone-moon/keystone/locale/en_AU/LC_MESSAGES/keystone.po @@ -0,0 +1,1542 @@ +# English (Australia) translations for keystone. +# Copyright (C) 2015 OpenStack Foundation +# This file is distributed under the same license as the keystone project. +# +# Translators: +# Tom Fifield , 2013 +msgid "" +msgstr "" +"Project-Id-Version: Keystone\n" +"Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" +"POT-Creation-Date: 2015-03-23 06:04+0000\n" +"PO-Revision-Date: 2015-03-21 23:03+0000\n" +"Last-Translator: openstackjenkins \n" +"Language-Team: English (Australia) " +"(http://www.transifex.com/projects/p/keystone/language/en_AU/)\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 1.3\n" + +#: keystone/clean.py:24 +#, python-format +msgid "%s cannot be empty." +msgstr "%s cannot be empty." + +#: keystone/clean.py:26 +#, python-format +msgid "%(property_name)s cannot be less than %(min_length)s characters." +msgstr "%(property_name)s cannot be less than %(min_length)s characters." + +#: keystone/clean.py:31 +#, python-format +msgid "%(property_name)s should not be greater than %(max_length)s characters." +msgstr "%(property_name)s should not be greater than %(max_length)s characters." + +#: keystone/clean.py:40 +#, python-format +msgid "%(property_name)s is not a %(display_expected_type)s" +msgstr "%(property_name)s is not a %(display_expected_type)s" + +#: keystone/cli.py:283 +msgid "At least one option must be provided" +msgstr "" + +#: keystone/cli.py:290 +msgid "--all option cannot be mixed with other options" +msgstr "" + +#: keystone/cli.py:301 +#, python-format +msgid "Unknown domain '%(name)s' specified by --domain-name" +msgstr "" + +#: keystone/cli.py:365 keystone/tests/unit/test_cli.py:213 +msgid "At least one option must be provided, use either --all or --domain-name" +msgstr "" + +#: keystone/cli.py:371 keystone/tests/unit/test_cli.py:229 +msgid "The --all option cannot be used with the --domain-name option" +msgstr "" + +#: keystone/cli.py:397 keystone/tests/unit/test_cli.py:246 +#, python-format +msgid "" +"Invalid domain name: %(domain)s found in config file name: %(file)s - " +"ignoring this file." +msgstr "" + +#: keystone/cli.py:405 keystone/tests/unit/test_cli.py:187 +#, python-format +msgid "" +"Domain: %(domain)s already has a configuration defined - ignoring file: " +"%(file)s." +msgstr "" + +#: keystone/cli.py:419 +#, python-format +msgid "Error parsing configuration file for domain: %(domain)s, file: %(file)s." +msgstr "" + +#: keystone/cli.py:452 +#, python-format +msgid "" +"To get a more detailed information on this error, re-run this command for" +" the specific domain, i.e.: keystone-manage domain_config_upload " +"--domain-name %s" +msgstr "" + +#: keystone/cli.py:470 +#, python-format +msgid "Unable to locate domain config directory: %s" +msgstr "Unable to locate domain config directory: %s" + +#: keystone/cli.py:503 +msgid "" +"Unable to access the keystone database, please check it is configured " +"correctly." +msgstr "" + +#: keystone/exception.py:79 +#, python-format +msgid "" +"Expecting to find %(attribute)s in %(target)s - the server could not " +"comply with the request since it is either malformed or otherwise " +"incorrect. The client is assumed to be in error." +msgstr "" + +#: keystone/exception.py:90 +#, python-format +msgid "%(detail)s" +msgstr "" + +#: keystone/exception.py:94 +msgid "" +"Timestamp not in expected format. The server could not comply with the " +"request since it is either malformed or otherwise incorrect. The client " +"is assumed to be in error." +msgstr "" +"Timestamp not in expected format. The server could not comply with the " +"request since it is either malformed or otherwise incorrect. The client " +"is assumed to be in error." + +#: keystone/exception.py:103 +#, python-format +msgid "" +"String length exceeded.The length of string '%(string)s' exceeded the " +"limit of column %(type)s(CHAR(%(length)d))." +msgstr "" +"String length exceeded.The length of string '%(string)s' exceeded the " +"limit of column %(type)s(CHAR(%(length)d))." + +#: keystone/exception.py:109 +#, python-format +msgid "" +"Request attribute %(attribute)s must be less than or equal to %(size)i. " +"The server could not comply with the request because the attribute size " +"is invalid (too large). The client is assumed to be in error." +msgstr "" +"Request attribute %(attribute)s must be less than or equal to %(size)i. " +"The server could not comply with the request because the attribute size " +"is invalid (too large). The client is assumed to be in error." + +#: keystone/exception.py:119 +#, python-format +msgid "" +"The specified parent region %(parent_region_id)s would create a circular " +"region hierarchy." +msgstr "" + +#: keystone/exception.py:126 +#, python-format +msgid "" +"The password length must be less than or equal to %(size)i. The server " +"could not comply with the request because the password is invalid." +msgstr "" + +#: keystone/exception.py:134 +#, python-format +msgid "" +"Unable to delete region %(region_id)s because it or its child regions " +"have associated endpoints." +msgstr "" + +#: keystone/exception.py:141 +msgid "" +"The certificates you requested are not available. It is likely that this " +"server does not use PKI tokens otherwise this is the result of " +"misconfiguration." +msgstr "" + +#: keystone/exception.py:150 +msgid "(Disable debug mode to suppress these details.)" +msgstr "" + +#: keystone/exception.py:155 +#, python-format +msgid "%(message)s %(amendment)s" +msgstr "" + +#: keystone/exception.py:163 +msgid "The request you have made requires authentication." +msgstr "The request you have made requires authentication." + +#: keystone/exception.py:169 +msgid "Authentication plugin error." +msgstr "Authentication plugin error." + +#: keystone/exception.py:177 +#, python-format +msgid "Unable to find valid groups while using mapping %(mapping_id)s" +msgstr "" + +#: keystone/exception.py:182 +msgid "Attempted to authenticate with an unsupported method." +msgstr "Attempted to authenticate with an unsupported method." + +#: keystone/exception.py:190 +msgid "Additional authentications steps required." +msgstr "Additional authentications steps required." + +#: keystone/exception.py:198 +msgid "You are not authorized to perform the requested action." +msgstr "You are not authorized to perform the requested action." + +#: keystone/exception.py:205 +#, python-format +msgid "You are not authorized to perform the requested action: %(action)s" +msgstr "" + +#: keystone/exception.py:210 +#, python-format +msgid "" +"Could not change immutable attribute(s) '%(attributes)s' in target " +"%(target)s" +msgstr "" + +#: keystone/exception.py:215 +#, python-format +msgid "" +"Group membership across backend boundaries is not allowed, group in " +"question is %(group_id)s, user is %(user_id)s" +msgstr "" + +#: keystone/exception.py:221 +#, python-format +msgid "" +"Invalid mix of entities for policy association - only Endpoint, Service " +"or Region+Service allowed. Request was - Endpoint: %(endpoint_id)s, " +"Service: %(service_id)s, Region: %(region_id)s" +msgstr "" + +#: keystone/exception.py:228 +#, python-format +msgid "Invalid domain specific configuration: %(reason)s" +msgstr "" + +#: keystone/exception.py:232 +#, python-format +msgid "Could not find: %(target)s" +msgstr "" + +#: keystone/exception.py:238 +#, python-format +msgid "Could not find endpoint: %(endpoint_id)s" +msgstr "" + +#: keystone/exception.py:245 +msgid "An unhandled exception has occurred: Could not find metadata." +msgstr "An unhandled exception has occurred: Could not find metadata." + +#: keystone/exception.py:250 +#, python-format +msgid "Could not find policy: %(policy_id)s" +msgstr "" + +#: keystone/exception.py:254 +msgid "Could not find policy association" +msgstr "" + +#: keystone/exception.py:258 +#, python-format +msgid "Could not find role: %(role_id)s" +msgstr "" + +#: keystone/exception.py:262 +#, python-format +msgid "" +"Could not find role assignment with role: %(role_id)s, user or group: " +"%(actor_id)s, project or domain: %(target_id)s" +msgstr "" + +#: keystone/exception.py:268 +#, python-format +msgid "Could not find region: %(region_id)s" +msgstr "" + +#: keystone/exception.py:272 +#, python-format +msgid "Could not find service: %(service_id)s" +msgstr "" + +#: keystone/exception.py:276 +#, python-format +msgid "Could not find domain: %(domain_id)s" +msgstr "" + +#: keystone/exception.py:280 +#, python-format +msgid "Could not find project: %(project_id)s" +msgstr "" + +#: keystone/exception.py:284 +#, python-format +msgid "Cannot create project with parent: %(project_id)s" +msgstr "" + +#: keystone/exception.py:288 +#, python-format +msgid "Could not find token: %(token_id)s" +msgstr "" + +#: keystone/exception.py:292 +#, python-format +msgid "Could not find user: %(user_id)s" +msgstr "" + +#: keystone/exception.py:296 +#, python-format +msgid "Could not find group: %(group_id)s" +msgstr "" + +#: keystone/exception.py:300 +#, python-format +msgid "Could not find mapping: %(mapping_id)s" +msgstr "" + +#: keystone/exception.py:304 +#, python-format +msgid "Could not find trust: %(trust_id)s" +msgstr "" + +#: keystone/exception.py:308 +#, python-format +msgid "No remaining uses for trust: %(trust_id)s" +msgstr "" + +#: keystone/exception.py:312 +#, python-format +msgid "Could not find credential: %(credential_id)s" +msgstr "" + +#: keystone/exception.py:316 +#, python-format +msgid "Could not find version: %(version)s" +msgstr "" + +#: keystone/exception.py:320 +#, python-format +msgid "Could not find Endpoint Group: %(endpoint_group_id)s" +msgstr "" + +#: keystone/exception.py:324 +#, python-format +msgid "Could not find Identity Provider: %(idp_id)s" +msgstr "" + +#: keystone/exception.py:328 +#, python-format +msgid "Could not find Service Provider: %(sp_id)s" +msgstr "" + +#: keystone/exception.py:332 +#, python-format +msgid "" +"Could not find federated protocol %(protocol_id)s for Identity Provider: " +"%(idp_id)s" +msgstr "" + +#: keystone/exception.py:343 +#, python-format +msgid "" +"Could not find %(group_or_option)s in domain configuration for domain " +"%(domain_id)s" +msgstr "" + +#: keystone/exception.py:348 +#, python-format +msgid "Conflict occurred attempting to store %(type)s - %(details)s" +msgstr "" + +#: keystone/exception.py:356 +msgid "An unexpected error prevented the server from fulfilling your request." +msgstr "" + +#: keystone/exception.py:359 +#, python-format +msgid "" +"An unexpected error prevented the server from fulfilling your request: " +"%(exception)s" +msgstr "" + +#: keystone/exception.py:382 +#, python-format +msgid "Unable to consume trust %(trust_id)s, unable to acquire lock." +msgstr "" + +#: keystone/exception.py:387 +msgid "" +"Expected signing certificates are not available on the server. Please " +"check Keystone configuration." +msgstr "" + +#: keystone/exception.py:393 +#, python-format +msgid "Malformed endpoint URL (%(endpoint)s), see ERROR log for details." +msgstr "Malformed endpoint URL (%(endpoint)s), see ERROR log for details." + +#: keystone/exception.py:398 +#, python-format +msgid "" +"Group %(group_id)s returned by mapping %(mapping_id)s was not found in " +"the backend." +msgstr "" + +#: keystone/exception.py:403 +#, python-format +msgid "Error while reading metadata file, %(reason)s" +msgstr "" + +#: keystone/exception.py:407 +#, python-format +msgid "" +"Unexpected combination of grant attributes - User: %(user_id)s, Group: " +"%(group_id)s, Project: %(project_id)s, Domain: %(domain_id)s" +msgstr "" + +#: keystone/exception.py:414 +msgid "The action you have requested has not been implemented." +msgstr "The action you have requested has not been implemented." + +#: keystone/exception.py:421 +msgid "The service you have requested is no longer available on this server." +msgstr "" + +#: keystone/exception.py:428 +#, python-format +msgid "The Keystone configuration file %(config_file)s could not be found." +msgstr "The Keystone configuration file %(config_file)s could not be found." + +#: keystone/exception.py:433 +msgid "" +"No encryption keys found; run keystone-manage fernet_setup to bootstrap " +"one." +msgstr "" + +#: keystone/exception.py:438 +#, python-format +msgid "" +"The Keystone domain-specific configuration has specified more than one " +"SQL driver (only one is permitted): %(source)s." +msgstr "" + +#: keystone/exception.py:445 +#, python-format +msgid "" +"%(mod_name)s doesn't provide database migrations. The migration " +"repository path at %(path)s doesn't exist or isn't a directory." +msgstr "" + +#: keystone/exception.py:457 +#, python-format +msgid "" +"Unable to sign SAML assertion. It is likely that this server does not " +"have xmlsec1 installed, or this is the result of misconfiguration. Reason" +" %(reason)s" +msgstr "" + +#: keystone/exception.py:465 +msgid "" +"No Authorization headers found, cannot proceed with OAuth related calls, " +"if running under HTTPd or Apache, ensure WSGIPassAuthorization is set to " +"On." +msgstr "" + +#: keystone/notifications.py:250 +#, python-format +msgid "%(event)s is not a valid notification event, must be one of: %(actions)s" +msgstr "" + +#: keystone/notifications.py:259 +#, python-format +msgid "Method not callable: %s" +msgstr "" + +#: keystone/assignment/controllers.py:107 keystone/identity/controllers.py:69 +#: keystone/resource/controllers.py:78 +msgid "Name field is required and cannot be empty" +msgstr "Name field is required and cannot be empty" + +#: keystone/assignment/controllers.py:330 +#: keystone/assignment/controllers.py:753 +msgid "Specify a domain or project, not both" +msgstr "Specify a domain or project, not both" + +#: keystone/assignment/controllers.py:333 +msgid "Specify one of domain or project" +msgstr "" + +#: keystone/assignment/controllers.py:338 +#: keystone/assignment/controllers.py:758 +msgid "Specify a user or group, not both" +msgstr "Specify a user or group, not both" + +#: keystone/assignment/controllers.py:341 +msgid "Specify one of user or group" +msgstr "" + +#: keystone/assignment/controllers.py:742 +msgid "Combining effective and group filter will always result in an empty list." +msgstr "" + +#: keystone/assignment/controllers.py:747 +msgid "" +"Combining effective, domain and inherited filters will always result in " +"an empty list." +msgstr "" + +#: keystone/assignment/core.py:228 +msgid "Must specify either domain or project" +msgstr "" + +#: keystone/assignment/core.py:493 +#, python-format +msgid "Project (%s)" +msgstr "Project (%s)" + +#: keystone/assignment/core.py:495 +#, python-format +msgid "Domain (%s)" +msgstr "Domain (%s)" + +#: keystone/assignment/core.py:497 +msgid "Unknown Target" +msgstr "Unknown Target" + +#: keystone/assignment/backends/ldap.py:92 +msgid "Domain metadata not supported by LDAP" +msgstr "" + +#: keystone/assignment/backends/ldap.py:381 +#, python-format +msgid "User %(user_id)s already has role %(role_id)s in tenant %(tenant_id)s" +msgstr "" + +#: keystone/assignment/backends/ldap.py:387 +#, python-format +msgid "Role %s not found" +msgstr "Role %s not found" + +#: keystone/assignment/backends/ldap.py:402 +#: keystone/assignment/backends/sql.py:335 +#, python-format +msgid "Cannot remove role that has not been granted, %s" +msgstr "Cannot remove role that has not been granted, %s" + +#: keystone/assignment/backends/sql.py:356 +#, python-format +msgid "Unexpected assignment type encountered, %s" +msgstr "" + +#: keystone/assignment/role_backends/ldap.py:61 keystone/catalog/core.py:103 +#: keystone/common/ldap/core.py:1400 keystone/resource/backends/ldap.py:149 +#, python-format +msgid "Duplicate ID, %s." +msgstr "Duplicate ID, %s." + +#: keystone/assignment/role_backends/ldap.py:69 +#: keystone/common/ldap/core.py:1390 +#, python-format +msgid "Duplicate name, %s." +msgstr "Duplicate name, %s." + +#: keystone/assignment/role_backends/ldap.py:119 +#, python-format +msgid "Cannot duplicate name %s" +msgstr "" + +#: keystone/auth/controllers.py:60 +#, python-format +msgid "" +"Cannot load an auth-plugin by class-name without a \"method\" attribute " +"defined: %s" +msgstr "" + +#: keystone/auth/controllers.py:71 +#, python-format +msgid "" +"Auth plugin %(plugin)s is requesting previously registered method " +"%(method)s" +msgstr "" + +#: keystone/auth/controllers.py:115 +#, python-format +msgid "" +"Unable to reconcile identity attribute %(attribute)s as it has " +"conflicting values %(new)s and %(old)s" +msgstr "" + +#: keystone/auth/controllers.py:336 +msgid "Scoping to both domain and project is not allowed" +msgstr "Scoping to both domain and project is not allowed" + +#: keystone/auth/controllers.py:339 +msgid "Scoping to both domain and trust is not allowed" +msgstr "Scoping to both domain and trust is not allowed" + +#: keystone/auth/controllers.py:342 +msgid "Scoping to both project and trust is not allowed" +msgstr "Scoping to both project and trust is not allowed" + +#: keystone/auth/controllers.py:512 +msgid "User not found" +msgstr "User not found" + +#: keystone/auth/controllers.py:616 +msgid "A project-scoped token is required to produce a service catalog." +msgstr "" + +#: keystone/auth/plugins/external.py:46 +msgid "No authenticated user" +msgstr "No authenticated user" + +#: keystone/auth/plugins/external.py:56 +#, python-format +msgid "Unable to lookup user %s" +msgstr "Unable to lookup user %s" + +#: keystone/auth/plugins/external.py:107 +msgid "auth_type is not Negotiate" +msgstr "" + +#: keystone/auth/plugins/mapped.py:244 +msgid "Could not map user" +msgstr "" + +#: keystone/auth/plugins/oauth1.py:39 +#, python-format +msgid "%s not supported" +msgstr "" + +#: keystone/auth/plugins/oauth1.py:57 +msgid "Access token is expired" +msgstr "Access token is expired" + +#: keystone/auth/plugins/oauth1.py:71 +msgid "Could not validate the access token" +msgstr "" + +#: keystone/auth/plugins/password.py:46 +msgid "Invalid username or password" +msgstr "Invalid username or password" + +#: keystone/auth/plugins/token.py:72 keystone/token/controllers.py:160 +msgid "rescope a scoped token" +msgstr "" + +#: keystone/catalog/controllers.py:168 +#, python-format +msgid "Conflicting region IDs specified: \"%(url_id)s\" != \"%(ref_id)s\"" +msgstr "" + +#: keystone/common/authorization.py:47 keystone/common/wsgi.py:64 +#, python-format +msgid "token reference must be a KeystoneToken type, got: %s" +msgstr "" + +#: keystone/common/base64utils.py:66 +msgid "pad must be single character" +msgstr "pad must be single character" + +#: keystone/common/base64utils.py:215 +#, python-format +msgid "text is multiple of 4, but pad \"%s\" occurs before 2nd to last char" +msgstr "text is multiple of 4, but pad \"%s\" occurs before 2nd to last char" + +#: keystone/common/base64utils.py:219 +#, python-format +msgid "text is multiple of 4, but pad \"%s\" occurs before non-pad last char" +msgstr "text is multiple of 4, but pad \"%s\" occurs before non-pad last char" + +#: keystone/common/base64utils.py:225 +#, python-format +msgid "text is not a multiple of 4, but contains pad \"%s\"" +msgstr "text is not a multiple of 4, but contains pad \"%s\"" + +#: keystone/common/base64utils.py:244 keystone/common/base64utils.py:265 +msgid "padded base64url text must be multiple of 4 characters" +msgstr "padded base64url text must be multiple of 4 characters" + +#: keystone/common/controller.py:237 keystone/token/providers/common.py:589 +msgid "Non-default domain is not supported" +msgstr "Non-default domain is not supported" + +#: keystone/common/controller.py:305 keystone/identity/core.py:428 +#: keystone/resource/core.py:761 keystone/resource/backends/ldap.py:61 +#, python-format +msgid "Expected dict or list: %s" +msgstr "Expected dict or list: %s" + +#: keystone/common/controller.py:318 +msgid "Marker could not be found" +msgstr "Marker could not be found" + +#: keystone/common/controller.py:329 +msgid "Invalid limit value" +msgstr "Invalid limit value" + +#: keystone/common/controller.py:637 +msgid "Cannot change Domain ID" +msgstr "" + +#: keystone/common/controller.py:666 +msgid "domain_id is required as part of entity" +msgstr "" + +#: keystone/common/controller.py:701 +msgid "A domain-scoped token must be used" +msgstr "" + +#: keystone/common/dependency.py:68 +#, python-format +msgid "Unregistered dependency: %(name)s for %(targets)s" +msgstr "" + +#: keystone/common/dependency.py:108 +msgid "event_callbacks must be a dict" +msgstr "" + +#: keystone/common/dependency.py:113 +#, python-format +msgid "event_callbacks[%s] must be a dict" +msgstr "" + +#: keystone/common/pemutils.py:223 +#, python-format +msgid "unknown pem_type \"%(pem_type)s\", valid types are: %(valid_pem_types)s" +msgstr "unknown pem_type \"%(pem_type)s\", valid types are: %(valid_pem_types)s" + +#: keystone/common/pemutils.py:242 +#, python-format +msgid "" +"unknown pem header \"%(pem_header)s\", valid headers are: " +"%(valid_pem_headers)s" +msgstr "" +"unknown pem header \"%(pem_header)s\", valid headers are: " +"%(valid_pem_headers)s" + +#: keystone/common/pemutils.py:298 +#, python-format +msgid "failed to find end matching \"%s\"" +msgstr "failed to find end matching \"%s\"" + +#: keystone/common/pemutils.py:302 +#, python-format +msgid "" +"beginning & end PEM headers do not match (%(begin_pem_header)s!= " +"%(end_pem_header)s)" +msgstr "" +"beginning & end PEM headers do not match (%(begin_pem_header)s!= " +"%(end_pem_header)s)" + +#: keystone/common/pemutils.py:377 +#, python-format +msgid "unknown pem_type: \"%s\"" +msgstr "unknown pem_type: \"%s\"" + +#: keystone/common/pemutils.py:389 +#, python-format +msgid "" +"failed to base64 decode %(pem_type)s PEM at position%(position)d: " +"%(err_msg)s" +msgstr "" +"failed to base64 decode %(pem_type)s PEM at position%(position)d: " +"%(err_msg)s" + +#: keystone/common/utils.py:164 keystone/credential/controllers.py:44 +msgid "Invalid blob in credential" +msgstr "Invalid blob in credential" + +#: keystone/common/wsgi.py:330 +#, python-format +msgid "%s field is required and cannot be empty" +msgstr "" + +#: keystone/common/wsgi.py:342 +#, python-format +msgid "%s field(s) cannot be empty" +msgstr "" + +#: keystone/common/wsgi.py:563 +msgid "The resource could not be found." +msgstr "The resource could not be found." + +#: keystone/common/wsgi.py:704 +#, python-format +msgid "Unexpected status requested for JSON Home response, %s" +msgstr "" + +#: keystone/common/cache/_memcache_pool.py:113 +#, python-format +msgid "Unable to get a connection from pool id %(id)s after %(seconds)s seconds." +msgstr "" + +#: keystone/common/cache/core.py:132 +msgid "region not type dogpile.cache.CacheRegion" +msgstr "region not type dogpile.cache.CacheRegion" + +#: keystone/common/cache/backends/mongo.py:231 +msgid "db_hosts value is required" +msgstr "" + +#: keystone/common/cache/backends/mongo.py:236 +msgid "database db_name is required" +msgstr "" + +#: keystone/common/cache/backends/mongo.py:241 +msgid "cache_collection name is required" +msgstr "" + +#: keystone/common/cache/backends/mongo.py:252 +msgid "integer value expected for w (write concern attribute)" +msgstr "" + +#: keystone/common/cache/backends/mongo.py:260 +msgid "replicaset_name required when use_replica is True" +msgstr "" + +#: keystone/common/cache/backends/mongo.py:275 +msgid "integer value expected for mongo_ttl_seconds" +msgstr "" + +#: keystone/common/cache/backends/mongo.py:301 +msgid "no ssl support available" +msgstr "" + +#: keystone/common/cache/backends/mongo.py:310 +#, python-format +msgid "" +"Invalid ssl_cert_reqs value of %s, must be one of \"NONE\", \"OPTIONAL\"," +" \"REQUIRED\"" +msgstr "" + +#: keystone/common/kvs/core.py:71 +#, python-format +msgid "Lock Timeout occurred for key, %(target)s" +msgstr "" + +#: keystone/common/kvs/core.py:106 +#, python-format +msgid "KVS region %s is already configured. Cannot reconfigure." +msgstr "" + +#: keystone/common/kvs/core.py:145 +#, python-format +msgid "Key Value Store not configured: %s" +msgstr "" + +#: keystone/common/kvs/core.py:198 +msgid "`key_mangler` option must be a function reference" +msgstr "" + +#: keystone/common/kvs/core.py:353 +#, python-format +msgid "Lock key must match target key: %(lock)s != %(target)s" +msgstr "" + +#: keystone/common/kvs/core.py:357 +msgid "Must be called within an active lock context." +msgstr "" + +#: keystone/common/kvs/backends/memcached.py:69 +#, python-format +msgid "Maximum lock attempts on %s occurred." +msgstr "" + +#: keystone/common/kvs/backends/memcached.py:108 +#, python-format +msgid "" +"Backend `%(driver)s` is not a valid memcached backend. Valid drivers: " +"%(driver_list)s" +msgstr "" + +#: keystone/common/kvs/backends/memcached.py:178 +msgid "`key_mangler` functions must be callable." +msgstr "" + +#: keystone/common/ldap/core.py:191 +#, python-format +msgid "Invalid LDAP deref option: %(option)s. Choose one of: %(options)s" +msgstr "" + +#: keystone/common/ldap/core.py:201 +#, python-format +msgid "Invalid LDAP TLS certs option: %(option)s. Choose one of: %(options)s" +msgstr "Invalid LDAP TLS certs option: %(option)s. Choose one of: %(options)s" + +#: keystone/common/ldap/core.py:213 +#, python-format +msgid "Invalid LDAP scope: %(scope)s. Choose one of: %(options)s" +msgstr "Invalid LDAP scope: %(scope)s. Choose one of: %(options)s" + +#: keystone/common/ldap/core.py:588 +msgid "Invalid TLS / LDAPS combination" +msgstr "Invalid TLS / LDAPS combination" + +#: keystone/common/ldap/core.py:593 +#, python-format +msgid "Invalid LDAP TLS_AVAIL option: %s. TLS not available" +msgstr "Invalid LDAP TLS_AVAIL option: %s. TLS not available" + +#: keystone/common/ldap/core.py:603 +#, python-format +msgid "tls_cacertfile %s not found or is not a file" +msgstr "tls_cacertfile %s not found or is not a file" + +#: keystone/common/ldap/core.py:615 +#, python-format +msgid "tls_cacertdir %s not found or is not a directory" +msgstr "tls_cacertdir %s not found or is not a directory" + +#: keystone/common/ldap/core.py:1325 +#, python-format +msgid "ID attribute %(id_attr)s not found in LDAP object %(dn)s" +msgstr "" + +#: keystone/common/ldap/core.py:1369 +#, python-format +msgid "LDAP %s create" +msgstr "LDAP %s create" + +#: keystone/common/ldap/core.py:1374 +#, python-format +msgid "LDAP %s update" +msgstr "LDAP %s update" + +#: keystone/common/ldap/core.py:1379 +#, python-format +msgid "LDAP %s delete" +msgstr "LDAP %s delete" + +#: keystone/common/ldap/core.py:1521 +msgid "" +"Disabling an entity where the 'enable' attribute is ignored by " +"configuration." +msgstr "" + +#: keystone/common/ldap/core.py:1532 +#, python-format +msgid "Cannot change %(option_name)s %(attr)s" +msgstr "Cannot change %(option_name)s %(attr)s" + +#: keystone/common/ldap/core.py:1619 +#, python-format +msgid "Member %(member)s is already a member of group %(group)s" +msgstr "" + +#: keystone/common/sql/core.py:219 +msgid "" +"Cannot truncate a driver call without hints list as first parameter after" +" self " +msgstr "" + +#: keystone/common/sql/core.py:410 +msgid "Duplicate Entry" +msgstr "" + +#: keystone/common/sql/core.py:426 +#, python-format +msgid "An unexpected error occurred when trying to store %s" +msgstr "" + +#: keystone/common/sql/migration_helpers.py:187 +#: keystone/common/sql/migration_helpers.py:245 +#, python-format +msgid "%s extension does not exist." +msgstr "" + +#: keystone/common/validation/validators.py:54 +#, python-format +msgid "Invalid input for field '%(path)s'. The value is '%(value)s'." +msgstr "" + +#: keystone/contrib/ec2/controllers.py:318 +msgid "Token belongs to another user" +msgstr "Token belongs to another user" + +#: keystone/contrib/ec2/controllers.py:346 +msgid "Credential belongs to another user" +msgstr "Credential belongs to another user" + +#: keystone/contrib/endpoint_filter/backends/sql.py:69 +#, python-format +msgid "Endpoint %(endpoint_id)s not found in project %(project_id)s" +msgstr "Endpoint %(endpoint_id)s not found in project %(project_id)s" + +#: keystone/contrib/endpoint_filter/backends/sql.py:180 +msgid "Endpoint Group Project Association not found" +msgstr "" + +#: keystone/contrib/endpoint_policy/core.py:258 +#, python-format +msgid "No policy is associated with endpoint %(endpoint_id)s." +msgstr "" + +#: keystone/contrib/federation/controllers.py:274 +msgid "Missing entity ID from environment" +msgstr "" + +#: keystone/contrib/federation/controllers.py:282 +msgid "Request must have an origin query parameter" +msgstr "" + +#: keystone/contrib/federation/controllers.py:292 +#, python-format +msgid "%(host)s is not a trusted dashboard host" +msgstr "" + +#: keystone/contrib/federation/controllers.py:333 +msgid "Use a project scoped token when attempting to create a SAML assertion" +msgstr "" + +#: keystone/contrib/federation/idp.py:454 +#, python-format +msgid "Cannot open certificate %(cert_file)s. Reason: %(reason)s" +msgstr "" + +#: keystone/contrib/federation/idp.py:521 +msgid "Ensure configuration option idp_entity_id is set." +msgstr "" + +#: keystone/contrib/federation/idp.py:524 +msgid "Ensure configuration option idp_sso_endpoint is set." +msgstr "" + +#: keystone/contrib/federation/idp.py:544 +msgid "" +"idp_contact_type must be one of: [technical, other, support, " +"administrative or billing." +msgstr "" + +#: keystone/contrib/federation/utils.py:178 +msgid "Federation token is expired" +msgstr "" + +#: keystone/contrib/federation/utils.py:208 +msgid "" +"Could not find Identity Provider identifier in environment, check " +"[federation] remote_id_attribute for details." +msgstr "" + +#: keystone/contrib/federation/utils.py:213 +msgid "" +"Incoming identity provider identifier not included among the accepted " +"identifiers." +msgstr "" + +#: keystone/contrib/federation/utils.py:501 +#, python-format +msgid "User type %s not supported" +msgstr "" + +#: keystone/contrib/federation/utils.py:537 +#, python-format +msgid "" +"Invalid rule: %(identity_value)s. Both 'groups' and 'domain' keywords " +"must be specified." +msgstr "" + +#: keystone/contrib/federation/utils.py:753 +#, python-format +msgid "Identity Provider %(idp)s is disabled" +msgstr "" + +#: keystone/contrib/federation/utils.py:761 +#, python-format +msgid "Service Provider %(sp)s is disabled" +msgstr "" + +#: keystone/contrib/oauth1/controllers.py:99 +msgid "Cannot change consumer secret" +msgstr "Cannot change consumer secret" + +#: keystone/contrib/oauth1/controllers.py:131 +msgid "Cannot list request tokens with a token issued via delegation." +msgstr "" + +#: keystone/contrib/oauth1/controllers.py:192 +#: keystone/contrib/oauth1/backends/sql.py:270 +msgid "User IDs do not match" +msgstr "User IDs do not match" + +#: keystone/contrib/oauth1/controllers.py:199 +msgid "Could not find role" +msgstr "Could not find role" + +#: keystone/contrib/oauth1/controllers.py:248 +msgid "Invalid signature" +msgstr "" + +#: keystone/contrib/oauth1/controllers.py:299 +#: keystone/contrib/oauth1/controllers.py:377 +msgid "Request token is expired" +msgstr "Request token is expired" + +#: keystone/contrib/oauth1/controllers.py:313 +msgid "There should not be any non-oauth parameters" +msgstr "There should not be any non-oauth parameters" + +#: keystone/contrib/oauth1/controllers.py:317 +msgid "provided consumer key does not match stored consumer key" +msgstr "provided consumer key does not match stored consumer key" + +#: keystone/contrib/oauth1/controllers.py:321 +msgid "provided verifier does not match stored verifier" +msgstr "provided verifier does not match stored verifier" + +#: keystone/contrib/oauth1/controllers.py:325 +msgid "provided request key does not match stored request key" +msgstr "provided request key does not match stored request key" + +#: keystone/contrib/oauth1/controllers.py:329 +msgid "Request Token does not have an authorizing user id" +msgstr "Request Token does not have an authorizing user id" + +#: keystone/contrib/oauth1/controllers.py:366 +msgid "Cannot authorize a request token with a token issued via delegation." +msgstr "" + +#: keystone/contrib/oauth1/controllers.py:396 +msgid "authorizing user does not have role required" +msgstr "authorizing user does not have role required" + +#: keystone/contrib/oauth1/controllers.py:409 +msgid "User is not a member of the requested project" +msgstr "User is not a member of the requested project" + +#: keystone/contrib/oauth1/backends/sql.py:91 +msgid "Consumer not found" +msgstr "Consumer not found" + +#: keystone/contrib/oauth1/backends/sql.py:186 +msgid "Request token not found" +msgstr "Request token not found" + +#: keystone/contrib/oauth1/backends/sql.py:250 +msgid "Access token not found" +msgstr "Access token not found" + +#: keystone/contrib/revoke/controllers.py:33 +#, python-format +msgid "invalid date format %s" +msgstr "" + +#: keystone/contrib/revoke/core.py:150 +msgid "" +"The revoke call must not have both domain_id and project_id. This is a " +"bug in the Keystone server. The current request is aborted." +msgstr "" + +#: keystone/contrib/revoke/core.py:218 keystone/token/provider.py:207 +#: keystone/token/provider.py:230 keystone/token/provider.py:296 +#: keystone/token/provider.py:303 +msgid "Failed to validate token" +msgstr "Failed to validate token" + +#: keystone/identity/controllers.py:72 +msgid "Enabled field must be a boolean" +msgstr "Enabled field must be a boolean" + +#: keystone/identity/controllers.py:98 +msgid "Enabled field should be a boolean" +msgstr "Enabled field should be a boolean" + +#: keystone/identity/core.py:112 +#, python-format +msgid "Database at /domains/%s/config" +msgstr "" + +#: keystone/identity/core.py:287 keystone/identity/backends/ldap.py:59 +#: keystone/identity/backends/ldap.py:61 keystone/identity/backends/ldap.py:67 +#: keystone/identity/backends/ldap.py:69 keystone/identity/backends/sql.py:104 +#: keystone/identity/backends/sql.py:106 +msgid "Invalid user / password" +msgstr "" + +#: keystone/identity/core.py:693 +#, python-format +msgid "User is disabled: %s" +msgstr "User is disabled: %s" + +#: keystone/identity/core.py:735 +msgid "Cannot change user ID" +msgstr "" + +#: keystone/identity/backends/ldap.py:99 +msgid "Cannot change user name" +msgstr "" + +#: keystone/identity/backends/ldap.py:188 keystone/identity/backends/sql.py:188 +#: keystone/identity/backends/sql.py:206 +#, python-format +msgid "User '%(user_id)s' not found in group '%(group_id)s'" +msgstr "" + +#: keystone/identity/backends/ldap.py:339 +#, python-format +msgid "User %(user_id)s is already a member of group %(group_id)s" +msgstr "User %(user_id)s is already a member of group %(group_id)s" + +#: keystone/models/token_model.py:61 +msgid "Found invalid token: scoped to both project and domain." +msgstr "" + +#: keystone/openstack/common/versionutils.py:108 +#, python-format +msgid "" +"%(what)s is deprecated as of %(as_of)s in favor of %(in_favor_of)s and " +"may be removed in %(remove_in)s." +msgstr "" +"%(what)s is deprecated as of %(as_of)s in favor of %(in_favor_of)s and " +"may be removed in %(remove_in)s." + +#: keystone/openstack/common/versionutils.py:112 +#, python-format +msgid "" +"%(what)s is deprecated as of %(as_of)s and may be removed in " +"%(remove_in)s. It will not be superseded." +msgstr "" +"%(what)s is deprecated as of %(as_of)s and may be removed in " +"%(remove_in)s. It will not be superseded." + +#: keystone/openstack/common/versionutils.py:116 +#, python-format +msgid "%(what)s is deprecated as of %(as_of)s in favor of %(in_favor_of)s." +msgstr "" + +#: keystone/openstack/common/versionutils.py:119 +#, python-format +msgid "%(what)s is deprecated as of %(as_of)s. It will not be superseded." +msgstr "" + +#: keystone/openstack/common/versionutils.py:241 +#, python-format +msgid "Deprecated: %s" +msgstr "Deprecated: %s" + +#: keystone/openstack/common/versionutils.py:259 +#, python-format +msgid "Fatal call to deprecated config: %(msg)s" +msgstr "Fatal call to deprecated config: %(msg)s" + +#: keystone/resource/controllers.py:231 +msgid "" +"Cannot use parents_as_list and parents_as_ids query params at the same " +"time." +msgstr "" + +#: keystone/resource/controllers.py:237 +msgid "" +"Cannot use subtree_as_list and subtree_as_ids query params at the same " +"time." +msgstr "" + +#: keystone/resource/core.py:80 +#, python-format +msgid "max hierarchy depth reached for %s branch." +msgstr "" + +#: keystone/resource/core.py:97 +msgid "cannot create a project within a different domain than its parents." +msgstr "" + +#: keystone/resource/core.py:101 +#, python-format +msgid "cannot create a project in a branch containing a disabled project: %s" +msgstr "" + +#: keystone/resource/core.py:123 +#, python-format +msgid "Domain is disabled: %s" +msgstr "Domain is disabled: %s" + +#: keystone/resource/core.py:141 +#, python-format +msgid "Domain cannot be named %s" +msgstr "" + +#: keystone/resource/core.py:144 +#, python-format +msgid "Domain cannot have ID %s" +msgstr "" + +#: keystone/resource/core.py:156 +#, python-format +msgid "Project is disabled: %s" +msgstr "Project is disabled: %s" + +#: keystone/resource/core.py:176 +#, python-format +msgid "cannot enable project %s since it has disabled parents" +msgstr "" + +#: keystone/resource/core.py:184 +#, python-format +msgid "cannot disable project %s since its subtree contains enabled projects" +msgstr "" + +#: keystone/resource/core.py:195 +msgid "Update of `parent_id` is not allowed." +msgstr "" + +#: keystone/resource/core.py:222 +#, python-format +msgid "cannot delete the project %s since it is not a leaf in the hierarchy." +msgstr "" + +#: keystone/resource/core.py:376 +msgid "Multiple domains are not supported" +msgstr "" + +#: keystone/resource/core.py:429 +msgid "delete the default domain" +msgstr "" + +#: keystone/resource/core.py:440 +msgid "cannot delete a domain that is enabled, please disable it first." +msgstr "" + +#: keystone/resource/core.py:841 +msgid "No options specified" +msgstr "No options specified" + +#: keystone/resource/core.py:847 +#, python-format +msgid "" +"The value of group %(group)s specified in the config should be a " +"dictionary of options" +msgstr "" + +#: keystone/resource/core.py:871 +#, python-format +msgid "" +"Option %(option)s found with no group specified while checking domain " +"configuration request" +msgstr "" + +#: keystone/resource/core.py:878 +#, python-format +msgid "Group %(group)s is not supported for domain specific configurations" +msgstr "" + +#: keystone/resource/core.py:885 +#, python-format +msgid "" +"Option %(option)s in group %(group)s is not supported for domain specific" +" configurations" +msgstr "" + +#: keystone/resource/core.py:938 +msgid "An unexpected error occurred when retrieving domain configs" +msgstr "" + +#: keystone/resource/core.py:1013 keystone/resource/core.py:1097 +#: keystone/resource/core.py:1167 keystone/resource/config_backends/sql.py:70 +#, python-format +msgid "option %(option)s in group %(group)s" +msgstr "" + +#: keystone/resource/core.py:1016 keystone/resource/core.py:1102 +#: keystone/resource/core.py:1163 +#, python-format +msgid "group %(group)s" +msgstr "" + +#: keystone/resource/core.py:1018 +msgid "any options" +msgstr "" + +#: keystone/resource/core.py:1062 +#, python-format +msgid "" +"Trying to update option %(option)s in group %(group)s, so that, and only " +"that, option must be specified in the config" +msgstr "" + +#: keystone/resource/core.py:1067 +#, python-format +msgid "" +"Trying to update group %(group)s, so that, and only that, group must be " +"specified in the config" +msgstr "" + +#: keystone/resource/core.py:1076 +#, python-format +msgid "" +"request to update group %(group)s, but config provided contains group " +"%(group_other)s instead" +msgstr "" + +#: keystone/resource/core.py:1083 +#, python-format +msgid "" +"Trying to update option %(option)s in group %(group)s, but config " +"provided contains option %(option_other)s instead" +msgstr "" + +#: keystone/resource/backends/ldap.py:151 +#: keystone/resource/backends/ldap.py:159 +#: keystone/resource/backends/ldap.py:163 +msgid "Domains are read-only against LDAP" +msgstr "" + +#: keystone/server/eventlet.py:77 +msgid "" +"Running keystone via eventlet is deprecated as of Kilo in favor of " +"running in a WSGI server (e.g. mod_wsgi). Support for keystone under " +"eventlet will be removed in the \"M\"-Release." +msgstr "" + +#: keystone/server/eventlet.py:90 +#, python-format +msgid "Failed to start the %(name)s server" +msgstr "" + +#: keystone/token/controllers.py:391 +#, python-format +msgid "User %(u_id)s is unauthorized for tenant %(t_id)s" +msgstr "User %(u_id)s is unauthorized for tenant %(t_id)s" + +#: keystone/token/controllers.py:410 keystone/token/controllers.py:413 +msgid "Token does not belong to specified tenant." +msgstr "Token does not belong to specified tenant." + +#: keystone/token/persistence/backends/kvs.py:133 +#, python-format +msgid "Unknown token version %s" +msgstr "" + +#: keystone/token/providers/common.py:250 +#: keystone/token/providers/common.py:355 +#, python-format +msgid "User %(user_id)s has no access to project %(project_id)s" +msgstr "User %(user_id)s has no access to project %(project_id)s" + +#: keystone/token/providers/common.py:255 +#: keystone/token/providers/common.py:360 +#, python-format +msgid "User %(user_id)s has no access to domain %(domain_id)s" +msgstr "User %(user_id)s has no access to domain %(domain_id)s" + +#: keystone/token/providers/common.py:282 +msgid "Trustor is disabled." +msgstr "Trustor is disabled." + +#: keystone/token/providers/common.py:346 +msgid "Trustee has no delegated roles." +msgstr "Trustee has no delegated roles." + +#: keystone/token/providers/common.py:407 +#, python-format +msgid "Invalid audit info data type: %(data)s (%(type)s)" +msgstr "" + +#: keystone/token/providers/common.py:435 +msgid "User is not a trustee." +msgstr "User is not a trustee." + +#: keystone/token/providers/common.py:579 +msgid "" +"Attempting to use OS-FEDERATION token with V2 Identity Service, use V3 " +"Authentication" +msgstr "" + +#: keystone/token/providers/common.py:597 +msgid "Domain scoped token is not supported" +msgstr "Domain scoped token is not supported" + +#: keystone/token/providers/pki.py:48 keystone/token/providers/pkiz.py:30 +msgid "Unable to sign token." +msgstr "Unable to sign token." + +#: keystone/token/providers/fernet/core.py:215 +msgid "" +"This is not a v2.0 Fernet token. Use v3 for trust, domain, or federated " +"tokens." +msgstr "" + +#: keystone/token/providers/fernet/token_formatters.py:189 +#, python-format +msgid "This is not a recognized Fernet payload version: %s" +msgstr "" + +#: keystone/trust/controllers.py:148 +msgid "Redelegation allowed for delegated by trust only" +msgstr "" + +#: keystone/trust/controllers.py:181 +msgid "The authenticated user should match the trustor." +msgstr "" + +#: keystone/trust/controllers.py:186 +msgid "At least one role should be specified." +msgstr "" + +#: keystone/trust/core.py:57 +#, python-format +msgid "" +"Remaining redelegation depth of %(redelegation_depth)d out of allowed " +"range of [0..%(max_count)d]" +msgstr "" + +#: keystone/trust/core.py:66 +#, python-format +msgid "" +"Field \"remaining_uses\" is set to %(value)s while it must not be set in " +"order to redelegate a trust" +msgstr "" + +#: keystone/trust/core.py:77 +msgid "Requested expiration time is more than redelegated trust can provide" +msgstr "" + +#: keystone/trust/core.py:87 +msgid "Some of requested roles are not in redelegated trust" +msgstr "" + +#: keystone/trust/core.py:116 +msgid "One of the trust agents is disabled or deleted" +msgstr "" + +#: keystone/trust/core.py:135 +msgid "remaining_uses must be a positive integer or null." +msgstr "" + +#: keystone/trust/core.py:141 +#, python-format +msgid "" +"Requested redelegation depth of %(requested_count)d is greater than " +"allowed %(max_count)d" +msgstr "" + +#: keystone/trust/core.py:147 +msgid "remaining_uses must not be set if redelegation is allowed" +msgstr "" + +#: keystone/trust/core.py:157 +msgid "" +"Modifying \"redelegation_count\" upon redelegation is forbidden. Omitting" +" this parameter is advised." +msgstr "" + diff --git a/keystone-moon/keystone/locale/en_GB/LC_MESSAGES/keystone-log-info.po b/keystone-moon/keystone/locale/en_GB/LC_MESSAGES/keystone-log-info.po new file mode 100644 index 00000000..a0da5eed --- /dev/null +++ b/keystone-moon/keystone/locale/en_GB/LC_MESSAGES/keystone-log-info.po @@ -0,0 +1,214 @@ +# Translations template for keystone. +# Copyright (C) 2015 OpenStack Foundation +# This file is distributed under the same license as the keystone project. +# +# Translators: +# Andi Chandler , 2014 +msgid "" +msgstr "" +"Project-Id-Version: Keystone\n" +"Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" +"POT-Creation-Date: 2015-03-09 06:03+0000\n" +"PO-Revision-Date: 2015-03-07 04:31+0000\n" +"Last-Translator: openstackjenkins \n" +"Language-Team: English (United Kingdom) (http://www.transifex.com/projects/p/" +"keystone/language/en_GB/)\n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 1.3\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: keystone/assignment/core.py:250 +#, python-format +msgid "Creating the default role %s because it does not exist." +msgstr "" + +#: keystone/assignment/core.py:258 +#, python-format +msgid "Creating the default role %s failed because it was already created" +msgstr "" + +#: keystone/auth/controllers.py:64 +msgid "Loading auth-plugins by class-name is deprecated." +msgstr "" + +#: keystone/auth/controllers.py:106 +#, python-format +msgid "" +"\"expires_at\" has conflicting values %(existing)s and %(new)s. Will use " +"the earliest value." +msgstr "" +"\"expires_at\" has conflicting values %(existing)s and %(new)s. Will use " +"the earliest value." + +#: keystone/common/openssl.py:81 +#, python-format +msgid "Running command - %s" +msgstr "" + +#: keystone/common/wsgi.py:79 +msgid "No bind information present in token" +msgstr "" + +#: keystone/common/wsgi.py:83 +#, python-format +msgid "Named bind mode %s not in bind information" +msgstr "" + +#: keystone/common/wsgi.py:90 +msgid "Kerberos credentials required and not present" +msgstr "" + +#: keystone/common/wsgi.py:94 +msgid "Kerberos credentials do not match those in bind" +msgstr "" + +#: keystone/common/wsgi.py:98 +msgid "Kerberos bind authentication successful" +msgstr "" + +#: keystone/common/wsgi.py:105 +#, python-format +msgid "Couldn't verify unknown bind: {%(bind_type)s: %(identifier)s}" +msgstr "" + +#: keystone/common/environment/eventlet_server.py:103 +#, python-format +msgid "Starting %(arg0)s on %(host)s:%(port)s" +msgstr "" + +#: keystone/common/kvs/core.py:138 +#, python-format +msgid "Adding proxy '%(proxy)s' to KVS %(name)s." +msgstr "" + +#: keystone/common/kvs/core.py:188 +#, python-format +msgid "Using %(func)s as KVS region %(name)s key_mangler" +msgstr "" + +#: keystone/common/kvs/core.py:200 +#, python-format +msgid "Using default dogpile sha1_mangle_key as KVS region %s key_mangler" +msgstr "" + +#: keystone/common/kvs/core.py:210 +#, python-format +msgid "KVS region %s key_mangler disabled." +msgstr "" + +#: keystone/contrib/example/core.py:64 keystone/contrib/example/core.py:73 +#, python-format +msgid "" +"Received the following notification: service %(service)s, resource_type: " +"%(resource_type)s, operation %(operation)s payload %(payload)s" +msgstr "" + +#: keystone/openstack/common/eventlet_backdoor.py:146 +#, python-format +msgid "Eventlet backdoor listening on %(port)s for process %(pid)d" +msgstr "Eventlet backdoor listening on %(port)s for process %(pid)d" + +#: keystone/openstack/common/service.py:173 +#, python-format +msgid "Caught %s, exiting" +msgstr "Caught %s, exiting" + +#: keystone/openstack/common/service.py:231 +msgid "Parent process has died unexpectedly, exiting" +msgstr "Parent process has died unexpectedly, exiting" + +#: keystone/openstack/common/service.py:262 +#, python-format +msgid "Child caught %s, exiting" +msgstr "Child caught %s, exiting" + +#: keystone/openstack/common/service.py:301 +msgid "Forking too fast, sleeping" +msgstr "Forking too fast, sleeping" + +#: keystone/openstack/common/service.py:320 +#, python-format +msgid "Started child %d" +msgstr "Started child %d" + +#: keystone/openstack/common/service.py:330 +#, python-format +msgid "Starting %d workers" +msgstr "Starting %d workers" + +#: keystone/openstack/common/service.py:347 +#, python-format +msgid "Child %(pid)d killed by signal %(sig)d" +msgstr "Child %(pid)d killed by signal %(sig)d" + +#: keystone/openstack/common/service.py:351 +#, python-format +msgid "Child %(pid)s exited with status %(code)d" +msgstr "Child %(pid)s exited with status %(code)d" + +#: keystone/openstack/common/service.py:390 +#, python-format +msgid "Caught %s, stopping children" +msgstr "Caught %s, stopping children" + +#: keystone/openstack/common/service.py:399 +msgid "Wait called after thread killed. Cleaning up." +msgstr "" + +#: keystone/openstack/common/service.py:415 +#, python-format +msgid "Waiting on %d children to exit" +msgstr "Waiting on %d children to exit" + +#: keystone/token/persistence/backends/sql.py:279 +#, python-format +msgid "Total expired tokens removed: %d" +msgstr "Total expired tokens removed: %d" + +#: keystone/token/providers/fernet/utils.py:72 +msgid "" +"[fernet_tokens] key_repository does not appear to exist; attempting to " +"create it" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:130 +#, python-format +msgid "Created a new key: %s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:143 +msgid "Key repository is already initialized; aborting." +msgstr "" + +#: keystone/token/providers/fernet/utils.py:179 +#, python-format +msgid "Starting key rotation with %(count)s key files: %(list)s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:185 +#, python-format +msgid "Current primary key is: %s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:187 +#, python-format +msgid "Next primary key will be: %s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:197 +#, python-format +msgid "Promoted key 0 to be the primary: %s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:213 +#, python-format +msgid "Excess keys to purge: %s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:237 +#, python-format +msgid "Loaded %(count)s encryption keys from: %(dir)s" +msgstr "" diff --git a/keystone-moon/keystone/locale/es/LC_MESSAGES/keystone-log-critical.po b/keystone-moon/keystone/locale/es/LC_MESSAGES/keystone-log-critical.po new file mode 100644 index 00000000..6ebff226 --- /dev/null +++ b/keystone-moon/keystone/locale/es/LC_MESSAGES/keystone-log-critical.po @@ -0,0 +1,25 @@ +# Translations template for keystone. +# Copyright (C) 2014 OpenStack Foundation +# This file is distributed under the same license as the keystone project. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: Keystone\n" +"Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" +"POT-Creation-Date: 2014-09-07 06:06+0000\n" +"PO-Revision-Date: 2014-08-31 15:19+0000\n" +"Last-Translator: openstackjenkins \n" +"Language-Team: Spanish (http://www.transifex.com/projects/p/keystone/" +"language/es/)\n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 1.3\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: keystone/catalog/backends/templated.py:106 +#, python-format +msgid "Unable to open template file %s" +msgstr "No se puede abrir el archivo de plantilla %s" diff --git a/keystone-moon/keystone/locale/es/LC_MESSAGES/keystone-log-error.po b/keystone-moon/keystone/locale/es/LC_MESSAGES/keystone-log-error.po new file mode 100644 index 00000000..d1c2eaa6 --- /dev/null +++ b/keystone-moon/keystone/locale/es/LC_MESSAGES/keystone-log-error.po @@ -0,0 +1,177 @@ +# Translations template for keystone. +# Copyright (C) 2015 OpenStack Foundation +# This file is distributed under the same license as the keystone project. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: Keystone\n" +"Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" +"POT-Creation-Date: 2015-03-09 06:03+0000\n" +"PO-Revision-Date: 2015-03-07 04:31+0000\n" +"Last-Translator: openstackjenkins \n" +"Language-Team: Spanish (http://www.transifex.com/projects/p/keystone/" +"language/es/)\n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 1.3\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: keystone/notifications.py:304 +msgid "Failed to construct notifier" +msgstr "" + +#: keystone/notifications.py:389 +#, python-format +msgid "Failed to send %(res_id)s %(event_type)s notification" +msgstr "" + +#: keystone/notifications.py:606 +#, python-format +msgid "Failed to send %(action)s %(event_type)s notification" +msgstr "" + +#: keystone/catalog/core.py:62 +#, python-format +msgid "Malformed endpoint - %(url)r is not a string" +msgstr "" + +#: keystone/catalog/core.py:66 +#, python-format +msgid "Malformed endpoint %(url)s - unknown key %(keyerror)s" +msgstr "" +"Punto final formado incorrectamente %(url)s - clave desconocida %(keyerror)s" + +#: keystone/catalog/core.py:71 +#, python-format +msgid "" +"Malformed endpoint '%(url)s'. The following type error occurred during " +"string substitution: %(typeerror)s" +msgstr "" + +#: keystone/catalog/core.py:77 +#, python-format +msgid "" +"Malformed endpoint %s - incomplete format (are you missing a type notifier ?)" +msgstr "" + +#: keystone/common/openssl.py:93 +#, python-format +msgid "Command %(to_exec)s exited with %(retcode)s- %(output)s" +msgstr "" + +#: keystone/common/openssl.py:121 +#, python-format +msgid "Failed to remove file %(file_path)r: %(error)s" +msgstr "" + +#: keystone/common/utils.py:239 +msgid "" +"Error setting up the debug environment. Verify that the option --debug-url " +"has the format : and that a debugger processes is listening on " +"that port." +msgstr "" +"Error configurando el entorno de depuración. Verifique que la opción --debug-" +"url tiene el formato : y que un proceso de depuración está " +"publicado en ese host y puerto" + +#: keystone/common/cache/core.py:100 +#, python-format +msgid "" +"Unable to build cache config-key. Expected format \":\". " +"Skipping unknown format: %s" +msgstr "" + +#: keystone/common/environment/eventlet_server.py:99 +#, python-format +msgid "Could not bind to %(host)s:%(port)s" +msgstr "No se puede asociar a %(host)s:%(port)s" + +#: keystone/common/environment/eventlet_server.py:185 +msgid "Server error" +msgstr "Error del servidor" + +#: keystone/contrib/endpoint_policy/core.py:129 +#: keystone/contrib/endpoint_policy/core.py:228 +#, python-format +msgid "" +"Circular reference or a repeated entry found in region tree - %(region_id)s." +msgstr "" + +#: keystone/contrib/federation/idp.py:410 +#, python-format +msgid "Error when signing assertion, reason: %(reason)s" +msgstr "" + +#: keystone/contrib/oauth1/core.py:136 +msgid "Cannot retrieve Authorization headers" +msgstr "" + +#: keystone/openstack/common/loopingcall.py:95 +msgid "in fixed duration looping call" +msgstr "en llamada en bucle de duración fija" + +#: keystone/openstack/common/loopingcall.py:138 +msgid "in dynamic looping call" +msgstr "en llamada en bucle dinámica" + +#: keystone/openstack/common/service.py:268 +msgid "Unhandled exception" +msgstr "Excepción no controlada" + +#: keystone/resource/core.py:477 +#, python-format +msgid "" +"Circular reference or a repeated entry found projects hierarchy - " +"%(project_id)s." +msgstr "" + +#: keystone/resource/core.py:939 +#, python-format +msgid "" +"Unexpected results in response for domain config - %(count)s responses, " +"first option is %(option)s, expected option %(expected)s" +msgstr "" + +#: keystone/resource/backends/sql.py:102 keystone/resource/backends/sql.py:121 +#, python-format +msgid "" +"Circular reference or a repeated entry found in projects hierarchy - " +"%(project_id)s." +msgstr "" + +#: keystone/token/provider.py:292 +#, python-format +msgid "Unexpected error or malformed token determining token expiry: %s" +msgstr "" + +#: keystone/token/persistence/backends/kvs.py:226 +#, python-format +msgid "" +"Reinitializing revocation list due to error in loading revocation list from " +"backend. Expected `list` type got `%(type)s`. Old revocation list data: " +"%(list)r" +msgstr "" + +#: keystone/token/providers/common.py:611 +msgid "Failed to validate token" +msgstr "Ha fallado la validación del token" + +#: keystone/token/providers/pki.py:47 +msgid "Unable to sign token" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:38 +#, python-format +msgid "" +"Either [fernet_tokens] key_repository does not exist or Keystone does not " +"have sufficient permission to access it: %s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:79 +msgid "" +"Failed to create [fernet_tokens] key_repository: either it already exists or " +"you don't have sufficient permissions to create it" +msgstr "" diff --git a/keystone-moon/keystone/locale/fr/LC_MESSAGES/keystone-log-critical.po b/keystone-moon/keystone/locale/fr/LC_MESSAGES/keystone-log-critical.po new file mode 100644 index 00000000..c40440be --- /dev/null +++ b/keystone-moon/keystone/locale/fr/LC_MESSAGES/keystone-log-critical.po @@ -0,0 +1,25 @@ +# Translations template for keystone. +# Copyright (C) 2014 OpenStack Foundation +# This file is distributed under the same license as the keystone project. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: Keystone\n" +"Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" +"POT-Creation-Date: 2014-09-07 06:06+0000\n" +"PO-Revision-Date: 2014-08-31 15:19+0000\n" +"Last-Translator: openstackjenkins \n" +"Language-Team: French (http://www.transifex.com/projects/p/keystone/language/" +"fr/)\n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 1.3\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: keystone/catalog/backends/templated.py:106 +#, python-format +msgid "Unable to open template file %s" +msgstr "Impossible d'ouvrir le fichier modèle %s" diff --git a/keystone-moon/keystone/locale/fr/LC_MESSAGES/keystone-log-error.po b/keystone-moon/keystone/locale/fr/LC_MESSAGES/keystone-log-error.po new file mode 100644 index 00000000..d8dc409f --- /dev/null +++ b/keystone-moon/keystone/locale/fr/LC_MESSAGES/keystone-log-error.po @@ -0,0 +1,184 @@ +# Translations template for keystone. +# Copyright (C) 2015 OpenStack Foundation +# This file is distributed under the same license as the keystone project. +# +# Translators: +# Bruno Cornec , 2014 +# Maxime COQUEREL , 2014 +msgid "" +msgstr "" +"Project-Id-Version: Keystone\n" +"Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" +"POT-Creation-Date: 2015-03-09 06:03+0000\n" +"PO-Revision-Date: 2015-03-07 04:31+0000\n" +"Last-Translator: openstackjenkins \n" +"Language-Team: French (http://www.transifex.com/projects/p/keystone/language/" +"fr/)\n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 1.3\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: keystone/notifications.py:304 +msgid "Failed to construct notifier" +msgstr "Échec de construction de la notification" + +#: keystone/notifications.py:389 +#, python-format +msgid "Failed to send %(res_id)s %(event_type)s notification" +msgstr "Échec de l'envoi de la notification %(res_id)s %(event_type)s" + +#: keystone/notifications.py:606 +#, python-format +msgid "Failed to send %(action)s %(event_type)s notification" +msgstr "Échec de l'envoi de la notification %(action)s %(event_type)s " + +#: keystone/catalog/core.py:62 +#, python-format +msgid "Malformed endpoint - %(url)r is not a string" +msgstr "Critère mal formé - %(url)r n'est pas une chaine de caractère" + +#: keystone/catalog/core.py:66 +#, python-format +msgid "Malformed endpoint %(url)s - unknown key %(keyerror)s" +msgstr "Noeud final incorrect %(url)s - clé inconnue %(keyerror)s" + +#: keystone/catalog/core.py:71 +#, python-format +msgid "" +"Malformed endpoint '%(url)s'. The following type error occurred during " +"string substitution: %(typeerror)s" +msgstr "" +"Noeud final incorrect '%(url)s'. L'erreur suivante est survenue pendant la " +"substitution de chaine : %(typeerror)s" + +#: keystone/catalog/core.py:77 +#, python-format +msgid "" +"Malformed endpoint %s - incomplete format (are you missing a type notifier ?)" +msgstr "" +"Noeud final incorrect '%s - Format incomplet (un type de notification manque-" +"t-il ?)" + +#: keystone/common/openssl.py:93 +#, python-format +msgid "Command %(to_exec)s exited with %(retcode)s- %(output)s" +msgstr "La commande %(to_exec)s a retourné %(retcode)s- %(output)s" + +#: keystone/common/openssl.py:121 +#, python-format +msgid "Failed to remove file %(file_path)r: %(error)s" +msgstr "Échec de la suppression du fichier %(file_path)r: %(error)s" + +#: keystone/common/utils.py:239 +msgid "" +"Error setting up the debug environment. Verify that the option --debug-url " +"has the format : and that a debugger processes is listening on " +"that port." +msgstr "" +"Erreur de configuration de l'environnement de débogage. Vérifiez que " +"l'option --debug-url a le format : et que le processus de " +"débogage écoute sur ce port." + +#: keystone/common/cache/core.py:100 +#, python-format +msgid "" +"Unable to build cache config-key. Expected format \":\". " +"Skipping unknown format: %s" +msgstr "" + +#: keystone/common/environment/eventlet_server.py:99 +#, python-format +msgid "Could not bind to %(host)s:%(port)s" +msgstr "Impossible de s'attacher à %(host)s:%(port)s" + +#: keystone/common/environment/eventlet_server.py:185 +msgid "Server error" +msgstr "Erreur serveur" + +#: keystone/contrib/endpoint_policy/core.py:129 +#: keystone/contrib/endpoint_policy/core.py:228 +#, python-format +msgid "" +"Circular reference or a repeated entry found in region tree - %(region_id)s." +msgstr "" +"Référence circulaire ou entrée dupliquée trouvée dans l'arbre de la région - " +"%(region_id)s." + +#: keystone/contrib/federation/idp.py:410 +#, python-format +msgid "Error when signing assertion, reason: %(reason)s" +msgstr "Erreur lors de la signature d'une assertion : %(reason)s" + +#: keystone/contrib/oauth1/core.py:136 +msgid "Cannot retrieve Authorization headers" +msgstr "" + +#: keystone/openstack/common/loopingcall.py:95 +msgid "in fixed duration looping call" +msgstr "dans l'appel en boucle de durée fixe" + +#: keystone/openstack/common/loopingcall.py:138 +msgid "in dynamic looping call" +msgstr "dans l'appel en boucle dynamique" + +#: keystone/openstack/common/service.py:268 +msgid "Unhandled exception" +msgstr "Exception non gérée" + +#: keystone/resource/core.py:477 +#, python-format +msgid "" +"Circular reference or a repeated entry found projects hierarchy - " +"%(project_id)s." +msgstr "" + +#: keystone/resource/core.py:939 +#, python-format +msgid "" +"Unexpected results in response for domain config - %(count)s responses, " +"first option is %(option)s, expected option %(expected)s" +msgstr "" + +#: keystone/resource/backends/sql.py:102 keystone/resource/backends/sql.py:121 +#, python-format +msgid "" +"Circular reference or a repeated entry found in projects hierarchy - " +"%(project_id)s." +msgstr "" + +#: keystone/token/provider.py:292 +#, python-format +msgid "Unexpected error or malformed token determining token expiry: %s" +msgstr "" + +#: keystone/token/persistence/backends/kvs.py:226 +#, python-format +msgid "" +"Reinitializing revocation list due to error in loading revocation list from " +"backend. Expected `list` type got `%(type)s`. Old revocation list data: " +"%(list)r" +msgstr "" + +#: keystone/token/providers/common.py:611 +msgid "Failed to validate token" +msgstr "Echec de validation du token" + +#: keystone/token/providers/pki.py:47 +msgid "Unable to sign token" +msgstr "Impossible de signer le jeton" + +#: keystone/token/providers/fernet/utils.py:38 +#, python-format +msgid "" +"Either [fernet_tokens] key_repository does not exist or Keystone does not " +"have sufficient permission to access it: %s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:79 +msgid "" +"Failed to create [fernet_tokens] key_repository: either it already exists or " +"you don't have sufficient permissions to create it" +msgstr "" diff --git a/keystone-moon/keystone/locale/fr/LC_MESSAGES/keystone-log-info.po b/keystone-moon/keystone/locale/fr/LC_MESSAGES/keystone-log-info.po new file mode 100644 index 00000000..065540dc --- /dev/null +++ b/keystone-moon/keystone/locale/fr/LC_MESSAGES/keystone-log-info.po @@ -0,0 +1,223 @@ +# Translations template for keystone. +# Copyright (C) 2015 OpenStack Foundation +# This file is distributed under the same license as the keystone project. +# +# Translators: +# Bruno Cornec , 2014 +# Maxime COQUEREL , 2014 +# Andrew_Melim , 2014 +msgid "" +msgstr "" +"Project-Id-Version: Keystone\n" +"Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" +"POT-Creation-Date: 2015-03-09 06:03+0000\n" +"PO-Revision-Date: 2015-03-08 17:01+0000\n" +"Last-Translator: openstackjenkins \n" +"Language-Team: French (http://www.transifex.com/projects/p/keystone/language/" +"fr/)\n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 1.3\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: keystone/assignment/core.py:250 +#, python-format +msgid "Creating the default role %s because it does not exist." +msgstr "Création du rôle par défaut %s, car il n'existe pas" + +#: keystone/assignment/core.py:258 +#, python-format +msgid "Creating the default role %s failed because it was already created" +msgstr "" + +#: keystone/auth/controllers.py:64 +msgid "Loading auth-plugins by class-name is deprecated." +msgstr "Chargement de auth-plugins par class-name est déprécié" + +#: keystone/auth/controllers.py:106 +#, python-format +msgid "" +"\"expires_at\" has conflicting values %(existing)s and %(new)s. Will use " +"the earliest value." +msgstr "" +"\"expires_at\" a des valeurs conflictuelles %(existing)s et %(new)s. " +"Utilsation de la première valeur." + +#: keystone/common/openssl.py:81 +#, python-format +msgid "Running command - %s" +msgstr "Exécution de la commande %s" + +#: keystone/common/wsgi.py:79 +msgid "No bind information present in token" +msgstr "Aucune information d'attachement n'est présente dans le jeton" + +#: keystone/common/wsgi.py:83 +#, python-format +msgid "Named bind mode %s not in bind information" +msgstr "" +"Le mode d'attachement nommé %s n'est pas dans l'information d'attachement" + +#: keystone/common/wsgi.py:90 +msgid "Kerberos credentials required and not present" +msgstr "L'identitification Kerberos est requise mais non présente" + +#: keystone/common/wsgi.py:94 +msgid "Kerberos credentials do not match those in bind" +msgstr "L'identification Kerberos ne correspond pas à celle de l'attachement" + +#: keystone/common/wsgi.py:98 +msgid "Kerberos bind authentication successful" +msgstr "Attachement Kerberos identifié correctement" + +#: keystone/common/wsgi.py:105 +#, python-format +msgid "Couldn't verify unknown bind: {%(bind_type)s: %(identifier)s}" +msgstr "" +"Impossible de vérifier l'attachement inconnu: {%(bind_type)s: " +"%(identifier)s}" + +#: keystone/common/environment/eventlet_server.py:103 +#, python-format +msgid "Starting %(arg0)s on %(host)s:%(port)s" +msgstr "Démarrage de %(arg0)s sur %(host)s:%(port)s" + +#: keystone/common/kvs/core.py:138 +#, python-format +msgid "Adding proxy '%(proxy)s' to KVS %(name)s." +msgstr "Ahour du mandataire '%(proxy)s' au KVS %(name)s." + +#: keystone/common/kvs/core.py:188 +#, python-format +msgid "Using %(func)s as KVS region %(name)s key_mangler" +msgstr "Utilise %(func)s comme région KVS %(name)s key_mangler" + +#: keystone/common/kvs/core.py:200 +#, python-format +msgid "Using default dogpile sha1_mangle_key as KVS region %s key_mangler" +msgstr "" +"Utilisation du dogpile sha1_mangle_key par défaut comme région KVS %s " +"key_mangler" + +#: keystone/common/kvs/core.py:210 +#, python-format +msgid "KVS region %s key_mangler disabled." +msgstr "Région KVS %s key_mangler désactivée" + +#: keystone/contrib/example/core.py:64 keystone/contrib/example/core.py:73 +#, python-format +msgid "" +"Received the following notification: service %(service)s, resource_type: " +"%(resource_type)s, operation %(operation)s payload %(payload)s" +msgstr "" +"Réception de la notification suivante: service %(service)s, resource_type: " +"%(resource_type)s, operation %(operation)s payload %(payload)s" + +#: keystone/openstack/common/eventlet_backdoor.py:146 +#, python-format +msgid "Eventlet backdoor listening on %(port)s for process %(pid)d" +msgstr "Eventlet backdoor en écoute sur le port %(port)s for process %(pid)d" + +#: keystone/openstack/common/service.py:173 +#, python-format +msgid "Caught %s, exiting" +msgstr "%s interceptée, sortie" + +#: keystone/openstack/common/service.py:231 +msgid "Parent process has died unexpectedly, exiting" +msgstr "Processus parent arrêté de manière inattendue, sortie" + +#: keystone/openstack/common/service.py:262 +#, python-format +msgid "Child caught %s, exiting" +msgstr "L'enfant a reçu %s, sortie" + +#: keystone/openstack/common/service.py:301 +msgid "Forking too fast, sleeping" +msgstr "Bifurcation trop rapide, pause" + +#: keystone/openstack/common/service.py:320 +#, python-format +msgid "Started child %d" +msgstr "Enfant démarré %d" + +#: keystone/openstack/common/service.py:330 +#, python-format +msgid "Starting %d workers" +msgstr "Démarrage des travailleurs %d" + +#: keystone/openstack/common/service.py:347 +#, python-format +msgid "Child %(pid)d killed by signal %(sig)d" +msgstr "Enfant %(pid)d arrêté par le signal %(sig)d" + +#: keystone/openstack/common/service.py:351 +#, python-format +msgid "Child %(pid)s exited with status %(code)d" +msgstr "Processus fils %(pid)s terminé avec le status %(code)d" + +#: keystone/openstack/common/service.py:390 +#, python-format +msgid "Caught %s, stopping children" +msgstr "%s interceptée, arrêt de l'enfant" + +#: keystone/openstack/common/service.py:399 +msgid "Wait called after thread killed. Cleaning up." +msgstr "Pause demandée après suppression de thread. Nettoyage." + +#: keystone/openstack/common/service.py:415 +#, python-format +msgid "Waiting on %d children to exit" +msgstr "En attente %d enfants pour sortie" + +#: keystone/token/persistence/backends/sql.py:279 +#, python-format +msgid "Total expired tokens removed: %d" +msgstr "Total des jetons expirés effacés: %d" + +#: keystone/token/providers/fernet/utils.py:72 +msgid "" +"[fernet_tokens] key_repository does not appear to exist; attempting to " +"create it" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:130 +#, python-format +msgid "Created a new key: %s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:143 +msgid "Key repository is already initialized; aborting." +msgstr "" + +#: keystone/token/providers/fernet/utils.py:179 +#, python-format +msgid "Starting key rotation with %(count)s key files: %(list)s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:185 +#, python-format +msgid "Current primary key is: %s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:187 +#, python-format +msgid "Next primary key will be: %s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:197 +#, python-format +msgid "Promoted key 0 to be the primary: %s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:213 +#, python-format +msgid "Excess keys to purge: %s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:237 +#, python-format +msgid "Loaded %(count)s encryption keys from: %(dir)s" +msgstr "" diff --git a/keystone-moon/keystone/locale/fr/LC_MESSAGES/keystone-log-warning.po b/keystone-moon/keystone/locale/fr/LC_MESSAGES/keystone-log-warning.po new file mode 100644 index 00000000..a83b88a5 --- /dev/null +++ b/keystone-moon/keystone/locale/fr/LC_MESSAGES/keystone-log-warning.po @@ -0,0 +1,303 @@ +# Translations template for keystone. +# Copyright (C) 2015 OpenStack Foundation +# This file is distributed under the same license as the keystone project. +# +# Translators: +# Bruno Cornec , 2014 +# Maxime COQUEREL , 2014 +msgid "" +msgstr "" +"Project-Id-Version: Keystone\n" +"Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" +"POT-Creation-Date: 2015-03-19 06:04+0000\n" +"PO-Revision-Date: 2015-03-19 02:24+0000\n" +"Last-Translator: openstackjenkins \n" +"Language-Team: French (http://www.transifex.com/projects/p/keystone/language/" +"fr/)\n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 1.3\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: keystone/cli.py:159 +msgid "keystone-manage pki_setup is not recommended for production use." +msgstr "" +"keystone-manage pki_setup n'est pas recommandé pour une utilisation en " +"production." + +#: keystone/cli.py:178 +msgid "keystone-manage ssl_setup is not recommended for production use." +msgstr "" +"keystone-manage ssl_setup n'est pas recommandé pour une utilisation en " +"production." + +#: keystone/cli.py:493 +#, python-format +msgid "Ignoring file (%s) while scanning domain config directory" +msgstr "" + +#: keystone/exception.py:49 +msgid "missing exception kwargs (programmer error)" +msgstr "" + +#: keystone/assignment/controllers.py:60 +#, python-format +msgid "Authentication failed: %s" +msgstr "L'authentification a échoué: %s" + +#: keystone/assignment/controllers.py:576 +#, python-format +msgid "" +"Group %(group)s not found for role-assignment - %(target)s with Role: " +"%(role)s" +msgstr "" + +#: keystone/auth/controllers.py:449 +#, python-format +msgid "" +"User %(user_id)s doesn't have access to default project %(project_id)s. The " +"token will be unscoped rather than scoped to the project." +msgstr "" + +#: keystone/auth/controllers.py:457 +#, python-format +msgid "" +"User %(user_id)s's default project %(project_id)s is disabled. The token " +"will be unscoped rather than scoped to the project." +msgstr "" + +#: keystone/auth/controllers.py:466 +#, python-format +msgid "" +"User %(user_id)s's default project %(project_id)s not found. The token will " +"be unscoped rather than scoped to the project." +msgstr "" + +#: keystone/common/authorization.py:55 +msgid "RBAC: Invalid user data in token" +msgstr "RBAC: Donnée utilisation non valide dans le token" + +#: keystone/common/controller.py:79 keystone/middleware/core.py:224 +msgid "RBAC: Invalid token" +msgstr "RBAC : Jeton non valide" + +#: keystone/common/controller.py:104 keystone/common/controller.py:201 +#: keystone/common/controller.py:740 +msgid "RBAC: Bypassing authorization" +msgstr "RBAC : Autorisation ignorée" + +#: keystone/common/controller.py:669 keystone/common/controller.py:704 +msgid "Invalid token found while getting domain ID for list request" +msgstr "" + +#: keystone/common/controller.py:677 +msgid "No domain information specified as part of list request" +msgstr "" + +#: keystone/common/utils.py:103 +#, python-format +msgid "Truncating user password to %d characters." +msgstr "" + +#: keystone/common/wsgi.py:242 +#, python-format +msgid "Authorization failed. %(exception)s from %(remote_addr)s" +msgstr "Echec d'autorisation. %(exception)s depuis %(remote_addr)s" + +#: keystone/common/wsgi.py:361 +msgid "Invalid token in _get_trust_id_for_request" +msgstr "Jeton invalide dans _get_trust_id_for_request" + +#: keystone/common/cache/backends/mongo.py:403 +#, python-format +msgid "" +"TTL index already exists on db collection <%(c_name)s>, remove index <" +"%(indx_name)s> first to make updated mongo_ttl_seconds value to be effective" +msgstr "" + +#: keystone/common/kvs/core.py:134 +#, python-format +msgid "%s is not a dogpile.proxy.ProxyBackend" +msgstr "%s n'est pas un dogpile.proxy.ProxyBackend" + +#: keystone/common/kvs/core.py:403 +#, python-format +msgid "KVS lock released (timeout reached) for: %s" +msgstr "Verrou KVS relaché (temps limite atteint) pour : %s" + +#: keystone/common/ldap/core.py:1026 +msgid "" +"LDAP Server does not support paging. Disable paging in keystone.conf to " +"avoid this message." +msgstr "" +"Le serveur LDAP ne prend pas en charge la pagination. Désactivez la " +"pagination dans keystone.conf pour éviter de recevoir ce message." + +#: keystone/common/ldap/core.py:1225 +#, python-format +msgid "" +"Invalid additional attribute mapping: \"%s\". Format must be " +":" +msgstr "" +"Mauvais mappage d'attribut additionnel: \"%s\". Le format doit être " +":" + +#: keystone/common/ldap/core.py:1336 +#, python-format +msgid "" +"ID attribute %(id_attr)s for LDAP object %(dn)s has multiple values and " +"therefore cannot be used as an ID. Will get the ID from DN instead" +msgstr "" +"L'attribut ID %(id_attr)s pour l'objet LDAP %(dn)s a de multiples valeurs et " +"par conséquent ne peut être utilisé comme un ID. Obtention de l'ID depuis le " +"DN à la place." + +#: keystone/common/ldap/core.py:1669 +#, python-format +msgid "" +"When deleting entries for %(search_base)s, could not delete nonexistent " +"entries %(entries)s%(dots)s" +msgstr "" + +#: keystone/contrib/endpoint_policy/core.py:91 +#, python-format +msgid "" +"Endpoint %(endpoint_id)s referenced in association for policy %(policy_id)s " +"not found." +msgstr "" +"Le point d'entrée %(endpoint_id)s référencé en association avec la politique " +"%(policy_id)s est introuvable." + +#: keystone/contrib/endpoint_policy/core.py:179 +#, python-format +msgid "" +"Unsupported policy association found - Policy %(policy_id)s, Endpoint " +"%(endpoint_id)s, Service %(service_id)s, Region %(region_id)s, " +msgstr "" + +#: keystone/contrib/endpoint_policy/core.py:195 +#, python-format +msgid "" +"Policy %(policy_id)s referenced in association for endpoint %(endpoint_id)s " +"not found." +msgstr "" + +#: keystone/contrib/federation/utils.py:200 +#, python-format +msgid "Impossible to identify the IdP %s " +msgstr "" + +#: keystone/contrib/federation/utils.py:523 +msgid "Ignoring user name" +msgstr "" + +#: keystone/identity/controllers.py:139 +#, python-format +msgid "Unable to remove user %(user)s from %(tenant)s." +msgstr "Impossible de supprimer l'utilisateur %(user)s depuis %(tenant)s." + +#: keystone/identity/controllers.py:158 +#, python-format +msgid "Unable to add user %(user)s to %(tenant)s." +msgstr "Impossible d'ajouter l'utilisateur %(user)s à %(tenant)s." + +#: keystone/identity/core.py:122 +#, python-format +msgid "Invalid domain name (%s) found in config file name" +msgstr "Non de domaine trouvé non valide (%s) dans le fichier de configuration" + +#: keystone/identity/core.py:160 +#, python-format +msgid "Unable to locate domain config directory: %s" +msgstr "Impossible de localiser le répertoire de configuration domaine: %s" + +#: keystone/middleware/core.py:149 +msgid "" +"XML support has been removed as of the Kilo release and should not be " +"referenced or used in deployment. Please remove references to " +"XmlBodyMiddleware from your configuration. This compatibility stub will be " +"removed in the L release" +msgstr "" + +#: keystone/middleware/core.py:234 +msgid "Auth context already exists in the request environment" +msgstr "" + +#: keystone/openstack/common/loopingcall.py:87 +#, python-format +msgid "task %(func_name)r run outlasted interval by %(delay).2f sec" +msgstr "" + +#: keystone/openstack/common/service.py:351 +#, python-format +msgid "pid %d not in child list" +msgstr "PID %d absent de la liste d'enfants" + +#: keystone/resource/core.py:1214 +#, python-format +msgid "" +"Found what looks like an unmatched config option substitution reference - " +"domain: %(domain)s, group: %(group)s, option: %(option)s, value: %(value)s. " +"Perhaps the config option to which it refers has yet to be added?" +msgstr "" + +#: keystone/resource/core.py:1221 +#, python-format +msgid "" +"Found what looks like an incorrectly constructed config option substitution " +"reference - domain: %(domain)s, group: %(group)s, option: %(option)s, value: " +"%(value)s." +msgstr "" + +#: keystone/token/persistence/core.py:228 +#, python-format +msgid "" +"`token_api.%s` is deprecated as of Juno in favor of utilizing methods on " +"`token_provider_api` and may be removed in Kilo." +msgstr "" + +#: keystone/token/persistence/backends/kvs.py:57 +msgid "" +"It is recommended to only use the base key-value-store implementation for " +"the token driver for testing purposes. Please use keystone.token.persistence." +"backends.memcache.Token or keystone.token.persistence.backends.sql.Token " +"instead." +msgstr "" + +#: keystone/token/persistence/backends/kvs.py:206 +#, python-format +msgid "Token `%s` is expired, not adding to the revocation list." +msgstr "" + +#: keystone/token/persistence/backends/kvs.py:240 +#, python-format +msgid "" +"Removing `%s` from revocation list due to invalid expires data in revocation " +"list." +msgstr "" + +#: keystone/token/providers/fernet/utils.py:46 +#, python-format +msgid "[fernet_tokens] key_repository is world readable: %s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:90 +#, python-format +msgid "" +"Unable to change the ownership of [fernet_tokens] key_repository without a " +"keystone user ID and keystone group ID both being provided: %s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:112 +#, python-format +msgid "" +"Unable to change the ownership of the new key without a keystone user ID and " +"keystone group ID both being provided: %s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:204 +msgid "" +"[fernet_tokens] max_active_keys must be at least 1 to maintain a primary key." +msgstr "" diff --git a/keystone-moon/keystone/locale/hu/LC_MESSAGES/keystone-log-critical.po b/keystone-moon/keystone/locale/hu/LC_MESSAGES/keystone-log-critical.po new file mode 100644 index 00000000..767c150e --- /dev/null +++ b/keystone-moon/keystone/locale/hu/LC_MESSAGES/keystone-log-critical.po @@ -0,0 +1,25 @@ +# Translations template for keystone. +# Copyright (C) 2014 OpenStack Foundation +# This file is distributed under the same license as the keystone project. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: Keystone\n" +"Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" +"POT-Creation-Date: 2014-09-07 06:06+0000\n" +"PO-Revision-Date: 2014-08-31 15:19+0000\n" +"Last-Translator: openstackjenkins \n" +"Language-Team: Hungarian (http://www.transifex.com/projects/p/keystone/" +"language/hu/)\n" +"Language: hu\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 1.3\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: keystone/catalog/backends/templated.py:106 +#, python-format +msgid "Unable to open template file %s" +msgstr "Nem nyitható meg a sablonfájl: %s" diff --git a/keystone-moon/keystone/locale/it/LC_MESSAGES/keystone-log-critical.po b/keystone-moon/keystone/locale/it/LC_MESSAGES/keystone-log-critical.po new file mode 100644 index 00000000..35010103 --- /dev/null +++ b/keystone-moon/keystone/locale/it/LC_MESSAGES/keystone-log-critical.po @@ -0,0 +1,25 @@ +# Translations template for keystone. +# Copyright (C) 2014 OpenStack Foundation +# This file is distributed under the same license as the keystone project. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: Keystone\n" +"Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" +"POT-Creation-Date: 2014-09-07 06:06+0000\n" +"PO-Revision-Date: 2014-08-31 15:19+0000\n" +"Last-Translator: openstackjenkins \n" +"Language-Team: Italian (http://www.transifex.com/projects/p/keystone/" +"language/it/)\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 1.3\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: keystone/catalog/backends/templated.py:106 +#, python-format +msgid "Unable to open template file %s" +msgstr "Impossibile aprire il file di template %s" diff --git a/keystone-moon/keystone/locale/it/LC_MESSAGES/keystone-log-error.po b/keystone-moon/keystone/locale/it/LC_MESSAGES/keystone-log-error.po new file mode 100644 index 00000000..d6ac2cf7 --- /dev/null +++ b/keystone-moon/keystone/locale/it/LC_MESSAGES/keystone-log-error.po @@ -0,0 +1,173 @@ +# Translations template for keystone. +# Copyright (C) 2015 OpenStack Foundation +# This file is distributed under the same license as the keystone project. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: Keystone\n" +"Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" +"POT-Creation-Date: 2015-03-09 06:03+0000\n" +"PO-Revision-Date: 2015-03-07 04:31+0000\n" +"Last-Translator: openstackjenkins \n" +"Language-Team: Italian (http://www.transifex.com/projects/p/keystone/" +"language/it/)\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 1.3\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: keystone/notifications.py:304 +msgid "Failed to construct notifier" +msgstr "" + +#: keystone/notifications.py:389 +#, python-format +msgid "Failed to send %(res_id)s %(event_type)s notification" +msgstr "" + +#: keystone/notifications.py:606 +#, python-format +msgid "Failed to send %(action)s %(event_type)s notification" +msgstr "" + +#: keystone/catalog/core.py:62 +#, python-format +msgid "Malformed endpoint - %(url)r is not a string" +msgstr "" + +#: keystone/catalog/core.py:66 +#, python-format +msgid "Malformed endpoint %(url)s - unknown key %(keyerror)s" +msgstr "Endpoint %(url)s non valdio - chiave sconosciuta %(keyerror)s" + +#: keystone/catalog/core.py:71 +#, python-format +msgid "" +"Malformed endpoint '%(url)s'. The following type error occurred during " +"string substitution: %(typeerror)s" +msgstr "" + +#: keystone/catalog/core.py:77 +#, python-format +msgid "" +"Malformed endpoint %s - incomplete format (are you missing a type notifier ?)" +msgstr "" + +#: keystone/common/openssl.py:93 +#, python-format +msgid "Command %(to_exec)s exited with %(retcode)s- %(output)s" +msgstr "" + +#: keystone/common/openssl.py:121 +#, python-format +msgid "Failed to remove file %(file_path)r: %(error)s" +msgstr "" + +#: keystone/common/utils.py:239 +msgid "" +"Error setting up the debug environment. Verify that the option --debug-url " +"has the format : and that a debugger processes is listening on " +"that port." +msgstr "" + +#: keystone/common/cache/core.py:100 +#, python-format +msgid "" +"Unable to build cache config-key. Expected format \":\". " +"Skipping unknown format: %s" +msgstr "" + +#: keystone/common/environment/eventlet_server.py:99 +#, python-format +msgid "Could not bind to %(host)s:%(port)s" +msgstr "Impossible fare il bind verso %(host)s:%(port)s" + +#: keystone/common/environment/eventlet_server.py:185 +msgid "Server error" +msgstr "Errore del server" + +#: keystone/contrib/endpoint_policy/core.py:129 +#: keystone/contrib/endpoint_policy/core.py:228 +#, python-format +msgid "" +"Circular reference or a repeated entry found in region tree - %(region_id)s." +msgstr "" + +#: keystone/contrib/federation/idp.py:410 +#, python-format +msgid "Error when signing assertion, reason: %(reason)s" +msgstr "" + +#: keystone/contrib/oauth1/core.py:136 +msgid "Cannot retrieve Authorization headers" +msgstr "" + +#: keystone/openstack/common/loopingcall.py:95 +msgid "in fixed duration looping call" +msgstr "chiamata in loop a durata fissa" + +#: keystone/openstack/common/loopingcall.py:138 +msgid "in dynamic looping call" +msgstr "chiamata in loop dinamico" + +#: keystone/openstack/common/service.py:268 +msgid "Unhandled exception" +msgstr "Eccezione non gestita" + +#: keystone/resource/core.py:477 +#, python-format +msgid "" +"Circular reference or a repeated entry found projects hierarchy - " +"%(project_id)s." +msgstr "" + +#: keystone/resource/core.py:939 +#, python-format +msgid "" +"Unexpected results in response for domain config - %(count)s responses, " +"first option is %(option)s, expected option %(expected)s" +msgstr "" + +#: keystone/resource/backends/sql.py:102 keystone/resource/backends/sql.py:121 +#, python-format +msgid "" +"Circular reference or a repeated entry found in projects hierarchy - " +"%(project_id)s." +msgstr "" + +#: keystone/token/provider.py:292 +#, python-format +msgid "Unexpected error or malformed token determining token expiry: %s" +msgstr "" + +#: keystone/token/persistence/backends/kvs.py:226 +#, python-format +msgid "" +"Reinitializing revocation list due to error in loading revocation list from " +"backend. Expected `list` type got `%(type)s`. Old revocation list data: " +"%(list)r" +msgstr "" + +#: keystone/token/providers/common.py:611 +msgid "Failed to validate token" +msgstr "" + +#: keystone/token/providers/pki.py:47 +msgid "Unable to sign token" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:38 +#, python-format +msgid "" +"Either [fernet_tokens] key_repository does not exist or Keystone does not " +"have sufficient permission to access it: %s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:79 +msgid "" +"Failed to create [fernet_tokens] key_repository: either it already exists or " +"you don't have sufficient permissions to create it" +msgstr "" diff --git a/keystone-moon/keystone/locale/it/LC_MESSAGES/keystone-log-info.po b/keystone-moon/keystone/locale/it/LC_MESSAGES/keystone-log-info.po new file mode 100644 index 00000000..b88a5de8 --- /dev/null +++ b/keystone-moon/keystone/locale/it/LC_MESSAGES/keystone-log-info.po @@ -0,0 +1,211 @@ +# Translations template for keystone. +# Copyright (C) 2015 OpenStack Foundation +# This file is distributed under the same license as the keystone project. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: Keystone\n" +"Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" +"POT-Creation-Date: 2015-03-09 06:03+0000\n" +"PO-Revision-Date: 2015-03-07 04:31+0000\n" +"Last-Translator: openstackjenkins \n" +"Language-Team: Italian (http://www.transifex.com/projects/p/keystone/" +"language/it/)\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 1.3\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: keystone/assignment/core.py:250 +#, python-format +msgid "Creating the default role %s because it does not exist." +msgstr "" + +#: keystone/assignment/core.py:258 +#, python-format +msgid "Creating the default role %s failed because it was already created" +msgstr "" + +#: keystone/auth/controllers.py:64 +msgid "Loading auth-plugins by class-name is deprecated." +msgstr "" + +#: keystone/auth/controllers.py:106 +#, python-format +msgid "" +"\"expires_at\" has conflicting values %(existing)s and %(new)s. Will use " +"the earliest value." +msgstr "" + +#: keystone/common/openssl.py:81 +#, python-format +msgid "Running command - %s" +msgstr "" + +#: keystone/common/wsgi.py:79 +msgid "No bind information present in token" +msgstr "" + +#: keystone/common/wsgi.py:83 +#, python-format +msgid "Named bind mode %s not in bind information" +msgstr "" + +#: keystone/common/wsgi.py:90 +msgid "Kerberos credentials required and not present" +msgstr "" + +#: keystone/common/wsgi.py:94 +msgid "Kerberos credentials do not match those in bind" +msgstr "" + +#: keystone/common/wsgi.py:98 +msgid "Kerberos bind authentication successful" +msgstr "" + +#: keystone/common/wsgi.py:105 +#, python-format +msgid "Couldn't verify unknown bind: {%(bind_type)s: %(identifier)s}" +msgstr "" + +#: keystone/common/environment/eventlet_server.py:103 +#, python-format +msgid "Starting %(arg0)s on %(host)s:%(port)s" +msgstr "Avvio %(arg0)s in %(host)s:%(port)s" + +#: keystone/common/kvs/core.py:138 +#, python-format +msgid "Adding proxy '%(proxy)s' to KVS %(name)s." +msgstr "" + +#: keystone/common/kvs/core.py:188 +#, python-format +msgid "Using %(func)s as KVS region %(name)s key_mangler" +msgstr "" + +#: keystone/common/kvs/core.py:200 +#, python-format +msgid "Using default dogpile sha1_mangle_key as KVS region %s key_mangler" +msgstr "" + +#: keystone/common/kvs/core.py:210 +#, python-format +msgid "KVS region %s key_mangler disabled." +msgstr "" + +#: keystone/contrib/example/core.py:64 keystone/contrib/example/core.py:73 +#, python-format +msgid "" +"Received the following notification: service %(service)s, resource_type: " +"%(resource_type)s, operation %(operation)s payload %(payload)s" +msgstr "" + +#: keystone/openstack/common/eventlet_backdoor.py:146 +#, python-format +msgid "Eventlet backdoor listening on %(port)s for process %(pid)d" +msgstr "Ascolto di eventlet backdoor su %(port)s per il processo %(pid)d" + +#: keystone/openstack/common/service.py:173 +#, python-format +msgid "Caught %s, exiting" +msgstr "Rilevato %s, esistente" + +#: keystone/openstack/common/service.py:231 +msgid "Parent process has died unexpectedly, exiting" +msgstr "Il processo principale è stato interrotto inaspettatamente, uscire" + +#: keystone/openstack/common/service.py:262 +#, python-format +msgid "Child caught %s, exiting" +msgstr "Cogliere Child %s, uscendo" + +#: keystone/openstack/common/service.py:301 +msgid "Forking too fast, sleeping" +msgstr "Sblocco troppo veloce, attendere" + +#: keystone/openstack/common/service.py:320 +#, python-format +msgid "Started child %d" +msgstr "Child avviato %d" + +#: keystone/openstack/common/service.py:330 +#, python-format +msgid "Starting %d workers" +msgstr "Avvio %d operatori" + +#: keystone/openstack/common/service.py:347 +#, python-format +msgid "Child %(pid)d killed by signal %(sig)d" +msgstr "Child %(pid)d interrotto dal segnale %(sig)d" + +#: keystone/openstack/common/service.py:351 +#, python-format +msgid "Child %(pid)s exited with status %(code)d" +msgstr "Child %(pid)s terminato con stato %(code)d" + +#: keystone/openstack/common/service.py:390 +#, python-format +msgid "Caught %s, stopping children" +msgstr "Intercettato %s, arresto in corso dei children" + +#: keystone/openstack/common/service.py:399 +msgid "Wait called after thread killed. Cleaning up." +msgstr "" + +#: keystone/openstack/common/service.py:415 +#, python-format +msgid "Waiting on %d children to exit" +msgstr "In attesa %d degli elementi secondari per uscire" + +#: keystone/token/persistence/backends/sql.py:279 +#, python-format +msgid "Total expired tokens removed: %d" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:72 +msgid "" +"[fernet_tokens] key_repository does not appear to exist; attempting to " +"create it" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:130 +#, python-format +msgid "Created a new key: %s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:143 +msgid "Key repository is already initialized; aborting." +msgstr "" + +#: keystone/token/providers/fernet/utils.py:179 +#, python-format +msgid "Starting key rotation with %(count)s key files: %(list)s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:185 +#, python-format +msgid "Current primary key is: %s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:187 +#, python-format +msgid "Next primary key will be: %s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:197 +#, python-format +msgid "Promoted key 0 to be the primary: %s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:213 +#, python-format +msgid "Excess keys to purge: %s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:237 +#, python-format +msgid "Loaded %(count)s encryption keys from: %(dir)s" +msgstr "" diff --git a/keystone-moon/keystone/locale/ja/LC_MESSAGES/keystone-log-critical.po b/keystone-moon/keystone/locale/ja/LC_MESSAGES/keystone-log-critical.po new file mode 100644 index 00000000..b83aaad2 --- /dev/null +++ b/keystone-moon/keystone/locale/ja/LC_MESSAGES/keystone-log-critical.po @@ -0,0 +1,25 @@ +# Translations template for keystone. +# Copyright (C) 2014 OpenStack Foundation +# This file is distributed under the same license as the keystone project. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: Keystone\n" +"Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" +"POT-Creation-Date: 2014-09-07 06:06+0000\n" +"PO-Revision-Date: 2014-08-31 15:19+0000\n" +"Last-Translator: openstackjenkins \n" +"Language-Team: Japanese (http://www.transifex.com/projects/p/keystone/" +"language/ja/)\n" +"Language: ja\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 1.3\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: keystone/catalog/backends/templated.py:106 +#, python-format +msgid "Unable to open template file %s" +msgstr "テンプレートファイル %s を開けません" diff --git a/keystone-moon/keystone/locale/ja/LC_MESSAGES/keystone-log-error.po b/keystone-moon/keystone/locale/ja/LC_MESSAGES/keystone-log-error.po new file mode 100644 index 00000000..d3e6062f --- /dev/null +++ b/keystone-moon/keystone/locale/ja/LC_MESSAGES/keystone-log-error.po @@ -0,0 +1,177 @@ +# Translations template for keystone. +# Copyright (C) 2015 OpenStack Foundation +# This file is distributed under the same license as the keystone project. +# +# Translators: +# Kuo(Kyohei MORIYAMA) <>, 2014 +msgid "" +msgstr "" +"Project-Id-Version: Keystone\n" +"Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" +"POT-Creation-Date: 2015-03-09 06:03+0000\n" +"PO-Revision-Date: 2015-03-07 04:31+0000\n" +"Last-Translator: openstackjenkins \n" +"Language-Team: Japanese (http://www.transifex.com/projects/p/keystone/" +"language/ja/)\n" +"Language: ja\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 1.3\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: keystone/notifications.py:304 +msgid "Failed to construct notifier" +msgstr "" + +#: keystone/notifications.py:389 +#, python-format +msgid "Failed to send %(res_id)s %(event_type)s notification" +msgstr "" + +#: keystone/notifications.py:606 +#, python-format +msgid "Failed to send %(action)s %(event_type)s notification" +msgstr "" + +#: keystone/catalog/core.py:62 +#, python-format +msgid "Malformed endpoint - %(url)r is not a string" +msgstr "" + +#: keystone/catalog/core.py:66 +#, python-format +msgid "Malformed endpoint %(url)s - unknown key %(keyerror)s" +msgstr "不正な形式のエンドポイント %(url)s - 未知のキー %(keyerror)s" + +#: keystone/catalog/core.py:71 +#, python-format +msgid "" +"Malformed endpoint '%(url)s'. The following type error occurred during " +"string substitution: %(typeerror)s" +msgstr "" + +#: keystone/catalog/core.py:77 +#, python-format +msgid "" +"Malformed endpoint %s - incomplete format (are you missing a type notifier ?)" +msgstr "" + +#: keystone/common/openssl.py:93 +#, python-format +msgid "Command %(to_exec)s exited with %(retcode)s- %(output)s" +msgstr "" + +#: keystone/common/openssl.py:121 +#, python-format +msgid "Failed to remove file %(file_path)r: %(error)s" +msgstr "" + +#: keystone/common/utils.py:239 +msgid "" +"Error setting up the debug environment. Verify that the option --debug-url " +"has the format : and that a debugger processes is listening on " +"that port." +msgstr "" +"デバッグ環境のセットアップ中にエラーが発生しました。オプション --debug-url " +"が : の形式を持ち、デバッガープロセスがそのポートにおいてリッスン" +"していることを確認してください。" + +#: keystone/common/cache/core.py:100 +#, python-format +msgid "" +"Unable to build cache config-key. Expected format \":\". " +"Skipping unknown format: %s" +msgstr "" + +#: keystone/common/environment/eventlet_server.py:99 +#, python-format +msgid "Could not bind to %(host)s:%(port)s" +msgstr "%(host)s:%(port)s がバインドできません。" + +#: keystone/common/environment/eventlet_server.py:185 +msgid "Server error" +msgstr "内部サーバーエラー" + +#: keystone/contrib/endpoint_policy/core.py:129 +#: keystone/contrib/endpoint_policy/core.py:228 +#, python-format +msgid "" +"Circular reference or a repeated entry found in region tree - %(region_id)s." +msgstr "" + +#: keystone/contrib/federation/idp.py:410 +#, python-format +msgid "Error when signing assertion, reason: %(reason)s" +msgstr "サインアサーション時にエラーが発生しました。理由:%(reason)s" + +#: keystone/contrib/oauth1/core.py:136 +msgid "Cannot retrieve Authorization headers" +msgstr "" + +#: keystone/openstack/common/loopingcall.py:95 +msgid "in fixed duration looping call" +msgstr "一定期間の呼び出しループ" + +#: keystone/openstack/common/loopingcall.py:138 +msgid "in dynamic looping call" +msgstr "動的呼び出しループ" + +#: keystone/openstack/common/service.py:268 +msgid "Unhandled exception" +msgstr "未処理例外" + +#: keystone/resource/core.py:477 +#, python-format +msgid "" +"Circular reference or a repeated entry found projects hierarchy - " +"%(project_id)s." +msgstr "" + +#: keystone/resource/core.py:939 +#, python-format +msgid "" +"Unexpected results in response for domain config - %(count)s responses, " +"first option is %(option)s, expected option %(expected)s" +msgstr "" + +#: keystone/resource/backends/sql.py:102 keystone/resource/backends/sql.py:121 +#, python-format +msgid "" +"Circular reference or a repeated entry found in projects hierarchy - " +"%(project_id)s." +msgstr "" + +#: keystone/token/provider.py:292 +#, python-format +msgid "Unexpected error or malformed token determining token expiry: %s" +msgstr "" + +#: keystone/token/persistence/backends/kvs.py:226 +#, python-format +msgid "" +"Reinitializing revocation list due to error in loading revocation list from " +"backend. Expected `list` type got `%(type)s`. Old revocation list data: " +"%(list)r" +msgstr "" + +#: keystone/token/providers/common.py:611 +msgid "Failed to validate token" +msgstr "" + +#: keystone/token/providers/pki.py:47 +msgid "Unable to sign token" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:38 +#, python-format +msgid "" +"Either [fernet_tokens] key_repository does not exist or Keystone does not " +"have sufficient permission to access it: %s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:79 +msgid "" +"Failed to create [fernet_tokens] key_repository: either it already exists or " +"you don't have sufficient permissions to create it" +msgstr "" diff --git a/keystone-moon/keystone/locale/keystone-log-critical.pot b/keystone-moon/keystone/locale/keystone-log-critical.pot new file mode 100644 index 00000000..e07dd7a9 --- /dev/null +++ b/keystone-moon/keystone/locale/keystone-log-critical.pot @@ -0,0 +1,24 @@ +# Translations template for keystone. +# Copyright (C) 2014 OpenStack Foundation +# This file is distributed under the same license as the keystone project. +# FIRST AUTHOR , 2014. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: keystone 2014.2.dev28.g7e410ae\n" +"Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" +"POT-Creation-Date: 2014-09-07 06:06+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 1.3\n" + +#: keystone/catalog/backends/templated.py:106 +#, python-format +msgid "Unable to open template file %s" +msgstr "" + diff --git a/keystone-moon/keystone/locale/keystone-log-error.pot b/keystone-moon/keystone/locale/keystone-log-error.pot new file mode 100644 index 00000000..bca25a19 --- /dev/null +++ b/keystone-moon/keystone/locale/keystone-log-error.pot @@ -0,0 +1,174 @@ +# Translations template for keystone. +# Copyright (C) 2015 OpenStack Foundation +# This file is distributed under the same license as the keystone project. +# FIRST AUTHOR , 2015. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: keystone 2015.1.dev362\n" +"Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" +"POT-Creation-Date: 2015-03-09 06:03+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 1.3\n" + +#: keystone/notifications.py:304 +msgid "Failed to construct notifier" +msgstr "" + +#: keystone/notifications.py:389 +#, python-format +msgid "Failed to send %(res_id)s %(event_type)s notification" +msgstr "" + +#: keystone/notifications.py:606 +#, python-format +msgid "Failed to send %(action)s %(event_type)s notification" +msgstr "" + +#: keystone/catalog/core.py:62 +#, python-format +msgid "Malformed endpoint - %(url)r is not a string" +msgstr "" + +#: keystone/catalog/core.py:66 +#, python-format +msgid "Malformed endpoint %(url)s - unknown key %(keyerror)s" +msgstr "" + +#: keystone/catalog/core.py:71 +#, python-format +msgid "" +"Malformed endpoint '%(url)s'. The following type error occurred during " +"string substitution: %(typeerror)s" +msgstr "" + +#: keystone/catalog/core.py:77 +#, python-format +msgid "" +"Malformed endpoint %s - incomplete format (are you missing a type " +"notifier ?)" +msgstr "" + +#: keystone/common/openssl.py:93 +#, python-format +msgid "Command %(to_exec)s exited with %(retcode)s- %(output)s" +msgstr "" + +#: keystone/common/openssl.py:121 +#, python-format +msgid "Failed to remove file %(file_path)r: %(error)s" +msgstr "" + +#: keystone/common/utils.py:239 +msgid "" +"Error setting up the debug environment. Verify that the option --debug-" +"url has the format : and that a debugger processes is " +"listening on that port." +msgstr "" + +#: keystone/common/cache/core.py:100 +#, python-format +msgid "" +"Unable to build cache config-key. Expected format \":\". " +"Skipping unknown format: %s" +msgstr "" + +#: keystone/common/environment/eventlet_server.py:99 +#, python-format +msgid "Could not bind to %(host)s:%(port)s" +msgstr "" + +#: keystone/common/environment/eventlet_server.py:185 +msgid "Server error" +msgstr "" + +#: keystone/contrib/endpoint_policy/core.py:129 +#: keystone/contrib/endpoint_policy/core.py:228 +#, python-format +msgid "" +"Circular reference or a repeated entry found in region tree - " +"%(region_id)s." +msgstr "" + +#: keystone/contrib/federation/idp.py:410 +#, python-format +msgid "Error when signing assertion, reason: %(reason)s" +msgstr "" + +#: keystone/contrib/oauth1/core.py:136 +msgid "Cannot retrieve Authorization headers" +msgstr "" + +#: keystone/openstack/common/loopingcall.py:95 +msgid "in fixed duration looping call" +msgstr "" + +#: keystone/openstack/common/loopingcall.py:138 +msgid "in dynamic looping call" +msgstr "" + +#: keystone/openstack/common/service.py:268 +msgid "Unhandled exception" +msgstr "" + +#: keystone/resource/core.py:477 +#, python-format +msgid "" +"Circular reference or a repeated entry found projects hierarchy - " +"%(project_id)s." +msgstr "" + +#: keystone/resource/core.py:939 +#, python-format +msgid "" +"Unexpected results in response for domain config - %(count)s responses, " +"first option is %(option)s, expected option %(expected)s" +msgstr "" + +#: keystone/resource/backends/sql.py:102 keystone/resource/backends/sql.py:121 +#, python-format +msgid "" +"Circular reference or a repeated entry found in projects hierarchy - " +"%(project_id)s." +msgstr "" + +#: keystone/token/provider.py:292 +#, python-format +msgid "Unexpected error or malformed token determining token expiry: %s" +msgstr "" + +#: keystone/token/persistence/backends/kvs.py:226 +#, python-format +msgid "" +"Reinitializing revocation list due to error in loading revocation list " +"from backend. Expected `list` type got `%(type)s`. Old revocation list " +"data: %(list)r" +msgstr "" + +#: keystone/token/providers/common.py:611 +msgid "Failed to validate token" +msgstr "" + +#: keystone/token/providers/pki.py:47 +msgid "Unable to sign token" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:38 +#, python-format +msgid "" +"Either [fernet_tokens] key_repository does not exist or Keystone does not" +" have sufficient permission to access it: %s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:79 +msgid "" +"Failed to create [fernet_tokens] key_repository: either it already exists" +" or you don't have sufficient permissions to create it" +msgstr "" + diff --git a/keystone-moon/keystone/locale/keystone-log-info.pot b/keystone-moon/keystone/locale/keystone-log-info.pot new file mode 100644 index 00000000..17abd1df --- /dev/null +++ b/keystone-moon/keystone/locale/keystone-log-info.pot @@ -0,0 +1,210 @@ +# Translations template for keystone. +# Copyright (C) 2015 OpenStack Foundation +# This file is distributed under the same license as the keystone project. +# FIRST AUTHOR , 2015. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: keystone 2015.1.dev362\n" +"Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" +"POT-Creation-Date: 2015-03-09 06:03+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 1.3\n" + +#: keystone/assignment/core.py:250 +#, python-format +msgid "Creating the default role %s because it does not exist." +msgstr "" + +#: keystone/assignment/core.py:258 +#, python-format +msgid "Creating the default role %s failed because it was already created" +msgstr "" + +#: keystone/auth/controllers.py:64 +msgid "Loading auth-plugins by class-name is deprecated." +msgstr "" + +#: keystone/auth/controllers.py:106 +#, python-format +msgid "" +"\"expires_at\" has conflicting values %(existing)s and %(new)s. Will use" +" the earliest value." +msgstr "" + +#: keystone/common/openssl.py:81 +#, python-format +msgid "Running command - %s" +msgstr "" + +#: keystone/common/wsgi.py:79 +msgid "No bind information present in token" +msgstr "" + +#: keystone/common/wsgi.py:83 +#, python-format +msgid "Named bind mode %s not in bind information" +msgstr "" + +#: keystone/common/wsgi.py:90 +msgid "Kerberos credentials required and not present" +msgstr "" + +#: keystone/common/wsgi.py:94 +msgid "Kerberos credentials do not match those in bind" +msgstr "" + +#: keystone/common/wsgi.py:98 +msgid "Kerberos bind authentication successful" +msgstr "" + +#: keystone/common/wsgi.py:105 +#, python-format +msgid "Couldn't verify unknown bind: {%(bind_type)s: %(identifier)s}" +msgstr "" + +#: keystone/common/environment/eventlet_server.py:103 +#, python-format +msgid "Starting %(arg0)s on %(host)s:%(port)s" +msgstr "" + +#: keystone/common/kvs/core.py:138 +#, python-format +msgid "Adding proxy '%(proxy)s' to KVS %(name)s." +msgstr "" + +#: keystone/common/kvs/core.py:188 +#, python-format +msgid "Using %(func)s as KVS region %(name)s key_mangler" +msgstr "" + +#: keystone/common/kvs/core.py:200 +#, python-format +msgid "Using default dogpile sha1_mangle_key as KVS region %s key_mangler" +msgstr "" + +#: keystone/common/kvs/core.py:210 +#, python-format +msgid "KVS region %s key_mangler disabled." +msgstr "" + +#: keystone/contrib/example/core.py:64 keystone/contrib/example/core.py:73 +#, python-format +msgid "" +"Received the following notification: service %(service)s, resource_type: " +"%(resource_type)s, operation %(operation)s payload %(payload)s" +msgstr "" + +#: keystone/openstack/common/eventlet_backdoor.py:146 +#, python-format +msgid "Eventlet backdoor listening on %(port)s for process %(pid)d" +msgstr "" + +#: keystone/openstack/common/service.py:173 +#, python-format +msgid "Caught %s, exiting" +msgstr "" + +#: keystone/openstack/common/service.py:231 +msgid "Parent process has died unexpectedly, exiting" +msgstr "" + +#: keystone/openstack/common/service.py:262 +#, python-format +msgid "Child caught %s, exiting" +msgstr "" + +#: keystone/openstack/common/service.py:301 +msgid "Forking too fast, sleeping" +msgstr "" + +#: keystone/openstack/common/service.py:320 +#, python-format +msgid "Started child %d" +msgstr "" + +#: keystone/openstack/common/service.py:330 +#, python-format +msgid "Starting %d workers" +msgstr "" + +#: keystone/openstack/common/service.py:347 +#, python-format +msgid "Child %(pid)d killed by signal %(sig)d" +msgstr "" + +#: keystone/openstack/common/service.py:351 +#, python-format +msgid "Child %(pid)s exited with status %(code)d" +msgstr "" + +#: keystone/openstack/common/service.py:390 +#, python-format +msgid "Caught %s, stopping children" +msgstr "" + +#: keystone/openstack/common/service.py:399 +msgid "Wait called after thread killed. Cleaning up." +msgstr "" + +#: keystone/openstack/common/service.py:415 +#, python-format +msgid "Waiting on %d children to exit" +msgstr "" + +#: keystone/token/persistence/backends/sql.py:279 +#, python-format +msgid "Total expired tokens removed: %d" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:72 +msgid "" +"[fernet_tokens] key_repository does not appear to exist; attempting to " +"create it" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:130 +#, python-format +msgid "Created a new key: %s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:143 +msgid "Key repository is already initialized; aborting." +msgstr "" + +#: keystone/token/providers/fernet/utils.py:179 +#, python-format +msgid "Starting key rotation with %(count)s key files: %(list)s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:185 +#, python-format +msgid "Current primary key is: %s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:187 +#, python-format +msgid "Next primary key will be: %s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:197 +#, python-format +msgid "Promoted key 0 to be the primary: %s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:213 +#, python-format +msgid "Excess keys to purge: %s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:237 +#, python-format +msgid "Loaded %(count)s encryption keys from: %(dir)s" +msgstr "" + diff --git a/keystone-moon/keystone/locale/keystone-log-warning.pot b/keystone-moon/keystone/locale/keystone-log-warning.pot new file mode 100644 index 00000000..ddf2931c --- /dev/null +++ b/keystone-moon/keystone/locale/keystone-log-warning.pot @@ -0,0 +1,290 @@ +# Translations template for keystone. +# Copyright (C) 2015 OpenStack Foundation +# This file is distributed under the same license as the keystone project. +# FIRST AUTHOR , 2015. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: keystone 2015.1.dev497\n" +"Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" +"POT-Creation-Date: 2015-03-19 06:04+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 1.3\n" + +#: keystone/cli.py:159 +msgid "keystone-manage pki_setup is not recommended for production use." +msgstr "" + +#: keystone/cli.py:178 +msgid "keystone-manage ssl_setup is not recommended for production use." +msgstr "" + +#: keystone/cli.py:493 +#, python-format +msgid "Ignoring file (%s) while scanning domain config directory" +msgstr "" + +#: keystone/exception.py:49 +msgid "missing exception kwargs (programmer error)" +msgstr "" + +#: keystone/assignment/controllers.py:60 +#, python-format +msgid "Authentication failed: %s" +msgstr "" + +#: keystone/assignment/controllers.py:576 +#, python-format +msgid "" +"Group %(group)s not found for role-assignment - %(target)s with Role: " +"%(role)s" +msgstr "" + +#: keystone/auth/controllers.py:449 +#, python-format +msgid "" +"User %(user_id)s doesn't have access to default project %(project_id)s. " +"The token will be unscoped rather than scoped to the project." +msgstr "" + +#: keystone/auth/controllers.py:457 +#, python-format +msgid "" +"User %(user_id)s's default project %(project_id)s is disabled. The token " +"will be unscoped rather than scoped to the project." +msgstr "" + +#: keystone/auth/controllers.py:466 +#, python-format +msgid "" +"User %(user_id)s's default project %(project_id)s not found. The token " +"will be unscoped rather than scoped to the project." +msgstr "" + +#: keystone/common/authorization.py:55 +msgid "RBAC: Invalid user data in token" +msgstr "" + +#: keystone/common/controller.py:79 keystone/middleware/core.py:224 +msgid "RBAC: Invalid token" +msgstr "" + +#: keystone/common/controller.py:104 keystone/common/controller.py:201 +#: keystone/common/controller.py:740 +msgid "RBAC: Bypassing authorization" +msgstr "" + +#: keystone/common/controller.py:669 keystone/common/controller.py:704 +msgid "Invalid token found while getting domain ID for list request" +msgstr "" + +#: keystone/common/controller.py:677 +msgid "No domain information specified as part of list request" +msgstr "" + +#: keystone/common/utils.py:103 +#, python-format +msgid "Truncating user password to %d characters." +msgstr "" + +#: keystone/common/wsgi.py:242 +#, python-format +msgid "Authorization failed. %(exception)s from %(remote_addr)s" +msgstr "" + +#: keystone/common/wsgi.py:361 +msgid "Invalid token in _get_trust_id_for_request" +msgstr "" + +#: keystone/common/cache/backends/mongo.py:403 +#, python-format +msgid "" +"TTL index already exists on db collection <%(c_name)s>, remove index " +"<%(indx_name)s> first to make updated mongo_ttl_seconds value to be " +"effective" +msgstr "" + +#: keystone/common/kvs/core.py:134 +#, python-format +msgid "%s is not a dogpile.proxy.ProxyBackend" +msgstr "" + +#: keystone/common/kvs/core.py:403 +#, python-format +msgid "KVS lock released (timeout reached) for: %s" +msgstr "" + +#: keystone/common/ldap/core.py:1026 +msgid "" +"LDAP Server does not support paging. Disable paging in keystone.conf to " +"avoid this message." +msgstr "" + +#: keystone/common/ldap/core.py:1225 +#, python-format +msgid "" +"Invalid additional attribute mapping: \"%s\". Format must be " +":" +msgstr "" + +#: keystone/common/ldap/core.py:1336 +#, python-format +msgid "" +"ID attribute %(id_attr)s for LDAP object %(dn)s has multiple values and " +"therefore cannot be used as an ID. Will get the ID from DN instead" +msgstr "" + +#: keystone/common/ldap/core.py:1669 +#, python-format +msgid "" +"When deleting entries for %(search_base)s, could not delete nonexistent " +"entries %(entries)s%(dots)s" +msgstr "" + +#: keystone/contrib/endpoint_policy/core.py:91 +#, python-format +msgid "" +"Endpoint %(endpoint_id)s referenced in association for policy " +"%(policy_id)s not found." +msgstr "" + +#: keystone/contrib/endpoint_policy/core.py:179 +#, python-format +msgid "" +"Unsupported policy association found - Policy %(policy_id)s, Endpoint " +"%(endpoint_id)s, Service %(service_id)s, Region %(region_id)s, " +msgstr "" + +#: keystone/contrib/endpoint_policy/core.py:195 +#, python-format +msgid "" +"Policy %(policy_id)s referenced in association for endpoint " +"%(endpoint_id)s not found." +msgstr "" + +#: keystone/contrib/federation/utils.py:200 +#, python-format +msgid "Impossible to identify the IdP %s " +msgstr "" + +#: keystone/contrib/federation/utils.py:523 +msgid "Ignoring user name" +msgstr "" + +#: keystone/identity/controllers.py:139 +#, python-format +msgid "Unable to remove user %(user)s from %(tenant)s." +msgstr "" + +#: keystone/identity/controllers.py:158 +#, python-format +msgid "Unable to add user %(user)s to %(tenant)s." +msgstr "" + +#: keystone/identity/core.py:122 +#, python-format +msgid "Invalid domain name (%s) found in config file name" +msgstr "" + +#: keystone/identity/core.py:160 +#, python-format +msgid "Unable to locate domain config directory: %s" +msgstr "" + +#: keystone/middleware/core.py:149 +msgid "" +"XML support has been removed as of the Kilo release and should not be " +"referenced or used in deployment. Please remove references to " +"XmlBodyMiddleware from your configuration. This compatibility stub will " +"be removed in the L release" +msgstr "" + +#: keystone/middleware/core.py:234 +msgid "Auth context already exists in the request environment" +msgstr "" + +#: keystone/openstack/common/loopingcall.py:87 +#, python-format +msgid "task %(func_name)r run outlasted interval by %(delay).2f sec" +msgstr "" + +#: keystone/openstack/common/service.py:351 +#, python-format +msgid "pid %d not in child list" +msgstr "" + +#: keystone/resource/core.py:1214 +#, python-format +msgid "" +"Found what looks like an unmatched config option substitution reference -" +" domain: %(domain)s, group: %(group)s, option: %(option)s, value: " +"%(value)s. Perhaps the config option to which it refers has yet to be " +"added?" +msgstr "" + +#: keystone/resource/core.py:1221 +#, python-format +msgid "" +"Found what looks like an incorrectly constructed config option " +"substitution reference - domain: %(domain)s, group: %(group)s, option: " +"%(option)s, value: %(value)s." +msgstr "" + +#: keystone/token/persistence/core.py:228 +#, python-format +msgid "" +"`token_api.%s` is deprecated as of Juno in favor of utilizing methods on " +"`token_provider_api` and may be removed in Kilo." +msgstr "" + +#: keystone/token/persistence/backends/kvs.py:57 +msgid "" +"It is recommended to only use the base key-value-store implementation for" +" the token driver for testing purposes. Please use " +"keystone.token.persistence.backends.memcache.Token or " +"keystone.token.persistence.backends.sql.Token instead." +msgstr "" + +#: keystone/token/persistence/backends/kvs.py:206 +#, python-format +msgid "Token `%s` is expired, not adding to the revocation list." +msgstr "" + +#: keystone/token/persistence/backends/kvs.py:240 +#, python-format +msgid "" +"Removing `%s` from revocation list due to invalid expires data in " +"revocation list." +msgstr "" + +#: keystone/token/providers/fernet/utils.py:46 +#, python-format +msgid "[fernet_tokens] key_repository is world readable: %s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:90 +#, python-format +msgid "" +"Unable to change the ownership of [fernet_tokens] key_repository without " +"a keystone user ID and keystone group ID both being provided: %s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:112 +#, python-format +msgid "" +"Unable to change the ownership of the new key without a keystone user ID " +"and keystone group ID both being provided: %s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:204 +msgid "" +"[fernet_tokens] max_active_keys must be at least 1 to maintain a primary " +"key." +msgstr "" + diff --git a/keystone-moon/keystone/locale/keystone.pot b/keystone-moon/keystone/locale/keystone.pot new file mode 100644 index 00000000..df46fa72 --- /dev/null +++ b/keystone-moon/keystone/locale/keystone.pot @@ -0,0 +1,1522 @@ +# Translations template for keystone. +# Copyright (C) 2015 OpenStack Foundation +# This file is distributed under the same license as the keystone project. +# FIRST AUTHOR , 2015. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: keystone 2015.1.dev497\n" +"Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" +"POT-Creation-Date: 2015-03-19 06:03+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 1.3\n" + +#: keystone/clean.py:24 +#, python-format +msgid "%s cannot be empty." +msgstr "" + +#: keystone/clean.py:26 +#, python-format +msgid "%(property_name)s cannot be less than %(min_length)s characters." +msgstr "" + +#: keystone/clean.py:31 +#, python-format +msgid "%(property_name)s should not be greater than %(max_length)s characters." +msgstr "" + +#: keystone/clean.py:40 +#, python-format +msgid "%(property_name)s is not a %(display_expected_type)s" +msgstr "" + +#: keystone/cli.py:283 +msgid "At least one option must be provided" +msgstr "" + +#: keystone/cli.py:290 +msgid "--all option cannot be mixed with other options" +msgstr "" + +#: keystone/cli.py:301 +#, python-format +msgid "Unknown domain '%(name)s' specified by --domain-name" +msgstr "" + +#: keystone/cli.py:365 keystone/tests/unit/test_cli.py:213 +msgid "At least one option must be provided, use either --all or --domain-name" +msgstr "" + +#: keystone/cli.py:371 keystone/tests/unit/test_cli.py:229 +msgid "The --all option cannot be used with the --domain-name option" +msgstr "" + +#: keystone/cli.py:397 keystone/tests/unit/test_cli.py:246 +#, python-format +msgid "" +"Invalid domain name: %(domain)s found in config file name: %(file)s - " +"ignoring this file." +msgstr "" + +#: keystone/cli.py:405 keystone/tests/unit/test_cli.py:187 +#, python-format +msgid "" +"Domain: %(domain)s already has a configuration defined - ignoring file: " +"%(file)s." +msgstr "" + +#: keystone/cli.py:419 +#, python-format +msgid "Error parsing configuration file for domain: %(domain)s, file: %(file)s." +msgstr "" + +#: keystone/cli.py:452 +#, python-format +msgid "" +"To get a more detailed information on this error, re-run this command for" +" the specific domain, i.e.: keystone-manage domain_config_upload " +"--domain-name %s" +msgstr "" + +#: keystone/cli.py:470 +#, python-format +msgid "Unable to locate domain config directory: %s" +msgstr "" + +#: keystone/cli.py:503 +msgid "" +"Unable to access the keystone database, please check it is configured " +"correctly." +msgstr "" + +#: keystone/exception.py:79 +#, python-format +msgid "" +"Expecting to find %(attribute)s in %(target)s - the server could not " +"comply with the request since it is either malformed or otherwise " +"incorrect. The client is assumed to be in error." +msgstr "" + +#: keystone/exception.py:90 +#, python-format +msgid "%(detail)s" +msgstr "" + +#: keystone/exception.py:94 +msgid "" +"Timestamp not in expected format. The server could not comply with the " +"request since it is either malformed or otherwise incorrect. The client " +"is assumed to be in error." +msgstr "" + +#: keystone/exception.py:103 +#, python-format +msgid "" +"String length exceeded.The length of string '%(string)s' exceeded the " +"limit of column %(type)s(CHAR(%(length)d))." +msgstr "" + +#: keystone/exception.py:109 +#, python-format +msgid "" +"Request attribute %(attribute)s must be less than or equal to %(size)i. " +"The server could not comply with the request because the attribute size " +"is invalid (too large). The client is assumed to be in error." +msgstr "" + +#: keystone/exception.py:119 +#, python-format +msgid "" +"The specified parent region %(parent_region_id)s would create a circular " +"region hierarchy." +msgstr "" + +#: keystone/exception.py:126 +#, python-format +msgid "" +"The password length must be less than or equal to %(size)i. The server " +"could not comply with the request because the password is invalid." +msgstr "" + +#: keystone/exception.py:134 +#, python-format +msgid "" +"Unable to delete region %(region_id)s because it or its child regions " +"have associated endpoints." +msgstr "" + +#: keystone/exception.py:141 +msgid "" +"The certificates you requested are not available. It is likely that this " +"server does not use PKI tokens otherwise this is the result of " +"misconfiguration." +msgstr "" + +#: keystone/exception.py:150 +msgid "(Disable debug mode to suppress these details.)" +msgstr "" + +#: keystone/exception.py:155 +#, python-format +msgid "%(message)s %(amendment)s" +msgstr "" + +#: keystone/exception.py:163 +msgid "The request you have made requires authentication." +msgstr "" + +#: keystone/exception.py:169 +msgid "Authentication plugin error." +msgstr "" + +#: keystone/exception.py:177 +#, python-format +msgid "Unable to find valid groups while using mapping %(mapping_id)s" +msgstr "" + +#: keystone/exception.py:182 +msgid "Attempted to authenticate with an unsupported method." +msgstr "" + +#: keystone/exception.py:190 +msgid "Additional authentications steps required." +msgstr "" + +#: keystone/exception.py:198 +msgid "You are not authorized to perform the requested action." +msgstr "" + +#: keystone/exception.py:205 +#, python-format +msgid "You are not authorized to perform the requested action: %(action)s" +msgstr "" + +#: keystone/exception.py:210 +#, python-format +msgid "" +"Could not change immutable attribute(s) '%(attributes)s' in target " +"%(target)s" +msgstr "" + +#: keystone/exception.py:215 +#, python-format +msgid "" +"Group membership across backend boundaries is not allowed, group in " +"question is %(group_id)s, user is %(user_id)s" +msgstr "" + +#: keystone/exception.py:221 +#, python-format +msgid "" +"Invalid mix of entities for policy association - only Endpoint, Service " +"or Region+Service allowed. Request was - Endpoint: %(endpoint_id)s, " +"Service: %(service_id)s, Region: %(region_id)s" +msgstr "" + +#: keystone/exception.py:228 +#, python-format +msgid "Invalid domain specific configuration: %(reason)s" +msgstr "" + +#: keystone/exception.py:232 +#, python-format +msgid "Could not find: %(target)s" +msgstr "" + +#: keystone/exception.py:238 +#, python-format +msgid "Could not find endpoint: %(endpoint_id)s" +msgstr "" + +#: keystone/exception.py:245 +msgid "An unhandled exception has occurred: Could not find metadata." +msgstr "" + +#: keystone/exception.py:250 +#, python-format +msgid "Could not find policy: %(policy_id)s" +msgstr "" + +#: keystone/exception.py:254 +msgid "Could not find policy association" +msgstr "" + +#: keystone/exception.py:258 +#, python-format +msgid "Could not find role: %(role_id)s" +msgstr "" + +#: keystone/exception.py:262 +#, python-format +msgid "" +"Could not find role assignment with role: %(role_id)s, user or group: " +"%(actor_id)s, project or domain: %(target_id)s" +msgstr "" + +#: keystone/exception.py:268 +#, python-format +msgid "Could not find region: %(region_id)s" +msgstr "" + +#: keystone/exception.py:272 +#, python-format +msgid "Could not find service: %(service_id)s" +msgstr "" + +#: keystone/exception.py:276 +#, python-format +msgid "Could not find domain: %(domain_id)s" +msgstr "" + +#: keystone/exception.py:280 +#, python-format +msgid "Could not find project: %(project_id)s" +msgstr "" + +#: keystone/exception.py:284 +#, python-format +msgid "Cannot create project with parent: %(project_id)s" +msgstr "" + +#: keystone/exception.py:288 +#, python-format +msgid "Could not find token: %(token_id)s" +msgstr "" + +#: keystone/exception.py:292 +#, python-format +msgid "Could not find user: %(user_id)s" +msgstr "" + +#: keystone/exception.py:296 +#, python-format +msgid "Could not find group: %(group_id)s" +msgstr "" + +#: keystone/exception.py:300 +#, python-format +msgid "Could not find mapping: %(mapping_id)s" +msgstr "" + +#: keystone/exception.py:304 +#, python-format +msgid "Could not find trust: %(trust_id)s" +msgstr "" + +#: keystone/exception.py:308 +#, python-format +msgid "No remaining uses for trust: %(trust_id)s" +msgstr "" + +#: keystone/exception.py:312 +#, python-format +msgid "Could not find credential: %(credential_id)s" +msgstr "" + +#: keystone/exception.py:316 +#, python-format +msgid "Could not find version: %(version)s" +msgstr "" + +#: keystone/exception.py:320 +#, python-format +msgid "Could not find Endpoint Group: %(endpoint_group_id)s" +msgstr "" + +#: keystone/exception.py:324 +#, python-format +msgid "Could not find Identity Provider: %(idp_id)s" +msgstr "" + +#: keystone/exception.py:328 +#, python-format +msgid "Could not find Service Provider: %(sp_id)s" +msgstr "" + +#: keystone/exception.py:332 +#, python-format +msgid "" +"Could not find federated protocol %(protocol_id)s for Identity Provider: " +"%(idp_id)s" +msgstr "" + +#: keystone/exception.py:343 +#, python-format +msgid "" +"Could not find %(group_or_option)s in domain configuration for domain " +"%(domain_id)s" +msgstr "" + +#: keystone/exception.py:348 +#, python-format +msgid "Conflict occurred attempting to store %(type)s - %(details)s" +msgstr "" + +#: keystone/exception.py:356 +msgid "An unexpected error prevented the server from fulfilling your request." +msgstr "" + +#: keystone/exception.py:359 +#, python-format +msgid "" +"An unexpected error prevented the server from fulfilling your request: " +"%(exception)s" +msgstr "" + +#: keystone/exception.py:382 +#, python-format +msgid "Unable to consume trust %(trust_id)s, unable to acquire lock." +msgstr "" + +#: keystone/exception.py:387 +msgid "" +"Expected signing certificates are not available on the server. Please " +"check Keystone configuration." +msgstr "" + +#: keystone/exception.py:393 +#, python-format +msgid "Malformed endpoint URL (%(endpoint)s), see ERROR log for details." +msgstr "" + +#: keystone/exception.py:398 +#, python-format +msgid "" +"Group %(group_id)s returned by mapping %(mapping_id)s was not found in " +"the backend." +msgstr "" + +#: keystone/exception.py:403 +#, python-format +msgid "Error while reading metadata file, %(reason)s" +msgstr "" + +#: keystone/exception.py:407 +#, python-format +msgid "" +"Unexpected combination of grant attributes - User: %(user_id)s, Group: " +"%(group_id)s, Project: %(project_id)s, Domain: %(domain_id)s" +msgstr "" + +#: keystone/exception.py:414 +msgid "The action you have requested has not been implemented." +msgstr "" + +#: keystone/exception.py:421 +msgid "The service you have requested is no longer available on this server." +msgstr "" + +#: keystone/exception.py:428 +#, python-format +msgid "The Keystone configuration file %(config_file)s could not be found." +msgstr "" + +#: keystone/exception.py:433 +msgid "" +"No encryption keys found; run keystone-manage fernet_setup to bootstrap " +"one." +msgstr "" + +#: keystone/exception.py:438 +#, python-format +msgid "" +"The Keystone domain-specific configuration has specified more than one " +"SQL driver (only one is permitted): %(source)s." +msgstr "" + +#: keystone/exception.py:445 +#, python-format +msgid "" +"%(mod_name)s doesn't provide database migrations. The migration " +"repository path at %(path)s doesn't exist or isn't a directory." +msgstr "" + +#: keystone/exception.py:457 +#, python-format +msgid "" +"Unable to sign SAML assertion. It is likely that this server does not " +"have xmlsec1 installed, or this is the result of misconfiguration. Reason" +" %(reason)s" +msgstr "" + +#: keystone/exception.py:465 +msgid "" +"No Authorization headers found, cannot proceed with OAuth related calls, " +"if running under HTTPd or Apache, ensure WSGIPassAuthorization is set to " +"On." +msgstr "" + +#: keystone/notifications.py:250 +#, python-format +msgid "%(event)s is not a valid notification event, must be one of: %(actions)s" +msgstr "" + +#: keystone/notifications.py:259 +#, python-format +msgid "Method not callable: %s" +msgstr "" + +#: keystone/assignment/controllers.py:107 keystone/identity/controllers.py:69 +#: keystone/resource/controllers.py:78 +msgid "Name field is required and cannot be empty" +msgstr "" + +#: keystone/assignment/controllers.py:330 +#: keystone/assignment/controllers.py:753 +msgid "Specify a domain or project, not both" +msgstr "" + +#: keystone/assignment/controllers.py:333 +msgid "Specify one of domain or project" +msgstr "" + +#: keystone/assignment/controllers.py:338 +#: keystone/assignment/controllers.py:758 +msgid "Specify a user or group, not both" +msgstr "" + +#: keystone/assignment/controllers.py:341 +msgid "Specify one of user or group" +msgstr "" + +#: keystone/assignment/controllers.py:742 +msgid "Combining effective and group filter will always result in an empty list." +msgstr "" + +#: keystone/assignment/controllers.py:747 +msgid "" +"Combining effective, domain and inherited filters will always result in " +"an empty list." +msgstr "" + +#: keystone/assignment/core.py:228 +msgid "Must specify either domain or project" +msgstr "" + +#: keystone/assignment/core.py:493 +#, python-format +msgid "Project (%s)" +msgstr "" + +#: keystone/assignment/core.py:495 +#, python-format +msgid "Domain (%s)" +msgstr "" + +#: keystone/assignment/core.py:497 +msgid "Unknown Target" +msgstr "" + +#: keystone/assignment/backends/ldap.py:92 +msgid "Domain metadata not supported by LDAP" +msgstr "" + +#: keystone/assignment/backends/ldap.py:381 +#, python-format +msgid "User %(user_id)s already has role %(role_id)s in tenant %(tenant_id)s" +msgstr "" + +#: keystone/assignment/backends/ldap.py:387 +#, python-format +msgid "Role %s not found" +msgstr "" + +#: keystone/assignment/backends/ldap.py:402 +#: keystone/assignment/backends/sql.py:335 +#, python-format +msgid "Cannot remove role that has not been granted, %s" +msgstr "" + +#: keystone/assignment/backends/sql.py:356 +#, python-format +msgid "Unexpected assignment type encountered, %s" +msgstr "" + +#: keystone/assignment/role_backends/ldap.py:61 keystone/catalog/core.py:103 +#: keystone/common/ldap/core.py:1401 keystone/resource/backends/ldap.py:149 +#, python-format +msgid "Duplicate ID, %s." +msgstr "" + +#: keystone/assignment/role_backends/ldap.py:69 +#: keystone/common/ldap/core.py:1391 +#, python-format +msgid "Duplicate name, %s." +msgstr "" + +#: keystone/assignment/role_backends/ldap.py:119 +#, python-format +msgid "Cannot duplicate name %s" +msgstr "" + +#: keystone/auth/controllers.py:60 +#, python-format +msgid "" +"Cannot load an auth-plugin by class-name without a \"method\" attribute " +"defined: %s" +msgstr "" + +#: keystone/auth/controllers.py:71 +#, python-format +msgid "" +"Auth plugin %(plugin)s is requesting previously registered method " +"%(method)s" +msgstr "" + +#: keystone/auth/controllers.py:115 +#, python-format +msgid "" +"Unable to reconcile identity attribute %(attribute)s as it has " +"conflicting values %(new)s and %(old)s" +msgstr "" + +#: keystone/auth/controllers.py:336 +msgid "Scoping to both domain and project is not allowed" +msgstr "" + +#: keystone/auth/controllers.py:339 +msgid "Scoping to both domain and trust is not allowed" +msgstr "" + +#: keystone/auth/controllers.py:342 +msgid "Scoping to both project and trust is not allowed" +msgstr "" + +#: keystone/auth/controllers.py:512 +msgid "User not found" +msgstr "" + +#: keystone/auth/controllers.py:616 +msgid "A project-scoped token is required to produce a service catalog." +msgstr "" + +#: keystone/auth/plugins/external.py:46 +msgid "No authenticated user" +msgstr "" + +#: keystone/auth/plugins/external.py:56 +#, python-format +msgid "Unable to lookup user %s" +msgstr "" + +#: keystone/auth/plugins/external.py:107 +msgid "auth_type is not Negotiate" +msgstr "" + +#: keystone/auth/plugins/mapped.py:244 +msgid "Could not map user" +msgstr "" + +#: keystone/auth/plugins/oauth1.py:39 +#, python-format +msgid "%s not supported" +msgstr "" + +#: keystone/auth/plugins/oauth1.py:57 +msgid "Access token is expired" +msgstr "" + +#: keystone/auth/plugins/oauth1.py:71 +msgid "Could not validate the access token" +msgstr "" + +#: keystone/auth/plugins/password.py:46 +msgid "Invalid username or password" +msgstr "" + +#: keystone/auth/plugins/token.py:72 keystone/token/controllers.py:160 +msgid "rescope a scoped token" +msgstr "" + +#: keystone/catalog/controllers.py:168 +#, python-format +msgid "Conflicting region IDs specified: \"%(url_id)s\" != \"%(ref_id)s\"" +msgstr "" + +#: keystone/common/authorization.py:47 keystone/common/wsgi.py:64 +#, python-format +msgid "token reference must be a KeystoneToken type, got: %s" +msgstr "" + +#: keystone/common/base64utils.py:66 +msgid "pad must be single character" +msgstr "" + +#: keystone/common/base64utils.py:215 +#, python-format +msgid "text is multiple of 4, but pad \"%s\" occurs before 2nd to last char" +msgstr "" + +#: keystone/common/base64utils.py:219 +#, python-format +msgid "text is multiple of 4, but pad \"%s\" occurs before non-pad last char" +msgstr "" + +#: keystone/common/base64utils.py:225 +#, python-format +msgid "text is not a multiple of 4, but contains pad \"%s\"" +msgstr "" + +#: keystone/common/base64utils.py:244 keystone/common/base64utils.py:265 +msgid "padded base64url text must be multiple of 4 characters" +msgstr "" + +#: keystone/common/controller.py:237 keystone/token/providers/common.py:589 +msgid "Non-default domain is not supported" +msgstr "" + +#: keystone/common/controller.py:305 keystone/identity/core.py:428 +#: keystone/resource/core.py:761 keystone/resource/backends/ldap.py:61 +#, python-format +msgid "Expected dict or list: %s" +msgstr "" + +#: keystone/common/controller.py:318 +msgid "Marker could not be found" +msgstr "" + +#: keystone/common/controller.py:329 +msgid "Invalid limit value" +msgstr "" + +#: keystone/common/controller.py:637 +msgid "Cannot change Domain ID" +msgstr "" + +#: keystone/common/controller.py:666 +msgid "domain_id is required as part of entity" +msgstr "" + +#: keystone/common/controller.py:701 +msgid "A domain-scoped token must be used" +msgstr "" + +#: keystone/common/dependency.py:68 +#, python-format +msgid "Unregistered dependency: %(name)s for %(targets)s" +msgstr "" + +#: keystone/common/dependency.py:108 +msgid "event_callbacks must be a dict" +msgstr "" + +#: keystone/common/dependency.py:113 +#, python-format +msgid "event_callbacks[%s] must be a dict" +msgstr "" + +#: keystone/common/pemutils.py:223 +#, python-format +msgid "unknown pem_type \"%(pem_type)s\", valid types are: %(valid_pem_types)s" +msgstr "" + +#: keystone/common/pemutils.py:242 +#, python-format +msgid "" +"unknown pem header \"%(pem_header)s\", valid headers are: " +"%(valid_pem_headers)s" +msgstr "" + +#: keystone/common/pemutils.py:298 +#, python-format +msgid "failed to find end matching \"%s\"" +msgstr "" + +#: keystone/common/pemutils.py:302 +#, python-format +msgid "" +"beginning & end PEM headers do not match (%(begin_pem_header)s!= " +"%(end_pem_header)s)" +msgstr "" + +#: keystone/common/pemutils.py:377 +#, python-format +msgid "unknown pem_type: \"%s\"" +msgstr "" + +#: keystone/common/pemutils.py:389 +#, python-format +msgid "" +"failed to base64 decode %(pem_type)s PEM at position%(position)d: " +"%(err_msg)s" +msgstr "" + +#: keystone/common/utils.py:164 keystone/credential/controllers.py:44 +msgid "Invalid blob in credential" +msgstr "" + +#: keystone/common/wsgi.py:330 +#, python-format +msgid "%s field is required and cannot be empty" +msgstr "" + +#: keystone/common/wsgi.py:342 +#, python-format +msgid "%s field(s) cannot be empty" +msgstr "" + +#: keystone/common/wsgi.py:563 +msgid "The resource could not be found." +msgstr "" + +#: keystone/common/wsgi.py:704 +#, python-format +msgid "Unexpected status requested for JSON Home response, %s" +msgstr "" + +#: keystone/common/cache/_memcache_pool.py:113 +#, python-format +msgid "Unable to get a connection from pool id %(id)s after %(seconds)s seconds." +msgstr "" + +#: keystone/common/cache/core.py:132 +msgid "region not type dogpile.cache.CacheRegion" +msgstr "" + +#: keystone/common/cache/backends/mongo.py:231 +msgid "db_hosts value is required" +msgstr "" + +#: keystone/common/cache/backends/mongo.py:236 +msgid "database db_name is required" +msgstr "" + +#: keystone/common/cache/backends/mongo.py:241 +msgid "cache_collection name is required" +msgstr "" + +#: keystone/common/cache/backends/mongo.py:252 +msgid "integer value expected for w (write concern attribute)" +msgstr "" + +#: keystone/common/cache/backends/mongo.py:260 +msgid "replicaset_name required when use_replica is True" +msgstr "" + +#: keystone/common/cache/backends/mongo.py:275 +msgid "integer value expected for mongo_ttl_seconds" +msgstr "" + +#: keystone/common/cache/backends/mongo.py:301 +msgid "no ssl support available" +msgstr "" + +#: keystone/common/cache/backends/mongo.py:310 +#, python-format +msgid "" +"Invalid ssl_cert_reqs value of %s, must be one of \"NONE\", \"OPTIONAL\"," +" \"REQUIRED\"" +msgstr "" + +#: keystone/common/kvs/core.py:71 +#, python-format +msgid "Lock Timeout occurred for key, %(target)s" +msgstr "" + +#: keystone/common/kvs/core.py:106 +#, python-format +msgid "KVS region %s is already configured. Cannot reconfigure." +msgstr "" + +#: keystone/common/kvs/core.py:145 +#, python-format +msgid "Key Value Store not configured: %s" +msgstr "" + +#: keystone/common/kvs/core.py:198 +msgid "`key_mangler` option must be a function reference" +msgstr "" + +#: keystone/common/kvs/core.py:353 +#, python-format +msgid "Lock key must match target key: %(lock)s != %(target)s" +msgstr "" + +#: keystone/common/kvs/core.py:357 +msgid "Must be called within an active lock context." +msgstr "" + +#: keystone/common/kvs/backends/memcached.py:69 +#, python-format +msgid "Maximum lock attempts on %s occurred." +msgstr "" + +#: keystone/common/kvs/backends/memcached.py:108 +#, python-format +msgid "" +"Backend `%(driver)s` is not a valid memcached backend. Valid drivers: " +"%(driver_list)s" +msgstr "" + +#: keystone/common/kvs/backends/memcached.py:178 +msgid "`key_mangler` functions must be callable." +msgstr "" + +#: keystone/common/ldap/core.py:191 +#, python-format +msgid "Invalid LDAP deref option: %(option)s. Choose one of: %(options)s" +msgstr "" + +#: keystone/common/ldap/core.py:201 +#, python-format +msgid "Invalid LDAP TLS certs option: %(option)s. Choose one of: %(options)s" +msgstr "" + +#: keystone/common/ldap/core.py:213 +#, python-format +msgid "Invalid LDAP scope: %(scope)s. Choose one of: %(options)s" +msgstr "" + +#: keystone/common/ldap/core.py:588 +msgid "Invalid TLS / LDAPS combination" +msgstr "" + +#: keystone/common/ldap/core.py:593 +#, python-format +msgid "Invalid LDAP TLS_AVAIL option: %s. TLS not available" +msgstr "" + +#: keystone/common/ldap/core.py:603 +#, python-format +msgid "tls_cacertfile %s not found or is not a file" +msgstr "" + +#: keystone/common/ldap/core.py:615 +#, python-format +msgid "tls_cacertdir %s not found or is not a directory" +msgstr "" + +#: keystone/common/ldap/core.py:1326 +#, python-format +msgid "ID attribute %(id_attr)s not found in LDAP object %(dn)s" +msgstr "" + +#: keystone/common/ldap/core.py:1370 +#, python-format +msgid "LDAP %s create" +msgstr "" + +#: keystone/common/ldap/core.py:1375 +#, python-format +msgid "LDAP %s update" +msgstr "" + +#: keystone/common/ldap/core.py:1380 +#, python-format +msgid "LDAP %s delete" +msgstr "" + +#: keystone/common/ldap/core.py:1522 +msgid "" +"Disabling an entity where the 'enable' attribute is ignored by " +"configuration." +msgstr "" + +#: keystone/common/ldap/core.py:1533 +#, python-format +msgid "Cannot change %(option_name)s %(attr)s" +msgstr "" + +#: keystone/common/ldap/core.py:1620 +#, python-format +msgid "Member %(member)s is already a member of group %(group)s" +msgstr "" + +#: keystone/common/sql/core.py:219 +msgid "" +"Cannot truncate a driver call without hints list as first parameter after" +" self " +msgstr "" + +#: keystone/common/sql/core.py:410 +msgid "Duplicate Entry" +msgstr "" + +#: keystone/common/sql/core.py:426 +#, python-format +msgid "An unexpected error occurred when trying to store %s" +msgstr "" + +#: keystone/common/sql/migration_helpers.py:187 +#: keystone/common/sql/migration_helpers.py:245 +#, python-format +msgid "%s extension does not exist." +msgstr "" + +#: keystone/common/validation/validators.py:54 +#, python-format +msgid "Invalid input for field '%(path)s'. The value is '%(value)s'." +msgstr "" + +#: keystone/contrib/ec2/controllers.py:318 +msgid "Token belongs to another user" +msgstr "" + +#: keystone/contrib/ec2/controllers.py:346 +msgid "Credential belongs to another user" +msgstr "" + +#: keystone/contrib/endpoint_filter/backends/sql.py:69 +#, python-format +msgid "Endpoint %(endpoint_id)s not found in project %(project_id)s" +msgstr "" + +#: keystone/contrib/endpoint_filter/backends/sql.py:180 +msgid "Endpoint Group Project Association not found" +msgstr "" + +#: keystone/contrib/endpoint_policy/core.py:258 +#, python-format +msgid "No policy is associated with endpoint %(endpoint_id)s." +msgstr "" + +#: keystone/contrib/federation/controllers.py:274 +msgid "Missing entity ID from environment" +msgstr "" + +#: keystone/contrib/federation/controllers.py:282 +msgid "Request must have an origin query parameter" +msgstr "" + +#: keystone/contrib/federation/controllers.py:292 +#, python-format +msgid "%(host)s is not a trusted dashboard host" +msgstr "" + +#: keystone/contrib/federation/controllers.py:333 +msgid "Use a project scoped token when attempting to create a SAML assertion" +msgstr "" + +#: keystone/contrib/federation/idp.py:454 +#, python-format +msgid "Cannot open certificate %(cert_file)s. Reason: %(reason)s" +msgstr "" + +#: keystone/contrib/federation/idp.py:521 +msgid "Ensure configuration option idp_entity_id is set." +msgstr "" + +#: keystone/contrib/federation/idp.py:524 +msgid "Ensure configuration option idp_sso_endpoint is set." +msgstr "" + +#: keystone/contrib/federation/idp.py:544 +msgid "" +"idp_contact_type must be one of: [technical, other, support, " +"administrative or billing." +msgstr "" + +#: keystone/contrib/federation/utils.py:178 +msgid "Federation token is expired" +msgstr "" + +#: keystone/contrib/federation/utils.py:208 +msgid "" +"Could not find Identity Provider identifier in environment, check " +"[federation] remote_id_attribute for details." +msgstr "" + +#: keystone/contrib/federation/utils.py:213 +msgid "" +"Incoming identity provider identifier not included among the accepted " +"identifiers." +msgstr "" + +#: keystone/contrib/federation/utils.py:501 +#, python-format +msgid "User type %s not supported" +msgstr "" + +#: keystone/contrib/federation/utils.py:537 +#, python-format +msgid "" +"Invalid rule: %(identity_value)s. Both 'groups' and 'domain' keywords " +"must be specified." +msgstr "" + +#: keystone/contrib/federation/utils.py:753 +#, python-format +msgid "Identity Provider %(idp)s is disabled" +msgstr "" + +#: keystone/contrib/federation/utils.py:761 +#, python-format +msgid "Service Provider %(sp)s is disabled" +msgstr "" + +#: keystone/contrib/oauth1/controllers.py:99 +msgid "Cannot change consumer secret" +msgstr "" + +#: keystone/contrib/oauth1/controllers.py:131 +msgid "Cannot list request tokens with a token issued via delegation." +msgstr "" + +#: keystone/contrib/oauth1/controllers.py:192 +#: keystone/contrib/oauth1/backends/sql.py:270 +msgid "User IDs do not match" +msgstr "" + +#: keystone/contrib/oauth1/controllers.py:199 +msgid "Could not find role" +msgstr "" + +#: keystone/contrib/oauth1/controllers.py:248 +msgid "Invalid signature" +msgstr "" + +#: keystone/contrib/oauth1/controllers.py:299 +#: keystone/contrib/oauth1/controllers.py:377 +msgid "Request token is expired" +msgstr "" + +#: keystone/contrib/oauth1/controllers.py:313 +msgid "There should not be any non-oauth parameters" +msgstr "" + +#: keystone/contrib/oauth1/controllers.py:317 +msgid "provided consumer key does not match stored consumer key" +msgstr "" + +#: keystone/contrib/oauth1/controllers.py:321 +msgid "provided verifier does not match stored verifier" +msgstr "" + +#: keystone/contrib/oauth1/controllers.py:325 +msgid "provided request key does not match stored request key" +msgstr "" + +#: keystone/contrib/oauth1/controllers.py:329 +msgid "Request Token does not have an authorizing user id" +msgstr "" + +#: keystone/contrib/oauth1/controllers.py:366 +msgid "Cannot authorize a request token with a token issued via delegation." +msgstr "" + +#: keystone/contrib/oauth1/controllers.py:396 +msgid "authorizing user does not have role required" +msgstr "" + +#: keystone/contrib/oauth1/controllers.py:409 +msgid "User is not a member of the requested project" +msgstr "" + +#: keystone/contrib/oauth1/backends/sql.py:91 +msgid "Consumer not found" +msgstr "" + +#: keystone/contrib/oauth1/backends/sql.py:186 +msgid "Request token not found" +msgstr "" + +#: keystone/contrib/oauth1/backends/sql.py:250 +msgid "Access token not found" +msgstr "" + +#: keystone/contrib/revoke/controllers.py:33 +#, python-format +msgid "invalid date format %s" +msgstr "" + +#: keystone/contrib/revoke/core.py:150 +msgid "" +"The revoke call must not have both domain_id and project_id. This is a " +"bug in the Keystone server. The current request is aborted." +msgstr "" + +#: keystone/contrib/revoke/core.py:218 keystone/token/provider.py:207 +#: keystone/token/provider.py:230 keystone/token/provider.py:296 +#: keystone/token/provider.py:303 +msgid "Failed to validate token" +msgstr "" + +#: keystone/identity/controllers.py:72 +msgid "Enabled field must be a boolean" +msgstr "" + +#: keystone/identity/controllers.py:98 +msgid "Enabled field should be a boolean" +msgstr "" + +#: keystone/identity/core.py:112 +#, python-format +msgid "Database at /domains/%s/config" +msgstr "" + +#: keystone/identity/core.py:287 keystone/identity/backends/ldap.py:59 +#: keystone/identity/backends/ldap.py:61 keystone/identity/backends/ldap.py:67 +#: keystone/identity/backends/ldap.py:69 keystone/identity/backends/sql.py:104 +#: keystone/identity/backends/sql.py:106 +msgid "Invalid user / password" +msgstr "" + +#: keystone/identity/core.py:693 +#, python-format +msgid "User is disabled: %s" +msgstr "" + +#: keystone/identity/core.py:735 +msgid "Cannot change user ID" +msgstr "" + +#: keystone/identity/backends/ldap.py:99 +msgid "Cannot change user name" +msgstr "" + +#: keystone/identity/backends/ldap.py:188 keystone/identity/backends/sql.py:188 +#: keystone/identity/backends/sql.py:206 +#, python-format +msgid "User '%(user_id)s' not found in group '%(group_id)s'" +msgstr "" + +#: keystone/identity/backends/ldap.py:339 +#, python-format +msgid "User %(user_id)s is already a member of group %(group_id)s" +msgstr "" + +#: keystone/models/token_model.py:61 +msgid "Found invalid token: scoped to both project and domain." +msgstr "" + +#: keystone/openstack/common/versionutils.py:108 +#, python-format +msgid "" +"%(what)s is deprecated as of %(as_of)s in favor of %(in_favor_of)s and " +"may be removed in %(remove_in)s." +msgstr "" + +#: keystone/openstack/common/versionutils.py:112 +#, python-format +msgid "" +"%(what)s is deprecated as of %(as_of)s and may be removed in " +"%(remove_in)s. It will not be superseded." +msgstr "" + +#: keystone/openstack/common/versionutils.py:116 +#, python-format +msgid "%(what)s is deprecated as of %(as_of)s in favor of %(in_favor_of)s." +msgstr "" + +#: keystone/openstack/common/versionutils.py:119 +#, python-format +msgid "%(what)s is deprecated as of %(as_of)s. It will not be superseded." +msgstr "" + +#: keystone/openstack/common/versionutils.py:241 +#, python-format +msgid "Deprecated: %s" +msgstr "" + +#: keystone/openstack/common/versionutils.py:259 +#, python-format +msgid "Fatal call to deprecated config: %(msg)s" +msgstr "" + +#: keystone/resource/controllers.py:231 +msgid "" +"Cannot use parents_as_list and parents_as_ids query params at the same " +"time." +msgstr "" + +#: keystone/resource/controllers.py:237 +msgid "" +"Cannot use subtree_as_list and subtree_as_ids query params at the same " +"time." +msgstr "" + +#: keystone/resource/core.py:80 +#, python-format +msgid "max hierarchy depth reached for %s branch." +msgstr "" + +#: keystone/resource/core.py:97 +msgid "cannot create a project within a different domain than its parents." +msgstr "" + +#: keystone/resource/core.py:101 +#, python-format +msgid "cannot create a project in a branch containing a disabled project: %s" +msgstr "" + +#: keystone/resource/core.py:123 +#, python-format +msgid "Domain is disabled: %s" +msgstr "" + +#: keystone/resource/core.py:141 +#, python-format +msgid "Domain cannot be named %s" +msgstr "" + +#: keystone/resource/core.py:144 +#, python-format +msgid "Domain cannot have ID %s" +msgstr "" + +#: keystone/resource/core.py:156 +#, python-format +msgid "Project is disabled: %s" +msgstr "" + +#: keystone/resource/core.py:176 +#, python-format +msgid "cannot enable project %s since it has disabled parents" +msgstr "" + +#: keystone/resource/core.py:184 +#, python-format +msgid "cannot disable project %s since its subtree contains enabled projects" +msgstr "" + +#: keystone/resource/core.py:195 +msgid "Update of `parent_id` is not allowed." +msgstr "" + +#: keystone/resource/core.py:222 +#, python-format +msgid "cannot delete the project %s since it is not a leaf in the hierarchy." +msgstr "" + +#: keystone/resource/core.py:376 +msgid "Multiple domains are not supported" +msgstr "" + +#: keystone/resource/core.py:429 +msgid "delete the default domain" +msgstr "" + +#: keystone/resource/core.py:440 +msgid "cannot delete a domain that is enabled, please disable it first." +msgstr "" + +#: keystone/resource/core.py:841 +msgid "No options specified" +msgstr "" + +#: keystone/resource/core.py:847 +#, python-format +msgid "" +"The value of group %(group)s specified in the config should be a " +"dictionary of options" +msgstr "" + +#: keystone/resource/core.py:871 +#, python-format +msgid "" +"Option %(option)s found with no group specified while checking domain " +"configuration request" +msgstr "" + +#: keystone/resource/core.py:878 +#, python-format +msgid "Group %(group)s is not supported for domain specific configurations" +msgstr "" + +#: keystone/resource/core.py:885 +#, python-format +msgid "" +"Option %(option)s in group %(group)s is not supported for domain specific" +" configurations" +msgstr "" + +#: keystone/resource/core.py:938 +msgid "An unexpected error occurred when retrieving domain configs" +msgstr "" + +#: keystone/resource/core.py:1013 keystone/resource/core.py:1097 +#: keystone/resource/core.py:1167 keystone/resource/config_backends/sql.py:70 +#, python-format +msgid "option %(option)s in group %(group)s" +msgstr "" + +#: keystone/resource/core.py:1016 keystone/resource/core.py:1102 +#: keystone/resource/core.py:1163 +#, python-format +msgid "group %(group)s" +msgstr "" + +#: keystone/resource/core.py:1018 +msgid "any options" +msgstr "" + +#: keystone/resource/core.py:1062 +#, python-format +msgid "" +"Trying to update option %(option)s in group %(group)s, so that, and only " +"that, option must be specified in the config" +msgstr "" + +#: keystone/resource/core.py:1067 +#, python-format +msgid "" +"Trying to update group %(group)s, so that, and only that, group must be " +"specified in the config" +msgstr "" + +#: keystone/resource/core.py:1076 +#, python-format +msgid "" +"request to update group %(group)s, but config provided contains group " +"%(group_other)s instead" +msgstr "" + +#: keystone/resource/core.py:1083 +#, python-format +msgid "" +"Trying to update option %(option)s in group %(group)s, but config " +"provided contains option %(option_other)s instead" +msgstr "" + +#: keystone/resource/backends/ldap.py:151 +#: keystone/resource/backends/ldap.py:159 +#: keystone/resource/backends/ldap.py:163 +msgid "Domains are read-only against LDAP" +msgstr "" + +#: keystone/server/eventlet.py:77 +msgid "" +"Running keystone via eventlet is deprecated as of Kilo in favor of " +"running in a WSGI server (e.g. mod_wsgi). Support for keystone under " +"eventlet will be removed in the \"M\"-Release." +msgstr "" + +#: keystone/server/eventlet.py:90 +#, python-format +msgid "Failed to start the %(name)s server" +msgstr "" + +#: keystone/token/controllers.py:391 +#, python-format +msgid "User %(u_id)s is unauthorized for tenant %(t_id)s" +msgstr "" + +#: keystone/token/controllers.py:410 keystone/token/controllers.py:413 +msgid "Token does not belong to specified tenant." +msgstr "" + +#: keystone/token/persistence/backends/kvs.py:133 +#, python-format +msgid "Unknown token version %s" +msgstr "" + +#: keystone/token/providers/common.py:250 +#: keystone/token/providers/common.py:355 +#, python-format +msgid "User %(user_id)s has no access to project %(project_id)s" +msgstr "" + +#: keystone/token/providers/common.py:255 +#: keystone/token/providers/common.py:360 +#, python-format +msgid "User %(user_id)s has no access to domain %(domain_id)s" +msgstr "" + +#: keystone/token/providers/common.py:282 +msgid "Trustor is disabled." +msgstr "" + +#: keystone/token/providers/common.py:346 +msgid "Trustee has no delegated roles." +msgstr "" + +#: keystone/token/providers/common.py:407 +#, python-format +msgid "Invalid audit info data type: %(data)s (%(type)s)" +msgstr "" + +#: keystone/token/providers/common.py:435 +msgid "User is not a trustee." +msgstr "" + +#: keystone/token/providers/common.py:579 +msgid "" +"Attempting to use OS-FEDERATION token with V2 Identity Service, use V3 " +"Authentication" +msgstr "" + +#: keystone/token/providers/common.py:597 +msgid "Domain scoped token is not supported" +msgstr "" + +#: keystone/token/providers/pki.py:48 keystone/token/providers/pkiz.py:30 +msgid "Unable to sign token." +msgstr "" + +#: keystone/token/providers/fernet/core.py:210 +msgid "" +"This is not a v2.0 Fernet token. Use v3 for trust, domain, or federated " +"tokens." +msgstr "" + +#: keystone/token/providers/fernet/token_formatters.py:189 +#, python-format +msgid "This is not a recognized Fernet payload version: %s" +msgstr "" + +#: keystone/trust/controllers.py:148 +msgid "Redelegation allowed for delegated by trust only" +msgstr "" + +#: keystone/trust/controllers.py:181 +msgid "The authenticated user should match the trustor." +msgstr "" + +#: keystone/trust/controllers.py:186 +msgid "At least one role should be specified." +msgstr "" + +#: keystone/trust/core.py:57 +#, python-format +msgid "" +"Remaining redelegation depth of %(redelegation_depth)d out of allowed " +"range of [0..%(max_count)d]" +msgstr "" + +#: keystone/trust/core.py:66 +#, python-format +msgid "" +"Field \"remaining_uses\" is set to %(value)s while it must not be set in " +"order to redelegate a trust" +msgstr "" + +#: keystone/trust/core.py:77 +msgid "Requested expiration time is more than redelegated trust can provide" +msgstr "" + +#: keystone/trust/core.py:87 +msgid "Some of requested roles are not in redelegated trust" +msgstr "" + +#: keystone/trust/core.py:116 +msgid "One of the trust agents is disabled or deleted" +msgstr "" + +#: keystone/trust/core.py:135 +msgid "remaining_uses must be a positive integer or null." +msgstr "" + +#: keystone/trust/core.py:141 +#, python-format +msgid "" +"Requested redelegation depth of %(requested_count)d is greater than " +"allowed %(max_count)d" +msgstr "" + +#: keystone/trust/core.py:147 +msgid "remaining_uses must not be set if redelegation is allowed" +msgstr "" + +#: keystone/trust/core.py:157 +msgid "" +"Modifying \"redelegation_count\" upon redelegation is forbidden. Omitting" +" this parameter is advised." +msgstr "" + diff --git a/keystone-moon/keystone/locale/ko_KR/LC_MESSAGES/keystone-log-critical.po b/keystone-moon/keystone/locale/ko_KR/LC_MESSAGES/keystone-log-critical.po new file mode 100644 index 00000000..b7f255c4 --- /dev/null +++ b/keystone-moon/keystone/locale/ko_KR/LC_MESSAGES/keystone-log-critical.po @@ -0,0 +1,25 @@ +# Translations template for keystone. +# Copyright (C) 2014 OpenStack Foundation +# This file is distributed under the same license as the keystone project. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: Keystone\n" +"Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" +"POT-Creation-Date: 2014-09-07 06:06+0000\n" +"PO-Revision-Date: 2014-08-31 15:19+0000\n" +"Last-Translator: openstackjenkins \n" +"Language-Team: Korean (Korea) (http://www.transifex.com/projects/p/keystone/" +"language/ko_KR/)\n" +"Language: ko_KR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 1.3\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: keystone/catalog/backends/templated.py:106 +#, python-format +msgid "Unable to open template file %s" +msgstr "템플리트 파일 %s을(를) 열 수 없음" diff --git a/keystone-moon/keystone/locale/pl_PL/LC_MESSAGES/keystone-log-critical.po b/keystone-moon/keystone/locale/pl_PL/LC_MESSAGES/keystone-log-critical.po new file mode 100644 index 00000000..b7749060 --- /dev/null +++ b/keystone-moon/keystone/locale/pl_PL/LC_MESSAGES/keystone-log-critical.po @@ -0,0 +1,26 @@ +# Translations template for keystone. +# Copyright (C) 2014 OpenStack Foundation +# This file is distributed under the same license as the keystone project. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: Keystone\n" +"Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" +"POT-Creation-Date: 2014-09-07 06:06+0000\n" +"PO-Revision-Date: 2014-08-31 15:19+0000\n" +"Last-Translator: openstackjenkins \n" +"Language-Team: Polish (Poland) (http://www.transifex.com/projects/p/keystone/" +"language/pl_PL/)\n" +"Language: pl_PL\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 1.3\n" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " +"|| n%100>=20) ? 1 : 2);\n" + +#: keystone/catalog/backends/templated.py:106 +#, python-format +msgid "Unable to open template file %s" +msgstr "Błąd podczas otwierania pliku %s" diff --git a/keystone-moon/keystone/locale/pt_BR/LC_MESSAGES/keystone-log-critical.po b/keystone-moon/keystone/locale/pt_BR/LC_MESSAGES/keystone-log-critical.po new file mode 100644 index 00000000..689a23ec --- /dev/null +++ b/keystone-moon/keystone/locale/pt_BR/LC_MESSAGES/keystone-log-critical.po @@ -0,0 +1,25 @@ +# Translations template for keystone. +# Copyright (C) 2014 OpenStack Foundation +# This file is distributed under the same license as the keystone project. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: Keystone\n" +"Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" +"POT-Creation-Date: 2014-09-07 06:06+0000\n" +"PO-Revision-Date: 2014-08-31 15:19+0000\n" +"Last-Translator: openstackjenkins \n" +"Language-Team: Portuguese (Brazil) (http://www.transifex.com/projects/p/" +"keystone/language/pt_BR/)\n" +"Language: pt_BR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 1.3\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: keystone/catalog/backends/templated.py:106 +#, python-format +msgid "Unable to open template file %s" +msgstr "Não é possível abrir o arquivo de modelo %s" diff --git a/keystone-moon/keystone/locale/pt_BR/LC_MESSAGES/keystone-log-error.po b/keystone-moon/keystone/locale/pt_BR/LC_MESSAGES/keystone-log-error.po new file mode 100644 index 00000000..5f81b98d --- /dev/null +++ b/keystone-moon/keystone/locale/pt_BR/LC_MESSAGES/keystone-log-error.po @@ -0,0 +1,179 @@ +# Translations template for keystone. +# Copyright (C) 2015 OpenStack Foundation +# This file is distributed under the same license as the keystone project. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: Keystone\n" +"Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" +"POT-Creation-Date: 2015-03-09 06:03+0000\n" +"PO-Revision-Date: 2015-03-07 04:31+0000\n" +"Last-Translator: openstackjenkins \n" +"Language-Team: Portuguese (Brazil) (http://www.transifex.com/projects/p/" +"keystone/language/pt_BR/)\n" +"Language: pt_BR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 1.3\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: keystone/notifications.py:304 +msgid "Failed to construct notifier" +msgstr "" + +#: keystone/notifications.py:389 +#, python-format +msgid "Failed to send %(res_id)s %(event_type)s notification" +msgstr "Falha ao enviar notificação %(res_id)s %(event_type)s" + +#: keystone/notifications.py:606 +#, python-format +msgid "Failed to send %(action)s %(event_type)s notification" +msgstr "" + +#: keystone/catalog/core.py:62 +#, python-format +msgid "Malformed endpoint - %(url)r is not a string" +msgstr "" + +#: keystone/catalog/core.py:66 +#, python-format +msgid "Malformed endpoint %(url)s - unknown key %(keyerror)s" +msgstr "Endpoint mal formado %(url)s - chave desconhecida %(keyerror)s" + +#: keystone/catalog/core.py:71 +#, python-format +msgid "" +"Malformed endpoint '%(url)s'. The following type error occurred during " +"string substitution: %(typeerror)s" +msgstr "" + +#: keystone/catalog/core.py:77 +#, python-format +msgid "" +"Malformed endpoint %s - incomplete format (are you missing a type notifier ?)" +msgstr "" + +#: keystone/common/openssl.py:93 +#, python-format +msgid "Command %(to_exec)s exited with %(retcode)s- %(output)s" +msgstr "" + +#: keystone/common/openssl.py:121 +#, python-format +msgid "Failed to remove file %(file_path)r: %(error)s" +msgstr "" + +#: keystone/common/utils.py:239 +msgid "" +"Error setting up the debug environment. Verify that the option --debug-url " +"has the format : and that a debugger processes is listening on " +"that port." +msgstr "" +"Erro configurando o ambiente de debug. Verifique que a opção --debug-url " +"possui o formato : e que o processo debugger está escutando " +"nesta porta." + +#: keystone/common/cache/core.py:100 +#, python-format +msgid "" +"Unable to build cache config-key. Expected format \":\". " +"Skipping unknown format: %s" +msgstr "" +"Não é possível construir chave de configuração do cache. Formato esperado " +"\":\". Pulando formato desconhecido: %s" + +#: keystone/common/environment/eventlet_server.py:99 +#, python-format +msgid "Could not bind to %(host)s:%(port)s" +msgstr "" + +#: keystone/common/environment/eventlet_server.py:185 +msgid "Server error" +msgstr "Erro do servidor" + +#: keystone/contrib/endpoint_policy/core.py:129 +#: keystone/contrib/endpoint_policy/core.py:228 +#, python-format +msgid "" +"Circular reference or a repeated entry found in region tree - %(region_id)s." +msgstr "" + +#: keystone/contrib/federation/idp.py:410 +#, python-format +msgid "Error when signing assertion, reason: %(reason)s" +msgstr "" + +#: keystone/contrib/oauth1/core.py:136 +msgid "Cannot retrieve Authorization headers" +msgstr "" + +#: keystone/openstack/common/loopingcall.py:95 +msgid "in fixed duration looping call" +msgstr "em uma chamada de laço de duração fixa" + +#: keystone/openstack/common/loopingcall.py:138 +msgid "in dynamic looping call" +msgstr "em chamada de laço dinâmico" + +#: keystone/openstack/common/service.py:268 +msgid "Unhandled exception" +msgstr "Exceção não tratada" + +#: keystone/resource/core.py:477 +#, python-format +msgid "" +"Circular reference or a repeated entry found projects hierarchy - " +"%(project_id)s." +msgstr "" + +#: keystone/resource/core.py:939 +#, python-format +msgid "" +"Unexpected results in response for domain config - %(count)s responses, " +"first option is %(option)s, expected option %(expected)s" +msgstr "" + +#: keystone/resource/backends/sql.py:102 keystone/resource/backends/sql.py:121 +#, python-format +msgid "" +"Circular reference or a repeated entry found in projects hierarchy - " +"%(project_id)s." +msgstr "" + +#: keystone/token/provider.py:292 +#, python-format +msgid "Unexpected error or malformed token determining token expiry: %s" +msgstr "" +"Erro inesperado ou token mal formado ao determinar validade do token: %s" + +#: keystone/token/persistence/backends/kvs.py:226 +#, python-format +msgid "" +"Reinitializing revocation list due to error in loading revocation list from " +"backend. Expected `list` type got `%(type)s`. Old revocation list data: " +"%(list)r" +msgstr "" + +#: keystone/token/providers/common.py:611 +msgid "Failed to validate token" +msgstr "Falha ao validar token" + +#: keystone/token/providers/pki.py:47 +msgid "Unable to sign token" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:38 +#, python-format +msgid "" +"Either [fernet_tokens] key_repository does not exist or Keystone does not " +"have sufficient permission to access it: %s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:79 +msgid "" +"Failed to create [fernet_tokens] key_repository: either it already exists or " +"you don't have sufficient permissions to create it" +msgstr "" diff --git a/keystone-moon/keystone/locale/pt_BR/LC_MESSAGES/keystone.po b/keystone-moon/keystone/locale/pt_BR/LC_MESSAGES/keystone.po new file mode 100644 index 00000000..fdb771c9 --- /dev/null +++ b/keystone-moon/keystone/locale/pt_BR/LC_MESSAGES/keystone.po @@ -0,0 +1,1546 @@ +# Portuguese (Brazil) translations for keystone. +# Copyright (C) 2015 OpenStack Foundation +# This file is distributed under the same license as the keystone project. +# +# Translators: +# Gabriel Wainer, 2013 +# Lucas Ribeiro , 2014 +# Volmar Oliveira Junior , 2013 +msgid "" +msgstr "" +"Project-Id-Version: Keystone\n" +"Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" +"POT-Creation-Date: 2015-03-23 06:04+0000\n" +"PO-Revision-Date: 2015-03-21 23:03+0000\n" +"Last-Translator: openstackjenkins \n" +"Language-Team: Portuguese (Brazil) " +"(http://www.transifex.com/projects/p/keystone/language/pt_BR/)\n" +"Plural-Forms: nplurals=2; plural=(n > 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 1.3\n" + +#: keystone/clean.py:24 +#, python-format +msgid "%s cannot be empty." +msgstr "%s não pode estar vazio." + +#: keystone/clean.py:26 +#, python-format +msgid "%(property_name)s cannot be less than %(min_length)s characters." +msgstr "%(property_name)s não pode ter menos de %(min_length)s caracteres." + +#: keystone/clean.py:31 +#, python-format +msgid "%(property_name)s should not be greater than %(max_length)s characters." +msgstr "%(property_name)s não deve ter mais de %(max_length)s caracteres." + +#: keystone/clean.py:40 +#, python-format +msgid "%(property_name)s is not a %(display_expected_type)s" +msgstr "%(property_name)s não é um %(display_expected_type)s" + +#: keystone/cli.py:283 +msgid "At least one option must be provided" +msgstr "" + +#: keystone/cli.py:290 +msgid "--all option cannot be mixed with other options" +msgstr "" + +#: keystone/cli.py:301 +#, python-format +msgid "Unknown domain '%(name)s' specified by --domain-name" +msgstr "" + +#: keystone/cli.py:365 keystone/tests/unit/test_cli.py:213 +msgid "At least one option must be provided, use either --all or --domain-name" +msgstr "" + +#: keystone/cli.py:371 keystone/tests/unit/test_cli.py:229 +msgid "The --all option cannot be used with the --domain-name option" +msgstr "" + +#: keystone/cli.py:397 keystone/tests/unit/test_cli.py:246 +#, python-format +msgid "" +"Invalid domain name: %(domain)s found in config file name: %(file)s - " +"ignoring this file." +msgstr "" + +#: keystone/cli.py:405 keystone/tests/unit/test_cli.py:187 +#, python-format +msgid "" +"Domain: %(domain)s already has a configuration defined - ignoring file: " +"%(file)s." +msgstr "" + +#: keystone/cli.py:419 +#, python-format +msgid "Error parsing configuration file for domain: %(domain)s, file: %(file)s." +msgstr "" + +#: keystone/cli.py:452 +#, python-format +msgid "" +"To get a more detailed information on this error, re-run this command for" +" the specific domain, i.e.: keystone-manage domain_config_upload " +"--domain-name %s" +msgstr "" + +#: keystone/cli.py:470 +#, python-format +msgid "Unable to locate domain config directory: %s" +msgstr "Não é possível localizar diretório de configuração de domínio: %s" + +#: keystone/cli.py:503 +msgid "" +"Unable to access the keystone database, please check it is configured " +"correctly." +msgstr "" + +#: keystone/exception.py:79 +#, python-format +msgid "" +"Expecting to find %(attribute)s in %(target)s - the server could not " +"comply with the request since it is either malformed or otherwise " +"incorrect. The client is assumed to be in error." +msgstr "" + +#: keystone/exception.py:90 +#, python-format +msgid "%(detail)s" +msgstr "" + +#: keystone/exception.py:94 +msgid "" +"Timestamp not in expected format. The server could not comply with the " +"request since it is either malformed or otherwise incorrect. The client " +"is assumed to be in error." +msgstr "" +"A data não está no formato especificado. O servidor não pôde realizar a " +"requisição pois ela está mal formada ou incorreta. Assume-se que o " +"cliente está com erro." + +#: keystone/exception.py:103 +#, python-format +msgid "" +"String length exceeded.The length of string '%(string)s' exceeded the " +"limit of column %(type)s(CHAR(%(length)d))." +msgstr "" +"Comprimento de string excedido. O comprimento de string '%(string)s' " +"excedeu o limite da coluna %(type)s(CHAR(%(length)d))." + +#: keystone/exception.py:109 +#, python-format +msgid "" +"Request attribute %(attribute)s must be less than or equal to %(size)i. " +"The server could not comply with the request because the attribute size " +"is invalid (too large). The client is assumed to be in error." +msgstr "" +"Atributo de requisição %(attribute)s deve ser menor ou igual a %(size)i. " +"O servidor não pôde atender a requisição porque o tamanho do atributo é " +"inválido (muito grande). Assume-se que o cliente está em erro." + +#: keystone/exception.py:119 +#, python-format +msgid "" +"The specified parent region %(parent_region_id)s would create a circular " +"region hierarchy." +msgstr "" + +#: keystone/exception.py:126 +#, python-format +msgid "" +"The password length must be less than or equal to %(size)i. The server " +"could not comply with the request because the password is invalid." +msgstr "" + +#: keystone/exception.py:134 +#, python-format +msgid "" +"Unable to delete region %(region_id)s because it or its child regions " +"have associated endpoints." +msgstr "" + +#: keystone/exception.py:141 +msgid "" +"The certificates you requested are not available. It is likely that this " +"server does not use PKI tokens otherwise this is the result of " +"misconfiguration." +msgstr "" + +#: keystone/exception.py:150 +msgid "(Disable debug mode to suppress these details.)" +msgstr "" + +#: keystone/exception.py:155 +#, python-format +msgid "%(message)s %(amendment)s" +msgstr "" + +#: keystone/exception.py:163 +msgid "The request you have made requires authentication." +msgstr "A requisição que você fez requer autenticação." + +#: keystone/exception.py:169 +msgid "Authentication plugin error." +msgstr "Erro do plugin de autenticação." + +#: keystone/exception.py:177 +#, python-format +msgid "Unable to find valid groups while using mapping %(mapping_id)s" +msgstr "" + +#: keystone/exception.py:182 +msgid "Attempted to authenticate with an unsupported method." +msgstr "Tentativa de autenticação com um método não suportado." + +#: keystone/exception.py:190 +msgid "Additional authentications steps required." +msgstr "Passos de autenticação adicionais requeridos." + +#: keystone/exception.py:198 +msgid "You are not authorized to perform the requested action." +msgstr "Você não está autorizado à realizar a ação solicitada." + +#: keystone/exception.py:205 +#, python-format +msgid "You are not authorized to perform the requested action: %(action)s" +msgstr "" + +#: keystone/exception.py:210 +#, python-format +msgid "" +"Could not change immutable attribute(s) '%(attributes)s' in target " +"%(target)s" +msgstr "" + +#: keystone/exception.py:215 +#, python-format +msgid "" +"Group membership across backend boundaries is not allowed, group in " +"question is %(group_id)s, user is %(user_id)s" +msgstr "" + +#: keystone/exception.py:221 +#, python-format +msgid "" +"Invalid mix of entities for policy association - only Endpoint, Service " +"or Region+Service allowed. Request was - Endpoint: %(endpoint_id)s, " +"Service: %(service_id)s, Region: %(region_id)s" +msgstr "" + +#: keystone/exception.py:228 +#, python-format +msgid "Invalid domain specific configuration: %(reason)s" +msgstr "" + +#: keystone/exception.py:232 +#, python-format +msgid "Could not find: %(target)s" +msgstr "" + +#: keystone/exception.py:238 +#, python-format +msgid "Could not find endpoint: %(endpoint_id)s" +msgstr "" + +#: keystone/exception.py:245 +msgid "An unhandled exception has occurred: Could not find metadata." +msgstr "Uma exceção não tratada ocorreu: Não foi possível encontrar metadados." + +#: keystone/exception.py:250 +#, python-format +msgid "Could not find policy: %(policy_id)s" +msgstr "" + +#: keystone/exception.py:254 +msgid "Could not find policy association" +msgstr "" + +#: keystone/exception.py:258 +#, python-format +msgid "Could not find role: %(role_id)s" +msgstr "" + +#: keystone/exception.py:262 +#, python-format +msgid "" +"Could not find role assignment with role: %(role_id)s, user or group: " +"%(actor_id)s, project or domain: %(target_id)s" +msgstr "" + +#: keystone/exception.py:268 +#, python-format +msgid "Could not find region: %(region_id)s" +msgstr "" + +#: keystone/exception.py:272 +#, python-format +msgid "Could not find service: %(service_id)s" +msgstr "" + +#: keystone/exception.py:276 +#, python-format +msgid "Could not find domain: %(domain_id)s" +msgstr "" + +#: keystone/exception.py:280 +#, python-format +msgid "Could not find project: %(project_id)s" +msgstr "" + +#: keystone/exception.py:284 +#, python-format +msgid "Cannot create project with parent: %(project_id)s" +msgstr "" + +#: keystone/exception.py:288 +#, python-format +msgid "Could not find token: %(token_id)s" +msgstr "" + +#: keystone/exception.py:292 +#, python-format +msgid "Could not find user: %(user_id)s" +msgstr "" + +#: keystone/exception.py:296 +#, python-format +msgid "Could not find group: %(group_id)s" +msgstr "" + +#: keystone/exception.py:300 +#, python-format +msgid "Could not find mapping: %(mapping_id)s" +msgstr "" + +#: keystone/exception.py:304 +#, python-format +msgid "Could not find trust: %(trust_id)s" +msgstr "" + +#: keystone/exception.py:308 +#, python-format +msgid "No remaining uses for trust: %(trust_id)s" +msgstr "" + +#: keystone/exception.py:312 +#, python-format +msgid "Could not find credential: %(credential_id)s" +msgstr "" + +#: keystone/exception.py:316 +#, python-format +msgid "Could not find version: %(version)s" +msgstr "" + +#: keystone/exception.py:320 +#, python-format +msgid "Could not find Endpoint Group: %(endpoint_group_id)s" +msgstr "" + +#: keystone/exception.py:324 +#, python-format +msgid "Could not find Identity Provider: %(idp_id)s" +msgstr "" + +#: keystone/exception.py:328 +#, python-format +msgid "Could not find Service Provider: %(sp_id)s" +msgstr "" + +#: keystone/exception.py:332 +#, python-format +msgid "" +"Could not find federated protocol %(protocol_id)s for Identity Provider: " +"%(idp_id)s" +msgstr "" + +#: keystone/exception.py:343 +#, python-format +msgid "" +"Could not find %(group_or_option)s in domain configuration for domain " +"%(domain_id)s" +msgstr "" + +#: keystone/exception.py:348 +#, python-format +msgid "Conflict occurred attempting to store %(type)s - %(details)s" +msgstr "" + +#: keystone/exception.py:356 +msgid "An unexpected error prevented the server from fulfilling your request." +msgstr "" + +#: keystone/exception.py:359 +#, python-format +msgid "" +"An unexpected error prevented the server from fulfilling your request: " +"%(exception)s" +msgstr "" + +#: keystone/exception.py:382 +#, python-format +msgid "Unable to consume trust %(trust_id)s, unable to acquire lock." +msgstr "" + +#: keystone/exception.py:387 +msgid "" +"Expected signing certificates are not available on the server. Please " +"check Keystone configuration." +msgstr "" + +#: keystone/exception.py:393 +#, python-format +msgid "Malformed endpoint URL (%(endpoint)s), see ERROR log for details." +msgstr "" +"URL de endpoint mal-formada (%(endpoint)s), veja o log de ERROS para " +"detalhes." + +#: keystone/exception.py:398 +#, python-format +msgid "" +"Group %(group_id)s returned by mapping %(mapping_id)s was not found in " +"the backend." +msgstr "" + +#: keystone/exception.py:403 +#, python-format +msgid "Error while reading metadata file, %(reason)s" +msgstr "" + +#: keystone/exception.py:407 +#, python-format +msgid "" +"Unexpected combination of grant attributes - User: %(user_id)s, Group: " +"%(group_id)s, Project: %(project_id)s, Domain: %(domain_id)s" +msgstr "" + +#: keystone/exception.py:414 +msgid "The action you have requested has not been implemented." +msgstr "A ação que você solicitou não foi implementada." + +#: keystone/exception.py:421 +msgid "The service you have requested is no longer available on this server." +msgstr "" + +#: keystone/exception.py:428 +#, python-format +msgid "The Keystone configuration file %(config_file)s could not be found." +msgstr "" + +#: keystone/exception.py:433 +msgid "" +"No encryption keys found; run keystone-manage fernet_setup to bootstrap " +"one." +msgstr "" + +#: keystone/exception.py:438 +#, python-format +msgid "" +"The Keystone domain-specific configuration has specified more than one " +"SQL driver (only one is permitted): %(source)s." +msgstr "" + +#: keystone/exception.py:445 +#, python-format +msgid "" +"%(mod_name)s doesn't provide database migrations. The migration " +"repository path at %(path)s doesn't exist or isn't a directory." +msgstr "" + +#: keystone/exception.py:457 +#, python-format +msgid "" +"Unable to sign SAML assertion. It is likely that this server does not " +"have xmlsec1 installed, or this is the result of misconfiguration. Reason" +" %(reason)s" +msgstr "" + +#: keystone/exception.py:465 +msgid "" +"No Authorization headers found, cannot proceed with OAuth related calls, " +"if running under HTTPd or Apache, ensure WSGIPassAuthorization is set to " +"On." +msgstr "" + +#: keystone/notifications.py:250 +#, python-format +msgid "%(event)s is not a valid notification event, must be one of: %(actions)s" +msgstr "" + +#: keystone/notifications.py:259 +#, python-format +msgid "Method not callable: %s" +msgstr "" + +#: keystone/assignment/controllers.py:107 keystone/identity/controllers.py:69 +#: keystone/resource/controllers.py:78 +msgid "Name field is required and cannot be empty" +msgstr "Campo nome é requerido e não pode ser vazio" + +#: keystone/assignment/controllers.py:330 +#: keystone/assignment/controllers.py:753 +msgid "Specify a domain or project, not both" +msgstr "Especifique um domínio ou projeto, não ambos" + +#: keystone/assignment/controllers.py:333 +msgid "Specify one of domain or project" +msgstr "" + +#: keystone/assignment/controllers.py:338 +#: keystone/assignment/controllers.py:758 +msgid "Specify a user or group, not both" +msgstr "Epecifique um usuário ou grupo, não ambos" + +#: keystone/assignment/controllers.py:341 +msgid "Specify one of user or group" +msgstr "" + +#: keystone/assignment/controllers.py:742 +msgid "Combining effective and group filter will always result in an empty list." +msgstr "" + +#: keystone/assignment/controllers.py:747 +msgid "" +"Combining effective, domain and inherited filters will always result in " +"an empty list." +msgstr "" + +#: keystone/assignment/core.py:228 +msgid "Must specify either domain or project" +msgstr "" + +#: keystone/assignment/core.py:493 +#, python-format +msgid "Project (%s)" +msgstr "Projeto (%s)" + +#: keystone/assignment/core.py:495 +#, python-format +msgid "Domain (%s)" +msgstr "Domínio (%s)" + +#: keystone/assignment/core.py:497 +msgid "Unknown Target" +msgstr "Alvo Desconhecido" + +#: keystone/assignment/backends/ldap.py:92 +msgid "Domain metadata not supported by LDAP" +msgstr "" + +#: keystone/assignment/backends/ldap.py:381 +#, python-format +msgid "User %(user_id)s already has role %(role_id)s in tenant %(tenant_id)s" +msgstr "" + +#: keystone/assignment/backends/ldap.py:387 +#, python-format +msgid "Role %s not found" +msgstr "Role %s não localizada" + +#: keystone/assignment/backends/ldap.py:402 +#: keystone/assignment/backends/sql.py:335 +#, python-format +msgid "Cannot remove role that has not been granted, %s" +msgstr "Não é possível remover role que não foi concedido, %s" + +#: keystone/assignment/backends/sql.py:356 +#, python-format +msgid "Unexpected assignment type encountered, %s" +msgstr "" + +#: keystone/assignment/role_backends/ldap.py:61 keystone/catalog/core.py:103 +#: keystone/common/ldap/core.py:1400 keystone/resource/backends/ldap.py:149 +#, python-format +msgid "Duplicate ID, %s." +msgstr "ID duplicado, %s." + +#: keystone/assignment/role_backends/ldap.py:69 +#: keystone/common/ldap/core.py:1390 +#, python-format +msgid "Duplicate name, %s." +msgstr "Nome duplicado, %s." + +#: keystone/assignment/role_backends/ldap.py:119 +#, python-format +msgid "Cannot duplicate name %s" +msgstr "" + +#: keystone/auth/controllers.py:60 +#, python-format +msgid "" +"Cannot load an auth-plugin by class-name without a \"method\" attribute " +"defined: %s" +msgstr "" + +#: keystone/auth/controllers.py:71 +#, python-format +msgid "" +"Auth plugin %(plugin)s is requesting previously registered method " +"%(method)s" +msgstr "" + +#: keystone/auth/controllers.py:115 +#, python-format +msgid "" +"Unable to reconcile identity attribute %(attribute)s as it has " +"conflicting values %(new)s and %(old)s" +msgstr "" + +#: keystone/auth/controllers.py:336 +msgid "Scoping to both domain and project is not allowed" +msgstr "A definição de escopo para o domínio e o projeto não é permitida" + +#: keystone/auth/controllers.py:339 +msgid "Scoping to both domain and trust is not allowed" +msgstr "A definição de escopo para o domínio e a trust não é permitida" + +#: keystone/auth/controllers.py:342 +msgid "Scoping to both project and trust is not allowed" +msgstr "A definição de escopo para o projeto e a trust não é permitida" + +#: keystone/auth/controllers.py:512 +msgid "User not found" +msgstr "Usuário não localizado" + +#: keystone/auth/controllers.py:616 +msgid "A project-scoped token is required to produce a service catalog." +msgstr "" + +#: keystone/auth/plugins/external.py:46 +msgid "No authenticated user" +msgstr "Nenhum usuário autenticado" + +#: keystone/auth/plugins/external.py:56 +#, python-format +msgid "Unable to lookup user %s" +msgstr "Não é possível consultar o usuário %s" + +#: keystone/auth/plugins/external.py:107 +msgid "auth_type is not Negotiate" +msgstr "" + +#: keystone/auth/plugins/mapped.py:244 +msgid "Could not map user" +msgstr "" + +#: keystone/auth/plugins/oauth1.py:39 +#, python-format +msgid "%s not supported" +msgstr "" + +#: keystone/auth/plugins/oauth1.py:57 +msgid "Access token is expired" +msgstr "Token de acesso expirou" + +#: keystone/auth/plugins/oauth1.py:71 +msgid "Could not validate the access token" +msgstr "" + +#: keystone/auth/plugins/password.py:46 +msgid "Invalid username or password" +msgstr "Nome de usuário ou senha inválidos" + +#: keystone/auth/plugins/token.py:72 keystone/token/controllers.py:160 +msgid "rescope a scoped token" +msgstr "" + +#: keystone/catalog/controllers.py:168 +#, python-format +msgid "Conflicting region IDs specified: \"%(url_id)s\" != \"%(ref_id)s\"" +msgstr "" + +#: keystone/common/authorization.py:47 keystone/common/wsgi.py:64 +#, python-format +msgid "token reference must be a KeystoneToken type, got: %s" +msgstr "" + +#: keystone/common/base64utils.py:66 +msgid "pad must be single character" +msgstr "" + +#: keystone/common/base64utils.py:215 +#, python-format +msgid "text is multiple of 4, but pad \"%s\" occurs before 2nd to last char" +msgstr "" + +#: keystone/common/base64utils.py:219 +#, python-format +msgid "text is multiple of 4, but pad \"%s\" occurs before non-pad last char" +msgstr "" + +#: keystone/common/base64utils.py:225 +#, python-format +msgid "text is not a multiple of 4, but contains pad \"%s\"" +msgstr "" + +#: keystone/common/base64utils.py:244 keystone/common/base64utils.py:265 +msgid "padded base64url text must be multiple of 4 characters" +msgstr "" + +#: keystone/common/controller.py:237 keystone/token/providers/common.py:589 +msgid "Non-default domain is not supported" +msgstr "O domínio não padrão não é suportado" + +#: keystone/common/controller.py:305 keystone/identity/core.py:428 +#: keystone/resource/core.py:761 keystone/resource/backends/ldap.py:61 +#, python-format +msgid "Expected dict or list: %s" +msgstr "Esperado dict ou list: %s" + +#: keystone/common/controller.py:318 +msgid "Marker could not be found" +msgstr "Marcador não pôde ser encontrado" + +#: keystone/common/controller.py:329 +msgid "Invalid limit value" +msgstr "Valor limite inválido" + +#: keystone/common/controller.py:637 +msgid "Cannot change Domain ID" +msgstr "" + +#: keystone/common/controller.py:666 +msgid "domain_id is required as part of entity" +msgstr "" + +#: keystone/common/controller.py:701 +msgid "A domain-scoped token must be used" +msgstr "" + +#: keystone/common/dependency.py:68 +#, python-format +msgid "Unregistered dependency: %(name)s for %(targets)s" +msgstr "" + +#: keystone/common/dependency.py:108 +msgid "event_callbacks must be a dict" +msgstr "" + +#: keystone/common/dependency.py:113 +#, python-format +msgid "event_callbacks[%s] must be a dict" +msgstr "" + +#: keystone/common/pemutils.py:223 +#, python-format +msgid "unknown pem_type \"%(pem_type)s\", valid types are: %(valid_pem_types)s" +msgstr "" + +#: keystone/common/pemutils.py:242 +#, python-format +msgid "" +"unknown pem header \"%(pem_header)s\", valid headers are: " +"%(valid_pem_headers)s" +msgstr "" + +#: keystone/common/pemutils.py:298 +#, python-format +msgid "failed to find end matching \"%s\"" +msgstr "" + +#: keystone/common/pemutils.py:302 +#, python-format +msgid "" +"beginning & end PEM headers do not match (%(begin_pem_header)s!= " +"%(end_pem_header)s)" +msgstr "" + +#: keystone/common/pemutils.py:377 +#, python-format +msgid "unknown pem_type: \"%s\"" +msgstr "" + +#: keystone/common/pemutils.py:389 +#, python-format +msgid "" +"failed to base64 decode %(pem_type)s PEM at position%(position)d: " +"%(err_msg)s" +msgstr "" + +#: keystone/common/utils.py:164 keystone/credential/controllers.py:44 +msgid "Invalid blob in credential" +msgstr "BLOB inválido na credencial" + +#: keystone/common/wsgi.py:330 +#, python-format +msgid "%s field is required and cannot be empty" +msgstr "" + +#: keystone/common/wsgi.py:342 +#, python-format +msgid "%s field(s) cannot be empty" +msgstr "" + +#: keystone/common/wsgi.py:563 +msgid "The resource could not be found." +msgstr "O recurso não pôde ser localizado." + +#: keystone/common/wsgi.py:704 +#, python-format +msgid "Unexpected status requested for JSON Home response, %s" +msgstr "" + +#: keystone/common/cache/_memcache_pool.py:113 +#, python-format +msgid "Unable to get a connection from pool id %(id)s after %(seconds)s seconds." +msgstr "" + +#: keystone/common/cache/core.py:132 +msgid "region not type dogpile.cache.CacheRegion" +msgstr "região não é do tipo dogpile.cache.CacheRegion" + +#: keystone/common/cache/backends/mongo.py:231 +msgid "db_hosts value is required" +msgstr "" + +#: keystone/common/cache/backends/mongo.py:236 +msgid "database db_name is required" +msgstr "" + +#: keystone/common/cache/backends/mongo.py:241 +msgid "cache_collection name is required" +msgstr "" + +#: keystone/common/cache/backends/mongo.py:252 +msgid "integer value expected for w (write concern attribute)" +msgstr "" + +#: keystone/common/cache/backends/mongo.py:260 +msgid "replicaset_name required when use_replica is True" +msgstr "" + +#: keystone/common/cache/backends/mongo.py:275 +msgid "integer value expected for mongo_ttl_seconds" +msgstr "" + +#: keystone/common/cache/backends/mongo.py:301 +msgid "no ssl support available" +msgstr "" + +#: keystone/common/cache/backends/mongo.py:310 +#, python-format +msgid "" +"Invalid ssl_cert_reqs value of %s, must be one of \"NONE\", \"OPTIONAL\"," +" \"REQUIRED\"" +msgstr "" + +#: keystone/common/kvs/core.py:71 +#, python-format +msgid "Lock Timeout occurred for key, %(target)s" +msgstr "" + +#: keystone/common/kvs/core.py:106 +#, python-format +msgid "KVS region %s is already configured. Cannot reconfigure." +msgstr "" + +#: keystone/common/kvs/core.py:145 +#, python-format +msgid "Key Value Store not configured: %s" +msgstr "" + +#: keystone/common/kvs/core.py:198 +msgid "`key_mangler` option must be a function reference" +msgstr "" + +#: keystone/common/kvs/core.py:353 +#, python-format +msgid "Lock key must match target key: %(lock)s != %(target)s" +msgstr "" + +#: keystone/common/kvs/core.py:357 +msgid "Must be called within an active lock context." +msgstr "" + +#: keystone/common/kvs/backends/memcached.py:69 +#, python-format +msgid "Maximum lock attempts on %s occurred." +msgstr "" + +#: keystone/common/kvs/backends/memcached.py:108 +#, python-format +msgid "" +"Backend `%(driver)s` is not a valid memcached backend. Valid drivers: " +"%(driver_list)s" +msgstr "" + +#: keystone/common/kvs/backends/memcached.py:178 +msgid "`key_mangler` functions must be callable." +msgstr "" + +#: keystone/common/ldap/core.py:191 +#, python-format +msgid "Invalid LDAP deref option: %(option)s. Choose one of: %(options)s" +msgstr "" + +#: keystone/common/ldap/core.py:201 +#, python-format +msgid "Invalid LDAP TLS certs option: %(option)s. Choose one of: %(options)s" +msgstr "" +"Opção de certificado LADP TLS inválida: %(option)s. Escolha uma de: " +"%(options)s" + +#: keystone/common/ldap/core.py:213 +#, python-format +msgid "Invalid LDAP scope: %(scope)s. Choose one of: %(options)s" +msgstr "Escopo LDAP inválido: %(scope)s. Escolha um de: %(options)s" + +#: keystone/common/ldap/core.py:588 +msgid "Invalid TLS / LDAPS combination" +msgstr "Combinação TLS / LADPS inválida" + +#: keystone/common/ldap/core.py:593 +#, python-format +msgid "Invalid LDAP TLS_AVAIL option: %s. TLS not available" +msgstr "Opção LDAP TLS_AVAIL inválida: %s. TLS não dsponível" + +#: keystone/common/ldap/core.py:603 +#, python-format +msgid "tls_cacertfile %s not found or is not a file" +msgstr "tls_cacertfile %s não encontrada ou não é um arquivo" + +#: keystone/common/ldap/core.py:615 +#, python-format +msgid "tls_cacertdir %s not found or is not a directory" +msgstr "tls_cacertdir %s não encontrado ou não é um diretório" + +#: keystone/common/ldap/core.py:1325 +#, python-format +msgid "ID attribute %(id_attr)s not found in LDAP object %(dn)s" +msgstr "" + +#: keystone/common/ldap/core.py:1369 +#, python-format +msgid "LDAP %s create" +msgstr "Criação de LDAP %s" + +#: keystone/common/ldap/core.py:1374 +#, python-format +msgid "LDAP %s update" +msgstr "Atualização de LDAP %s" + +#: keystone/common/ldap/core.py:1379 +#, python-format +msgid "LDAP %s delete" +msgstr "Exclusão de LDAP %s" + +#: keystone/common/ldap/core.py:1521 +msgid "" +"Disabling an entity where the 'enable' attribute is ignored by " +"configuration." +msgstr "" + +#: keystone/common/ldap/core.py:1532 +#, python-format +msgid "Cannot change %(option_name)s %(attr)s" +msgstr "Não é possível alterar %(option_name)s %(attr)s" + +#: keystone/common/ldap/core.py:1619 +#, python-format +msgid "Member %(member)s is already a member of group %(group)s" +msgstr "" + +#: keystone/common/sql/core.py:219 +msgid "" +"Cannot truncate a driver call without hints list as first parameter after" +" self " +msgstr "" + +#: keystone/common/sql/core.py:410 +msgid "Duplicate Entry" +msgstr "" + +#: keystone/common/sql/core.py:426 +#, python-format +msgid "An unexpected error occurred when trying to store %s" +msgstr "" + +#: keystone/common/sql/migration_helpers.py:187 +#: keystone/common/sql/migration_helpers.py:245 +#, python-format +msgid "%s extension does not exist." +msgstr "" + +#: keystone/common/validation/validators.py:54 +#, python-format +msgid "Invalid input for field '%(path)s'. The value is '%(value)s'." +msgstr "" + +#: keystone/contrib/ec2/controllers.py:318 +msgid "Token belongs to another user" +msgstr "O token pertence à outro usuário" + +#: keystone/contrib/ec2/controllers.py:346 +msgid "Credential belongs to another user" +msgstr "A credencial pertence à outro usuário" + +#: keystone/contrib/endpoint_filter/backends/sql.py:69 +#, python-format +msgid "Endpoint %(endpoint_id)s not found in project %(project_id)s" +msgstr "Endpoint %(endpoint_id)s não encontrado no projeto %(project_id)s" + +#: keystone/contrib/endpoint_filter/backends/sql.py:180 +msgid "Endpoint Group Project Association not found" +msgstr "" + +#: keystone/contrib/endpoint_policy/core.py:258 +#, python-format +msgid "No policy is associated with endpoint %(endpoint_id)s." +msgstr "" + +#: keystone/contrib/federation/controllers.py:274 +msgid "Missing entity ID from environment" +msgstr "" + +#: keystone/contrib/federation/controllers.py:282 +msgid "Request must have an origin query parameter" +msgstr "" + +#: keystone/contrib/federation/controllers.py:292 +#, python-format +msgid "%(host)s is not a trusted dashboard host" +msgstr "" + +#: keystone/contrib/federation/controllers.py:333 +msgid "Use a project scoped token when attempting to create a SAML assertion" +msgstr "" + +#: keystone/contrib/federation/idp.py:454 +#, python-format +msgid "Cannot open certificate %(cert_file)s. Reason: %(reason)s" +msgstr "" + +#: keystone/contrib/federation/idp.py:521 +msgid "Ensure configuration option idp_entity_id is set." +msgstr "" + +#: keystone/contrib/federation/idp.py:524 +msgid "Ensure configuration option idp_sso_endpoint is set." +msgstr "" + +#: keystone/contrib/federation/idp.py:544 +msgid "" +"idp_contact_type must be one of: [technical, other, support, " +"administrative or billing." +msgstr "" + +#: keystone/contrib/federation/utils.py:178 +msgid "Federation token is expired" +msgstr "" + +#: keystone/contrib/federation/utils.py:208 +msgid "" +"Could not find Identity Provider identifier in environment, check " +"[federation] remote_id_attribute for details." +msgstr "" + +#: keystone/contrib/federation/utils.py:213 +msgid "" +"Incoming identity provider identifier not included among the accepted " +"identifiers." +msgstr "" + +#: keystone/contrib/federation/utils.py:501 +#, python-format +msgid "User type %s not supported" +msgstr "" + +#: keystone/contrib/federation/utils.py:537 +#, python-format +msgid "" +"Invalid rule: %(identity_value)s. Both 'groups' and 'domain' keywords " +"must be specified." +msgstr "" + +#: keystone/contrib/federation/utils.py:753 +#, python-format +msgid "Identity Provider %(idp)s is disabled" +msgstr "" + +#: keystone/contrib/federation/utils.py:761 +#, python-format +msgid "Service Provider %(sp)s is disabled" +msgstr "" + +#: keystone/contrib/oauth1/controllers.py:99 +msgid "Cannot change consumer secret" +msgstr "Não é possível alterar segredo do consumidor" + +#: keystone/contrib/oauth1/controllers.py:131 +msgid "Cannot list request tokens with a token issued via delegation." +msgstr "" + +#: keystone/contrib/oauth1/controllers.py:192 +#: keystone/contrib/oauth1/backends/sql.py:270 +msgid "User IDs do not match" +msgstr "ID de usuário não confere" + +#: keystone/contrib/oauth1/controllers.py:199 +msgid "Could not find role" +msgstr "Não é possível encontrar role" + +#: keystone/contrib/oauth1/controllers.py:248 +msgid "Invalid signature" +msgstr "" + +#: keystone/contrib/oauth1/controllers.py:299 +#: keystone/contrib/oauth1/controllers.py:377 +msgid "Request token is expired" +msgstr "Token de requisição expirou" + +#: keystone/contrib/oauth1/controllers.py:313 +msgid "There should not be any non-oauth parameters" +msgstr "Não deve haver nenhum parâmetro não oauth" + +#: keystone/contrib/oauth1/controllers.py:317 +msgid "provided consumer key does not match stored consumer key" +msgstr "" +"Chave de consumidor fornecida não confere com a chave de consumidor " +"armazenada" + +#: keystone/contrib/oauth1/controllers.py:321 +msgid "provided verifier does not match stored verifier" +msgstr "Verificador fornecido não confere com o verificador armazenado" + +#: keystone/contrib/oauth1/controllers.py:325 +msgid "provided request key does not match stored request key" +msgstr "" +"Chave de requisição do provedor não confere com a chave de requisição " +"armazenada" + +#: keystone/contrib/oauth1/controllers.py:329 +msgid "Request Token does not have an authorizing user id" +msgstr "Token de Requisição não possui um ID de usuário autorizado" + +#: keystone/contrib/oauth1/controllers.py:366 +msgid "Cannot authorize a request token with a token issued via delegation." +msgstr "" + +#: keystone/contrib/oauth1/controllers.py:396 +msgid "authorizing user does not have role required" +msgstr "Usuário autorizado não possui o role necessário" + +#: keystone/contrib/oauth1/controllers.py:409 +msgid "User is not a member of the requested project" +msgstr "Usuário não é um membro do projeto requisitado" + +#: keystone/contrib/oauth1/backends/sql.py:91 +msgid "Consumer not found" +msgstr "Consumidor não encontrado" + +#: keystone/contrib/oauth1/backends/sql.py:186 +msgid "Request token not found" +msgstr "Token de requisição não encontrado" + +#: keystone/contrib/oauth1/backends/sql.py:250 +msgid "Access token not found" +msgstr "Token de acesso não encontrado" + +#: keystone/contrib/revoke/controllers.py:33 +#, python-format +msgid "invalid date format %s" +msgstr "" + +#: keystone/contrib/revoke/core.py:150 +msgid "" +"The revoke call must not have both domain_id and project_id. This is a " +"bug in the Keystone server. The current request is aborted." +msgstr "" + +#: keystone/contrib/revoke/core.py:218 keystone/token/provider.py:207 +#: keystone/token/provider.py:230 keystone/token/provider.py:296 +#: keystone/token/provider.py:303 +msgid "Failed to validate token" +msgstr "Falha ao validar token" + +#: keystone/identity/controllers.py:72 +msgid "Enabled field must be a boolean" +msgstr "Campo habilitado precisa ser um booleano" + +#: keystone/identity/controllers.py:98 +msgid "Enabled field should be a boolean" +msgstr "Campo habilitado deve ser um booleano" + +#: keystone/identity/core.py:112 +#, python-format +msgid "Database at /domains/%s/config" +msgstr "" + +#: keystone/identity/core.py:287 keystone/identity/backends/ldap.py:59 +#: keystone/identity/backends/ldap.py:61 keystone/identity/backends/ldap.py:67 +#: keystone/identity/backends/ldap.py:69 keystone/identity/backends/sql.py:104 +#: keystone/identity/backends/sql.py:106 +msgid "Invalid user / password" +msgstr "" + +#: keystone/identity/core.py:693 +#, python-format +msgid "User is disabled: %s" +msgstr "O usuário está desativado: %s" + +#: keystone/identity/core.py:735 +msgid "Cannot change user ID" +msgstr "" + +#: keystone/identity/backends/ldap.py:99 +msgid "Cannot change user name" +msgstr "" + +#: keystone/identity/backends/ldap.py:188 keystone/identity/backends/sql.py:188 +#: keystone/identity/backends/sql.py:206 +#, python-format +msgid "User '%(user_id)s' not found in group '%(group_id)s'" +msgstr "" + +#: keystone/identity/backends/ldap.py:339 +#, python-format +msgid "User %(user_id)s is already a member of group %(group_id)s" +msgstr "Usuário %(user_id)s já é membro do grupo %(group_id)s" + +#: keystone/models/token_model.py:61 +msgid "Found invalid token: scoped to both project and domain." +msgstr "" + +#: keystone/openstack/common/versionutils.py:108 +#, python-format +msgid "" +"%(what)s is deprecated as of %(as_of)s in favor of %(in_favor_of)s and " +"may be removed in %(remove_in)s." +msgstr "" +"%(what)s está deprecado desde %(as_of)s em favor de %(in_favor_of)s e " +"pode ser removido em %(remove_in)s." + +#: keystone/openstack/common/versionutils.py:112 +#, python-format +msgid "" +"%(what)s is deprecated as of %(as_of)s and may be removed in " +"%(remove_in)s. It will not be superseded." +msgstr "" +"%(what)s está deprecado desde %(as_of)s e pode ser removido em " +"%(remove_in)s. Ele não será substituído." + +#: keystone/openstack/common/versionutils.py:116 +#, python-format +msgid "%(what)s is deprecated as of %(as_of)s in favor of %(in_favor_of)s." +msgstr "" + +#: keystone/openstack/common/versionutils.py:119 +#, python-format +msgid "%(what)s is deprecated as of %(as_of)s. It will not be superseded." +msgstr "" + +#: keystone/openstack/common/versionutils.py:241 +#, python-format +msgid "Deprecated: %s" +msgstr "Deprecado: %s" + +#: keystone/openstack/common/versionutils.py:259 +#, python-format +msgid "Fatal call to deprecated config: %(msg)s" +msgstr "Chamada fatal à configuração deprecada: %(msg)s" + +#: keystone/resource/controllers.py:231 +msgid "" +"Cannot use parents_as_list and parents_as_ids query params at the same " +"time." +msgstr "" + +#: keystone/resource/controllers.py:237 +msgid "" +"Cannot use subtree_as_list and subtree_as_ids query params at the same " +"time." +msgstr "" + +#: keystone/resource/core.py:80 +#, python-format +msgid "max hierarchy depth reached for %s branch." +msgstr "" + +#: keystone/resource/core.py:97 +msgid "cannot create a project within a different domain than its parents." +msgstr "" + +#: keystone/resource/core.py:101 +#, python-format +msgid "cannot create a project in a branch containing a disabled project: %s" +msgstr "" + +#: keystone/resource/core.py:123 +#, python-format +msgid "Domain is disabled: %s" +msgstr "O domínio está desativado: %s" + +#: keystone/resource/core.py:141 +#, python-format +msgid "Domain cannot be named %s" +msgstr "" + +#: keystone/resource/core.py:144 +#, python-format +msgid "Domain cannot have ID %s" +msgstr "" + +#: keystone/resource/core.py:156 +#, python-format +msgid "Project is disabled: %s" +msgstr "O projeto está desativado: %s" + +#: keystone/resource/core.py:176 +#, python-format +msgid "cannot enable project %s since it has disabled parents" +msgstr "" + +#: keystone/resource/core.py:184 +#, python-format +msgid "cannot disable project %s since its subtree contains enabled projects" +msgstr "" + +#: keystone/resource/core.py:195 +msgid "Update of `parent_id` is not allowed." +msgstr "" + +#: keystone/resource/core.py:222 +#, python-format +msgid "cannot delete the project %s since it is not a leaf in the hierarchy." +msgstr "" + +#: keystone/resource/core.py:376 +msgid "Multiple domains are not supported" +msgstr "" + +#: keystone/resource/core.py:429 +msgid "delete the default domain" +msgstr "" + +#: keystone/resource/core.py:440 +msgid "cannot delete a domain that is enabled, please disable it first." +msgstr "" + +#: keystone/resource/core.py:841 +msgid "No options specified" +msgstr "Nenhuma opção especificada" + +#: keystone/resource/core.py:847 +#, python-format +msgid "" +"The value of group %(group)s specified in the config should be a " +"dictionary of options" +msgstr "" + +#: keystone/resource/core.py:871 +#, python-format +msgid "" +"Option %(option)s found with no group specified while checking domain " +"configuration request" +msgstr "" + +#: keystone/resource/core.py:878 +#, python-format +msgid "Group %(group)s is not supported for domain specific configurations" +msgstr "" + +#: keystone/resource/core.py:885 +#, python-format +msgid "" +"Option %(option)s in group %(group)s is not supported for domain specific" +" configurations" +msgstr "" + +#: keystone/resource/core.py:938 +msgid "An unexpected error occurred when retrieving domain configs" +msgstr "" + +#: keystone/resource/core.py:1013 keystone/resource/core.py:1097 +#: keystone/resource/core.py:1167 keystone/resource/config_backends/sql.py:70 +#, python-format +msgid "option %(option)s in group %(group)s" +msgstr "" + +#: keystone/resource/core.py:1016 keystone/resource/core.py:1102 +#: keystone/resource/core.py:1163 +#, python-format +msgid "group %(group)s" +msgstr "" + +#: keystone/resource/core.py:1018 +msgid "any options" +msgstr "" + +#: keystone/resource/core.py:1062 +#, python-format +msgid "" +"Trying to update option %(option)s in group %(group)s, so that, and only " +"that, option must be specified in the config" +msgstr "" + +#: keystone/resource/core.py:1067 +#, python-format +msgid "" +"Trying to update group %(group)s, so that, and only that, group must be " +"specified in the config" +msgstr "" + +#: keystone/resource/core.py:1076 +#, python-format +msgid "" +"request to update group %(group)s, but config provided contains group " +"%(group_other)s instead" +msgstr "" + +#: keystone/resource/core.py:1083 +#, python-format +msgid "" +"Trying to update option %(option)s in group %(group)s, but config " +"provided contains option %(option_other)s instead" +msgstr "" + +#: keystone/resource/backends/ldap.py:151 +#: keystone/resource/backends/ldap.py:159 +#: keystone/resource/backends/ldap.py:163 +msgid "Domains are read-only against LDAP" +msgstr "" + +#: keystone/server/eventlet.py:77 +msgid "" +"Running keystone via eventlet is deprecated as of Kilo in favor of " +"running in a WSGI server (e.g. mod_wsgi). Support for keystone under " +"eventlet will be removed in the \"M\"-Release." +msgstr "" + +#: keystone/server/eventlet.py:90 +#, python-format +msgid "Failed to start the %(name)s server" +msgstr "" + +#: keystone/token/controllers.py:391 +#, python-format +msgid "User %(u_id)s is unauthorized for tenant %(t_id)s" +msgstr "Usuário %(u_id)s não está autorizado para o tenant %(t_id)s" + +#: keystone/token/controllers.py:410 keystone/token/controllers.py:413 +msgid "Token does not belong to specified tenant." +msgstr "O token não pertence ao tenant especificado." + +#: keystone/token/persistence/backends/kvs.py:133 +#, python-format +msgid "Unknown token version %s" +msgstr "" + +#: keystone/token/providers/common.py:250 +#: keystone/token/providers/common.py:355 +#, python-format +msgid "User %(user_id)s has no access to project %(project_id)s" +msgstr "O usuário %(user_id)s não tem acesso ao projeto %(project_id)s" + +#: keystone/token/providers/common.py:255 +#: keystone/token/providers/common.py:360 +#, python-format +msgid "User %(user_id)s has no access to domain %(domain_id)s" +msgstr "O usuário %(user_id)s não tem acesso ao domínio %(domain_id)s" + +#: keystone/token/providers/common.py:282 +msgid "Trustor is disabled." +msgstr "O fiador está desativado." + +#: keystone/token/providers/common.py:346 +msgid "Trustee has no delegated roles." +msgstr "Fiador não possui roles delegados." + +#: keystone/token/providers/common.py:407 +#, python-format +msgid "Invalid audit info data type: %(data)s (%(type)s)" +msgstr "" + +#: keystone/token/providers/common.py:435 +msgid "User is not a trustee." +msgstr "Usuário não é confiável." + +#: keystone/token/providers/common.py:579 +msgid "" +"Attempting to use OS-FEDERATION token with V2 Identity Service, use V3 " +"Authentication" +msgstr "" + +#: keystone/token/providers/common.py:597 +msgid "Domain scoped token is not supported" +msgstr "O token de escopo de domínio não é suportado" + +#: keystone/token/providers/pki.py:48 keystone/token/providers/pkiz.py:30 +msgid "Unable to sign token." +msgstr "Não é possível assinar o token." + +#: keystone/token/providers/fernet/core.py:215 +msgid "" +"This is not a v2.0 Fernet token. Use v3 for trust, domain, or federated " +"tokens." +msgstr "" + +#: keystone/token/providers/fernet/token_formatters.py:189 +#, python-format +msgid "This is not a recognized Fernet payload version: %s" +msgstr "" + +#: keystone/trust/controllers.py:148 +msgid "Redelegation allowed for delegated by trust only" +msgstr "" + +#: keystone/trust/controllers.py:181 +msgid "The authenticated user should match the trustor." +msgstr "" + +#: keystone/trust/controllers.py:186 +msgid "At least one role should be specified." +msgstr "" + +#: keystone/trust/core.py:57 +#, python-format +msgid "" +"Remaining redelegation depth of %(redelegation_depth)d out of allowed " +"range of [0..%(max_count)d]" +msgstr "" + +#: keystone/trust/core.py:66 +#, python-format +msgid "" +"Field \"remaining_uses\" is set to %(value)s while it must not be set in " +"order to redelegate a trust" +msgstr "" + +#: keystone/trust/core.py:77 +msgid "Requested expiration time is more than redelegated trust can provide" +msgstr "" + +#: keystone/trust/core.py:87 +msgid "Some of requested roles are not in redelegated trust" +msgstr "" + +#: keystone/trust/core.py:116 +msgid "One of the trust agents is disabled or deleted" +msgstr "" + +#: keystone/trust/core.py:135 +msgid "remaining_uses must be a positive integer or null." +msgstr "" + +#: keystone/trust/core.py:141 +#, python-format +msgid "" +"Requested redelegation depth of %(requested_count)d is greater than " +"allowed %(max_count)d" +msgstr "" + +#: keystone/trust/core.py:147 +msgid "remaining_uses must not be set if redelegation is allowed" +msgstr "" + +#: keystone/trust/core.py:157 +msgid "" +"Modifying \"redelegation_count\" upon redelegation is forbidden. Omitting" +" this parameter is advised." +msgstr "" + diff --git a/keystone-moon/keystone/locale/ru/LC_MESSAGES/keystone-log-critical.po b/keystone-moon/keystone/locale/ru/LC_MESSAGES/keystone-log-critical.po new file mode 100644 index 00000000..f8d060b3 --- /dev/null +++ b/keystone-moon/keystone/locale/ru/LC_MESSAGES/keystone-log-critical.po @@ -0,0 +1,26 @@ +# Translations template for keystone. +# Copyright (C) 2014 OpenStack Foundation +# This file is distributed under the same license as the keystone project. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: Keystone\n" +"Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" +"POT-Creation-Date: 2014-09-07 06:06+0000\n" +"PO-Revision-Date: 2014-08-31 15:19+0000\n" +"Last-Translator: openstackjenkins \n" +"Language-Team: Russian (http://www.transifex.com/projects/p/keystone/" +"language/ru/)\n" +"Language: ru\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 1.3\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +#: keystone/catalog/backends/templated.py:106 +#, python-format +msgid "Unable to open template file %s" +msgstr "Не удается открыть файл шаблона %s" diff --git a/keystone-moon/keystone/locale/vi_VN/LC_MESSAGES/keystone-log-info.po b/keystone-moon/keystone/locale/vi_VN/LC_MESSAGES/keystone-log-info.po new file mode 100644 index 00000000..bcb9ab4e --- /dev/null +++ b/keystone-moon/keystone/locale/vi_VN/LC_MESSAGES/keystone-log-info.po @@ -0,0 +1,211 @@ +# Translations template for keystone. +# Copyright (C) 2015 OpenStack Foundation +# This file is distributed under the same license as the keystone project. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: Keystone\n" +"Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" +"POT-Creation-Date: 2015-03-09 06:03+0000\n" +"PO-Revision-Date: 2015-03-07 04:31+0000\n" +"Last-Translator: openstackjenkins \n" +"Language-Team: Vietnamese (Viet Nam) (http://www.transifex.com/projects/p/" +"keystone/language/vi_VN/)\n" +"Language: vi_VN\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 1.3\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: keystone/assignment/core.py:250 +#, python-format +msgid "Creating the default role %s because it does not exist." +msgstr "" + +#: keystone/assignment/core.py:258 +#, python-format +msgid "Creating the default role %s failed because it was already created" +msgstr "" + +#: keystone/auth/controllers.py:64 +msgid "Loading auth-plugins by class-name is deprecated." +msgstr "" + +#: keystone/auth/controllers.py:106 +#, python-format +msgid "" +"\"expires_at\" has conflicting values %(existing)s and %(new)s. Will use " +"the earliest value." +msgstr "" + +#: keystone/common/openssl.py:81 +#, python-format +msgid "Running command - %s" +msgstr "" + +#: keystone/common/wsgi.py:79 +msgid "No bind information present in token" +msgstr "" + +#: keystone/common/wsgi.py:83 +#, python-format +msgid "Named bind mode %s not in bind information" +msgstr "" + +#: keystone/common/wsgi.py:90 +msgid "Kerberos credentials required and not present" +msgstr "" + +#: keystone/common/wsgi.py:94 +msgid "Kerberos credentials do not match those in bind" +msgstr "" + +#: keystone/common/wsgi.py:98 +msgid "Kerberos bind authentication successful" +msgstr "" + +#: keystone/common/wsgi.py:105 +#, python-format +msgid "Couldn't verify unknown bind: {%(bind_type)s: %(identifier)s}" +msgstr "" + +#: keystone/common/environment/eventlet_server.py:103 +#, python-format +msgid "Starting %(arg0)s on %(host)s:%(port)s" +msgstr "" + +#: keystone/common/kvs/core.py:138 +#, python-format +msgid "Adding proxy '%(proxy)s' to KVS %(name)s." +msgstr "" + +#: keystone/common/kvs/core.py:188 +#, python-format +msgid "Using %(func)s as KVS region %(name)s key_mangler" +msgstr "" + +#: keystone/common/kvs/core.py:200 +#, python-format +msgid "Using default dogpile sha1_mangle_key as KVS region %s key_mangler" +msgstr "" + +#: keystone/common/kvs/core.py:210 +#, python-format +msgid "KVS region %s key_mangler disabled." +msgstr "" + +#: keystone/contrib/example/core.py:64 keystone/contrib/example/core.py:73 +#, python-format +msgid "" +"Received the following notification: service %(service)s, resource_type: " +"%(resource_type)s, operation %(operation)s payload %(payload)s" +msgstr "" + +#: keystone/openstack/common/eventlet_backdoor.py:146 +#, python-format +msgid "Eventlet backdoor listening on %(port)s for process %(pid)d" +msgstr "Eventlet backdoor lắng nghe trên %(port)s đối với tiến trình %(pid)d" + +#: keystone/openstack/common/service.py:173 +#, python-format +msgid "Caught %s, exiting" +msgstr "Bắt %s, thoát" + +#: keystone/openstack/common/service.py:231 +msgid "Parent process has died unexpectedly, exiting" +msgstr "Tiến trình cha bị chết đột ngột, thoát" + +#: keystone/openstack/common/service.py:262 +#, python-format +msgid "Child caught %s, exiting" +msgstr "Tiến trình con bắt %s, thoát" + +#: keystone/openstack/common/service.py:301 +msgid "Forking too fast, sleeping" +msgstr "Tạo tiến trình con quá nhanh, nghỉ" + +#: keystone/openstack/common/service.py:320 +#, python-format +msgid "Started child %d" +msgstr "Tiến trình con đã được khởi động %d " + +#: keystone/openstack/common/service.py:330 +#, python-format +msgid "Starting %d workers" +msgstr "Khởi động %d động cơ" + +#: keystone/openstack/common/service.py:347 +#, python-format +msgid "Child %(pid)d killed by signal %(sig)d" +msgstr "Tiến trình con %(pid)d bị huỷ bởi tín hiệu %(sig)d" + +#: keystone/openstack/common/service.py:351 +#, python-format +msgid "Child %(pid)s exited with status %(code)d" +msgstr "Tiến trình con %(pid)s đã thiaast với trạng thái %(code)d" + +#: keystone/openstack/common/service.py:390 +#, python-format +msgid "Caught %s, stopping children" +msgstr "Bắt %s, đang dừng tiến trình con" + +#: keystone/openstack/common/service.py:399 +msgid "Wait called after thread killed. Cleaning up." +msgstr "" + +#: keystone/openstack/common/service.py:415 +#, python-format +msgid "Waiting on %d children to exit" +msgstr "Chờ đợi %d tiến trình con để thoát " + +#: keystone/token/persistence/backends/sql.py:279 +#, python-format +msgid "Total expired tokens removed: %d" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:72 +msgid "" +"[fernet_tokens] key_repository does not appear to exist; attempting to " +"create it" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:130 +#, python-format +msgid "Created a new key: %s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:143 +msgid "Key repository is already initialized; aborting." +msgstr "" + +#: keystone/token/providers/fernet/utils.py:179 +#, python-format +msgid "Starting key rotation with %(count)s key files: %(list)s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:185 +#, python-format +msgid "Current primary key is: %s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:187 +#, python-format +msgid "Next primary key will be: %s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:197 +#, python-format +msgid "Promoted key 0 to be the primary: %s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:213 +#, python-format +msgid "Excess keys to purge: %s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:237 +#, python-format +msgid "Loaded %(count)s encryption keys from: %(dir)s" +msgstr "" diff --git a/keystone-moon/keystone/locale/zh_CN/LC_MESSAGES/keystone-log-critical.po b/keystone-moon/keystone/locale/zh_CN/LC_MESSAGES/keystone-log-critical.po new file mode 100644 index 00000000..a3a728e9 --- /dev/null +++ b/keystone-moon/keystone/locale/zh_CN/LC_MESSAGES/keystone-log-critical.po @@ -0,0 +1,25 @@ +# Translations template for keystone. +# Copyright (C) 2014 OpenStack Foundation +# This file is distributed under the same license as the keystone project. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: Keystone\n" +"Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" +"POT-Creation-Date: 2014-09-07 06:06+0000\n" +"PO-Revision-Date: 2014-08-31 15:19+0000\n" +"Last-Translator: openstackjenkins \n" +"Language-Team: Chinese (China) (http://www.transifex.com/projects/p/keystone/" +"language/zh_CN/)\n" +"Language: zh_CN\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 1.3\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: keystone/catalog/backends/templated.py:106 +#, python-format +msgid "Unable to open template file %s" +msgstr "无法打开模板文件 %s" diff --git a/keystone-moon/keystone/locale/zh_CN/LC_MESSAGES/keystone-log-error.po b/keystone-moon/keystone/locale/zh_CN/LC_MESSAGES/keystone-log-error.po new file mode 100644 index 00000000..a48b9382 --- /dev/null +++ b/keystone-moon/keystone/locale/zh_CN/LC_MESSAGES/keystone-log-error.po @@ -0,0 +1,177 @@ +# Translations template for keystone. +# Copyright (C) 2015 OpenStack Foundation +# This file is distributed under the same license as the keystone project. +# +# Translators: +# Xiao Xi LIU , 2014 +msgid "" +msgstr "" +"Project-Id-Version: Keystone\n" +"Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" +"POT-Creation-Date: 2015-03-09 06:03+0000\n" +"PO-Revision-Date: 2015-03-07 04:31+0000\n" +"Last-Translator: openstackjenkins \n" +"Language-Team: Chinese (China) (http://www.transifex.com/projects/p/keystone/" +"language/zh_CN/)\n" +"Language: zh_CN\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 1.3\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: keystone/notifications.py:304 +msgid "Failed to construct notifier" +msgstr "" + +#: keystone/notifications.py:389 +#, python-format +msgid "Failed to send %(res_id)s %(event_type)s notification" +msgstr "" + +#: keystone/notifications.py:606 +#, python-format +msgid "Failed to send %(action)s %(event_type)s notification" +msgstr "" + +#: keystone/catalog/core.py:62 +#, python-format +msgid "Malformed endpoint - %(url)r is not a string" +msgstr "" + +#: keystone/catalog/core.py:66 +#, python-format +msgid "Malformed endpoint %(url)s - unknown key %(keyerror)s" +msgstr "端点 %(url)s 的格式不正确 - 键 %(keyerror)s 未知" + +#: keystone/catalog/core.py:71 +#, python-format +msgid "" +"Malformed endpoint '%(url)s'. The following type error occurred during " +"string substitution: %(typeerror)s" +msgstr "" +"端点 '%(url)s' 的格式不正确。在字符串替换时发生以下类型错误:%(typeerror)s" + +#: keystone/catalog/core.py:77 +#, python-format +msgid "" +"Malformed endpoint %s - incomplete format (are you missing a type notifier ?)" +msgstr "端点 %s 的格式不完整 - (是否缺少了类型通告者?)" + +#: keystone/common/openssl.py:93 +#, python-format +msgid "Command %(to_exec)s exited with %(retcode)s- %(output)s" +msgstr "命令 %(to_exec)s 已退出,退出码及输出为 %(retcode)s- %(output)s" + +#: keystone/common/openssl.py:121 +#, python-format +msgid "Failed to remove file %(file_path)r: %(error)s" +msgstr "无法删除文件%(file_path)r: %(error)s" + +#: keystone/common/utils.py:239 +msgid "" +"Error setting up the debug environment. Verify that the option --debug-url " +"has the format : and that a debugger processes is listening on " +"that port." +msgstr "" +"设置调试环境出错。请确保选项--debug-url 的格式是这样的: ,和确保" +"有一个调试进程正在监听那个端口" + +#: keystone/common/cache/core.py:100 +#, python-format +msgid "" +"Unable to build cache config-key. Expected format \":\". " +"Skipping unknown format: %s" +msgstr "" + +#: keystone/common/environment/eventlet_server.py:99 +#, python-format +msgid "Could not bind to %(host)s:%(port)s" +msgstr "无法绑定至 %(host)s:%(port)s" + +#: keystone/common/environment/eventlet_server.py:185 +msgid "Server error" +msgstr "服务器报错" + +#: keystone/contrib/endpoint_policy/core.py:129 +#: keystone/contrib/endpoint_policy/core.py:228 +#, python-format +msgid "" +"Circular reference or a repeated entry found in region tree - %(region_id)s." +msgstr "在域树- %(region_id)s 中发现循环引用或重复项。" + +#: keystone/contrib/federation/idp.py:410 +#, python-format +msgid "Error when signing assertion, reason: %(reason)s" +msgstr "对断言进行签名时出错,原因:%(reason)s" + +#: keystone/contrib/oauth1/core.py:136 +msgid "Cannot retrieve Authorization headers" +msgstr "" + +#: keystone/openstack/common/loopingcall.py:95 +msgid "in fixed duration looping call" +msgstr "在固定时段内循环调用" + +#: keystone/openstack/common/loopingcall.py:138 +msgid "in dynamic looping call" +msgstr "在动态循环调用中" + +#: keystone/openstack/common/service.py:268 +msgid "Unhandled exception" +msgstr "存在未处理的异常" + +#: keystone/resource/core.py:477 +#, python-format +msgid "" +"Circular reference or a repeated entry found projects hierarchy - " +"%(project_id)s." +msgstr "" + +#: keystone/resource/core.py:939 +#, python-format +msgid "" +"Unexpected results in response for domain config - %(count)s responses, " +"first option is %(option)s, expected option %(expected)s" +msgstr "" + +#: keystone/resource/backends/sql.py:102 keystone/resource/backends/sql.py:121 +#, python-format +msgid "" +"Circular reference or a repeated entry found in projects hierarchy - " +"%(project_id)s." +msgstr "" + +#: keystone/token/provider.py:292 +#, python-format +msgid "Unexpected error or malformed token determining token expiry: %s" +msgstr "" + +#: keystone/token/persistence/backends/kvs.py:226 +#, python-format +msgid "" +"Reinitializing revocation list due to error in loading revocation list from " +"backend. Expected `list` type got `%(type)s`. Old revocation list data: " +"%(list)r" +msgstr "" + +#: keystone/token/providers/common.py:611 +msgid "Failed to validate token" +msgstr "token验证失败" + +#: keystone/token/providers/pki.py:47 +msgid "Unable to sign token" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:38 +#, python-format +msgid "" +"Either [fernet_tokens] key_repository does not exist or Keystone does not " +"have sufficient permission to access it: %s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:79 +msgid "" +"Failed to create [fernet_tokens] key_repository: either it already exists or " +"you don't have sufficient permissions to create it" +msgstr "" diff --git a/keystone-moon/keystone/locale/zh_CN/LC_MESSAGES/keystone-log-info.po b/keystone-moon/keystone/locale/zh_CN/LC_MESSAGES/keystone-log-info.po new file mode 100644 index 00000000..0e848ee1 --- /dev/null +++ b/keystone-moon/keystone/locale/zh_CN/LC_MESSAGES/keystone-log-info.po @@ -0,0 +1,215 @@ +# Translations template for keystone. +# Copyright (C) 2015 OpenStack Foundation +# This file is distributed under the same license as the keystone project. +# +# Translators: +# Xiao Xi LIU , 2014 +msgid "" +msgstr "" +"Project-Id-Version: Keystone\n" +"Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" +"POT-Creation-Date: 2015-03-09 06:03+0000\n" +"PO-Revision-Date: 2015-03-07 08:47+0000\n" +"Last-Translator: openstackjenkins \n" +"Language-Team: Chinese (China) (http://www.transifex.com/projects/p/keystone/" +"language/zh_CN/)\n" +"Language: zh_CN\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 1.3\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: keystone/assignment/core.py:250 +#, python-format +msgid "Creating the default role %s because it does not exist." +msgstr "正在创建默认角色%s,因为它之前不存在。" + +#: keystone/assignment/core.py:258 +#, python-format +msgid "Creating the default role %s failed because it was already created" +msgstr "" + +#: keystone/auth/controllers.py:64 +msgid "Loading auth-plugins by class-name is deprecated." +msgstr "通过class-name(类名)加载auth-plugins(认证插件)的方式已被弃用。" + +#: keystone/auth/controllers.py:106 +#, python-format +msgid "" +"\"expires_at\" has conflicting values %(existing)s and %(new)s. Will use " +"the earliest value." +msgstr "" +"\"expires_at\" 被赋予矛盾的值: %(existing)s 和 %(new)s。将采用时间上较早的那" +"个值。" + +#: keystone/common/openssl.py:81 +#, python-format +msgid "Running command - %s" +msgstr "正在运行命令 - %s" + +#: keystone/common/wsgi.py:79 +msgid "No bind information present in token" +msgstr "令牌中暂无绑定信息" + +#: keystone/common/wsgi.py:83 +#, python-format +msgid "Named bind mode %s not in bind information" +msgstr "在绑定信息中没有命名绑定模式%s" + +#: keystone/common/wsgi.py:90 +msgid "Kerberos credentials required and not present" +msgstr "没有所需的Kerberos凭证" + +#: keystone/common/wsgi.py:94 +msgid "Kerberos credentials do not match those in bind" +msgstr "在绑定中没有匹配的Kerberos凭证" + +#: keystone/common/wsgi.py:98 +msgid "Kerberos bind authentication successful" +msgstr "Kerberos绑定认证成功" + +#: keystone/common/wsgi.py:105 +#, python-format +msgid "Couldn't verify unknown bind: {%(bind_type)s: %(identifier)s}" +msgstr "不能验证未知绑定: {%(bind_type)s: %(identifier)s}" + +#: keystone/common/environment/eventlet_server.py:103 +#, python-format +msgid "Starting %(arg0)s on %(host)s:%(port)s" +msgstr "正在 %(host)s:%(port)s 上启动 %(arg0)s" + +#: keystone/common/kvs/core.py:138 +#, python-format +msgid "Adding proxy '%(proxy)s' to KVS %(name)s." +msgstr "正在将代理'%(proxy)s'加入KVS %(name)s 中。" + +#: keystone/common/kvs/core.py:188 +#, python-format +msgid "Using %(func)s as KVS region %(name)s key_mangler" +msgstr "使用 %(func)s 作为KVS域 %(name)s 的key_mangler处理函数" + +#: keystone/common/kvs/core.py:200 +#, python-format +msgid "Using default dogpile sha1_mangle_key as KVS region %s key_mangler" +msgstr "" +"使用默认的dogpile sha1_mangle_key函数作为KVS域 %s 的key_mangler处理函数" + +#: keystone/common/kvs/core.py:210 +#, python-format +msgid "KVS region %s key_mangler disabled." +msgstr "KVS域 %s 的key_mangler处理函数被禁用。" + +#: keystone/contrib/example/core.py:64 keystone/contrib/example/core.py:73 +#, python-format +msgid "" +"Received the following notification: service %(service)s, resource_type: " +"%(resource_type)s, operation %(operation)s payload %(payload)s" +msgstr "" + +#: keystone/openstack/common/eventlet_backdoor.py:146 +#, python-format +msgid "Eventlet backdoor listening on %(port)s for process %(pid)d" +msgstr "携程为进程 %(pid)d 在后台监听 %(port)s " + +#: keystone/openstack/common/service.py:173 +#, python-format +msgid "Caught %s, exiting" +msgstr "捕获到 %s,正在退出" + +#: keystone/openstack/common/service.py:231 +msgid "Parent process has died unexpectedly, exiting" +msgstr "父进程已意外终止,正在退出" + +#: keystone/openstack/common/service.py:262 +#, python-format +msgid "Child caught %s, exiting" +msgstr "子代捕获 %s,正在退出" + +#: keystone/openstack/common/service.py:301 +msgid "Forking too fast, sleeping" +msgstr "派生速度太快,正在休眠" + +#: keystone/openstack/common/service.py:320 +#, python-format +msgid "Started child %d" +msgstr "已启动子代 %d" + +#: keystone/openstack/common/service.py:330 +#, python-format +msgid "Starting %d workers" +msgstr "正在启动 %d 工作程序" + +#: keystone/openstack/common/service.py:347 +#, python-format +msgid "Child %(pid)d killed by signal %(sig)d" +msgstr "信号 %(sig)d 已终止子代 %(pid)d" + +#: keystone/openstack/common/service.py:351 +#, python-format +msgid "Child %(pid)s exited with status %(code)d" +msgstr "子代 %(pid)s 已退出,状态为 %(code)d" + +#: keystone/openstack/common/service.py:390 +#, python-format +msgid "Caught %s, stopping children" +msgstr "捕获到 %s,正在停止子代" + +#: keystone/openstack/common/service.py:399 +msgid "Wait called after thread killed. Cleaning up." +msgstr "线程结束,正在清理" + +#: keystone/openstack/common/service.py:415 +#, python-format +msgid "Waiting on %d children to exit" +msgstr "正在等待 %d 个子代退出" + +#: keystone/token/persistence/backends/sql.py:279 +#, python-format +msgid "Total expired tokens removed: %d" +msgstr "被移除的失效令牌总数:%d" + +#: keystone/token/providers/fernet/utils.py:72 +msgid "" +"[fernet_tokens] key_repository does not appear to exist; attempting to " +"create it" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:130 +#, python-format +msgid "Created a new key: %s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:143 +msgid "Key repository is already initialized; aborting." +msgstr "" + +#: keystone/token/providers/fernet/utils.py:179 +#, python-format +msgid "Starting key rotation with %(count)s key files: %(list)s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:185 +#, python-format +msgid "Current primary key is: %s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:187 +#, python-format +msgid "Next primary key will be: %s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:197 +#, python-format +msgid "Promoted key 0 to be the primary: %s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:213 +#, python-format +msgid "Excess keys to purge: %s" +msgstr "" + +#: keystone/token/providers/fernet/utils.py:237 +#, python-format +msgid "Loaded %(count)s encryption keys from: %(dir)s" +msgstr "" diff --git a/keystone-moon/keystone/locale/zh_TW/LC_MESSAGES/keystone-log-critical.po b/keystone-moon/keystone/locale/zh_TW/LC_MESSAGES/keystone-log-critical.po new file mode 100644 index 00000000..b0ff57c9 --- /dev/null +++ b/keystone-moon/keystone/locale/zh_TW/LC_MESSAGES/keystone-log-critical.po @@ -0,0 +1,25 @@ +# Translations template for keystone. +# Copyright (C) 2014 OpenStack Foundation +# This file is distributed under the same license as the keystone project. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: Keystone\n" +"Report-Msgid-Bugs-To: https://bugs.launchpad.net/keystone\n" +"POT-Creation-Date: 2014-09-07 06:06+0000\n" +"PO-Revision-Date: 2014-08-31 15:19+0000\n" +"Last-Translator: openstackjenkins \n" +"Language-Team: Chinese (Taiwan) (http://www.transifex.com/projects/p/" +"keystone/language/zh_TW/)\n" +"Language: zh_TW\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 1.3\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: keystone/catalog/backends/templated.py:106 +#, python-format +msgid "Unable to open template file %s" +msgstr "無法開啟範本檔 %s" diff --git a/keystone-moon/keystone/middleware/__init__.py b/keystone-moon/keystone/middleware/__init__.py new file mode 100644 index 00000000..efbaa7c9 --- /dev/null +++ b/keystone-moon/keystone/middleware/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 keystone.middleware.core import * # noqa diff --git a/keystone-moon/keystone/middleware/core.py b/keystone-moon/keystone/middleware/core.py new file mode 100644 index 00000000..bf86cd2b --- /dev/null +++ b/keystone-moon/keystone/middleware/core.py @@ -0,0 +1,240 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 oslo_config import cfg +from oslo_log import log +from oslo_middleware import sizelimit +from oslo_serialization import jsonutils +import six + +from keystone.common import authorization +from keystone.common import wsgi +from keystone import exception +from keystone.i18n import _LW +from keystone.models import token_model +from keystone.openstack.common import versionutils + +CONF = cfg.CONF +LOG = log.getLogger(__name__) + + +# Header used to transmit the auth token +AUTH_TOKEN_HEADER = 'X-Auth-Token' + + +# Header used to transmit the subject token +SUBJECT_TOKEN_HEADER = 'X-Subject-Token' + + +# Environment variable used to pass the request context +CONTEXT_ENV = wsgi.CONTEXT_ENV + + +# Environment variable used to pass the request params +PARAMS_ENV = wsgi.PARAMS_ENV + + +class TokenAuthMiddleware(wsgi.Middleware): + def process_request(self, request): + token = request.headers.get(AUTH_TOKEN_HEADER) + context = request.environ.get(CONTEXT_ENV, {}) + context['token_id'] = token + if SUBJECT_TOKEN_HEADER in request.headers: + context['subject_token_id'] = ( + request.headers.get(SUBJECT_TOKEN_HEADER)) + request.environ[CONTEXT_ENV] = context + + +class AdminTokenAuthMiddleware(wsgi.Middleware): + """A trivial filter that checks for a pre-defined admin token. + + Sets 'is_admin' to true in the context, expected to be checked by + methods that are admin-only. + + """ + + def process_request(self, request): + token = request.headers.get(AUTH_TOKEN_HEADER) + context = request.environ.get(CONTEXT_ENV, {}) + context['is_admin'] = (token == CONF.admin_token) + request.environ[CONTEXT_ENV] = context + + +class PostParamsMiddleware(wsgi.Middleware): + """Middleware to allow method arguments to be passed as POST parameters. + + Filters out the parameters `self`, `context` and anything beginning with + an underscore. + + """ + + def process_request(self, request): + params_parsed = request.params + params = {} + for k, v in six.iteritems(params_parsed): + if k in ('self', 'context'): + continue + if k.startswith('_'): + continue + params[k] = v + + request.environ[PARAMS_ENV] = params + + +class JsonBodyMiddleware(wsgi.Middleware): + """Middleware to allow method arguments to be passed as serialized JSON. + + Accepting arguments as JSON is useful for accepting data that may be more + complex than simple primitives. + + Filters out the parameters `self`, `context` and anything beginning with + an underscore. + + """ + def process_request(self, request): + # Abort early if we don't have any work to do + params_json = request.body + if not params_json: + return + + # Reject unrecognized content types. Empty string indicates + # the client did not explicitly set the header + if request.content_type not in ('application/json', ''): + e = exception.ValidationError(attribute='application/json', + target='Content-Type header') + return wsgi.render_exception(e, request=request) + + params_parsed = {} + try: + params_parsed = jsonutils.loads(params_json) + except ValueError: + e = exception.ValidationError(attribute='valid JSON', + target='request body') + return wsgi.render_exception(e, request=request) + finally: + if not params_parsed: + params_parsed = {} + + if not isinstance(params_parsed, dict): + e = exception.ValidationError(attribute='valid JSON object', + target='request body') + return wsgi.render_exception(e, request=request) + + params = {} + for k, v in six.iteritems(params_parsed): + if k in ('self', 'context'): + continue + if k.startswith('_'): + continue + params[k] = v + + request.environ[PARAMS_ENV] = params + + +class XmlBodyMiddleware(wsgi.Middleware): + """De/serialize XML to/from JSON.""" + + def print_warning(self): + LOG.warning(_LW('XML support has been removed as of the Kilo release ' + 'and should not be referenced or used in deployment. ' + 'Please remove references to XmlBodyMiddleware from ' + 'your configuration. This compatibility stub will be ' + 'removed in the L release')) + + def __init__(self, *args, **kwargs): + super(XmlBodyMiddleware, self).__init__(*args, **kwargs) + self.print_warning() + + +class XmlBodyMiddlewareV2(XmlBodyMiddleware): + """De/serialize XML to/from JSON for v2.0 API.""" + + def __init__(self, *args, **kwargs): + pass + + +class XmlBodyMiddlewareV3(XmlBodyMiddleware): + """De/serialize XML to/from JSON for v3 API.""" + + def __init__(self, *args, **kwargs): + pass + + +class NormalizingFilter(wsgi.Middleware): + """Middleware filter to handle URL normalization.""" + + def process_request(self, request): + """Normalizes URLs.""" + # Removes a trailing slash from the given path, if any. + if (len(request.environ['PATH_INFO']) > 1 and + request.environ['PATH_INFO'][-1] == '/'): + request.environ['PATH_INFO'] = request.environ['PATH_INFO'][:-1] + # Rewrites path to root if no path is given. + elif not request.environ['PATH_INFO']: + request.environ['PATH_INFO'] = '/' + + +class RequestBodySizeLimiter(sizelimit.RequestBodySizeLimiter): + @versionutils.deprecated( + versionutils.deprecated.KILO, + in_favor_of='oslo_middleware.sizelimit.RequestBodySizeLimiter', + remove_in=+1, + what='keystone.middleware.RequestBodySizeLimiter') + def __init__(self, *args, **kwargs): + super(RequestBodySizeLimiter, self).__init__(*args, **kwargs) + + +class AuthContextMiddleware(wsgi.Middleware): + """Build the authentication context from the request auth token.""" + + def _build_auth_context(self, request): + token_id = request.headers.get(AUTH_TOKEN_HEADER).strip() + + if token_id == CONF.admin_token: + # NOTE(gyee): no need to proceed any further as the special admin + # token is being handled by AdminTokenAuthMiddleware. This code + # will not be impacted even if AdminTokenAuthMiddleware is removed + # from the pipeline as "is_admin" is default to "False". This code + # is independent of AdminTokenAuthMiddleware. + return {} + + context = {'token_id': token_id} + context['environment'] = request.environ + + try: + token_ref = token_model.KeystoneToken( + token_id=token_id, + token_data=self.token_provider_api.validate_token(token_id)) + # TODO(gyee): validate_token_bind should really be its own + # middleware + wsgi.validate_token_bind(context, token_ref) + return authorization.token_to_auth_context(token_ref) + except exception.TokenNotFound: + LOG.warning(_LW('RBAC: Invalid token')) + raise exception.Unauthorized() + + def process_request(self, request): + if AUTH_TOKEN_HEADER not in request.headers: + LOG.debug(('Auth token not in the request header. ' + 'Will not build auth context.')) + return + + if authorization.AUTH_CONTEXT_ENV in request.environ: + msg = _LW('Auth context already exists in the request environment') + LOG.warning(msg) + return + + auth_context = self._build_auth_context(request) + LOG.debug('RBAC: auth_context: %s', auth_context) + request.environ[authorization.AUTH_CONTEXT_ENV] = auth_context diff --git a/keystone-moon/keystone/middleware/ec2_token.py b/keystone-moon/keystone/middleware/ec2_token.py new file mode 100644 index 00000000..771b74f8 --- /dev/null +++ b/keystone-moon/keystone/middleware/ec2_token.py @@ -0,0 +1,44 @@ +# Copyright 2012 OpenStack Foundation +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# 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. + +""" +Starting point for routing EC2 requests. + +The EC2 Token Middleware has been deprecated as of Juno. It has been moved into +keystonemiddleware, `keystonemiddleware.ec2_token`. + +""" + +from keystonemiddleware import ec2_token + +from keystone.openstack.common import versionutils + + +class EC2Token(ec2_token.EC2Token): + + @versionutils.deprecated( + versionutils.deprecated.JUNO, + in_favor_of='keystonemiddleware.ec2_token.EC2Token', + remove_in=+2, + what='keystone.middleware.ec2_token.EC2Token') + def __init__(self, *args, **kwargs): + super(EC2Token, self).__init__(*args, **kwargs) + + +filter_factory = ec2_token.filter_factory +app_factory = ec2_token.app_factory +keystone_ec2_opts = ec2_token.keystone_ec2_opts diff --git a/keystone-moon/keystone/models/__init__.py b/keystone-moon/keystone/models/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/models/token_model.py b/keystone-moon/keystone/models/token_model.py new file mode 100644 index 00000000..3be22b96 --- /dev/null +++ b/keystone-moon/keystone/models/token_model.py @@ -0,0 +1,335 @@ +# 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. + +"""Unified in-memory token model.""" + +from keystoneclient.common import cms +from oslo_config import cfg +from oslo_utils import timeutils +import six + +from keystone.contrib import federation +from keystone import exception +from keystone.i18n import _ + + +CONF = cfg.CONF +# supported token versions +V2 = 'v2.0' +V3 = 'v3.0' +VERSIONS = frozenset([V2, V3]) + + +def _parse_and_normalize_time(time_data): + if isinstance(time_data, six.string_types): + time_data = timeutils.parse_isotime(time_data) + return timeutils.normalize_time(time_data) + + +class KeystoneToken(dict): + """An in-memory representation that unifies v2 and v3 tokens.""" + # TODO(morganfainberg): Align this in-memory representation with the + # objects in keystoneclient. This object should be eventually updated + # to be the source of token data with the ability to emit any version + # of the token instead of only consuming the token dict and providing + # property accessors for the underlying data. + + def __init__(self, token_id, token_data): + self.token_data = token_data + if 'access' in token_data: + super(KeystoneToken, self).__init__(**token_data['access']) + self.version = V2 + elif 'token' in token_data and 'methods' in token_data['token']: + super(KeystoneToken, self).__init__(**token_data['token']) + self.version = V3 + else: + raise exception.UnsupportedTokenVersionException() + self.token_id = token_id + self.short_id = cms.cms_hash_token(token_id, + mode=CONF.token.hash_algorithm) + + if self.project_scoped and self.domain_scoped: + raise exception.UnexpectedError(_('Found invalid token: scoped to ' + 'both project and domain.')) + + def __repr__(self): + desc = ('<%(type)s (audit_id=%(audit_id)s, ' + 'audit_chain_id=%(audit_chain_id)s) at %(loc)s>') + return desc % {'type': self.__class__.__name__, + 'audit_id': self.audit_id, + 'audit_chain_id': self.audit_chain_id, + 'loc': hex(id(self))} + + @property + def expires(self): + if self.version is V3: + expires_at = self['expires_at'] + else: + expires_at = self['token']['expires'] + return _parse_and_normalize_time(expires_at) + + @property + def issued(self): + if self.version is V3: + issued_at = self['issued_at'] + else: + issued_at = self['token']['issued_at'] + return _parse_and_normalize_time(issued_at) + + @property + def audit_id(self): + if self.version is V3: + return self.get('audit_ids', [None])[0] + return self['token'].get('audit_ids', [None])[0] + + @property + def audit_chain_id(self): + if self.version is V3: + return self.get('audit_ids', [None])[-1] + return self['token'].get('audit_ids', [None])[-1] + + @property + def auth_token(self): + return self.token_id + + @property + def user_id(self): + return self['user']['id'] + + @property + def user_name(self): + return self['user']['name'] + + @property + def user_domain_name(self): + try: + if self.version == V3: + return self['user']['domain']['name'] + elif 'user' in self: + return "Default" + except KeyError: + # Do not raise KeyError, raise UnexpectedError + pass + raise exception.UnexpectedError() + + @property + def user_domain_id(self): + try: + if self.version == V3: + return self['user']['domain']['id'] + elif 'user' in self: + return CONF.identity.default_domain_id + except KeyError: + # Do not raise KeyError, raise UnexpectedError + pass + raise exception.UnexpectedError() + + @property + def domain_id(self): + if self.version is V3: + try: + return self['domain']['id'] + except KeyError: + # Do not raise KeyError, raise UnexpectedError + raise exception.UnexpectedError() + # No domain scoped tokens in V2. + raise NotImplementedError() + + @property + def domain_name(self): + if self.version is V3: + try: + return self['domain']['name'] + except KeyError: + # Do not raise KeyError, raise UnexpectedError + raise exception.UnexpectedError() + # No domain scoped tokens in V2. + raise NotImplementedError() + + @property + def project_id(self): + try: + if self.version is V3: + return self['project']['id'] + else: + return self['token']['tenant']['id'] + except KeyError: + # Do not raise KeyError, raise UnexpectedError + raise exception.UnexpectedError() + + @property + def project_name(self): + try: + if self.version is V3: + return self['project']['name'] + else: + return self['token']['tenant']['name'] + except KeyError: + # Do not raise KeyError, raise UnexpectedError + raise exception.UnexpectedError() + + @property + def project_domain_id(self): + try: + if self.version is V3: + return self['project']['domain']['id'] + elif 'tenant' in self['token']: + return CONF.identity.default_domain_id + except KeyError: + # Do not raise KeyError, raise UnexpectedError + pass + + raise exception.UnexpectedError() + + @property + def project_domain_name(self): + try: + if self.version is V3: + return self['project']['domain']['name'] + if 'tenant' in self['token']: + return 'Default' + except KeyError: + # Do not raise KeyError, raise UnexpectedError + pass + + raise exception.UnexpectedError() + + @property + def project_scoped(self): + if self.version is V3: + return 'project' in self + else: + return 'tenant' in self['token'] + + @property + def domain_scoped(self): + if self.version is V3: + return 'domain' in self + return False + + @property + def scoped(self): + return self.project_scoped or self.domain_scoped + + @property + def trust_id(self): + if self.version is V3: + return self.get('OS-TRUST:trust', {}).get('id') + else: + return self.get('trust', {}).get('id') + + @property + def trust_scoped(self): + if self.version is V3: + return 'OS-TRUST:trust' in self + else: + return 'trust' in self + + @property + def trustee_user_id(self): + if self.version is V3: + return self.get( + 'OS-TRUST:trust', {}).get('trustee_user_id') + else: + return self.get('trust', {}).get('trustee_user_id') + + @property + def trustor_user_id(self): + if self.version is V3: + return self.get( + 'OS-TRUST:trust', {}).get('trustor_user_id') + else: + return self.get('trust', {}).get('trustor_user_id') + + @property + def trust_impersonation(self): + if self.version is V3: + return self.get('OS-TRUST:trust', {}).get('impersonation') + else: + return self.get('trust', {}).get('impersonation') + + @property + def oauth_scoped(self): + return 'OS-OAUTH1' in self + + @property + def oauth_access_token_id(self): + if self.version is V3 and self.oauth_scoped: + return self['OS-OAUTH1']['access_token_id'] + return None + + @property + def oauth_consumer_id(self): + if self.version is V3 and self.oauth_scoped: + return self['OS-OAUTH1']['consumer_id'] + return None + + @property + def role_ids(self): + if self.version is V3: + return [r['id'] for r in self.get('roles', [])] + else: + return self.get('metadata', {}).get('roles', []) + + @property + def role_names(self): + if self.version is V3: + return [r['name'] for r in self.get('roles', [])] + else: + return [r['name'] for r in self['user'].get('roles', [])] + + @property + def bind(self): + if self.version is V3: + return self.get('bind') + return self.get('token', {}).get('bind') + + @property + def is_federated_user(self): + try: + return self.version is V3 and federation.FEDERATION in self['user'] + except KeyError: + raise exception.UnexpectedError() + + @property + def federation_group_ids(self): + if self.is_federated_user: + if self.version is V3: + try: + groups = self['user'][federation.FEDERATION].get( + 'groups', []) + return [g['id'] for g in groups] + except KeyError: + raise exception.UnexpectedError() + return [] + + @property + def federation_idp_id(self): + if self.version is not V3 or not self.is_federated_user: + return None + return self['user'][federation.FEDERATION]['identity_provider']['id'] + + @property + def federation_protocol_id(self): + if self.version is V3 and self.is_federated_user: + return self['user'][federation.FEDERATION]['protocol']['id'] + return None + + @property + def metadata(self): + return self.get('metadata', {}) + + @property + def methods(self): + if self.version is V3: + return self.get('methods', []) + return [] diff --git a/keystone-moon/keystone/notifications.py b/keystone-moon/keystone/notifications.py new file mode 100644 index 00000000..4a1cd333 --- /dev/null +++ b/keystone-moon/keystone/notifications.py @@ -0,0 +1,686 @@ +# Copyright 2013 IBM Corp. +# +# 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. + +"""Notifications module for OpenStack Identity Service resources""" + +import collections +import inspect +import logging +import socket + +from oslo_config import cfg +from oslo_log import log +import oslo_messaging +import pycadf +from pycadf import cadftaxonomy as taxonomy +from pycadf import cadftype +from pycadf import credential +from pycadf import eventfactory +from pycadf import resource + +from keystone.i18n import _, _LE + + +notifier_opts = [ + cfg.StrOpt('default_publisher_id', + help='Default publisher_id for outgoing notifications'), + cfg.StrOpt('notification_format', default='basic', + help='Define the notification format for Identity Service ' + 'events. A "basic" notification has information about ' + 'the resource being operated on. A "cadf" notification ' + 'has the same information, as well as information about ' + 'the initiator of the event. Valid options are: basic ' + 'and cadf'), +] + +config_section = None +list_opts = lambda: [(config_section, notifier_opts), ] + +LOG = log.getLogger(__name__) +# NOTE(gyee): actions that can be notified. One must update this list whenever +# a new action is supported. +_ACTIONS = collections.namedtuple( + 'NotificationActions', + 'created, deleted, disabled, updated, internal') +ACTIONS = _ACTIONS(created='created', deleted='deleted', disabled='disabled', + updated='updated', internal='internal') + +CADF_TYPE_MAP = { + 'group': taxonomy.SECURITY_GROUP, + 'project': taxonomy.SECURITY_PROJECT, + 'role': taxonomy.SECURITY_ROLE, + 'user': taxonomy.SECURITY_ACCOUNT_USER, + 'domain': taxonomy.SECURITY_DOMAIN, + 'region': taxonomy.SECURITY_REGION, + 'endpoint': taxonomy.SECURITY_ENDPOINT, + 'service': taxonomy.SECURITY_SERVICE, + 'policy': taxonomy.SECURITY_POLICY, + 'OS-TRUST:trust': taxonomy.SECURITY_TRUST, + 'OS-OAUTH1:access_token': taxonomy.SECURITY_CREDENTIAL, + 'OS-OAUTH1:request_token': taxonomy.SECURITY_CREDENTIAL, + 'OS-OAUTH1:consumer': taxonomy.SECURITY_ACCOUNT, +} + +SAML_AUDIT_TYPE = 'http://docs.oasis-open.org/security/saml/v2.0' +# resource types that can be notified +_SUBSCRIBERS = {} +_notifier = None +SERVICE = 'identity' + + +CONF = cfg.CONF +CONF.register_opts(notifier_opts) + +# NOTE(morganfainberg): Special case notifications that are only used +# internally for handling token persistence token deletions +INVALIDATE_USER_TOKEN_PERSISTENCE = 'invalidate_user_tokens' +INVALIDATE_USER_PROJECT_TOKEN_PERSISTENCE = 'invalidate_user_project_tokens' +INVALIDATE_USER_OAUTH_CONSUMER_TOKENS = 'invalidate_user_consumer_tokens' + + +class Audit(object): + """Namespace for audit notification functions. + + This is a namespace object to contain all of the direct notification + functions utilized for ``Manager`` methods. + """ + + @classmethod + def _emit(cls, operation, resource_type, resource_id, initiator, public): + """Directly send an event notification. + + :param operation: one of the values from ACTIONS + :param resource_type: type of resource being affected + :param resource_id: ID of the resource affected + :param initiator: CADF representation of the user that created the + request + :param public: If True (default), the event will be sent to the + notifier API. If False, the event will only be sent via + notify_event_callbacks to in process listeners + """ + # NOTE(stevemar): the _send_notification function is + # overloaded, it's used to register callbacks and to actually + # send the notification externally. Thus, we should check + # the desired notification format in the function instead + # of before it. + _send_notification( + operation, + resource_type, + resource_id, + public=public) + + if CONF.notification_format == 'cadf' and public: + outcome = taxonomy.OUTCOME_SUCCESS + _create_cadf_payload(operation, resource_type, resource_id, + outcome, initiator) + + @classmethod + def created(cls, resource_type, resource_id, initiator=None, + public=True): + cls._emit(ACTIONS.created, resource_type, resource_id, initiator, + public) + + @classmethod + def updated(cls, resource_type, resource_id, initiator=None, + public=True): + cls._emit(ACTIONS.updated, resource_type, resource_id, initiator, + public) + + @classmethod + def disabled(cls, resource_type, resource_id, initiator=None, + public=True): + cls._emit(ACTIONS.disabled, resource_type, resource_id, initiator, + public) + + @classmethod + def deleted(cls, resource_type, resource_id, initiator=None, + public=True): + cls._emit(ACTIONS.deleted, resource_type, resource_id, initiator, + public) + + +class ManagerNotificationWrapper(object): + """Send event notifications for ``Manager`` methods. + + Sends a notification if the wrapped Manager method does not raise an + ``Exception`` (such as ``keystone.exception.NotFound``). + + :param operation: one of the values from ACTIONS + :param resource_type: type of resource being affected + :param public: If True (default), the event will be sent to the notifier + API. If False, the event will only be sent via + notify_event_callbacks to in process listeners + + """ + def __init__(self, operation, resource_type, public=True, + resource_id_arg_index=1, result_id_arg_attr=None): + self.operation = operation + self.resource_type = resource_type + self.public = public + self.resource_id_arg_index = resource_id_arg_index + self.result_id_arg_attr = result_id_arg_attr + + def __call__(self, f): + def wrapper(*args, **kwargs): + """Send a notification if the wrapped callable is successful.""" + try: + result = f(*args, **kwargs) + except Exception: + raise + else: + if self.result_id_arg_attr is not None: + resource_id = result[self.result_id_arg_attr] + else: + resource_id = args[self.resource_id_arg_index] + + # NOTE(stevemar): the _send_notification function is + # overloaded, it's used to register callbacks and to actually + # send the notification externally. Thus, we should check + # the desired notification format in the function instead + # of before it. + _send_notification( + self.operation, + self.resource_type, + resource_id, + public=self.public) + + # Only emit CADF notifications for public events + if CONF.notification_format == 'cadf' and self.public: + outcome = taxonomy.OUTCOME_SUCCESS + # NOTE(morganfainberg): The decorator form will always use + # a 'None' initiator, since we do not pass context around + # in a manner that allows the decorator to inspect context + # and extract the needed information. + initiator = None + _create_cadf_payload(self.operation, self.resource_type, + resource_id, outcome, initiator) + return result + + return wrapper + + +def created(*args, **kwargs): + """Decorator to send notifications for ``Manager.create_*`` methods.""" + return ManagerNotificationWrapper(ACTIONS.created, *args, **kwargs) + + +def updated(*args, **kwargs): + """Decorator to send notifications for ``Manager.update_*`` methods.""" + return ManagerNotificationWrapper(ACTIONS.updated, *args, **kwargs) + + +def disabled(*args, **kwargs): + """Decorator to send notifications when an object is disabled.""" + return ManagerNotificationWrapper(ACTIONS.disabled, *args, **kwargs) + + +def deleted(*args, **kwargs): + """Decorator to send notifications for ``Manager.delete_*`` methods.""" + return ManagerNotificationWrapper(ACTIONS.deleted, *args, **kwargs) + + +def internal(*args, **kwargs): + """Decorator to send notifications for internal notifications only.""" + kwargs['public'] = False + return ManagerNotificationWrapper(ACTIONS.internal, *args, **kwargs) + + +def _get_callback_info(callback): + """Return list containing callback's module and name. + + If the callback is an instance method also return the class name. + + :param callback: Function to call + :type callback: function + :returns: List containing parent module, (optional class,) function name + :rtype: list + """ + if getattr(callback, 'im_class', None): + return [getattr(callback, '__module__', None), + callback.im_class.__name__, + callback.__name__] + else: + return [getattr(callback, '__module__', None), callback.__name__] + + +def register_event_callback(event, resource_type, callbacks): + """Register each callback with the event. + + :param event: Action being registered + :type event: keystone.notifications.ACTIONS + :param resource_type: Type of resource being operated on + :type resource_type: str + :param callbacks: Callback items to be registered with event + :type callbacks: list + :raises ValueError: If event is not a valid ACTION + :raises TypeError: If callback is not callable + """ + if event not in ACTIONS: + raise ValueError(_('%(event)s is not a valid notification event, must ' + 'be one of: %(actions)s') % + {'event': event, 'actions': ', '.join(ACTIONS)}) + + if not hasattr(callbacks, '__iter__'): + callbacks = [callbacks] + + for callback in callbacks: + if not callable(callback): + msg = _('Method not callable: %s') % callback + LOG.error(msg) + raise TypeError(msg) + _SUBSCRIBERS.setdefault(event, {}).setdefault(resource_type, set()) + _SUBSCRIBERS[event][resource_type].add(callback) + + if LOG.logger.getEffectiveLevel() <= logging.DEBUG: + # Do this only if its going to appear in the logs. + msg = 'Callback: `%(callback)s` subscribed to event `%(event)s`.' + callback_info = _get_callback_info(callback) + callback_str = '.'.join(i for i in callback_info if i is not None) + event_str = '.'.join(['identity', resource_type, event]) + LOG.debug(msg, {'callback': callback_str, 'event': event_str}) + + +def notify_event_callbacks(service, resource_type, operation, payload): + """Sends a notification to registered extensions.""" + if operation in _SUBSCRIBERS: + if resource_type in _SUBSCRIBERS[operation]: + for cb in _SUBSCRIBERS[operation][resource_type]: + subst_dict = {'cb_name': cb.__name__, + 'service': service, + 'resource_type': resource_type, + 'operation': operation, + 'payload': payload} + LOG.debug('Invoking callback %(cb_name)s for event ' + '%(service)s %(resource_type)s %(operation)s for' + '%(payload)s', subst_dict) + cb(service, resource_type, operation, payload) + + +def _get_notifier(): + """Return a notifier object. + + If _notifier is None it means that a notifier object has not been set. + If _notifier is False it means that a notifier has previously failed to + construct. + Otherwise it is a constructed Notifier object. + """ + global _notifier + + if _notifier is None: + host = CONF.default_publisher_id or socket.gethostname() + try: + transport = oslo_messaging.get_transport(CONF) + _notifier = oslo_messaging.Notifier(transport, + "identity.%s" % host) + except Exception: + LOG.exception(_LE("Failed to construct notifier")) + _notifier = False + + return _notifier + + +def clear_subscribers(): + """Empty subscribers dictionary. + + This effectively stops notifications since there will be no subscribers + to publish to. + """ + _SUBSCRIBERS.clear() + + +def reset_notifier(): + """Reset the notifications internal state. + + This is used only for testing purposes. + + """ + global _notifier + _notifier = None + + +def _create_cadf_payload(operation, resource_type, resource_id, + outcome, initiator): + """Prepare data for CADF audit notifier. + + Transform the arguments into content to be consumed by the function that + emits CADF events (_send_audit_notification). Specifically the + ``resource_type`` (role, user, etc) must be transformed into a CADF + keyword, such as: ``data/security/role``. The ``resource_id`` is added as a + top level value for the ``resource_info`` key. Lastly, the ``operation`` is + used to create the CADF ``action``, and the ``event_type`` name. + + As per the CADF specification, the ``action`` must start with create, + update, delete, etc... i.e.: created.user or deleted.role + + However the ``event_type`` is an OpenStack-ism that is typically of the + form project.resource.operation. i.e.: identity.project.updated + + :param operation: operation being performed (created, updated, or deleted) + :param resource_type: type of resource being operated on (role, user, etc) + :param resource_id: ID of resource being operated on + :param outcome: outcomes of the operation (SUCCESS, FAILURE, etc) + :param initiator: CADF representation of the user that created the request + """ + + if resource_type not in CADF_TYPE_MAP: + target_uri = taxonomy.UNKNOWN + else: + target_uri = CADF_TYPE_MAP.get(resource_type) + target = resource.Resource(typeURI=target_uri, + id=resource_id) + + audit_kwargs = {'resource_info': resource_id} + cadf_action = '%s.%s' % (operation, resource_type) + event_type = '%s.%s.%s' % (SERVICE, resource_type, operation) + + _send_audit_notification(cadf_action, initiator, outcome, + target, event_type, **audit_kwargs) + + +def _send_notification(operation, resource_type, resource_id, public=True): + """Send notification to inform observers about the affected resource. + + This method doesn't raise an exception when sending the notification fails. + + :param operation: operation being performed (created, updated, or deleted) + :param resource_type: type of resource being operated on + :param resource_id: ID of resource being operated on + :param public: if True (default), the event will be sent + to the notifier API. + if False, the event will only be sent via + notify_event_callbacks to in process listeners. + """ + payload = {'resource_info': resource_id} + + notify_event_callbacks(SERVICE, resource_type, operation, payload) + + # Only send this notification if the 'basic' format is used, otherwise + # let the CADF functions handle sending the notification. But we check + # here so as to not disrupt the notify_event_callbacks function. + if public and CONF.notification_format == 'basic': + notifier = _get_notifier() + if notifier: + context = {} + event_type = '%(service)s.%(resource_type)s.%(operation)s' % { + 'service': SERVICE, + 'resource_type': resource_type, + 'operation': operation} + try: + notifier.info(context, event_type, payload) + except Exception: + LOG.exception(_LE( + 'Failed to send %(res_id)s %(event_type)s notification'), + {'res_id': resource_id, 'event_type': event_type}) + + +def _get_request_audit_info(context, user_id=None): + """Collect audit information about the request used for CADF. + + :param context: Request context + :param user_id: Optional user ID, alternatively collected from context + :returns: Auditing data about the request + :rtype: :class:`pycadf.Resource` + """ + + remote_addr = None + http_user_agent = None + project_id = None + domain_id = None + + if context and 'environment' in context and context['environment']: + environment = context['environment'] + remote_addr = environment.get('REMOTE_ADDR') + http_user_agent = environment.get('HTTP_USER_AGENT') + if not user_id: + user_id = environment.get('KEYSTONE_AUTH_CONTEXT', + {}).get('user_id') + project_id = environment.get('KEYSTONE_AUTH_CONTEXT', + {}).get('project_id') + domain_id = environment.get('KEYSTONE_AUTH_CONTEXT', + {}).get('domain_id') + + host = pycadf.host.Host(address=remote_addr, agent=http_user_agent) + initiator = resource.Resource(typeURI=taxonomy.ACCOUNT_USER, + id=user_id, host=host) + if project_id: + initiator.project_id = project_id + if domain_id: + initiator.domain_id = domain_id + + return initiator + + +class CadfNotificationWrapper(object): + """Send CADF event notifications for various methods. + + This function is only used for Authentication events. Its ``action`` and + ``event_type`` are dictated below. + + - action: authenticate + - event_type: identity.authenticate + + Sends CADF notifications for events such as whether an authentication was + successful or not. + + :param operation: The authentication related action being performed + + """ + + def __init__(self, operation): + self.action = operation + self.event_type = '%s.%s' % (SERVICE, operation) + + def __call__(self, f): + def wrapper(wrapped_self, context, user_id, *args, **kwargs): + """Always send a notification.""" + + initiator = _get_request_audit_info(context, user_id) + target = resource.Resource(typeURI=taxonomy.ACCOUNT_USER) + try: + result = f(wrapped_self, context, user_id, *args, **kwargs) + except Exception: + # For authentication failure send a cadf event as well + _send_audit_notification(self.action, initiator, + taxonomy.OUTCOME_FAILURE, + target, self.event_type) + raise + else: + _send_audit_notification(self.action, initiator, + taxonomy.OUTCOME_SUCCESS, + target, self.event_type) + return result + + return wrapper + + +class CadfRoleAssignmentNotificationWrapper(object): + """Send CADF notifications for ``role_assignment`` methods. + + This function is only used for role assignment events. Its ``action`` and + ``event_type`` are dictated below. + + - action: created.role_assignment or deleted.role_assignment + - event_type: identity.role_assignment.created or + identity.role_assignment.deleted + + Sends a CADF notification if the wrapped method does not raise an + ``Exception`` (such as ``keystone.exception.NotFound``). + + :param operation: one of the values from ACTIONS (create or delete) + """ + + ROLE_ASSIGNMENT = 'role_assignment' + + def __init__(self, operation): + self.action = '%s.%s' % (operation, self.ROLE_ASSIGNMENT) + self.event_type = '%s.%s.%s' % (SERVICE, operation, + self.ROLE_ASSIGNMENT) + + def __call__(self, f): + def wrapper(wrapped_self, role_id, *args, **kwargs): + """Send a notification if the wrapped callable is successful.""" + + """ NOTE(stevemar): The reason we go through checking kwargs + and args for possible target and actor values is because the + create_grant() (and delete_grant()) method are called + differently in various tests. + Using named arguments, i.e.: + create_grant(user_id=user['id'], domain_id=domain['id'], + role_id=role['id']) + + Or, using positional arguments, i.e.: + create_grant(role_id['id'], user['id'], None, + domain_id=domain['id'], None) + + Or, both, i.e.: + create_grant(role_id['id'], user_id=user['id'], + domain_id=domain['id']) + + Checking the values for kwargs is easy enough, since it comes + in as a dictionary + + The actual method signature is + create_grant(role_id, user_id=None, group_id=None, + domain_id=None, project_id=None, + inherited_to_projects=False) + + So, if the values of actor or target are still None after + checking kwargs, we can check the positional arguments, + based on the method signature. + """ + call_args = inspect.getcallargs( + f, wrapped_self, role_id, *args, **kwargs) + inherited = call_args['inherited_to_projects'] + context = call_args['context'] + + initiator = _get_request_audit_info(context) + target = resource.Resource(typeURI=taxonomy.ACCOUNT_USER) + + audit_kwargs = {} + if call_args['project_id']: + audit_kwargs['project'] = call_args['project_id'] + elif call_args['domain_id']: + audit_kwargs['domain'] = call_args['domain_id'] + + if call_args['user_id']: + audit_kwargs['user'] = call_args['user_id'] + elif call_args['group_id']: + audit_kwargs['group'] = call_args['group_id'] + + audit_kwargs['inherited_to_projects'] = inherited + audit_kwargs['role'] = role_id + + try: + result = f(wrapped_self, role_id, *args, **kwargs) + except Exception: + _send_audit_notification(self.action, initiator, + taxonomy.OUTCOME_FAILURE, + target, self.event_type, + **audit_kwargs) + raise + else: + _send_audit_notification(self.action, initiator, + taxonomy.OUTCOME_SUCCESS, + target, self.event_type, + **audit_kwargs) + return result + + return wrapper + + +def send_saml_audit_notification(action, context, user_id, group_ids, + identity_provider, protocol, token_id, + outcome): + """Send notification to inform observers about SAML events. + + :param action: Action being audited + :type action: str + :param context: Current request context to collect request info from + :type context: dict + :param user_id: User ID from Keystone token + :type user_id: str + :param group_ids: List of Group IDs from Keystone token + :type group_ids: list + :param identity_provider: ID of the IdP from the Keystone token + :type identity_provider: str or None + :param protocol: Protocol ID for IdP from the Keystone token + :type protocol: str + :param token_id: audit_id from Keystone token + :type token_id: str or None + :param outcome: One of :class:`pycadf.cadftaxonomy` + :type outcome: str + """ + + initiator = _get_request_audit_info(context) + target = resource.Resource(typeURI=taxonomy.ACCOUNT_USER) + audit_type = SAML_AUDIT_TYPE + user_id = user_id or taxonomy.UNKNOWN + token_id = token_id or taxonomy.UNKNOWN + group_ids = group_ids or [] + cred = credential.FederatedCredential(token=token_id, type=audit_type, + identity_provider=identity_provider, + user=user_id, groups=group_ids) + initiator.credential = cred + event_type = '%s.%s' % (SERVICE, action) + _send_audit_notification(action, initiator, outcome, target, event_type) + + +def _send_audit_notification(action, initiator, outcome, target, + event_type, **kwargs): + """Send CADF notification to inform observers about the affected resource. + + This method logs an exception when sending the notification fails. + + :param action: CADF action being audited (e.g., 'authenticate') + :param initiator: CADF resource representing the initiator + :param outcome: The CADF outcome (taxonomy.OUTCOME_PENDING, + taxonomy.OUTCOME_SUCCESS, taxonomy.OUTCOME_FAILURE) + :param target: CADF resource representing the target + :param event_type: An OpenStack-ism, typically this is the meter name that + Ceilometer uses to poll events. + :param kwargs: Any additional arguments passed in will be added as + key-value pairs to the CADF event. + + """ + + event = eventfactory.EventFactory().new_event( + eventType=cadftype.EVENTTYPE_ACTIVITY, + outcome=outcome, + action=action, + initiator=initiator, + target=target, + observer=resource.Resource(typeURI=taxonomy.SERVICE_SECURITY)) + + for key, value in kwargs.items(): + setattr(event, key, value) + + context = {} + payload = event.as_dict() + notifier = _get_notifier() + + if notifier: + try: + notifier.info(context, event_type, payload) + except Exception: + # diaper defense: any exception that occurs while emitting the + # notification should not interfere with the API request + LOG.exception(_LE( + 'Failed to send %(action)s %(event_type)s notification'), + {'action': action, 'event_type': event_type}) + + +emit_event = CadfNotificationWrapper + + +role_assignment = CadfRoleAssignmentNotificationWrapper diff --git a/keystone-moon/keystone/openstack/__init__.py b/keystone-moon/keystone/openstack/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/openstack/common/README b/keystone-moon/keystone/openstack/common/README new file mode 100644 index 00000000..0700c72b --- /dev/null +++ b/keystone-moon/keystone/openstack/common/README @@ -0,0 +1,13 @@ +openstack-common +---------------- + +A number of modules from openstack-common are imported into this project. + +These modules are "incubating" in openstack-common and are kept in sync +with the help of openstack-common's update.py script. See: + + https://wiki.openstack.org/wiki/Oslo#Syncing_Code_from_Incubator + +The copy of the code should never be directly modified here. Please +always update openstack-common first and then run the script to copy +the changes across. diff --git a/keystone-moon/keystone/openstack/common/__init__.py b/keystone-moon/keystone/openstack/common/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/openstack/common/_i18n.py b/keystone-moon/keystone/openstack/common/_i18n.py new file mode 100644 index 00000000..76a74c05 --- /dev/null +++ b/keystone-moon/keystone/openstack/common/_i18n.py @@ -0,0 +1,45 @@ +# 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. + +"""oslo.i18n integration module. + +See http://docs.openstack.org/developer/oslo.i18n/usage.html + +""" + +try: + import oslo_i18n + + # NOTE(dhellmann): This reference to o-s-l-o will be replaced by the + # application name when this module is synced into the separate + # 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='keystone') + + # The primary translation function using the well-known name "_" + _ = _translators.primary + + # Translators for log levels. + # + # The abbreviated names are meant to reflect the usual use of a short + # name like '_'. The "L" is for "log" and the other letter comes from + # the level. + _LI = _translators.log_info + _LW = _translators.log_warning + _LE = _translators.log_error + _LC = _translators.log_critical +except ImportError: + # NOTE(dims): Support for cases where a project wants to use + # code from oslo-incubator, but is not ready to be internationalized + # (like tempest) + _ = _LI = _LW = _LE = _LC = lambda x: x diff --git a/keystone-moon/keystone/openstack/common/eventlet_backdoor.py b/keystone-moon/keystone/openstack/common/eventlet_backdoor.py new file mode 100644 index 00000000..c656d81b --- /dev/null +++ b/keystone-moon/keystone/openstack/common/eventlet_backdoor.py @@ -0,0 +1,151 @@ +# Copyright (c) 2012 OpenStack Foundation. +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# 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 __future__ import print_function + +import copy +import errno +import gc +import logging +import os +import pprint +import socket +import sys +import traceback + +import eventlet.backdoor +import greenlet +from oslo_config import cfg + +from keystone.openstack.common._i18n import _LI + +help_for_backdoor_port = ( + "Acceptable values are 0, , and :, where 0 results " + "in listening on a random tcp port number; results in listening " + "on the specified port number (and not enabling backdoor if that port " + "is in use); and : results in listening on the smallest " + "unused port number within the specified range of port numbers. The " + "chosen port is displayed in the service's log file.") +eventlet_backdoor_opts = [ + cfg.StrOpt('backdoor_port', + help="Enable eventlet backdoor. %s" % help_for_backdoor_port) +] + +CONF = cfg.CONF +CONF.register_opts(eventlet_backdoor_opts) +LOG = logging.getLogger(__name__) + + +def list_opts(): + """Entry point for oslo-config-generator. + """ + return [(None, copy.deepcopy(eventlet_backdoor_opts))] + + +class EventletBackdoorConfigValueError(Exception): + def __init__(self, port_range, help_msg, ex): + msg = ('Invalid backdoor_port configuration %(range)s: %(ex)s. ' + '%(help)s' % + {'range': port_range, 'ex': ex, 'help': help_msg}) + super(EventletBackdoorConfigValueError, self).__init__(msg) + self.port_range = port_range + + +def _dont_use_this(): + print("Don't use this, just disconnect instead") + + +def _find_objects(t): + return [o for o in gc.get_objects() if isinstance(o, t)] + + +def _print_greenthreads(): + for i, gt in enumerate(_find_objects(greenlet.greenlet)): + print(i, gt) + traceback.print_stack(gt.gr_frame) + print() + + +def _print_nativethreads(): + for threadId, stack in sys._current_frames().items(): + print(threadId) + traceback.print_stack(stack) + print() + + +def _parse_port_range(port_range): + if ':' not in port_range: + start, end = port_range, port_range + else: + start, end = port_range.split(':', 1) + try: + start, end = int(start), int(end) + if end < start: + raise ValueError + return start, end + except ValueError as ex: + raise EventletBackdoorConfigValueError(port_range, ex, + help_for_backdoor_port) + + +def _listen(host, start_port, end_port, listen_func): + try_port = start_port + while True: + try: + return listen_func((host, try_port)) + except socket.error as exc: + if (exc.errno != errno.EADDRINUSE or + try_port >= end_port): + raise + try_port += 1 + + +def initialize_if_enabled(): + backdoor_locals = { + 'exit': _dont_use_this, # So we don't exit the entire process + 'quit': _dont_use_this, # So we don't exit the entire process + 'fo': _find_objects, + 'pgt': _print_greenthreads, + 'pnt': _print_nativethreads, + } + + if CONF.backdoor_port is None: + return None + + start_port, end_port = _parse_port_range(str(CONF.backdoor_port)) + + # NOTE(johannes): The standard sys.displayhook will print the value of + # the last expression and set it to __builtin__._, which overwrites + # the __builtin__._ that gettext sets. Let's switch to using pprint + # since it won't interact poorly with gettext, and it's easier to + # read the output too. + def displayhook(val): + if val is not None: + pprint.pprint(val) + sys.displayhook = displayhook + + sock = _listen('localhost', start_port, end_port, eventlet.listen) + + # In the case of backdoor port being zero, a port number is assigned by + # listen(). In any case, pull the port number out here. + port = sock.getsockname()[1] + LOG.info( + _LI('Eventlet backdoor listening on %(port)s for process %(pid)d') % + {'port': port, 'pid': os.getpid()} + ) + eventlet.spawn_n(eventlet.backdoor.backdoor_server, sock, + locals=backdoor_locals) + return port diff --git a/keystone-moon/keystone/openstack/common/fileutils.py b/keystone-moon/keystone/openstack/common/fileutils.py new file mode 100644 index 00000000..9097c35d --- /dev/null +++ b/keystone-moon/keystone/openstack/common/fileutils.py @@ -0,0 +1,149 @@ +# Copyright 2011 OpenStack Foundation. +# All Rights Reserved. +# +# 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 contextlib +import errno +import logging +import os +import stat +import tempfile + +from oslo_utils import excutils + +LOG = logging.getLogger(__name__) + +_FILE_CACHE = {} +DEFAULT_MODE = stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO + + +def ensure_tree(path, mode=DEFAULT_MODE): + """Create a directory (and any ancestor directories required) + + :param path: Directory to create + :param mode: Directory creation permissions + """ + try: + os.makedirs(path, mode) + except OSError as exc: + if exc.errno == errno.EEXIST: + if not os.path.isdir(path): + raise + else: + raise + + +def read_cached_file(filename, force_reload=False): + """Read from a file if it has been modified. + + :param force_reload: Whether to reload the file. + :returns: A tuple with a boolean specifying if the data is fresh + or not. + """ + global _FILE_CACHE + + if force_reload: + delete_cached_file(filename) + + reloaded = False + mtime = os.path.getmtime(filename) + cache_info = _FILE_CACHE.setdefault(filename, {}) + + if not cache_info or mtime > cache_info.get('mtime', 0): + LOG.debug("Reloading cached file %s" % filename) + with open(filename) as fap: + cache_info['data'] = fap.read() + cache_info['mtime'] = mtime + reloaded = True + return (reloaded, cache_info['data']) + + +def delete_cached_file(filename): + """Delete cached file if present. + + :param filename: filename to delete + """ + global _FILE_CACHE + + if filename in _FILE_CACHE: + del _FILE_CACHE[filename] + + +def delete_if_exists(path, remove=os.unlink): + """Delete a file, but ignore file not found error. + + :param path: File to delete + :param remove: Optional function to remove passed path + """ + + try: + remove(path) + except OSError as e: + if e.errno != errno.ENOENT: + raise + + +@contextlib.contextmanager +def remove_path_on_error(path, remove=delete_if_exists): + """Protect code that wants to operate on PATH atomically. + Any exception will cause PATH to be removed. + + :param path: File to work with + :param remove: Optional function to remove passed path + """ + + try: + yield + except Exception: + with excutils.save_and_reraise_exception(): + remove(path) + + +def file_open(*args, **kwargs): + """Open file + + see built-in open() documentation for more details + + Note: The reason this is kept in a separate module is to easily + be able to provide a stub module that doesn't alter system + state at all (for unit tests) + """ + return open(*args, **kwargs) + + +def write_to_tempfile(content, path=None, suffix='', prefix='tmp'): + """Create temporary file or use existing file. + + This util is needed for creating temporary file with + specified content, suffix and prefix. If path is not None, + it will be used for writing content. If the path doesn't + exist it'll be created. + + :param content: content for temporary file. + :param path: same as parameter 'dir' for mkstemp + :param suffix: same as parameter 'suffix' for mkstemp + :param prefix: same as parameter 'prefix' for mkstemp + + For example: it can be used in database tests for creating + configuration files. + """ + if path: + ensure_tree(path) + + (fd, path) = tempfile.mkstemp(suffix=suffix, dir=path, prefix=prefix) + try: + os.write(fd, content) + finally: + os.close(fd) + return path diff --git a/keystone-moon/keystone/openstack/common/loopingcall.py b/keystone-moon/keystone/openstack/common/loopingcall.py new file mode 100644 index 00000000..39eed47d --- /dev/null +++ b/keystone-moon/keystone/openstack/common/loopingcall.py @@ -0,0 +1,147 @@ +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# Copyright 2011 Justin Santa Barbara +# All Rights Reserved. +# +# 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 sys +import time + +from eventlet import event +from eventlet import greenthread + +from keystone.openstack.common._i18n import _LE, _LW + +LOG = logging.getLogger(__name__) + +# NOTE(zyluo): This lambda function was declared to avoid mocking collisions +# with time.time() called in the standard logging module +# during unittests. +_ts = lambda: time.time() + + +class LoopingCallDone(Exception): + """Exception to break out and stop a LoopingCallBase. + + The poll-function passed to LoopingCallBase can raise this exception to + break out of the loop normally. This is somewhat analogous to + StopIteration. + + An optional return-value can be included as the argument to the exception; + this return-value will be returned by LoopingCallBase.wait() + + """ + + def __init__(self, retvalue=True): + """:param retvalue: Value that LoopingCallBase.wait() should return.""" + self.retvalue = retvalue + + +class LoopingCallBase(object): + def __init__(self, f=None, *args, **kw): + self.args = args + self.kw = kw + self.f = f + self._running = False + self.done = None + + def stop(self): + self._running = False + + def wait(self): + return self.done.wait() + + +class FixedIntervalLoopingCall(LoopingCallBase): + """A fixed interval looping call.""" + + def start(self, interval, initial_delay=None): + self._running = True + done = event.Event() + + def _inner(): + if initial_delay: + greenthread.sleep(initial_delay) + + try: + while self._running: + start = _ts() + self.f(*self.args, **self.kw) + end = _ts() + if not self._running: + break + delay = end - start - interval + if delay > 0: + LOG.warn(_LW('task %(func_name)r run outlasted ' + 'interval by %(delay).2f sec'), + {'func_name': self.f, 'delay': delay}) + greenthread.sleep(-delay if delay < 0 else 0) + except LoopingCallDone as e: + self.stop() + done.send(e.retvalue) + except Exception: + LOG.exception(_LE('in fixed duration looping call')) + done.send_exception(*sys.exc_info()) + return + else: + done.send(True) + + self.done = done + + greenthread.spawn_n(_inner) + return self.done + + +class DynamicLoopingCall(LoopingCallBase): + """A looping call which sleeps until the next known event. + + The function called should return how long to sleep for before being + called again. + """ + + def start(self, initial_delay=None, periodic_interval_max=None): + self._running = True + done = event.Event() + + def _inner(): + if initial_delay: + greenthread.sleep(initial_delay) + + try: + while self._running: + idle = self.f(*self.args, **self.kw) + if not self._running: + break + + if periodic_interval_max is not None: + idle = min(idle, periodic_interval_max) + LOG.debug('Dynamic looping call %(func_name)r sleeping ' + 'for %(idle).02f seconds', + {'func_name': self.f, 'idle': idle}) + greenthread.sleep(idle) + except LoopingCallDone as e: + self.stop() + done.send(e.retvalue) + except Exception: + LOG.exception(_LE('in dynamic looping call')) + done.send_exception(*sys.exc_info()) + return + else: + done.send(True) + + self.done = done + + greenthread.spawn(_inner) + return self.done diff --git a/keystone-moon/keystone/openstack/common/service.py b/keystone-moon/keystone/openstack/common/service.py new file mode 100644 index 00000000..cfae56b7 --- /dev/null +++ b/keystone-moon/keystone/openstack/common/service.py @@ -0,0 +1,495 @@ +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# Copyright 2011 Justin Santa Barbara +# All Rights Reserved. +# +# 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. + +"""Generic Node base class for all workers that run on hosts.""" + +import errno +import logging +import os +import random +import signal +import sys +import time + +try: + # Importing just the symbol here because the io module does not + # exist in Python 2.6. + from io import UnsupportedOperation # noqa +except ImportError: + # Python 2.6 + UnsupportedOperation = None + +import eventlet +from eventlet import event +from oslo_config import cfg + +from keystone.openstack.common import eventlet_backdoor +from keystone.openstack.common._i18n import _LE, _LI, _LW +from keystone.openstack.common import systemd +from keystone.openstack.common import threadgroup + + +CONF = cfg.CONF +LOG = logging.getLogger(__name__) + + +def _sighup_supported(): + return hasattr(signal, 'SIGHUP') + + +def _is_daemon(): + # The process group for a foreground process will match the + # process group of the controlling terminal. If those values do + # not match, or ioctl() fails on the stdout file handle, we assume + # the process is running in the background as a daemon. + # http://www.gnu.org/software/bash/manual/bashref.html#Job-Control-Basics + try: + is_daemon = os.getpgrp() != os.tcgetpgrp(sys.stdout.fileno()) + except OSError as err: + if err.errno == errno.ENOTTY: + # Assume we are a daemon because there is no terminal. + is_daemon = True + else: + raise + except UnsupportedOperation: + # Could not get the fileno for stdout, so we must be a daemon. + is_daemon = True + return is_daemon + + +def _is_sighup_and_daemon(signo): + if not (_sighup_supported() and signo == signal.SIGHUP): + # Avoid checking if we are a daemon, because the signal isn't + # SIGHUP. + return False + return _is_daemon() + + +def _signo_to_signame(signo): + signals = {signal.SIGTERM: 'SIGTERM', + signal.SIGINT: 'SIGINT'} + if _sighup_supported(): + signals[signal.SIGHUP] = 'SIGHUP' + return signals[signo] + + +def _set_signals_handler(handler): + signal.signal(signal.SIGTERM, handler) + signal.signal(signal.SIGINT, handler) + if _sighup_supported(): + signal.signal(signal.SIGHUP, handler) + + +class Launcher(object): + """Launch one or more services and wait for them to complete.""" + + def __init__(self): + """Initialize the service launcher. + + :returns: None + + """ + self.services = Services() + self.backdoor_port = eventlet_backdoor.initialize_if_enabled() + + def launch_service(self, service): + """Load and start the given service. + + :param service: The service you would like to start. + :returns: None + + """ + service.backdoor_port = self.backdoor_port + self.services.add(service) + + def stop(self): + """Stop all services which are currently running. + + :returns: None + + """ + self.services.stop() + + def wait(self): + """Waits until all services have been stopped, and then returns. + + :returns: None + + """ + self.services.wait() + + def restart(self): + """Reload config files and restart service. + + :returns: None + + """ + cfg.CONF.reload_config_files() + self.services.restart() + + +class SignalExit(SystemExit): + def __init__(self, signo, exccode=1): + super(SignalExit, self).__init__(exccode) + self.signo = signo + + +class ServiceLauncher(Launcher): + def _handle_signal(self, signo, frame): + # Allow the process to be killed again and die from natural causes + _set_signals_handler(signal.SIG_DFL) + raise SignalExit(signo) + + def handle_signal(self): + _set_signals_handler(self._handle_signal) + + def _wait_for_exit_or_signal(self, ready_callback=None): + status = None + signo = 0 + + LOG.debug('Full set of CONF:') + CONF.log_opt_values(LOG, logging.DEBUG) + + try: + if ready_callback: + ready_callback() + super(ServiceLauncher, self).wait() + except SignalExit as exc: + signame = _signo_to_signame(exc.signo) + LOG.info(_LI('Caught %s, exiting'), signame) + status = exc.code + signo = exc.signo + except SystemExit as exc: + status = exc.code + finally: + self.stop() + + return status, signo + + def wait(self, ready_callback=None): + systemd.notify_once() + while True: + self.handle_signal() + status, signo = self._wait_for_exit_or_signal(ready_callback) + if not _is_sighup_and_daemon(signo): + return status + self.restart() + + +class ServiceWrapper(object): + def __init__(self, service, workers): + self.service = service + self.workers = workers + self.children = set() + self.forktimes = [] + + +class ProcessLauncher(object): + def __init__(self): + """Constructor.""" + + self.children = {} + self.sigcaught = None + self.running = True + rfd, self.writepipe = os.pipe() + self.readpipe = eventlet.greenio.GreenPipe(rfd, 'r') + self.handle_signal() + + def handle_signal(self): + _set_signals_handler(self._handle_signal) + + def _handle_signal(self, signo, frame): + self.sigcaught = signo + self.running = False + + # Allow the process to be killed again and die from natural causes + _set_signals_handler(signal.SIG_DFL) + + def _pipe_watcher(self): + # This will block until the write end is closed when the parent + # dies unexpectedly + self.readpipe.read() + + LOG.info(_LI('Parent process has died unexpectedly, exiting')) + + sys.exit(1) + + def _child_process_handle_signal(self): + # Setup child signal handlers differently + def _sigterm(*args): + signal.signal(signal.SIGTERM, signal.SIG_DFL) + raise SignalExit(signal.SIGTERM) + + def _sighup(*args): + signal.signal(signal.SIGHUP, signal.SIG_DFL) + raise SignalExit(signal.SIGHUP) + + signal.signal(signal.SIGTERM, _sigterm) + if _sighup_supported(): + signal.signal(signal.SIGHUP, _sighup) + # Block SIGINT and let the parent send us a SIGTERM + signal.signal(signal.SIGINT, signal.SIG_IGN) + + def _child_wait_for_exit_or_signal(self, launcher): + status = 0 + signo = 0 + + # NOTE(johannes): All exceptions are caught to ensure this + # doesn't fallback into the loop spawning children. It would + # be bad for a child to spawn more children. + try: + launcher.wait() + except SignalExit as exc: + signame = _signo_to_signame(exc.signo) + LOG.info(_LI('Child caught %s, exiting'), signame) + status = exc.code + signo = exc.signo + except SystemExit as exc: + status = exc.code + except BaseException: + LOG.exception(_LE('Unhandled exception')) + status = 2 + finally: + launcher.stop() + + return status, signo + + def _child_process(self, service): + self._child_process_handle_signal() + + # Reopen the eventlet hub to make sure we don't share an epoll + # fd with parent and/or siblings, which would be bad + eventlet.hubs.use_hub() + + # Close write to ensure only parent has it open + os.close(self.writepipe) + # Create greenthread to watch for parent to close pipe + eventlet.spawn_n(self._pipe_watcher) + + # Reseed random number generator + random.seed() + + launcher = Launcher() + launcher.launch_service(service) + return launcher + + def _start_child(self, wrap): + if len(wrap.forktimes) > wrap.workers: + # Limit ourselves to one process a second (over the period of + # number of workers * 1 second). This will allow workers to + # start up quickly but ensure we don't fork off children that + # die instantly too quickly. + if time.time() - wrap.forktimes[0] < wrap.workers: + LOG.info(_LI('Forking too fast, sleeping')) + time.sleep(1) + + wrap.forktimes.pop(0) + + wrap.forktimes.append(time.time()) + + pid = os.fork() + if pid == 0: + launcher = self._child_process(wrap.service) + while True: + self._child_process_handle_signal() + status, signo = self._child_wait_for_exit_or_signal(launcher) + if not _is_sighup_and_daemon(signo): + break + launcher.restart() + + os._exit(status) + + LOG.info(_LI('Started child %d'), pid) + + wrap.children.add(pid) + self.children[pid] = wrap + + return pid + + def launch_service(self, service, workers=1): + wrap = ServiceWrapper(service, workers) + + LOG.info(_LI('Starting %d workers'), wrap.workers) + while self.running and len(wrap.children) < wrap.workers: + self._start_child(wrap) + + def _wait_child(self): + try: + # Block while any of child processes have exited + pid, status = os.waitpid(0, 0) + if not pid: + return None + except OSError as exc: + if exc.errno not in (errno.EINTR, errno.ECHILD): + raise + return None + + if os.WIFSIGNALED(status): + sig = os.WTERMSIG(status) + LOG.info(_LI('Child %(pid)d killed by signal %(sig)d'), + dict(pid=pid, sig=sig)) + else: + code = os.WEXITSTATUS(status) + LOG.info(_LI('Child %(pid)s exited with status %(code)d'), + dict(pid=pid, code=code)) + + if pid not in self.children: + LOG.warning(_LW('pid %d not in child list'), pid) + return None + + wrap = self.children.pop(pid) + wrap.children.remove(pid) + return wrap + + def _respawn_children(self): + while self.running: + wrap = self._wait_child() + if not wrap: + continue + while self.running and len(wrap.children) < wrap.workers: + self._start_child(wrap) + + def wait(self): + """Loop waiting on children to die and respawning as necessary.""" + + systemd.notify_once() + LOG.debug('Full set of CONF:') + CONF.log_opt_values(LOG, logging.DEBUG) + + try: + while True: + self.handle_signal() + self._respawn_children() + # No signal means that stop was called. Don't clean up here. + if not self.sigcaught: + return + + signame = _signo_to_signame(self.sigcaught) + LOG.info(_LI('Caught %s, stopping children'), signame) + if not _is_sighup_and_daemon(self.sigcaught): + break + + for pid in self.children: + os.kill(pid, signal.SIGHUP) + self.running = True + self.sigcaught = None + except eventlet.greenlet.GreenletExit: + LOG.info(_LI("Wait called after thread killed. Cleaning up.")) + + self.stop() + + def stop(self): + """Terminate child processes and wait on each.""" + self.running = False + for pid in self.children: + try: + os.kill(pid, signal.SIGTERM) + except OSError as exc: + if exc.errno != errno.ESRCH: + raise + + # Wait for children to die + if self.children: + LOG.info(_LI('Waiting on %d children to exit'), len(self.children)) + while self.children: + self._wait_child() + + +class Service(object): + """Service object for binaries running on hosts.""" + + def __init__(self, threads=1000): + self.tg = threadgroup.ThreadGroup(threads) + + # signal that the service is done shutting itself down: + self._done = event.Event() + + def reset(self): + # NOTE(Fengqian): docs for Event.reset() recommend against using it + self._done = event.Event() + + def start(self): + pass + + def stop(self, graceful=False): + self.tg.stop(graceful) + self.tg.wait() + # Signal that service cleanup is done: + if not self._done.ready(): + self._done.send() + + def wait(self): + self._done.wait() + + +class Services(object): + + def __init__(self): + self.services = [] + self.tg = threadgroup.ThreadGroup() + self.done = event.Event() + + def add(self, service): + self.services.append(service) + self.tg.add_thread(self.run_service, service, self.done) + + def stop(self): + # wait for graceful shutdown of services: + for service in self.services: + service.stop() + service.wait() + + # Each service has performed cleanup, now signal that the run_service + # wrapper threads can now die: + if not self.done.ready(): + self.done.send() + + # reap threads: + self.tg.stop() + + def wait(self): + self.tg.wait() + + def restart(self): + self.stop() + self.done = event.Event() + for restart_service in self.services: + restart_service.reset() + self.tg.add_thread(self.run_service, restart_service, self.done) + + @staticmethod + def run_service(service, done): + """Service start wrapper. + + :param service: service to run + :param done: event to wait on until a shutdown is triggered + :returns: None + + """ + service.start() + done.wait() + + +def launch(service, workers=1): + if workers is None or workers == 1: + launcher = ServiceLauncher() + launcher.launch_service(service) + else: + launcher = ProcessLauncher() + launcher.launch_service(service, workers=workers) + + return launcher diff --git a/keystone-moon/keystone/openstack/common/systemd.py b/keystone-moon/keystone/openstack/common/systemd.py new file mode 100644 index 00000000..36243b34 --- /dev/null +++ b/keystone-moon/keystone/openstack/common/systemd.py @@ -0,0 +1,105 @@ +# Copyright 2012-2014 Red Hat, Inc. +# +# 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. + +""" +Helper module for systemd service readiness notification. +""" + +import logging +import os +import socket +import sys + + +LOG = logging.getLogger(__name__) + + +def _abstractify(socket_name): + if socket_name.startswith('@'): + # abstract namespace socket + socket_name = '\0%s' % socket_name[1:] + return socket_name + + +def _sd_notify(unset_env, msg): + notify_socket = os.getenv('NOTIFY_SOCKET') + if notify_socket: + sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) + try: + sock.connect(_abstractify(notify_socket)) + sock.sendall(msg) + if unset_env: + del os.environ['NOTIFY_SOCKET'] + except EnvironmentError: + LOG.debug("Systemd notification failed", exc_info=True) + finally: + sock.close() + + +def notify(): + """Send notification to Systemd that service is ready. + + For details see + http://www.freedesktop.org/software/systemd/man/sd_notify.html + """ + _sd_notify(False, 'READY=1') + + +def notify_once(): + """Send notification once to Systemd that service is ready. + + Systemd sets NOTIFY_SOCKET environment variable with the name of the + socket listening for notifications from services. + This method removes the NOTIFY_SOCKET environment variable to ensure + notification is sent only once. + """ + _sd_notify(True, 'READY=1') + + +def onready(notify_socket, timeout): + """Wait for systemd style notification on the socket. + + :param notify_socket: local socket address + :type notify_socket: string + :param timeout: socket timeout + :type timeout: float + :returns: 0 service ready + 1 service not ready + 2 timeout occurred + """ + sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) + sock.settimeout(timeout) + sock.bind(_abstractify(notify_socket)) + try: + msg = sock.recv(512) + except socket.timeout: + return 2 + finally: + sock.close() + if 'READY=1' in msg: + return 0 + else: + return 1 + + +if __name__ == '__main__': + # simple CLI for testing + if len(sys.argv) == 1: + notify() + elif len(sys.argv) >= 2: + timeout = float(sys.argv[1]) + notify_socket = os.getenv('NOTIFY_SOCKET') + if notify_socket: + retval = onready(notify_socket, timeout) + sys.exit(retval) diff --git a/keystone-moon/keystone/openstack/common/threadgroup.py b/keystone-moon/keystone/openstack/common/threadgroup.py new file mode 100644 index 00000000..fc0bcb53 --- /dev/null +++ b/keystone-moon/keystone/openstack/common/threadgroup.py @@ -0,0 +1,149 @@ +# Copyright 2012 Red Hat, Inc. +# +# 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 threading + +import eventlet +from eventlet import greenpool + +from keystone.openstack.common import loopingcall + + +LOG = logging.getLogger(__name__) + + +def _thread_done(gt, *args, **kwargs): + """Callback function to be passed to GreenThread.link() when we spawn() + Calls the :class:`ThreadGroup` to notify if. + + """ + kwargs['group'].thread_done(kwargs['thread']) + + +class Thread(object): + """Wrapper around a greenthread, that holds a reference to the + :class:`ThreadGroup`. The Thread will notify the :class:`ThreadGroup` when + it has done so it can be removed from the threads list. + """ + def __init__(self, thread, group): + self.thread = thread + self.thread.link(_thread_done, group=group, thread=self) + + def stop(self): + self.thread.kill() + + def wait(self): + return self.thread.wait() + + def link(self, func, *args, **kwargs): + self.thread.link(func, *args, **kwargs) + + +class ThreadGroup(object): + """The point of the ThreadGroup class is to: + + * keep track of timers and greenthreads (making it easier to stop them + when need be). + * provide an easy API to add timers. + """ + def __init__(self, thread_pool_size=10): + self.pool = greenpool.GreenPool(thread_pool_size) + self.threads = [] + self.timers = [] + + def add_dynamic_timer(self, callback, initial_delay=None, + periodic_interval_max=None, *args, **kwargs): + timer = loopingcall.DynamicLoopingCall(callback, *args, **kwargs) + timer.start(initial_delay=initial_delay, + periodic_interval_max=periodic_interval_max) + self.timers.append(timer) + + def add_timer(self, interval, callback, initial_delay=None, + *args, **kwargs): + pulse = loopingcall.FixedIntervalLoopingCall(callback, *args, **kwargs) + pulse.start(interval=interval, + initial_delay=initial_delay) + self.timers.append(pulse) + + def add_thread(self, callback, *args, **kwargs): + gt = self.pool.spawn(callback, *args, **kwargs) + th = Thread(gt, self) + self.threads.append(th) + return th + + def thread_done(self, thread): + self.threads.remove(thread) + + def _stop_threads(self): + current = threading.current_thread() + + # Iterate over a copy of self.threads so thread_done doesn't + # modify the list while we're iterating + for x in self.threads[:]: + if x is current: + # don't kill the current thread. + continue + try: + x.stop() + except eventlet.greenlet.GreenletExit: + pass + except Exception as ex: + LOG.exception(ex) + + def stop_timers(self): + for x in self.timers: + try: + x.stop() + except Exception as ex: + LOG.exception(ex) + self.timers = [] + + def stop(self, graceful=False): + """stop function has the option of graceful=True/False. + + * In case of graceful=True, wait for all threads to be finished. + Never kill threads. + * In case of graceful=False, kill threads immediately. + """ + self.stop_timers() + if graceful: + # In case of graceful=True, wait for all threads to be + # finished, never kill threads + self.wait() + else: + # In case of graceful=False(Default), kill threads + # immediately + self._stop_threads() + + def wait(self): + for x in self.timers: + try: + x.wait() + except eventlet.greenlet.GreenletExit: + pass + except Exception as ex: + LOG.exception(ex) + current = threading.current_thread() + + # Iterate over a copy of self.threads so thread_done doesn't + # modify the list while we're iterating + for x in self.threads[:]: + if x is current: + continue + try: + x.wait() + except eventlet.greenlet.GreenletExit: + pass + except Exception as ex: + LOG.exception(ex) diff --git a/keystone-moon/keystone/openstack/common/versionutils.py b/keystone-moon/keystone/openstack/common/versionutils.py new file mode 100644 index 00000000..111bfd6f --- /dev/null +++ b/keystone-moon/keystone/openstack/common/versionutils.py @@ -0,0 +1,262 @@ +# Copyright (c) 2013 OpenStack Foundation +# All Rights Reserved. +# +# 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. + +""" +Helpers for comparing version strings. +""" + +import copy +import functools +import inspect +import logging + +from oslo_config import cfg +import pkg_resources +import six + +from keystone.openstack.common._i18n import _ + + +LOG = logging.getLogger(__name__) +CONF = cfg.CONF + + +deprecated_opts = [ + cfg.BoolOpt('fatal_deprecations', + default=False, + help='Enables or disables fatal status of deprecations.'), +] + + +def list_opts(): + """Entry point for oslo.config-generator. + """ + return [(None, copy.deepcopy(deprecated_opts))] + + +class deprecated(object): + """A decorator to mark callables as deprecated. + + This decorator logs a deprecation message when the callable it decorates is + used. The message will include the release where the callable was + deprecated, the release where it may be removed and possibly an optional + replacement. + + Examples: + + 1. Specifying the required deprecated release + + >>> @deprecated(as_of=deprecated.ICEHOUSE) + ... def a(): pass + + 2. Specifying a replacement: + + >>> @deprecated(as_of=deprecated.ICEHOUSE, in_favor_of='f()') + ... def b(): pass + + 3. Specifying the release where the functionality may be removed: + + >>> @deprecated(as_of=deprecated.ICEHOUSE, remove_in=+1) + ... def c(): pass + + 4. Specifying the deprecated functionality will not be removed: + >>> @deprecated(as_of=deprecated.ICEHOUSE, remove_in=0) + ... def d(): pass + + 5. Specifying a replacement, deprecated functionality will not be removed: + >>> @deprecated(as_of=deprecated.ICEHOUSE, in_favor_of='f()', remove_in=0) + ... def e(): pass + + """ + + # NOTE(morganfainberg): Bexar is used for unit test purposes, it is + # expected we maintain a gap between Bexar and Folsom in this list. + BEXAR = 'B' + FOLSOM = 'F' + GRIZZLY = 'G' + HAVANA = 'H' + ICEHOUSE = 'I' + JUNO = 'J' + KILO = 'K' + LIBERTY = 'L' + + _RELEASES = { + # NOTE(morganfainberg): Bexar is used for unit test purposes, it is + # expected we maintain a gap between Bexar and Folsom in this list. + 'B': 'Bexar', + 'F': 'Folsom', + 'G': 'Grizzly', + 'H': 'Havana', + 'I': 'Icehouse', + 'J': 'Juno', + 'K': 'Kilo', + 'L': 'Liberty', + } + + _deprecated_msg_with_alternative = _( + '%(what)s is deprecated as of %(as_of)s in favor of ' + '%(in_favor_of)s and may be removed in %(remove_in)s.') + + _deprecated_msg_no_alternative = _( + '%(what)s is deprecated as of %(as_of)s and may be ' + 'removed in %(remove_in)s. It will not be superseded.') + + _deprecated_msg_with_alternative_no_removal = _( + '%(what)s is deprecated as of %(as_of)s in favor of %(in_favor_of)s.') + + _deprecated_msg_with_no_alternative_no_removal = _( + '%(what)s is deprecated as of %(as_of)s. It will not be superseded.') + + def __init__(self, as_of, in_favor_of=None, remove_in=2, what=None): + """Initialize decorator + + :param as_of: the release deprecating the callable. Constants + are define in this class for convenience. + :param in_favor_of: the replacement for the callable (optional) + :param remove_in: an integer specifying how many releases to wait + before removing (default: 2) + :param what: name of the thing being deprecated (default: the + callable's name) + + """ + self.as_of = as_of + self.in_favor_of = in_favor_of + self.remove_in = remove_in + self.what = what + + def __call__(self, func_or_cls): + if not self.what: + self.what = func_or_cls.__name__ + '()' + msg, details = self._build_message() + + if inspect.isfunction(func_or_cls): + + @six.wraps(func_or_cls) + def wrapped(*args, **kwargs): + report_deprecated_feature(LOG, msg, details) + return func_or_cls(*args, **kwargs) + return wrapped + elif inspect.isclass(func_or_cls): + orig_init = func_or_cls.__init__ + + # TODO(tsufiev): change `functools` module to `six` as + # soon as six 1.7.4 (with fix for passing `assigned` + # argument to underlying `functools.wraps`) is released + # and added to the oslo-incubator requrements + @functools.wraps(orig_init, assigned=('__name__', '__doc__')) + def new_init(self, *args, **kwargs): + report_deprecated_feature(LOG, msg, details) + orig_init(self, *args, **kwargs) + func_or_cls.__init__ = new_init + return func_or_cls + else: + raise TypeError('deprecated can be used only with functions or ' + 'classes') + + def _get_safe_to_remove_release(self, release): + # TODO(dstanek): this method will have to be reimplemented once + # when we get to the X release because once we get to the Y + # release, what is Y+2? + new_release = chr(ord(release) + self.remove_in) + if new_release in self._RELEASES: + return self._RELEASES[new_release] + else: + return new_release + + def _build_message(self): + details = dict(what=self.what, + as_of=self._RELEASES[self.as_of], + remove_in=self._get_safe_to_remove_release(self.as_of)) + + if self.in_favor_of: + details['in_favor_of'] = self.in_favor_of + if self.remove_in > 0: + msg = self._deprecated_msg_with_alternative + else: + # There are no plans to remove this function, but it is + # now deprecated. + msg = self._deprecated_msg_with_alternative_no_removal + else: + if self.remove_in > 0: + msg = self._deprecated_msg_no_alternative + else: + # There are no plans to remove this function, but it is + # now deprecated. + msg = self._deprecated_msg_with_no_alternative_no_removal + return msg, details + + +def is_compatible(requested_version, current_version, same_major=True): + """Determine whether `requested_version` is satisfied by + `current_version`; in other words, `current_version` is >= + `requested_version`. + + :param requested_version: version to check for compatibility + :param current_version: version to check against + :param same_major: if True, the major version must be identical between + `requested_version` and `current_version`. This is used when a + major-version difference indicates incompatibility between the two + versions. Since this is the common-case in practice, the default is + True. + :returns: True if compatible, False if not + """ + requested_parts = pkg_resources.parse_version(requested_version) + current_parts = pkg_resources.parse_version(current_version) + + if same_major and (requested_parts[0] != current_parts[0]): + return False + + return current_parts >= requested_parts + + +# Track the messages we have sent already. See +# report_deprecated_feature(). +_deprecated_messages_sent = {} + + +def report_deprecated_feature(logger, msg, *args, **kwargs): + """Call this function when a deprecated feature is used. + + If the system is configured for fatal deprecations then the message + is logged at the 'critical' level and :class:`DeprecatedConfig` will + be raised. + + Otherwise, the message will be logged (once) at the 'warn' level. + + :raises: :class:`DeprecatedConfig` if the system is configured for + fatal deprecations. + """ + stdmsg = _("Deprecated: %s") % msg + CONF.register_opts(deprecated_opts) + if CONF.fatal_deprecations: + logger.critical(stdmsg, *args, **kwargs) + raise DeprecatedConfig(msg=stdmsg) + + # Using a list because a tuple with dict can't be stored in a set. + sent_args = _deprecated_messages_sent.setdefault(msg, list()) + + if args in sent_args: + # Already logged this message, so don't log it again. + return + + sent_args.append(args) + logger.warn(stdmsg, *args, **kwargs) + + +class DeprecatedConfig(Exception): + message = _("Fatal call to deprecated config: %(msg)s") + + def __init__(self, msg): + super(Exception, self).__init__(self.message % dict(msg=msg)) diff --git a/keystone-moon/keystone/policy/__init__.py b/keystone-moon/keystone/policy/__init__.py new file mode 100644 index 00000000..4cd96793 --- /dev/null +++ b/keystone-moon/keystone/policy/__init__.py @@ -0,0 +1,17 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 keystone.policy import controllers # noqa +from keystone.policy.core import * # noqa +from keystone.policy import routers # noqa diff --git a/keystone-moon/keystone/policy/backends/__init__.py b/keystone-moon/keystone/policy/backends/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/policy/backends/rules.py b/keystone-moon/keystone/policy/backends/rules.py new file mode 100644 index 00000000..011dd542 --- /dev/null +++ b/keystone-moon/keystone/policy/backends/rules.py @@ -0,0 +1,92 @@ +# Copyright (c) 2011 OpenStack, LLC. +# All Rights Reserved. +# +# 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. + +"""Policy engine for keystone""" + +from oslo_config import cfg +from oslo_log import log +from oslo_policy import policy as common_policy + +from keystone import exception +from keystone import policy + + +CONF = cfg.CONF +LOG = log.getLogger(__name__) + + +_ENFORCER = None + + +def reset(): + global _ENFORCER + _ENFORCER = None + + +def init(): + global _ENFORCER + if not _ENFORCER: + _ENFORCER = common_policy.Enforcer(CONF) + + +def enforce(credentials, action, target, do_raise=True): + """Verifies that the action is valid on the target in this context. + + :param credentials: user credentials + :param action: string representing the action to be checked, which + should be colon separated for clarity. + :param target: dictionary representing the object of the action + for object creation this should be a dictionary + representing the location of the object e.g. + {'project_id': object.project_id} + :raises: `exception.Forbidden` if verification fails. + + Actions should be colon separated for clarity. For example: + + * identity:list_users + + """ + init() + + # Add the exception arguments if asked to do a raise + extra = {} + if do_raise: + extra.update(exc=exception.ForbiddenAction, action=action, + do_raise=do_raise) + + return _ENFORCER.enforce(action, target, credentials, **extra) + + +class Policy(policy.Driver): + def enforce(self, credentials, action, target): + LOG.debug('enforce %(action)s: %(credentials)s', { + 'action': action, + 'credentials': credentials}) + enforce(credentials, action, target) + + def create_policy(self, policy_id, policy): + raise exception.NotImplemented() + + def list_policies(self): + raise exception.NotImplemented() + + def get_policy(self, policy_id): + raise exception.NotImplemented() + + def update_policy(self, policy_id, policy): + raise exception.NotImplemented() + + def delete_policy(self, policy_id): + raise exception.NotImplemented() diff --git a/keystone-moon/keystone/policy/backends/sql.py b/keystone-moon/keystone/policy/backends/sql.py new file mode 100644 index 00000000..b2cccd01 --- /dev/null +++ b/keystone-moon/keystone/policy/backends/sql.py @@ -0,0 +1,79 @@ +# Copyright 2012 OpenStack LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystone.common import sql +from keystone import exception +from keystone.policy.backends import rules + + +class PolicyModel(sql.ModelBase, sql.DictBase): + __tablename__ = 'policy' + attributes = ['id', 'blob', 'type'] + id = sql.Column(sql.String(64), primary_key=True) + blob = sql.Column(sql.JsonBlob(), nullable=False) + type = sql.Column(sql.String(255), nullable=False) + extra = sql.Column(sql.JsonBlob()) + + +class Policy(rules.Policy): + + @sql.handle_conflicts(conflict_type='policy') + def create_policy(self, policy_id, policy): + session = sql.get_session() + + with session.begin(): + ref = PolicyModel.from_dict(policy) + session.add(ref) + + return ref.to_dict() + + def list_policies(self): + session = sql.get_session() + + refs = session.query(PolicyModel).all() + return [ref.to_dict() for ref in refs] + + def _get_policy(self, session, policy_id): + """Private method to get a policy model object (NOT a dictionary).""" + ref = session.query(PolicyModel).get(policy_id) + if not ref: + raise exception.PolicyNotFound(policy_id=policy_id) + return ref + + def get_policy(self, policy_id): + session = sql.get_session() + + return self._get_policy(session, policy_id).to_dict() + + @sql.handle_conflicts(conflict_type='policy') + def update_policy(self, policy_id, policy): + session = sql.get_session() + + with session.begin(): + ref = self._get_policy(session, policy_id) + old_dict = ref.to_dict() + old_dict.update(policy) + new_policy = PolicyModel.from_dict(old_dict) + ref.blob = new_policy.blob + ref.type = new_policy.type + ref.extra = new_policy.extra + + return ref.to_dict() + + def delete_policy(self, policy_id): + session = sql.get_session() + + with session.begin(): + ref = self._get_policy(session, policy_id) + session.delete(ref) diff --git a/keystone-moon/keystone/policy/controllers.py b/keystone-moon/keystone/policy/controllers.py new file mode 100644 index 00000000..e6eb9bca --- /dev/null +++ b/keystone-moon/keystone/policy/controllers.py @@ -0,0 +1,56 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 keystone.common import controller +from keystone.common import dependency +from keystone.common import validation +from keystone import notifications +from keystone.policy import schema + + +@dependency.requires('policy_api') +class PolicyV3(controller.V3Controller): + collection_name = 'policies' + member_name = 'policy' + + @controller.protected() + @validation.validated(schema.policy_create, 'policy') + def create_policy(self, context, policy): + ref = self._assign_unique_id(self._normalize_dict(policy)) + initiator = notifications._get_request_audit_info(context) + ref = self.policy_api.create_policy(ref['id'], ref, initiator) + return PolicyV3.wrap_member(context, ref) + + @controller.filterprotected('type') + def list_policies(self, context, filters): + hints = PolicyV3.build_driver_hints(context, filters) + refs = self.policy_api.list_policies(hints=hints) + return PolicyV3.wrap_collection(context, refs, hints=hints) + + @controller.protected() + def get_policy(self, context, policy_id): + ref = self.policy_api.get_policy(policy_id) + return PolicyV3.wrap_member(context, ref) + + @controller.protected() + @validation.validated(schema.policy_update, 'policy') + def update_policy(self, context, policy_id, policy): + initiator = notifications._get_request_audit_info(context) + ref = self.policy_api.update_policy(policy_id, policy, initiator) + return PolicyV3.wrap_member(context, ref) + + @controller.protected() + def delete_policy(self, context, policy_id): + initiator = notifications._get_request_audit_info(context) + return self.policy_api.delete_policy(policy_id, initiator) diff --git a/keystone-moon/keystone/policy/core.py b/keystone-moon/keystone/policy/core.py new file mode 100644 index 00000000..1f02803f --- /dev/null +++ b/keystone-moon/keystone/policy/core.py @@ -0,0 +1,135 @@ +# Copyright 2012 OpenStack Foundation +# +# 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. + +"""Main entry point into the Policy service.""" + +import abc + +from oslo_config import cfg +import six + +from keystone.common import dependency +from keystone.common import manager +from keystone import exception +from keystone import notifications + + +CONF = cfg.CONF + + +@dependency.provider('policy_api') +class Manager(manager.Manager): + """Default pivot point for the Policy backend. + + See :mod:`keystone.common.manager.Manager` for more details on how this + dynamically calls the backend. + + """ + _POLICY = 'policy' + + def __init__(self): + super(Manager, self).__init__(CONF.policy.driver) + + def create_policy(self, policy_id, policy, initiator=None): + ref = self.driver.create_policy(policy_id, policy) + notifications.Audit.created(self._POLICY, policy_id, initiator) + return ref + + def get_policy(self, policy_id): + try: + return self.driver.get_policy(policy_id) + except exception.NotFound: + raise exception.PolicyNotFound(policy_id=policy_id) + + def update_policy(self, policy_id, policy, initiator=None): + if 'id' in policy and policy_id != policy['id']: + raise exception.ValidationError('Cannot change policy ID') + try: + ref = self.driver.update_policy(policy_id, policy) + except exception.NotFound: + raise exception.PolicyNotFound(policy_id=policy_id) + notifications.Audit.updated(self._POLICY, policy_id, initiator) + return ref + + @manager.response_truncated + def list_policies(self, hints=None): + # NOTE(henry-nash): Since the advantage of filtering or list limiting + # of policies at the driver level is minimal, we leave this to the + # caller. + return self.driver.list_policies() + + def delete_policy(self, policy_id, initiator=None): + try: + ret = self.driver.delete_policy(policy_id) + except exception.NotFound: + raise exception.PolicyNotFound(policy_id=policy_id) + notifications.Audit.deleted(self._POLICY, policy_id, initiator) + return ret + + +@six.add_metaclass(abc.ABCMeta) +class Driver(object): + + def _get_list_limit(self): + return CONF.policy.list_limit or CONF.list_limit + + @abc.abstractmethod + def enforce(self, context, credentials, action, target): + """Verify that a user is authorized to perform action. + + For more information on a full implementation of this see: + `keystone.policy.backends.rules.Policy.enforce` + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def create_policy(self, policy_id, policy): + """Store a policy blob. + + :raises: keystone.exception.Conflict + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_policies(self): + """List all policies.""" + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def get_policy(self, policy_id): + """Retrieve a specific policy blob. + + :raises: keystone.exception.PolicyNotFound + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def update_policy(self, policy_id, policy): + """Update a policy blob. + + :raises: keystone.exception.PolicyNotFound + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def delete_policy(self, policy_id): + """Remove a policy blob. + + :raises: keystone.exception.PolicyNotFound + + """ + raise exception.NotImplemented() # pragma: no cover diff --git a/keystone-moon/keystone/policy/routers.py b/keystone-moon/keystone/policy/routers.py new file mode 100644 index 00000000..5daadc81 --- /dev/null +++ b/keystone-moon/keystone/policy/routers.py @@ -0,0 +1,24 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 keystone.common import router +from keystone.common import wsgi +from keystone.policy import controllers + + +class Routers(wsgi.RoutersBase): + + def append_v3_routers(self, mapper, routers): + policy_controller = controllers.PolicyV3() + routers.append(router.Router(policy_controller, 'policies', 'policy', + resource_descriptions=self.v3_resources)) diff --git a/keystone-moon/keystone/policy/schema.py b/keystone-moon/keystone/policy/schema.py new file mode 100644 index 00000000..512c4ce7 --- /dev/null +++ b/keystone-moon/keystone/policy/schema.py @@ -0,0 +1,36 @@ +# 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. + + +_policy_properties = { + 'blob': { + 'type': 'string' + }, + 'type': { + 'type': 'string', + 'maxLength': 255 + } +} + +policy_create = { + 'type': 'object', + 'properties': _policy_properties, + 'required': ['blob', 'type'], + 'additionalProperties': True +} + +policy_update = { + 'type': 'object', + 'properties': _policy_properties, + 'minProperties': 1, + 'additionalProperties': True +} diff --git a/keystone-moon/keystone/resource/__init__.py b/keystone-moon/keystone/resource/__init__.py new file mode 100644 index 00000000..c0070a12 --- /dev/null +++ b/keystone-moon/keystone/resource/__init__.py @@ -0,0 +1,15 @@ +# 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 keystone.resource import controllers # noqa +from keystone.resource.core import * # noqa +from keystone.resource import routers # noqa diff --git a/keystone-moon/keystone/resource/backends/__init__.py b/keystone-moon/keystone/resource/backends/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/resource/backends/ldap.py b/keystone-moon/keystone/resource/backends/ldap.py new file mode 100644 index 00000000..434c2b04 --- /dev/null +++ b/keystone-moon/keystone/resource/backends/ldap.py @@ -0,0 +1,196 @@ +# 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 __future__ import absolute_import + +import uuid + +from oslo_config import cfg +from oslo_log import log + +from keystone import clean +from keystone.common import driver_hints +from keystone.common import ldap as common_ldap +from keystone.common import models +from keystone import exception +from keystone.i18n import _ +from keystone.identity.backends import ldap as ldap_identity +from keystone import resource + + +CONF = cfg.CONF +LOG = log.getLogger(__name__) + + +class Resource(resource.Driver): + def __init__(self): + super(Resource, self).__init__() + self.LDAP_URL = CONF.ldap.url + self.LDAP_USER = CONF.ldap.user + self.LDAP_PASSWORD = CONF.ldap.password + self.suffix = CONF.ldap.suffix + + # This is the only deep dependency from resource back to identity. + # This is safe to do since if you are using LDAP for resource, it is + # required that you are using it for identity as well. + self.user = ldap_identity.UserApi(CONF) + + self.project = ProjectApi(CONF) + + def default_assignment_driver(self): + return 'keystone.assignment.backends.ldap.Assignment' + + def _set_default_parent_project(self, ref): + """If the parent project ID has not been set, set it to None.""" + if isinstance(ref, dict): + if 'parent_id' not in ref: + ref = dict(ref, parent_id=None) + return ref + elif isinstance(ref, list): + return [self._set_default_parent_project(x) for x in ref] + else: + raise ValueError(_('Expected dict or list: %s') % type(ref)) + + def _validate_parent_project_is_none(self, ref): + """If a parent_id different from None was given, + raises InvalidProjectException. + + """ + parent_id = ref.get('parent_id') + if parent_id is not None: + raise exception.InvalidParentProject(parent_id) + + def _set_default_attributes(self, project_ref): + project_ref = self._set_default_domain(project_ref) + return self._set_default_parent_project(project_ref) + + def get_project(self, tenant_id): + return self._set_default_attributes( + self.project.get(tenant_id)) + + def list_projects(self, hints): + return self._set_default_attributes( + self.project.get_all_filtered(hints)) + + def list_projects_in_domain(self, domain_id): + # We don't support multiple domains within this driver, so ignore + # any domain specified + return self.list_projects(driver_hints.Hints()) + + def list_projects_in_subtree(self, project_id): + # We don't support projects hierarchy within this driver, so a + # project will never have children + return [] + + def list_project_parents(self, project_id): + # We don't support projects hierarchy within this driver, so a + # project will never have parents + return [] + + def is_leaf_project(self, project_id): + # We don't support projects hierarchy within this driver, so a + # project will always be a root and a leaf at the same time + return True + + def list_projects_from_ids(self, ids): + return [self.get_project(id) for id in ids] + + def list_project_ids_from_domain_ids(self, domain_ids): + # We don't support multiple domains within this driver, so ignore + # any domain specified + return [x.id for x in self.list_projects(driver_hints.Hints())] + + def get_project_by_name(self, tenant_name, domain_id): + self._validate_default_domain_id(domain_id) + return self._set_default_attributes( + self.project.get_by_name(tenant_name)) + + def create_project(self, tenant_id, tenant): + self.project.check_allow_create() + tenant = self._validate_default_domain(tenant) + self._validate_parent_project_is_none(tenant) + tenant['name'] = clean.project_name(tenant['name']) + data = tenant.copy() + if 'id' not in data or data['id'] is None: + data['id'] = str(uuid.uuid4().hex) + if 'description' in data and data['description'] in ['', None]: + data.pop('description') + return self._set_default_attributes( + self.project.create(data)) + + def update_project(self, tenant_id, tenant): + self.project.check_allow_update() + tenant = self._validate_default_domain(tenant) + if 'name' in tenant: + tenant['name'] = clean.project_name(tenant['name']) + return self._set_default_attributes( + self.project.update(tenant_id, tenant)) + + def delete_project(self, tenant_id): + self.project.check_allow_delete() + if self.project.subtree_delete_enabled: + self.project.deleteTree(tenant_id) + else: + # The manager layer will call assignments to delete the + # role assignments, so we just have to delete the project itself. + self.project.delete(tenant_id) + + def create_domain(self, domain_id, domain): + if domain_id == CONF.identity.default_domain_id: + msg = _('Duplicate ID, %s.') % domain_id + raise exception.Conflict(type='domain', details=msg) + raise exception.Forbidden(_('Domains are read-only against LDAP')) + + def get_domain(self, domain_id): + self._validate_default_domain_id(domain_id) + return resource.calc_default_domain() + + def update_domain(self, domain_id, domain): + self._validate_default_domain_id(domain_id) + raise exception.Forbidden(_('Domains are read-only against LDAP')) + + def delete_domain(self, domain_id): + self._validate_default_domain_id(domain_id) + raise exception.Forbidden(_('Domains are read-only against LDAP')) + + def list_domains(self, hints): + return [resource.calc_default_domain()] + + def list_domains_from_ids(self, ids): + return [resource.calc_default_domain()] + + def get_domain_by_name(self, domain_name): + default_domain = resource.calc_default_domain() + if domain_name != default_domain['name']: + raise exception.DomainNotFound(domain_id=domain_name) + return default_domain + + +# TODO(termie): turn this into a data object and move logic to driver +class ProjectApi(common_ldap.ProjectLdapStructureMixin, + common_ldap.EnabledEmuMixIn, common_ldap.BaseLdap): + + model = models.Project + + def create(self, values): + data = values.copy() + if data.get('id') is None: + data['id'] = uuid.uuid4().hex + return super(ProjectApi, self).create(data) + + def update(self, project_id, values): + old_obj = self.get(project_id) + return super(ProjectApi, self).update(project_id, values, old_obj) + + def get_all_filtered(self, hints): + query = self.filter_query(hints) + return super(ProjectApi, self).get_all(query) diff --git a/keystone-moon/keystone/resource/backends/sql.py b/keystone-moon/keystone/resource/backends/sql.py new file mode 100644 index 00000000..fb117240 --- /dev/null +++ b/keystone-moon/keystone/resource/backends/sql.py @@ -0,0 +1,260 @@ +# 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 oslo_config import cfg +from oslo_log import log + +from keystone import clean +from keystone.common import sql +from keystone import exception +from keystone.i18n import _LE +from keystone import resource as keystone_resource + + +CONF = cfg.CONF +LOG = log.getLogger(__name__) + + +class Resource(keystone_resource.Driver): + + def default_assignment_driver(self): + return 'keystone.assignment.backends.sql.Assignment' + + def _get_project(self, session, project_id): + project_ref = session.query(Project).get(project_id) + if project_ref is None: + raise exception.ProjectNotFound(project_id=project_id) + return project_ref + + def get_project(self, tenant_id): + with sql.transaction() as session: + return self._get_project(session, tenant_id).to_dict() + + def get_project_by_name(self, tenant_name, domain_id): + with sql.transaction() as session: + query = session.query(Project) + query = query.filter_by(name=tenant_name) + query = query.filter_by(domain_id=domain_id) + try: + project_ref = query.one() + except sql.NotFound: + raise exception.ProjectNotFound(project_id=tenant_name) + return project_ref.to_dict() + + @sql.truncated + def list_projects(self, hints): + with sql.transaction() as session: + query = session.query(Project) + project_refs = sql.filter_limit_query(Project, query, hints) + return [project_ref.to_dict() for project_ref in project_refs] + + def list_projects_from_ids(self, ids): + if not ids: + return [] + else: + with sql.transaction() as session: + query = session.query(Project) + query = query.filter(Project.id.in_(ids)) + return [project_ref.to_dict() for project_ref in query.all()] + + def list_project_ids_from_domain_ids(self, domain_ids): + if not domain_ids: + return [] + else: + with sql.transaction() as session: + query = session.query(Project.id) + query = ( + query.filter(Project.domain_id.in_(domain_ids))) + return [x.id for x in query.all()] + + def list_projects_in_domain(self, domain_id): + with sql.transaction() as session: + self._get_domain(session, domain_id) + query = session.query(Project) + project_refs = query.filter_by(domain_id=domain_id) + return [project_ref.to_dict() for project_ref in project_refs] + + def _get_children(self, session, project_ids): + query = session.query(Project) + query = query.filter(Project.parent_id.in_(project_ids)) + project_refs = query.all() + return [project_ref.to_dict() for project_ref in project_refs] + + def list_projects_in_subtree(self, project_id): + with sql.transaction() as session: + project = self._get_project(session, project_id).to_dict() + children = self._get_children(session, [project['id']]) + subtree = [] + examined = set(project['id']) + while children: + children_ids = set() + for ref in children: + if ref['id'] in examined: + msg = _LE('Circular reference or a repeated ' + 'entry found in projects hierarchy - ' + '%(project_id)s.') + LOG.error(msg, {'project_id': ref['id']}) + return + children_ids.add(ref['id']) + + examined.union(children_ids) + subtree += children + children = self._get_children(session, children_ids) + return subtree + + def list_project_parents(self, project_id): + with sql.transaction() as session: + project = self._get_project(session, project_id).to_dict() + parents = [] + examined = set() + while project.get('parent_id') is not None: + if project['id'] in examined: + msg = _LE('Circular reference or a repeated ' + 'entry found in projects hierarchy - ' + '%(project_id)s.') + LOG.error(msg, {'project_id': project['id']}) + return + + examined.add(project['id']) + parent_project = self._get_project( + session, project['parent_id']).to_dict() + parents.append(parent_project) + project = parent_project + return parents + + def is_leaf_project(self, project_id): + with sql.transaction() as session: + project_refs = self._get_children(session, [project_id]) + return not project_refs + + # CRUD + @sql.handle_conflicts(conflict_type='project') + def create_project(self, tenant_id, tenant): + tenant['name'] = clean.project_name(tenant['name']) + with sql.transaction() as session: + tenant_ref = Project.from_dict(tenant) + session.add(tenant_ref) + return tenant_ref.to_dict() + + @sql.handle_conflicts(conflict_type='project') + def update_project(self, tenant_id, tenant): + if 'name' in tenant: + tenant['name'] = clean.project_name(tenant['name']) + + with sql.transaction() as session: + tenant_ref = self._get_project(session, tenant_id) + old_project_dict = tenant_ref.to_dict() + for k in tenant: + old_project_dict[k] = tenant[k] + new_project = Project.from_dict(old_project_dict) + for attr in Project.attributes: + if attr != 'id': + setattr(tenant_ref, attr, getattr(new_project, attr)) + tenant_ref.extra = new_project.extra + return tenant_ref.to_dict(include_extra_dict=True) + + @sql.handle_conflicts(conflict_type='project') + def delete_project(self, tenant_id): + with sql.transaction() as session: + tenant_ref = self._get_project(session, tenant_id) + session.delete(tenant_ref) + + # domain crud + + @sql.handle_conflicts(conflict_type='domain') + def create_domain(self, domain_id, domain): + with sql.transaction() as session: + ref = Domain.from_dict(domain) + session.add(ref) + return ref.to_dict() + + @sql.truncated + def list_domains(self, hints): + with sql.transaction() as session: + query = session.query(Domain) + refs = sql.filter_limit_query(Domain, query, hints) + return [ref.to_dict() for ref in refs] + + def list_domains_from_ids(self, ids): + if not ids: + return [] + else: + with sql.transaction() as session: + query = session.query(Domain) + query = query.filter(Domain.id.in_(ids)) + domain_refs = query.all() + return [domain_ref.to_dict() for domain_ref in domain_refs] + + def _get_domain(self, session, domain_id): + ref = session.query(Domain).get(domain_id) + if ref is None: + raise exception.DomainNotFound(domain_id=domain_id) + return ref + + def get_domain(self, domain_id): + with sql.transaction() as session: + return self._get_domain(session, domain_id).to_dict() + + def get_domain_by_name(self, domain_name): + with sql.transaction() as session: + try: + ref = (session.query(Domain). + filter_by(name=domain_name).one()) + except sql.NotFound: + raise exception.DomainNotFound(domain_id=domain_name) + return ref.to_dict() + + @sql.handle_conflicts(conflict_type='domain') + def update_domain(self, domain_id, domain): + with sql.transaction() as session: + ref = self._get_domain(session, domain_id) + old_dict = ref.to_dict() + for k in domain: + old_dict[k] = domain[k] + new_domain = Domain.from_dict(old_dict) + for attr in Domain.attributes: + if attr != 'id': + setattr(ref, attr, getattr(new_domain, attr)) + ref.extra = new_domain.extra + return ref.to_dict() + + def delete_domain(self, domain_id): + with sql.transaction() as session: + ref = self._get_domain(session, domain_id) + session.delete(ref) + + +class Domain(sql.ModelBase, sql.DictBase): + __tablename__ = 'domain' + attributes = ['id', 'name', 'enabled'] + id = sql.Column(sql.String(64), primary_key=True) + name = sql.Column(sql.String(64), nullable=False) + enabled = sql.Column(sql.Boolean, default=True, nullable=False) + extra = sql.Column(sql.JsonBlob()) + __table_args__ = (sql.UniqueConstraint('name'), {}) + + +class Project(sql.ModelBase, sql.DictBase): + __tablename__ = 'project' + attributes = ['id', 'name', 'domain_id', 'description', 'enabled', + 'parent_id'] + id = sql.Column(sql.String(64), primary_key=True) + name = sql.Column(sql.String(64), nullable=False) + domain_id = sql.Column(sql.String(64), sql.ForeignKey('domain.id'), + nullable=False) + description = sql.Column(sql.Text()) + enabled = sql.Column(sql.Boolean) + extra = sql.Column(sql.JsonBlob()) + parent_id = sql.Column(sql.String(64), sql.ForeignKey('project.id')) + # Unique constraint across two columns to create the separation + # rather than just only 'name' being unique + __table_args__ = (sql.UniqueConstraint('domain_id', 'name'), {}) diff --git a/keystone-moon/keystone/resource/config_backends/__init__.py b/keystone-moon/keystone/resource/config_backends/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/resource/config_backends/sql.py b/keystone-moon/keystone/resource/config_backends/sql.py new file mode 100644 index 00000000..e54bf22b --- /dev/null +++ b/keystone-moon/keystone/resource/config_backends/sql.py @@ -0,0 +1,119 @@ +# 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 keystone.common import sql +from keystone import exception +from keystone.i18n import _ +from keystone import resource + + +class WhiteListedConfig(sql.ModelBase, sql.ModelDictMixin): + __tablename__ = 'whitelisted_config' + domain_id = sql.Column(sql.String(64), primary_key=True) + group = sql.Column(sql.String(255), primary_key=True) + option = sql.Column(sql.String(255), primary_key=True) + value = sql.Column(sql.JsonBlob(), nullable=False) + + def to_dict(self): + d = super(WhiteListedConfig, self).to_dict() + d.pop('domain_id') + return d + + +class SensitiveConfig(sql.ModelBase, sql.ModelDictMixin): + __tablename__ = 'sensitive_config' + domain_id = sql.Column(sql.String(64), primary_key=True) + group = sql.Column(sql.String(255), primary_key=True) + option = sql.Column(sql.String(255), primary_key=True) + value = sql.Column(sql.JsonBlob(), nullable=False) + + def to_dict(self): + d = super(SensitiveConfig, self).to_dict() + d.pop('domain_id') + return d + + +class DomainConfig(resource.DomainConfigDriver): + + def choose_table(self, sensitive): + if sensitive: + return SensitiveConfig + else: + return WhiteListedConfig + + @sql.handle_conflicts(conflict_type='domain_config') + def create_config_option(self, domain_id, group, option, value, + sensitive=False): + with sql.transaction() as session: + config_table = self.choose_table(sensitive) + ref = config_table(domain_id=domain_id, group=group, + option=option, value=value) + session.add(ref) + return ref.to_dict() + + def _get_config_option(self, session, domain_id, group, option, sensitive): + try: + config_table = self.choose_table(sensitive) + ref = (session.query(config_table). + filter_by(domain_id=domain_id, group=group, + option=option).one()) + except sql.NotFound: + msg = _('option %(option)s in group %(group)s') % { + 'group': group, 'option': option} + raise exception.DomainConfigNotFound( + domain_id=domain_id, group_or_option=msg) + return ref + + def get_config_option(self, domain_id, group, option, sensitive=False): + with sql.transaction() as session: + ref = self._get_config_option(session, domain_id, group, option, + sensitive) + return ref.to_dict() + + def list_config_options(self, domain_id, group=None, option=None, + sensitive=False): + with sql.transaction() as session: + config_table = self.choose_table(sensitive) + query = session.query(config_table) + query = query.filter_by(domain_id=domain_id) + if group: + query = query.filter_by(group=group) + if option: + query = query.filter_by(option=option) + return [ref.to_dict() for ref in query.all()] + + def update_config_option(self, domain_id, group, option, value, + sensitive=False): + with sql.transaction() as session: + ref = self._get_config_option(session, domain_id, group, option, + sensitive) + ref.value = value + return ref.to_dict() + + def delete_config_options(self, domain_id, group=None, option=None, + sensitive=False): + """Deletes config options that match the filter parameters. + + Since the public API is broken down into calls for delete in both the + whitelisted and sensitive methods, we are silent at the driver level + if there was nothing to delete. + + """ + with sql.transaction() as session: + config_table = self.choose_table(sensitive) + query = session.query(config_table) + query = query.filter_by(domain_id=domain_id) + if group: + query = query.filter_by(group=group) + if option: + query = query.filter_by(option=option) + query.delete(False) diff --git a/keystone-moon/keystone/resource/controllers.py b/keystone-moon/keystone/resource/controllers.py new file mode 100644 index 00000000..886b5eb1 --- /dev/null +++ b/keystone-moon/keystone/resource/controllers.py @@ -0,0 +1,281 @@ +# Copyright 2013 Metacloud, Inc. +# Copyright 2012 OpenStack Foundation +# +# 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. + +"""Workflow Logic the Resource service.""" + +import uuid + +from oslo_config import cfg +from oslo_log import log + +from keystone.common import controller +from keystone.common import dependency +from keystone.common import validation +from keystone.common import wsgi +from keystone import exception +from keystone.i18n import _ +from keystone import notifications +from keystone.resource import schema + + +CONF = cfg.CONF +LOG = log.getLogger(__name__) + + +@dependency.requires('resource_api') +class Tenant(controller.V2Controller): + + @controller.v2_deprecated + def get_all_projects(self, context, **kw): + """Gets a list of all tenants for an admin user.""" + if 'name' in context['query_string']: + return self.get_project_by_name( + context, context['query_string'].get('name')) + + self.assert_admin(context) + tenant_refs = self.resource_api.list_projects_in_domain( + CONF.identity.default_domain_id) + for tenant_ref in tenant_refs: + tenant_ref = self.filter_domain_id(tenant_ref) + params = { + 'limit': context['query_string'].get('limit'), + 'marker': context['query_string'].get('marker'), + } + return self.format_project_list(tenant_refs, **params) + + @controller.v2_deprecated + def get_project(self, context, tenant_id): + # TODO(termie): this stuff should probably be moved to middleware + self.assert_admin(context) + ref = self.resource_api.get_project(tenant_id) + return {'tenant': self.filter_domain_id(ref)} + + @controller.v2_deprecated + def get_project_by_name(self, context, tenant_name): + self.assert_admin(context) + ref = self.resource_api.get_project_by_name( + tenant_name, CONF.identity.default_domain_id) + return {'tenant': self.filter_domain_id(ref)} + + # CRUD Extension + @controller.v2_deprecated + def create_project(self, context, tenant): + tenant_ref = self._normalize_dict(tenant) + + if 'name' not in tenant_ref or not tenant_ref['name']: + msg = _('Name field is required and cannot be empty') + raise exception.ValidationError(message=msg) + + self.assert_admin(context) + tenant_ref['id'] = tenant_ref.get('id', uuid.uuid4().hex) + tenant = self.resource_api.create_project( + tenant_ref['id'], + self._normalize_domain_id(context, tenant_ref)) + return {'tenant': self.filter_domain_id(tenant)} + + @controller.v2_deprecated + def update_project(self, context, tenant_id, tenant): + self.assert_admin(context) + # Remove domain_id if specified - a v2 api caller should not + # be specifying that + clean_tenant = tenant.copy() + clean_tenant.pop('domain_id', None) + + tenant_ref = self.resource_api.update_project( + tenant_id, clean_tenant) + return {'tenant': tenant_ref} + + @controller.v2_deprecated + def delete_project(self, context, tenant_id): + self.assert_admin(context) + self.resource_api.delete_project(tenant_id) + + +@dependency.requires('resource_api') +class DomainV3(controller.V3Controller): + collection_name = 'domains' + member_name = 'domain' + + def __init__(self): + super(DomainV3, self).__init__() + self.get_member_from_driver = self.resource_api.get_domain + + @controller.protected() + @validation.validated(schema.domain_create, 'domain') + def create_domain(self, context, domain): + ref = self._assign_unique_id(self._normalize_dict(domain)) + initiator = notifications._get_request_audit_info(context) + ref = self.resource_api.create_domain(ref['id'], ref, initiator) + return DomainV3.wrap_member(context, ref) + + @controller.filterprotected('enabled', 'name') + def list_domains(self, context, filters): + hints = DomainV3.build_driver_hints(context, filters) + refs = self.resource_api.list_domains(hints=hints) + return DomainV3.wrap_collection(context, refs, hints=hints) + + @controller.protected() + def get_domain(self, context, domain_id): + ref = self.resource_api.get_domain(domain_id) + return DomainV3.wrap_member(context, ref) + + @controller.protected() + @validation.validated(schema.domain_update, 'domain') + def update_domain(self, context, domain_id, domain): + self._require_matching_id(domain_id, domain) + initiator = notifications._get_request_audit_info(context) + ref = self.resource_api.update_domain(domain_id, domain, initiator) + return DomainV3.wrap_member(context, ref) + + @controller.protected() + def delete_domain(self, context, domain_id): + initiator = notifications._get_request_audit_info(context) + return self.resource_api.delete_domain(domain_id, initiator) + + +@dependency.requires('domain_config_api') +class DomainConfigV3(controller.V3Controller): + member_name = 'config' + + @controller.protected() + def create_domain_config(self, context, domain_id, config): + original_config = ( + self.domain_config_api.get_config_with_sensitive_info(domain_id)) + ref = self.domain_config_api.create_config(domain_id, config) + if original_config: + # Return status code 200, since config already existed + return wsgi.render_response(body={self.member_name: ref}) + else: + return wsgi.render_response(body={self.member_name: ref}, + status=('201', 'Created')) + + @controller.protected() + def get_domain_config(self, context, domain_id, group=None, option=None): + ref = self.domain_config_api.get_config(domain_id, group, option) + return {self.member_name: ref} + + @controller.protected() + def update_domain_config( + self, context, domain_id, config, group, option): + ref = self.domain_config_api.update_config( + domain_id, config, group, option) + return wsgi.render_response(body={self.member_name: ref}) + + def update_domain_config_group(self, context, domain_id, group, config): + return self.update_domain_config( + context, domain_id, config, group, option=None) + + def update_domain_config_only(self, context, domain_id, config): + return self.update_domain_config( + context, domain_id, config, group=None, option=None) + + @controller.protected() + def delete_domain_config( + self, context, domain_id, group=None, option=None): + self.domain_config_api.delete_config(domain_id, group, option) + + +@dependency.requires('resource_api') +class ProjectV3(controller.V3Controller): + collection_name = 'projects' + member_name = 'project' + + def __init__(self): + super(ProjectV3, self).__init__() + self.get_member_from_driver = self.resource_api.get_project + + @controller.protected() + @validation.validated(schema.project_create, 'project') + def create_project(self, context, project): + ref = self._assign_unique_id(self._normalize_dict(project)) + ref = self._normalize_domain_id(context, ref) + initiator = notifications._get_request_audit_info(context) + ref = self.resource_api.create_project(ref['id'], ref, + initiator=initiator) + return ProjectV3.wrap_member(context, ref) + + @controller.filterprotected('domain_id', 'enabled', 'name', + 'parent_id') + def list_projects(self, context, filters): + hints = ProjectV3.build_driver_hints(context, filters) + refs = self.resource_api.list_projects(hints=hints) + return ProjectV3.wrap_collection(context, refs, hints=hints) + + def _expand_project_ref(self, context, ref): + params = context['query_string'] + + parents_as_list = 'parents_as_list' in params and ( + self.query_filter_is_true(params['parents_as_list'])) + parents_as_ids = 'parents_as_ids' in params and ( + self.query_filter_is_true(params['parents_as_ids'])) + + subtree_as_list = 'subtree_as_list' in params and ( + self.query_filter_is_true(params['subtree_as_list'])) + subtree_as_ids = 'subtree_as_ids' in params and ( + self.query_filter_is_true(params['subtree_as_ids'])) + + # parents_as_list and parents_as_ids are mutually exclusive + if parents_as_list and parents_as_ids: + msg = _('Cannot use parents_as_list and parents_as_ids query ' + 'params at the same time.') + raise exception.ValidationError(msg) + + # subtree_as_list and subtree_as_ids are mutually exclusive + if subtree_as_list and subtree_as_ids: + msg = _('Cannot use subtree_as_list and subtree_as_ids query ' + 'params at the same time.') + raise exception.ValidationError(msg) + + user_id = self.get_auth_context(context).get('user_id') + + if parents_as_list: + parents = self.resource_api.list_project_parents( + ref['id'], user_id) + ref['parents'] = [ProjectV3.wrap_member(context, p) + for p in parents] + elif parents_as_ids: + ref['parents'] = self.resource_api.get_project_parents_as_ids(ref) + + if subtree_as_list: + subtree = self.resource_api.list_projects_in_subtree( + ref['id'], user_id) + ref['subtree'] = [ProjectV3.wrap_member(context, p) + for p in subtree] + elif subtree_as_ids: + ref['subtree'] = self.resource_api.get_projects_in_subtree_as_ids( + ref['id']) + + @controller.protected() + def get_project(self, context, project_id): + ref = self.resource_api.get_project(project_id) + self._expand_project_ref(context, ref) + return ProjectV3.wrap_member(context, ref) + + @controller.protected() + @validation.validated(schema.project_update, 'project') + def update_project(self, context, project_id, project): + self._require_matching_id(project_id, project) + self._require_matching_domain_id( + project_id, project, self.resource_api.get_project) + initiator = notifications._get_request_audit_info(context) + ref = self.resource_api.update_project(project_id, project, + initiator=initiator) + return ProjectV3.wrap_member(context, ref) + + @controller.protected() + def delete_project(self, context, project_id): + initiator = notifications._get_request_audit_info(context) + return self.resource_api.delete_project(project_id, + initiator=initiator) diff --git a/keystone-moon/keystone/resource/core.py b/keystone-moon/keystone/resource/core.py new file mode 100644 index 00000000..017eb4e7 --- /dev/null +++ b/keystone-moon/keystone/resource/core.py @@ -0,0 +1,1324 @@ +# 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. + +"""Main entry point into the resource service.""" + +import abc + +from oslo_config import cfg +from oslo_log import log +import six + +from keystone import clean +from keystone.common import cache +from keystone.common import dependency +from keystone.common import driver_hints +from keystone.common import manager +from keystone.contrib import federation +from keystone import exception +from keystone.i18n import _, _LE, _LW +from keystone import notifications + + +CONF = cfg.CONF +LOG = log.getLogger(__name__) +MEMOIZE = cache.get_memoization_decorator(section='resource') + + +def calc_default_domain(): + return {'description': + (u'Owns users and tenants (i.e. projects)' + ' available on Identity API v2.'), + 'enabled': True, + 'id': CONF.identity.default_domain_id, + 'name': u'Default'} + + +@dependency.provider('resource_api') +@dependency.requires('assignment_api', 'credential_api', 'domain_config_api', + 'identity_api', 'revoke_api') +class Manager(manager.Manager): + """Default pivot point for the resource backend. + + See :mod:`keystone.common.manager.Manager` for more details on how this + dynamically calls the backend. + + """ + _DOMAIN = 'domain' + _PROJECT = 'project' + + def __init__(self): + # If there is a specific driver specified for resource, then use it. + # Otherwise retrieve the driver type from the assignment driver. + resource_driver = CONF.resource.driver + + if resource_driver is None: + assignment_driver = ( + dependency.get_provider('assignment_api').driver) + resource_driver = assignment_driver.default_resource_driver() + + super(Manager, self).__init__(resource_driver) + + def _get_hierarchy_depth(self, parents_list): + return len(parents_list) + 1 + + def _assert_max_hierarchy_depth(self, project_id, parents_list=None): + if parents_list is None: + parents_list = self.list_project_parents(project_id) + max_depth = CONF.max_project_tree_depth + if self._get_hierarchy_depth(parents_list) > max_depth: + raise exception.ForbiddenAction( + action=_('max hierarchy depth reached for ' + '%s branch.') % project_id) + + def create_project(self, tenant_id, tenant, initiator=None): + tenant = tenant.copy() + tenant.setdefault('enabled', True) + tenant['enabled'] = clean.project_enabled(tenant['enabled']) + tenant.setdefault('description', '') + tenant.setdefault('parent_id', None) + + if tenant.get('parent_id') is not None: + parent_ref = self.get_project(tenant.get('parent_id')) + parents_list = self.list_project_parents(parent_ref['id']) + parents_list.append(parent_ref) + for ref in parents_list: + if ref.get('domain_id') != tenant.get('domain_id'): + raise exception.ForbiddenAction( + action=_('cannot create a project within a different ' + 'domain than its parents.')) + if not ref.get('enabled', True): + raise exception.ForbiddenAction( + action=_('cannot create a project in a ' + 'branch containing a disabled ' + 'project: %s') % ref['id']) + self._assert_max_hierarchy_depth(tenant.get('parent_id'), + parents_list) + + ret = self.driver.create_project(tenant_id, tenant) + notifications.Audit.created(self._PROJECT, tenant_id, initiator) + if MEMOIZE.should_cache(ret): + self.get_project.set(ret, self, tenant_id) + self.get_project_by_name.set(ret, self, ret['name'], + ret['domain_id']) + return ret + + def assert_domain_enabled(self, domain_id, domain=None): + """Assert the Domain is enabled. + + :raise AssertionError if domain is disabled. + """ + if domain is None: + domain = self.get_domain(domain_id) + if not domain.get('enabled', True): + raise AssertionError(_('Domain is disabled: %s') % domain_id) + + def assert_domain_not_federated(self, domain_id, domain): + """Assert the Domain's name and id do not match the reserved keyword. + + Note that the reserved keyword is defined in the configuration file, + by default, it is 'Federated', it is also case insensitive. + If config's option is empty the default hardcoded value 'Federated' + will be used. + + :raise AssertionError if domain named match the value in the config. + + """ + # NOTE(marek-denis): We cannot create this attribute in the __init__ as + # config values are always initialized to default value. + federated_domain = (CONF.federation.federated_domain_name or + federation.FEDERATED_DOMAIN_KEYWORD).lower() + if (domain.get('name') and domain['name'].lower() == federated_domain): + raise AssertionError(_('Domain cannot be named %s') + % federated_domain) + if (domain_id.lower() == federated_domain): + raise AssertionError(_('Domain cannot have ID %s') + % federated_domain) + + def assert_project_enabled(self, project_id, project=None): + """Assert the project is enabled and its associated domain is enabled. + + :raise AssertionError if the project or domain is disabled. + """ + if project is None: + project = self.get_project(project_id) + self.assert_domain_enabled(domain_id=project['domain_id']) + if not project.get('enabled', True): + raise AssertionError(_('Project is disabled: %s') % project_id) + + @notifications.disabled(_PROJECT, public=False) + def _disable_project(self, project_id): + """Emit a notification to the callback system project is been disabled. + + This method, and associated callback listeners, removes the need for + making direct calls to other managers to take action (e.g. revoking + project scoped tokens) when a project is disabled. + + :param project_id: project identifier + :type project_id: string + """ + pass + + def _assert_all_parents_are_enabled(self, project_id): + parents_list = self.list_project_parents(project_id) + for project in parents_list: + if not project.get('enabled', True): + raise exception.ForbiddenAction( + action=_('cannot enable project %s since it has ' + 'disabled parents') % project_id) + + def _assert_whole_subtree_is_disabled(self, project_id): + subtree_list = self.driver.list_projects_in_subtree(project_id) + for ref in subtree_list: + if ref.get('enabled', True): + raise exception.ForbiddenAction( + action=_('cannot disable project %s since ' + 'its subtree contains enabled ' + 'projects') % project_id) + + def update_project(self, tenant_id, tenant, initiator=None): + original_tenant = self.driver.get_project(tenant_id) + tenant = tenant.copy() + + parent_id = original_tenant.get('parent_id') + if 'parent_id' in tenant and tenant.get('parent_id') != parent_id: + raise exception.ForbiddenAction( + action=_('Update of `parent_id` is not allowed.')) + + if 'enabled' in tenant: + tenant['enabled'] = clean.project_enabled(tenant['enabled']) + + # NOTE(rodrigods): for the current implementation we only allow to + # disable a project if all projects below it in the hierarchy are + # already disabled. This also means that we can not enable a + # project that has disabled parents. + original_tenant_enabled = original_tenant.get('enabled', True) + tenant_enabled = tenant.get('enabled', True) + if not original_tenant_enabled and tenant_enabled: + self._assert_all_parents_are_enabled(tenant_id) + if original_tenant_enabled and not tenant_enabled: + self._assert_whole_subtree_is_disabled(tenant_id) + self._disable_project(tenant_id) + + ret = self.driver.update_project(tenant_id, tenant) + notifications.Audit.updated(self._PROJECT, tenant_id, initiator) + self.get_project.invalidate(self, tenant_id) + self.get_project_by_name.invalidate(self, original_tenant['name'], + original_tenant['domain_id']) + return ret + + def delete_project(self, tenant_id, initiator=None): + if not self.driver.is_leaf_project(tenant_id): + raise exception.ForbiddenAction( + action=_('cannot delete the project %s since it is not ' + 'a leaf in the hierarchy.') % tenant_id) + + project = self.driver.get_project(tenant_id) + project_user_ids = ( + self.assignment_api.list_user_ids_for_project(tenant_id)) + for user_id in project_user_ids: + payload = {'user_id': user_id, 'project_id': tenant_id} + self._emit_invalidate_user_project_tokens_notification(payload) + ret = self.driver.delete_project(tenant_id) + self.assignment_api.delete_project_assignments(tenant_id) + self.get_project.invalidate(self, tenant_id) + self.get_project_by_name.invalidate(self, project['name'], + project['domain_id']) + self.credential_api.delete_credentials_for_project(tenant_id) + notifications.Audit.deleted(self._PROJECT, tenant_id, initiator) + return ret + + def _filter_projects_list(self, projects_list, user_id): + user_projects = self.assignment_api.list_projects_for_user(user_id) + user_projects_ids = set([proj['id'] for proj in user_projects]) + # Keep only the projects present in user_projects + projects_list = [proj for proj in projects_list + if proj['id'] in user_projects_ids] + + def list_project_parents(self, project_id, user_id=None): + parents = self.driver.list_project_parents(project_id) + # If a user_id was provided, the returned list should be filtered + # against the projects this user has access to. + if user_id: + self._filter_projects_list(parents, user_id) + return parents + + def _build_parents_as_ids_dict(self, project, parents_by_id): + # NOTE(rodrigods): we don't rely in the order of the projects returned + # by the list_project_parents() method. Thus, we create a project cache + # (parents_by_id) in order to access each parent in constant time and + # traverse up the hierarchy. + def traverse_parents_hierarchy(project): + parent_id = project.get('parent_id') + if not parent_id: + return None + + parent = parents_by_id[parent_id] + return {parent_id: traverse_parents_hierarchy(parent)} + + return traverse_parents_hierarchy(project) + + def get_project_parents_as_ids(self, project): + """Gets the IDs from the parents from a given project. + + The project IDs are returned as a structured dictionary traversing up + the hierarchy to the top level project. For example, considering the + following project hierarchy:: + + A + | + +-B-+ + | | + C D + + If we query for project C parents, the expected return is the following + dictionary:: + + 'parents': { + B['id']: { + A['id']: None + } + } + + """ + parents_list = self.list_project_parents(project['id']) + parents_as_ids = self._build_parents_as_ids_dict( + project, {proj['id']: proj for proj in parents_list}) + return parents_as_ids + + def list_projects_in_subtree(self, project_id, user_id=None): + subtree = self.driver.list_projects_in_subtree(project_id) + # If a user_id was provided, the returned list should be filtered + # against the projects this user has access to. + if user_id: + self._filter_projects_list(subtree, user_id) + return subtree + + def _build_subtree_as_ids_dict(self, project_id, subtree_by_parent): + # NOTE(rodrigods): we perform a depth first search to construct the + # dictionaries representing each level of the subtree hierarchy. In + # order to improve this traversal performance, we create a cache of + # projects (subtree_py_parent) that accesses in constant time the + # direct children of a given project. + def traverse_subtree_hierarchy(project_id): + children = subtree_by_parent.get(project_id) + if not children: + return None + + children_ids = {} + for child in children: + children_ids[child['id']] = traverse_subtree_hierarchy( + child['id']) + return children_ids + + return traverse_subtree_hierarchy(project_id) + + def get_projects_in_subtree_as_ids(self, project_id): + """Gets the IDs from the projects in the subtree from a given project. + + The project IDs are returned as a structured dictionary representing + their hierarchy. For example, considering the following project + hierarchy:: + + A + | + +-B-+ + | | + C D + + If we query for project A subtree, the expected return is the following + dictionary:: + + 'subtree': { + B['id']: { + C['id']: None, + D['id']: None + } + } + + """ + def _projects_indexed_by_parent(projects_list): + projects_by_parent = {} + for proj in projects_list: + parent_id = proj.get('parent_id') + if parent_id: + if parent_id in projects_by_parent: + projects_by_parent[parent_id].append(proj) + else: + projects_by_parent[parent_id] = [proj] + return projects_by_parent + + subtree_list = self.list_projects_in_subtree(project_id) + subtree_as_ids = self._build_subtree_as_ids_dict( + project_id, _projects_indexed_by_parent(subtree_list)) + return subtree_as_ids + + @MEMOIZE + def get_domain(self, domain_id): + return self.driver.get_domain(domain_id) + + @MEMOIZE + def get_domain_by_name(self, domain_name): + return self.driver.get_domain_by_name(domain_name) + + def create_domain(self, domain_id, domain, initiator=None): + if (not self.identity_api.multiple_domains_supported and + domain_id != CONF.identity.default_domain_id): + raise exception.Forbidden(_('Multiple domains are not supported')) + self.assert_domain_not_federated(domain_id, domain) + domain.setdefault('enabled', True) + domain['enabled'] = clean.domain_enabled(domain['enabled']) + ret = self.driver.create_domain(domain_id, domain) + + notifications.Audit.created(self._DOMAIN, domain_id, initiator) + + if MEMOIZE.should_cache(ret): + self.get_domain.set(ret, self, domain_id) + self.get_domain_by_name.set(ret, self, ret['name']) + return ret + + @manager.response_truncated + def list_domains(self, hints=None): + return self.driver.list_domains(hints or driver_hints.Hints()) + + @notifications.disabled(_DOMAIN, public=False) + def _disable_domain(self, domain_id): + """Emit a notification to the callback system domain is been disabled. + + This method, and associated callback listeners, removes the need for + making direct calls to other managers to take action (e.g. revoking + domain scoped tokens) when a domain is disabled. + + :param domain_id: domain identifier + :type domain_id: string + """ + pass + + def update_domain(self, domain_id, domain, initiator=None): + self.assert_domain_not_federated(domain_id, domain) + original_domain = self.driver.get_domain(domain_id) + if 'enabled' in domain: + domain['enabled'] = clean.domain_enabled(domain['enabled']) + ret = self.driver.update_domain(domain_id, domain) + notifications.Audit.updated(self._DOMAIN, domain_id, initiator) + # disable owned users & projects when the API user specifically set + # enabled=False + if (original_domain.get('enabled', True) and + not domain.get('enabled', True)): + notifications.Audit.disabled(self._DOMAIN, domain_id, initiator, + public=False) + + self.get_domain.invalidate(self, domain_id) + self.get_domain_by_name.invalidate(self, original_domain['name']) + return ret + + def delete_domain(self, domain_id, initiator=None): + # explicitly forbid deleting the default domain (this should be a + # carefully orchestrated manual process involving configuration + # changes, etc) + if domain_id == CONF.identity.default_domain_id: + raise exception.ForbiddenAction(action=_('delete the default ' + 'domain')) + + domain = self.driver.get_domain(domain_id) + + # To help avoid inadvertent deletes, we insist that the domain + # has been previously disabled. This also prevents a user deleting + # their own domain since, once it is disabled, they won't be able + # to get a valid token to issue this delete. + if domain['enabled']: + raise exception.ForbiddenAction( + action=_('cannot delete a domain that is enabled, ' + 'please disable it first.')) + + self._delete_domain_contents(domain_id) + # Delete any database stored domain config + self.domain_config_api.delete_config_options(domain_id) + self.domain_config_api.delete_config_options(domain_id, sensitive=True) + # TODO(henry-nash): Although the controller will ensure deletion of + # all users & groups within the domain (which will cause all + # assignments for those users/groups to also be deleted), there + # could still be assignments on this domain for users/groups in + # other domains - so we should delete these here by making a call + # to the backend to delete all assignments for this domain. + # (see Bug #1277847) + self.driver.delete_domain(domain_id) + notifications.Audit.deleted(self._DOMAIN, domain_id, initiator) + self.get_domain.invalidate(self, domain_id) + self.get_domain_by_name.invalidate(self, domain['name']) + + def _delete_domain_contents(self, domain_id): + """Delete the contents of a domain. + + Before we delete a domain, we need to remove all the entities + that are owned by it, i.e. Projects. To do this we + call the delete function for these entities, which are + themselves responsible for deleting any credentials and role grants + associated with them as well as revoking any relevant tokens. + + """ + + def _delete_projects(project, projects, examined): + if project['id'] in examined: + msg = _LE('Circular reference or a repeated entry found ' + 'projects hierarchy - %(project_id)s.') + LOG.error(msg, {'project_id': project['id']}) + return + + examined.add(project['id']) + children = [proj for proj in projects + if proj.get('parent_id') == project['id']] + for proj in children: + _delete_projects(proj, projects, examined) + + try: + self.delete_project(project['id']) + except exception.ProjectNotFound: + LOG.debug(('Project %(projectid)s not found when ' + 'deleting domain contents for %(domainid)s, ' + 'continuing with cleanup.'), + {'projectid': project['id'], + 'domainid': domain_id}) + + proj_refs = self.list_projects_in_domain(domain_id) + + # Deleting projects recursively + roots = [x for x in proj_refs if x.get('parent_id') is None] + examined = set() + for project in roots: + _delete_projects(project, proj_refs, examined) + + @manager.response_truncated + def list_projects(self, hints=None): + return self.driver.list_projects(hints or driver_hints.Hints()) + + # NOTE(henry-nash): list_projects_in_domain is actually an internal method + # and not exposed via the API. Therefore there is no need to support + # driver hints for it. + def list_projects_in_domain(self, domain_id): + return self.driver.list_projects_in_domain(domain_id) + + @MEMOIZE + def get_project(self, project_id): + return self.driver.get_project(project_id) + + @MEMOIZE + def get_project_by_name(self, tenant_name, domain_id): + return self.driver.get_project_by_name(tenant_name, domain_id) + + @notifications.internal( + notifications.INVALIDATE_USER_PROJECT_TOKEN_PERSISTENCE) + def _emit_invalidate_user_project_tokens_notification(self, payload): + # This notification's payload is a dict of user_id and + # project_id so the token provider can invalidate the tokens + # from persistence if persistence is enabled. + pass + + +@six.add_metaclass(abc.ABCMeta) +class Driver(object): + + def _get_list_limit(self): + return CONF.resource.list_limit or CONF.list_limit + + @abc.abstractmethod + def get_project_by_name(self, tenant_name, domain_id): + """Get a tenant by name. + + :returns: tenant_ref + :raises: keystone.exception.ProjectNotFound + + """ + raise exception.NotImplemented() # pragma: no cover + + # domain crud + @abc.abstractmethod + def create_domain(self, domain_id, domain): + """Creates a new domain. + + :raises: keystone.exception.Conflict + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_domains(self, hints): + """List domains in the system. + + :param hints: filter hints which the driver should + implement if at all possible. + + :returns: a list of domain_refs or an empty list. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_domains_from_ids(self, domain_ids): + """List domains for the provided list of ids. + + :param domain_ids: list of ids + + :returns: a list of domain_refs. + + This method is used internally by the assignment manager to bulk read + a set of domains given their ids. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def get_domain(self, domain_id): + """Get a domain by ID. + + :returns: domain_ref + :raises: keystone.exception.DomainNotFound + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def get_domain_by_name(self, domain_name): + """Get a domain by name. + + :returns: domain_ref + :raises: keystone.exception.DomainNotFound + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def update_domain(self, domain_id, domain): + """Updates an existing domain. + + :raises: keystone.exception.DomainNotFound, + keystone.exception.Conflict + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def delete_domain(self, domain_id): + """Deletes an existing domain. + + :raises: keystone.exception.DomainNotFound + + """ + raise exception.NotImplemented() # pragma: no cover + + # project crud + @abc.abstractmethod + def create_project(self, project_id, project): + """Creates a new project. + + :raises: keystone.exception.Conflict + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_projects(self, hints): + """List projects in the system. + + :param hints: filter hints which the driver should + implement if at all possible. + + :returns: a list of project_refs or an empty list. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_projects_from_ids(self, project_ids): + """List projects for the provided list of ids. + + :param project_ids: list of ids + + :returns: a list of project_refs. + + This method is used internally by the assignment manager to bulk read + a set of projects given their ids. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_project_ids_from_domain_ids(self, domain_ids): + """List project ids for the provided list of domain ids. + + :param domain_ids: list of domain ids + + :returns: a list of project ids owned by the specified domain ids. + + This method is used internally by the assignment manager to bulk read + a set of project ids given a list of domain ids. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_projects_in_domain(self, domain_id): + """List projects in the domain. + + :param domain_id: the driver MUST only return projects + within this domain. + + :returns: a list of project_refs or an empty list. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def get_project(self, project_id): + """Get a project by ID. + + :returns: project_ref + :raises: keystone.exception.ProjectNotFound + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def update_project(self, project_id, project): + """Updates an existing project. + + :raises: keystone.exception.ProjectNotFound, + keystone.exception.Conflict + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def delete_project(self, project_id): + """Deletes an existing project. + + :raises: keystone.exception.ProjectNotFound + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_project_parents(self, project_id): + """List all parents from a project by its ID. + + :param project_id: the driver will list the parents of this + project. + + :returns: a list of project_refs or an empty list. + :raises: keystone.exception.ProjectNotFound + + """ + raise exception.NotImplemented() + + @abc.abstractmethod + def list_projects_in_subtree(self, project_id): + """List all projects in the subtree below the hierarchy of the + given project. + + :param project_id: the driver will get the subtree under + this project. + + :returns: a list of project_refs or an empty list + :raises: keystone.exception.ProjectNotFound + + """ + raise exception.NotImplemented() + + @abc.abstractmethod + def is_leaf_project(self, project_id): + """Checks if a project is a leaf in the hierarchy. + + :param project_id: the driver will check if this project + is a leaf in the hierarchy. + + :raises: keystone.exception.ProjectNotFound + + """ + raise exception.NotImplemented() + + # Domain management functions for backends that only allow a single + # domain. Currently, this is only LDAP, but might be used by other + # backends in the future. + def _set_default_domain(self, ref): + """If the domain ID has not been set, set it to the default.""" + if isinstance(ref, dict): + if 'domain_id' not in ref: + ref = ref.copy() + ref['domain_id'] = CONF.identity.default_domain_id + return ref + elif isinstance(ref, list): + return [self._set_default_domain(x) for x in ref] + else: + raise ValueError(_('Expected dict or list: %s') % type(ref)) + + def _validate_default_domain(self, ref): + """Validate that either the default domain or nothing is specified. + + Also removes the domain from the ref so that LDAP doesn't have to + persist the attribute. + + """ + ref = ref.copy() + domain_id = ref.pop('domain_id', CONF.identity.default_domain_id) + self._validate_default_domain_id(domain_id) + return ref + + def _validate_default_domain_id(self, domain_id): + """Validate that the domain ID specified belongs to the default domain. + + """ + if domain_id != CONF.identity.default_domain_id: + raise exception.DomainNotFound(domain_id=domain_id) + + +@dependency.provider('domain_config_api') +class DomainConfigManager(manager.Manager): + """Default pivot point for the Domain Config backend.""" + + # NOTE(henry-nash): In order for a config option to be stored in the + # standard table, it must be explicitly whitelisted. Options marked as + # sensitive are stored in a separate table. Attempting to store options + # that are not listed as either whitelisted or sensitive will raise an + # exception. + # + # Only those options that affect the domain-specific driver support in + # the identity manager are supported. + + whitelisted_options = { + 'identity': ['driver'], + 'ldap': [ + 'url', 'user', 'suffix', 'use_dumb_member', 'dumb_member', + 'allow_subtree_delete', 'query_scope', 'page_size', + 'alias_dereferencing', 'debug_level', 'chase_referrals', + 'user_tree_dn', 'user_filter', 'user_objectclass', + 'user_id_attribute', 'user_name_attribute', 'user_mail_attribute', + 'user_pass_attribute', 'user_enabled_attribute', + 'user_enabled_invert', 'user_enabled_mask', 'user_enabled_default', + 'user_attribute_ignore', 'user_default_project_id_attribute', + 'user_allow_create', 'user_allow_update', 'user_allow_delete', + 'user_enabled_emulation', 'user_enabled_emulation_dn', + 'user_additional_attribute_mapping', 'group_tree_dn', + 'group_filter', 'group_objectclass', 'group_id_attribute', + 'group_name_attribute', 'group_member_attribute', + 'group_desc_attribute', 'group_attribute_ignore', + 'group_allow_create', 'group_allow_update', 'group_allow_delete', + 'group_additional_attribute_mapping', 'tls_cacertfile', + 'tls_cacertdir', 'use_tls', 'tls_req_cert', 'use_pool', + 'pool_size', 'pool_retry_max', 'pool_retry_delay', + 'pool_connection_timeout', 'pool_connection_lifetime', + 'use_auth_pool', 'auth_pool_size', 'auth_pool_connection_lifetime' + ] + } + sensitive_options = { + 'identity': [], + 'ldap': ['password'] + } + + def __init__(self): + super(DomainConfigManager, self).__init__(CONF.domain_config.driver) + + def _assert_valid_config(self, config): + """Ensure the options in the config are valid. + + This method is called to validate the request config in create and + update manager calls. + + :param config: config structure being created or updated + + """ + # Something must be defined in the request + if not config: + raise exception.InvalidDomainConfig( + reason=_('No options specified')) + + # Make sure the groups/options defined in config itself are valid + for group in config: + if (not config[group] or not + isinstance(config[group], dict)): + msg = _('The value of group %(group)s specified in the ' + 'config should be a dictionary of options') % { + 'group': group} + raise exception.InvalidDomainConfig(reason=msg) + for option in config[group]: + self._assert_valid_group_and_option(group, option) + + def _assert_valid_group_and_option(self, group, option): + """Ensure the combination of group and option is valid. + + :param group: optional group name, if specified it must be one + we support + :param option: optional option name, if specified it must be one + we support and a group must also be specified + + """ + if not group and not option: + # For all calls, it's OK for neither to be defined, it means you + # are operating on all config options for that domain. + return + + if not group and option: + # Our API structure should prevent this from ever happening, so if + # it does, then this is coding error. + msg = _('Option %(option)s found with no group specified while ' + 'checking domain configuration request') % { + 'option': option} + raise exception.UnexpectedError(exception=msg) + + if (group and group not in self.whitelisted_options and + group not in self.sensitive_options): + msg = _('Group %(group)s is not supported ' + 'for domain specific configurations') % {'group': group} + raise exception.InvalidDomainConfig(reason=msg) + + if option: + if (option not in self.whitelisted_options[group] and option not in + self.sensitive_options[group]): + msg = _('Option %(option)s in group %(group)s is not ' + 'supported for domain specific configurations') % { + 'group': group, 'option': option} + raise exception.InvalidDomainConfig(reason=msg) + + def _is_sensitive(self, group, option): + return option in self.sensitive_options[group] + + def _config_to_list(self, config): + """Build whitelisted and sensitive lists for use by backend drivers.""" + + whitelisted = [] + sensitive = [] + for group in config: + for option in config[group]: + the_list = (sensitive if self._is_sensitive(group, option) + else whitelisted) + the_list.append({ + 'group': group, 'option': option, + 'value': config[group][option]}) + + return whitelisted, sensitive + + def _list_to_config(self, whitelisted, sensitive=None, req_option=None): + """Build config dict from a list of option dicts. + + :param whitelisted: list of dicts containing options and their groups, + this has already been filtered to only contain + those options to include in the output. + :param sensitive: list of dicts containing sensitive options and their + groups, this has already been filtered to only + contain those options to include in the output. + :param req_option: the individual option requested + + :returns: a config dict, including sensitive if specified + + """ + the_list = whitelisted + (sensitive or []) + if not the_list: + return {} + + if req_option: + # The request was specific to an individual option, so + # no need to include the group in the output. We first check that + # there is only one option in the answer (and that it's the right + # one) - if not, something has gone wrong and we raise an error + if len(the_list) > 1 or the_list[0]['option'] != req_option: + LOG.error(_LE('Unexpected results in response for domain ' + 'config - %(count)s responses, first option is ' + '%(option)s, expected option %(expected)s'), + {'count': len(the_list), 'option': list[0]['option'], + 'expected': req_option}) + raise exception.UnexpectedError( + _('An unexpected error occurred when retrieving domain ' + 'configs')) + return {the_list[0]['option']: the_list[0]['value']} + + config = {} + for option in the_list: + config.setdefault(option['group'], {}) + config[option['group']][option['option']] = option['value'] + + return config + + def create_config(self, domain_id, config): + """Create config for a domain + + :param domain_id: the domain in question + :param config: the dict of config groups/options to assign to the + domain + + Creates a new config, overwriting any previous config (no Conflict + error will be generated). + + :returns: a dict of group dicts containing the options, with any that + are sensitive removed + :raises keystone.exception.InvalidDomainConfig: when the config + contains options we do not support + + """ + self._assert_valid_config(config) + whitelisted, sensitive = self._config_to_list(config) + # Delete any existing config + self.delete_config_options(domain_id) + self.delete_config_options(domain_id, sensitive=True) + # ...and create the new one + for option in whitelisted: + self.create_config_option( + domain_id, option['group'], option['option'], option['value']) + for option in sensitive: + self.create_config_option( + domain_id, option['group'], option['option'], option['value'], + sensitive=True) + return self._list_to_config(whitelisted) + + def get_config(self, domain_id, group=None, option=None): + """Get config, or partial config, for a domain + + :param domain_id: the domain in question + :param group: an optional specific group of options + :param option: an optional specific option within the group + + :returns: a dict of group dicts containing the whitelisted options, + filtered by group and option specified + :raises keystone.exception.DomainConfigNotFound: when no config found + that matches domain_id, group and option specified + :raises keystone.exception.InvalidDomainConfig: when the config + and group/option parameters specify an option we do not + support + + An example response:: + + { + 'ldap': { + 'url': 'myurl' + 'user_tree_dn': 'OU=myou'}, + 'identity': { + 'driver': 'keystone.identity.backends.ldap.Identity'} + + } + + """ + self._assert_valid_group_and_option(group, option) + whitelisted = self.list_config_options(domain_id, group, option) + if whitelisted: + return self._list_to_config(whitelisted, req_option=option) + + if option: + msg = _('option %(option)s in group %(group)s') % { + 'group': group, 'option': option} + elif group: + msg = _('group %(group)s') % {'group': group} + else: + msg = _('any options') + raise exception.DomainConfigNotFound( + domain_id=domain_id, group_or_option=msg) + + def update_config(self, domain_id, config, group=None, option=None): + """Update config, or partial config, for a domain + + :param domain_id: the domain in question + :param config: the config dict containing and groups/options being + updated + :param group: an optional specific group of options, which if specified + must appear in config, with no other groups + :param option: an optional specific option within the group, which if + specified must appear in config, with no other options + + The contents of the supplied config will be merged with the existing + config for this domain, updating or creating new options if these did + not previously exist. If group or option is specified, then the update + will be limited to those specified items and the inclusion of other + options in the supplied config will raise an exception, as will the + situation when those options do not already exist in the current + config. + + :returns: a dict of groups containing all whitelisted options + :raises keystone.exception.InvalidDomainConfig: when the config + and group/option parameters specify an option we do not + support or one that does not exist in the original config + + """ + def _assert_valid_update(domain_id, config, group=None, option=None): + """Ensure the combination of config, group and option is valid.""" + + self._assert_valid_config(config) + self._assert_valid_group_and_option(group, option) + + # If a group has been specified, then the request is to + # explicitly only update the options in that group - so the config + # must not contain anything else. Further, that group must exist in + # the original config. Likewise, if an option has been specified, + # then the group in the config must only contain that option and it + # also must exist in the original config. + if group: + if len(config) != 1 or (option and len(config[group]) != 1): + if option: + msg = _('Trying to update option %(option)s in group ' + '%(group)s, so that, and only that, option ' + 'must be specified in the config') % { + 'group': group, 'option': option} + else: + msg = _('Trying to update group %(group)s, so that, ' + 'and only that, group must be specified in ' + 'the config') % {'group': group} + raise exception.InvalidDomainConfig(reason=msg) + + # So we now know we have the right number of entries in the + # config that align with a group/option being specified, but we + # must also make sure they match. + if group not in config: + msg = _('request to update group %(group)s, but config ' + 'provided contains group %(group_other)s ' + 'instead') % { + 'group': group, + 'group_other': config.keys()[0]} + raise exception.InvalidDomainConfig(reason=msg) + if option and option not in config[group]: + msg = _('Trying to update option %(option)s in group ' + '%(group)s, but config provided contains option ' + '%(option_other)s instead') % { + 'group': group, 'option': option, + 'option_other': config[group].keys()[0]} + raise exception.InvalidDomainConfig(reason=msg) + + # Finally, we need to check if the group/option specified + # already exists in the original config - since if not, to keep + # with the semantics of an update, we need to fail with + # a DomainConfigNotFound + if not self.get_config_with_sensitive_info(domain_id, + group, option): + if option: + msg = _('option %(option)s in group %(group)s') % { + 'group': group, 'option': option} + raise exception.DomainConfigNotFound( + domain_id=domain_id, group_or_option=msg) + else: + msg = _('group %(group)s') % {'group': group} + raise exception.DomainConfigNotFound( + domain_id=domain_id, group_or_option=msg) + + def _update_or_create(domain_id, option, sensitive): + """Update the option, if it doesn't exist then create it.""" + + try: + self.create_config_option( + domain_id, option['group'], option['option'], + option['value'], sensitive=sensitive) + except exception.Conflict: + self.update_config_option( + domain_id, option['group'], option['option'], + option['value'], sensitive=sensitive) + + update_config = config + if group and option: + # The config will just be a dict containing the option and + # its value, so make it look like a single option under the + # group in question + update_config = {group: config} + + _assert_valid_update(domain_id, update_config, group, option) + + whitelisted, sensitive = self._config_to_list(update_config) + + for new_option in whitelisted: + _update_or_create(domain_id, new_option, sensitive=False) + for new_option in sensitive: + _update_or_create(domain_id, new_option, sensitive=True) + + return self.get_config(domain_id) + + def delete_config(self, domain_id, group=None, option=None): + """Delete config, or partial config, for the domain. + + :param domain_id: the domain in question + :param group: an optional specific group of options + :param option: an optional specific option within the group + + If group and option are None, then the entire config for the domain + is deleted. If group is not None, then just that group of options will + be deleted. If group and option are both specified, then just that + option is deleted. + + :raises keystone.exception.InvalidDomainConfig: when group/option + parameters specify an option we do not support or one that + does not exist in the original config. + + """ + self._assert_valid_group_and_option(group, option) + if group: + # As this is a partial delete, then make sure the items requested + # are valid and exist in the current config + current_config = self.get_config_with_sensitive_info(domain_id) + # Raise an exception if the group/options specified don't exist in + # the current config so that the delete method provides the + # correct error semantics. + current_group = current_config.get(group) + if not current_group: + msg = _('group %(group)s') % {'group': group} + raise exception.DomainConfigNotFound( + domain_id=domain_id, group_or_option=msg) + if option and not current_group.get(option): + msg = _('option %(option)s in group %(group)s') % { + 'group': group, 'option': option} + raise exception.DomainConfigNotFound( + domain_id=domain_id, group_or_option=msg) + + self.delete_config_options(domain_id, group, option) + self.delete_config_options(domain_id, group, option, sensitive=True) + + def get_config_with_sensitive_info(self, domain_id, group=None, + option=None): + """Get config for a domain with sensitive info included. + + This method is not exposed via the public API, but is used by the + identity manager to initialize a domain with the fully formed config + options. + + """ + whitelisted = self.list_config_options(domain_id, group, option) + sensitive = self.list_config_options(domain_id, group, option, + sensitive=True) + + # Check if there are any sensitive substitutions needed. We first try + # and simply ensure any sensitive options that have valid substitution + # references in the whitelisted options are substituted. We then check + # the resulting whitelisted option and raise a warning if there + # appears to be an unmatched or incorrectly constructed substitution + # reference. To avoid the risk of logging any sensitive options that + # have already been substituted, we first take a copy of the + # whitelisted option. + + # Build a dict of the sensitive options ready to try substitution + sensitive_dict = {s['option']: s['value'] for s in sensitive} + + for each_whitelisted in whitelisted: + if not isinstance(each_whitelisted['value'], six.string_types): + # We only support substitutions into string types, if its an + # integer, list etc. then just continue onto the next one + continue + + # Store away the original value in case we need to raise a warning + # after substitution. + original_value = each_whitelisted['value'] + warning_msg = '' + try: + each_whitelisted['value'] = ( + each_whitelisted['value'] % sensitive_dict) + except KeyError: + warning_msg = _LW( + 'Found what looks like an unmatched config option ' + 'substitution reference - domain: %(domain)s, group: ' + '%(group)s, option: %(option)s, value: %(value)s. Perhaps ' + 'the config option to which it refers has yet to be ' + 'added?') + except (ValueError, TypeError): + warning_msg = _LW( + 'Found what looks like an incorrectly constructed ' + 'config option substitution reference - domain: ' + '%(domain)s, group: %(group)s, option: %(option)s, ' + 'value: %(value)s.') + + if warning_msg: + LOG.warn(warning_msg % { + 'domain': domain_id, + 'group': each_whitelisted['group'], + 'option': each_whitelisted['option'], + 'value': original_value}) + + return self._list_to_config(whitelisted, sensitive) + + +@six.add_metaclass(abc.ABCMeta) +class DomainConfigDriver(object): + """Interface description for a Domain Config driver.""" + + @abc.abstractmethod + def create_config_option(self, domain_id, group, option, value, + sensitive=False): + """Creates a config option for a domain. + + :param domain_id: the domain for this option + :param group: the group name + :param option: the option name + :param value: the value to assign to this option + :param sensitive: whether the option is sensitive + + :returns: dict containing group, option and value + :raises: keystone.exception.Conflict + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def get_config_option(self, domain_id, group, option, sensitive=False): + """Gets the config option for a domain. + + :param domain_id: the domain for this option + :param group: the group name + :param option: the option name + :param sensitive: whether the option is sensitive + + :returns: dict containing group, option and value + :raises: keystone.exception.DomainConfigNotFound: the option doesn't + exist. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_config_options(self, domain_id, group=None, option=False, + sensitive=False): + """Gets a config options for a domain. + + :param domain_id: the domain for this option + :param group: optional group option name + :param option: optional option name. If group is None, then this + parameter is ignored + :param sensitive: whether the option is sensitive + + :returns: list of dicts containing group, option and value + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def update_config_option(self, domain_id, group, option, value, + sensitive=False): + """Updates a config option for a domain. + + :param domain_id: the domain for this option + :param group: the group option name + :param option: the option name + :param value: the value to assign to this option + :param sensitive: whether the option is sensitive + + :returns: dict containing updated group, option and value + :raises: keystone.exception.DomainConfigNotFound: the option doesn't + exist. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def delete_config_options(self, domain_id, group=None, option=None, + sensitive=False): + """Deletes config options for a domain. + + Allows deletion of all options for a domain, all options in a group + or a specific option. The driver is silent if there are no options + to delete. + + :param domain_id: the domain for this option + :param group: optional group option name + :param option: optional option name. If group is None, then this + parameter is ignored + :param sensitive: whether the option is sensitive + + """ + raise exception.NotImplemented() # pragma: no cover diff --git a/keystone-moon/keystone/resource/routers.py b/keystone-moon/keystone/resource/routers.py new file mode 100644 index 00000000..8ccd10aa --- /dev/null +++ b/keystone-moon/keystone/resource/routers.py @@ -0,0 +1,94 @@ +# Copyright 2013 Metacloud, Inc. +# Copyright 2012 OpenStack Foundation +# +# 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. + +"""WSGI Routers for the Resource service.""" + +from keystone.common import json_home +from keystone.common import router +from keystone.common import wsgi +from keystone.resource import controllers + + +class Admin(wsgi.ComposableRouter): + def add_routes(self, mapper): + # Tenant Operations + tenant_controller = controllers.Tenant() + mapper.connect('/tenants', + controller=tenant_controller, + action='get_all_projects', + conditions=dict(method=['GET'])) + mapper.connect('/tenants/{tenant_id}', + controller=tenant_controller, + action='get_project', + conditions=dict(method=['GET'])) + + +class Routers(wsgi.RoutersBase): + + def append_v3_routers(self, mapper, routers): + routers.append( + router.Router(controllers.DomainV3(), + 'domains', 'domain', + resource_descriptions=self.v3_resources)) + + config_controller = controllers.DomainConfigV3() + + self._add_resource( + mapper, config_controller, + path='/domains/{domain_id}/config', + get_head_action='get_domain_config', + put_action='create_domain_config', + patch_action='update_domain_config_only', + delete_action='delete_domain_config', + rel=json_home.build_v3_resource_relation('domain_config'), + status=json_home.Status.EXPERIMENTAL, + path_vars={ + 'domain_id': json_home.Parameters.DOMAIN_ID + }) + + config_group_param = ( + json_home.build_v3_parameter_relation('config_group')) + self._add_resource( + mapper, config_controller, + path='/domains/{domain_id}/config/{group}', + get_head_action='get_domain_config', + patch_action='update_domain_config_group', + delete_action='delete_domain_config', + rel=json_home.build_v3_resource_relation('domain_config_group'), + status=json_home.Status.EXPERIMENTAL, + path_vars={ + 'domain_id': json_home.Parameters.DOMAIN_ID, + 'group': config_group_param + }) + + self._add_resource( + mapper, config_controller, + path='/domains/{domain_id}/config/{group}/{option}', + get_head_action='get_domain_config', + patch_action='update_domain_config', + delete_action='delete_domain_config', + rel=json_home.build_v3_resource_relation('domain_config_option'), + status=json_home.Status.EXPERIMENTAL, + path_vars={ + 'domain_id': json_home.Parameters.DOMAIN_ID, + 'group': config_group_param, + 'option': json_home.build_v3_parameter_relation( + 'config_option') + }) + + routers.append( + router.Router(controllers.ProjectV3(), + 'projects', 'project', + resource_descriptions=self.v3_resources)) diff --git a/keystone-moon/keystone/resource/schema.py b/keystone-moon/keystone/resource/schema.py new file mode 100644 index 00000000..0fd59e3f --- /dev/null +++ b/keystone-moon/keystone/resource/schema.py @@ -0,0 +1,75 @@ +# 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 keystone.common import validation +from keystone.common.validation import parameter_types + + +_project_properties = { + 'description': validation.nullable(parameter_types.description), + # NOTE(lbragstad): domain_id isn't nullable according to some backends. + # The identity-api should be updated to be consistent with the + # implementation. + 'domain_id': parameter_types.id_string, + 'enabled': parameter_types.boolean, + 'parent_id': validation.nullable(parameter_types.id_string), + 'name': { + 'type': 'string', + 'minLength': 1, + 'maxLength': 64 + } +} + +project_create = { + 'type': 'object', + 'properties': _project_properties, + # NOTE(lbragstad): A project name is the only parameter required for + # project creation according to the Identity V3 API. We should think + # about using the maxProperties validator here, and in update. + 'required': ['name'], + 'additionalProperties': True +} + +project_update = { + 'type': 'object', + 'properties': _project_properties, + # NOTE(lbragstad) Make sure at least one property is being updated + 'minProperties': 1, + 'additionalProperties': True +} + +_domain_properties = { + 'description': validation.nullable(parameter_types.description), + 'enabled': parameter_types.boolean, + 'name': { + 'type': 'string', + 'minLength': 1, + 'maxLength': 64 + } +} + +domain_create = { + 'type': 'object', + 'properties': _domain_properties, + # TODO(lbragstad): According to the V3 API spec, name isn't required but + # the current implementation in assignment.controller:DomainV3 requires a + # name for the domain. + 'required': ['name'], + 'additionalProperties': True +} + +domain_update = { + 'type': 'object', + 'properties': _domain_properties, + 'minProperties': 1, + 'additionalProperties': True +} diff --git a/keystone-moon/keystone/routers.py b/keystone-moon/keystone/routers.py new file mode 100644 index 00000000..a0f9ed22 --- /dev/null +++ b/keystone-moon/keystone/routers.py @@ -0,0 +1,80 @@ +# Copyright 2012 OpenStack Foundation +# +# 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. +""" +The only types of routers in this file should be ``ComposingRouters``. + +The routers for the backends should be in the backend-specific router modules. +For example, the ``ComposableRouter`` for ``identity`` belongs in:: + + keystone.identity.routers + +""" + + +from keystone.common import wsgi +from keystone import controllers + + +class Extension(wsgi.ComposableRouter): + def __init__(self, is_admin=True): + if is_admin: + self.controller = controllers.AdminExtensions() + else: + self.controller = controllers.PublicExtensions() + + def add_routes(self, mapper): + extensions_controller = self.controller + mapper.connect('/extensions', + controller=extensions_controller, + action='get_extensions_info', + conditions=dict(method=['GET'])) + mapper.connect('/extensions/{extension_alias}', + controller=extensions_controller, + action='get_extension_info', + conditions=dict(method=['GET'])) + + +class VersionV2(wsgi.ComposableRouter): + def __init__(self, description): + self.description = description + + def add_routes(self, mapper): + version_controller = controllers.Version(self.description) + mapper.connect('/', + controller=version_controller, + action='get_version_v2') + + +class VersionV3(wsgi.ComposableRouter): + def __init__(self, description, routers): + self.description = description + self._routers = routers + + def add_routes(self, mapper): + version_controller = controllers.Version(self.description, + routers=self._routers) + mapper.connect('/', + controller=version_controller, + action='get_version_v3') + + +class Versions(wsgi.ComposableRouter): + def __init__(self, description): + self.description = description + + def add_routes(self, mapper): + version_controller = controllers.Version(self.description) + mapper.connect('/', + controller=version_controller, + action='get_versions') diff --git a/keystone-moon/keystone/server/__init__.py b/keystone-moon/keystone/server/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/server/common.py b/keystone-moon/keystone/server/common.py new file mode 100644 index 00000000..fda44eea --- /dev/null +++ b/keystone-moon/keystone/server/common.py @@ -0,0 +1,45 @@ + +# 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 oslo_config import cfg + +from keystone import backends +from keystone.common import dependency +from keystone.common import sql +from keystone import config + + +CONF = cfg.CONF + + +def configure(version=None, config_files=None, + pre_setup_logging_fn=lambda: None): + config.configure() + sql.initialize() + config.set_default_for_default_log_levels() + + CONF(project='keystone', version=version, + default_config_files=config_files) + + pre_setup_logging_fn() + config.setup_logging() + + +def setup_backends(load_extra_backends_fn=lambda: {}, + startup_application_fn=lambda: None): + drivers = backends.load_backends() + drivers.update(load_extra_backends_fn()) + res = startup_application_fn() + drivers.update(dependency.resolve_future_dependencies()) + return drivers, res diff --git a/keystone-moon/keystone/server/eventlet.py b/keystone-moon/keystone/server/eventlet.py new file mode 100644 index 00000000..5bedaf9b --- /dev/null +++ b/keystone-moon/keystone/server/eventlet.py @@ -0,0 +1,156 @@ + +# Copyright 2013 OpenStack Foundation +# +# 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 os +import socket + +from oslo_concurrency import processutils +from oslo_config import cfg +import oslo_i18n +import pbr.version + + +# NOTE(dstanek): i18n.enable_lazy() must be called before +# keystone.i18n._() is called to ensure it has the desired lazy lookup +# behavior. This includes cases, like keystone.exceptions, where +# keystone.i18n._() is called at import time. +oslo_i18n.enable_lazy() + + +from keystone.common import environment +from keystone.common import utils +from keystone import config +from keystone.i18n import _ +from keystone.openstack.common import service +from keystone.openstack.common import systemd +from keystone.server import common +from keystone import service as keystone_service + + +CONF = cfg.CONF + + +class ServerWrapper(object): + """Wraps a Server with some launching info & capabilities.""" + + def __init__(self, server, workers): + self.server = server + self.workers = workers + + def launch_with(self, launcher): + self.server.listen() + if self.workers > 1: + # Use multi-process launcher + launcher.launch_service(self.server, self.workers) + else: + # Use single process launcher + launcher.launch_service(self.server) + + +def create_server(conf, name, host, port, workers): + app = keystone_service.loadapp('config:%s' % conf, name) + server = environment.Server(app, host=host, port=port, + keepalive=CONF.eventlet_server.tcp_keepalive, + keepidle=CONF.eventlet_server.tcp_keepidle) + if CONF.eventlet_server_ssl.enable: + server.set_ssl(CONF.eventlet_server_ssl.certfile, + CONF.eventlet_server_ssl.keyfile, + CONF.eventlet_server_ssl.ca_certs, + CONF.eventlet_server_ssl.cert_required) + return name, ServerWrapper(server, workers) + + +def serve(*servers): + logging.warning(_('Running keystone via eventlet is deprecated as of Kilo ' + 'in favor of running in a WSGI server (e.g. mod_wsgi). ' + 'Support for keystone under eventlet will be removed in ' + 'the "M"-Release.')) + if max([server[1].workers for server in servers]) > 1: + launcher = service.ProcessLauncher() + else: + launcher = service.ServiceLauncher() + + for name, server in servers: + try: + server.launch_with(launcher) + except socket.error: + logging.exception(_('Failed to start the %(name)s server') % { + 'name': name}) + raise + + # notify calling process we are ready to serve + systemd.notify_once() + + for name, server in servers: + launcher.wait() + + +def _get_workers(worker_type_config_opt): + # Get the value from config, if the config value is None (not set), return + # the number of cpus with a minimum of 2. + worker_count = CONF.eventlet_server.get(worker_type_config_opt) + if not worker_count: + worker_count = max(2, processutils.get_worker_count()) + return worker_count + + +def configure_threading(): + monkeypatch_thread = not CONF.standard_threads + pydev_debug_url = utils.setup_remote_pydev_debug() + if pydev_debug_url: + # in order to work around errors caused by monkey patching we have to + # set the thread to False. An explanation is here: + # http://lists.openstack.org/pipermail/openstack-dev/2012-August/ + # 000794.html + monkeypatch_thread = False + environment.use_eventlet(monkeypatch_thread) + + +def run(possible_topdir): + dev_conf = os.path.join(possible_topdir, + 'etc', + 'keystone.conf') + config_files = None + if os.path.exists(dev_conf): + config_files = [dev_conf] + + common.configure( + version=pbr.version.VersionInfo('keystone').version_string(), + config_files=config_files, + pre_setup_logging_fn=configure_threading) + + paste_config = config.find_paste_config() + + def create_servers(): + admin_worker_count = _get_workers('admin_workers') + public_worker_count = _get_workers('public_workers') + + servers = [] + servers.append(create_server(paste_config, + 'admin', + CONF.eventlet_server.admin_bind_host, + CONF.eventlet_server.admin_port, + admin_worker_count)) + servers.append(create_server(paste_config, + 'main', + CONF.eventlet_server.public_bind_host, + CONF.eventlet_server.public_port, + public_worker_count)) + return servers + + _unused, servers = common.setup_backends( + startup_application_fn=create_servers) + serve(*servers) diff --git a/keystone-moon/keystone/server/wsgi.py b/keystone-moon/keystone/server/wsgi.py new file mode 100644 index 00000000..863f13bc --- /dev/null +++ b/keystone-moon/keystone/server/wsgi.py @@ -0,0 +1,52 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 oslo_config import cfg +import oslo_i18n + + +# NOTE(dstanek): i18n.enable_lazy() must be called before +# keystone.i18n._() is called to ensure it has the desired lazy lookup +# behavior. This includes cases, like keystone.exceptions, where +# keystone.i18n._() is called at import time. +oslo_i18n.enable_lazy() + + +from keystone.common import environment +from keystone import config +from keystone.server import common +from keystone import service as keystone_service + + +CONF = cfg.CONF + + +def initialize_application(name): + common.configure() + + # Log the options used when starting if we're in debug mode... + if CONF.debug: + CONF.log_opt_values(logging.getLogger(CONF.prog), logging.DEBUG) + + environment.use_stdlib() + + def loadapp(): + return keystone_service.loadapp( + 'config:%s' % config.find_paste_config(), name) + + _unused, application = common.setup_backends( + startup_application_fn=loadapp) + return application diff --git a/keystone-moon/keystone/service.py b/keystone-moon/keystone/service.py new file mode 100644 index 00000000..e9a0748e --- /dev/null +++ b/keystone-moon/keystone/service.py @@ -0,0 +1,118 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 functools +import sys + +from oslo_config import cfg +from oslo_log import log +from paste import deploy +import routes + +from keystone import assignment +from keystone import auth +from keystone import catalog +from keystone.common import wsgi +from keystone import controllers +from keystone import credential +from keystone import identity +from keystone import policy +from keystone import resource +from keystone import routers +from keystone import token +from keystone import trust +from keystone.contrib import moon as authz + +CONF = cfg.CONF +LOG = log.getLogger(__name__) + + +def loadapp(conf, name): + # NOTE(blk-u): Save the application being loaded in the controllers module. + # This is similar to how public_app_factory() and v3_app_factory() + # register the version with the controllers module. + controllers.latest_app = deploy.loadapp(conf, name=name) + return controllers.latest_app + + +def fail_gracefully(f): + """Logs exceptions and aborts.""" + @functools.wraps(f) + def wrapper(*args, **kw): + try: + return f(*args, **kw) + except Exception as e: + LOG.debug(e, exc_info=True) + + # exception message is printed to all logs + LOG.critical(e) + sys.exit(1) + + return wrapper + + +@fail_gracefully +def public_app_factory(global_conf, **local_conf): + controllers.register_version('v2.0') + return wsgi.ComposingRouter(routes.Mapper(), + [assignment.routers.Public(), + token.routers.Router(), + routers.VersionV2('public'), + routers.Extension(False)]) + + +@fail_gracefully +def admin_app_factory(global_conf, **local_conf): + controllers.register_version('v2.0') + return wsgi.ComposingRouter(routes.Mapper(), + [identity.routers.Admin(), + assignment.routers.Admin(), + token.routers.Router(), + resource.routers.Admin(), + routers.VersionV2('admin'), + routers.Extension()]) + + +@fail_gracefully +def public_version_app_factory(global_conf, **local_conf): + return wsgi.ComposingRouter(routes.Mapper(), + [routers.Versions('public')]) + + +@fail_gracefully +def admin_version_app_factory(global_conf, **local_conf): + return wsgi.ComposingRouter(routes.Mapper(), + [routers.Versions('admin')]) + + +@fail_gracefully +def v3_app_factory(global_conf, **local_conf): + controllers.register_version('v3') + mapper = routes.Mapper() + sub_routers = [] + _routers = [] + + router_modules = [assignment, auth, catalog, credential, identity, policy, + resource, authz] + if CONF.trust.enabled: + router_modules.append(trust) + + for module in router_modules: + routers_instance = module.routers.Routers() + _routers.append(routers_instance) + routers_instance.append_v3_routers(mapper, sub_routers) + + # Add in the v3 version api + sub_routers.append(routers.VersionV3('public', _routers)) + return wsgi.ComposingRouter(mapper, sub_routers) diff --git a/keystone-moon/keystone/tests/__init__.py b/keystone-moon/keystone/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/tests/moon/__init__.py b/keystone-moon/keystone/tests/moon/__init__.py new file mode 100644 index 00000000..1b678d53 --- /dev/null +++ b/keystone-moon/keystone/tests/moon/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2015 Open Platform for NFV Project, Inc. and its contributors +# This software is distributed under the terms and conditions of the 'Apache-2.0' +# license which can be found in the file 'LICENSE' in this package distribution +# or at 'http://www.apache.org/licenses/LICENSE-2.0'. diff --git a/keystone-moon/keystone/tests/moon/func/__init__.py b/keystone-moon/keystone/tests/moon/func/__init__.py new file mode 100644 index 00000000..1b678d53 --- /dev/null +++ b/keystone-moon/keystone/tests/moon/func/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2015 Open Platform for NFV Project, Inc. and its contributors +# This software is distributed under the terms and conditions of the 'Apache-2.0' +# license which can be found in the file 'LICENSE' in this package distribution +# or at 'http://www.apache.org/licenses/LICENSE-2.0'. diff --git a/keystone-moon/keystone/tests/moon/func/test_func_api_authz.py b/keystone-moon/keystone/tests/moon/func/test_func_api_authz.py new file mode 100644 index 00000000..77438e95 --- /dev/null +++ b/keystone-moon/keystone/tests/moon/func/test_func_api_authz.py @@ -0,0 +1,129 @@ +# Copyright 2015 Open Platform for NFV Project, Inc. and its contributors +# This software is distributed under the terms and conditions of the 'Apache-2.0' +# license which can be found in the file 'LICENSE' in this package distribution +# or at 'http://www.apache.org/licenses/LICENSE-2.0'. + +import unittest +import json +import httplib + + +CREDENTIALS = { + "host": "127.0.0.1", + "port": "35357", + "login": "admin", + "password": "nomoresecrete", + "tenant_name": "demo", + "sessionid": "kxb50d9uusiywfcs2fiidmu1j5nsyckr", + "csrftoken": "", + "x-subject-token": "" +} + + +def get_url(url, post_data=None, delete_data=None, crsftoken=None, method="GET", authtoken=None): + # MOON_SERVER_IP["URL"] = url + # _url = "http://{HOST}:{PORT}".format(**MOON_SERVER_IP) + if post_data: + method = "POST" + if delete_data: + method = "DELETE" + print("\033[32m{} {}\033[m".format(method, url)) + conn = httplib.HTTPConnection(CREDENTIALS["host"], CREDENTIALS["port"]) + headers = { + "Content-type": "application/x-www-form-urlencoded", + # "Accept": "text/plain", + "Accept": "text/plain,text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + 'Cookie': 'sessionid={}'.format(CREDENTIALS["sessionid"]), + } + if crsftoken: + headers["Cookie"] = "csrftoken={}; sessionid={}; NG_TRANSLATE_LANG_KEY:\"en\"".format(crsftoken, CREDENTIALS["sessionid"]) + CREDENTIALS["crsftoken"] = crsftoken + if authtoken: + headers["X-Auth-Token"] = CREDENTIALS["x-subject-token"] + if post_data: + method = "POST" + headers["Content-type"] = "application/json" + if crsftoken: + post_data = "&".join(map(lambda x: "=".join(x), post_data)) + elif "crsftoken" in CREDENTIALS and "sessionid" in CREDENTIALS: + post_data = json.dumps(post_data) + headers["Cookie"] = "csrftoken={}; sessionid={}; NG_TRANSLATE_LANG_KEY:\"en\"".format( + CREDENTIALS["crsftoken"], + CREDENTIALS["sessionid"]) + else: + post_data = json.dumps(post_data) + # conn.request(method, url, json.dumps(post_data), headers=headers) + conn.request(method, url, post_data, headers=headers) + elif delete_data: + method = "DELETE" + conn.request(method, url, json.dumps(delete_data), headers=headers) + else: + conn.request(method, url, headers=headers) + resp = conn.getresponse() + headers = resp.getheaders() + try: + CREDENTIALS["x-subject-token"] = dict(headers)["x-subject-token"] + except KeyError: + pass + if crsftoken: + sessionid_start = dict(headers)["set-cookie"].index("sessionid=")+len("sessionid=") + sessionid_end = dict(headers)["set-cookie"].index(";", sessionid_start) + sessionid = dict(headers)["set-cookie"][sessionid_start:sessionid_end] + CREDENTIALS["sessionid"] = sessionid + content = resp.read() + conn.close() + try: + return json.loads(content) + except ValueError: + return {"content": content} + + +class AuthTest(unittest.TestCase): + + def setUp(self): + post = { + "auth": { + "identity": { + "methods": [ + "password" + ], + "password": { + "user": { + "domain": { + "id": "Default" + }, + "name": "admin", + "password": "nomoresecrete" + } + } + }, + "scope": { + "project": { + "domain": { + "id": "Default" + }, + "name": "demo" + } + } + } + } + data = get_url("/v3/auth/tokens", post_data=post) + self.assertIn("token", data) + + def tearDown(self): + pass + + def test_authz(self): + data = get_url("/v3/OS-MOON/authz/1234567890/1111111/2222222/3333333", authtoken=True) + for key in ("authz", "subject_id", "tenant_id", "object_id", "action_id"): + self.assertIn(key, data) + print(data) + data = get_url("/v3/OS-MOON/authz/961420e0aeed4fd88e09cf4ae2ae700e/" + "4cff0936eeed42439d746e8071245235/df60c814-bafd-44a8-ad34-6c649e75295f/unpause", authtoken=True) + for key in ("authz", "subject_id", "tenant_id", "object_id", "action_id"): + self.assertIn(key, data) + print(data) + + +if __name__ == "__main__": + unittest.main() diff --git a/keystone-moon/keystone/tests/moon/func/test_func_api_intra_extension_admin.py b/keystone-moon/keystone/tests/moon/func/test_func_api_intra_extension_admin.py new file mode 100644 index 00000000..607691ea --- /dev/null +++ b/keystone-moon/keystone/tests/moon/func/test_func_api_intra_extension_admin.py @@ -0,0 +1,1011 @@ +# Copyright 2015 Open Platform for NFV Project, Inc. and its contributors +# This software is distributed under the terms and conditions of the 'Apache-2.0' +# license which can be found in the file 'LICENSE' in this package distribution +# or at 'http://www.apache.org/licenses/LICENSE-2.0'. + +import unittest +import json +import httplib +from uuid import uuid4 +import copy + +CREDENTIALS = { + "host": "127.0.0.1", + "port": "35357", + "login": "admin", + "password": "nomoresecrete", + "tenant_name": "demo", + "sessionid": "kxb50d9uusiywfcs2fiidmu1j5nsyckr", + "csrftoken": "", + "x-subject-token": "" +} + + +def get_url(url, post_data=None, delete_data=None, crsftoken=None, method="GET", authtoken=None): + # MOON_SERVER_IP["URL"] = url + # _url = "http://{HOST}:{PORT}".format(**MOON_SERVER_IP) + if post_data: + method = "POST" + if delete_data: + method = "DELETE" + # print("\033[32m{} {}\033[m".format(method, url)) + conn = httplib.HTTPConnection(CREDENTIALS["host"], CREDENTIALS["port"]) + headers = { + "Content-type": "application/x-www-form-urlencoded", + # "Accept": "text/plain", + "Accept": "text/plain,text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + 'Cookie': 'sessionid={}'.format(CREDENTIALS["sessionid"]), + } + if crsftoken: + headers["Cookie"] = "csrftoken={}; sessionid={}; NG_TRANSLATE_LANG_KEY:\"en\"".format(crsftoken, CREDENTIALS["sessionid"]) + CREDENTIALS["crsftoken"] = crsftoken + if authtoken: + headers["X-Auth-Token"] = CREDENTIALS["x-subject-token"] + if post_data: + method = "POST" + headers["Content-type"] = "application/json" + if crsftoken: + post_data = "&".join(map(lambda x: "=".join(x), post_data)) + elif "crsftoken" in CREDENTIALS and "sessionid" in CREDENTIALS: + post_data = json.dumps(post_data) + headers["Cookie"] = "csrftoken={}; sessionid={}; NG_TRANSLATE_LANG_KEY:\"en\"".format( + CREDENTIALS["crsftoken"], + CREDENTIALS["sessionid"]) + else: + post_data = json.dumps(post_data) + # conn.request(method, url, json.dumps(post_data), headers=headers) + conn.request(method, url, post_data, headers=headers) + elif delete_data: + method = "DELETE" + conn.request(method, url, json.dumps(delete_data), headers=headers) + else: + conn.request(method, url, headers=headers) + resp = conn.getresponse() + headers = resp.getheaders() + try: + CREDENTIALS["x-subject-token"] = dict(headers)["x-subject-token"] + except KeyError: + pass + if crsftoken: + sessionid_start = dict(headers)["set-cookie"].index("sessionid=")+len("sessionid=") + sessionid_end = dict(headers)["set-cookie"].index(";", sessionid_start) + sessionid = dict(headers)["set-cookie"][sessionid_start:sessionid_end] + CREDENTIALS["sessionid"] = sessionid + content = resp.read() + conn.close() + try: + return json.loads(content) + except ValueError: + return {"content": content} + +def get_keystone_user(name="demo", intra_extension_uuid=None): + users = get_url("/v3/users", authtoken=True)["users"] + demo_user_uuid = None + for user in users: + if user["name"] == name: + demo_user_uuid = user["id"] + break + # if user "name" is not present, fallback to admin + if user["name"] == "admin": + demo_user_uuid = user["id"] + if intra_extension_uuid: + post_data = {"subject_id": demo_user_uuid} + get_url("/v3/OS-MOON/intra_extensions/{}/subjects".format( + intra_extension_uuid), post_data=post_data, authtoken=True) + return demo_user_uuid + +class IntraExtensionsTest(unittest.TestCase): + + def setUp(self): + post = { + "auth": { + "identity": { + "methods": [ + "password" + ], + "password": { + "user": { + "domain": { + "id": "Default" + }, + "name": "admin", + "password": "nomoresecrete" + } + } + }, + "scope": { + "project": { + "domain": { + "id": "Default" + }, + "name": "demo" + } + } + } + } + data = get_url("/v3/auth/tokens", post_data=post) + self.assertIn("token", data) + + def tearDown(self): + pass + + def test_create_intra_extensions(self): + data = get_url("/v3/OS-MOON/intra_extensions", authtoken=True) + self.assertIn("intra_extensions", data) + data = get_url("/v3/OS-MOON/authz_policies", authtoken=True) + self.assertIn("authz_policies", data) + for model in data["authz_policies"]: + # Create a new intra_extension + new_ie = { + "name": "new_intra_extension", + "description": "new_intra_extension", + "policymodel": model + } + data = get_url("/v3/OS-MOON/intra_extensions/", post_data=new_ie, authtoken=True) + for key in [u'model', u'id', u'name', u'description']: + self.assertIn(key, data) + ie_id = data["id"] + data = get_url("/v3/OS-MOON/intra_extensions", authtoken=True) + self.assertIn(ie_id, data["intra_extensions"]) + + # Get all subjects + data = get_url("/v3/OS-MOON/intra_extensions/{}/subjects".format(ie_id), authtoken=True) + self.assertIn("subjects", data) + self.assertIs(type(data["subjects"]), dict) + + # Get all objects + data = get_url("/v3/OS-MOON/intra_extensions/{}/objects".format(ie_id), authtoken=True) + self.assertIn("objects", data) + self.assertIsInstance(data["objects"], dict) + + # Get all actions + data = get_url("/v3/OS-MOON/intra_extensions/{}/actions".format(ie_id), authtoken=True) + self.assertIn("actions", data) + self.assertIsInstance(data["actions"], dict) + + # # get current tenant + # data = get_url("/v3/OS-MOON/intra_extensions/{}/tenant".format(ie_id), authtoken=True) + # self.assertIn("tenant", data) + # self.assertIn(type(data["tenant"]), (str, unicode)) + # + # # set current tenant + # tenants = get_url("/v3/projects", authtoken=True)["projects"] + # post_data = {"tenant_id": ""} + # for tenant in tenants: + # if tenant["name"] == "admin": + # post_data = {"tenant_id": tenant["id"]} + # break + # data = get_url("/v3/OS-MOON/intra_extensions/{}/tenant".format(ie_id), + # post_data=post_data, + # authtoken=True) + # self.assertIn("tenant", data) + # self.assertIn(type(data["tenant"]), (str, unicode)) + # self.assertEqual(data["tenant"], post_data["tenant_id"]) + # + # # check current tenant + # data = get_url("/v3/OS-MOON/intra_extensions/{}/tenant".format(ie_id), authtoken=True) + # self.assertIn("tenant", data) + # self.assertIn(type(data["tenant"]), (str, unicode)) + # self.assertEqual(data["tenant"], post_data["tenant_id"]) + + # Delete the intra_extension + data = get_url("/v3/OS-MOON/intra_extensions/{}".format(ie_id), method="DELETE", authtoken=True) + data = get_url("/v3/OS-MOON/intra_extensions", authtoken=True) + self.assertNotIn(ie_id, data["intra_extensions"]) + + def test_perimeter_data(self): + data = get_url("/v3/OS-MOON/intra_extensions", authtoken=True) + self.assertIn("intra_extensions", data) + data = get_url("/v3/OS-MOON/authz_policies", authtoken=True) + self.assertIn("authz_policies", data) + for model in data["authz_policies"]: + # Create a new intra_extension + new_ie = { + "name": "new_intra_extension", + "description": "new_intra_extension", + "policymodel": model + } + data = get_url("/v3/OS-MOON/intra_extensions/", post_data=new_ie, authtoken=True) + for key in [u'model', u'id', u'name', u'description']: + self.assertIn(key, data) + ie_id = data["id"] + data = get_url("/v3/OS-MOON/intra_extensions", authtoken=True) + self.assertIn(ie_id, data["intra_extensions"]) + + # Get all subjects + data = get_url("/v3/OS-MOON/intra_extensions/{}/subjects".format(ie_id), authtoken=True) + self.assertIn("subjects", data) + self.assertIs(type(data["subjects"]), dict) + self.assertTrue(len(data["subjects"]) > 0) + + # Add a new subject + users = get_url("/v3/users", authtoken=True)["users"] + demo_user_uuid = None + for user in users: + if user["name"] == "demo": + demo_user_uuid = user["id"] + break + # if user demo is not present + if user["name"] == "admin": + demo_user_uuid = user["id"] + post_data = {"subject_id": demo_user_uuid} + data = get_url("/v3/OS-MOON/intra_extensions/{}/subjects".format(ie_id), post_data=post_data, authtoken=True) + self.assertIn("subject", data) + self.assertIs(type(data["subject"]), dict) + self.assertEqual(post_data["subject_id"], data["subject"]["uuid"]) + data = get_url("/v3/OS-MOON/intra_extensions/{}/subjects".format(ie_id), authtoken=True) + self.assertIn("subjects", data) + self.assertIsInstance(data["subjects"], dict) + self.assertIn(post_data["subject_id"], data["subjects"]) + # delete the previous subject + data = get_url("/v3/OS-MOON/intra_extensions/{}/subjects/{}".format(ie_id, post_data["subject_id"]), + method="DELETE", authtoken=True) + data = get_url("/v3/OS-MOON/intra_extensions/{}/subjects".format(ie_id), authtoken=True) + self.assertIn("subjects", data) + self.assertIsInstance(data["subjects"], dict) + self.assertNotIn(post_data["subject_id"], data["subjects"]) + + # Get all objects + data = get_url("/v3/OS-MOON/intra_extensions/{}/objects".format(ie_id), authtoken=True) + self.assertIn("objects", data) + self.assertIs(type(data["objects"]), dict) + self.assertTrue(len(data["objects"]) > 0) + + # Add a new object + post_data = {"object_id": "my_new_object"} + data = get_url("/v3/OS-MOON/intra_extensions/{}/objects".format(ie_id), post_data=post_data, authtoken=True) + self.assertIn("object", data) + self.assertIsInstance(data["object"], dict) + self.assertEqual(post_data["object_id"], data["object"]["name"]) + object_id = data["object"]["uuid"] + data = get_url("/v3/OS-MOON/intra_extensions/{}/objects".format(ie_id), authtoken=True) + self.assertIn("objects", data) + self.assertIsInstance(data["objects"], dict) + self.assertIn(post_data["object_id"], data["objects"].values()) + + # delete the previous object + data = get_url("/v3/OS-MOON/intra_extensions/{}/objects/{}".format(ie_id, object_id), + method="DELETE", authtoken=True) + data = get_url("/v3/OS-MOON/intra_extensions/{}/objects".format(ie_id), authtoken=True) + self.assertIn("objects", data) + self.assertIsInstance(data["objects"], dict) + self.assertNotIn(post_data["object_id"], data["objects"].values()) + + # Get all actions + data = get_url("/v3/OS-MOON/intra_extensions/{}/actions".format(ie_id), authtoken=True) + self.assertIn("actions", data) + self.assertIs(type(data["actions"]), dict) + self.assertTrue(len(data["actions"]) > 0) + + # Add a new action + post_data = {"action_id": "create2"} + data = get_url("/v3/OS-MOON/intra_extensions/{}/actions".format(ie_id), post_data=post_data, authtoken=True) + action_id = data["action"]["uuid"] + self.assertIn("action", data) + self.assertIsInstance(data["action"], dict) + self.assertEqual(post_data["action_id"], data["action"]["name"]) + data = get_url("/v3/OS-MOON/intra_extensions/{}/actions".format(ie_id), authtoken=True) + self.assertIn("actions", data) + self.assertIsInstance(data["actions"], dict) + self.assertIn(post_data["action_id"], data["actions"].values()) + + # delete the previous action + data = get_url("/v3/OS-MOON/intra_extensions/{}/actions/{}".format(ie_id, action_id), + method="DELETE", authtoken=True) + data = get_url("/v3/OS-MOON/intra_extensions/{}/actions".format(ie_id), authtoken=True) + self.assertIn("actions", data) + self.assertIsInstance(data["actions"], dict) + self.assertNotIn(post_data["action_id"], data["actions"]) + + # Delete the intra_extension + data = get_url("/v3/OS-MOON/intra_extensions/{}".format(ie_id), method="DELETE", authtoken=True) + data = get_url("/v3/OS-MOON/intra_extensions", authtoken=True) + self.assertNotIn(ie_id, data["intra_extensions"]) + + def test_assignments_data(self): + data = get_url("/v3/OS-MOON/intra_extensions", authtoken=True) + self.assertIn("intra_extensions", data) + data = get_url("/v3/OS-MOON/authz_policies", authtoken=True) + self.assertIn("authz_policies", data) + for model in data["authz_policies"]: + # Create a new intra_extension + new_ie = { + "name": "new_intra_extension", + "description": "new_intra_extension", + "policymodel": model + } + data = get_url("/v3/OS-MOON/intra_extensions/", post_data=new_ie, authtoken=True) + for key in [u'model', u'id', u'name', u'description']: + self.assertIn(key, data) + ie_id = data["id"] + data = get_url("/v3/OS-MOON/intra_extensions", authtoken=True) + self.assertIn(ie_id, data["intra_extensions"]) + + # Get all subject_assignments + data = get_url("/v3/OS-MOON/intra_extensions/{}/subject_assignments/{}".format( + ie_id, get_keystone_user(intra_extension_uuid=ie_id)), authtoken=True) + self.assertIn("subject_category_assignments", data) + self.assertIs(type(data["subject_category_assignments"]), dict) + + # Add subject_assignments + # get one subject + data = get_url("/v3/OS-MOON/intra_extensions/{}/subjects".format(ie_id), authtoken=True) + self.assertIn("subjects", data) + self.assertIs(type(data["subjects"]), dict) + # subject_id = data["subjects"].keys()[0] + subject_id = get_keystone_user() + # get one subject category + data = get_url("/v3/OS-MOON/intra_extensions/{}/subject_categories".format(ie_id), authtoken=True) + self.assertIn("subject_categories", data) + self.assertIs(type(data["subject_categories"]), dict) + subject_category_id = data["subject_categories"].keys()[0] + # get all subject category scope + data = get_url("/v3/OS-MOON/intra_extensions/{}/subject_category_scope/{}".format( + ie_id, subject_category_id), authtoken=True) + self.assertIn("subject_category_scope", data) + self.assertIs(type(data["subject_category_scope"]), dict) + subject_category_scope_id = data["subject_category_scope"][subject_category_id].keys()[0] + post_data = { + "subject_id": subject_id, + "subject_category": subject_category_id, + "subject_category_scope": subject_category_scope_id + } + data = get_url("/v3/OS-MOON/intra_extensions/{}/subject_assignments".format(ie_id), post_data=post_data, authtoken=True) + self.assertIn("subject_category_assignments", data) + self.assertIs(type(data["subject_category_assignments"]), dict) + self.assertIn(post_data["subject_category"], data["subject_category_assignments"][subject_id]) + self.assertIn(post_data["subject_category"], data["subject_category_assignments"][subject_id]) + self.assertIn(post_data["subject_category_scope"], + data["subject_category_assignments"][subject_id][post_data["subject_category"]]) + # data = get_url("/v3/OS-MOON/intra_extensions/{}/subjects".format(ie_id), authtoken=True) + # self.assertIn("subjects", data) + # self.assertIsInstance(data["subjects"], dict) + # self.assertIn(post_data["subject_id"], data["subjects"]) + + # delete the previous subject assignment + get_url("/v3/OS-MOON/intra_extensions/{}/subject_assignments/{}/{}/{}".format( + ie_id, + post_data["subject_id"], + post_data["subject_category"], + post_data["subject_category_scope"], + ), + method="DELETE", authtoken=True) + data = get_url("/v3/OS-MOON/intra_extensions/{}/subject_assignments/{}".format( + ie_id, get_keystone_user()), authtoken=True) + self.assertIn("subject_category_assignments", data) + self.assertIs(type(data["subject_category_assignments"]), dict) + if post_data["subject_category"] in data["subject_category_assignments"][subject_id]: + if post_data["subject_category"] in data["subject_category_assignments"][subject_id]: + self.assertNotIn(post_data["subject_category_scope"], + data["subject_category_assignments"][subject_id][post_data["subject_category"]]) + + # Get all object_assignments + + # get one object + post_data = {"object_id": "my_new_object"} + new_object = get_url("/v3/OS-MOON/intra_extensions/{}/objects".format(ie_id), post_data=post_data, authtoken=True) + object_id = new_object["object"]["uuid"] + + data = get_url("/v3/OS-MOON/intra_extensions/{}/object_assignments/{}".format( + ie_id, object_id), authtoken=True) + self.assertIn("object_category_assignments", data) + self.assertIsInstance(data["object_category_assignments"], dict) + + # Add object_assignments + # get one object category + post_data = {"object_category_id": uuid4().hex} + object_category = get_url("/v3/OS-MOON/intra_extensions/{}/object_categories".format(ie_id), + post_data=post_data, + authtoken=True) + object_category_id = object_category["object_category"]["uuid"] + # get all object category scope + post_data = { + "object_category_id": object_category_id, + "object_category_scope_id": uuid4().hex + } + data = get_url("/v3/OS-MOON/intra_extensions/{}/object_category_scope".format(ie_id), + post_data=post_data, + authtoken=True) + object_category_scope_id = data["object_category_scope"]["uuid"] + data = get_url("/v3/OS-MOON/intra_extensions/{}/object_category_scope/{}".format( + ie_id, object_category_id), authtoken=True) + self.assertIn("object_category_scope", data) + self.assertIs(type(data["object_category_scope"]), dict) + post_data = { + "object_id": object_id, + "object_category": object_category_id, + "object_category_scope": object_category_scope_id + } + data = get_url("/v3/OS-MOON/intra_extensions/{}/object_assignments".format(ie_id), post_data=post_data, authtoken=True) + self.assertIn("object_category_assignments", data) + self.assertIs(type(data["object_category_assignments"]), dict) + self.assertIn(post_data["object_id"], data["object_category_assignments"]) + self.assertIn(post_data["object_category"], data["object_category_assignments"][post_data["object_id"]]) + self.assertIn(post_data["object_category_scope"], + data["object_category_assignments"][post_data["object_id"]][post_data["object_category"]]) + data = get_url("/v3/OS-MOON/intra_extensions/{}/objects".format(ie_id), authtoken=True) + self.assertIn("objects", data) + self.assertIsInstance(data["objects"], dict) + self.assertIn(post_data["object_id"], data["objects"]) + # delete the previous object + data = get_url("/v3/OS-MOON/intra_extensions/{}/objects/{}".format(ie_id, post_data["object_id"]), + method="DELETE", authtoken=True) + data = get_url("/v3/OS-MOON/intra_extensions/{}/objects".format(ie_id), authtoken=True) + self.assertIn("objects", data) + self.assertIsInstance(data["objects"], dict) + self.assertNotIn(post_data["object_id"], data["objects"]) + + # Get all actions_assignments + + # get one action + post_data = {"action_id": "my_new_action"} + new_object = get_url("/v3/OS-MOON/intra_extensions/{}/actions".format(ie_id), post_data=post_data, authtoken=True) + action_id = new_object["action"]["uuid"] + + post_data = {"action_category_id": uuid4().hex} + action_category = get_url("/v3/OS-MOON/intra_extensions/{}/action_categories".format(ie_id), + post_data=post_data, + authtoken=True) + action_category_id = action_category["action_category"]["uuid"] + + data = get_url("/v3/OS-MOON/intra_extensions/{}/action_assignments/{}".format( + ie_id, action_id), authtoken=True) + self.assertIn("action_category_assignments", data) + self.assertIsInstance(data["action_category_assignments"], dict) + + # Add action_assignments + # get one action category + # data = get_url("/v3/OS-MOON/intra_extensions/{}/action_categories".format(ie_id), authtoken=True) + # self.assertIn("action_categories", data) + # self.assertIs(type(data["action_categories"]), dict) + # action_category_id = data["action_categories"][0] + # get all action category scope + post_data = { + "action_category_id": action_category_id, + "action_category_scope_id": uuid4().hex + } + data = get_url("/v3/OS-MOON/intra_extensions/{}/action_category_scope".format(ie_id), + post_data=post_data, + authtoken=True) + action_category_scope_id = data["action_category_scope"]["uuid"] + data = get_url("/v3/OS-MOON/intra_extensions/{}/action_category_scope/{}".format( + ie_id, action_category_id), authtoken=True) + self.assertIn("action_category_scope", data) + self.assertIs(type(data["action_category_scope"]), dict) + # action_category_scope_id = data["action_category_scope"][action_category_id].keys()[0] + post_data = { + "action_id": action_id, + "action_category": action_category_id, + "action_category_scope": action_category_scope_id + } + data = get_url("/v3/OS-MOON/intra_extensions/{}/action_assignments".format(ie_id), post_data=post_data, authtoken=True) + self.assertIn("action_category_assignments", data) + self.assertIs(type(data["action_category_assignments"]), dict) + self.assertIn(post_data["action_id"], data["action_category_assignments"]) + self.assertIn(post_data["action_category"], data["action_category_assignments"][post_data["action_id"]]) + self.assertIn(post_data["action_category_scope"], + data["action_category_assignments"][post_data["action_id"]][post_data["action_category"]]) + data = get_url("/v3/OS-MOON/intra_extensions/{}/actions".format(ie_id), authtoken=True) + self.assertIn("actions", data) + self.assertIsInstance(data["actions"], dict) + self.assertIn(post_data["action_id"], data["actions"]) + # delete the previous action + data = get_url("/v3/OS-MOON/intra_extensions/{}/actions/{}".format(ie_id, post_data["action_id"]), + method="DELETE", authtoken=True) + data = get_url("/v3/OS-MOON/intra_extensions/{}/actions".format(ie_id), authtoken=True) + self.assertIn("actions", data) + self.assertIsInstance(data["actions"], dict) + self.assertNotIn(post_data["action_id"], data["actions"]) + + # Delete the intra_extension + get_url("/v3/OS-MOON/intra_extensions/{}".format(ie_id), method="DELETE", authtoken=True) + data = get_url("/v3/OS-MOON/intra_extensions", authtoken=True) + self.assertNotIn(ie_id, data["intra_extensions"]) + + def test_metadata_data(self): + data = get_url("/v3/OS-MOON/intra_extensions", authtoken=True) + self.assertIn("intra_extensions", data) + data = get_url("/v3/OS-MOON/authz_policies", authtoken=True) + self.assertIn("authz_policies", data) + for model in data["authz_policies"]: + # Create a new intra_extension + new_ie = { + "name": "new_intra_extension", + "description": "new_intra_extension", + "policymodel": model + } + data = get_url("/v3/OS-MOON/intra_extensions/", post_data=new_ie, authtoken=True) + for key in [u'model', u'id', u'name', u'description']: + self.assertIn(key, data) + ie_id = data["id"] + data = get_url("/v3/OS-MOON/intra_extensions", authtoken=True) + self.assertIn(ie_id, data["intra_extensions"]) + + # Get all subject_categories + data = get_url("/v3/OS-MOON/intra_extensions/{}/subject_categories".format(ie_id), authtoken=True) + self.assertIn("subject_categories", data) + self.assertIs(type(data["subject_categories"]), dict) + + # Add a new subject_category + post_data = {"subject_category_id": uuid4().hex} + data = get_url("/v3/OS-MOON/intra_extensions/{}/subject_categories".format(ie_id), + post_data=post_data, + authtoken=True) + self.assertIn("subject_category", data) + self.assertIsInstance(data["subject_category"], dict) + self.assertEqual(post_data["subject_category_id"], data["subject_category"]["name"]) + subject_category_id = data["subject_category"]["uuid"] + data = get_url("/v3/OS-MOON/intra_extensions/{}/subject_categories".format(ie_id), authtoken=True) + self.assertIn("subject_categories", data) + self.assertIsInstance(data["subject_categories"], dict) + self.assertIn(post_data["subject_category_id"], data["subject_categories"].values()) + # delete the previous subject_category + get_url("/v3/OS-MOON/intra_extensions/{}/subject_categories/{}".format(ie_id, + subject_category_id), + method="DELETE", + authtoken=True) + data = get_url("/v3/OS-MOON/intra_extensions/{}/subject_categories".format(ie_id), authtoken=True) + self.assertIn("subject_categories", data) + self.assertIsInstance(data["subject_categories"], dict) + self.assertNotIn(post_data["subject_category_id"], data["subject_categories"].values()) + + # Get all object_categories + data = get_url("/v3/OS-MOON/intra_extensions/{}/object_categories".format(ie_id), authtoken=True) + self.assertIn("object_categories", data) + self.assertIsInstance(data["object_categories"], dict) + + # Add a new object_category + post_data = {"object_category_id": uuid4().hex} + data = get_url("/v3/OS-MOON/intra_extensions/{}/object_categories".format(ie_id), + post_data=post_data, + authtoken=True) + self.assertIn("object_category", data) + self.assertIsInstance(data["object_category"], dict) + self.assertIn(post_data["object_category_id"], data["object_category"]["name"]) + object_category_id = data["object_category"]["uuid"] + data = get_url("/v3/OS-MOON/intra_extensions/{}/object_categories".format(ie_id), authtoken=True) + self.assertIn("object_categories", data) + self.assertIsInstance(data["object_categories"], dict) + self.assertIn(post_data["object_category_id"], data["object_categories"].values()) + # delete the previous subject_category + get_url("/v3/OS-MOON/intra_extensions/{}/object_categories/{}".format(ie_id, + object_category_id), + method="DELETE", + authtoken=True) + data = get_url("/v3/OS-MOON/intra_extensions/{}/object_categories".format(ie_id), authtoken=True) + self.assertIn("object_categories", data) + self.assertIsInstance(data["object_categories"], dict) + self.assertNotIn(post_data["object_category_id"], data["object_categories"].values()) + + # Get all action_categories + data = get_url("/v3/OS-MOON/intra_extensions/{}/action_categories".format(ie_id), authtoken=True) + self.assertIn("action_categories", data) + self.assertIsInstance(data["action_categories"], dict) + + # Add a new action_category + post_data = {"action_category_id": uuid4().hex} + data = get_url("/v3/OS-MOON/intra_extensions/{}/action_categories".format(ie_id), + post_data=post_data, + authtoken=True) + self.assertIn("action_category", data) + self.assertIsInstance(data["action_category"], dict) + self.assertIn(post_data["action_category_id"], data["action_category"]["name"]) + action_category_id = data["action_category"]["uuid"] + data = get_url("/v3/OS-MOON/intra_extensions/{}/action_categories".format(ie_id), authtoken=True) + self.assertIn("action_categories", data) + self.assertIsInstance(data["action_categories"], dict) + self.assertIn(post_data["action_category_id"], data["action_categories"].values()) + # delete the previous subject_category + get_url("/v3/OS-MOON/intra_extensions/{}/action_categories/{}".format(ie_id, + action_category_id), + method="DELETE", + authtoken=True) + data = get_url("/v3/OS-MOON/intra_extensions/{}/action_categories".format(ie_id), authtoken=True) + self.assertIn("action_categories", data) + self.assertIsInstance(data["action_categories"], dict) + self.assertNotIn(post_data["action_category_id"], data["action_categories"].values()) + + # Delete the intra_extension + get_url("/v3/OS-MOON/intra_extensions/{}".format(ie_id), method="DELETE", authtoken=True) + data = get_url("/v3/OS-MOON/intra_extensions", authtoken=True) + self.assertNotIn(ie_id, data["intra_extensions"]) + + def test_scope_data(self): + data = get_url("/v3/OS-MOON/intra_extensions", authtoken=True) + self.assertIn("intra_extensions", data) + data = get_url("/v3/OS-MOON/authz_policies", authtoken=True) + self.assertIn("authz_policies", data) + for model in data["authz_policies"]: + # Create a new intra_extension + new_ie = { + "name": "new_intra_extension", + "description": "new_intra_extension", + "policymodel": model + } + data = get_url("/v3/OS-MOON/intra_extensions/", post_data=new_ie, authtoken=True) + for key in [u'model', u'id', u'name', u'description']: + self.assertIn(key, data) + ie_id = data["id"] + data = get_url("/v3/OS-MOON/intra_extensions", authtoken=True) + self.assertIn(ie_id, data["intra_extensions"]) + + # Get all subject_category_scope + categories = get_url("/v3/OS-MOON/intra_extensions/{}/subject_categories".format(ie_id), authtoken=True) + for category in categories["subject_categories"]: + data = get_url("/v3/OS-MOON/intra_extensions/{}/subject_category_scope/{}".format( + ie_id, category), authtoken=True) + self.assertIn("subject_category_scope", data) + self.assertIs(type(data["subject_category_scope"]), dict) + + # Add a new subject_category_scope + post_data = { + "subject_category_id": category, + "subject_category_scope_id": uuid4().hex + } + data = get_url("/v3/OS-MOON/intra_extensions/{}/subject_category_scope".format(ie_id), + post_data=post_data, + authtoken=True) + self.assertIn("subject_category_scope", data) + self.assertIsInstance(data["subject_category_scope"], dict) + self.assertEqual(post_data["subject_category_scope_id"], data["subject_category_scope"]["name"]) + data = get_url("/v3/OS-MOON/intra_extensions/{}/subject_category_scope/{}".format( + ie_id, category), authtoken=True) + self.assertIn("subject_category_scope", data) + self.assertIsInstance(data["subject_category_scope"], dict) + self.assertIn(post_data["subject_category_id"], data["subject_category_scope"]) + self.assertIn(post_data["subject_category_scope_id"], + data["subject_category_scope"][category].values()) + # delete the previous subject_category_scope + get_url("/v3/OS-MOON/intra_extensions/{}/subject_category_scope/{}/{}".format( + ie_id, + post_data["subject_category_id"], + post_data["subject_category_scope_id"]), + method="DELETE", + authtoken=True) + data = get_url("/v3/OS-MOON/intra_extensions/{}/subject_category_scope/{}".format( + ie_id, category), authtoken=True) + self.assertIn("subject_category_scope", data) + self.assertIsInstance(data["subject_category_scope"], dict) + self.assertIn(post_data["subject_category_id"], data["subject_category_scope"]) + self.assertNotIn(post_data["subject_category_scope_id"], + data["subject_category_scope"][post_data["subject_category_id"]]) + + # Get all object_category_scope + # get object_categories + categories = get_url("/v3/OS-MOON/intra_extensions/{}/object_categories".format(ie_id), authtoken=True) + for category in categories["object_categories"]: + post_data = { + "object_category_id": category, + "object_category_scope_id": uuid4().hex + } + data = get_url("/v3/OS-MOON/intra_extensions/{}/object_category_scope".format(ie_id), + post_data=post_data, + authtoken=True) + self.assertIn("object_category_scope", data) + self.assertIsInstance(data["object_category_scope"], dict) + self.assertEqual(post_data["object_category_scope_id"], data["object_category_scope"]["name"]) + data = get_url("/v3/OS-MOON/intra_extensions/{}/object_category_scope/{}".format( + ie_id, category), authtoken=True) + self.assertIn("object_category_scope", data) + self.assertIsInstance(data["object_category_scope"], dict) + self.assertIn(post_data["object_category_id"], data["object_category_scope"]) + self.assertIn(post_data["object_category_scope_id"], + data["object_category_scope"][category].values()) + # delete the previous object_category_scope + get_url("/v3/OS-MOON/intra_extensions/{}/object_category_scope/{}/{}".format( + ie_id, + post_data["object_category_id"], + post_data["object_category_scope_id"]), + method="DELETE", + authtoken=True) + data = get_url("/v3/OS-MOON/intra_extensions/{}/object_category_scope/{}".format( + ie_id, category), authtoken=True) + self.assertIn("object_category_scope", data) + self.assertIsInstance(data["object_category_scope"], dict) + self.assertIn(post_data["object_category_id"], data["object_category_scope"]) + self.assertNotIn(post_data["object_category_scope_id"], + data["object_category_scope"][post_data["object_category_id"]]) + + # Get all action_category_scope + categories = get_url("/v3/OS-MOON/intra_extensions/{}/action_categories".format(ie_id), authtoken=True) + print(categories) + for category in categories["action_categories"]: + print(category) + data = get_url("/v3/OS-MOON/intra_extensions/{}/action_category_scope/{}".format( + ie_id, category), authtoken=True) + self.assertIn("action_category_scope", data) + self.assertIsInstance(data["action_category_scope"], dict) + + # Add a new action_category_scope + post_data = { + "action_category_id": category, + "action_category_scope_id": uuid4().hex + } + data = get_url("/v3/OS-MOON/intra_extensions/{}/action_category_scope".format(ie_id), + post_data=post_data, + authtoken=True) + self.assertIn("action_category_scope", data) + self.assertIsInstance(data["action_category_scope"], dict) + self.assertEqual(post_data["action_category_scope_id"], data["action_category_scope"]["name"]) + data = get_url("/v3/OS-MOON/intra_extensions/{}/action_category_scope/{}".format( + ie_id, category), authtoken=True) + self.assertIn("action_category_scope", data) + self.assertIsInstance(data["action_category_scope"], dict) + self.assertIn(post_data["action_category_id"], data["action_category_scope"]) + self.assertIn(post_data["action_category_scope_id"], + data["action_category_scope"][category].values()) + # delete the previous action_category_scope + get_url("/v3/OS-MOON/intra_extensions/{}/action_category_scope/{}/{}".format( + ie_id, + post_data["action_category_id"], + post_data["action_category_scope_id"]), + method="DELETE", + authtoken=True) + data = get_url("/v3/OS-MOON/intra_extensions/{}/action_category_scope/{}".format( + ie_id, category), authtoken=True) + self.assertIn("action_category_scope", data) + self.assertIsInstance(data["action_category_scope"], dict) + self.assertIn(post_data["action_category_id"], data["action_category_scope"]) + self.assertNotIn(post_data["action_category_scope_id"], + data["action_category_scope"][post_data["action_category_id"]]) + + # Delete the intra_extension + get_url("/v3/OS-MOON/intra_extensions/{}".format(ie_id), method="DELETE", authtoken=True) + data = get_url("/v3/OS-MOON/intra_extensions", authtoken=True) + self.assertNotIn(ie_id, data["intra_extensions"]) + + def test_metarule_data(self): + data = get_url("/v3/OS-MOON/intra_extensions", authtoken=True) + self.assertIn("intra_extensions", data) + data = get_url("/v3/OS-MOON/authz_policies", authtoken=True) + self.assertIn("authz_policies", data) + for model in data["authz_policies"]: + # Create a new intra_extension + new_ie = { + "name": "new_intra_extension", + "description": "new_intra_extension", + "policymodel": model + } + data = get_url("/v3/OS-MOON/intra_extensions/", post_data=new_ie, authtoken=True) + for key in [u'model', u'id', u'name', u'description']: + self.assertIn(key, data) + ie_id = data["id"] + data = get_url("/v3/OS-MOON/intra_extensions", authtoken=True) + self.assertIn(ie_id, data["intra_extensions"]) + + # Get all aggregation_algorithms + data = get_url("/v3/OS-MOON/intra_extensions/{}/aggregation_algorithms".format(ie_id), authtoken=True) + self.assertIn("aggregation_algorithms", data) + self.assertIs(type(data["aggregation_algorithms"]), list) + aggregation_algorithms = data["aggregation_algorithms"] + + # Get all sub_meta_rule_relations + data = get_url("/v3/OS-MOON/intra_extensions/{}/sub_meta_rule_relations".format(ie_id), authtoken=True) + self.assertIn("sub_meta_rule_relations", data) + self.assertIs(type(data["sub_meta_rule_relations"]), list) + sub_meta_rule_relations = data["sub_meta_rule_relations"] + + # Get current aggregation_algorithm + data = get_url("/v3/OS-MOON/intra_extensions/{}/aggregation_algorithm".format(ie_id), authtoken=True) + self.assertIn("aggregation", data) + self.assertIn(type(data["aggregation"]), (str, unicode)) + aggregation_algorithm = data["aggregation"] + + # Set current aggregation_algorithm + post_data = {"aggregation_algorithm": ""} + for _algo in aggregation_algorithms: + if _algo != aggregation_algorithm: + post_data = {"aggregation_algorithm": _algo} + data = get_url("/v3/OS-MOON/intra_extensions/{}/aggregation_algorithm".format(ie_id), + post_data=post_data, + authtoken=True) + self.assertIn("aggregation", data) + self.assertIn(type(data["aggregation"]), (str, unicode)) + self.assertEqual(post_data["aggregation_algorithm"], data["aggregation"]) + new_aggregation_algorithm = data["aggregation"] + data = get_url("/v3/OS-MOON/intra_extensions/{}/aggregation_algorithm".format(ie_id), authtoken=True) + self.assertIn("aggregation", data) + self.assertIn(type(data["aggregation"]), (str, unicode)) + self.assertEqual(post_data["aggregation_algorithm"], new_aggregation_algorithm) + # Get back to the old value + post_data = {"aggregation_algorithm": aggregation_algorithm} + data = get_url("/v3/OS-MOON/intra_extensions/{}/aggregation_algorithm".format(ie_id), + post_data=post_data, + authtoken=True) + self.assertIn("aggregation", data) + self.assertIn(type(data["aggregation"]), (str, unicode)) + self.assertEqual(post_data["aggregation_algorithm"], aggregation_algorithm) + + # Get current sub_meta_rule + data = get_url("/v3/OS-MOON/intra_extensions/{}/sub_meta_rule".format(ie_id), authtoken=True) + self.assertIn("sub_meta_rules", data) + self.assertIs(type(data["sub_meta_rules"]), dict) + self.assertGreater(len(data["sub_meta_rules"].keys()), 0) + relation = data["sub_meta_rules"].keys()[0] + new_relation = "" + self.assertIn(relation, sub_meta_rule_relations) + sub_meta_rule = data["sub_meta_rules"] + post_data = dict() + for _relation in sub_meta_rule_relations: + if _relation != data["sub_meta_rules"].keys()[0]: + post_data[_relation] = copy.deepcopy(sub_meta_rule[relation]) + post_data[_relation]["relation"] = _relation + new_relation = _relation + break + # Add a new subject category + subject_category = uuid4().hex + data = get_url("/v3/OS-MOON/intra_extensions/{}/subject_categories".format(ie_id), + post_data={"subject_category_id": subject_category}, + authtoken=True) + self.assertIn("subject_category", data) + self.assertIsInstance(data["subject_category"], dict) + self.assertIn(subject_category, data["subject_category"].values()) + subject_category_id = data["subject_category"]['uuid'] + # Add a new object category + object_category = uuid4().hex + data = get_url("/v3/OS-MOON/intra_extensions/{}/object_categories".format(ie_id), + post_data={"object_category_id": object_category}, + authtoken=True) + self.assertIn("object_category", data) + self.assertIsInstance(data["object_category"], dict) + self.assertIn(object_category, data["object_category"].values()) + object_category_id = data["object_category"]['uuid'] + # Add a new action category + action_category = uuid4().hex + data = get_url("/v3/OS-MOON/intra_extensions/{}/action_categories".format(ie_id), + post_data={"action_category_id": action_category}, + authtoken=True) + self.assertIn("action_category", data) + self.assertIsInstance(data["action_category"], dict) + self.assertIn(action_category, data["action_category"].values()) + action_category_id = data["action_category"]['uuid'] + # Modify the post_data to add new categories + post_data[new_relation]["subject_categories"].append(subject_category_id) + post_data[new_relation]["object_categories"].append(object_category_id) + post_data[new_relation]["action_categories"].append(action_category_id) + data = get_url("/v3/OS-MOON/intra_extensions/{}/sub_meta_rule".format(ie_id), + post_data=post_data, + authtoken=True) + self.assertIn("sub_meta_rules", data) + self.assertIs(type(data["sub_meta_rules"]), dict) + self.assertGreater(len(data["sub_meta_rules"].keys()), 0) + self.assertEqual(new_relation, data["sub_meta_rules"].keys()[0]) + self.assertIn(subject_category_id, data["sub_meta_rules"][new_relation]["subject_categories"]) + self.assertIn(object_category_id, data["sub_meta_rules"][new_relation]["object_categories"]) + self.assertIn(action_category_id, data["sub_meta_rules"][new_relation]["action_categories"]) + self.assertEqual(new_relation, data["sub_meta_rules"][new_relation]["relation"]) + + # Delete the intra_extension + data = get_url("/v3/OS-MOON/intra_extensions/{}".format(ie_id), method="DELETE", authtoken=True) + data = get_url("/v3/OS-MOON/intra_extensions", authtoken=True) + self.assertNotIn(ie_id, data["intra_extensions"]) + + def test_rules_data(self): + data = get_url("/v3/OS-MOON/intra_extensions", authtoken=True) + self.assertIn("intra_extensions", data) + data = get_url("/v3/OS-MOON/authz_policies", authtoken=True) + self.assertIn("authz_policies", data) + for model in data["authz_policies"]: + # Create a new intra_extension + print("=====> {}".format(model)) + new_ie = { + "name": "new_intra_extension", + "description": "new_intra_extension", + "policymodel": model + } + data = get_url("/v3/OS-MOON/intra_extensions/", post_data=new_ie, authtoken=True) + for key in [u'model', u'id', u'name', u'description']: + self.assertIn(key, data) + ie_id = data["id"] + data = get_url("/v3/OS-MOON/intra_extensions", authtoken=True) + self.assertIn(ie_id, data["intra_extensions"]) + + # Get all sub_meta_rule_relations + data = get_url("/v3/OS-MOON/intra_extensions/{}/sub_meta_rule_relations".format(ie_id), authtoken=True) + self.assertIn("sub_meta_rule_relations", data) + self.assertIs(type(data["sub_meta_rule_relations"]), list) + sub_meta_rule_relations = data["sub_meta_rule_relations"] + + # Get current sub_meta_rule + data = get_url("/v3/OS-MOON/intra_extensions/{}/sub_meta_rule".format(ie_id), authtoken=True) + self.assertIn("sub_meta_rules", data) + self.assertIs(type(data["sub_meta_rules"]), dict) + self.assertGreater(len(data["sub_meta_rules"].keys()), 0) + relation = data["sub_meta_rules"].keys()[0] + self.assertIn(relation, sub_meta_rule_relations) + sub_meta_rule = data["sub_meta_rules"] + sub_meta_rule_length = dict() + sub_meta_rule_length[relation] = len(data["sub_meta_rules"][relation]["subject_categories"]) + \ + len(data["sub_meta_rules"][relation]["object_categories"]) + \ + len(data["sub_meta_rules"][relation]["action_categories"]) +1 + + # Get all rules + data = get_url("/v3/OS-MOON/intra_extensions/{}/sub_rules".format(ie_id), authtoken=True) + self.assertIn("rules", data) + self.assertIs(type(data["rules"]), dict) + length = dict() + for key in data["rules"]: + self.assertIn(key, sub_meta_rule_relations) + self.assertGreater(len(data["rules"][key]), 0) + self.assertIs(type(data["rules"][key]), list) + for sub_rule in data["rules"][key]: + self.assertEqual(len(sub_rule), sub_meta_rule_length[key]) + length[key] = len(data["rules"][key]) + + # Get one value of subject category scope + # FIXME: a better test would be to add a new value in scope and then add it to a new sub-rule + categories = get_url("/v3/OS-MOON/intra_extensions/{}/subject_categories".format(ie_id), + authtoken=True)["subject_categories"].keys() + data = get_url("/v3/OS-MOON/intra_extensions/{}/subject_category_scope/{}".format( + ie_id, categories[0]), authtoken=True) + self.assertIn("subject_category_scope", data) + self.assertIs(type(data["subject_category_scope"]), dict) + subject_category = categories.pop() + subject_value = data["subject_category_scope"][subject_category].keys()[0] + # Get one value of object category scope + # FIXME: a better test would be to add a new value in scope and then add it to a new sub-rule + categories = get_url("/v3/OS-MOON/intra_extensions/{}/object_categories".format(ie_id), + authtoken=True)["object_categories"].keys() + data = get_url("/v3/OS-MOON/intra_extensions/{}/object_category_scope/{}".format( + ie_id, categories[0]), authtoken=True) + self.assertIn("object_category_scope", data) + self.assertIs(type(data["object_category_scope"]), dict) + object_category = categories.pop() + object_value = data["object_category_scope"][object_category].keys()[0] + # Get one or more values in action category scope + _sub_meta_action_value = list() + for _sub_meta_cat in sub_meta_rule[relation]["action_categories"]: + data = get_url("/v3/OS-MOON/intra_extensions/{}/action_category_scope/{}".format( + ie_id, _sub_meta_cat), authtoken=True) + action_value = data["action_category_scope"][_sub_meta_cat].keys()[0] + _sub_meta_action_value.append(action_value) + _sub_meta_rules = list() + _sub_meta_rules.append(subject_value) + _sub_meta_rules.extend(_sub_meta_action_value) + _sub_meta_rules.append(object_value) + # Must append True because the sub_rule need a boolean to know if it is a positive or a negative value + _sub_meta_rules.append(True) + post_data = { + "rule": _sub_meta_rules, + "relation": "relation_super" + } + # Add a new sub-rule + data = get_url("/v3/OS-MOON/intra_extensions/{}/sub_rules".format(ie_id), + post_data=post_data, authtoken=True) + self.assertIn("rules", data) + self.assertIs(type(data["rules"]), dict) + for key in data["rules"]: + self.assertIn(key, sub_meta_rule_relations) + self.assertGreater(len(data["rules"][key]), 0) + for sub_rule in data["rules"][key]: + self.assertEqual(len(sub_rule), sub_meta_rule_length[key]) + if key == "relation_super": + self.assertEqual(len(data["rules"][key]), length[key]+1) + else: + self.assertEqual(len(data["rules"][key]), length[key]) + + # Delete the new sub-rule + data = get_url("/v3/OS-MOON/intra_extensions/{}/sub_rules/{rel}/{rule}".format( + ie_id, + rel=post_data["relation"], + rule="+".join(map(lambda x: str(x), post_data["rule"]))), + method="DELETE", authtoken=True) + self.assertIn("rules", data) + self.assertIs(type(data["rules"]), dict) + for key in data["rules"]: + self.assertIn(key, sub_meta_rule_relations) + self.assertGreater(len(data["rules"][key]), 0) + for sub_rule in data["rules"][key]: + if key == "relation_super": + self.assertEqual(len(data["rules"][key]), length[key]) + else: + self.assertEqual(len(data["rules"][key]), length[key]) + + # Delete the intra_extension + data = get_url("/v3/OS-MOON/intra_extensions/{}".format(ie_id), method="DELETE", authtoken=True) + data = get_url("/v3/OS-MOON/intra_extensions", authtoken=True) + self.assertNotIn(ie_id, data["intra_extensions"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/keystone-moon/keystone/tests/moon/func/test_func_api_log.py b/keystone-moon/keystone/tests/moon/func/test_func_api_log.py new file mode 100644 index 00000000..f081aef1 --- /dev/null +++ b/keystone-moon/keystone/tests/moon/func/test_func_api_log.py @@ -0,0 +1,148 @@ +# Copyright 2015 Open Platform for NFV Project, Inc. and its contributors +# This software is distributed under the terms and conditions of the 'Apache-2.0' +# license which can be found in the file 'LICENSE' in this package distribution +# or at 'http://www.apache.org/licenses/LICENSE-2.0'. + +import unittest +import json +import httplib +import time +from uuid import uuid4 +import copy + +CREDENTIALS = { + "host": "127.0.0.1", + "port": "35357", + "login": "admin", + "password": "nomoresecrete", + "tenant_name": "demo", + "sessionid": "kxb50d9uusiywfcs2fiidmu1j5nsyckr", + "csrftoken": "", + "x-subject-token": "" +} + + +def get_url(url, post_data=None, delete_data=None, crsftoken=None, method="GET", authtoken=None): + # MOON_SERVER_IP["URL"] = url + # _url = "http://{HOST}:{PORT}".format(**MOON_SERVER_IP) + if post_data: + method = "POST" + if delete_data: + method = "DELETE" + # print("\033[32m{} {}\033[m".format(method, url)) + conn = httplib.HTTPConnection(CREDENTIALS["host"], CREDENTIALS["port"]) + headers = { + "Content-type": "application/x-www-form-urlencoded", + # "Accept": "text/plain", + "Accept": "text/plain,text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + 'Cookie': 'sessionid={}'.format(CREDENTIALS["sessionid"]), + } + if crsftoken: + headers["Cookie"] = "csrftoken={}; sessionid={}; NG_TRANSLATE_LANG_KEY:\"en\"".format(crsftoken, CREDENTIALS["sessionid"]) + CREDENTIALS["crsftoken"] = crsftoken + if authtoken: + headers["X-Auth-Token"] = CREDENTIALS["x-subject-token"] + if post_data: + method = "POST" + headers["Content-type"] = "application/json" + if crsftoken: + post_data = "&".join(map(lambda x: "=".join(x), post_data)) + elif "crsftoken" in CREDENTIALS and "sessionid" in CREDENTIALS: + post_data = json.dumps(post_data) + headers["Cookie"] = "csrftoken={}; sessionid={}; NG_TRANSLATE_LANG_KEY:\"en\"".format( + CREDENTIALS["crsftoken"], + CREDENTIALS["sessionid"]) + else: + post_data = json.dumps(post_data) + # conn.request(method, url, json.dumps(post_data), headers=headers) + conn.request(method, url, post_data, headers=headers) + elif delete_data: + method = "DELETE" + conn.request(method, url, json.dumps(delete_data), headers=headers) + else: + conn.request(method, url, headers=headers) + resp = conn.getresponse() + headers = resp.getheaders() + try: + CREDENTIALS["x-subject-token"] = dict(headers)["x-subject-token"] + except KeyError: + pass + if crsftoken: + sessionid_start = dict(headers)["set-cookie"].index("sessionid=")+len("sessionid=") + sessionid_end = dict(headers)["set-cookie"].index(";", sessionid_start) + sessionid = dict(headers)["set-cookie"][sessionid_start:sessionid_end] + CREDENTIALS["sessionid"] = sessionid + content = resp.read() + conn.close() + try: + return json.loads(content) + except ValueError: + return {"content": content} + + +class IntraExtensionsTest(unittest.TestCase): + + TIME_FORMAT = '%Y-%m-%d-%H:%M:%S' + + def setUp(self): + post = { + "auth": { + "identity": { + "methods": [ + "password" + ], + "password": { + "user": { + "domain": { + "id": "Default" + }, + "name": "admin", + "password": "nomoresecrete" + } + } + }, + "scope": { + "project": { + "domain": { + "id": "Default" + }, + "name": "demo" + } + } + } + } + data = get_url("/v3/auth/tokens", post_data=post) + self.assertIn("token", data) + + def tearDown(self): + pass + + def test_get_logs(self): + all_data = get_url("/v3/OS-MOON/logs", authtoken=True) + len_all_data = len(all_data["logs"]) + data_1 = all_data["logs"][len_all_data/2] + time_data_1 = data_1.split(" ")[0] + data_2 = all_data["logs"][len_all_data/2+10] + time_data_2 = data_2.split(" ")[0] + self.assertIn("logs", all_data) + data = get_url("/v3/OS-MOON/logs/filter=authz", authtoken=True) + self.assertIn("logs", data) + self.assertGreater(len_all_data, len(data["logs"])) + data = get_url("/v3/OS-MOON/logs/from={}".format(time_data_1), authtoken=True) + self.assertIn("logs", data) + self.assertGreater(len_all_data, len(data["logs"])) + # for _data in data["logs"]: + # self.assertGreater(time.strptime(_data.split(" "), self.TIME_FORMAT), + # time.strptime(time_data_1, self.TIME_FORMAT)) + data = get_url("/v3/OS-MOON/logs/from={},to={}".format(time_data_1, time_data_2), authtoken=True) + self.assertIn("logs", data) + self.assertGreater(len_all_data, len(data["logs"])) + self.assertEqual(10, len(data["logs"])) + data = get_url("/v3/OS-MOON/logs/event_number=20", authtoken=True) + self.assertIn("logs", data) + self.assertGreater(len_all_data, len(data["logs"])) + self.assertEqual(20, len(data["logs"])) + + +if __name__ == "__main__": + unittest.main() diff --git a/keystone-moon/keystone/tests/moon/func/test_func_api_tenant.py b/keystone-moon/keystone/tests/moon/func/test_func_api_tenant.py new file mode 100644 index 00000000..c52e068e --- /dev/null +++ b/keystone-moon/keystone/tests/moon/func/test_func_api_tenant.py @@ -0,0 +1,154 @@ +# Copyright 2015 Open Platform for NFV Project, Inc. and its contributors +# This software is distributed under the terms and conditions of the 'Apache-2.0' +# license which can be found in the file 'LICENSE' in this package distribution +# or at 'http://www.apache.org/licenses/LICENSE-2.0'. + +import unittest +import json +import httplib +import time +from uuid import uuid4 +import copy + +CREDENTIALS = { + "host": "127.0.0.1", + "port": "35357", + "login": "admin", + "password": "nomoresecrete", + "tenant_name": "demo", + "sessionid": "kxb50d9uusiywfcs2fiidmu1j5nsyckr", + "csrftoken": "", + "x-subject-token": "" +} + + +def get_url(url, post_data=None, delete_data=None, crsftoken=None, method="GET", authtoken=None): + # MOON_SERVER_IP["URL"] = url + # _url = "http://{HOST}:{PORT}".format(**MOON_SERVER_IP) + if post_data: + method = "POST" + if delete_data: + method = "DELETE" + # print("\033[32m{} {}\033[m".format(method, url)) + conn = httplib.HTTPConnection(CREDENTIALS["host"], CREDENTIALS["port"]) + headers = { + "Content-type": "application/x-www-form-urlencoded", + # "Accept": "text/plain", + "Accept": "text/plain,text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + 'Cookie': 'sessionid={}'.format(CREDENTIALS["sessionid"]), + } + if crsftoken: + headers["Cookie"] = "csrftoken={}; sessionid={}; NG_TRANSLATE_LANG_KEY:\"en\"".format(crsftoken, CREDENTIALS["sessionid"]) + CREDENTIALS["crsftoken"] = crsftoken + if authtoken: + headers["X-Auth-Token"] = CREDENTIALS["x-subject-token"] + if post_data: + method = "POST" + headers["Content-type"] = "application/json" + if crsftoken: + post_data = "&".join(map(lambda x: "=".join(x), post_data)) + elif "crsftoken" in CREDENTIALS and "sessionid" in CREDENTIALS: + post_data = json.dumps(post_data) + headers["Cookie"] = "csrftoken={}; sessionid={}; NG_TRANSLATE_LANG_KEY:\"en\"".format( + CREDENTIALS["crsftoken"], + CREDENTIALS["sessionid"]) + else: + post_data = json.dumps(post_data) + # conn.request(method, url, json.dumps(post_data), headers=headers) + conn.request(method, url, post_data, headers=headers) + elif delete_data: + method = "DELETE" + conn.request(method, url, json.dumps(delete_data), headers=headers) + else: + conn.request(method, url, headers=headers) + resp = conn.getresponse() + headers = resp.getheaders() + try: + CREDENTIALS["x-subject-token"] = dict(headers)["x-subject-token"] + except KeyError: + pass + if crsftoken: + sessionid_start = dict(headers)["set-cookie"].index("sessionid=")+len("sessionid=") + sessionid_end = dict(headers)["set-cookie"].index(";", sessionid_start) + sessionid = dict(headers)["set-cookie"][sessionid_start:sessionid_end] + CREDENTIALS["sessionid"] = sessionid + content = resp.read() + conn.close() + try: + return json.loads(content) + except ValueError: + return {"content": content} + + +class MappingsTest(unittest.TestCase): + + def setUp(self): + post = { + "auth": { + "identity": { + "methods": [ + "password" + ], + "password": { + "user": { + "domain": { + "id": "Default" + }, + "name": "admin", + "password": "nomoresecrete" + } + } + }, + "scope": { + "project": { + "domain": { + "id": "Default" + }, + "name": "demo" + } + } + } + } + data = get_url("/v3/auth/tokens", post_data=post) + self.assertIn("token", data) + + def tearDown(self): + pass + + def test_get_tenants(self): + data = get_url("/v3/OS-MOON/tenants", authtoken=True) + self.assertIn("tenants", data) + self.assertIsInstance(data["tenants"], list) + print(data) + + def test_add_delete_mapping(self): + data = get_url("/v3/projects", authtoken=True) + project_id = None + for project in data["projects"]: + if project["name"] == "demo": + project_id = project["id"] + data = get_url("/v3/OS-MOON/tenant", + post_data={ + "id": project_id, + "name": "tenant1", + "authz": "intra_extension_uuid1", + "admin": "intra_extension_uuid2" + }, + authtoken=True) + self.assertIn("tenant", data) + self.assertIsInstance(data["tenant"], dict) + uuid = data["tenant"]["id"] + data = get_url("/v3/OS-MOON/tenants", authtoken=True) + self.assertIn("tenants", data) + self.assertIsInstance(data["tenants"], list) + print(data) + data = get_url("/v3/OS-MOON/tenant/{}".format(uuid), + method="DELETE", + authtoken=True) + data = get_url("/v3/OS-MOON/tenants", authtoken=True) + self.assertIn("tenants", data) + self.assertIsInstance(data["tenants"], list) + print(data) + +if __name__ == "__main__": + unittest.main() diff --git a/keystone-moon/keystone/tests/moon/unit/__init__.py b/keystone-moon/keystone/tests/moon/unit/__init__.py new file mode 100644 index 00000000..1b678d53 --- /dev/null +++ b/keystone-moon/keystone/tests/moon/unit/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2015 Open Platform for NFV Project, Inc. and its contributors +# This software is distributed under the terms and conditions of the 'Apache-2.0' +# license which can be found in the file 'LICENSE' in this package distribution +# or at 'http://www.apache.org/licenses/LICENSE-2.0'. diff --git a/keystone-moon/keystone/tests/moon/unit/test_unit_core_intra_extension_admin.py b/keystone-moon/keystone/tests/moon/unit/test_unit_core_intra_extension_admin.py new file mode 100644 index 00000000..03ef845c --- /dev/null +++ b/keystone-moon/keystone/tests/moon/unit/test_unit_core_intra_extension_admin.py @@ -0,0 +1,1229 @@ +# Copyright 2015 Open Platform for NFV Project, Inc. and its contributors +# This software is distributed under the terms and conditions of the 'Apache-2.0' +# license which can be found in the file 'LICENSE' in this package distribution +# or at 'http://www.apache.org/licenses/LICENSE-2.0'. + +"""Unit tests for core IntraExtensionAdminManager""" + +import json +import os +import uuid +from oslo_config import cfg +from keystone.tests import unit as tests +from keystone.contrib.moon.core import IntraExtensionAdminManager, IntraExtensionAuthzManager +from keystone.tests.unit.ksfixtures import database +from keystone import resource +from keystone.contrib.moon.exception import * +from keystone.tests.unit import default_fixtures +from keystone.contrib.moon.core import LogManager, TenantManager + +CONF = cfg.CONF + +USER_ADMIN = { + 'name': 'admin', + 'domain_id': "default", + 'password': 'admin' +} + +IE = { + "name": "test IE", + "policymodel": "policy_rbac_authz", + "description": "a simple description." +} + +class TestIntraExtensionAdminManager(tests.TestCase): + + def setUp(self): + self.useFixture(database.Database()) + super(TestIntraExtensionAdminManager, self).setUp() + self.load_backends() + self.load_fixtures(default_fixtures) + self.manager = IntraExtensionAdminManager() + + def __get_key_from_value(self, value, values_dict): + return filter(lambda v: v[1] == value, values_dict.iteritems())[0][0] + + def load_extra_backends(self): + return { + "moonlog_api": LogManager(), + "tenant_api": TenantManager(), + # "resource_api": resource.Manager(), + } + + def config_overrides(self): + super(TestIntraExtensionAdminManager, self).config_overrides() + self.policy_directory = '../../../examples/moon/policies' + self.config_fixture.config( + group='moon', + intraextension_driver='keystone.contrib.moon.backends.sql.IntraExtensionConnector') + self.config_fixture.config( + group='moon', + policy_directory=self.policy_directory) + + def create_intra_extension(self, policy_model="policy_rbac_admin"): + # Create the admin user because IntraExtension needs it + self.admin = self.identity_api.create_user(USER_ADMIN) + IE["policymodel"] = policy_model + self.ref = self.manager.load_intra_extension(IE) + self.assertIsInstance(self.ref, dict) + self.create_tenant(self.ref["id"]) + + def create_tenant(self, authz_uuid): + tenant = { + "id": uuid.uuid4().hex, + "name": "TestAuthzIntraExtensionManager", + "enabled": True, + "description": "", + "domain_id": "default" + } + project = self.resource_api.create_project(tenant["id"], tenant) + mapping = self.tenant_api.set_tenant_dict(project["id"], project["name"], authz_uuid, None) + self.assertIsInstance(mapping, dict) + self.assertIn("authz", mapping) + self.assertEqual(mapping["authz"], authz_uuid) + return mapping + + def create_user(self, username="TestAdminIntraExtensionManagerUser"): + user = { + "id": uuid.uuid4().hex, + "name": username, + "enabled": True, + "description": "", + "domain_id": "default" + } + _user = self.identity_api.create_user(user) + return _user + + def delete_admin_intra_extension(self): + self.manager.delete_intra_extension(self.ref["id"]) + + def test_subjects(self): + self.create_intra_extension() + + subjects = self.manager.get_subject_dict("admin", self.ref["id"]) + self.assertIsInstance(subjects, dict) + self.assertIn("subjects", subjects) + self.assertIn("id", subjects) + self.assertIn("intra_extension_uuid", subjects) + self.assertEqual(self.ref["id"], subjects["intra_extension_uuid"]) + self.assertIsInstance(subjects["subjects"], dict) + + new_subject = self.create_user() + new_subjects = dict() + new_subjects[new_subject["id"]] = new_subject["name"] + subjects = self.manager.set_subject_dict("admin", self.ref["id"], new_subjects) + self.assertIsInstance(subjects, dict) + self.assertIn("subjects", subjects) + self.assertIn("id", subjects) + self.assertIn("intra_extension_uuid", subjects) + self.assertEqual(self.ref["id"], subjects["intra_extension_uuid"]) + self.assertEqual(subjects["subjects"], new_subjects) + self.assertIn(new_subject["id"], subjects["subjects"]) + + # Delete the new subject + self.manager.del_subject("admin", self.ref["id"], new_subject["id"]) + subjects = self.manager.get_subject_dict("admin", self.ref["id"]) + self.assertIsInstance(subjects, dict) + self.assertIn("subjects", subjects) + self.assertIn("id", subjects) + self.assertIn("intra_extension_uuid", subjects) + self.assertEqual(self.ref["id"], subjects["intra_extension_uuid"]) + self.assertNotIn(new_subject["id"], subjects["subjects"]) + + # Add a particular subject + subjects = self.manager.add_subject_dict("admin", self.ref["id"], new_subject["id"]) + self.assertIsInstance(subjects, dict) + self.assertIn("subject", subjects) + self.assertIn("uuid", subjects["subject"]) + self.assertEqual(new_subject["name"], subjects["subject"]["name"]) + subjects = self.manager.get_subject_dict("admin", self.ref["id"]) + self.assertIsInstance(subjects, dict) + self.assertIn("subjects", subjects) + self.assertIn("id", subjects) + self.assertIn("intra_extension_uuid", subjects) + self.assertEqual(self.ref["id"], subjects["intra_extension_uuid"]) + self.assertIn(new_subject["id"], subjects["subjects"]) + + def test_objects(self): + self.create_intra_extension() + + objects = self.manager.get_object_dict("admin", self.ref["id"]) + self.assertIsInstance(objects, dict) + self.assertIn("objects", objects) + self.assertIn("id", objects) + self.assertIn("intra_extension_uuid", objects) + self.assertEqual(self.ref["id"], objects["intra_extension_uuid"]) + self.assertIsInstance(objects["objects"], dict) + + new_object = self.create_user() + new_objects = dict() + new_objects[new_object["id"]] = new_object["name"] + objects = self.manager.set_object_dict("admin", self.ref["id"], new_objects) + self.assertIsInstance(objects, dict) + self.assertIn("objects", objects) + self.assertIn("id", objects) + self.assertIn("intra_extension_uuid", objects) + self.assertEqual(self.ref["id"], objects["intra_extension_uuid"]) + self.assertEqual(objects["objects"], new_objects) + self.assertIn(new_object["id"], objects["objects"]) + + # Delete the new object + self.manager.del_object("admin", self.ref["id"], new_object["id"]) + objects = self.manager.get_object_dict("admin", self.ref["id"]) + self.assertIsInstance(objects, dict) + self.assertIn("objects", objects) + self.assertIn("id", objects) + self.assertIn("intra_extension_uuid", objects) + self.assertEqual(self.ref["id"], objects["intra_extension_uuid"]) + self.assertNotIn(new_object["id"], objects["objects"]) + + # Add a particular object + objects = self.manager.add_object_dict("admin", self.ref["id"], new_object["name"]) + self.assertIsInstance(objects, dict) + self.assertIn("object", objects) + self.assertIn("uuid", objects["object"]) + self.assertEqual(new_object["name"], objects["object"]["name"]) + new_object["id"] = objects["object"]["uuid"] + objects = self.manager.get_object_dict("admin", self.ref["id"]) + self.assertIsInstance(objects, dict) + self.assertIn("objects", objects) + self.assertIn("id", objects) + self.assertIn("intra_extension_uuid", objects) + self.assertEqual(self.ref["id"], objects["intra_extension_uuid"]) + self.assertIn(new_object["id"], objects["objects"]) + + def test_actions(self): + self.create_intra_extension() + + actions = self.manager.get_action_dict("admin", self.ref["id"]) + self.assertIsInstance(actions, dict) + self.assertIn("actions", actions) + self.assertIn("id", actions) + self.assertIn("intra_extension_uuid", actions) + self.assertEqual(self.ref["id"], actions["intra_extension_uuid"]) + self.assertIsInstance(actions["actions"], dict) + + new_action = self.create_user() + new_actions = dict() + new_actions[new_action["id"]] = new_action["name"] + actions = self.manager.set_action_dict("admin", self.ref["id"], new_actions) + self.assertIsInstance(actions, dict) + self.assertIn("actions", actions) + self.assertIn("id", actions) + self.assertIn("intra_extension_uuid", actions) + self.assertEqual(self.ref["id"], actions["intra_extension_uuid"]) + self.assertEqual(actions["actions"], new_actions) + self.assertIn(new_action["id"], actions["actions"]) + + # Delete the new action + self.manager.del_action("admin", self.ref["id"], new_action["id"]) + actions = self.manager.get_action_dict("admin", self.ref["id"]) + self.assertIsInstance(actions, dict) + self.assertIn("actions", actions) + self.assertIn("id", actions) + self.assertIn("intra_extension_uuid", actions) + self.assertEqual(self.ref["id"], actions["intra_extension_uuid"]) + self.assertNotIn(new_action["id"], actions["actions"]) + + # Add a particular action + actions = self.manager.add_action_dict("admin", self.ref["id"], new_action["name"]) + self.assertIsInstance(actions, dict) + self.assertIn("action", actions) + self.assertIn("uuid", actions["action"]) + self.assertEqual(new_action["name"], actions["action"]["name"]) + new_action["id"] = actions["action"]["uuid"] + actions = self.manager.get_action_dict("admin", self.ref["id"]) + self.assertIsInstance(actions, dict) + self.assertIn("actions", actions) + self.assertIn("id", actions) + self.assertIn("intra_extension_uuid", actions) + self.assertEqual(self.ref["id"], actions["intra_extension_uuid"]) + self.assertIn(new_action["id"], actions["actions"]) + + def test_subject_categories(self): + self.create_intra_extension() + + subject_categories = self.manager.get_subject_category_dict("admin", self.ref["id"]) + self.assertIsInstance(subject_categories, dict) + self.assertIn("subject_categories", subject_categories) + self.assertIn("id", subject_categories) + self.assertIn("intra_extension_uuid", subject_categories) + self.assertEqual(self.ref["id"], subject_categories["intra_extension_uuid"]) + self.assertIsInstance(subject_categories["subject_categories"], dict) + + new_subject_category = {"id": uuid.uuid4().hex, "name": "subject_category_test"} + new_subject_categories = dict() + new_subject_categories[new_subject_category["id"]] = new_subject_category["name"] + subject_categories = self.manager.set_subject_category_dict("admin", self.ref["id"], new_subject_categories) + self.assertIsInstance(subject_categories, dict) + self.assertIn("subject_categories", subject_categories) + self.assertIn("id", subject_categories) + self.assertIn("intra_extension_uuid", subject_categories) + self.assertEqual(self.ref["id"], subject_categories["intra_extension_uuid"]) + self.assertEqual(subject_categories["subject_categories"], new_subject_categories) + self.assertIn(new_subject_category["id"], subject_categories["subject_categories"]) + + # Delete the new subject_category + self.manager.del_subject_category("admin", self.ref["id"], new_subject_category["id"]) + subject_categories = self.manager.get_subject_category_dict("admin", self.ref["id"]) + self.assertIsInstance(subject_categories, dict) + self.assertIn("subject_categories", subject_categories) + self.assertIn("id", subject_categories) + self.assertIn("intra_extension_uuid", subject_categories) + self.assertEqual(self.ref["id"], subject_categories["intra_extension_uuid"]) + self.assertNotIn(new_subject_category["id"], subject_categories["subject_categories"]) + + # Add a particular subject_category + subject_categories = self.manager.add_subject_category_dict( + "admin", + self.ref["id"], + new_subject_category["name"]) + self.assertIsInstance(subject_categories, dict) + self.assertIn("subject_category", subject_categories) + self.assertIn("uuid", subject_categories["subject_category"]) + self.assertEqual(new_subject_category["name"], subject_categories["subject_category"]["name"]) + new_subject_category["id"] = subject_categories["subject_category"]["uuid"] + subject_categories = self.manager.get_subject_category_dict( + "admin", + self.ref["id"]) + self.assertIsInstance(subject_categories, dict) + self.assertIn("subject_categories", subject_categories) + self.assertIn("id", subject_categories) + self.assertIn("intra_extension_uuid", subject_categories) + self.assertEqual(self.ref["id"], subject_categories["intra_extension_uuid"]) + self.assertIn(new_subject_category["id"], subject_categories["subject_categories"]) + + def test_object_categories(self): + self.create_intra_extension() + + object_categories = self.manager.get_object_category_dict("admin", self.ref["id"]) + self.assertIsInstance(object_categories, dict) + self.assertIn("object_categories", object_categories) + self.assertIn("id", object_categories) + self.assertIn("intra_extension_uuid", object_categories) + self.assertEqual(self.ref["id"], object_categories["intra_extension_uuid"]) + self.assertIsInstance(object_categories["object_categories"], dict) + + new_object_category = {"id": uuid.uuid4().hex, "name": "object_category_test"} + new_object_categories = dict() + new_object_categories[new_object_category["id"]] = new_object_category["name"] + object_categories = self.manager.set_object_category_dict("admin", self.ref["id"], new_object_categories) + self.assertIsInstance(object_categories, dict) + self.assertIn("object_categories", object_categories) + self.assertIn("id", object_categories) + self.assertIn("intra_extension_uuid", object_categories) + self.assertEqual(self.ref["id"], object_categories["intra_extension_uuid"]) + self.assertEqual(object_categories["object_categories"], new_object_categories) + self.assertIn(new_object_category["id"], object_categories["object_categories"]) + + # Delete the new object_category + self.manager.del_object_category("admin", self.ref["id"], new_object_category["id"]) + object_categories = self.manager.get_object_category_dict("admin", self.ref["id"]) + self.assertIsInstance(object_categories, dict) + self.assertIn("object_categories", object_categories) + self.assertIn("id", object_categories) + self.assertIn("intra_extension_uuid", object_categories) + self.assertEqual(self.ref["id"], object_categories["intra_extension_uuid"]) + self.assertNotIn(new_object_category["id"], object_categories["object_categories"]) + + # Add a particular object_category + object_categories = self.manager.add_object_category_dict( + "admin", + self.ref["id"], + new_object_category["name"]) + self.assertIsInstance(object_categories, dict) + self.assertIn("object_category", object_categories) + self.assertIn("uuid", object_categories["object_category"]) + self.assertEqual(new_object_category["name"], object_categories["object_category"]["name"]) + new_object_category["id"] = object_categories["object_category"]["uuid"] + object_categories = self.manager.get_object_category_dict( + "admin", + self.ref["id"]) + self.assertIsInstance(object_categories, dict) + self.assertIn("object_categories", object_categories) + self.assertIn("id", object_categories) + self.assertIn("intra_extension_uuid", object_categories) + self.assertEqual(self.ref["id"], object_categories["intra_extension_uuid"]) + self.assertIn(new_object_category["id"], object_categories["object_categories"]) + + def test_action_categories(self): + self.create_intra_extension() + + action_categories = self.manager.get_action_category_dict("admin", self.ref["id"]) + self.assertIsInstance(action_categories, dict) + self.assertIn("action_categories", action_categories) + self.assertIn("id", action_categories) + self.assertIn("intra_extension_uuid", action_categories) + self.assertEqual(self.ref["id"], action_categories["intra_extension_uuid"]) + self.assertIsInstance(action_categories["action_categories"], dict) + + new_action_category = {"id": uuid.uuid4().hex, "name": "action_category_test"} + new_action_categories = dict() + new_action_categories[new_action_category["id"]] = new_action_category["name"] + action_categories = self.manager.set_action_category_dict("admin", self.ref["id"], new_action_categories) + self.assertIsInstance(action_categories, dict) + self.assertIn("action_categories", action_categories) + self.assertIn("id", action_categories) + self.assertIn("intra_extension_uuid", action_categories) + self.assertEqual(self.ref["id"], action_categories["intra_extension_uuid"]) + self.assertEqual(action_categories["action_categories"], new_action_categories) + self.assertIn(new_action_category["id"], action_categories["action_categories"]) + + # Delete the new action_category + self.manager.del_action_category("admin", self.ref["id"], new_action_category["id"]) + action_categories = self.manager.get_action_category_dict("admin", self.ref["id"]) + self.assertIsInstance(action_categories, dict) + self.assertIn("action_categories", action_categories) + self.assertIn("id", action_categories) + self.assertIn("intra_extension_uuid", action_categories) + self.assertEqual(self.ref["id"], action_categories["intra_extension_uuid"]) + self.assertNotIn(new_action_category["id"], action_categories["action_categories"]) + + # Add a particular action_category + action_categories = self.manager.add_action_category_dict( + "admin", + self.ref["id"], + new_action_category["name"]) + self.assertIsInstance(action_categories, dict) + self.assertIn("action_category", action_categories) + self.assertIn("uuid", action_categories["action_category"]) + self.assertEqual(new_action_category["name"], action_categories["action_category"]["name"]) + new_action_category["id"] = action_categories["action_category"]["uuid"] + action_categories = self.manager.get_action_category_dict( + "admin", + self.ref["id"]) + self.assertIsInstance(action_categories, dict) + self.assertIn("action_categories", action_categories) + self.assertIn("id", action_categories) + self.assertIn("intra_extension_uuid", action_categories) + self.assertEqual(self.ref["id"], action_categories["intra_extension_uuid"]) + self.assertIn(new_action_category["id"], action_categories["action_categories"]) + + def test_subject_category_scope(self): + self.create_intra_extension() + + subject_categories = self.manager.set_subject_category_dict( + "admin", + self.ref["id"], + { + uuid.uuid4().hex: "admin", + uuid.uuid4().hex: "dev", + } + ) + + for subject_category in subject_categories["subject_categories"]: + subject_category_scope = self.manager.get_subject_category_scope_dict( + "admin", + self.ref["id"], + subject_category) + self.assertIsInstance(subject_category_scope, dict) + self.assertIn("subject_category_scope", subject_category_scope) + self.assertIn("id", subject_category_scope) + self.assertIn("intra_extension_uuid", subject_category_scope) + self.assertEqual(self.ref["id"], subject_category_scope["intra_extension_uuid"]) + self.assertIsInstance(subject_category_scope["subject_category_scope"], dict) + + new_subject_category_scope = dict() + new_subject_category_scope_uuid = uuid.uuid4().hex + new_subject_category_scope[new_subject_category_scope_uuid] = "new_subject_category_scope" + subject_category_scope = self.manager.set_subject_category_scope_dict( + "admin", + self.ref["id"], + subject_category, + new_subject_category_scope) + self.assertIsInstance(subject_category_scope, dict) + self.assertIn("subject_category_scope", subject_category_scope) + self.assertIn("id", subject_category_scope) + self.assertIn("intra_extension_uuid", subject_category_scope) + self.assertEqual(self.ref["id"], subject_category_scope["intra_extension_uuid"]) + self.assertIn(new_subject_category_scope[new_subject_category_scope_uuid], + subject_category_scope["subject_category_scope"][subject_category].values()) + + # Delete the new subject_category_scope + self.manager.del_subject_category_scope( + "admin", + self.ref["id"], + subject_category, + new_subject_category_scope_uuid) + subject_category_scope = self.manager.get_subject_category_scope_dict( + "admin", + self.ref["id"], + subject_category) + self.assertIsInstance(subject_category_scope, dict) + self.assertIn("subject_category_scope", subject_category_scope) + self.assertIn("id", subject_category_scope) + self.assertIn("intra_extension_uuid", subject_category_scope) + self.assertEqual(self.ref["id"], subject_category_scope["intra_extension_uuid"]) + self.assertNotIn(new_subject_category_scope_uuid, subject_category_scope["subject_category_scope"]) + + # Add a particular subject_category_scope + subject_category_scope = self.manager.add_subject_category_scope_dict( + "admin", + self.ref["id"], + subject_category, + new_subject_category_scope[new_subject_category_scope_uuid]) + self.assertIsInstance(subject_category_scope, dict) + self.assertIn("subject_category_scope", subject_category_scope) + self.assertIn("uuid", subject_category_scope["subject_category_scope"]) + self.assertEqual(new_subject_category_scope[new_subject_category_scope_uuid], + subject_category_scope["subject_category_scope"]["name"]) + subject_category_scope = self.manager.get_subject_category_scope_dict( + "admin", + self.ref["id"], + subject_category) + self.assertIsInstance(subject_category_scope, dict) + self.assertIn("subject_category_scope", subject_category_scope) + self.assertIn("id", subject_category_scope) + self.assertIn("intra_extension_uuid", subject_category_scope) + self.assertEqual(self.ref["id"], subject_category_scope["intra_extension_uuid"]) + self.assertNotIn(new_subject_category_scope_uuid, subject_category_scope["subject_category_scope"]) + + def test_object_category_scope(self): + self.create_intra_extension() + + object_categories = self.manager.set_object_category_dict( + "admin", + self.ref["id"], + { + uuid.uuid4().hex: "id", + uuid.uuid4().hex: "domain", + } + ) + + for object_category in object_categories["object_categories"]: + object_category_scope = self.manager.get_object_category_scope_dict( + "admin", + self.ref["id"], + object_category) + self.assertIsInstance(object_category_scope, dict) + self.assertIn("object_category_scope", object_category_scope) + self.assertIn("id", object_category_scope) + self.assertIn("intra_extension_uuid", object_category_scope) + self.assertEqual(self.ref["id"], object_category_scope["intra_extension_uuid"]) + self.assertIsInstance(object_category_scope["object_category_scope"], dict) + + new_object_category_scope = dict() + new_object_category_scope_uuid = uuid.uuid4().hex + new_object_category_scope[new_object_category_scope_uuid] = "new_object_category_scope" + object_category_scope = self.manager.set_object_category_scope_dict( + "admin", + self.ref["id"], + object_category, + new_object_category_scope) + self.assertIsInstance(object_category_scope, dict) + self.assertIn("object_category_scope", object_category_scope) + self.assertIn("id", object_category_scope) + self.assertIn("intra_extension_uuid", object_category_scope) + self.assertEqual(self.ref["id"], object_category_scope["intra_extension_uuid"]) + self.assertIn(new_object_category_scope[new_object_category_scope_uuid], + object_category_scope["object_category_scope"][object_category].values()) + + # Delete the new object_category_scope + self.manager.del_object_category_scope( + "admin", + self.ref["id"], + object_category, + new_object_category_scope_uuid) + object_category_scope = self.manager.get_object_category_scope_dict( + "admin", + self.ref["id"], + object_category) + self.assertIsInstance(object_category_scope, dict) + self.assertIn("object_category_scope", object_category_scope) + self.assertIn("id", object_category_scope) + self.assertIn("intra_extension_uuid", object_category_scope) + self.assertEqual(self.ref["id"], object_category_scope["intra_extension_uuid"]) + self.assertNotIn(new_object_category_scope_uuid, object_category_scope["object_category_scope"]) + + # Add a particular object_category_scope + object_category_scope = self.manager.add_object_category_scope_dict( + "admin", + self.ref["id"], + object_category, + new_object_category_scope[new_object_category_scope_uuid]) + self.assertIsInstance(object_category_scope, dict) + self.assertIn("object_category_scope", object_category_scope) + self.assertIn("uuid", object_category_scope["object_category_scope"]) + self.assertEqual(new_object_category_scope[new_object_category_scope_uuid], + object_category_scope["object_category_scope"]["name"]) + object_category_scope = self.manager.get_object_category_scope_dict( + "admin", + self.ref["id"], + object_category) + self.assertIsInstance(object_category_scope, dict) + self.assertIn("object_category_scope", object_category_scope) + self.assertIn("id", object_category_scope) + self.assertIn("intra_extension_uuid", object_category_scope) + self.assertEqual(self.ref["id"], object_category_scope["intra_extension_uuid"]) + self.assertNotIn(new_object_category_scope_uuid, object_category_scope["object_category_scope"]) + + def test_action_category_scope(self): + self.create_intra_extension() + + action_categories = self.manager.set_action_category_dict( + "admin", + self.ref["id"], + { + uuid.uuid4().hex: "compute", + uuid.uuid4().hex: "identity", + } + ) + + for action_category in action_categories["action_categories"]: + action_category_scope = self.manager.get_action_category_scope_dict( + "admin", + self.ref["id"], + action_category) + self.assertIsInstance(action_category_scope, dict) + self.assertIn("action_category_scope", action_category_scope) + self.assertIn("id", action_category_scope) + self.assertIn("intra_extension_uuid", action_category_scope) + self.assertEqual(self.ref["id"], action_category_scope["intra_extension_uuid"]) + self.assertIsInstance(action_category_scope["action_category_scope"], dict) + + new_action_category_scope = dict() + new_action_category_scope_uuid = uuid.uuid4().hex + new_action_category_scope[new_action_category_scope_uuid] = "new_action_category_scope" + action_category_scope = self.manager.set_action_category_scope_dict( + "admin", + self.ref["id"], + action_category, + new_action_category_scope) + self.assertIsInstance(action_category_scope, dict) + self.assertIn("action_category_scope", action_category_scope) + self.assertIn("id", action_category_scope) + self.assertIn("intra_extension_uuid", action_category_scope) + self.assertEqual(self.ref["id"], action_category_scope["intra_extension_uuid"]) + self.assertIn(new_action_category_scope[new_action_category_scope_uuid], + action_category_scope["action_category_scope"][action_category].values()) + + # Delete the new action_category_scope + self.manager.del_action_category_scope( + "admin", + self.ref["id"], + action_category, + new_action_category_scope_uuid) + action_category_scope = self.manager.get_action_category_scope_dict( + "admin", + self.ref["id"], + action_category) + self.assertIsInstance(action_category_scope, dict) + self.assertIn("action_category_scope", action_category_scope) + self.assertIn("id", action_category_scope) + self.assertIn("intra_extension_uuid", action_category_scope) + self.assertEqual(self.ref["id"], action_category_scope["intra_extension_uuid"]) + self.assertNotIn(new_action_category_scope_uuid, action_category_scope["action_category_scope"]) + + # Add a particular action_category_scope + action_category_scope = self.manager.add_action_category_scope_dict( + "admin", + self.ref["id"], + action_category, + new_action_category_scope[new_action_category_scope_uuid]) + self.assertIsInstance(action_category_scope, dict) + self.assertIn("action_category_scope", action_category_scope) + self.assertIn("uuid", action_category_scope["action_category_scope"]) + self.assertEqual(new_action_category_scope[new_action_category_scope_uuid], + action_category_scope["action_category_scope"]["name"]) + action_category_scope = self.manager.get_action_category_scope_dict( + "admin", + self.ref["id"], + action_category) + self.assertIsInstance(action_category_scope, dict) + self.assertIn("action_category_scope", action_category_scope) + self.assertIn("id", action_category_scope) + self.assertIn("intra_extension_uuid", action_category_scope) + self.assertEqual(self.ref["id"], action_category_scope["intra_extension_uuid"]) + self.assertNotIn(new_action_category_scope_uuid, action_category_scope["action_category_scope"]) + + def test_subject_category_assignment(self): + self.create_intra_extension() + + new_subject = self.create_user() + new_subjects = dict() + new_subjects[new_subject["id"]] = new_subject["name"] + subjects = self.manager.set_subject_dict("admin", self.ref["id"], new_subjects) + + new_subject_category_uuid = uuid.uuid4().hex + new_subject_category_value = "role" + subject_categories = self.manager.set_subject_category_dict( + "admin", + self.ref["id"], + { + new_subject_category_uuid: new_subject_category_value + } + ) + + for subject_category in subject_categories["subject_categories"]: + subject_category_scope = self.manager.get_subject_category_scope_dict( + "admin", + self.ref["id"], + subject_category) + self.assertIsInstance(subject_category_scope, dict) + self.assertIn("subject_category_scope", subject_category_scope) + self.assertIn("id", subject_category_scope) + self.assertIn("intra_extension_uuid", subject_category_scope) + self.assertEqual(self.ref["id"], subject_category_scope["intra_extension_uuid"]) + self.assertIsInstance(subject_category_scope["subject_category_scope"], dict) + + new_subject_category_scope = dict() + new_subject_category_scope_uuid = uuid.uuid4().hex + new_subject_category_scope[new_subject_category_scope_uuid] = "admin" + subject_category_scope = self.manager.set_subject_category_scope_dict( + "admin", + self.ref["id"], + subject_category, + new_subject_category_scope) + self.assertIsInstance(subject_category_scope, dict) + self.assertIn("subject_category_scope", subject_category_scope) + self.assertIn("id", subject_category_scope) + self.assertIn("intra_extension_uuid", subject_category_scope) + self.assertEqual(self.ref["id"], subject_category_scope["intra_extension_uuid"]) + self.assertIn(new_subject_category_scope[new_subject_category_scope_uuid], + subject_category_scope["subject_category_scope"][subject_category].values()) + + new_subject_category_scope2 = dict() + new_subject_category_scope2_uuid = uuid.uuid4().hex + new_subject_category_scope2[new_subject_category_scope2_uuid] = "dev" + subject_category_scope = self.manager.set_subject_category_scope_dict( + "admin", + self.ref["id"], + subject_category, + new_subject_category_scope2) + self.assertIsInstance(subject_category_scope, dict) + self.assertIn("subject_category_scope", subject_category_scope) + self.assertIn("id", subject_category_scope) + self.assertIn("intra_extension_uuid", subject_category_scope) + self.assertEqual(self.ref["id"], subject_category_scope["intra_extension_uuid"]) + self.assertIn(new_subject_category_scope2[new_subject_category_scope2_uuid], + subject_category_scope["subject_category_scope"][subject_category].values()) + + subject_category_assignments = self.manager.get_subject_category_assignment_dict( + "admin", + self.ref["id"], + new_subject["id"] + ) + self.assertIsInstance(subject_category_assignments, dict) + self.assertIn("subject_category_assignments", subject_category_assignments) + self.assertIn("id", subject_category_assignments) + self.assertIn("intra_extension_uuid", subject_category_assignments) + self.assertEqual(self.ref["id"], subject_category_assignments["intra_extension_uuid"]) + self.assertEqual({}, subject_category_assignments["subject_category_assignments"][new_subject["id"]]) + + subject_category_assignments = self.manager.set_subject_category_assignment_dict( + "admin", + self.ref["id"], + new_subject["id"], + { + new_subject_category_uuid: [new_subject_category_scope_uuid, new_subject_category_scope2_uuid], + } + ) + self.assertIsInstance(subject_category_assignments, dict) + self.assertIn("subject_category_assignments", subject_category_assignments) + self.assertIn("id", subject_category_assignments) + self.assertIn("intra_extension_uuid", subject_category_assignments) + self.assertEqual(self.ref["id"], subject_category_assignments["intra_extension_uuid"]) + self.assertEqual( + {new_subject_category_uuid: [new_subject_category_scope_uuid, new_subject_category_scope2_uuid]}, + subject_category_assignments["subject_category_assignments"][new_subject["id"]]) + subject_category_assignments = self.manager.get_subject_category_assignment_dict( + "admin", + self.ref["id"], + new_subject["id"] + ) + self.assertIsInstance(subject_category_assignments, dict) + self.assertIn("subject_category_assignments", subject_category_assignments) + self.assertIn("id", subject_category_assignments) + self.assertIn("intra_extension_uuid", subject_category_assignments) + self.assertEqual(self.ref["id"], subject_category_assignments["intra_extension_uuid"]) + self.assertEqual( + {new_subject_category_uuid: [new_subject_category_scope_uuid, new_subject_category_scope2_uuid]}, + subject_category_assignments["subject_category_assignments"][new_subject["id"]]) + + self.manager.del_subject_category_assignment( + "admin", + self.ref["id"], + new_subject["id"], + new_subject_category_uuid, + new_subject_category_scope_uuid + ) + subject_category_assignments = self.manager.get_subject_category_assignment_dict( + "admin", + self.ref["id"], + new_subject["id"] + ) + self.assertIsInstance(subject_category_assignments, dict) + self.assertIn("subject_category_assignments", subject_category_assignments) + self.assertIn("id", subject_category_assignments) + self.assertIn("intra_extension_uuid", subject_category_assignments) + self.assertEqual(self.ref["id"], subject_category_assignments["intra_extension_uuid"]) + self.assertEqual( + {new_subject_category_uuid: [new_subject_category_scope2_uuid, ]}, + subject_category_assignments["subject_category_assignments"][new_subject["id"]]) + + data = self.manager.add_subject_category_assignment_dict( + "admin", + self.ref["id"], + new_subject["id"], + new_subject_category_uuid, + new_subject_category_scope_uuid + ) + + subject_category_assignments = self.manager.get_subject_category_assignment_dict( + "admin", + self.ref["id"], + new_subject["id"] + ) + self.assertIsInstance(subject_category_assignments, dict) + self.assertIn("subject_category_assignments", subject_category_assignments) + self.assertIn("id", subject_category_assignments) + self.assertIn("intra_extension_uuid", subject_category_assignments) + self.assertEqual(self.ref["id"], subject_category_assignments["intra_extension_uuid"]) + self.assertEqual( + {new_subject_category_uuid: [new_subject_category_scope2_uuid, new_subject_category_scope_uuid]}, + subject_category_assignments["subject_category_assignments"][new_subject["id"]]) + + def test_object_category_assignment(self): + self.create_intra_extension() + + new_object = self.create_user() + new_objects = dict() + new_objects[new_object["id"]] = new_object["name"] + objects = self.manager.set_object_dict("admin", self.ref["id"], new_objects) + + new_object_category_uuid = uuid.uuid4().hex + new_object_category_value = "role" + object_categories = self.manager.set_object_category_dict( + "admin", + self.ref["id"], + { + new_object_category_uuid: new_object_category_value + } + ) + + for object_category in object_categories["object_categories"]: + object_category_scope = self.manager.get_object_category_scope_dict( + "admin", + self.ref["id"], + object_category) + self.assertIsInstance(object_category_scope, dict) + self.assertIn("object_category_scope", object_category_scope) + self.assertIn("id", object_category_scope) + self.assertIn("intra_extension_uuid", object_category_scope) + self.assertEqual(self.ref["id"], object_category_scope["intra_extension_uuid"]) + self.assertIsInstance(object_category_scope["object_category_scope"], dict) + + new_object_category_scope = dict() + new_object_category_scope_uuid = uuid.uuid4().hex + new_object_category_scope[new_object_category_scope_uuid] = "admin" + object_category_scope = self.manager.set_object_category_scope_dict( + "admin", + self.ref["id"], + object_category, + new_object_category_scope) + self.assertIsInstance(object_category_scope, dict) + self.assertIn("object_category_scope", object_category_scope) + self.assertIn("id", object_category_scope) + self.assertIn("intra_extension_uuid", object_category_scope) + self.assertEqual(self.ref["id"], object_category_scope["intra_extension_uuid"]) + self.assertIn(new_object_category_scope[new_object_category_scope_uuid], + object_category_scope["object_category_scope"][object_category].values()) + + new_object_category_scope2 = dict() + new_object_category_scope2_uuid = uuid.uuid4().hex + new_object_category_scope2[new_object_category_scope2_uuid] = "dev" + object_category_scope = self.manager.set_object_category_scope_dict( + "admin", + self.ref["id"], + object_category, + new_object_category_scope2) + self.assertIsInstance(object_category_scope, dict) + self.assertIn("object_category_scope", object_category_scope) + self.assertIn("id", object_category_scope) + self.assertIn("intra_extension_uuid", object_category_scope) + self.assertEqual(self.ref["id"], object_category_scope["intra_extension_uuid"]) + self.assertIn(new_object_category_scope2[new_object_category_scope2_uuid], + object_category_scope["object_category_scope"][object_category].values()) + + object_category_assignments = self.manager.get_object_category_assignment_dict( + "admin", + self.ref["id"], + new_object["id"] + ) + self.assertIsInstance(object_category_assignments, dict) + self.assertIn("object_category_assignments", object_category_assignments) + self.assertIn("id", object_category_assignments) + self.assertIn("intra_extension_uuid", object_category_assignments) + self.assertEqual(self.ref["id"], object_category_assignments["intra_extension_uuid"]) + self.assertEqual({}, object_category_assignments["object_category_assignments"][new_object["id"]]) + + object_category_assignments = self.manager.set_object_category_assignment_dict( + "admin", + self.ref["id"], + new_object["id"], + { + new_object_category_uuid: [new_object_category_scope_uuid, new_object_category_scope2_uuid], + } + ) + self.assertIsInstance(object_category_assignments, dict) + self.assertIn("object_category_assignments", object_category_assignments) + self.assertIn("id", object_category_assignments) + self.assertIn("intra_extension_uuid", object_category_assignments) + self.assertEqual(self.ref["id"], object_category_assignments["intra_extension_uuid"]) + self.assertEqual( + {new_object_category_uuid: [new_object_category_scope_uuid, new_object_category_scope2_uuid]}, + object_category_assignments["object_category_assignments"][new_object["id"]]) + object_category_assignments = self.manager.get_object_category_assignment_dict( + "admin", + self.ref["id"], + new_object["id"] + ) + self.assertIsInstance(object_category_assignments, dict) + self.assertIn("object_category_assignments", object_category_assignments) + self.assertIn("id", object_category_assignments) + self.assertIn("intra_extension_uuid", object_category_assignments) + self.assertEqual(self.ref["id"], object_category_assignments["intra_extension_uuid"]) + self.assertEqual( + {new_object_category_uuid: [new_object_category_scope_uuid, new_object_category_scope2_uuid]}, + object_category_assignments["object_category_assignments"][new_object["id"]]) + + self.manager.del_object_category_assignment( + "admin", + self.ref["id"], + new_object["id"], + new_object_category_uuid, + new_object_category_scope_uuid + ) + object_category_assignments = self.manager.get_object_category_assignment_dict( + "admin", + self.ref["id"], + new_object["id"] + ) + self.assertIsInstance(object_category_assignments, dict) + self.assertIn("object_category_assignments", object_category_assignments) + self.assertIn("id", object_category_assignments) + self.assertIn("intra_extension_uuid", object_category_assignments) + self.assertEqual(self.ref["id"], object_category_assignments["intra_extension_uuid"]) + self.assertEqual( + {new_object_category_uuid: [new_object_category_scope2_uuid, ]}, + object_category_assignments["object_category_assignments"][new_object["id"]]) + + self.manager.add_object_category_assignment_dict( + "admin", + self.ref["id"], + new_object["id"], + new_object_category_uuid, + new_object_category_scope_uuid + ) + + object_category_assignments = self.manager.get_object_category_assignment_dict( + "admin", + self.ref["id"], + new_object["id"] + ) + self.assertIsInstance(object_category_assignments, dict) + self.assertIn("object_category_assignments", object_category_assignments) + self.assertIn("id", object_category_assignments) + self.assertIn("intra_extension_uuid", object_category_assignments) + self.assertEqual(self.ref["id"], object_category_assignments["intra_extension_uuid"]) + self.assertEqual( + {new_object_category_uuid: [new_object_category_scope2_uuid, new_object_category_scope_uuid]}, + object_category_assignments["object_category_assignments"][new_object["id"]]) + + def test_action_category_assignment(self): + self.create_intra_extension() + + new_action = self.create_user() + new_actions = dict() + new_actions[new_action["id"]] = new_action["name"] + actions = self.manager.set_action_dict("admin", self.ref["id"], new_actions) + + new_action_category_uuid = uuid.uuid4().hex + new_action_category_value = "role" + action_categories = self.manager.set_action_category_dict( + "admin", + self.ref["id"], + { + new_action_category_uuid: new_action_category_value + } + ) + + for action_category in action_categories["action_categories"]: + action_category_scope = self.manager.get_action_category_scope_dict( + "admin", + self.ref["id"], + action_category) + self.assertIsInstance(action_category_scope, dict) + self.assertIn("action_category_scope", action_category_scope) + self.assertIn("id", action_category_scope) + self.assertIn("intra_extension_uuid", action_category_scope) + self.assertEqual(self.ref["id"], action_category_scope["intra_extension_uuid"]) + self.assertIsInstance(action_category_scope["action_category_scope"], dict) + + new_action_category_scope = dict() + new_action_category_scope_uuid = uuid.uuid4().hex + new_action_category_scope[new_action_category_scope_uuid] = "admin" + action_category_scope = self.manager.set_action_category_scope_dict( + "admin", + self.ref["id"], + action_category, + new_action_category_scope) + self.assertIsInstance(action_category_scope, dict) + self.assertIn("action_category_scope", action_category_scope) + self.assertIn("id", action_category_scope) + self.assertIn("intra_extension_uuid", action_category_scope) + self.assertEqual(self.ref["id"], action_category_scope["intra_extension_uuid"]) + self.assertIn(new_action_category_scope[new_action_category_scope_uuid], + action_category_scope["action_category_scope"][action_category].values()) + + new_action_category_scope2 = dict() + new_action_category_scope2_uuid = uuid.uuid4().hex + new_action_category_scope2[new_action_category_scope2_uuid] = "dev" + action_category_scope = self.manager.set_action_category_scope_dict( + "admin", + self.ref["id"], + action_category, + new_action_category_scope2) + self.assertIsInstance(action_category_scope, dict) + self.assertIn("action_category_scope", action_category_scope) + self.assertIn("id", action_category_scope) + self.assertIn("intra_extension_uuid", action_category_scope) + self.assertEqual(self.ref["id"], action_category_scope["intra_extension_uuid"]) + self.assertIn(new_action_category_scope2[new_action_category_scope2_uuid], + action_category_scope["action_category_scope"][action_category].values()) + + action_category_assignments = self.manager.get_action_category_assignment_dict( + "admin", + self.ref["id"], + new_action["id"] + ) + self.assertIsInstance(action_category_assignments, dict) + self.assertIn("action_category_assignments", action_category_assignments) + self.assertIn("id", action_category_assignments) + self.assertIn("intra_extension_uuid", action_category_assignments) + self.assertEqual(self.ref["id"], action_category_assignments["intra_extension_uuid"]) + self.assertEqual({}, action_category_assignments["action_category_assignments"][new_action["id"]]) + + action_category_assignments = self.manager.set_action_category_assignment_dict( + "admin", + self.ref["id"], + new_action["id"], + { + new_action_category_uuid: [new_action_category_scope_uuid, new_action_category_scope2_uuid], + } + ) + self.assertIsInstance(action_category_assignments, dict) + self.assertIn("action_category_assignments", action_category_assignments) + self.assertIn("id", action_category_assignments) + self.assertIn("intra_extension_uuid", action_category_assignments) + self.assertEqual(self.ref["id"], action_category_assignments["intra_extension_uuid"]) + self.assertEqual( + {new_action_category_uuid: [new_action_category_scope_uuid, new_action_category_scope2_uuid]}, + action_category_assignments["action_category_assignments"][new_action["id"]]) + action_category_assignments = self.manager.get_action_category_assignment_dict( + "admin", + self.ref["id"], + new_action["id"] + ) + self.assertIsInstance(action_category_assignments, dict) + self.assertIn("action_category_assignments", action_category_assignments) + self.assertIn("id", action_category_assignments) + self.assertIn("intra_extension_uuid", action_category_assignments) + self.assertEqual(self.ref["id"], action_category_assignments["intra_extension_uuid"]) + self.assertEqual( + {new_action_category_uuid: [new_action_category_scope_uuid, new_action_category_scope2_uuid]}, + action_category_assignments["action_category_assignments"][new_action["id"]]) + + self.manager.del_action_category_assignment( + "admin", + self.ref["id"], + new_action["id"], + new_action_category_uuid, + new_action_category_scope_uuid + ) + action_category_assignments = self.manager.get_action_category_assignment_dict( + "admin", + self.ref["id"], + new_action["id"] + ) + self.assertIsInstance(action_category_assignments, dict) + self.assertIn("action_category_assignments", action_category_assignments) + self.assertIn("id", action_category_assignments) + self.assertIn("intra_extension_uuid", action_category_assignments) + self.assertEqual(self.ref["id"], action_category_assignments["intra_extension_uuid"]) + self.assertEqual( + {new_action_category_uuid: [new_action_category_scope2_uuid, ]}, + action_category_assignments["action_category_assignments"][new_action["id"]]) + + self.manager.add_action_category_assignment_dict( + "admin", + self.ref["id"], + new_action["id"], + new_action_category_uuid, + new_action_category_scope_uuid + ) + + action_category_assignments = self.manager.get_action_category_assignment_dict( + "admin", + self.ref["id"], + new_action["id"] + ) + self.assertIsInstance(action_category_assignments, dict) + self.assertIn("action_category_assignments", action_category_assignments) + self.assertIn("id", action_category_assignments) + self.assertIn("intra_extension_uuid", action_category_assignments) + self.assertEqual(self.ref["id"], action_category_assignments["intra_extension_uuid"]) + self.assertEqual( + {new_action_category_uuid: [new_action_category_scope2_uuid, new_action_category_scope_uuid]}, + action_category_assignments["action_category_assignments"][new_action["id"]]) + + def test_sub_meta_rules(self): + self.create_intra_extension() + + aggregation_algorithms = self.manager.get_aggregation_algorithms("admin", self.ref["id"]) + self.assertIsInstance(aggregation_algorithms, dict) + self.assertIsInstance(aggregation_algorithms["aggregation_algorithms"], list) + self.assertIn("and_true_aggregation", aggregation_algorithms["aggregation_algorithms"]) + self.assertIn("test_aggregation", aggregation_algorithms["aggregation_algorithms"]) + + aggregation_algorithm = self.manager.get_aggregation_algorithm("admin", self.ref["id"]) + self.assertIsInstance(aggregation_algorithm, dict) + self.assertIn("aggregation", aggregation_algorithm) + self.assertIn(aggregation_algorithm["aggregation"], aggregation_algorithms["aggregation_algorithms"]) + + _aggregation_algorithm = list(aggregation_algorithms["aggregation_algorithms"]) + _aggregation_algorithm.remove(aggregation_algorithm["aggregation"]) + aggregation_algorithm = self.manager.set_aggregation_algorithm("admin", self.ref["id"], _aggregation_algorithm[0]) + self.assertIsInstance(aggregation_algorithm, dict) + self.assertIn("aggregation", aggregation_algorithm) + self.assertIn(aggregation_algorithm["aggregation"], aggregation_algorithms["aggregation_algorithms"]) + + sub_meta_rules = self.manager.get_sub_meta_rule("admin", self.ref["id"]) + self.assertIsInstance(sub_meta_rules, dict) + self.assertIn("sub_meta_rules", sub_meta_rules) + sub_meta_rules_conf = json.load(open(os.path.join(self.policy_directory, self.ref["model"], "metarule.json"))) + metarule = dict() + categories = { + "subject_categories": self.manager.get_subject_category_dict("admin", self.ref["id"]), + "object_categories": self.manager.get_object_category_dict("admin", self.ref["id"]), + "action_categories": self.manager.get_action_category_dict("admin", self.ref["id"]) + } + for relation in sub_meta_rules_conf["sub_meta_rules"]: + metarule[relation] = dict() + for item in ("subject_categories", "object_categories", "action_categories"): + metarule[relation][item] = list() + for element in sub_meta_rules_conf["sub_meta_rules"][relation][item]: + metarule[relation][item].append(self.__get_key_from_value( + element, + categories[item][item] + )) + + for relation in sub_meta_rules["sub_meta_rules"]: + self.assertIn(relation, metarule) + for item in ("subject_categories", "object_categories", "action_categories"): + self.assertEqual( + sub_meta_rules["sub_meta_rules"][relation][item], + metarule[relation][item] + ) + + new_subject_category = {"id": uuid.uuid4().hex, "name": "subject_category_test"} + # Add a particular subject_category + data = self.manager.add_subject_category_dict( + "admin", + self.ref["id"], + new_subject_category["name"]) + new_subject_category["id"] = data["subject_category"]["uuid"] + subject_categories = self.manager.get_subject_category_dict( + "admin", + self.ref["id"]) + self.assertIsInstance(subject_categories, dict) + self.assertIn("subject_categories", subject_categories) + self.assertIn("id", subject_categories) + self.assertIn("intra_extension_uuid", subject_categories) + self.assertEqual(self.ref["id"], subject_categories["intra_extension_uuid"]) + self.assertIn(new_subject_category["id"], subject_categories["subject_categories"]) + metarule[relation]["subject_categories"].append(new_subject_category["id"]) + _sub_meta_rules = self.manager.set_sub_meta_rule("admin", self.ref["id"], metarule) + self.assertIn(relation, metarule) + for item in ("subject_categories", "object_categories", "action_categories"): + self.assertEqual( + _sub_meta_rules["sub_meta_rules"][relation][item], + metarule[relation][item] + ) + + def test_sub_rules(self): + self.create_intra_extension() + + sub_meta_rules = self.manager.get_sub_meta_rule("admin", self.ref["id"]) + self.assertIsInstance(sub_meta_rules, dict) + self.assertIn("sub_meta_rules", sub_meta_rules) + + sub_rules = self.manager.get_sub_rules("admin", self.ref["id"]) + self.assertIsInstance(sub_rules, dict) + self.assertIn("rules", sub_rules) + rules = dict() + for relation in sub_rules["rules"]: + self.assertIn(relation, self.manager.get_sub_meta_rule_relations("admin", self.ref["id"])["sub_meta_rule_relations"]) + rules[relation] = list() + for rule in sub_rules["rules"][relation]: + print(rule) + for cat, cat_func, func_name in ( + ("subject_categories", self.manager.get_subject_category_scope_dict, "subject_category_scope"), + ("action_categories", self.manager.get_action_category_scope_dict, "action_category_scope"), + ("object_categories", self.manager.get_object_category_scope_dict, "object_category_scope"), + ): + for cat_value in sub_meta_rules["sub_meta_rules"][relation][cat]: + scope = cat_func( + "admin", + self.ref["id"], + cat_value + ) + a_scope = rule.pop(0) + print(a_scope) + if type(a_scope) is not bool: + self.assertIn(a_scope, scope[func_name][cat_value]) + + # add a new subrule + + relation = sub_rules["rules"].keys()[0] + sub_rule = [] + for cat, cat_func, func_name in ( + ("subject_categories", self.manager.get_subject_category_scope_dict, "subject_category_scope"), + ("action_categories", self.manager.get_action_category_scope_dict, "action_category_scope"), + ("object_categories", self.manager.get_object_category_scope_dict, "object_category_scope"), + ): + for cat_value in sub_meta_rules["sub_meta_rules"][relation][cat]: + scope = cat_func( + "admin", + self.ref["id"], + cat_value + ) + sub_rule.append(scope[func_name][cat_value].keys()[0]) + + sub_rule.append(True) + sub_rules = self.manager.set_sub_rule("admin", self.ref["id"], relation, sub_rule) + self.assertIsInstance(sub_rules, dict) + self.assertIn("rules", sub_rules) + rules = dict() + self.assertIn(sub_rule, sub_rules["rules"][relation]) + for relation in sub_rules["rules"]: + self.assertIn(relation, self.manager.get_sub_meta_rule_relations("admin", self.ref["id"])["sub_meta_rule_relations"]) + rules[relation] = list() + for rule in sub_rules["rules"][relation]: + for cat, cat_func, func_name in ( + ("subject_categories", self.manager.get_subject_category_scope_dict, "subject_category_scope"), + ("action_categories", self.manager.get_action_category_scope_dict, "action_category_scope"), + ("object_categories", self.manager.get_object_category_scope_dict, "object_category_scope"), + ): + for cat_value in sub_meta_rules["sub_meta_rules"][relation][cat]: + scope = cat_func( + "admin", + self.ref["id"], + cat_value + ) + a_scope = rule.pop(0) + self.assertIn(a_scope, scope[func_name][cat_value]) + + + + diff --git a/keystone-moon/keystone/tests/moon/unit/test_unit_core_intra_extension_authz.py b/keystone-moon/keystone/tests/moon/unit/test_unit_core_intra_extension_authz.py new file mode 100644 index 00000000..d08ecf39 --- /dev/null +++ b/keystone-moon/keystone/tests/moon/unit/test_unit_core_intra_extension_authz.py @@ -0,0 +1,861 @@ +# Copyright 2015 Open Platform for NFV Project, Inc. and its contributors +# This software is distributed under the terms and conditions of the 'Apache-2.0' +# license which can be found in the file 'LICENSE' in this package distribution +# or at 'http://www.apache.org/licenses/LICENSE-2.0'. + +"""Unit tests for core IntraExtensionAuthzManager""" + +import json +import os +import uuid +from oslo_config import cfg +from keystone.tests import unit as tests +from keystone.contrib.moon.core import IntraExtensionAdminManager, IntraExtensionAuthzManager +from keystone.tests.unit.ksfixtures import database +from keystone import resource +from keystone.contrib.moon.exception import * +from keystone.tests.unit import default_fixtures +from keystone.contrib.moon.core import LogManager, TenantManager + +CONF = cfg.CONF + +USER_ADMIN = { + 'name': 'admin', + 'domain_id': "default", + 'password': 'admin' +} + +IE = { + "name": "test IE", + "policymodel": "policy_rbac_authz", + "description": "a simple description." +} + +class TestIntraExtensionAuthzManagerAuthz(tests.TestCase): + + def setUp(self): + self.useFixture(database.Database()) + super(TestIntraExtensionAuthzManager, self).setUp() + self.load_backends() + self.load_fixtures(default_fixtures) + self.manager = IntraExtensionAuthzManager() + self.admin_manager = IntraExtensionAdminManager() + + def __get_key_from_value(self, value, values_dict): + return filter(lambda v: v[1] == value, values_dict.iteritems())[0][0] + + def load_extra_backends(self): + return { + "moonlog_api": LogManager(), + "tenant_api": TenantManager(), + # "resource_api": resource.Manager(), + } + + def config_overrides(self): + super(TestIntraExtensionAuthzManager, self).config_overrides() + self.policy_directory = '../../../examples/moon/policies' + self.config_fixture.config( + group='moon', + intraextension_driver='keystone.contrib.moon.backends.sql.IntraExtensionConnector') + self.config_fixture.config( + group='moon', + policy_directory=self.policy_directory) + + +class TestIntraExtensionAuthzManager(tests.TestCase): + + def setUp(self): + self.useFixture(database.Database()) + super(TestIntraExtensionAuthzManager, self).setUp() + self.load_backends() + self.load_fixtures(default_fixtures) + self.manager = IntraExtensionAuthzManager() + self.admin_manager = IntraExtensionAdminManager() + + def __get_key_from_value(self, value, values_dict): + return filter(lambda v: v[1] == value, values_dict.iteritems())[0][0] + + def load_extra_backends(self): + return { + "moonlog_api": LogManager(), + "tenant_api": TenantManager(), + # "resource_api": resource.Manager(), + } + + def config_overrides(self): + super(TestIntraExtensionAuthzManager, self).config_overrides() + self.policy_directory = '../../../examples/moon/policies' + self.config_fixture.config( + group='moon', + intraextension_driver='keystone.contrib.moon.backends.sql.IntraExtensionConnector') + self.config_fixture.config( + group='moon', + policy_directory=self.policy_directory) + + def create_intra_extension(self, policy_model="policy_rbac_authz"): + # Create the admin user because IntraExtension needs it + self.admin = self.identity_api.create_user(USER_ADMIN) + IE["policymodel"] = policy_model + self.ref = self.admin_manager.load_intra_extension(IE) + self.assertIsInstance(self.ref, dict) + self.create_tenant(self.ref["id"]) + + def create_tenant(self, authz_uuid): + tenant = { + "id": uuid.uuid4().hex, + "name": "TestIntraExtensionAuthzManager", + "enabled": True, + "description": "", + "domain_id": "default" + } + project = self.resource_api.create_project(tenant["id"], tenant) + mapping = self.tenant_api.set_tenant_dict(project["id"], project["name"], authz_uuid, None) + self.assertIsInstance(mapping, dict) + self.assertIn("authz", mapping) + self.assertEqual(mapping["authz"], authz_uuid) + return mapping + + def create_user(self, username="TestIntraExtensionAuthzManagerUser"): + user = { + "id": uuid.uuid4().hex, + "name": username, + "enabled": True, + "description": "", + "domain_id": "default" + } + _user = self.identity_api.create_user(user) + return _user + + def delete_admin_intra_extension(self): + self.assertRaises( + AuthIntraExtensionModificationNotAuthorized, + self.manager.delete_intra_extension, + self.ref["id"]) + + def test_subjects(self): + self.create_intra_extension() + + subjects = self.manager.get_subject_dict("admin", self.ref["id"]) + self.assertIsInstance(subjects, dict) + self.assertIn("subjects", subjects) + self.assertIn("id", subjects) + self.assertIn("intra_extension_uuid", subjects) + self.assertEqual(self.ref["id"], subjects["intra_extension_uuid"]) + self.assertIsInstance(subjects["subjects"], dict) + + new_subject = self.create_user() + new_subjects = dict() + new_subjects[new_subject["id"]] = new_subject["name"] + self.assertRaises( + AuthIntraExtensionModificationNotAuthorized, + self.manager.set_subject_dict, + "admin", self.ref["id"], new_subjects) + + # Delete the new subject + self.assertRaises( + AuthIntraExtensionModificationNotAuthorized, + self.manager.del_subject, + "admin", self.ref["id"], new_subject["id"]) + + # Add a particular subject + self.assertRaises( + AuthIntraExtensionModificationNotAuthorized, + self.manager.add_subject_dict, + "admin", self.ref["id"], new_subject["id"]) + + def test_objects(self): + self.create_intra_extension() + + objects = self.manager.get_object_dict("admin", self.ref["id"]) + self.assertIsInstance(objects, dict) + self.assertIn("objects", objects) + self.assertIn("id", objects) + self.assertIn("intra_extension_uuid", objects) + self.assertEqual(self.ref["id"], objects["intra_extension_uuid"]) + self.assertIsInstance(objects["objects"], dict) + + new_object = self.create_user() + new_objects = dict() + new_objects[new_object["id"]] = new_object["name"] + self.assertRaises( + AuthIntraExtensionModificationNotAuthorized, + self.manager.set_object_dict, + "admin", self.ref["id"], new_object["id"]) + + # Delete the new object + self.assertRaises( + AuthIntraExtensionModificationNotAuthorized, + self.manager.del_object, + "admin", self.ref["id"], new_object["id"]) + + # Add a particular object + self.assertRaises( + AuthIntraExtensionModificationNotAuthorized, + self.manager.add_object_dict, + "admin", self.ref["id"], new_object["name"]) + + def test_actions(self): + self.create_intra_extension() + + actions = self.manager.get_action_dict("admin", self.ref["id"]) + self.assertIsInstance(actions, dict) + self.assertIn("actions", actions) + self.assertIn("id", actions) + self.assertIn("intra_extension_uuid", actions) + self.assertEqual(self.ref["id"], actions["intra_extension_uuid"]) + self.assertIsInstance(actions["actions"], dict) + + new_action = self.create_user() + new_actions = dict() + new_actions[new_action["id"]] = new_action["name"] + self.assertRaises( + AuthIntraExtensionModificationNotAuthorized, + self.manager.set_action_dict, + "admin", self.ref["id"], new_actions) + + # Delete the new action + self.assertRaises( + AuthIntraExtensionModificationNotAuthorized, + self.manager.del_action, + "admin", self.ref["id"], new_action["id"]) + + # Add a particular action + self.assertRaises( + AuthIntraExtensionModificationNotAuthorized, + self.manager.add_action_dict, + "admin", self.ref["id"], new_action["id"]) + + def test_subject_categories(self): + self.create_intra_extension() + + subject_categories = self.manager.get_subject_category_dict("admin", self.ref["id"]) + self.assertIsInstance(subject_categories, dict) + self.assertIn("subject_categories", subject_categories) + self.assertIn("id", subject_categories) + self.assertIn("intra_extension_uuid", subject_categories) + self.assertEqual(self.ref["id"], subject_categories["intra_extension_uuid"]) + self.assertIsInstance(subject_categories["subject_categories"], dict) + + new_subject_category = {"id": uuid.uuid4().hex, "name": "subject_category_test"} + new_subject_categories = dict() + new_subject_categories[new_subject_category["id"]] = new_subject_category["name"] + self.assertRaises( + AuthIntraExtensionModificationNotAuthorized, + self.manager.set_subject_category_dict, + "admin", self.ref["id"], new_subject_categories) + + # Delete the new subject_category + self.assertRaises( + AuthIntraExtensionModificationNotAuthorized, + self.manager.del_subject_category, + "admin", self.ref["id"], new_subject_category["id"]) + + # Add a particular subject_category + self.assertRaises( + AuthIntraExtensionModificationNotAuthorized, + self.manager.add_subject_category_dict, + "admin", self.ref["id"], new_subject_category["name"]) + + def test_object_categories(self): + self.create_intra_extension() + + object_categories = self.manager.get_object_category_dict("admin", self.ref["id"]) + self.assertIsInstance(object_categories, dict) + self.assertIn("object_categories", object_categories) + self.assertIn("id", object_categories) + self.assertIn("intra_extension_uuid", object_categories) + self.assertEqual(self.ref["id"], object_categories["intra_extension_uuid"]) + self.assertIsInstance(object_categories["object_categories"], dict) + + new_object_category = {"id": uuid.uuid4().hex, "name": "object_category_test"} + new_object_categories = dict() + new_object_categories[new_object_category["id"]] = new_object_category["name"] + self.assertRaises( + AuthIntraExtensionModificationNotAuthorized, + self.manager.set_object_category_dict, + "admin", self.ref["id"], new_object_categories) + + # Delete the new object_category + self.assertRaises( + AuthIntraExtensionModificationNotAuthorized, + self.manager.del_object_category, + "admin", self.ref["id"], new_object_category["id"]) + + # Add a particular object_category + self.assertRaises( + AuthIntraExtensionModificationNotAuthorized, + self.manager.add_object_category_dict, + "admin", self.ref["id"], new_object_category["name"]) + + def test_action_categories(self): + self.create_intra_extension() + + action_categories = self.manager.get_action_category_dict("admin", self.ref["id"]) + self.assertIsInstance(action_categories, dict) + self.assertIn("action_categories", action_categories) + self.assertIn("id", action_categories) + self.assertIn("intra_extension_uuid", action_categories) + self.assertEqual(self.ref["id"], action_categories["intra_extension_uuid"]) + self.assertIsInstance(action_categories["action_categories"], dict) + + new_action_category = {"id": uuid.uuid4().hex, "name": "action_category_test"} + new_action_categories = dict() + new_action_categories[new_action_category["id"]] = new_action_category["name"] + self.assertRaises( + AuthIntraExtensionModificationNotAuthorized, + self.manager.set_action_category_dict, + "admin", self.ref["id"], new_action_categories) + + # Delete the new action_category + self.assertRaises( + AuthIntraExtensionModificationNotAuthorized, + self.manager.del_action_category, + "admin", self.ref["id"], new_action_category["id"]) + + # Add a particular action_category + self.assertRaises( + AuthIntraExtensionModificationNotAuthorized, + self.manager.add_action_category_dict, + "admin", self.ref["id"], new_action_category["name"]) + + def test_subject_category_scope(self): + self.create_intra_extension() + + subject_categories = self.admin_manager.set_subject_category_dict( + "admin", + self.ref["id"], + { + uuid.uuid4().hex: "admin", + uuid.uuid4().hex: "dev", + } + ) + + for subject_category in subject_categories["subject_categories"]: + subject_category_scope = self.manager.get_subject_category_scope_dict( + "admin", + self.ref["id"], + subject_category) + self.assertIsInstance(subject_category_scope, dict) + self.assertIn("subject_category_scope", subject_category_scope) + self.assertIn("id", subject_category_scope) + self.assertIn("intra_extension_uuid", subject_category_scope) + self.assertEqual(self.ref["id"], subject_category_scope["intra_extension_uuid"]) + self.assertIsInstance(subject_category_scope["subject_category_scope"], dict) + + new_subject_category_scope = dict() + new_subject_category_scope_uuid = uuid.uuid4().hex + new_subject_category_scope[new_subject_category_scope_uuid] = "new_subject_category_scope" + self.assertRaises( + AuthIntraExtensionModificationNotAuthorized, + self.manager.set_subject_category_scope_dict, + "admin", self.ref["id"], subject_category, new_subject_category_scope) + + # Delete the new subject_category_scope + self.assertRaises( + AuthIntraExtensionModificationNotAuthorized, + self.manager.del_subject_category_scope, + "admin", self.ref["id"], subject_category, new_subject_category_scope_uuid) + + # Add a particular subject_category_scope + self.assertRaises( + AuthIntraExtensionModificationNotAuthorized, + self.manager.add_subject_category_scope_dict, + "admin", self.ref["id"], subject_category, new_subject_category_scope[new_subject_category_scope_uuid]) + + def test_object_category_scope(self): + self.create_intra_extension() + + object_categories = self.admin_manager.set_object_category_dict( + "admin", + self.ref["id"], + { + uuid.uuid4().hex: "id", + uuid.uuid4().hex: "domain", + } + ) + + for object_category in object_categories["object_categories"]: + object_category_scope = self.manager.get_object_category_scope_dict( + "admin", + self.ref["id"], + object_category) + self.assertIsInstance(object_category_scope, dict) + self.assertIn("object_category_scope", object_category_scope) + self.assertIn("id", object_category_scope) + self.assertIn("intra_extension_uuid", object_category_scope) + self.assertEqual(self.ref["id"], object_category_scope["intra_extension_uuid"]) + self.assertIsInstance(object_category_scope["object_category_scope"], dict) + + new_object_category_scope = dict() + new_object_category_scope_uuid = uuid.uuid4().hex + new_object_category_scope[new_object_category_scope_uuid] = "new_object_category_scope" + self.assertRaises( + AuthIntraExtensionModificationNotAuthorized, + self.manager.set_object_category_scope_dict, + "admin", self.ref["id"], object_category, new_object_category_scope) + + # Delete the new object_category_scope + self.assertRaises( + AuthIntraExtensionModificationNotAuthorized, + self.manager.del_object_category_scope, + "admin", self.ref["id"], object_category, new_object_category_scope_uuid) + + # Add a particular object_category_scope + self.assertRaises( + AuthIntraExtensionModificationNotAuthorized, + self.manager.add_object_category_scope_dict, + "admin", self.ref["id"], object_category, new_object_category_scope[new_object_category_scope_uuid]) + + def test_action_category_scope(self): + self.create_intra_extension() + + action_categories = self.admin_manager.set_action_category_dict( + "admin", + self.ref["id"], + { + uuid.uuid4().hex: "compute", + uuid.uuid4().hex: "identity", + } + ) + + for action_category in action_categories["action_categories"]: + action_category_scope = self.manager.get_action_category_scope_dict( + "admin", + self.ref["id"], + action_category) + self.assertIsInstance(action_category_scope, dict) + self.assertIn("action_category_scope", action_category_scope) + self.assertIn("id", action_category_scope) + self.assertIn("intra_extension_uuid", action_category_scope) + self.assertEqual(self.ref["id"], action_category_scope["intra_extension_uuid"]) + self.assertIsInstance(action_category_scope["action_category_scope"], dict) + + new_action_category_scope = dict() + new_action_category_scope_uuid = uuid.uuid4().hex + new_action_category_scope[new_action_category_scope_uuid] = "new_action_category_scope" + self.assertRaises( + AuthIntraExtensionModificationNotAuthorized, + self.manager.set_action_category_scope_dict, + "admin", self.ref["id"], action_category, new_action_category_scope) + + # Delete the new action_category_scope + self.assertRaises( + AuthIntraExtensionModificationNotAuthorized, + self.manager.del_action_category_scope, + "admin", self.ref["id"], action_category, new_action_category_scope_uuid) + + # Add a particular action_category_scope + self.assertRaises( + AuthIntraExtensionModificationNotAuthorized, + self.manager.add_action_category_scope_dict, + "admin", self.ref["id"], action_category, new_action_category_scope[new_action_category_scope_uuid]) + + def test_subject_category_assignment(self): + self.create_intra_extension() + + new_subject = self.create_user() + new_subjects = dict() + new_subjects[new_subject["id"]] = new_subject["name"] + subjects = self.admin_manager.set_subject_dict("admin", self.ref["id"], new_subjects) + + new_subject_category_uuid = uuid.uuid4().hex + new_subject_category_value = "role" + subject_categories = self.admin_manager.set_subject_category_dict( + "admin", + self.ref["id"], + { + new_subject_category_uuid: new_subject_category_value + } + ) + + for subject_category in subject_categories["subject_categories"]: + subject_category_scope = self.admin_manager.get_subject_category_scope_dict( + "admin", + self.ref["id"], + subject_category) + self.assertIsInstance(subject_category_scope, dict) + self.assertIn("subject_category_scope", subject_category_scope) + self.assertIn("id", subject_category_scope) + self.assertIn("intra_extension_uuid", subject_category_scope) + self.assertEqual(self.ref["id"], subject_category_scope["intra_extension_uuid"]) + self.assertIsInstance(subject_category_scope["subject_category_scope"], dict) + + new_subject_category_scope = dict() + new_subject_category_scope_uuid = uuid.uuid4().hex + new_subject_category_scope[new_subject_category_scope_uuid] = "admin" + subject_category_scope = self.admin_manager.set_subject_category_scope_dict( + "admin", + self.ref["id"], + subject_category, + new_subject_category_scope) + self.assertIsInstance(subject_category_scope, dict) + self.assertIn("subject_category_scope", subject_category_scope) + self.assertIn("id", subject_category_scope) + self.assertIn("intra_extension_uuid", subject_category_scope) + self.assertEqual(self.ref["id"], subject_category_scope["intra_extension_uuid"]) + self.assertIn(new_subject_category_scope[new_subject_category_scope_uuid], + subject_category_scope["subject_category_scope"][subject_category].values()) + + new_subject_category_scope2 = dict() + new_subject_category_scope2_uuid = uuid.uuid4().hex + new_subject_category_scope2[new_subject_category_scope2_uuid] = "dev" + subject_category_scope = self.admin_manager.set_subject_category_scope_dict( + "admin", + self.ref["id"], + subject_category, + new_subject_category_scope2) + self.assertIsInstance(subject_category_scope, dict) + self.assertIn("subject_category_scope", subject_category_scope) + self.assertIn("id", subject_category_scope) + self.assertIn("intra_extension_uuid", subject_category_scope) + self.assertEqual(self.ref["id"], subject_category_scope["intra_extension_uuid"]) + self.assertIn(new_subject_category_scope2[new_subject_category_scope2_uuid], + subject_category_scope["subject_category_scope"][subject_category].values()) + + subject_category_assignments = self.manager.get_subject_category_assignment_dict( + "admin", + self.ref["id"], + new_subject["id"] + ) + self.assertIsInstance(subject_category_assignments, dict) + self.assertIn("subject_category_assignments", subject_category_assignments) + self.assertIn("id", subject_category_assignments) + self.assertIn("intra_extension_uuid", subject_category_assignments) + self.assertEqual(self.ref["id"], subject_category_assignments["intra_extension_uuid"]) + self.assertEqual({}, subject_category_assignments["subject_category_assignments"][new_subject["id"]]) + + self.assertRaises( + AuthIntraExtensionModificationNotAuthorized, + self.manager.set_subject_category_assignment_dict, + "admin", self.ref["id"], new_subject["id"], + { + new_subject_category_uuid: [new_subject_category_scope_uuid, new_subject_category_scope2_uuid], + }) + + self.assertRaises( + AuthIntraExtensionModificationNotAuthorized, + self.manager.del_subject_category_assignment, + "admin", self.ref["id"], new_subject["id"], + new_subject_category_uuid, + new_subject_category_scope_uuid) + + self.assertRaises( + AuthIntraExtensionModificationNotAuthorized, + self.manager.add_subject_category_assignment_dict, + "admin", self.ref["id"], new_subject["id"], + new_subject_category_uuid, + new_subject_category_scope_uuid) + + def test_object_category_assignment(self): + self.create_intra_extension() + + new_object = self.create_user() + new_objects = dict() + new_objects[new_object["id"]] = new_object["name"] + objects = self.admin_manager.set_object_dict("admin", self.ref["id"], new_objects) + + new_object_category_uuid = uuid.uuid4().hex + new_object_category_value = "role" + object_categories = self.admin_manager.set_object_category_dict( + "admin", + self.ref["id"], + { + new_object_category_uuid: new_object_category_value + } + ) + + for object_category in object_categories["object_categories"]: + object_category_scope = self.admin_manager.get_object_category_scope_dict( + "admin", + self.ref["id"], + object_category) + self.assertIsInstance(object_category_scope, dict) + self.assertIn("object_category_scope", object_category_scope) + self.assertIn("id", object_category_scope) + self.assertIn("intra_extension_uuid", object_category_scope) + self.assertEqual(self.ref["id"], object_category_scope["intra_extension_uuid"]) + self.assertIsInstance(object_category_scope["object_category_scope"], dict) + + new_object_category_scope = dict() + new_object_category_scope_uuid = uuid.uuid4().hex + new_object_category_scope[new_object_category_scope_uuid] = "admin" + object_category_scope = self.admin_manager.set_object_category_scope_dict( + "admin", + self.ref["id"], + object_category, + new_object_category_scope) + self.assertIsInstance(object_category_scope, dict) + self.assertIn("object_category_scope", object_category_scope) + self.assertIn("id", object_category_scope) + self.assertIn("intra_extension_uuid", object_category_scope) + self.assertEqual(self.ref["id"], object_category_scope["intra_extension_uuid"]) + self.assertIn(new_object_category_scope[new_object_category_scope_uuid], + object_category_scope["object_category_scope"][object_category].values()) + + new_object_category_scope2 = dict() + new_object_category_scope2_uuid = uuid.uuid4().hex + new_object_category_scope2[new_object_category_scope2_uuid] = "dev" + object_category_scope = self.admin_manager.set_object_category_scope_dict( + "admin", + self.ref["id"], + object_category, + new_object_category_scope2) + self.assertIsInstance(object_category_scope, dict) + self.assertIn("object_category_scope", object_category_scope) + self.assertIn("id", object_category_scope) + self.assertIn("intra_extension_uuid", object_category_scope) + self.assertEqual(self.ref["id"], object_category_scope["intra_extension_uuid"]) + self.assertIn(new_object_category_scope2[new_object_category_scope2_uuid], + object_category_scope["object_category_scope"][object_category].values()) + + object_category_assignments = self.manager.get_object_category_assignment_dict( + "admin", + self.ref["id"], + new_object["id"] + ) + self.assertIsInstance(object_category_assignments, dict) + self.assertIn("object_category_assignments", object_category_assignments) + self.assertIn("id", object_category_assignments) + self.assertIn("intra_extension_uuid", object_category_assignments) + self.assertEqual(self.ref["id"], object_category_assignments["intra_extension_uuid"]) + self.assertEqual({}, object_category_assignments["object_category_assignments"][new_object["id"]]) + + self.assertRaises( + AuthIntraExtensionModificationNotAuthorized, + self.manager.set_object_category_assignment_dict, + "admin", self.ref["id"], new_object["id"], + { + new_object_category_uuid: [new_object_category_scope_uuid, new_object_category_scope2_uuid], + }) + + self.assertRaises( + AuthIntraExtensionModificationNotAuthorized, + self.manager.del_object_category_assignment, + "admin", self.ref["id"], new_object["id"], + new_object_category_uuid, + new_object_category_scope_uuid) + + self.assertRaises( + AuthIntraExtensionModificationNotAuthorized, + self.manager.add_object_category_assignment_dict, + "admin", self.ref["id"], new_object["id"], + new_object_category_uuid, + new_object_category_scope_uuid) + + def test_action_category_assignment(self): + self.create_intra_extension() + + new_action = self.create_user() + new_actions = dict() + new_actions[new_action["id"]] = new_action["name"] + actions = self.admin_manager.set_action_dict("admin", self.ref["id"], new_actions) + + new_action_category_uuid = uuid.uuid4().hex + new_action_category_value = "role" + action_categories = self.admin_manager.set_action_category_dict( + "admin", + self.ref["id"], + { + new_action_category_uuid: new_action_category_value + } + ) + + for action_category in action_categories["action_categories"]: + action_category_scope = self.admin_manager.get_action_category_scope_dict( + "admin", + self.ref["id"], + action_category) + self.assertIsInstance(action_category_scope, dict) + self.assertIn("action_category_scope", action_category_scope) + self.assertIn("id", action_category_scope) + self.assertIn("intra_extension_uuid", action_category_scope) + self.assertEqual(self.ref["id"], action_category_scope["intra_extension_uuid"]) + self.assertIsInstance(action_category_scope["action_category_scope"], dict) + + new_action_category_scope = dict() + new_action_category_scope_uuid = uuid.uuid4().hex + new_action_category_scope[new_action_category_scope_uuid] = "admin" + action_category_scope = self.admin_manager.set_action_category_scope_dict( + "admin", + self.ref["id"], + action_category, + new_action_category_scope) + self.assertIsInstance(action_category_scope, dict) + self.assertIn("action_category_scope", action_category_scope) + self.assertIn("id", action_category_scope) + self.assertIn("intra_extension_uuid", action_category_scope) + self.assertEqual(self.ref["id"], action_category_scope["intra_extension_uuid"]) + self.assertIn(new_action_category_scope[new_action_category_scope_uuid], + action_category_scope["action_category_scope"][action_category].values()) + + new_action_category_scope2 = dict() + new_action_category_scope2_uuid = uuid.uuid4().hex + new_action_category_scope2[new_action_category_scope2_uuid] = "dev" + action_category_scope = self.admin_manager.set_action_category_scope_dict( + "admin", + self.ref["id"], + action_category, + new_action_category_scope2) + self.assertIsInstance(action_category_scope, dict) + self.assertIn("action_category_scope", action_category_scope) + self.assertIn("id", action_category_scope) + self.assertIn("intra_extension_uuid", action_category_scope) + self.assertEqual(self.ref["id"], action_category_scope["intra_extension_uuid"]) + self.assertIn(new_action_category_scope2[new_action_category_scope2_uuid], + action_category_scope["action_category_scope"][action_category].values()) + + action_category_assignments = self.manager.get_action_category_assignment_dict( + "admin", + self.ref["id"], + new_action["id"] + ) + self.assertIsInstance(action_category_assignments, dict) + self.assertIn("action_category_assignments", action_category_assignments) + self.assertIn("id", action_category_assignments) + self.assertIn("intra_extension_uuid", action_category_assignments) + self.assertEqual(self.ref["id"], action_category_assignments["intra_extension_uuid"]) + self.assertEqual({}, action_category_assignments["action_category_assignments"][new_action["id"]]) + + self.assertRaises( + AuthIntraExtensionModificationNotAuthorized, + self.manager.set_action_category_assignment_dict, + "admin", self.ref["id"], new_action["id"], + { + new_action_category_uuid: [new_action_category_scope_uuid, new_action_category_scope2_uuid], + }) + + self.assertRaises( + AuthIntraExtensionModificationNotAuthorized, + self.manager.del_action_category_assignment, + "admin", self.ref["id"], new_action["id"], + new_action_category_uuid, + new_action_category_scope_uuid) + + self.assertRaises( + AuthIntraExtensionModificationNotAuthorized, + self.manager.add_action_category_assignment_dict, + "admin", self.ref["id"], new_action["id"], + new_action_category_uuid, + new_action_category_scope_uuid) + + def test_sub_meta_rules(self): + self.create_intra_extension() + + aggregation_algorithms = self.manager.get_aggregation_algorithms("admin", self.ref["id"]) + self.assertIsInstance(aggregation_algorithms, dict) + self.assertIsInstance(aggregation_algorithms["aggregation_algorithms"], list) + self.assertIn("and_true_aggregation", aggregation_algorithms["aggregation_algorithms"]) + self.assertIn("test_aggregation", aggregation_algorithms["aggregation_algorithms"]) + + aggregation_algorithm = self.manager.get_aggregation_algorithm("admin", self.ref["id"]) + self.assertIsInstance(aggregation_algorithm, dict) + self.assertIn("aggregation", aggregation_algorithm) + self.assertIn(aggregation_algorithm["aggregation"], aggregation_algorithms["aggregation_algorithms"]) + + _aggregation_algorithm = list(aggregation_algorithms["aggregation_algorithms"]) + _aggregation_algorithm.remove(aggregation_algorithm["aggregation"]) + self.assertRaises( + AuthIntraExtensionModificationNotAuthorized, + self.manager.set_aggregation_algorithm, + "admin", self.ref["id"], _aggregation_algorithm[0]) + + sub_meta_rules = self.manager.get_sub_meta_rule("admin", self.ref["id"]) + self.assertIsInstance(sub_meta_rules, dict) + self.assertIn("sub_meta_rules", sub_meta_rules) + sub_meta_rules_conf = json.load(open(os.path.join(self.policy_directory, self.ref["model"], "metarule.json"))) + metarule = dict() + categories = { + "subject_categories": self.manager.get_subject_category_dict("admin", self.ref["id"]), + "object_categories": self.manager.get_object_category_dict("admin", self.ref["id"]), + "action_categories": self.manager.get_action_category_dict("admin", self.ref["id"]) + } + for relation in sub_meta_rules_conf["sub_meta_rules"]: + metarule[relation] = dict() + for item in ("subject_categories", "object_categories", "action_categories"): + metarule[relation][item] = list() + for element in sub_meta_rules_conf["sub_meta_rules"][relation][item]: + metarule[relation][item].append(self.__get_key_from_value( + element, + categories[item][item] + )) + + for relation in sub_meta_rules["sub_meta_rules"]: + self.assertIn(relation, metarule) + for item in ("subject_categories", "object_categories", "action_categories"): + self.assertEqual( + sub_meta_rules["sub_meta_rules"][relation][item], + metarule[relation][item] + ) + + new_subject_category = {"id": uuid.uuid4().hex, "name": "subject_category_test"} + # Add a particular subject_category + data = self.admin_manager.add_subject_category_dict( + "admin", + self.ref["id"], + new_subject_category["name"]) + new_subject_category["id"] = data["subject_category"]["uuid"] + subject_categories = self.manager.get_subject_category_dict( + "admin", + self.ref["id"]) + self.assertIsInstance(subject_categories, dict) + self.assertIn("subject_categories", subject_categories) + self.assertIn("id", subject_categories) + self.assertIn("intra_extension_uuid", subject_categories) + self.assertEqual(self.ref["id"], subject_categories["intra_extension_uuid"]) + self.assertIn(new_subject_category["id"], subject_categories["subject_categories"]) + metarule[relation]["subject_categories"].append(new_subject_category["id"]) + self.assertRaises( + AuthIntraExtensionModificationNotAuthorized, + self.manager.set_sub_meta_rule, + "admin", self.ref["id"], metarule) + + def test_sub_rules(self): + self.create_intra_extension() + + sub_meta_rules = self.manager.get_sub_meta_rule("admin", self.ref["id"]) + self.assertIsInstance(sub_meta_rules, dict) + self.assertIn("sub_meta_rules", sub_meta_rules) + + sub_rules = self.manager.get_sub_rules("admin", self.ref["id"]) + self.assertIsInstance(sub_rules, dict) + self.assertIn("rules", sub_rules) + rules = dict() + for relation in sub_rules["rules"]: + self.assertIn(relation, self.manager.get_sub_meta_rule_relations("admin", self.ref["id"])["sub_meta_rule_relations"]) + rules[relation] = list() + for rule in sub_rules["rules"][relation]: + for cat, cat_func, func_name in ( + ("subject_categories", self.manager.get_subject_category_scope_dict, "subject_category_scope"), + ("action_categories", self.manager.get_action_category_scope_dict, "action_category_scope"), + ("object_categories", self.manager.get_object_category_scope_dict, "object_category_scope"), + ): + for cat_value in sub_meta_rules["sub_meta_rules"][relation][cat]: + scope = cat_func( + "admin", + self.ref["id"], + cat_value + ) + a_scope = rule.pop(0) + self.assertIn(a_scope, scope[func_name][cat_value]) + + # add a new subrule + + relation = sub_rules["rules"].keys()[0] + sub_rule = [] + for cat, cat_func, func_name in ( + ("subject_categories", self.manager.get_subject_category_scope_dict, "subject_category_scope"), + ("action_categories", self.manager.get_action_category_scope_dict, "action_category_scope"), + ("object_categories", self.manager.get_object_category_scope_dict, "object_category_scope"), + ): + for cat_value in sub_meta_rules["sub_meta_rules"][relation][cat]: + scope = cat_func( + "admin", + self.ref["id"], + cat_value + ) + sub_rule.append(scope[func_name][cat_value].keys()[0]) + + self.assertRaises( + AuthIntraExtensionModificationNotAuthorized, + self.manager.set_sub_rule, + "admin", self.ref["id"], relation, sub_rule) diff --git a/keystone-moon/keystone/tests/moon/unit/test_unit_core_log.py b/keystone-moon/keystone/tests/moon/unit/test_unit_core_log.py new file mode 100644 index 00000000..1b678d53 --- /dev/null +++ b/keystone-moon/keystone/tests/moon/unit/test_unit_core_log.py @@ -0,0 +1,4 @@ +# Copyright 2015 Open Platform for NFV Project, Inc. and its contributors +# This software is distributed under the terms and conditions of the 'Apache-2.0' +# license which can be found in the file 'LICENSE' in this package distribution +# or at 'http://www.apache.org/licenses/LICENSE-2.0'. diff --git a/keystone-moon/keystone/tests/moon/unit/test_unit_core_tenant.py b/keystone-moon/keystone/tests/moon/unit/test_unit_core_tenant.py new file mode 100644 index 00000000..d9c17bd5 --- /dev/null +++ b/keystone-moon/keystone/tests/moon/unit/test_unit_core_tenant.py @@ -0,0 +1,162 @@ +# Copyright 2015 Open Platform for NFV Project, Inc. and its contributors +# This software is distributed under the terms and conditions of the 'Apache-2.0' +# license which can be found in the file 'LICENSE' in this package distribution +# or at 'http://www.apache.org/licenses/LICENSE-2.0'. + +"""Unit tests for core tenant.""" + +import uuid +from oslo_config import cfg +from keystone.tests import unit as tests +from keystone.contrib.moon.core import TenantManager +from keystone.tests.unit.ksfixtures import database +from keystone.contrib.moon.exception import * +from keystone.tests.unit import default_fixtures +from keystone.contrib.moon.core import LogManager + +CONF = cfg.CONF + + +class TestTenantManager(tests.TestCase): + + def setUp(self): + self.useFixture(database.Database()) + super(TestTenantManager, self).setUp() + self.load_backends() + self.load_fixtures(default_fixtures) + self.manager = TenantManager() + + def load_extra_backends(self): + return { + "moonlog_api": LogManager() + } + + def config_overrides(self): + super(TestTenantManager, self).config_overrides() + self.config_fixture.config( + group='moon', + tenant_driver='keystone.contrib.moon.backends.sql.TenantConnector') + + def test_add_tenant(self): + _uuid = uuid.uuid4().hex + new_mapping = { + _uuid: { + "name": uuid.uuid4().hex, + "authz": uuid.uuid4().hex, + "admin": uuid.uuid4().hex, + } + } + data = self.manager.set_tenant_dict( + tenant_uuid=_uuid, + name=new_mapping[_uuid]["name"], + authz_extension_uuid=new_mapping[_uuid]["authz"], + admin_extension_uuid=new_mapping[_uuid]["admin"] + ) + self.assertEquals(_uuid, data["id"]) + self.assertEquals(data["name"], new_mapping[_uuid]["name"]) + self.assertEquals(data["authz"], new_mapping[_uuid]["authz"]) + self.assertEquals(data["admin"], new_mapping[_uuid]["admin"]) + data = self.manager.get_tenant_dict() + self.assertNotEqual(data, {}) + data = self.manager.get_tenant_uuid(new_mapping[_uuid]["authz"]) + self.assertEquals(_uuid, data) + data = self.manager.get_tenant_uuid(new_mapping[_uuid]["admin"]) + self.assertEquals(_uuid, data) + data = self.manager.get_admin_extension_uuid(new_mapping[_uuid]["authz"]) + self.assertEquals(new_mapping[_uuid]["admin"], data) + + def test_tenant_list_empty(self): + data = self.manager.get_tenant_dict() + self.assertEqual(data, {}) + + def test_set_tenant_name(self): + _uuid = uuid.uuid4().hex + new_mapping = { + _uuid: { + "name": uuid.uuid4().hex, + "authz": uuid.uuid4().hex, + "admin": uuid.uuid4().hex, + } + } + data = self.manager.set_tenant_dict( + tenant_uuid=_uuid, + name=new_mapping[_uuid]["name"], + authz_extension_uuid=new_mapping[_uuid]["authz"], + admin_extension_uuid=new_mapping[_uuid]["admin"] + ) + self.assertEquals(_uuid, data["id"]) + self.assertEquals(data["name"], new_mapping[_uuid]["name"]) + data = self.manager.set_tenant_name(_uuid, "new name") + self.assertEquals(_uuid, data["id"]) + self.assertEquals(data["name"], "new name") + data = self.manager.get_tenant_name(_uuid) + self.assertEquals(data, "new name") + + def test_delete_tenant(self): + _uuid = uuid.uuid4().hex + new_mapping = { + _uuid: { + "name": uuid.uuid4().hex, + "authz": uuid.uuid4().hex, + "admin": uuid.uuid4().hex, + } + } + data = self.manager.set_tenant_dict( + tenant_uuid=_uuid, + name=new_mapping[_uuid]["name"], + authz_extension_uuid=new_mapping[_uuid]["authz"], + admin_extension_uuid=new_mapping[_uuid]["admin"] + ) + self.assertEquals(_uuid, data["id"]) + self.assertEquals(data["name"], new_mapping[_uuid]["name"]) + self.assertEquals(data["authz"], new_mapping[_uuid]["authz"]) + self.assertEquals(data["admin"], new_mapping[_uuid]["admin"]) + data = self.manager.get_tenant_dict() + self.assertNotEqual(data, {}) + self.manager.delete(new_mapping[_uuid]["authz"]) + data = self.manager.get_tenant_dict() + self.assertEqual(data, {}) + + def test_get_extension_uuid(self): + _uuid = uuid.uuid4().hex + new_mapping = { + _uuid: { + "name": uuid.uuid4().hex, + "authz": uuid.uuid4().hex, + "admin": uuid.uuid4().hex, + } + } + data = self.manager.set_tenant_dict( + tenant_uuid=_uuid, + name=new_mapping[_uuid]["name"], + authz_extension_uuid=new_mapping[_uuid]["authz"], + admin_extension_uuid=new_mapping[_uuid]["admin"] + ) + self.assertEquals(_uuid, data["id"]) + data = self.manager.get_extension_uuid(_uuid) + self.assertEqual(data, new_mapping[_uuid]["authz"]) + data = self.manager.get_extension_uuid(_uuid, "admin") + self.assertEqual(data, new_mapping[_uuid]["admin"]) + + def test_unkown_tenant_uuid(self): + self.assertRaises(TenantNotFoundError, self.manager.get_tenant_name, uuid.uuid4().hex) + self.assertRaises(TenantNotFoundError, self.manager.set_tenant_name, uuid.uuid4().hex, "new name") + self.assertRaises(TenantNotFoundError, self.manager.get_extension_uuid, uuid.uuid4().hex) + _uuid = uuid.uuid4().hex + new_mapping = { + _uuid: { + "name": uuid.uuid4().hex, + "authz": uuid.uuid4().hex, + "admin": uuid.uuid4().hex, + } + } + data = self.manager.set_tenant_dict( + tenant_uuid=_uuid, + name=new_mapping[_uuid]["name"], + authz_extension_uuid=new_mapping[_uuid]["authz"], + admin_extension_uuid="" + ) + self.assertEquals(_uuid, data["id"]) + self.assertRaises(IntraExtensionNotFound, self.manager.get_extension_uuid, _uuid, "admin") + self.assertRaises(TenantNotFoundError, self.manager.get_tenant_uuid, uuid.uuid4().hex) + # self.assertRaises(AdminIntraExtensionNotFound, self.manager.get_admin_extension_uuid, uuid.uuid4().hex) diff --git a/keystone-moon/keystone/tests/unit/__init__.py b/keystone-moon/keystone/tests/unit/__init__.py new file mode 100644 index 00000000..c97ce253 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/__init__.py @@ -0,0 +1,41 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 oslo_i18n +import six + + +if six.PY3: + # NOTE(dstanek): This block will monkey patch libraries that are not + # yet supported in Python3. We do this that that it is possible to + # execute any tests at all. Without monkey patching modules the + # tests will fail with import errors. + + import sys + from unittest import mock # noqa: our import detection is naive? + + sys.modules['eventlet'] = mock.Mock() + sys.modules['eventlet.green'] = mock.Mock() + sys.modules['eventlet.wsgi'] = mock.Mock() + sys.modules['oslo'].messaging = mock.Mock() + sys.modules['pycadf'] = mock.Mock() + sys.modules['paste'] = mock.Mock() + +# NOTE(dstanek): oslo_i18n.enable_lazy() must be called before +# keystone.i18n._() is called to ensure it has the desired lazy lookup +# behavior. This includes cases, like keystone.exceptions, where +# keystone.i18n._() is called at import time. +oslo_i18n.enable_lazy() + +from keystone.tests.unit.core import * # noqa diff --git a/keystone-moon/keystone/tests/unit/backend/__init__.py b/keystone-moon/keystone/tests/unit/backend/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/tests/unit/backend/core_ldap.py b/keystone-moon/keystone/tests/unit/backend/core_ldap.py new file mode 100644 index 00000000..9d6b23e1 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/backend/core_ldap.py @@ -0,0 +1,161 @@ +# 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 ldap + +from oslo_config import cfg + +from keystone.common import cache +from keystone.common import ldap as common_ldap +from keystone.common.ldap import core as common_ldap_core +from keystone.common import sql +from keystone.tests import unit as tests +from keystone.tests.unit import default_fixtures +from keystone.tests.unit import fakeldap +from keystone.tests.unit.ksfixtures import database + + +CONF = cfg.CONF + + +def create_group_container(identity_api): + # Create the groups base entry (ou=Groups,cn=example,cn=com) + group_api = identity_api.driver.group + conn = group_api.get_connection() + dn = 'ou=Groups,cn=example,cn=com' + conn.add_s(dn, [('objectclass', ['organizationalUnit']), + ('ou', ['Groups'])]) + + +class BaseBackendLdapCommon(object): + """Mixin class to set up generic LDAP backends.""" + + def setUp(self): + super(BaseBackendLdapCommon, self).setUp() + + common_ldap.register_handler('fake://', fakeldap.FakeLdap) + self.load_backends() + self.load_fixtures(default_fixtures) + + self.addCleanup(common_ldap_core._HANDLERS.clear) + self.addCleanup(self.clear_database) + + def _get_domain_fixture(self): + """Domains in LDAP are read-only, so just return the static one.""" + return self.resource_api.get_domain(CONF.identity.default_domain_id) + + def clear_database(self): + for shelf in fakeldap.FakeShelves: + fakeldap.FakeShelves[shelf].clear() + + def reload_backends(self, domain_id): + # Only one backend unless we are using separate domain backends + self.load_backends() + + def get_config(self, domain_id): + # Only one conf structure unless we are using separate domain backends + return CONF + + def config_overrides(self): + super(BaseBackendLdapCommon, self).config_overrides() + self.config_fixture.config( + group='identity', + driver='keystone.identity.backends.ldap.Identity') + + def config_files(self): + config_files = super(BaseBackendLdapCommon, self).config_files() + config_files.append(tests.dirs.tests_conf('backend_ldap.conf')) + return config_files + + def get_user_enabled_vals(self, user): + user_dn = ( + self.identity_api.driver.user._id_to_dn_string(user['id'])) + enabled_attr_name = CONF.ldap.user_enabled_attribute + + ldap_ = self.identity_api.driver.user.get_connection() + res = ldap_.search_s(user_dn, + ldap.SCOPE_BASE, + u'(sn=%s)' % user['name']) + if enabled_attr_name in res[0][1]: + return res[0][1][enabled_attr_name] + else: + return None + + +class BaseBackendLdap(object): + """Mixin class to set up an all-LDAP configuration.""" + def setUp(self): + # NOTE(dstanek): The database must be setup prior to calling the + # parent's setUp. The parent's setUp uses services (like + # credentials) that require a database. + self.useFixture(database.Database()) + super(BaseBackendLdap, self).setUp() + + def load_fixtures(self, fixtures): + # Override super impl since need to create group container. + create_group_container(self.identity_api) + super(BaseBackendLdap, self).load_fixtures(fixtures) + + +class BaseBackendLdapIdentitySqlEverythingElse(tests.SQLDriverOverrides): + """Mixin base for Identity LDAP, everything else SQL backend tests.""" + + def config_files(self): + config_files = super(BaseBackendLdapIdentitySqlEverythingElse, + self).config_files() + config_files.append(tests.dirs.tests_conf('backend_ldap_sql.conf')) + return config_files + + def setUp(self): + self.useFixture(database.Database()) + super(BaseBackendLdapIdentitySqlEverythingElse, self).setUp() + self.clear_database() + self.load_backends() + cache.configure_cache_region(cache.REGION) + self.engine = sql.get_engine() + self.addCleanup(sql.cleanup) + + sql.ModelBase.metadata.create_all(bind=self.engine) + self.addCleanup(sql.ModelBase.metadata.drop_all, bind=self.engine) + + self.load_fixtures(default_fixtures) + # defaulted by the data load + self.user_foo['enabled'] = True + + def config_overrides(self): + super(BaseBackendLdapIdentitySqlEverythingElse, + self).config_overrides() + self.config_fixture.config( + group='identity', + driver='keystone.identity.backends.ldap.Identity') + self.config_fixture.config( + group='resource', + driver='keystone.resource.backends.sql.Resource') + self.config_fixture.config( + group='assignment', + driver='keystone.assignment.backends.sql.Assignment') + + +class BaseBackendLdapIdentitySqlEverythingElseWithMapping(object): + """Mixin base class to test mapping of default LDAP backend. + + The default configuration is not to enable mapping when using a single + backend LDAP driver. However, a cloud provider might want to enable + the mapping, hence hiding the LDAP IDs from any clients of keystone. + Setting backward_compatible_ids to False will enable this mapping. + + """ + def config_overrides(self): + super(BaseBackendLdapIdentitySqlEverythingElseWithMapping, + self).config_overrides() + self.config_fixture.config(group='identity_mapping', + backward_compatible_ids=False) diff --git a/keystone-moon/keystone/tests/unit/backend/core_sql.py b/keystone-moon/keystone/tests/unit/backend/core_sql.py new file mode 100644 index 00000000..9cbd858e --- /dev/null +++ b/keystone-moon/keystone/tests/unit/backend/core_sql.py @@ -0,0 +1,53 @@ +# 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 sqlalchemy + +from keystone.common import sql +from keystone.tests import unit as tests +from keystone.tests.unit import default_fixtures +from keystone.tests.unit.ksfixtures import database + + +class BaseBackendSqlTests(tests.SQLDriverOverrides, tests.TestCase): + + def setUp(self): + super(BaseBackendSqlTests, self).setUp() + self.useFixture(database.Database()) + self.load_backends() + + # populate the engine with tables & fixtures + self.load_fixtures(default_fixtures) + # defaulted by the data load + self.user_foo['enabled'] = True + + def config_files(self): + config_files = super(BaseBackendSqlTests, self).config_files() + config_files.append(tests.dirs.tests_conf('backend_sql.conf')) + return config_files + + +class BaseBackendSqlModels(BaseBackendSqlTests): + + def select_table(self, name): + table = sqlalchemy.Table(name, + sql.ModelBase.metadata, + autoload=True) + s = sqlalchemy.select([table]) + return s + + def assertExpectedSchema(self, table, cols): + table = self.select_table(table) + for col, type_, length in cols: + self.assertIsInstance(table.c[col].type, type_) + if length: + self.assertEqual(length, table.c[col].type.length) diff --git a/keystone-moon/keystone/tests/unit/backend/domain_config/__init__.py b/keystone-moon/keystone/tests/unit/backend/domain_config/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/tests/unit/backend/domain_config/core.py b/keystone-moon/keystone/tests/unit/backend/domain_config/core.py new file mode 100644 index 00000000..da2e9bd9 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/backend/domain_config/core.py @@ -0,0 +1,523 @@ +# 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 +import uuid + +import mock +from testtools import matchers + +from keystone import exception + + +class DomainConfigTests(object): + + def setUp(self): + self.domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(self.domain['id'], self.domain) + self.addCleanup(self.clean_up_domain) + + def clean_up_domain(self): + # NOTE(henry-nash): Deleting the domain will also delete any domain + # configs for this domain. + self.domain['enabled'] = False + self.resource_api.update_domain(self.domain['id'], self.domain) + self.resource_api.delete_domain(self.domain['id']) + del self.domain + + def _domain_config_crud(self, sensitive): + group = uuid.uuid4().hex + option = uuid.uuid4().hex + value = uuid.uuid4().hex + self.domain_config_api.create_config_option( + self.domain['id'], group, option, value, sensitive) + res = self.domain_config_api.get_config_option( + self.domain['id'], group, option, sensitive) + config = {'group': group, 'option': option, 'value': value} + self.assertEqual(config, res) + + value = uuid.uuid4().hex + self.domain_config_api.update_config_option( + self.domain['id'], group, option, value, sensitive) + res = self.domain_config_api.get_config_option( + self.domain['id'], group, option, sensitive) + config = {'group': group, 'option': option, 'value': value} + self.assertEqual(config, res) + + self.domain_config_api.delete_config_options( + self.domain['id'], group, option, sensitive) + self.assertRaises(exception.DomainConfigNotFound, + self.domain_config_api.get_config_option, + self.domain['id'], group, option, sensitive) + # ...and silent if we try to delete it again + self.domain_config_api.delete_config_options( + self.domain['id'], group, option, sensitive) + + def test_whitelisted_domain_config_crud(self): + self._domain_config_crud(sensitive=False) + + def test_sensitive_domain_config_crud(self): + self._domain_config_crud(sensitive=True) + + def _list_domain_config(self, sensitive): + """Test listing by combination of domain, group & option.""" + + config1 = {'group': uuid.uuid4().hex, 'option': uuid.uuid4().hex, + 'value': uuid.uuid4().hex} + # Put config2 in the same group as config1 + config2 = {'group': config1['group'], 'option': uuid.uuid4().hex, + 'value': uuid.uuid4().hex} + config3 = {'group': uuid.uuid4().hex, 'option': uuid.uuid4().hex, + 'value': 100} + for config in [config1, config2, config3]: + self.domain_config_api.create_config_option( + self.domain['id'], config['group'], config['option'], + config['value'], sensitive) + + # Try listing all items from a domain + res = self.domain_config_api.list_config_options( + self.domain['id'], sensitive=sensitive) + self.assertThat(res, matchers.HasLength(3)) + for res_entry in res: + self.assertIn(res_entry, [config1, config2, config3]) + + # Try listing by domain and group + res = self.domain_config_api.list_config_options( + self.domain['id'], group=config1['group'], sensitive=sensitive) + self.assertThat(res, matchers.HasLength(2)) + for res_entry in res: + self.assertIn(res_entry, [config1, config2]) + + # Try listing by domain, group and option + res = self.domain_config_api.list_config_options( + self.domain['id'], group=config2['group'], + option=config2['option'], sensitive=sensitive) + self.assertThat(res, matchers.HasLength(1)) + self.assertEqual(config2, res[0]) + + def test_list_whitelisted_domain_config_crud(self): + self._list_domain_config(False) + + def test_list_sensitive_domain_config_crud(self): + self._list_domain_config(True) + + def _delete_domain_configs(self, sensitive): + """Test deleting by combination of domain, group & option.""" + + config1 = {'group': uuid.uuid4().hex, 'option': uuid.uuid4().hex, + 'value': uuid.uuid4().hex} + # Put config2 and config3 in the same group as config1 + config2 = {'group': config1['group'], 'option': uuid.uuid4().hex, + 'value': uuid.uuid4().hex} + config3 = {'group': config1['group'], 'option': uuid.uuid4().hex, + 'value': uuid.uuid4().hex} + config4 = {'group': uuid.uuid4().hex, 'option': uuid.uuid4().hex, + 'value': uuid.uuid4().hex} + for config in [config1, config2, config3, config4]: + self.domain_config_api.create_config_option( + self.domain['id'], config['group'], config['option'], + config['value'], sensitive) + + # Try deleting by domain, group and option + res = self.domain_config_api.delete_config_options( + self.domain['id'], group=config2['group'], + option=config2['option'], sensitive=sensitive) + res = self.domain_config_api.list_config_options( + self.domain['id'], sensitive=sensitive) + self.assertThat(res, matchers.HasLength(3)) + for res_entry in res: + self.assertIn(res_entry, [config1, config3, config4]) + + # Try deleting by domain and group + res = self.domain_config_api.delete_config_options( + self.domain['id'], group=config4['group'], sensitive=sensitive) + res = self.domain_config_api.list_config_options( + self.domain['id'], sensitive=sensitive) + self.assertThat(res, matchers.HasLength(2)) + for res_entry in res: + self.assertIn(res_entry, [config1, config3]) + + # Try deleting all items from a domain + res = self.domain_config_api.delete_config_options( + self.domain['id'], sensitive=sensitive) + res = self.domain_config_api.list_config_options( + self.domain['id'], sensitive=sensitive) + self.assertThat(res, matchers.HasLength(0)) + + def test_delete_whitelisted_domain_configs(self): + self._delete_domain_configs(False) + + def test_delete_sensitive_domain_configs(self): + self._delete_domain_configs(True) + + def _create_domain_config_twice(self, sensitive): + """Test conflict error thrown if create the same option twice.""" + + config = {'group': uuid.uuid4().hex, 'option': uuid.uuid4().hex, + 'value': uuid.uuid4().hex} + + self.domain_config_api.create_config_option( + self.domain['id'], config['group'], config['option'], + config['value'], sensitive=sensitive) + self.assertRaises(exception.Conflict, + self.domain_config_api.create_config_option, + self.domain['id'], config['group'], config['option'], + config['value'], sensitive=sensitive) + + def test_create_whitelisted_domain_config_twice(self): + self._create_domain_config_twice(False) + + def test_create_sensitive_domain_config_twice(self): + self._create_domain_config_twice(True) + + def test_delete_domain_deletes_configs(self): + """Test domain deletion clears the domain configs.""" + + domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(domain['id'], domain) + config1 = {'group': uuid.uuid4().hex, 'option': uuid.uuid4().hex, + 'value': uuid.uuid4().hex} + # Put config2 in the same group as config1 + config2 = {'group': config1['group'], 'option': uuid.uuid4().hex, + 'value': uuid.uuid4().hex} + self.domain_config_api.create_config_option( + domain['id'], config1['group'], config1['option'], + config1['value']) + self.domain_config_api.create_config_option( + domain['id'], config2['group'], config2['option'], + config2['value'], sensitive=True) + res = self.domain_config_api.list_config_options( + domain['id']) + self.assertThat(res, matchers.HasLength(1)) + res = self.domain_config_api.list_config_options( + domain['id'], sensitive=True) + self.assertThat(res, matchers.HasLength(1)) + + # Now delete the domain + domain['enabled'] = False + self.resource_api.update_domain(domain['id'], domain) + self.resource_api.delete_domain(domain['id']) + + # Check domain configs have also been deleted + res = self.domain_config_api.list_config_options( + domain['id']) + self.assertThat(res, matchers.HasLength(0)) + res = self.domain_config_api.list_config_options( + domain['id'], sensitive=True) + self.assertThat(res, matchers.HasLength(0)) + + def test_create_domain_config_including_sensitive_option(self): + config = {'ldap': {'url': uuid.uuid4().hex, + 'user_tree_dn': uuid.uuid4().hex, + 'password': uuid.uuid4().hex}} + self.domain_config_api.create_config(self.domain['id'], config) + + # password is sensitive, so check that the whitelisted portion and + # the sensitive piece have been stored in the appropriate locations. + res = self.domain_config_api.get_config(self.domain['id']) + config_whitelisted = copy.deepcopy(config) + config_whitelisted['ldap'].pop('password') + self.assertEqual(config_whitelisted, res) + res = self.domain_config_api.get_config_option( + self.domain['id'], 'ldap', 'password', sensitive=True) + self.assertEqual(config['ldap']['password'], res['value']) + + # Finally, use the non-public API to get back the whole config + res = self.domain_config_api.get_config_with_sensitive_info( + self.domain['id']) + self.assertEqual(config, res) + + def test_get_partial_domain_config(self): + config = {'ldap': {'url': uuid.uuid4().hex, + 'user_tree_dn': uuid.uuid4().hex, + 'password': uuid.uuid4().hex}, + 'identity': {'driver': uuid.uuid4().hex}} + self.domain_config_api.create_config(self.domain['id'], config) + + res = self.domain_config_api.get_config(self.domain['id'], + group='identity') + config_partial = copy.deepcopy(config) + config_partial.pop('ldap') + self.assertEqual(config_partial, res) + res = self.domain_config_api.get_config( + self.domain['id'], group='ldap', option='user_tree_dn') + self.assertEqual({'user_tree_dn': config['ldap']['user_tree_dn']}, res) + # ...but we should fail to get a sensitive option + self.assertRaises(exception.DomainConfigNotFound, + self.domain_config_api.get_config, self.domain['id'], + group='ldap', option='password') + + def test_delete_partial_domain_config(self): + config = {'ldap': {'url': uuid.uuid4().hex, + 'user_tree_dn': uuid.uuid4().hex, + 'password': uuid.uuid4().hex}, + 'identity': {'driver': uuid.uuid4().hex}} + self.domain_config_api.create_config(self.domain['id'], config) + + self.domain_config_api.delete_config( + self.domain['id'], group='identity') + config_partial = copy.deepcopy(config) + config_partial.pop('identity') + config_partial['ldap'].pop('password') + res = self.domain_config_api.get_config(self.domain['id']) + self.assertEqual(config_partial, res) + + self.domain_config_api.delete_config( + self.domain['id'], group='ldap', option='url') + config_partial = copy.deepcopy(config_partial) + config_partial['ldap'].pop('url') + res = self.domain_config_api.get_config(self.domain['id']) + self.assertEqual(config_partial, res) + + def test_get_options_not_in_domain_config(self): + self.assertRaises(exception.DomainConfigNotFound, + self.domain_config_api.get_config, self.domain['id']) + config = {'ldap': {'url': uuid.uuid4().hex}} + + self.domain_config_api.create_config(self.domain['id'], config) + + self.assertRaises(exception.DomainConfigNotFound, + self.domain_config_api.get_config, self.domain['id'], + group='identity') + self.assertRaises(exception.DomainConfigNotFound, + self.domain_config_api.get_config, self.domain['id'], + group='ldap', option='user_tree_dn') + + def test_get_sensitive_config(self): + config = {'ldap': {'url': uuid.uuid4().hex, + 'user_tree_dn': uuid.uuid4().hex, + 'password': uuid.uuid4().hex}, + 'identity': {'driver': uuid.uuid4().hex}} + res = self.domain_config_api.get_config_with_sensitive_info( + self.domain['id']) + self.assertEqual({}, res) + self.domain_config_api.create_config(self.domain['id'], config) + res = self.domain_config_api.get_config_with_sensitive_info( + self.domain['id']) + self.assertEqual(config, res) + + def test_update_partial_domain_config(self): + config = {'ldap': {'url': uuid.uuid4().hex, + 'user_tree_dn': uuid.uuid4().hex, + 'password': uuid.uuid4().hex}, + 'identity': {'driver': uuid.uuid4().hex}} + self.domain_config_api.create_config(self.domain['id'], config) + + # Try updating a group + new_config = {'ldap': {'url': uuid.uuid4().hex, + 'user_filter': uuid.uuid4().hex}} + res = self.domain_config_api.update_config( + self.domain['id'], new_config, group='ldap') + expected_config = copy.deepcopy(config) + expected_config['ldap']['url'] = new_config['ldap']['url'] + expected_config['ldap']['user_filter'] = ( + new_config['ldap']['user_filter']) + expected_full_config = copy.deepcopy(expected_config) + expected_config['ldap'].pop('password') + res = self.domain_config_api.get_config(self.domain['id']) + self.assertEqual(expected_config, res) + # The sensitive option should still existsss + res = self.domain_config_api.get_config_with_sensitive_info( + self.domain['id']) + self.assertEqual(expected_full_config, res) + + # Try updating a single whitelisted option + self.domain_config_api.delete_config(self.domain['id']) + self.domain_config_api.create_config(self.domain['id'], config) + new_config = {'url': uuid.uuid4().hex} + res = self.domain_config_api.update_config( + self.domain['id'], new_config, group='ldap', option='url') + + # Make sure whitelisted and full config is updated + expected_whitelisted_config = copy.deepcopy(config) + expected_whitelisted_config['ldap']['url'] = new_config['url'] + expected_full_config = copy.deepcopy(expected_whitelisted_config) + expected_whitelisted_config['ldap'].pop('password') + self.assertEqual(expected_whitelisted_config, res) + res = self.domain_config_api.get_config(self.domain['id']) + self.assertEqual(expected_whitelisted_config, res) + res = self.domain_config_api.get_config_with_sensitive_info( + self.domain['id']) + self.assertEqual(expected_full_config, res) + + # Try updating a single sensitive option + self.domain_config_api.delete_config(self.domain['id']) + self.domain_config_api.create_config(self.domain['id'], config) + new_config = {'password': uuid.uuid4().hex} + res = self.domain_config_api.update_config( + self.domain['id'], new_config, group='ldap', option='password') + # The whitelisted config should not have changed... + expected_whitelisted_config = copy.deepcopy(config) + expected_full_config = copy.deepcopy(config) + expected_whitelisted_config['ldap'].pop('password') + self.assertEqual(expected_whitelisted_config, res) + res = self.domain_config_api.get_config(self.domain['id']) + self.assertEqual(expected_whitelisted_config, res) + expected_full_config['ldap']['password'] = new_config['password'] + res = self.domain_config_api.get_config_with_sensitive_info( + self.domain['id']) + # ...but the sensitive piece should have. + self.assertEqual(expected_full_config, res) + + def test_update_invalid_partial_domain_config(self): + config = {'ldap': {'url': uuid.uuid4().hex, + 'user_tree_dn': uuid.uuid4().hex, + 'password': uuid.uuid4().hex}, + 'identity': {'driver': uuid.uuid4().hex}} + # An extra group, when specifying one group should fail + self.assertRaises(exception.InvalidDomainConfig, + self.domain_config_api.update_config, + self.domain['id'], config, group='ldap') + # An extra option, when specifying one option should fail + self.assertRaises(exception.InvalidDomainConfig, + self.domain_config_api.update_config, + self.domain['id'], config['ldap'], + group='ldap', option='url') + + # Now try the right number of groups/options, but just not + # ones that are in the config provided + config = {'ldap': {'user_tree_dn': uuid.uuid4().hex}} + self.assertRaises(exception.InvalidDomainConfig, + self.domain_config_api.update_config, + self.domain['id'], config, group='identity') + self.assertRaises(exception.InvalidDomainConfig, + self.domain_config_api.update_config, + self.domain['id'], config['ldap'], group='ldap', + option='url') + + # Now some valid groups/options, but just not ones that are in the + # existing config + config = {'ldap': {'user_tree_dn': uuid.uuid4().hex}} + self.domain_config_api.create_config(self.domain['id'], config) + config_wrong_group = {'identity': {'driver': uuid.uuid4().hex}} + self.assertRaises(exception.DomainConfigNotFound, + self.domain_config_api.update_config, + self.domain['id'], config_wrong_group, + group='identity') + config_wrong_option = {'url': uuid.uuid4().hex} + self.assertRaises(exception.DomainConfigNotFound, + self.domain_config_api.update_config, + self.domain['id'], config_wrong_option, + group='ldap', option='url') + + # And finally just some bad groups/options + bad_group = uuid.uuid4().hex + config = {bad_group: {'user': uuid.uuid4().hex}} + self.assertRaises(exception.InvalidDomainConfig, + self.domain_config_api.update_config, + self.domain['id'], config, group=bad_group, + option='user') + bad_option = uuid.uuid4().hex + config = {'ldap': {bad_option: uuid.uuid4().hex}} + self.assertRaises(exception.InvalidDomainConfig, + self.domain_config_api.update_config, + self.domain['id'], config, group='ldap', + option=bad_option) + + def test_create_invalid_domain_config(self): + self.assertRaises(exception.InvalidDomainConfig, + self.domain_config_api.create_config, + self.domain['id'], {}) + config = {uuid.uuid4().hex: uuid.uuid4().hex} + self.assertRaises(exception.InvalidDomainConfig, + self.domain_config_api.create_config, + self.domain['id'], config) + config = {uuid.uuid4().hex: {uuid.uuid4().hex: uuid.uuid4().hex}} + self.assertRaises(exception.InvalidDomainConfig, + self.domain_config_api.create_config, + self.domain['id'], config) + config = {'ldap': {uuid.uuid4().hex: uuid.uuid4().hex}} + self.assertRaises(exception.InvalidDomainConfig, + self.domain_config_api.create_config, + self.domain['id'], config) + # Try an option that IS in the standard conf, but neither whitelisted + # or marked as sensitive + config = {'ldap': {'role_tree_dn': uuid.uuid4().hex}} + self.assertRaises(exception.InvalidDomainConfig, + self.domain_config_api.create_config, + self.domain['id'], config) + + def test_delete_invalid_partial_domain_config(self): + config = {'ldap': {'url': uuid.uuid4().hex}} + self.domain_config_api.create_config(self.domain['id'], config) + # Try deleting a group not in the config + self.assertRaises(exception.DomainConfigNotFound, + self.domain_config_api.delete_config, + self.domain['id'], group='identity') + # Try deleting an option not in the config + self.assertRaises(exception.DomainConfigNotFound, + self.domain_config_api.delete_config, + self.domain['id'], + group='ldap', option='user_tree_dn') + + def test_sensitive_substitution_in_domain_config(self): + # Create a config that contains a whitelisted option that requires + # substitution of a sensitive option. + config = {'ldap': {'url': 'my_url/%(password)s', + 'user_tree_dn': uuid.uuid4().hex, + 'password': uuid.uuid4().hex}, + 'identity': {'driver': uuid.uuid4().hex}} + self.domain_config_api.create_config(self.domain['id'], config) + + # Read back the config with the internal method and ensure that the + # substitution has taken place. + res = self.domain_config_api.get_config_with_sensitive_info( + self.domain['id']) + expected_url = ( + config['ldap']['url'] % {'password': config['ldap']['password']}) + self.assertEqual(expected_url, res['ldap']['url']) + + def test_invalid_sensitive_substitution_in_domain_config(self): + """Check that invalid substitutions raise warnings.""" + + mock_log = mock.Mock() + + invalid_option_config = { + 'ldap': {'user_tree_dn': uuid.uuid4().hex, + 'password': uuid.uuid4().hex}, + 'identity': {'driver': uuid.uuid4().hex}} + + for invalid_option in ['my_url/%(passssword)s', + 'my_url/%(password', + 'my_url/%(password)', + 'my_url/%(password)d']: + invalid_option_config['ldap']['url'] = invalid_option + self.domain_config_api.create_config( + self.domain['id'], invalid_option_config) + + with mock.patch('keystone.resource.core.LOG', mock_log): + res = self.domain_config_api.get_config_with_sensitive_info( + self.domain['id']) + mock_log.warn.assert_any_call(mock.ANY) + self.assertEqual( + invalid_option_config['ldap']['url'], res['ldap']['url']) + + def test_escaped_sequence_in_domain_config(self): + """Check that escaped '%(' doesn't get interpreted.""" + + mock_log = mock.Mock() + + escaped_option_config = { + 'ldap': {'url': 'my_url/%%(password)s', + 'user_tree_dn': uuid.uuid4().hex, + 'password': uuid.uuid4().hex}, + 'identity': {'driver': uuid.uuid4().hex}} + + self.domain_config_api.create_config( + self.domain['id'], escaped_option_config) + + with mock.patch('keystone.resource.core.LOG', mock_log): + res = self.domain_config_api.get_config_with_sensitive_info( + self.domain['id']) + self.assertFalse(mock_log.warn.called) + # The escaping '%' should have been removed + self.assertEqual('my_url/%(password)s', res['ldap']['url']) diff --git a/keystone-moon/keystone/tests/unit/backend/domain_config/test_sql.py b/keystone-moon/keystone/tests/unit/backend/domain_config/test_sql.py new file mode 100644 index 00000000..6459ede1 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/backend/domain_config/test_sql.py @@ -0,0 +1,41 @@ +# 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 keystone.common import sql +from keystone.tests.unit.backend import core_sql +from keystone.tests.unit.backend.domain_config import core + + +class SqlDomainConfigModels(core_sql.BaseBackendSqlModels): + + def test_whitelisted_model(self): + cols = (('domain_id', sql.String, 64), + ('group', sql.String, 255), + ('option', sql.String, 255), + ('value', sql.JsonBlob, None)) + self.assertExpectedSchema('whitelisted_config', cols) + + def test_sensitive_model(self): + cols = (('domain_id', sql.String, 64), + ('group', sql.String, 255), + ('option', sql.String, 255), + ('value', sql.JsonBlob, None)) + self.assertExpectedSchema('sensitive_config', cols) + + +class SqlDomainConfig(core_sql.BaseBackendSqlTests, core.DomainConfigTests): + def setUp(self): + super(SqlDomainConfig, self).setUp() + # core.DomainConfigTests is effectively a mixin class, so make sure we + # call its setup + core.DomainConfigTests.setUp(self) diff --git a/keystone-moon/keystone/tests/unit/backend/role/__init__.py b/keystone-moon/keystone/tests/unit/backend/role/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/tests/unit/backend/role/core.py b/keystone-moon/keystone/tests/unit/backend/role/core.py new file mode 100644 index 00000000..f6e47fe9 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/backend/role/core.py @@ -0,0 +1,130 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 +import uuid + +from keystone import exception +from keystone.tests import unit as tests +from keystone.tests.unit import default_fixtures + + +class RoleTests(object): + + def test_get_role_404(self): + self.assertRaises(exception.RoleNotFound, + self.role_api.get_role, + uuid.uuid4().hex) + + def test_create_duplicate_role_name_fails(self): + role = {'id': 'fake1', + 'name': 'fake1name'} + self.role_api.create_role('fake1', role) + role['id'] = 'fake2' + self.assertRaises(exception.Conflict, + self.role_api.create_role, + 'fake2', + role) + + def test_rename_duplicate_role_name_fails(self): + role1 = { + 'id': 'fake1', + 'name': 'fake1name' + } + role2 = { + 'id': 'fake2', + 'name': 'fake2name' + } + self.role_api.create_role('fake1', role1) + self.role_api.create_role('fake2', role2) + role1['name'] = 'fake2name' + self.assertRaises(exception.Conflict, + self.role_api.update_role, + 'fake1', + role1) + + def test_role_crud(self): + role = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.role_api.create_role(role['id'], role) + role_ref = self.role_api.get_role(role['id']) + role_ref_dict = {x: role_ref[x] for x in role_ref} + self.assertDictEqual(role_ref_dict, role) + + role['name'] = uuid.uuid4().hex + updated_role_ref = self.role_api.update_role(role['id'], role) + role_ref = self.role_api.get_role(role['id']) + role_ref_dict = {x: role_ref[x] for x in role_ref} + self.assertDictEqual(role_ref_dict, role) + self.assertDictEqual(role_ref_dict, updated_role_ref) + + self.role_api.delete_role(role['id']) + self.assertRaises(exception.RoleNotFound, + self.role_api.get_role, + role['id']) + + def test_update_role_404(self): + role = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.assertRaises(exception.RoleNotFound, + self.role_api.update_role, + role['id'], + role) + + def test_list_roles(self): + roles = self.role_api.list_roles() + self.assertEqual(len(default_fixtures.ROLES), len(roles)) + role_ids = set(role['id'] for role in roles) + expected_role_ids = set(role['id'] for role in default_fixtures.ROLES) + self.assertEqual(expected_role_ids, role_ids) + + @tests.skip_if_cache_disabled('role') + def test_cache_layer_role_crud(self): + role = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + role_id = role['id'] + # Create role + self.role_api.create_role(role_id, role) + role_ref = self.role_api.get_role(role_id) + updated_role_ref = copy.deepcopy(role_ref) + updated_role_ref['name'] = uuid.uuid4().hex + # Update role, bypassing the role api manager + self.role_api.driver.update_role(role_id, updated_role_ref) + # Verify get_role still returns old ref + self.assertDictEqual(role_ref, self.role_api.get_role(role_id)) + # Invalidate Cache + self.role_api.get_role.invalidate(self.role_api, role_id) + # Verify get_role returns the new role_ref + self.assertDictEqual(updated_role_ref, + self.role_api.get_role(role_id)) + # Update role back to original via the assignment api manager + self.role_api.update_role(role_id, role_ref) + # Verify get_role returns the original role ref + self.assertDictEqual(role_ref, self.role_api.get_role(role_id)) + # Delete role bypassing the role api manager + self.role_api.driver.delete_role(role_id) + # Verify get_role still returns the role_ref + self.assertDictEqual(role_ref, self.role_api.get_role(role_id)) + # Invalidate cache + self.role_api.get_role.invalidate(self.role_api, role_id) + # Verify RoleNotFound is now raised + self.assertRaises(exception.RoleNotFound, + self.role_api.get_role, + role_id) + # recreate role + self.role_api.create_role(role_id, role) + self.role_api.get_role(role_id) + # delete role via the assignment api manager + self.role_api.delete_role(role_id) + # verity RoleNotFound is now raised + self.assertRaises(exception.RoleNotFound, + self.role_api.get_role, + role_id) diff --git a/keystone-moon/keystone/tests/unit/backend/role/test_ldap.py b/keystone-moon/keystone/tests/unit/backend/role/test_ldap.py new file mode 100644 index 00000000..ba4b7c6e --- /dev/null +++ b/keystone-moon/keystone/tests/unit/backend/role/test_ldap.py @@ -0,0 +1,161 @@ +# -*- coding: utf-8 -*- +# 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 oslo_config import cfg + +from keystone import exception +from keystone.tests import unit as tests +from keystone.tests.unit.backend import core_ldap +from keystone.tests.unit.backend.role import core as core_role +from keystone.tests.unit import default_fixtures + + +CONF = cfg.CONF + + +class LdapRoleCommon(core_ldap.BaseBackendLdapCommon, core_role.RoleTests): + """Tests that should be run in every LDAP configuration. + + Include additional tests that are unique to LDAP (or need to be overridden) + which should be run for all the various LDAP configurations we test. + + """ + pass + + +class LdapRole(LdapRoleCommon, core_ldap.BaseBackendLdap, tests.TestCase): + """Test in an all-LDAP configuration. + + Include additional tests that are unique to LDAP (or need to be overridden) + which only need to be run in a basic LDAP configurations. + + """ + def test_configurable_allowed_role_actions(self): + role = {'id': u'fäké1', 'name': u'fäké1'} + self.role_api.create_role(u'fäké1', role) + role_ref = self.role_api.get_role(u'fäké1') + self.assertEqual(u'fäké1', role_ref['id']) + + role['name'] = u'fäké2' + self.role_api.update_role(u'fäké1', role) + + self.role_api.delete_role(u'fäké1') + self.assertRaises(exception.RoleNotFound, + self.role_api.get_role, + u'fäké1') + + def test_configurable_forbidden_role_actions(self): + self.config_fixture.config( + group='ldap', role_allow_create=False, role_allow_update=False, + role_allow_delete=False) + self.load_backends() + + role = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.assertRaises(exception.ForbiddenAction, + self.role_api.create_role, + role['id'], + role) + + self.role_member['name'] = uuid.uuid4().hex + self.assertRaises(exception.ForbiddenAction, + self.role_api.update_role, + self.role_member['id'], + self.role_member) + + self.assertRaises(exception.ForbiddenAction, + self.role_api.delete_role, + self.role_member['id']) + + def test_role_filter(self): + role_ref = self.role_api.get_role(self.role_member['id']) + self.assertDictEqual(role_ref, self.role_member) + + self.config_fixture.config(group='ldap', + role_filter='(CN=DOES_NOT_MATCH)') + self.load_backends() + # NOTE(morganfainberg): CONF.ldap.role_filter will not be + # dynamically changed at runtime. This invalidate is a work-around for + # the expectation that it is safe to change config values in tests that + # could affect what the drivers would return up to the manager. This + # solves this assumption when working with aggressive (on-create) + # cache population. + self.role_api.get_role.invalidate(self.role_api, + self.role_member['id']) + self.assertRaises(exception.RoleNotFound, + self.role_api.get_role, + self.role_member['id']) + + def test_role_attribute_mapping(self): + self.config_fixture.config(group='ldap', role_name_attribute='ou') + self.clear_database() + self.load_backends() + self.load_fixtures(default_fixtures) + # NOTE(morganfainberg): CONF.ldap.role_name_attribute will not be + # dynamically changed at runtime. This invalidate is a work-around for + # the expectation that it is safe to change config values in tests that + # could affect what the drivers would return up to the manager. This + # solves this assumption when working with aggressive (on-create) + # cache population. + self.role_api.get_role.invalidate(self.role_api, + self.role_member['id']) + role_ref = self.role_api.get_role(self.role_member['id']) + self.assertEqual(self.role_member['id'], role_ref['id']) + self.assertEqual(self.role_member['name'], role_ref['name']) + + self.config_fixture.config(group='ldap', role_name_attribute='sn') + self.load_backends() + # NOTE(morganfainberg): CONF.ldap.role_name_attribute will not be + # dynamically changed at runtime. This invalidate is a work-around for + # the expectation that it is safe to change config values in tests that + # could affect what the drivers would return up to the manager. This + # solves this assumption when working with aggressive (on-create) + # cache population. + self.role_api.get_role.invalidate(self.role_api, + self.role_member['id']) + role_ref = self.role_api.get_role(self.role_member['id']) + self.assertEqual(self.role_member['id'], role_ref['id']) + self.assertNotIn('name', role_ref) + + def test_role_attribute_ignore(self): + self.config_fixture.config(group='ldap', + role_attribute_ignore=['name']) + self.clear_database() + self.load_backends() + self.load_fixtures(default_fixtures) + # NOTE(morganfainberg): CONF.ldap.role_attribute_ignore will not be + # dynamically changed at runtime. This invalidate is a work-around for + # the expectation that it is safe to change config values in tests that + # could affect what the drivers would return up to the manager. This + # solves this assumption when working with aggressive (on-create) + # cache population. + self.role_api.get_role.invalidate(self.role_api, + self.role_member['id']) + role_ref = self.role_api.get_role(self.role_member['id']) + self.assertEqual(self.role_member['id'], role_ref['id']) + self.assertNotIn('name', role_ref) + + +class LdapIdentitySqlEverythingElseRole( + core_ldap.BaseBackendLdapIdentitySqlEverythingElse, LdapRoleCommon, + tests.TestCase): + """Test Identity in LDAP, Everything else in SQL.""" + pass + + +class LdapIdentitySqlEverythingElseWithMappingRole( + LdapIdentitySqlEverythingElseRole, + core_ldap.BaseBackendLdapIdentitySqlEverythingElseWithMapping): + """Test ID mapping of default LDAP backend.""" + pass diff --git a/keystone-moon/keystone/tests/unit/backend/role/test_sql.py b/keystone-moon/keystone/tests/unit/backend/role/test_sql.py new file mode 100644 index 00000000..79ff148a --- /dev/null +++ b/keystone-moon/keystone/tests/unit/backend/role/test_sql.py @@ -0,0 +1,40 @@ +# 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 keystone.common import sql +from keystone import exception +from keystone.tests.unit.backend import core_sql +from keystone.tests.unit.backend.role import core + + +class SqlRoleModels(core_sql.BaseBackendSqlModels): + + def test_role_model(self): + cols = (('id', sql.String, 64), + ('name', sql.String, 255)) + self.assertExpectedSchema('role', cols) + + +class SqlRole(core_sql.BaseBackendSqlTests, core.RoleTests): + + def test_create_null_role_name(self): + role = {'id': uuid.uuid4().hex, + 'name': None} + self.assertRaises(exception.UnexpectedError, + self.role_api.create_role, + role['id'], + role) + self.assertRaises(exception.RoleNotFound, + self.role_api.get_role, + role['id']) diff --git a/keystone-moon/keystone/tests/unit/catalog/__init__.py b/keystone-moon/keystone/tests/unit/catalog/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/tests/unit/catalog/test_core.py b/keystone-moon/keystone/tests/unit/catalog/test_core.py new file mode 100644 index 00000000..99a34280 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/catalog/test_core.py @@ -0,0 +1,74 @@ +# 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 oslo_config import cfg +import testtools + +from keystone.catalog import core +from keystone import exception + + +CONF = cfg.CONF + + +class FormatUrlTests(testtools.TestCase): + + def test_successful_formatting(self): + url_template = ('http://$(public_bind_host)s:$(admin_port)d/' + '$(tenant_id)s/$(user_id)s') + values = {'public_bind_host': 'server', 'admin_port': 9090, + 'tenant_id': 'A', 'user_id': 'B'} + actual_url = core.format_url(url_template, values) + + expected_url = 'http://server:9090/A/B' + self.assertEqual(actual_url, expected_url) + + def test_raises_malformed_on_missing_key(self): + self.assertRaises(exception.MalformedEndpoint, + core.format_url, + "http://$(public_bind_host)s/$(public_port)d", + {"public_bind_host": "1"}) + + def test_raises_malformed_on_wrong_type(self): + self.assertRaises(exception.MalformedEndpoint, + core.format_url, + "http://$(public_bind_host)d", + {"public_bind_host": "something"}) + + def test_raises_malformed_on_incomplete_format(self): + self.assertRaises(exception.MalformedEndpoint, + core.format_url, + "http://$(public_bind_host)", + {"public_bind_host": "1"}) + + def test_formatting_a_non_string(self): + def _test(url_template): + self.assertRaises(exception.MalformedEndpoint, + core.format_url, + url_template, + {}) + + _test(None) + _test(object()) + + def test_substitution_with_key_not_allowed(self): + # If the url template contains a substitution that's not in the allowed + # list then MalformedEndpoint is raised. + # For example, admin_token isn't allowed. + url_template = ('http://$(public_bind_host)s:$(public_port)d/' + '$(tenant_id)s/$(user_id)s/$(admin_token)s') + values = {'public_bind_host': 'server', 'public_port': 9090, + 'tenant_id': 'A', 'user_id': 'B', 'admin_token': 'C'} + self.assertRaises(exception.MalformedEndpoint, + core.format_url, + url_template, + values) diff --git a/keystone-moon/keystone/tests/unit/common/__init__.py b/keystone-moon/keystone/tests/unit/common/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/tests/unit/common/test_base64utils.py b/keystone-moon/keystone/tests/unit/common/test_base64utils.py new file mode 100644 index 00000000..b0b75578 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/common/test_base64utils.py @@ -0,0 +1,208 @@ +# Copyright 2013 Red Hat, Inc. +# +# 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 keystone.common import base64utils +from keystone.tests import unit as tests + +base64_alphabet = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ' + 'abcdefghijklmnopqrstuvwxyz' + '0123456789' + '+/=') # includes pad char + +base64url_alphabet = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ' + 'abcdefghijklmnopqrstuvwxyz' + '0123456789' + '-_=') # includes pad char + + +class TestValid(tests.BaseTestCase): + def test_valid_base64(self): + self.assertTrue(base64utils.is_valid_base64('+/==')) + self.assertTrue(base64utils.is_valid_base64('+/+=')) + self.assertTrue(base64utils.is_valid_base64('+/+/')) + + self.assertFalse(base64utils.is_valid_base64('-_==')) + self.assertFalse(base64utils.is_valid_base64('-_-=')) + self.assertFalse(base64utils.is_valid_base64('-_-_')) + + self.assertTrue(base64utils.is_valid_base64('abcd')) + self.assertFalse(base64utils.is_valid_base64('abcde')) + self.assertFalse(base64utils.is_valid_base64('abcde==')) + self.assertFalse(base64utils.is_valid_base64('abcdef')) + self.assertTrue(base64utils.is_valid_base64('abcdef==')) + self.assertFalse(base64utils.is_valid_base64('abcdefg')) + self.assertTrue(base64utils.is_valid_base64('abcdefg=')) + self.assertTrue(base64utils.is_valid_base64('abcdefgh')) + + self.assertFalse(base64utils.is_valid_base64('-_==')) + + def test_valid_base64url(self): + self.assertFalse(base64utils.is_valid_base64url('+/==')) + self.assertFalse(base64utils.is_valid_base64url('+/+=')) + self.assertFalse(base64utils.is_valid_base64url('+/+/')) + + self.assertTrue(base64utils.is_valid_base64url('-_==')) + self.assertTrue(base64utils.is_valid_base64url('-_-=')) + self.assertTrue(base64utils.is_valid_base64url('-_-_')) + + self.assertTrue(base64utils.is_valid_base64url('abcd')) + self.assertFalse(base64utils.is_valid_base64url('abcde')) + self.assertFalse(base64utils.is_valid_base64url('abcde==')) + self.assertFalse(base64utils.is_valid_base64url('abcdef')) + self.assertTrue(base64utils.is_valid_base64url('abcdef==')) + self.assertFalse(base64utils.is_valid_base64url('abcdefg')) + self.assertTrue(base64utils.is_valid_base64url('abcdefg=')) + self.assertTrue(base64utils.is_valid_base64url('abcdefgh')) + + self.assertTrue(base64utils.is_valid_base64url('-_==')) + + +class TestBase64Padding(tests.BaseTestCase): + + def test_filter(self): + self.assertEqual('', base64utils.filter_formatting('')) + self.assertEqual('', base64utils.filter_formatting(' ')) + self.assertEqual('a', base64utils.filter_formatting('a')) + self.assertEqual('a', base64utils.filter_formatting(' a')) + self.assertEqual('a', base64utils.filter_formatting('a ')) + self.assertEqual('ab', base64utils.filter_formatting('ab')) + self.assertEqual('ab', base64utils.filter_formatting(' ab')) + self.assertEqual('ab', base64utils.filter_formatting('ab ')) + self.assertEqual('ab', base64utils.filter_formatting('a b')) + self.assertEqual('ab', base64utils.filter_formatting(' a b')) + self.assertEqual('ab', base64utils.filter_formatting('a b ')) + self.assertEqual('ab', base64utils.filter_formatting('a\nb\n ')) + + text = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ' + 'abcdefghijklmnopqrstuvwxyz' + '0123456789' + '+/=') + self.assertEqual(base64_alphabet, + base64utils.filter_formatting(text)) + + text = (' ABCDEFGHIJKLMNOPQRSTUVWXYZ\n' + ' abcdefghijklmnopqrstuvwxyz\n' + '\t\f\r' + ' 0123456789\n' + ' +/=') + self.assertEqual(base64_alphabet, + base64utils.filter_formatting(text)) + self.assertEqual(base64url_alphabet, + base64utils.base64_to_base64url(base64_alphabet)) + + text = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ' + 'abcdefghijklmnopqrstuvwxyz' + '0123456789' + '-_=') + self.assertEqual(base64url_alphabet, + base64utils.filter_formatting(text)) + + text = (' ABCDEFGHIJKLMNOPQRSTUVWXYZ\n' + ' abcdefghijklmnopqrstuvwxyz\n' + '\t\f\r' + ' 0123456789\n' + '-_=') + self.assertEqual(base64url_alphabet, + base64utils.filter_formatting(text)) + + def test_alphabet_conversion(self): + self.assertEqual(base64url_alphabet, + base64utils.base64_to_base64url(base64_alphabet)) + + self.assertEqual(base64_alphabet, + base64utils.base64url_to_base64(base64url_alphabet)) + + def test_is_padded(self): + self.assertTrue(base64utils.base64_is_padded('ABCD')) + self.assertTrue(base64utils.base64_is_padded('ABC=')) + self.assertTrue(base64utils.base64_is_padded('AB==')) + + self.assertTrue(base64utils.base64_is_padded('1234ABCD')) + self.assertTrue(base64utils.base64_is_padded('1234ABC=')) + self.assertTrue(base64utils.base64_is_padded('1234AB==')) + + self.assertFalse(base64utils.base64_is_padded('ABC')) + self.assertFalse(base64utils.base64_is_padded('AB')) + self.assertFalse(base64utils.base64_is_padded('A')) + self.assertFalse(base64utils.base64_is_padded('')) + + self.assertRaises(base64utils.InvalidBase64Error, + base64utils.base64_is_padded, '=') + + self.assertRaises(base64utils.InvalidBase64Error, + base64utils.base64_is_padded, 'AB=C') + + self.assertRaises(base64utils.InvalidBase64Error, + base64utils.base64_is_padded, 'AB=') + + self.assertRaises(base64utils.InvalidBase64Error, + base64utils.base64_is_padded, 'ABCD=') + + self.assertRaises(ValueError, base64utils.base64_is_padded, + 'ABC', pad='==') + self.assertRaises(base64utils.InvalidBase64Error, + base64utils.base64_is_padded, 'A=BC') + + def test_strip_padding(self): + self.assertEqual('ABCD', base64utils.base64_strip_padding('ABCD')) + self.assertEqual('ABC', base64utils.base64_strip_padding('ABC=')) + self.assertEqual('AB', base64utils.base64_strip_padding('AB==')) + self.assertRaises(ValueError, base64utils.base64_strip_padding, + 'ABC=', pad='==') + self.assertEqual('ABC', base64utils.base64_strip_padding('ABC')) + + def test_assure_padding(self): + self.assertEqual('ABCD', base64utils.base64_assure_padding('ABCD')) + self.assertEqual('ABC=', base64utils.base64_assure_padding('ABC')) + self.assertEqual('ABC=', base64utils.base64_assure_padding('ABC=')) + self.assertEqual('AB==', base64utils.base64_assure_padding('AB')) + self.assertEqual('AB==', base64utils.base64_assure_padding('AB==')) + self.assertRaises(ValueError, base64utils.base64_assure_padding, + 'ABC', pad='==') + + def test_base64_percent_encoding(self): + self.assertEqual('ABCD', base64utils.base64url_percent_encode('ABCD')) + self.assertEqual('ABC%3D', + base64utils.base64url_percent_encode('ABC=')) + self.assertEqual('AB%3D%3D', + base64utils.base64url_percent_encode('AB==')) + + self.assertEqual('ABCD', base64utils.base64url_percent_decode('ABCD')) + self.assertEqual('ABC=', + base64utils.base64url_percent_decode('ABC%3D')) + self.assertEqual('AB==', + base64utils.base64url_percent_decode('AB%3D%3D')) + self.assertRaises(base64utils.InvalidBase64Error, + base64utils.base64url_percent_encode, 'chars') + self.assertRaises(base64utils.InvalidBase64Error, + base64utils.base64url_percent_decode, 'AB%3D%3') + + +class TestTextWrap(tests.BaseTestCase): + + def test_wrapping(self): + raw_text = 'abcdefgh' + wrapped_text = 'abc\ndef\ngh\n' + + self.assertEqual(wrapped_text, + base64utils.base64_wrap(raw_text, width=3)) + + t = '\n'.join(base64utils.base64_wrap_iter(raw_text, width=3)) + '\n' + self.assertEqual(wrapped_text, t) + + raw_text = 'abcdefgh' + wrapped_text = 'abcd\nefgh\n' + + self.assertEqual(wrapped_text, + base64utils.base64_wrap(raw_text, width=4)) diff --git a/keystone-moon/keystone/tests/unit/common/test_connection_pool.py b/keystone-moon/keystone/tests/unit/common/test_connection_pool.py new file mode 100644 index 00000000..74d0420c --- /dev/null +++ b/keystone-moon/keystone/tests/unit/common/test_connection_pool.py @@ -0,0 +1,119 @@ +# 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 time + +import mock +from six.moves import queue +import testtools +from testtools import matchers + +from keystone.common.cache import _memcache_pool +from keystone import exception +from keystone.tests.unit import core + + +class _TestConnectionPool(_memcache_pool.ConnectionPool): + destroyed_value = 'destroyed' + + def _create_connection(self): + return mock.MagicMock() + + def _destroy_connection(self, conn): + conn(self.destroyed_value) + + +class TestConnectionPool(core.TestCase): + def setUp(self): + super(TestConnectionPool, self).setUp() + self.unused_timeout = 10 + self.maxsize = 2 + self.connection_pool = _TestConnectionPool( + maxsize=self.maxsize, + unused_timeout=self.unused_timeout) + self.addCleanup(self.cleanup_instance('connection_pool')) + + def test_get_context_manager(self): + self.assertThat(self.connection_pool.queue, matchers.HasLength(0)) + with self.connection_pool.acquire() as conn: + self.assertEqual(1, self.connection_pool._acquired) + self.assertEqual(0, self.connection_pool._acquired) + self.assertThat(self.connection_pool.queue, matchers.HasLength(1)) + self.assertEqual(conn, self.connection_pool.queue[0].connection) + + def test_cleanup_pool(self): + self.test_get_context_manager() + newtime = time.time() + self.unused_timeout * 2 + non_expired_connection = _memcache_pool._PoolItem( + ttl=(newtime * 2), + connection=mock.MagicMock()) + self.connection_pool.queue.append(non_expired_connection) + self.assertThat(self.connection_pool.queue, matchers.HasLength(2)) + with mock.patch.object(time, 'time', return_value=newtime): + conn = self.connection_pool.queue[0].connection + with self.connection_pool.acquire(): + pass + conn.assert_has_calls( + [mock.call(self.connection_pool.destroyed_value)]) + self.assertThat(self.connection_pool.queue, matchers.HasLength(1)) + self.assertEqual(0, non_expired_connection.connection.call_count) + + def test_acquire_conn_exception_returns_acquired_count(self): + class TestException(Exception): + pass + + with mock.patch.object(_TestConnectionPool, '_create_connection', + side_effect=TestException): + with testtools.ExpectedException(TestException): + with self.connection_pool.acquire(): + pass + self.assertThat(self.connection_pool.queue, + matchers.HasLength(0)) + self.assertEqual(0, self.connection_pool._acquired) + + def test_connection_pool_limits_maximum_connections(self): + # NOTE(morganfainberg): To ensure we don't lockup tests until the + # job limit, explicitly call .get_nowait() and .put_nowait() in this + # case. + conn1 = self.connection_pool.get_nowait() + conn2 = self.connection_pool.get_nowait() + + # Use a nowait version to raise an Empty exception indicating we would + # not get another connection until one is placed back into the queue. + self.assertRaises(queue.Empty, self.connection_pool.get_nowait) + + # Place the connections back into the pool. + self.connection_pool.put_nowait(conn1) + self.connection_pool.put_nowait(conn2) + + # Make sure we can get a connection out of the pool again. + self.connection_pool.get_nowait() + + def test_connection_pool_maximum_connection_get_timeout(self): + connection_pool = _TestConnectionPool( + maxsize=1, + unused_timeout=self.unused_timeout, + conn_get_timeout=0) + + def _acquire_connection(): + with connection_pool.acquire(): + pass + + # Make sure we've consumed the only available connection from the pool + conn = connection_pool.get_nowait() + + self.assertRaises(exception.UnexpectedError, _acquire_connection) + + # Put the connection back and ensure we can acquire the connection + # after it is available. + connection_pool.put_nowait(conn) + _acquire_connection() diff --git a/keystone-moon/keystone/tests/unit/common/test_injection.py b/keystone-moon/keystone/tests/unit/common/test_injection.py new file mode 100644 index 00000000..86bb3c24 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/common/test_injection.py @@ -0,0 +1,293 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 keystone.common import dependency +from keystone.tests import unit as tests + + +class TestDependencyInjection(tests.BaseTestCase): + def setUp(self): + super(TestDependencyInjection, self).setUp() + self.addCleanup(dependency.reset) + + def test_dependency_injection(self): + class Interface(object): + def do_work(self): + assert False + + @dependency.provider('first_api') + class FirstImplementation(Interface): + def do_work(self): + return True + + @dependency.provider('second_api') + class SecondImplementation(Interface): + def do_work(self): + return True + + @dependency.requires('first_api', 'second_api') + class Consumer(object): + def do_work_with_dependencies(self): + assert self.first_api.do_work() + assert self.second_api.do_work() + + # initialize dependency providers + first_api = FirstImplementation() + second_api = SecondImplementation() + + # ... sometime later, initialize a dependency consumer + consumer = Consumer() + + # the expected dependencies should be available to the consumer + self.assertIs(consumer.first_api, first_api) + self.assertIs(consumer.second_api, second_api) + self.assertIsInstance(consumer.first_api, Interface) + self.assertIsInstance(consumer.second_api, Interface) + consumer.do_work_with_dependencies() + + def test_dependency_provider_configuration(self): + @dependency.provider('api') + class Configurable(object): + def __init__(self, value=None): + self.value = value + + def get_value(self): + return self.value + + @dependency.requires('api') + class Consumer(object): + def get_value(self): + return self.api.get_value() + + # initialize dependency providers + api = Configurable(value=True) + + # ... sometime later, initialize a dependency consumer + consumer = Consumer() + + # the expected dependencies should be available to the consumer + self.assertIs(consumer.api, api) + self.assertIsInstance(consumer.api, Configurable) + self.assertTrue(consumer.get_value()) + + def test_dependency_consumer_configuration(self): + @dependency.provider('api') + class Provider(object): + def get_value(self): + return True + + @dependency.requires('api') + class Configurable(object): + def __init__(self, value=None): + self.value = value + + def get_value(self): + if self.value: + return self.api.get_value() + + # initialize dependency providers + api = Provider() + + # ... sometime later, initialize a dependency consumer + consumer = Configurable(value=True) + + # the expected dependencies should be available to the consumer + self.assertIs(consumer.api, api) + self.assertIsInstance(consumer.api, Provider) + self.assertTrue(consumer.get_value()) + + def test_inherited_dependency(self): + class Interface(object): + def do_work(self): + assert False + + @dependency.provider('first_api') + class FirstImplementation(Interface): + def do_work(self): + return True + + @dependency.provider('second_api') + class SecondImplementation(Interface): + def do_work(self): + return True + + @dependency.requires('first_api') + class ParentConsumer(object): + def do_work_with_dependencies(self): + assert self.first_api.do_work() + + @dependency.requires('second_api') + class ChildConsumer(ParentConsumer): + def do_work_with_dependencies(self): + assert self.second_api.do_work() + super(ChildConsumer, self).do_work_with_dependencies() + + # initialize dependency providers + first_api = FirstImplementation() + second_api = SecondImplementation() + + # ... sometime later, initialize a dependency consumer + consumer = ChildConsumer() + + # dependencies should be naturally inherited + self.assertEqual( + set(['first_api']), + ParentConsumer._dependencies) + self.assertEqual( + set(['first_api', 'second_api']), + ChildConsumer._dependencies) + self.assertEqual( + set(['first_api', 'second_api']), + consumer._dependencies) + + # the expected dependencies should be available to the consumer + self.assertIs(consumer.first_api, first_api) + self.assertIs(consumer.second_api, second_api) + self.assertIsInstance(consumer.first_api, Interface) + self.assertIsInstance(consumer.second_api, Interface) + consumer.do_work_with_dependencies() + + def test_unresolvable_dependency(self): + @dependency.requires(uuid.uuid4().hex) + class Consumer(object): + pass + + def for_test(): + Consumer() + dependency.resolve_future_dependencies() + + self.assertRaises(dependency.UnresolvableDependencyException, for_test) + + def test_circular_dependency(self): + p1_name = uuid.uuid4().hex + p2_name = uuid.uuid4().hex + + @dependency.provider(p1_name) + @dependency.requires(p2_name) + class P1(object): + pass + + @dependency.provider(p2_name) + @dependency.requires(p1_name) + class P2(object): + pass + + p1 = P1() + p2 = P2() + + dependency.resolve_future_dependencies() + + self.assertIs(getattr(p1, p2_name), p2) + self.assertIs(getattr(p2, p1_name), p1) + + def test_reset(self): + # Can reset the registry of providers. + + p_id = uuid.uuid4().hex + + @dependency.provider(p_id) + class P(object): + pass + + p_inst = P() + + self.assertIs(dependency.get_provider(p_id), p_inst) + + dependency.reset() + + self.assertFalse(dependency._REGISTRY) + + def test_optional_dependency_not_provided(self): + requirement_name = uuid.uuid4().hex + + @dependency.optional(requirement_name) + class C1(object): + pass + + c1_inst = C1() + + dependency.resolve_future_dependencies() + + self.assertIsNone(getattr(c1_inst, requirement_name)) + + def test_optional_dependency_provided(self): + requirement_name = uuid.uuid4().hex + + @dependency.optional(requirement_name) + class C1(object): + pass + + @dependency.provider(requirement_name) + class P1(object): + pass + + c1_inst = C1() + p1_inst = P1() + + dependency.resolve_future_dependencies() + + self.assertIs(getattr(c1_inst, requirement_name), p1_inst) + + def test_optional_and_required(self): + p1_name = uuid.uuid4().hex + p2_name = uuid.uuid4().hex + optional_name = uuid.uuid4().hex + + @dependency.provider(p1_name) + @dependency.requires(p2_name) + @dependency.optional(optional_name) + class P1(object): + pass + + @dependency.provider(p2_name) + @dependency.requires(p1_name) + class P2(object): + pass + + p1 = P1() + p2 = P2() + + dependency.resolve_future_dependencies() + + self.assertIs(getattr(p1, p2_name), p2) + self.assertIs(getattr(p2, p1_name), p1) + self.assertIsNone(getattr(p1, optional_name)) + + def test_get_provider(self): + # Can get the instance of a provider using get_provider + + provider_name = uuid.uuid4().hex + + @dependency.provider(provider_name) + class P(object): + pass + + provider_instance = P() + retrieved_provider_instance = dependency.get_provider(provider_name) + self.assertIs(provider_instance, retrieved_provider_instance) + + def test_get_provider_not_provided_error(self): + # If no provider and provider is required then fails. + + provider_name = uuid.uuid4().hex + self.assertRaises(KeyError, dependency.get_provider, provider_name) + + def test_get_provider_not_provided_optional(self): + # If no provider and provider is optional then returns None. + + provider_name = uuid.uuid4().hex + self.assertIsNone(dependency.get_provider(provider_name, + dependency.GET_OPTIONAL)) diff --git a/keystone-moon/keystone/tests/unit/common/test_json_home.py b/keystone-moon/keystone/tests/unit/common/test_json_home.py new file mode 100644 index 00000000..fb7f8448 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/common/test_json_home.py @@ -0,0 +1,91 @@ +# Copyright 2014 IBM Corp. +# +# 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 testtools import matchers + +from keystone.common import json_home +from keystone.tests import unit as tests + + +class JsonHomeTest(tests.BaseTestCase): + def test_build_v3_resource_relation(self): + resource_name = self.getUniqueString() + relation = json_home.build_v3_resource_relation(resource_name) + exp_relation = ( + 'http://docs.openstack.org/api/openstack-identity/3/rel/%s' % + resource_name) + self.assertThat(relation, matchers.Equals(exp_relation)) + + def test_build_v3_extension_resource_relation(self): + extension_name = self.getUniqueString() + extension_version = self.getUniqueString() + resource_name = self.getUniqueString() + relation = json_home.build_v3_extension_resource_relation( + extension_name, extension_version, resource_name) + exp_relation = ( + 'http://docs.openstack.org/api/openstack-identity/3/ext/%s/%s/rel/' + '%s' % (extension_name, extension_version, resource_name)) + self.assertThat(relation, matchers.Equals(exp_relation)) + + def test_build_v3_parameter_relation(self): + parameter_name = self.getUniqueString() + relation = json_home.build_v3_parameter_relation(parameter_name) + exp_relation = ( + 'http://docs.openstack.org/api/openstack-identity/3/param/%s' % + parameter_name) + self.assertThat(relation, matchers.Equals(exp_relation)) + + def test_build_v3_extension_parameter_relation(self): + extension_name = self.getUniqueString() + extension_version = self.getUniqueString() + parameter_name = self.getUniqueString() + relation = json_home.build_v3_extension_parameter_relation( + extension_name, extension_version, parameter_name) + exp_relation = ( + 'http://docs.openstack.org/api/openstack-identity/3/ext/%s/%s/' + 'param/%s' % (extension_name, extension_version, parameter_name)) + self.assertThat(relation, matchers.Equals(exp_relation)) + + def test_translate_urls(self): + href_rel = self.getUniqueString() + href = self.getUniqueString() + href_template_rel = self.getUniqueString() + href_template = self.getUniqueString() + href_vars = {self.getUniqueString(): self.getUniqueString()} + original_json_home = { + 'resources': { + href_rel: {'href': href}, + href_template_rel: { + 'href-template': href_template, + 'href-vars': href_vars} + } + } + + new_json_home = copy.deepcopy(original_json_home) + new_prefix = self.getUniqueString() + json_home.translate_urls(new_json_home, new_prefix) + + exp_json_home = { + 'resources': { + href_rel: {'href': new_prefix + href}, + href_template_rel: { + 'href-template': new_prefix + href_template, + 'href-vars': href_vars} + } + } + + self.assertThat(new_json_home, matchers.Equals(exp_json_home)) diff --git a/keystone-moon/keystone/tests/unit/common/test_ldap.py b/keystone-moon/keystone/tests/unit/common/test_ldap.py new file mode 100644 index 00000000..41568890 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/common/test_ldap.py @@ -0,0 +1,502 @@ +# -*- coding: utf-8 -*- +# 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 + +import ldap.dn +import mock +from oslo_config import cfg +from testtools import matchers + +import os +import shutil +import tempfile + +from keystone.common import ldap as ks_ldap +from keystone.common.ldap import core as common_ldap_core +from keystone.tests import unit as tests +from keystone.tests.unit import default_fixtures +from keystone.tests.unit import fakeldap + +CONF = cfg.CONF + + +class DnCompareTest(tests.BaseTestCase): + """Tests for the DN comparison functions in keystone.common.ldap.core.""" + + def test_prep(self): + # prep_case_insensitive returns the string with spaces at the front and + # end if it's already lowercase and no insignificant characters. + value = 'lowercase value' + self.assertEqual(value, ks_ldap.prep_case_insensitive(value)) + + def test_prep_lowercase(self): + # prep_case_insensitive returns the string with spaces at the front and + # end and lowercases the value. + value = 'UPPERCASE VALUE' + exp_value = value.lower() + self.assertEqual(exp_value, ks_ldap.prep_case_insensitive(value)) + + def test_prep_insignificant(self): + # prep_case_insensitive remove insignificant spaces. + value = 'before after' + exp_value = 'before after' + self.assertEqual(exp_value, ks_ldap.prep_case_insensitive(value)) + + def test_prep_insignificant_pre_post(self): + # prep_case_insensitive remove insignificant spaces. + value = ' value ' + exp_value = 'value' + self.assertEqual(exp_value, ks_ldap.prep_case_insensitive(value)) + + def test_ava_equal_same(self): + # is_ava_value_equal returns True if the two values are the same. + value = 'val1' + self.assertTrue(ks_ldap.is_ava_value_equal('cn', value, value)) + + def test_ava_equal_complex(self): + # is_ava_value_equal returns True if the two values are the same using + # a value that's got different capitalization and insignificant chars. + val1 = 'before after' + val2 = ' BEFORE afTer ' + self.assertTrue(ks_ldap.is_ava_value_equal('cn', val1, val2)) + + def test_ava_different(self): + # is_ava_value_equal returns False if the values aren't the same. + self.assertFalse(ks_ldap.is_ava_value_equal('cn', 'val1', 'val2')) + + def test_rdn_same(self): + # is_rdn_equal returns True if the two values are the same. + rdn = ldap.dn.str2dn('cn=val1')[0] + self.assertTrue(ks_ldap.is_rdn_equal(rdn, rdn)) + + def test_rdn_diff_length(self): + # is_rdn_equal returns False if the RDNs have a different number of + # AVAs. + rdn1 = ldap.dn.str2dn('cn=cn1')[0] + rdn2 = ldap.dn.str2dn('cn=cn1+ou=ou1')[0] + self.assertFalse(ks_ldap.is_rdn_equal(rdn1, rdn2)) + + def test_rdn_multi_ava_same_order(self): + # is_rdn_equal returns True if the RDNs have the same number of AVAs + # and the values are the same. + rdn1 = ldap.dn.str2dn('cn=cn1+ou=ou1')[0] + rdn2 = ldap.dn.str2dn('cn=CN1+ou=OU1')[0] + self.assertTrue(ks_ldap.is_rdn_equal(rdn1, rdn2)) + + def test_rdn_multi_ava_diff_order(self): + # is_rdn_equal returns True if the RDNs have the same number of AVAs + # and the values are the same, even if in a different order + rdn1 = ldap.dn.str2dn('cn=cn1+ou=ou1')[0] + rdn2 = ldap.dn.str2dn('ou=OU1+cn=CN1')[0] + self.assertTrue(ks_ldap.is_rdn_equal(rdn1, rdn2)) + + def test_rdn_multi_ava_diff_type(self): + # is_rdn_equal returns False if the RDNs have the same number of AVAs + # and the attribute types are different. + rdn1 = ldap.dn.str2dn('cn=cn1+ou=ou1')[0] + rdn2 = ldap.dn.str2dn('cn=cn1+sn=sn1')[0] + self.assertFalse(ks_ldap.is_rdn_equal(rdn1, rdn2)) + + def test_rdn_attr_type_case_diff(self): + # is_rdn_equal returns True for same RDNs even when attr type case is + # different. + rdn1 = ldap.dn.str2dn('cn=cn1')[0] + rdn2 = ldap.dn.str2dn('CN=cn1')[0] + self.assertTrue(ks_ldap.is_rdn_equal(rdn1, rdn2)) + + def test_rdn_attr_type_alias(self): + # is_rdn_equal returns False for same RDNs even when attr type alias is + # used. Note that this is a limitation since an LDAP server should + # consider them equal. + rdn1 = ldap.dn.str2dn('cn=cn1')[0] + rdn2 = ldap.dn.str2dn('2.5.4.3=cn1')[0] + self.assertFalse(ks_ldap.is_rdn_equal(rdn1, rdn2)) + + def test_dn_same(self): + # is_dn_equal returns True if the DNs are the same. + dn = 'cn=Babs Jansen,ou=OpenStack' + self.assertTrue(ks_ldap.is_dn_equal(dn, dn)) + + def test_dn_equal_unicode(self): + # is_dn_equal can accept unicode + dn = u'cn=fäké,ou=OpenStack' + self.assertTrue(ks_ldap.is_dn_equal(dn, dn)) + + def test_dn_diff_length(self): + # is_dn_equal returns False if the DNs don't have the same number of + # RDNs + dn1 = 'cn=Babs Jansen,ou=OpenStack' + dn2 = 'cn=Babs Jansen,ou=OpenStack,dc=example.com' + self.assertFalse(ks_ldap.is_dn_equal(dn1, dn2)) + + def test_dn_equal_rdns(self): + # is_dn_equal returns True if the DNs have the same number of RDNs + # and each RDN is the same. + dn1 = 'cn=Babs Jansen,ou=OpenStack+cn=OpenSource' + dn2 = 'CN=Babs Jansen,cn=OpenSource+ou=OpenStack' + self.assertTrue(ks_ldap.is_dn_equal(dn1, dn2)) + + def test_dn_parsed_dns(self): + # is_dn_equal can also accept parsed DNs. + dn_str1 = ldap.dn.str2dn('cn=Babs Jansen,ou=OpenStack+cn=OpenSource') + dn_str2 = ldap.dn.str2dn('CN=Babs Jansen,cn=OpenSource+ou=OpenStack') + self.assertTrue(ks_ldap.is_dn_equal(dn_str1, dn_str2)) + + def test_startswith_under_child(self): + # dn_startswith returns True if descendant_dn is a child of dn. + child = 'cn=Babs Jansen,ou=OpenStack' + parent = 'ou=OpenStack' + self.assertTrue(ks_ldap.dn_startswith(child, parent)) + + def test_startswith_parent(self): + # dn_startswith returns False if descendant_dn is a parent of dn. + child = 'cn=Babs Jansen,ou=OpenStack' + parent = 'ou=OpenStack' + self.assertFalse(ks_ldap.dn_startswith(parent, child)) + + def test_startswith_same(self): + # dn_startswith returns False if DNs are the same. + dn = 'cn=Babs Jansen,ou=OpenStack' + self.assertFalse(ks_ldap.dn_startswith(dn, dn)) + + def test_startswith_not_parent(self): + # dn_startswith returns False if descendant_dn is not under the dn + child = 'cn=Babs Jansen,ou=OpenStack' + parent = 'dc=example.com' + self.assertFalse(ks_ldap.dn_startswith(child, parent)) + + def test_startswith_descendant(self): + # dn_startswith returns True if descendant_dn is a descendant of dn. + descendant = 'cn=Babs Jansen,ou=Keystone,ou=OpenStack,dc=example.com' + dn = 'ou=OpenStack,dc=example.com' + self.assertTrue(ks_ldap.dn_startswith(descendant, dn)) + + descendant = 'uid=12345,ou=Users,dc=example,dc=com' + dn = 'ou=Users,dc=example,dc=com' + self.assertTrue(ks_ldap.dn_startswith(descendant, dn)) + + def test_startswith_parsed_dns(self): + # dn_startswith also accepts parsed DNs. + descendant = ldap.dn.str2dn('cn=Babs Jansen,ou=OpenStack') + dn = ldap.dn.str2dn('ou=OpenStack') + self.assertTrue(ks_ldap.dn_startswith(descendant, dn)) + + def test_startswith_unicode(self): + # dn_startswith accepts unicode. + child = u'cn=cn=fäké,ou=OpenStäck' + parent = 'ou=OpenStäck' + self.assertTrue(ks_ldap.dn_startswith(child, parent)) + + +class LDAPDeleteTreeTest(tests.TestCase): + + def setUp(self): + super(LDAPDeleteTreeTest, self).setUp() + + ks_ldap.register_handler('fake://', + fakeldap.FakeLdapNoSubtreeDelete) + self.load_backends() + self.load_fixtures(default_fixtures) + + self.addCleanup(self.clear_database) + self.addCleanup(common_ldap_core._HANDLERS.clear) + + def clear_database(self): + for shelf in fakeldap.FakeShelves: + fakeldap.FakeShelves[shelf].clear() + + def config_overrides(self): + super(LDAPDeleteTreeTest, self).config_overrides() + self.config_fixture.config( + group='identity', + driver='keystone.identity.backends.ldap.Identity') + + def config_files(self): + config_files = super(LDAPDeleteTreeTest, self).config_files() + config_files.append(tests.dirs.tests_conf('backend_ldap.conf')) + return config_files + + def test_deleteTree(self): + """Test manually deleting a tree. + + Few LDAP servers support CONTROL_DELETETREE. This test + exercises the alternate code paths in BaseLdap.deleteTree. + + """ + conn = self.identity_api.user.get_connection() + id_attr = self.identity_api.user.id_attr + objclass = self.identity_api.user.object_class.lower() + tree_dn = self.identity_api.user.tree_dn + + def create_entry(name, parent_dn=None): + if not parent_dn: + parent_dn = tree_dn + dn = '%s=%s,%s' % (id_attr, name, parent_dn) + attrs = [('objectclass', [objclass, 'ldapsubentry']), + (id_attr, [name])] + conn.add_s(dn, attrs) + return dn + + # create 3 entries like this: + # cn=base + # cn=child,cn=base + # cn=grandchild,cn=child,cn=base + # then attempt to deleteTree(cn=base) + base_id = 'base' + base_dn = create_entry(base_id) + child_dn = create_entry('child', base_dn) + grandchild_dn = create_entry('grandchild', child_dn) + + # verify that the three entries were created + scope = ldap.SCOPE_SUBTREE + filt = '(|(objectclass=*)(objectclass=ldapsubentry))' + entries = conn.search_s(base_dn, scope, filt, + attrlist=common_ldap_core.DN_ONLY) + self.assertThat(entries, matchers.HasLength(3)) + sort_ents = sorted([e[0] for e in entries], key=len, reverse=True) + self.assertEqual([grandchild_dn, child_dn, base_dn], sort_ents) + + # verify that a non-leaf node can't be deleted directly by the + # LDAP server + self.assertRaises(ldap.NOT_ALLOWED_ON_NONLEAF, + conn.delete_s, base_dn) + self.assertRaises(ldap.NOT_ALLOWED_ON_NONLEAF, + conn.delete_s, child_dn) + + # call our deleteTree implementation + self.identity_api.user.deleteTree(base_id) + self.assertRaises(ldap.NO_SUCH_OBJECT, + conn.search_s, base_dn, ldap.SCOPE_BASE) + self.assertRaises(ldap.NO_SUCH_OBJECT, + conn.search_s, child_dn, ldap.SCOPE_BASE) + self.assertRaises(ldap.NO_SUCH_OBJECT, + conn.search_s, grandchild_dn, ldap.SCOPE_BASE) + + +class SslTlsTest(tests.TestCase): + """Tests for the SSL/TLS functionality in keystone.common.ldap.core.""" + + @mock.patch.object(ks_ldap.core.KeystoneLDAPHandler, 'simple_bind_s') + @mock.patch.object(ldap.ldapobject.LDAPObject, 'start_tls_s') + def _init_ldap_connection(self, config, mock_ldap_one, mock_ldap_two): + # Attempt to connect to initialize python-ldap. + base_ldap = ks_ldap.BaseLdap(config) + base_ldap.get_connection() + + def test_certfile_trust_tls(self): + # We need this to actually exist, so we create a tempfile. + (handle, certfile) = tempfile.mkstemp() + self.addCleanup(os.unlink, certfile) + self.addCleanup(os.close, handle) + self.config_fixture.config(group='ldap', + url='ldap://localhost', + use_tls=True, + tls_cacertfile=certfile) + + self._init_ldap_connection(CONF) + + # Ensure the cert trust option is set. + self.assertEqual(certfile, ldap.get_option(ldap.OPT_X_TLS_CACERTFILE)) + + def test_certdir_trust_tls(self): + # We need this to actually exist, so we create a tempdir. + certdir = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, certdir) + self.config_fixture.config(group='ldap', + url='ldap://localhost', + use_tls=True, + tls_cacertdir=certdir) + + self._init_ldap_connection(CONF) + + # Ensure the cert trust option is set. + self.assertEqual(certdir, ldap.get_option(ldap.OPT_X_TLS_CACERTDIR)) + + def test_certfile_trust_ldaps(self): + # We need this to actually exist, so we create a tempfile. + (handle, certfile) = tempfile.mkstemp() + self.addCleanup(os.unlink, certfile) + self.addCleanup(os.close, handle) + self.config_fixture.config(group='ldap', + url='ldaps://localhost', + use_tls=False, + tls_cacertfile=certfile) + + self._init_ldap_connection(CONF) + + # Ensure the cert trust option is set. + self.assertEqual(certfile, ldap.get_option(ldap.OPT_X_TLS_CACERTFILE)) + + def test_certdir_trust_ldaps(self): + # We need this to actually exist, so we create a tempdir. + certdir = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, certdir) + self.config_fixture.config(group='ldap', + url='ldaps://localhost', + use_tls=False, + tls_cacertdir=certdir) + + self._init_ldap_connection(CONF) + + # Ensure the cert trust option is set. + self.assertEqual(certdir, ldap.get_option(ldap.OPT_X_TLS_CACERTDIR)) + + +class LDAPPagedResultsTest(tests.TestCase): + """Tests the paged results functionality in keystone.common.ldap.core.""" + + def setUp(self): + super(LDAPPagedResultsTest, self).setUp() + self.clear_database() + + ks_ldap.register_handler('fake://', fakeldap.FakeLdap) + self.addCleanup(common_ldap_core._HANDLERS.clear) + + self.load_backends() + self.load_fixtures(default_fixtures) + + def clear_database(self): + for shelf in fakeldap.FakeShelves: + fakeldap.FakeShelves[shelf].clear() + + def config_overrides(self): + super(LDAPPagedResultsTest, self).config_overrides() + self.config_fixture.config( + group='identity', + driver='keystone.identity.backends.ldap.Identity') + + def config_files(self): + config_files = super(LDAPPagedResultsTest, self).config_files() + config_files.append(tests.dirs.tests_conf('backend_ldap.conf')) + return config_files + + @mock.patch.object(fakeldap.FakeLdap, 'search_ext') + @mock.patch.object(fakeldap.FakeLdap, 'result3') + def test_paged_results_control_api(self, mock_result3, mock_search_ext): + mock_result3.return_value = ('', [], 1, []) + + self.config_fixture.config(group='ldap', + page_size=1) + + conn = self.identity_api.user.get_connection() + conn._paged_search_s('dc=example,dc=test', + ldap.SCOPE_SUBTREE, + 'objectclass=*') + + +class CommonLdapTestCase(tests.BaseTestCase): + """These test cases call functions in keystone.common.ldap.""" + + def test_binary_attribute_values(self): + result = [( + 'cn=junk,dc=example,dc=com', + { + 'cn': ['junk'], + 'sn': [uuid.uuid4().hex], + 'mail': [uuid.uuid4().hex], + 'binary_attr': ['\x00\xFF\x00\xFF'] + } + ), ] + py_result = ks_ldap.convert_ldap_result(result) + # The attribute containing the binary value should + # not be present in the converted result. + self.assertNotIn('binary_attr', py_result[0][1]) + + def test_utf8_conversion(self): + value_unicode = u'fäké1' + value_utf8 = value_unicode.encode('utf-8') + + result_utf8 = ks_ldap.utf8_encode(value_unicode) + self.assertEqual(value_utf8, result_utf8) + + result_utf8 = ks_ldap.utf8_encode(value_utf8) + self.assertEqual(value_utf8, result_utf8) + + result_unicode = ks_ldap.utf8_decode(value_utf8) + self.assertEqual(value_unicode, result_unicode) + + result_unicode = ks_ldap.utf8_decode(value_unicode) + self.assertEqual(value_unicode, result_unicode) + + self.assertRaises(TypeError, + ks_ldap.utf8_encode, + 100) + + result_unicode = ks_ldap.utf8_decode(100) + self.assertEqual(u'100', result_unicode) + + def test_user_id_begins_with_0(self): + user_id = '0123456' + result = [( + 'cn=dummy,dc=example,dc=com', + { + 'user_id': [user_id], + 'enabled': ['TRUE'] + } + ), ] + py_result = ks_ldap.convert_ldap_result(result) + # The user id should be 0123456, and the enabled + # flag should be True + self.assertIs(py_result[0][1]['enabled'][0], True) + self.assertEqual(user_id, py_result[0][1]['user_id'][0]) + + def test_user_id_begins_with_0_and_enabled_bit_mask(self): + user_id = '0123456' + bitmask = '225' + expected_bitmask = 225 + result = [( + 'cn=dummy,dc=example,dc=com', + { + 'user_id': [user_id], + 'enabled': [bitmask] + } + ), ] + py_result = ks_ldap.convert_ldap_result(result) + # The user id should be 0123456, and the enabled + # flag should be 225 + self.assertEqual(expected_bitmask, py_result[0][1]['enabled'][0]) + self.assertEqual(user_id, py_result[0][1]['user_id'][0]) + + def test_user_id_and_bitmask_begins_with_0(self): + user_id = '0123456' + bitmask = '0225' + expected_bitmask = 225 + result = [( + 'cn=dummy,dc=example,dc=com', + { + 'user_id': [user_id], + 'enabled': [bitmask] + } + ), ] + py_result = ks_ldap.convert_ldap_result(result) + # The user id should be 0123456, and the enabled + # flag should be 225, the 0 is dropped. + self.assertEqual(expected_bitmask, py_result[0][1]['enabled'][0]) + self.assertEqual(user_id, py_result[0][1]['user_id'][0]) + + def test_user_id_and_user_name_with_boolean_string(self): + boolean_strings = ['TRUE', 'FALSE', 'true', 'false', 'True', 'False', + 'TrUe' 'FaLse'] + for user_name in boolean_strings: + user_id = uuid.uuid4().hex + result = [( + 'cn=dummy,dc=example,dc=com', + { + 'user_id': [user_id], + 'user_name': [user_name] + } + ), ] + py_result = ks_ldap.convert_ldap_result(result) + # The user name should still be a string value. + self.assertEqual(user_name, py_result[0][1]['user_name'][0]) diff --git a/keystone-moon/keystone/tests/unit/common/test_notifications.py b/keystone-moon/keystone/tests/unit/common/test_notifications.py new file mode 100644 index 00000000..55dd556d --- /dev/null +++ b/keystone-moon/keystone/tests/unit/common/test_notifications.py @@ -0,0 +1,974 @@ +# Copyright 2013 IBM Corp. +# +# 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 uuid + +import mock +from oslo_config import cfg +from oslo_config import fixture as config_fixture +from oslotest import mockpatch +from pycadf import cadftaxonomy +from pycadf import cadftype +from pycadf import eventfactory +from pycadf import resource as cadfresource +import testtools + +from keystone.common import dependency +from keystone import notifications +from keystone.tests.unit import test_v3 + + +CONF = cfg.CONF + +EXP_RESOURCE_TYPE = uuid.uuid4().hex +CREATED_OPERATION = notifications.ACTIONS.created +UPDATED_OPERATION = notifications.ACTIONS.updated +DELETED_OPERATION = notifications.ACTIONS.deleted +DISABLED_OPERATION = notifications.ACTIONS.disabled + + +class ArbitraryException(Exception): + pass + + +def register_callback(operation, resource_type=EXP_RESOURCE_TYPE): + """Helper for creating and registering a mock callback. + + """ + callback = mock.Mock(__name__='callback', + im_class=mock.Mock(__name__='class')) + notifications.register_event_callback(operation, resource_type, callback) + return callback + + +class AuditNotificationsTestCase(testtools.TestCase): + def setUp(self): + super(AuditNotificationsTestCase, self).setUp() + self.config_fixture = self.useFixture(config_fixture.Config(CONF)) + self.addCleanup(notifications.clear_subscribers) + + def _test_notification_operation(self, notify_function, operation): + exp_resource_id = uuid.uuid4().hex + callback = register_callback(operation) + notify_function(EXP_RESOURCE_TYPE, exp_resource_id) + callback.assert_called_once_with('identity', EXP_RESOURCE_TYPE, + operation, + {'resource_info': exp_resource_id}) + self.config_fixture.config(notification_format='cadf') + with mock.patch( + 'keystone.notifications._create_cadf_payload') as cadf_notify: + notify_function(EXP_RESOURCE_TYPE, exp_resource_id) + initiator = None + cadf_notify.assert_called_once_with( + operation, EXP_RESOURCE_TYPE, exp_resource_id, + notifications.taxonomy.OUTCOME_SUCCESS, initiator) + notify_function(EXP_RESOURCE_TYPE, exp_resource_id, public=False) + cadf_notify.assert_called_once_with( + operation, EXP_RESOURCE_TYPE, exp_resource_id, + notifications.taxonomy.OUTCOME_SUCCESS, initiator) + + def test_resource_created_notification(self): + self._test_notification_operation(notifications.Audit.created, + CREATED_OPERATION) + + def test_resource_updated_notification(self): + self._test_notification_operation(notifications.Audit.updated, + UPDATED_OPERATION) + + def test_resource_deleted_notification(self): + self._test_notification_operation(notifications.Audit.deleted, + DELETED_OPERATION) + + def test_resource_disabled_notification(self): + self._test_notification_operation(notifications.Audit.disabled, + DISABLED_OPERATION) + + +class NotificationsWrapperTestCase(testtools.TestCase): + def create_fake_ref(self): + resource_id = uuid.uuid4().hex + return resource_id, { + 'id': resource_id, + 'key': uuid.uuid4().hex + } + + @notifications.created(EXP_RESOURCE_TYPE) + def create_resource(self, resource_id, data): + return data + + def test_resource_created_notification(self): + exp_resource_id, data = self.create_fake_ref() + callback = register_callback(CREATED_OPERATION) + + self.create_resource(exp_resource_id, data) + callback.assert_called_with('identity', EXP_RESOURCE_TYPE, + CREATED_OPERATION, + {'resource_info': exp_resource_id}) + + @notifications.updated(EXP_RESOURCE_TYPE) + def update_resource(self, resource_id, data): + return data + + def test_resource_updated_notification(self): + exp_resource_id, data = self.create_fake_ref() + callback = register_callback(UPDATED_OPERATION) + + self.update_resource(exp_resource_id, data) + callback.assert_called_with('identity', EXP_RESOURCE_TYPE, + UPDATED_OPERATION, + {'resource_info': exp_resource_id}) + + @notifications.deleted(EXP_RESOURCE_TYPE) + def delete_resource(self, resource_id): + pass + + def test_resource_deleted_notification(self): + exp_resource_id = uuid.uuid4().hex + callback = register_callback(DELETED_OPERATION) + + self.delete_resource(exp_resource_id) + callback.assert_called_with('identity', EXP_RESOURCE_TYPE, + DELETED_OPERATION, + {'resource_info': exp_resource_id}) + + @notifications.created(EXP_RESOURCE_TYPE) + def create_exception(self, resource_id): + raise ArbitraryException() + + def test_create_exception_without_notification(self): + callback = register_callback(CREATED_OPERATION) + self.assertRaises( + ArbitraryException, self.create_exception, uuid.uuid4().hex) + self.assertFalse(callback.called) + + @notifications.created(EXP_RESOURCE_TYPE) + def update_exception(self, resource_id): + raise ArbitraryException() + + def test_update_exception_without_notification(self): + callback = register_callback(UPDATED_OPERATION) + self.assertRaises( + ArbitraryException, self.update_exception, uuid.uuid4().hex) + self.assertFalse(callback.called) + + @notifications.deleted(EXP_RESOURCE_TYPE) + def delete_exception(self, resource_id): + raise ArbitraryException() + + def test_delete_exception_without_notification(self): + callback = register_callback(DELETED_OPERATION) + self.assertRaises( + ArbitraryException, self.delete_exception, uuid.uuid4().hex) + self.assertFalse(callback.called) + + +class NotificationsTestCase(testtools.TestCase): + def setUp(self): + super(NotificationsTestCase, self).setUp() + + # these should use self.config_fixture.config(), but they haven't + # been registered yet + CONF.rpc_backend = 'fake' + CONF.notification_driver = ['fake'] + + def test_send_notification(self): + """Test the private method _send_notification to ensure event_type, + payload, and context are built and passed properly. + """ + resource = uuid.uuid4().hex + resource_type = EXP_RESOURCE_TYPE + operation = CREATED_OPERATION + + # NOTE(ldbragst): Even though notifications._send_notification doesn't + # contain logic that creates cases, this is supposed to test that + # context is always empty and that we ensure the resource ID of the + # resource in the notification is contained in the payload. It was + # agreed that context should be empty in Keystone's case, which is + # also noted in the /keystone/notifications.py module. This test + # ensures and maintains these conditions. + expected_args = [ + {}, # empty context + 'identity.%s.created' % resource_type, # event_type + {'resource_info': resource}, # payload + 'INFO', # priority is always INFO... + ] + + with mock.patch.object(notifications._get_notifier(), + '_notify') as mocked: + notifications._send_notification(operation, resource_type, + resource) + mocked.assert_called_once_with(*expected_args) + + +class BaseNotificationTest(test_v3.RestfulTestCase): + + def setUp(self): + super(BaseNotificationTest, self).setUp() + + self._notifications = [] + self._audits = [] + + def fake_notify(operation, resource_type, resource_id, + public=True): + note = { + 'resource_id': resource_id, + 'operation': operation, + 'resource_type': resource_type, + 'send_notification_called': True, + 'public': public} + self._notifications.append(note) + + self.useFixture(mockpatch.PatchObject( + notifications, '_send_notification', fake_notify)) + + def fake_audit(action, initiator, outcome, target, + event_type, **kwargs): + service_security = cadftaxonomy.SERVICE_SECURITY + + event = eventfactory.EventFactory().new_event( + eventType=cadftype.EVENTTYPE_ACTIVITY, + outcome=outcome, + action=action, + initiator=initiator, + target=target, + observer=cadfresource.Resource(typeURI=service_security)) + + for key, value in kwargs.items(): + setattr(event, key, value) + + audit = { + 'payload': event.as_dict(), + 'event_type': event_type, + 'send_notification_called': True} + self._audits.append(audit) + + self.useFixture(mockpatch.PatchObject( + notifications, '_send_audit_notification', fake_audit)) + + def _assert_last_note(self, resource_id, operation, resource_type): + # NOTE(stevemar): If 'basic' format is not used, then simply + # return since this assertion is not valid. + if CONF.notification_format != 'basic': + return + self.assertTrue(len(self._notifications) > 0) + note = self._notifications[-1] + self.assertEqual(note['operation'], operation) + self.assertEqual(note['resource_id'], resource_id) + self.assertEqual(note['resource_type'], resource_type) + self.assertTrue(note['send_notification_called']) + + def _assert_last_audit(self, resource_id, operation, resource_type, + target_uri): + # NOTE(stevemar): If 'cadf' format is not used, then simply + # return since this assertion is not valid. + if CONF.notification_format != 'cadf': + return + self.assertTrue(len(self._audits) > 0) + audit = self._audits[-1] + payload = audit['payload'] + self.assertEqual(resource_id, payload['resource_info']) + action = '%s.%s' % (operation, resource_type) + self.assertEqual(action, payload['action']) + self.assertEqual(target_uri, payload['target']['typeURI']) + self.assertEqual(resource_id, payload['target']['id']) + event_type = '%s.%s.%s' % ('identity', resource_type, operation) + self.assertEqual(event_type, audit['event_type']) + self.assertTrue(audit['send_notification_called']) + + def _assert_notify_not_sent(self, resource_id, operation, resource_type, + public=True): + unexpected = { + 'resource_id': resource_id, + 'operation': operation, + 'resource_type': resource_type, + 'send_notification_called': True, + 'public': public} + for note in self._notifications: + self.assertNotEqual(unexpected, note) + + def _assert_notify_sent(self, resource_id, operation, resource_type, + public=True): + expected = { + 'resource_id': resource_id, + 'operation': operation, + 'resource_type': resource_type, + 'send_notification_called': True, + 'public': public} + for note in self._notifications: + if expected == note: + break + else: + self.fail("Notification not sent.") + + +class NotificationsForEntities(BaseNotificationTest): + + def test_create_group(self): + group_ref = self.new_group_ref(domain_id=self.domain_id) + group_ref = self.identity_api.create_group(group_ref) + self._assert_last_note(group_ref['id'], CREATED_OPERATION, 'group') + self._assert_last_audit(group_ref['id'], CREATED_OPERATION, 'group', + cadftaxonomy.SECURITY_GROUP) + + def test_create_project(self): + project_ref = self.new_project_ref(domain_id=self.domain_id) + self.assignment_api.create_project(project_ref['id'], project_ref) + self._assert_last_note( + project_ref['id'], CREATED_OPERATION, 'project') + self._assert_last_audit(project_ref['id'], CREATED_OPERATION, + 'project', cadftaxonomy.SECURITY_PROJECT) + + def test_create_role(self): + role_ref = self.new_role_ref() + self.role_api.create_role(role_ref['id'], role_ref) + self._assert_last_note(role_ref['id'], CREATED_OPERATION, 'role') + self._assert_last_audit(role_ref['id'], CREATED_OPERATION, 'role', + cadftaxonomy.SECURITY_ROLE) + + def test_create_user(self): + user_ref = self.new_user_ref(domain_id=self.domain_id) + user_ref = self.identity_api.create_user(user_ref) + self._assert_last_note(user_ref['id'], CREATED_OPERATION, 'user') + self._assert_last_audit(user_ref['id'], CREATED_OPERATION, 'user', + cadftaxonomy.SECURITY_ACCOUNT_USER) + + def test_create_trust(self): + trustor = self.new_user_ref(domain_id=self.domain_id) + trustor = self.identity_api.create_user(trustor) + trustee = self.new_user_ref(domain_id=self.domain_id) + trustee = self.identity_api.create_user(trustee) + role_ref = self.new_role_ref() + self.role_api.create_role(role_ref['id'], role_ref) + trust_ref = self.new_trust_ref(trustor['id'], + trustee['id']) + self.trust_api.create_trust(trust_ref['id'], + trust_ref, + [role_ref]) + self._assert_last_note( + trust_ref['id'], CREATED_OPERATION, 'OS-TRUST:trust') + self._assert_last_audit(trust_ref['id'], CREATED_OPERATION, + 'OS-TRUST:trust', cadftaxonomy.SECURITY_TRUST) + + def test_delete_group(self): + group_ref = self.new_group_ref(domain_id=self.domain_id) + group_ref = self.identity_api.create_group(group_ref) + self.identity_api.delete_group(group_ref['id']) + self._assert_last_note(group_ref['id'], DELETED_OPERATION, 'group') + self._assert_last_audit(group_ref['id'], DELETED_OPERATION, 'group', + cadftaxonomy.SECURITY_GROUP) + + def test_delete_project(self): + project_ref = self.new_project_ref(domain_id=self.domain_id) + self.assignment_api.create_project(project_ref['id'], project_ref) + self.assignment_api.delete_project(project_ref['id']) + self._assert_last_note( + project_ref['id'], DELETED_OPERATION, 'project') + self._assert_last_audit(project_ref['id'], DELETED_OPERATION, + 'project', cadftaxonomy.SECURITY_PROJECT) + + def test_delete_role(self): + role_ref = self.new_role_ref() + self.role_api.create_role(role_ref['id'], role_ref) + self.role_api.delete_role(role_ref['id']) + self._assert_last_note(role_ref['id'], DELETED_OPERATION, 'role') + self._assert_last_audit(role_ref['id'], DELETED_OPERATION, 'role', + cadftaxonomy.SECURITY_ROLE) + + def test_delete_user(self): + user_ref = self.new_user_ref(domain_id=self.domain_id) + user_ref = self.identity_api.create_user(user_ref) + self.identity_api.delete_user(user_ref['id']) + self._assert_last_note(user_ref['id'], DELETED_OPERATION, 'user') + self._assert_last_audit(user_ref['id'], DELETED_OPERATION, 'user', + cadftaxonomy.SECURITY_ACCOUNT_USER) + + def test_create_domain(self): + domain_ref = self.new_domain_ref() + self.resource_api.create_domain(domain_ref['id'], domain_ref) + self._assert_last_note(domain_ref['id'], CREATED_OPERATION, 'domain') + self._assert_last_audit(domain_ref['id'], CREATED_OPERATION, 'domain', + cadftaxonomy.SECURITY_DOMAIN) + + def test_update_domain(self): + domain_ref = self.new_domain_ref() + self.assignment_api.create_domain(domain_ref['id'], domain_ref) + domain_ref['description'] = uuid.uuid4().hex + self.assignment_api.update_domain(domain_ref['id'], domain_ref) + self._assert_last_note(domain_ref['id'], UPDATED_OPERATION, 'domain') + self._assert_last_audit(domain_ref['id'], UPDATED_OPERATION, 'domain', + cadftaxonomy.SECURITY_DOMAIN) + + def test_delete_domain(self): + domain_ref = self.new_domain_ref() + self.assignment_api.create_domain(domain_ref['id'], domain_ref) + domain_ref['enabled'] = False + self.assignment_api.update_domain(domain_ref['id'], domain_ref) + self.assignment_api.delete_domain(domain_ref['id']) + self._assert_last_note(domain_ref['id'], DELETED_OPERATION, 'domain') + self._assert_last_audit(domain_ref['id'], DELETED_OPERATION, 'domain', + cadftaxonomy.SECURITY_DOMAIN) + + def test_delete_trust(self): + trustor = self.new_user_ref(domain_id=self.domain_id) + trustor = self.identity_api.create_user(trustor) + trustee = self.new_user_ref(domain_id=self.domain_id) + trustee = self.identity_api.create_user(trustee) + role_ref = self.new_role_ref() + trust_ref = self.new_trust_ref(trustor['id'], trustee['id']) + self.trust_api.create_trust(trust_ref['id'], + trust_ref, + [role_ref]) + self.trust_api.delete_trust(trust_ref['id']) + self._assert_last_note( + trust_ref['id'], DELETED_OPERATION, 'OS-TRUST:trust') + self._assert_last_audit(trust_ref['id'], DELETED_OPERATION, + 'OS-TRUST:trust', cadftaxonomy.SECURITY_TRUST) + + def test_create_endpoint(self): + endpoint_ref = self.new_endpoint_ref(service_id=self.service_id) + self.catalog_api.create_endpoint(endpoint_ref['id'], endpoint_ref) + self._assert_notify_sent(endpoint_ref['id'], CREATED_OPERATION, + 'endpoint') + self._assert_last_audit(endpoint_ref['id'], CREATED_OPERATION, + 'endpoint', cadftaxonomy.SECURITY_ENDPOINT) + + def test_update_endpoint(self): + endpoint_ref = self.new_endpoint_ref(service_id=self.service_id) + self.catalog_api.create_endpoint(endpoint_ref['id'], endpoint_ref) + self.catalog_api.update_endpoint(endpoint_ref['id'], endpoint_ref) + self._assert_notify_sent(endpoint_ref['id'], UPDATED_OPERATION, + 'endpoint') + self._assert_last_audit(endpoint_ref['id'], UPDATED_OPERATION, + 'endpoint', cadftaxonomy.SECURITY_ENDPOINT) + + def test_delete_endpoint(self): + endpoint_ref = self.new_endpoint_ref(service_id=self.service_id) + self.catalog_api.create_endpoint(endpoint_ref['id'], endpoint_ref) + self.catalog_api.delete_endpoint(endpoint_ref['id']) + self._assert_notify_sent(endpoint_ref['id'], DELETED_OPERATION, + 'endpoint') + self._assert_last_audit(endpoint_ref['id'], DELETED_OPERATION, + 'endpoint', cadftaxonomy.SECURITY_ENDPOINT) + + def test_create_service(self): + service_ref = self.new_service_ref() + self.catalog_api.create_service(service_ref['id'], service_ref) + self._assert_notify_sent(service_ref['id'], CREATED_OPERATION, + 'service') + self._assert_last_audit(service_ref['id'], CREATED_OPERATION, + 'service', cadftaxonomy.SECURITY_SERVICE) + + def test_update_service(self): + service_ref = self.new_service_ref() + self.catalog_api.create_service(service_ref['id'], service_ref) + self.catalog_api.update_service(service_ref['id'], service_ref) + self._assert_notify_sent(service_ref['id'], UPDATED_OPERATION, + 'service') + self._assert_last_audit(service_ref['id'], UPDATED_OPERATION, + 'service', cadftaxonomy.SECURITY_SERVICE) + + def test_delete_service(self): + service_ref = self.new_service_ref() + self.catalog_api.create_service(service_ref['id'], service_ref) + self.catalog_api.delete_service(service_ref['id']) + self._assert_notify_sent(service_ref['id'], DELETED_OPERATION, + 'service') + self._assert_last_audit(service_ref['id'], DELETED_OPERATION, + 'service', cadftaxonomy.SECURITY_SERVICE) + + def test_create_region(self): + region_ref = self.new_region_ref() + self.catalog_api.create_region(region_ref) + self._assert_notify_sent(region_ref['id'], CREATED_OPERATION, + 'region') + self._assert_last_audit(region_ref['id'], CREATED_OPERATION, + 'region', cadftaxonomy.SECURITY_REGION) + + def test_update_region(self): + region_ref = self.new_region_ref() + self.catalog_api.create_region(region_ref) + self.catalog_api.update_region(region_ref['id'], region_ref) + self._assert_notify_sent(region_ref['id'], UPDATED_OPERATION, + 'region') + self._assert_last_audit(region_ref['id'], UPDATED_OPERATION, + 'region', cadftaxonomy.SECURITY_REGION) + + def test_delete_region(self): + region_ref = self.new_region_ref() + self.catalog_api.create_region(region_ref) + self.catalog_api.delete_region(region_ref['id']) + self._assert_notify_sent(region_ref['id'], DELETED_OPERATION, + 'region') + self._assert_last_audit(region_ref['id'], DELETED_OPERATION, + 'region', cadftaxonomy.SECURITY_REGION) + + def test_create_policy(self): + policy_ref = self.new_policy_ref() + self.policy_api.create_policy(policy_ref['id'], policy_ref) + self._assert_notify_sent(policy_ref['id'], CREATED_OPERATION, + 'policy') + self._assert_last_audit(policy_ref['id'], CREATED_OPERATION, + 'policy', cadftaxonomy.SECURITY_POLICY) + + def test_update_policy(self): + policy_ref = self.new_policy_ref() + self.policy_api.create_policy(policy_ref['id'], policy_ref) + self.policy_api.update_policy(policy_ref['id'], policy_ref) + self._assert_notify_sent(policy_ref['id'], UPDATED_OPERATION, + 'policy') + self._assert_last_audit(policy_ref['id'], UPDATED_OPERATION, + 'policy', cadftaxonomy.SECURITY_POLICY) + + def test_delete_policy(self): + policy_ref = self.new_policy_ref() + self.policy_api.create_policy(policy_ref['id'], policy_ref) + self.policy_api.delete_policy(policy_ref['id']) + self._assert_notify_sent(policy_ref['id'], DELETED_OPERATION, + 'policy') + self._assert_last_audit(policy_ref['id'], DELETED_OPERATION, + 'policy', cadftaxonomy.SECURITY_POLICY) + + def test_disable_domain(self): + domain_ref = self.new_domain_ref() + self.assignment_api.create_domain(domain_ref['id'], domain_ref) + domain_ref['enabled'] = False + self.assignment_api.update_domain(domain_ref['id'], domain_ref) + self._assert_notify_sent(domain_ref['id'], 'disabled', 'domain', + public=False) + + def test_disable_of_disabled_domain_does_not_notify(self): + domain_ref = self.new_domain_ref() + domain_ref['enabled'] = False + self.assignment_api.create_domain(domain_ref['id'], domain_ref) + # The domain_ref above is not changed during the create process. We + # can use the same ref to perform the update. + self.assignment_api.update_domain(domain_ref['id'], domain_ref) + self._assert_notify_not_sent(domain_ref['id'], 'disabled', 'domain', + public=False) + + def test_update_group(self): + group_ref = self.new_group_ref(domain_id=self.domain_id) + group_ref = self.identity_api.create_group(group_ref) + self.identity_api.update_group(group_ref['id'], group_ref) + self._assert_last_note(group_ref['id'], UPDATED_OPERATION, 'group') + self._assert_last_audit(group_ref['id'], UPDATED_OPERATION, 'group', + cadftaxonomy.SECURITY_GROUP) + + def test_update_project(self): + project_ref = self.new_project_ref(domain_id=self.domain_id) + self.assignment_api.create_project(project_ref['id'], project_ref) + self.assignment_api.update_project(project_ref['id'], project_ref) + self._assert_notify_sent( + project_ref['id'], UPDATED_OPERATION, 'project', public=True) + self._assert_last_audit(project_ref['id'], UPDATED_OPERATION, + 'project', cadftaxonomy.SECURITY_PROJECT) + + def test_disable_project(self): + project_ref = self.new_project_ref(domain_id=self.domain_id) + self.assignment_api.create_project(project_ref['id'], project_ref) + project_ref['enabled'] = False + self.assignment_api.update_project(project_ref['id'], project_ref) + self._assert_notify_sent(project_ref['id'], 'disabled', 'project', + public=False) + + def test_disable_of_disabled_project_does_not_notify(self): + project_ref = self.new_project_ref(domain_id=self.domain_id) + project_ref['enabled'] = False + self.assignment_api.create_project(project_ref['id'], project_ref) + # The project_ref above is not changed during the create process. We + # can use the same ref to perform the update. + self.assignment_api.update_project(project_ref['id'], project_ref) + self._assert_notify_not_sent(project_ref['id'], 'disabled', 'project', + public=False) + + def test_update_project_does_not_send_disable(self): + project_ref = self.new_project_ref(domain_id=self.domain_id) + self.assignment_api.create_project(project_ref['id'], project_ref) + project_ref['enabled'] = True + self.assignment_api.update_project(project_ref['id'], project_ref) + self._assert_last_note( + project_ref['id'], UPDATED_OPERATION, 'project') + self._assert_notify_not_sent(project_ref['id'], 'disabled', 'project') + + def test_update_role(self): + role_ref = self.new_role_ref() + self.role_api.create_role(role_ref['id'], role_ref) + self.role_api.update_role(role_ref['id'], role_ref) + self._assert_last_note(role_ref['id'], UPDATED_OPERATION, 'role') + self._assert_last_audit(role_ref['id'], UPDATED_OPERATION, 'role', + cadftaxonomy.SECURITY_ROLE) + + def test_update_user(self): + user_ref = self.new_user_ref(domain_id=self.domain_id) + user_ref = self.identity_api.create_user(user_ref) + self.identity_api.update_user(user_ref['id'], user_ref) + self._assert_last_note(user_ref['id'], UPDATED_OPERATION, 'user') + self._assert_last_audit(user_ref['id'], UPDATED_OPERATION, 'user', + cadftaxonomy.SECURITY_ACCOUNT_USER) + + def test_config_option_no_events(self): + self.config_fixture.config(notification_format='basic') + role_ref = self.new_role_ref() + self.role_api.create_role(role_ref['id'], role_ref) + # The regular notifications will still be emitted, since they are + # used for callback handling. + self._assert_last_note(role_ref['id'], CREATED_OPERATION, 'role') + # No audit event should have occurred + self.assertEqual(0, len(self._audits)) + + +class CADFNotificationsForEntities(NotificationsForEntities): + + def setUp(self): + super(CADFNotificationsForEntities, self).setUp() + self.config_fixture.config(notification_format='cadf') + + def test_initiator_data_is_set(self): + ref = self.new_domain_ref() + resp = self.post('/domains', body={'domain': ref}) + resource_id = resp.result.get('domain').get('id') + self._assert_last_audit(resource_id, CREATED_OPERATION, 'domain', + cadftaxonomy.SECURITY_DOMAIN) + self.assertTrue(len(self._audits) > 0) + audit = self._audits[-1] + payload = audit['payload'] + self.assertEqual(self.user_id, payload['initiator']['id']) + self.assertEqual(self.project_id, payload['initiator']['project_id']) + + +class TestEventCallbacks(test_v3.RestfulTestCase): + + def setUp(self): + super(TestEventCallbacks, self).setUp() + self.has_been_called = False + + def _project_deleted_callback(self, service, resource_type, operation, + payload): + self.has_been_called = True + + def _project_created_callback(self, service, resource_type, operation, + payload): + self.has_been_called = True + + def test_notification_received(self): + callback = register_callback(CREATED_OPERATION, 'project') + project_ref = self.new_project_ref(domain_id=self.domain_id) + self.assignment_api.create_project(project_ref['id'], project_ref) + self.assertTrue(callback.called) + + def test_notification_method_not_callable(self): + fake_method = None + self.assertRaises(TypeError, + notifications.register_event_callback, + UPDATED_OPERATION, + 'project', + [fake_method]) + + def test_notification_event_not_valid(self): + self.assertRaises(ValueError, + notifications.register_event_callback, + uuid.uuid4().hex, + 'project', + self._project_deleted_callback) + + def test_event_registration_for_unknown_resource_type(self): + # Registration for unknown resource types should succeed. If no event + # is issued for that resource type, the callback wont be triggered. + notifications.register_event_callback(DELETED_OPERATION, + uuid.uuid4().hex, + self._project_deleted_callback) + resource_type = uuid.uuid4().hex + notifications.register_event_callback(DELETED_OPERATION, + resource_type, + self._project_deleted_callback) + + def test_provider_event_callbacks_subscription(self): + callback_called = [] + + @dependency.provider('foo_api') + class Foo(object): + def __init__(self): + self.event_callbacks = { + CREATED_OPERATION: {'project': [self.foo_callback]}} + + def foo_callback(self, service, resource_type, operation, + payload): + # uses callback_called from the closure + callback_called.append(True) + + Foo() + project_ref = self.new_project_ref(domain_id=self.domain_id) + self.assignment_api.create_project(project_ref['id'], project_ref) + self.assertEqual([True], callback_called) + + def test_invalid_event_callbacks(self): + @dependency.provider('foo_api') + class Foo(object): + def __init__(self): + self.event_callbacks = 'bogus' + + self.assertRaises(ValueError, Foo) + + def test_invalid_event_callbacks_event(self): + @dependency.provider('foo_api') + class Foo(object): + def __init__(self): + self.event_callbacks = {CREATED_OPERATION: 'bogus'} + + self.assertRaises(ValueError, Foo) + + +class CadfNotificationsWrapperTestCase(test_v3.RestfulTestCase): + + LOCAL_HOST = 'localhost' + ACTION = 'authenticate' + ROLE_ASSIGNMENT = 'role_assignment' + + def setUp(self): + super(CadfNotificationsWrapperTestCase, self).setUp() + self._notifications = [] + + def fake_notify(action, initiator, outcome, target, + event_type, **kwargs): + service_security = cadftaxonomy.SERVICE_SECURITY + + event = eventfactory.EventFactory().new_event( + eventType=cadftype.EVENTTYPE_ACTIVITY, + outcome=outcome, + action=action, + initiator=initiator, + target=target, + observer=cadfresource.Resource(typeURI=service_security)) + + for key, value in kwargs.items(): + setattr(event, key, value) + + note = { + 'action': action, + 'initiator': initiator, + 'event': event, + 'send_notification_called': True} + self._notifications.append(note) + + self.useFixture(mockpatch.PatchObject( + notifications, '_send_audit_notification', fake_notify)) + + def _assert_last_note(self, action, user_id): + self.assertTrue(self._notifications) + note = self._notifications[-1] + self.assertEqual(note['action'], action) + initiator = note['initiator'] + self.assertEqual(initiator.id, user_id) + self.assertEqual(initiator.host.address, self.LOCAL_HOST) + self.assertTrue(note['send_notification_called']) + + def _assert_event(self, role_id, project=None, domain=None, + user=None, group=None, inherit=False): + """Assert that the CADF event is valid. + + In the case of role assignments, the event will have extra data, + specifically, the role, target, actor, and if the role is inherited. + + An example event, as a dictionary is seen below: + { + 'typeURI': 'http://schemas.dmtf.org/cloud/audit/1.0/event', + 'initiator': { + 'typeURI': 'service/security/account/user', + 'host': {'address': 'localhost'}, + 'id': 'openstack:0a90d95d-582c-4efb-9cbc-e2ca7ca9c341', + 'name': u'bccc2d9bfc2a46fd9e33bcf82f0b5c21' + }, + 'target': { + 'typeURI': 'service/security/account/user', + 'id': 'openstack:d48ea485-ef70-4f65-8d2b-01aa9d7ec12d' + }, + 'observer': { + 'typeURI': 'service/security', + 'id': 'openstack:d51dd870-d929-4aba-8d75-dcd7555a0c95' + }, + 'eventType': 'activity', + 'eventTime': '2014-08-21T21:04:56.204536+0000', + 'role': u'0e6b990380154a2599ce6b6e91548a68', + 'domain': u'24bdcff1aab8474895dbaac509793de1', + 'inherited_to_projects': False, + 'group': u'c1e22dc67cbd469ea0e33bf428fe597a', + 'action': 'created.role_assignment', + 'outcome': 'success', + 'id': 'openstack:782689dd-f428-4f13-99c7-5c70f94a5ac1' + } + """ + + note = self._notifications[-1] + event = note['event'] + if project: + self.assertEqual(project, event.project) + if domain: + self.assertEqual(domain, event.domain) + if user: + self.assertEqual(user, event.user) + if group: + self.assertEqual(group, event.group) + self.assertEqual(role_id, event.role) + self.assertEqual(inherit, event.inherited_to_projects) + + def test_v3_authenticate_user_name_and_domain_id(self): + user_id = self.user_id + user_name = self.user['name'] + password = self.user['password'] + domain_id = self.domain_id + data = self.build_authentication_request(username=user_name, + user_domain_id=domain_id, + password=password) + self.post('/auth/tokens', body=data) + self._assert_last_note(self.ACTION, user_id) + + def test_v3_authenticate_user_id(self): + user_id = self.user_id + password = self.user['password'] + data = self.build_authentication_request(user_id=user_id, + password=password) + self.post('/auth/tokens', body=data) + self._assert_last_note(self.ACTION, user_id) + + def test_v3_authenticate_user_name_and_domain_name(self): + user_id = self.user_id + user_name = self.user['name'] + password = self.user['password'] + domain_name = self.domain['name'] + data = self.build_authentication_request(username=user_name, + user_domain_name=domain_name, + password=password) + self.post('/auth/tokens', body=data) + self._assert_last_note(self.ACTION, user_id) + + def _test_role_assignment(self, url, role, project=None, domain=None, + user=None, group=None): + self.put(url) + action = "%s.%s" % (CREATED_OPERATION, self.ROLE_ASSIGNMENT) + self._assert_last_note(action, self.user_id) + self._assert_event(role, project, domain, user, group) + self.delete(url) + action = "%s.%s" % (DELETED_OPERATION, self.ROLE_ASSIGNMENT) + self._assert_last_note(action, self.user_id) + self._assert_event(role, project, domain, user, group) + + def test_user_project_grant(self): + url = ('/projects/%s/users/%s/roles/%s' % + (self.project_id, self.user_id, self.role_id)) + self._test_role_assignment(url, self.role_id, + project=self.project_id, + user=self.user_id) + + def test_group_domain_grant(self): + group_ref = self.new_group_ref(domain_id=self.domain_id) + group = self.identity_api.create_group(group_ref) + url = ('/domains/%s/groups/%s/roles/%s' % + (self.domain_id, group['id'], self.role_id)) + self._test_role_assignment(url, self.role_id, + domain=self.domain_id, + group=group['id']) + + +class TestCallbackRegistration(testtools.TestCase): + def setUp(self): + super(TestCallbackRegistration, self).setUp() + self.mock_log = mock.Mock() + # Force the callback logging to occur + self.mock_log.logger.getEffectiveLevel.return_value = logging.DEBUG + + def verify_log_message(self, data): + """Tests that use this are a little brittle because adding more + logging can break them. + + TODO(dstanek): remove the need for this in a future refactoring + + """ + log_fn = self.mock_log.debug + self.assertEqual(len(data), log_fn.call_count) + for datum in data: + log_fn.assert_any_call(mock.ANY, datum) + + def test_a_function_callback(self): + def callback(*args, **kwargs): + pass + + resource_type = 'thing' + with mock.patch('keystone.notifications.LOG', self.mock_log): + notifications.register_event_callback( + CREATED_OPERATION, resource_type, callback) + + callback = 'keystone.tests.unit.common.test_notifications.callback' + expected_log_data = { + 'callback': callback, + 'event': 'identity.%s.created' % resource_type + } + self.verify_log_message([expected_log_data]) + + def test_a_method_callback(self): + class C(object): + def callback(self, *args, **kwargs): + pass + + with mock.patch('keystone.notifications.LOG', self.mock_log): + notifications.register_event_callback( + CREATED_OPERATION, 'thing', C.callback) + + callback = 'keystone.tests.unit.common.test_notifications.C.callback' + expected_log_data = { + 'callback': callback, + 'event': 'identity.thing.created' + } + self.verify_log_message([expected_log_data]) + + def test_a_list_of_callbacks(self): + def callback(*args, **kwargs): + pass + + class C(object): + def callback(self, *args, **kwargs): + pass + + with mock.patch('keystone.notifications.LOG', self.mock_log): + notifications.register_event_callback( + CREATED_OPERATION, 'thing', [callback, C.callback]) + + callback_1 = 'keystone.tests.unit.common.test_notifications.callback' + callback_2 = 'keystone.tests.unit.common.test_notifications.C.callback' + expected_log_data = [ + { + 'callback': callback_1, + 'event': 'identity.thing.created' + }, + { + 'callback': callback_2, + 'event': 'identity.thing.created' + }, + ] + self.verify_log_message(expected_log_data) + + def test_an_invalid_callback(self): + self.assertRaises(TypeError, + notifications.register_event_callback, + (CREATED_OPERATION, 'thing', object())) + + def test_an_invalid_event(self): + def callback(*args, **kwargs): + pass + + self.assertRaises(ValueError, + notifications.register_event_callback, + uuid.uuid4().hex, + 'thing', + callback) diff --git a/keystone-moon/keystone/tests/unit/common/test_pemutils.py b/keystone-moon/keystone/tests/unit/common/test_pemutils.py new file mode 100644 index 00000000..c2f58518 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/common/test_pemutils.py @@ -0,0 +1,337 @@ +# Copyright 2013 Red Hat, Inc. +# +# 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 + +from six import moves + +from keystone.common import pemutils +from keystone.tests import unit as tests + + +# List of 2-tuples, (pem_type, pem_header) +headers = pemutils.PEM_TYPE_TO_HEADER.items() + + +def make_data(size, offset=0): + return ''.join([chr(x % 255) for x in moves.range(offset, size + offset)]) + + +def make_base64_from_data(data): + return base64.b64encode(data) + + +def wrap_base64(base64_text): + wrapped_text = '\n'.join([base64_text[x:x + 64] + for x in moves.range(0, len(base64_text), 64)]) + wrapped_text += '\n' + return wrapped_text + + +def make_pem(header, data): + base64_text = make_base64_from_data(data) + wrapped_text = wrap_base64(base64_text) + + result = '-----BEGIN %s-----\n' % header + result += wrapped_text + result += '-----END %s-----\n' % header + + return result + + +class PEM(object): + """PEM text and it's associated data broken out, used for testing. + + """ + def __init__(self, pem_header='CERTIFICATE', pem_type='cert', + data_size=70, data_offset=0): + self.pem_header = pem_header + self.pem_type = pem_type + self.data_size = data_size + self.data_offset = data_offset + self.data = make_data(self.data_size, self.data_offset) + self.base64_text = make_base64_from_data(self.data) + self.wrapped_base64 = wrap_base64(self.base64_text) + self.pem_text = make_pem(self.pem_header, self.data) + + +class TestPEMParseResult(tests.BaseTestCase): + + def test_pem_types(self): + for pem_type in pemutils.pem_types: + pem_header = pemutils.PEM_TYPE_TO_HEADER[pem_type] + r = pemutils.PEMParseResult(pem_type=pem_type) + self.assertEqual(pem_type, r.pem_type) + self.assertEqual(pem_header, r.pem_header) + + pem_type = 'xxx' + self.assertRaises(ValueError, + pemutils.PEMParseResult, pem_type=pem_type) + + def test_pem_headers(self): + for pem_header in pemutils.pem_headers: + pem_type = pemutils.PEM_HEADER_TO_TYPE[pem_header] + r = pemutils.PEMParseResult(pem_header=pem_header) + self.assertEqual(pem_type, r.pem_type) + self.assertEqual(pem_header, r.pem_header) + + pem_header = 'xxx' + self.assertRaises(ValueError, + pemutils.PEMParseResult, pem_header=pem_header) + + +class TestPEMParse(tests.BaseTestCase): + def test_parse_none(self): + text = '' + text += 'bla bla\n' + text += 'yada yada yada\n' + text += 'burfl blatz bingo\n' + + parse_results = pemutils.parse_pem(text) + self.assertEqual(0, len(parse_results)) + + self.assertEqual(False, pemutils.is_pem(text)) + + def test_parse_invalid(self): + p = PEM(pem_type='xxx', + pem_header='XXX') + text = p.pem_text + + self.assertRaises(ValueError, + pemutils.parse_pem, text) + + def test_parse_one(self): + data_size = 70 + count = len(headers) + pems = [] + + for i in moves.range(count): + pems.append(PEM(pem_type=headers[i][0], + pem_header=headers[i][1], + data_size=data_size + i, + data_offset=i)) + + for i in moves.range(count): + p = pems[i] + text = p.pem_text + + parse_results = pemutils.parse_pem(text) + self.assertEqual(1, len(parse_results)) + + r = parse_results[0] + self.assertEqual(p.pem_type, r.pem_type) + self.assertEqual(p.pem_header, r.pem_header) + self.assertEqual(p.pem_text, + text[r.pem_start:r.pem_end]) + self.assertEqual(p.wrapped_base64, + text[r.base64_start:r.base64_end]) + self.assertEqual(p.data, r.binary_data) + + def test_parse_one_embedded(self): + p = PEM(data_offset=0) + text = '' + text += 'bla bla\n' + text += 'yada yada yada\n' + text += p.pem_text + text += 'burfl blatz bingo\n' + + parse_results = pemutils.parse_pem(text) + self.assertEqual(1, len(parse_results)) + + r = parse_results[0] + self.assertEqual(p.pem_type, r.pem_type) + self.assertEqual(p.pem_header, r.pem_header) + self.assertEqual(p.pem_text, + text[r.pem_start:r.pem_end]) + self.assertEqual(p.wrapped_base64, + text[r.base64_start: r.base64_end]) + self.assertEqual(p.data, r.binary_data) + + def test_parse_multple(self): + data_size = 70 + count = len(headers) + pems = [] + text = '' + + for i in moves.range(count): + pems.append(PEM(pem_type=headers[i][0], + pem_header=headers[i][1], + data_size=data_size + i, + data_offset=i)) + + for i in moves.range(count): + text += pems[i].pem_text + + parse_results = pemutils.parse_pem(text) + self.assertEqual(count, len(parse_results)) + + for i in moves.range(count): + r = parse_results[i] + p = pems[i] + + self.assertEqual(p.pem_type, r.pem_type) + self.assertEqual(p.pem_header, r.pem_header) + self.assertEqual(p.pem_text, + text[r.pem_start:r.pem_end]) + self.assertEqual(p.wrapped_base64, + text[r.base64_start: r.base64_end]) + self.assertEqual(p.data, r.binary_data) + + def test_parse_multple_find_specific(self): + data_size = 70 + count = len(headers) + pems = [] + text = '' + + for i in moves.range(count): + pems.append(PEM(pem_type=headers[i][0], + pem_header=headers[i][1], + data_size=data_size + i, + data_offset=i)) + + for i in moves.range(count): + text += pems[i].pem_text + + for i in moves.range(count): + parse_results = pemutils.parse_pem(text, pem_type=headers[i][0]) + self.assertEqual(1, len(parse_results)) + + r = parse_results[0] + p = pems[i] + + self.assertEqual(p.pem_type, r.pem_type) + self.assertEqual(p.pem_header, r.pem_header) + self.assertEqual(p.pem_text, + text[r.pem_start:r.pem_end]) + self.assertEqual(p.wrapped_base64, + text[r.base64_start:r.base64_end]) + self.assertEqual(p.data, r.binary_data) + + def test_parse_multple_embedded(self): + data_size = 75 + count = len(headers) + pems = [] + text = '' + + for i in moves.range(count): + pems.append(PEM(pem_type=headers[i][0], + pem_header=headers[i][1], + data_size=data_size + i, + data_offset=i)) + + for i in moves.range(count): + text += 'bla bla\n' + text += 'yada yada yada\n' + text += pems[i].pem_text + text += 'burfl blatz bingo\n' + + parse_results = pemutils.parse_pem(text) + self.assertEqual(count, len(parse_results)) + + for i in moves.range(count): + r = parse_results[i] + p = pems[i] + + self.assertEqual(p.pem_type, r.pem_type) + self.assertEqual(p.pem_header, r.pem_header) + self.assertEqual(p.pem_text, + text[r.pem_start:r.pem_end]) + self.assertEqual(p.wrapped_base64, + text[r.base64_start:r.base64_end]) + self.assertEqual(p.data, r.binary_data) + + def test_get_pem_data_none(self): + text = '' + text += 'bla bla\n' + text += 'yada yada yada\n' + text += 'burfl blatz bingo\n' + + data = pemutils.get_pem_data(text) + self.assertIsNone(data) + + def test_get_pem_data_invalid(self): + p = PEM(pem_type='xxx', + pem_header='XXX') + text = p.pem_text + + self.assertRaises(ValueError, + pemutils.get_pem_data, text) + + def test_get_pem_data(self): + data_size = 70 + count = len(headers) + pems = [] + + for i in moves.range(count): + pems.append(PEM(pem_type=headers[i][0], + pem_header=headers[i][1], + data_size=data_size + i, + data_offset=i)) + + for i in moves.range(count): + p = pems[i] + text = p.pem_text + + data = pemutils.get_pem_data(text, p.pem_type) + self.assertEqual(p.data, data) + + def test_is_pem(self): + data_size = 70 + count = len(headers) + pems = [] + + for i in moves.range(count): + pems.append(PEM(pem_type=headers[i][0], + pem_header=headers[i][1], + data_size=data_size + i, + data_offset=i)) + + for i in moves.range(count): + p = pems[i] + text = p.pem_text + self.assertTrue(pemutils.is_pem(text, pem_type=p.pem_type)) + self.assertFalse(pemutils.is_pem(text, + pem_type=p.pem_type + 'xxx')) + + def test_base64_to_pem(self): + data_size = 70 + count = len(headers) + pems = [] + + for i in moves.range(count): + pems.append(PEM(pem_type=headers[i][0], + pem_header=headers[i][1], + data_size=data_size + i, + data_offset=i)) + + for i in moves.range(count): + p = pems[i] + pem = pemutils.base64_to_pem(p.base64_text, p.pem_type) + self.assertEqual(pemutils.get_pem_data(pem, p.pem_type), p.data) + + def test_binary_to_pem(self): + data_size = 70 + count = len(headers) + pems = [] + + for i in moves.range(count): + pems.append(PEM(pem_type=headers[i][0], + pem_header=headers[i][1], + data_size=data_size + i, + data_offset=i)) + + for i in moves.range(count): + p = pems[i] + pem = pemutils.binary_to_pem(p.data, p.pem_type) + self.assertEqual(pemutils.get_pem_data(pem, p.pem_type), p.data) diff --git a/keystone-moon/keystone/tests/unit/common/test_sql_core.py b/keystone-moon/keystone/tests/unit/common/test_sql_core.py new file mode 100644 index 00000000..1f33cfc3 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/common/test_sql_core.py @@ -0,0 +1,52 @@ +# 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 sqlalchemy.ext import declarative + +from keystone.common import sql +from keystone.tests import unit as tests +from keystone.tests.unit import utils + + +ModelBase = declarative.declarative_base() + + +class TestModel(ModelBase, sql.ModelDictMixin): + __tablename__ = 'testmodel' + id = sql.Column(sql.String(64), primary_key=True) + text = sql.Column(sql.String(64), nullable=False) + + +class TestModelDictMixin(tests.BaseTestCase): + + def test_creating_a_model_instance_from_a_dict(self): + d = {'id': utils.new_uuid(), 'text': utils.new_uuid()} + m = TestModel.from_dict(d) + self.assertEqual(m.id, d['id']) + self.assertEqual(m.text, d['text']) + + def test_creating_a_dict_from_a_model_instance(self): + m = TestModel(id=utils.new_uuid(), text=utils.new_uuid()) + d = m.to_dict() + self.assertEqual(m.id, d['id']) + self.assertEqual(m.text, d['text']) + + def test_creating_a_model_instance_from_an_invalid_dict(self): + d = {'id': utils.new_uuid(), 'text': utils.new_uuid(), 'extra': None} + self.assertRaises(TypeError, TestModel.from_dict, d) + + def test_creating_a_dict_from_a_model_instance_that_has_extra_attrs(self): + expected = {'id': utils.new_uuid(), 'text': utils.new_uuid()} + m = TestModel(id=expected['id'], text=expected['text']) + m.extra = 'this should not be in the dictionary' + self.assertEqual(m.to_dict(), expected) diff --git a/keystone-moon/keystone/tests/unit/common/test_utils.py b/keystone-moon/keystone/tests/unit/common/test_utils.py new file mode 100644 index 00000000..184c8141 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/common/test_utils.py @@ -0,0 +1,164 @@ +# 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 datetime +import uuid + +from oslo_config import cfg +from oslo_config import fixture as config_fixture +from oslo_serialization import jsonutils + +from keystone.common import utils as common_utils +from keystone import exception +from keystone import service +from keystone.tests import unit as tests +from keystone.tests.unit import utils + + +CONF = cfg.CONF + +TZ = utils.TZ + + +class UtilsTestCase(tests.BaseTestCase): + OPTIONAL = object() + + def setUp(self): + super(UtilsTestCase, self).setUp() + self.config_fixture = self.useFixture(config_fixture.Config(CONF)) + + def test_hash(self): + password = 'right' + wrong = 'wrongwrong' # Two wrongs don't make a right + hashed = common_utils.hash_password(password) + self.assertTrue(common_utils.check_password(password, hashed)) + self.assertFalse(common_utils.check_password(wrong, hashed)) + + def test_verify_normal_password_strict(self): + self.config_fixture.config(strict_password_check=False) + password = uuid.uuid4().hex + verified = common_utils.verify_length_and_trunc_password(password) + self.assertEqual(password, verified) + + def test_that_a_hash_can_not_be_validated_against_a_hash(self): + # NOTE(dstanek): Bug 1279849 reported a problem where passwords + # were not being hashed if they already looked like a hash. This + # would allow someone to hash their password ahead of time + # (potentially getting around password requirements, like + # length) and then they could auth with their original password. + password = uuid.uuid4().hex + hashed_password = common_utils.hash_password(password) + new_hashed_password = common_utils.hash_password(hashed_password) + self.assertFalse(common_utils.check_password(password, + new_hashed_password)) + + def test_verify_long_password_strict(self): + self.config_fixture.config(strict_password_check=False) + self.config_fixture.config(group='identity', max_password_length=5) + max_length = CONF.identity.max_password_length + invalid_password = 'passw0rd' + trunc = common_utils.verify_length_and_trunc_password(invalid_password) + self.assertEqual(invalid_password[:max_length], trunc) + + def test_verify_long_password_strict_raises_exception(self): + self.config_fixture.config(strict_password_check=True) + self.config_fixture.config(group='identity', max_password_length=5) + invalid_password = 'passw0rd' + self.assertRaises(exception.PasswordVerificationError, + common_utils.verify_length_and_trunc_password, + invalid_password) + + def test_hash_long_password_truncation(self): + self.config_fixture.config(strict_password_check=False) + invalid_length_password = '0' * 9999999 + hashed = common_utils.hash_password(invalid_length_password) + self.assertTrue(common_utils.check_password(invalid_length_password, + hashed)) + + def test_hash_long_password_strict(self): + self.config_fixture.config(strict_password_check=True) + invalid_length_password = '0' * 9999999 + self.assertRaises(exception.PasswordVerificationError, + common_utils.hash_password, + invalid_length_password) + + def _create_test_user(self, password=OPTIONAL): + user = {"name": "hthtest"} + if password is not self.OPTIONAL: + user['password'] = password + + return user + + def test_hash_user_password_without_password(self): + user = self._create_test_user() + hashed = common_utils.hash_user_password(user) + self.assertEqual(user, hashed) + + def test_hash_user_password_with_null_password(self): + user = self._create_test_user(password=None) + hashed = common_utils.hash_user_password(user) + self.assertEqual(user, hashed) + + def test_hash_user_password_with_empty_password(self): + password = '' + user = self._create_test_user(password=password) + user_hashed = common_utils.hash_user_password(user) + password_hashed = user_hashed['password'] + self.assertTrue(common_utils.check_password(password, password_hashed)) + + def test_hash_edge_cases(self): + hashed = common_utils.hash_password('secret') + self.assertFalse(common_utils.check_password('', hashed)) + self.assertFalse(common_utils.check_password(None, hashed)) + + def test_hash_unicode(self): + password = u'Comment \xe7a va' + wrong = 'Comment ?a va' + hashed = common_utils.hash_password(password) + self.assertTrue(common_utils.check_password(password, hashed)) + self.assertFalse(common_utils.check_password(wrong, hashed)) + + def test_auth_str_equal(self): + self.assertTrue(common_utils.auth_str_equal('abc123', 'abc123')) + self.assertFalse(common_utils.auth_str_equal('a', 'aaaaa')) + self.assertFalse(common_utils.auth_str_equal('aaaaa', 'a')) + self.assertFalse(common_utils.auth_str_equal('ABC123', 'abc123')) + + def test_unixtime(self): + global TZ + + @utils.timezone + def _test_unixtime(): + epoch = common_utils.unixtime(dt) + self.assertEqual(epoch, epoch_ans, "TZ=%s" % TZ) + + dt = datetime.datetime(1970, 1, 2, 3, 4, 56, 0) + epoch_ans = 56 + 4 * 60 + 3 * 3600 + 86400 + for d in ['+0', '-11', '-8', '-5', '+5', '+8', '+14']: + TZ = 'UTC' + d + _test_unixtime() + + def test_pki_encoder(self): + data = {'field': 'value'} + json = jsonutils.dumps(data, cls=common_utils.PKIEncoder) + expected_json = b'{"field":"value"}' + self.assertEqual(expected_json, json) + + +class ServiceHelperTests(tests.BaseTestCase): + + @service.fail_gracefully + def _do_test(self): + raise Exception("Test Exc") + + def test_fail_gracefully(self): + self.assertRaises(tests.UnexpectedExit, self._do_test) diff --git a/keystone-moon/keystone/tests/unit/config_files/backend_db2.conf b/keystone-moon/keystone/tests/unit/config_files/backend_db2.conf new file mode 100644 index 00000000..2bd0c1a6 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/config_files/backend_db2.conf @@ -0,0 +1,4 @@ +#Used for running the Migrate tests against a live DB2 Server +#See _sql_livetest.py +[database] +connection = ibm_db_sa://keystone:keystone@/staktest?charset=utf8 diff --git a/keystone-moon/keystone/tests/unit/config_files/backend_ldap.conf b/keystone-moon/keystone/tests/unit/config_files/backend_ldap.conf new file mode 100644 index 00000000..32161185 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/config_files/backend_ldap.conf @@ -0,0 +1,5 @@ +[ldap] +url = fake://memory +user = cn=Admin +password = password +suffix = cn=example,cn=com diff --git a/keystone-moon/keystone/tests/unit/config_files/backend_ldap_pool.conf b/keystone-moon/keystone/tests/unit/config_files/backend_ldap_pool.conf new file mode 100644 index 00000000..36fa1ac9 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/config_files/backend_ldap_pool.conf @@ -0,0 +1,41 @@ +[ldap] +url = fakepool://memory +user = cn=Admin +password = password +backend_entities = ['Tenant', 'User', 'UserRoleAssociation', 'Role', 'Group', 'Domain'] +suffix = cn=example,cn=com + +# Connection pooling specific attributes + +# Enable LDAP connection pooling. (boolean value) +use_pool=true + +# Connection pool size. (integer value) +pool_size=5 + +# Maximum count of reconnect trials. (integer value) +pool_retry_max=2 + +# Time span in seconds to wait between two reconnect trials. +# (floating point value) +pool_retry_delay=0.2 + +# Connector timeout in seconds. Value -1 indicates indefinite +# wait for response. (integer value) +pool_connection_timeout=-1 + +# Connection lifetime in seconds. +# (integer value) +pool_connection_lifetime=600 + +# Enable LDAP connection pooling for end user authentication. +# If use_pool is disabled, then this setting is meaningless +# and is not used at all. (boolean value) +use_auth_pool=true + +# End user auth connection pool size. (integer value) +auth_pool_size=50 + +# End user auth connection lifetime in seconds. (integer +# value) +auth_pool_connection_lifetime=60 \ No newline at end of file diff --git a/keystone-moon/keystone/tests/unit/config_files/backend_ldap_sql.conf b/keystone-moon/keystone/tests/unit/config_files/backend_ldap_sql.conf new file mode 100644 index 00000000..8a06f2f9 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/config_files/backend_ldap_sql.conf @@ -0,0 +1,14 @@ +[database] +#For a specific location file based sqlite use: +#connection = sqlite:////tmp/keystone.db +#To Test MySQL: +#connection = mysql://keystone:keystone@localhost/keystone?charset=utf8 +#To Test PostgreSQL: +#connection = postgresql://keystone:keystone@localhost/keystone?client_encoding=utf8 +idle_timeout = 200 + +[ldap] +url = fake://memory +user = cn=Admin +password = password +suffix = cn=example,cn=com diff --git a/keystone-moon/keystone/tests/unit/config_files/backend_liveldap.conf b/keystone-moon/keystone/tests/unit/config_files/backend_liveldap.conf new file mode 100644 index 00000000..59cb8577 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/config_files/backend_liveldap.conf @@ -0,0 +1,14 @@ +[ldap] +url = ldap://localhost +user = cn=Manager,dc=openstack,dc=org +password = test +suffix = dc=openstack,dc=org +group_tree_dn = ou=UserGroups,dc=openstack,dc=org +role_tree_dn = ou=Roles,dc=openstack,dc=org +project_tree_dn = ou=Projects,dc=openstack,dc=org +user_tree_dn = ou=Users,dc=openstack,dc=org +project_enabled_emulation = True +user_enabled_emulation = True +user_mail_attribute = mail +use_dumb_member = True + diff --git a/keystone-moon/keystone/tests/unit/config_files/backend_multi_ldap_sql.conf b/keystone-moon/keystone/tests/unit/config_files/backend_multi_ldap_sql.conf new file mode 100644 index 00000000..2d04d83d --- /dev/null +++ b/keystone-moon/keystone/tests/unit/config_files/backend_multi_ldap_sql.conf @@ -0,0 +1,9 @@ +[database] +connection = sqlite:// +#For a file based sqlite use +#connection = sqlite:////tmp/keystone.db +#To Test MySQL: +#connection = mysql://keystone:keystone@localhost/keystone?charset=utf8 +#To Test PostgreSQL: +#connection = postgresql://keystone:keystone@localhost/keystone?client_encoding=utf8 +idle_timeout = 200 diff --git a/keystone-moon/keystone/tests/unit/config_files/backend_mysql.conf b/keystone-moon/keystone/tests/unit/config_files/backend_mysql.conf new file mode 100644 index 00000000..d612f729 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/config_files/backend_mysql.conf @@ -0,0 +1,4 @@ +#Used for running the Migrate tests against a live Mysql Server +#See _sql_livetest.py +[database] +connection = mysql://keystone:keystone@localhost/keystone_test?charset=utf8 diff --git a/keystone-moon/keystone/tests/unit/config_files/backend_pool_liveldap.conf b/keystone-moon/keystone/tests/unit/config_files/backend_pool_liveldap.conf new file mode 100644 index 00000000..a85f5226 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/config_files/backend_pool_liveldap.conf @@ -0,0 +1,35 @@ +[ldap] +url = ldap://localhost +user = cn=Manager,dc=openstack,dc=org +password = test +suffix = dc=openstack,dc=org +group_tree_dn = ou=UserGroups,dc=openstack,dc=org +role_tree_dn = ou=Roles,dc=openstack,dc=org +project_tree_dn = ou=Projects,dc=openstack,dc=org +user_tree_dn = ou=Users,dc=openstack,dc=org +project_enabled_emulation = True +user_enabled_emulation = True +user_mail_attribute = mail +use_dumb_member = True + +# Connection pooling specific attributes + +# Enable LDAP connection pooling. (boolean value) +use_pool=true +# Connection pool size. (integer value) +pool_size=5 +# Connection lifetime in seconds. +# (integer value) +pool_connection_lifetime=60 + +# Enable LDAP connection pooling for end user authentication. +# If use_pool is disabled, then this setting is meaningless +# and is not used at all. (boolean value) +use_auth_pool=true + +# End user auth connection pool size. (integer value) +auth_pool_size=50 + +# End user auth connection lifetime in seconds. (integer +# value) +auth_pool_connection_lifetime=300 \ No newline at end of file diff --git a/keystone-moon/keystone/tests/unit/config_files/backend_postgresql.conf b/keystone-moon/keystone/tests/unit/config_files/backend_postgresql.conf new file mode 100644 index 00000000..001805df --- /dev/null +++ b/keystone-moon/keystone/tests/unit/config_files/backend_postgresql.conf @@ -0,0 +1,4 @@ +#Used for running the Migrate tests against a live Postgresql Server +#See _sql_livetest.py +[database] +connection = postgresql://keystone:keystone@localhost/keystone_test?client_encoding=utf8 diff --git a/keystone-moon/keystone/tests/unit/config_files/backend_sql.conf b/keystone-moon/keystone/tests/unit/config_files/backend_sql.conf new file mode 100644 index 00000000..9d401af3 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/config_files/backend_sql.conf @@ -0,0 +1,8 @@ +[database] +#For a specific location file based sqlite use: +#connection = sqlite:////tmp/keystone.db +#To Test MySQL: +#connection = mysql://keystone:keystone@localhost/keystone?charset=utf8 +#To Test PostgreSQL: +#connection = postgresql://keystone:keystone@localhost/keystone?client_encoding=utf8 +idle_timeout = 200 diff --git a/keystone-moon/keystone/tests/unit/config_files/backend_tls_liveldap.conf b/keystone-moon/keystone/tests/unit/config_files/backend_tls_liveldap.conf new file mode 100644 index 00000000..d35b9139 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/config_files/backend_tls_liveldap.conf @@ -0,0 +1,17 @@ +[ldap] +url = ldap:// +user = dc=Manager,dc=openstack,dc=org +password = test +suffix = dc=openstack,dc=org +group_tree_dn = ou=UserGroups,dc=openstack,dc=org +role_tree_dn = ou=Roles,dc=openstack,dc=org +project_tree_dn = ou=Projects,dc=openstack,dc=org +user_tree_dn = ou=Users,dc=openstack,dc=org +project_enabled_emulation = True +user_enabled_emulation = True +user_mail_attribute = mail +use_dumb_member = True +use_tls = True +tls_cacertfile = /etc/keystone/ssl/certs/cacert.pem +tls_cacertdir = /etc/keystone/ssl/certs/ +tls_req_cert = demand diff --git a/keystone-moon/keystone/tests/unit/config_files/deprecated.conf b/keystone-moon/keystone/tests/unit/config_files/deprecated.conf new file mode 100644 index 00000000..515e663a --- /dev/null +++ b/keystone-moon/keystone/tests/unit/config_files/deprecated.conf @@ -0,0 +1,8 @@ +# Options in this file are deprecated. See test_config. + +[sql] +# These options were deprecated in Icehouse with the switch to oslo's +# db.sqlalchemy. + +connection = sqlite://deprecated +idle_timeout = 54321 diff --git a/keystone-moon/keystone/tests/unit/config_files/deprecated_override.conf b/keystone-moon/keystone/tests/unit/config_files/deprecated_override.conf new file mode 100644 index 00000000..1d1c926f --- /dev/null +++ b/keystone-moon/keystone/tests/unit/config_files/deprecated_override.conf @@ -0,0 +1,15 @@ +# Options in this file are deprecated. See test_config. + +[sql] +# These options were deprecated in Icehouse with the switch to oslo's +# db.sqlalchemy. + +connection = sqlite://deprecated +idle_timeout = 54321 + + +[database] +# These are the new options from the [sql] section. + +connection = sqlite://new +idle_timeout = 65432 diff --git a/keystone-moon/keystone/tests/unit/config_files/domain_configs_default_ldap_one_sql/keystone.domain1.conf b/keystone-moon/keystone/tests/unit/config_files/domain_configs_default_ldap_one_sql/keystone.domain1.conf new file mode 100644 index 00000000..a4492a67 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/config_files/domain_configs_default_ldap_one_sql/keystone.domain1.conf @@ -0,0 +1,5 @@ +# The domain-specific configuration file for the test domain +# 'domain1' for use with unit tests. + +[identity] +driver = keystone.identity.backends.sql.Identity \ No newline at end of file diff --git a/keystone-moon/keystone/tests/unit/config_files/domain_configs_multi_ldap/keystone.Default.conf b/keystone-moon/keystone/tests/unit/config_files/domain_configs_multi_ldap/keystone.Default.conf new file mode 100644 index 00000000..7049afed --- /dev/null +++ b/keystone-moon/keystone/tests/unit/config_files/domain_configs_multi_ldap/keystone.Default.conf @@ -0,0 +1,14 @@ +# The domain-specific configuration file for the default domain for +# use with unit tests. +# +# The domain_name of the default domain is 'Default', hence the +# strange mix of upper/lower case in the file name. + +[ldap] +url = fake://memory +user = cn=Admin +password = password +suffix = cn=example,cn=com + +[identity] +driver = keystone.identity.backends.ldap.Identity \ No newline at end of file diff --git a/keystone-moon/keystone/tests/unit/config_files/domain_configs_multi_ldap/keystone.domain1.conf b/keystone-moon/keystone/tests/unit/config_files/domain_configs_multi_ldap/keystone.domain1.conf new file mode 100644 index 00000000..6b7e2488 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/config_files/domain_configs_multi_ldap/keystone.domain1.conf @@ -0,0 +1,11 @@ +# The domain-specific configuration file for the test domain +# 'domain1' for use with unit tests. + +[ldap] +url = fake://memory1 +user = cn=Admin +password = password +suffix = cn=example,cn=com + +[identity] +driver = keystone.identity.backends.ldap.Identity \ No newline at end of file diff --git a/keystone-moon/keystone/tests/unit/config_files/domain_configs_multi_ldap/keystone.domain2.conf b/keystone-moon/keystone/tests/unit/config_files/domain_configs_multi_ldap/keystone.domain2.conf new file mode 100644 index 00000000..0ed68eb9 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/config_files/domain_configs_multi_ldap/keystone.domain2.conf @@ -0,0 +1,13 @@ +# The domain-specific configuration file for the test domain +# 'domain2' for use with unit tests. + +[ldap] +url = fake://memory +user = cn=Admin +password = password +suffix = cn=myroot,cn=com +group_tree_dn = ou=UserGroups,dc=myroot,dc=org +user_tree_dn = ou=Users,dc=myroot,dc=org + +[identity] +driver = keystone.identity.backends.ldap.Identity \ No newline at end of file diff --git a/keystone-moon/keystone/tests/unit/config_files/domain_configs_one_extra_sql/keystone.domain2.conf b/keystone-moon/keystone/tests/unit/config_files/domain_configs_one_extra_sql/keystone.domain2.conf new file mode 100644 index 00000000..81b44462 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/config_files/domain_configs_one_extra_sql/keystone.domain2.conf @@ -0,0 +1,5 @@ +# The domain-specific configuration file for the test domain +# 'domain2' for use with unit tests. + +[identity] +driver = keystone.identity.backends.sql.Identity \ No newline at end of file diff --git a/keystone-moon/keystone/tests/unit/config_files/domain_configs_one_sql_one_ldap/keystone.Default.conf b/keystone-moon/keystone/tests/unit/config_files/domain_configs_one_sql_one_ldap/keystone.Default.conf new file mode 100644 index 00000000..7049afed --- /dev/null +++ b/keystone-moon/keystone/tests/unit/config_files/domain_configs_one_sql_one_ldap/keystone.Default.conf @@ -0,0 +1,14 @@ +# The domain-specific configuration file for the default domain for +# use with unit tests. +# +# The domain_name of the default domain is 'Default', hence the +# strange mix of upper/lower case in the file name. + +[ldap] +url = fake://memory +user = cn=Admin +password = password +suffix = cn=example,cn=com + +[identity] +driver = keystone.identity.backends.ldap.Identity \ No newline at end of file diff --git a/keystone-moon/keystone/tests/unit/config_files/domain_configs_one_sql_one_ldap/keystone.domain1.conf b/keystone-moon/keystone/tests/unit/config_files/domain_configs_one_sql_one_ldap/keystone.domain1.conf new file mode 100644 index 00000000..a4492a67 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/config_files/domain_configs_one_sql_one_ldap/keystone.domain1.conf @@ -0,0 +1,5 @@ +# The domain-specific configuration file for the test domain +# 'domain1' for use with unit tests. + +[identity] +driver = keystone.identity.backends.sql.Identity \ No newline at end of file diff --git a/keystone-moon/keystone/tests/unit/config_files/test_auth_plugin.conf b/keystone-moon/keystone/tests/unit/config_files/test_auth_plugin.conf new file mode 100644 index 00000000..abcc43ba --- /dev/null +++ b/keystone-moon/keystone/tests/unit/config_files/test_auth_plugin.conf @@ -0,0 +1,7 @@ +[auth] +methods = external,password,token,simple_challenge_response,saml2,openid,x509 +simple_challenge_response = keystone.tests.unit.test_auth_plugin.SimpleChallengeResponse +saml2 = keystone.auth.plugins.mapped.Mapped +openid = keystone.auth.plugins.mapped.Mapped +x509 = keystone.auth.plugins.mapped.Mapped + diff --git a/keystone-moon/keystone/tests/unit/core.py b/keystone-moon/keystone/tests/unit/core.py new file mode 100644 index 00000000..caca7dbd --- /dev/null +++ b/keystone-moon/keystone/tests/unit/core.py @@ -0,0 +1,660 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 __future__ import absolute_import +import atexit +import functools +import logging +import os +import re +import shutil +import socket +import sys +import warnings + +import fixtures +from oslo_config import cfg +from oslo_config import fixture as config_fixture +from oslo_log import log +import oslotest.base as oslotest +from oslotest import mockpatch +import six +from sqlalchemy import exc +from testtools import testcase +import webob + +# NOTE(ayoung) +# environment.use_eventlet must run before any of the code that will +# call the eventlet monkeypatching. +from keystone.common import environment # noqa +environment.use_eventlet() + +from keystone import auth +from keystone.common import config as common_cfg +from keystone.common import dependency +from keystone.common import kvs +from keystone.common.kvs import core as kvs_core +from keystone import config +from keystone import controllers +from keystone import exception +from keystone import notifications +from keystone.policy.backends import rules +from keystone.server import common +from keystone import service +from keystone.tests.unit import ksfixtures + + +config.configure() + +LOG = log.getLogger(__name__) +PID = six.text_type(os.getpid()) +TESTSDIR = os.path.dirname(os.path.abspath(__file__)) +TESTCONF = os.path.join(TESTSDIR, 'config_files') +ROOTDIR = os.path.normpath(os.path.join(TESTSDIR, '..', '..', '..')) +VENDOR = os.path.join(ROOTDIR, 'vendor') +ETCDIR = os.path.join(ROOTDIR, 'etc') + + +def _calc_tmpdir(): + env_val = os.environ.get('KEYSTONE_TEST_TEMP_DIR') + if not env_val: + return os.path.join(TESTSDIR, 'tmp', PID) + return os.path.join(env_val, PID) + + +TMPDIR = _calc_tmpdir() + +CONF = cfg.CONF +log.register_options(CONF) +rules.init() + +IN_MEM_DB_CONN_STRING = 'sqlite://' + +exception._FATAL_EXCEPTION_FORMAT_ERRORS = True +os.makedirs(TMPDIR) +atexit.register(shutil.rmtree, TMPDIR) + + +class dirs(object): + @staticmethod + def root(*p): + return os.path.join(ROOTDIR, *p) + + @staticmethod + def etc(*p): + return os.path.join(ETCDIR, *p) + + @staticmethod + def tests(*p): + return os.path.join(TESTSDIR, *p) + + @staticmethod + def tmp(*p): + return os.path.join(TMPDIR, *p) + + @staticmethod + def tests_conf(*p): + return os.path.join(TESTCONF, *p) + + +# keystone.common.sql.initialize() for testing. +DEFAULT_TEST_DB_FILE = dirs.tmp('test.db') + + +@atexit.register +def remove_test_databases(): + db = dirs.tmp('test.db') + if os.path.exists(db): + os.unlink(db) + pristine = dirs.tmp('test.db.pristine') + if os.path.exists(pristine): + os.unlink(pristine) + + +def generate_paste_config(extension_name): + # Generate a file, based on keystone-paste.ini, that is named: + # extension_name.ini, and includes extension_name in the pipeline + with open(dirs.etc('keystone-paste.ini'), 'r') as f: + contents = f.read() + + new_contents = contents.replace(' service_v3', + ' %s service_v3' % (extension_name)) + + new_paste_file = dirs.tmp(extension_name + '.ini') + with open(new_paste_file, 'w') as f: + f.write(new_contents) + + return new_paste_file + + +def remove_generated_paste_config(extension_name): + # Remove the generated paste config file, named extension_name.ini + paste_file_to_remove = dirs.tmp(extension_name + '.ini') + os.remove(paste_file_to_remove) + + +def skip_if_cache_disabled(*sections): + """This decorator is used to skip a test if caching is disabled either + globally or for the specific section. + + In the code fragment:: + + @skip_if_cache_is_disabled('assignment', 'token') + def test_method(*args): + ... + + The method test_method would be skipped if caching is disabled globally via + the `enabled` option in the `cache` section of the configuration or if + the `caching` option is set to false in either `assignment` or `token` + sections of the configuration. This decorator can be used with no + arguments to only check global caching. + + If a specified configuration section does not define the `caching` option, + this decorator makes the same assumption as the `should_cache_fn` in + keystone.common.cache that caching should be enabled. + """ + def wrapper(f): + @functools.wraps(f) + def inner(*args, **kwargs): + if not CONF.cache.enabled: + raise testcase.TestSkipped('Cache globally disabled.') + for s in sections: + conf_sec = getattr(CONF, s, None) + if conf_sec is not None: + if not getattr(conf_sec, 'caching', True): + raise testcase.TestSkipped('%s caching disabled.' % s) + return f(*args, **kwargs) + return inner + return wrapper + + +def skip_if_no_multiple_domains_support(f): + """This decorator is used to skip a test if an identity driver + does not support multiple domains. + """ + @functools.wraps(f) + def wrapper(*args, **kwargs): + test_obj = args[0] + if not test_obj.identity_api.multiple_domains_supported: + raise testcase.TestSkipped('No multiple domains support') + return f(*args, **kwargs) + return wrapper + + +class UnexpectedExit(Exception): + pass + + +class BadLog(Exception): + """Raised on invalid call to logging (parameter mismatch).""" + pass + + +class TestClient(object): + def __init__(self, app=None, token=None): + self.app = app + self.token = token + + def request(self, method, path, headers=None, body=None): + if headers is None: + headers = {} + + if self.token: + headers.setdefault('X-Auth-Token', self.token) + + req = webob.Request.blank(path) + req.method = method + for k, v in six.iteritems(headers): + req.headers[k] = v + if body: + req.body = body + return req.get_response(self.app) + + def get(self, path, headers=None): + return self.request('GET', path=path, headers=headers) + + def post(self, path, headers=None, body=None): + return self.request('POST', path=path, headers=headers, body=body) + + def put(self, path, headers=None, body=None): + return self.request('PUT', path=path, headers=headers, body=body) + + +class BaseTestCase(oslotest.BaseTestCase): + """Light weight base test class. + + This is a placeholder that will eventually go away once the + setup/teardown in TestCase is properly trimmed down to the bare + essentials. This is really just a play to speed up the tests by + eliminating unnecessary work. + """ + + def setUp(self): + super(BaseTestCase, self).setUp() + self.useFixture(mockpatch.PatchObject(sys, 'exit', + side_effect=UnexpectedExit)) + + def cleanup_instance(self, *names): + """Create a function suitable for use with self.addCleanup. + + :returns: a callable that uses a closure to delete instance attributes + + """ + def cleanup(): + for name in names: + # TODO(dstanek): remove this 'if' statement once + # load_backend in test_backend_ldap is only called once + # per test + if hasattr(self, name): + delattr(self, name) + return cleanup + + +@dependency.requires('revoke_api') +class TestCase(BaseTestCase): + + def config_files(self): + return [] + + def config_overrides(self): + signing_certfile = 'examples/pki/certs/signing_cert.pem' + signing_keyfile = 'examples/pki/private/signing_key.pem' + self.config_fixture.config(group='oslo_policy', + policy_file=dirs.etc('policy.json')) + self.config_fixture.config( + # TODO(morganfainberg): Make Cache Testing a separate test case + # in tempest, and move it out of the base unit tests. + group='cache', + backend='dogpile.cache.memory', + enabled=True, + proxies=['keystone.tests.unit.test_cache.CacheIsolatingProxy']) + self.config_fixture.config( + group='catalog', + driver='keystone.catalog.backends.templated.Catalog', + template_file=dirs.tests('default_catalog.templates')) + self.config_fixture.config( + group='identity', + driver='keystone.identity.backends.sql.Identity') + self.config_fixture.config( + group='kvs', + backends=[ + ('keystone.tests.unit.test_kvs.' + 'KVSBackendForcedKeyMangleFixture'), + 'keystone.tests.unit.test_kvs.KVSBackendFixture']) + self.config_fixture.config( + group='revoke', + driver='keystone.contrib.revoke.backends.kvs.Revoke') + self.config_fixture.config( + group='signing', certfile=signing_certfile, + keyfile=signing_keyfile, + ca_certs='examples/pki/certs/cacert.pem') + self.config_fixture.config( + group='token', + driver='keystone.token.persistence.backends.kvs.Token') + self.config_fixture.config( + group='trust', + driver='keystone.trust.backends.sql.Trust') + self.config_fixture.config( + group='saml', certfile=signing_certfile, keyfile=signing_keyfile) + self.config_fixture.config( + default_log_levels=[ + 'amqp=WARN', + 'amqplib=WARN', + 'boto=WARN', + 'qpid=WARN', + 'sqlalchemy=WARN', + 'suds=INFO', + 'oslo.messaging=INFO', + 'iso8601=WARN', + 'requests.packages.urllib3.connectionpool=WARN', + 'routes.middleware=INFO', + 'stevedore.extension=INFO', + 'keystone.notifications=INFO', + 'keystone.common._memcache_pool=INFO', + 'keystone.common.ldap=INFO', + ]) + self.auth_plugin_config_override() + + def auth_plugin_config_override(self, methods=None, **method_classes): + if methods is None: + methods = ['external', 'password', 'token', ] + if not method_classes: + method_classes = dict( + external='keystone.auth.plugins.external.DefaultDomain', + password='keystone.auth.plugins.password.Password', + token='keystone.auth.plugins.token.Token', + ) + self.config_fixture.config(group='auth', methods=methods) + common_cfg.setup_authentication() + if method_classes: + self.config_fixture.config(group='auth', **method_classes) + + def setUp(self): + super(TestCase, self).setUp() + self.addCleanup(self.cleanup_instance('config_fixture', 'logger')) + + self.addCleanup(CONF.reset) + + self.useFixture(mockpatch.PatchObject(logging.Handler, 'handleError', + side_effect=BadLog)) + self.config_fixture = self.useFixture(config_fixture.Config(CONF)) + self.config(self.config_files()) + + # NOTE(morganfainberg): mock the auth plugin setup to use the config + # fixture which automatically unregisters options when performing + # cleanup. + def mocked_register_auth_plugin_opt(conf, opt): + self.config_fixture.register_opt(opt, group='auth') + self.register_auth_plugin_opt_patch = self.useFixture( + mockpatch.PatchObject(common_cfg, '_register_auth_plugin_opt', + new=mocked_register_auth_plugin_opt)) + + self.config_overrides() + + self.logger = self.useFixture(fixtures.FakeLogger(level=logging.DEBUG)) + + # NOTE(morganfainberg): This code is a copy from the oslo-incubator + # log module. This is not in a function or otherwise available to use + # without having a CONF object to setup logging. This should help to + # reduce the log size by limiting what we log (similar to how Keystone + # would run under mod_wsgi or eventlet). + for pair in CONF.default_log_levels: + mod, _sep, level_name = pair.partition('=') + logger = logging.getLogger(mod) + logger.setLevel(level_name) + + warnings.filterwarnings('error', category=DeprecationWarning, + module='^keystone\\.') + warnings.simplefilter('error', exc.SAWarning) + self.addCleanup(warnings.resetwarnings) + + self.useFixture(ksfixtures.Cache()) + + # Clear the registry of providers so that providers from previous + # tests aren't used. + self.addCleanup(dependency.reset) + + self.addCleanup(kvs.INMEMDB.clear) + + # Ensure Notification subscriptions and resource types are empty + self.addCleanup(notifications.clear_subscribers) + self.addCleanup(notifications.reset_notifier) + + # Reset the auth-plugin registry + self.addCleanup(self.clear_auth_plugin_registry) + + self.addCleanup(setattr, controllers, '_VERSIONS', []) + + def config(self, config_files): + CONF(args=[], project='keystone', default_config_files=config_files) + + def load_backends(self): + """Initializes each manager and assigns them to an attribute.""" + + # TODO(blk-u): Shouldn't need to clear the registry here, but some + # tests call load_backends multiple times. These should be fixed to + # only call load_backends once. + dependency.reset() + + # TODO(morganfainberg): Shouldn't need to clear the registry here, but + # some tests call load_backends multiple times. Since it is not + # possible to re-configure a backend, we need to clear the list. This + # should eventually be removed once testing has been cleaned up. + kvs_core.KEY_VALUE_STORE_REGISTRY.clear() + + self.clear_auth_plugin_registry() + drivers, _unused = common.setup_backends( + load_extra_backends_fn=self.load_extra_backends) + + for manager_name, manager in six.iteritems(drivers): + setattr(self, manager_name, manager) + self.addCleanup(self.cleanup_instance(*drivers.keys())) + + def load_extra_backends(self): + """Override to load managers that aren't loaded by default. + + This is useful to load managers initialized by extensions. No extra + backends are loaded by default. + + :return: dict of name -> manager + """ + return {} + + def load_fixtures(self, fixtures): + """Hacky basic and naive fixture loading based on a python module. + + Expects that the various APIs into the various services are already + defined on `self`. + + """ + # NOTE(dstanek): create a list of attribute names to be removed + # from this instance during cleanup + fixtures_to_cleanup = [] + + # TODO(termie): doing something from json, probably based on Django's + # loaddata will be much preferred. + if (hasattr(self, 'identity_api') and + hasattr(self, 'assignment_api') and + hasattr(self, 'resource_api')): + for domain in fixtures.DOMAINS: + try: + rv = self.resource_api.create_domain(domain['id'], domain) + except exception.Conflict: + rv = self.resource_api.get_domain(domain['id']) + except exception.NotImplemented: + rv = domain + attrname = 'domain_%s' % domain['id'] + setattr(self, attrname, rv) + fixtures_to_cleanup.append(attrname) + + for tenant in fixtures.TENANTS: + if hasattr(self, 'tenant_%s' % tenant['id']): + try: + # This will clear out any roles on the project as well + self.resource_api.delete_project(tenant['id']) + except exception.ProjectNotFound: + pass + rv = self.resource_api.create_project( + tenant['id'], tenant) + + attrname = 'tenant_%s' % tenant['id'] + setattr(self, attrname, rv) + fixtures_to_cleanup.append(attrname) + + for role in fixtures.ROLES: + try: + rv = self.role_api.create_role(role['id'], role) + except exception.Conflict: + rv = self.role_api.get_role(role['id']) + attrname = 'role_%s' % role['id'] + setattr(self, attrname, rv) + fixtures_to_cleanup.append(attrname) + + for user in fixtures.USERS: + user_copy = user.copy() + tenants = user_copy.pop('tenants') + try: + existing_user = getattr(self, 'user_%s' % user['id'], None) + if existing_user is not None: + self.identity_api.delete_user(existing_user['id']) + except exception.UserNotFound: + pass + + # For users, the manager layer will generate the ID + user_copy = self.identity_api.create_user(user_copy) + # Our tests expect that the password is still in the user + # record so that they can reference it, so put it back into + # the dict returned. + user_copy['password'] = user['password'] + + for tenant_id in tenants: + try: + self.assignment_api.add_user_to_project( + tenant_id, user_copy['id']) + except exception.Conflict: + pass + # Use the ID from the fixture as the attribute name, so + # that our tests can easily reference each user dict, while + # the ID in the dict will be the real public ID. + attrname = 'user_%s' % user['id'] + setattr(self, attrname, user_copy) + fixtures_to_cleanup.append(attrname) + + self.addCleanup(self.cleanup_instance(*fixtures_to_cleanup)) + + def _paste_config(self, config): + if not config.startswith('config:'): + test_path = os.path.join(TESTSDIR, config) + etc_path = os.path.join(ROOTDIR, 'etc', config) + for path in [test_path, etc_path]: + if os.path.exists('%s-paste.ini' % path): + return 'config:%s-paste.ini' % path + return config + + def loadapp(self, config, name='main'): + return service.loadapp(self._paste_config(config), name=name) + + def clear_auth_plugin_registry(self): + auth.controllers.AUTH_METHODS.clear() + auth.controllers.AUTH_PLUGINS_LOADED = False + + def assertCloseEnoughForGovernmentWork(self, a, b, delta=3): + """Asserts that two datetimes are nearly equal within a small delta. + + :param delta: Maximum allowable time delta, defined in seconds. + """ + msg = '%s != %s within %s delta' % (a, b, delta) + + self.assertTrue(abs(a - b).seconds <= delta, msg) + + def assertNotEmpty(self, l): + self.assertTrue(len(l)) + + def assertDictEqual(self, d1, d2, msg=None): + self.assertIsInstance(d1, dict) + self.assertIsInstance(d2, dict) + self.assertEqual(d1, d2, msg) + + def assertRaisesRegexp(self, expected_exception, expected_regexp, + callable_obj, *args, **kwargs): + """Asserts that the message in a raised exception matches a regexp. + """ + try: + callable_obj(*args, **kwargs) + except expected_exception as exc_value: + if isinstance(expected_regexp, six.string_types): + expected_regexp = re.compile(expected_regexp) + + if isinstance(exc_value.args[0], unicode): + if not expected_regexp.search(unicode(exc_value)): + raise self.failureException( + '"%s" does not match "%s"' % + (expected_regexp.pattern, unicode(exc_value))) + else: + if not expected_regexp.search(str(exc_value)): + raise self.failureException( + '"%s" does not match "%s"' % + (expected_regexp.pattern, str(exc_value))) + else: + if hasattr(expected_exception, '__name__'): + excName = expected_exception.__name__ + else: + excName = str(expected_exception) + raise self.failureException("%s not raised" % excName) + + def assertDictContainsSubset(self, expected, actual, msg=None): + """Checks whether actual is a superset of expected.""" + + def safe_repr(obj, short=False): + _MAX_LENGTH = 80 + try: + result = repr(obj) + except Exception: + result = object.__repr__(obj) + if not short or len(result) < _MAX_LENGTH: + return result + return result[:_MAX_LENGTH] + ' [truncated]...' + + missing = [] + mismatched = [] + for key, value in six.iteritems(expected): + if key not in actual: + missing.append(key) + elif value != actual[key]: + mismatched.append('%s, expected: %s, actual: %s' % + (safe_repr(key), safe_repr(value), + safe_repr(actual[key]))) + + if not (missing or mismatched): + return + + standardMsg = '' + if missing: + standardMsg = 'Missing: %s' % ','.join(safe_repr(m) for m in + missing) + if mismatched: + if standardMsg: + standardMsg += '; ' + standardMsg += 'Mismatched values: %s' % ','.join(mismatched) + + self.fail(self._formatMessage(msg, standardMsg)) + + @property + def ipv6_enabled(self): + if socket.has_ipv6: + sock = None + try: + sock = socket.socket(socket.AF_INET6) + # NOTE(Mouad): Try to bind to IPv6 loopback ip address. + sock.bind(("::1", 0)) + return True + except socket.error: + pass + finally: + if sock: + sock.close() + return False + + def skip_if_no_ipv6(self): + if not self.ipv6_enabled: + raise self.skipTest("IPv6 is not enabled in the system") + + def skip_if_env_not_set(self, env_var): + if not os.environ.get(env_var): + self.skipTest('Env variable %s is not set.' % env_var) + + +class SQLDriverOverrides(object): + """A mixin for consolidating sql-specific test overrides.""" + def config_overrides(self): + super(SQLDriverOverrides, self).config_overrides() + # SQL specific driver overrides + self.config_fixture.config( + group='catalog', + driver='keystone.catalog.backends.sql.Catalog') + self.config_fixture.config( + group='identity', + driver='keystone.identity.backends.sql.Identity') + self.config_fixture.config( + group='policy', + driver='keystone.policy.backends.sql.Policy') + self.config_fixture.config( + group='revoke', + driver='keystone.contrib.revoke.backends.sql.Revoke') + self.config_fixture.config( + group='token', + driver='keystone.token.persistence.backends.sql.Token') + self.config_fixture.config( + group='trust', + driver='keystone.trust.backends.sql.Trust') diff --git a/keystone-moon/keystone/tests/unit/default_catalog.templates b/keystone-moon/keystone/tests/unit/default_catalog.templates new file mode 100644 index 00000000..faf87eb5 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/default_catalog.templates @@ -0,0 +1,14 @@ +# config for templated.Catalog, using camelCase because I don't want to do +# translations for keystone compat +catalog.RegionOne.identity.publicURL = http://localhost:$(public_port)s/v2.0 +catalog.RegionOne.identity.adminURL = http://localhost:$(admin_port)s/v2.0 +catalog.RegionOne.identity.internalURL = http://localhost:$(admin_port)s/v2.0 +catalog.RegionOne.identity.name = 'Identity Service' +catalog.RegionOne.identity.id = 1 + +# fake compute service for now to help novaclient tests work +catalog.RegionOne.compute.publicURL = http://localhost:8774/v1.1/$(tenant_id)s +catalog.RegionOne.compute.adminURL = http://localhost:8774/v1.1/$(tenant_id)s +catalog.RegionOne.compute.internalURL = http://localhost:8774/v1.1/$(tenant_id)s +catalog.RegionOne.compute.name = 'Compute Service' +catalog.RegionOne.compute.id = 2 diff --git a/keystone-moon/keystone/tests/unit/default_fixtures.py b/keystone-moon/keystone/tests/unit/default_fixtures.py new file mode 100644 index 00000000..f7e2064f --- /dev/null +++ b/keystone-moon/keystone/tests/unit/default_fixtures.py @@ -0,0 +1,121 @@ +# Copyright 2012 OpenStack Foundation +# +# 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. + +# NOTE(dolph): please try to avoid additional fixtures if possible; test suite +# performance may be negatively affected. + +DEFAULT_DOMAIN_ID = 'default' + +TENANTS = [ + { + 'id': 'bar', + 'name': 'BAR', + 'domain_id': DEFAULT_DOMAIN_ID, + 'description': 'description', + 'enabled': True, + 'parent_id': None, + }, { + 'id': 'baz', + 'name': 'BAZ', + 'domain_id': DEFAULT_DOMAIN_ID, + 'description': 'description', + 'enabled': True, + 'parent_id': None, + }, { + 'id': 'mtu', + 'name': 'MTU', + 'description': 'description', + 'enabled': True, + 'domain_id': DEFAULT_DOMAIN_ID, + 'parent_id': None, + }, { + 'id': 'service', + 'name': 'service', + 'description': 'description', + 'enabled': True, + 'domain_id': DEFAULT_DOMAIN_ID, + 'parent_id': None, + } +] + +# NOTE(ja): a role of keystone_admin is done in setUp +USERS = [ + { + 'id': 'foo', + 'name': 'FOO', + 'domain_id': DEFAULT_DOMAIN_ID, + 'password': 'foo2', + 'tenants': ['bar'], + 'enabled': True, + 'email': 'foo@bar.com', + }, { + 'id': 'two', + 'name': 'TWO', + 'domain_id': DEFAULT_DOMAIN_ID, + 'password': 'two2', + 'enabled': True, + 'default_project_id': 'baz', + 'tenants': ['baz'], + 'email': 'two@three.com', + }, { + 'id': 'badguy', + 'name': 'BadGuy', + 'domain_id': DEFAULT_DOMAIN_ID, + 'password': 'bad', + 'enabled': False, + 'default_project_id': 'baz', + 'tenants': ['baz'], + 'email': 'bad@guy.com', + }, { + 'id': 'sna', + 'name': 'SNA', + 'domain_id': DEFAULT_DOMAIN_ID, + 'password': 'snafu', + 'enabled': True, + 'tenants': ['bar'], + 'email': 'sna@snl.coom', + } +] + +ROLES = [ + { + 'id': 'admin', + 'name': 'admin', + }, { + 'id': 'member', + 'name': 'Member', + }, { + 'id': '9fe2ff9ee4384b1894a90878d3e92bab', + 'name': '_member_', + }, { + 'id': 'other', + 'name': 'Other', + }, { + 'id': 'browser', + 'name': 'Browser', + }, { + 'id': 'writer', + 'name': 'Writer', + }, { + 'id': 'service', + 'name': 'Service', + } +] + +DOMAINS = [{'description': + (u'Owns users and tenants (i.e. projects)' + ' available on Identity API v2.'), + 'enabled': True, + 'id': DEFAULT_DOMAIN_ID, + 'name': u'Default'}] diff --git a/keystone-moon/keystone/tests/unit/fakeldap.py b/keystone-moon/keystone/tests/unit/fakeldap.py new file mode 100644 index 00000000..85aaadfe --- /dev/null +++ b/keystone-moon/keystone/tests/unit/fakeldap.py @@ -0,0 +1,602 @@ +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# 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. + +"""Fake LDAP server for test harness. + +This class does very little error checking, and knows nothing about ldap +class definitions. It implements the minimum emulation of the python ldap +library to work with nova. + +""" + +import re +import shelve + +import ldap +from oslo_config import cfg +from oslo_log import log +import six +from six import moves + +from keystone.common.ldap import core +from keystone import exception + + +SCOPE_NAMES = { + ldap.SCOPE_BASE: 'SCOPE_BASE', + ldap.SCOPE_ONELEVEL: 'SCOPE_ONELEVEL', + ldap.SCOPE_SUBTREE: 'SCOPE_SUBTREE', +} + +# http://msdn.microsoft.com/en-us/library/windows/desktop/aa366991(v=vs.85).aspx # noqa +CONTROL_TREEDELETE = '1.2.840.113556.1.4.805' + +LOG = log.getLogger(__name__) +CONF = cfg.CONF + + +def _internal_attr(attr_name, value_or_values): + def normalize_value(value): + return core.utf8_decode(value) + + def normalize_dn(dn): + # Capitalize the attribute names as an LDAP server might. + + # NOTE(blk-u): Special case for this tested value, used with + # test_user_id_comma. The call to str2dn here isn't always correct + # here, because `dn` is escaped for an LDAP filter. str2dn() normally + # works only because there's no special characters in `dn`. + if dn == 'cn=Doe\\5c, John,ou=Users,cn=example,cn=com': + return 'CN=Doe\\, John,OU=Users,CN=example,CN=com' + + # NOTE(blk-u): Another special case for this tested value. When a + # roleOccupant has an escaped comma, it gets converted to \2C. + if dn == 'cn=Doe\\, John,ou=Users,cn=example,cn=com': + return 'CN=Doe\\2C John,OU=Users,CN=example,CN=com' + + dn = ldap.dn.str2dn(core.utf8_encode(dn)) + norm = [] + for part in dn: + name, val, i = part[0] + name = core.utf8_decode(name) + name = name.upper() + name = core.utf8_encode(name) + norm.append([(name, val, i)]) + return core.utf8_decode(ldap.dn.dn2str(norm)) + + if attr_name in ('member', 'roleOccupant'): + attr_fn = normalize_dn + else: + attr_fn = normalize_value + + if isinstance(value_or_values, list): + return [attr_fn(x) for x in value_or_values] + return [attr_fn(value_or_values)] + + +def _match_query(query, attrs): + """Match an ldap query to an attribute dictionary. + + The characters &, |, and ! are supported in the query. No syntax checking + is performed, so malformed queries will not work correctly. + """ + # cut off the parentheses + inner = query[1:-1] + if inner.startswith(('&', '|')): + if inner[0] == '&': + matchfn = all + else: + matchfn = any + # cut off the & or | + groups = _paren_groups(inner[1:]) + return matchfn(_match_query(group, attrs) for group in groups) + if inner.startswith('!'): + # cut off the ! and the nested parentheses + return not _match_query(query[2:-1], attrs) + + (k, _sep, v) = inner.partition('=') + return _match(k, v, attrs) + + +def _paren_groups(source): + """Split a string into parenthesized groups.""" + count = 0 + start = 0 + result = [] + for pos in moves.range(len(source)): + if source[pos] == '(': + if count == 0: + start = pos + count += 1 + if source[pos] == ')': + count -= 1 + if count == 0: + result.append(source[start:pos + 1]) + return result + + +def _match(key, value, attrs): + """Match a given key and value against an attribute list.""" + + def match_with_wildcards(norm_val, val_list): + # Case insensitive checking with wildcards + if norm_val.startswith('*'): + if norm_val.endswith('*'): + # Is the string anywhere in the target? + for x in val_list: + if norm_val[1:-1] in x: + return True + else: + # Is the string at the end of the target? + for x in val_list: + if (norm_val[1:] == + x[len(x) - len(norm_val) + 1:]): + return True + elif norm_val.endswith('*'): + # Is the string at the start of the target? + for x in val_list: + if norm_val[:-1] == x[:len(norm_val) - 1]: + return True + else: + # Is the string an exact match? + for x in val_list: + if check_value == x: + return True + return False + + if key not in attrs: + return False + # This is a pure wild card search, so the answer must be yes! + if value == '*': + return True + if key == 'serviceId': + # for serviceId, the backend is returning a list of numbers + # make sure we convert them to strings first before comparing + # them + str_sids = [six.text_type(x) for x in attrs[key]] + return six.text_type(value) in str_sids + if key != 'objectclass': + check_value = _internal_attr(key, value)[0].lower() + norm_values = list( + _internal_attr(key, x)[0].lower() for x in attrs[key]) + return match_with_wildcards(check_value, norm_values) + # it is an objectclass check, so check subclasses + values = _subs(value) + for v in values: + if v in attrs[key]: + return True + return False + + +def _subs(value): + """Returns a list of subclass strings. + + The strings represent the ldap objectclass plus any subclasses that + inherit from it. Fakeldap doesn't know about the ldap object structure, + so subclasses need to be defined manually in the dictionary below. + + """ + subs = {'groupOfNames': ['keystoneTenant', + 'keystoneRole', + 'keystoneTenantRole']} + if value in subs: + return [value] + subs[value] + return [value] + + +server_fail = False + + +class FakeShelve(dict): + + def sync(self): + pass + + +FakeShelves = {} + + +class FakeLdap(core.LDAPHandler): + '''Emulate the python-ldap API. + + The python-ldap API requires all strings to be UTF-8 encoded. This + is assured by the caller of this interface + (i.e. KeystoneLDAPHandler). + + However, internally this emulation MUST process and store strings + in a canonical form which permits operations on + characters. Encoded strings do not provide the ability to operate + on characters. Therefore this emulation accepts UTF-8 encoded + strings, decodes them to unicode for operations internal to this + emulation, and encodes them back to UTF-8 when returning values + from the emulation. + ''' + + __prefix = 'ldap:' + + def __init__(self, conn=None): + super(FakeLdap, self).__init__(conn=conn) + self._ldap_options = {ldap.OPT_DEREF: ldap.DEREF_NEVER} + + def connect(self, url, page_size=0, alias_dereferencing=None, + use_tls=False, tls_cacertfile=None, tls_cacertdir=None, + tls_req_cert='demand', chase_referrals=None, debug_level=None, + use_pool=None, pool_size=None, pool_retry_max=None, + pool_retry_delay=None, pool_conn_timeout=None, + pool_conn_lifetime=None): + if url.startswith('fake://memory'): + if url not in FakeShelves: + FakeShelves[url] = FakeShelve() + self.db = FakeShelves[url] + else: + self.db = shelve.open(url[7:]) + + using_ldaps = url.lower().startswith("ldaps") + + if use_tls and using_ldaps: + raise AssertionError('Invalid TLS / LDAPS combination') + + if use_tls: + if tls_cacertfile: + ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, tls_cacertfile) + elif tls_cacertdir: + ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, tls_cacertdir) + if tls_req_cert in core.LDAP_TLS_CERTS.values(): + ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, tls_req_cert) + else: + raise ValueError("invalid TLS_REQUIRE_CERT tls_req_cert=%s", + tls_req_cert) + + if alias_dereferencing is not None: + self.set_option(ldap.OPT_DEREF, alias_dereferencing) + self.page_size = page_size + + self.use_pool = use_pool + self.pool_size = pool_size + self.pool_retry_max = pool_retry_max + self.pool_retry_delay = pool_retry_delay + self.pool_conn_timeout = pool_conn_timeout + self.pool_conn_lifetime = pool_conn_lifetime + + def dn(self, dn): + return core.utf8_decode(dn) + + def _dn_to_id_attr(self, dn): + return core.utf8_decode(ldap.dn.str2dn(core.utf8_encode(dn))[0][0][0]) + + def _dn_to_id_value(self, dn): + return core.utf8_decode(ldap.dn.str2dn(core.utf8_encode(dn))[0][0][1]) + + def key(self, dn): + return '%s%s' % (self.__prefix, self.dn(dn)) + + def simple_bind_s(self, who='', cred='', + serverctrls=None, clientctrls=None): + """This method is ignored, but provided for compatibility.""" + if server_fail: + raise ldap.SERVER_DOWN + whos = ['cn=Admin', CONF.ldap.user] + if who in whos and cred in ['password', CONF.ldap.password]: + return + + try: + attrs = self.db[self.key(who)] + except KeyError: + LOG.debug('bind fail: who=%s not found', core.utf8_decode(who)) + raise ldap.NO_SUCH_OBJECT + + db_password = None + try: + db_password = attrs['userPassword'][0] + except (KeyError, IndexError): + LOG.debug('bind fail: password for who=%s not found', + core.utf8_decode(who)) + raise ldap.INAPPROPRIATE_AUTH + + if cred != db_password: + LOG.debug('bind fail: password for who=%s does not match', + core.utf8_decode(who)) + raise ldap.INVALID_CREDENTIALS + + def unbind_s(self): + """This method is ignored, but provided for compatibility.""" + if server_fail: + raise ldap.SERVER_DOWN + + def add_s(self, dn, modlist): + """Add an object with the specified attributes at dn.""" + if server_fail: + raise ldap.SERVER_DOWN + + id_attr_in_modlist = False + id_attr = self._dn_to_id_attr(dn) + id_value = self._dn_to_id_value(dn) + + # The LDAP API raises a TypeError if attr name is None. + for k, dummy_v in modlist: + if k is None: + raise TypeError('must be string, not None. modlist=%s' % + modlist) + + if k == id_attr: + for val in dummy_v: + if core.utf8_decode(val) == id_value: + id_attr_in_modlist = True + + if not id_attr_in_modlist: + LOG.debug('id_attribute=%(attr)s missing, attributes=%(attrs)s' % + {'attr': id_attr, 'attrs': modlist}) + raise ldap.NAMING_VIOLATION + key = self.key(dn) + LOG.debug('add item: dn=%(dn)s, attrs=%(attrs)s', { + 'dn': core.utf8_decode(dn), 'attrs': modlist}) + if key in self.db: + LOG.debug('add item failed: dn=%s is already in store.', + core.utf8_decode(dn)) + raise ldap.ALREADY_EXISTS(dn) + + self.db[key] = {k: _internal_attr(k, v) for k, v in modlist} + self.db.sync() + + def delete_s(self, dn): + """Remove the ldap object at specified dn.""" + return self.delete_ext_s(dn, serverctrls=[]) + + def _getChildren(self, dn): + return [k for k, v in six.iteritems(self.db) + if re.match('%s.*,%s' % ( + re.escape(self.__prefix), + re.escape(self.dn(dn))), k)] + + def delete_ext_s(self, dn, serverctrls, clientctrls=None): + """Remove the ldap object at specified dn.""" + if server_fail: + raise ldap.SERVER_DOWN + + try: + if CONTROL_TREEDELETE in [c.controlType for c in serverctrls]: + LOG.debug('FakeLdap subtree_delete item: dn=%s', + core.utf8_decode(dn)) + children = self._getChildren(dn) + for c in children: + del self.db[c] + + key = self.key(dn) + LOG.debug('FakeLdap delete item: dn=%s', core.utf8_decode(dn)) + del self.db[key] + except KeyError: + LOG.debug('delete item failed: dn=%s not found.', + core.utf8_decode(dn)) + raise ldap.NO_SUCH_OBJECT + self.db.sync() + + def modify_s(self, dn, modlist): + """Modify the object at dn using the attribute list. + + :param dn: an LDAP DN + :param modlist: a list of tuples in the following form: + ([MOD_ADD | MOD_DELETE | MOD_REPACE], attribute, value) + """ + if server_fail: + raise ldap.SERVER_DOWN + + key = self.key(dn) + LOG.debug('modify item: dn=%(dn)s attrs=%(attrs)s', { + 'dn': core.utf8_decode(dn), 'attrs': modlist}) + try: + entry = self.db[key] + except KeyError: + LOG.debug('modify item failed: dn=%s not found.', + core.utf8_decode(dn)) + raise ldap.NO_SUCH_OBJECT + + for cmd, k, v in modlist: + values = entry.setdefault(k, []) + if cmd == ldap.MOD_ADD: + v = _internal_attr(k, v) + for x in v: + if x in values: + raise ldap.TYPE_OR_VALUE_EXISTS + values += v + elif cmd == ldap.MOD_REPLACE: + values[:] = _internal_attr(k, v) + elif cmd == ldap.MOD_DELETE: + if v is None: + if not values: + LOG.debug('modify item failed: ' + 'item has no attribute "%s" to delete', k) + raise ldap.NO_SUCH_ATTRIBUTE + values[:] = [] + else: + for val in _internal_attr(k, v): + try: + values.remove(val) + except ValueError: + LOG.debug('modify item failed: ' + 'item has no attribute "%(k)s" with ' + 'value "%(v)s" to delete', { + 'k': k, 'v': val}) + raise ldap.NO_SUCH_ATTRIBUTE + else: + LOG.debug('modify item failed: unknown command %s', cmd) + raise NotImplementedError('modify_s action %s not' + ' implemented' % cmd) + self.db[key] = entry + self.db.sync() + + def search_s(self, base, scope, + filterstr='(objectClass=*)', attrlist=None, attrsonly=0): + """Search for all matching objects under base using the query. + + Args: + base -- dn to search under + scope -- search scope (base, subtree, onelevel) + filterstr -- filter objects by + attrlist -- attrs to return. Returns all attrs if not specified + + """ + if server_fail: + raise ldap.SERVER_DOWN + + if scope == ldap.SCOPE_BASE: + try: + item_dict = self.db[self.key(base)] + except KeyError: + LOG.debug('search fail: dn not found for SCOPE_BASE') + raise ldap.NO_SUCH_OBJECT + results = [(base, item_dict)] + elif scope == ldap.SCOPE_SUBTREE: + # FIXME - LDAP search with SUBTREE scope must return the base + # entry, but the code below does _not_. Unfortunately, there are + # several tests that depend on this broken behavior, and fail + # when the base entry is returned in the search results. The + # fix is easy here, just initialize results as above for + # the SCOPE_BASE case. + # https://bugs.launchpad.net/keystone/+bug/1368772 + try: + item_dict = self.db[self.key(base)] + except KeyError: + LOG.debug('search fail: dn not found for SCOPE_SUBTREE') + raise ldap.NO_SUCH_OBJECT + results = [(base, item_dict)] + extraresults = [(k[len(self.__prefix):], v) + for k, v in six.iteritems(self.db) + if re.match('%s.*,%s' % + (re.escape(self.__prefix), + re.escape(self.dn(base))), k)] + results.extend(extraresults) + elif scope == ldap.SCOPE_ONELEVEL: + + def get_entries(): + base_dn = ldap.dn.str2dn(core.utf8_encode(base)) + base_len = len(base_dn) + + for k, v in six.iteritems(self.db): + if not k.startswith(self.__prefix): + continue + k_dn_str = k[len(self.__prefix):] + k_dn = ldap.dn.str2dn(core.utf8_encode(k_dn_str)) + if len(k_dn) != base_len + 1: + continue + if k_dn[-base_len:] != base_dn: + continue + yield (k_dn_str, v) + + results = list(get_entries()) + + else: + # openldap client/server raises PROTOCOL_ERROR for unexpected scope + raise ldap.PROTOCOL_ERROR + + objects = [] + for dn, attrs in results: + # filter the objects by filterstr + id_attr, id_val, _ = ldap.dn.str2dn(core.utf8_encode(dn))[0][0] + id_attr = core.utf8_decode(id_attr) + id_val = core.utf8_decode(id_val) + match_attrs = attrs.copy() + match_attrs[id_attr] = [id_val] + if not filterstr or _match_query(filterstr, match_attrs): + # filter the attributes by attrlist + attrs = {k: v for k, v in six.iteritems(attrs) + if not attrlist or k in attrlist} + objects.append((dn, attrs)) + + return objects + + def set_option(self, option, invalue): + self._ldap_options[option] = invalue + + def get_option(self, option): + value = self._ldap_options.get(option, None) + return value + + def search_ext(self, base, scope, + filterstr='(objectClass=*)', attrlist=None, attrsonly=0, + serverctrls=None, clientctrls=None, + timeout=-1, sizelimit=0): + raise exception.NotImplemented() + + def result3(self, msgid=ldap.RES_ANY, all=1, timeout=None, + resp_ctrl_classes=None): + raise exception.NotImplemented() + + +class FakeLdapPool(FakeLdap): + '''Emulate the python-ldap API with pooled connections using existing + FakeLdap logic. + + This class is used as connector class in PooledLDAPHandler. + ''' + + def __init__(self, uri, retry_max=None, retry_delay=None, conn=None): + super(FakeLdapPool, self).__init__(conn=conn) + self.url = uri + self.connected = None + self.conn = self + self._connection_time = 5 # any number greater than 0 + + def get_lifetime(self): + return self._connection_time + + def simple_bind_s(self, who=None, cred=None, + serverctrls=None, clientctrls=None): + if self.url.startswith('fakepool://memory'): + if self.url not in FakeShelves: + FakeShelves[self.url] = FakeShelve() + self.db = FakeShelves[self.url] + else: + self.db = shelve.open(self.url[11:]) + + if not who: + who = 'cn=Admin' + if not cred: + cred = 'password' + + super(FakeLdapPool, self).simple_bind_s(who=who, cred=cred, + serverctrls=serverctrls, + clientctrls=clientctrls) + + def unbind_ext_s(self): + '''Added to extend FakeLdap as connector class.''' + pass + + +class FakeLdapNoSubtreeDelete(FakeLdap): + """FakeLdap subclass that does not support subtree delete + + Same as FakeLdap except delete will throw the LDAP error + ldap.NOT_ALLOWED_ON_NONLEAF if there is an attempt to delete + an entry that has children. + """ + + def delete_ext_s(self, dn, serverctrls, clientctrls=None): + """Remove the ldap object at specified dn.""" + if server_fail: + raise ldap.SERVER_DOWN + + try: + children = self._getChildren(dn) + if children: + raise ldap.NOT_ALLOWED_ON_NONLEAF + + except KeyError: + LOG.debug('delete item failed: dn=%s not found.', + core.utf8_decode(dn)) + raise ldap.NO_SUCH_OBJECT + super(FakeLdapNoSubtreeDelete, self).delete_ext_s(dn, + serverctrls, + clientctrls) diff --git a/keystone-moon/keystone/tests/unit/federation_fixtures.py b/keystone-moon/keystone/tests/unit/federation_fixtures.py new file mode 100644 index 00000000..d4527d9c --- /dev/null +++ b/keystone-moon/keystone/tests/unit/federation_fixtures.py @@ -0,0 +1,28 @@ +# 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. + + +IDP_ENTITY_ID = 'https://localhost/v3/OS-FEDERATION/saml2/idp' +IDP_SSO_ENDPOINT = 'https://localhost/v3/OS-FEDERATION/saml2/SSO' + +# Organization info +IDP_ORGANIZATION_NAME = 'ACME INC' +IDP_ORGANIZATION_DISPLAY_NAME = 'ACME' +IDP_ORGANIZATION_URL = 'https://acme.example.com' + +# Contact info +IDP_CONTACT_COMPANY = 'ACME Sub' +IDP_CONTACT_GIVEN_NAME = 'Joe' +IDP_CONTACT_SURNAME = 'Hacker' +IDP_CONTACT_EMAIL = 'joe@acme.example.com' +IDP_CONTACT_TELEPHONE_NUMBER = '1234567890' +IDP_CONTACT_TYPE = 'technical' diff --git a/keystone-moon/keystone/tests/unit/filtering.py b/keystone-moon/keystone/tests/unit/filtering.py new file mode 100644 index 00000000..1a31a23f --- /dev/null +++ b/keystone-moon/keystone/tests/unit/filtering.py @@ -0,0 +1,96 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 oslo_config import cfg + + +CONF = cfg.CONF + + +class FilterTests(object): + + # Provide support for checking if a batch of list items all + # exist within a contiguous range in a total list + def _match_with_list(self, this_batch, total_list, + batch_size=None, + list_start=None, list_end=None): + if batch_size is None: + batch_size = len(this_batch) + if list_start is None: + list_start = 0 + if list_end is None: + list_end = len(total_list) + for batch_item in range(0, batch_size): + found = False + for list_item in range(list_start, list_end): + if this_batch[batch_item]['id'] == total_list[list_item]['id']: + found = True + self.assertTrue(found) + + def _create_entity(self, entity_type): + f = getattr(self.identity_api, 'create_%s' % entity_type, None) + if f is None: + f = getattr(self.assignment_api, 'create_%s' % entity_type) + return f + + def _delete_entity(self, entity_type): + f = getattr(self.identity_api, 'delete_%s' % entity_type, None) + if f is None: + f = getattr(self.assignment_api, 'delete_%s' % entity_type) + return f + + def _list_entities(self, entity_type): + f = getattr(self.identity_api, 'list_%ss' % entity_type, None) + if f is None: + f = getattr(self.assignment_api, 'list_%ss' % entity_type) + return f + + def _create_one_entity(self, entity_type, domain_id, name): + new_entity = {'name': name, + 'domain_id': domain_id} + if entity_type in ['user', 'group']: + # The manager layer creates the ID for users and groups + new_entity = self._create_entity(entity_type)(new_entity) + else: + new_entity['id'] = '0000' + uuid.uuid4().hex + self._create_entity(entity_type)(new_entity['id'], new_entity) + return new_entity + + def _create_test_data(self, entity_type, number, domain_id=None, + name_dict=None): + """Create entity test data + + :param entity_type: type of entity to create, e.g. 'user', group' etc. + :param number: number of entities to create, + :param domain_id: if not defined, all users will be created in the + default domain. + :param name_dict: optional dict containing entity number and name pairs + + """ + entity_list = [] + if domain_id is None: + domain_id = CONF.identity.default_domain_id + name_dict = name_dict or {} + for x in range(number): + # If this index has a name defined in the name_dict, then use it + name = name_dict.get(x, uuid.uuid4().hex) + new_entity = self._create_one_entity(entity_type, domain_id, name) + entity_list.append(new_entity) + return entity_list + + def _delete_test_data(self, entity_type, entity_list): + for entity in entity_list: + self._delete_entity(entity_type)(entity['id']) diff --git a/keystone-moon/keystone/tests/unit/identity/__init__.py b/keystone-moon/keystone/tests/unit/identity/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/tests/unit/identity/test_core.py b/keystone-moon/keystone/tests/unit/identity/test_core.py new file mode 100644 index 00000000..6c8faebb --- /dev/null +++ b/keystone-moon/keystone/tests/unit/identity/test_core.py @@ -0,0 +1,125 @@ +# 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. + +"""Unit tests for core identity behavior.""" + +import os +import uuid + +import mock +from oslo_config import cfg + +from keystone import exception +from keystone import identity +from keystone.tests import unit as tests +from keystone.tests.unit.ksfixtures import database + + +CONF = cfg.CONF + + +class TestDomainConfigs(tests.BaseTestCase): + + def setUp(self): + super(TestDomainConfigs, self).setUp() + self.addCleanup(CONF.reset) + + self.tmp_dir = tests.dirs.tmp() + CONF.set_override('domain_config_dir', self.tmp_dir, 'identity') + + def test_config_for_nonexistent_domain(self): + """Having a config for a non-existent domain will be ignored. + + There are no assertions in this test because there are no side + effects. If there is a config file for a domain that does not + exist it should be ignored. + + """ + domain_id = uuid.uuid4().hex + domain_config_filename = os.path.join(self.tmp_dir, + 'keystone.%s.conf' % domain_id) + self.addCleanup(lambda: os.remove(domain_config_filename)) + with open(domain_config_filename, 'w'): + """Write an empty config file.""" + + e = exception.DomainNotFound(domain_id=domain_id) + mock_assignment_api = mock.Mock() + mock_assignment_api.get_domain_by_name.side_effect = e + + domain_config = identity.DomainConfigs() + fake_standard_driver = None + domain_config.setup_domain_drivers(fake_standard_driver, + mock_assignment_api) + + def test_config_for_dot_name_domain(self): + # Ensure we can get the right domain name which has dots within it + # from filename. + domain_config_filename = os.path.join(self.tmp_dir, + 'keystone.abc.def.com.conf') + with open(domain_config_filename, 'w'): + """Write an empty config file.""" + self.addCleanup(os.remove, domain_config_filename) + + with mock.patch.object(identity.DomainConfigs, + '_load_config_from_file') as mock_load_config: + domain_config = identity.DomainConfigs() + fake_assignment_api = None + fake_standard_driver = None + domain_config.setup_domain_drivers(fake_standard_driver, + fake_assignment_api) + mock_load_config.assert_called_once_with(fake_assignment_api, + [domain_config_filename], + 'abc.def.com') + + +class TestDatabaseDomainConfigs(tests.TestCase): + + def setUp(self): + super(TestDatabaseDomainConfigs, self).setUp() + self.useFixture(database.Database()) + self.load_backends() + + def test_domain_config_in_database_disabled_by_default(self): + self.assertFalse(CONF.identity.domain_configurations_from_database) + + def test_loading_config_from_database(self): + CONF.set_override('domain_configurations_from_database', True, + 'identity') + domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(domain['id'], domain) + # Override two config options for our domain + conf = {'ldap': {'url': uuid.uuid4().hex, + 'suffix': uuid.uuid4().hex}, + 'identity': { + 'driver': 'keystone.identity.backends.ldap.Identity'}} + self.domain_config_api.create_config(domain['id'], conf) + fake_standard_driver = None + domain_config = identity.DomainConfigs() + domain_config.setup_domain_drivers(fake_standard_driver, + self.resource_api) + # Make sure our two overrides are in place, and others are not affected + res = domain_config.get_domain_conf(domain['id']) + self.assertEqual(conf['ldap']['url'], res.ldap.url) + self.assertEqual(conf['ldap']['suffix'], res.ldap.suffix) + self.assertEqual(CONF.ldap.query_scope, res.ldap.query_scope) + + # Now turn off using database domain configuration and check that the + # default config file values are now seen instead of the overrides. + CONF.set_override('domain_configurations_from_database', False, + 'identity') + domain_config = identity.DomainConfigs() + domain_config.setup_domain_drivers(fake_standard_driver, + self.resource_api) + res = domain_config.get_domain_conf(domain['id']) + self.assertEqual(CONF.ldap.url, res.ldap.url) + self.assertEqual(CONF.ldap.suffix, res.ldap.suffix) + self.assertEqual(CONF.ldap.query_scope, res.ldap.query_scope) diff --git a/keystone-moon/keystone/tests/unit/identity_mapping.py b/keystone-moon/keystone/tests/unit/identity_mapping.py new file mode 100644 index 00000000..7fb8063f --- /dev/null +++ b/keystone-moon/keystone/tests/unit/identity_mapping.py @@ -0,0 +1,23 @@ +# 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 keystone.common import sql +from keystone.identity.mapping_backends import sql as mapping_sql + + +def list_id_mappings(): + """List all id_mappings for testing purposes.""" + + a_session = sql.get_session() + refs = a_session.query(mapping_sql.IDMapping).all() + return [x.to_dict() for x in refs] diff --git a/keystone-moon/keystone/tests/unit/ksfixtures/__init__.py b/keystone-moon/keystone/tests/unit/ksfixtures/__init__.py new file mode 100644 index 00000000..81b80298 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/ksfixtures/__init__.py @@ -0,0 +1,15 @@ +# 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 keystone.tests.unit.ksfixtures.cache import Cache # noqa +from keystone.tests.unit.ksfixtures.key_repository import KeyRepository # noqa diff --git a/keystone-moon/keystone/tests/unit/ksfixtures/appserver.py b/keystone-moon/keystone/tests/unit/ksfixtures/appserver.py new file mode 100644 index 00000000..ea1e6255 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/ksfixtures/appserver.py @@ -0,0 +1,79 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 __future__ import absolute_import + +import fixtures +from oslo_config import cfg +from paste import deploy + +from keystone.common import environment + + +CONF = cfg.CONF + +MAIN = 'main' +ADMIN = 'admin' + + +class AppServer(fixtures.Fixture): + """A fixture for managing an application server instance. + """ + + def __init__(self, config, name, cert=None, key=None, ca=None, + cert_required=False, host='127.0.0.1', port=0): + super(AppServer, self).__init__() + self.config = config + self.name = name + self.cert = cert + self.key = key + self.ca = ca + self.cert_required = cert_required + self.host = host + self.port = port + + def setUp(self): + super(AppServer, self).setUp() + + app = deploy.loadapp(self.config, name=self.name) + self.server = environment.Server(app, self.host, self.port) + self._setup_SSL_if_requested() + self.server.start(key='socket') + + # some tests need to know the port we ran on. + self.port = self.server.socket_info['socket'][1] + self._update_config_opt() + + self.addCleanup(self.server.stop) + + def _setup_SSL_if_requested(self): + # TODO(dstanek): fix environment.Server to take a SSLOpts instance + # so that the params are either always set or not + if (self.cert is not None and + self.ca is not None and + self.key is not None): + self.server.set_ssl(certfile=self.cert, + keyfile=self.key, + ca_certs=self.ca, + cert_required=self.cert_required) + + def _update_config_opt(self): + """Updates the config with the actual port used.""" + opt_name = self._get_config_option_for_section_name() + CONF.set_override(opt_name, self.port, group='eventlet_server') + + def _get_config_option_for_section_name(self): + """Maps Paster config section names to port option names.""" + return {'admin': 'admin_port', 'main': 'public_port'}[self.name] diff --git a/keystone-moon/keystone/tests/unit/ksfixtures/cache.py b/keystone-moon/keystone/tests/unit/ksfixtures/cache.py new file mode 100644 index 00000000..74566f1e --- /dev/null +++ b/keystone-moon/keystone/tests/unit/ksfixtures/cache.py @@ -0,0 +1,36 @@ +# 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 fixtures + +from keystone.common import cache + + +class Cache(fixtures.Fixture): + """A fixture for setting up and tearing down the cache between test cases. + """ + + def setUp(self): + super(Cache, self).setUp() + + # NOTE(dstanek): We must remove the existing cache backend in the + # setUp instead of the tearDown because it defaults to a no-op cache + # and we want the configure call below to create the correct backend. + + # NOTE(morganfainberg): The only way to reconfigure the CacheRegion + # object on each setUp() call is to remove the .backend property. + if cache.REGION.is_configured: + del cache.REGION.backend + + # ensure the cache region instance is setup + cache.configure_cache_region(cache.REGION) diff --git a/keystone-moon/keystone/tests/unit/ksfixtures/database.py b/keystone-moon/keystone/tests/unit/ksfixtures/database.py new file mode 100644 index 00000000..15597539 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/ksfixtures/database.py @@ -0,0 +1,124 @@ +# 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 functools +import os +import shutil + +import fixtures +from oslo_config import cfg +from oslo_db import options as db_options +from oslo_db.sqlalchemy import migration + +from keystone.common import sql +from keystone.common.sql import migration_helpers +from keystone.tests import unit as tests + + +CONF = cfg.CONF + + +def run_once(f): + """A decorator to ensure the decorated function is only executed once. + + The decorated function cannot expect any arguments. + """ + @functools.wraps(f) + def wrapper(): + if not wrapper.already_ran: + f() + wrapper.already_ran = True + wrapper.already_ran = False + return wrapper + + +def _setup_database(extensions=None): + if CONF.database.connection != tests.IN_MEM_DB_CONN_STRING: + db = tests.dirs.tmp('test.db') + pristine = tests.dirs.tmp('test.db.pristine') + + if os.path.exists(db): + os.unlink(db) + if not os.path.exists(pristine): + migration.db_sync(sql.get_engine(), + migration_helpers.find_migrate_repo()) + for extension in (extensions or []): + migration_helpers.sync_database_to_version(extension=extension) + shutil.copyfile(db, pristine) + else: + shutil.copyfile(pristine, db) + + +# NOTE(I159): Every execution all the options will be cleared. The method must +# be called at the every fixture initialization. +def initialize_sql_session(): + # Make sure the DB is located in the correct location, in this case set + # the default value, as this should be able to be overridden in some + # test cases. + db_options.set_defaults( + CONF, + connection=tests.IN_MEM_DB_CONN_STRING) + + +@run_once +def _load_sqlalchemy_models(): + """Find all modules containing SQLAlchemy models and import them. + + This creates more consistent, deterministic test runs because tables + for all core and extension models are always created in the test + database. We ensure this by importing all modules that contain model + definitions. + + The database schema during test runs is created using reflection. + Reflection is simply SQLAlchemy taking the model definitions for + all models currently imported and making tables for each of them. + The database schema created during test runs may vary between tests + as more models are imported. Importing all models at the start of + the test run avoids this problem. + + """ + keystone_root = os.path.normpath(os.path.join( + os.path.dirname(__file__), '..', '..', '..')) + for root, dirs, files in os.walk(keystone_root): + # NOTE(morganfainberg): Slice the keystone_root off the root to ensure + # we do not end up with a module name like: + # Users.home.openstack.keystone.assignment.backends.sql + root = root[len(keystone_root):] + if root.endswith('backends') and 'sql.py' in files: + # The root will be prefixed with an instance of os.sep, which will + # make the root after replacement '.', the 'keystone' part + # of the module path is always added to the front + module_name = ('keystone.%s.sql' % + root.replace(os.sep, '.').lstrip('.')) + __import__(module_name) + + +class Database(fixtures.Fixture): + """A fixture for setting up and tearing down a database. + + """ + + def __init__(self, extensions=None): + super(Database, self).__init__() + self._extensions = extensions + initialize_sql_session() + _load_sqlalchemy_models() + + def setUp(self): + super(Database, self).setUp() + _setup_database(extensions=self._extensions) + + self.engine = sql.get_engine() + sql.ModelBase.metadata.create_all(bind=self.engine) + self.addCleanup(sql.cleanup) + self.addCleanup(sql.ModelBase.metadata.drop_all, bind=self.engine) diff --git a/keystone-moon/keystone/tests/unit/ksfixtures/hacking.py b/keystone-moon/keystone/tests/unit/ksfixtures/hacking.py new file mode 100644 index 00000000..47ef6b4b --- /dev/null +++ b/keystone-moon/keystone/tests/unit/ksfixtures/hacking.py @@ -0,0 +1,489 @@ +# 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. + +# NOTE(morganfainberg) This file shouldn't have flake8 run on it as it has +# code examples that will fail normal CI pep8/flake8 tests. This is expected. +# The code has been moved here to ensure that proper tests occur on the +# test_hacking_checks test cases. +# flake8: noqa + +import fixtures + + +class HackingCode(fixtures.Fixture): + """A fixture to house the various code examples for the keystone hacking + style checks. + """ + + mutable_default_args = { + 'code': """ + def f(): + pass + + def f(a, b='', c=None): + pass + + def f(bad=[]): + pass + + def f(foo, bad=[], more_bad=[x for x in range(3)]): + pass + + def f(foo, bad={}): + pass + + def f(foo, bad={}, another_bad=[], fine=None): + pass + + def f(bad=[]): # noqa + pass + + def funcs(bad=dict(), more_bad=list(), even_more_bad=set()): + "creating mutables through builtins" + + def funcs(bad=something(), more_bad=some_object.something()): + "defaults from any functions" + + def f(bad=set(), more_bad={x for x in range(3)}, + even_more_bad={1, 2, 3}): + "set and set comprehession" + + def f(bad={x: x for x in range(3)}): + "dict comprehension" + """, + 'expected_errors': [ + (7, 10, 'K001'), + (10, 15, 'K001'), + (10, 29, 'K001'), + (13, 15, 'K001'), + (16, 15, 'K001'), + (16, 31, 'K001'), + (22, 14, 'K001'), + (22, 31, 'K001'), + (22, 53, 'K001'), + (25, 14, 'K001'), + (25, 36, 'K001'), + (28, 10, 'K001'), + (28, 27, 'K001'), + (29, 21, 'K001'), + (32, 11, 'K001'), + ]} + + comments_begin_with_space = { + 'code': """ + # This is a good comment + + #This is a bad one + + # This is alright and can + # be continued with extra indentation + # if that's what the developer wants. + """, + 'expected_errors': [ + (3, 0, 'K002'), + ]} + + asserting_none_equality = { + 'code': """ + class Test(object): + + def test(self): + self.assertEqual('', '') + self.assertEqual('', None) + self.assertEqual(None, '') + self.assertNotEqual('', None) + self.assertNotEqual(None, '') + self.assertNotEqual('', None) # noqa + self.assertNotEqual(None, '') # noqa + """, + 'expected_errors': [ + (5, 8, 'K003'), + (6, 8, 'K003'), + (7, 8, 'K004'), + (8, 8, 'K004'), + ]} + + assert_no_translations_for_debug_logging = { + 'code': """ + import logging + import logging as stlib_logging + from keystone.i18n import _ + from keystone.i18n import _ as oslo_i18n + from keystone.openstack.common import log + from keystone.openstack.common import log as oslo_logging + + # stdlib logging + L0 = logging.getLogger() + L0.debug(_('text')) + class C: + def __init__(self): + L0.debug(oslo_i18n('text', {})) + + # stdlib logging w/ alias and specifying a logger + class C: + def __init__(self): + self.L1 = logging.getLogger(__name__) + def m(self): + self.L1.debug( + _('text'), {} + ) + + # oslo logging and specifying a logger + L2 = log.getLogger(__name__) + L2.debug(oslo_i18n('text')) + + # oslo logging w/ alias + class C: + def __init__(self): + self.L3 = oslo_logging.getLogger() + self.L3.debug(_('text')) + + # translation on a separate line + msg = _('text') + L2.debug(msg) + + # this should not fail + if True: + msg = _('message %s') % X + L2.error(msg) + raise TypeError(msg) + if True: + msg = 'message' + L2.debug(msg) + + # this should not fail + if True: + if True: + msg = _('message') + else: + msg = _('message') + L2.debug(msg) + raise Exception(msg) + """, + 'expected_errors': [ + (10, 9, 'K005'), + (13, 17, 'K005'), + (21, 12, 'K005'), + (26, 9, 'K005'), + (32, 22, 'K005'), + (36, 9, 'K005'), + ] + } + + oslo_namespace_imports = { + 'code': """ + import oslo.utils + 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 + + import oslo.serialization + import oslo_serialization + import oslo.serialization.jsonutils + import oslo_serialization.jsonutils + from oslo import serialization + from oslo.serialization import jsonutils + from oslo_serialization import jsonutils + + import oslo.messaging + import oslo_messaging + import oslo.messaging.conffixture + import oslo_messaging.conffixture + from oslo import messaging + from oslo.messaging import conffixture + from oslo_messaging import conffixture + + import oslo.db + import oslo_db + import oslo.db.api + import oslo_db.api + from oslo import db + from oslo.db import api + from oslo_db import api + + import oslo.config + import oslo_config + import oslo.config.cfg + import oslo_config.cfg + from oslo import config + from oslo.config import cfg + from oslo_config import cfg + + import oslo.i18n + import oslo_i18n + import oslo.i18n.log + import oslo_i18n.log + from oslo import i18n + from oslo.i18n import log + from oslo_i18n import log + """, + 'expected_errors': [ + (1, 0, 'K333'), + (3, 0, 'K333'), + (5, 0, 'K333'), + (6, 0, 'K333'), + (9, 0, 'K333'), + (11, 0, 'K333'), + (13, 0, 'K333'), + (14, 0, 'K333'), + (17, 0, 'K333'), + (19, 0, 'K333'), + (21, 0, 'K333'), + (22, 0, 'K333'), + (25, 0, 'K333'), + (27, 0, 'K333'), + (29, 0, 'K333'), + (30, 0, 'K333'), + (33, 0, 'K333'), + (35, 0, 'K333'), + (37, 0, 'K333'), + (38, 0, 'K333'), + (41, 0, 'K333'), + (43, 0, 'K333'), + (45, 0, 'K333'), + (46, 0, 'K333'), + ], + } + + dict_constructor = { + 'code': """ + lower_res = {k.lower(): v for k, v in six.iteritems(res[1])} + fool = dict(a='a', b='b') + lower_res = dict((k.lower(), v) for k, v in six.iteritems(res[1])) + attrs = dict([(k, _from_json(v))]) + dict([[i,i] for i in range(3)]) + dict(({1:2})) + """, + 'expected_errors': [ + (3, 0, 'K008'), + (4, 0, 'K008'), + (5, 0, 'K008'), + ]} + + +class HackingLogging(fixtures.Fixture): + + shared_imports = """ + import logging + import logging as stlib_logging + from keystone.i18n import _ + from keystone.i18n import _ as oslo_i18n + from keystone.i18n import _LC + from keystone.i18n import _LE + from keystone.i18n import _LE as error_hint + from keystone.i18n import _LI + from keystone.i18n import _LW + from keystone.openstack.common import log + from keystone.openstack.common import log as oslo_logging + """ + + examples = [ + { + 'code': """ + # stdlib logging + LOG = logging.getLogger() + LOG.info(_('text')) + class C: + def __init__(self): + LOG.warn(oslo_i18n('text', {})) + LOG.warn(_LW('text', {})) + """, + 'expected_errors': [ + (3, 9, 'K006'), + (6, 17, 'K006'), + ], + }, + { + 'code': """ + # stdlib logging w/ alias and specifying a logger + class C: + def __init__(self): + self.L = logging.getLogger(__name__) + def m(self): + self.L.warning( + _('text'), {} + ) + self.L.warning( + _LW('text'), {} + ) + """, + 'expected_errors': [ + (7, 12, 'K006'), + ], + }, + { + 'code': """ + # oslo logging and specifying a logger + L = log.getLogger(__name__) + L.error(oslo_i18n('text')) + L.error(error_hint('text')) + """, + 'expected_errors': [ + (3, 8, 'K006'), + ], + }, + { + 'code': """ + # oslo logging w/ alias + class C: + def __init__(self): + self.LOG = oslo_logging.getLogger() + self.LOG.critical(_('text')) + self.LOG.critical(_LC('text')) + """, + 'expected_errors': [ + (5, 26, 'K006'), + ], + }, + { + 'code': """ + LOG = log.getLogger(__name__) + # translation on a separate line + msg = _('text') + LOG.exception(msg) + msg = _LE('text') + LOG.exception(msg) + """, + 'expected_errors': [ + (4, 14, 'K006'), + ], + }, + { + 'code': """ + LOG = logging.getLogger() + + # ensure the correct helper is being used + LOG.warn(_LI('this should cause an error')) + + # debug should not allow any helpers either + LOG.debug(_LI('this should cause an error')) + """, + 'expected_errors': [ + (4, 9, 'K006'), + (7, 10, 'K005'), + ], + }, + { + 'code': """ + # this should not be an error + L = log.getLogger(__name__) + msg = _('text') + L.warn(msg) + raise Exception(msg) + """, + 'expected_errors': [], + }, + { + 'code': """ + L = log.getLogger(__name__) + def f(): + msg = _('text') + L2.warn(msg) + something = True # add an extra statement here + raise Exception(msg) + """, + 'expected_errors': [], + }, + { + 'code': """ + LOG = log.getLogger(__name__) + def func(): + msg = _('text') + LOG.warn(msg) + raise Exception('some other message') + """, + 'expected_errors': [ + (4, 13, 'K006'), + ], + }, + { + 'code': """ + LOG = log.getLogger(__name__) + if True: + msg = _('text') + else: + msg = _('text') + LOG.warn(msg) + raise Exception(msg) + """, + 'expected_errors': [ + ], + }, + { + 'code': """ + LOG = log.getLogger(__name__) + if True: + msg = _('text') + else: + msg = _('text') + LOG.warn(msg) + """, + 'expected_errors': [ + (6, 9, 'K006'), + ], + }, + { + 'code': """ + LOG = log.getLogger(__name__) + msg = _LW('text') + LOG.warn(msg) + raise Exception(msg) + """, + 'expected_errors': [ + (3, 9, 'K007'), + ], + }, + { + 'code': """ + LOG = log.getLogger(__name__) + msg = _LW('text') + LOG.warn(msg) + msg = _('something else') + raise Exception(msg) + """, + 'expected_errors': [], + }, + { + 'code': """ + LOG = log.getLogger(__name__) + msg = _LW('hello %s') % 'world' + LOG.warn(msg) + raise Exception(msg) + """, + 'expected_errors': [ + (3, 9, 'K007'), + ], + }, + { + 'code': """ + LOG = log.getLogger(__name__) + msg = _LW('hello %s') % 'world' + LOG.warn(msg) + """, + 'expected_errors': [], + }, + { + 'code': """ + # this should not be an error + LOG = log.getLogger(__name__) + try: + something = True + except AssertionError as e: + LOG.warning(six.text_type(e)) + raise exception.Unauthorized(e) + """, + 'expected_errors': [], + }, + ] diff --git a/keystone-moon/keystone/tests/unit/ksfixtures/key_repository.py b/keystone-moon/keystone/tests/unit/ksfixtures/key_repository.py new file mode 100644 index 00000000..d1ac2ab4 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/ksfixtures/key_repository.py @@ -0,0 +1,34 @@ +# 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 shutil +import tempfile + +import fixtures + +from keystone.token.providers.fernet import utils + + +class KeyRepository(fixtures.Fixture): + def __init__(self, config_fixture): + super(KeyRepository, self).__init__() + self.config_fixture = config_fixture + + def setUp(self): + super(KeyRepository, self).setUp() + directory = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, directory) + self.config_fixture.config(group='fernet_tokens', + key_repository=directory) + + utils.create_key_directory() + utils.initialize_key_repository() diff --git a/keystone-moon/keystone/tests/unit/ksfixtures/temporaryfile.py b/keystone-moon/keystone/tests/unit/ksfixtures/temporaryfile.py new file mode 100644 index 00000000..a4be06f8 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/ksfixtures/temporaryfile.py @@ -0,0 +1,29 @@ +# 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 os +import tempfile + +import fixtures + + +class SecureTempFile(fixtures.Fixture): + """A fixture for creating a secure temp file.""" + + def setUp(self): + super(SecureTempFile, self).setUp() + + _fd, self.file_name = tempfile.mkstemp() + # Make sure no file descriptors are leaked, close the unused FD. + os.close(_fd) + self.addCleanup(os.remove, self.file_name) diff --git a/keystone-moon/keystone/tests/unit/mapping_fixtures.py b/keystone-moon/keystone/tests/unit/mapping_fixtures.py new file mode 100644 index 00000000..0892ada5 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/mapping_fixtures.py @@ -0,0 +1,1023 @@ +# 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. + +"""Fixtures for Federation Mapping.""" + +EMPLOYEE_GROUP_ID = "0cd5e9" +CONTRACTOR_GROUP_ID = "85a868" +TESTER_GROUP_ID = "123" +TESTER_GROUP_NAME = "tester" +DEVELOPER_GROUP_ID = "xyz" +DEVELOPER_GROUP_NAME = "Developer" +CONTRACTOR_GROUP_NAME = "Contractor" +DEVELOPER_GROUP_DOMAIN_NAME = "outsourcing" +DEVELOPER_GROUP_DOMAIN_ID = "5abc43" +FEDERATED_DOMAIN = "Federated" +LOCAL_DOMAIN = "Local" + +# Mapping summary: +# LastName Smith & Not Contractor or SubContractor -> group 0cd5e9 +# FirstName Jill & Contractor or SubContractor -> to group 85a868 +MAPPING_SMALL = { + "rules": [ + { + "local": [ + { + "group": { + "id": EMPLOYEE_GROUP_ID + } + }, + { + "user": { + "name": "{0}" + } + } + ], + "remote": [ + { + "type": "UserName" + }, + { + "type": "orgPersonType", + "not_any_of": [ + "Contractor", + "SubContractor" + ] + }, + { + "type": "LastName", + "any_one_of": [ + "Bo" + ] + } + ] + }, + { + "local": [ + { + "group": { + "id": CONTRACTOR_GROUP_ID + } + }, + { + "user": { + "name": "{0}" + } + } + ], + "remote": [ + { + "type": "UserName" + }, + { + "type": "orgPersonType", + "any_one_of": [ + "Contractor", + "SubContractor" + ] + }, + { + "type": "FirstName", + "any_one_of": [ + "Jill" + ] + } + ] + } + ] +} + +# Mapping summary: +# orgPersonType Admin or Big Cheese -> name {0} {1} email {2} and group 0cd5e9 +# orgPersonType Customer -> user name {0} email {1} +# orgPersonType Test and email ^@example.com$ -> group 123 and xyz +MAPPING_LARGE = { + "rules": [ + { + "local": [ + { + "user": { + "name": "{0} {1}", + "email": "{2}" + }, + "group": { + "id": EMPLOYEE_GROUP_ID + } + } + ], + "remote": [ + { + "type": "FirstName" + }, + { + "type": "LastName" + }, + { + "type": "Email" + }, + { + "type": "orgPersonType", + "any_one_of": [ + "Admin", + "Big Cheese" + ] + } + ] + }, + { + "local": [ + { + "user": { + "name": "{0}", + "email": "{1}" + } + } + ], + "remote": [ + { + "type": "UserName" + }, + { + "type": "Email" + }, + { + "type": "orgPersonType", + "not_any_of": [ + "Admin", + "Employee", + "Contractor", + "Tester" + ] + } + ] + }, + { + "local": [ + { + "group": { + "id": TESTER_GROUP_ID + } + }, + { + "group": { + "id": DEVELOPER_GROUP_ID + } + }, + { + "user": { + "name": "{0}" + } + } + ], + "remote": [ + { + "type": "UserName" + }, + { + "type": "orgPersonType", + "any_one_of": [ + "Tester" + ] + }, + { + "type": "Email", + "any_one_of": [ + ".*@example.com$" + ], + "regex": True + } + ] + } + ] +} + +MAPPING_BAD_REQ = { + "rules": [ + { + "local": [ + { + "user": "name" + } + ], + "remote": [ + { + "type": "UserName", + "bad_requirement": [ + "Young" + ] + } + ] + } + ] +} + +MAPPING_BAD_VALUE = { + "rules": [ + { + "local": [ + { + "user": "name" + } + ], + "remote": [ + { + "type": "UserName", + "any_one_of": "should_be_list" + } + ] + } + ] +} + +MAPPING_NO_RULES = { + 'rules': [] +} + +MAPPING_NO_REMOTE = { + "rules": [ + { + "local": [ + { + "user": "name" + } + ], + "remote": [] + } + ] +} + +MAPPING_MISSING_LOCAL = { + "rules": [ + { + "remote": [ + { + "type": "UserName", + "any_one_of": "should_be_list" + } + ] + } + ] +} + +MAPPING_WRONG_TYPE = { + "rules": [ + { + "local": [ + { + "user": "{1}" + } + ], + "remote": [ + { + "not_type": "UserName" + } + ] + } + ] +} + +MAPPING_MISSING_TYPE = { + "rules": [ + { + "local": [ + { + "user": "{1}" + } + ], + "remote": [ + {} + ] + } + ] +} + +MAPPING_EXTRA_REMOTE_PROPS_NOT_ANY_OF = { + "rules": [ + { + "local": [ + { + "group": { + "id": "0cd5e9" + } + }, + { + "user": { + "name": "{0}" + } + } + ], + "remote": [ + { + "type": "UserName" + }, + { + "type": "orgPersonType", + "not_any_of": [ + "SubContractor" + ], + "invalid_type": "xyz" + } + ] + } + ] +} + +MAPPING_EXTRA_REMOTE_PROPS_ANY_ONE_OF = { + "rules": [ + { + "local": [ + { + "group": { + "id": "0cd5e9" + } + }, + { + "user": { + "name": "{0}" + } + } + ], + "remote": [ + { + "type": "UserName" + }, + { + "type": "orgPersonType", + "any_one_of": [ + "SubContractor" + ], + "invalid_type": "xyz" + } + ] + } + ] +} + +MAPPING_EXTRA_REMOTE_PROPS_JUST_TYPE = { + "rules": [ + { + "local": [ + { + "group": { + "id": "0cd5e9" + } + }, + { + "user": { + "name": "{0}" + } + } + ], + "remote": [ + { + "type": "UserName" + }, + { + "type": "orgPersonType", + "invalid_type": "xyz" + } + ] + } + ] +} + +MAPPING_EXTRA_RULES_PROPS = { + "rules": [ + { + "local": [ + { + "group": { + "id": "0cd5e9" + } + }, + { + "user": { + "name": "{0}" + } + } + ], + "invalid_type": { + "id": "xyz", + }, + "remote": [ + { + "type": "UserName" + }, + { + "type": "orgPersonType", + "not_any_of": [ + "SubContractor" + ] + } + ] + } + ] +} + +MAPPING_TESTER_REGEX = { + "rules": [ + { + "local": [ + { + "user": { + "name": "{0}", + } + } + ], + "remote": [ + { + "type": "UserName" + } + ] + }, + { + "local": [ + { + "group": { + "id": TESTER_GROUP_ID + } + } + ], + "remote": [ + { + "type": "orgPersonType", + "any_one_of": [ + ".*Tester*" + ], + "regex": True + } + ] + } + ] +} + +MAPPING_DEVELOPER_REGEX = { + "rules": [ + { + "local": [ + { + "user": { + "name": "{0}", + }, + "group": { + "id": DEVELOPER_GROUP_ID + } + } + ], + "remote": [ + { + "type": "UserName" + }, + { + "type": "orgPersonType", + "any_one_of": [ + "Developer" + ], + }, + { + "type": "Email", + "not_any_of": [ + ".*@example.org$" + ], + "regex": True + } + ] + } + ] +} + +MAPPING_GROUP_NAMES = { + + "rules": [ + { + "local": [ + { + "user": { + "name": "{0}", + } + } + ], + "remote": [ + { + "type": "UserName" + } + ] + }, + { + "local": [ + { + "group": { + "name": DEVELOPER_GROUP_NAME, + "domain": { + "name": DEVELOPER_GROUP_DOMAIN_NAME + } + } + } + ], + "remote": [ + { + "type": "orgPersonType", + "any_one_of": [ + "Employee" + ], + } + ] + }, + { + "local": [ + { + "group": { + "name": TESTER_GROUP_NAME, + "domain": { + "id": DEVELOPER_GROUP_DOMAIN_ID + } + } + } + ], + "remote": [ + { + "type": "orgPersonType", + "any_one_of": [ + "BuildingX" + ] + } + ] + }, + ] +} + +MAPPING_EPHEMERAL_USER = { + "rules": [ + { + "local": [ + { + "user": { + "name": "{0}", + "domain": { + "id": FEDERATED_DOMAIN + }, + "type": "ephemeral" + } + } + ], + "remote": [ + { + "type": "UserName" + }, + { + "type": "UserName", + "any_one_of": [ + "tbo" + ] + } + ] + } + ] +} + +MAPPING_GROUPS_WHITELIST = { + "rules": [ + { + "remote": [ + { + "type": "orgPersonType", + "whitelist": [ + "Developer", "Contractor" + ] + }, + { + "type": "UserName" + } + ], + "local": [ + { + "groups": "{0}", + "domain": { + "id": DEVELOPER_GROUP_DOMAIN_ID + } + }, + { + "user": { + "name": "{1}" + } + } + ] + } + ] +} + +MAPPING_EPHEMERAL_USER_LOCAL_DOMAIN = { + "rules": [ + { + "local": [ + { + "user": { + "name": "{0}", + "domain": { + "id": LOCAL_DOMAIN + }, + "type": "ephemeral" + } + } + ], + "remote": [ + { + "type": "UserName" + }, + { + "type": "UserName", + "any_one_of": [ + "jsmith" + ] + } + ] + } + ] +} + +MAPPING_GROUPS_WHITELIST_MISSING_DOMAIN = { + "rules": [ + { + "remote": [ + { + "type": "orgPersonType", + "whitelist": [ + "Developer", "Contractor" + ] + }, + ], + "local": [ + { + "groups": "{0}", + } + ] + } + ] +} + +MAPPING_LOCAL_USER_LOCAL_DOMAIN = { + "rules": [ + { + "local": [ + { + "user": { + "name": "{0}", + "domain": { + "id": LOCAL_DOMAIN + }, + "type": "local" + } + } + ], + "remote": [ + { + "type": "UserName" + }, + { + "type": "UserName", + "any_one_of": [ + "jsmith" + ] + } + ] + } + ] +} + +MAPPING_GROUPS_BLACKLIST_MULTIPLES = { + "rules": [ + { + "remote": [ + { + "type": "orgPersonType", + "blacklist": [ + "Developer", "Manager" + ] + }, + { + "type": "Thing" # this could be variable length! + }, + { + "type": "UserName" + }, + ], + "local": [ + { + "groups": "{0}", + "domain": { + "id": DEVELOPER_GROUP_DOMAIN_ID + } + }, + { + "user": { + "name": "{2}", + } + } + ] + } + ] +} +MAPPING_GROUPS_BLACKLIST = { + "rules": [ + { + "remote": [ + { + "type": "orgPersonType", + "blacklist": [ + "Developer", "Manager" + ] + }, + { + "type": "UserName" + } + ], + "local": [ + { + "groups": "{0}", + "domain": { + "id": DEVELOPER_GROUP_DOMAIN_ID + } + }, + { + "user": { + "name": "{1}" + } + } + ] + } + ] +} + +# Excercise all possibilities of user identitfication. Values are hardcoded on +# purpose. +MAPPING_USER_IDS = { + "rules": [ + { + "local": [ + { + "user": { + "name": "{0}" + } + } + ], + "remote": [ + { + "type": "UserName" + }, + { + "type": "UserName", + "any_one_of": [ + "jsmith" + ] + } + ] + }, + { + "local": [ + { + "user": { + "name": "{0}", + "domain": { + "id": "federated" + } + } + } + ], + "remote": [ + { + "type": "UserName" + }, + { + "type": "UserName", + "any_one_of": [ + "tbo" + ] + } + ] + }, + { + "local": [ + { + "user": { + "id": "{0}" + } + } + ], + "remote": [ + { + "type": "UserName" + }, + { + "type": "UserName", + "any_one_of": [ + "bob" + ] + } + ] + }, + { + "local": [ + { + "user": { + "id": "abc123", + "name": "{0}", + "domain": { + "id": "federated" + } + } + } + ], + "remote": [ + { + "type": "UserName" + }, + { + "type": "UserName", + "any_one_of": [ + "bwilliams" + ] + } + ] + } + ] +} + +MAPPING_GROUPS_BLACKLIST_MISSING_DOMAIN = { + "rules": [ + { + "remote": [ + { + "type": "orgPersonType", + "blacklist": [ + "Developer", "Manager" + ] + }, + ], + "local": [ + { + "groups": "{0}", + }, + ] + } + ] +} + +MAPPING_GROUPS_WHITELIST_AND_BLACKLIST = { + "rules": [ + { + "remote": [ + { + "type": "orgPersonType", + "blacklist": [ + "Employee" + ], + "whitelist": [ + "Contractor" + ] + }, + ], + "local": [ + { + "groups": "{0}", + "domain": { + "id": DEVELOPER_GROUP_DOMAIN_ID + } + }, + ] + } + ] +} + +EMPLOYEE_ASSERTION = { + 'Email': 'tim@example.com', + 'UserName': 'tbo', + 'FirstName': 'Tim', + 'LastName': 'Bo', + 'orgPersonType': 'Employee;BuildingX' +} + +EMPLOYEE_ASSERTION_MULTIPLE_GROUPS = { + 'Email': 'tim@example.com', + 'UserName': 'tbo', + 'FirstName': 'Tim', + 'LastName': 'Bo', + 'orgPersonType': 'Developer;Manager;Contractor', + 'Thing': 'yes!;maybe!;no!!' +} + +EMPLOYEE_ASSERTION_PREFIXED = { + 'PREFIX_Email': 'tim@example.com', + 'PREFIX_UserName': 'tbo', + 'PREFIX_FirstName': 'Tim', + 'PREFIX_LastName': 'Bo', + 'PREFIX_orgPersonType': 'SuperEmployee;BuildingX' +} + +CONTRACTOR_ASSERTION = { + 'Email': 'jill@example.com', + 'UserName': 'jsmith', + 'FirstName': 'Jill', + 'LastName': 'Smith', + 'orgPersonType': 'Contractor;Non-Dev' +} + +ADMIN_ASSERTION = { + 'Email': 'bob@example.com', + 'UserName': 'bob', + 'FirstName': 'Bob', + 'LastName': 'Thompson', + 'orgPersonType': 'Admin;Chief' +} + +CUSTOMER_ASSERTION = { + 'Email': 'beth@example.com', + 'UserName': 'bwilliams', + 'FirstName': 'Beth', + 'LastName': 'Williams', + 'orgPersonType': 'Customer' +} + +ANOTHER_CUSTOMER_ASSERTION = { + 'Email': 'mark@example.com', + 'UserName': 'markcol', + 'FirstName': 'Mark', + 'LastName': 'Collins', + 'orgPersonType': 'Managers;CEO;CTO' +} + +TESTER_ASSERTION = { + 'Email': 'testacct@example.com', + 'UserName': 'testacct', + 'FirstName': 'Test', + 'LastName': 'Account', + 'orgPersonType': 'MadeupGroup;Tester;GroupX' +} + +ANOTHER_TESTER_ASSERTION = { + 'UserName': 'IamTester' +} + +BAD_TESTER_ASSERTION = { + 'Email': 'eviltester@example.org', + 'UserName': 'Evil', + 'FirstName': 'Test', + 'LastName': 'Account', + 'orgPersonType': 'Tester' +} + +BAD_DEVELOPER_ASSERTION = { + 'Email': 'evildeveloper@example.org', + 'UserName': 'Evil', + 'FirstName': 'Develop', + 'LastName': 'Account', + 'orgPersonType': 'Developer' +} + +MALFORMED_TESTER_ASSERTION = { + 'Email': 'testacct@example.com', + 'UserName': 'testacct', + 'FirstName': 'Test', + 'LastName': 'Account', + 'orgPersonType': 'Tester', + 'object': object(), + 'dictionary': dict(zip('teststring', xrange(10))), + 'tuple': tuple(xrange(5)) +} + +DEVELOPER_ASSERTION = { + 'Email': 'developacct@example.com', + 'UserName': 'developacct', + 'FirstName': 'Develop', + 'LastName': 'Account', + 'orgPersonType': 'Developer' +} + +CONTRACTOR_MALFORMED_ASSERTION = { + 'UserName': 'user', + 'FirstName': object(), + 'orgPersonType': 'Contractor' +} + +LOCAL_USER_ASSERTION = { + 'UserName': 'marek', + 'UserType': 'random' +} + +ANOTHER_LOCAL_USER_ASSERTION = { + 'UserName': 'marek', + 'Position': 'DirectorGeneral' +} + +UNMATCHED_GROUP_ASSERTION = { + 'REMOTE_USER': 'Any Momoose', + 'REMOTE_USER_GROUPS': 'EXISTS;NO_EXISTS' +} diff --git a/keystone-moon/keystone/tests/unit/rest.py b/keystone-moon/keystone/tests/unit/rest.py new file mode 100644 index 00000000..16513024 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/rest.py @@ -0,0 +1,245 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 oslo_serialization import jsonutils +import six +import webtest + +from keystone.auth import controllers as auth_controllers +from keystone.tests import unit as tests +from keystone.tests.unit import default_fixtures +from keystone.tests.unit.ksfixtures import database + + +class RestfulTestCase(tests.TestCase): + """Performs restful tests against the WSGI app over HTTP. + + This class launches public & admin WSGI servers for every test, which can + be accessed by calling ``public_request()`` or ``admin_request()``, + respectfully. + + ``restful_request()`` and ``request()`` methods are also exposed if you + need to bypass restful conventions or access HTTP details in your test + implementation. + + Three new asserts are provided: + + * ``assertResponseSuccessful``: called automatically for every request + unless an ``expected_status`` is provided + * ``assertResponseStatus``: called instead of ``assertResponseSuccessful``, + if an ``expected_status`` is provided + * ``assertValidResponseHeaders``: validates that the response headers + appear as expected + + Requests are automatically serialized according to the defined + ``content_type``. Responses are automatically deserialized as well, and + available in the ``response.body`` attribute. The original body content is + available in the ``response.raw`` attribute. + + """ + + # default content type to test + content_type = 'json' + + def get_extensions(self): + return None + + def setUp(self, app_conf='keystone'): + super(RestfulTestCase, self).setUp() + + # Will need to reset the plug-ins + self.addCleanup(setattr, auth_controllers, 'AUTH_METHODS', {}) + + self.useFixture(database.Database(extensions=self.get_extensions())) + self.load_backends() + self.load_fixtures(default_fixtures) + + self.public_app = webtest.TestApp( + self.loadapp(app_conf, name='main')) + self.addCleanup(delattr, self, 'public_app') + self.admin_app = webtest.TestApp( + self.loadapp(app_conf, name='admin')) + self.addCleanup(delattr, self, 'admin_app') + + def request(self, app, path, body=None, headers=None, token=None, + expected_status=None, **kwargs): + if headers: + headers = {str(k): str(v) for k, v in six.iteritems(headers)} + else: + headers = {} + + if token: + headers['X-Auth-Token'] = str(token) + + # sets environ['REMOTE_ADDR'] + kwargs.setdefault('remote_addr', 'localhost') + + response = app.request(path, headers=headers, + status=expected_status, body=body, + **kwargs) + + return response + + def assertResponseSuccessful(self, response): + """Asserts that a status code lies inside the 2xx range. + + :param response: :py:class:`httplib.HTTPResponse` to be + verified to have a status code between 200 and 299. + + example:: + + self.assertResponseSuccessful(response) + """ + self.assertTrue( + response.status_code >= 200 and response.status_code <= 299, + 'Status code %d is outside of the expected range (2xx)\n\n%s' % + (response.status, response.body)) + + def assertResponseStatus(self, response, expected_status): + """Asserts a specific status code on the response. + + :param response: :py:class:`httplib.HTTPResponse` + :param expected_status: The specific ``status`` result expected + + example:: + + self.assertResponseStatus(response, 204) + """ + self.assertEqual( + response.status_code, + expected_status, + 'Status code %s is not %s, as expected)\n\n%s' % + (response.status_code, expected_status, response.body)) + + def assertValidResponseHeaders(self, response): + """Ensures that response headers appear as expected.""" + self.assertIn('X-Auth-Token', response.headers.get('Vary')) + + def assertValidErrorResponse(self, response, expected_status=400): + """Verify that the error response is valid. + + Subclasses can override this function based on the expected response. + + """ + self.assertEqual(response.status_code, expected_status) + error = response.result['error'] + self.assertEqual(error['code'], response.status_code) + self.assertIsNotNone(error.get('title')) + + def _to_content_type(self, body, headers, content_type=None): + """Attempt to encode JSON and XML automatically.""" + content_type = content_type or self.content_type + + if content_type == 'json': + headers['Accept'] = 'application/json' + if body: + headers['Content-Type'] = 'application/json' + return jsonutils.dumps(body) + + def _from_content_type(self, response, content_type=None): + """Attempt to decode JSON and XML automatically, if detected.""" + content_type = content_type or self.content_type + + if response.body is not None and response.body.strip(): + # if a body is provided, a Content-Type is also expected + header = response.headers.get('Content-Type') + self.assertIn(content_type, header) + + if content_type == 'json': + response.result = jsonutils.loads(response.body) + else: + response.result = response.body + + def restful_request(self, method='GET', headers=None, body=None, + content_type=None, response_content_type=None, + **kwargs): + """Serializes/deserializes json as request/response body. + + .. WARNING:: + + * Existing Accept header will be overwritten. + * Existing Content-Type header will be overwritten. + + """ + # Initialize headers dictionary + headers = {} if not headers else headers + + body = self._to_content_type(body, headers, content_type) + + # Perform the HTTP request/response + response = self.request(method=method, headers=headers, body=body, + **kwargs) + + response_content_type = response_content_type or content_type + self._from_content_type(response, content_type=response_content_type) + + # we can save some code & improve coverage by always doing this + if method != 'HEAD' and response.status_code >= 400: + self.assertValidErrorResponse(response) + + # Contains the decoded response.body + return response + + def _request(self, convert=True, **kwargs): + if convert: + response = self.restful_request(**kwargs) + else: + response = self.request(**kwargs) + + self.assertValidResponseHeaders(response) + return response + + def public_request(self, **kwargs): + return self._request(app=self.public_app, **kwargs) + + def admin_request(self, **kwargs): + return self._request(app=self.admin_app, **kwargs) + + def _get_token(self, body): + """Convenience method so that we can test authenticated requests.""" + r = self.public_request(method='POST', path='/v2.0/tokens', body=body) + return self._get_token_id(r) + + def get_unscoped_token(self): + """Convenience method so that we can test authenticated requests.""" + return self._get_token({ + 'auth': { + 'passwordCredentials': { + 'username': self.user_foo['name'], + 'password': self.user_foo['password'], + }, + }, + }) + + def get_scoped_token(self, tenant_id=None): + """Convenience method so that we can test authenticated requests.""" + if not tenant_id: + tenant_id = self.tenant_bar['id'] + return self._get_token({ + 'auth': { + 'passwordCredentials': { + 'username': self.user_foo['name'], + 'password': self.user_foo['password'], + }, + 'tenantId': tenant_id, + }, + }) + + def _get_token_id(self, r): + """Helper method to return a token ID from a response. + + This needs to be overridden by child classes for on their content type. + + """ + raise NotImplementedError() diff --git a/keystone-moon/keystone/tests/unit/saml2/idp_saml2_metadata.xml b/keystone-moon/keystone/tests/unit/saml2/idp_saml2_metadata.xml new file mode 100644 index 00000000..db235f7c --- /dev/null +++ b/keystone-moon/keystone/tests/unit/saml2/idp_saml2_metadata.xml @@ -0,0 +1,25 @@ + + + + + + + MIIDpTCCAo0CAREwDQYJKoZIhvcNAQEFBQAwgZ4xCjAIBgNVBAUTATUxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTESMBAGA1UEBxMJU3Vubnl2YWxlMRIwEAYDVQQKEwlPcGVuU3RhY2sxETAPBgNVBAsTCEtleXN0b25lMSUwIwYJKoZIhvcNAQkBFhZrZXlzdG9uZUBvcGVuc3RhY2sub3JnMRQwEgYDVQQDEwtTZWxmIFNpZ25lZDAgFw0xMzA3MDkxNjI1MDBaGA8yMDcyMDEwMTE2MjUwMFowgY8xCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTESMBAGA1UEBxMJU3Vubnl2YWxlMRIwEAYDVQQKEwlPcGVuU3RhY2sxETAPBgNVBAsTCEtleXN0b25lMSUwIwYJKoZIhvcNAQkBFhZrZXlzdG9uZUBvcGVuc3RhY2sub3JnMREwDwYDVQQDEwhLZXlzdG9uZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMTC6IdNd9Cg1DshcrT5gRVRF36nEmjSA9QWdik7B925PK70U4F6j4pz/5JL7plIo/8rJ4jJz9ccE7m0iA+IuABtEhEwXkG9rj47Oy0J4ZyDGSh2K1Bl78PA9zxXSzysUTSjBKdAh29dPYbJY7cgZJ0uC3AtfVceYiAOIi14SdFeZ0LZLDXBuLaqUmSMrmKwJ9wAMOCb/jbBP9/3Ycd0GYjlvrSBU4Bqb8/NHasyO4DpPN68OAoyD5r5jUtV8QZN03UjIsoux8e0lrL6+MVtJo0OfWvlSrlzS5HKSryY+uqqQEuxtZKpJM2MV85ujvjc8eDSChh2shhDjBem3FIlHKUCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAed9fHgdJrk+gZcO5gsqq6uURfDOuYD66GsSdZw4BqHjYAcnyWq2da+iw7Uxkqu7iLf2k4+Hu3xjDFrce479OwZkSnbXmqB7XspTGOuM8MgT7jB/ypKTOZ6qaZKSWK1Hta995hMrVVlhUNBLh0MPGqoVWYA4d7mblujgH9vp+4mpCciJagHks8K5FBmI+pobB+uFdSYDoRzX9LTpStspK4e3IoY8baILuGcdKimRNBv6ItG4hMrntAe1/nWMJyUu5rDTGf2V/vAaS0S/faJBwQSz1o38QHMTWHNspfwIdX3yMqI9u7/vYlz3rLy5WdBdUgZrZ3/VLmJTiJVZu5Owq4Q== + + + + + + + openstack + openstack + openstack + + + openstack + first + lastname + admin@example.com + 555-555-5555 + + diff --git a/keystone-moon/keystone/tests/unit/saml2/signed_saml2_assertion.xml b/keystone-moon/keystone/tests/unit/saml2/signed_saml2_assertion.xml new file mode 100644 index 00000000..410f9388 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/saml2/signed_saml2_assertion.xml @@ -0,0 +1,63 @@ + + https://acme.com/FIM/sps/openstack/saml20 + + + + + + + + + + + Lem2TKyYt+/tJy2iSos1t0KxcJE= + + + b//GXtGeCIJPFsMAHrx4+3yjrL4smSpRLXG9PB3TLMJvU4fx8n2PzK7+VbtWNbZG +vSgbvbQR52jq77iyaRfQ2iELuFEY+YietLRi7hsitkJCEayPmU+BDlNIGuCXZjAy +7tmtGFkLlZZJaom1jAzHfZ5JPjZdM5hvQwrhCI2Kzyk= + + + MIICtjCCAh+gAwIBAgIJAJTeBUN2i9ZNMA0GCSqGSIb3DQEBBQUAME4xCzAJBgNV +BAYTAkhSMQ8wDQYDVQQIEwZaYWdyZWIxITAfBgNVBAoTGE5la2Egb3JnYW5pemFj +aWphIGQuby5vLjELMAkGA1UEAxMCQ0EwHhcNMTIxMjI4MTYwODA1WhcNMTQxMjI4 +MTYwODA1WjBvMQswCQYDVQQGEwJIUjEPMA0GA1UECBMGWmFncmViMQ8wDQYDVQQH +EwZaYWdyZWIxITAfBgNVBAoTGE5la2Egb3JnYW5pemFjaWphIGQuby5vLjEbMBkG +A1UEAxMSUHJvZ3JhbWVyc2thIGZpcm1hMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB +iQKBgQCgWApHV5cma0GY/v/vmwgciDQBgITcitx2rG0F+ghXtGiEJeK75VY7jQwE +UFCbgV+AaOY2NQChK2FKec7Hss/5y+jbWfX2yVwX6TYcCwnOGXenz+cgx2Fwqpu3 +ncL6dYJMfdbKvojBaJQLJTaNjRJsZACButDsDtXDSH9QaRy+hQIDAQABo3sweTAJ +BgNVHRMEAjAAMCwGCWCGSAGG+EIBDQQfFh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0 +aWZpY2F0ZTAdBgNVHQ4EFgQUSo9ThP/MOg8QIRWxoPo8qKR8O2wwHwYDVR0jBBgw +FoAUAelckr4bx8MwZ7y+VlHE46Mbo+cwDQYJKoZIhvcNAQEFBQADgYEAy19Z7Z5/ +/MlWkogu41s0RxL9ffG60QQ0Y8hhDTmgHNx1itj0wT8pB7M4KVMbZ4hjjSFsfRq4 +Vj7jm6LwU0WtZ3HGl8TygTh8AAJvbLROnTjLL5MqI9d9pKvIIfZ2Qs3xmJ7JEv4H +UHeBXxQq/GmfBv3l+V5ObQ+EHKnyDodLHCk= + + + + + test_user + + + + + + + urn:oasis:names:tc:SAML:2.0:ac:classes:Password + https://acme.com/FIM/sps/openstack/saml20 + + + + + test_user + + + admin + member + + + development + + + diff --git a/keystone-moon/keystone/tests/unit/test_associate_project_endpoint_extension.py b/keystone-moon/keystone/tests/unit/test_associate_project_endpoint_extension.py new file mode 100644 index 00000000..e0159b76 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_associate_project_endpoint_extension.py @@ -0,0 +1,1129 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 +import uuid + +from testtools import matchers + +# NOTE(morganfainberg): import endpoint filter to populate the SQL model +from keystone.contrib import endpoint_filter # noqa +from keystone.tests.unit import test_v3 + + +class TestExtensionCase(test_v3.RestfulTestCase): + + EXTENSION_NAME = 'endpoint_filter' + EXTENSION_TO_ADD = 'endpoint_filter_extension' + + def config_overrides(self): + super(TestExtensionCase, self).config_overrides() + self.config_fixture.config( + group='catalog', + driver='keystone.contrib.endpoint_filter.backends.catalog_sql.' + 'EndpointFilterCatalog') + + def setUp(self): + super(TestExtensionCase, self).setUp() + self.default_request_url = ( + '/OS-EP-FILTER/projects/%(project_id)s' + '/endpoints/%(endpoint_id)s' % { + 'project_id': self.default_domain_project_id, + 'endpoint_id': self.endpoint_id}) + + +class EndpointFilterCRUDTestCase(TestExtensionCase): + + def test_create_endpoint_project_association(self): + """PUT /OS-EP-FILTER/projects/{project_id}/endpoints/{endpoint_id} + + Valid endpoint and project id test case. + + """ + self.put(self.default_request_url, + body='', + expected_status=204) + + def test_create_endpoint_project_association_with_invalid_project(self): + """PUT OS-EP-FILTER/projects/{project_id}/endpoints/{endpoint_id} + + Invalid project id test case. + + """ + self.put('/OS-EP-FILTER/projects/%(project_id)s' + '/endpoints/%(endpoint_id)s' % { + 'project_id': uuid.uuid4().hex, + 'endpoint_id': self.endpoint_id}, + body='', + expected_status=404) + + def test_create_endpoint_project_association_with_invalid_endpoint(self): + """PUT /OS-EP-FILTER/projects/{project_id}/endpoints/{endpoint_id} + + Invalid endpoint id test case. + + """ + self.put('/OS-EP-FILTER/projects/%(project_id)s' + '/endpoints/%(endpoint_id)s' % { + 'project_id': self.default_domain_project_id, + 'endpoint_id': uuid.uuid4().hex}, + body='', + expected_status=404) + + def test_create_endpoint_project_association_with_unexpected_body(self): + """PUT /OS-EP-FILTER/projects/{project_id}/endpoints/{endpoint_id} + + Unexpected body in request. The body should be ignored. + + """ + self.put(self.default_request_url, + body={'project_id': self.default_domain_project_id}, + expected_status=204) + + def test_check_endpoint_project_association(self): + """HEAD /OS-EP-FILTER/projects/{project_id}/endpoints/{endpoint_id} + + Valid project and endpoint id test case. + + """ + self.put(self.default_request_url, + body='', + expected_status=204) + self.head('/OS-EP-FILTER/projects/%(project_id)s' + '/endpoints/%(endpoint_id)s' % { + 'project_id': self.default_domain_project_id, + 'endpoint_id': self.endpoint_id}, + expected_status=204) + + def test_check_endpoint_project_association_with_invalid_project(self): + """HEAD /OS-EP-FILTER/projects/{project_id}/endpoints/{endpoint_id} + + Invalid project id test case. + + """ + self.put(self.default_request_url) + self.head('/OS-EP-FILTER/projects/%(project_id)s' + '/endpoints/%(endpoint_id)s' % { + 'project_id': uuid.uuid4().hex, + 'endpoint_id': self.endpoint_id}, + body='', + expected_status=404) + + def test_check_endpoint_project_association_with_invalid_endpoint(self): + """HEAD /OS-EP-FILTER/projects/{project_id}/endpoints/{endpoint_id} + + Invalid endpoint id test case. + + """ + self.put(self.default_request_url) + self.head('/OS-EP-FILTER/projects/%(project_id)s' + '/endpoints/%(endpoint_id)s' % { + 'project_id': self.default_domain_project_id, + 'endpoint_id': uuid.uuid4().hex}, + body='', + expected_status=404) + + def test_list_endpoints_associated_with_valid_project(self): + """GET /OS-EP-FILTER/projects/{project_id}/endpoints + + Valid project and endpoint id test case. + + """ + self.put(self.default_request_url) + resource_url = '/OS-EP-FILTER/projects/%(project_id)s/endpoints' % { + 'project_id': self.default_domain_project_id} + r = self.get(resource_url) + self.assertValidEndpointListResponse(r, self.endpoint, + resource_url=resource_url) + + def test_list_endpoints_associated_with_invalid_project(self): + """GET /OS-EP-FILTER/projects/{project_id}/endpoints + + Invalid project id test case. + + """ + self.put(self.default_request_url) + self.get('/OS-EP-FILTER/projects/%(project_id)s/endpoints' % { + 'project_id': uuid.uuid4().hex}, + body='', + expected_status=404) + + def test_list_projects_associated_with_endpoint(self): + """GET /OS-EP-FILTER/endpoints/{endpoint_id}/projects + + Valid endpoint-project association test case. + + """ + self.put(self.default_request_url) + resource_url = '/OS-EP-FILTER/endpoints/%(endpoint_id)s/projects' % { + 'endpoint_id': self.endpoint_id} + r = self.get(resource_url) + self.assertValidProjectListResponse(r, self.default_domain_project, + resource_url=resource_url) + + def test_list_projects_with_no_endpoint_project_association(self): + """GET /OS-EP-FILTER/endpoints/{endpoint_id}/projects + + Valid endpoint id but no endpoint-project associations test case. + + """ + r = self.get('/OS-EP-FILTER/endpoints/%(endpoint_id)s/projects' % + {'endpoint_id': self.endpoint_id}, + expected_status=200) + self.assertValidProjectListResponse(r, expected_length=0) + + def test_list_projects_associated_with_invalid_endpoint(self): + """GET /OS-EP-FILTER/endpoints/{endpoint_id}/projects + + Invalid endpoint id test case. + + """ + self.get('/OS-EP-FILTER/endpoints/%(endpoint_id)s/projects' % + {'endpoint_id': uuid.uuid4().hex}, + expected_status=404) + + def test_remove_endpoint_project_association(self): + """DELETE /OS-EP-FILTER/projects/{project_id}/endpoints/{endpoint_id} + + Valid project id and endpoint id test case. + + """ + self.put(self.default_request_url) + self.delete('/OS-EP-FILTER/projects/%(project_id)s' + '/endpoints/%(endpoint_id)s' % { + 'project_id': self.default_domain_project_id, + 'endpoint_id': self.endpoint_id}, + expected_status=204) + + def test_remove_endpoint_project_association_with_invalid_project(self): + """DELETE /OS-EP-FILTER/projects/{project_id}/endpoints/{endpoint_id} + + Invalid project id test case. + + """ + self.put(self.default_request_url) + self.delete('/OS-EP-FILTER/projects/%(project_id)s' + '/endpoints/%(endpoint_id)s' % { + 'project_id': uuid.uuid4().hex, + 'endpoint_id': self.endpoint_id}, + body='', + expected_status=404) + + def test_remove_endpoint_project_association_with_invalid_endpoint(self): + """DELETE /OS-EP-FILTER/projects/{project_id}/endpoints/{endpoint_id} + + Invalid endpoint id test case. + + """ + self.put(self.default_request_url) + self.delete('/OS-EP-FILTER/projects/%(project_id)s' + '/endpoints/%(endpoint_id)s' % { + 'project_id': self.default_domain_project_id, + 'endpoint_id': uuid.uuid4().hex}, + body='', + expected_status=404) + + def test_endpoint_project_association_cleanup_when_project_deleted(self): + self.put(self.default_request_url) + association_url = ('/OS-EP-FILTER/endpoints/%(endpoint_id)s/projects' % + {'endpoint_id': self.endpoint_id}) + r = self.get(association_url, expected_status=200) + self.assertValidProjectListResponse(r, expected_length=1) + + self.delete('/projects/%(project_id)s' % { + 'project_id': self.default_domain_project_id}) + + r = self.get(association_url, expected_status=200) + self.assertValidProjectListResponse(r, expected_length=0) + + def test_endpoint_project_association_cleanup_when_endpoint_deleted(self): + self.put(self.default_request_url) + association_url = '/OS-EP-FILTER/projects/%(project_id)s/endpoints' % { + 'project_id': self.default_domain_project_id} + r = self.get(association_url, expected_status=200) + self.assertValidEndpointListResponse(r, expected_length=1) + + self.delete('/endpoints/%(endpoint_id)s' % { + 'endpoint_id': self.endpoint_id}) + + r = self.get(association_url, expected_status=200) + self.assertValidEndpointListResponse(r, expected_length=0) + + +class EndpointFilterTokenRequestTestCase(TestExtensionCase): + + def test_project_scoped_token_using_endpoint_filter(self): + """Verify endpoints from project scoped token filtered.""" + # create a project to work with + ref = self.new_project_ref(domain_id=self.domain_id) + r = self.post('/projects', body={'project': ref}) + project = self.assertValidProjectResponse(r, ref) + + # grant the user a role on the project + self.put( + '/projects/%(project_id)s/users/%(user_id)s/roles/%(role_id)s' % { + 'user_id': self.user['id'], + 'project_id': project['id'], + 'role_id': self.role['id']}) + + # set the user's preferred project + body = {'user': {'default_project_id': project['id']}} + r = self.patch('/users/%(user_id)s' % { + 'user_id': self.user['id']}, + body=body) + self.assertValidUserResponse(r) + + # add one endpoint to the project + self.put('/OS-EP-FILTER/projects/%(project_id)s' + '/endpoints/%(endpoint_id)s' % { + 'project_id': project['id'], + 'endpoint_id': self.endpoint_id}, + body='', + expected_status=204) + + # attempt to authenticate without requesting a project + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password']) + r = self.post('/auth/tokens', body=auth_data) + self.assertValidProjectScopedTokenResponse( + r, + require_catalog=True, + endpoint_filter=True, + ep_filter_assoc=1) + self.assertEqual(r.result['token']['project']['id'], project['id']) + + def test_default_scoped_token_using_endpoint_filter(self): + """Verify endpoints from default scoped token filtered.""" + # add one endpoint to default project + self.put('/OS-EP-FILTER/projects/%(project_id)s' + '/endpoints/%(endpoint_id)s' % { + 'project_id': self.project['id'], + 'endpoint_id': self.endpoint_id}, + body='', + expected_status=204) + + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + project_id=self.project['id']) + r = self.post('/auth/tokens', body=auth_data) + self.assertValidProjectScopedTokenResponse( + r, + require_catalog=True, + endpoint_filter=True, + ep_filter_assoc=1) + self.assertEqual(r.result['token']['project']['id'], + self.project['id']) + + def test_project_scoped_token_with_no_catalog_using_endpoint_filter(self): + """Verify endpoint filter when project scoped token returns no catalog. + + Test that the project scoped token response is valid for a given + endpoint-project association when no service catalog is returned. + + """ + # create a project to work with + ref = self.new_project_ref(domain_id=self.domain_id) + r = self.post('/projects', body={'project': ref}) + project = self.assertValidProjectResponse(r, ref) + + # grant the user a role on the project + self.put( + '/projects/%(project_id)s/users/%(user_id)s/roles/%(role_id)s' % { + 'user_id': self.user['id'], + 'project_id': project['id'], + 'role_id': self.role['id']}) + + # set the user's preferred project + body = {'user': {'default_project_id': project['id']}} + r = self.patch('/users/%(user_id)s' % { + 'user_id': self.user['id']}, + body=body) + self.assertValidUserResponse(r) + + # add one endpoint to the project + self.put('/OS-EP-FILTER/projects/%(project_id)s' + '/endpoints/%(endpoint_id)s' % { + 'project_id': project['id'], + 'endpoint_id': self.endpoint_id}, + body='', + expected_status=204) + + # attempt to authenticate without requesting a project + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password']) + r = self.post('/auth/tokens?nocatalog', body=auth_data) + self.assertValidProjectScopedTokenResponse( + r, + require_catalog=False, + endpoint_filter=True, + ep_filter_assoc=1) + self.assertEqual(r.result['token']['project']['id'], project['id']) + + def test_default_scoped_token_with_no_catalog_using_endpoint_filter(self): + """Verify endpoint filter when default scoped token returns no catalog. + + Test that the default project scoped token response is valid for a + given endpoint-project association when no service catalog is returned. + + """ + # add one endpoint to default project + self.put('/OS-EP-FILTER/projects/%(project_id)s' + '/endpoints/%(endpoint_id)s' % { + 'project_id': self.project['id'], + 'endpoint_id': self.endpoint_id}, + body='', + expected_status=204) + + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + project_id=self.project['id']) + r = self.post('/auth/tokens?nocatalog', body=auth_data) + self.assertValidProjectScopedTokenResponse( + r, + require_catalog=False, + endpoint_filter=True, + ep_filter_assoc=1) + self.assertEqual(r.result['token']['project']['id'], + self.project['id']) + + def test_project_scoped_token_with_no_endpoint_project_association(self): + """Verify endpoint filter when no endpoint-project association. + + Test that the project scoped token response is valid when there are + no endpoint-project associations defined. + + """ + # create a project to work with + ref = self.new_project_ref(domain_id=self.domain_id) + r = self.post('/projects', body={'project': ref}) + project = self.assertValidProjectResponse(r, ref) + + # grant the user a role on the project + self.put( + '/projects/%(project_id)s/users/%(user_id)s/roles/%(role_id)s' % { + 'user_id': self.user['id'], + 'project_id': project['id'], + 'role_id': self.role['id']}) + + # set the user's preferred project + body = {'user': {'default_project_id': project['id']}} + r = self.patch('/users/%(user_id)s' % { + 'user_id': self.user['id']}, + body=body) + self.assertValidUserResponse(r) + + # attempt to authenticate without requesting a project + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password']) + r = self.post('/auth/tokens?nocatalog', body=auth_data) + self.assertValidProjectScopedTokenResponse( + r, + require_catalog=False, + endpoint_filter=True) + self.assertEqual(r.result['token']['project']['id'], project['id']) + + def test_default_scoped_token_with_no_endpoint_project_association(self): + """Verify endpoint filter when no endpoint-project association. + + Test that the default project scoped token response is valid when + there are no endpoint-project associations defined. + + """ + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + project_id=self.project['id']) + r = self.post('/auth/tokens?nocatalog', body=auth_data) + self.assertValidProjectScopedTokenResponse( + r, + require_catalog=False, + endpoint_filter=True,) + self.assertEqual(r.result['token']['project']['id'], + self.project['id']) + + def test_invalid_endpoint_project_association(self): + """Verify an invalid endpoint-project association is handled.""" + # add first endpoint to default project + self.put('/OS-EP-FILTER/projects/%(project_id)s' + '/endpoints/%(endpoint_id)s' % { + 'project_id': self.project['id'], + 'endpoint_id': self.endpoint_id}, + body='', + expected_status=204) + + # create a second temporary endpoint + self.endpoint_id2 = uuid.uuid4().hex + self.endpoint2 = self.new_endpoint_ref(service_id=self.service_id) + self.endpoint2['id'] = self.endpoint_id2 + self.catalog_api.create_endpoint( + self.endpoint_id2, + self.endpoint2.copy()) + + # add second endpoint to default project + self.put('/OS-EP-FILTER/projects/%(project_id)s' + '/endpoints/%(endpoint_id)s' % { + 'project_id': self.project['id'], + 'endpoint_id': self.endpoint_id2}, + body='', + expected_status=204) + + # remove the temporary reference + # this will create inconsistency in the endpoint filter table + # which is fixed during the catalog creation for token request + self.catalog_api.delete_endpoint(self.endpoint_id2) + + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + project_id=self.project['id']) + r = self.post('/auth/tokens', body=auth_data) + self.assertValidProjectScopedTokenResponse( + r, + require_catalog=True, + endpoint_filter=True, + ep_filter_assoc=1) + self.assertEqual(r.result['token']['project']['id'], + self.project['id']) + + def test_disabled_endpoint(self): + """Test that a disabled endpoint is handled.""" + # Add an enabled endpoint to the default project + self.put('/OS-EP-FILTER/projects/%(project_id)s' + '/endpoints/%(endpoint_id)s' % { + 'project_id': self.project['id'], + 'endpoint_id': self.endpoint_id}, + expected_status=204) + + # Add a disabled endpoint to the default project. + + # Create a disabled endpoint that's like the enabled one. + disabled_endpoint_ref = copy.copy(self.endpoint) + disabled_endpoint_id = uuid.uuid4().hex + disabled_endpoint_ref.update({ + 'id': disabled_endpoint_id, + 'enabled': False, + 'interface': 'internal' + }) + self.catalog_api.create_endpoint(disabled_endpoint_id, + disabled_endpoint_ref) + + self.put('/OS-EP-FILTER/projects/%(project_id)s' + '/endpoints/%(endpoint_id)s' % { + 'project_id': self.project['id'], + 'endpoint_id': disabled_endpoint_id}, + expected_status=204) + + # Authenticate to get token with catalog + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + project_id=self.project['id']) + r = self.post('/auth/tokens', body=auth_data) + + endpoints = r.result['token']['catalog'][0]['endpoints'] + endpoint_ids = [ep['id'] for ep in endpoints] + self.assertEqual([self.endpoint_id], endpoint_ids) + + def test_multiple_endpoint_project_associations(self): + + def _create_an_endpoint(): + endpoint_ref = self.new_endpoint_ref(service_id=self.service_id) + r = self.post('/endpoints', body={'endpoint': endpoint_ref}) + return r.result['endpoint']['id'] + + # create three endpoints + endpoint_id1 = _create_an_endpoint() + endpoint_id2 = _create_an_endpoint() + _create_an_endpoint() + + # only associate two endpoints with project + self.put('/OS-EP-FILTER/projects/%(project_id)s' + '/endpoints/%(endpoint_id)s' % { + 'project_id': self.project['id'], + 'endpoint_id': endpoint_id1}, + expected_status=204) + self.put('/OS-EP-FILTER/projects/%(project_id)s' + '/endpoints/%(endpoint_id)s' % { + 'project_id': self.project['id'], + 'endpoint_id': endpoint_id2}, + expected_status=204) + + # there should be only two endpoints in token catalog + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + project_id=self.project['id']) + r = self.post('/auth/tokens', body=auth_data) + self.assertValidProjectScopedTokenResponse( + r, + require_catalog=True, + endpoint_filter=True, + ep_filter_assoc=2) + + +class JsonHomeTests(TestExtensionCase, test_v3.JsonHomeTestMixin): + JSON_HOME_DATA = { + 'http://docs.openstack.org/api/openstack-identity/3/ext/OS-EP-FILTER/' + '1.0/rel/endpoint_projects': { + 'href-template': '/OS-EP-FILTER/endpoints/{endpoint_id}/projects', + 'href-vars': { + 'endpoint_id': + 'http://docs.openstack.org/api/openstack-identity/3/param/' + 'endpoint_id', + }, + }, + 'http://docs.openstack.org/api/openstack-identity/3/ext/OS-EP-FILTER/' + '1.0/rel/endpoint_groups': { + 'href': '/OS-EP-FILTER/endpoint_groups', + }, + 'http://docs.openstack.org/api/openstack-identity/3/ext/OS-EP-FILTER/' + '1.0/rel/endpoint_group': { + 'href-template': '/OS-EP-FILTER/endpoint_groups/' + '{endpoint_group_id}', + 'href-vars': { + 'endpoint_group_id': + 'http://docs.openstack.org/api/openstack-identity/3/' + 'ext/OS-EP-FILTER/1.0/param/endpoint_group_id', + }, + }, + 'http://docs.openstack.org/api/openstack-identity/3/ext/OS-EP-FILTER/' + '1.0/rel/endpoint_group_to_project_association': { + 'href-template': '/OS-EP-FILTER/endpoint_groups/' + '{endpoint_group_id}/projects/{project_id}', + 'href-vars': { + 'project_id': + 'http://docs.openstack.org/api/openstack-identity/3/param/' + 'project_id', + 'endpoint_group_id': + 'http://docs.openstack.org/api/openstack-identity/3/' + 'ext/OS-EP-FILTER/1.0/param/endpoint_group_id', + }, + }, + 'http://docs.openstack.org/api/openstack-identity/3/ext/OS-EP-FILTER/' + '1.0/rel/projects_associated_with_endpoint_group': { + 'href-template': '/OS-EP-FILTER/endpoint_groups/' + '{endpoint_group_id}/projects', + 'href-vars': { + 'endpoint_group_id': + 'http://docs.openstack.org/api/openstack-identity/3/' + 'ext/OS-EP-FILTER/1.0/param/endpoint_group_id', + }, + }, + 'http://docs.openstack.org/api/openstack-identity/3/ext/OS-EP-FILTER/' + '1.0/rel/endpoints_in_endpoint_group': { + 'href-template': '/OS-EP-FILTER/endpoint_groups/' + '{endpoint_group_id}/endpoints', + 'href-vars': { + 'endpoint_group_id': + 'http://docs.openstack.org/api/openstack-identity/3/' + 'ext/OS-EP-FILTER/1.0/param/endpoint_group_id', + }, + }, + } + + +class EndpointGroupCRUDTestCase(TestExtensionCase): + + DEFAULT_ENDPOINT_GROUP_BODY = { + 'endpoint_group': { + 'description': 'endpoint group description', + 'filters': { + 'interface': 'admin' + }, + 'name': 'endpoint_group_name' + } + } + + DEFAULT_ENDPOINT_GROUP_URL = '/OS-EP-FILTER/endpoint_groups' + + def test_create_endpoint_group(self): + """POST /OS-EP-FILTER/endpoint_groups + + Valid endpoint group test case. + + """ + r = self.post(self.DEFAULT_ENDPOINT_GROUP_URL, + body=self.DEFAULT_ENDPOINT_GROUP_BODY) + expected_filters = (self.DEFAULT_ENDPOINT_GROUP_BODY + ['endpoint_group']['filters']) + expected_name = (self.DEFAULT_ENDPOINT_GROUP_BODY + ['endpoint_group']['name']) + self.assertEqual(expected_filters, + r.result['endpoint_group']['filters']) + self.assertEqual(expected_name, r.result['endpoint_group']['name']) + self.assertThat( + r.result['endpoint_group']['links']['self'], + matchers.EndsWith( + '/OS-EP-FILTER/endpoint_groups/%(endpoint_group_id)s' % { + 'endpoint_group_id': r.result['endpoint_group']['id']})) + + def test_create_invalid_endpoint_group(self): + """POST /OS-EP-FILTER/endpoint_groups + + Invalid endpoint group creation test case. + + """ + invalid_body = copy.deepcopy(self.DEFAULT_ENDPOINT_GROUP_BODY) + invalid_body['endpoint_group']['filters'] = {'foobar': 'admin'} + self.post(self.DEFAULT_ENDPOINT_GROUP_URL, + body=invalid_body, + expected_status=400) + + def test_get_endpoint_group(self): + """GET /OS-EP-FILTER/endpoint_groups/{endpoint_group} + + Valid endpoint group test case. + + """ + # create an endpoint group to work with + response = self.post(self.DEFAULT_ENDPOINT_GROUP_URL, + body=self.DEFAULT_ENDPOINT_GROUP_BODY) + endpoint_group_id = response.result['endpoint_group']['id'] + endpoint_group_filters = response.result['endpoint_group']['filters'] + endpoint_group_name = response.result['endpoint_group']['name'] + url = '/OS-EP-FILTER/endpoint_groups/%(endpoint_group_id)s' % { + 'endpoint_group_id': endpoint_group_id} + self.get(url) + self.assertEqual(endpoint_group_id, + response.result['endpoint_group']['id']) + self.assertEqual(endpoint_group_filters, + response.result['endpoint_group']['filters']) + self.assertEqual(endpoint_group_name, + response.result['endpoint_group']['name']) + self.assertThat(response.result['endpoint_group']['links']['self'], + matchers.EndsWith(url)) + + def test_get_invalid_endpoint_group(self): + """GET /OS-EP-FILTER/endpoint_groups/{endpoint_group} + + Invalid endpoint group test case. + + """ + endpoint_group_id = 'foobar' + url = '/OS-EP-FILTER/endpoint_groups/%(endpoint_group_id)s' % { + 'endpoint_group_id': endpoint_group_id} + self.get(url, expected_status=404) + + def test_check_endpoint_group(self): + """HEAD /OS-EP-FILTER/endpoint_groups/{endpoint_group_id} + + Valid endpoint_group_id test case. + + """ + # create an endpoint group to work with + endpoint_group_id = self._create_valid_endpoint_group( + self.DEFAULT_ENDPOINT_GROUP_URL, self.DEFAULT_ENDPOINT_GROUP_BODY) + url = '/OS-EP-FILTER/endpoint_groups/%(endpoint_group_id)s' % { + 'endpoint_group_id': endpoint_group_id} + self.head(url, expected_status=200) + + def test_check_invalid_endpoint_group(self): + """HEAD /OS-EP-FILTER/endpoint_groups/{endpoint_group_id} + + Invalid endpoint_group_id test case. + + """ + endpoint_group_id = 'foobar' + url = '/OS-EP-FILTER/endpoint_groups/%(endpoint_group_id)s' % { + 'endpoint_group_id': endpoint_group_id} + self.head(url, expected_status=404) + + def test_patch_endpoint_group(self): + """PATCH /OS-EP-FILTER/endpoint_groups/{endpoint_group} + + Valid endpoint group patch test case. + + """ + body = copy.deepcopy(self.DEFAULT_ENDPOINT_GROUP_BODY) + body['endpoint_group']['filters'] = {'region_id': 'UK'} + body['endpoint_group']['name'] = 'patch_test' + # create an endpoint group to work with + endpoint_group_id = self._create_valid_endpoint_group( + self.DEFAULT_ENDPOINT_GROUP_URL, self.DEFAULT_ENDPOINT_GROUP_BODY) + url = '/OS-EP-FILTER/endpoint_groups/%(endpoint_group_id)s' % { + 'endpoint_group_id': endpoint_group_id} + r = self.patch(url, body=body) + self.assertEqual(endpoint_group_id, + r.result['endpoint_group']['id']) + self.assertEqual(body['endpoint_group']['filters'], + r.result['endpoint_group']['filters']) + self.assertThat(r.result['endpoint_group']['links']['self'], + matchers.EndsWith(url)) + + def test_patch_nonexistent_endpoint_group(self): + """PATCH /OS-EP-FILTER/endpoint_groups/{endpoint_group} + + Invalid endpoint group patch test case. + + """ + body = { + 'endpoint_group': { + 'name': 'patch_test' + } + } + url = '/OS-EP-FILTER/endpoint_groups/%(endpoint_group_id)s' % { + 'endpoint_group_id': 'ABC'} + self.patch(url, body=body, expected_status=404) + + def test_patch_invalid_endpoint_group(self): + """PATCH /OS-EP-FILTER/endpoint_groups/{endpoint_group} + + Valid endpoint group patch test case. + + """ + body = { + 'endpoint_group': { + 'description': 'endpoint group description', + 'filters': { + 'region': 'UK' + }, + 'name': 'patch_test' + } + } + # create an endpoint group to work with + endpoint_group_id = self._create_valid_endpoint_group( + self.DEFAULT_ENDPOINT_GROUP_URL, self.DEFAULT_ENDPOINT_GROUP_BODY) + url = '/OS-EP-FILTER/endpoint_groups/%(endpoint_group_id)s' % { + 'endpoint_group_id': endpoint_group_id} + self.patch(url, body=body, expected_status=400) + + # Perform a GET call to ensure that the content remains + # the same (as DEFAULT_ENDPOINT_GROUP_BODY) after attempting to update + # with an invalid filter + url = '/OS-EP-FILTER/endpoint_groups/%(endpoint_group_id)s' % { + 'endpoint_group_id': endpoint_group_id} + r = self.get(url) + del r.result['endpoint_group']['id'] + del r.result['endpoint_group']['links'] + self.assertDictEqual(self.DEFAULT_ENDPOINT_GROUP_BODY, r.result) + + def test_delete_endpoint_group(self): + """GET /OS-EP-FILTER/endpoint_groups/{endpoint_group} + + Valid endpoint group test case. + + """ + # create an endpoint group to work with + endpoint_group_id = self._create_valid_endpoint_group( + self.DEFAULT_ENDPOINT_GROUP_URL, self.DEFAULT_ENDPOINT_GROUP_BODY) + url = '/OS-EP-FILTER/endpoint_groups/%(endpoint_group_id)s' % { + 'endpoint_group_id': endpoint_group_id} + self.delete(url) + self.get(url, expected_status=404) + + def test_delete_invalid_endpoint_group(self): + """GET /OS-EP-FILTER/endpoint_groups/{endpoint_group} + + Invalid endpoint group test case. + + """ + endpoint_group_id = 'foobar' + url = '/OS-EP-FILTER/endpoint_groups/%(endpoint_group_id)s' % { + 'endpoint_group_id': endpoint_group_id} + self.delete(url, expected_status=404) + + def test_add_endpoint_group_to_project(self): + """Create a valid endpoint group and project association.""" + endpoint_group_id = self._create_valid_endpoint_group( + self.DEFAULT_ENDPOINT_GROUP_URL, self.DEFAULT_ENDPOINT_GROUP_BODY) + self._create_endpoint_group_project_association(endpoint_group_id, + self.project_id) + + def test_add_endpoint_group_to_project_with_invalid_project_id(self): + """Create an invalid endpoint group and project association.""" + # create an endpoint group to work with + endpoint_group_id = self._create_valid_endpoint_group( + self.DEFAULT_ENDPOINT_GROUP_URL, self.DEFAULT_ENDPOINT_GROUP_BODY) + + # associate endpoint group with project + project_id = uuid.uuid4().hex + url = self._get_project_endpoint_group_url( + endpoint_group_id, project_id) + self.put(url, expected_status=404) + + def test_get_endpoint_group_in_project(self): + """Test retrieving project endpoint group association.""" + # create an endpoint group to work with + endpoint_group_id = self._create_valid_endpoint_group( + self.DEFAULT_ENDPOINT_GROUP_URL, self.DEFAULT_ENDPOINT_GROUP_BODY) + + # associate endpoint group with project + url = self._get_project_endpoint_group_url( + endpoint_group_id, self.project_id) + self.put(url) + response = self.get(url) + self.assertEqual( + endpoint_group_id, + response.result['project_endpoint_group']['endpoint_group_id']) + self.assertEqual( + self.project_id, + response.result['project_endpoint_group']['project_id']) + + def test_get_invalid_endpoint_group_in_project(self): + """Test retrieving project endpoint group association.""" + endpoint_group_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + url = self._get_project_endpoint_group_url( + endpoint_group_id, project_id) + self.get(url, expected_status=404) + + def test_check_endpoint_group_to_project(self): + """Test HEAD with a valid endpoint group and project association.""" + endpoint_group_id = self._create_valid_endpoint_group( + self.DEFAULT_ENDPOINT_GROUP_URL, self.DEFAULT_ENDPOINT_GROUP_BODY) + self._create_endpoint_group_project_association(endpoint_group_id, + self.project_id) + url = self._get_project_endpoint_group_url( + endpoint_group_id, self.project_id) + self.head(url, expected_status=200) + + def test_check_endpoint_group_to_project_with_invalid_project_id(self): + """Test HEAD with an invalid endpoint group and project association.""" + # create an endpoint group to work with + endpoint_group_id = self._create_valid_endpoint_group( + self.DEFAULT_ENDPOINT_GROUP_URL, self.DEFAULT_ENDPOINT_GROUP_BODY) + + # create an endpoint group to project association + url = self._get_project_endpoint_group_url( + endpoint_group_id, self.project_id) + self.put(url) + + # send a head request with an invalid project id + project_id = uuid.uuid4().hex + url = self._get_project_endpoint_group_url( + endpoint_group_id, project_id) + self.head(url, expected_status=404) + + def test_list_endpoint_groups(self): + """GET /OS-EP-FILTER/endpoint_groups.""" + # create an endpoint group to work with + endpoint_group_id = self._create_valid_endpoint_group( + self.DEFAULT_ENDPOINT_GROUP_URL, self.DEFAULT_ENDPOINT_GROUP_BODY) + + # recover all endpoint groups + url = '/OS-EP-FILTER/endpoint_groups' + r = self.get(url) + self.assertNotEmpty(r.result['endpoint_groups']) + self.assertEqual(endpoint_group_id, + r.result['endpoint_groups'][0].get('id')) + + def test_list_projects_associated_with_endpoint_group(self): + """GET /OS-EP-FILTER/endpoint_groups/{endpoint_group}/projects + + Valid endpoint group test case. + + """ + # create an endpoint group to work with + endpoint_group_id = self._create_valid_endpoint_group( + self.DEFAULT_ENDPOINT_GROUP_URL, self.DEFAULT_ENDPOINT_GROUP_BODY) + + # associate endpoint group with project + self._create_endpoint_group_project_association(endpoint_group_id, + self.project_id) + + # recover list of projects associated with endpoint group + url = ('/OS-EP-FILTER/endpoint_groups/%(endpoint_group_id)s' + '/projects' % + {'endpoint_group_id': endpoint_group_id}) + self.get(url) + + def test_list_endpoints_associated_with_endpoint_group(self): + """GET /OS-EP-FILTER/endpoint_groups/{endpoint_group}/endpoints + + Valid endpoint group test case. + + """ + # create a service + service_ref = self.new_service_ref() + response = self.post( + '/services', + body={'service': service_ref}) + + service_id = response.result['service']['id'] + + # create an endpoint + endpoint_ref = self.new_endpoint_ref(service_id=service_id) + response = self.post( + '/endpoints', + body={'endpoint': endpoint_ref}) + endpoint_id = response.result['endpoint']['id'] + + # create an endpoint group + body = copy.deepcopy(self.DEFAULT_ENDPOINT_GROUP_BODY) + body['endpoint_group']['filters'] = {'service_id': service_id} + endpoint_group_id = self._create_valid_endpoint_group( + self.DEFAULT_ENDPOINT_GROUP_URL, body) + + # create association + self._create_endpoint_group_project_association(endpoint_group_id, + self.project_id) + + # recover list of endpoints associated with endpoint group + url = ('/OS-EP-FILTER/endpoint_groups/%(endpoint_group_id)s' + '/endpoints' % {'endpoint_group_id': endpoint_group_id}) + r = self.get(url) + self.assertNotEmpty(r.result['endpoints']) + self.assertEqual(endpoint_id, r.result['endpoints'][0].get('id')) + + def test_list_endpoints_associated_with_project_endpoint_group(self): + """GET /OS-EP-FILTER/projects/{project_id}/endpoints + + Valid project, endpoint id, and endpoint group test case. + + """ + # create a temporary service + service_ref = self.new_service_ref() + response = self.post('/services', body={'service': service_ref}) + service_id2 = response.result['service']['id'] + + # create additional endpoints + self._create_endpoint_and_associations( + self.default_domain_project_id, service_id2) + self._create_endpoint_and_associations( + self.default_domain_project_id) + + # create project and endpoint association with default endpoint: + self.put(self.default_request_url) + + # create an endpoint group that contains a different endpoint + body = copy.deepcopy(self.DEFAULT_ENDPOINT_GROUP_BODY) + body['endpoint_group']['filters'] = {'service_id': service_id2} + endpoint_group_id = self._create_valid_endpoint_group( + self.DEFAULT_ENDPOINT_GROUP_URL, body) + + # associate endpoint group with project + self._create_endpoint_group_project_association( + endpoint_group_id, self.default_domain_project_id) + + # Now get a list of the filtered endpoints + endpoints_url = '/OS-EP-FILTER/projects/%(project_id)s/endpoints' % { + 'project_id': self.default_domain_project_id} + r = self.get(endpoints_url) + endpoints = self.assertValidEndpointListResponse(r) + self.assertEqual(len(endpoints), 2) + + # Now remove project endpoint group association + url = self._get_project_endpoint_group_url( + endpoint_group_id, self.default_domain_project_id) + self.delete(url) + + # Now remove endpoint group + url = '/OS-EP-FILTER/endpoint_groups/%(endpoint_group_id)s' % { + 'endpoint_group_id': endpoint_group_id} + self.delete(url) + + r = self.get(endpoints_url) + endpoints = self.assertValidEndpointListResponse(r) + self.assertEqual(len(endpoints), 1) + + def test_endpoint_group_project_cleanup_with_project(self): + # create endpoint group + endpoint_group_id = self._create_valid_endpoint_group( + self.DEFAULT_ENDPOINT_GROUP_URL, self.DEFAULT_ENDPOINT_GROUP_BODY) + + # create new project and associate with endpoint_group + project_ref = self.new_project_ref(domain_id=self.domain_id) + r = self.post('/projects', body={'project': project_ref}) + project = self.assertValidProjectResponse(r, project_ref) + url = self._get_project_endpoint_group_url(endpoint_group_id, + project['id']) + self.put(url) + + # check that we can recover the project endpoint group association + self.get(url) + + # Now delete the project and then try and retrieve the project + # endpoint group association again + self.delete('/projects/%(project_id)s' % { + 'project_id': project['id']}) + self.get(url, expected_status=404) + + def test_endpoint_group_project_cleanup_with_endpoint_group(self): + # create endpoint group + endpoint_group_id = self._create_valid_endpoint_group( + self.DEFAULT_ENDPOINT_GROUP_URL, self.DEFAULT_ENDPOINT_GROUP_BODY) + + # create new project and associate with endpoint_group + project_ref = self.new_project_ref(domain_id=self.domain_id) + r = self.post('/projects', body={'project': project_ref}) + project = self.assertValidProjectResponse(r, project_ref) + url = self._get_project_endpoint_group_url(endpoint_group_id, + project['id']) + self.put(url) + + # check that we can recover the project endpoint group association + self.get(url) + + # now remove the project endpoint group association + self.delete(url) + self.get(url, expected_status=404) + + def test_removing_an_endpoint_group_project(self): + # create an endpoint group + endpoint_group_id = self._create_valid_endpoint_group( + self.DEFAULT_ENDPOINT_GROUP_URL, self.DEFAULT_ENDPOINT_GROUP_BODY) + + # create an endpoint_group project + url = self._get_project_endpoint_group_url( + endpoint_group_id, self.default_domain_project_id) + self.put(url) + + # remove the endpoint group project + self.delete(url) + self.get(url, expected_status=404) + + def _create_valid_endpoint_group(self, url, body): + r = self.post(url, body=body) + return r.result['endpoint_group']['id'] + + def _create_endpoint_group_project_association(self, + endpoint_group_id, + project_id): + url = self._get_project_endpoint_group_url(endpoint_group_id, + project_id) + self.put(url) + + def _get_project_endpoint_group_url(self, + endpoint_group_id, + project_id): + return ('/OS-EP-FILTER/endpoint_groups/%(endpoint_group_id)s' + '/projects/%(project_id)s' % + {'endpoint_group_id': endpoint_group_id, + 'project_id': project_id}) + + def _create_endpoint_and_associations(self, project_id, service_id=None): + """Creates an endpoint associated with service and project.""" + if not service_id: + # create a new service + service_ref = self.new_service_ref() + response = self.post( + '/services', body={'service': service_ref}) + service_id = response.result['service']['id'] + + # create endpoint + endpoint_ref = self.new_endpoint_ref(service_id=service_id) + response = self.post('/endpoints', body={'endpoint': endpoint_ref}) + endpoint = response.result['endpoint'] + + # now add endpoint to project + self.put('/OS-EP-FILTER/projects/%(project_id)s' + '/endpoints/%(endpoint_id)s' % { + 'project_id': self.project['id'], + 'endpoint_id': endpoint['id']}) + return endpoint diff --git a/keystone-moon/keystone/tests/unit/test_auth.py b/keystone-moon/keystone/tests/unit/test_auth.py new file mode 100644 index 00000000..295e028d --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_auth.py @@ -0,0 +1,1328 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 +import datetime +import uuid + +import mock +from oslo_config import cfg +from oslo_utils import timeutils +from testtools import matchers + +from keystone import assignment +from keystone import auth +from keystone.common import authorization +from keystone import config +from keystone import exception +from keystone.models import token_model +from keystone.tests import unit as tests +from keystone.tests.unit import default_fixtures +from keystone.tests.unit.ksfixtures import database +from keystone import token +from keystone.token import provider +from keystone import trust + + +CONF = cfg.CONF +TIME_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ' +DEFAULT_DOMAIN_ID = CONF.identity.default_domain_id + +HOST_URL = 'http://keystone:5001' + + +def _build_user_auth(token=None, user_id=None, username=None, + password=None, tenant_id=None, tenant_name=None, + trust_id=None): + """Build auth dictionary. + + It will create an auth dictionary based on all the arguments + that it receives. + """ + auth_json = {} + if token is not None: + auth_json['token'] = token + if username or password: + auth_json['passwordCredentials'] = {} + if username is not None: + auth_json['passwordCredentials']['username'] = username + if user_id is not None: + auth_json['passwordCredentials']['userId'] = user_id + if password is not None: + auth_json['passwordCredentials']['password'] = password + if tenant_name is not None: + auth_json['tenantName'] = tenant_name + if tenant_id is not None: + auth_json['tenantId'] = tenant_id + if trust_id is not None: + auth_json['trust_id'] = trust_id + return auth_json + + +class AuthTest(tests.TestCase): + def setUp(self): + self.useFixture(database.Database()) + super(AuthTest, self).setUp() + + self.load_backends() + self.load_fixtures(default_fixtures) + + self.context_with_remote_user = {'environment': + {'REMOTE_USER': 'FOO', + 'AUTH_TYPE': 'Negotiate'}} + self.empty_context = {'environment': {}} + + self.controller = token.controllers.Auth() + + def assertEqualTokens(self, a, b, enforce_audit_ids=True): + """Assert that two tokens are equal. + + Compare two tokens except for their ids. This also truncates + the time in the comparison. + """ + def normalize(token): + token['access']['token']['id'] = 'dummy' + del token['access']['token']['expires'] + del token['access']['token']['issued_at'] + del token['access']['token']['audit_ids'] + return token + + self.assertCloseEnoughForGovernmentWork( + timeutils.parse_isotime(a['access']['token']['expires']), + timeutils.parse_isotime(b['access']['token']['expires'])) + self.assertCloseEnoughForGovernmentWork( + timeutils.parse_isotime(a['access']['token']['issued_at']), + timeutils.parse_isotime(b['access']['token']['issued_at'])) + if enforce_audit_ids: + self.assertIn(a['access']['token']['audit_ids'][0], + b['access']['token']['audit_ids']) + self.assertThat(len(a['access']['token']['audit_ids']), + matchers.LessThan(3)) + self.assertThat(len(b['access']['token']['audit_ids']), + matchers.LessThan(3)) + + return self.assertDictEqual(normalize(a), normalize(b)) + + +class AuthBadRequests(AuthTest): + def test_no_external_auth(self): + """Verify that _authenticate_external() raises exception if N/A.""" + self.assertRaises( + token.controllers.ExternalAuthNotApplicable, + self.controller._authenticate_external, + context={}, auth={}) + + def test_empty_remote_user(self): + """Verify that _authenticate_external() raises exception if + REMOTE_USER is set as the empty string. + """ + context = {'environment': {'REMOTE_USER': ''}} + self.assertRaises( + token.controllers.ExternalAuthNotApplicable, + self.controller._authenticate_external, + context=context, auth={}) + + def test_no_token_in_auth(self): + """Verify that _authenticate_token() raises exception if no token.""" + self.assertRaises( + exception.ValidationError, + self.controller._authenticate_token, + None, {}) + + def test_no_credentials_in_auth(self): + """Verify that _authenticate_local() raises exception if no creds.""" + self.assertRaises( + exception.ValidationError, + self.controller._authenticate_local, + None, {}) + + def test_empty_username_and_userid_in_auth(self): + """Verify that empty username and userID raises ValidationError.""" + self.assertRaises( + exception.ValidationError, + self.controller._authenticate_local, + None, {'passwordCredentials': {'password': 'abc', + 'userId': '', 'username': ''}}) + + def test_authenticate_blank_request_body(self): + """Verify sending empty json dict raises the right exception.""" + self.assertRaises(exception.ValidationError, + self.controller.authenticate, + {}, {}) + + def test_authenticate_blank_auth(self): + """Verify sending blank 'auth' raises the right exception.""" + body_dict = _build_user_auth() + self.assertRaises(exception.ValidationError, + self.controller.authenticate, + {}, body_dict) + + def test_authenticate_invalid_auth_content(self): + """Verify sending invalid 'auth' raises the right exception.""" + self.assertRaises(exception.ValidationError, + self.controller.authenticate, + {}, {'auth': 'abcd'}) + + def test_authenticate_user_id_too_large(self): + """Verify sending large 'userId' raises the right exception.""" + body_dict = _build_user_auth(user_id='0' * 65, username='FOO', + password='foo2') + self.assertRaises(exception.ValidationSizeError, + self.controller.authenticate, + {}, body_dict) + + def test_authenticate_username_too_large(self): + """Verify sending large 'username' raises the right exception.""" + body_dict = _build_user_auth(username='0' * 65, password='foo2') + self.assertRaises(exception.ValidationSizeError, + self.controller.authenticate, + {}, body_dict) + + def test_authenticate_tenant_id_too_large(self): + """Verify sending large 'tenantId' raises the right exception.""" + body_dict = _build_user_auth(username='FOO', password='foo2', + tenant_id='0' * 65) + self.assertRaises(exception.ValidationSizeError, + self.controller.authenticate, + {}, body_dict) + + def test_authenticate_tenant_name_too_large(self): + """Verify sending large 'tenantName' raises the right exception.""" + body_dict = _build_user_auth(username='FOO', password='foo2', + tenant_name='0' * 65) + self.assertRaises(exception.ValidationSizeError, + self.controller.authenticate, + {}, body_dict) + + def test_authenticate_token_too_large(self): + """Verify sending large 'token' raises the right exception.""" + body_dict = _build_user_auth(token={'id': '0' * 8193}) + self.assertRaises(exception.ValidationSizeError, + self.controller.authenticate, + {}, body_dict) + + def test_authenticate_password_too_large(self): + """Verify sending large 'password' raises the right exception.""" + length = CONF.identity.max_password_length + 1 + body_dict = _build_user_auth(username='FOO', password='0' * length) + self.assertRaises(exception.ValidationSizeError, + self.controller.authenticate, + {}, body_dict) + + +class AuthWithToken(AuthTest): + def test_unscoped_token(self): + """Verify getting an unscoped token with password creds.""" + body_dict = _build_user_auth(username='FOO', + password='foo2') + unscoped_token = self.controller.authenticate({}, body_dict) + self.assertNotIn('tenant', unscoped_token['access']['token']) + + def test_auth_invalid_token(self): + """Verify exception is raised if invalid token.""" + body_dict = _build_user_auth(token={"id": uuid.uuid4().hex}) + self.assertRaises( + exception.Unauthorized, + self.controller.authenticate, + {}, body_dict) + + def test_auth_bad_formatted_token(self): + """Verify exception is raised if invalid token.""" + body_dict = _build_user_auth(token={}) + self.assertRaises( + exception.ValidationError, + self.controller.authenticate, + {}, body_dict) + + def test_auth_unscoped_token_no_project(self): + """Verify getting an unscoped token with an unscoped token.""" + body_dict = _build_user_auth( + username='FOO', + password='foo2') + unscoped_token = self.controller.authenticate({}, body_dict) + + body_dict = _build_user_auth( + token=unscoped_token["access"]["token"]) + unscoped_token_2 = self.controller.authenticate({}, body_dict) + + self.assertEqualTokens(unscoped_token, unscoped_token_2) + + def test_auth_unscoped_token_project(self): + """Verify getting a token in a tenant with an unscoped token.""" + # Add a role in so we can check we get this back + self.assignment_api.add_role_to_user_and_project( + self.user_foo['id'], + self.tenant_bar['id'], + self.role_member['id']) + # Get an unscoped tenant + body_dict = _build_user_auth( + username='FOO', + password='foo2') + unscoped_token = self.controller.authenticate({}, body_dict) + # Get a token on BAR tenant using the unscoped tenant + body_dict = _build_user_auth( + token=unscoped_token["access"]["token"], + tenant_name="BAR") + scoped_token = self.controller.authenticate({}, body_dict) + + tenant = scoped_token["access"]["token"]["tenant"] + roles = scoped_token["access"]["metadata"]["roles"] + self.assertEqual(self.tenant_bar['id'], tenant["id"]) + self.assertThat(roles, matchers.Contains(self.role_member['id'])) + + def test_auth_token_project_group_role(self): + """Verify getting a token in a tenant with group roles.""" + # Add a v2 style role in so we can check we get this back + self.assignment_api.add_role_to_user_and_project( + self.user_foo['id'], + self.tenant_bar['id'], + self.role_member['id']) + # Now create a group role for this user as well + domain1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(domain1['id'], domain1) + new_group = {'domain_id': domain1['id'], 'name': uuid.uuid4().hex} + new_group = self.identity_api.create_group(new_group) + self.identity_api.add_user_to_group(self.user_foo['id'], + new_group['id']) + self.assignment_api.create_grant( + group_id=new_group['id'], + project_id=self.tenant_bar['id'], + role_id=self.role_admin['id']) + + # Get a scoped token for the tenant + body_dict = _build_user_auth( + username='FOO', + password='foo2', + tenant_name="BAR") + + scoped_token = self.controller.authenticate({}, body_dict) + + tenant = scoped_token["access"]["token"]["tenant"] + roles = scoped_token["access"]["metadata"]["roles"] + self.assertEqual(self.tenant_bar['id'], tenant["id"]) + self.assertIn(self.role_member['id'], roles) + self.assertIn(self.role_admin['id'], roles) + + def test_belongs_to_no_tenant(self): + r = self.controller.authenticate( + {}, + auth={ + 'passwordCredentials': { + 'username': self.user_foo['name'], + 'password': self.user_foo['password'] + } + }) + unscoped_token_id = r['access']['token']['id'] + self.assertRaises( + exception.Unauthorized, + self.controller.validate_token, + dict(is_admin=True, query_string={'belongsTo': 'BAR'}), + token_id=unscoped_token_id) + + def test_belongs_to(self): + body_dict = _build_user_auth( + username='FOO', + password='foo2', + tenant_name="BAR") + + scoped_token = self.controller.authenticate({}, body_dict) + scoped_token_id = scoped_token['access']['token']['id'] + + self.assertRaises( + exception.Unauthorized, + self.controller.validate_token, + dict(is_admin=True, query_string={'belongsTo': 'me'}), + token_id=scoped_token_id) + + self.assertRaises( + exception.Unauthorized, + self.controller.validate_token, + dict(is_admin=True, query_string={'belongsTo': 'BAR'}), + token_id=scoped_token_id) + + def test_token_auth_with_binding(self): + self.config_fixture.config(group='token', bind=['kerberos']) + body_dict = _build_user_auth() + unscoped_token = self.controller.authenticate( + self.context_with_remote_user, body_dict) + + # the token should have bind information in it + bind = unscoped_token['access']['token']['bind'] + self.assertEqual('FOO', bind['kerberos']) + + body_dict = _build_user_auth( + token=unscoped_token['access']['token'], + tenant_name='BAR') + + # using unscoped token without remote user context fails + self.assertRaises( + exception.Unauthorized, + self.controller.authenticate, + self.empty_context, body_dict) + + # using token with remote user context succeeds + scoped_token = self.controller.authenticate( + self.context_with_remote_user, body_dict) + + # the bind information should be carried over from the original token + bind = scoped_token['access']['token']['bind'] + self.assertEqual('FOO', bind['kerberos']) + + def test_deleting_role_revokes_token(self): + role_controller = assignment.controllers.Role() + project1 = {'id': 'Project1', 'name': uuid.uuid4().hex, + 'domain_id': DEFAULT_DOMAIN_ID} + self.resource_api.create_project(project1['id'], project1) + role_one = {'id': 'role_one', 'name': uuid.uuid4().hex} + self.role_api.create_role(role_one['id'], role_one) + self.assignment_api.add_role_to_user_and_project( + self.user_foo['id'], project1['id'], role_one['id']) + no_context = {} + + # Get a scoped token for the tenant + body_dict = _build_user_auth( + username=self.user_foo['name'], + password=self.user_foo['password'], + tenant_name=project1['name']) + token = self.controller.authenticate(no_context, body_dict) + # Ensure it is valid + token_id = token['access']['token']['id'] + self.controller.validate_token( + dict(is_admin=True, query_string={}), + token_id=token_id) + + # Delete the role, which should invalidate the token + role_controller.delete_role( + dict(is_admin=True, query_string={}), role_one['id']) + + # Check the token is now invalid + self.assertRaises( + exception.TokenNotFound, + self.controller.validate_token, + dict(is_admin=True, query_string={}), + token_id=token_id) + + def test_only_original_audit_id_is_kept(self): + context = {} + + def get_audit_ids(token): + return token['access']['token']['audit_ids'] + + # get a token + body_dict = _build_user_auth(username='FOO', password='foo2') + unscoped_token = self.controller.authenticate(context, body_dict) + starting_audit_id = get_audit_ids(unscoped_token)[0] + self.assertIsNotNone(starting_audit_id) + + # get another token to ensure the correct parent audit_id is set + body_dict = _build_user_auth(token=unscoped_token["access"]["token"]) + unscoped_token_2 = self.controller.authenticate(context, body_dict) + audit_ids = get_audit_ids(unscoped_token_2) + self.assertThat(audit_ids, matchers.HasLength(2)) + self.assertThat(audit_ids[-1], matchers.Equals(starting_audit_id)) + + # get another token from token 2 and ensure the correct parent + # audit_id is set + body_dict = _build_user_auth(token=unscoped_token_2["access"]["token"]) + unscoped_token_3 = self.controller.authenticate(context, body_dict) + audit_ids = get_audit_ids(unscoped_token_3) + self.assertThat(audit_ids, matchers.HasLength(2)) + self.assertThat(audit_ids[-1], matchers.Equals(starting_audit_id)) + + def test_revoke_by_audit_chain_id_original_token(self): + self.config_fixture.config(group='token', revoke_by_id=False) + context = {} + + # get a token + body_dict = _build_user_auth(username='FOO', password='foo2') + unscoped_token = self.controller.authenticate(context, body_dict) + token_id = unscoped_token['access']['token']['id'] + # get a second token + body_dict = _build_user_auth(token=unscoped_token["access"]["token"]) + unscoped_token_2 = self.controller.authenticate(context, body_dict) + token_2_id = unscoped_token_2['access']['token']['id'] + + self.token_provider_api.revoke_token(token_id, revoke_chain=True) + + self.assertRaises(exception.TokenNotFound, + self.token_provider_api.validate_v2_token, + token_id=token_id) + self.assertRaises(exception.TokenNotFound, + self.token_provider_api.validate_v2_token, + token_id=token_2_id) + + def test_revoke_by_audit_chain_id_chained_token(self): + self.config_fixture.config(group='token', revoke_by_id=False) + context = {} + + # get a token + body_dict = _build_user_auth(username='FOO', password='foo2') + unscoped_token = self.controller.authenticate(context, body_dict) + token_id = unscoped_token['access']['token']['id'] + # get a second token + body_dict = _build_user_auth(token=unscoped_token["access"]["token"]) + unscoped_token_2 = self.controller.authenticate(context, body_dict) + token_2_id = unscoped_token_2['access']['token']['id'] + + self.token_provider_api.revoke_token(token_2_id, revoke_chain=True) + + self.assertRaises(exception.TokenNotFound, + self.token_provider_api.validate_v2_token, + token_id=token_id) + self.assertRaises(exception.TokenNotFound, + self.token_provider_api.validate_v2_token, + token_id=token_2_id) + + def _mock_audit_info(self, parent_audit_id): + # NOTE(morgainfainberg): The token model and other cases that are + # extracting the audit id expect 'None' if the audit id doesn't + # exist. This ensures that the audit_id is None and the + # audit_chain_id will also return None. + return [None, None] + + def test_revoke_with_no_audit_info(self): + self.config_fixture.config(group='token', revoke_by_id=False) + context = {} + + with mock.patch.object(provider, 'audit_info', self._mock_audit_info): + # get a token + body_dict = _build_user_auth(username='FOO', password='foo2') + unscoped_token = self.controller.authenticate(context, body_dict) + token_id = unscoped_token['access']['token']['id'] + # get a second token + body_dict = _build_user_auth( + token=unscoped_token['access']['token']) + unscoped_token_2 = self.controller.authenticate(context, body_dict) + token_2_id = unscoped_token_2['access']['token']['id'] + + self.token_provider_api.revoke_token(token_id, revoke_chain=True) + + revoke_events = self.revoke_api.list_events() + self.assertThat(revoke_events, matchers.HasLength(1)) + revoke_event = revoke_events[0].to_dict() + self.assertIn('expires_at', revoke_event) + self.assertEqual(unscoped_token_2['access']['token']['expires'], + revoke_event['expires_at']) + + self.assertRaises(exception.TokenNotFound, + self.token_provider_api.validate_v2_token, + token_id=token_id) + self.assertRaises(exception.TokenNotFound, + self.token_provider_api.validate_v2_token, + token_id=token_2_id) + + # get a new token, with no audit info + body_dict = _build_user_auth(username='FOO', password='foo2') + unscoped_token = self.controller.authenticate(context, body_dict) + token_id = unscoped_token['access']['token']['id'] + # get a second token + body_dict = _build_user_auth( + token=unscoped_token['access']['token']) + unscoped_token_2 = self.controller.authenticate(context, body_dict) + token_2_id = unscoped_token_2['access']['token']['id'] + + # Revoke by audit_id, no audit_info means both parent and child + # token are revoked. + self.token_provider_api.revoke_token(token_id) + + revoke_events = self.revoke_api.list_events() + self.assertThat(revoke_events, matchers.HasLength(2)) + revoke_event = revoke_events[1].to_dict() + self.assertIn('expires_at', revoke_event) + self.assertEqual(unscoped_token_2['access']['token']['expires'], + revoke_event['expires_at']) + + self.assertRaises(exception.TokenNotFound, + self.token_provider_api.validate_v2_token, + token_id=token_id) + self.assertRaises(exception.TokenNotFound, + self.token_provider_api.validate_v2_token, + token_id=token_2_id) + + +class AuthWithPasswordCredentials(AuthTest): + def test_auth_invalid_user(self): + """Verify exception is raised if invalid user.""" + body_dict = _build_user_auth( + username=uuid.uuid4().hex, + password=uuid.uuid4().hex) + self.assertRaises( + exception.Unauthorized, + self.controller.authenticate, + {}, body_dict) + + def test_auth_valid_user_invalid_password(self): + """Verify exception is raised if invalid password.""" + body_dict = _build_user_auth( + username="FOO", + password=uuid.uuid4().hex) + self.assertRaises( + exception.Unauthorized, + self.controller.authenticate, + {}, body_dict) + + def test_auth_empty_password(self): + """Verify exception is raised if empty password.""" + body_dict = _build_user_auth( + username="FOO", + password="") + self.assertRaises( + exception.Unauthorized, + self.controller.authenticate, + {}, body_dict) + + def test_auth_no_password(self): + """Verify exception is raised if empty password.""" + body_dict = _build_user_auth(username="FOO") + self.assertRaises( + exception.ValidationError, + self.controller.authenticate, + {}, body_dict) + + def test_authenticate_blank_password_credentials(self): + """Sending empty dict as passwordCredentials raises a 400 error.""" + body_dict = {'passwordCredentials': {}, 'tenantName': 'demo'} + self.assertRaises(exception.ValidationError, + self.controller.authenticate, + {}, body_dict) + + def test_authenticate_no_username(self): + """Verify skipping username raises the right exception.""" + body_dict = _build_user_auth(password="pass", + tenant_name="demo") + self.assertRaises(exception.ValidationError, + self.controller.authenticate, + {}, body_dict) + + def test_bind_without_remote_user(self): + self.config_fixture.config(group='token', bind=['kerberos']) + body_dict = _build_user_auth(username='FOO', password='foo2', + tenant_name='BAR') + token = self.controller.authenticate({}, body_dict) + self.assertNotIn('bind', token['access']['token']) + + def test_change_default_domain_id(self): + # If the default_domain_id config option is not the default then the + # user in auth data is from the new default domain. + + # 1) Create a new domain. + new_domain_id = uuid.uuid4().hex + new_domain = { + 'description': uuid.uuid4().hex, + 'enabled': True, + 'id': new_domain_id, + 'name': uuid.uuid4().hex, + } + + self.resource_api.create_domain(new_domain_id, new_domain) + + # 2) Create user "foo" in new domain with different password than + # default-domain foo. + new_user_password = uuid.uuid4().hex + new_user = { + 'name': self.user_foo['name'], + 'domain_id': new_domain_id, + 'password': new_user_password, + 'email': 'foo@bar2.com', + } + + new_user = self.identity_api.create_user(new_user) + + # 3) Update the default_domain_id config option to the new domain + + self.config_fixture.config(group='identity', + default_domain_id=new_domain_id) + + # 4) Authenticate as "foo" using the password in the new domain. + + body_dict = _build_user_auth( + username=self.user_foo['name'], + password=new_user_password) + + # The test is successful if this doesn't raise, so no need to assert. + self.controller.authenticate({}, body_dict) + + +class AuthWithRemoteUser(AuthTest): + def test_unscoped_remote_authn(self): + """Verify getting an unscoped token with external authn.""" + body_dict = _build_user_auth( + username='FOO', + password='foo2') + local_token = self.controller.authenticate( + {}, body_dict) + + body_dict = _build_user_auth() + remote_token = self.controller.authenticate( + self.context_with_remote_user, body_dict) + + self.assertEqualTokens(local_token, remote_token, + enforce_audit_ids=False) + + def test_unscoped_remote_authn_jsonless(self): + """Verify that external auth with invalid request fails.""" + self.assertRaises( + exception.ValidationError, + self.controller.authenticate, + {'REMOTE_USER': 'FOO'}, + None) + + def test_scoped_remote_authn(self): + """Verify getting a token with external authn.""" + body_dict = _build_user_auth( + username='FOO', + password='foo2', + tenant_name='BAR') + local_token = self.controller.authenticate( + {}, body_dict) + + body_dict = _build_user_auth( + tenant_name='BAR') + remote_token = self.controller.authenticate( + self.context_with_remote_user, body_dict) + + self.assertEqualTokens(local_token, remote_token, + enforce_audit_ids=False) + + def test_scoped_nometa_remote_authn(self): + """Verify getting a token with external authn and no metadata.""" + body_dict = _build_user_auth( + username='TWO', + password='two2', + tenant_name='BAZ') + local_token = self.controller.authenticate( + {}, body_dict) + + body_dict = _build_user_auth(tenant_name='BAZ') + remote_token = self.controller.authenticate( + {'environment': {'REMOTE_USER': 'TWO'}}, body_dict) + + self.assertEqualTokens(local_token, remote_token, + enforce_audit_ids=False) + + def test_scoped_remote_authn_invalid_user(self): + """Verify that external auth with invalid user fails.""" + body_dict = _build_user_auth(tenant_name="BAR") + self.assertRaises( + exception.Unauthorized, + self.controller.authenticate, + {'environment': {'REMOTE_USER': uuid.uuid4().hex}}, + body_dict) + + def test_bind_with_kerberos(self): + self.config_fixture.config(group='token', bind=['kerberos']) + body_dict = _build_user_auth(tenant_name="BAR") + token = self.controller.authenticate(self.context_with_remote_user, + body_dict) + self.assertEqual('FOO', token['access']['token']['bind']['kerberos']) + + def test_bind_without_config_opt(self): + self.config_fixture.config(group='token', bind=['x509']) + body_dict = _build_user_auth(tenant_name='BAR') + token = self.controller.authenticate(self.context_with_remote_user, + body_dict) + self.assertNotIn('bind', token['access']['token']) + + +class AuthWithTrust(AuthTest): + def setUp(self): + super(AuthWithTrust, self).setUp() + + self.trust_controller = trust.controllers.TrustV3() + self.auth_v3_controller = auth.controllers.Auth() + self.trustor = self.user_foo + self.trustee = self.user_two + self.assigned_roles = [self.role_member['id'], + self.role_browser['id']] + for assigned_role in self.assigned_roles: + self.assignment_api.add_role_to_user_and_project( + self.trustor['id'], self.tenant_bar['id'], assigned_role) + + self.sample_data = {'trustor_user_id': self.trustor['id'], + 'trustee_user_id': self.trustee['id'], + 'project_id': self.tenant_bar['id'], + 'impersonation': True, + 'roles': [{'id': self.role_browser['id']}, + {'name': self.role_member['name']}]} + + def config_overrides(self): + super(AuthWithTrust, self).config_overrides() + self.config_fixture.config(group='trust', enabled=True) + + def _create_auth_context(self, token_id): + token_ref = token_model.KeystoneToken( + token_id=token_id, + token_data=self.token_provider_api.validate_token(token_id)) + auth_context = authorization.token_to_auth_context(token_ref) + return {'environment': {authorization.AUTH_CONTEXT_ENV: auth_context}, + 'token_id': token_id, + 'host_url': HOST_URL} + + def create_trust(self, trust_data, trustor_name, expires_at=None, + impersonation=True): + username = trustor_name + password = 'foo2' + unscoped_token = self.get_unscoped_token(username, password) + context = self._create_auth_context( + unscoped_token['access']['token']['id']) + trust_data_copy = copy.deepcopy(trust_data) + trust_data_copy['expires_at'] = expires_at + trust_data_copy['impersonation'] = impersonation + + return self.trust_controller.create_trust( + context, trust=trust_data_copy)['trust'] + + def get_unscoped_token(self, username, password='foo2'): + body_dict = _build_user_auth(username=username, password=password) + return self.controller.authenticate({}, body_dict) + + def build_v2_token_request(self, username, password, trust, + tenant_id=None): + if not tenant_id: + tenant_id = self.tenant_bar['id'] + unscoped_token = self.get_unscoped_token(username, password) + unscoped_token_id = unscoped_token['access']['token']['id'] + request_body = _build_user_auth(token={'id': unscoped_token_id}, + trust_id=trust['id'], + tenant_id=tenant_id) + return request_body + + def test_create_trust_bad_data_fails(self): + unscoped_token = self.get_unscoped_token(self.trustor['name']) + context = self._create_auth_context( + unscoped_token['access']['token']['id']) + bad_sample_data = {'trustor_user_id': self.trustor['id'], + 'project_id': self.tenant_bar['id'], + 'roles': [{'id': self.role_browser['id']}]} + + self.assertRaises(exception.ValidationError, + self.trust_controller.create_trust, + context, trust=bad_sample_data) + + def test_create_trust_no_roles(self): + unscoped_token = self.get_unscoped_token(self.trustor['name']) + context = {'token_id': unscoped_token['access']['token']['id']} + self.sample_data['roles'] = [] + self.assertRaises(exception.Forbidden, + self.trust_controller.create_trust, + context, trust=self.sample_data) + + def test_create_trust(self): + expires_at = timeutils.strtime(timeutils.utcnow() + + datetime.timedelta(minutes=10), + fmt=TIME_FORMAT) + new_trust = self.create_trust(self.sample_data, self.trustor['name'], + expires_at=expires_at) + self.assertEqual(self.trustor['id'], new_trust['trustor_user_id']) + self.assertEqual(self.trustee['id'], new_trust['trustee_user_id']) + role_ids = [self.role_browser['id'], self.role_member['id']] + self.assertTrue(timeutils.parse_strtime(new_trust['expires_at'], + fmt=TIME_FORMAT)) + self.assertIn('%s/v3/OS-TRUST/' % HOST_URL, + new_trust['links']['self']) + self.assertIn('%s/v3/OS-TRUST/' % HOST_URL, + new_trust['roles_links']['self']) + + for role in new_trust['roles']: + self.assertIn(role['id'], role_ids) + + def test_create_trust_expires_bad(self): + self.assertRaises(exception.ValidationTimeStampError, + self.create_trust, self.sample_data, + self.trustor['name'], expires_at="bad") + self.assertRaises(exception.ValidationTimeStampError, + self.create_trust, self.sample_data, + self.trustor['name'], expires_at="") + self.assertRaises(exception.ValidationTimeStampError, + self.create_trust, self.sample_data, + self.trustor['name'], expires_at="Z") + + def test_create_trust_without_project_id(self): + """Verify that trust can be created without project id and + token can be generated with that trust. + """ + unscoped_token = self.get_unscoped_token(self.trustor['name']) + context = self._create_auth_context( + unscoped_token['access']['token']['id']) + self.sample_data['project_id'] = None + self.sample_data['roles'] = [] + new_trust = self.trust_controller.create_trust( + context, trust=self.sample_data)['trust'] + self.assertEqual(self.trustor['id'], new_trust['trustor_user_id']) + self.assertEqual(self.trustee['id'], new_trust['trustee_user_id']) + self.assertIs(new_trust['impersonation'], True) + auth_response = self.fetch_v2_token_from_trust(new_trust) + token_user = auth_response['access']['user'] + self.assertEqual(token_user['id'], new_trust['trustor_user_id']) + + def test_get_trust(self): + unscoped_token = self.get_unscoped_token(self.trustor['name']) + context = {'token_id': unscoped_token['access']['token']['id'], + 'host_url': HOST_URL} + new_trust = self.trust_controller.create_trust( + context, trust=self.sample_data)['trust'] + trust = self.trust_controller.get_trust(context, + new_trust['id'])['trust'] + self.assertEqual(self.trustor['id'], trust['trustor_user_id']) + self.assertEqual(self.trustee['id'], trust['trustee_user_id']) + role_ids = [self.role_browser['id'], self.role_member['id']] + for role in new_trust['roles']: + self.assertIn(role['id'], role_ids) + + def test_create_trust_no_impersonation(self): + new_trust = self.create_trust(self.sample_data, self.trustor['name'], + expires_at=None, impersonation=False) + self.assertEqual(self.trustor['id'], new_trust['trustor_user_id']) + self.assertEqual(self.trustee['id'], new_trust['trustee_user_id']) + self.assertIs(new_trust['impersonation'], False) + auth_response = self.fetch_v2_token_from_trust(new_trust) + token_user = auth_response['access']['user'] + self.assertEqual(token_user['id'], new_trust['trustee_user_id']) + + # TODO(ayoung): Endpoints + + def test_create_trust_impersonation(self): + new_trust = self.create_trust(self.sample_data, self.trustor['name']) + self.assertEqual(self.trustor['id'], new_trust['trustor_user_id']) + self.assertEqual(self.trustee['id'], new_trust['trustee_user_id']) + self.assertIs(new_trust['impersonation'], True) + auth_response = self.fetch_v2_token_from_trust(new_trust) + token_user = auth_response['access']['user'] + self.assertEqual(token_user['id'], new_trust['trustor_user_id']) + + def test_token_from_trust_wrong_user_fails(self): + new_trust = self.create_trust(self.sample_data, self.trustor['name']) + request_body = self.build_v2_token_request('FOO', 'foo2', new_trust) + self.assertRaises(exception.Forbidden, self.controller.authenticate, + {}, request_body) + + def test_token_from_trust_wrong_project_fails(self): + for assigned_role in self.assigned_roles: + self.assignment_api.add_role_to_user_and_project( + self.trustor['id'], self.tenant_baz['id'], assigned_role) + new_trust = self.create_trust(self.sample_data, self.trustor['name']) + request_body = self.build_v2_token_request('TWO', 'two2', new_trust, + self.tenant_baz['id']) + self.assertRaises(exception.Forbidden, self.controller.authenticate, + {}, request_body) + + def fetch_v2_token_from_trust(self, trust): + request_body = self.build_v2_token_request('TWO', 'two2', trust) + auth_response = self.controller.authenticate({}, request_body) + return auth_response + + def fetch_v3_token_from_trust(self, trust, trustee): + v3_password_data = { + 'identity': { + "methods": ["password"], + "password": { + "user": { + "id": trustee["id"], + "password": trustee["password"] + } + } + }, + 'scope': { + 'project': { + 'id': self.tenant_baz['id'] + } + } + } + auth_response = (self.auth_v3_controller.authenticate_for_token + ({'environment': {}, + 'query_string': {}}, + v3_password_data)) + token = auth_response.headers['X-Subject-Token'] + + v3_req_with_trust = { + "identity": { + "methods": ["token"], + "token": {"id": token}}, + "scope": { + "OS-TRUST:trust": {"id": trust['id']}}} + token_auth_response = (self.auth_v3_controller.authenticate_for_token + ({'environment': {}, + 'query_string': {}}, + v3_req_with_trust)) + return token_auth_response + + def test_create_v3_token_from_trust(self): + new_trust = self.create_trust(self.sample_data, self.trustor['name']) + auth_response = self.fetch_v3_token_from_trust(new_trust, self.trustee) + + trust_token_user = auth_response.json['token']['user'] + self.assertEqual(self.trustor['id'], trust_token_user['id']) + + trust_token_trust = auth_response.json['token']['OS-TRUST:trust'] + self.assertEqual(trust_token_trust['id'], new_trust['id']) + self.assertEqual(self.trustor['id'], + trust_token_trust['trustor_user']['id']) + self.assertEqual(self.trustee['id'], + trust_token_trust['trustee_user']['id']) + + trust_token_roles = auth_response.json['token']['roles'] + self.assertEqual(2, len(trust_token_roles)) + + def test_v3_trust_token_get_token_fails(self): + new_trust = self.create_trust(self.sample_data, self.trustor['name']) + auth_response = self.fetch_v3_token_from_trust(new_trust, self.trustee) + trust_token = auth_response.headers['X-Subject-Token'] + v3_token_data = {'identity': { + 'methods': ['token'], + 'token': {'id': trust_token} + }} + self.assertRaises( + exception.Forbidden, + self.auth_v3_controller.authenticate_for_token, + {'environment': {}, + 'query_string': {}}, v3_token_data) + + def test_token_from_trust(self): + new_trust = self.create_trust(self.sample_data, self.trustor['name']) + auth_response = self.fetch_v2_token_from_trust(new_trust) + + self.assertIsNotNone(auth_response) + self.assertEqual(2, + len(auth_response['access']['metadata']['roles']), + "user_foo has three roles, but the token should" + " only get the two roles specified in the trust.") + + def assert_token_count_for_trust(self, trust, expected_value): + tokens = self.token_provider_api._persistence._list_tokens( + self.trustee['id'], trust_id=trust['id']) + token_count = len(tokens) + self.assertEqual(expected_value, token_count) + + def test_delete_tokens_for_user_invalidates_tokens_from_trust(self): + new_trust = self.create_trust(self.sample_data, self.trustor['name']) + self.assert_token_count_for_trust(new_trust, 0) + self.fetch_v2_token_from_trust(new_trust) + self.assert_token_count_for_trust(new_trust, 1) + self.token_provider_api._persistence.delete_tokens_for_user( + self.trustee['id']) + self.assert_token_count_for_trust(new_trust, 0) + + def test_token_from_trust_cant_get_another_token(self): + new_trust = self.create_trust(self.sample_data, self.trustor['name']) + auth_response = self.fetch_v2_token_from_trust(new_trust) + trust_token_id = auth_response['access']['token']['id'] + request_body = _build_user_auth(token={'id': trust_token_id}, + tenant_id=self.tenant_bar['id']) + self.assertRaises( + exception.Forbidden, + self.controller.authenticate, {}, request_body) + + def test_delete_trust_revokes_token(self): + unscoped_token = self.get_unscoped_token(self.trustor['name']) + new_trust = self.create_trust(self.sample_data, self.trustor['name']) + context = self._create_auth_context( + unscoped_token['access']['token']['id']) + self.fetch_v2_token_from_trust(new_trust) + trust_id = new_trust['id'] + tokens = self.token_provider_api._persistence._list_tokens( + self.trustor['id'], + trust_id=trust_id) + self.assertEqual(1, len(tokens)) + self.trust_controller.delete_trust(context, trust_id=trust_id) + tokens = self.token_provider_api._persistence._list_tokens( + self.trustor['id'], + trust_id=trust_id) + self.assertEqual(0, len(tokens)) + + def test_token_from_trust_with_no_role_fails(self): + new_trust = self.create_trust(self.sample_data, self.trustor['name']) + for assigned_role in self.assigned_roles: + self.assignment_api.remove_role_from_user_and_project( + self.trustor['id'], self.tenant_bar['id'], assigned_role) + request_body = self.build_v2_token_request('TWO', 'two2', new_trust) + self.assertRaises( + exception.Forbidden, + self.controller.authenticate, {}, request_body) + + def test_expired_trust_get_token_fails(self): + expiry = "1999-02-18T10:10:00Z" + new_trust = self.create_trust(self.sample_data, self.trustor['name'], + expiry) + request_body = self.build_v2_token_request('TWO', 'two2', new_trust) + self.assertRaises( + exception.Forbidden, + self.controller.authenticate, {}, request_body) + + def test_token_from_trust_with_wrong_role_fails(self): + new_trust = self.create_trust(self.sample_data, self.trustor['name']) + self.assignment_api.add_role_to_user_and_project( + self.trustor['id'], + self.tenant_bar['id'], + self.role_other['id']) + for assigned_role in self.assigned_roles: + self.assignment_api.remove_role_from_user_and_project( + self.trustor['id'], self.tenant_bar['id'], assigned_role) + + request_body = self.build_v2_token_request('TWO', 'two2', new_trust) + + self.assertRaises( + exception.Forbidden, + self.controller.authenticate, {}, request_body) + + def test_do_not_consume_remaining_uses_when_get_token_fails(self): + trust_data = copy.deepcopy(self.sample_data) + trust_data['remaining_uses'] = 3 + new_trust = self.create_trust(trust_data, self.trustor['name']) + + for assigned_role in self.assigned_roles: + self.assignment_api.remove_role_from_user_and_project( + self.trustor['id'], self.tenant_bar['id'], assigned_role) + + request_body = self.build_v2_token_request('TWO', 'two2', new_trust) + self.assertRaises(exception.Forbidden, + self.controller.authenticate, {}, request_body) + + unscoped_token = self.get_unscoped_token(self.trustor['name']) + context = self._create_auth_context( + unscoped_token['access']['token']['id']) + trust = self.trust_controller.get_trust(context, + new_trust['id'])['trust'] + self.assertEqual(3, trust['remaining_uses']) + + def test_v2_trust_token_contains_trustor_user_id_and_impersonation(self): + new_trust = self.create_trust(self.sample_data, self.trustor['name']) + auth_response = self.fetch_v2_token_from_trust(new_trust) + + self.assertEqual(new_trust['trustee_user_id'], + auth_response['access']['trust']['trustee_user_id']) + self.assertEqual(new_trust['trustor_user_id'], + auth_response['access']['trust']['trustor_user_id']) + self.assertEqual(new_trust['impersonation'], + auth_response['access']['trust']['impersonation']) + self.assertEqual(new_trust['id'], + auth_response['access']['trust']['id']) + + validate_response = self.controller.validate_token( + context=dict(is_admin=True, query_string={}), + token_id=auth_response['access']['token']['id']) + self.assertEqual( + new_trust['trustee_user_id'], + validate_response['access']['trust']['trustee_user_id']) + self.assertEqual( + new_trust['trustor_user_id'], + validate_response['access']['trust']['trustor_user_id']) + self.assertEqual( + new_trust['impersonation'], + validate_response['access']['trust']['impersonation']) + self.assertEqual( + new_trust['id'], + validate_response['access']['trust']['id']) + + def disable_user(self, user): + user['enabled'] = False + self.identity_api.update_user(user['id'], user) + + def test_trust_get_token_fails_if_trustor_disabled(self): + new_trust = self.create_trust(self.sample_data, self.trustor['name']) + request_body = self.build_v2_token_request(self.trustee['name'], + self.trustee['password'], + new_trust) + self.disable_user(self.trustor) + self.assertRaises( + exception.Forbidden, + self.controller.authenticate, {}, request_body) + + def test_trust_get_token_fails_if_trustee_disabled(self): + new_trust = self.create_trust(self.sample_data, self.trustor['name']) + request_body = self.build_v2_token_request(self.trustee['name'], + self.trustee['password'], + new_trust) + self.disable_user(self.trustee) + self.assertRaises( + exception.Unauthorized, + self.controller.authenticate, {}, request_body) + + +class TokenExpirationTest(AuthTest): + + @mock.patch.object(timeutils, 'utcnow') + def _maintain_token_expiration(self, mock_utcnow): + """Token expiration should be maintained after re-auth & validation.""" + now = datetime.datetime.utcnow() + mock_utcnow.return_value = now + + r = self.controller.authenticate( + {}, + auth={ + 'passwordCredentials': { + 'username': self.user_foo['name'], + 'password': self.user_foo['password'] + } + }) + unscoped_token_id = r['access']['token']['id'] + original_expiration = r['access']['token']['expires'] + + mock_utcnow.return_value = now + datetime.timedelta(seconds=1) + + r = self.controller.validate_token( + dict(is_admin=True, query_string={}), + token_id=unscoped_token_id) + self.assertEqual(original_expiration, r['access']['token']['expires']) + + mock_utcnow.return_value = now + datetime.timedelta(seconds=2) + + r = self.controller.authenticate( + {}, + auth={ + 'token': { + 'id': unscoped_token_id, + }, + 'tenantId': self.tenant_bar['id'], + }) + scoped_token_id = r['access']['token']['id'] + self.assertEqual(original_expiration, r['access']['token']['expires']) + + mock_utcnow.return_value = now + datetime.timedelta(seconds=3) + + r = self.controller.validate_token( + dict(is_admin=True, query_string={}), + token_id=scoped_token_id) + self.assertEqual(original_expiration, r['access']['token']['expires']) + + def test_maintain_uuid_token_expiration(self): + self.config_fixture.config( + group='token', + provider='keystone.token.providers.uuid.Provider') + self._maintain_token_expiration() + + +class AuthCatalog(tests.SQLDriverOverrides, AuthTest): + """Tests for the catalog provided in the auth response.""" + + def config_files(self): + config_files = super(AuthCatalog, self).config_files() + # We need to use a backend that supports disabled endpoints, like the + # SQL backend. + config_files.append(tests.dirs.tests_conf('backend_sql.conf')) + return config_files + + def _create_endpoints(self): + def create_region(**kwargs): + ref = {'id': uuid.uuid4().hex} + ref.update(kwargs) + self.catalog_api.create_region(ref) + return ref + + def create_endpoint(service_id, region, **kwargs): + id_ = uuid.uuid4().hex + ref = { + 'id': id_, + 'interface': 'public', + 'region_id': region, + 'service_id': service_id, + 'url': 'http://localhost/%s' % uuid.uuid4().hex, + } + ref.update(kwargs) + self.catalog_api.create_endpoint(id_, ref) + return ref + + # Create a service for use with the endpoints. + def create_service(**kwargs): + id_ = uuid.uuid4().hex + ref = { + 'id': id_, + 'name': uuid.uuid4().hex, + 'type': uuid.uuid4().hex, + } + ref.update(kwargs) + self.catalog_api.create_service(id_, ref) + return ref + + enabled_service_ref = create_service(enabled=True) + disabled_service_ref = create_service(enabled=False) + + region = create_region() + + # Create endpoints + enabled_endpoint_ref = create_endpoint( + enabled_service_ref['id'], region['id']) + create_endpoint( + enabled_service_ref['id'], region['id'], enabled=False, + interface='internal') + create_endpoint( + disabled_service_ref['id'], region['id']) + + return enabled_endpoint_ref + + def test_auth_catalog_disabled_endpoint(self): + """On authenticate, get a catalog that excludes disabled endpoints.""" + endpoint_ref = self._create_endpoints() + + # Authenticate + body_dict = _build_user_auth( + username='FOO', + password='foo2', + tenant_name="BAR") + + token = self.controller.authenticate({}, body_dict) + + # Check the catalog + self.assertEqual(1, len(token['access']['serviceCatalog'])) + endpoint = token['access']['serviceCatalog'][0]['endpoints'][0] + self.assertEqual( + 1, len(token['access']['serviceCatalog'][0]['endpoints'])) + + exp_endpoint = { + 'id': endpoint_ref['id'], + 'publicURL': endpoint_ref['url'], + 'region': endpoint_ref['region_id'], + } + + self.assertEqual(exp_endpoint, endpoint) + + def test_validate_catalog_disabled_endpoint(self): + """On validate, get back a catalog that excludes disabled endpoints.""" + endpoint_ref = self._create_endpoints() + + # Authenticate + body_dict = _build_user_auth( + username='FOO', + password='foo2', + tenant_name="BAR") + + token = self.controller.authenticate({}, body_dict) + + # Validate + token_id = token['access']['token']['id'] + validate_ref = self.controller.validate_token( + dict(is_admin=True, query_string={}), + token_id=token_id) + + # Check the catalog + self.assertEqual(1, len(token['access']['serviceCatalog'])) + endpoint = validate_ref['access']['serviceCatalog'][0]['endpoints'][0] + self.assertEqual( + 1, len(token['access']['serviceCatalog'][0]['endpoints'])) + + exp_endpoint = { + 'id': endpoint_ref['id'], + 'publicURL': endpoint_ref['url'], + 'region': endpoint_ref['region_id'], + } + + self.assertEqual(exp_endpoint, endpoint) + + +class NonDefaultAuthTest(tests.TestCase): + + def test_add_non_default_auth_method(self): + self.config_fixture.config(group='auth', + methods=['password', 'token', 'custom']) + config.setup_authentication() + self.assertTrue(hasattr(CONF.auth, 'custom')) diff --git a/keystone-moon/keystone/tests/unit/test_auth_plugin.py b/keystone-moon/keystone/tests/unit/test_auth_plugin.py new file mode 100644 index 00000000..11df95a5 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_auth_plugin.py @@ -0,0 +1,220 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 + +import mock + +from keystone import auth +from keystone import exception +from keystone.tests import unit as tests + + +# for testing purposes only +METHOD_NAME = 'simple_challenge_response' +EXPECTED_RESPONSE = uuid.uuid4().hex +DEMO_USER_ID = uuid.uuid4().hex + + +class SimpleChallengeResponse(auth.AuthMethodHandler): + + method = METHOD_NAME + + def authenticate(self, context, auth_payload, user_context): + if 'response' in auth_payload: + if auth_payload['response'] != EXPECTED_RESPONSE: + raise exception.Unauthorized('Wrong answer') + user_context['user_id'] = DEMO_USER_ID + else: + return {"challenge": "What's the name of your high school?"} + + +class DuplicateAuthPlugin(SimpleChallengeResponse): + """Duplicate simple challenge response auth plugin.""" + + +class MismatchedAuthPlugin(SimpleChallengeResponse): + method = uuid.uuid4().hex + + +class NoMethodAuthPlugin(auth.AuthMethodHandler): + """An auth plugin that does not supply a method attribute.""" + def authenticate(self, context, auth_payload, auth_context): + pass + + +class TestAuthPlugin(tests.SQLDriverOverrides, tests.TestCase): + def setUp(self): + super(TestAuthPlugin, self).setUp() + self.load_backends() + + self.api = auth.controllers.Auth() + + def config_overrides(self): + super(TestAuthPlugin, self).config_overrides() + method_opts = { + 'external': 'keystone.auth.plugins.external.DefaultDomain', + 'password': 'keystone.auth.plugins.password.Password', + 'token': 'keystone.auth.plugins.token.Token', + METHOD_NAME: + 'keystone.tests.unit.test_auth_plugin.SimpleChallengeResponse', + } + + self.auth_plugin_config_override( + methods=['external', 'password', 'token', METHOD_NAME], + **method_opts) + + def test_unsupported_auth_method(self): + method_name = uuid.uuid4().hex + auth_data = {'methods': [method_name]} + auth_data[method_name] = {'test': 'test'} + auth_data = {'identity': auth_data} + self.assertRaises(exception.AuthMethodNotSupported, + auth.controllers.AuthInfo.create, + None, + auth_data) + + def test_addition_auth_steps(self): + auth_data = {'methods': [METHOD_NAME]} + auth_data[METHOD_NAME] = { + 'test': 'test'} + auth_data = {'identity': auth_data} + auth_info = auth.controllers.AuthInfo.create(None, auth_data) + auth_context = {'extras': {}, 'method_names': []} + try: + self.api.authenticate({'environment': {}}, auth_info, auth_context) + except exception.AdditionalAuthRequired as e: + self.assertIn('methods', e.authentication) + self.assertIn(METHOD_NAME, e.authentication['methods']) + self.assertIn(METHOD_NAME, e.authentication) + self.assertIn('challenge', e.authentication[METHOD_NAME]) + + # test correct response + auth_data = {'methods': [METHOD_NAME]} + auth_data[METHOD_NAME] = { + 'response': EXPECTED_RESPONSE} + auth_data = {'identity': auth_data} + auth_info = auth.controllers.AuthInfo.create(None, auth_data) + auth_context = {'extras': {}, 'method_names': []} + self.api.authenticate({'environment': {}}, auth_info, auth_context) + self.assertEqual(DEMO_USER_ID, auth_context['user_id']) + + # test incorrect response + auth_data = {'methods': [METHOD_NAME]} + auth_data[METHOD_NAME] = { + 'response': uuid.uuid4().hex} + auth_data = {'identity': auth_data} + auth_info = auth.controllers.AuthInfo.create(None, auth_data) + auth_context = {'extras': {}, 'method_names': []} + self.assertRaises(exception.Unauthorized, + self.api.authenticate, + {'environment': {}}, + auth_info, + auth_context) + + +class TestAuthPluginDynamicOptions(TestAuthPlugin): + def config_overrides(self): + super(TestAuthPluginDynamicOptions, self).config_overrides() + # Clear the override for the [auth] ``methods`` option so it is + # possible to load the options from the config file. + self.config_fixture.conf.clear_override('methods', group='auth') + + def config_files(self): + config_files = super(TestAuthPluginDynamicOptions, self).config_files() + config_files.append(tests.dirs.tests_conf('test_auth_plugin.conf')) + return config_files + + +class TestInvalidAuthMethodRegistration(tests.TestCase): + def test_duplicate_auth_method_registration(self): + self.config_fixture.config( + group='auth', + methods=[ + 'keystone.tests.unit.test_auth_plugin.SimpleChallengeResponse', + 'keystone.tests.unit.test_auth_plugin.DuplicateAuthPlugin']) + self.clear_auth_plugin_registry() + self.assertRaises(ValueError, auth.controllers.load_auth_methods) + + def test_no_method_attribute_auth_method_by_class_name_registration(self): + self.config_fixture.config( + group='auth', + methods=['keystone.tests.unit.test_auth_plugin.NoMethodAuthPlugin'] + ) + self.clear_auth_plugin_registry() + self.assertRaises(ValueError, auth.controllers.load_auth_methods) + + +class TestMapped(tests.TestCase): + def setUp(self): + super(TestMapped, self).setUp() + self.load_backends() + + self.api = auth.controllers.Auth() + + def config_files(self): + config_files = super(TestMapped, self).config_files() + config_files.append(tests.dirs.tests_conf('test_auth_plugin.conf')) + return config_files + + def config_overrides(self): + # don't override configs so we can use test_auth_plugin.conf only + pass + + def _test_mapped_invocation_with_method_name(self, method_name): + with mock.patch.object(auth.plugins.mapped.Mapped, + 'authenticate', + return_value=None) as authenticate: + context = {'environment': {}} + auth_data = { + 'identity': { + 'methods': [method_name], + method_name: {'protocol': method_name}, + } + } + auth_info = auth.controllers.AuthInfo.create(context, auth_data) + auth_context = {'extras': {}, + 'method_names': [], + 'user_id': uuid.uuid4().hex} + self.api.authenticate(context, auth_info, auth_context) + # make sure Mapped plugin got invoked with the correct payload + ((context, auth_payload, auth_context), + kwargs) = authenticate.call_args + self.assertEqual(method_name, auth_payload['protocol']) + + def test_mapped_with_remote_user(self): + with mock.patch.object(auth.plugins.mapped.Mapped, + 'authenticate', + return_value=None) as authenticate: + # external plugin should fail and pass to mapped plugin + method_name = 'saml2' + auth_data = {'methods': [method_name]} + # put the method name in the payload so its easier to correlate + # method name with payload + auth_data[method_name] = {'protocol': method_name} + auth_data = {'identity': auth_data} + auth_info = auth.controllers.AuthInfo.create(None, auth_data) + auth_context = {'extras': {}, + 'method_names': [], + 'user_id': uuid.uuid4().hex} + environment = {'environment': {'REMOTE_USER': 'foo@idp.com'}} + self.api.authenticate(environment, auth_info, auth_context) + # make sure Mapped plugin got invoked with the correct payload + ((context, auth_payload, auth_context), + kwargs) = authenticate.call_args + self.assertEqual(auth_payload['protocol'], method_name) + + def test_supporting_multiple_methods(self): + for method_name in ['saml2', 'openid', 'x509']: + self._test_mapped_invocation_with_method_name(method_name) diff --git a/keystone-moon/keystone/tests/unit/test_backend.py b/keystone-moon/keystone/tests/unit/test_backend.py new file mode 100644 index 00000000..6cf06494 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_backend.py @@ -0,0 +1,5741 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 +import datetime +import hashlib +import uuid + +from keystoneclient.common import cms +import mock +from oslo_config import cfg +from oslo_utils import timeutils +import six +from testtools import matchers + +from keystone.catalog import core +from keystone.common import driver_hints +from keystone import exception +from keystone.tests import unit as tests +from keystone.tests.unit import default_fixtures +from keystone.tests.unit import filtering +from keystone.tests.unit import utils as test_utils +from keystone.token import provider + + +CONF = cfg.CONF +DEFAULT_DOMAIN_ID = CONF.identity.default_domain_id +NULL_OBJECT = object() + + +class IdentityTests(object): + def _get_domain_fixture(self): + domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(domain['id'], domain) + return domain + + def _set_domain_scope(self, domain_id): + # We only provide a domain scope if we have multiple drivers + if CONF.identity.domain_specific_drivers_enabled: + return domain_id + + def test_project_add_and_remove_user_role(self): + user_ids = self.assignment_api.list_user_ids_for_project( + self.tenant_bar['id']) + self.assertNotIn(self.user_two['id'], user_ids) + + self.assignment_api.add_role_to_user_and_project( + tenant_id=self.tenant_bar['id'], + user_id=self.user_two['id'], + role_id=self.role_other['id']) + user_ids = self.assignment_api.list_user_ids_for_project( + self.tenant_bar['id']) + self.assertIn(self.user_two['id'], user_ids) + + self.assignment_api.remove_role_from_user_and_project( + tenant_id=self.tenant_bar['id'], + user_id=self.user_two['id'], + role_id=self.role_other['id']) + + user_ids = self.assignment_api.list_user_ids_for_project( + self.tenant_bar['id']) + self.assertNotIn(self.user_two['id'], user_ids) + + def test_remove_user_role_not_assigned(self): + # Expect failure if attempt to remove a role that was never assigned to + # the user. + self.assertRaises(exception.RoleNotFound, + self.assignment_api. + remove_role_from_user_and_project, + tenant_id=self.tenant_bar['id'], + user_id=self.user_two['id'], + role_id=self.role_other['id']) + + def test_authenticate_bad_user(self): + self.assertRaises(AssertionError, + self.identity_api.authenticate, + context={}, + user_id=uuid.uuid4().hex, + password=self.user_foo['password']) + + def test_authenticate_bad_password(self): + self.assertRaises(AssertionError, + self.identity_api.authenticate, + context={}, + user_id=self.user_foo['id'], + password=uuid.uuid4().hex) + + def test_authenticate(self): + user_ref = self.identity_api.authenticate( + context={}, + user_id=self.user_sna['id'], + password=self.user_sna['password']) + # NOTE(termie): the password field is left in user_sna to make + # it easier to authenticate in tests, but should + # not be returned by the api + self.user_sna.pop('password') + self.user_sna['enabled'] = True + self.assertDictEqual(user_ref, self.user_sna) + + def test_authenticate_and_get_roles_no_metadata(self): + user = { + 'name': 'NO_META', + 'domain_id': DEFAULT_DOMAIN_ID, + 'password': 'no_meta2', + } + new_user = self.identity_api.create_user(user) + self.assignment_api.add_user_to_project(self.tenant_baz['id'], + new_user['id']) + user_ref = self.identity_api.authenticate( + context={}, + user_id=new_user['id'], + password=user['password']) + self.assertNotIn('password', user_ref) + # NOTE(termie): the password field is left in user_sna to make + # it easier to authenticate in tests, but should + # not be returned by the api + user.pop('password') + self.assertDictContainsSubset(user, user_ref) + role_list = self.assignment_api.get_roles_for_user_and_project( + new_user['id'], self.tenant_baz['id']) + self.assertEqual(1, len(role_list)) + self.assertIn(CONF.member_role_id, role_list) + + def test_authenticate_if_no_password_set(self): + id_ = uuid.uuid4().hex + user = { + 'name': uuid.uuid4().hex, + 'domain_id': DEFAULT_DOMAIN_ID, + } + self.identity_api.create_user(user) + + self.assertRaises(AssertionError, + self.identity_api.authenticate, + context={}, + user_id=id_, + password='password') + + def test_create_unicode_user_name(self): + unicode_name = u'name \u540d\u5b57' + user = {'name': unicode_name, + 'domain_id': DEFAULT_DOMAIN_ID, + 'password': uuid.uuid4().hex} + ref = self.identity_api.create_user(user) + self.assertEqual(unicode_name, ref['name']) + + def test_get_project(self): + tenant_ref = self.resource_api.get_project(self.tenant_bar['id']) + self.assertDictEqual(tenant_ref, self.tenant_bar) + + def test_get_project_404(self): + self.assertRaises(exception.ProjectNotFound, + self.resource_api.get_project, + uuid.uuid4().hex) + + def test_get_project_by_name(self): + tenant_ref = self.resource_api.get_project_by_name( + self.tenant_bar['name'], + DEFAULT_DOMAIN_ID) + self.assertDictEqual(tenant_ref, self.tenant_bar) + + def test_get_project_by_name_404(self): + self.assertRaises(exception.ProjectNotFound, + self.resource_api.get_project_by_name, + uuid.uuid4().hex, + DEFAULT_DOMAIN_ID) + + def test_list_user_ids_for_project(self): + user_ids = self.assignment_api.list_user_ids_for_project( + self.tenant_baz['id']) + self.assertEqual(2, len(user_ids)) + self.assertIn(self.user_two['id'], user_ids) + self.assertIn(self.user_badguy['id'], user_ids) + + def test_list_user_ids_for_project_no_duplicates(self): + # Create user + user_ref = { + 'name': uuid.uuid4().hex, + 'domain_id': DEFAULT_DOMAIN_ID, + 'password': uuid.uuid4().hex, + 'enabled': True} + user_ref = self.identity_api.create_user(user_ref) + # Create project + project_ref = { + 'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'domain_id': DEFAULT_DOMAIN_ID} + self.resource_api.create_project( + project_ref['id'], project_ref) + # Create 2 roles and give user each role in project + for i in range(2): + role_ref = { + 'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex} + self.role_api.create_role(role_ref['id'], role_ref) + self.assignment_api.add_role_to_user_and_project( + user_id=user_ref['id'], + tenant_id=project_ref['id'], + role_id=role_ref['id']) + # Get the list of user_ids in project + user_ids = self.assignment_api.list_user_ids_for_project( + project_ref['id']) + # Ensure the user is only returned once + self.assertEqual(1, len(user_ids)) + + def test_get_project_user_ids_404(self): + self.assertRaises(exception.ProjectNotFound, + self.assignment_api.list_user_ids_for_project, + uuid.uuid4().hex) + + def test_get_user(self): + user_ref = self.identity_api.get_user(self.user_foo['id']) + # NOTE(termie): the password field is left in user_foo to make + # it easier to authenticate in tests, but should + # not be returned by the api + self.user_foo.pop('password') + self.assertDictEqual(user_ref, self.user_foo) + + @tests.skip_if_cache_disabled('identity') + def test_cache_layer_get_user(self): + user = { + 'name': uuid.uuid4().hex.lower(), + 'domain_id': DEFAULT_DOMAIN_ID + } + self.identity_api.create_user(user) + ref = self.identity_api.get_user_by_name(user['name'], + user['domain_id']) + # cache the result. + self.identity_api.get_user(ref['id']) + # delete bypassing identity api + domain_id, driver, entity_id = ( + self.identity_api._get_domain_driver_and_entity_id(ref['id'])) + driver.delete_user(entity_id) + + self.assertDictEqual(ref, self.identity_api.get_user(ref['id'])) + self.identity_api.get_user.invalidate(self.identity_api, ref['id']) + self.assertRaises(exception.UserNotFound, + self.identity_api.get_user, ref['id']) + user = { + 'name': uuid.uuid4().hex.lower(), + 'domain_id': DEFAULT_DOMAIN_ID + } + self.identity_api.create_user(user) + ref = self.identity_api.get_user_by_name(user['name'], + user['domain_id']) + user['description'] = uuid.uuid4().hex + # cache the result. + self.identity_api.get_user(ref['id']) + # update using identity api and get back updated user. + user_updated = self.identity_api.update_user(ref['id'], user) + self.assertDictContainsSubset(self.identity_api.get_user(ref['id']), + user_updated) + self.assertDictContainsSubset( + self.identity_api.get_user_by_name(ref['name'], ref['domain_id']), + user_updated) + + def test_get_user_404(self): + self.assertRaises(exception.UserNotFound, + self.identity_api.get_user, + uuid.uuid4().hex) + + def test_get_user_by_name(self): + user_ref = self.identity_api.get_user_by_name( + self.user_foo['name'], DEFAULT_DOMAIN_ID) + # NOTE(termie): the password field is left in user_foo to make + # it easier to authenticate in tests, but should + # not be returned by the api + self.user_foo.pop('password') + self.assertDictEqual(user_ref, self.user_foo) + + @tests.skip_if_cache_disabled('identity') + def test_cache_layer_get_user_by_name(self): + user = { + 'name': uuid.uuid4().hex.lower(), + 'domain_id': DEFAULT_DOMAIN_ID + } + self.identity_api.create_user(user) + ref = self.identity_api.get_user_by_name(user['name'], + user['domain_id']) + # delete bypassing the identity api. + domain_id, driver, entity_id = ( + self.identity_api._get_domain_driver_and_entity_id(ref['id'])) + driver.delete_user(entity_id) + + self.assertDictEqual(ref, self.identity_api.get_user_by_name( + user['name'], DEFAULT_DOMAIN_ID)) + self.identity_api.get_user_by_name.invalidate( + self.identity_api, user['name'], DEFAULT_DOMAIN_ID) + self.assertRaises(exception.UserNotFound, + self.identity_api.get_user_by_name, + user['name'], DEFAULT_DOMAIN_ID) + user = { + 'name': uuid.uuid4().hex.lower(), + 'domain_id': DEFAULT_DOMAIN_ID + } + self.identity_api.create_user(user) + ref = self.identity_api.get_user_by_name(user['name'], + user['domain_id']) + user['description'] = uuid.uuid4().hex + user_updated = self.identity_api.update_user(ref['id'], user) + self.assertDictContainsSubset(self.identity_api.get_user(ref['id']), + user_updated) + self.assertDictContainsSubset( + self.identity_api.get_user_by_name(ref['name'], ref['domain_id']), + user_updated) + + def test_get_user_by_name_404(self): + self.assertRaises(exception.UserNotFound, + self.identity_api.get_user_by_name, + uuid.uuid4().hex, + DEFAULT_DOMAIN_ID) + + def test_create_duplicate_user_name_fails(self): + user = {'name': 'fake1', + 'domain_id': DEFAULT_DOMAIN_ID, + 'password': 'fakepass', + 'tenants': ['bar']} + user = self.identity_api.create_user(user) + self.assertRaises(exception.Conflict, + self.identity_api.create_user, + user) + + def test_create_duplicate_user_name_in_different_domains(self): + new_domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(new_domain['id'], new_domain) + user1 = {'name': uuid.uuid4().hex, + 'domain_id': DEFAULT_DOMAIN_ID, + 'password': uuid.uuid4().hex} + user2 = {'name': user1['name'], + 'domain_id': new_domain['id'], + 'password': uuid.uuid4().hex} + self.identity_api.create_user(user1) + self.identity_api.create_user(user2) + + def test_move_user_between_domains(self): + domain1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(domain1['id'], domain1) + domain2 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(domain2['id'], domain2) + user = {'name': uuid.uuid4().hex, + 'domain_id': domain1['id'], + 'password': uuid.uuid4().hex} + user = self.identity_api.create_user(user) + user['domain_id'] = domain2['id'] + self.identity_api.update_user(user['id'], user) + + def test_move_user_between_domains_with_clashing_names_fails(self): + domain1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(domain1['id'], domain1) + domain2 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(domain2['id'], domain2) + # First, create a user in domain1 + user1 = {'name': uuid.uuid4().hex, + 'domain_id': domain1['id'], + 'password': uuid.uuid4().hex} + user1 = self.identity_api.create_user(user1) + # Now create a user in domain2 with a potentially clashing + # name - which should work since we have domain separation + user2 = {'name': user1['name'], + 'domain_id': domain2['id'], + 'password': uuid.uuid4().hex} + user2 = self.identity_api.create_user(user2) + # Now try and move user1 into the 2nd domain - which should + # fail since the names clash + user1['domain_id'] = domain2['id'] + self.assertRaises(exception.Conflict, + self.identity_api.update_user, + user1['id'], + user1) + + def test_rename_duplicate_user_name_fails(self): + user1 = {'name': 'fake1', + 'domain_id': DEFAULT_DOMAIN_ID, + 'password': 'fakepass', + 'tenants': ['bar']} + user2 = {'name': 'fake2', + 'domain_id': DEFAULT_DOMAIN_ID, + 'password': 'fakepass', + 'tenants': ['bar']} + self.identity_api.create_user(user1) + user2 = self.identity_api.create_user(user2) + user2['name'] = 'fake1' + self.assertRaises(exception.Conflict, + self.identity_api.update_user, + user2['id'], + user2) + + def test_update_user_id_fails(self): + user = {'name': 'fake1', + 'domain_id': DEFAULT_DOMAIN_ID, + 'password': 'fakepass', + 'tenants': ['bar']} + user = self.identity_api.create_user(user) + original_id = user['id'] + user['id'] = 'fake2' + self.assertRaises(exception.ValidationError, + self.identity_api.update_user, + original_id, + user) + user_ref = self.identity_api.get_user(original_id) + self.assertEqual(original_id, user_ref['id']) + self.assertRaises(exception.UserNotFound, + self.identity_api.get_user, + 'fake2') + + def test_create_duplicate_project_id_fails(self): + tenant = {'id': 'fake1', 'name': 'fake1', + 'domain_id': DEFAULT_DOMAIN_ID} + self.resource_api.create_project('fake1', tenant) + tenant['name'] = 'fake2' + self.assertRaises(exception.Conflict, + self.resource_api.create_project, + 'fake1', + tenant) + + def test_create_duplicate_project_name_fails(self): + tenant = {'id': 'fake1', 'name': 'fake', + 'domain_id': DEFAULT_DOMAIN_ID} + self.resource_api.create_project('fake1', tenant) + tenant['id'] = 'fake2' + self.assertRaises(exception.Conflict, + self.resource_api.create_project, + 'fake1', + tenant) + + def test_create_duplicate_project_name_in_different_domains(self): + new_domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(new_domain['id'], new_domain) + tenant1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'domain_id': DEFAULT_DOMAIN_ID} + tenant2 = {'id': uuid.uuid4().hex, 'name': tenant1['name'], + 'domain_id': new_domain['id']} + self.resource_api.create_project(tenant1['id'], tenant1) + self.resource_api.create_project(tenant2['id'], tenant2) + + def test_move_project_between_domains(self): + domain1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(domain1['id'], domain1) + domain2 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(domain2['id'], domain2) + project = {'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'domain_id': domain1['id']} + self.resource_api.create_project(project['id'], project) + project['domain_id'] = domain2['id'] + self.resource_api.update_project(project['id'], project) + + def test_move_project_between_domains_with_clashing_names_fails(self): + domain1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(domain1['id'], domain1) + domain2 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(domain2['id'], domain2) + # First, create a project in domain1 + project1 = {'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'domain_id': domain1['id']} + self.resource_api.create_project(project1['id'], project1) + # Now create a project in domain2 with a potentially clashing + # name - which should work since we have domain separation + project2 = {'id': uuid.uuid4().hex, + 'name': project1['name'], + 'domain_id': domain2['id']} + self.resource_api.create_project(project2['id'], project2) + # Now try and move project1 into the 2nd domain - which should + # fail since the names clash + project1['domain_id'] = domain2['id'] + self.assertRaises(exception.Conflict, + self.resource_api.update_project, + project1['id'], + project1) + + def test_rename_duplicate_project_name_fails(self): + tenant1 = {'id': 'fake1', 'name': 'fake1', + 'domain_id': DEFAULT_DOMAIN_ID} + tenant2 = {'id': 'fake2', 'name': 'fake2', + 'domain_id': DEFAULT_DOMAIN_ID} + self.resource_api.create_project('fake1', tenant1) + self.resource_api.create_project('fake2', tenant2) + tenant2['name'] = 'fake1' + self.assertRaises(exception.Error, + self.resource_api.update_project, + 'fake2', + tenant2) + + def test_update_project_id_does_nothing(self): + tenant = {'id': 'fake1', 'name': 'fake1', + 'domain_id': DEFAULT_DOMAIN_ID} + self.resource_api.create_project('fake1', tenant) + tenant['id'] = 'fake2' + self.resource_api.update_project('fake1', tenant) + tenant_ref = self.resource_api.get_project('fake1') + self.assertEqual('fake1', tenant_ref['id']) + self.assertRaises(exception.ProjectNotFound, + self.resource_api.get_project, + 'fake2') + + def test_list_role_assignments_unfiltered(self): + """Test for unfiltered listing role assignments. + + Test Plan: + + - Create a domain, with a user, group & project + - Find how many role assignments already exist (from default + fixtures) + - Create a grant of each type (user/group on project/domain) + - Check the number of assignments has gone up by 4 and that + the entries we added are in the list returned + - Check that if we list assignments by role_id, then we get back + assignments that only contain that role. + + """ + new_domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(new_domain['id'], new_domain) + new_user = {'name': uuid.uuid4().hex, 'password': uuid.uuid4().hex, + 'enabled': True, 'domain_id': new_domain['id']} + new_user = self.identity_api.create_user(new_user) + new_group = {'domain_id': new_domain['id'], 'name': uuid.uuid4().hex} + new_group = self.identity_api.create_group(new_group) + new_project = {'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'domain_id': new_domain['id']} + self.resource_api.create_project(new_project['id'], new_project) + + # First check how many role grants already exist + existing_assignments = len(self.assignment_api.list_role_assignments()) + existing_assignments_for_role = len( + self.assignment_api.list_role_assignments_for_role( + role_id='admin')) + + # Now create the grants (roles are defined in default_fixtures) + self.assignment_api.create_grant(user_id=new_user['id'], + domain_id=new_domain['id'], + role_id='member') + self.assignment_api.create_grant(user_id=new_user['id'], + project_id=new_project['id'], + role_id='other') + self.assignment_api.create_grant(group_id=new_group['id'], + domain_id=new_domain['id'], + role_id='admin') + self.assignment_api.create_grant(group_id=new_group['id'], + project_id=new_project['id'], + role_id='admin') + + # Read back the full list of assignments - check it is gone up by 4 + assignment_list = self.assignment_api.list_role_assignments() + self.assertEqual(existing_assignments + 4, len(assignment_list)) + + # Now check that each of our four new entries are in the list + self.assertIn( + {'user_id': new_user['id'], 'domain_id': new_domain['id'], + 'role_id': 'member'}, + assignment_list) + self.assertIn( + {'user_id': new_user['id'], 'project_id': new_project['id'], + 'role_id': 'other'}, + assignment_list) + self.assertIn( + {'group_id': new_group['id'], 'domain_id': new_domain['id'], + 'role_id': 'admin'}, + assignment_list) + self.assertIn( + {'group_id': new_group['id'], 'project_id': new_project['id'], + 'role_id': 'admin'}, + assignment_list) + + # Read back the list of assignments for just the admin role, checking + # this only goes up by two. + assignment_list = self.assignment_api.list_role_assignments_for_role( + role_id='admin') + self.assertEqual(existing_assignments_for_role + 2, + len(assignment_list)) + + # Now check that each of our two new entries are in the list + self.assertIn( + {'group_id': new_group['id'], 'domain_id': new_domain['id'], + 'role_id': 'admin'}, + assignment_list) + self.assertIn( + {'group_id': new_group['id'], 'project_id': new_project['id'], + 'role_id': 'admin'}, + assignment_list) + + def test_list_group_role_assignment(self): + # When a group role assignment is created and the role assignments are + # listed then the group role assignment is included in the list. + + MEMBER_ROLE_ID = 'member' + + def get_member_assignments(): + assignments = self.assignment_api.list_role_assignments() + return filter(lambda x: x['role_id'] == MEMBER_ROLE_ID, + assignments) + + orig_member_assignments = get_member_assignments() + + # Create a group. + new_group = { + 'domain_id': DEFAULT_DOMAIN_ID, + 'name': self.getUniqueString(prefix='tlgra')} + new_group = self.identity_api.create_group(new_group) + + # Create a project. + new_project = { + 'id': uuid.uuid4().hex, + 'name': self.getUniqueString(prefix='tlgra'), + 'domain_id': DEFAULT_DOMAIN_ID} + self.resource_api.create_project(new_project['id'], new_project) + + # Assign a role to the group. + self.assignment_api.create_grant( + group_id=new_group['id'], project_id=new_project['id'], + role_id=MEMBER_ROLE_ID) + + # List role assignments + new_member_assignments = get_member_assignments() + + expected_member_assignments = orig_member_assignments + [{ + 'group_id': new_group['id'], 'project_id': new_project['id'], + 'role_id': MEMBER_ROLE_ID}] + self.assertThat(new_member_assignments, + matchers.Equals(expected_member_assignments)) + + def test_list_role_assignments_bad_role(self): + assignment_list = self.assignment_api.list_role_assignments_for_role( + role_id=uuid.uuid4().hex) + self.assertEqual([], assignment_list) + + def test_add_duplicate_role_grant(self): + roles_ref = self.assignment_api.get_roles_for_user_and_project( + self.user_foo['id'], self.tenant_bar['id']) + self.assertNotIn(self.role_admin['id'], roles_ref) + self.assignment_api.add_role_to_user_and_project( + self.user_foo['id'], self.tenant_bar['id'], self.role_admin['id']) + self.assertRaises(exception.Conflict, + self.assignment_api.add_role_to_user_and_project, + self.user_foo['id'], + self.tenant_bar['id'], + self.role_admin['id']) + + def test_get_role_by_user_and_project_with_user_in_group(self): + """Test for get role by user and project, user was added into a group. + + Test Plan: + + - Create a user, a project & a group, add this user to group + - Create roles and grant them to user and project + - Check the role list get by the user and project was as expected + + """ + user_ref = {'name': uuid.uuid4().hex, + 'domain_id': DEFAULT_DOMAIN_ID, + 'password': uuid.uuid4().hex, + 'enabled': True} + user_ref = self.identity_api.create_user(user_ref) + + project_ref = {'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'domain_id': DEFAULT_DOMAIN_ID} + self.resource_api.create_project(project_ref['id'], project_ref) + + group = {'name': uuid.uuid4().hex, + 'domain_id': DEFAULT_DOMAIN_ID} + group_id = self.identity_api.create_group(group)['id'] + self.identity_api.add_user_to_group(user_ref['id'], group_id) + + role_ref_list = [] + for i in range(2): + role_ref = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.role_api.create_role(role_ref['id'], role_ref) + role_ref_list.append(role_ref) + + self.assignment_api.add_role_to_user_and_project( + user_id=user_ref['id'], + tenant_id=project_ref['id'], + role_id=role_ref['id']) + + role_list = self.assignment_api.get_roles_for_user_and_project( + user_id=user_ref['id'], + tenant_id=project_ref['id']) + + self.assertEqual(set(role_list), + set([r['id'] for r in role_ref_list])) + + def test_get_role_by_user_and_project(self): + roles_ref = self.assignment_api.get_roles_for_user_and_project( + self.user_foo['id'], self.tenant_bar['id']) + self.assertNotIn(self.role_admin['id'], roles_ref) + self.assignment_api.add_role_to_user_and_project( + self.user_foo['id'], self.tenant_bar['id'], self.role_admin['id']) + roles_ref = self.assignment_api.get_roles_for_user_and_project( + self.user_foo['id'], self.tenant_bar['id']) + self.assertIn(self.role_admin['id'], roles_ref) + self.assertNotIn('member', roles_ref) + + self.assignment_api.add_role_to_user_and_project( + self.user_foo['id'], self.tenant_bar['id'], 'member') + roles_ref = self.assignment_api.get_roles_for_user_and_project( + self.user_foo['id'], self.tenant_bar['id']) + self.assertIn(self.role_admin['id'], roles_ref) + self.assertIn('member', roles_ref) + + def test_get_roles_for_user_and_domain(self): + """Test for getting roles for user on a domain. + + Test Plan: + + - Create a domain, with 2 users + - Check no roles yet exit + - Give user1 two roles on the domain, user2 one role + - Get roles on user1 and the domain - maybe sure we only + get back the 2 roles on user1 + - Delete both roles from user1 + - Check we get no roles back for user1 on domain + + """ + new_domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(new_domain['id'], new_domain) + new_user1 = {'name': uuid.uuid4().hex, 'password': uuid.uuid4().hex, + 'enabled': True, 'domain_id': new_domain['id']} + new_user1 = self.identity_api.create_user(new_user1) + new_user2 = {'name': uuid.uuid4().hex, 'password': uuid.uuid4().hex, + 'enabled': True, 'domain_id': new_domain['id']} + new_user2 = self.identity_api.create_user(new_user2) + roles_ref = self.assignment_api.list_grants( + user_id=new_user1['id'], + domain_id=new_domain['id']) + self.assertEqual(0, len(roles_ref)) + # Now create the grants (roles are defined in default_fixtures) + self.assignment_api.create_grant(user_id=new_user1['id'], + domain_id=new_domain['id'], + role_id='member') + self.assignment_api.create_grant(user_id=new_user1['id'], + domain_id=new_domain['id'], + role_id='other') + self.assignment_api.create_grant(user_id=new_user2['id'], + domain_id=new_domain['id'], + role_id='admin') + # Read back the roles for user1 on domain + roles_ids = self.assignment_api.get_roles_for_user_and_domain( + new_user1['id'], new_domain['id']) + self.assertEqual(2, len(roles_ids)) + self.assertIn(self.role_member['id'], roles_ids) + self.assertIn(self.role_other['id'], roles_ids) + + # Now delete both grants for user1 + self.assignment_api.delete_grant(user_id=new_user1['id'], + domain_id=new_domain['id'], + role_id='member') + self.assignment_api.delete_grant(user_id=new_user1['id'], + domain_id=new_domain['id'], + role_id='other') + roles_ref = self.assignment_api.list_grants( + user_id=new_user1['id'], + domain_id=new_domain['id']) + self.assertEqual(0, len(roles_ref)) + + def test_get_roles_for_user_and_domain_404(self): + """Test errors raised when getting roles for user on a domain. + + Test Plan: + + - Check non-existing user gives UserNotFound + - Check non-existing domain gives DomainNotFound + + """ + new_domain = self._get_domain_fixture() + new_user1 = {'name': uuid.uuid4().hex, 'password': uuid.uuid4().hex, + 'enabled': True, 'domain_id': new_domain['id']} + new_user1 = self.identity_api.create_user(new_user1) + + self.assertRaises(exception.UserNotFound, + self.assignment_api.get_roles_for_user_and_domain, + uuid.uuid4().hex, + new_domain['id']) + + self.assertRaises(exception.DomainNotFound, + self.assignment_api.get_roles_for_user_and_domain, + new_user1['id'], + uuid.uuid4().hex) + + def test_get_roles_for_user_and_project_404(self): + self.assertRaises(exception.UserNotFound, + self.assignment_api.get_roles_for_user_and_project, + uuid.uuid4().hex, + self.tenant_bar['id']) + + self.assertRaises(exception.ProjectNotFound, + self.assignment_api.get_roles_for_user_and_project, + self.user_foo['id'], + uuid.uuid4().hex) + + def test_add_role_to_user_and_project_404(self): + self.assertRaises(exception.ProjectNotFound, + self.assignment_api.add_role_to_user_and_project, + self.user_foo['id'], + uuid.uuid4().hex, + self.role_admin['id']) + + self.assertRaises(exception.RoleNotFound, + self.assignment_api.add_role_to_user_and_project, + self.user_foo['id'], + self.tenant_bar['id'], + uuid.uuid4().hex) + + def test_add_role_to_user_and_project_no_user(self): + # If add_role_to_user_and_project and the user doesn't exist, then + # no error. + user_id_not_exist = uuid.uuid4().hex + self.assignment_api.add_role_to_user_and_project( + user_id_not_exist, self.tenant_bar['id'], self.role_admin['id']) + + def test_remove_role_from_user_and_project(self): + self.assignment_api.add_role_to_user_and_project( + self.user_foo['id'], self.tenant_bar['id'], 'member') + self.assignment_api.remove_role_from_user_and_project( + self.user_foo['id'], self.tenant_bar['id'], 'member') + roles_ref = self.assignment_api.get_roles_for_user_and_project( + self.user_foo['id'], self.tenant_bar['id']) + self.assertNotIn('member', roles_ref) + self.assertRaises(exception.NotFound, + self.assignment_api. + remove_role_from_user_and_project, + self.user_foo['id'], + self.tenant_bar['id'], + 'member') + + def test_get_role_grant_by_user_and_project(self): + roles_ref = self.assignment_api.list_grants( + user_id=self.user_foo['id'], + project_id=self.tenant_bar['id']) + self.assertEqual(1, len(roles_ref)) + self.assignment_api.create_grant(user_id=self.user_foo['id'], + project_id=self.tenant_bar['id'], + role_id=self.role_admin['id']) + roles_ref = self.assignment_api.list_grants( + user_id=self.user_foo['id'], + project_id=self.tenant_bar['id']) + self.assertIn(self.role_admin['id'], + [role_ref['id'] for role_ref in roles_ref]) + + self.assignment_api.create_grant(user_id=self.user_foo['id'], + project_id=self.tenant_bar['id'], + role_id='member') + roles_ref = self.assignment_api.list_grants( + user_id=self.user_foo['id'], + project_id=self.tenant_bar['id']) + + roles_ref_ids = [] + for ref in roles_ref: + roles_ref_ids.append(ref['id']) + self.assertIn(self.role_admin['id'], roles_ref_ids) + self.assertIn('member', roles_ref_ids) + + def test_remove_role_grant_from_user_and_project(self): + self.assignment_api.create_grant(user_id=self.user_foo['id'], + project_id=self.tenant_baz['id'], + role_id='member') + roles_ref = self.assignment_api.list_grants( + user_id=self.user_foo['id'], + project_id=self.tenant_baz['id']) + self.assertDictEqual(roles_ref[0], self.role_member) + + self.assignment_api.delete_grant(user_id=self.user_foo['id'], + project_id=self.tenant_baz['id'], + role_id='member') + roles_ref = self.assignment_api.list_grants( + user_id=self.user_foo['id'], + project_id=self.tenant_baz['id']) + self.assertEqual(0, len(roles_ref)) + self.assertRaises(exception.RoleAssignmentNotFound, + self.assignment_api.delete_grant, + user_id=self.user_foo['id'], + project_id=self.tenant_baz['id'], + role_id='member') + + def test_get_role_assignment_by_project_not_found(self): + self.assertRaises(exception.RoleAssignmentNotFound, + self.assignment_api.check_grant_role_id, + user_id=self.user_foo['id'], + project_id=self.tenant_baz['id'], + role_id='member') + + self.assertRaises(exception.RoleAssignmentNotFound, + self.assignment_api.check_grant_role_id, + group_id=uuid.uuid4().hex, + project_id=self.tenant_baz['id'], + role_id='member') + + def test_get_role_assignment_by_domain_not_found(self): + self.assertRaises(exception.RoleAssignmentNotFound, + self.assignment_api.check_grant_role_id, + user_id=self.user_foo['id'], + domain_id=self.domain_default['id'], + role_id='member') + + self.assertRaises(exception.RoleAssignmentNotFound, + self.assignment_api.check_grant_role_id, + group_id=uuid.uuid4().hex, + domain_id=self.domain_default['id'], + role_id='member') + + def test_del_role_assignment_by_project_not_found(self): + self.assertRaises(exception.RoleAssignmentNotFound, + self.assignment_api.delete_grant, + user_id=self.user_foo['id'], + project_id=self.tenant_baz['id'], + role_id='member') + + self.assertRaises(exception.RoleAssignmentNotFound, + self.assignment_api.delete_grant, + group_id=uuid.uuid4().hex, + project_id=self.tenant_baz['id'], + role_id='member') + + def test_del_role_assignment_by_domain_not_found(self): + self.assertRaises(exception.RoleAssignmentNotFound, + self.assignment_api.delete_grant, + user_id=self.user_foo['id'], + domain_id=self.domain_default['id'], + role_id='member') + + self.assertRaises(exception.RoleAssignmentNotFound, + self.assignment_api.delete_grant, + group_id=uuid.uuid4().hex, + domain_id=self.domain_default['id'], + role_id='member') + + def test_get_and_remove_role_grant_by_group_and_project(self): + new_domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(new_domain['id'], new_domain) + new_group = {'domain_id': new_domain['id'], 'name': uuid.uuid4().hex} + new_group = self.identity_api.create_group(new_group) + new_user = {'name': 'new_user', 'password': 'secret', + 'enabled': True, 'domain_id': new_domain['id']} + new_user = self.identity_api.create_user(new_user) + self.identity_api.add_user_to_group(new_user['id'], + new_group['id']) + roles_ref = self.assignment_api.list_grants( + group_id=new_group['id'], + project_id=self.tenant_bar['id']) + self.assertEqual(0, len(roles_ref)) + self.assignment_api.create_grant(group_id=new_group['id'], + project_id=self.tenant_bar['id'], + role_id='member') + roles_ref = self.assignment_api.list_grants( + group_id=new_group['id'], + project_id=self.tenant_bar['id']) + self.assertDictEqual(roles_ref[0], self.role_member) + + self.assignment_api.delete_grant(group_id=new_group['id'], + project_id=self.tenant_bar['id'], + role_id='member') + roles_ref = self.assignment_api.list_grants( + group_id=new_group['id'], + project_id=self.tenant_bar['id']) + self.assertEqual(0, len(roles_ref)) + self.assertRaises(exception.RoleAssignmentNotFound, + self.assignment_api.delete_grant, + group_id=new_group['id'], + project_id=self.tenant_bar['id'], + role_id='member') + + def test_get_and_remove_role_grant_by_group_and_domain(self): + new_domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(new_domain['id'], new_domain) + new_group = {'domain_id': new_domain['id'], 'name': uuid.uuid4().hex} + new_group = self.identity_api.create_group(new_group) + new_user = {'name': 'new_user', 'password': uuid.uuid4().hex, + 'enabled': True, 'domain_id': new_domain['id']} + new_user = self.identity_api.create_user(new_user) + self.identity_api.add_user_to_group(new_user['id'], + new_group['id']) + + roles_ref = self.assignment_api.list_grants( + group_id=new_group['id'], + domain_id=new_domain['id']) + self.assertEqual(0, len(roles_ref)) + + self.assignment_api.create_grant(group_id=new_group['id'], + domain_id=new_domain['id'], + role_id='member') + + roles_ref = self.assignment_api.list_grants( + group_id=new_group['id'], + domain_id=new_domain['id']) + self.assertDictEqual(roles_ref[0], self.role_member) + + self.assignment_api.delete_grant(group_id=new_group['id'], + domain_id=new_domain['id'], + role_id='member') + roles_ref = self.assignment_api.list_grants( + group_id=new_group['id'], + domain_id=new_domain['id']) + self.assertEqual(0, len(roles_ref)) + self.assertRaises(exception.RoleAssignmentNotFound, + self.assignment_api.delete_grant, + group_id=new_group['id'], + domain_id=new_domain['id'], + role_id='member') + + def test_get_and_remove_correct_role_grant_from_a_mix(self): + new_domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(new_domain['id'], new_domain) + new_project = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'domain_id': new_domain['id']} + self.resource_api.create_project(new_project['id'], new_project) + new_group = {'domain_id': new_domain['id'], 'name': uuid.uuid4().hex} + new_group = self.identity_api.create_group(new_group) + new_group2 = {'domain_id': new_domain['id'], 'name': uuid.uuid4().hex} + new_group2 = self.identity_api.create_group(new_group2) + new_user = {'name': 'new_user', 'password': uuid.uuid4().hex, + 'enabled': True, 'domain_id': new_domain['id']} + new_user = self.identity_api.create_user(new_user) + new_user2 = {'name': 'new_user2', 'password': uuid.uuid4().hex, + 'enabled': True, 'domain_id': new_domain['id']} + new_user2 = self.identity_api.create_user(new_user2) + self.identity_api.add_user_to_group(new_user['id'], + new_group['id']) + # First check we have no grants + roles_ref = self.assignment_api.list_grants( + group_id=new_group['id'], + domain_id=new_domain['id']) + self.assertEqual(0, len(roles_ref)) + # Now add the grant we are going to test for, and some others as + # well just to make sure we get back the right one + self.assignment_api.create_grant(group_id=new_group['id'], + domain_id=new_domain['id'], + role_id='member') + + self.assignment_api.create_grant(group_id=new_group2['id'], + domain_id=new_domain['id'], + role_id=self.role_admin['id']) + self.assignment_api.create_grant(user_id=new_user2['id'], + domain_id=new_domain['id'], + role_id=self.role_admin['id']) + self.assignment_api.create_grant(group_id=new_group['id'], + project_id=new_project['id'], + role_id=self.role_admin['id']) + + roles_ref = self.assignment_api.list_grants( + group_id=new_group['id'], + domain_id=new_domain['id']) + self.assertDictEqual(roles_ref[0], self.role_member) + + self.assignment_api.delete_grant(group_id=new_group['id'], + domain_id=new_domain['id'], + role_id='member') + roles_ref = self.assignment_api.list_grants( + group_id=new_group['id'], + domain_id=new_domain['id']) + self.assertEqual(0, len(roles_ref)) + self.assertRaises(exception.RoleAssignmentNotFound, + self.assignment_api.delete_grant, + group_id=new_group['id'], + domain_id=new_domain['id'], + role_id='member') + + def test_get_and_remove_role_grant_by_user_and_domain(self): + new_domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(new_domain['id'], new_domain) + new_user = {'name': 'new_user', 'password': 'secret', + 'enabled': True, 'domain_id': new_domain['id']} + new_user = self.identity_api.create_user(new_user) + roles_ref = self.assignment_api.list_grants( + user_id=new_user['id'], + domain_id=new_domain['id']) + self.assertEqual(0, len(roles_ref)) + self.assignment_api.create_grant(user_id=new_user['id'], + domain_id=new_domain['id'], + role_id='member') + roles_ref = self.assignment_api.list_grants( + user_id=new_user['id'], + domain_id=new_domain['id']) + self.assertDictEqual(roles_ref[0], self.role_member) + + self.assignment_api.delete_grant(user_id=new_user['id'], + domain_id=new_domain['id'], + role_id='member') + roles_ref = self.assignment_api.list_grants( + user_id=new_user['id'], + domain_id=new_domain['id']) + self.assertEqual(0, len(roles_ref)) + self.assertRaises(exception.RoleAssignmentNotFound, + self.assignment_api.delete_grant, + user_id=new_user['id'], + domain_id=new_domain['id'], + role_id='member') + + def test_get_and_remove_role_grant_by_group_and_cross_domain(self): + group1_domain1_role = {'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex} + self.role_api.create_role(group1_domain1_role['id'], + group1_domain1_role) + group1_domain2_role = {'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex} + self.role_api.create_role(group1_domain2_role['id'], + group1_domain2_role) + domain1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(domain1['id'], domain1) + domain2 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(domain2['id'], domain2) + group1 = {'domain_id': domain1['id'], 'name': uuid.uuid4().hex} + group1 = self.identity_api.create_group(group1) + roles_ref = self.assignment_api.list_grants( + group_id=group1['id'], + domain_id=domain1['id']) + self.assertEqual(0, len(roles_ref)) + roles_ref = self.assignment_api.list_grants( + group_id=group1['id'], + domain_id=domain2['id']) + self.assertEqual(0, len(roles_ref)) + self.assignment_api.create_grant(group_id=group1['id'], + domain_id=domain1['id'], + role_id=group1_domain1_role['id']) + self.assignment_api.create_grant(group_id=group1['id'], + domain_id=domain2['id'], + role_id=group1_domain2_role['id']) + roles_ref = self.assignment_api.list_grants( + group_id=group1['id'], + domain_id=domain1['id']) + self.assertDictEqual(roles_ref[0], group1_domain1_role) + roles_ref = self.assignment_api.list_grants( + group_id=group1['id'], + domain_id=domain2['id']) + self.assertDictEqual(roles_ref[0], group1_domain2_role) + + self.assignment_api.delete_grant(group_id=group1['id'], + domain_id=domain2['id'], + role_id=group1_domain2_role['id']) + roles_ref = self.assignment_api.list_grants( + group_id=group1['id'], + domain_id=domain2['id']) + self.assertEqual(0, len(roles_ref)) + self.assertRaises(exception.RoleAssignmentNotFound, + self.assignment_api.delete_grant, + group_id=group1['id'], + domain_id=domain2['id'], + role_id=group1_domain2_role['id']) + + def test_get_and_remove_role_grant_by_user_and_cross_domain(self): + user1_domain1_role = {'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex} + self.role_api.create_role(user1_domain1_role['id'], user1_domain1_role) + user1_domain2_role = {'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex} + self.role_api.create_role(user1_domain2_role['id'], user1_domain2_role) + domain1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(domain1['id'], domain1) + domain2 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(domain2['id'], domain2) + user1 = {'name': uuid.uuid4().hex, 'domain_id': domain1['id'], + 'password': uuid.uuid4().hex, 'enabled': True} + user1 = self.identity_api.create_user(user1) + roles_ref = self.assignment_api.list_grants( + user_id=user1['id'], + domain_id=domain1['id']) + self.assertEqual(0, len(roles_ref)) + roles_ref = self.assignment_api.list_grants( + user_id=user1['id'], + domain_id=domain2['id']) + self.assertEqual(0, len(roles_ref)) + self.assignment_api.create_grant(user_id=user1['id'], + domain_id=domain1['id'], + role_id=user1_domain1_role['id']) + self.assignment_api.create_grant(user_id=user1['id'], + domain_id=domain2['id'], + role_id=user1_domain2_role['id']) + roles_ref = self.assignment_api.list_grants( + user_id=user1['id'], + domain_id=domain1['id']) + self.assertDictEqual(roles_ref[0], user1_domain1_role) + roles_ref = self.assignment_api.list_grants( + user_id=user1['id'], + domain_id=domain2['id']) + self.assertDictEqual(roles_ref[0], user1_domain2_role) + + self.assignment_api.delete_grant(user_id=user1['id'], + domain_id=domain2['id'], + role_id=user1_domain2_role['id']) + roles_ref = self.assignment_api.list_grants( + user_id=user1['id'], + domain_id=domain2['id']) + self.assertEqual(0, len(roles_ref)) + self.assertRaises(exception.RoleAssignmentNotFound, + self.assignment_api.delete_grant, + user_id=user1['id'], + domain_id=domain2['id'], + role_id=user1_domain2_role['id']) + + def test_role_grant_by_group_and_cross_domain_project(self): + role1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.role_api.create_role(role1['id'], role1) + role2 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.role_api.create_role(role2['id'], role2) + domain1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(domain1['id'], domain1) + domain2 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(domain2['id'], domain2) + group1 = {'name': uuid.uuid4().hex, 'domain_id': domain1['id'], + 'enabled': True} + group1 = self.identity_api.create_group(group1) + project1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'domain_id': domain2['id']} + self.resource_api.create_project(project1['id'], project1) + roles_ref = self.assignment_api.list_grants( + group_id=group1['id'], + project_id=project1['id']) + self.assertEqual(0, len(roles_ref)) + self.assignment_api.create_grant(group_id=group1['id'], + project_id=project1['id'], + role_id=role1['id']) + self.assignment_api.create_grant(group_id=group1['id'], + project_id=project1['id'], + role_id=role2['id']) + roles_ref = self.assignment_api.list_grants( + group_id=group1['id'], + project_id=project1['id']) + + roles_ref_ids = [] + for ref in roles_ref: + roles_ref_ids.append(ref['id']) + self.assertIn(role1['id'], roles_ref_ids) + self.assertIn(role2['id'], roles_ref_ids) + + self.assignment_api.delete_grant(group_id=group1['id'], + project_id=project1['id'], + role_id=role1['id']) + roles_ref = self.assignment_api.list_grants( + group_id=group1['id'], + project_id=project1['id']) + self.assertEqual(1, len(roles_ref)) + self.assertDictEqual(roles_ref[0], role2) + + def test_role_grant_by_user_and_cross_domain_project(self): + role1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.role_api.create_role(role1['id'], role1) + role2 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.role_api.create_role(role2['id'], role2) + domain1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(domain1['id'], domain1) + domain2 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(domain2['id'], domain2) + user1 = {'name': uuid.uuid4().hex, 'domain_id': domain1['id'], + 'password': uuid.uuid4().hex, 'enabled': True} + user1 = self.identity_api.create_user(user1) + project1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'domain_id': domain2['id']} + self.resource_api.create_project(project1['id'], project1) + roles_ref = self.assignment_api.list_grants( + user_id=user1['id'], + project_id=project1['id']) + self.assertEqual(0, len(roles_ref)) + self.assignment_api.create_grant(user_id=user1['id'], + project_id=project1['id'], + role_id=role1['id']) + self.assignment_api.create_grant(user_id=user1['id'], + project_id=project1['id'], + role_id=role2['id']) + roles_ref = self.assignment_api.list_grants( + user_id=user1['id'], + project_id=project1['id']) + + roles_ref_ids = [] + for ref in roles_ref: + roles_ref_ids.append(ref['id']) + self.assertIn(role1['id'], roles_ref_ids) + self.assertIn(role2['id'], roles_ref_ids) + + self.assignment_api.delete_grant(user_id=user1['id'], + project_id=project1['id'], + role_id=role1['id']) + roles_ref = self.assignment_api.list_grants( + user_id=user1['id'], + project_id=project1['id']) + self.assertEqual(1, len(roles_ref)) + self.assertDictEqual(roles_ref[0], role2) + + def test_delete_user_grant_no_user(self): + # Can delete a grant where the user doesn't exist. + role_id = uuid.uuid4().hex + role = {'id': role_id, 'name': uuid.uuid4().hex} + self.role_api.create_role(role_id, role) + + user_id = uuid.uuid4().hex + + self.assignment_api.create_grant(role_id, user_id=user_id, + project_id=self.tenant_bar['id']) + + self.assignment_api.delete_grant(role_id, user_id=user_id, + project_id=self.tenant_bar['id']) + + def test_delete_group_grant_no_group(self): + # Can delete a grant where the group doesn't exist. + role_id = uuid.uuid4().hex + role = {'id': role_id, 'name': uuid.uuid4().hex} + self.role_api.create_role(role_id, role) + + group_id = uuid.uuid4().hex + + self.assignment_api.create_grant(role_id, group_id=group_id, + project_id=self.tenant_bar['id']) + + self.assignment_api.delete_grant(role_id, group_id=group_id, + project_id=self.tenant_bar['id']) + + def test_grant_crud_throws_exception_if_invalid_role(self): + """Ensure RoleNotFound thrown if role does not exist.""" + + def assert_role_not_found_exception(f, **kwargs): + self.assertRaises(exception.RoleNotFound, f, + role_id=uuid.uuid4().hex, **kwargs) + + user = {'name': uuid.uuid4().hex, 'domain_id': DEFAULT_DOMAIN_ID, + 'password': uuid.uuid4().hex, 'enabled': True} + user_resp = self.identity_api.create_user(user) + group = {'name': uuid.uuid4().hex, 'domain_id': DEFAULT_DOMAIN_ID, + 'enabled': True} + group_resp = self.identity_api.create_group(group) + project = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'domain_id': DEFAULT_DOMAIN_ID} + project_resp = self.resource_api.create_project(project['id'], project) + + for manager_call in [self.assignment_api.create_grant, + self.assignment_api.get_grant, + self.assignment_api.delete_grant]: + assert_role_not_found_exception( + manager_call, + user_id=user_resp['id'], project_id=project_resp['id']) + assert_role_not_found_exception( + manager_call, + group_id=group_resp['id'], project_id=project_resp['id']) + assert_role_not_found_exception( + manager_call, + user_id=user_resp['id'], domain_id=DEFAULT_DOMAIN_ID) + assert_role_not_found_exception( + manager_call, + group_id=group_resp['id'], domain_id=DEFAULT_DOMAIN_ID) + + def test_multi_role_grant_by_user_group_on_project_domain(self): + role_list = [] + for _ in range(10): + role = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.role_api.create_role(role['id'], role) + role_list.append(role) + domain1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(domain1['id'], domain1) + user1 = {'name': uuid.uuid4().hex, 'domain_id': domain1['id'], + 'password': uuid.uuid4().hex, 'enabled': True} + user1 = self.identity_api.create_user(user1) + group1 = {'name': uuid.uuid4().hex, 'domain_id': domain1['id'], + 'enabled': True} + group1 = self.identity_api.create_group(group1) + group2 = {'name': uuid.uuid4().hex, 'domain_id': domain1['id'], + 'enabled': True} + group2 = self.identity_api.create_group(group2) + project1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'domain_id': domain1['id']} + self.resource_api.create_project(project1['id'], project1) + + self.identity_api.add_user_to_group(user1['id'], + group1['id']) + self.identity_api.add_user_to_group(user1['id'], + group2['id']) + + roles_ref = self.assignment_api.list_grants( + user_id=user1['id'], + project_id=project1['id']) + self.assertEqual(0, len(roles_ref)) + self.assignment_api.create_grant(user_id=user1['id'], + domain_id=domain1['id'], + role_id=role_list[0]['id']) + self.assignment_api.create_grant(user_id=user1['id'], + domain_id=domain1['id'], + role_id=role_list[1]['id']) + self.assignment_api.create_grant(group_id=group1['id'], + domain_id=domain1['id'], + role_id=role_list[2]['id']) + self.assignment_api.create_grant(group_id=group1['id'], + domain_id=domain1['id'], + role_id=role_list[3]['id']) + self.assignment_api.create_grant(user_id=user1['id'], + project_id=project1['id'], + role_id=role_list[4]['id']) + self.assignment_api.create_grant(user_id=user1['id'], + project_id=project1['id'], + role_id=role_list[5]['id']) + self.assignment_api.create_grant(group_id=group1['id'], + project_id=project1['id'], + role_id=role_list[6]['id']) + self.assignment_api.create_grant(group_id=group1['id'], + project_id=project1['id'], + role_id=role_list[7]['id']) + roles_ref = self.assignment_api.list_grants(user_id=user1['id'], + domain_id=domain1['id']) + self.assertEqual(2, len(roles_ref)) + self.assertIn(role_list[0], roles_ref) + self.assertIn(role_list[1], roles_ref) + roles_ref = self.assignment_api.list_grants(group_id=group1['id'], + domain_id=domain1['id']) + self.assertEqual(2, len(roles_ref)) + self.assertIn(role_list[2], roles_ref) + self.assertIn(role_list[3], roles_ref) + roles_ref = self.assignment_api.list_grants(user_id=user1['id'], + project_id=project1['id']) + self.assertEqual(2, len(roles_ref)) + self.assertIn(role_list[4], roles_ref) + self.assertIn(role_list[5], roles_ref) + roles_ref = self.assignment_api.list_grants(group_id=group1['id'], + project_id=project1['id']) + self.assertEqual(2, len(roles_ref)) + self.assertIn(role_list[6], roles_ref) + self.assertIn(role_list[7], roles_ref) + + # Now test the alternate way of getting back lists of grants, + # where user and group roles are combined. These should match + # the above results. + combined_list = self.assignment_api.get_roles_for_user_and_project( + user1['id'], project1['id']) + self.assertEqual(4, len(combined_list)) + self.assertIn(role_list[4]['id'], combined_list) + self.assertIn(role_list[5]['id'], combined_list) + self.assertIn(role_list[6]['id'], combined_list) + self.assertIn(role_list[7]['id'], combined_list) + + combined_role_list = self.assignment_api.get_roles_for_user_and_domain( + user1['id'], domain1['id']) + self.assertEqual(4, len(combined_role_list)) + self.assertIn(role_list[0]['id'], combined_role_list) + self.assertIn(role_list[1]['id'], combined_role_list) + self.assertIn(role_list[2]['id'], combined_role_list) + self.assertIn(role_list[3]['id'], combined_role_list) + + def test_multi_group_grants_on_project_domain(self): + """Test multiple group roles for user on project and domain. + + Test Plan: + + - Create 6 roles + - Create a domain, with a project, user and two groups + - Make the user a member of both groups + - Check no roles yet exit + - Assign a role to each user and both groups on both the + project and domain + - Get a list of effective roles for the user on both the + project and domain, checking we get back the correct three + roles + + """ + role_list = [] + for _ in range(6): + role = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.role_api.create_role(role['id'], role) + role_list.append(role) + domain1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(domain1['id'], domain1) + user1 = {'name': uuid.uuid4().hex, 'domain_id': domain1['id'], + 'password': uuid.uuid4().hex, 'enabled': True} + user1 = self.identity_api.create_user(user1) + group1 = {'name': uuid.uuid4().hex, 'domain_id': domain1['id'], + 'enabled': True} + group1 = self.identity_api.create_group(group1) + group2 = {'name': uuid.uuid4().hex, 'domain_id': domain1['id'], + 'enabled': True} + group2 = self.identity_api.create_group(group2) + project1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'domain_id': domain1['id']} + self.resource_api.create_project(project1['id'], project1) + + self.identity_api.add_user_to_group(user1['id'], + group1['id']) + self.identity_api.add_user_to_group(user1['id'], + group2['id']) + + roles_ref = self.assignment_api.list_grants( + user_id=user1['id'], + project_id=project1['id']) + self.assertEqual(0, len(roles_ref)) + self.assignment_api.create_grant(user_id=user1['id'], + domain_id=domain1['id'], + role_id=role_list[0]['id']) + self.assignment_api.create_grant(group_id=group1['id'], + domain_id=domain1['id'], + role_id=role_list[1]['id']) + self.assignment_api.create_grant(group_id=group2['id'], + domain_id=domain1['id'], + role_id=role_list[2]['id']) + self.assignment_api.create_grant(user_id=user1['id'], + project_id=project1['id'], + role_id=role_list[3]['id']) + self.assignment_api.create_grant(group_id=group1['id'], + project_id=project1['id'], + role_id=role_list[4]['id']) + self.assignment_api.create_grant(group_id=group2['id'], + project_id=project1['id'], + role_id=role_list[5]['id']) + + # Read by the roles, ensuring we get the correct 3 roles for + # both project and domain + combined_list = self.assignment_api.get_roles_for_user_and_project( + user1['id'], project1['id']) + self.assertEqual(3, len(combined_list)) + self.assertIn(role_list[3]['id'], combined_list) + self.assertIn(role_list[4]['id'], combined_list) + self.assertIn(role_list[5]['id'], combined_list) + + combined_role_list = self.assignment_api.get_roles_for_user_and_domain( + user1['id'], domain1['id']) + self.assertEqual(3, len(combined_role_list)) + self.assertIn(role_list[0]['id'], combined_role_list) + self.assertIn(role_list[1]['id'], combined_role_list) + self.assertIn(role_list[2]['id'], combined_role_list) + + def test_delete_role_with_user_and_group_grants(self): + role1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.role_api.create_role(role1['id'], role1) + domain1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(domain1['id'], domain1) + project1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'domain_id': domain1['id']} + self.resource_api.create_project(project1['id'], project1) + user1 = {'name': uuid.uuid4().hex, 'domain_id': domain1['id'], + 'password': uuid.uuid4().hex, 'enabled': True} + user1 = self.identity_api.create_user(user1) + group1 = {'name': uuid.uuid4().hex, 'domain_id': domain1['id'], + 'enabled': True} + group1 = self.identity_api.create_group(group1) + self.assignment_api.create_grant(user_id=user1['id'], + project_id=project1['id'], + role_id=role1['id']) + self.assignment_api.create_grant(user_id=user1['id'], + domain_id=domain1['id'], + role_id=role1['id']) + self.assignment_api.create_grant(group_id=group1['id'], + project_id=project1['id'], + role_id=role1['id']) + self.assignment_api.create_grant(group_id=group1['id'], + domain_id=domain1['id'], + role_id=role1['id']) + roles_ref = self.assignment_api.list_grants( + user_id=user1['id'], + project_id=project1['id']) + self.assertEqual(1, len(roles_ref)) + roles_ref = self.assignment_api.list_grants( + group_id=group1['id'], + project_id=project1['id']) + self.assertEqual(1, len(roles_ref)) + roles_ref = self.assignment_api.list_grants( + user_id=user1['id'], + domain_id=domain1['id']) + self.assertEqual(1, len(roles_ref)) + roles_ref = self.assignment_api.list_grants( + group_id=group1['id'], + domain_id=domain1['id']) + self.assertEqual(1, len(roles_ref)) + self.role_api.delete_role(role1['id']) + roles_ref = self.assignment_api.list_grants( + user_id=user1['id'], + project_id=project1['id']) + self.assertEqual(0, len(roles_ref)) + roles_ref = self.assignment_api.list_grants( + group_id=group1['id'], + project_id=project1['id']) + self.assertEqual(0, len(roles_ref)) + roles_ref = self.assignment_api.list_grants( + user_id=user1['id'], + domain_id=domain1['id']) + self.assertEqual(0, len(roles_ref)) + roles_ref = self.assignment_api.list_grants( + group_id=group1['id'], + domain_id=domain1['id']) + self.assertEqual(0, len(roles_ref)) + + def test_delete_user_with_group_project_domain_links(self): + role1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.role_api.create_role(role1['id'], role1) + domain1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(domain1['id'], domain1) + project1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'domain_id': domain1['id']} + self.resource_api.create_project(project1['id'], project1) + user1 = {'name': uuid.uuid4().hex, 'domain_id': domain1['id'], + 'password': uuid.uuid4().hex, 'enabled': True} + user1 = self.identity_api.create_user(user1) + group1 = {'name': uuid.uuid4().hex, 'domain_id': domain1['id'], + 'enabled': True} + group1 = self.identity_api.create_group(group1) + self.assignment_api.create_grant(user_id=user1['id'], + project_id=project1['id'], + role_id=role1['id']) + self.assignment_api.create_grant(user_id=user1['id'], + domain_id=domain1['id'], + role_id=role1['id']) + self.identity_api.add_user_to_group(user_id=user1['id'], + group_id=group1['id']) + roles_ref = self.assignment_api.list_grants( + user_id=user1['id'], + project_id=project1['id']) + self.assertEqual(1, len(roles_ref)) + roles_ref = self.assignment_api.list_grants( + user_id=user1['id'], + domain_id=domain1['id']) + self.assertEqual(1, len(roles_ref)) + self.identity_api.check_user_in_group( + user_id=user1['id'], + group_id=group1['id']) + self.identity_api.delete_user(user1['id']) + self.assertRaises(exception.NotFound, + self.identity_api.check_user_in_group, + user1['id'], + group1['id']) + + def test_delete_group_with_user_project_domain_links(self): + role1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.role_api.create_role(role1['id'], role1) + domain1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(domain1['id'], domain1) + project1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'domain_id': domain1['id']} + self.resource_api.create_project(project1['id'], project1) + user1 = {'name': uuid.uuid4().hex, 'domain_id': domain1['id'], + 'password': uuid.uuid4().hex, 'enabled': True} + user1 = self.identity_api.create_user(user1) + group1 = {'name': uuid.uuid4().hex, 'domain_id': domain1['id'], + 'enabled': True} + group1 = self.identity_api.create_group(group1) + + self.assignment_api.create_grant(group_id=group1['id'], + project_id=project1['id'], + role_id=role1['id']) + self.assignment_api.create_grant(group_id=group1['id'], + domain_id=domain1['id'], + role_id=role1['id']) + self.identity_api.add_user_to_group(user_id=user1['id'], + group_id=group1['id']) + roles_ref = self.assignment_api.list_grants( + group_id=group1['id'], + project_id=project1['id']) + self.assertEqual(1, len(roles_ref)) + roles_ref = self.assignment_api.list_grants( + group_id=group1['id'], + domain_id=domain1['id']) + self.assertEqual(1, len(roles_ref)) + self.identity_api.check_user_in_group( + user_id=user1['id'], + group_id=group1['id']) + self.identity_api.delete_group(group1['id']) + self.identity_api.get_user(user1['id']) + + def test_delete_domain_with_user_group_project_links(self): + # TODO(chungg):add test case once expected behaviour defined + pass + + def test_add_user_to_project(self): + self.assignment_api.add_user_to_project(self.tenant_baz['id'], + self.user_foo['id']) + tenants = self.assignment_api.list_projects_for_user( + self.user_foo['id']) + self.assertIn(self.tenant_baz, tenants) + + def test_add_user_to_project_missing_default_role(self): + self.role_api.delete_role(CONF.member_role_id) + self.assertRaises(exception.RoleNotFound, + self.role_api.get_role, + CONF.member_role_id) + self.assignment_api.add_user_to_project(self.tenant_baz['id'], + self.user_foo['id']) + tenants = ( + self.assignment_api.list_projects_for_user(self.user_foo['id'])) + self.assertIn(self.tenant_baz, tenants) + default_role = self.role_api.get_role(CONF.member_role_id) + self.assertIsNotNone(default_role) + + def test_add_user_to_project_404(self): + self.assertRaises(exception.ProjectNotFound, + self.assignment_api.add_user_to_project, + uuid.uuid4().hex, + self.user_foo['id']) + + def test_add_user_to_project_no_user(self): + # If add_user_to_project and the user doesn't exist, then + # no error. + user_id_not_exist = uuid.uuid4().hex + self.assignment_api.add_user_to_project(self.tenant_bar['id'], + user_id_not_exist) + + def test_remove_user_from_project(self): + self.assignment_api.add_user_to_project(self.tenant_baz['id'], + self.user_foo['id']) + self.assignment_api.remove_user_from_project(self.tenant_baz['id'], + self.user_foo['id']) + tenants = self.assignment_api.list_projects_for_user( + self.user_foo['id']) + self.assertNotIn(self.tenant_baz, tenants) + + def test_remove_user_from_project_race_delete_role(self): + self.assignment_api.add_user_to_project(self.tenant_baz['id'], + self.user_foo['id']) + self.assignment_api.add_role_to_user_and_project( + tenant_id=self.tenant_baz['id'], + user_id=self.user_foo['id'], + role_id=self.role_other['id']) + + # Mock a race condition, delete a role after + # get_roles_for_user_and_project() is called in + # remove_user_from_project(). + roles = self.assignment_api.get_roles_for_user_and_project( + self.user_foo['id'], self.tenant_baz['id']) + self.role_api.delete_role(self.role_other['id']) + self.assignment_api.get_roles_for_user_and_project = mock.Mock( + return_value=roles) + self.assignment_api.remove_user_from_project(self.tenant_baz['id'], + self.user_foo['id']) + tenants = self.assignment_api.list_projects_for_user( + self.user_foo['id']) + self.assertNotIn(self.tenant_baz, tenants) + + def test_remove_user_from_project_404(self): + self.assertRaises(exception.ProjectNotFound, + self.assignment_api.remove_user_from_project, + uuid.uuid4().hex, + self.user_foo['id']) + + self.assertRaises(exception.UserNotFound, + self.assignment_api.remove_user_from_project, + self.tenant_bar['id'], + uuid.uuid4().hex) + + self.assertRaises(exception.NotFound, + self.assignment_api.remove_user_from_project, + self.tenant_baz['id'], + self.user_foo['id']) + + def test_list_user_project_ids_404(self): + self.assertRaises(exception.UserNotFound, + self.assignment_api.list_projects_for_user, + uuid.uuid4().hex) + + def test_update_project_404(self): + self.assertRaises(exception.ProjectNotFound, + self.resource_api.update_project, + uuid.uuid4().hex, + dict()) + + def test_delete_project_404(self): + self.assertRaises(exception.ProjectNotFound, + self.resource_api.delete_project, + uuid.uuid4().hex) + + def test_update_user_404(self): + user_id = uuid.uuid4().hex + self.assertRaises(exception.UserNotFound, + self.identity_api.update_user, + user_id, + {'id': user_id, + 'domain_id': DEFAULT_DOMAIN_ID}) + + def test_delete_user_with_project_association(self): + user = {'name': uuid.uuid4().hex, + 'domain_id': DEFAULT_DOMAIN_ID, + 'password': uuid.uuid4().hex} + user = self.identity_api.create_user(user) + self.assignment_api.add_user_to_project(self.tenant_bar['id'], + user['id']) + self.identity_api.delete_user(user['id']) + self.assertRaises(exception.UserNotFound, + self.assignment_api.list_projects_for_user, + user['id']) + + def test_delete_user_with_project_roles(self): + user = {'name': uuid.uuid4().hex, + 'domain_id': DEFAULT_DOMAIN_ID, + 'password': uuid.uuid4().hex} + user = self.identity_api.create_user(user) + self.assignment_api.add_role_to_user_and_project( + user['id'], + self.tenant_bar['id'], + self.role_member['id']) + self.identity_api.delete_user(user['id']) + self.assertRaises(exception.UserNotFound, + self.assignment_api.list_projects_for_user, + user['id']) + + def test_delete_user_404(self): + self.assertRaises(exception.UserNotFound, + self.identity_api.delete_user, + uuid.uuid4().hex) + + def test_delete_role_404(self): + self.assertRaises(exception.RoleNotFound, + self.role_api.delete_role, + uuid.uuid4().hex) + + def test_create_update_delete_unicode_project(self): + unicode_project_name = u'name \u540d\u5b57' + project = {'id': uuid.uuid4().hex, + 'name': unicode_project_name, + 'description': uuid.uuid4().hex, + 'domain_id': CONF.identity.default_domain_id} + self.resource_api.create_project(project['id'], project) + self.resource_api.update_project(project['id'], project) + self.resource_api.delete_project(project['id']) + + def test_create_project_with_no_enabled_field(self): + ref = { + 'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex.lower(), + 'domain_id': DEFAULT_DOMAIN_ID} + self.resource_api.create_project(ref['id'], ref) + + project = self.resource_api.get_project(ref['id']) + self.assertIs(project['enabled'], True) + + def test_create_project_long_name_fails(self): + tenant = {'id': 'fake1', 'name': 'a' * 65, + 'domain_id': DEFAULT_DOMAIN_ID} + self.assertRaises(exception.ValidationError, + self.resource_api.create_project, + tenant['id'], + tenant) + + def test_create_project_blank_name_fails(self): + tenant = {'id': 'fake1', 'name': '', + 'domain_id': DEFAULT_DOMAIN_ID} + self.assertRaises(exception.ValidationError, + self.resource_api.create_project, + tenant['id'], + tenant) + + def test_create_project_invalid_name_fails(self): + tenant = {'id': 'fake1', 'name': None, + 'domain_id': DEFAULT_DOMAIN_ID} + self.assertRaises(exception.ValidationError, + self.resource_api.create_project, + tenant['id'], + tenant) + tenant = {'id': 'fake1', 'name': 123, + 'domain_id': DEFAULT_DOMAIN_ID} + self.assertRaises(exception.ValidationError, + self.resource_api.create_project, + tenant['id'], + tenant) + + def test_update_project_blank_name_fails(self): + tenant = {'id': 'fake1', 'name': 'fake1', + 'domain_id': DEFAULT_DOMAIN_ID} + self.resource_api.create_project('fake1', tenant) + tenant['name'] = '' + self.assertRaises(exception.ValidationError, + self.resource_api.update_project, + tenant['id'], + tenant) + + def test_update_project_long_name_fails(self): + tenant = {'id': 'fake1', 'name': 'fake1', + 'domain_id': DEFAULT_DOMAIN_ID} + self.resource_api.create_project('fake1', tenant) + tenant['name'] = 'a' * 65 + self.assertRaises(exception.ValidationError, + self.resource_api.update_project, + tenant['id'], + tenant) + + def test_update_project_invalid_name_fails(self): + tenant = {'id': 'fake1', 'name': 'fake1', + 'domain_id': DEFAULT_DOMAIN_ID} + self.resource_api.create_project('fake1', tenant) + tenant['name'] = None + self.assertRaises(exception.ValidationError, + self.resource_api.update_project, + tenant['id'], + tenant) + + tenant['name'] = 123 + self.assertRaises(exception.ValidationError, + self.resource_api.update_project, + tenant['id'], + tenant) + + def test_create_user_long_name_fails(self): + user = {'name': 'a' * 256, + 'domain_id': DEFAULT_DOMAIN_ID} + self.assertRaises(exception.ValidationError, + self.identity_api.create_user, + user) + + def test_create_user_blank_name_fails(self): + user = {'name': '', + 'domain_id': DEFAULT_DOMAIN_ID} + self.assertRaises(exception.ValidationError, + self.identity_api.create_user, + user) + + def test_create_user_missed_password(self): + user = {'name': 'fake1', + 'domain_id': DEFAULT_DOMAIN_ID} + user = self.identity_api.create_user(user) + self.identity_api.get_user(user['id']) + # Make sure the user is not allowed to login + # with a password that is empty string or None + self.assertRaises(AssertionError, + self.identity_api.authenticate, + context={}, + user_id=user['id'], + password='') + self.assertRaises(AssertionError, + self.identity_api.authenticate, + context={}, + user_id=user['id'], + password=None) + + def test_create_user_none_password(self): + user = {'name': 'fake1', 'password': None, + 'domain_id': DEFAULT_DOMAIN_ID} + user = self.identity_api.create_user(user) + self.identity_api.get_user(user['id']) + # Make sure the user is not allowed to login + # with a password that is empty string or None + self.assertRaises(AssertionError, + self.identity_api.authenticate, + context={}, + user_id=user['id'], + password='') + self.assertRaises(AssertionError, + self.identity_api.authenticate, + context={}, + user_id=user['id'], + password=None) + + def test_create_user_invalid_name_fails(self): + user = {'name': None, + 'domain_id': DEFAULT_DOMAIN_ID} + self.assertRaises(exception.ValidationError, + self.identity_api.create_user, + user) + + user = {'name': 123, + 'domain_id': DEFAULT_DOMAIN_ID} + self.assertRaises(exception.ValidationError, + self.identity_api.create_user, + user) + + def test_update_project_invalid_enabled_type_string(self): + project = {'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'enabled': True, + 'domain_id': DEFAULT_DOMAIN_ID} + self.resource_api.create_project(project['id'], project) + project_ref = self.resource_api.get_project(project['id']) + self.assertEqual(True, project_ref['enabled']) + + # Strings are not valid boolean values + project['enabled'] = "false" + self.assertRaises(exception.ValidationError, + self.resource_api.update_project, + project['id'], + project) + + def test_create_project_invalid_enabled_type_string(self): + project = {'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'domain_id': DEFAULT_DOMAIN_ID, + # invalid string value + 'enabled': "true"} + self.assertRaises(exception.ValidationError, + self.resource_api.create_project, + project['id'], + project) + + def test_create_user_invalid_enabled_type_string(self): + user = {'name': uuid.uuid4().hex, + 'domain_id': DEFAULT_DOMAIN_ID, + 'password': uuid.uuid4().hex, + # invalid string value + 'enabled': "true"} + self.assertRaises(exception.ValidationError, + self.identity_api.create_user, + user) + + def test_update_user_long_name_fails(self): + user = {'name': 'fake1', + 'domain_id': DEFAULT_DOMAIN_ID} + user = self.identity_api.create_user(user) + user['name'] = 'a' * 256 + self.assertRaises(exception.ValidationError, + self.identity_api.update_user, + user['id'], + user) + + def test_update_user_blank_name_fails(self): + user = {'name': 'fake1', + 'domain_id': DEFAULT_DOMAIN_ID} + user = self.identity_api.create_user(user) + user['name'] = '' + self.assertRaises(exception.ValidationError, + self.identity_api.update_user, + user['id'], + user) + + def test_update_user_invalid_name_fails(self): + user = {'name': 'fake1', + 'domain_id': DEFAULT_DOMAIN_ID} + user = self.identity_api.create_user(user) + + user['name'] = None + self.assertRaises(exception.ValidationError, + self.identity_api.update_user, + user['id'], + user) + + user['name'] = 123 + self.assertRaises(exception.ValidationError, + self.identity_api.update_user, + user['id'], + user) + + def test_list_users(self): + users = self.identity_api.list_users( + domain_scope=self._set_domain_scope(DEFAULT_DOMAIN_ID)) + self.assertEqual(len(default_fixtures.USERS), len(users)) + user_ids = set(user['id'] for user in users) + expected_user_ids = set(getattr(self, 'user_%s' % user['id'])['id'] + for user in default_fixtures.USERS) + for user_ref in users: + self.assertNotIn('password', user_ref) + self.assertEqual(expected_user_ids, user_ids) + + def test_list_groups(self): + group1 = { + 'domain_id': DEFAULT_DOMAIN_ID, + 'name': uuid.uuid4().hex} + group2 = { + 'domain_id': DEFAULT_DOMAIN_ID, + 'name': uuid.uuid4().hex} + group1 = self.identity_api.create_group(group1) + group2 = self.identity_api.create_group(group2) + groups = self.identity_api.list_groups( + domain_scope=self._set_domain_scope(DEFAULT_DOMAIN_ID)) + self.assertEqual(2, len(groups)) + group_ids = [] + for group in groups: + group_ids.append(group.get('id')) + self.assertIn(group1['id'], group_ids) + self.assertIn(group2['id'], group_ids) + + def test_list_domains(self): + domain1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + domain2 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(domain1['id'], domain1) + self.resource_api.create_domain(domain2['id'], domain2) + domains = self.resource_api.list_domains() + self.assertEqual(3, len(domains)) + domain_ids = [] + for domain in domains: + domain_ids.append(domain.get('id')) + self.assertIn(DEFAULT_DOMAIN_ID, domain_ids) + self.assertIn(domain1['id'], domain_ids) + self.assertIn(domain2['id'], domain_ids) + + def test_list_projects(self): + projects = self.resource_api.list_projects() + self.assertEqual(4, len(projects)) + project_ids = [] + for project in projects: + project_ids.append(project.get('id')) + self.assertIn(self.tenant_bar['id'], project_ids) + self.assertIn(self.tenant_baz['id'], project_ids) + + def test_list_projects_with_multiple_filters(self): + # Create a project + project = {'id': uuid.uuid4().hex, 'domain_id': DEFAULT_DOMAIN_ID, + 'name': uuid.uuid4().hex, 'description': uuid.uuid4().hex, + 'enabled': True, 'parent_id': None} + self.resource_api.create_project(project['id'], project) + + # Build driver hints with the project's name and inexistent description + hints = driver_hints.Hints() + hints.add_filter('name', project['name']) + hints.add_filter('description', uuid.uuid4().hex) + + # Retrieve projects based on hints and check an empty list is returned + projects = self.resource_api.list_projects(hints) + self.assertEqual([], projects) + + # Build correct driver hints + hints = driver_hints.Hints() + hints.add_filter('name', project['name']) + hints.add_filter('description', project['description']) + + # Retrieve projects based on hints + projects = self.resource_api.list_projects(hints) + + # Check that the returned list contains only the first project + self.assertEqual(1, len(projects)) + self.assertEqual(project, projects[0]) + + def test_list_projects_for_domain(self): + project_ids = ([x['id'] for x in + self.resource_api.list_projects_in_domain( + DEFAULT_DOMAIN_ID)]) + self.assertEqual(4, len(project_ids)) + self.assertIn(self.tenant_bar['id'], project_ids) + self.assertIn(self.tenant_baz['id'], project_ids) + self.assertIn(self.tenant_mtu['id'], project_ids) + self.assertIn(self.tenant_service['id'], project_ids) + + @tests.skip_if_no_multiple_domains_support + def test_list_projects_for_alternate_domain(self): + domain1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(domain1['id'], domain1) + project1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'domain_id': domain1['id']} + self.resource_api.create_project(project1['id'], project1) + project2 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'domain_id': domain1['id']} + self.resource_api.create_project(project2['id'], project2) + project_ids = ([x['id'] for x in + self.resource_api.list_projects_in_domain( + domain1['id'])]) + self.assertEqual(2, len(project_ids)) + self.assertIn(project1['id'], project_ids) + self.assertIn(project2['id'], project_ids) + + def _create_projects_hierarchy(self, hierarchy_size=2, + domain_id=DEFAULT_DOMAIN_ID): + """Creates a project hierarchy with specified size. + + :param hierarchy_size: the desired hierarchy size, default is 2 - + a project with one child. + :param domain_id: domain where the projects hierarchy will be created. + + :returns projects: a list of the projects in the created hierarchy. + + """ + project_id = uuid.uuid4().hex + project = {'id': project_id, + 'description': '', + 'domain_id': domain_id, + 'enabled': True, + 'name': uuid.uuid4().hex, + 'parent_id': None} + self.resource_api.create_project(project_id, project) + + projects = [project] + for i in range(1, hierarchy_size): + new_project = {'id': uuid.uuid4().hex, + 'description': '', + 'domain_id': domain_id, + 'enabled': True, + 'name': uuid.uuid4().hex, + 'parent_id': project_id} + self.resource_api.create_project(new_project['id'], new_project) + projects.append(new_project) + project_id = new_project['id'] + + return projects + + def test_check_leaf_projects(self): + projects_hierarchy = self._create_projects_hierarchy() + root_project = projects_hierarchy[0] + leaf_project = projects_hierarchy[1] + + self.assertFalse(self.resource_api.is_leaf_project( + root_project['id'])) + self.assertTrue(self.resource_api.is_leaf_project( + leaf_project['id'])) + + # Delete leaf_project + self.resource_api.delete_project(leaf_project['id']) + + # Now, root_project should be leaf + self.assertTrue(self.resource_api.is_leaf_project( + root_project['id'])) + + def test_list_projects_in_subtree(self): + projects_hierarchy = self._create_projects_hierarchy(hierarchy_size=3) + project1 = projects_hierarchy[0] + project2 = projects_hierarchy[1] + project3 = projects_hierarchy[2] + project4 = {'id': uuid.uuid4().hex, + 'description': '', + 'domain_id': DEFAULT_DOMAIN_ID, + 'enabled': True, + 'name': uuid.uuid4().hex, + 'parent_id': project2['id']} + self.resource_api.create_project(project4['id'], project4) + + subtree = self.resource_api.list_projects_in_subtree(project1['id']) + self.assertEqual(3, len(subtree)) + self.assertIn(project2, subtree) + self.assertIn(project3, subtree) + self.assertIn(project4, subtree) + + subtree = self.resource_api.list_projects_in_subtree(project2['id']) + self.assertEqual(2, len(subtree)) + self.assertIn(project3, subtree) + self.assertIn(project4, subtree) + + subtree = self.resource_api.list_projects_in_subtree(project3['id']) + self.assertEqual(0, len(subtree)) + + def test_list_project_parents(self): + projects_hierarchy = self._create_projects_hierarchy(hierarchy_size=3) + project1 = projects_hierarchy[0] + project2 = projects_hierarchy[1] + project3 = projects_hierarchy[2] + project4 = {'id': uuid.uuid4().hex, + 'description': '', + 'domain_id': DEFAULT_DOMAIN_ID, + 'enabled': True, + 'name': uuid.uuid4().hex, + 'parent_id': project2['id']} + self.resource_api.create_project(project4['id'], project4) + + parents1 = self.resource_api.list_project_parents(project3['id']) + self.assertEqual(2, len(parents1)) + self.assertIn(project1, parents1) + self.assertIn(project2, parents1) + + parents2 = self.resource_api.list_project_parents(project4['id']) + self.assertEqual(parents1, parents2) + + parents = self.resource_api.list_project_parents(project1['id']) + self.assertEqual(0, len(parents)) + + def test_delete_project_with_role_assignments(self): + tenant = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'domain_id': DEFAULT_DOMAIN_ID} + self.resource_api.create_project(tenant['id'], tenant) + self.assignment_api.add_role_to_user_and_project( + self.user_foo['id'], tenant['id'], 'member') + self.resource_api.delete_project(tenant['id']) + self.assertRaises(exception.NotFound, + self.resource_api.get_project, + tenant['id']) + + def test_delete_role_check_role_grant(self): + role = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + alt_role = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.role_api.create_role(role['id'], role) + self.role_api.create_role(alt_role['id'], alt_role) + self.assignment_api.add_role_to_user_and_project( + self.user_foo['id'], self.tenant_bar['id'], role['id']) + self.assignment_api.add_role_to_user_and_project( + self.user_foo['id'], self.tenant_bar['id'], alt_role['id']) + self.role_api.delete_role(role['id']) + roles_ref = self.assignment_api.get_roles_for_user_and_project( + self.user_foo['id'], self.tenant_bar['id']) + self.assertNotIn(role['id'], roles_ref) + self.assertIn(alt_role['id'], roles_ref) + + def test_create_project_doesnt_modify_passed_in_dict(self): + new_project = {'id': 'tenant_id', 'name': uuid.uuid4().hex, + 'domain_id': DEFAULT_DOMAIN_ID} + original_project = new_project.copy() + self.resource_api.create_project('tenant_id', new_project) + self.assertDictEqual(original_project, new_project) + + def test_create_user_doesnt_modify_passed_in_dict(self): + new_user = {'name': uuid.uuid4().hex, 'password': uuid.uuid4().hex, + 'domain_id': DEFAULT_DOMAIN_ID} + original_user = new_user.copy() + self.identity_api.create_user(new_user) + self.assertDictEqual(original_user, new_user) + + def test_update_user_enable(self): + user = {'name': 'fake1', 'enabled': True, + 'domain_id': DEFAULT_DOMAIN_ID} + user = self.identity_api.create_user(user) + user_ref = self.identity_api.get_user(user['id']) + self.assertEqual(True, user_ref['enabled']) + + user['enabled'] = False + self.identity_api.update_user(user['id'], user) + user_ref = self.identity_api.get_user(user['id']) + self.assertEqual(user['enabled'], user_ref['enabled']) + + # If not present, enabled field should not be updated + del user['enabled'] + self.identity_api.update_user(user['id'], user) + user_ref = self.identity_api.get_user(user['id']) + self.assertEqual(False, user_ref['enabled']) + + user['enabled'] = True + self.identity_api.update_user(user['id'], user) + user_ref = self.identity_api.get_user(user['id']) + self.assertEqual(user['enabled'], user_ref['enabled']) + + del user['enabled'] + self.identity_api.update_user(user['id'], user) + user_ref = self.identity_api.get_user(user['id']) + self.assertEqual(True, user_ref['enabled']) + + # Integers are valid Python's booleans. Explicitly test it. + user['enabled'] = 0 + self.identity_api.update_user(user['id'], user) + user_ref = self.identity_api.get_user(user['id']) + self.assertEqual(False, user_ref['enabled']) + + # Any integers other than 0 are interpreted as True + user['enabled'] = -42 + self.identity_api.update_user(user['id'], user) + user_ref = self.identity_api.get_user(user['id']) + self.assertEqual(True, user_ref['enabled']) + + def test_update_user_name(self): + user = {'name': uuid.uuid4().hex, + 'enabled': True, + 'domain_id': DEFAULT_DOMAIN_ID} + user = self.identity_api.create_user(user) + user_ref = self.identity_api.get_user(user['id']) + self.assertEqual(user['name'], user_ref['name']) + + changed_name = user_ref['name'] + '_changed' + user_ref['name'] = changed_name + updated_user = self.identity_api.update_user(user_ref['id'], user_ref) + + # NOTE(dstanek): the SQL backend adds an 'extra' field containing a + # dictionary of the extra fields in addition to the + # fields in the object. For the details see: + # SqlIdentity.test_update_project_returns_extra + updated_user.pop('extra', None) + + self.assertDictEqual(user_ref, updated_user) + + user_ref = self.identity_api.get_user(user_ref['id']) + self.assertEqual(changed_name, user_ref['name']) + + def test_update_user_enable_fails(self): + user = {'name': 'fake1', 'enabled': True, + 'domain_id': DEFAULT_DOMAIN_ID} + user = self.identity_api.create_user(user) + user_ref = self.identity_api.get_user(user['id']) + self.assertEqual(True, user_ref['enabled']) + + # Strings are not valid boolean values + user['enabled'] = "false" + self.assertRaises(exception.ValidationError, + self.identity_api.update_user, + user['id'], + user) + + def test_update_project_enable(self): + tenant = {'id': 'fake1', 'name': 'fake1', 'enabled': True, + 'domain_id': DEFAULT_DOMAIN_ID} + self.resource_api.create_project('fake1', tenant) + tenant_ref = self.resource_api.get_project('fake1') + self.assertEqual(True, tenant_ref['enabled']) + + tenant['enabled'] = False + self.resource_api.update_project('fake1', tenant) + tenant_ref = self.resource_api.get_project('fake1') + self.assertEqual(tenant['enabled'], tenant_ref['enabled']) + + # If not present, enabled field should not be updated + del tenant['enabled'] + self.resource_api.update_project('fake1', tenant) + tenant_ref = self.resource_api.get_project('fake1') + self.assertEqual(False, tenant_ref['enabled']) + + tenant['enabled'] = True + self.resource_api.update_project('fake1', tenant) + tenant_ref = self.resource_api.get_project('fake1') + self.assertEqual(tenant['enabled'], tenant_ref['enabled']) + + del tenant['enabled'] + self.resource_api.update_project('fake1', tenant) + tenant_ref = self.resource_api.get_project('fake1') + self.assertEqual(True, tenant_ref['enabled']) + + def test_add_user_to_group(self): + domain = self._get_domain_fixture() + new_group = {'domain_id': domain['id'], 'name': uuid.uuid4().hex} + new_group = self.identity_api.create_group(new_group) + new_user = {'name': 'new_user', 'password': uuid.uuid4().hex, + 'enabled': True, 'domain_id': domain['id']} + new_user = self.identity_api.create_user(new_user) + self.identity_api.add_user_to_group(new_user['id'], + new_group['id']) + groups = self.identity_api.list_groups_for_user(new_user['id']) + + found = False + for x in groups: + if (x['id'] == new_group['id']): + found = True + self.assertTrue(found) + + def test_add_user_to_group_404(self): + domain = self._get_domain_fixture() + new_user = {'name': 'new_user', 'password': uuid.uuid4().hex, + 'enabled': True, 'domain_id': domain['id']} + new_user = self.identity_api.create_user(new_user) + self.assertRaises(exception.GroupNotFound, + self.identity_api.add_user_to_group, + new_user['id'], + uuid.uuid4().hex) + + new_group = {'domain_id': domain['id'], 'name': uuid.uuid4().hex} + new_group = self.identity_api.create_group(new_group) + self.assertRaises(exception.UserNotFound, + self.identity_api.add_user_to_group, + uuid.uuid4().hex, + new_group['id']) + + self.assertRaises(exception.NotFound, + self.identity_api.add_user_to_group, + uuid.uuid4().hex, + uuid.uuid4().hex) + + def test_check_user_in_group(self): + domain = self._get_domain_fixture() + new_group = {'domain_id': domain['id'], 'name': uuid.uuid4().hex} + new_group = self.identity_api.create_group(new_group) + new_user = {'name': 'new_user', 'password': uuid.uuid4().hex, + 'enabled': True, 'domain_id': domain['id']} + new_user = self.identity_api.create_user(new_user) + self.identity_api.add_user_to_group(new_user['id'], + new_group['id']) + self.identity_api.check_user_in_group(new_user['id'], new_group['id']) + + def test_create_invalid_domain_fails(self): + new_group = {'domain_id': "doesnotexist", 'name': uuid.uuid4().hex} + self.assertRaises(exception.DomainNotFound, + self.identity_api.create_group, + new_group) + new_user = {'name': 'new_user', 'password': uuid.uuid4().hex, + 'enabled': True, 'domain_id': "doesnotexist"} + self.assertRaises(exception.DomainNotFound, + self.identity_api.create_user, + new_user) + + def test_check_user_not_in_group(self): + new_group = { + 'domain_id': DEFAULT_DOMAIN_ID, + 'name': uuid.uuid4().hex} + new_group = self.identity_api.create_group(new_group) + + new_user = {'name': 'new_user', 'password': uuid.uuid4().hex, + 'enabled': True, 'domain_id': DEFAULT_DOMAIN_ID} + new_user = self.identity_api.create_user(new_user) + + self.assertRaises(exception.NotFound, + self.identity_api.check_user_in_group, + new_user['id'], + new_group['id']) + + def test_check_user_in_group_404(self): + new_user = {'name': 'new_user', 'password': uuid.uuid4().hex, + 'enabled': True, 'domain_id': DEFAULT_DOMAIN_ID} + new_user = self.identity_api.create_user(new_user) + + new_group = { + 'domain_id': DEFAULT_DOMAIN_ID, + 'name': uuid.uuid4().hex} + new_group = self.identity_api.create_group(new_group) + + self.assertRaises(exception.UserNotFound, + self.identity_api.check_user_in_group, + uuid.uuid4().hex, + new_group['id']) + + self.assertRaises(exception.GroupNotFound, + self.identity_api.check_user_in_group, + new_user['id'], + uuid.uuid4().hex) + + self.assertRaises(exception.NotFound, + self.identity_api.check_user_in_group, + uuid.uuid4().hex, + uuid.uuid4().hex) + + def test_list_users_in_group(self): + domain = self._get_domain_fixture() + new_group = {'domain_id': domain['id'], 'name': uuid.uuid4().hex} + new_group = self.identity_api.create_group(new_group) + # Make sure we get an empty list back on a new group, not an error. + user_refs = self.identity_api.list_users_in_group(new_group['id']) + self.assertEqual([], user_refs) + # Make sure we get the correct users back once they have been added + # to the group. + new_user = {'name': 'new_user', 'password': uuid.uuid4().hex, + 'enabled': True, 'domain_id': domain['id']} + new_user = self.identity_api.create_user(new_user) + self.identity_api.add_user_to_group(new_user['id'], + new_group['id']) + user_refs = self.identity_api.list_users_in_group(new_group['id']) + found = False + for x in user_refs: + if (x['id'] == new_user['id']): + found = True + self.assertNotIn('password', x) + self.assertTrue(found) + + def test_list_users_in_group_404(self): + self.assertRaises(exception.GroupNotFound, + self.identity_api.list_users_in_group, + uuid.uuid4().hex) + + def test_list_groups_for_user(self): + domain = self._get_domain_fixture() + test_groups = [] + test_users = [] + GROUP_COUNT = 3 + USER_COUNT = 2 + + for x in range(0, USER_COUNT): + new_user = {'name': uuid.uuid4().hex, 'password': uuid.uuid4().hex, + 'enabled': True, 'domain_id': domain['id']} + new_user = self.identity_api.create_user(new_user) + test_users.append(new_user) + positive_user = test_users[0] + negative_user = test_users[1] + + for x in range(0, USER_COUNT): + group_refs = self.identity_api.list_groups_for_user( + test_users[x]['id']) + self.assertEqual(0, len(group_refs)) + + for x in range(0, GROUP_COUNT): + before_count = x + after_count = x + 1 + new_group = {'domain_id': domain['id'], + 'name': uuid.uuid4().hex} + new_group = self.identity_api.create_group(new_group) + test_groups.append(new_group) + + # add the user to the group and ensure that the + # group count increases by one for each + group_refs = self.identity_api.list_groups_for_user( + positive_user['id']) + self.assertEqual(before_count, len(group_refs)) + self.identity_api.add_user_to_group( + positive_user['id'], + new_group['id']) + group_refs = self.identity_api.list_groups_for_user( + positive_user['id']) + self.assertEqual(after_count, len(group_refs)) + + # Make sure the group count for the unrelated user did not change + group_refs = self.identity_api.list_groups_for_user( + negative_user['id']) + self.assertEqual(0, len(group_refs)) + + # remove the user from each group and ensure that + # the group count reduces by one for each + for x in range(0, 3): + before_count = GROUP_COUNT - x + after_count = GROUP_COUNT - x - 1 + group_refs = self.identity_api.list_groups_for_user( + positive_user['id']) + self.assertEqual(before_count, len(group_refs)) + self.identity_api.remove_user_from_group( + positive_user['id'], + test_groups[x]['id']) + group_refs = self.identity_api.list_groups_for_user( + positive_user['id']) + self.assertEqual(after_count, len(group_refs)) + # Make sure the group count for the unrelated user + # did not change + group_refs = self.identity_api.list_groups_for_user( + negative_user['id']) + self.assertEqual(0, len(group_refs)) + + def test_remove_user_from_group(self): + domain = self._get_domain_fixture() + new_group = {'domain_id': domain['id'], 'name': uuid.uuid4().hex} + new_group = self.identity_api.create_group(new_group) + new_user = {'name': 'new_user', 'password': uuid.uuid4().hex, + 'enabled': True, 'domain_id': domain['id']} + new_user = self.identity_api.create_user(new_user) + self.identity_api.add_user_to_group(new_user['id'], + new_group['id']) + groups = self.identity_api.list_groups_for_user(new_user['id']) + self.assertIn(new_group['id'], [x['id'] for x in groups]) + self.identity_api.remove_user_from_group(new_user['id'], + new_group['id']) + groups = self.identity_api.list_groups_for_user(new_user['id']) + self.assertNotIn(new_group['id'], [x['id'] for x in groups]) + + def test_remove_user_from_group_404(self): + domain = self._get_domain_fixture() + new_user = {'name': 'new_user', 'password': uuid.uuid4().hex, + 'enabled': True, 'domain_id': domain['id']} + new_user = self.identity_api.create_user(new_user) + new_group = {'domain_id': domain['id'], 'name': uuid.uuid4().hex} + new_group = self.identity_api.create_group(new_group) + self.assertRaises(exception.GroupNotFound, + self.identity_api.remove_user_from_group, + new_user['id'], + uuid.uuid4().hex) + + self.assertRaises(exception.UserNotFound, + self.identity_api.remove_user_from_group, + uuid.uuid4().hex, + new_group['id']) + + self.assertRaises(exception.NotFound, + self.identity_api.remove_user_from_group, + uuid.uuid4().hex, + uuid.uuid4().hex) + + def test_group_crud(self): + domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(domain['id'], domain) + group = {'domain_id': domain['id'], 'name': uuid.uuid4().hex} + group = self.identity_api.create_group(group) + group_ref = self.identity_api.get_group(group['id']) + self.assertDictContainsSubset(group, group_ref) + + group['name'] = uuid.uuid4().hex + self.identity_api.update_group(group['id'], group) + group_ref = self.identity_api.get_group(group['id']) + self.assertDictContainsSubset(group, group_ref) + + self.identity_api.delete_group(group['id']) + self.assertRaises(exception.GroupNotFound, + self.identity_api.get_group, + group['id']) + + def test_get_group_by_name(self): + group_name = uuid.uuid4().hex + group = {'domain_id': DEFAULT_DOMAIN_ID, 'name': group_name} + group = self.identity_api.create_group(group) + spoiler = {'domain_id': DEFAULT_DOMAIN_ID, 'name': uuid.uuid4().hex} + self.identity_api.create_group(spoiler) + + group_ref = self.identity_api.get_group_by_name( + group_name, DEFAULT_DOMAIN_ID) + self.assertDictEqual(group_ref, group) + + def test_get_group_by_name_404(self): + self.assertRaises(exception.GroupNotFound, + self.identity_api.get_group_by_name, + uuid.uuid4().hex, + DEFAULT_DOMAIN_ID) + + @tests.skip_if_cache_disabled('identity') + def test_cache_layer_group_crud(self): + group = {'domain_id': DEFAULT_DOMAIN_ID, 'name': uuid.uuid4().hex} + group = self.identity_api.create_group(group) + # cache the result + group_ref = self.identity_api.get_group(group['id']) + # delete the group bypassing identity api. + domain_id, driver, entity_id = ( + self.identity_api._get_domain_driver_and_entity_id(group['id'])) + driver.delete_group(entity_id) + + self.assertEqual(group_ref, self.identity_api.get_group(group['id'])) + self.identity_api.get_group.invalidate(self.identity_api, group['id']) + self.assertRaises(exception.GroupNotFound, + self.identity_api.get_group, group['id']) + + group = {'domain_id': DEFAULT_DOMAIN_ID, 'name': uuid.uuid4().hex} + group = self.identity_api.create_group(group) + # cache the result + self.identity_api.get_group(group['id']) + group['name'] = uuid.uuid4().hex + group_ref = self.identity_api.update_group(group['id'], group) + # after updating through identity api, get updated group + self.assertDictContainsSubset(self.identity_api.get_group(group['id']), + group_ref) + + def test_create_duplicate_group_name_fails(self): + group1 = {'domain_id': DEFAULT_DOMAIN_ID, 'name': uuid.uuid4().hex} + group2 = {'domain_id': DEFAULT_DOMAIN_ID, 'name': group1['name']} + group1 = self.identity_api.create_group(group1) + self.assertRaises(exception.Conflict, + self.identity_api.create_group, + group2) + + def test_create_duplicate_group_name_in_different_domains(self): + new_domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(new_domain['id'], new_domain) + group1 = {'domain_id': DEFAULT_DOMAIN_ID, 'name': uuid.uuid4().hex} + group2 = {'domain_id': new_domain['id'], 'name': group1['name']} + group1 = self.identity_api.create_group(group1) + group2 = self.identity_api.create_group(group2) + + def test_move_group_between_domains(self): + domain1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(domain1['id'], domain1) + domain2 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(domain2['id'], domain2) + group = {'name': uuid.uuid4().hex, + 'domain_id': domain1['id']} + group = self.identity_api.create_group(group) + group['domain_id'] = domain2['id'] + self.identity_api.update_group(group['id'], group) + + def test_move_group_between_domains_with_clashing_names_fails(self): + domain1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(domain1['id'], domain1) + domain2 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(domain2['id'], domain2) + # First, create a group in domain1 + group1 = {'name': uuid.uuid4().hex, + 'domain_id': domain1['id']} + group1 = self.identity_api.create_group(group1) + # Now create a group in domain2 with a potentially clashing + # name - which should work since we have domain separation + group2 = {'name': group1['name'], + 'domain_id': domain2['id']} + group2 = self.identity_api.create_group(group2) + # Now try and move group1 into the 2nd domain - which should + # fail since the names clash + group1['domain_id'] = domain2['id'] + self.assertRaises(exception.Conflict, + self.identity_api.update_group, + group1['id'], + group1) + + @tests.skip_if_no_multiple_domains_support + def test_project_crud(self): + domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'enabled': True} + self.resource_api.create_domain(domain['id'], domain) + project = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'domain_id': domain['id']} + self.resource_api.create_project(project['id'], project) + project_ref = self.resource_api.get_project(project['id']) + self.assertDictContainsSubset(project, project_ref) + + project['name'] = uuid.uuid4().hex + self.resource_api.update_project(project['id'], project) + project_ref = self.resource_api.get_project(project['id']) + self.assertDictContainsSubset(project, project_ref) + + self.resource_api.delete_project(project['id']) + self.assertRaises(exception.ProjectNotFound, + self.resource_api.get_project, + project['id']) + + def test_domain_delete_hierarchy(self): + domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'enabled': True} + self.resource_api.create_domain(domain['id'], domain) + + # Creating a root and a leaf project inside the domain + projects_hierarchy = self._create_projects_hierarchy( + domain_id=domain['id']) + root_project = projects_hierarchy[0] + leaf_project = projects_hierarchy[0] + + # Disable the domain + domain['enabled'] = False + self.resource_api.update_domain(domain['id'], domain) + + # Delete the domain + self.resource_api.delete_domain(domain['id']) + + # Make sure the domain no longer exists + self.assertRaises(exception.DomainNotFound, + self.resource_api.get_domain, + domain['id']) + + # Make sure the root project no longer exists + self.assertRaises(exception.ProjectNotFound, + self.resource_api.get_project, + root_project['id']) + + # Make sure the leaf project no longer exists + self.assertRaises(exception.ProjectNotFound, + self.resource_api.get_project, + leaf_project['id']) + + def test_hierarchical_projects_crud(self): + # create a hierarchy with just a root project (which is a leaf as well) + projects_hierarchy = self._create_projects_hierarchy(hierarchy_size=1) + root_project1 = projects_hierarchy[0] + + # create a hierarchy with one root project and one leaf project + projects_hierarchy = self._create_projects_hierarchy() + root_project2 = projects_hierarchy[0] + leaf_project = projects_hierarchy[1] + + # update description from leaf_project + leaf_project['description'] = 'new description' + self.resource_api.update_project(leaf_project['id'], leaf_project) + proj_ref = self.resource_api.get_project(leaf_project['id']) + self.assertDictEqual(proj_ref, leaf_project) + + # update the parent_id is not allowed + leaf_project['parent_id'] = root_project1['id'] + self.assertRaises(exception.ForbiddenAction, + self.resource_api.update_project, + leaf_project['id'], + leaf_project) + + # delete root_project1 + self.resource_api.delete_project(root_project1['id']) + self.assertRaises(exception.ProjectNotFound, + self.resource_api.get_project, + root_project1['id']) + + # delete root_project2 is not allowed since it is not a leaf project + self.assertRaises(exception.ForbiddenAction, + self.resource_api.delete_project, + root_project2['id']) + + def test_create_project_with_invalid_parent(self): + project = {'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'description': '', + 'domain_id': DEFAULT_DOMAIN_ID, + 'enabled': True, + 'parent_id': 'fake'} + self.assertRaises(exception.ProjectNotFound, + self.resource_api.create_project, + project['id'], + project) + + def test_create_leaf_project_with_invalid_domain(self): + root_project = {'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'description': '', + 'domain_id': DEFAULT_DOMAIN_ID, + 'enabled': True, + 'parent_id': None} + self.resource_api.create_project(root_project['id'], root_project) + + leaf_project = {'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'description': '', + 'domain_id': 'fake', + 'enabled': True, + 'parent_id': root_project['id']} + + self.assertRaises(exception.ForbiddenAction, + self.resource_api.create_project, + leaf_project['id'], + leaf_project) + + def test_delete_hierarchical_leaf_project(self): + projects_hierarchy = self._create_projects_hierarchy() + root_project = projects_hierarchy[0] + leaf_project = projects_hierarchy[1] + + self.resource_api.delete_project(leaf_project['id']) + self.assertRaises(exception.ProjectNotFound, + self.resource_api.get_project, + leaf_project['id']) + + self.resource_api.delete_project(root_project['id']) + self.assertRaises(exception.ProjectNotFound, + self.resource_api.get_project, + root_project['id']) + + def test_delete_hierarchical_not_leaf_project(self): + projects_hierarchy = self._create_projects_hierarchy() + root_project = projects_hierarchy[0] + + self.assertRaises(exception.ForbiddenAction, + self.resource_api.delete_project, + root_project['id']) + + def test_update_project_parent(self): + projects_hierarchy = self._create_projects_hierarchy(hierarchy_size=3) + project1 = projects_hierarchy[0] + project2 = projects_hierarchy[1] + project3 = projects_hierarchy[2] + + # project2 is the parent from project3 + self.assertEqual(project3.get('parent_id'), project2['id']) + + # try to update project3 parent to parent1 + project3['parent_id'] = project1['id'] + self.assertRaises(exception.ForbiddenAction, + self.resource_api.update_project, + project3['id'], + project3) + + def test_create_project_under_disabled_one(self): + project1 = {'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'domain_id': DEFAULT_DOMAIN_ID, + 'enabled': False, + 'parent_id': None} + self.resource_api.create_project(project1['id'], project1) + + project2 = {'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'domain_id': DEFAULT_DOMAIN_ID, + 'parent_id': project1['id']} + + # It's not possible to create a project under a disabled one in the + # hierarchy + self.assertRaises(exception.ForbiddenAction, + self.resource_api.create_project, + project2['id'], + project2) + + def test_disable_hierarchical_leaf_project(self): + projects_hierarchy = self._create_projects_hierarchy() + leaf_project = projects_hierarchy[1] + + leaf_project['enabled'] = False + self.resource_api.update_project(leaf_project['id'], leaf_project) + + project_ref = self.resource_api.get_project(leaf_project['id']) + self.assertEqual(project_ref['enabled'], leaf_project['enabled']) + + def test_disable_hierarchical_not_leaf_project(self): + projects_hierarchy = self._create_projects_hierarchy() + root_project = projects_hierarchy[0] + + root_project['enabled'] = False + self.assertRaises(exception.ForbiddenAction, + self.resource_api.update_project, + root_project['id'], + root_project) + + def test_enable_project_with_disabled_parent(self): + projects_hierarchy = self._create_projects_hierarchy() + root_project = projects_hierarchy[0] + leaf_project = projects_hierarchy[1] + + # Disable leaf and root + leaf_project['enabled'] = False + self.resource_api.update_project(leaf_project['id'], leaf_project) + root_project['enabled'] = False + self.resource_api.update_project(root_project['id'], root_project) + + # Try to enable the leaf project, it's not possible since it has + # a disabled parent + leaf_project['enabled'] = True + self.assertRaises(exception.ForbiddenAction, + self.resource_api.update_project, + leaf_project['id'], + leaf_project) + + def _get_hierarchy_depth(self, project_id): + return len(self.resource_api.list_project_parents(project_id)) + 1 + + def test_check_hierarchy_depth(self): + # First create a hierarchy with the max allowed depth + projects_hierarchy = self._create_projects_hierarchy( + CONF.max_project_tree_depth) + leaf_project = projects_hierarchy[CONF.max_project_tree_depth - 1] + + depth = self._get_hierarchy_depth(leaf_project['id']) + self.assertEqual(CONF.max_project_tree_depth, depth) + + # Creating another project in the hierarchy shouldn't be allowed + project_id = uuid.uuid4().hex + project = { + 'id': project_id, + 'name': uuid.uuid4().hex, + 'domain_id': DEFAULT_DOMAIN_ID, + 'parent_id': leaf_project['id']} + self.assertRaises(exception.ForbiddenAction, + self.resource_api.create_project, + project_id, + project) + + def test_project_update_missing_attrs_with_a_value(self): + # Creating a project with no description attribute. + project = {'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'domain_id': DEFAULT_DOMAIN_ID, + 'enabled': True, + 'parent_id': None} + self.resource_api.create_project(project['id'], project) + + # Add a description attribute. + project['description'] = uuid.uuid4().hex + self.resource_api.update_project(project['id'], project) + + project_ref = self.resource_api.get_project(project['id']) + self.assertDictEqual(project_ref, project) + + def test_project_update_missing_attrs_with_a_falsey_value(self): + # Creating a project with no description attribute. + project = {'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'domain_id': DEFAULT_DOMAIN_ID, + 'enabled': True, + 'parent_id': None} + self.resource_api.create_project(project['id'], project) + + # Add a description attribute. + project['description'] = '' + self.resource_api.update_project(project['id'], project) + + project_ref = self.resource_api.get_project(project['id']) + self.assertDictEqual(project_ref, project) + + def test_domain_crud(self): + domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'enabled': True} + self.resource_api.create_domain(domain['id'], domain) + domain_ref = self.resource_api.get_domain(domain['id']) + self.assertDictEqual(domain_ref, domain) + + domain['name'] = uuid.uuid4().hex + self.resource_api.update_domain(domain['id'], domain) + domain_ref = self.resource_api.get_domain(domain['id']) + self.assertDictEqual(domain_ref, domain) + + # Ensure an 'enabled' domain cannot be deleted + self.assertRaises(exception.ForbiddenAction, + self.resource_api.delete_domain, + domain_id=domain['id']) + + # Disable the domain + domain['enabled'] = False + self.resource_api.update_domain(domain['id'], domain) + + # Delete the domain + self.resource_api.delete_domain(domain['id']) + + # Make sure the domain no longer exists + self.assertRaises(exception.DomainNotFound, + self.resource_api.get_domain, + domain['id']) + + @tests.skip_if_no_multiple_domains_support + def test_create_domain_case_sensitivity(self): + # create a ref with a lowercase name + ref = { + 'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex.lower()} + self.resource_api.create_domain(ref['id'], ref) + + # assign a new ID with the same name, but this time in uppercase + ref['id'] = uuid.uuid4().hex + ref['name'] = ref['name'].upper() + self.resource_api.create_domain(ref['id'], ref) + + def test_attribute_update(self): + project = { + 'domain_id': DEFAULT_DOMAIN_ID, + 'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex} + self.resource_api.create_project(project['id'], project) + + # pick a key known to be non-existent + key = 'description' + + def assert_key_equals(value): + project_ref = self.resource_api.update_project( + project['id'], project) + self.assertEqual(value, project_ref[key]) + project_ref = self.resource_api.get_project(project['id']) + self.assertEqual(value, project_ref[key]) + + def assert_get_key_is(value): + project_ref = self.resource_api.update_project( + project['id'], project) + self.assertIs(project_ref.get(key), value) + project_ref = self.resource_api.get_project(project['id']) + self.assertIs(project_ref.get(key), value) + + # add an attribute that doesn't exist, set it to a falsey value + value = '' + project[key] = value + assert_key_equals(value) + + # set an attribute with a falsey value to null + value = None + project[key] = value + assert_get_key_is(value) + + # do it again, in case updating from this situation is handled oddly + value = None + project[key] = value + assert_get_key_is(value) + + # set a possibly-null value to a falsey value + value = '' + project[key] = value + assert_key_equals(value) + + # set a falsey value to a truthy value + value = uuid.uuid4().hex + project[key] = value + assert_key_equals(value) + + def test_user_crud(self): + user_dict = {'domain_id': DEFAULT_DOMAIN_ID, + 'name': uuid.uuid4().hex, 'password': 'passw0rd'} + user = self.identity_api.create_user(user_dict) + user_ref = self.identity_api.get_user(user['id']) + del user_dict['password'] + user_ref_dict = {x: user_ref[x] for x in user_ref} + self.assertDictContainsSubset(user_dict, user_ref_dict) + + user_dict['password'] = uuid.uuid4().hex + self.identity_api.update_user(user['id'], user_dict) + user_ref = self.identity_api.get_user(user['id']) + del user_dict['password'] + user_ref_dict = {x: user_ref[x] for x in user_ref} + self.assertDictContainsSubset(user_dict, user_ref_dict) + + self.identity_api.delete_user(user['id']) + self.assertRaises(exception.UserNotFound, + self.identity_api.get_user, + user['id']) + + def test_list_projects_for_user(self): + domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(domain['id'], domain) + user1 = {'name': uuid.uuid4().hex, 'password': uuid.uuid4().hex, + 'domain_id': domain['id'], 'enabled': True} + user1 = self.identity_api.create_user(user1) + user_projects = self.assignment_api.list_projects_for_user(user1['id']) + self.assertEqual(0, len(user_projects)) + self.assignment_api.create_grant(user_id=user1['id'], + project_id=self.tenant_bar['id'], + role_id=self.role_member['id']) + self.assignment_api.create_grant(user_id=user1['id'], + project_id=self.tenant_baz['id'], + role_id=self.role_member['id']) + user_projects = self.assignment_api.list_projects_for_user(user1['id']) + self.assertEqual(2, len(user_projects)) + + def test_list_projects_for_user_with_grants(self): + # Create two groups each with a role on a different project, and + # make user1 a member of both groups. Both these new projects + # should now be included, along with any direct user grants. + domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(domain['id'], domain) + user1 = {'name': uuid.uuid4().hex, 'password': uuid.uuid4().hex, + 'domain_id': domain['id'], 'enabled': True} + user1 = self.identity_api.create_user(user1) + group1 = {'name': uuid.uuid4().hex, 'domain_id': domain['id']} + group1 = self.identity_api.create_group(group1) + group2 = {'name': uuid.uuid4().hex, 'domain_id': domain['id']} + group2 = self.identity_api.create_group(group2) + project1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'domain_id': domain['id']} + self.resource_api.create_project(project1['id'], project1) + project2 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'domain_id': domain['id']} + self.resource_api.create_project(project2['id'], project2) + self.identity_api.add_user_to_group(user1['id'], group1['id']) + self.identity_api.add_user_to_group(user1['id'], group2['id']) + + # Create 3 grants, one user grant, the other two as group grants + self.assignment_api.create_grant(user_id=user1['id'], + project_id=self.tenant_bar['id'], + role_id=self.role_member['id']) + self.assignment_api.create_grant(group_id=group1['id'], + project_id=project1['id'], + role_id=self.role_admin['id']) + self.assignment_api.create_grant(group_id=group2['id'], + project_id=project2['id'], + role_id=self.role_admin['id']) + user_projects = self.assignment_api.list_projects_for_user(user1['id']) + self.assertEqual(3, len(user_projects)) + + @tests.skip_if_cache_disabled('resource') + @tests.skip_if_no_multiple_domains_support + def test_domain_rename_invalidates_get_domain_by_name_cache(self): + domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'enabled': True} + domain_id = domain['id'] + domain_name = domain['name'] + self.resource_api.create_domain(domain_id, domain) + domain_ref = self.resource_api.get_domain_by_name(domain_name) + domain_ref['name'] = uuid.uuid4().hex + self.resource_api.update_domain(domain_id, domain_ref) + self.assertRaises(exception.DomainNotFound, + self.resource_api.get_domain_by_name, + domain_name) + + @tests.skip_if_cache_disabled('resource') + def test_cache_layer_domain_crud(self): + domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'enabled': True} + domain_id = domain['id'] + # Create Domain + self.resource_api.create_domain(domain_id, domain) + domain_ref = self.resource_api.get_domain(domain_id) + updated_domain_ref = copy.deepcopy(domain_ref) + updated_domain_ref['name'] = uuid.uuid4().hex + # Update domain, bypassing resource api manager + self.resource_api.driver.update_domain(domain_id, updated_domain_ref) + # Verify get_domain still returns the domain + self.assertDictContainsSubset( + domain_ref, self.resource_api.get_domain(domain_id)) + # Invalidate cache + self.resource_api.get_domain.invalidate(self.resource_api, + domain_id) + # Verify get_domain returns the updated domain + self.assertDictContainsSubset( + updated_domain_ref, self.resource_api.get_domain(domain_id)) + # Update the domain back to original ref, using the assignment api + # manager + self.resource_api.update_domain(domain_id, domain_ref) + self.assertDictContainsSubset( + domain_ref, self.resource_api.get_domain(domain_id)) + # Make sure domain is 'disabled', bypass resource api manager + domain_ref_disabled = domain_ref.copy() + domain_ref_disabled['enabled'] = False + self.resource_api.driver.update_domain(domain_id, + domain_ref_disabled) + # Delete domain, bypassing resource api manager + self.resource_api.driver.delete_domain(domain_id) + # Verify get_domain still returns the domain + self.assertDictContainsSubset( + domain_ref, self.resource_api.get_domain(domain_id)) + # Invalidate cache + self.resource_api.get_domain.invalidate(self.resource_api, + domain_id) + # Verify get_domain now raises DomainNotFound + self.assertRaises(exception.DomainNotFound, + self.resource_api.get_domain, domain_id) + # Recreate Domain + self.resource_api.create_domain(domain_id, domain) + self.resource_api.get_domain(domain_id) + # Make sure domain is 'disabled', bypass resource api manager + domain['enabled'] = False + self.resource_api.driver.update_domain(domain_id, domain) + # Delete domain + self.resource_api.delete_domain(domain_id) + # verify DomainNotFound raised + self.assertRaises(exception.DomainNotFound, + self.resource_api.get_domain, + domain_id) + + @tests.skip_if_cache_disabled('resource') + @tests.skip_if_no_multiple_domains_support + def test_project_rename_invalidates_get_project_by_name_cache(self): + domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'enabled': True} + project = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'domain_id': domain['id']} + project_id = project['id'] + project_name = project['name'] + self.resource_api.create_domain(domain['id'], domain) + # Create a project + self.resource_api.create_project(project_id, project) + self.resource_api.get_project_by_name(project_name, domain['id']) + project['name'] = uuid.uuid4().hex + self.resource_api.update_project(project_id, project) + self.assertRaises(exception.ProjectNotFound, + self.resource_api.get_project_by_name, + project_name, + domain['id']) + + @tests.skip_if_cache_disabled('resource') + @tests.skip_if_no_multiple_domains_support + def test_cache_layer_project_crud(self): + domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'enabled': True} + project = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'domain_id': domain['id']} + project_id = project['id'] + self.resource_api.create_domain(domain['id'], domain) + # Create a project + self.resource_api.create_project(project_id, project) + self.resource_api.get_project(project_id) + updated_project = copy.deepcopy(project) + updated_project['name'] = uuid.uuid4().hex + # Update project, bypassing resource manager + self.resource_api.driver.update_project(project_id, + updated_project) + # Verify get_project still returns the original project_ref + self.assertDictContainsSubset( + project, self.resource_api.get_project(project_id)) + # Invalidate cache + self.resource_api.get_project.invalidate(self.resource_api, + project_id) + # Verify get_project now returns the new project + self.assertDictContainsSubset( + updated_project, + self.resource_api.get_project(project_id)) + # Update project using the resource_api manager back to original + self.resource_api.update_project(project['id'], project) + # Verify get_project returns the original project_ref + self.assertDictContainsSubset( + project, self.resource_api.get_project(project_id)) + # Delete project bypassing resource + self.resource_api.driver.delete_project(project_id) + # Verify get_project still returns the project_ref + self.assertDictContainsSubset( + project, self.resource_api.get_project(project_id)) + # Invalidate cache + self.resource_api.get_project.invalidate(self.resource_api, + project_id) + # Verify ProjectNotFound now raised + self.assertRaises(exception.ProjectNotFound, + self.resource_api.get_project, + project_id) + # recreate project + self.resource_api.create_project(project_id, project) + self.resource_api.get_project(project_id) + # delete project + self.resource_api.delete_project(project_id) + # Verify ProjectNotFound is raised + self.assertRaises(exception.ProjectNotFound, + self.resource_api.get_project, + project_id) + + def create_user_dict(self, **attributes): + user_dict = {'name': uuid.uuid4().hex, + 'domain_id': DEFAULT_DOMAIN_ID, + 'enabled': True} + user_dict.update(attributes) + return user_dict + + def test_arbitrary_attributes_are_returned_from_create_user(self): + attr_value = uuid.uuid4().hex + user_data = self.create_user_dict(arbitrary_attr=attr_value) + + user = self.identity_api.create_user(user_data) + + self.assertEqual(attr_value, user['arbitrary_attr']) + + def test_arbitrary_attributes_are_returned_from_get_user(self): + attr_value = uuid.uuid4().hex + user_data = self.create_user_dict(arbitrary_attr=attr_value) + + user_data = self.identity_api.create_user(user_data) + + user = self.identity_api.get_user(user_data['id']) + self.assertEqual(attr_value, user['arbitrary_attr']) + + def test_new_arbitrary_attributes_are_returned_from_update_user(self): + user_data = self.create_user_dict() + + user = self.identity_api.create_user(user_data) + attr_value = uuid.uuid4().hex + user['arbitrary_attr'] = attr_value + updated_user = self.identity_api.update_user(user['id'], user) + + self.assertEqual(attr_value, updated_user['arbitrary_attr']) + + def test_updated_arbitrary_attributes_are_returned_from_update_user(self): + attr_value = uuid.uuid4().hex + user_data = self.create_user_dict(arbitrary_attr=attr_value) + + new_attr_value = uuid.uuid4().hex + user = self.identity_api.create_user(user_data) + user['arbitrary_attr'] = new_attr_value + updated_user = self.identity_api.update_user(user['id'], user) + + self.assertEqual(new_attr_value, updated_user['arbitrary_attr']) + + def test_create_grant_no_user(self): + # If call create_grant with a user that doesn't exist, doesn't fail. + self.assignment_api.create_grant( + self.role_other['id'], + user_id=uuid.uuid4().hex, + project_id=self.tenant_bar['id']) + + def test_create_grant_no_group(self): + # If call create_grant with a group that doesn't exist, doesn't fail. + self.assignment_api.create_grant( + self.role_other['id'], + group_id=uuid.uuid4().hex, + project_id=self.tenant_bar['id']) + + @tests.skip_if_no_multiple_domains_support + def test_get_default_domain_by_name(self): + domain_name = 'default' + + domain = {'id': uuid.uuid4().hex, 'name': domain_name, 'enabled': True} + self.resource_api.create_domain(domain['id'], domain) + + domain_ref = self.resource_api.get_domain_by_name(domain_name) + self.assertEqual(domain, domain_ref) + + def test_get_not_default_domain_by_name(self): + domain_name = 'foo' + self.assertRaises(exception.DomainNotFound, + self.resource_api.get_domain_by_name, + domain_name) + + def test_project_update_and_project_get_return_same_response(self): + project = { + 'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'domain_id': CONF.identity.default_domain_id, + 'description': uuid.uuid4().hex, + 'enabled': True} + + self.resource_api.create_project(project['id'], project) + + updated_project = {'enabled': False} + updated_project_ref = self.resource_api.update_project( + project['id'], updated_project) + + # SQL backend adds 'extra' field + updated_project_ref.pop('extra', None) + + self.assertIs(False, updated_project_ref['enabled']) + + project_ref = self.resource_api.get_project(project['id']) + self.assertDictEqual(project_ref, updated_project_ref) + + def test_user_update_and_user_get_return_same_response(self): + user = { + 'name': uuid.uuid4().hex, + 'domain_id': CONF.identity.default_domain_id, + 'description': uuid.uuid4().hex, + 'enabled': True} + + user = self.identity_api.create_user(user) + + updated_user = {'enabled': False} + updated_user_ref = self.identity_api.update_user( + user['id'], updated_user) + + # SQL backend adds 'extra' field + updated_user_ref.pop('extra', None) + + self.assertIs(False, updated_user_ref['enabled']) + + user_ref = self.identity_api.get_user(user['id']) + self.assertDictEqual(user_ref, updated_user_ref) + + def test_delete_group_removes_role_assignments(self): + # When a group is deleted any role assignments for the group are + # removed. + + MEMBER_ROLE_ID = 'member' + + def get_member_assignments(): + assignments = self.assignment_api.list_role_assignments() + return filter(lambda x: x['role_id'] == MEMBER_ROLE_ID, + assignments) + + orig_member_assignments = get_member_assignments() + + # Create a group. + new_group = { + 'domain_id': DEFAULT_DOMAIN_ID, + 'name': self.getUniqueString(prefix='tdgrra')} + new_group = self.identity_api.create_group(new_group) + + # Create a project. + new_project = { + 'id': uuid.uuid4().hex, + 'name': self.getUniqueString(prefix='tdgrra'), + 'domain_id': DEFAULT_DOMAIN_ID} + self.resource_api.create_project(new_project['id'], new_project) + + # Assign a role to the group. + self.assignment_api.create_grant( + group_id=new_group['id'], project_id=new_project['id'], + role_id=MEMBER_ROLE_ID) + + # Delete the group. + self.identity_api.delete_group(new_group['id']) + + # Check that the role assignment for the group is gone + member_assignments = get_member_assignments() + + self.assertThat(member_assignments, + matchers.Equals(orig_member_assignments)) + + def test_get_roles_for_groups_on_domain(self): + """Test retrieving group domain roles. + + Test Plan: + + - Create a domain, three groups and three roles + - Assign one an inherited and the others a non-inherited group role + to the domain + - Ensure that only the non-inherited roles are returned on the domain + + """ + domain1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(domain1['id'], domain1) + group_list = [] + group_id_list = [] + role_list = [] + for _ in range(3): + group = {'name': uuid.uuid4().hex, 'domain_id': domain1['id']} + group = self.identity_api.create_group(group) + group_list.append(group) + group_id_list.append(group['id']) + + role = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.role_api.create_role(role['id'], role) + role_list.append(role) + + # Assign the roles - one is inherited + self.assignment_api.create_grant(group_id=group_list[0]['id'], + domain_id=domain1['id'], + role_id=role_list[0]['id']) + self.assignment_api.create_grant(group_id=group_list[1]['id'], + domain_id=domain1['id'], + role_id=role_list[1]['id']) + self.assignment_api.create_grant(group_id=group_list[2]['id'], + domain_id=domain1['id'], + role_id=role_list[2]['id'], + inherited_to_projects=True) + + # Now get the effective roles for the groups on the domain project. We + # shouldn't get back the inherited role. + + role_refs = self.assignment_api.get_roles_for_groups( + group_id_list, domain_id=domain1['id']) + + self.assertThat(role_refs, matchers.HasLength(2)) + self.assertIn(role_list[0], role_refs) + self.assertIn(role_list[1], role_refs) + + def test_get_roles_for_groups_on_project(self): + """Test retrieving group project roles. + + Test Plan: + + - Create two domains, two projects, six groups and six roles + - Project1 is in Domain1, Project2 is in Domain2 + - Domain2/Project2 are spoilers + - Assign a different direct group role to each project as well + as both an inherited and non-inherited role to each domain + - Get the group roles for Project 1 - depending on whether we have + enabled inheritance, we should either get back just the direct role + or both the direct one plus the inherited domain role from Domain 1 + + """ + domain1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(domain1['id'], domain1) + domain2 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(domain2['id'], domain2) + project1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'domain_id': domain1['id']} + self.resource_api.create_project(project1['id'], project1) + project2 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'domain_id': domain2['id']} + self.resource_api.create_project(project2['id'], project2) + group_list = [] + group_id_list = [] + role_list = [] + for _ in range(6): + group = {'name': uuid.uuid4().hex, 'domain_id': domain1['id']} + group = self.identity_api.create_group(group) + group_list.append(group) + group_id_list.append(group['id']) + + role = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.role_api.create_role(role['id'], role) + role_list.append(role) + + # Assign the roles - one inherited and one non-inherited on Domain1, + # plus one on Project1 + self.assignment_api.create_grant(group_id=group_list[0]['id'], + domain_id=domain1['id'], + role_id=role_list[0]['id']) + self.assignment_api.create_grant(group_id=group_list[1]['id'], + domain_id=domain1['id'], + role_id=role_list[1]['id'], + inherited_to_projects=True) + self.assignment_api.create_grant(group_id=group_list[2]['id'], + project_id=project1['id'], + role_id=role_list[2]['id']) + + # ...and a duplicate set of spoiler assignments to Domain2/Project2 + self.assignment_api.create_grant(group_id=group_list[3]['id'], + domain_id=domain2['id'], + role_id=role_list[3]['id']) + self.assignment_api.create_grant(group_id=group_list[4]['id'], + domain_id=domain2['id'], + role_id=role_list[4]['id'], + inherited_to_projects=True) + self.assignment_api.create_grant(group_id=group_list[5]['id'], + project_id=project2['id'], + role_id=role_list[5]['id']) + + # Now get the effective roles for all groups on the Project1. With + # inheritance off, we should only get back the direct role. + + self.config_fixture.config(group='os_inherit', enabled=False) + role_refs = self.assignment_api.get_roles_for_groups( + group_id_list, project_id=project1['id']) + + self.assertThat(role_refs, matchers.HasLength(1)) + self.assertIn(role_list[2], role_refs) + + # With inheritance on, we should also get back the inherited role from + # its owning domain. + + self.config_fixture.config(group='os_inherit', enabled=True) + role_refs = self.assignment_api.get_roles_for_groups( + group_id_list, project_id=project1['id']) + + self.assertThat(role_refs, matchers.HasLength(2)) + self.assertIn(role_list[1], role_refs) + self.assertIn(role_list[2], role_refs) + + def test_list_domains_for_groups(self): + """Test retrieving domains for a list of groups. + + Test Plan: + + - Create three domains, three groups and one role + - Assign a non-inherited group role to two domains, and an inherited + group role to the third + - Ensure only the domains with non-inherited roles are returned + + """ + domain_list = [] + group_list = [] + group_id_list = [] + for _ in range(3): + domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(domain['id'], domain) + domain_list.append(domain) + + group = {'name': uuid.uuid4().hex, 'domain_id': domain['id']} + group = self.identity_api.create_group(group) + group_list.append(group) + group_id_list.append(group['id']) + + role1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.role_api.create_role(role1['id'], role1) + + # Assign the roles - one is inherited + self.assignment_api.create_grant(group_id=group_list[0]['id'], + domain_id=domain_list[0]['id'], + role_id=role1['id']) + self.assignment_api.create_grant(group_id=group_list[1]['id'], + domain_id=domain_list[1]['id'], + role_id=role1['id']) + self.assignment_api.create_grant(group_id=group_list[2]['id'], + domain_id=domain_list[2]['id'], + role_id=role1['id'], + inherited_to_projects=True) + + # Now list the domains that have roles for any of the 3 groups + # We shouldn't get back domain[2] since that had an inherited role. + + domain_refs = ( + self.assignment_api.list_domains_for_groups(group_id_list)) + + self.assertThat(domain_refs, matchers.HasLength(2)) + self.assertIn(domain_list[0], domain_refs) + self.assertIn(domain_list[1], domain_refs) + + def test_list_projects_for_groups(self): + """Test retrieving projects for a list of groups. + + Test Plan: + + - Create two domains, four projects, seven groups and seven roles + - Project1-3 are in Domain1, Project4 is in Domain2 + - Domain2/Project4 are spoilers + - Project1 and 2 have direct group roles, Project3 has no direct + roles but should inherit a group role from Domain1 + - Get the projects for the group roles that are assigned to Project1 + Project2 and the inherited one on Domain1. Depending on whether we + have enabled inheritance, we should either get back just the projects + with direct roles (Project 1 and 2) or also Project3 due to its + inherited role from Domain1. + + """ + domain1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(domain1['id'], domain1) + domain2 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(domain2['id'], domain2) + project1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'domain_id': domain1['id']} + project1 = self.resource_api.create_project(project1['id'], project1) + project2 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'domain_id': domain1['id']} + project2 = self.resource_api.create_project(project2['id'], project2) + project3 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'domain_id': domain1['id']} + project3 = self.resource_api.create_project(project3['id'], project3) + project4 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'domain_id': domain2['id']} + project4 = self.resource_api.create_project(project4['id'], project4) + group_list = [] + role_list = [] + for _ in range(7): + group = {'name': uuid.uuid4().hex, 'domain_id': domain1['id']} + group = self.identity_api.create_group(group) + group_list.append(group) + + role = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.role_api.create_role(role['id'], role) + role_list.append(role) + + # Assign the roles - one inherited and one non-inherited on Domain1, + # plus one on Project1 and Project2 + self.assignment_api.create_grant(group_id=group_list[0]['id'], + domain_id=domain1['id'], + role_id=role_list[0]['id']) + self.assignment_api.create_grant(group_id=group_list[1]['id'], + domain_id=domain1['id'], + role_id=role_list[1]['id'], + inherited_to_projects=True) + self.assignment_api.create_grant(group_id=group_list[2]['id'], + project_id=project1['id'], + role_id=role_list[2]['id']) + self.assignment_api.create_grant(group_id=group_list[3]['id'], + project_id=project2['id'], + role_id=role_list[3]['id']) + + # ...and a few of spoiler assignments to Domain2/Project4 + self.assignment_api.create_grant(group_id=group_list[4]['id'], + domain_id=domain2['id'], + role_id=role_list[4]['id']) + self.assignment_api.create_grant(group_id=group_list[5]['id'], + domain_id=domain2['id'], + role_id=role_list[5]['id'], + inherited_to_projects=True) + self.assignment_api.create_grant(group_id=group_list[6]['id'], + project_id=project4['id'], + role_id=role_list[6]['id']) + + # Now get the projects for the groups that have roles on Project1, + # Project2 and the inherited role on Domain!. With inheritance off, + # we should only get back the projects with direct role. + + self.config_fixture.config(group='os_inherit', enabled=False) + group_id_list = [group_list[1]['id'], group_list[2]['id'], + group_list[3]['id']] + project_refs = ( + self.assignment_api.list_projects_for_groups(group_id_list)) + + self.assertThat(project_refs, matchers.HasLength(2)) + self.assertIn(project1, project_refs) + self.assertIn(project2, project_refs) + + # With inheritance on, we should also get back the Project3 due to the + # inherited role from its owning domain. + + self.config_fixture.config(group='os_inherit', enabled=True) + project_refs = ( + self.assignment_api.list_projects_for_groups(group_id_list)) + + self.assertThat(project_refs, matchers.HasLength(3)) + self.assertIn(project1, project_refs) + self.assertIn(project2, project_refs) + self.assertIn(project3, project_refs) + + def test_update_role_no_name(self): + # A user can update a role and not include the name. + + # description is picked just because it's not name. + self.role_api.update_role(self.role_member['id'], + {'description': uuid.uuid4().hex}) + # If the previous line didn't raise an exception then the test passes. + + def test_update_role_same_name(self): + # A user can update a role and set the name to be the same as it was. + + self.role_api.update_role(self.role_member['id'], + {'name': self.role_member['name']}) + # If the previous line didn't raise an exception then the test passes. + + +class TokenTests(object): + def _create_token_id(self): + # Use a token signed by the cms module + token_id = "" + for i in range(1, 20): + token_id += uuid.uuid4().hex + return cms.cms_sign_token(token_id, + CONF.signing.certfile, + CONF.signing.keyfile) + + def _assert_revoked_token_list_matches_token_persistence( + self, revoked_token_id_list): + # Assert that the list passed in matches the list returned by the + # token persistence service + persistence_list = [ + x['id'] + for x in self.token_provider_api.list_revoked_tokens() + ] + self.assertEqual(persistence_list, revoked_token_id_list) + + def test_token_crud(self): + token_id = self._create_token_id() + data = {'id': token_id, 'a': 'b', + 'trust_id': None, + 'user': {'id': 'testuserid'}} + data_ref = self.token_provider_api._persistence.create_token(token_id, + data) + expires = data_ref.pop('expires') + data_ref.pop('user_id') + self.assertIsInstance(expires, datetime.datetime) + data_ref.pop('id') + data.pop('id') + self.assertDictEqual(data_ref, data) + + new_data_ref = self.token_provider_api._persistence.get_token(token_id) + expires = new_data_ref.pop('expires') + self.assertIsInstance(expires, datetime.datetime) + new_data_ref.pop('user_id') + new_data_ref.pop('id') + + self.assertEqual(data, new_data_ref) + + self.token_provider_api._persistence.delete_token(token_id) + self.assertRaises( + exception.TokenNotFound, + self.token_provider_api._persistence.get_token, token_id) + self.assertRaises( + exception.TokenNotFound, + self.token_provider_api._persistence.delete_token, token_id) + + def create_token_sample_data(self, token_id=None, tenant_id=None, + trust_id=None, user_id=None, expires=None): + if token_id is None: + token_id = self._create_token_id() + if user_id is None: + user_id = 'testuserid' + # FIXME(morganfainberg): These tokens look nothing like "Real" tokens. + # This should be fixed when token issuance is cleaned up. + data = {'id': token_id, 'a': 'b', + 'user': {'id': user_id}} + if tenant_id is not None: + data['tenant'] = {'id': tenant_id, 'name': tenant_id} + if tenant_id is NULL_OBJECT: + data['tenant'] = None + if expires is not None: + data['expires'] = expires + if trust_id is not None: + data['trust_id'] = trust_id + data.setdefault('access', {}).setdefault('trust', {}) + # Testuserid2 is used here since a trustee will be different in + # the cases of impersonation and therefore should not match the + # token's user_id. + data['access']['trust']['trustee_user_id'] = 'testuserid2' + data['token_version'] = provider.V2 + # Issue token stores a copy of all token data at token['token_data']. + # This emulates that assumption as part of the test. + data['token_data'] = copy.deepcopy(data) + new_token = self.token_provider_api._persistence.create_token(token_id, + data) + return new_token['id'], data + + def test_delete_tokens(self): + tokens = self.token_provider_api._persistence._list_tokens( + 'testuserid') + self.assertEqual(0, len(tokens)) + token_id1, data = self.create_token_sample_data( + tenant_id='testtenantid') + token_id2, data = self.create_token_sample_data( + tenant_id='testtenantid') + token_id3, data = self.create_token_sample_data( + tenant_id='testtenantid', + user_id='testuserid1') + tokens = self.token_provider_api._persistence._list_tokens( + 'testuserid') + self.assertEqual(2, len(tokens)) + self.assertIn(token_id2, tokens) + self.assertIn(token_id1, tokens) + self.token_provider_api._persistence.delete_tokens( + user_id='testuserid', + tenant_id='testtenantid') + tokens = self.token_provider_api._persistence._list_tokens( + 'testuserid') + self.assertEqual(0, len(tokens)) + self.assertRaises(exception.TokenNotFound, + self.token_provider_api._persistence.get_token, + token_id1) + self.assertRaises(exception.TokenNotFound, + self.token_provider_api._persistence.get_token, + token_id2) + + self.token_provider_api._persistence.get_token(token_id3) + + def test_delete_tokens_trust(self): + tokens = self.token_provider_api._persistence._list_tokens( + user_id='testuserid') + self.assertEqual(0, len(tokens)) + token_id1, data = self.create_token_sample_data( + tenant_id='testtenantid', + trust_id='testtrustid') + token_id2, data = self.create_token_sample_data( + tenant_id='testtenantid', + user_id='testuserid1', + trust_id='testtrustid1') + tokens = self.token_provider_api._persistence._list_tokens( + 'testuserid') + self.assertEqual(1, len(tokens)) + self.assertIn(token_id1, tokens) + self.token_provider_api._persistence.delete_tokens( + user_id='testuserid', + tenant_id='testtenantid', + trust_id='testtrustid') + self.assertRaises(exception.TokenNotFound, + self.token_provider_api._persistence.get_token, + token_id1) + self.token_provider_api._persistence.get_token(token_id2) + + def _test_token_list(self, token_list_fn): + tokens = token_list_fn('testuserid') + self.assertEqual(0, len(tokens)) + token_id1, data = self.create_token_sample_data() + tokens = token_list_fn('testuserid') + self.assertEqual(1, len(tokens)) + self.assertIn(token_id1, tokens) + token_id2, data = self.create_token_sample_data() + tokens = token_list_fn('testuserid') + self.assertEqual(2, len(tokens)) + self.assertIn(token_id2, tokens) + self.assertIn(token_id1, tokens) + self.token_provider_api._persistence.delete_token(token_id1) + tokens = token_list_fn('testuserid') + self.assertIn(token_id2, tokens) + self.assertNotIn(token_id1, tokens) + self.token_provider_api._persistence.delete_token(token_id2) + tokens = token_list_fn('testuserid') + self.assertNotIn(token_id2, tokens) + self.assertNotIn(token_id1, tokens) + + # tenant-specific tokens + tenant1 = uuid.uuid4().hex + tenant2 = uuid.uuid4().hex + token_id3, data = self.create_token_sample_data(tenant_id=tenant1) + token_id4, data = self.create_token_sample_data(tenant_id=tenant2) + # test for existing but empty tenant (LP:1078497) + token_id5, data = self.create_token_sample_data(tenant_id=NULL_OBJECT) + tokens = token_list_fn('testuserid') + self.assertEqual(3, len(tokens)) + self.assertNotIn(token_id1, tokens) + self.assertNotIn(token_id2, tokens) + self.assertIn(token_id3, tokens) + self.assertIn(token_id4, tokens) + self.assertIn(token_id5, tokens) + tokens = token_list_fn('testuserid', tenant2) + self.assertEqual(1, len(tokens)) + self.assertNotIn(token_id1, tokens) + self.assertNotIn(token_id2, tokens) + self.assertNotIn(token_id3, tokens) + self.assertIn(token_id4, tokens) + + def test_token_list(self): + self._test_token_list( + self.token_provider_api._persistence._list_tokens) + + def test_token_list_trust(self): + trust_id = uuid.uuid4().hex + token_id5, data = self.create_token_sample_data(trust_id=trust_id) + tokens = self.token_provider_api._persistence._list_tokens( + 'testuserid', trust_id=trust_id) + self.assertEqual(1, len(tokens)) + self.assertIn(token_id5, tokens) + + def test_get_token_404(self): + self.assertRaises(exception.TokenNotFound, + self.token_provider_api._persistence.get_token, + uuid.uuid4().hex) + self.assertRaises(exception.TokenNotFound, + self.token_provider_api._persistence.get_token, + None) + + def test_delete_token_404(self): + self.assertRaises(exception.TokenNotFound, + self.token_provider_api._persistence.delete_token, + uuid.uuid4().hex) + + def test_expired_token(self): + token_id = uuid.uuid4().hex + expire_time = timeutils.utcnow() - datetime.timedelta(minutes=1) + data = {'id_hash': token_id, 'id': token_id, 'a': 'b', + 'expires': expire_time, + 'trust_id': None, + 'user': {'id': 'testuserid'}} + data_ref = self.token_provider_api._persistence.create_token(token_id, + data) + data_ref.pop('user_id') + self.assertDictEqual(data_ref, data) + self.assertRaises(exception.TokenNotFound, + self.token_provider_api._persistence.get_token, + token_id) + + def test_null_expires_token(self): + token_id = uuid.uuid4().hex + data = {'id': token_id, 'id_hash': token_id, 'a': 'b', 'expires': None, + 'user': {'id': 'testuserid'}} + data_ref = self.token_provider_api._persistence.create_token(token_id, + data) + self.assertIsNotNone(data_ref['expires']) + new_data_ref = self.token_provider_api._persistence.get_token(token_id) + + # MySQL doesn't store microseconds, so discard them before testing + data_ref['expires'] = data_ref['expires'].replace(microsecond=0) + new_data_ref['expires'] = new_data_ref['expires'].replace( + microsecond=0) + + self.assertEqual(data_ref, new_data_ref) + + def check_list_revoked_tokens(self, token_ids): + revoked_ids = [x['id'] + for x in self.token_provider_api.list_revoked_tokens()] + self._assert_revoked_token_list_matches_token_persistence(revoked_ids) + for token_id in token_ids: + self.assertIn(token_id, revoked_ids) + + def delete_token(self): + token_id = uuid.uuid4().hex + data = {'id_hash': token_id, 'id': token_id, 'a': 'b', + 'user': {'id': 'testuserid'}} + data_ref = self.token_provider_api._persistence.create_token(token_id, + data) + self.token_provider_api._persistence.delete_token(token_id) + self.assertRaises( + exception.TokenNotFound, + self.token_provider_api._persistence.get_token, + data_ref['id']) + self.assertRaises( + exception.TokenNotFound, + self.token_provider_api._persistence.delete_token, + data_ref['id']) + return token_id + + def test_list_revoked_tokens_returns_empty_list(self): + revoked_ids = [x['id'] + for x in self.token_provider_api.list_revoked_tokens()] + self._assert_revoked_token_list_matches_token_persistence(revoked_ids) + self.assertEqual([], revoked_ids) + + def test_list_revoked_tokens_for_single_token(self): + self.check_list_revoked_tokens([self.delete_token()]) + + def test_list_revoked_tokens_for_multiple_tokens(self): + self.check_list_revoked_tokens([self.delete_token() + for x in six.moves.range(2)]) + + def test_flush_expired_token(self): + token_id = uuid.uuid4().hex + expire_time = timeutils.utcnow() - datetime.timedelta(minutes=1) + data = {'id_hash': token_id, 'id': token_id, 'a': 'b', + 'expires': expire_time, + 'trust_id': None, + 'user': {'id': 'testuserid'}} + data_ref = self.token_provider_api._persistence.create_token(token_id, + data) + data_ref.pop('user_id') + self.assertDictEqual(data_ref, data) + + token_id = uuid.uuid4().hex + expire_time = timeutils.utcnow() + datetime.timedelta(minutes=1) + data = {'id_hash': token_id, 'id': token_id, 'a': 'b', + 'expires': expire_time, + 'trust_id': None, + 'user': {'id': 'testuserid'}} + data_ref = self.token_provider_api._persistence.create_token(token_id, + data) + data_ref.pop('user_id') + self.assertDictEqual(data_ref, data) + + self.token_provider_api._persistence.flush_expired_tokens() + tokens = self.token_provider_api._persistence._list_tokens( + 'testuserid') + self.assertEqual(1, len(tokens)) + self.assertIn(token_id, tokens) + + @tests.skip_if_cache_disabled('token') + def test_revocation_list_cache(self): + expire_time = timeutils.utcnow() + datetime.timedelta(minutes=10) + token_id = uuid.uuid4().hex + token_data = {'id_hash': token_id, 'id': token_id, 'a': 'b', + 'expires': expire_time, + 'trust_id': None, + 'user': {'id': 'testuserid'}} + token2_id = uuid.uuid4().hex + token2_data = {'id_hash': token2_id, 'id': token2_id, 'a': 'b', + 'expires': expire_time, + 'trust_id': None, + 'user': {'id': 'testuserid'}} + # Create 2 Tokens. + self.token_provider_api._persistence.create_token(token_id, + token_data) + self.token_provider_api._persistence.create_token(token2_id, + token2_data) + # Verify the revocation list is empty. + self.assertEqual( + [], self.token_provider_api._persistence.list_revoked_tokens()) + self.assertEqual([], self.token_provider_api.list_revoked_tokens()) + # Delete a token directly, bypassing the manager. + self.token_provider_api._persistence.driver.delete_token(token_id) + # Verify the revocation list is still empty. + self.assertEqual( + [], self.token_provider_api._persistence.list_revoked_tokens()) + self.assertEqual([], self.token_provider_api.list_revoked_tokens()) + # Invalidate the revocation list. + self.token_provider_api._persistence.invalidate_revocation_list() + # Verify the deleted token is in the revocation list. + revoked_ids = [x['id'] + for x in self.token_provider_api.list_revoked_tokens()] + self._assert_revoked_token_list_matches_token_persistence(revoked_ids) + self.assertIn(token_id, revoked_ids) + # Delete the second token, through the manager + self.token_provider_api._persistence.delete_token(token2_id) + revoked_ids = [x['id'] + for x in self.token_provider_api.list_revoked_tokens()] + self._assert_revoked_token_list_matches_token_persistence(revoked_ids) + # Verify both tokens are in the revocation list. + self.assertIn(token_id, revoked_ids) + self.assertIn(token2_id, revoked_ids) + + def _test_predictable_revoked_pki_token_id(self, hash_fn): + token_id = self._create_token_id() + token_id_hash = hash_fn(token_id).hexdigest() + token = {'user': {'id': uuid.uuid4().hex}} + + self.token_provider_api._persistence.create_token(token_id, token) + self.token_provider_api._persistence.delete_token(token_id) + + revoked_ids = [x['id'] + for x in self.token_provider_api.list_revoked_tokens()] + self._assert_revoked_token_list_matches_token_persistence(revoked_ids) + self.assertIn(token_id_hash, revoked_ids) + self.assertNotIn(token_id, revoked_ids) + for t in self.token_provider_api._persistence.list_revoked_tokens(): + self.assertIn('expires', t) + + def test_predictable_revoked_pki_token_id_default(self): + self._test_predictable_revoked_pki_token_id(hashlib.md5) + + def test_predictable_revoked_pki_token_id_sha256(self): + self.config_fixture.config(group='token', hash_algorithm='sha256') + self._test_predictable_revoked_pki_token_id(hashlib.sha256) + + def test_predictable_revoked_uuid_token_id(self): + token_id = uuid.uuid4().hex + token = {'user': {'id': uuid.uuid4().hex}} + + self.token_provider_api._persistence.create_token(token_id, token) + self.token_provider_api._persistence.delete_token(token_id) + + revoked_tokens = self.token_provider_api.list_revoked_tokens() + revoked_ids = [x['id'] for x in revoked_tokens] + self._assert_revoked_token_list_matches_token_persistence(revoked_ids) + self.assertIn(token_id, revoked_ids) + for t in revoked_tokens: + self.assertIn('expires', t) + + def test_create_unicode_token_id(self): + token_id = six.text_type(self._create_token_id()) + self.create_token_sample_data(token_id=token_id) + self.token_provider_api._persistence.get_token(token_id) + + def test_create_unicode_user_id(self): + user_id = six.text_type(uuid.uuid4().hex) + token_id, data = self.create_token_sample_data(user_id=user_id) + self.token_provider_api._persistence.get_token(token_id) + + def test_token_expire_timezone(self): + + @test_utils.timezone + def _create_token(expire_time): + token_id = uuid.uuid4().hex + user_id = six.text_type(uuid.uuid4().hex) + return self.create_token_sample_data(token_id=token_id, + user_id=user_id, + expires=expire_time) + + for d in ['+0', '-11', '-8', '-5', '+5', '+8', '+14']: + test_utils.TZ = 'UTC' + d + expire_time = timeutils.utcnow() + datetime.timedelta(minutes=1) + token_id, data_in = _create_token(expire_time) + data_get = self.token_provider_api._persistence.get_token(token_id) + + self.assertEqual(data_in['id'], data_get['id'], + 'TZ=%s' % test_utils.TZ) + + expire_time_expired = ( + timeutils.utcnow() + datetime.timedelta(minutes=-1)) + token_id, data_in = _create_token(expire_time_expired) + self.assertRaises(exception.TokenNotFound, + self.token_provider_api._persistence.get_token, + data_in['id']) + + +class TokenCacheInvalidation(object): + def _create_test_data(self): + self.user = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'password': uuid.uuid4().hex, + 'domain_id': DEFAULT_DOMAIN_ID, 'enabled': True} + self.tenant = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'domain_id': DEFAULT_DOMAIN_ID, 'enabled': True} + + # Create an equivalent of a scoped token + token_dict = {'user': self.user, 'tenant': self.tenant, + 'metadata': {}, 'id': 'placeholder'} + token_id, data = self.token_provider_api.issue_v2_token(token_dict) + self.scoped_token_id = token_id + + # ..and an un-scoped one + token_dict = {'user': self.user, 'tenant': None, + 'metadata': {}, 'id': 'placeholder'} + token_id, data = self.token_provider_api.issue_v2_token(token_dict) + self.unscoped_token_id = token_id + + # Validate them, in the various ways possible - this will load the + # responses into the token cache. + self._check_scoped_tokens_are_valid() + self._check_unscoped_tokens_are_valid() + + def _check_unscoped_tokens_are_invalid(self): + self.assertRaises( + exception.TokenNotFound, + self.token_provider_api.validate_token, + self.unscoped_token_id) + self.assertRaises( + exception.TokenNotFound, + self.token_provider_api.validate_v2_token, + self.unscoped_token_id) + + def _check_scoped_tokens_are_invalid(self): + self.assertRaises( + exception.TokenNotFound, + self.token_provider_api.validate_token, + self.scoped_token_id) + self.assertRaises( + exception.TokenNotFound, + self.token_provider_api.validate_token, + self.scoped_token_id, + self.tenant['id']) + self.assertRaises( + exception.TokenNotFound, + self.token_provider_api.validate_v2_token, + self.scoped_token_id) + self.assertRaises( + exception.TokenNotFound, + self.token_provider_api.validate_v2_token, + self.scoped_token_id, + self.tenant['id']) + + def _check_scoped_tokens_are_valid(self): + self.token_provider_api.validate_token(self.scoped_token_id) + self.token_provider_api.validate_token( + self.scoped_token_id, belongs_to=self.tenant['id']) + self.token_provider_api.validate_v2_token(self.scoped_token_id) + self.token_provider_api.validate_v2_token( + self.scoped_token_id, belongs_to=self.tenant['id']) + + def _check_unscoped_tokens_are_valid(self): + self.token_provider_api.validate_token(self.unscoped_token_id) + self.token_provider_api.validate_v2_token(self.unscoped_token_id) + + def test_delete_unscoped_token(self): + self.token_provider_api._persistence.delete_token( + self.unscoped_token_id) + self._check_unscoped_tokens_are_invalid() + self._check_scoped_tokens_are_valid() + + def test_delete_scoped_token_by_id(self): + self.token_provider_api._persistence.delete_token(self.scoped_token_id) + self._check_scoped_tokens_are_invalid() + self._check_unscoped_tokens_are_valid() + + def test_delete_scoped_token_by_user(self): + self.token_provider_api._persistence.delete_tokens(self.user['id']) + # Since we are deleting all tokens for this user, they should all + # now be invalid. + self._check_scoped_tokens_are_invalid() + self._check_unscoped_tokens_are_invalid() + + def test_delete_scoped_token_by_user_and_tenant(self): + self.token_provider_api._persistence.delete_tokens( + self.user['id'], + tenant_id=self.tenant['id']) + self._check_scoped_tokens_are_invalid() + self._check_unscoped_tokens_are_valid() + + +class TrustTests(object): + def create_sample_trust(self, new_id, remaining_uses=None): + self.trustor = self.user_foo + self.trustee = self.user_two + trust_data = (self.trust_api.create_trust + (new_id, + {'trustor_user_id': self.trustor['id'], + 'trustee_user_id': self.user_two['id'], + 'project_id': self.tenant_bar['id'], + 'expires_at': timeutils. + parse_isotime('2031-02-18T18:10:00Z'), + 'impersonation': True, + 'remaining_uses': remaining_uses}, + roles=[{"id": "member"}, + {"id": "other"}, + {"id": "browser"}])) + return trust_data + + def test_delete_trust(self): + new_id = uuid.uuid4().hex + trust_data = self.create_sample_trust(new_id) + trust_id = trust_data['id'] + self.assertIsNotNone(trust_data) + trust_data = self.trust_api.get_trust(trust_id) + self.assertEqual(new_id, trust_data['id']) + self.trust_api.delete_trust(trust_id) + self.assertIsNone(self.trust_api.get_trust(trust_id)) + + def test_delete_trust_not_found(self): + trust_id = uuid.uuid4().hex + self.assertRaises(exception.TrustNotFound, + self.trust_api.delete_trust, + trust_id) + + def test_get_trust(self): + new_id = uuid.uuid4().hex + trust_data = self.create_sample_trust(new_id) + trust_id = trust_data['id'] + self.assertIsNotNone(trust_data) + trust_data = self.trust_api.get_trust(trust_id) + self.assertEqual(new_id, trust_data['id']) + self.trust_api.delete_trust(trust_data['id']) + + def test_get_deleted_trust(self): + new_id = uuid.uuid4().hex + trust_data = self.create_sample_trust(new_id) + self.assertIsNotNone(trust_data) + self.assertIsNone(trust_data['deleted_at']) + self.trust_api.delete_trust(new_id) + self.assertIsNone(self.trust_api.get_trust(new_id)) + deleted_trust = self.trust_api.get_trust(trust_data['id'], + deleted=True) + self.assertEqual(trust_data['id'], deleted_trust['id']) + self.assertIsNotNone(deleted_trust.get('deleted_at')) + + def test_create_trust(self): + new_id = uuid.uuid4().hex + trust_data = self.create_sample_trust(new_id) + + self.assertEqual(new_id, trust_data['id']) + self.assertEqual(self.trustee['id'], trust_data['trustee_user_id']) + self.assertEqual(self.trustor['id'], trust_data['trustor_user_id']) + self.assertTrue(timeutils.normalize_time(trust_data['expires_at']) > + timeutils.utcnow()) + + self.assertEqual([{'id': 'member'}, + {'id': 'other'}, + {'id': 'browser'}], trust_data['roles']) + + def test_list_trust_by_trustee(self): + for i in range(3): + self.create_sample_trust(uuid.uuid4().hex) + trusts = self.trust_api.list_trusts_for_trustee(self.trustee['id']) + self.assertEqual(3, len(trusts)) + self.assertEqual(trusts[0]["trustee_user_id"], self.trustee['id']) + trusts = self.trust_api.list_trusts_for_trustee(self.trustor['id']) + self.assertEqual(0, len(trusts)) + + def test_list_trust_by_trustor(self): + for i in range(3): + self.create_sample_trust(uuid.uuid4().hex) + trusts = self.trust_api.list_trusts_for_trustor(self.trustor['id']) + self.assertEqual(3, len(trusts)) + self.assertEqual(trusts[0]["trustor_user_id"], self.trustor['id']) + trusts = self.trust_api.list_trusts_for_trustor(self.trustee['id']) + self.assertEqual(0, len(trusts)) + + def test_list_trusts(self): + for i in range(3): + self.create_sample_trust(uuid.uuid4().hex) + trusts = self.trust_api.list_trusts() + self.assertEqual(3, len(trusts)) + + def test_trust_has_remaining_uses_positive(self): + # create a trust with limited uses, check that we have uses left + trust_data = self.create_sample_trust(uuid.uuid4().hex, + remaining_uses=5) + self.assertEqual(5, trust_data['remaining_uses']) + # create a trust with unlimited uses, check that we have uses left + trust_data = self.create_sample_trust(uuid.uuid4().hex) + self.assertIsNone(trust_data['remaining_uses']) + + def test_trust_has_remaining_uses_negative(self): + # try to create a trust with no remaining uses, check that it fails + self.assertRaises(exception.ValidationError, + self.create_sample_trust, + uuid.uuid4().hex, + remaining_uses=0) + # try to create a trust with negative remaining uses, + # check that it fails + self.assertRaises(exception.ValidationError, + self.create_sample_trust, + uuid.uuid4().hex, + remaining_uses=-12) + + def test_consume_use(self): + # consume a trust repeatedly until it has no uses anymore + trust_data = self.create_sample_trust(uuid.uuid4().hex, + remaining_uses=2) + self.trust_api.consume_use(trust_data['id']) + t = self.trust_api.get_trust(trust_data['id']) + self.assertEqual(1, t['remaining_uses']) + self.trust_api.consume_use(trust_data['id']) + # This was the last use, the trust isn't available anymore + self.assertIsNone(self.trust_api.get_trust(trust_data['id'])) + + +class CatalogTests(object): + + _legacy_endpoint_id_in_endpoint = False + _enabled_default_to_true_when_creating_endpoint = False + + def test_region_crud(self): + # create + region_id = '0' * 255 + new_region = { + 'id': region_id, + 'description': uuid.uuid4().hex, + } + res = self.catalog_api.create_region( + new_region.copy()) + # Ensure that we don't need to have a + # parent_region_id in the original supplied + # ref dict, but that it will be returned from + # the endpoint, with None value. + expected_region = new_region.copy() + expected_region['parent_region_id'] = None + self.assertDictEqual(res, expected_region) + + # Test adding another region with the one above + # as its parent. We will check below whether deleting + # the parent successfully deletes any child regions. + parent_region_id = region_id + region_id = uuid.uuid4().hex + new_region = { + 'id': region_id, + 'description': uuid.uuid4().hex, + 'parent_region_id': parent_region_id, + } + res = self.catalog_api.create_region( + new_region.copy()) + self.assertDictEqual(new_region, res) + + # list + regions = self.catalog_api.list_regions() + self.assertThat(regions, matchers.HasLength(2)) + region_ids = [x['id'] for x in regions] + self.assertIn(parent_region_id, region_ids) + self.assertIn(region_id, region_ids) + + # update + region_desc_update = {'description': uuid.uuid4().hex} + res = self.catalog_api.update_region(region_id, region_desc_update) + expected_region = new_region.copy() + expected_region['description'] = region_desc_update['description'] + self.assertDictEqual(expected_region, res) + + # delete + self.catalog_api.delete_region(parent_region_id) + self.assertRaises(exception.RegionNotFound, + self.catalog_api.delete_region, + parent_region_id) + self.assertRaises(exception.RegionNotFound, + self.catalog_api.get_region, + parent_region_id) + # Ensure the child is also gone... + self.assertRaises(exception.RegionNotFound, + self.catalog_api.get_region, + region_id) + + def _create_region_with_parent_id(self, parent_id=None): + new_region = { + 'id': uuid.uuid4().hex, + 'description': uuid.uuid4().hex, + 'parent_region_id': parent_id + } + self.catalog_api.create_region( + new_region) + return new_region + + def test_list_regions_filtered_by_parent_region_id(self): + new_region = self._create_region_with_parent_id() + parent_id = new_region['id'] + new_region = self._create_region_with_parent_id(parent_id) + new_region = self._create_region_with_parent_id(parent_id) + + # filter by parent_region_id + hints = driver_hints.Hints() + hints.add_filter('parent_region_id', parent_id) + regions = self.catalog_api.list_regions(hints) + for region in regions: + self.assertEqual(parent_id, region['parent_region_id']) + + @tests.skip_if_cache_disabled('catalog') + def test_cache_layer_region_crud(self): + region_id = uuid.uuid4().hex + new_region = { + 'id': region_id, + 'description': uuid.uuid4().hex, + } + self.catalog_api.create_region(new_region.copy()) + updated_region = copy.deepcopy(new_region) + updated_region['description'] = uuid.uuid4().hex + # cache the result + self.catalog_api.get_region(region_id) + # update the region bypassing catalog_api + self.catalog_api.driver.update_region(region_id, updated_region) + self.assertDictContainsSubset(new_region, + self.catalog_api.get_region(region_id)) + self.catalog_api.get_region.invalidate(self.catalog_api, region_id) + self.assertDictContainsSubset(updated_region, + self.catalog_api.get_region(region_id)) + # delete the region + self.catalog_api.driver.delete_region(region_id) + # still get the old region + self.assertDictContainsSubset(updated_region, + self.catalog_api.get_region(region_id)) + self.catalog_api.get_region.invalidate(self.catalog_api, region_id) + self.assertRaises(exception.RegionNotFound, + self.catalog_api.get_region, region_id) + + @tests.skip_if_cache_disabled('catalog') + def test_invalidate_cache_when_updating_region(self): + region_id = uuid.uuid4().hex + new_region = { + 'id': region_id, + 'description': uuid.uuid4().hex + } + self.catalog_api.create_region(new_region) + + # cache the region + self.catalog_api.get_region(region_id) + + # update the region via catalog_api + new_description = {'description': uuid.uuid4().hex} + self.catalog_api.update_region(region_id, new_description) + + # assert that we can get the new region + current_region = self.catalog_api.get_region(region_id) + self.assertEqual(new_description['description'], + current_region['description']) + + def test_create_region_with_duplicate_id(self): + region_id = uuid.uuid4().hex + new_region = { + 'id': region_id, + 'description': uuid.uuid4().hex + } + self.catalog_api.create_region(new_region) + # Create region again with duplicate id + self.assertRaises(exception.Conflict, + self.catalog_api.create_region, + new_region) + + def test_get_region_404(self): + self.assertRaises(exception.RegionNotFound, + self.catalog_api.get_region, + uuid.uuid4().hex) + + def test_delete_region_404(self): + self.assertRaises(exception.RegionNotFound, + self.catalog_api.delete_region, + uuid.uuid4().hex) + + def test_create_region_invalid_parent_region_404(self): + region_id = uuid.uuid4().hex + new_region = { + 'id': region_id, + 'description': uuid.uuid4().hex, + 'parent_region_id': 'nonexisting' + } + self.assertRaises(exception.RegionNotFound, + self.catalog_api.create_region, + new_region) + + def test_avoid_creating_circular_references_in_regions_update(self): + region_one = self._create_region_with_parent_id() + + # self circle: region_one->region_one + self.assertRaises(exception.CircularRegionHierarchyError, + self.catalog_api.update_region, + region_one['id'], + {'parent_region_id': region_one['id']}) + + # region_one->region_two->region_one + region_two = self._create_region_with_parent_id(region_one['id']) + self.assertRaises(exception.CircularRegionHierarchyError, + self.catalog_api.update_region, + region_one['id'], + {'parent_region_id': region_two['id']}) + + # region_one region_two->region_three->region_four->region_two + region_three = self._create_region_with_parent_id(region_two['id']) + region_four = self._create_region_with_parent_id(region_three['id']) + self.assertRaises(exception.CircularRegionHierarchyError, + self.catalog_api.update_region, + region_two['id'], + {'parent_region_id': region_four['id']}) + + @mock.patch.object(core.Driver, + "_ensure_no_circle_in_hierarchical_regions") + def test_circular_regions_can_be_deleted(self, mock_ensure_on_circle): + # turn off the enforcement so that cycles can be created for the test + mock_ensure_on_circle.return_value = None + + region_one = self._create_region_with_parent_id() + + # self circle: region_one->region_one + self.catalog_api.update_region( + region_one['id'], + {'parent_region_id': region_one['id']}) + self.catalog_api.delete_region(region_one['id']) + self.assertRaises(exception.RegionNotFound, + self.catalog_api.get_region, + region_one['id']) + + # region_one->region_two->region_one + region_one = self._create_region_with_parent_id() + region_two = self._create_region_with_parent_id(region_one['id']) + self.catalog_api.update_region( + region_one['id'], + {'parent_region_id': region_two['id']}) + self.catalog_api.delete_region(region_one['id']) + self.assertRaises(exception.RegionNotFound, + self.catalog_api.get_region, + region_one['id']) + self.assertRaises(exception.RegionNotFound, + self.catalog_api.get_region, + region_two['id']) + + # region_one->region_two->region_three->region_one + region_one = self._create_region_with_parent_id() + region_two = self._create_region_with_parent_id(region_one['id']) + region_three = self._create_region_with_parent_id(region_two['id']) + self.catalog_api.update_region( + region_one['id'], + {'parent_region_id': region_three['id']}) + self.catalog_api.delete_region(region_two['id']) + self.assertRaises(exception.RegionNotFound, + self.catalog_api.get_region, + region_two['id']) + self.assertRaises(exception.RegionNotFound, + self.catalog_api.get_region, + region_one['id']) + self.assertRaises(exception.RegionNotFound, + self.catalog_api.get_region, + region_three['id']) + + def test_service_crud(self): + # create + service_id = uuid.uuid4().hex + new_service = { + 'id': service_id, + 'type': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'description': uuid.uuid4().hex, + } + res = self.catalog_api.create_service( + service_id, + new_service.copy()) + new_service['enabled'] = True + self.assertDictEqual(new_service, res) + + # list + services = self.catalog_api.list_services() + self.assertIn(service_id, [x['id'] for x in services]) + + # update + service_name_update = {'name': uuid.uuid4().hex} + res = self.catalog_api.update_service(service_id, service_name_update) + expected_service = new_service.copy() + expected_service['name'] = service_name_update['name'] + self.assertDictEqual(expected_service, res) + + # delete + self.catalog_api.delete_service(service_id) + self.assertRaises(exception.ServiceNotFound, + self.catalog_api.delete_service, + service_id) + self.assertRaises(exception.ServiceNotFound, + self.catalog_api.get_service, + service_id) + + def _create_random_service(self): + service_id = uuid.uuid4().hex + new_service = { + 'id': service_id, + 'type': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'description': uuid.uuid4().hex, + } + return self.catalog_api.create_service(service_id, new_service.copy()) + + def test_service_filtering(self): + target_service = self._create_random_service() + unrelated_service1 = self._create_random_service() + unrelated_service2 = self._create_random_service() + + # filter by type + hint_for_type = driver_hints.Hints() + hint_for_type.add_filter(name="type", value=target_service['type']) + services = self.catalog_api.list_services(hint_for_type) + + self.assertEqual(1, len(services)) + filtered_service = services[0] + self.assertEqual(target_service['type'], filtered_service['type']) + self.assertEqual(target_service['id'], filtered_service['id']) + + # filter should have been removed, since it was already used by the + # backend + self.assertEqual(0, len(hint_for_type.filters)) + + # the backend shouldn't filter by name, since this is handled by the + # front end + hint_for_name = driver_hints.Hints() + hint_for_name.add_filter(name="name", value=target_service['name']) + services = self.catalog_api.list_services(hint_for_name) + + self.assertEqual(3, len(services)) + + # filter should still be there, since it wasn't used by the backend + self.assertEqual(1, len(hint_for_name.filters)) + + self.catalog_api.delete_service(target_service['id']) + self.catalog_api.delete_service(unrelated_service1['id']) + self.catalog_api.delete_service(unrelated_service2['id']) + + @tests.skip_if_cache_disabled('catalog') + def test_cache_layer_service_crud(self): + service_id = uuid.uuid4().hex + new_service = { + 'id': service_id, + 'type': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'description': uuid.uuid4().hex, + } + res = self.catalog_api.create_service( + service_id, + new_service.copy()) + new_service['enabled'] = True + self.assertDictEqual(new_service, res) + self.catalog_api.get_service(service_id) + updated_service = copy.deepcopy(new_service) + updated_service['description'] = uuid.uuid4().hex + # update bypassing catalog api + self.catalog_api.driver.update_service(service_id, updated_service) + self.assertDictContainsSubset(new_service, + self.catalog_api.get_service(service_id)) + self.catalog_api.get_service.invalidate(self.catalog_api, service_id) + self.assertDictContainsSubset(updated_service, + self.catalog_api.get_service(service_id)) + + # delete bypassing catalog api + self.catalog_api.driver.delete_service(service_id) + self.assertDictContainsSubset(updated_service, + self.catalog_api.get_service(service_id)) + self.catalog_api.get_service.invalidate(self.catalog_api, service_id) + self.assertRaises(exception.ServiceNotFound, + self.catalog_api.delete_service, + service_id) + self.assertRaises(exception.ServiceNotFound, + self.catalog_api.get_service, + service_id) + + @tests.skip_if_cache_disabled('catalog') + def test_invalidate_cache_when_updating_service(self): + service_id = uuid.uuid4().hex + new_service = { + 'id': service_id, + 'type': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'description': uuid.uuid4().hex, + } + self.catalog_api.create_service( + service_id, + new_service.copy()) + + # cache the service + self.catalog_api.get_service(service_id) + + # update the service via catalog api + new_type = {'type': uuid.uuid4().hex} + self.catalog_api.update_service(service_id, new_type) + + # assert that we can get the new service + current_service = self.catalog_api.get_service(service_id) + self.assertEqual(new_type['type'], current_service['type']) + + def test_delete_service_with_endpoint(self): + # create a service + service = { + 'id': uuid.uuid4().hex, + 'type': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'description': uuid.uuid4().hex, + } + self.catalog_api.create_service(service['id'], service) + + # create an endpoint attached to the service + endpoint = { + 'id': uuid.uuid4().hex, + 'region': uuid.uuid4().hex, + 'interface': uuid.uuid4().hex[:8], + 'url': uuid.uuid4().hex, + 'service_id': service['id'], + } + self.catalog_api.create_endpoint(endpoint['id'], endpoint) + + # deleting the service should also delete the endpoint + self.catalog_api.delete_service(service['id']) + self.assertRaises(exception.EndpointNotFound, + self.catalog_api.get_endpoint, + endpoint['id']) + self.assertRaises(exception.EndpointNotFound, + self.catalog_api.delete_endpoint, + endpoint['id']) + + def test_cache_layer_delete_service_with_endpoint(self): + service = { + 'id': uuid.uuid4().hex, + 'type': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'description': uuid.uuid4().hex, + } + self.catalog_api.create_service(service['id'], service) + + # create an endpoint attached to the service + endpoint = { + 'id': uuid.uuid4().hex, + 'region_id': None, + 'interface': uuid.uuid4().hex[:8], + 'url': uuid.uuid4().hex, + 'service_id': service['id'], + } + self.catalog_api.create_endpoint(endpoint['id'], endpoint) + # cache the result + self.catalog_api.get_service(service['id']) + self.catalog_api.get_endpoint(endpoint['id']) + # delete the service bypassing catalog api + self.catalog_api.driver.delete_service(service['id']) + self.assertDictContainsSubset(endpoint, + self.catalog_api. + get_endpoint(endpoint['id'])) + self.assertDictContainsSubset(service, + self.catalog_api. + get_service(service['id'])) + self.catalog_api.get_endpoint.invalidate(self.catalog_api, + endpoint['id']) + self.assertRaises(exception.EndpointNotFound, + self.catalog_api.get_endpoint, + endpoint['id']) + self.assertRaises(exception.EndpointNotFound, + self.catalog_api.delete_endpoint, + endpoint['id']) + # multiple endpoints associated with a service + second_endpoint = { + 'id': uuid.uuid4().hex, + 'region_id': None, + 'interface': uuid.uuid4().hex[:8], + 'url': uuid.uuid4().hex, + 'service_id': service['id'], + } + self.catalog_api.create_service(service['id'], service) + self.catalog_api.create_endpoint(endpoint['id'], endpoint) + self.catalog_api.create_endpoint(second_endpoint['id'], + second_endpoint) + self.catalog_api.delete_service(service['id']) + self.assertRaises(exception.EndpointNotFound, + self.catalog_api.get_endpoint, + endpoint['id']) + self.assertRaises(exception.EndpointNotFound, + self.catalog_api.delete_endpoint, + endpoint['id']) + self.assertRaises(exception.EndpointNotFound, + self.catalog_api.get_endpoint, + second_endpoint['id']) + self.assertRaises(exception.EndpointNotFound, + self.catalog_api.delete_endpoint, + second_endpoint['id']) + + def test_get_service_404(self): + self.assertRaises(exception.ServiceNotFound, + self.catalog_api.get_service, + uuid.uuid4().hex) + + def test_delete_service_404(self): + self.assertRaises(exception.ServiceNotFound, + self.catalog_api.delete_service, + uuid.uuid4().hex) + + def test_create_endpoint_nonexistent_service(self): + endpoint = { + 'id': uuid.uuid4().hex, + 'service_id': uuid.uuid4().hex, + } + self.assertRaises(exception.ValidationError, + self.catalog_api.create_endpoint, + endpoint['id'], + endpoint) + + def test_update_endpoint_nonexistent_service(self): + dummy_service, enabled_endpoint, dummy_disabled_endpoint = ( + self._create_endpoints()) + new_endpoint = { + 'service_id': uuid.uuid4().hex, + } + self.assertRaises(exception.ValidationError, + self.catalog_api.update_endpoint, + enabled_endpoint['id'], + new_endpoint) + + def test_create_endpoint_nonexistent_region(self): + service = { + 'id': uuid.uuid4().hex, + 'type': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'description': uuid.uuid4().hex, + } + self.catalog_api.create_service(service['id'], service.copy()) + + endpoint = { + 'id': uuid.uuid4().hex, + 'region_id': None, + 'service_id': service['id'], + 'interface': 'public', + 'url': uuid.uuid4().hex, + 'region_id': uuid.uuid4().hex, + } + self.assertRaises(exception.ValidationError, + self.catalog_api.create_endpoint, + endpoint['id'], + endpoint) + + def test_update_endpoint_nonexistent_region(self): + dummy_service, enabled_endpoint, dummy_disabled_endpoint = ( + self._create_endpoints()) + new_endpoint = { + 'region_id': uuid.uuid4().hex, + } + self.assertRaises(exception.ValidationError, + self.catalog_api.update_endpoint, + enabled_endpoint['id'], + new_endpoint) + + def test_get_endpoint_404(self): + self.assertRaises(exception.EndpointNotFound, + self.catalog_api.get_endpoint, + uuid.uuid4().hex) + + def test_delete_endpoint_404(self): + self.assertRaises(exception.EndpointNotFound, + self.catalog_api.delete_endpoint, + uuid.uuid4().hex) + + def test_create_endpoint(self): + service = { + 'id': uuid.uuid4().hex, + 'type': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'description': uuid.uuid4().hex, + } + self.catalog_api.create_service(service['id'], service.copy()) + + endpoint = { + 'id': uuid.uuid4().hex, + 'region_id': None, + 'service_id': service['id'], + 'interface': 'public', + 'url': uuid.uuid4().hex, + } + self.catalog_api.create_endpoint(endpoint['id'], endpoint.copy()) + + def test_update_endpoint(self): + dummy_service_ref, endpoint_ref, dummy_disabled_endpoint_ref = ( + self._create_endpoints()) + res = self.catalog_api.update_endpoint(endpoint_ref['id'], + {'interface': 'private'}) + expected_endpoint = endpoint_ref.copy() + expected_endpoint['interface'] = 'private' + if self._legacy_endpoint_id_in_endpoint: + expected_endpoint['legacy_endpoint_id'] = None + if self._enabled_default_to_true_when_creating_endpoint: + expected_endpoint['enabled'] = True + self.assertDictEqual(expected_endpoint, res) + + def _create_endpoints(self): + # Creates a service and 2 endpoints for the service in the same region. + # The 'public' interface is enabled and the 'internal' interface is + # disabled. + + def create_endpoint(service_id, region, **kwargs): + id_ = uuid.uuid4().hex + ref = { + 'id': id_, + 'interface': 'public', + 'region_id': region, + 'service_id': service_id, + 'url': 'http://localhost/%s' % uuid.uuid4().hex, + } + ref.update(kwargs) + self.catalog_api.create_endpoint(id_, ref) + return ref + + # Create a service for use with the endpoints. + service_id = uuid.uuid4().hex + service_ref = { + 'id': service_id, + 'name': uuid.uuid4().hex, + 'type': uuid.uuid4().hex, + } + self.catalog_api.create_service(service_id, service_ref) + + region = {'id': uuid.uuid4().hex} + self.catalog_api.create_region(region) + + # Create endpoints + enabled_endpoint_ref = create_endpoint(service_id, region['id']) + disabled_endpoint_ref = create_endpoint( + service_id, region['id'], enabled=False, interface='internal') + + return service_ref, enabled_endpoint_ref, disabled_endpoint_ref + + def test_get_catalog_endpoint_disabled(self): + """Get back only enabled endpoints when get the v2 catalog.""" + + service_ref, enabled_endpoint_ref, dummy_disabled_endpoint_ref = ( + self._create_endpoints()) + + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + catalog = self.catalog_api.get_catalog(user_id, project_id) + + exp_entry = { + 'id': enabled_endpoint_ref['id'], + 'name': service_ref['name'], + 'publicURL': enabled_endpoint_ref['url'], + } + + region = enabled_endpoint_ref['region_id'] + self.assertEqual(exp_entry, catalog[region][service_ref['type']]) + + def test_get_v3_catalog_endpoint_disabled(self): + """Get back only enabled endpoints when get the v3 catalog.""" + + enabled_endpoint_ref = self._create_endpoints()[1] + + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + catalog = self.catalog_api.get_v3_catalog(user_id, project_id) + + endpoint_ids = [x['id'] for x in catalog[0]['endpoints']] + self.assertEqual([enabled_endpoint_ref['id']], endpoint_ids) + + @tests.skip_if_cache_disabled('catalog') + def test_invalidate_cache_when_updating_endpoint(self): + service = { + 'id': uuid.uuid4().hex, + 'type': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'description': uuid.uuid4().hex, + } + self.catalog_api.create_service(service['id'], service) + + # create an endpoint attached to the service + endpoint_id = uuid.uuid4().hex + endpoint = { + 'id': endpoint_id, + 'region_id': None, + 'interface': uuid.uuid4().hex[:8], + 'url': uuid.uuid4().hex, + 'service_id': service['id'], + } + self.catalog_api.create_endpoint(endpoint_id, endpoint) + + # cache the endpoint + self.catalog_api.get_endpoint(endpoint_id) + + # update the endpoint via catalog api + new_url = {'url': uuid.uuid4().hex} + self.catalog_api.update_endpoint(endpoint_id, new_url) + + # assert that we can get the new endpoint + current_endpoint = self.catalog_api.get_endpoint(endpoint_id) + self.assertEqual(new_url['url'], current_endpoint['url']) + + +class PolicyTests(object): + def _new_policy_ref(self): + return { + 'id': uuid.uuid4().hex, + 'policy': uuid.uuid4().hex, + 'type': uuid.uuid4().hex, + 'endpoint_id': uuid.uuid4().hex, + } + + def assertEqualPolicies(self, a, b): + self.assertEqual(a['id'], b['id']) + self.assertEqual(a['endpoint_id'], b['endpoint_id']) + self.assertEqual(a['policy'], b['policy']) + self.assertEqual(a['type'], b['type']) + + def test_create(self): + ref = self._new_policy_ref() + res = self.policy_api.create_policy(ref['id'], ref) + self.assertEqualPolicies(ref, res) + + def test_get(self): + ref = self._new_policy_ref() + res = self.policy_api.create_policy(ref['id'], ref) + + res = self.policy_api.get_policy(ref['id']) + self.assertEqualPolicies(ref, res) + + def test_list(self): + ref = self._new_policy_ref() + self.policy_api.create_policy(ref['id'], ref) + + res = self.policy_api.list_policies() + res = [x for x in res if x['id'] == ref['id']][0] + self.assertEqualPolicies(ref, res) + + def test_update(self): + ref = self._new_policy_ref() + self.policy_api.create_policy(ref['id'], ref) + orig = ref + + ref = self._new_policy_ref() + + # (cannot change policy ID) + self.assertRaises(exception.ValidationError, + self.policy_api.update_policy, + orig['id'], + ref) + + ref['id'] = orig['id'] + res = self.policy_api.update_policy(orig['id'], ref) + self.assertEqualPolicies(ref, res) + + def test_delete(self): + ref = self._new_policy_ref() + self.policy_api.create_policy(ref['id'], ref) + + self.policy_api.delete_policy(ref['id']) + self.assertRaises(exception.PolicyNotFound, + self.policy_api.delete_policy, + ref['id']) + self.assertRaises(exception.PolicyNotFound, + self.policy_api.get_policy, + ref['id']) + res = self.policy_api.list_policies() + self.assertFalse(len([x for x in res if x['id'] == ref['id']])) + + def test_get_policy_404(self): + self.assertRaises(exception.PolicyNotFound, + self.policy_api.get_policy, + uuid.uuid4().hex) + + def test_update_policy_404(self): + ref = self._new_policy_ref() + self.assertRaises(exception.PolicyNotFound, + self.policy_api.update_policy, + ref['id'], + ref) + + def test_delete_policy_404(self): + self.assertRaises(exception.PolicyNotFound, + self.policy_api.delete_policy, + uuid.uuid4().hex) + + +class InheritanceTests(object): + + def test_inherited_role_grants_for_user(self): + """Test inherited user roles. + + Test Plan: + + - Enable OS-INHERIT extension + - Create 3 roles + - Create a domain, with a project and a user + - Check no roles yet exit + - Assign a direct user role to the project and a (non-inherited) + user role to the domain + - Get a list of effective roles - should only get the one direct role + - Now add an inherited user role to the domain + - Get a list of effective roles - should have two roles, one + direct and one by virtue of the inherited user role + - Also get effective roles for the domain - the role marked as + inherited should not show up + + """ + self.config_fixture.config(group='os_inherit', enabled=True) + role_list = [] + for _ in range(3): + role = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.role_api.create_role(role['id'], role) + role_list.append(role) + domain1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(domain1['id'], domain1) + user1 = {'name': uuid.uuid4().hex, 'domain_id': domain1['id'], + 'password': uuid.uuid4().hex, 'enabled': True} + user1 = self.identity_api.create_user(user1) + project1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'domain_id': domain1['id']} + self.resource_api.create_project(project1['id'], project1) + + roles_ref = self.assignment_api.list_grants( + user_id=user1['id'], + project_id=project1['id']) + self.assertEqual(0, len(roles_ref)) + + # Create the first two roles - the domain one is not inherited + self.assignment_api.create_grant(user_id=user1['id'], + project_id=project1['id'], + role_id=role_list[0]['id']) + self.assignment_api.create_grant(user_id=user1['id'], + domain_id=domain1['id'], + role_id=role_list[1]['id']) + + # Now get the effective roles for the user and project, this + # should only include the direct role assignment on the project + combined_list = self.assignment_api.get_roles_for_user_and_project( + user1['id'], project1['id']) + self.assertEqual(1, len(combined_list)) + self.assertIn(role_list[0]['id'], combined_list) + + # Now add an inherited role on the domain + self.assignment_api.create_grant(user_id=user1['id'], + domain_id=domain1['id'], + role_id=role_list[2]['id'], + inherited_to_projects=True) + + # Now get the effective roles for the user and project again, this + # should now include the inherited role on the domain + combined_list = self.assignment_api.get_roles_for_user_and_project( + user1['id'], project1['id']) + self.assertEqual(2, len(combined_list)) + self.assertIn(role_list[0]['id'], combined_list) + self.assertIn(role_list[2]['id'], combined_list) + + # Finally, check that the inherited role does not appear as a valid + # directly assigned role on the domain itself + combined_role_list = self.assignment_api.get_roles_for_user_and_domain( + user1['id'], domain1['id']) + self.assertEqual(1, len(combined_role_list)) + self.assertIn(role_list[1]['id'], combined_role_list) + + def test_inherited_role_grants_for_group(self): + """Test inherited group roles. + + Test Plan: + + - Enable OS-INHERIT extension + - Create 4 roles + - Create a domain, with a project, user and two groups + - Make the user a member of both groups + - Check no roles yet exit + - Assign a direct user role to the project and a (non-inherited) + group role on the domain + - Get a list of effective roles - should only get the one direct role + - Now add two inherited group roles to the domain + - Get a list of effective roles - should have three roles, one + direct and two by virtue of inherited group roles + + """ + self.config_fixture.config(group='os_inherit', enabled=True) + role_list = [] + for _ in range(4): + role = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.role_api.create_role(role['id'], role) + role_list.append(role) + domain1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(domain1['id'], domain1) + user1 = {'name': uuid.uuid4().hex, 'domain_id': domain1['id'], + 'password': uuid.uuid4().hex, 'enabled': True} + user1 = self.identity_api.create_user(user1) + group1 = {'name': uuid.uuid4().hex, 'domain_id': domain1['id'], + 'enabled': True} + group1 = self.identity_api.create_group(group1) + group2 = {'name': uuid.uuid4().hex, 'domain_id': domain1['id'], + 'enabled': True} + group2 = self.identity_api.create_group(group2) + project1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'domain_id': domain1['id']} + self.resource_api.create_project(project1['id'], project1) + + self.identity_api.add_user_to_group(user1['id'], + group1['id']) + self.identity_api.add_user_to_group(user1['id'], + group2['id']) + + roles_ref = self.assignment_api.list_grants( + user_id=user1['id'], + project_id=project1['id']) + self.assertEqual(0, len(roles_ref)) + + # Create two roles - the domain one is not inherited + self.assignment_api.create_grant(user_id=user1['id'], + project_id=project1['id'], + role_id=role_list[0]['id']) + self.assignment_api.create_grant(group_id=group1['id'], + domain_id=domain1['id'], + role_id=role_list[1]['id']) + + # Now get the effective roles for the user and project, this + # should only include the direct role assignment on the project + combined_list = self.assignment_api.get_roles_for_user_and_project( + user1['id'], project1['id']) + self.assertEqual(1, len(combined_list)) + self.assertIn(role_list[0]['id'], combined_list) + + # Now add to more group roles, both inherited, to the domain + self.assignment_api.create_grant(group_id=group2['id'], + domain_id=domain1['id'], + role_id=role_list[2]['id'], + inherited_to_projects=True) + self.assignment_api.create_grant(group_id=group2['id'], + domain_id=domain1['id'], + role_id=role_list[3]['id'], + inherited_to_projects=True) + + # Now get the effective roles for the user and project again, this + # should now include the inherited roles on the domain + combined_list = self.assignment_api.get_roles_for_user_and_project( + user1['id'], project1['id']) + self.assertEqual(3, len(combined_list)) + self.assertIn(role_list[0]['id'], combined_list) + self.assertIn(role_list[2]['id'], combined_list) + self.assertIn(role_list[3]['id'], combined_list) + + def test_list_projects_for_user_with_inherited_grants(self): + """Test inherited user roles. + + Test Plan: + + - Enable OS-INHERIT extension + - Create a domain, with two projects and a user + - Assign an inherited user role on the domain, as well as a direct + user role to a separate project in a different domain + - Get a list of projects for user, should return all three projects + + """ + self.config_fixture.config(group='os_inherit', enabled=True) + domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(domain['id'], domain) + user1 = {'name': uuid.uuid4().hex, 'password': uuid.uuid4().hex, + 'domain_id': domain['id'], 'enabled': True} + user1 = self.identity_api.create_user(user1) + project1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'domain_id': domain['id']} + self.resource_api.create_project(project1['id'], project1) + project2 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'domain_id': domain['id']} + self.resource_api.create_project(project2['id'], project2) + + # Create 2 grants, one on a project and one inherited grant + # on the domain + self.assignment_api.create_grant(user_id=user1['id'], + project_id=self.tenant_bar['id'], + role_id=self.role_member['id']) + self.assignment_api.create_grant(user_id=user1['id'], + domain_id=domain['id'], + role_id=self.role_admin['id'], + inherited_to_projects=True) + # Should get back all three projects, one by virtue of the direct + # grant, plus both projects in the domain + user_projects = self.assignment_api.list_projects_for_user(user1['id']) + self.assertEqual(3, len(user_projects)) + + def test_list_projects_for_user_with_inherited_user_project_grants(self): + """Test inherited role assignments for users on nested projects. + + Test Plan: + + - Enable OS-INHERIT extension + - Create a hierarchy of projects with one root and one leaf project + - Assign an inherited user role on root project + - Assign a non-inherited user role on root project + - Get a list of projects for user, should return both projects + - Disable OS-INHERIT extension + - Get a list of projects for user, should return only root project + + """ + # Enable OS-INHERIT extension + self.config_fixture.config(group='os_inherit', enabled=True) + root_project = {'id': uuid.uuid4().hex, + 'description': '', + 'domain_id': DEFAULT_DOMAIN_ID, + 'enabled': True, + 'name': uuid.uuid4().hex, + 'parent_id': None} + self.resource_api.create_project(root_project['id'], root_project) + leaf_project = {'id': uuid.uuid4().hex, + 'description': '', + 'domain_id': DEFAULT_DOMAIN_ID, + 'enabled': True, + 'name': uuid.uuid4().hex, + 'parent_id': root_project['id']} + self.resource_api.create_project(leaf_project['id'], leaf_project) + + user = {'name': uuid.uuid4().hex, 'password': uuid.uuid4().hex, + 'domain_id': DEFAULT_DOMAIN_ID, 'enabled': True} + user = self.identity_api.create_user(user) + + # Grant inherited user role + self.assignment_api.create_grant(user_id=user['id'], + project_id=root_project['id'], + role_id=self.role_admin['id'], + inherited_to_projects=True) + # Grant non-inherited user role + self.assignment_api.create_grant(user_id=user['id'], + project_id=root_project['id'], + role_id=self.role_member['id']) + # Should get back both projects: because the direct role assignment for + # the root project and inherited role assignment for leaf project + user_projects = self.assignment_api.list_projects_for_user(user['id']) + self.assertEqual(2, len(user_projects)) + self.assertIn(root_project, user_projects) + self.assertIn(leaf_project, user_projects) + + # Disable OS-INHERIT extension + self.config_fixture.config(group='os_inherit', enabled=False) + # Should get back just root project - due the direct role assignment + user_projects = self.assignment_api.list_projects_for_user(user['id']) + self.assertEqual(1, len(user_projects)) + self.assertIn(root_project, user_projects) + + def test_list_projects_for_user_with_inherited_group_grants(self): + """Test inherited group roles. + + Test Plan: + + - Enable OS-INHERIT extension + - Create two domains, each with two projects + - Create a user and group + - Make the user a member of the group + - Assign a user role two projects, an inherited + group role to one domain and an inherited regular role on + the other domain + - Get a list of projects for user, should return both pairs of projects + from the domain, plus the one separate project + + """ + self.config_fixture.config(group='os_inherit', enabled=True) + domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(domain['id'], domain) + domain2 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(domain2['id'], domain2) + project1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'domain_id': domain['id']} + self.resource_api.create_project(project1['id'], project1) + project2 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'domain_id': domain['id']} + self.resource_api.create_project(project2['id'], project2) + project3 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'domain_id': domain2['id']} + self.resource_api.create_project(project3['id'], project3) + project4 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'domain_id': domain2['id']} + self.resource_api.create_project(project4['id'], project4) + user1 = {'name': uuid.uuid4().hex, 'password': uuid.uuid4().hex, + 'domain_id': domain['id'], 'enabled': True} + user1 = self.identity_api.create_user(user1) + group1 = {'name': uuid.uuid4().hex, 'domain_id': domain['id']} + group1 = self.identity_api.create_group(group1) + self.identity_api.add_user_to_group(user1['id'], group1['id']) + + # Create 4 grants: + # - one user grant on a project in domain2 + # - one user grant on a project in the default domain + # - one inherited user grant on domain + # - one inherited group grant on domain2 + self.assignment_api.create_grant(user_id=user1['id'], + project_id=project3['id'], + role_id=self.role_member['id']) + self.assignment_api.create_grant(user_id=user1['id'], + project_id=self.tenant_bar['id'], + role_id=self.role_member['id']) + self.assignment_api.create_grant(user_id=user1['id'], + domain_id=domain['id'], + role_id=self.role_admin['id'], + inherited_to_projects=True) + self.assignment_api.create_grant(group_id=group1['id'], + domain_id=domain2['id'], + role_id=self.role_admin['id'], + inherited_to_projects=True) + # Should get back all five projects, but without a duplicate for + # project3 (since it has both a direct user role and an inherited role) + user_projects = self.assignment_api.list_projects_for_user(user1['id']) + self.assertEqual(5, len(user_projects)) + + def test_list_projects_for_user_with_inherited_group_project_grants(self): + """Test inherited role assignments for groups on nested projects. + + Test Plan: + + - Enable OS-INHERIT extension + - Create a hierarchy of projects with one root and one leaf project + - Assign an inherited group role on root project + - Assign a non-inherited group role on root project + - Get a list of projects for user, should return both projects + - Disable OS-INHERIT extension + - Get a list of projects for user, should return only root project + + """ + self.config_fixture.config(group='os_inherit', enabled=True) + root_project = {'id': uuid.uuid4().hex, + 'description': '', + 'domain_id': DEFAULT_DOMAIN_ID, + 'enabled': True, + 'name': uuid.uuid4().hex, + 'parent_id': None} + self.resource_api.create_project(root_project['id'], root_project) + leaf_project = {'id': uuid.uuid4().hex, + 'description': '', + 'domain_id': DEFAULT_DOMAIN_ID, + 'enabled': True, + 'name': uuid.uuid4().hex, + 'parent_id': root_project['id']} + self.resource_api.create_project(leaf_project['id'], leaf_project) + + user = {'name': uuid.uuid4().hex, 'password': uuid.uuid4().hex, + 'domain_id': DEFAULT_DOMAIN_ID, 'enabled': True} + user = self.identity_api.create_user(user) + + group = {'name': uuid.uuid4().hex, 'domain_id': DEFAULT_DOMAIN_ID} + group = self.identity_api.create_group(group) + self.identity_api.add_user_to_group(user['id'], group['id']) + + # Grant inherited group role + self.assignment_api.create_grant(group_id=group['id'], + project_id=root_project['id'], + role_id=self.role_admin['id'], + inherited_to_projects=True) + # Grant non-inherited group role + self.assignment_api.create_grant(group_id=group['id'], + project_id=root_project['id'], + role_id=self.role_member['id']) + # Should get back both projects: because the direct role assignment for + # the root project and inherited role assignment for leaf project + user_projects = self.assignment_api.list_projects_for_user(user['id']) + self.assertEqual(2, len(user_projects)) + self.assertIn(root_project, user_projects) + self.assertIn(leaf_project, user_projects) + + # Disable OS-INHERIT extension + self.config_fixture.config(group='os_inherit', enabled=False) + # Should get back just root project - due the direct role assignment + user_projects = self.assignment_api.list_projects_for_user(user['id']) + self.assertEqual(1, len(user_projects)) + self.assertIn(root_project, user_projects) + + +class FilterTests(filtering.FilterTests): + def test_list_entities_filtered(self): + for entity in ['user', 'group', 'project']: + # Create 20 entities + entity_list = self._create_test_data(entity, 20) + + # Try filtering to get one an exact item out of the list + hints = driver_hints.Hints() + hints.add_filter('name', entity_list[10]['name']) + entities = self._list_entities(entity)(hints=hints) + self.assertEqual(1, len(entities)) + self.assertEqual(entities[0]['id'], entity_list[10]['id']) + # Check the driver has removed the filter from the list hints + self.assertFalse(hints.get_exact_filter_by_name('name')) + self._delete_test_data(entity, entity_list) + + def test_list_users_inexact_filtered(self): + # Create 20 users, some with specific names. We set the names at create + # time (rather than updating them), since the LDAP driver does not + # support name updates. + user_name_data = { + # user index: name for user + 5: 'The', + 6: 'The Ministry', + 7: 'The Ministry of', + 8: 'The Ministry of Silly', + 9: 'The Ministry of Silly Walks', + # ...and one for useful case insensitivity testing + 10: 'The ministry of silly walks OF' + } + user_list = self._create_test_data( + 'user', 20, domain_id=DEFAULT_DOMAIN_ID, name_dict=user_name_data) + + hints = driver_hints.Hints() + hints.add_filter('name', 'ministry', comparator='contains') + users = self.identity_api.list_users(hints=hints) + self.assertEqual(5, len(users)) + self._match_with_list(users, user_list, + list_start=6, list_end=11) + # TODO(henry-nash) Check inexact filter has been removed. + + hints = driver_hints.Hints() + hints.add_filter('name', 'The', comparator='startswith') + users = self.identity_api.list_users(hints=hints) + self.assertEqual(6, len(users)) + self._match_with_list(users, user_list, + list_start=5, list_end=11) + # TODO(henry-nash) Check inexact filter has been removed. + + hints = driver_hints.Hints() + hints.add_filter('name', 'of', comparator='endswith') + users = self.identity_api.list_users(hints=hints) + self.assertEqual(2, len(users)) + # We can't assume we will get back the users in any particular order + self.assertIn(user_list[7]['id'], [users[0]['id'], users[1]['id']]) + self.assertIn(user_list[10]['id'], [users[0]['id'], users[1]['id']]) + # TODO(henry-nash) Check inexact filter has been removed. + + # TODO(henry-nash): Add some case sensitive tests. However, + # these would be hard to validate currently, since: + # + # For SQL, the issue is that MySQL 0.7, by default, is installed in + # case insensitive mode (which is what is run by default for our + # SQL backend tests). For production deployments. OpenStack + # assumes a case sensitive database. For these tests, therefore, we + # need to be able to check the sensitivity of the database so as to + # know whether to run case sensitive tests here. + # + # For LDAP/AD, although dependent on the schema being used, attributes + # are typically configured to be case aware, but not case sensitive. + + self._delete_test_data('user', user_list) + + def test_groups_for_user_filtered(self): + """Test use of filtering doesn't break groups_for_user listing. + + Some backends may use filtering to achieve the list of groups for a + user, so test that it can combine a second filter. + + Test Plan: + + - Create 10 groups, some with names we can filter on + - Create 2 users + - Assign 1 of those users to most of the groups, including some of the + well known named ones + - Assign the other user to other groups as spoilers + - Ensure that when we list groups for users with a filter on the group + name, both restrictions have been enforced on what is returned. + + """ + + number_of_groups = 10 + group_name_data = { + # entity index: name for entity + 5: 'The', + 6: 'The Ministry', + 9: 'The Ministry of Silly Walks', + } + group_list = self._create_test_data( + 'group', number_of_groups, + domain_id=DEFAULT_DOMAIN_ID, name_dict=group_name_data) + user_list = self._create_test_data('user', 2) + + for group in range(7): + # Create membership, including with two out of the three groups + # with well know names + self.identity_api.add_user_to_group(user_list[0]['id'], + group_list[group]['id']) + # ...and some spoiler memberships + for group in range(7, number_of_groups): + self.identity_api.add_user_to_group(user_list[1]['id'], + group_list[group]['id']) + + hints = driver_hints.Hints() + hints.add_filter('name', 'The', comparator='startswith') + groups = self.identity_api.list_groups_for_user( + user_list[0]['id'], hints=hints) + # We should only get back 2 out of the 3 groups that start with 'The' + # hence showing that both "filters" have been applied + self.assertThat(len(groups), matchers.Equals(2)) + self.assertIn(group_list[5]['id'], [groups[0]['id'], groups[1]['id']]) + self.assertIn(group_list[6]['id'], [groups[0]['id'], groups[1]['id']]) + self._delete_test_data('user', user_list) + self._delete_test_data('group', group_list) + + +class LimitTests(filtering.FilterTests): + ENTITIES = ['user', 'group', 'project'] + + def setUp(self): + """Setup for Limit Test Cases.""" + + self.domain1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(self.domain1['id'], self.domain1) + self.addCleanup(self.clean_up_domain) + + self.entity_lists = {} + self.domain1_entity_lists = {} + + for entity in self.ENTITIES: + # Create 20 entities, 14 of which are in domain1 + self.entity_lists[entity] = self._create_test_data(entity, 6) + self.domain1_entity_lists[entity] = self._create_test_data( + entity, 14, self.domain1['id']) + self.addCleanup(self.clean_up_entities) + + def clean_up_domain(self): + """Clean up domain test data from Limit Test Cases.""" + + self.domain1['enabled'] = False + self.resource_api.update_domain(self.domain1['id'], self.domain1) + self.resource_api.delete_domain(self.domain1['id']) + del self.domain1 + + def clean_up_entities(self): + """Clean up entity test data from Limit Test Cases.""" + for entity in self.ENTITIES: + self._delete_test_data(entity, self.entity_lists[entity]) + self._delete_test_data(entity, self.domain1_entity_lists[entity]) + del self.entity_lists + del self.domain1_entity_lists + + def _test_list_entity_filtered_and_limited(self, entity): + self.config_fixture.config(list_limit=10) + # Should get back just 10 entities in domain1 + hints = driver_hints.Hints() + hints.add_filter('domain_id', self.domain1['id']) + entities = self._list_entities(entity)(hints=hints) + self.assertEqual(hints.limit['limit'], len(entities)) + self.assertTrue(hints.limit['truncated']) + self._match_with_list(entities, self.domain1_entity_lists[entity]) + + # Override with driver specific limit + if entity == 'project': + self.config_fixture.config(group='resource', list_limit=5) + else: + self.config_fixture.config(group='identity', list_limit=5) + + # Should get back just 5 users in domain1 + hints = driver_hints.Hints() + hints.add_filter('domain_id', self.domain1['id']) + entities = self._list_entities(entity)(hints=hints) + self.assertEqual(hints.limit['limit'], len(entities)) + self._match_with_list(entities, self.domain1_entity_lists[entity]) + + # Finally, let's pretend we want to get the full list of entities, + # even with the limits set, as part of some internal calculation. + # Calling the API without a hints list should achieve this, and + # return at least the 20 entries we created (there may be other + # entities lying around created by other tests/setup). + entities = self._list_entities(entity)() + self.assertTrue(len(entities) >= 20) + + def test_list_users_filtered_and_limited(self): + self._test_list_entity_filtered_and_limited('user') + + def test_list_groups_filtered_and_limited(self): + self._test_list_entity_filtered_and_limited('group') + + def test_list_projects_filtered_and_limited(self): + self._test_list_entity_filtered_and_limited('project') diff --git a/keystone-moon/keystone/tests/unit/test_backend_endpoint_policy.py b/keystone-moon/keystone/tests/unit/test_backend_endpoint_policy.py new file mode 100644 index 00000000..cc41d977 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_backend_endpoint_policy.py @@ -0,0 +1,247 @@ +# Copyright 2014 IBM Corp. +# +# 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 testtools import matchers + +from keystone import exception + + +class PolicyAssociationTests(object): + + def _assert_correct_policy(self, endpoint, policy): + ref = ( + self.endpoint_policy_api.get_policy_for_endpoint(endpoint['id'])) + self.assertEqual(policy['id'], ref['id']) + + def _assert_correct_endpoints(self, policy, endpoint_list): + endpoint_id_list = [ep['id'] for ep in endpoint_list] + endpoints = ( + self.endpoint_policy_api.list_endpoints_for_policy(policy['id'])) + self.assertThat(endpoints, matchers.HasLength(len(endpoint_list))) + for endpoint in endpoints: + self.assertIn(endpoint['id'], endpoint_id_list) + + def load_sample_data(self): + """Create sample data to test policy associations. + + The following data is created: + + - 3 regions, in a hierarchy, 0 -> 1 -> 2 (where 0 is top) + - 3 services + - 6 endpoints, 2 in each region, with a mixture of services: + 0 - region 0, Service 0 + 1 - region 0, Service 1 + 2 - region 1, Service 1 + 3 - region 1, Service 2 + 4 - region 2, Service 2 + 5 - region 2, Service 0 + + """ + + def new_endpoint(region_id, service_id): + endpoint = {'id': uuid.uuid4().hex, 'interface': 'test', + 'region_id': region_id, 'service_id': service_id, + 'url': '/url'} + self.endpoint.append(self.catalog_api.create_endpoint( + endpoint['id'], endpoint)) + + self.policy = [] + self.endpoint = [] + self.service = [] + self.region = [] + for i in range(3): + policy = {'id': uuid.uuid4().hex, 'type': uuid.uuid4().hex, + 'blob': {'data': uuid.uuid4().hex}} + self.policy.append(self.policy_api.create_policy(policy['id'], + policy)) + service = {'id': uuid.uuid4().hex, 'type': uuid.uuid4().hex} + self.service.append(self.catalog_api.create_service(service['id'], + service)) + region = {'id': uuid.uuid4().hex, 'description': uuid.uuid4().hex} + # Link the 3 regions together as a hierarchy, [0] at the top + if i != 0: + region['parent_region_id'] = self.region[i - 1]['id'] + self.region.append(self.catalog_api.create_region(region)) + + new_endpoint(self.region[0]['id'], self.service[0]['id']) + new_endpoint(self.region[0]['id'], self.service[1]['id']) + new_endpoint(self.region[1]['id'], self.service[1]['id']) + new_endpoint(self.region[1]['id'], self.service[2]['id']) + new_endpoint(self.region[2]['id'], self.service[2]['id']) + new_endpoint(self.region[2]['id'], self.service[0]['id']) + + def test_policy_to_endpoint_association_crud(self): + self.endpoint_policy_api.create_policy_association( + self.policy[0]['id'], endpoint_id=self.endpoint[0]['id']) + self.endpoint_policy_api.check_policy_association( + self.policy[0]['id'], endpoint_id=self.endpoint[0]['id']) + self.endpoint_policy_api.delete_policy_association( + self.policy[0]['id'], endpoint_id=self.endpoint[0]['id']) + self.assertRaises(exception.NotFound, + self.endpoint_policy_api.check_policy_association, + self.policy[0]['id'], + endpoint_id=self.endpoint[0]['id']) + + def test_overwriting_policy_to_endpoint_association(self): + self.endpoint_policy_api.create_policy_association( + self.policy[0]['id'], endpoint_id=self.endpoint[0]['id']) + self.endpoint_policy_api.create_policy_association( + self.policy[1]['id'], endpoint_id=self.endpoint[0]['id']) + self.assertRaises(exception.NotFound, + self.endpoint_policy_api.check_policy_association, + self.policy[0]['id'], + endpoint_id=self.endpoint[0]['id']) + self.endpoint_policy_api.check_policy_association( + self.policy[1]['id'], endpoint_id=self.endpoint[0]['id']) + + def test_invalid_policy_to_endpoint_association(self): + self.assertRaises(exception.InvalidPolicyAssociation, + self.endpoint_policy_api.create_policy_association, + self.policy[0]['id']) + self.assertRaises(exception.InvalidPolicyAssociation, + self.endpoint_policy_api.create_policy_association, + self.policy[0]['id'], + endpoint_id=self.endpoint[0]['id'], + region_id=self.region[0]['id']) + self.assertRaises(exception.InvalidPolicyAssociation, + self.endpoint_policy_api.create_policy_association, + self.policy[0]['id'], + endpoint_id=self.endpoint[0]['id'], + service_id=self.service[0]['id']) + self.assertRaises(exception.InvalidPolicyAssociation, + self.endpoint_policy_api.create_policy_association, + self.policy[0]['id'], + region_id=self.region[0]['id']) + + def test_policy_to_explicit_endpoint_association(self): + # Associate policy 0 with endpoint 0 + self.endpoint_policy_api.create_policy_association( + self.policy[0]['id'], endpoint_id=self.endpoint[0]['id']) + self._assert_correct_policy(self.endpoint[0], self.policy[0]) + self._assert_correct_endpoints(self.policy[0], [self.endpoint[0]]) + self.assertRaises(exception.NotFound, + self.endpoint_policy_api.get_policy_for_endpoint, + uuid.uuid4().hex) + + def test_policy_to_service_association(self): + self.endpoint_policy_api.create_policy_association( + self.policy[0]['id'], service_id=self.service[0]['id']) + self.endpoint_policy_api.create_policy_association( + self.policy[1]['id'], service_id=self.service[1]['id']) + + # Endpoints 0 and 5 are part of service 0 + self._assert_correct_policy(self.endpoint[0], self.policy[0]) + self._assert_correct_policy(self.endpoint[5], self.policy[0]) + self._assert_correct_endpoints( + self.policy[0], [self.endpoint[0], self.endpoint[5]]) + + # Endpoints 1 and 2 are part of service 1 + self._assert_correct_policy(self.endpoint[1], self.policy[1]) + self._assert_correct_policy(self.endpoint[2], self.policy[1]) + self._assert_correct_endpoints( + self.policy[1], [self.endpoint[1], self.endpoint[2]]) + + def test_policy_to_region_and_service_association(self): + self.endpoint_policy_api.create_policy_association( + self.policy[0]['id'], service_id=self.service[0]['id'], + region_id=self.region[0]['id']) + self.endpoint_policy_api.create_policy_association( + self.policy[1]['id'], service_id=self.service[1]['id'], + region_id=self.region[1]['id']) + self.endpoint_policy_api.create_policy_association( + self.policy[2]['id'], service_id=self.service[2]['id'], + region_id=self.region[2]['id']) + + # Endpoint 0 is in region 0 with service 0, so should get policy 0 + self._assert_correct_policy(self.endpoint[0], self.policy[0]) + # Endpoint 5 is in Region 2 with service 0, so should also get + # policy 0 by searching up the tree to Region 0 + self._assert_correct_policy(self.endpoint[5], self.policy[0]) + + # Looking the other way round, policy 2 should only be in use by + # endpoint 4, since that's the only endpoint in region 2 with the + # correct service + self._assert_correct_endpoints( + self.policy[2], [self.endpoint[4]]) + # Policy 1 should only be in use by endpoint 2, since that's the only + # endpoint in region 1 (and region 2 below it) with the correct service + self._assert_correct_endpoints( + self.policy[1], [self.endpoint[2]]) + # Policy 0 should be in use by endpoint 0, as well as 5 (since 5 is + # of the correct service and in region 2 below it) + self._assert_correct_endpoints( + self.policy[0], [self.endpoint[0], self.endpoint[5]]) + + def test_delete_association_by_entity(self): + self.endpoint_policy_api.create_policy_association( + self.policy[0]['id'], endpoint_id=self.endpoint[0]['id']) + self.endpoint_policy_api.delete_association_by_endpoint( + self.endpoint[0]['id']) + self.assertRaises(exception.NotFound, + self.endpoint_policy_api.check_policy_association, + self.policy[0]['id'], + endpoint_id=self.endpoint[0]['id']) + # Make sure deleting it again is silent - since this method is used + # in response to notifications by the controller. + self.endpoint_policy_api.delete_association_by_endpoint( + self.endpoint[0]['id']) + + # Now try with service - ensure both combined region & service + # associations and explicit service ones are removed + self.endpoint_policy_api.create_policy_association( + self.policy[0]['id'], service_id=self.service[0]['id'], + region_id=self.region[0]['id']) + self.endpoint_policy_api.create_policy_association( + self.policy[1]['id'], service_id=self.service[0]['id'], + region_id=self.region[1]['id']) + self.endpoint_policy_api.create_policy_association( + self.policy[0]['id'], service_id=self.service[0]['id']) + + self.endpoint_policy_api.delete_association_by_service( + self.service[0]['id']) + + self.assertRaises(exception.NotFound, + self.endpoint_policy_api.check_policy_association, + self.policy[0]['id'], + service_id=self.service[0]['id'], + region_id=self.region[0]['id']) + self.assertRaises(exception.NotFound, + self.endpoint_policy_api.check_policy_association, + self.policy[1]['id'], + service_id=self.service[0]['id'], + region_id=self.region[1]['id']) + self.assertRaises(exception.NotFound, + self.endpoint_policy_api.check_policy_association, + self.policy[0]['id'], + service_id=self.service[0]['id']) + + # Finally, check delete by region + self.endpoint_policy_api.create_policy_association( + self.policy[0]['id'], service_id=self.service[0]['id'], + region_id=self.region[0]['id']) + + self.endpoint_policy_api.delete_association_by_region( + self.region[0]['id']) + + self.assertRaises(exception.NotFound, + self.endpoint_policy_api.check_policy_association, + self.policy[0]['id'], + service_id=self.service[0]['id'], + region_id=self.region[0]['id']) + self.assertRaises(exception.NotFound, + self.endpoint_policy_api.check_policy_association, + self.policy[0]['id'], + service_id=self.service[0]['id']) diff --git a/keystone-moon/keystone/tests/unit/test_backend_endpoint_policy_sql.py b/keystone-moon/keystone/tests/unit/test_backend_endpoint_policy_sql.py new file mode 100644 index 00000000..dab02859 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_backend_endpoint_policy_sql.py @@ -0,0 +1,37 @@ +# Copyright 2014 IBM Corp. +# +# 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 keystone.common import sql +from keystone.tests.unit import test_backend_endpoint_policy +from keystone.tests.unit import test_backend_sql + + +class SqlPolicyAssociationTable(test_backend_sql.SqlModels): + """Set of tests for checking SQL Policy Association Mapping.""" + + def test_policy_association_mapping(self): + cols = (('policy_id', sql.String, 64), + ('endpoint_id', sql.String, 64), + ('service_id', sql.String, 64), + ('region_id', sql.String, 64)) + self.assertExpectedSchema('policy_association', cols) + + +class SqlPolicyAssociationTests( + test_backend_sql.SqlTests, + test_backend_endpoint_policy.PolicyAssociationTests): + + def load_fixtures(self, fixtures): + super(SqlPolicyAssociationTests, self).load_fixtures(fixtures) + self.load_sample_data() diff --git a/keystone-moon/keystone/tests/unit/test_backend_federation_sql.py b/keystone-moon/keystone/tests/unit/test_backend_federation_sql.py new file mode 100644 index 00000000..48ebad6c --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_backend_federation_sql.py @@ -0,0 +1,46 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 keystone.common import sql +from keystone.tests.unit import test_backend_sql + + +class SqlFederation(test_backend_sql.SqlModels): + """Set of tests for checking SQL Federation.""" + + def test_identity_provider(self): + cols = (('id', sql.String, 64), + ('remote_id', sql.String, 256), + ('enabled', sql.Boolean, None), + ('description', sql.Text, None)) + self.assertExpectedSchema('identity_provider', cols) + + def test_federated_protocol(self): + cols = (('id', sql.String, 64), + ('idp_id', sql.String, 64), + ('mapping_id', sql.String, 64)) + self.assertExpectedSchema('federation_protocol', cols) + + def test_mapping(self): + cols = (('id', sql.String, 64), + ('rules', sql.JsonBlob, None)) + self.assertExpectedSchema('mapping', cols) + + def test_service_provider(self): + cols = (('auth_url', sql.String, 256), + ('id', sql.String, 64), + ('enabled', sql.Boolean, None), + ('description', sql.Text, None), + ('sp_url', sql.String, 256)) + self.assertExpectedSchema('service_provider', cols) diff --git a/keystone-moon/keystone/tests/unit/test_backend_id_mapping_sql.py b/keystone-moon/keystone/tests/unit/test_backend_id_mapping_sql.py new file mode 100644 index 00000000..6b691e5a --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_backend_id_mapping_sql.py @@ -0,0 +1,197 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 IBM Corp. +# +# 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 testtools import matchers + +from keystone.common import sql +from keystone.identity.mapping_backends import mapping +from keystone.tests.unit import identity_mapping as mapping_sql +from keystone.tests.unit import test_backend_sql + + +class SqlIDMappingTable(test_backend_sql.SqlModels): + """Set of tests for checking SQL Identity ID Mapping.""" + + def test_id_mapping(self): + cols = (('public_id', sql.String, 64), + ('domain_id', sql.String, 64), + ('local_id', sql.String, 64), + ('entity_type', sql.Enum, None)) + self.assertExpectedSchema('id_mapping', cols) + + +class SqlIDMapping(test_backend_sql.SqlTests): + + def setUp(self): + super(SqlIDMapping, self).setUp() + self.load_sample_data() + + def load_sample_data(self): + self.addCleanup(self.clean_sample_data) + domainA = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.domainA = self.resource_api.create_domain(domainA['id'], domainA) + domainB = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.domainB = self.resource_api.create_domain(domainB['id'], domainB) + + def clean_sample_data(self): + if hasattr(self, 'domainA'): + self.domainA['enabled'] = False + self.resource_api.update_domain(self.domainA['id'], self.domainA) + self.resource_api.delete_domain(self.domainA['id']) + if hasattr(self, 'domainB'): + self.domainB['enabled'] = False + self.resource_api.update_domain(self.domainB['id'], self.domainB) + self.resource_api.delete_domain(self.domainB['id']) + + def test_invalid_public_key(self): + self.assertIsNone(self.id_mapping_api.get_id_mapping(uuid.uuid4().hex)) + + def test_id_mapping_crud(self): + initial_mappings = len(mapping_sql.list_id_mappings()) + local_id1 = uuid.uuid4().hex + local_id2 = uuid.uuid4().hex + local_entity1 = {'domain_id': self.domainA['id'], + 'local_id': local_id1, + 'entity_type': mapping.EntityType.USER} + local_entity2 = {'domain_id': self.domainB['id'], + 'local_id': local_id2, + 'entity_type': mapping.EntityType.GROUP} + + # Check no mappings for the new local entities + self.assertIsNone(self.id_mapping_api.get_public_id(local_entity1)) + self.assertIsNone(self.id_mapping_api.get_public_id(local_entity2)) + + # Create the new mappings and then read them back + public_id1 = self.id_mapping_api.create_id_mapping(local_entity1) + public_id2 = self.id_mapping_api.create_id_mapping(local_entity2) + self.assertThat(mapping_sql.list_id_mappings(), + matchers.HasLength(initial_mappings + 2)) + self.assertEqual( + public_id1, self.id_mapping_api.get_public_id(local_entity1)) + self.assertEqual( + public_id2, self.id_mapping_api.get_public_id(local_entity2)) + + local_id_ref = self.id_mapping_api.get_id_mapping(public_id1) + self.assertEqual(self.domainA['id'], local_id_ref['domain_id']) + self.assertEqual(local_id1, local_id_ref['local_id']) + self.assertEqual(mapping.EntityType.USER, local_id_ref['entity_type']) + # Check we have really created a new external ID + self.assertNotEqual(local_id1, public_id1) + + local_id_ref = self.id_mapping_api.get_id_mapping(public_id2) + self.assertEqual(self.domainB['id'], local_id_ref['domain_id']) + self.assertEqual(local_id2, local_id_ref['local_id']) + self.assertEqual(mapping.EntityType.GROUP, local_id_ref['entity_type']) + # Check we have really created a new external ID + self.assertNotEqual(local_id2, public_id2) + + # Create another mappings, this time specifying a public ID to use + new_public_id = uuid.uuid4().hex + public_id3 = self.id_mapping_api.create_id_mapping( + {'domain_id': self.domainB['id'], 'local_id': local_id2, + 'entity_type': mapping.EntityType.USER}, + public_id=new_public_id) + self.assertEqual(new_public_id, public_id3) + self.assertThat(mapping_sql.list_id_mappings(), + matchers.HasLength(initial_mappings + 3)) + + # Delete the mappings we created, and make sure the mapping count + # goes back to where it was + self.id_mapping_api.delete_id_mapping(public_id1) + self.id_mapping_api.delete_id_mapping(public_id2) + self.id_mapping_api.delete_id_mapping(public_id3) + self.assertThat(mapping_sql.list_id_mappings(), + matchers.HasLength(initial_mappings)) + + def test_id_mapping_handles_unicode(self): + initial_mappings = len(mapping_sql.list_id_mappings()) + local_id = u'fäké1' + local_entity = {'domain_id': self.domainA['id'], + 'local_id': local_id, + 'entity_type': mapping.EntityType.USER} + + # Check no mappings for the new local entity + self.assertIsNone(self.id_mapping_api.get_public_id(local_entity)) + + # Create the new mapping and then read it back + public_id = self.id_mapping_api.create_id_mapping(local_entity) + self.assertThat(mapping_sql.list_id_mappings(), + matchers.HasLength(initial_mappings + 1)) + self.assertEqual( + public_id, self.id_mapping_api.get_public_id(local_entity)) + + def test_delete_public_id_is_silent(self): + # Test that deleting an invalid public key is silent + self.id_mapping_api.delete_id_mapping(uuid.uuid4().hex) + + def test_purge_mappings(self): + initial_mappings = len(mapping_sql.list_id_mappings()) + local_id1 = uuid.uuid4().hex + local_id2 = uuid.uuid4().hex + local_id3 = uuid.uuid4().hex + local_id4 = uuid.uuid4().hex + local_id5 = uuid.uuid4().hex + + # Create five mappings,two in domainA, three in domainB + self.id_mapping_api.create_id_mapping( + {'domain_id': self.domainA['id'], 'local_id': local_id1, + 'entity_type': mapping.EntityType.USER}) + self.id_mapping_api.create_id_mapping( + {'domain_id': self.domainA['id'], 'local_id': local_id2, + 'entity_type': mapping.EntityType.USER}) + public_id3 = self.id_mapping_api.create_id_mapping( + {'domain_id': self.domainB['id'], 'local_id': local_id3, + 'entity_type': mapping.EntityType.GROUP}) + public_id4 = self.id_mapping_api.create_id_mapping( + {'domain_id': self.domainB['id'], 'local_id': local_id4, + 'entity_type': mapping.EntityType.USER}) + public_id5 = self.id_mapping_api.create_id_mapping( + {'domain_id': self.domainB['id'], 'local_id': local_id5, + 'entity_type': mapping.EntityType.USER}) + + self.assertThat(mapping_sql.list_id_mappings(), + matchers.HasLength(initial_mappings + 5)) + + # Purge mappings for domainA, should be left with those in B + self.id_mapping_api.purge_mappings( + {'domain_id': self.domainA['id']}) + self.assertThat(mapping_sql.list_id_mappings(), + matchers.HasLength(initial_mappings + 3)) + self.id_mapping_api.get_id_mapping(public_id3) + self.id_mapping_api.get_id_mapping(public_id4) + self.id_mapping_api.get_id_mapping(public_id5) + + # Purge mappings for type Group, should purge one more + self.id_mapping_api.purge_mappings( + {'entity_type': mapping.EntityType.GROUP}) + self.assertThat(mapping_sql.list_id_mappings(), + matchers.HasLength(initial_mappings + 2)) + self.id_mapping_api.get_id_mapping(public_id4) + self.id_mapping_api.get_id_mapping(public_id5) + + # Purge mapping for a specific local identifier + self.id_mapping_api.purge_mappings( + {'domain_id': self.domainB['id'], 'local_id': local_id4, + 'entity_type': mapping.EntityType.USER}) + self.assertThat(mapping_sql.list_id_mappings(), + matchers.HasLength(initial_mappings + 1)) + self.id_mapping_api.get_id_mapping(public_id5) + + # Purge mappings the remaining mappings + self.id_mapping_api.purge_mappings({}) + self.assertThat(mapping_sql.list_id_mappings(), + matchers.HasLength(initial_mappings)) diff --git a/keystone-moon/keystone/tests/unit/test_backend_kvs.py b/keystone-moon/keystone/tests/unit/test_backend_kvs.py new file mode 100644 index 00000000..c0997ad9 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_backend_kvs.py @@ -0,0 +1,172 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 datetime +import uuid + +from oslo_config import cfg +from oslo_utils import timeutils +import six + +from keystone import exception +from keystone.tests import unit as tests +from keystone.tests.unit import test_backend + + +CONF = cfg.CONF + + +class KvsToken(tests.TestCase, test_backend.TokenTests): + def setUp(self): + super(KvsToken, self).setUp() + self.load_backends() + + def test_flush_expired_token(self): + self.assertRaises( + exception.NotImplemented, + self.token_provider_api._persistence.flush_expired_tokens) + + def _update_user_token_index_direct(self, user_key, token_id, new_data): + persistence = self.token_provider_api._persistence + token_list = persistence.driver._get_user_token_list_with_expiry( + user_key) + # Update the user-index so that the expires time is _actually_ expired + # since we do not do an explicit get on the token, we only reference + # the data in the user index (to save extra round-trips to the kvs + # backend). + for i, data in enumerate(token_list): + if data[0] == token_id: + token_list[i] = new_data + break + self.token_provider_api._persistence.driver._store.set(user_key, + token_list) + + def test_cleanup_user_index_on_create(self): + user_id = six.text_type(uuid.uuid4().hex) + valid_token_id, data = self.create_token_sample_data(user_id=user_id) + expired_token_id, expired_data = self.create_token_sample_data( + user_id=user_id) + + expire_delta = datetime.timedelta(seconds=86400) + + # NOTE(morganfainberg): Directly access the data cache since we need to + # get expired tokens as well as valid tokens. + token_persistence = self.token_provider_api._persistence + user_key = token_persistence.driver._prefix_user_id(user_id) + user_token_list = token_persistence.driver._store.get(user_key) + valid_token_ref = token_persistence.get_token(valid_token_id) + expired_token_ref = token_persistence.get_token(expired_token_id) + expected_user_token_list = [ + (valid_token_id, timeutils.isotime(valid_token_ref['expires'], + subsecond=True)), + (expired_token_id, timeutils.isotime(expired_token_ref['expires'], + subsecond=True))] + self.assertEqual(expected_user_token_list, user_token_list) + new_expired_data = (expired_token_id, + timeutils.isotime( + (timeutils.utcnow() - expire_delta), + subsecond=True)) + self._update_user_token_index_direct(user_key, expired_token_id, + new_expired_data) + valid_token_id_2, valid_data_2 = self.create_token_sample_data( + user_id=user_id) + valid_token_ref_2 = token_persistence.get_token(valid_token_id_2) + expected_user_token_list = [ + (valid_token_id, timeutils.isotime(valid_token_ref['expires'], + subsecond=True)), + (valid_token_id_2, timeutils.isotime(valid_token_ref_2['expires'], + subsecond=True))] + user_token_list = token_persistence.driver._store.get(user_key) + self.assertEqual(expected_user_token_list, user_token_list) + + # Test that revoked tokens are removed from the list on create. + token_persistence.delete_token(valid_token_id_2) + new_token_id, data = self.create_token_sample_data(user_id=user_id) + new_token_ref = token_persistence.get_token(new_token_id) + expected_user_token_list = [ + (valid_token_id, timeutils.isotime(valid_token_ref['expires'], + subsecond=True)), + (new_token_id, timeutils.isotime(new_token_ref['expires'], + subsecond=True))] + user_token_list = token_persistence.driver._store.get(user_key) + self.assertEqual(expected_user_token_list, user_token_list) + + +class KvsCatalog(tests.TestCase, test_backend.CatalogTests): + def setUp(self): + super(KvsCatalog, self).setUp() + self.load_backends() + self._load_fake_catalog() + + def config_overrides(self): + super(KvsCatalog, self).config_overrides() + self.config_fixture.config( + group='catalog', + driver='keystone.catalog.backends.kvs.Catalog') + + def _load_fake_catalog(self): + self.catalog_foobar = self.catalog_api.driver._create_catalog( + 'foo', 'bar', + {'RegionFoo': {'service_bar': {'foo': 'bar'}}}) + + def test_get_catalog_404(self): + # FIXME(dolph): this test should be moved up to test_backend + # FIXME(dolph): exceptions should be UserNotFound and ProjectNotFound + self.assertRaises(exception.NotFound, + self.catalog_api.get_catalog, + uuid.uuid4().hex, + 'bar') + + self.assertRaises(exception.NotFound, + self.catalog_api.get_catalog, + 'foo', + uuid.uuid4().hex) + + def test_get_catalog(self): + catalog_ref = self.catalog_api.get_catalog('foo', 'bar') + self.assertDictEqual(catalog_ref, self.catalog_foobar) + + def test_get_catalog_endpoint_disabled(self): + # This test doesn't apply to KVS because with the KVS backend the + # application creates the catalog (including the endpoints) for each + # user and project. Whether endpoints are enabled or disabled isn't + # a consideration. + f = super(KvsCatalog, self).test_get_catalog_endpoint_disabled + self.assertRaises(exception.NotFound, f) + + def test_get_v3_catalog_endpoint_disabled(self): + # There's no need to have disabled endpoints in the kvs catalog. Those + # endpoints should just be removed from the store. This just tests + # what happens currently when the super impl is called. + f = super(KvsCatalog, self).test_get_v3_catalog_endpoint_disabled + self.assertRaises(exception.NotFound, f) + + def test_list_regions_filtered_by_parent_region_id(self): + self.skipTest('KVS backend does not support hints') + + def test_service_filtering(self): + self.skipTest("kvs backend doesn't support filtering") + + +class KvsTokenCacheInvalidation(tests.TestCase, + test_backend.TokenCacheInvalidation): + def setUp(self): + super(KvsTokenCacheInvalidation, self).setUp() + self.load_backends() + self._create_test_data() + + def config_overrides(self): + super(KvsTokenCacheInvalidation, self).config_overrides() + self.config_fixture.config( + group='token', + driver='keystone.token.persistence.backends.kvs.Token') diff --git a/keystone-moon/keystone/tests/unit/test_backend_ldap.py b/keystone-moon/keystone/tests/unit/test_backend_ldap.py new file mode 100644 index 00000000..10119808 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_backend_ldap.py @@ -0,0 +1,3049 @@ +# -*- coding: utf-8 -*- +# Copyright 2012 OpenStack Foundation +# Copyright 2013 IBM Corp. +# +# 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 +import uuid + +import ldap +import mock +from oslo_config import cfg +from testtools import matchers + +from keystone.common import cache +from keystone.common import ldap as common_ldap +from keystone.common.ldap import core as common_ldap_core +from keystone.common import sql +from keystone import exception +from keystone import identity +from keystone.identity.mapping_backends import mapping as map +from keystone import resource +from keystone.tests import unit as tests +from keystone.tests.unit import default_fixtures +from keystone.tests.unit import fakeldap +from keystone.tests.unit import identity_mapping as mapping_sql +from keystone.tests.unit.ksfixtures import database +from keystone.tests.unit import test_backend + + +CONF = cfg.CONF + + +def create_group_container(identity_api): + # Create the groups base entry (ou=Groups,cn=example,cn=com) + group_api = identity_api.driver.group + conn = group_api.get_connection() + dn = 'ou=Groups,cn=example,cn=com' + conn.add_s(dn, [('objectclass', ['organizationalUnit']), + ('ou', ['Groups'])]) + + +class BaseLDAPIdentity(test_backend.IdentityTests): + + def setUp(self): + super(BaseLDAPIdentity, self).setUp() + self.clear_database() + + common_ldap.register_handler('fake://', fakeldap.FakeLdap) + self.load_backends() + self.load_fixtures(default_fixtures) + + self.addCleanup(common_ldap_core._HANDLERS.clear) + + def _get_domain_fixture(self): + """Domains in LDAP are read-only, so just return the static one.""" + return self.resource_api.get_domain(CONF.identity.default_domain_id) + + def clear_database(self): + for shelf in fakeldap.FakeShelves: + fakeldap.FakeShelves[shelf].clear() + + def reload_backends(self, domain_id): + # Only one backend unless we are using separate domain backends + self.load_backends() + + def get_config(self, domain_id): + # Only one conf structure unless we are using separate domain backends + return CONF + + def config_overrides(self): + super(BaseLDAPIdentity, self).config_overrides() + self.config_fixture.config( + group='identity', + driver='keystone.identity.backends.ldap.Identity') + + def config_files(self): + config_files = super(BaseLDAPIdentity, self).config_files() + config_files.append(tests.dirs.tests_conf('backend_ldap.conf')) + return config_files + + def get_user_enabled_vals(self, user): + user_dn = ( + self.identity_api.driver.user._id_to_dn_string(user['id'])) + enabled_attr_name = CONF.ldap.user_enabled_attribute + + ldap_ = self.identity_api.driver.user.get_connection() + res = ldap_.search_s(user_dn, + ldap.SCOPE_BASE, + u'(sn=%s)' % user['name']) + if enabled_attr_name in res[0][1]: + return res[0][1][enabled_attr_name] + else: + return None + + def test_build_tree(self): + """Regression test for building the tree names + """ + user_api = identity.backends.ldap.UserApi(CONF) + self.assertTrue(user_api) + self.assertEqual("ou=Users,%s" % CONF.ldap.suffix, user_api.tree_dn) + + def test_configurable_allowed_user_actions(self): + user = {'name': u'fäké1', + 'password': u'fäképass1', + 'domain_id': CONF.identity.default_domain_id, + 'tenants': ['bar']} + user = self.identity_api.create_user(user) + self.identity_api.get_user(user['id']) + + user['password'] = u'fäképass2' + self.identity_api.update_user(user['id'], user) + + self.identity_api.delete_user(user['id']) + self.assertRaises(exception.UserNotFound, + self.identity_api.get_user, + user['id']) + + def test_configurable_forbidden_user_actions(self): + conf = self.get_config(CONF.identity.default_domain_id) + conf.ldap.user_allow_create = False + conf.ldap.user_allow_update = False + conf.ldap.user_allow_delete = False + self.reload_backends(CONF.identity.default_domain_id) + + user = {'name': u'fäké1', + 'password': u'fäképass1', + 'domain_id': CONF.identity.default_domain_id, + 'tenants': ['bar']} + self.assertRaises(exception.ForbiddenAction, + self.identity_api.create_user, + user) + + self.user_foo['password'] = u'fäképass2' + self.assertRaises(exception.ForbiddenAction, + self.identity_api.update_user, + self.user_foo['id'], + self.user_foo) + + self.assertRaises(exception.ForbiddenAction, + self.identity_api.delete_user, + self.user_foo['id']) + + def test_configurable_forbidden_create_existing_user(self): + conf = self.get_config(CONF.identity.default_domain_id) + conf.ldap.user_allow_create = False + self.reload_backends(CONF.identity.default_domain_id) + + self.assertRaises(exception.ForbiddenAction, + self.identity_api.create_user, + self.user_foo) + + def test_user_filter(self): + user_ref = self.identity_api.get_user(self.user_foo['id']) + self.user_foo.pop('password') + self.assertDictEqual(user_ref, self.user_foo) + + conf = self.get_config(user_ref['domain_id']) + conf.ldap.user_filter = '(CN=DOES_NOT_MATCH)' + self.reload_backends(user_ref['domain_id']) + # invalidate the cache if the result is cached. + self.identity_api.get_user.invalidate(self.identity_api, + self.user_foo['id']) + self.assertRaises(exception.UserNotFound, + self.identity_api.get_user, + self.user_foo['id']) + + def test_remove_role_grant_from_user_and_project(self): + self.assignment_api.create_grant(user_id=self.user_foo['id'], + project_id=self.tenant_baz['id'], + role_id='member') + roles_ref = self.assignment_api.list_grants( + user_id=self.user_foo['id'], + project_id=self.tenant_baz['id']) + self.assertDictEqual(roles_ref[0], self.role_member) + + self.assignment_api.delete_grant(user_id=self.user_foo['id'], + project_id=self.tenant_baz['id'], + role_id='member') + roles_ref = self.assignment_api.list_grants( + user_id=self.user_foo['id'], + project_id=self.tenant_baz['id']) + self.assertEqual(0, len(roles_ref)) + self.assertRaises(exception.RoleAssignmentNotFound, + self.assignment_api.delete_grant, + user_id=self.user_foo['id'], + project_id=self.tenant_baz['id'], + role_id='member') + + def test_get_and_remove_role_grant_by_group_and_project(self): + new_domain = self._get_domain_fixture() + new_group = {'domain_id': new_domain['id'], + 'name': uuid.uuid4().hex} + new_group = self.identity_api.create_group(new_group) + new_user = {'name': 'new_user', 'enabled': True, + 'domain_id': new_domain['id']} + new_user = self.identity_api.create_user(new_user) + self.identity_api.add_user_to_group(new_user['id'], + new_group['id']) + + roles_ref = self.assignment_api.list_grants( + group_id=new_group['id'], + project_id=self.tenant_bar['id']) + self.assertEqual([], roles_ref) + self.assertEqual(0, len(roles_ref)) + + self.assignment_api.create_grant(group_id=new_group['id'], + project_id=self.tenant_bar['id'], + role_id='member') + roles_ref = self.assignment_api.list_grants( + group_id=new_group['id'], + project_id=self.tenant_bar['id']) + self.assertNotEmpty(roles_ref) + self.assertDictEqual(roles_ref[0], self.role_member) + + self.assignment_api.delete_grant(group_id=new_group['id'], + project_id=self.tenant_bar['id'], + role_id='member') + roles_ref = self.assignment_api.list_grants( + group_id=new_group['id'], + project_id=self.tenant_bar['id']) + self.assertEqual(0, len(roles_ref)) + self.assertRaises(exception.RoleAssignmentNotFound, + self.assignment_api.delete_grant, + group_id=new_group['id'], + project_id=self.tenant_bar['id'], + role_id='member') + + def test_get_and_remove_role_grant_by_group_and_domain(self): + self.skipTest('N/A: LDAP does not support multiple domains') + + def test_get_role_assignment_by_domain_not_found(self): + self.skipTest('N/A: LDAP does not support multiple domains') + + def test_del_role_assignment_by_domain_not_found(self): + self.skipTest('N/A: LDAP does not support multiple domains') + + def test_get_and_remove_role_grant_by_user_and_domain(self): + self.skipTest('N/A: LDAP does not support multiple domains') + + def test_get_and_remove_correct_role_grant_from_a_mix(self): + self.skipTest('Blocked by bug 1101287') + + def test_get_and_remove_role_grant_by_group_and_cross_domain(self): + self.skipTest('N/A: LDAP does not support multiple domains') + + def test_get_and_remove_role_grant_by_user_and_cross_domain(self): + self.skipTest('N/A: LDAP does not support multiple domains') + + def test_role_grant_by_group_and_cross_domain_project(self): + self.skipTest('N/A: LDAP does not support multiple domains') + + def test_role_grant_by_user_and_cross_domain_project(self): + self.skipTest('N/A: LDAP does not support multiple domains') + + def test_multi_role_grant_by_user_group_on_project_domain(self): + self.skipTest('N/A: LDAP does not support multiple domains') + + def test_delete_role_with_user_and_group_grants(self): + self.skipTest('Blocked by bug 1101287') + + def test_delete_user_with_group_project_domain_links(self): + self.skipTest('N/A: LDAP does not support multiple domains') + + def test_delete_group_with_user_project_domain_links(self): + self.skipTest('N/A: LDAP does not support multiple domains') + + def test_list_projects_for_user(self): + domain = self._get_domain_fixture() + user1 = {'name': uuid.uuid4().hex, 'password': uuid.uuid4().hex, + 'domain_id': domain['id'], 'enabled': True} + user1 = self.identity_api.create_user(user1) + user_projects = self.assignment_api.list_projects_for_user(user1['id']) + self.assertThat(user_projects, matchers.HasLength(0)) + + # new grant(user1, role_member, tenant_bar) + self.assignment_api.create_grant(user_id=user1['id'], + project_id=self.tenant_bar['id'], + role_id=self.role_member['id']) + # new grant(user1, role_member, tenant_baz) + self.assignment_api.create_grant(user_id=user1['id'], + project_id=self.tenant_baz['id'], + role_id=self.role_member['id']) + user_projects = self.assignment_api.list_projects_for_user(user1['id']) + self.assertThat(user_projects, matchers.HasLength(2)) + + # Now, check number of projects through groups + user2 = {'name': uuid.uuid4().hex, 'password': uuid.uuid4().hex, + 'domain_id': domain['id'], 'enabled': True} + user2 = self.identity_api.create_user(user2) + + group1 = {'name': uuid.uuid4().hex, 'domain_id': domain['id']} + group1 = self.identity_api.create_group(group1) + + self.identity_api.add_user_to_group(user2['id'], group1['id']) + + # new grant(group1(user2), role_member, tenant_bar) + self.assignment_api.create_grant(group_id=group1['id'], + project_id=self.tenant_bar['id'], + role_id=self.role_member['id']) + # new grant(group1(user2), role_member, tenant_baz) + self.assignment_api.create_grant(group_id=group1['id'], + project_id=self.tenant_baz['id'], + role_id=self.role_member['id']) + user_projects = self.assignment_api.list_projects_for_user(user2['id']) + self.assertThat(user_projects, matchers.HasLength(2)) + + # new grant(group1(user2), role_other, tenant_bar) + self.assignment_api.create_grant(group_id=group1['id'], + project_id=self.tenant_bar['id'], + role_id=self.role_other['id']) + user_projects = self.assignment_api.list_projects_for_user(user2['id']) + self.assertThat(user_projects, matchers.HasLength(2)) + + def test_list_projects_for_user_and_groups(self): + domain = self._get_domain_fixture() + # Create user1 + user1 = {'name': uuid.uuid4().hex, 'password': uuid.uuid4().hex, + 'domain_id': domain['id'], 'enabled': True} + user1 = self.identity_api.create_user(user1) + + # Create new group for user1 + group1 = {'name': uuid.uuid4().hex, 'domain_id': domain['id']} + group1 = self.identity_api.create_group(group1) + + # Add user1 to group1 + self.identity_api.add_user_to_group(user1['id'], group1['id']) + + # Now, add grant to user1 and group1 in tenant_bar + self.assignment_api.create_grant(user_id=user1['id'], + project_id=self.tenant_bar['id'], + role_id=self.role_member['id']) + self.assignment_api.create_grant(group_id=group1['id'], + project_id=self.tenant_bar['id'], + role_id=self.role_member['id']) + + # The result is user1 has only one project granted + user_projects = self.assignment_api.list_projects_for_user(user1['id']) + self.assertThat(user_projects, matchers.HasLength(1)) + + # Now, delete user1 grant into tenant_bar and check + self.assignment_api.delete_grant(user_id=user1['id'], + project_id=self.tenant_bar['id'], + role_id=self.role_member['id']) + + # The result is user1 has only one project granted. + # Granted through group1. + user_projects = self.assignment_api.list_projects_for_user(user1['id']) + self.assertThat(user_projects, matchers.HasLength(1)) + + def test_list_projects_for_user_with_grants(self): + domain = self._get_domain_fixture() + new_user = {'name': 'new_user', 'password': uuid.uuid4().hex, + 'enabled': True, 'domain_id': domain['id']} + new_user = self.identity_api.create_user(new_user) + + group1 = {'name': uuid.uuid4().hex, 'domain_id': domain['id']} + group1 = self.identity_api.create_group(group1) + group2 = {'name': uuid.uuid4().hex, 'domain_id': domain['id']} + group2 = self.identity_api.create_group(group2) + + project1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'domain_id': domain['id']} + self.resource_api.create_project(project1['id'], project1) + project2 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'domain_id': domain['id']} + self.resource_api.create_project(project2['id'], project2) + + self.identity_api.add_user_to_group(new_user['id'], + group1['id']) + self.identity_api.add_user_to_group(new_user['id'], + group2['id']) + + self.assignment_api.create_grant(user_id=new_user['id'], + project_id=self.tenant_bar['id'], + role_id=self.role_member['id']) + self.assignment_api.create_grant(user_id=new_user['id'], + project_id=project1['id'], + role_id=self.role_admin['id']) + self.assignment_api.create_grant(group_id=group2['id'], + project_id=project2['id'], + role_id=self.role_admin['id']) + + user_projects = self.assignment_api.list_projects_for_user( + new_user['id']) + self.assertEqual(3, len(user_projects)) + + def test_create_duplicate_user_name_in_different_domains(self): + self.skipTest('Domains are read-only against LDAP') + + def test_create_duplicate_project_name_in_different_domains(self): + self.skipTest('Domains are read-only against LDAP') + + def test_create_duplicate_group_name_in_different_domains(self): + self.skipTest( + 'N/A: LDAP does not support multiple domains') + + def test_move_user_between_domains(self): + self.skipTest('Domains are read-only against LDAP') + + def test_move_user_between_domains_with_clashing_names_fails(self): + self.skipTest('Domains are read-only against LDAP') + + def test_move_group_between_domains(self): + self.skipTest( + 'N/A: LDAP does not support multiple domains') + + def test_move_group_between_domains_with_clashing_names_fails(self): + self.skipTest('Domains are read-only against LDAP') + + def test_move_project_between_domains(self): + self.skipTest('Domains are read-only against LDAP') + + def test_move_project_between_domains_with_clashing_names_fails(self): + self.skipTest('Domains are read-only against LDAP') + + def test_get_roles_for_user_and_domain(self): + self.skipTest('N/A: LDAP does not support multiple domains') + + def test_get_roles_for_groups_on_domain(self): + self.skipTest('Blocked by bug: 1390125') + + def test_get_roles_for_groups_on_project(self): + self.skipTest('Blocked by bug: 1390125') + + def test_list_domains_for_groups(self): + self.skipTest('N/A: LDAP does not support multiple domains') + + def test_list_projects_for_groups(self): + self.skipTest('Blocked by bug: 1390125') + + def test_domain_delete_hierarchy(self): + self.skipTest('Domains are read-only against LDAP') + + def test_list_role_assignments_unfiltered(self): + new_domain = self._get_domain_fixture() + new_user = {'name': uuid.uuid4().hex, 'password': uuid.uuid4().hex, + 'enabled': True, 'domain_id': new_domain['id']} + new_user = self.identity_api.create_user(new_user) + new_group = {'domain_id': new_domain['id'], 'name': uuid.uuid4().hex} + new_group = self.identity_api.create_group(new_group) + new_project = {'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'domain_id': new_domain['id']} + self.resource_api.create_project(new_project['id'], new_project) + + # First check how many role grant already exist + existing_assignments = len(self.assignment_api.list_role_assignments()) + + self.assignment_api.create_grant(user_id=new_user['id'], + project_id=new_project['id'], + role_id='other') + self.assignment_api.create_grant(group_id=new_group['id'], + project_id=new_project['id'], + role_id='admin') + + # Read back the list of assignments - check it is gone up by 2 + after_assignments = len(self.assignment_api.list_role_assignments()) + self.assertEqual(existing_assignments + 2, after_assignments) + + def test_list_role_assignments_dumb_member(self): + self.config_fixture.config(group='ldap', use_dumb_member=True) + self.clear_database() + self.load_backends() + self.load_fixtures(default_fixtures) + + new_domain = self._get_domain_fixture() + new_user = {'name': uuid.uuid4().hex, 'password': uuid.uuid4().hex, + 'enabled': True, 'domain_id': new_domain['id']} + new_user = self.identity_api.create_user(new_user) + new_project = {'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'domain_id': new_domain['id']} + self.resource_api.create_project(new_project['id'], new_project) + self.assignment_api.create_grant(user_id=new_user['id'], + project_id=new_project['id'], + role_id='other') + + # Read back the list of assignments and ensure + # that the LDAP dumb member isn't listed. + assignment_ids = [a['user_id'] for a in + self.assignment_api.list_role_assignments()] + dumb_id = common_ldap.BaseLdap._dn_to_id(CONF.ldap.dumb_member) + self.assertNotIn(dumb_id, assignment_ids) + + def test_list_user_ids_for_project_dumb_member(self): + self.config_fixture.config(group='ldap', use_dumb_member=True) + self.clear_database() + self.load_backends() + self.load_fixtures(default_fixtures) + + user = {'name': uuid.uuid4().hex, 'password': uuid.uuid4().hex, + 'enabled': True, 'domain_id': test_backend.DEFAULT_DOMAIN_ID} + + user = self.identity_api.create_user(user) + self.assignment_api.add_user_to_project(self.tenant_baz['id'], + user['id']) + user_ids = self.assignment_api.list_user_ids_for_project( + self.tenant_baz['id']) + + self.assertIn(user['id'], user_ids) + + dumb_id = common_ldap.BaseLdap._dn_to_id(CONF.ldap.dumb_member) + self.assertNotIn(dumb_id, user_ids) + + def test_multi_group_grants_on_project_domain(self): + self.skipTest('Blocked by bug 1101287') + + def test_list_group_members_missing_entry(self): + """List group members with deleted user. + + If a group has a deleted entry for a member, the non-deleted members + are returned. + + """ + + # Create a group + group = dict(name=uuid.uuid4().hex, + domain_id=CONF.identity.default_domain_id) + group_id = self.identity_api.create_group(group)['id'] + + # Create a couple of users and add them to the group. + user = dict(name=uuid.uuid4().hex, + domain_id=CONF.identity.default_domain_id) + user_1_id = self.identity_api.create_user(user)['id'] + + self.identity_api.add_user_to_group(user_1_id, group_id) + + user = dict(name=uuid.uuid4().hex, + domain_id=CONF.identity.default_domain_id) + user_2_id = self.identity_api.create_user(user)['id'] + + self.identity_api.add_user_to_group(user_2_id, group_id) + + # Delete user 2 + # NOTE(blk-u): need to go directly to user interface to keep from + # updating the group. + unused, driver, entity_id = ( + self.identity_api._get_domain_driver_and_entity_id(user_2_id)) + driver.user.delete(entity_id) + + # List group users and verify only user 1. + res = self.identity_api.list_users_in_group(group_id) + + self.assertEqual(1, len(res), "Expected 1 entry (user_1)") + self.assertEqual(user_1_id, res[0]['id'], "Expected user 1 id") + + def test_list_group_members_when_no_members(self): + # List group members when there is no member in the group. + # No exception should be raised. + group = { + 'domain_id': CONF.identity.default_domain_id, + 'name': uuid.uuid4().hex, + 'description': uuid.uuid4().hex} + group = self.identity_api.create_group(group) + + # If this doesn't raise, then the test is successful. + self.identity_api.list_users_in_group(group['id']) + + def test_list_group_members_dumb_member(self): + self.config_fixture.config(group='ldap', use_dumb_member=True) + self.clear_database() + self.load_backends() + self.load_fixtures(default_fixtures) + + # Create a group + group = dict(name=uuid.uuid4().hex, + domain_id=CONF.identity.default_domain_id) + group_id = self.identity_api.create_group(group)['id'] + + # Create a user + user = dict(name=uuid.uuid4().hex, + domain_id=CONF.identity.default_domain_id) + user_id = self.identity_api.create_user(user)['id'] + + # Add user to the group + self.identity_api.add_user_to_group(user_id, group_id) + + user_ids = self.identity_api.list_users_in_group(group_id) + dumb_id = common_ldap.BaseLdap._dn_to_id(CONF.ldap.dumb_member) + + self.assertNotIn(dumb_id, user_ids) + + def test_list_domains(self): + domains = self.resource_api.list_domains() + self.assertEqual( + [resource.calc_default_domain()], + domains) + + def test_list_domains_non_default_domain_id(self): + # If change the default_domain_id, the ID of the default domain + # returned by list_domains changes is the new default_domain_id. + + new_domain_id = uuid.uuid4().hex + self.config_fixture.config(group='identity', + default_domain_id=new_domain_id) + + domains = self.resource_api.list_domains() + + self.assertEqual(new_domain_id, domains[0]['id']) + + def test_authenticate_requires_simple_bind(self): + user = { + 'name': 'NO_META', + 'domain_id': test_backend.DEFAULT_DOMAIN_ID, + 'password': 'no_meta2', + 'enabled': True, + } + user = self.identity_api.create_user(user) + self.assignment_api.add_user_to_project(self.tenant_baz['id'], + user['id']) + driver = self.identity_api._select_identity_driver( + user['domain_id']) + driver.user.LDAP_USER = None + driver.user.LDAP_PASSWORD = None + + self.assertRaises(AssertionError, + self.identity_api.authenticate, + context={}, + user_id=user['id'], + password=None) + + # (spzala)The group and domain crud tests below override the standard ones + # in test_backend.py so that we can exclude the update name test, since we + # do not yet support the update of either group or domain names with LDAP. + # In the tests below, the update is demonstrated by updating description. + # Refer to bug 1136403 for more detail. + def test_group_crud(self): + group = { + 'domain_id': CONF.identity.default_domain_id, + 'name': uuid.uuid4().hex, + 'description': uuid.uuid4().hex} + group = self.identity_api.create_group(group) + group_ref = self.identity_api.get_group(group['id']) + self.assertDictEqual(group_ref, group) + group['description'] = uuid.uuid4().hex + self.identity_api.update_group(group['id'], group) + group_ref = self.identity_api.get_group(group['id']) + self.assertDictEqual(group_ref, group) + + self.identity_api.delete_group(group['id']) + self.assertRaises(exception.GroupNotFound, + self.identity_api.get_group, + group['id']) + + @tests.skip_if_cache_disabled('identity') + def test_cache_layer_group_crud(self): + group = { + 'domain_id': CONF.identity.default_domain_id, + 'name': uuid.uuid4().hex} + group = self.identity_api.create_group(group) + # cache the result + group_ref = self.identity_api.get_group(group['id']) + # delete the group bypassing identity api. + domain_id, driver, entity_id = ( + self.identity_api._get_domain_driver_and_entity_id(group['id'])) + driver.delete_group(entity_id) + + self.assertEqual(group_ref, + self.identity_api.get_group(group['id'])) + self.identity_api.get_group.invalidate(self.identity_api, group['id']) + self.assertRaises(exception.GroupNotFound, + self.identity_api.get_group, group['id']) + + group = { + 'domain_id': CONF.identity.default_domain_id, + 'name': uuid.uuid4().hex} + group = self.identity_api.create_group(group) + # cache the result + self.identity_api.get_group(group['id']) + group['description'] = uuid.uuid4().hex + group_ref = self.identity_api.update_group(group['id'], group) + self.assertDictContainsSubset(self.identity_api.get_group(group['id']), + group_ref) + + def test_create_user_none_mapping(self): + # When create a user where an attribute maps to None, the entry is + # created without that attribute and it doesn't fail with a TypeError. + conf = self.get_config(CONF.identity.default_domain_id) + conf.ldap.user_attribute_ignore = ['enabled', 'email', + 'tenants', 'tenantId'] + self.reload_backends(CONF.identity.default_domain_id) + + user = {'name': u'fäké1', + 'password': u'fäképass1', + 'domain_id': CONF.identity.default_domain_id, + 'default_project_id': 'maps_to_none', + } + + # If this doesn't raise, then the test is successful. + user = self.identity_api.create_user(user) + + def test_create_user_with_boolean_string_names(self): + # Ensure that any attribute that is equal to the string 'TRUE' + # or 'FALSE' will not be converted to a boolean value, it + # should be returned as is. + boolean_strings = ['TRUE', 'FALSE', 'true', 'false', 'True', 'False', + 'TrUe' 'FaLse'] + for name in boolean_strings: + user = { + 'name': name, + 'domain_id': CONF.identity.default_domain_id} + user_ref = self.identity_api.create_user(user) + user_info = self.identity_api.get_user(user_ref['id']) + self.assertEqual(name, user_info['name']) + # Delete the user to ensure that the Keystone uniqueness + # requirements combined with the case-insensitive nature of a + # typical LDAP schema does not cause subsequent names in + # boolean_strings to clash. + self.identity_api.delete_user(user_ref['id']) + + def test_unignored_user_none_mapping(self): + # Ensure that an attribute that maps to None that is not explicitly + # ignored in configuration is implicitly ignored without triggering + # an error. + conf = self.get_config(CONF.identity.default_domain_id) + conf.ldap.user_attribute_ignore = ['enabled', 'email', + 'tenants', 'tenantId'] + self.reload_backends(CONF.identity.default_domain_id) + + user = {'name': u'fäké1', + 'password': u'fäképass1', + 'domain_id': CONF.identity.default_domain_id, + } + + user_ref = self.identity_api.create_user(user) + + # If this doesn't raise, then the test is successful. + self.identity_api.get_user(user_ref['id']) + + def test_update_user_name(self): + """A user's name cannot be changed through the LDAP driver.""" + self.assertRaises(exception.Conflict, + super(BaseLDAPIdentity, self).test_update_user_name) + + def test_arbitrary_attributes_are_returned_from_get_user(self): + self.skipTest("Using arbitrary attributes doesn't work under LDAP") + + def test_new_arbitrary_attributes_are_returned_from_update_user(self): + self.skipTest("Using arbitrary attributes doesn't work under LDAP") + + def test_updated_arbitrary_attributes_are_returned_from_update_user(self): + self.skipTest("Using arbitrary attributes doesn't work under LDAP") + + def test_cache_layer_domain_crud(self): + # TODO(morganfainberg): This also needs to be removed when full LDAP + # implementation is submitted. No need to duplicate the above test, + # just skip this time. + self.skipTest('Domains are read-only against LDAP') + + def test_user_id_comma(self): + """Even if the user has a , in their ID, groups can be listed.""" + + # Create a user with a , in their ID + # NOTE(blk-u): the DN for this user is hard-coded in fakeldap! + + # Since we want to fake up this special ID, we'll squirt this + # direct into the driver and bypass the manager layer. + user_id = u'Doe, John' + user = { + 'id': user_id, + 'name': self.getUniqueString(), + 'password': self.getUniqueString(), + 'domain_id': CONF.identity.default_domain_id, + } + user = self.identity_api.driver.create_user(user_id, user) + + # Now we'll use the manager to discover it, which will create a + # Public ID for it. + ref_list = self.identity_api.list_users() + public_user_id = None + for ref in ref_list: + if ref['name'] == user['name']: + public_user_id = ref['id'] + break + + # Create a group + group_id = uuid.uuid4().hex + group = { + 'id': group_id, + 'name': self.getUniqueString(prefix='tuidc'), + 'description': self.getUniqueString(), + 'domain_id': CONF.identity.default_domain_id, + } + group = self.identity_api.driver.create_group(group_id, group) + # Now we'll use the manager to discover it, which will create a + # Public ID for it. + ref_list = self.identity_api.list_groups() + public_group_id = None + for ref in ref_list: + if ref['name'] == group['name']: + public_group_id = ref['id'] + break + + # Put the user in the group + self.identity_api.add_user_to_group(public_user_id, public_group_id) + + # List groups for user. + ref_list = self.identity_api.list_groups_for_user(public_user_id) + + group['id'] = public_group_id + self.assertThat(ref_list, matchers.Equals([group])) + + def test_user_id_comma_grants(self): + """Even if the user has a , in their ID, can get user and group grants. + """ + + # Create a user with a , in their ID + # NOTE(blk-u): the DN for this user is hard-coded in fakeldap! + + # Since we want to fake up this special ID, we'll squirt this + # direct into the driver and bypass the manager layer + user_id = u'Doe, John' + user = { + 'id': user_id, + 'name': self.getUniqueString(), + 'password': self.getUniqueString(), + 'domain_id': CONF.identity.default_domain_id, + } + self.identity_api.driver.create_user(user_id, user) + + # Now we'll use the manager to discover it, which will create a + # Public ID for it. + ref_list = self.identity_api.list_users() + public_user_id = None + for ref in ref_list: + if ref['name'] == user['name']: + public_user_id = ref['id'] + break + + # Grant the user a role on a project. + + role_id = 'member' + project_id = self.tenant_baz['id'] + + self.assignment_api.create_grant(role_id, user_id=public_user_id, + project_id=project_id) + + role_ref = self.assignment_api.get_grant(role_id, + user_id=public_user_id, + project_id=project_id) + + self.assertEqual(role_id, role_ref['id']) + + def test_user_enabled_ignored_disable_error(self): + # When the server is configured so that the enabled attribute is + # ignored for users, users cannot be disabled. + + self.config_fixture.config(group='ldap', + user_attribute_ignore=['enabled']) + + # Need to re-load backends for the config change to take effect. + self.load_backends() + + # Attempt to disable the user. + self.assertRaises(exception.ForbiddenAction, + self.identity_api.update_user, self.user_foo['id'], + {'enabled': False}) + + user_info = self.identity_api.get_user(self.user_foo['id']) + + # If 'enabled' is ignored then 'enabled' isn't returned as part of the + # ref. + self.assertNotIn('enabled', user_info) + + def test_group_enabled_ignored_disable_error(self): + # When the server is configured so that the enabled attribute is + # ignored for groups, groups cannot be disabled. + + self.config_fixture.config(group='ldap', + group_attribute_ignore=['enabled']) + + # Need to re-load backends for the config change to take effect. + self.load_backends() + + # There's no group fixture so create a group. + new_domain = self._get_domain_fixture() + new_group = {'domain_id': new_domain['id'], + 'name': uuid.uuid4().hex} + new_group = self.identity_api.create_group(new_group) + + # Attempt to disable the group. + self.assertRaises(exception.ForbiddenAction, + self.identity_api.update_group, new_group['id'], + {'enabled': False}) + + group_info = self.identity_api.get_group(new_group['id']) + + # If 'enabled' is ignored then 'enabled' isn't returned as part of the + # ref. + self.assertNotIn('enabled', group_info) + + def test_project_enabled_ignored_disable_error(self): + # When the server is configured so that the enabled attribute is + # ignored for projects, projects cannot be disabled. + + self.config_fixture.config(group='ldap', + project_attribute_ignore=['enabled']) + + # Need to re-load backends for the config change to take effect. + self.load_backends() + + # Attempt to disable the project. + self.assertRaises(exception.ForbiddenAction, + self.resource_api.update_project, + self.tenant_baz['id'], {'enabled': False}) + + project_info = self.resource_api.get_project(self.tenant_baz['id']) + + # Unlike other entities, if 'enabled' is ignored then 'enabled' is + # returned as part of the ref. + self.assertIs(True, project_info['enabled']) + + +class LDAPIdentity(BaseLDAPIdentity, tests.TestCase): + + def setUp(self): + # NOTE(dstanek): The database must be setup prior to calling the + # parent's setUp. The parent's setUp uses services (like + # credentials) that require a database. + self.useFixture(database.Database()) + super(LDAPIdentity, self).setUp() + + def load_fixtures(self, fixtures): + # Override super impl since need to create group container. + create_group_container(self.identity_api) + super(LDAPIdentity, self).load_fixtures(fixtures) + + def test_configurable_allowed_project_actions(self): + tenant = {'id': u'fäké1', 'name': u'fäké1', 'enabled': True} + self.resource_api.create_project(u'fäké1', tenant) + tenant_ref = self.resource_api.get_project(u'fäké1') + self.assertEqual(u'fäké1', tenant_ref['id']) + + tenant['enabled'] = False + self.resource_api.update_project(u'fäké1', tenant) + + self.resource_api.delete_project(u'fäké1') + self.assertRaises(exception.ProjectNotFound, + self.resource_api.get_project, + u'fäké1') + + def test_configurable_subtree_delete(self): + self.config_fixture.config(group='ldap', allow_subtree_delete=True) + self.load_backends() + + project1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'domain_id': CONF.identity.default_domain_id} + self.resource_api.create_project(project1['id'], project1) + + role1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.role_api.create_role(role1['id'], role1) + + user1 = {'name': uuid.uuid4().hex, + 'domain_id': CONF.identity.default_domain_id, + 'password': uuid.uuid4().hex, + 'enabled': True} + user1 = self.identity_api.create_user(user1) + + self.assignment_api.add_role_to_user_and_project( + user_id=user1['id'], + tenant_id=project1['id'], + role_id=role1['id']) + + self.resource_api.delete_project(project1['id']) + self.assertRaises(exception.ProjectNotFound, + self.resource_api.get_project, + project1['id']) + + self.resource_api.create_project(project1['id'], project1) + + list = self.assignment_api.get_roles_for_user_and_project( + user1['id'], + project1['id']) + self.assertEqual(0, len(list)) + + def test_configurable_forbidden_project_actions(self): + self.config_fixture.config( + group='ldap', project_allow_create=False, + project_allow_update=False, project_allow_delete=False) + self.load_backends() + + tenant = {'id': u'fäké1', 'name': u'fäké1'} + self.assertRaises(exception.ForbiddenAction, + self.resource_api.create_project, + u'fäké1', + tenant) + + self.tenant_bar['enabled'] = False + self.assertRaises(exception.ForbiddenAction, + self.resource_api.update_project, + self.tenant_bar['id'], + self.tenant_bar) + self.assertRaises(exception.ForbiddenAction, + self.resource_api.delete_project, + self.tenant_bar['id']) + + def test_project_filter(self): + tenant_ref = self.resource_api.get_project(self.tenant_bar['id']) + self.assertDictEqual(tenant_ref, self.tenant_bar) + + self.config_fixture.config(group='ldap', + project_filter='(CN=DOES_NOT_MATCH)') + self.load_backends() + # NOTE(morganfainberg): CONF.ldap.project_filter will not be + # dynamically changed at runtime. This invalidate is a work-around for + # the expectation that it is safe to change config values in tests that + # could affect what the drivers would return up to the manager. This + # solves this assumption when working with aggressive (on-create) + # cache population. + self.role_api.get_role.invalidate(self.role_api, + self.role_member['id']) + self.role_api.get_role(self.role_member['id']) + self.resource_api.get_project.invalidate(self.resource_api, + self.tenant_bar['id']) + self.assertRaises(exception.ProjectNotFound, + self.resource_api.get_project, + self.tenant_bar['id']) + + def test_dumb_member(self): + self.config_fixture.config(group='ldap', use_dumb_member=True) + self.clear_database() + self.load_backends() + self.load_fixtures(default_fixtures) + dumb_id = common_ldap.BaseLdap._dn_to_id(CONF.ldap.dumb_member) + self.assertRaises(exception.UserNotFound, + self.identity_api.get_user, + dumb_id) + + def test_project_attribute_mapping(self): + self.config_fixture.config( + group='ldap', project_name_attribute='ou', + project_desc_attribute='description', + project_enabled_attribute='enabled') + self.clear_database() + self.load_backends() + self.load_fixtures(default_fixtures) + # NOTE(morganfainberg): CONF.ldap.project_name_attribute, + # CONF.ldap.project_desc_attribute, and + # CONF.ldap.project_enabled_attribute will not be + # dynamically changed at runtime. This invalidate is a work-around for + # the expectation that it is safe to change config values in tests that + # could affect what the drivers would return up to the manager. This + # solves this assumption when working with aggressive (on-create) + # cache population. + self.resource_api.get_project.invalidate(self.resource_api, + self.tenant_baz['id']) + tenant_ref = self.resource_api.get_project(self.tenant_baz['id']) + self.assertEqual(self.tenant_baz['id'], tenant_ref['id']) + self.assertEqual(self.tenant_baz['name'], tenant_ref['name']) + self.assertEqual( + self.tenant_baz['description'], + tenant_ref['description']) + self.assertEqual(self.tenant_baz['enabled'], tenant_ref['enabled']) + + self.config_fixture.config(group='ldap', + project_name_attribute='description', + project_desc_attribute='ou') + self.load_backends() + # NOTE(morganfainberg): CONF.ldap.project_name_attribute, + # CONF.ldap.project_desc_attribute, and + # CONF.ldap.project_enabled_attribute will not be + # dynamically changed at runtime. This invalidate is a work-around for + # the expectation that it is safe to change config values in tests that + # could affect what the drivers would return up to the manager. This + # solves this assumption when working with aggressive (on-create) + # cache population. + self.resource_api.get_project.invalidate(self.resource_api, + self.tenant_baz['id']) + tenant_ref = self.resource_api.get_project(self.tenant_baz['id']) + self.assertEqual(self.tenant_baz['id'], tenant_ref['id']) + self.assertEqual(self.tenant_baz['description'], tenant_ref['name']) + self.assertEqual(self.tenant_baz['name'], tenant_ref['description']) + self.assertEqual(self.tenant_baz['enabled'], tenant_ref['enabled']) + + def test_project_attribute_ignore(self): + self.config_fixture.config( + group='ldap', + project_attribute_ignore=['name', 'description', 'enabled']) + self.clear_database() + self.load_backends() + self.load_fixtures(default_fixtures) + # NOTE(morganfainberg): CONF.ldap.project_attribute_ignore will not be + # dynamically changed at runtime. This invalidate is a work-around for + # the expectation that it is safe to change configs values in tests + # that could affect what the drivers would return up to the manager. + # This solves this assumption when working with aggressive (on-create) + # cache population. + self.resource_api.get_project.invalidate(self.resource_api, + self.tenant_baz['id']) + tenant_ref = self.resource_api.get_project(self.tenant_baz['id']) + self.assertEqual(self.tenant_baz['id'], tenant_ref['id']) + self.assertNotIn('name', tenant_ref) + self.assertNotIn('description', tenant_ref) + self.assertNotIn('enabled', tenant_ref) + + def test_user_enable_attribute_mask(self): + self.config_fixture.config(group='ldap', user_enabled_mask=2, + user_enabled_default='512') + self.clear_database() + self.load_backends() + self.load_fixtures(default_fixtures) + + user = {'name': u'fäké1', 'enabled': True, + 'domain_id': CONF.identity.default_domain_id} + + user_ref = self.identity_api.create_user(user) + + # Use assertIs rather than assertTrue because assertIs will assert the + # value is a Boolean as expected. + self.assertIs(user_ref['enabled'], True) + self.assertNotIn('enabled_nomask', user_ref) + + enabled_vals = self.get_user_enabled_vals(user_ref) + self.assertEqual([512], enabled_vals) + + user_ref = self.identity_api.get_user(user_ref['id']) + self.assertIs(user_ref['enabled'], True) + self.assertNotIn('enabled_nomask', user_ref) + + user['enabled'] = False + user_ref = self.identity_api.update_user(user_ref['id'], user) + self.assertIs(user_ref['enabled'], False) + self.assertNotIn('enabled_nomask', user_ref) + + enabled_vals = self.get_user_enabled_vals(user_ref) + self.assertEqual([514], enabled_vals) + + user_ref = self.identity_api.get_user(user_ref['id']) + self.assertIs(user_ref['enabled'], False) + self.assertNotIn('enabled_nomask', user_ref) + + user['enabled'] = True + user_ref = self.identity_api.update_user(user_ref['id'], user) + self.assertIs(user_ref['enabled'], True) + self.assertNotIn('enabled_nomask', user_ref) + + enabled_vals = self.get_user_enabled_vals(user_ref) + self.assertEqual([512], enabled_vals) + + user_ref = self.identity_api.get_user(user_ref['id']) + self.assertIs(user_ref['enabled'], True) + self.assertNotIn('enabled_nomask', user_ref) + + def test_user_enabled_invert(self): + self.config_fixture.config(group='ldap', user_enabled_invert=True, + user_enabled_default=False) + self.clear_database() + self.load_backends() + self.load_fixtures(default_fixtures) + + user1 = {'name': u'fäké1', 'enabled': True, + 'domain_id': CONF.identity.default_domain_id} + + user2 = {'name': u'fäké2', 'enabled': False, + 'domain_id': CONF.identity.default_domain_id} + + user3 = {'name': u'fäké3', + 'domain_id': CONF.identity.default_domain_id} + + # Ensure that the LDAP attribute is False for a newly created + # enabled user. + user_ref = self.identity_api.create_user(user1) + self.assertIs(True, user_ref['enabled']) + enabled_vals = self.get_user_enabled_vals(user_ref) + self.assertEqual([False], enabled_vals) + user_ref = self.identity_api.get_user(user_ref['id']) + self.assertIs(True, user_ref['enabled']) + + # Ensure that the LDAP attribute is True for a disabled user. + user1['enabled'] = False + user_ref = self.identity_api.update_user(user_ref['id'], user1) + self.assertIs(False, user_ref['enabled']) + enabled_vals = self.get_user_enabled_vals(user_ref) + self.assertEqual([True], enabled_vals) + + # Enable the user and ensure that the LDAP attribute is True again. + user1['enabled'] = True + user_ref = self.identity_api.update_user(user_ref['id'], user1) + self.assertIs(True, user_ref['enabled']) + enabled_vals = self.get_user_enabled_vals(user_ref) + self.assertEqual([False], enabled_vals) + + # Ensure that the LDAP attribute is True for a newly created + # disabled user. + user_ref = self.identity_api.create_user(user2) + self.assertIs(False, user_ref['enabled']) + enabled_vals = self.get_user_enabled_vals(user_ref) + self.assertEqual([True], enabled_vals) + user_ref = self.identity_api.get_user(user_ref['id']) + self.assertIs(False, user_ref['enabled']) + + # Ensure that the LDAP attribute is inverted for a newly created + # user when the user_enabled_default setting is used. + user_ref = self.identity_api.create_user(user3) + self.assertIs(True, user_ref['enabled']) + enabled_vals = self.get_user_enabled_vals(user_ref) + self.assertEqual([False], enabled_vals) + user_ref = self.identity_api.get_user(user_ref['id']) + self.assertIs(True, user_ref['enabled']) + + @mock.patch.object(common_ldap_core.BaseLdap, '_ldap_get') + def test_user_enabled_invert_no_enabled_value(self, mock_ldap_get): + self.config_fixture.config(group='ldap', user_enabled_invert=True, + user_enabled_default=False) + # Mock the search results to return an entry with + # no enabled value. + mock_ldap_get.return_value = ( + 'cn=junk,dc=example,dc=com', + { + 'sn': [uuid.uuid4().hex], + 'email': [uuid.uuid4().hex], + 'cn': ['junk'] + } + ) + + user_api = identity.backends.ldap.UserApi(CONF) + user_ref = user_api.get('junk') + # Ensure that the model enabled attribute is inverted + # from the resource default. + self.assertIs(not CONF.ldap.user_enabled_default, user_ref['enabled']) + + @mock.patch.object(common_ldap_core.BaseLdap, '_ldap_get') + def test_user_enabled_invert_default_str_value(self, mock_ldap_get): + self.config_fixture.config(group='ldap', user_enabled_invert=True, + user_enabled_default='False') + # Mock the search results to return an entry with + # no enabled value. + mock_ldap_get.return_value = ( + 'cn=junk,dc=example,dc=com', + { + 'sn': [uuid.uuid4().hex], + 'email': [uuid.uuid4().hex], + 'cn': ['junk'] + } + ) + + user_api = identity.backends.ldap.UserApi(CONF) + user_ref = user_api.get('junk') + # Ensure that the model enabled attribute is inverted + # from the resource default. + self.assertIs(True, user_ref['enabled']) + + @mock.patch.object(common_ldap_core.BaseLdap, '_ldap_get') + def test_user_enabled_attribute_handles_expired(self, mock_ldap_get): + # If using 'passwordisexpired' as enabled attribute, and inverting it, + # Then an unauthorized user (expired password) should not be enabled. + self.config_fixture.config(group='ldap', user_enabled_invert=True, + user_enabled_attribute='passwordisexpired') + mock_ldap_get.return_value = ( + u'uid=123456789,c=us,ou=our_ldap,o=acme.com', + { + 'uid': [123456789], + 'mail': ['shaun@acme.com'], + 'passwordisexpired': ['TRUE'], + 'cn': ['uid=123456789,c=us,ou=our_ldap,o=acme.com'] + } + ) + + user_api = identity.backends.ldap.UserApi(CONF) + user_ref = user_api.get('123456789') + self.assertIs(False, user_ref['enabled']) + + @mock.patch.object(common_ldap_core.BaseLdap, '_ldap_get') + def test_user_enabled_attribute_handles_utf8(self, mock_ldap_get): + # If using 'passwordisexpired' as enabled attribute, and inverting it, + # and the result is utf8 encoded, then the an authorized user should + # be enabled. + self.config_fixture.config(group='ldap', user_enabled_invert=True, + user_enabled_attribute='passwordisexpired') + mock_ldap_get.return_value = ( + u'uid=123456789,c=us,ou=our_ldap,o=acme.com', + { + 'uid': [123456789], + 'mail': [u'shaun@acme.com'], + 'passwordisexpired': [u'false'], + 'cn': [u'uid=123456789,c=us,ou=our_ldap,o=acme.com'] + } + ) + + user_api = identity.backends.ldap.UserApi(CONF) + user_ref = user_api.get('123456789') + self.assertIs(True, user_ref['enabled']) + + @mock.patch.object(common_ldap_core.KeystoneLDAPHandler, 'simple_bind_s') + def test_user_api_get_connection_no_user_password(self, mocked_method): + """Don't bind in case the user and password are blank.""" + # Ensure the username/password are in-fact blank + self.config_fixture.config(group='ldap', user=None, password=None) + user_api = identity.backends.ldap.UserApi(CONF) + user_api.get_connection(user=None, password=None) + self.assertFalse(mocked_method.called, + msg='`simple_bind_s` method was unexpectedly called') + + @mock.patch.object(common_ldap_core.KeystoneLDAPHandler, 'connect') + def test_chase_referrals_off(self, mocked_fakeldap): + self.config_fixture.config( + group='ldap', + url='fake://memory', + chase_referrals=False) + user_api = identity.backends.ldap.UserApi(CONF) + user_api.get_connection(user=None, password=None) + + # The last call_arg should be a dictionary and should contain + # chase_referrals. Check to make sure the value of chase_referrals + # is as expected. + self.assertFalse(mocked_fakeldap.call_args[-1]['chase_referrals']) + + @mock.patch.object(common_ldap_core.KeystoneLDAPHandler, 'connect') + def test_chase_referrals_on(self, mocked_fakeldap): + self.config_fixture.config( + group='ldap', + url='fake://memory', + chase_referrals=True) + user_api = identity.backends.ldap.UserApi(CONF) + user_api.get_connection(user=None, password=None) + + # The last call_arg should be a dictionary and should contain + # chase_referrals. Check to make sure the value of chase_referrals + # is as expected. + self.assertTrue(mocked_fakeldap.call_args[-1]['chase_referrals']) + + @mock.patch.object(common_ldap_core.KeystoneLDAPHandler, 'connect') + def test_debug_level_set(self, mocked_fakeldap): + level = 12345 + self.config_fixture.config( + group='ldap', + url='fake://memory', + debug_level=level) + user_api = identity.backends.ldap.UserApi(CONF) + user_api.get_connection(user=None, password=None) + + # The last call_arg should be a dictionary and should contain + # debug_level. Check to make sure the value of debug_level + # is as expected. + self.assertEqual(level, mocked_fakeldap.call_args[-1]['debug_level']) + + def test_wrong_ldap_scope(self): + self.config_fixture.config(group='ldap', query_scope=uuid.uuid4().hex) + self.assertRaisesRegexp( + ValueError, + 'Invalid LDAP scope: %s. *' % CONF.ldap.query_scope, + identity.backends.ldap.Identity) + + def test_wrong_alias_dereferencing(self): + self.config_fixture.config(group='ldap', + alias_dereferencing=uuid.uuid4().hex) + self.assertRaisesRegexp( + ValueError, + 'Invalid LDAP deref option: %s\.' % CONF.ldap.alias_dereferencing, + identity.backends.ldap.Identity) + + def test_is_dumb_member(self): + self.config_fixture.config(group='ldap', + use_dumb_member=True) + self.load_backends() + + dn = 'cn=dumb,dc=nonexistent' + self.assertTrue(self.identity_api.driver.user._is_dumb_member(dn)) + + def test_is_dumb_member_upper_case_keys(self): + self.config_fixture.config(group='ldap', + use_dumb_member=True) + self.load_backends() + + dn = 'CN=dumb,DC=nonexistent' + self.assertTrue(self.identity_api.driver.user._is_dumb_member(dn)) + + def test_is_dumb_member_with_false_use_dumb_member(self): + self.config_fixture.config(group='ldap', + use_dumb_member=False) + self.load_backends() + dn = 'cn=dumb,dc=nonexistent' + self.assertFalse(self.identity_api.driver.user._is_dumb_member(dn)) + + def test_is_dumb_member_not_dumb(self): + self.config_fixture.config(group='ldap', + use_dumb_member=True) + self.load_backends() + dn = 'ou=some,dc=example.com' + self.assertFalse(self.identity_api.driver.user._is_dumb_member(dn)) + + def test_user_extra_attribute_mapping(self): + self.config_fixture.config( + group='ldap', + user_additional_attribute_mapping=['description:name']) + self.load_backends() + user = { + 'name': 'EXTRA_ATTRIBUTES', + 'password': 'extra', + 'domain_id': CONF.identity.default_domain_id + } + user = self.identity_api.create_user(user) + dn, attrs = self.identity_api.driver.user._ldap_get(user['id']) + self.assertThat([user['name']], matchers.Equals(attrs['description'])) + + def test_user_extra_attribute_mapping_description_is_returned(self): + # Given a mapping like description:description, the description is + # returned. + + self.config_fixture.config( + group='ldap', + user_additional_attribute_mapping=['description:description']) + self.load_backends() + + description = uuid.uuid4().hex + user = { + 'name': uuid.uuid4().hex, + 'description': description, + 'password': uuid.uuid4().hex, + 'domain_id': CONF.identity.default_domain_id + } + user = self.identity_api.create_user(user) + res = self.identity_api.driver.user.get_all() + + new_user = [u for u in res if u['id'] == user['id']][0] + self.assertThat(new_user['description'], matchers.Equals(description)) + + @mock.patch.object(common_ldap_core.BaseLdap, '_ldap_get') + def test_user_mixed_case_attribute(self, mock_ldap_get): + # Mock the search results to return attribute names + # with unexpected case. + mock_ldap_get.return_value = ( + 'cn=junk,dc=example,dc=com', + { + 'sN': [uuid.uuid4().hex], + 'MaIl': [uuid.uuid4().hex], + 'cn': ['junk'] + } + ) + user = self.identity_api.get_user('junk') + self.assertEqual(mock_ldap_get.return_value[1]['sN'][0], + user['name']) + self.assertEqual(mock_ldap_get.return_value[1]['MaIl'][0], + user['email']) + + def test_parse_extra_attribute_mapping(self): + option_list = ['description:name', 'gecos:password', + 'fake:invalid', 'invalid1', 'invalid2:', + 'description:name:something'] + mapping = self.identity_api.driver.user._parse_extra_attrs(option_list) + expected_dict = {'description': 'name', 'gecos': 'password', + 'fake': 'invalid', 'invalid2': ''} + self.assertDictEqual(expected_dict, mapping) + +# TODO(henry-nash): These need to be removed when the full LDAP implementation +# is submitted - see Bugs 1092187, 1101287, 1101276, 1101289 + + def test_domain_crud(self): + domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'enabled': True, 'description': uuid.uuid4().hex} + self.assertRaises(exception.Forbidden, + self.resource_api.create_domain, + domain['id'], + domain) + self.assertRaises(exception.Conflict, + self.resource_api.create_domain, + CONF.identity.default_domain_id, + domain) + self.assertRaises(exception.DomainNotFound, + self.resource_api.get_domain, + domain['id']) + + domain['description'] = uuid.uuid4().hex + self.assertRaises(exception.DomainNotFound, + self.resource_api.update_domain, + domain['id'], + domain) + self.assertRaises(exception.Forbidden, + self.resource_api.update_domain, + CONF.identity.default_domain_id, + domain) + self.assertRaises(exception.DomainNotFound, + self.resource_api.get_domain, + domain['id']) + self.assertRaises(exception.DomainNotFound, + self.resource_api.delete_domain, + domain['id']) + self.assertRaises(exception.Forbidden, + self.resource_api.delete_domain, + CONF.identity.default_domain_id) + self.assertRaises(exception.DomainNotFound, + self.resource_api.get_domain, + domain['id']) + + @tests.skip_if_no_multiple_domains_support + def test_create_domain_case_sensitivity(self): + # domains are read-only, so case sensitivity isn't an issue + ref = { + 'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex} + self.assertRaises(exception.Forbidden, + self.resource_api.create_domain, + ref['id'], + ref) + + def test_cache_layer_domain_crud(self): + # TODO(morganfainberg): This also needs to be removed when full LDAP + # implementation is submitted. No need to duplicate the above test, + # just skip this time. + self.skipTest('Domains are read-only against LDAP') + + def test_domain_rename_invalidates_get_domain_by_name_cache(self): + parent = super(LDAPIdentity, self) + self.assertRaises( + exception.Forbidden, + parent.test_domain_rename_invalidates_get_domain_by_name_cache) + + def test_project_rename_invalidates_get_project_by_name_cache(self): + parent = super(LDAPIdentity, self) + self.assertRaises( + exception.Forbidden, + parent.test_project_rename_invalidates_get_project_by_name_cache) + + def test_project_crud(self): + # NOTE(topol): LDAP implementation does not currently support the + # updating of a project name so this method override + # provides a different update test + project = {'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'domain_id': CONF.identity.default_domain_id, + 'description': uuid.uuid4().hex, + 'enabled': True, + 'parent_id': None} + self.resource_api.create_project(project['id'], project) + project_ref = self.resource_api.get_project(project['id']) + + self.assertDictEqual(project_ref, project) + + project['description'] = uuid.uuid4().hex + self.resource_api.update_project(project['id'], project) + project_ref = self.resource_api.get_project(project['id']) + self.assertDictEqual(project_ref, project) + + self.resource_api.delete_project(project['id']) + self.assertRaises(exception.ProjectNotFound, + self.resource_api.get_project, + project['id']) + + @tests.skip_if_cache_disabled('assignment') + def test_cache_layer_project_crud(self): + # NOTE(morganfainberg): LDAP implementation does not currently support + # updating project names. This method override provides a different + # update test. + project = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'domain_id': CONF.identity.default_domain_id, + 'description': uuid.uuid4().hex} + project_id = project['id'] + # Create a project + self.resource_api.create_project(project_id, project) + self.resource_api.get_project(project_id) + updated_project = copy.deepcopy(project) + updated_project['description'] = uuid.uuid4().hex + # Update project, bypassing resource manager + self.resource_api.driver.update_project(project_id, + updated_project) + # Verify get_project still returns the original project_ref + self.assertDictContainsSubset( + project, self.resource_api.get_project(project_id)) + # Invalidate cache + self.resource_api.get_project.invalidate(self.resource_api, + project_id) + # Verify get_project now returns the new project + self.assertDictContainsSubset( + updated_project, + self.resource_api.get_project(project_id)) + # Update project using the resource_api manager back to original + self.resource_api.update_project(project['id'], project) + # Verify get_project returns the original project_ref + self.assertDictContainsSubset( + project, self.resource_api.get_project(project_id)) + # Delete project bypassing resource_api + self.resource_api.driver.delete_project(project_id) + # Verify get_project still returns the project_ref + self.assertDictContainsSubset( + project, self.resource_api.get_project(project_id)) + # Invalidate cache + self.resource_api.get_project.invalidate(self.resource_api, + project_id) + # Verify ProjectNotFound now raised + self.assertRaises(exception.ProjectNotFound, + self.resource_api.get_project, + project_id) + # recreate project + self.resource_api.create_project(project_id, project) + self.resource_api.get_project(project_id) + # delete project + self.resource_api.delete_project(project_id) + # Verify ProjectNotFound is raised + self.assertRaises(exception.ProjectNotFound, + self.resource_api.get_project, + project_id) + + def _assert_create_hierarchy_not_allowed(self): + domain = self._get_domain_fixture() + + project1 = {'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'description': '', + 'domain_id': domain['id'], + 'enabled': True, + 'parent_id': None} + self.resource_api.create_project(project1['id'], project1) + + # Creating project2 under project1. LDAP will not allow + # the creation of a project with parent_id being set + project2 = {'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'description': '', + 'domain_id': domain['id'], + 'enabled': True, + 'parent_id': project1['id']} + + self.assertRaises(exception.InvalidParentProject, + self.resource_api.create_project, + project2['id'], + project2) + + # Now, we'll create project 2 with no parent + project2['parent_id'] = None + self.resource_api.create_project(project2['id'], project2) + + # Returning projects to be used across the tests + return [project1, project2] + + def test_check_leaf_projects(self): + projects = self._assert_create_hierarchy_not_allowed() + for project in projects: + self.assertTrue(self.resource_api.is_leaf_project(project)) + + def test_list_projects_in_subtree(self): + projects = self._assert_create_hierarchy_not_allowed() + for project in projects: + subtree_list = self.resource_api.list_projects_in_subtree( + project) + self.assertEqual(0, len(subtree_list)) + + def test_list_project_parents(self): + projects = self._assert_create_hierarchy_not_allowed() + for project in projects: + parents_list = self.resource_api.list_project_parents(project) + self.assertEqual(0, len(parents_list)) + + def test_hierarchical_projects_crud(self): + self._assert_create_hierarchy_not_allowed() + + def test_create_project_under_disabled_one(self): + self._assert_create_hierarchy_not_allowed() + + def test_create_project_with_invalid_parent(self): + self._assert_create_hierarchy_not_allowed() + + def test_create_leaf_project_with_invalid_domain(self): + self._assert_create_hierarchy_not_allowed() + + def test_update_project_parent(self): + self._assert_create_hierarchy_not_allowed() + + def test_enable_project_with_disabled_parent(self): + self._assert_create_hierarchy_not_allowed() + + def test_disable_hierarchical_leaf_project(self): + self._assert_create_hierarchy_not_allowed() + + def test_disable_hierarchical_not_leaf_project(self): + self._assert_create_hierarchy_not_allowed() + + def test_delete_hierarchical_leaf_project(self): + self._assert_create_hierarchy_not_allowed() + + def test_delete_hierarchical_not_leaf_project(self): + self._assert_create_hierarchy_not_allowed() + + def test_check_hierarchy_depth(self): + projects = self._assert_create_hierarchy_not_allowed() + for project in projects: + depth = self._get_hierarchy_depth(project['id']) + self.assertEqual(1, depth) + + def test_multi_role_grant_by_user_group_on_project_domain(self): + # This is a partial implementation of the standard test that + # is defined in test_backend.py. It omits both domain and + # group grants. since neither of these are yet supported by + # the ldap backend. + + role_list = [] + for _ in range(2): + role = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.role_api.create_role(role['id'], role) + role_list.append(role) + + user1 = {'name': uuid.uuid4().hex, + 'domain_id': CONF.identity.default_domain_id, + 'password': uuid.uuid4().hex, + 'enabled': True} + user1 = self.identity_api.create_user(user1) + project1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'domain_id': CONF.identity.default_domain_id} + self.resource_api.create_project(project1['id'], project1) + + self.assignment_api.add_role_to_user_and_project( + user_id=user1['id'], + tenant_id=project1['id'], + role_id=role_list[0]['id']) + self.assignment_api.add_role_to_user_and_project( + user_id=user1['id'], + tenant_id=project1['id'], + role_id=role_list[1]['id']) + + # Although list_grants are not yet supported, we can test the + # alternate way of getting back lists of grants, where user + # and group roles are combined. Only directly assigned user + # roles are available, since group grants are not yet supported + + combined_list = self.assignment_api.get_roles_for_user_and_project( + user1['id'], + project1['id']) + self.assertEqual(2, len(combined_list)) + self.assertIn(role_list[0]['id'], combined_list) + self.assertIn(role_list[1]['id'], combined_list) + + # Finally, although domain roles are not implemented, check we can + # issue the combined get roles call with benign results, since thus is + # used in token generation + + combined_role_list = self.assignment_api.get_roles_for_user_and_domain( + user1['id'], CONF.identity.default_domain_id) + self.assertEqual(0, len(combined_role_list)) + + def test_list_projects_for_alternate_domain(self): + self.skipTest( + 'N/A: LDAP does not support multiple domains') + + def test_get_default_domain_by_name(self): + domain = self._get_domain_fixture() + + domain_ref = self.resource_api.get_domain_by_name(domain['name']) + self.assertEqual(domain_ref, domain) + + def test_base_ldap_connection_deref_option(self): + def get_conn(deref_name): + self.config_fixture.config(group='ldap', + alias_dereferencing=deref_name) + base_ldap = common_ldap.BaseLdap(CONF) + return base_ldap.get_connection() + + conn = get_conn('default') + self.assertEqual(ldap.get_option(ldap.OPT_DEREF), + conn.get_option(ldap.OPT_DEREF)) + + conn = get_conn('always') + self.assertEqual(ldap.DEREF_ALWAYS, + conn.get_option(ldap.OPT_DEREF)) + + conn = get_conn('finding') + self.assertEqual(ldap.DEREF_FINDING, + conn.get_option(ldap.OPT_DEREF)) + + conn = get_conn('never') + self.assertEqual(ldap.DEREF_NEVER, + conn.get_option(ldap.OPT_DEREF)) + + conn = get_conn('searching') + self.assertEqual(ldap.DEREF_SEARCHING, + conn.get_option(ldap.OPT_DEREF)) + + def test_list_users_no_dn(self): + users = self.identity_api.list_users() + self.assertEqual(len(default_fixtures.USERS), len(users)) + user_ids = set(user['id'] for user in users) + expected_user_ids = set(getattr(self, 'user_%s' % user['id'])['id'] + for user in default_fixtures.USERS) + for user_ref in users: + self.assertNotIn('dn', user_ref) + self.assertEqual(expected_user_ids, user_ids) + + def test_list_groups_no_dn(self): + # Create some test groups. + domain = self._get_domain_fixture() + expected_group_ids = [] + numgroups = 3 + for _ in range(numgroups): + group = {'name': uuid.uuid4().hex, 'domain_id': domain['id']} + group = self.identity_api.create_group(group) + expected_group_ids.append(group['id']) + # Fetch the test groups and ensure that they don't contain a dn. + groups = self.identity_api.list_groups() + self.assertEqual(numgroups, len(groups)) + group_ids = set(group['id'] for group in groups) + for group_ref in groups: + self.assertNotIn('dn', group_ref) + self.assertEqual(set(expected_group_ids), group_ids) + + def test_list_groups_for_user_no_dn(self): + # Create a test user. + user = {'name': uuid.uuid4().hex, + 'domain_id': CONF.identity.default_domain_id, + 'password': uuid.uuid4().hex, 'enabled': True} + user = self.identity_api.create_user(user) + # Create some test groups and add the test user as a member. + domain = self._get_domain_fixture() + expected_group_ids = [] + numgroups = 3 + for _ in range(numgroups): + group = {'name': uuid.uuid4().hex, 'domain_id': domain['id']} + group = self.identity_api.create_group(group) + expected_group_ids.append(group['id']) + self.identity_api.add_user_to_group(user['id'], group['id']) + # Fetch the groups for the test user + # and ensure they don't contain a dn. + groups = self.identity_api.list_groups_for_user(user['id']) + self.assertEqual(numgroups, len(groups)) + group_ids = set(group['id'] for group in groups) + for group_ref in groups: + self.assertNotIn('dn', group_ref) + self.assertEqual(set(expected_group_ids), group_ids) + + def test_user_id_attribute_in_create(self): + conf = self.get_config(CONF.identity.default_domain_id) + conf.ldap.user_id_attribute = 'mail' + self.reload_backends(CONF.identity.default_domain_id) + + user = {'name': u'fäké1', + 'password': u'fäképass1', + 'domain_id': CONF.identity.default_domain_id} + user = self.identity_api.create_user(user) + user_ref = self.identity_api.get_user(user['id']) + # 'email' attribute should've created because it is also being used + # as user_id + self.assertEqual(user_ref['id'], user_ref['email']) + + def test_user_id_attribute_map(self): + conf = self.get_config(CONF.identity.default_domain_id) + conf.ldap.user_id_attribute = 'mail' + self.reload_backends(CONF.identity.default_domain_id) + + user_ref = self.identity_api.get_user(self.user_foo['email']) + # the user_id_attribute map should be honored, which means + # user_ref['id'] should contains the email attribute + self.assertEqual(self.user_foo['email'], user_ref['id']) + + @mock.patch.object(common_ldap_core.BaseLdap, '_ldap_get') + def test_get_id_from_dn_for_multivalued_attribute_id(self, mock_ldap_get): + conf = self.get_config(CONF.identity.default_domain_id) + conf.ldap.user_id_attribute = 'mail' + self.reload_backends(CONF.identity.default_domain_id) + + # make 'email' multivalued so we can test the error condition + email1 = uuid.uuid4().hex + email2 = uuid.uuid4().hex + mock_ldap_get.return_value = ( + 'cn=nobodycares,dc=example,dc=com', + { + 'sn': [uuid.uuid4().hex], + 'mail': [email1, email2], + 'cn': 'nobodycares' + } + ) + + user_ref = self.identity_api.get_user(email1) + # make sure we get the ID from DN (old behavior) if the ID attribute + # has multiple values + self.assertEqual('nobodycares', user_ref['id']) + + @mock.patch.object(common_ldap_core.BaseLdap, '_ldap_get') + def test_id_attribute_not_found(self, mock_ldap_get): + mock_ldap_get.return_value = ( + 'cn=nobodycares,dc=example,dc=com', + { + 'sn': [uuid.uuid4().hex], + } + ) + + user_api = identity.backends.ldap.UserApi(CONF) + self.assertRaises(exception.NotFound, + user_api.get, + 'nobodycares') + + @mock.patch.object(common_ldap_core.BaseLdap, '_ldap_get') + def test_user_id_not_in_dn(self, mock_ldap_get): + conf = self.get_config(CONF.identity.default_domain_id) + conf.ldap.user_id_attribute = 'uid' + conf.ldap.user_name_attribute = 'cn' + self.reload_backends(CONF.identity.default_domain_id) + + mock_ldap_get.return_value = ( + 'foo=bar,dc=example,dc=com', + { + 'sn': [uuid.uuid4().hex], + 'foo': ['bar'], + 'cn': ['junk'], + 'uid': ['crap'] + } + ) + user_ref = self.identity_api.get_user('crap') + self.assertEqual('crap', user_ref['id']) + self.assertEqual('junk', user_ref['name']) + + @mock.patch.object(common_ldap_core.BaseLdap, '_ldap_get') + def test_user_name_in_dn(self, mock_ldap_get): + conf = self.get_config(CONF.identity.default_domain_id) + conf.ldap.user_id_attribute = 'sAMAccountName' + conf.ldap.user_name_attribute = 'cn' + self.reload_backends(CONF.identity.default_domain_id) + + mock_ldap_get.return_value = ( + 'cn=Foo Bar,dc=example,dc=com', + { + 'sn': [uuid.uuid4().hex], + 'cn': ['Foo Bar'], + 'SAMAccountName': ['crap'] + } + ) + user_ref = self.identity_api.get_user('crap') + self.assertEqual('crap', user_ref['id']) + self.assertEqual('Foo Bar', user_ref['name']) + + +class LDAPIdentityEnabledEmulation(LDAPIdentity): + def setUp(self): + super(LDAPIdentityEnabledEmulation, self).setUp() + self.clear_database() + self.load_backends() + self.load_fixtures(default_fixtures) + for obj in [self.tenant_bar, self.tenant_baz, self.user_foo, + self.user_two, self.user_badguy]: + obj.setdefault('enabled', True) + + def load_fixtures(self, fixtures): + # Override super impl since need to create group container. + create_group_container(self.identity_api) + super(LDAPIdentity, self).load_fixtures(fixtures) + + def config_files(self): + config_files = super(LDAPIdentityEnabledEmulation, self).config_files() + config_files.append(tests.dirs.tests_conf('backend_ldap.conf')) + return config_files + + def config_overrides(self): + super(LDAPIdentityEnabledEmulation, self).config_overrides() + self.config_fixture.config(group='ldap', + user_enabled_emulation=True, + project_enabled_emulation=True) + + def test_project_crud(self): + # NOTE(topol): LDAPIdentityEnabledEmulation will create an + # enabled key in the project dictionary so this + # method override handles this side-effect + project = { + 'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'domain_id': CONF.identity.default_domain_id, + 'description': uuid.uuid4().hex, + 'parent_id': None} + + self.resource_api.create_project(project['id'], project) + project_ref = self.resource_api.get_project(project['id']) + + # self.resource_api.create_project adds an enabled + # key with a value of True when LDAPIdentityEnabledEmulation + # is used so we now add this expected key to the project dictionary + project['enabled'] = True + self.assertDictEqual(project_ref, project) + + project['description'] = uuid.uuid4().hex + self.resource_api.update_project(project['id'], project) + project_ref = self.resource_api.get_project(project['id']) + self.assertDictEqual(project_ref, project) + + self.resource_api.delete_project(project['id']) + self.assertRaises(exception.ProjectNotFound, + self.resource_api.get_project, + project['id']) + + def test_user_crud(self): + user_dict = { + 'domain_id': CONF.identity.default_domain_id, + 'name': uuid.uuid4().hex, + 'password': uuid.uuid4().hex} + user = self.identity_api.create_user(user_dict) + user_dict['enabled'] = True + user_ref = self.identity_api.get_user(user['id']) + del user_dict['password'] + user_ref_dict = {x: user_ref[x] for x in user_ref} + self.assertDictContainsSubset(user_dict, user_ref_dict) + + user_dict['password'] = uuid.uuid4().hex + self.identity_api.update_user(user['id'], user) + user_ref = self.identity_api.get_user(user['id']) + del user_dict['password'] + user_ref_dict = {x: user_ref[x] for x in user_ref} + self.assertDictContainsSubset(user_dict, user_ref_dict) + + self.identity_api.delete_user(user['id']) + self.assertRaises(exception.UserNotFound, + self.identity_api.get_user, + user['id']) + + def test_user_auth_emulated(self): + self.config_fixture.config(group='ldap', + user_enabled_emulation_dn='cn=test,dc=test') + self.reload_backends(CONF.identity.default_domain_id) + self.identity_api.authenticate( + context={}, + user_id=self.user_foo['id'], + password=self.user_foo['password']) + + def test_user_enable_attribute_mask(self): + self.skipTest( + "Enabled emulation conflicts with enabled mask") + + def test_user_enabled_invert(self): + self.config_fixture.config(group='ldap', user_enabled_invert=True, + user_enabled_default=False) + self.clear_database() + self.load_backends() + self.load_fixtures(default_fixtures) + + user1 = {'name': u'fäké1', 'enabled': True, + 'domain_id': CONF.identity.default_domain_id} + + user2 = {'name': u'fäké2', 'enabled': False, + 'domain_id': CONF.identity.default_domain_id} + + user3 = {'name': u'fäké3', + 'domain_id': CONF.identity.default_domain_id} + + # Ensure that the enabled LDAP attribute is not set for a + # newly created enabled user. + user_ref = self.identity_api.create_user(user1) + self.assertIs(True, user_ref['enabled']) + self.assertIsNone(self.get_user_enabled_vals(user_ref)) + user_ref = self.identity_api.get_user(user_ref['id']) + self.assertIs(True, user_ref['enabled']) + + # Ensure that an enabled LDAP attribute is not set for a disabled user. + user1['enabled'] = False + user_ref = self.identity_api.update_user(user_ref['id'], user1) + self.assertIs(False, user_ref['enabled']) + self.assertIsNone(self.get_user_enabled_vals(user_ref)) + + # Enable the user and ensure that the LDAP enabled + # attribute is not set. + user1['enabled'] = True + user_ref = self.identity_api.update_user(user_ref['id'], user1) + self.assertIs(True, user_ref['enabled']) + self.assertIsNone(self.get_user_enabled_vals(user_ref)) + + # Ensure that the LDAP enabled attribute is not set for a + # newly created disabled user. + user_ref = self.identity_api.create_user(user2) + self.assertIs(False, user_ref['enabled']) + self.assertIsNone(self.get_user_enabled_vals(user_ref)) + user_ref = self.identity_api.get_user(user_ref['id']) + self.assertIs(False, user_ref['enabled']) + + # Ensure that the LDAP enabled attribute is not set for a newly created + # user when the user_enabled_default setting is used. + user_ref = self.identity_api.create_user(user3) + self.assertIs(True, user_ref['enabled']) + self.assertIsNone(self.get_user_enabled_vals(user_ref)) + user_ref = self.identity_api.get_user(user_ref['id']) + self.assertIs(True, user_ref['enabled']) + + def test_user_enabled_invert_no_enabled_value(self): + self.skipTest( + "N/A: Covered by test_user_enabled_invert") + + def test_user_enabled_invert_default_str_value(self): + self.skipTest( + "N/A: Covered by test_user_enabled_invert") + + @mock.patch.object(common_ldap_core.BaseLdap, '_ldap_get') + def test_user_enabled_attribute_handles_utf8(self, mock_ldap_get): + # Since user_enabled_emulation is enabled in this test, this test will + # fail since it's using user_enabled_invert. + self.config_fixture.config(group='ldap', user_enabled_invert=True, + user_enabled_attribute='passwordisexpired') + mock_ldap_get.return_value = ( + u'uid=123456789,c=us,ou=our_ldap,o=acme.com', + { + 'uid': [123456789], + 'mail': [u'shaun@acme.com'], + 'passwordisexpired': [u'false'], + 'cn': [u'uid=123456789,c=us,ou=our_ldap,o=acme.com'] + } + ) + + user_api = identity.backends.ldap.UserApi(CONF) + user_ref = user_api.get('123456789') + self.assertIs(False, user_ref['enabled']) + + +class LdapIdentitySqlAssignment(BaseLDAPIdentity, tests.SQLDriverOverrides, + tests.TestCase): + + def config_files(self): + config_files = super(LdapIdentitySqlAssignment, self).config_files() + config_files.append(tests.dirs.tests_conf('backend_ldap_sql.conf')) + return config_files + + def setUp(self): + self.useFixture(database.Database()) + super(LdapIdentitySqlAssignment, self).setUp() + self.clear_database() + self.load_backends() + cache.configure_cache_region(cache.REGION) + self.engine = sql.get_engine() + self.addCleanup(sql.cleanup) + + sql.ModelBase.metadata.create_all(bind=self.engine) + self.addCleanup(sql.ModelBase.metadata.drop_all, bind=self.engine) + + self.load_fixtures(default_fixtures) + # defaulted by the data load + self.user_foo['enabled'] = True + + def config_overrides(self): + super(LdapIdentitySqlAssignment, self).config_overrides() + self.config_fixture.config( + group='identity', + driver='keystone.identity.backends.ldap.Identity') + self.config_fixture.config( + group='resource', + driver='keystone.resource.backends.sql.Resource') + self.config_fixture.config( + group='assignment', + driver='keystone.assignment.backends.sql.Assignment') + + def test_domain_crud(self): + pass + + def test_list_domains(self): + domains = self.resource_api.list_domains() + self.assertEqual([resource.calc_default_domain()], domains) + + def test_list_domains_non_default_domain_id(self): + # If change the default_domain_id, the ID of the default domain + # returned by list_domains doesn't change because the SQL identity + # backend reads it from the database, which doesn't get updated by + # config change. + + orig_default_domain_id = CONF.identity.default_domain_id + + new_domain_id = uuid.uuid4().hex + self.config_fixture.config(group='identity', + default_domain_id=new_domain_id) + + domains = self.resource_api.list_domains() + + self.assertEqual(orig_default_domain_id, domains[0]['id']) + + def test_create_domain(self): + domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'enabled': True} + self.assertRaises(exception.Forbidden, + self.resource_api.create_domain, + domain['id'], + domain) + + def test_get_and_remove_role_grant_by_group_and_domain(self): + # TODO(henry-nash): We should really rewrite the tests in test_backend + # to be more flexible as to where the domains are sourced from, so + # that we would not need to override such tests here. This is raised + # as bug 1373865. + new_domain = self._get_domain_fixture() + new_group = {'domain_id': new_domain['id'], 'name': uuid.uuid4().hex} + new_group = self.identity_api.create_group(new_group) + new_user = {'name': 'new_user', 'password': uuid.uuid4().hex, + 'enabled': True, 'domain_id': new_domain['id']} + new_user = self.identity_api.create_user(new_user) + self.identity_api.add_user_to_group(new_user['id'], + new_group['id']) + + roles_ref = self.assignment_api.list_grants( + group_id=new_group['id'], + domain_id=new_domain['id']) + self.assertEqual(0, len(roles_ref)) + + self.assignment_api.create_grant(group_id=new_group['id'], + domain_id=new_domain['id'], + role_id='member') + + roles_ref = self.assignment_api.list_grants( + group_id=new_group['id'], + domain_id=new_domain['id']) + self.assertDictEqual(roles_ref[0], self.role_member) + + self.assignment_api.delete_grant(group_id=new_group['id'], + domain_id=new_domain['id'], + role_id='member') + roles_ref = self.assignment_api.list_grants( + group_id=new_group['id'], + domain_id=new_domain['id']) + self.assertEqual(0, len(roles_ref)) + self.assertRaises(exception.NotFound, + self.assignment_api.delete_grant, + group_id=new_group['id'], + domain_id=new_domain['id'], + role_id='member') + + def test_project_enabled_ignored_disable_error(self): + # Override + self.skipTest("Doesn't apply since LDAP configuration is ignored for " + "SQL assignment backend.") + + +class LdapIdentitySqlAssignmentWithMapping(LdapIdentitySqlAssignment): + """Class to test mapping of default LDAP backend. + + The default configuration is not to enable mapping when using a single + backend LDAP driver. However, a cloud provider might want to enable + the mapping, hence hiding the LDAP IDs from any clients of keystone. + Setting backward_compatible_ids to False will enable this mapping. + + """ + def config_overrides(self): + super(LdapIdentitySqlAssignmentWithMapping, self).config_overrides() + self.config_fixture.config(group='identity_mapping', + backward_compatible_ids=False) + + def test_dynamic_mapping_build(self): + """Test to ensure entities not create via controller are mapped. + + Many LDAP backends will, essentially, by Read Only. In these cases + the mapping is not built by creating objects, rather from enumerating + the entries. We test this here my manually deleting the mapping and + then trying to re-read the entries. + + """ + initial_mappings = len(mapping_sql.list_id_mappings()) + user1 = {'name': uuid.uuid4().hex, + 'domain_id': CONF.identity.default_domain_id, + 'password': uuid.uuid4().hex, 'enabled': True} + user1 = self.identity_api.create_user(user1) + user2 = {'name': uuid.uuid4().hex, + 'domain_id': CONF.identity.default_domain_id, + 'password': uuid.uuid4().hex, 'enabled': True} + user2 = self.identity_api.create_user(user2) + mappings = mapping_sql.list_id_mappings() + self.assertEqual(initial_mappings + 2, len(mappings)) + + # Now delete the mappings for the two users above + self.id_mapping_api.purge_mappings({'public_id': user1['id']}) + self.id_mapping_api.purge_mappings({'public_id': user2['id']}) + + # We should no longer be able to get these users via their old IDs + self.assertRaises(exception.UserNotFound, + self.identity_api.get_user, + user1['id']) + self.assertRaises(exception.UserNotFound, + self.identity_api.get_user, + user2['id']) + + # Now enumerate all users...this should re-build the mapping, and + # we should be able to find the users via their original public IDs. + self.identity_api.list_users() + self.identity_api.get_user(user1['id']) + self.identity_api.get_user(user2['id']) + + def test_get_roles_for_user_and_project_user_group_same_id(self): + self.skipTest('N/A: We never generate the same ID for a user and ' + 'group in our mapping table') + + +class BaseMultiLDAPandSQLIdentity(object): + """Mixin class with support methods for domain-specific config testing.""" + + def create_user(self, domain_id): + user = {'name': uuid.uuid4().hex, + 'domain_id': domain_id, + 'password': uuid.uuid4().hex, + 'enabled': True} + user_ref = self.identity_api.create_user(user) + # Put the password back in, since this is used later by tests to + # authenticate. + user_ref['password'] = user['password'] + return user_ref + + def create_users_across_domains(self): + """Create a set of users, each with a role on their own domain.""" + + # We also will check that the right number of id mappings get created + initial_mappings = len(mapping_sql.list_id_mappings()) + + self.users['user0'] = self.create_user( + self.domains['domain_default']['id']) + self.assignment_api.create_grant( + user_id=self.users['user0']['id'], + domain_id=self.domains['domain_default']['id'], + role_id=self.role_member['id']) + for x in range(1, self.domain_count): + self.users['user%s' % x] = self.create_user( + self.domains['domain%s' % x]['id']) + self.assignment_api.create_grant( + user_id=self.users['user%s' % x]['id'], + domain_id=self.domains['domain%s' % x]['id'], + role_id=self.role_member['id']) + + # So how many new id mappings should have been created? One for each + # user created in a domain that is using the non default driver.. + self.assertEqual(initial_mappings + self.domain_specific_count, + len(mapping_sql.list_id_mappings())) + + def check_user(self, user, domain_id, expected_status): + """Check user is in correct backend. + + As part of the tests, we want to force ourselves to manually + select the driver for a given domain, to make sure the entity + ended up in the correct backend. + + """ + driver = self.identity_api._select_identity_driver(domain_id) + unused, unused, entity_id = ( + self.identity_api._get_domain_driver_and_entity_id( + user['id'])) + + if expected_status == 200: + ref = driver.get_user(entity_id) + ref = self.identity_api._set_domain_id_and_mapping( + ref, domain_id, driver, map.EntityType.USER) + user = user.copy() + del user['password'] + self.assertDictEqual(ref, user) + else: + # TODO(henry-nash): Use AssertRaises here, although + # there appears to be an issue with using driver.get_user + # inside that construct + try: + driver.get_user(entity_id) + except expected_status: + pass + + def setup_initial_domains(self): + + def create_domain(domain): + try: + ref = self.resource_api.create_domain( + domain['id'], domain) + except exception.Conflict: + ref = ( + self.resource_api.get_domain_by_name(domain['name'])) + return ref + + self.domains = {} + for x in range(1, self.domain_count): + domain = 'domain%s' % x + self.domains[domain] = create_domain( + {'id': uuid.uuid4().hex, 'name': domain}) + self.domains['domain_default'] = create_domain( + resource.calc_default_domain()) + + def test_authenticate_to_each_domain(self): + """Test that a user in each domain can authenticate.""" + for user_num in range(self.domain_count): + user = 'user%s' % user_num + self.identity_api.authenticate( + context={}, + user_id=self.users[user]['id'], + password=self.users[user]['password']) + + +class MultiLDAPandSQLIdentity(BaseLDAPIdentity, tests.SQLDriverOverrides, + tests.TestCase, BaseMultiLDAPandSQLIdentity): + """Class to test common SQL plus individual LDAP backends. + + We define a set of domains and domain-specific backends: + + - A separate LDAP backend for the default domain + - A separate LDAP backend for domain1 + - domain2 shares the same LDAP as domain1, but uses a different + tree attach point + - An SQL backend for all other domains (which will include domain3 + and domain4) + + Normally one would expect that the default domain would be handled as + part of the "other domains" - however the above provides better + test coverage since most of the existing backend tests use the default + domain. + + """ + def setUp(self): + self.useFixture(database.Database()) + super(MultiLDAPandSQLIdentity, self).setUp() + + self.load_backends() + + self.engine = sql.get_engine() + self.addCleanup(sql.cleanup) + + sql.ModelBase.metadata.create_all(bind=self.engine) + self.addCleanup(sql.ModelBase.metadata.drop_all, bind=self.engine) + + self.domain_count = 5 + self.domain_specific_count = 3 + self.setup_initial_domains() + self._setup_initial_users() + + # All initial test data setup complete, time to switch on support + # for separate backends per domain. + self.enable_multi_domain() + + self.clear_database() + self.load_fixtures(default_fixtures) + self.create_users_across_domains() + + def config_overrides(self): + super(MultiLDAPandSQLIdentity, self).config_overrides() + # Make sure identity and assignment are actually SQL drivers, + # BaseLDAPIdentity sets these options to use LDAP. + self.config_fixture.config( + group='identity', + driver='keystone.identity.backends.sql.Identity') + self.config_fixture.config( + group='resource', + driver='keystone.resource.backends.sql.Resource') + self.config_fixture.config( + group='assignment', + driver='keystone.assignment.backends.sql.Assignment') + + def _setup_initial_users(self): + # Create some identity entities BEFORE we switch to multi-backend, so + # we can test that these are still accessible + self.users = {} + self.users['userA'] = self.create_user( + self.domains['domain_default']['id']) + self.users['userB'] = self.create_user( + self.domains['domain1']['id']) + self.users['userC'] = self.create_user( + self.domains['domain3']['id']) + + def enable_multi_domain(self): + """Enable the chosen form of multi domain configuration support. + + This method enables the file-based configuration support. Child classes + that wish to use the database domain configuration support should + override this method and set the appropriate config_fixture option. + + """ + self.config_fixture.config( + group='identity', domain_specific_drivers_enabled=True, + domain_config_dir=tests.TESTCONF + '/domain_configs_multi_ldap') + self.config_fixture.config(group='identity_mapping', + backward_compatible_ids=False) + + def reload_backends(self, domain_id): + # Just reload the driver for this domain - which will pickup + # any updated cfg + self.identity_api.domain_configs.reload_domain_driver(domain_id) + + def get_config(self, domain_id): + # Get the config for this domain, will return CONF + # if no specific config defined for this domain + return self.identity_api.domain_configs.get_domain_conf(domain_id) + + def test_list_domains(self): + self.skipTest( + 'N/A: Not relevant for multi ldap testing') + + def test_list_domains_non_default_domain_id(self): + self.skipTest( + 'N/A: Not relevant for multi ldap testing') + + def test_list_users(self): + # Override the standard list users, since we have added an extra user + # to the default domain, so the number of expected users is one more + # than in the standard test. + users = self.identity_api.list_users( + domain_scope=self._set_domain_scope( + CONF.identity.default_domain_id)) + self.assertEqual(len(default_fixtures.USERS) + 1, len(users)) + user_ids = set(user['id'] for user in users) + expected_user_ids = set(getattr(self, 'user_%s' % user['id'])['id'] + for user in default_fixtures.USERS) + expected_user_ids.add(self.users['user0']['id']) + for user_ref in users: + self.assertNotIn('password', user_ref) + self.assertEqual(expected_user_ids, user_ids) + + def test_domain_segregation(self): + """Test that separate configs have segregated the domain. + + Test Plan: + + - Users were created in each domain as part of setup, now make sure + you can only find a given user in its relevant domain/backend + - Make sure that for a backend that supports multiple domains + you can get the users via any of its domains + + """ + # Check that I can read a user with the appropriate domain-selected + # driver, but won't find it via any other domain driver + + check_user = self.check_user + check_user(self.users['user0'], + self.domains['domain_default']['id'], 200) + for domain in [self.domains['domain1']['id'], + self.domains['domain2']['id'], + self.domains['domain3']['id'], + self.domains['domain4']['id']]: + check_user(self.users['user0'], domain, exception.UserNotFound) + + check_user(self.users['user1'], self.domains['domain1']['id'], 200) + for domain in [self.domains['domain_default']['id'], + self.domains['domain2']['id'], + self.domains['domain3']['id'], + self.domains['domain4']['id']]: + check_user(self.users['user1'], domain, exception.UserNotFound) + + check_user(self.users['user2'], self.domains['domain2']['id'], 200) + for domain in [self.domains['domain_default']['id'], + self.domains['domain1']['id'], + self.domains['domain3']['id'], + self.domains['domain4']['id']]: + check_user(self.users['user2'], domain, exception.UserNotFound) + + # domain3 and domain4 share the same backend, so you should be + # able to see user3 and user4 from either. + + check_user(self.users['user3'], self.domains['domain3']['id'], 200) + check_user(self.users['user3'], self.domains['domain4']['id'], 200) + check_user(self.users['user4'], self.domains['domain3']['id'], 200) + check_user(self.users['user4'], self.domains['domain4']['id'], 200) + + for domain in [self.domains['domain_default']['id'], + self.domains['domain1']['id'], + self.domains['domain2']['id']]: + check_user(self.users['user3'], domain, exception.UserNotFound) + check_user(self.users['user4'], domain, exception.UserNotFound) + + # Finally, going through the regular manager layer, make sure we + # only see the right number of users in each of the non-default + # domains. One might have expected two users in domain1 (since we + # created one before we switched to multi-backend), however since + # that domain changed backends in the switch we don't find it anymore. + # This is as designed - we don't support moving domains between + # backends. + # + # The listing of the default domain is already handled in the + # test_lists_users() method. + for domain in [self.domains['domain1']['id'], + self.domains['domain2']['id'], + self.domains['domain4']['id']]: + self.assertThat( + self.identity_api.list_users(domain_scope=domain), + matchers.HasLength(1)) + + # domain3 had a user created before we switched on + # multiple backends, plus one created afterwards - and its + # backend has not changed - so we should find two. + self.assertThat( + self.identity_api.list_users( + domain_scope=self.domains['domain3']['id']), + matchers.HasLength(2)) + + def test_existing_uuids_work(self): + """Test that 'uni-domain' created IDs still work. + + Throwing the switch to domain-specific backends should not cause + existing identities to be inaccessible via ID. + + """ + self.identity_api.get_user(self.users['userA']['id']) + self.identity_api.get_user(self.users['userB']['id']) + self.identity_api.get_user(self.users['userC']['id']) + + def test_scanning_of_config_dir(self): + """Test the Manager class scans the config directory. + + The setup for the main tests above load the domain configs directly + so that the test overrides can be included. This test just makes sure + that the standard config directory scanning does pick up the relevant + domain config files. + + """ + # Confirm that config has drivers_enabled as True, which we will + # check has been set to False later in this test + self.assertTrue(CONF.identity.domain_specific_drivers_enabled) + self.load_backends() + # Execute any command to trigger the lazy loading of domain configs + self.identity_api.list_users( + domain_scope=self.domains['domain1']['id']) + # ...and now check the domain configs have been set up + self.assertIn('default', self.identity_api.domain_configs) + self.assertIn(self.domains['domain1']['id'], + self.identity_api.domain_configs) + self.assertIn(self.domains['domain2']['id'], + self.identity_api.domain_configs) + self.assertNotIn(self.domains['domain3']['id'], + self.identity_api.domain_configs) + self.assertNotIn(self.domains['domain4']['id'], + self.identity_api.domain_configs) + + # Finally check that a domain specific config contains items from both + # the primary config and the domain specific config + conf = self.identity_api.domain_configs.get_domain_conf( + self.domains['domain1']['id']) + # This should now be false, as is the default, since this is not + # set in the standard primary config file + self.assertFalse(conf.identity.domain_specific_drivers_enabled) + # ..and make sure a domain-specific options is also set + self.assertEqual('fake://memory1', conf.ldap.url) + + def test_delete_domain_with_user_added(self): + domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'enabled': True} + project = {'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'domain_id': domain['id'], + 'description': uuid.uuid4().hex, + 'parent_id': None, + 'enabled': True} + self.resource_api.create_domain(domain['id'], domain) + self.resource_api.create_project(project['id'], project) + project_ref = self.resource_api.get_project(project['id']) + self.assertDictEqual(project_ref, project) + + self.assignment_api.create_grant(user_id=self.user_foo['id'], + project_id=project['id'], + role_id=self.role_member['id']) + self.assignment_api.delete_grant(user_id=self.user_foo['id'], + project_id=project['id'], + role_id=self.role_member['id']) + domain['enabled'] = False + self.resource_api.update_domain(domain['id'], domain) + self.resource_api.delete_domain(domain['id']) + self.assertRaises(exception.DomainNotFound, + self.resource_api.get_domain, + domain['id']) + + def test_user_enabled_ignored_disable_error(self): + # Override. + self.skipTest("Doesn't apply since LDAP config has no affect on the " + "SQL identity backend.") + + def test_group_enabled_ignored_disable_error(self): + # Override. + self.skipTest("Doesn't apply since LDAP config has no affect on the " + "SQL identity backend.") + + def test_project_enabled_ignored_disable_error(self): + # Override + self.skipTest("Doesn't apply since LDAP configuration is ignored for " + "SQL assignment backend.") + + +class MultiLDAPandSQLIdentityDomainConfigsInSQL(MultiLDAPandSQLIdentity): + """Class to test the use of domain configs stored in the database. + + Repeat the same tests as MultiLDAPandSQLIdentity, but instead of using the + domain specific config files, store the domain specific values in the + database. + + """ + def enable_multi_domain(self): + # The values below are the same as in the domain_configs_multi_ldap + # cdirectory of test config_files. + default_config = { + 'ldap': {'url': 'fake://memory', + 'user': 'cn=Admin', + 'password': 'password', + 'suffix': 'cn=example,cn=com'}, + 'identity': {'driver': 'keystone.identity.backends.ldap.Identity'} + } + domain1_config = { + 'ldap': {'url': 'fake://memory1', + 'user': 'cn=Admin', + 'password': 'password', + 'suffix': 'cn=example,cn=com'}, + 'identity': {'driver': 'keystone.identity.backends.ldap.Identity'} + } + domain2_config = { + 'ldap': {'url': 'fake://memory', + 'user': 'cn=Admin', + 'password': 'password', + 'suffix': 'cn=myroot,cn=com', + 'group_tree_dn': 'ou=UserGroups,dc=myroot,dc=org', + 'user_tree_dn': 'ou=Users,dc=myroot,dc=org'}, + 'identity': {'driver': 'keystone.identity.backends.ldap.Identity'} + } + + self.domain_config_api.create_config(CONF.identity.default_domain_id, + default_config) + self.domain_config_api.create_config(self.domains['domain1']['id'], + domain1_config) + self.domain_config_api.create_config(self.domains['domain2']['id'], + domain2_config) + + self.config_fixture.config( + group='identity', domain_specific_drivers_enabled=True, + domain_configurations_from_database=True) + self.config_fixture.config(group='identity_mapping', + backward_compatible_ids=False) + + def test_domain_config_has_no_impact_if_database_support_disabled(self): + """Ensure database domain configs have no effect if disabled. + + Set reading from database configs to false, restart the backends + and then try and set and use database configs. + + """ + self.config_fixture.config( + group='identity', domain_configurations_from_database=False) + self.load_backends() + new_config = {'ldap': {'url': uuid.uuid4().hex}} + self.domain_config_api.create_config( + CONF.identity.default_domain_id, new_config) + # Trigger the identity backend to initialise any domain specific + # configurations + self.identity_api.list_users() + # Check that the new config has not been passed to the driver for + # the default domain. + default_config = ( + self.identity_api.domain_configs.get_domain_conf( + CONF.identity.default_domain_id)) + self.assertEqual(CONF.ldap.url, default_config.ldap.url) + + +class DomainSpecificLDAPandSQLIdentity( + BaseLDAPIdentity, tests.SQLDriverOverrides, tests.TestCase, + BaseMultiLDAPandSQLIdentity): + """Class to test when all domains use specific configs, including SQL. + + We define a set of domains and domain-specific backends: + + - A separate LDAP backend for the default domain + - A separate SQL backend for domain1 + + Although the default driver still exists, we don't use it. + + """ + def setUp(self): + self.useFixture(database.Database()) + super(DomainSpecificLDAPandSQLIdentity, self).setUp() + self.initial_setup() + + def initial_setup(self): + # We aren't setting up any initial data ahead of switching to + # domain-specific operation, so make the switch straight away. + self.config_fixture.config( + group='identity', domain_specific_drivers_enabled=True, + domain_config_dir=( + tests.TESTCONF + '/domain_configs_one_sql_one_ldap')) + self.config_fixture.config(group='identity_mapping', + backward_compatible_ids=False) + + self.load_backends() + + self.engine = sql.get_engine() + self.addCleanup(sql.cleanup) + + sql.ModelBase.metadata.create_all(bind=self.engine) + self.addCleanup(sql.ModelBase.metadata.drop_all, bind=self.engine) + + self.domain_count = 2 + self.domain_specific_count = 2 + self.setup_initial_domains() + self.users = {} + + self.clear_database() + self.load_fixtures(default_fixtures) + self.create_users_across_domains() + + def config_overrides(self): + super(DomainSpecificLDAPandSQLIdentity, self).config_overrides() + # Make sure resource & assignment are actually SQL drivers, + # BaseLDAPIdentity causes this option to use LDAP. + self.config_fixture.config( + group='resource', + driver='keystone.resource.backends.sql.Resource') + self.config_fixture.config( + group='assignment', + driver='keystone.assignment.backends.sql.Assignment') + + def reload_backends(self, domain_id): + # Just reload the driver for this domain - which will pickup + # any updated cfg + self.identity_api.domain_configs.reload_domain_driver(domain_id) + + def get_config(self, domain_id): + # Get the config for this domain, will return CONF + # if no specific config defined for this domain + return self.identity_api.domain_configs.get_domain_conf(domain_id) + + def test_list_domains(self): + self.skipTest( + 'N/A: Not relevant for multi ldap testing') + + def test_list_domains_non_default_domain_id(self): + self.skipTest( + 'N/A: Not relevant for multi ldap testing') + + def test_domain_crud(self): + self.skipTest( + 'N/A: Not relevant for multi ldap testing') + + def test_list_users(self): + # Override the standard list users, since we have added an extra user + # to the default domain, so the number of expected users is one more + # than in the standard test. + users = self.identity_api.list_users( + domain_scope=self._set_domain_scope( + CONF.identity.default_domain_id)) + self.assertEqual(len(default_fixtures.USERS) + 1, len(users)) + user_ids = set(user['id'] for user in users) + expected_user_ids = set(getattr(self, 'user_%s' % user['id'])['id'] + for user in default_fixtures.USERS) + expected_user_ids.add(self.users['user0']['id']) + for user_ref in users: + self.assertNotIn('password', user_ref) + self.assertEqual(expected_user_ids, user_ids) + + def test_domain_segregation(self): + """Test that separate configs have segregated the domain. + + Test Plan: + + - Users were created in each domain as part of setup, now make sure + you can only find a given user in its relevant domain/backend + - Make sure that for a backend that supports multiple domains + you can get the users via any of its domains + + """ + # Check that I can read a user with the appropriate domain-selected + # driver, but won't find it via any other domain driver + + self.check_user(self.users['user0'], + self.domains['domain_default']['id'], 200) + self.check_user(self.users['user0'], + self.domains['domain1']['id'], exception.UserNotFound) + + self.check_user(self.users['user1'], + self.domains['domain1']['id'], 200) + self.check_user(self.users['user1'], + self.domains['domain_default']['id'], + exception.UserNotFound) + + # Finally, going through the regular manager layer, make sure we + # only see the right number of users in the non-default domain. + + self.assertThat( + self.identity_api.list_users( + domain_scope=self.domains['domain1']['id']), + matchers.HasLength(1)) + + def test_add_role_grant_to_user_and_project_404(self): + self.skipTest('Blocked by bug 1101287') + + def test_get_role_grants_for_user_and_project_404(self): + self.skipTest('Blocked by bug 1101287') + + def test_list_projects_for_user_with_grants(self): + self.skipTest('Blocked by bug 1221805') + + def test_get_roles_for_user_and_project_user_group_same_id(self): + self.skipTest('N/A: We never generate the same ID for a user and ' + 'group in our mapping table') + + def test_user_id_comma(self): + self.skipTest('Only valid if it is guaranteed to be talking to ' + 'the fakeldap backend') + + def test_user_id_comma_grants(self): + self.skipTest('Only valid if it is guaranteed to be talking to ' + 'the fakeldap backend') + + def test_user_enabled_ignored_disable_error(self): + # Override. + self.skipTest("Doesn't apply since LDAP config has no affect on the " + "SQL identity backend.") + + def test_group_enabled_ignored_disable_error(self): + # Override. + self.skipTest("Doesn't apply since LDAP config has no affect on the " + "SQL identity backend.") + + def test_project_enabled_ignored_disable_error(self): + # Override + self.skipTest("Doesn't apply since LDAP configuration is ignored for " + "SQL assignment backend.") + + +class DomainSpecificSQLIdentity(DomainSpecificLDAPandSQLIdentity): + """Class to test simplest use of domain-specific SQL driver. + + The simplest use of an SQL domain-specific backend is when it is used to + augment the standard case when LDAP is the default driver defined in the + main config file. This would allow, for example, service users to be + stored in SQL while LDAP handles the rest. Hence we define: + + - The default driver uses the LDAP backend for the default domain + - A separate SQL backend for domain1 + + """ + def initial_setup(self): + # We aren't setting up any initial data ahead of switching to + # domain-specific operation, so make the switch straight away. + self.config_fixture.config( + group='identity', domain_specific_drivers_enabled=True, + domain_config_dir=( + tests.TESTCONF + '/domain_configs_default_ldap_one_sql')) + # Part of the testing counts how many new mappings get created as + # we create users, so ensure we are NOT using mapping for the default + # LDAP domain so this doesn't confuse the calculation. + self.config_fixture.config(group='identity_mapping', + backward_compatible_ids=True) + + self.load_backends() + + self.engine = sql.get_engine() + self.addCleanup(sql.cleanup) + + sql.ModelBase.metadata.create_all(bind=self.engine) + self.addCleanup(sql.ModelBase.metadata.drop_all, bind=self.engine) + + self.domain_count = 2 + self.domain_specific_count = 1 + self.setup_initial_domains() + self.users = {} + + self.load_fixtures(default_fixtures) + self.create_users_across_domains() + + def config_overrides(self): + super(DomainSpecificSQLIdentity, self).config_overrides() + self.config_fixture.config( + group='identity', + driver='keystone.identity.backends.ldap.Identity') + self.config_fixture.config( + group='resource', + driver='keystone.resource.backends.sql.Resource') + self.config_fixture.config( + group='assignment', + driver='keystone.assignment.backends.sql.Assignment') + + def get_config(self, domain_id): + if domain_id == CONF.identity.default_domain_id: + return CONF + else: + return self.identity_api.domain_configs.get_domain_conf(domain_id) + + def reload_backends(self, domain_id): + if domain_id == CONF.identity.default_domain_id: + self.load_backends() + else: + # Just reload the driver for this domain - which will pickup + # any updated cfg + self.identity_api.domain_configs.reload_domain_driver(domain_id) + + def test_default_sql_plus_sql_specific_driver_fails(self): + # First confirm that if ldap is default driver, domain1 can be + # loaded as sql + self.config_fixture.config( + group='identity', + driver='keystone.identity.backends.ldap.Identity') + self.config_fixture.config( + group='assignment', + driver='keystone.assignment.backends.sql.Assignment') + self.load_backends() + # Make any identity call to initiate the lazy loading of configs + self.identity_api.list_users( + domain_scope=CONF.identity.default_domain_id) + self.assertIsNotNone(self.get_config(self.domains['domain1']['id'])) + + # Now re-initialize, but with sql as the default identity driver + self.config_fixture.config( + group='identity', + driver='keystone.identity.backends.sql.Identity') + self.config_fixture.config( + group='assignment', + driver='keystone.assignment.backends.sql.Assignment') + self.load_backends() + # Make any identity call to initiate the lazy loading of configs, which + # should fail since we would now have two sql drivers. + self.assertRaises(exception.MultipleSQLDriversInConfig, + self.identity_api.list_users, + domain_scope=CONF.identity.default_domain_id) + + def test_multiple_sql_specific_drivers_fails(self): + self.config_fixture.config( + group='identity', + driver='keystone.identity.backends.ldap.Identity') + self.config_fixture.config( + group='assignment', + driver='keystone.assignment.backends.sql.Assignment') + self.load_backends() + # Ensure default, domain1 and domain2 exist + self.domain_count = 3 + self.setup_initial_domains() + # Make any identity call to initiate the lazy loading of configs + self.identity_api.list_users( + domain_scope=CONF.identity.default_domain_id) + # This will only load domain1, since the domain2 config file is + # not stored in the same location + self.assertIsNotNone(self.get_config(self.domains['domain1']['id'])) + + # Now try and manually load a 2nd sql specific driver, for domain2, + # which should fail. + self.assertRaises( + exception.MultipleSQLDriversInConfig, + self.identity_api.domain_configs._load_config_from_file, + self.resource_api, + [tests.TESTCONF + '/domain_configs_one_extra_sql/' + + 'keystone.domain2.conf'], + 'domain2') + + +class LdapFilterTests(test_backend.FilterTests, tests.TestCase): + + def setUp(self): + super(LdapFilterTests, self).setUp() + self.useFixture(database.Database()) + self.clear_database() + + common_ldap.register_handler('fake://', fakeldap.FakeLdap) + self.load_backends() + self.load_fixtures(default_fixtures) + + self.engine = sql.get_engine() + self.addCleanup(sql.cleanup) + sql.ModelBase.metadata.create_all(bind=self.engine) + + self.addCleanup(sql.ModelBase.metadata.drop_all, bind=self.engine) + self.addCleanup(common_ldap_core._HANDLERS.clear) + + def config_overrides(self): + super(LdapFilterTests, self).config_overrides() + self.config_fixture.config( + group='identity', + driver='keystone.identity.backends.ldap.Identity') + + def config_files(self): + config_files = super(LdapFilterTests, self).config_files() + config_files.append(tests.dirs.tests_conf('backend_ldap.conf')) + return config_files + + def clear_database(self): + for shelf in fakeldap.FakeShelves: + fakeldap.FakeShelves[shelf].clear() diff --git a/keystone-moon/keystone/tests/unit/test_backend_ldap_pool.py b/keystone-moon/keystone/tests/unit/test_backend_ldap_pool.py new file mode 100644 index 00000000..eee03b8b --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_backend_ldap_pool.py @@ -0,0 +1,244 @@ +# -*- coding: utf-8 -*- +# Copyright 2012 OpenStack Foundation +# Copyright 2013 IBM Corp. +# +# 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 ldappool +import mock +from oslo_config import cfg +from oslotest import mockpatch + +from keystone.common.ldap import core as ldap_core +from keystone.identity.backends import ldap +from keystone.tests import unit as tests +from keystone.tests.unit import fakeldap +from keystone.tests.unit import test_backend_ldap + +CONF = cfg.CONF + + +class LdapPoolCommonTestMixin(object): + """LDAP pool specific common tests used here and in live tests.""" + + def cleanup_pools(self): + ldap_core.PooledLDAPHandler.connection_pools.clear() + + def test_handler_with_use_pool_enabled(self): + # by default use_pool and use_auth_pool is enabled in test pool config + user_ref = self.identity_api.get_user(self.user_foo['id']) + self.user_foo.pop('password') + self.assertDictEqual(user_ref, self.user_foo) + + handler = ldap_core._get_connection(CONF.ldap.url, use_pool=True) + self.assertIsInstance(handler, ldap_core.PooledLDAPHandler) + + @mock.patch.object(ldap_core.KeystoneLDAPHandler, 'connect') + @mock.patch.object(ldap_core.KeystoneLDAPHandler, 'simple_bind_s') + def test_handler_with_use_pool_not_enabled(self, bind_method, + connect_method): + self.config_fixture.config(group='ldap', use_pool=False) + self.config_fixture.config(group='ldap', use_auth_pool=True) + self.cleanup_pools() + + user_api = ldap.UserApi(CONF) + handler = user_api.get_connection(user=None, password=None, + end_user_auth=True) + # use_auth_pool flag does not matter when use_pool is False + # still handler is non pool version + self.assertIsInstance(handler.conn, ldap_core.PythonLDAPHandler) + + @mock.patch.object(ldap_core.KeystoneLDAPHandler, 'connect') + @mock.patch.object(ldap_core.KeystoneLDAPHandler, 'simple_bind_s') + def test_handler_with_end_user_auth_use_pool_not_enabled(self, bind_method, + connect_method): + # by default use_pool is enabled in test pool config + # now disabling use_auth_pool flag to test handler instance + self.config_fixture.config(group='ldap', use_auth_pool=False) + self.cleanup_pools() + + user_api = ldap.UserApi(CONF) + handler = user_api.get_connection(user=None, password=None, + end_user_auth=True) + self.assertIsInstance(handler.conn, ldap_core.PythonLDAPHandler) + + # For end_user_auth case, flag should not be false otherwise + # it will use, admin connections ldap pool + handler = user_api.get_connection(user=None, password=None, + end_user_auth=False) + self.assertIsInstance(handler.conn, ldap_core.PooledLDAPHandler) + + def test_pool_size_set(self): + # get related connection manager instance + ldappool_cm = self.conn_pools[CONF.ldap.url] + self.assertEqual(CONF.ldap.pool_size, ldappool_cm.size) + + def test_pool_retry_max_set(self): + # get related connection manager instance + ldappool_cm = self.conn_pools[CONF.ldap.url] + self.assertEqual(CONF.ldap.pool_retry_max, ldappool_cm.retry_max) + + def test_pool_retry_delay_set(self): + # just make one identity call to initiate ldap connection if not there + self.identity_api.get_user(self.user_foo['id']) + + # get related connection manager instance + ldappool_cm = self.conn_pools[CONF.ldap.url] + self.assertEqual(CONF.ldap.pool_retry_delay, ldappool_cm.retry_delay) + + def test_pool_use_tls_set(self): + # get related connection manager instance + ldappool_cm = self.conn_pools[CONF.ldap.url] + self.assertEqual(CONF.ldap.use_tls, ldappool_cm.use_tls) + + def test_pool_timeout_set(self): + # get related connection manager instance + ldappool_cm = self.conn_pools[CONF.ldap.url] + self.assertEqual(CONF.ldap.pool_connection_timeout, + ldappool_cm.timeout) + + def test_pool_use_pool_set(self): + # get related connection manager instance + ldappool_cm = self.conn_pools[CONF.ldap.url] + self.assertEqual(CONF.ldap.use_pool, ldappool_cm.use_pool) + + def test_pool_connection_lifetime_set(self): + # get related connection manager instance + ldappool_cm = self.conn_pools[CONF.ldap.url] + self.assertEqual(CONF.ldap.pool_connection_lifetime, + ldappool_cm.max_lifetime) + + def test_max_connection_error_raised(self): + + who = CONF.ldap.user + cred = CONF.ldap.password + # get related connection manager instance + ldappool_cm = self.conn_pools[CONF.ldap.url] + ldappool_cm.size = 2 + + # 3rd connection attempt should raise Max connection error + with ldappool_cm.connection(who, cred) as _: # conn1 + with ldappool_cm.connection(who, cred) as _: # conn2 + try: + with ldappool_cm.connection(who, cred) as _: # conn3 + _.unbind_s() + self.fail() + except Exception as ex: + self.assertIsInstance(ex, + ldappool.MaxConnectionReachedError) + ldappool_cm.size = CONF.ldap.pool_size + + def test_pool_size_expands_correctly(self): + + who = CONF.ldap.user + cred = CONF.ldap.password + # get related connection manager instance + ldappool_cm = self.conn_pools[CONF.ldap.url] + ldappool_cm.size = 3 + + def _get_conn(): + return ldappool_cm.connection(who, cred) + + # Open 3 connections first + with _get_conn() as _: # conn1 + self.assertEqual(len(ldappool_cm), 1) + with _get_conn() as _: # conn2 + self.assertEqual(len(ldappool_cm), 2) + with _get_conn() as _: # conn2 + _.unbind_ext_s() + self.assertEqual(len(ldappool_cm), 3) + + # Then open 3 connections again and make sure size does not grow + # over 3 + with _get_conn() as _: # conn1 + self.assertEqual(len(ldappool_cm), 1) + with _get_conn() as _: # conn2 + self.assertEqual(len(ldappool_cm), 2) + with _get_conn() as _: # conn3 + _.unbind_ext_s() + self.assertEqual(len(ldappool_cm), 3) + + def test_password_change_with_pool(self): + old_password = self.user_sna['password'] + self.cleanup_pools() + + # authenticate so that connection is added to pool before password + # change + user_ref = self.identity_api.authenticate( + context={}, + user_id=self.user_sna['id'], + password=self.user_sna['password']) + + self.user_sna.pop('password') + self.user_sna['enabled'] = True + self.assertDictEqual(user_ref, self.user_sna) + + new_password = 'new_password' + user_ref['password'] = new_password + self.identity_api.update_user(user_ref['id'], user_ref) + + # now authenticate again to make sure new password works with + # conneciton pool + user_ref2 = self.identity_api.authenticate( + context={}, + user_id=self.user_sna['id'], + password=new_password) + + user_ref.pop('password') + self.assertDictEqual(user_ref, user_ref2) + + # Authentication with old password would not work here as there + # is only one connection in pool which get bind again with updated + # password..so no old bind is maintained in this case. + self.assertRaises(AssertionError, + self.identity_api.authenticate, + context={}, + user_id=self.user_sna['id'], + password=old_password) + + +class LdapIdentitySqlAssignment(LdapPoolCommonTestMixin, + test_backend_ldap.LdapIdentitySqlAssignment, + tests.TestCase): + '''Executes existing base class 150+ tests with pooled LDAP handler to make + sure it works without any error. + ''' + def setUp(self): + self.useFixture(mockpatch.PatchObject( + ldap_core.PooledLDAPHandler, 'Connector', fakeldap.FakeLdapPool)) + super(LdapIdentitySqlAssignment, self).setUp() + + self.addCleanup(self.cleanup_pools) + # storing to local variable to avoid long references + self.conn_pools = ldap_core.PooledLDAPHandler.connection_pools + # super class loads db fixtures which establishes ldap connection + # so adding dummy call to highlight connection pool initialization + # as its not that obvious though its not needed here + self.identity_api.get_user(self.user_foo['id']) + + def config_files(self): + config_files = super(LdapIdentitySqlAssignment, self).config_files() + config_files.append(tests.dirs.tests_conf('backend_ldap_pool.conf')) + return config_files + + @mock.patch.object(ldap_core, 'utf8_encode') + def test_utf8_encoded_is_used_in_pool(self, mocked_method): + def side_effect(arg): + return arg + mocked_method.side_effect = side_effect + # invalidate the cache to get utf8_encode function called. + self.identity_api.get_user.invalidate(self.identity_api, + self.user_foo['id']) + self.identity_api.get_user(self.user_foo['id']) + mocked_method.assert_any_call(CONF.ldap.user) + mocked_method.assert_any_call(CONF.ldap.password) diff --git a/keystone-moon/keystone/tests/unit/test_backend_rules.py b/keystone-moon/keystone/tests/unit/test_backend_rules.py new file mode 100644 index 00000000..c9c4f151 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_backend_rules.py @@ -0,0 +1,62 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 keystone import exception +from keystone.tests import unit as tests +from keystone.tests.unit import test_backend + + +class RulesPolicy(tests.TestCase, test_backend.PolicyTests): + def setUp(self): + super(RulesPolicy, self).setUp() + self.load_backends() + + def config_overrides(self): + super(RulesPolicy, self).config_overrides() + self.config_fixture.config( + group='policy', + driver='keystone.policy.backends.rules.Policy') + + def test_create(self): + self.assertRaises(exception.NotImplemented, + super(RulesPolicy, self).test_create) + + def test_get(self): + self.assertRaises(exception.NotImplemented, + super(RulesPolicy, self).test_get) + + def test_list(self): + self.assertRaises(exception.NotImplemented, + super(RulesPolicy, self).test_list) + + def test_update(self): + self.assertRaises(exception.NotImplemented, + super(RulesPolicy, self).test_update) + + def test_delete(self): + self.assertRaises(exception.NotImplemented, + super(RulesPolicy, self).test_delete) + + def test_get_policy_404(self): + self.assertRaises(exception.NotImplemented, + super(RulesPolicy, self).test_get_policy_404) + + def test_update_policy_404(self): + self.assertRaises(exception.NotImplemented, + super(RulesPolicy, self).test_update_policy_404) + + def test_delete_policy_404(self): + self.assertRaises(exception.NotImplemented, + super(RulesPolicy, self).test_delete_policy_404) diff --git a/keystone-moon/keystone/tests/unit/test_backend_sql.py b/keystone-moon/keystone/tests/unit/test_backend_sql.py new file mode 100644 index 00000000..a7c63bf6 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_backend_sql.py @@ -0,0 +1,948 @@ +# -*- coding: utf-8 -*- +# Copyright 2012 OpenStack Foundation +# +# 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 functools +import uuid + +import mock +from oslo_config import cfg +from oslo_db import exception as db_exception +from oslo_db import options +import sqlalchemy +from sqlalchemy import exc +from testtools import matchers + +from keystone.common import driver_hints +from keystone.common import sql +from keystone import exception +from keystone.identity.backends import sql as identity_sql +from keystone.openstack.common import versionutils +from keystone.tests import unit as tests +from keystone.tests.unit import default_fixtures +from keystone.tests.unit.ksfixtures import database +from keystone.tests.unit import test_backend +from keystone.token.persistence.backends import sql as token_sql + + +CONF = cfg.CONF +DEFAULT_DOMAIN_ID = CONF.identity.default_domain_id + + +class SqlTests(tests.SQLDriverOverrides, tests.TestCase): + + def setUp(self): + super(SqlTests, self).setUp() + self.useFixture(database.Database()) + self.load_backends() + + # populate the engine with tables & fixtures + self.load_fixtures(default_fixtures) + # defaulted by the data load + self.user_foo['enabled'] = True + + def config_files(self): + config_files = super(SqlTests, self).config_files() + config_files.append(tests.dirs.tests_conf('backend_sql.conf')) + return config_files + + +class SqlModels(SqlTests): + + def select_table(self, name): + table = sqlalchemy.Table(name, + sql.ModelBase.metadata, + autoload=True) + s = sqlalchemy.select([table]) + return s + + def assertExpectedSchema(self, table, cols): + table = self.select_table(table) + for col, type_, length in cols: + self.assertIsInstance(table.c[col].type, type_) + if length: + self.assertEqual(length, table.c[col].type.length) + + def test_user_model(self): + cols = (('id', sql.String, 64), + ('name', sql.String, 255), + ('password', sql.String, 128), + ('domain_id', sql.String, 64), + ('enabled', sql.Boolean, None), + ('extra', sql.JsonBlob, None)) + self.assertExpectedSchema('user', cols) + + def test_group_model(self): + cols = (('id', sql.String, 64), + ('name', sql.String, 64), + ('description', sql.Text, None), + ('domain_id', sql.String, 64), + ('extra', sql.JsonBlob, None)) + self.assertExpectedSchema('group', cols) + + def test_domain_model(self): + cols = (('id', sql.String, 64), + ('name', sql.String, 64), + ('enabled', sql.Boolean, None)) + self.assertExpectedSchema('domain', cols) + + def test_project_model(self): + cols = (('id', sql.String, 64), + ('name', sql.String, 64), + ('description', sql.Text, None), + ('domain_id', sql.String, 64), + ('enabled', sql.Boolean, None), + ('extra', sql.JsonBlob, None), + ('parent_id', sql.String, 64)) + self.assertExpectedSchema('project', cols) + + def test_role_assignment_model(self): + cols = (('type', sql.Enum, None), + ('actor_id', sql.String, 64), + ('target_id', sql.String, 64), + ('role_id', sql.String, 64), + ('inherited', sql.Boolean, False)) + self.assertExpectedSchema('assignment', cols) + + def test_user_group_membership(self): + cols = (('group_id', sql.String, 64), + ('user_id', sql.String, 64)) + self.assertExpectedSchema('user_group_membership', cols) + + +class SqlIdentity(SqlTests, test_backend.IdentityTests): + def test_password_hashed(self): + session = sql.get_session() + user_ref = self.identity_api._get_user(session, self.user_foo['id']) + self.assertNotEqual(user_ref['password'], self.user_foo['password']) + + def test_delete_user_with_project_association(self): + user = {'name': uuid.uuid4().hex, + 'domain_id': DEFAULT_DOMAIN_ID, + 'password': uuid.uuid4().hex} + user = self.identity_api.create_user(user) + self.assignment_api.add_user_to_project(self.tenant_bar['id'], + user['id']) + self.identity_api.delete_user(user['id']) + self.assertRaises(exception.UserNotFound, + self.assignment_api.list_projects_for_user, + user['id']) + + def test_create_null_user_name(self): + user = {'name': None, + 'domain_id': DEFAULT_DOMAIN_ID, + 'password': uuid.uuid4().hex} + self.assertRaises(exception.ValidationError, + self.identity_api.create_user, + user) + self.assertRaises(exception.UserNotFound, + self.identity_api.get_user_by_name, + user['name'], + DEFAULT_DOMAIN_ID) + + def test_create_user_case_sensitivity(self): + # user name case sensitivity is down to the fact that it is marked as + # an SQL UNIQUE column, which may not be valid for other backends, like + # LDAP. + + # create a ref with a lowercase name + ref = { + 'name': uuid.uuid4().hex.lower(), + 'domain_id': DEFAULT_DOMAIN_ID} + ref = self.identity_api.create_user(ref) + + # assign a new ID with the same name, but this time in uppercase + ref['name'] = ref['name'].upper() + self.identity_api.create_user(ref) + + def test_create_project_case_sensitivity(self): + # project name case sensitivity is down to the fact that it is marked + # as an SQL UNIQUE column, which may not be valid for other backends, + # like LDAP. + + # create a ref with a lowercase name + ref = { + 'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex.lower(), + 'domain_id': DEFAULT_DOMAIN_ID} + self.resource_api.create_project(ref['id'], ref) + + # assign a new ID with the same name, but this time in uppercase + ref['id'] = uuid.uuid4().hex + ref['name'] = ref['name'].upper() + self.resource_api.create_project(ref['id'], ref) + + def test_create_null_project_name(self): + tenant = {'id': uuid.uuid4().hex, + 'name': None, + 'domain_id': DEFAULT_DOMAIN_ID} + self.assertRaises(exception.ValidationError, + self.resource_api.create_project, + tenant['id'], + tenant) + self.assertRaises(exception.ProjectNotFound, + self.resource_api.get_project, + tenant['id']) + self.assertRaises(exception.ProjectNotFound, + self.resource_api.get_project_by_name, + tenant['name'], + DEFAULT_DOMAIN_ID) + + def test_delete_project_with_user_association(self): + user = {'name': 'fakeuser', + 'domain_id': DEFAULT_DOMAIN_ID, + 'password': 'passwd'} + user = self.identity_api.create_user(user) + self.assignment_api.add_user_to_project(self.tenant_bar['id'], + user['id']) + self.resource_api.delete_project(self.tenant_bar['id']) + tenants = self.assignment_api.list_projects_for_user(user['id']) + self.assertEqual([], tenants) + + def test_metadata_removed_on_delete_user(self): + # A test to check that the internal representation + # or roles is correctly updated when a user is deleted + user = {'name': uuid.uuid4().hex, + 'domain_id': DEFAULT_DOMAIN_ID, + 'password': 'passwd'} + user = self.identity_api.create_user(user) + role = {'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex} + self.role_api.create_role(role['id'], role) + self.assignment_api.add_role_to_user_and_project( + user['id'], + self.tenant_bar['id'], + role['id']) + self.identity_api.delete_user(user['id']) + + # Now check whether the internal representation of roles + # has been deleted + self.assertRaises(exception.MetadataNotFound, + self.assignment_api._get_metadata, + user['id'], + self.tenant_bar['id']) + + def test_metadata_removed_on_delete_project(self): + # A test to check that the internal representation + # or roles is correctly updated when a project is deleted + user = {'name': uuid.uuid4().hex, + 'domain_id': DEFAULT_DOMAIN_ID, + 'password': 'passwd'} + user = self.identity_api.create_user(user) + role = {'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex} + self.role_api.create_role(role['id'], role) + self.assignment_api.add_role_to_user_and_project( + user['id'], + self.tenant_bar['id'], + role['id']) + self.resource_api.delete_project(self.tenant_bar['id']) + + # Now check whether the internal representation of roles + # has been deleted + self.assertRaises(exception.MetadataNotFound, + self.assignment_api._get_metadata, + user['id'], + self.tenant_bar['id']) + + def test_update_project_returns_extra(self): + """This tests for backwards-compatibility with an essex/folsom bug. + + Non-indexed attributes were returned in an 'extra' attribute, instead + of on the entity itself; for consistency and backwards compatibility, + those attributes should be included twice. + + This behavior is specific to the SQL driver. + + """ + tenant_id = uuid.uuid4().hex + arbitrary_key = uuid.uuid4().hex + arbitrary_value = uuid.uuid4().hex + tenant = { + 'id': tenant_id, + 'name': uuid.uuid4().hex, + 'domain_id': DEFAULT_DOMAIN_ID, + arbitrary_key: arbitrary_value} + ref = self.resource_api.create_project(tenant_id, tenant) + self.assertEqual(arbitrary_value, ref[arbitrary_key]) + self.assertIsNone(ref.get('extra')) + + tenant['name'] = uuid.uuid4().hex + ref = self.resource_api.update_project(tenant_id, tenant) + self.assertEqual(arbitrary_value, ref[arbitrary_key]) + self.assertEqual(arbitrary_value, ref['extra'][arbitrary_key]) + + def test_update_user_returns_extra(self): + """This tests for backwards-compatibility with an essex/folsom bug. + + Non-indexed attributes were returned in an 'extra' attribute, instead + of on the entity itself; for consistency and backwards compatibility, + those attributes should be included twice. + + This behavior is specific to the SQL driver. + + """ + arbitrary_key = uuid.uuid4().hex + arbitrary_value = uuid.uuid4().hex + user = { + 'name': uuid.uuid4().hex, + 'domain_id': DEFAULT_DOMAIN_ID, + 'password': uuid.uuid4().hex, + arbitrary_key: arbitrary_value} + ref = self.identity_api.create_user(user) + self.assertEqual(arbitrary_value, ref[arbitrary_key]) + self.assertIsNone(ref.get('password')) + self.assertIsNone(ref.get('extra')) + + user['name'] = uuid.uuid4().hex + user['password'] = uuid.uuid4().hex + ref = self.identity_api.update_user(ref['id'], user) + self.assertIsNone(ref.get('password')) + self.assertIsNone(ref['extra'].get('password')) + self.assertEqual(arbitrary_value, ref[arbitrary_key]) + self.assertEqual(arbitrary_value, ref['extra'][arbitrary_key]) + + def test_sql_user_to_dict_null_default_project_id(self): + user = { + 'name': uuid.uuid4().hex, + 'domain_id': DEFAULT_DOMAIN_ID, + 'password': uuid.uuid4().hex} + + user = self.identity_api.create_user(user) + session = sql.get_session() + query = session.query(identity_sql.User) + query = query.filter_by(id=user['id']) + raw_user_ref = query.one() + self.assertIsNone(raw_user_ref.default_project_id) + user_ref = raw_user_ref.to_dict() + self.assertNotIn('default_project_id', user_ref) + session.close() + + def test_list_domains_for_user(self): + domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(domain['id'], domain) + user = {'name': uuid.uuid4().hex, 'password': uuid.uuid4().hex, + 'domain_id': domain['id'], 'enabled': True} + + test_domain1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(test_domain1['id'], test_domain1) + test_domain2 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(test_domain2['id'], test_domain2) + + user = self.identity_api.create_user(user) + user_domains = self.assignment_api.list_domains_for_user(user['id']) + self.assertEqual(0, len(user_domains)) + self.assignment_api.create_grant(user_id=user['id'], + domain_id=test_domain1['id'], + role_id=self.role_member['id']) + self.assignment_api.create_grant(user_id=user['id'], + domain_id=test_domain2['id'], + role_id=self.role_member['id']) + user_domains = self.assignment_api.list_domains_for_user(user['id']) + self.assertThat(user_domains, matchers.HasLength(2)) + + def test_list_domains_for_user_with_grants(self): + # Create two groups each with a role on a different domain, and + # make user1 a member of both groups. Both these new domains + # should now be included, along with any direct user grants. + domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(domain['id'], domain) + user = {'name': uuid.uuid4().hex, 'password': uuid.uuid4().hex, + 'domain_id': domain['id'], 'enabled': True} + user = self.identity_api.create_user(user) + group1 = {'name': uuid.uuid4().hex, 'domain_id': domain['id']} + group1 = self.identity_api.create_group(group1) + group2 = {'name': uuid.uuid4().hex, 'domain_id': domain['id']} + group2 = self.identity_api.create_group(group2) + + test_domain1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(test_domain1['id'], test_domain1) + test_domain2 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(test_domain2['id'], test_domain2) + test_domain3 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(test_domain3['id'], test_domain3) + + self.identity_api.add_user_to_group(user['id'], group1['id']) + self.identity_api.add_user_to_group(user['id'], group2['id']) + + # Create 3 grants, one user grant, the other two as group grants + self.assignment_api.create_grant(user_id=user['id'], + domain_id=test_domain1['id'], + role_id=self.role_member['id']) + self.assignment_api.create_grant(group_id=group1['id'], + domain_id=test_domain2['id'], + role_id=self.role_admin['id']) + self.assignment_api.create_grant(group_id=group2['id'], + domain_id=test_domain3['id'], + role_id=self.role_admin['id']) + user_domains = self.assignment_api.list_domains_for_user(user['id']) + self.assertThat(user_domains, matchers.HasLength(3)) + + def test_list_domains_for_user_with_inherited_grants(self): + """Test that inherited roles on the domain are excluded. + + Test Plan: + + - Create two domains, one user, group and role + - Domain1 is given an inherited user role, Domain2 an inherited + group role (for a group of which the user is a member) + - When listing domains for user, neither domain should be returned + + """ + domain1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + domain1 = self.resource_api.create_domain(domain1['id'], domain1) + domain2 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + domain2 = self.resource_api.create_domain(domain2['id'], domain2) + user = {'name': uuid.uuid4().hex, 'password': uuid.uuid4().hex, + 'domain_id': domain1['id'], 'enabled': True} + user = self.identity_api.create_user(user) + group = {'name': uuid.uuid4().hex, 'domain_id': domain1['id']} + group = self.identity_api.create_group(group) + self.identity_api.add_user_to_group(user['id'], group['id']) + role = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.role_api.create_role(role['id'], role) + + # Create a grant on each domain, one user grant, one group grant, + # both inherited. + self.assignment_api.create_grant(user_id=user['id'], + domain_id=domain1['id'], + role_id=role['id'], + inherited_to_projects=True) + self.assignment_api.create_grant(group_id=group['id'], + domain_id=domain2['id'], + role_id=role['id'], + inherited_to_projects=True) + + user_domains = self.assignment_api.list_domains_for_user(user['id']) + # No domains should be returned since both domains have only inherited + # roles assignments. + self.assertThat(user_domains, matchers.HasLength(0)) + + +class SqlTrust(SqlTests, test_backend.TrustTests): + pass + + +class SqlToken(SqlTests, test_backend.TokenTests): + def test_token_revocation_list_uses_right_columns(self): + # This query used to be heavy with too many columns. We want + # to make sure it is only running with the minimum columns + # necessary. + + expected_query_args = (token_sql.TokenModel.id, + token_sql.TokenModel.expires) + + with mock.patch.object(token_sql, 'sql') as mock_sql: + tok = token_sql.Token() + tok.list_revoked_tokens() + + mock_query = mock_sql.get_session().query + mock_query.assert_called_with(*expected_query_args) + + def test_flush_expired_tokens_batch(self): + # TODO(dstanek): This test should be rewritten to be less + # brittle. The code will likely need to be changed first. I + # just copied the spirit of the existing test when I rewrote + # mox -> mock. These tests are brittle because they have the + # call structure for SQLAlchemy encoded in them. + + # test sqlite dialect + with mock.patch.object(token_sql, 'sql') as mock_sql: + mock_sql.get_session().bind.dialect.name = 'sqlite' + tok = token_sql.Token() + tok.flush_expired_tokens() + + filter_mock = mock_sql.get_session().query().filter() + self.assertFalse(filter_mock.limit.called) + self.assertTrue(filter_mock.delete.called_once) + + def test_flush_expired_tokens_batch_mysql(self): + # test mysql dialect, we don't need to test IBM DB SA separately, since + # other tests below test the differences between how they use the batch + # strategy + with mock.patch.object(token_sql, 'sql') as mock_sql: + mock_sql.get_session().query().filter().delete.return_value = 0 + mock_sql.get_session().bind.dialect.name = 'mysql' + tok = token_sql.Token() + expiry_mock = mock.Mock() + ITERS = [1, 2, 3] + expiry_mock.return_value = iter(ITERS) + token_sql._expiry_range_batched = expiry_mock + tok.flush_expired_tokens() + + # The expiry strategy is only invoked once, the other calls are via + # the yield return. + self.assertEqual(1, expiry_mock.call_count) + mock_delete = mock_sql.get_session().query().filter().delete + self.assertThat(mock_delete.call_args_list, + matchers.HasLength(len(ITERS))) + + def test_expiry_range_batched(self): + upper_bound_mock = mock.Mock(side_effect=[1, "final value"]) + sess_mock = mock.Mock() + query_mock = sess_mock.query().filter().order_by().offset().limit() + query_mock.one.side_effect = [['test'], sql.NotFound()] + for i, x in enumerate(token_sql._expiry_range_batched(sess_mock, + upper_bound_mock, + batch_size=50)): + if i == 0: + # The first time the batch iterator returns, it should return + # the first result that comes back from the database. + self.assertEqual(x, 'test') + elif i == 1: + # The second time, the database range function should return + # nothing, so the batch iterator returns the result of the + # upper_bound function + self.assertEqual(x, "final value") + else: + self.fail("range batch function returned more than twice") + + def test_expiry_range_strategy_sqlite(self): + tok = token_sql.Token() + sqlite_strategy = tok._expiry_range_strategy('sqlite') + self.assertEqual(token_sql._expiry_range_all, sqlite_strategy) + + def test_expiry_range_strategy_ibm_db_sa(self): + tok = token_sql.Token() + db2_strategy = tok._expiry_range_strategy('ibm_db_sa') + self.assertIsInstance(db2_strategy, functools.partial) + self.assertEqual(db2_strategy.func, token_sql._expiry_range_batched) + self.assertEqual(db2_strategy.keywords, {'batch_size': 100}) + + def test_expiry_range_strategy_mysql(self): + tok = token_sql.Token() + mysql_strategy = tok._expiry_range_strategy('mysql') + self.assertIsInstance(mysql_strategy, functools.partial) + self.assertEqual(mysql_strategy.func, token_sql._expiry_range_batched) + self.assertEqual(mysql_strategy.keywords, {'batch_size': 1000}) + + +class SqlCatalog(SqlTests, test_backend.CatalogTests): + + _legacy_endpoint_id_in_endpoint = True + _enabled_default_to_true_when_creating_endpoint = True + + def test_catalog_ignored_malformed_urls(self): + service = { + 'id': uuid.uuid4().hex, + 'type': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'description': uuid.uuid4().hex, + } + self.catalog_api.create_service(service['id'], service.copy()) + + malformed_url = "http://192.168.1.104:8774/v2/$(tenant)s" + endpoint = { + 'id': uuid.uuid4().hex, + 'region_id': None, + 'service_id': service['id'], + 'interface': 'public', + 'url': malformed_url, + } + self.catalog_api.create_endpoint(endpoint['id'], endpoint.copy()) + + # NOTE(dstanek): there are no valid URLs, so nothing is in the catalog + catalog = self.catalog_api.get_catalog('fake-user', 'fake-tenant') + self.assertEqual({}, catalog) + + def test_get_catalog_with_empty_public_url(self): + service = { + 'id': uuid.uuid4().hex, + 'type': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'description': uuid.uuid4().hex, + } + self.catalog_api.create_service(service['id'], service.copy()) + + endpoint = { + 'id': uuid.uuid4().hex, + 'region_id': None, + 'interface': 'public', + 'url': '', + 'service_id': service['id'], + } + self.catalog_api.create_endpoint(endpoint['id'], endpoint.copy()) + + catalog = self.catalog_api.get_catalog('user', 'tenant') + catalog_endpoint = catalog[endpoint['region_id']][service['type']] + self.assertEqual(service['name'], catalog_endpoint['name']) + self.assertEqual(endpoint['id'], catalog_endpoint['id']) + self.assertEqual('', catalog_endpoint['publicURL']) + self.assertIsNone(catalog_endpoint.get('adminURL')) + self.assertIsNone(catalog_endpoint.get('internalURL')) + + def test_create_endpoint_region_404(self): + service = { + 'id': uuid.uuid4().hex, + 'type': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'description': uuid.uuid4().hex, + } + self.catalog_api.create_service(service['id'], service.copy()) + + endpoint = { + 'id': uuid.uuid4().hex, + 'region_id': uuid.uuid4().hex, + 'service_id': service['id'], + 'interface': 'public', + 'url': uuid.uuid4().hex, + } + + self.assertRaises(exception.ValidationError, + self.catalog_api.create_endpoint, + endpoint['id'], + endpoint.copy()) + + def test_create_region_invalid_id(self): + region = { + 'id': '0' * 256, + 'description': '', + 'extra': {}, + } + + self.assertRaises(exception.StringLengthExceeded, + self.catalog_api.create_region, + region.copy()) + + def test_create_region_invalid_parent_id(self): + region = { + 'id': uuid.uuid4().hex, + 'parent_region_id': '0' * 256, + } + + self.assertRaises(exception.RegionNotFound, + self.catalog_api.create_region, + region) + + def test_delete_region_with_endpoint(self): + # create a region + region = { + 'id': uuid.uuid4().hex, + 'description': uuid.uuid4().hex, + } + self.catalog_api.create_region(region) + + # create a child region + child_region = { + 'id': uuid.uuid4().hex, + 'description': uuid.uuid4().hex, + 'parent_id': region['id'] + } + self.catalog_api.create_region(child_region) + # create a service + service = { + 'id': uuid.uuid4().hex, + 'type': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'description': uuid.uuid4().hex, + } + self.catalog_api.create_service(service['id'], service) + + # create an endpoint attached to the service and child region + child_endpoint = { + 'id': uuid.uuid4().hex, + 'region_id': child_region['id'], + 'interface': uuid.uuid4().hex[:8], + 'url': uuid.uuid4().hex, + 'service_id': service['id'], + } + self.catalog_api.create_endpoint(child_endpoint['id'], child_endpoint) + self.assertRaises(exception.RegionDeletionError, + self.catalog_api.delete_region, + child_region['id']) + + # create an endpoint attached to the service and parent region + endpoint = { + 'id': uuid.uuid4().hex, + 'region_id': region['id'], + 'interface': uuid.uuid4().hex[:8], + 'url': uuid.uuid4().hex, + 'service_id': service['id'], + } + self.catalog_api.create_endpoint(endpoint['id'], endpoint) + self.assertRaises(exception.RegionDeletionError, + self.catalog_api.delete_region, + region['id']) + + +class SqlPolicy(SqlTests, test_backend.PolicyTests): + pass + + +class SqlInheritance(SqlTests, test_backend.InheritanceTests): + pass + + +class SqlTokenCacheInvalidation(SqlTests, test_backend.TokenCacheInvalidation): + def setUp(self): + super(SqlTokenCacheInvalidation, self).setUp() + self._create_test_data() + + +class SqlFilterTests(SqlTests, test_backend.FilterTests): + + def clean_up_entities(self): + """Clean up entity test data from Filter Test Cases.""" + + for entity in ['user', 'group', 'project']: + self._delete_test_data(entity, self.entity_list[entity]) + self._delete_test_data(entity, self.domain1_entity_list[entity]) + del self.entity_list + del self.domain1_entity_list + self.domain1['enabled'] = False + self.resource_api.update_domain(self.domain1['id'], self.domain1) + self.resource_api.delete_domain(self.domain1['id']) + del self.domain1 + + def test_list_entities_filtered_by_domain(self): + # NOTE(henry-nash): This method is here rather than in test_backend + # since any domain filtering with LDAP is handled by the manager + # layer (and is already tested elsewhere) not at the driver level. + self.addCleanup(self.clean_up_entities) + self.domain1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(self.domain1['id'], self.domain1) + + self.entity_list = {} + self.domain1_entity_list = {} + for entity in ['user', 'group', 'project']: + # Create 5 entities, 3 of which are in domain1 + DOMAIN1_ENTITIES = 3 + self.entity_list[entity] = self._create_test_data(entity, 2) + self.domain1_entity_list[entity] = self._create_test_data( + entity, DOMAIN1_ENTITIES, self.domain1['id']) + + # Should get back the DOMAIN1_ENTITIES in domain1 + hints = driver_hints.Hints() + hints.add_filter('domain_id', self.domain1['id']) + entities = self._list_entities(entity)(hints=hints) + self.assertEqual(DOMAIN1_ENTITIES, len(entities)) + self._match_with_list(entities, self.domain1_entity_list[entity]) + # Check the driver has removed the filter from the list hints + self.assertFalse(hints.get_exact_filter_by_name('domain_id')) + + def test_filter_sql_injection_attack(self): + """Test against sql injection attack on filters + + Test Plan: + - Attempt to get all entities back by passing a two-term attribute + - Attempt to piggyback filter to damage DB (e.g. drop table) + + """ + # Check we have some users + users = self.identity_api.list_users() + self.assertTrue(len(users) > 0) + + hints = driver_hints.Hints() + hints.add_filter('name', "anything' or 'x'='x") + users = self.identity_api.list_users(hints=hints) + self.assertEqual(0, len(users)) + + # See if we can add a SQL command...use the group table instead of the + # user table since 'user' is reserved word for SQLAlchemy. + group = {'name': uuid.uuid4().hex, 'domain_id': DEFAULT_DOMAIN_ID} + group = self.identity_api.create_group(group) + + hints = driver_hints.Hints() + hints.add_filter('name', "x'; drop table group") + groups = self.identity_api.list_groups(hints=hints) + self.assertEqual(0, len(groups)) + + groups = self.identity_api.list_groups() + self.assertTrue(len(groups) > 0) + + def test_groups_for_user_filtered(self): + # The SQL identity driver currently does not support filtering on the + # listing groups for a given user, so will fail this test. This is + # raised as bug #1412447. + try: + super(SqlFilterTests, self).test_groups_for_user_filtered() + except matchers.MismatchError: + return + # We shouldn't get here...if we do, it means someone has fixed the + # above defect, so we can remove this test override. As an aside, it + # would be nice to have used self.assertRaises() around the call above + # to achieve the logic here...but that does not seem to work when + # wrapping another assert (it won't seem to catch the error). + self.assertTrue(False) + + +class SqlLimitTests(SqlTests, test_backend.LimitTests): + def setUp(self): + super(SqlLimitTests, self).setUp() + test_backend.LimitTests.setUp(self) + + +class FakeTable(sql.ModelBase): + __tablename__ = 'test_table' + col = sql.Column(sql.String(32), primary_key=True) + + @sql.handle_conflicts('keystone') + def insert(self): + raise db_exception.DBDuplicateEntry + + @sql.handle_conflicts('keystone') + def update(self): + raise db_exception.DBError( + inner_exception=exc.IntegrityError('a', 'a', 'a')) + + @sql.handle_conflicts('keystone') + def lookup(self): + raise KeyError + + +class SqlDecorators(tests.TestCase): + + def test_initialization_fail(self): + self.assertRaises(exception.StringLengthExceeded, + FakeTable, col='a' * 64) + + def test_initialization(self): + tt = FakeTable(col='a') + self.assertEqual('a', tt.col) + + def test_non_ascii_init(self): + # NOTE(I159): Non ASCII characters must cause UnicodeDecodeError + # if encoding is not provided explicitly. + self.assertRaises(UnicodeDecodeError, FakeTable, col='Я') + + def test_conflict_happend(self): + self.assertRaises(exception.Conflict, FakeTable().insert) + self.assertRaises(exception.UnexpectedError, FakeTable().update) + + def test_not_conflict_error(self): + self.assertRaises(KeyError, FakeTable().lookup) + + +class SqlModuleInitialization(tests.TestCase): + + @mock.patch.object(sql.core, 'CONF') + @mock.patch.object(options, 'set_defaults') + def test_initialize_module(self, set_defaults, CONF): + sql.initialize() + set_defaults.assert_called_with(CONF, + connection='sqlite:///keystone.db') + + +class SqlCredential(SqlTests): + + def _create_credential_with_user_id(self, user_id=uuid.uuid4().hex): + credential_id = uuid.uuid4().hex + new_credential = { + 'id': credential_id, + 'user_id': user_id, + 'project_id': uuid.uuid4().hex, + 'blob': uuid.uuid4().hex, + 'type': uuid.uuid4().hex, + 'extra': uuid.uuid4().hex + } + self.credential_api.create_credential(credential_id, new_credential) + return new_credential + + def _validateCredentialList(self, retrieved_credentials, + expected_credentials): + self.assertEqual(len(retrieved_credentials), len(expected_credentials)) + retrived_ids = [c['id'] for c in retrieved_credentials] + for cred in expected_credentials: + self.assertIn(cred['id'], retrived_ids) + + def setUp(self): + super(SqlCredential, self).setUp() + self.credentials = [] + for _ in range(3): + self.credentials.append( + self._create_credential_with_user_id()) + self.user_credentials = [] + for _ in range(3): + cred = self._create_credential_with_user_id(self.user_foo['id']) + self.user_credentials.append(cred) + self.credentials.append(cred) + + def test_list_credentials(self): + credentials = self.credential_api.list_credentials() + self._validateCredentialList(credentials, self.credentials) + # test filtering using hints + hints = driver_hints.Hints() + hints.add_filter('user_id', self.user_foo['id']) + credentials = self.credential_api.list_credentials(hints) + self._validateCredentialList(credentials, self.user_credentials) + + def test_list_credentials_for_user(self): + credentials = self.credential_api.list_credentials_for_user( + self.user_foo['id']) + self._validateCredentialList(credentials, self.user_credentials) + + +class DeprecatedDecorators(SqlTests): + + def test_assignment_to_role_api(self): + """Test that calling one of the methods does call LOG.deprecated. + + This method is really generic to the type of backend, but we need + one to execute the test, so the SQL backend is as good as any. + + """ + + # Rather than try and check that a log message is issued, we + # enable fatal_deprecations so that we can check for the + # raising of the exception. + + # First try to create a role without enabling fatal deprecations, + # which should work due to the cross manager deprecated calls. + role_ref = { + 'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex} + self.assignment_api.create_role(role_ref['id'], role_ref) + self.role_api.get_role(role_ref['id']) + + # Now enable fatal exceptions - creating a role by calling the + # old manager should now fail. + self.config_fixture.config(fatal_deprecations=True) + role_ref = { + 'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex} + self.assertRaises(versionutils.DeprecatedConfig, + self.assignment_api.create_role, + role_ref['id'], role_ref) + + def test_assignment_to_resource_api(self): + """Test that calling one of the methods does call LOG.deprecated. + + This method is really generic to the type of backend, but we need + one to execute the test, so the SQL backend is as good as any. + + """ + + # Rather than try and check that a log message is issued, we + # enable fatal_deprecations so that we can check for the + # raising of the exception. + + # First try to create a project without enabling fatal deprecations, + # which should work due to the cross manager deprecated calls. + project_ref = { + 'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'domain_id': DEFAULT_DOMAIN_ID} + self.resource_api.create_project(project_ref['id'], project_ref) + self.resource_api.get_project(project_ref['id']) + + # Now enable fatal exceptions - creating a project by calling the + # old manager should now fail. + self.config_fixture.config(fatal_deprecations=True) + project_ref = { + 'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'domain_id': DEFAULT_DOMAIN_ID} + self.assertRaises(versionutils.DeprecatedConfig, + self.assignment_api.create_project, + project_ref['id'], project_ref) diff --git a/keystone-moon/keystone/tests/unit/test_backend_templated.py b/keystone-moon/keystone/tests/unit/test_backend_templated.py new file mode 100644 index 00000000..a1c15fb1 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_backend_templated.py @@ -0,0 +1,127 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 os +import uuid + +from keystone.tests import unit as tests +from keystone.tests.unit import default_fixtures +from keystone.tests.unit.ksfixtures import database +from keystone.tests.unit import test_backend + + +DEFAULT_CATALOG_TEMPLATES = os.path.abspath(os.path.join( + os.path.dirname(__file__), + 'default_catalog.templates')) + + +class TestTemplatedCatalog(tests.TestCase, test_backend.CatalogTests): + + DEFAULT_FIXTURE = { + 'RegionOne': { + 'compute': { + 'adminURL': 'http://localhost:8774/v1.1/bar', + 'publicURL': 'http://localhost:8774/v1.1/bar', + 'internalURL': 'http://localhost:8774/v1.1/bar', + 'name': "'Compute Service'", + 'id': '2' + }, + 'identity': { + 'adminURL': 'http://localhost:35357/v2.0', + 'publicURL': 'http://localhost:5000/v2.0', + 'internalURL': 'http://localhost:35357/v2.0', + 'name': "'Identity Service'", + 'id': '1' + } + } + } + + def setUp(self): + super(TestTemplatedCatalog, self).setUp() + self.useFixture(database.Database()) + self.load_backends() + self.load_fixtures(default_fixtures) + + def config_overrides(self): + super(TestTemplatedCatalog, self).config_overrides() + self.config_fixture.config(group='catalog', + template_file=DEFAULT_CATALOG_TEMPLATES) + + def test_get_catalog(self): + catalog_ref = self.catalog_api.get_catalog('foo', 'bar') + self.assertDictEqual(catalog_ref, self.DEFAULT_FIXTURE) + + def test_catalog_ignored_malformed_urls(self): + # both endpoints are in the catalog + catalog_ref = self.catalog_api.get_catalog('foo', 'bar') + self.assertEqual(2, len(catalog_ref['RegionOne'])) + + region = self.catalog_api.driver.templates['RegionOne'] + region['compute']['adminURL'] = 'http://localhost:8774/v1.1/$(tenant)s' + + # the malformed one has been removed + catalog_ref = self.catalog_api.get_catalog('foo', 'bar') + self.assertEqual(1, len(catalog_ref['RegionOne'])) + + def test_get_catalog_endpoint_disabled(self): + self.skipTest("Templated backend doesn't have disabled endpoints") + + def test_get_v3_catalog_endpoint_disabled(self): + self.skipTest("Templated backend doesn't have disabled endpoints") + + def assert_catalogs_equal(self, expected, observed): + for e, o in zip(sorted(expected), sorted(observed)): + expected_endpoints = e.pop('endpoints') + observed_endpoints = o.pop('endpoints') + self.assertDictEqual(e, o) + self.assertItemsEqual(expected_endpoints, observed_endpoints) + + def test_get_v3_catalog(self): + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + catalog_ref = self.catalog_api.get_v3_catalog(user_id, project_id) + exp_catalog = [ + {'endpoints': [ + {'interface': 'admin', + 'region': 'RegionOne', + 'url': 'http://localhost:8774/v1.1/%s' % project_id}, + {'interface': 'public', + 'region': 'RegionOne', + 'url': 'http://localhost:8774/v1.1/%s' % project_id}, + {'interface': 'internal', + 'region': 'RegionOne', + 'url': 'http://localhost:8774/v1.1/%s' % project_id}], + 'type': 'compute', + 'name': "'Compute Service'", + 'id': '2'}, + {'endpoints': [ + {'interface': 'admin', + 'region': 'RegionOne', + 'url': 'http://localhost:35357/v2.0'}, + {'interface': 'public', + 'region': 'RegionOne', + 'url': 'http://localhost:5000/v2.0'}, + {'interface': 'internal', + 'region': 'RegionOne', + 'url': 'http://localhost:35357/v2.0'}], + 'type': 'identity', + 'name': "'Identity Service'", + 'id': '1'}] + self.assert_catalogs_equal(exp_catalog, catalog_ref) + + def test_list_regions_filtered_by_parent_region_id(self): + self.skipTest('Templated backend does not support hints') + + def test_service_filtering(self): + self.skipTest("Templated backend doesn't support filtering") diff --git a/keystone-moon/keystone/tests/unit/test_cache.py b/keystone-moon/keystone/tests/unit/test_cache.py new file mode 100644 index 00000000..5a778a07 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_cache.py @@ -0,0 +1,322 @@ +# Copyright 2013 Metacloud +# +# 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 +import time +import uuid + +from dogpile.cache import api +from dogpile.cache import proxy +import mock +from oslo_config import cfg + +from keystone.common import cache +from keystone import exception +from keystone.tests import unit as tests + + +CONF = cfg.CONF +NO_VALUE = api.NO_VALUE + + +def _copy_value(value): + if value is not NO_VALUE: + value = copy.deepcopy(value) + return value + + +# NOTE(morganfainberg): WARNING - It is not recommended to use the Memory +# backend for dogpile.cache in a real deployment under any circumstances. The +# backend does no cleanup of expired values and therefore will leak memory. The +# backend is not implemented in a way to share data across processes (e.g. +# Keystone in HTTPD. This proxy is a hack to get around the lack of isolation +# of values in memory. Currently it blindly stores and retrieves the values +# from the cache, and modifications to dicts/lists/etc returned can result in +# changes to the cached values. In short, do not use the dogpile.cache.memory +# backend unless you are running tests or expecting odd/strange results. +class CacheIsolatingProxy(proxy.ProxyBackend): + """Proxy that forces a memory copy of stored values. + The default in-memory cache-region does not perform a copy on values it + is meant to cache. Therefore if the value is modified after set or after + get, the cached value also is modified. This proxy does a copy as the last + thing before storing data. + """ + def get(self, key): + return _copy_value(self.proxied.get(key)) + + def set(self, key, value): + self.proxied.set(key, _copy_value(value)) + + +class TestProxy(proxy.ProxyBackend): + def get(self, key): + value = _copy_value(self.proxied.get(key)) + if value is not NO_VALUE: + if isinstance(value[0], TestProxyValue): + value[0].cached = True + return value + + +class TestProxyValue(object): + def __init__(self, value): + self.value = value + self.cached = False + + +class CacheRegionTest(tests.TestCase): + + def setUp(self): + super(CacheRegionTest, self).setUp() + self.region = cache.make_region() + cache.configure_cache_region(self.region) + self.region.wrap(TestProxy) + self.test_value = TestProxyValue('Decorator Test') + + def _add_test_caching_option(self): + self.config_fixture.register_opt( + cfg.BoolOpt('caching', default=True), group='cache') + + def _get_cacheable_function(self): + with mock.patch.object(cache.REGION, 'cache_on_arguments', + self.region.cache_on_arguments): + memoize = cache.get_memoization_decorator(section='cache') + + @memoize + def cacheable_function(value): + return value + + return cacheable_function + + def test_region_built_with_proxy_direct_cache_test(self): + # Verify cache regions are properly built with proxies. + test_value = TestProxyValue('Direct Cache Test') + self.region.set('cache_test', test_value) + cached_value = self.region.get('cache_test') + self.assertTrue(cached_value.cached) + + def test_cache_region_no_error_multiple_config(self): + # Verify configuring the CacheRegion again doesn't error. + cache.configure_cache_region(self.region) + cache.configure_cache_region(self.region) + + def _get_cache_fallthrough_fn(self, cache_time): + with mock.patch.object(cache.REGION, 'cache_on_arguments', + self.region.cache_on_arguments): + memoize = cache.get_memoization_decorator( + section='cache', + expiration_section='assignment') + + class _test_obj(object): + def __init__(self, value): + self.test_value = value + + @memoize + def get_test_value(self): + return self.test_value + + def _do_test(value): + + test_obj = _test_obj(value) + + # Ensure the value has been cached + test_obj.get_test_value() + # Get the now cached value + cached_value = test_obj.get_test_value() + self.assertTrue(cached_value.cached) + self.assertEqual(value.value, cached_value.value) + self.assertEqual(cached_value.value, test_obj.test_value.value) + # Change the underlying value on the test object. + test_obj.test_value = TestProxyValue(uuid.uuid4().hex) + self.assertEqual(cached_value.value, + test_obj.get_test_value().value) + # override the system time to ensure the non-cached new value + # is returned + new_time = time.time() + (cache_time * 2) + with mock.patch.object(time, 'time', + return_value=new_time): + overriden_cache_value = test_obj.get_test_value() + self.assertNotEqual(cached_value.value, + overriden_cache_value.value) + self.assertEqual(test_obj.test_value.value, + overriden_cache_value.value) + + return _do_test + + def test_cache_no_fallthrough_expiration_time_fn(self): + # Since we do not re-configure the cache region, for ease of testing + # this value is set the same as the expiration_time default in the + # [cache] section + cache_time = 600 + expiration_time = cache.get_expiration_time_fn('role') + do_test = self._get_cache_fallthrough_fn(cache_time) + # Run the test with the assignment cache_time value + self.config_fixture.config(cache_time=cache_time, + group='role') + test_value = TestProxyValue(uuid.uuid4().hex) + self.assertEqual(cache_time, expiration_time()) + do_test(value=test_value) + + def test_cache_fallthrough_expiration_time_fn(self): + # Since we do not re-configure the cache region, for ease of testing + # this value is set the same as the expiration_time default in the + # [cache] section + cache_time = 599 + expiration_time = cache.get_expiration_time_fn('role') + do_test = self._get_cache_fallthrough_fn(cache_time) + # Run the test with the assignment cache_time value set to None and + # the global value set. + self.config_fixture.config(cache_time=None, group='role') + test_value = TestProxyValue(uuid.uuid4().hex) + self.assertIsNone(expiration_time()) + do_test(value=test_value) + + def test_should_cache_fn_global_cache_enabled(self): + # Verify should_cache_fn generates a sane function for subsystem and + # functions as expected with caching globally enabled. + cacheable_function = self._get_cacheable_function() + + self.config_fixture.config(group='cache', enabled=True) + cacheable_function(self.test_value) + cached_value = cacheable_function(self.test_value) + self.assertTrue(cached_value.cached) + + def test_should_cache_fn_global_cache_disabled(self): + # Verify should_cache_fn generates a sane function for subsystem and + # functions as expected with caching globally disabled. + cacheable_function = self._get_cacheable_function() + + self.config_fixture.config(group='cache', enabled=False) + cacheable_function(self.test_value) + cached_value = cacheable_function(self.test_value) + self.assertFalse(cached_value.cached) + + def test_should_cache_fn_global_cache_disabled_section_cache_enabled(self): + # Verify should_cache_fn generates a sane function for subsystem and + # functions as expected with caching globally disabled and the specific + # section caching enabled. + cacheable_function = self._get_cacheable_function() + + self._add_test_caching_option() + self.config_fixture.config(group='cache', enabled=False) + self.config_fixture.config(group='cache', caching=True) + + cacheable_function(self.test_value) + cached_value = cacheable_function(self.test_value) + self.assertFalse(cached_value.cached) + + def test_should_cache_fn_global_cache_enabled_section_cache_disabled(self): + # Verify should_cache_fn generates a sane function for subsystem and + # functions as expected with caching globally enabled and the specific + # section caching disabled. + cacheable_function = self._get_cacheable_function() + + self._add_test_caching_option() + self.config_fixture.config(group='cache', enabled=True) + self.config_fixture.config(group='cache', caching=False) + + cacheable_function(self.test_value) + cached_value = cacheable_function(self.test_value) + self.assertFalse(cached_value.cached) + + def test_should_cache_fn_global_cache_enabled_section_cache_enabled(self): + # Verify should_cache_fn generates a sane function for subsystem and + # functions as expected with caching globally enabled and the specific + # section caching enabled. + cacheable_function = self._get_cacheable_function() + + self._add_test_caching_option() + self.config_fixture.config(group='cache', enabled=True) + self.config_fixture.config(group='cache', caching=True) + + cacheable_function(self.test_value) + cached_value = cacheable_function(self.test_value) + self.assertTrue(cached_value.cached) + + def test_cache_dictionary_config_builder(self): + """Validate we build a sane dogpile.cache dictionary config.""" + self.config_fixture.config(group='cache', + config_prefix='test_prefix', + backend='some_test_backend', + expiration_time=86400, + backend_argument=['arg1:test', + 'arg2:test:test', + 'arg3.invalid']) + + config_dict = cache.build_cache_config() + self.assertEqual( + CONF.cache.backend, config_dict['test_prefix.backend']) + self.assertEqual( + CONF.cache.expiration_time, + config_dict['test_prefix.expiration_time']) + self.assertEqual('test', config_dict['test_prefix.arguments.arg1']) + self.assertEqual('test:test', + config_dict['test_prefix.arguments.arg2']) + self.assertNotIn('test_prefix.arguments.arg3', config_dict) + + def test_cache_debug_proxy(self): + single_value = 'Test Value' + single_key = 'testkey' + multi_values = {'key1': 1, 'key2': 2, 'key3': 3} + + self.region.set(single_key, single_value) + self.assertEqual(single_value, self.region.get(single_key)) + + self.region.delete(single_key) + self.assertEqual(NO_VALUE, self.region.get(single_key)) + + self.region.set_multi(multi_values) + cached_values = self.region.get_multi(multi_values.keys()) + for value in multi_values.values(): + self.assertIn(value, cached_values) + self.assertEqual(len(multi_values.values()), len(cached_values)) + + self.region.delete_multi(multi_values.keys()) + for value in self.region.get_multi(multi_values.keys()): + self.assertEqual(NO_VALUE, value) + + def test_configure_non_region_object_raises_error(self): + self.assertRaises(exception.ValidationError, + cache.configure_cache_region, + "bogus") + + +class CacheNoopBackendTest(tests.TestCase): + + def setUp(self): + super(CacheNoopBackendTest, self).setUp() + self.region = cache.make_region() + cache.configure_cache_region(self.region) + + def config_overrides(self): + super(CacheNoopBackendTest, self).config_overrides() + self.config_fixture.config(group='cache', + backend='keystone.common.cache.noop') + + def test_noop_backend(self): + single_value = 'Test Value' + single_key = 'testkey' + multi_values = {'key1': 1, 'key2': 2, 'key3': 3} + + self.region.set(single_key, single_value) + self.assertEqual(NO_VALUE, self.region.get(single_key)) + + self.region.set_multi(multi_values) + cached_values = self.region.get_multi(multi_values.keys()) + self.assertEqual(len(cached_values), len(multi_values.values())) + for value in cached_values: + self.assertEqual(NO_VALUE, value) + + # Delete should not raise exceptions + self.region.delete(single_key) + self.region.delete_multi(multi_values.keys()) diff --git a/keystone-moon/keystone/tests/unit/test_cache_backend_mongo.py b/keystone-moon/keystone/tests/unit/test_cache_backend_mongo.py new file mode 100644 index 00000000..a56bf754 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_cache_backend_mongo.py @@ -0,0 +1,727 @@ +# Copyright 2014 Hewlett-Packard Development Company, L.P. +# +# 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 collections +import copy +import functools +import uuid + +from dogpile.cache import api +from dogpile.cache import region as dp_region +import six + +from keystone.common.cache.backends import mongo +from keystone import exception +from keystone.tests import unit as tests + + +# Mock database structure sample where 'ks_cache' is database and +# 'cache' is collection. Dogpile CachedValue data is divided in two +# fields `value` (CachedValue.payload) and `meta` (CachedValue.metadata) +ks_cache = { + "cache": [ + { + "value": { + "serviceType": "identity", + "allVersionsUrl": "https://dummyUrl", + "dateLastModified": "ISODDate(2014-02-08T18:39:13.237Z)", + "serviceName": "Identity", + "enabled": "True" + }, + "meta": { + "v": 1, + "ct": 1392371422.015121 + }, + "doc_date": "ISODate('2014-02-14T09:50:22.015Z')", + "_id": "8251dc95f63842719c077072f1047ddf" + }, + { + "value": "dummyValueX", + "meta": { + "v": 1, + "ct": 1392371422.014058 + }, + "doc_date": "ISODate('2014-02-14T09:50:22.014Z')", + "_id": "66730b9534d146f0804d23729ad35436" + } + ] +} + + +COLLECTIONS = {} +SON_MANIPULATOR = None + + +class MockCursor(object): + + def __init__(self, collection, dataset_factory): + super(MockCursor, self).__init__() + self.collection = collection + self._factory = dataset_factory + self._dataset = self._factory() + self._limit = None + self._skip = None + + def __iter__(self): + return self + + def __next__(self): + if self._skip: + for _ in range(self._skip): + next(self._dataset) + self._skip = None + if self._limit is not None and self._limit <= 0: + raise StopIteration() + if self._limit is not None: + self._limit -= 1 + return next(self._dataset) + + next = __next__ + + def __getitem__(self, index): + arr = [x for x in self._dataset] + self._dataset = iter(arr) + return arr[index] + + +class MockCollection(object): + + def __init__(self, db, name): + super(MockCollection, self).__init__() + self.name = name + self._collection_database = db + self._documents = {} + self.write_concern = {} + + def __getattr__(self, name): + if name == 'database': + return self._collection_database + + def ensure_index(self, key_or_list, *args, **kwargs): + pass + + def index_information(self): + return {} + + def find_one(self, spec_or_id=None, *args, **kwargs): + if spec_or_id is None: + spec_or_id = {} + if not isinstance(spec_or_id, collections.Mapping): + spec_or_id = {'_id': spec_or_id} + + try: + return next(self.find(spec_or_id, *args, **kwargs)) + except StopIteration: + return None + + def find(self, spec=None, *args, **kwargs): + return MockCursor(self, functools.partial(self._get_dataset, spec)) + + def _get_dataset(self, spec): + dataset = (self._copy_doc(document, dict) for document in + self._iter_documents(spec)) + return dataset + + def _iter_documents(self, spec=None): + return (SON_MANIPULATOR.transform_outgoing(document, self) for + document in six.itervalues(self._documents) + if self._apply_filter(document, spec)) + + def _apply_filter(self, document, query): + for key, search in six.iteritems(query): + doc_val = document.get(key) + if isinstance(search, dict): + op_dict = {'$in': lambda dv, sv: dv in sv} + is_match = all( + op_str in op_dict and op_dict[op_str](doc_val, search_val) + for op_str, search_val in six.iteritems(search) + ) + else: + is_match = doc_val == search + + return is_match + + def _copy_doc(self, obj, container): + if isinstance(obj, list): + new = [] + for item in obj: + new.append(self._copy_doc(item, container)) + return new + if isinstance(obj, dict): + new = container() + for key, value in obj.items(): + new[key] = self._copy_doc(value, container) + return new + else: + return copy.copy(obj) + + def insert(self, data, manipulate=True, **kwargs): + if isinstance(data, list): + return [self._insert(element) for element in data] + return self._insert(data) + + def save(self, data, manipulate=True, **kwargs): + return self._insert(data) + + def _insert(self, data): + if '_id' not in data: + data['_id'] = uuid.uuid4().hex + object_id = data['_id'] + self._documents[object_id] = self._internalize_dict(data) + return object_id + + def find_and_modify(self, spec, document, upsert=False, **kwargs): + self.update(spec, document, upsert, **kwargs) + + def update(self, spec, document, upsert=False, **kwargs): + + existing_docs = [doc for doc in six.itervalues(self._documents) + if self._apply_filter(doc, spec)] + if existing_docs: + existing_doc = existing_docs[0] # should find only 1 match + _id = existing_doc['_id'] + existing_doc.clear() + existing_doc['_id'] = _id + existing_doc.update(self._internalize_dict(document)) + elif upsert: + existing_doc = self._documents[self._insert(document)] + + def _internalize_dict(self, d): + return {k: copy.deepcopy(v) for k, v in six.iteritems(d)} + + def remove(self, spec_or_id=None, search_filter=None): + """Remove objects matching spec_or_id from the collection.""" + if spec_or_id is None: + spec_or_id = search_filter if search_filter else {} + if not isinstance(spec_or_id, dict): + spec_or_id = {'_id': spec_or_id} + to_delete = list(self.find(spec=spec_or_id)) + for doc in to_delete: + doc_id = doc['_id'] + del self._documents[doc_id] + + return { + "connectionId": uuid.uuid4().hex, + "n": len(to_delete), + "ok": 1.0, + "err": None, + } + + +class MockMongoDB(object): + def __init__(self, dbname): + self._dbname = dbname + self.mainpulator = None + + def authenticate(self, username, password): + pass + + def add_son_manipulator(self, manipulator): + global SON_MANIPULATOR + SON_MANIPULATOR = manipulator + + def __getattr__(self, name): + if name == 'authenticate': + return self.authenticate + elif name == 'name': + return self._dbname + elif name == 'add_son_manipulator': + return self.add_son_manipulator + else: + return get_collection(self._dbname, name) + + def __getitem__(self, name): + return get_collection(self._dbname, name) + + +class MockMongoClient(object): + def __init__(self, *args, **kwargs): + pass + + def __getattr__(self, dbname): + return MockMongoDB(dbname) + + +def get_collection(db_name, collection_name): + mongo_collection = MockCollection(MockMongoDB(db_name), collection_name) + return mongo_collection + + +def pymongo_override(): + global pymongo + import pymongo + if pymongo.MongoClient is not MockMongoClient: + pymongo.MongoClient = MockMongoClient + if pymongo.MongoReplicaSetClient is not MockMongoClient: + pymongo.MongoClient = MockMongoClient + + +class MyTransformer(mongo.BaseTransform): + """Added here just to check manipulator logic is used correctly.""" + + def transform_incoming(self, son, collection): + return super(MyTransformer, self).transform_incoming(son, collection) + + def transform_outgoing(self, son, collection): + return super(MyTransformer, self).transform_outgoing(son, collection) + + +class MongoCache(tests.BaseTestCase): + def setUp(self): + super(MongoCache, self).setUp() + global COLLECTIONS + COLLECTIONS = {} + mongo.MongoApi._DB = {} + mongo.MongoApi._MONGO_COLLS = {} + pymongo_override() + # using typical configuration + self.arguments = { + 'db_hosts': 'localhost:27017', + 'db_name': 'ks_cache', + 'cache_collection': 'cache', + 'username': 'test_user', + 'password': 'test_password' + } + + def test_missing_db_hosts(self): + self.arguments.pop('db_hosts') + region = dp_region.make_region() + self.assertRaises(exception.ValidationError, region.configure, + 'keystone.cache.mongo', + arguments=self.arguments) + + def test_missing_db_name(self): + self.arguments.pop('db_name') + region = dp_region.make_region() + self.assertRaises(exception.ValidationError, region.configure, + 'keystone.cache.mongo', + arguments=self.arguments) + + def test_missing_cache_collection_name(self): + self.arguments.pop('cache_collection') + region = dp_region.make_region() + self.assertRaises(exception.ValidationError, region.configure, + 'keystone.cache.mongo', + arguments=self.arguments) + + def test_incorrect_write_concern(self): + self.arguments['w'] = 'one value' + region = dp_region.make_region() + self.assertRaises(exception.ValidationError, region.configure, + 'keystone.cache.mongo', + arguments=self.arguments) + + def test_correct_write_concern(self): + self.arguments['w'] = 1 + region = dp_region.make_region().configure('keystone.cache.mongo', + arguments=self.arguments) + + random_key = uuid.uuid4().hex + region.set(random_key, "dummyValue10") + # There is no proxy so can access MongoCacheBackend directly + self.assertEqual(1, region.backend.api.w) + + def test_incorrect_read_preference(self): + self.arguments['read_preference'] = 'inValidValue' + region = dp_region.make_region().configure('keystone.cache.mongo', + arguments=self.arguments) + # As per delayed loading of pymongo, read_preference value should + # still be string and NOT enum + self.assertEqual('inValidValue', region.backend.api.read_preference) + + random_key = uuid.uuid4().hex + self.assertRaises(ValueError, region.set, + random_key, "dummyValue10") + + def test_correct_read_preference(self): + self.arguments['read_preference'] = 'secondaryPreferred' + region = dp_region.make_region().configure('keystone.cache.mongo', + arguments=self.arguments) + # As per delayed loading of pymongo, read_preference value should + # still be string and NOT enum + self.assertEqual('secondaryPreferred', + region.backend.api.read_preference) + + random_key = uuid.uuid4().hex + region.set(random_key, "dummyValue10") + + # Now as pymongo is loaded so expected read_preference value is enum. + # There is no proxy so can access MongoCacheBackend directly + self.assertEqual(3, region.backend.api.read_preference) + + def test_missing_replica_set_name(self): + self.arguments['use_replica'] = True + region = dp_region.make_region() + self.assertRaises(exception.ValidationError, region.configure, + 'keystone.cache.mongo', + arguments=self.arguments) + + def test_provided_replica_set_name(self): + self.arguments['use_replica'] = True + self.arguments['replicaset_name'] = 'my_replica' + dp_region.make_region().configure('keystone.cache.mongo', + arguments=self.arguments) + self.assertTrue(True) # reached here means no initialization error + + def test_incorrect_mongo_ttl_seconds(self): + self.arguments['mongo_ttl_seconds'] = 'sixty' + region = dp_region.make_region() + self.assertRaises(exception.ValidationError, region.configure, + 'keystone.cache.mongo', + arguments=self.arguments) + + def test_cache_configuration_values_assertion(self): + self.arguments['use_replica'] = True + self.arguments['replicaset_name'] = 'my_replica' + self.arguments['mongo_ttl_seconds'] = 60 + self.arguments['ssl'] = False + region = dp_region.make_region().configure('keystone.cache.mongo', + arguments=self.arguments) + # There is no proxy so can access MongoCacheBackend directly + self.assertEqual('localhost:27017', region.backend.api.hosts) + self.assertEqual('ks_cache', region.backend.api.db_name) + self.assertEqual('cache', region.backend.api.cache_collection) + self.assertEqual('test_user', region.backend.api.username) + self.assertEqual('test_password', region.backend.api.password) + self.assertEqual(True, region.backend.api.use_replica) + self.assertEqual('my_replica', region.backend.api.replicaset_name) + self.assertEqual(False, region.backend.api.conn_kwargs['ssl']) + self.assertEqual(60, region.backend.api.ttl_seconds) + + def test_multiple_region_cache_configuration(self): + arguments1 = copy.copy(self.arguments) + arguments1['cache_collection'] = 'cache_region1' + + region1 = dp_region.make_region().configure('keystone.cache.mongo', + arguments=arguments1) + # There is no proxy so can access MongoCacheBackend directly + self.assertEqual('localhost:27017', region1.backend.api.hosts) + self.assertEqual('ks_cache', region1.backend.api.db_name) + self.assertEqual('cache_region1', region1.backend.api.cache_collection) + self.assertEqual('test_user', region1.backend.api.username) + self.assertEqual('test_password', region1.backend.api.password) + # Should be None because of delayed initialization + self.assertIsNone(region1.backend.api._data_manipulator) + + random_key1 = uuid.uuid4().hex + region1.set(random_key1, "dummyValue10") + self.assertEqual("dummyValue10", region1.get(random_key1)) + # Now should have initialized + self.assertIsInstance(region1.backend.api._data_manipulator, + mongo.BaseTransform) + + class_name = '%s.%s' % (MyTransformer.__module__, "MyTransformer") + + arguments2 = copy.copy(self.arguments) + arguments2['cache_collection'] = 'cache_region2' + arguments2['son_manipulator'] = class_name + + region2 = dp_region.make_region().configure('keystone.cache.mongo', + arguments=arguments2) + # There is no proxy so can access MongoCacheBackend directly + self.assertEqual('localhost:27017', region2.backend.api.hosts) + self.assertEqual('ks_cache', region2.backend.api.db_name) + self.assertEqual('cache_region2', region2.backend.api.cache_collection) + + # Should be None because of delayed initialization + self.assertIsNone(region2.backend.api._data_manipulator) + + random_key = uuid.uuid4().hex + region2.set(random_key, "dummyValue20") + self.assertEqual("dummyValue20", region2.get(random_key)) + # Now should have initialized + self.assertIsInstance(region2.backend.api._data_manipulator, + MyTransformer) + + region1.set(random_key1, "dummyValue22") + self.assertEqual("dummyValue22", region1.get(random_key1)) + + def test_typical_configuration(self): + + dp_region.make_region().configure( + 'keystone.cache.mongo', + arguments=self.arguments + ) + self.assertTrue(True) # reached here means no initialization error + + def test_backend_get_missing_data(self): + + region = dp_region.make_region().configure( + 'keystone.cache.mongo', + arguments=self.arguments + ) + + random_key = uuid.uuid4().hex + # should return NO_VALUE as key does not exist in cache + self.assertEqual(api.NO_VALUE, region.get(random_key)) + + def test_backend_set_data(self): + + region = dp_region.make_region().configure( + 'keystone.cache.mongo', + arguments=self.arguments + ) + + random_key = uuid.uuid4().hex + region.set(random_key, "dummyValue") + self.assertEqual("dummyValue", region.get(random_key)) + + def test_backend_set_data_with_string_as_valid_ttl(self): + + self.arguments['mongo_ttl_seconds'] = '3600' + region = dp_region.make_region().configure('keystone.cache.mongo', + arguments=self.arguments) + self.assertEqual(3600, region.backend.api.ttl_seconds) + random_key = uuid.uuid4().hex + region.set(random_key, "dummyValue") + self.assertEqual("dummyValue", region.get(random_key)) + + def test_backend_set_data_with_int_as_valid_ttl(self): + + self.arguments['mongo_ttl_seconds'] = 1800 + region = dp_region.make_region().configure('keystone.cache.mongo', + arguments=self.arguments) + self.assertEqual(1800, region.backend.api.ttl_seconds) + random_key = uuid.uuid4().hex + region.set(random_key, "dummyValue") + self.assertEqual("dummyValue", region.get(random_key)) + + def test_backend_set_none_as_data(self): + + region = dp_region.make_region().configure( + 'keystone.cache.mongo', + arguments=self.arguments + ) + + random_key = uuid.uuid4().hex + region.set(random_key, None) + self.assertIsNone(region.get(random_key)) + + def test_backend_set_blank_as_data(self): + + region = dp_region.make_region().configure( + 'keystone.cache.mongo', + arguments=self.arguments + ) + + random_key = uuid.uuid4().hex + region.set(random_key, "") + self.assertEqual("", region.get(random_key)) + + def test_backend_set_same_key_multiple_times(self): + + region = dp_region.make_region().configure( + 'keystone.cache.mongo', + arguments=self.arguments + ) + + random_key = uuid.uuid4().hex + region.set(random_key, "dummyValue") + self.assertEqual("dummyValue", region.get(random_key)) + + dict_value = {'key1': 'value1'} + region.set(random_key, dict_value) + self.assertEqual(dict_value, region.get(random_key)) + + region.set(random_key, "dummyValue2") + self.assertEqual("dummyValue2", region.get(random_key)) + + def test_backend_multi_set_data(self): + + region = dp_region.make_region().configure( + 'keystone.cache.mongo', + arguments=self.arguments + ) + random_key = uuid.uuid4().hex + random_key1 = uuid.uuid4().hex + random_key2 = uuid.uuid4().hex + random_key3 = uuid.uuid4().hex + mapping = {random_key1: 'dummyValue1', + random_key2: 'dummyValue2', + random_key3: 'dummyValue3'} + region.set_multi(mapping) + # should return NO_VALUE as key does not exist in cache + self.assertEqual(api.NO_VALUE, region.get(random_key)) + self.assertFalse(region.get(random_key)) + self.assertEqual("dummyValue1", region.get(random_key1)) + self.assertEqual("dummyValue2", region.get(random_key2)) + self.assertEqual("dummyValue3", region.get(random_key3)) + + def test_backend_multi_get_data(self): + + region = dp_region.make_region().configure( + 'keystone.cache.mongo', + arguments=self.arguments + ) + random_key = uuid.uuid4().hex + random_key1 = uuid.uuid4().hex + random_key2 = uuid.uuid4().hex + random_key3 = uuid.uuid4().hex + mapping = {random_key1: 'dummyValue1', + random_key2: '', + random_key3: 'dummyValue3'} + region.set_multi(mapping) + + keys = [random_key, random_key1, random_key2, random_key3] + results = region.get_multi(keys) + # should return NO_VALUE as key does not exist in cache + self.assertEqual(api.NO_VALUE, results[0]) + self.assertEqual("dummyValue1", results[1]) + self.assertEqual("", results[2]) + self.assertEqual("dummyValue3", results[3]) + + def test_backend_multi_set_should_update_existing(self): + + region = dp_region.make_region().configure( + 'keystone.cache.mongo', + arguments=self.arguments + ) + random_key = uuid.uuid4().hex + random_key1 = uuid.uuid4().hex + random_key2 = uuid.uuid4().hex + random_key3 = uuid.uuid4().hex + mapping = {random_key1: 'dummyValue1', + random_key2: 'dummyValue2', + random_key3: 'dummyValue3'} + region.set_multi(mapping) + # should return NO_VALUE as key does not exist in cache + self.assertEqual(api.NO_VALUE, region.get(random_key)) + self.assertEqual("dummyValue1", region.get(random_key1)) + self.assertEqual("dummyValue2", region.get(random_key2)) + self.assertEqual("dummyValue3", region.get(random_key3)) + + mapping = {random_key1: 'dummyValue4', + random_key2: 'dummyValue5'} + region.set_multi(mapping) + self.assertEqual(api.NO_VALUE, region.get(random_key)) + self.assertEqual("dummyValue4", region.get(random_key1)) + self.assertEqual("dummyValue5", region.get(random_key2)) + self.assertEqual("dummyValue3", region.get(random_key3)) + + def test_backend_multi_set_get_with_blanks_none(self): + + region = dp_region.make_region().configure( + 'keystone.cache.mongo', + arguments=self.arguments + ) + random_key = uuid.uuid4().hex + random_key1 = uuid.uuid4().hex + random_key2 = uuid.uuid4().hex + random_key3 = uuid.uuid4().hex + random_key4 = uuid.uuid4().hex + mapping = {random_key1: 'dummyValue1', + random_key2: None, + random_key3: '', + random_key4: 'dummyValue4'} + region.set_multi(mapping) + # should return NO_VALUE as key does not exist in cache + self.assertEqual(api.NO_VALUE, region.get(random_key)) + self.assertEqual("dummyValue1", region.get(random_key1)) + self.assertIsNone(region.get(random_key2)) + self.assertEqual("", region.get(random_key3)) + self.assertEqual("dummyValue4", region.get(random_key4)) + + keys = [random_key, random_key1, random_key2, random_key3, random_key4] + results = region.get_multi(keys) + + # should return NO_VALUE as key does not exist in cache + self.assertEqual(api.NO_VALUE, results[0]) + self.assertEqual("dummyValue1", results[1]) + self.assertIsNone(results[2]) + self.assertEqual("", results[3]) + self.assertEqual("dummyValue4", results[4]) + + mapping = {random_key1: 'dummyValue5', + random_key2: 'dummyValue6'} + region.set_multi(mapping) + self.assertEqual(api.NO_VALUE, region.get(random_key)) + self.assertEqual("dummyValue5", region.get(random_key1)) + self.assertEqual("dummyValue6", region.get(random_key2)) + self.assertEqual("", region.get(random_key3)) + + def test_backend_delete_data(self): + + region = dp_region.make_region().configure( + 'keystone.cache.mongo', + arguments=self.arguments + ) + + random_key = uuid.uuid4().hex + region.set(random_key, "dummyValue") + self.assertEqual("dummyValue", region.get(random_key)) + + region.delete(random_key) + # should return NO_VALUE as key no longer exists in cache + self.assertEqual(api.NO_VALUE, region.get(random_key)) + + def test_backend_multi_delete_data(self): + + region = dp_region.make_region().configure( + 'keystone.cache.mongo', + arguments=self.arguments + ) + random_key = uuid.uuid4().hex + random_key1 = uuid.uuid4().hex + random_key2 = uuid.uuid4().hex + random_key3 = uuid.uuid4().hex + mapping = {random_key1: 'dummyValue1', + random_key2: 'dummyValue2', + random_key3: 'dummyValue3'} + region.set_multi(mapping) + # should return NO_VALUE as key does not exist in cache + self.assertEqual(api.NO_VALUE, region.get(random_key)) + self.assertEqual("dummyValue1", region.get(random_key1)) + self.assertEqual("dummyValue2", region.get(random_key2)) + self.assertEqual("dummyValue3", region.get(random_key3)) + self.assertEqual(api.NO_VALUE, region.get("InvalidKey")) + + keys = mapping.keys() + + region.delete_multi(keys) + + self.assertEqual(api.NO_VALUE, region.get("InvalidKey")) + # should return NO_VALUE as keys no longer exist in cache + self.assertEqual(api.NO_VALUE, region.get(random_key1)) + self.assertEqual(api.NO_VALUE, region.get(random_key2)) + self.assertEqual(api.NO_VALUE, region.get(random_key3)) + + def test_additional_crud_method_arguments_support(self): + """Additional arguments should works across find/insert/update.""" + + self.arguments['wtimeout'] = 30000 + self.arguments['j'] = True + self.arguments['continue_on_error'] = True + self.arguments['secondary_acceptable_latency_ms'] = 60 + region = dp_region.make_region().configure( + 'keystone.cache.mongo', + arguments=self.arguments + ) + + # There is no proxy so can access MongoCacheBackend directly + api_methargs = region.backend.api.meth_kwargs + self.assertEqual(30000, api_methargs['wtimeout']) + self.assertEqual(True, api_methargs['j']) + self.assertEqual(True, api_methargs['continue_on_error']) + self.assertEqual(60, api_methargs['secondary_acceptable_latency_ms']) + + random_key = uuid.uuid4().hex + region.set(random_key, "dummyValue1") + self.assertEqual("dummyValue1", region.get(random_key)) + + region.set(random_key, "dummyValue2") + self.assertEqual("dummyValue2", region.get(random_key)) + + random_key = uuid.uuid4().hex + region.set(random_key, "dummyValue3") + self.assertEqual("dummyValue3", region.get(random_key)) diff --git a/keystone-moon/keystone/tests/unit/test_catalog.py b/keystone-moon/keystone/tests/unit/test_catalog.py new file mode 100644 index 00000000..9dda5d83 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_catalog.py @@ -0,0 +1,219 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 + +import six + +from keystone import catalog +from keystone.tests import unit as tests +from keystone.tests.unit.ksfixtures import database +from keystone.tests.unit import rest + + +BASE_URL = 'http://127.0.0.1:35357/v2' +SERVICE_FIXTURE = object() + + +class V2CatalogTestCase(rest.RestfulTestCase): + def setUp(self): + super(V2CatalogTestCase, self).setUp() + self.useFixture(database.Database()) + + self.service_id = uuid.uuid4().hex + self.service = self.new_service_ref() + self.service['id'] = self.service_id + self.catalog_api.create_service( + self.service_id, + self.service.copy()) + + # TODO(termie): add an admin user to the fixtures and use that user + # override the fixtures, for now + self.assignment_api.add_role_to_user_and_project( + self.user_foo['id'], + self.tenant_bar['id'], + self.role_admin['id']) + + def config_overrides(self): + super(V2CatalogTestCase, self).config_overrides() + self.config_fixture.config( + group='catalog', + driver='keystone.catalog.backends.sql.Catalog') + + def new_ref(self): + """Populates a ref with attributes common to all API entities.""" + return { + 'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'description': uuid.uuid4().hex, + 'enabled': True} + + def new_service_ref(self): + ref = self.new_ref() + ref['type'] = uuid.uuid4().hex + return ref + + def _get_token_id(self, r): + """Applicable only to JSON.""" + return r.result['access']['token']['id'] + + def _endpoint_create(self, expected_status=200, service_id=SERVICE_FIXTURE, + publicurl='http://localhost:8080', + internalurl='http://localhost:8080', + adminurl='http://localhost:8080'): + if service_id is SERVICE_FIXTURE: + service_id = self.service_id + # FIXME(dolph): expected status should actually be 201 Created + path = '/v2.0/endpoints' + body = { + 'endpoint': { + 'adminurl': adminurl, + 'service_id': service_id, + 'region': 'RegionOne', + 'internalurl': internalurl, + 'publicurl': publicurl + } + } + + r = self.admin_request(method='POST', token=self.get_scoped_token(), + path=path, expected_status=expected_status, + body=body) + return body, r + + def test_endpoint_create(self): + req_body, response = self._endpoint_create() + self.assertIn('endpoint', response.result) + self.assertIn('id', response.result['endpoint']) + for field, value in six.iteritems(req_body['endpoint']): + self.assertEqual(response.result['endpoint'][field], value) + + def test_endpoint_create_with_null_adminurl(self): + req_body, response = self._endpoint_create(adminurl=None) + self.assertIsNone(req_body['endpoint']['adminurl']) + self.assertNotIn('adminurl', response.result['endpoint']) + + def test_endpoint_create_with_empty_adminurl(self): + req_body, response = self._endpoint_create(adminurl='') + self.assertEqual('', req_body['endpoint']['adminurl']) + self.assertNotIn("adminurl", response.result['endpoint']) + + def test_endpoint_create_with_null_internalurl(self): + req_body, response = self._endpoint_create(internalurl=None) + self.assertIsNone(req_body['endpoint']['internalurl']) + self.assertNotIn('internalurl', response.result['endpoint']) + + def test_endpoint_create_with_empty_internalurl(self): + req_body, response = self._endpoint_create(internalurl='') + self.assertEqual('', req_body['endpoint']['internalurl']) + self.assertNotIn("internalurl", response.result['endpoint']) + + def test_endpoint_create_with_null_publicurl(self): + self._endpoint_create(expected_status=400, publicurl=None) + + def test_endpoint_create_with_empty_publicurl(self): + self._endpoint_create(expected_status=400, publicurl='') + + def test_endpoint_create_with_null_service_id(self): + self._endpoint_create(expected_status=400, service_id=None) + + def test_endpoint_create_with_empty_service_id(self): + self._endpoint_create(expected_status=400, service_id='') + + +class TestV2CatalogAPISQL(tests.TestCase): + + def setUp(self): + super(TestV2CatalogAPISQL, self).setUp() + self.useFixture(database.Database()) + self.catalog_api = catalog.Manager() + + self.service_id = uuid.uuid4().hex + service = {'id': self.service_id, 'name': uuid.uuid4().hex} + self.catalog_api.create_service(self.service_id, service) + + endpoint = self.new_endpoint_ref(service_id=self.service_id) + self.catalog_api.create_endpoint(endpoint['id'], endpoint) + + def config_overrides(self): + super(TestV2CatalogAPISQL, self).config_overrides() + self.config_fixture.config( + group='catalog', + driver='keystone.catalog.backends.sql.Catalog') + + def new_endpoint_ref(self, service_id): + return { + 'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'description': uuid.uuid4().hex, + 'interface': uuid.uuid4().hex[:8], + 'service_id': service_id, + 'url': uuid.uuid4().hex, + 'region': uuid.uuid4().hex, + } + + def test_get_catalog_ignores_endpoints_with_invalid_urls(self): + user_id = uuid.uuid4().hex + tenant_id = uuid.uuid4().hex + + # the only endpoint in the catalog is the one created in setUp + catalog = self.catalog_api.get_catalog(user_id, tenant_id) + self.assertEqual(1, len(catalog)) + # it's also the only endpoint in the backend + self.assertEqual(1, len(self.catalog_api.list_endpoints())) + + # create a new, invalid endpoint - malformed type declaration + endpoint = self.new_endpoint_ref(self.service_id) + endpoint['url'] = 'http://keystone/%(tenant_id)' + self.catalog_api.create_endpoint(endpoint['id'], endpoint) + + # create a new, invalid endpoint - nonexistent key + endpoint = self.new_endpoint_ref(self.service_id) + endpoint['url'] = 'http://keystone/%(you_wont_find_me)s' + self.catalog_api.create_endpoint(endpoint['id'], endpoint) + + # verify that the invalid endpoints don't appear in the catalog + catalog = self.catalog_api.get_catalog(user_id, tenant_id) + self.assertEqual(1, len(catalog)) + # all three endpoints appear in the backend + self.assertEqual(3, len(self.catalog_api.list_endpoints())) + + def test_get_catalog_always_returns_service_name(self): + user_id = uuid.uuid4().hex + tenant_id = uuid.uuid4().hex + + # create a service, with a name + named_svc = { + 'id': uuid.uuid4().hex, + 'type': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + } + self.catalog_api.create_service(named_svc['id'], named_svc) + endpoint = self.new_endpoint_ref(service_id=named_svc['id']) + self.catalog_api.create_endpoint(endpoint['id'], endpoint) + + # create a service, with no name + unnamed_svc = { + 'id': uuid.uuid4().hex, + 'type': uuid.uuid4().hex + } + self.catalog_api.create_service(unnamed_svc['id'], unnamed_svc) + endpoint = self.new_endpoint_ref(service_id=unnamed_svc['id']) + self.catalog_api.create_endpoint(endpoint['id'], endpoint) + + region = None + catalog = self.catalog_api.get_catalog(user_id, tenant_id) + + self.assertEqual(named_svc['name'], + catalog[region][named_svc['type']]['name']) + self.assertEqual('', catalog[region][unnamed_svc['type']]['name']) diff --git a/keystone-moon/keystone/tests/unit/test_cert_setup.py b/keystone-moon/keystone/tests/unit/test_cert_setup.py new file mode 100644 index 00000000..d1e9ccfd --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_cert_setup.py @@ -0,0 +1,246 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 os +import shutil + +import mock +from testtools import matchers + +from keystone.common import environment +from keystone.common import openssl +from keystone import exception +from keystone.tests import unit as tests +from keystone.tests.unit import rest +from keystone import token + + +SSLDIR = tests.dirs.tmp('ssl') +CONF = tests.CONF +DEFAULT_DOMAIN_ID = CONF.identity.default_domain_id + + +CERTDIR = os.path.join(SSLDIR, 'certs') +KEYDIR = os.path.join(SSLDIR, 'private') + + +class CertSetupTestCase(rest.RestfulTestCase): + + def setUp(self): + super(CertSetupTestCase, self).setUp() + + def cleanup_ssldir(): + try: + shutil.rmtree(SSLDIR) + except OSError: + pass + + self.addCleanup(cleanup_ssldir) + + def config_overrides(self): + super(CertSetupTestCase, self).config_overrides() + ca_certs = os.path.join(CERTDIR, 'ca.pem') + ca_key = os.path.join(CERTDIR, 'cakey.pem') + + self.config_fixture.config( + group='signing', + certfile=os.path.join(CERTDIR, 'signing_cert.pem'), + ca_certs=ca_certs, + ca_key=ca_key, + keyfile=os.path.join(KEYDIR, 'signing_key.pem')) + self.config_fixture.config( + group='ssl', + ca_key=ca_key) + self.config_fixture.config( + group='eventlet_server_ssl', + ca_certs=ca_certs, + certfile=os.path.join(CERTDIR, 'keystone.pem'), + keyfile=os.path.join(KEYDIR, 'keystonekey.pem')) + self.config_fixture.config( + group='token', + provider='keystone.token.providers.pkiz.Provider') + + def test_can_handle_missing_certs(self): + controller = token.controllers.Auth() + + self.config_fixture.config(group='signing', certfile='invalid') + password = 'fake1' + user = { + 'name': 'fake1', + 'password': password, + 'domain_id': DEFAULT_DOMAIN_ID + } + user = self.identity_api.create_user(user) + body_dict = { + 'passwordCredentials': { + 'userId': user['id'], + 'password': password, + }, + } + self.assertRaises(exception.UnexpectedError, + controller.authenticate, + {}, body_dict) + + def test_create_pki_certs(self, rebuild=False): + pki = openssl.ConfigurePKI(None, None, rebuild=rebuild) + pki.run() + self.assertTrue(os.path.exists(CONF.signing.certfile)) + self.assertTrue(os.path.exists(CONF.signing.ca_certs)) + self.assertTrue(os.path.exists(CONF.signing.keyfile)) + + def test_create_ssl_certs(self, rebuild=False): + ssl = openssl.ConfigureSSL(None, None, rebuild=rebuild) + ssl.run() + self.assertTrue(os.path.exists(CONF.eventlet_server_ssl.ca_certs)) + self.assertTrue(os.path.exists(CONF.eventlet_server_ssl.certfile)) + self.assertTrue(os.path.exists(CONF.eventlet_server_ssl.keyfile)) + + def test_fetch_signing_cert(self, rebuild=False): + pki = openssl.ConfigurePKI(None, None, rebuild=rebuild) + pki.run() + + # NOTE(jamielennox): Use request directly because certificate + # requests don't have some of the normal information + signing_resp = self.request(self.public_app, + '/v2.0/certificates/signing', + method='GET', expected_status=200) + + cacert_resp = self.request(self.public_app, + '/v2.0/certificates/ca', + method='GET', expected_status=200) + + with open(CONF.signing.certfile) as f: + self.assertEqual(f.read(), signing_resp.text) + + with open(CONF.signing.ca_certs) as f: + self.assertEqual(f.read(), cacert_resp.text) + + # NOTE(jamielennox): This is weird behaviour that we need to enforce. + # It doesn't matter what you ask for it's always going to give text + # with a text/html content_type. + + for path in ['/v2.0/certificates/signing', '/v2.0/certificates/ca']: + for accept in [None, 'text/html', 'application/json', 'text/xml']: + headers = {'Accept': accept} if accept else {} + resp = self.request(self.public_app, path, method='GET', + expected_status=200, + headers=headers) + + self.assertEqual('text/html', resp.content_type) + + def test_fetch_signing_cert_when_rebuild(self): + pki = openssl.ConfigurePKI(None, None) + pki.run() + self.test_fetch_signing_cert(rebuild=True) + + def test_failure(self): + for path in ['/v2.0/certificates/signing', '/v2.0/certificates/ca']: + self.request(self.public_app, path, method='GET', + expected_status=500) + + def test_pki_certs_rebuild(self): + self.test_create_pki_certs() + with open(CONF.signing.certfile) as f: + cert_file1 = f.read() + + self.test_create_pki_certs(rebuild=True) + with open(CONF.signing.certfile) as f: + cert_file2 = f.read() + + self.assertNotEqual(cert_file1, cert_file2) + + def test_ssl_certs_rebuild(self): + self.test_create_ssl_certs() + with open(CONF.eventlet_server_ssl.certfile) as f: + cert_file1 = f.read() + + self.test_create_ssl_certs(rebuild=True) + with open(CONF.eventlet_server_ssl.certfile) as f: + cert_file2 = f.read() + + self.assertNotEqual(cert_file1, cert_file2) + + @mock.patch.object(os, 'remove') + def test_rebuild_pki_certs_remove_error(self, mock_remove): + self.test_create_pki_certs() + with open(CONF.signing.certfile) as f: + cert_file1 = f.read() + + mock_remove.side_effect = OSError() + self.test_create_pki_certs(rebuild=True) + with open(CONF.signing.certfile) as f: + cert_file2 = f.read() + + self.assertEqual(cert_file1, cert_file2) + + @mock.patch.object(os, 'remove') + def test_rebuild_ssl_certs_remove_error(self, mock_remove): + self.test_create_ssl_certs() + with open(CONF.eventlet_server_ssl.certfile) as f: + cert_file1 = f.read() + + mock_remove.side_effect = OSError() + self.test_create_ssl_certs(rebuild=True) + with open(CONF.eventlet_server_ssl.certfile) as f: + cert_file2 = f.read() + + self.assertEqual(cert_file1, cert_file2) + + def test_create_pki_certs_twice_without_rebuild(self): + self.test_create_pki_certs() + with open(CONF.signing.certfile) as f: + cert_file1 = f.read() + + self.test_create_pki_certs() + with open(CONF.signing.certfile) as f: + cert_file2 = f.read() + + self.assertEqual(cert_file1, cert_file2) + + def test_create_ssl_certs_twice_without_rebuild(self): + self.test_create_ssl_certs() + with open(CONF.eventlet_server_ssl.certfile) as f: + cert_file1 = f.read() + + self.test_create_ssl_certs() + with open(CONF.eventlet_server_ssl.certfile) as f: + cert_file2 = f.read() + + self.assertEqual(cert_file1, cert_file2) + + +class TestExecCommand(tests.TestCase): + + @mock.patch.object(environment.subprocess.Popen, 'poll') + def test_running_a_successful_command(self, mock_poll): + mock_poll.return_value = 0 + + ssl = openssl.ConfigureSSL('keystone_user', 'keystone_group') + ssl.exec_command(['ls']) + + @mock.patch.object(environment.subprocess.Popen, 'communicate') + @mock.patch.object(environment.subprocess.Popen, 'poll') + def test_running_an_invalid_command(self, mock_poll, mock_communicate): + output = 'this is the output string' + + mock_communicate.return_value = (output, '') + mock_poll.return_value = 1 + + cmd = ['ls'] + ssl = openssl.ConfigureSSL('keystone_user', 'keystone_group') + e = self.assertRaises(environment.subprocess.CalledProcessError, + ssl.exec_command, + cmd) + self.assertThat(e.output, matchers.Equals(output)) diff --git a/keystone-moon/keystone/tests/unit/test_cli.py b/keystone-moon/keystone/tests/unit/test_cli.py new file mode 100644 index 00000000..20aa03e6 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_cli.py @@ -0,0 +1,252 @@ +# Copyright 2014 IBM Corp. +# +# 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 os +import uuid + +import mock +from oslo_config import cfg + +from keystone import cli +from keystone.common import dependency +from keystone.i18n import _ +from keystone import resource +from keystone.tests import unit as tests +from keystone.tests.unit.ksfixtures import database + +CONF = cfg.CONF + + +class CliTestCase(tests.SQLDriverOverrides, tests.TestCase): + def config_files(self): + config_files = super(CliTestCase, self).config_files() + config_files.append(tests.dirs.tests_conf('backend_sql.conf')) + return config_files + + def test_token_flush(self): + self.useFixture(database.Database()) + self.load_backends() + cli.TokenFlush.main() + + +class CliDomainConfigAllTestCase(tests.SQLDriverOverrides, tests.TestCase): + + def setUp(self): + self.useFixture(database.Database()) + super(CliDomainConfigAllTestCase, self).setUp() + self.load_backends() + self.config_fixture.config( + group='identity', + domain_config_dir=tests.TESTCONF + '/domain_configs_multi_ldap') + self.domain_count = 3 + self.setup_initial_domains() + + def config_files(self): + self.config_fixture.register_cli_opt(cli.command_opt) + self.addCleanup(self.cleanup) + config_files = super(CliDomainConfigAllTestCase, self).config_files() + config_files.append(tests.dirs.tests_conf('backend_sql.conf')) + return config_files + + def cleanup(self): + CONF.reset() + CONF.unregister_opt(cli.command_opt) + + def cleanup_domains(self): + for domain in self.domains: + if domain == 'domain_default': + # Not allowed to delete the default domain, but should at least + # delete any domain-specific config for it. + self.domain_config_api.delete_config( + CONF.identity.default_domain_id) + continue + this_domain = self.domains[domain] + this_domain['enabled'] = False + self.resource_api.update_domain(this_domain['id'], this_domain) + self.resource_api.delete_domain(this_domain['id']) + self.domains = {} + + def config(self, config_files): + CONF(args=['domain_config_upload', '--all'], project='keystone', + default_config_files=config_files) + + def setup_initial_domains(self): + + def create_domain(domain): + return self.resource_api.create_domain(domain['id'], domain) + + self.domains = {} + self.addCleanup(self.cleanup_domains) + for x in range(1, self.domain_count): + domain = 'domain%s' % x + self.domains[domain] = create_domain( + {'id': uuid.uuid4().hex, 'name': domain}) + self.domains['domain_default'] = create_domain( + resource.calc_default_domain()) + + def test_config_upload(self): + # The values below are the same as in the domain_configs_multi_ldap + # directory of test config_files. + default_config = { + 'ldap': {'url': 'fake://memory', + 'user': 'cn=Admin', + 'password': 'password', + 'suffix': 'cn=example,cn=com'}, + 'identity': {'driver': 'keystone.identity.backends.ldap.Identity'} + } + domain1_config = { + 'ldap': {'url': 'fake://memory1', + 'user': 'cn=Admin', + 'password': 'password', + 'suffix': 'cn=example,cn=com'}, + 'identity': {'driver': 'keystone.identity.backends.ldap.Identity'} + } + domain2_config = { + 'ldap': {'url': 'fake://memory', + 'user': 'cn=Admin', + 'password': 'password', + 'suffix': 'cn=myroot,cn=com', + 'group_tree_dn': 'ou=UserGroups,dc=myroot,dc=org', + 'user_tree_dn': 'ou=Users,dc=myroot,dc=org'}, + 'identity': {'driver': 'keystone.identity.backends.ldap.Identity'} + } + + # Clear backend dependencies, since cli loads these manually + dependency.reset() + cli.DomainConfigUpload.main() + + res = self.domain_config_api.get_config_with_sensitive_info( + CONF.identity.default_domain_id) + self.assertEqual(default_config, res) + res = self.domain_config_api.get_config_with_sensitive_info( + self.domains['domain1']['id']) + self.assertEqual(domain1_config, res) + res = self.domain_config_api.get_config_with_sensitive_info( + self.domains['domain2']['id']) + self.assertEqual(domain2_config, res) + + +class CliDomainConfigSingleDomainTestCase(CliDomainConfigAllTestCase): + + def config(self, config_files): + CONF(args=['domain_config_upload', '--domain-name', 'Default'], + project='keystone', default_config_files=config_files) + + def test_config_upload(self): + # The values below are the same as in the domain_configs_multi_ldap + # directory of test config_files. + default_config = { + 'ldap': {'url': 'fake://memory', + 'user': 'cn=Admin', + 'password': 'password', + 'suffix': 'cn=example,cn=com'}, + 'identity': {'driver': 'keystone.identity.backends.ldap.Identity'} + } + + # Clear backend dependencies, since cli loads these manually + dependency.reset() + cli.DomainConfigUpload.main() + + res = self.domain_config_api.get_config_with_sensitive_info( + CONF.identity.default_domain_id) + self.assertEqual(default_config, res) + res = self.domain_config_api.get_config_with_sensitive_info( + self.domains['domain1']['id']) + self.assertEqual({}, res) + res = self.domain_config_api.get_config_with_sensitive_info( + self.domains['domain2']['id']) + self.assertEqual({}, res) + + def test_no_overwrite_config(self): + # Create a config for the default domain + default_config = { + 'ldap': {'url': uuid.uuid4().hex}, + 'identity': {'driver': 'keystone.identity.backends.ldap.Identity'} + } + self.domain_config_api.create_config( + CONF.identity.default_domain_id, default_config) + + # Now try and upload the settings in the configuration file for the + # default domain + dependency.reset() + with mock.patch('__builtin__.print') as mock_print: + self.assertRaises(SystemExit, cli.DomainConfigUpload.main) + file_name = ('keystone.%s.conf' % + resource.calc_default_domain()['name']) + error_msg = _( + 'Domain: %(domain)s already has a configuration defined - ' + 'ignoring file: %(file)s.') % { + 'domain': resource.calc_default_domain()['name'], + 'file': os.path.join(CONF.identity.domain_config_dir, + file_name)} + mock_print.assert_has_calls([mock.call(error_msg)]) + + res = self.domain_config_api.get_config( + CONF.identity.default_domain_id) + # The initial config should not have been overwritten + self.assertEqual(default_config, res) + + +class CliDomainConfigNoOptionsTestCase(CliDomainConfigAllTestCase): + + def config(self, config_files): + CONF(args=['domain_config_upload'], + project='keystone', default_config_files=config_files) + + def test_config_upload(self): + dependency.reset() + with mock.patch('__builtin__.print') as mock_print: + self.assertRaises(SystemExit, cli.DomainConfigUpload.main) + mock_print.assert_has_calls( + [mock.call( + _('At least one option must be provided, use either ' + '--all or --domain-name'))]) + + +class CliDomainConfigTooManyOptionsTestCase(CliDomainConfigAllTestCase): + + def config(self, config_files): + CONF(args=['domain_config_upload', '--all', '--domain-name', + 'Default'], + project='keystone', default_config_files=config_files) + + def test_config_upload(self): + dependency.reset() + with mock.patch('__builtin__.print') as mock_print: + self.assertRaises(SystemExit, cli.DomainConfigUpload.main) + mock_print.assert_has_calls( + [mock.call(_('The --all option cannot be used with ' + 'the --domain-name option'))]) + + +class CliDomainConfigInvalidDomainTestCase(CliDomainConfigAllTestCase): + + def config(self, config_files): + self.invalid_domain_name = uuid.uuid4().hex + CONF(args=['domain_config_upload', '--domain-name', + self.invalid_domain_name], + project='keystone', default_config_files=config_files) + + def test_config_upload(self): + dependency.reset() + with mock.patch('__builtin__.print') as mock_print: + self.assertRaises(SystemExit, cli.DomainConfigUpload.main) + file_name = 'keystone.%s.conf' % self.invalid_domain_name + error_msg = (_( + 'Invalid domain name: %(domain)s found in config file name: ' + '%(file)s - ignoring this file.') % { + 'domain': self.invalid_domain_name, + 'file': os.path.join(CONF.identity.domain_config_dir, + file_name)}) + mock_print.assert_has_calls([mock.call(error_msg)]) diff --git a/keystone-moon/keystone/tests/unit/test_config.py b/keystone-moon/keystone/tests/unit/test_config.py new file mode 100644 index 00000000..15cfac81 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_config.py @@ -0,0 +1,84 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 oslo_config import cfg + +from keystone import config +from keystone import exception +from keystone.tests import unit as tests + + +CONF = cfg.CONF + + +class ConfigTestCase(tests.TestCase): + + def config_files(self): + config_files = super(ConfigTestCase, self).config_files() + # Insert the keystone sample as the first config file to be loaded + # since it is used in one of the code paths to determine the paste-ini + # location. + config_files.insert(0, tests.dirs.etc('keystone.conf.sample')) + return config_files + + def test_paste_config(self): + self.assertEqual(tests.dirs.etc('keystone-paste.ini'), + config.find_paste_config()) + self.config_fixture.config(group='paste_deploy', + config_file=uuid.uuid4().hex) + self.assertRaises(exception.ConfigFileNotFound, + config.find_paste_config) + self.config_fixture.config(group='paste_deploy', config_file='') + self.assertEqual(tests.dirs.etc('keystone.conf.sample'), + config.find_paste_config()) + + def test_config_default(self): + self.assertEqual('keystone.auth.plugins.password.Password', + CONF.auth.password) + self.assertEqual('keystone.auth.plugins.token.Token', + CONF.auth.token) + + +class DeprecatedTestCase(tests.TestCase): + """Test using the original (deprecated) name for renamed options.""" + + def config_files(self): + config_files = super(DeprecatedTestCase, self).config_files() + config_files.append(tests.dirs.tests_conf('deprecated.conf')) + return config_files + + def test_sql(self): + # Options in [sql] were moved to [database] in Icehouse for the change + # to use oslo-incubator's db.sqlalchemy.sessions. + + self.assertEqual('sqlite://deprecated', CONF.database.connection) + self.assertEqual(54321, CONF.database.idle_timeout) + + +class DeprecatedOverrideTestCase(tests.TestCase): + """Test using the deprecated AND new name for renamed options.""" + + def config_files(self): + config_files = super(DeprecatedOverrideTestCase, self).config_files() + config_files.append(tests.dirs.tests_conf('deprecated_override.conf')) + return config_files + + def test_sql(self): + # Options in [sql] were moved to [database] in Icehouse for the change + # to use oslo-incubator's db.sqlalchemy.sessions. + + self.assertEqual('sqlite://new', CONF.database.connection) + self.assertEqual(65432, CONF.database.idle_timeout) diff --git a/keystone-moon/keystone/tests/unit/test_contrib_s3_core.py b/keystone-moon/keystone/tests/unit/test_contrib_s3_core.py new file mode 100644 index 00000000..43ea1ac5 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_contrib_s3_core.py @@ -0,0 +1,55 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 keystone.contrib import s3 +from keystone import exception +from keystone.tests import unit as tests + + +class S3ContribCore(tests.TestCase): + def setUp(self): + super(S3ContribCore, self).setUp() + + self.load_backends() + + self.controller = s3.S3Controller() + + def test_good_signature(self): + creds_ref = {'secret': + 'b121dd41cdcc42fe9f70e572e84295aa'} + credentials = {'token': + 'UFVUCjFCMk0yWThBc2dUcGdBbVk3UGhDZmc9PQphcHB' + 'saWNhdGlvbi9vY3RldC1zdHJlYW0KVHVlLCAxMSBEZWMgMjAxM' + 'iAyMTo0MTo0MSBHTVQKL2NvbnRfczMvdXBsb2FkZWRfZnJ' + 'vbV9zMy50eHQ=', + 'signature': 'IL4QLcLVaYgylF9iHj6Wb8BGZsw='} + + self.assertIsNone(self.controller.check_signature(creds_ref, + credentials)) + + def test_bad_signature(self): + creds_ref = {'secret': + 'b121dd41cdcc42fe9f70e572e84295aa'} + credentials = {'token': + 'UFVUCjFCMk0yWThBc2dUcGdBbVk3UGhDZmc9PQphcHB' + 'saWNhdGlvbi9vY3RldC1zdHJlYW0KVHVlLCAxMSBEZWMgMjAxM' + 'iAyMTo0MTo0MSBHTVQKL2NvbnRfczMvdXBsb2FkZWRfZnJ' + 'vbV9zMy50eHQ=', + 'signature': uuid.uuid4().hex} + + self.assertRaises(exception.Unauthorized, + self.controller.check_signature, + creds_ref, credentials) diff --git a/keystone-moon/keystone/tests/unit/test_contrib_simple_cert.py b/keystone-moon/keystone/tests/unit/test_contrib_simple_cert.py new file mode 100644 index 00000000..8664e2c3 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_contrib_simple_cert.py @@ -0,0 +1,57 @@ +# 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 keystone.tests.unit import test_v3 + + +class BaseTestCase(test_v3.RestfulTestCase): + + EXTENSION_TO_ADD = 'simple_cert_extension' + + CA_PATH = '/v3/OS-SIMPLE-CERT/ca' + CERT_PATH = '/v3/OS-SIMPLE-CERT/certificates' + + +class TestSimpleCert(BaseTestCase): + + def request_cert(self, path): + content_type = 'application/x-pem-file' + response = self.request(app=self.public_app, + method='GET', + path=path, + headers={'Accept': content_type}, + expected_status=200) + + self.assertEqual(content_type, response.content_type.lower()) + self.assertIn('---BEGIN', response.body) + + return response + + def test_ca_cert(self): + self.request_cert(self.CA_PATH) + + def test_signing_cert(self): + self.request_cert(self.CERT_PATH) + + def test_missing_file(self): + # these files do not exist + self.config_fixture.config(group='signing', + ca_certs=uuid.uuid4().hex, + certfile=uuid.uuid4().hex) + + for path in [self.CA_PATH, self.CERT_PATH]: + self.request(app=self.public_app, + method='GET', + path=path, + expected_status=500) diff --git a/keystone-moon/keystone/tests/unit/test_driver_hints.py b/keystone-moon/keystone/tests/unit/test_driver_hints.py new file mode 100644 index 00000000..c20d2ae7 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_driver_hints.py @@ -0,0 +1,60 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 keystone.common import driver_hints +from keystone.tests.unit import core as test + + +class ListHintsTests(test.TestCase): + + def test_create_iterate_satisfy(self): + hints = driver_hints.Hints() + hints.add_filter('t1', 'data1') + hints.add_filter('t2', 'data2') + self.assertEqual(2, len(hints.filters)) + filter = hints.get_exact_filter_by_name('t1') + self.assertEqual('t1', filter['name']) + self.assertEqual('data1', filter['value']) + self.assertEqual('equals', filter['comparator']) + self.assertEqual(False, filter['case_sensitive']) + + hints.filters.remove(filter) + filter_count = 0 + for filter in hints.filters: + filter_count += 1 + self.assertEqual('t2', filter['name']) + self.assertEqual(1, filter_count) + + def test_multiple_creates(self): + hints = driver_hints.Hints() + hints.add_filter('t1', 'data1') + hints.add_filter('t2', 'data2') + self.assertEqual(2, len(hints.filters)) + hints2 = driver_hints.Hints() + hints2.add_filter('t4', 'data1') + hints2.add_filter('t5', 'data2') + self.assertEqual(2, len(hints.filters)) + + def test_limits(self): + hints = driver_hints.Hints() + self.assertIsNone(hints.limit) + hints.set_limit(10) + self.assertEqual(10, hints.limit['limit']) + self.assertFalse(hints.limit['truncated']) + hints.set_limit(11) + self.assertEqual(11, hints.limit['limit']) + self.assertFalse(hints.limit['truncated']) + hints.set_limit(10, truncated=True) + self.assertEqual(10, hints.limit['limit']) + self.assertTrue(hints.limit['truncated']) diff --git a/keystone-moon/keystone/tests/unit/test_ec2_token_middleware.py b/keystone-moon/keystone/tests/unit/test_ec2_token_middleware.py new file mode 100644 index 00000000..03c95e27 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_ec2_token_middleware.py @@ -0,0 +1,34 @@ +# 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 keystonemiddleware import ec2_token as ksm_ec2_token + +from keystone.middleware import ec2_token +from keystone.tests import unit as tests + + +class EC2TokenMiddlewareTestBase(tests.BaseTestCase): + def test_symbols(self): + """Verify ec2 middleware symbols. + + Verify that the keystone version of ec2_token middleware forwards the + public symbols from the keystonemiddleware version of the ec2_token + middleware for backwards compatibility. + + """ + + self.assertIs(ksm_ec2_token.app_factory, ec2_token.app_factory) + self.assertIs(ksm_ec2_token.filter_factory, ec2_token.filter_factory) + self.assertTrue( + issubclass(ec2_token.EC2Token, ksm_ec2_token.EC2Token), + 'ec2_token.EC2Token is not subclass of ' + 'keystonemiddleware.ec2_token.EC2Token') diff --git a/keystone-moon/keystone/tests/unit/test_exception.py b/keystone-moon/keystone/tests/unit/test_exception.py new file mode 100644 index 00000000..f91fa2a7 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_exception.py @@ -0,0 +1,227 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 oslo_config import cfg +from oslo_config import fixture as config_fixture +from oslo_serialization import jsonutils +import six + +from keystone.common import wsgi +from keystone import exception +from keystone.tests import unit as tests + + +class ExceptionTestCase(tests.BaseTestCase): + def assertValidJsonRendering(self, e): + resp = wsgi.render_exception(e) + self.assertEqual(e.code, resp.status_int) + self.assertEqual('%s %s' % (e.code, e.title), resp.status) + + j = jsonutils.loads(resp.body) + self.assertIsNotNone(j.get('error')) + self.assertIsNotNone(j['error'].get('code')) + self.assertIsNotNone(j['error'].get('title')) + self.assertIsNotNone(j['error'].get('message')) + self.assertNotIn('\n', j['error']['message']) + self.assertNotIn(' ', j['error']['message']) + self.assertTrue(type(j['error']['code']) is int) + + def test_all_json_renderings(self): + """Everything callable in the exception module should be renderable. + + ... except for the base error class (exception.Error), which is not + user-facing. + + This test provides a custom message to bypass docstring parsing, which + should be tested separately. + + """ + for cls in [x for x in exception.__dict__.values() if callable(x)]: + if cls is not exception.Error and isinstance(cls, exception.Error): + self.assertValidJsonRendering(cls(message='Overridden.')) + + def test_validation_error(self): + target = uuid.uuid4().hex + attribute = uuid.uuid4().hex + e = exception.ValidationError(target=target, attribute=attribute) + self.assertValidJsonRendering(e) + self.assertIn(target, six.text_type(e)) + self.assertIn(attribute, six.text_type(e)) + + def test_not_found(self): + target = uuid.uuid4().hex + e = exception.NotFound(target=target) + self.assertValidJsonRendering(e) + self.assertIn(target, six.text_type(e)) + + def test_403_title(self): + e = exception.Forbidden() + resp = wsgi.render_exception(e) + j = jsonutils.loads(resp.body) + self.assertEqual('Forbidden', e.title) + self.assertEqual('Forbidden', j['error'].get('title')) + + def test_unicode_message(self): + message = u'Comment \xe7a va' + e = exception.Error(message) + + try: + self.assertEqual(message, six.text_type(e)) + except UnicodeEncodeError: + self.fail("unicode error message not supported") + + def test_unicode_string(self): + e = exception.ValidationError(attribute='xx', + target='Long \xe2\x80\x93 Dash') + + self.assertIn(u'\u2013', six.text_type(e)) + + def test_invalid_unicode_string(self): + # NOTE(jamielennox): This is a complete failure case so what is + # returned in the exception message is not that important so long + # as there is an error with a message + e = exception.ValidationError(attribute='xx', + target='\xe7a va') + self.assertIn('%(attribute)', six.text_type(e)) + + +class UnexpectedExceptionTestCase(ExceptionTestCase): + """Tests if internal info is exposed to the API user on UnexpectedError.""" + + class SubClassExc(exception.UnexpectedError): + debug_message_format = 'Debug Message: %(debug_info)s' + + def setUp(self): + super(UnexpectedExceptionTestCase, self).setUp() + self.exc_str = uuid.uuid4().hex + self.config_fixture = self.useFixture(config_fixture.Config(cfg.CONF)) + + def test_unexpected_error_no_debug(self): + self.config_fixture.config(debug=False) + e = exception.UnexpectedError(exception=self.exc_str) + self.assertNotIn(self.exc_str, six.text_type(e)) + + def test_unexpected_error_debug(self): + self.config_fixture.config(debug=True) + e = exception.UnexpectedError(exception=self.exc_str) + self.assertIn(self.exc_str, six.text_type(e)) + + def test_unexpected_error_subclass_no_debug(self): + self.config_fixture.config(debug=False) + e = UnexpectedExceptionTestCase.SubClassExc( + debug_info=self.exc_str) + self.assertEqual(exception.UnexpectedError._message_format, + six.text_type(e)) + + def test_unexpected_error_subclass_debug(self): + self.config_fixture.config(debug=True) + subclass = self.SubClassExc + + e = subclass(debug_info=self.exc_str) + expected = subclass.debug_message_format % {'debug_info': self.exc_str} + translated_amendment = six.text_type(exception.SecurityError.amendment) + self.assertEqual( + expected + six.text_type(' ') + translated_amendment, + six.text_type(e)) + + def test_unexpected_error_custom_message_no_debug(self): + self.config_fixture.config(debug=False) + e = exception.UnexpectedError(self.exc_str) + self.assertEqual(exception.UnexpectedError._message_format, + six.text_type(e)) + + def test_unexpected_error_custom_message_debug(self): + self.config_fixture.config(debug=True) + e = exception.UnexpectedError(self.exc_str) + translated_amendment = six.text_type(exception.SecurityError.amendment) + self.assertEqual( + self.exc_str + six.text_type(' ') + translated_amendment, + six.text_type(e)) + + +class SecurityErrorTestCase(ExceptionTestCase): + """Tests whether security-related info is exposed to the API user.""" + + def setUp(self): + super(SecurityErrorTestCase, self).setUp() + self.config_fixture = self.useFixture(config_fixture.Config(cfg.CONF)) + + def test_unauthorized_exposure(self): + self.config_fixture.config(debug=False) + + risky_info = uuid.uuid4().hex + e = exception.Unauthorized(message=risky_info) + self.assertValidJsonRendering(e) + self.assertNotIn(risky_info, six.text_type(e)) + + def test_unauthorized_exposure_in_debug(self): + self.config_fixture.config(debug=True) + + risky_info = uuid.uuid4().hex + e = exception.Unauthorized(message=risky_info) + self.assertValidJsonRendering(e) + self.assertIn(risky_info, six.text_type(e)) + + def test_forbidden_exposure(self): + self.config_fixture.config(debug=False) + + risky_info = uuid.uuid4().hex + e = exception.Forbidden(message=risky_info) + self.assertValidJsonRendering(e) + self.assertNotIn(risky_info, six.text_type(e)) + + def test_forbidden_exposure_in_debug(self): + self.config_fixture.config(debug=True) + + risky_info = uuid.uuid4().hex + e = exception.Forbidden(message=risky_info) + self.assertValidJsonRendering(e) + self.assertIn(risky_info, six.text_type(e)) + + def test_forbidden_action_exposure(self): + self.config_fixture.config(debug=False) + + risky_info = uuid.uuid4().hex + action = uuid.uuid4().hex + e = exception.ForbiddenAction(message=risky_info, action=action) + self.assertValidJsonRendering(e) + self.assertNotIn(risky_info, six.text_type(e)) + self.assertIn(action, six.text_type(e)) + + e = exception.ForbiddenAction(action=risky_info) + self.assertValidJsonRendering(e) + self.assertIn(risky_info, six.text_type(e)) + + def test_forbidden_action_exposure_in_debug(self): + self.config_fixture.config(debug=True) + + risky_info = uuid.uuid4().hex + + e = exception.ForbiddenAction(message=risky_info) + self.assertValidJsonRendering(e) + self.assertIn(risky_info, six.text_type(e)) + + e = exception.ForbiddenAction(action=risky_info) + self.assertValidJsonRendering(e) + self.assertIn(risky_info, six.text_type(e)) + + def test_unicode_argument_message(self): + self.config_fixture.config(debug=False) + + risky_info = u'\u7ee7\u7eed\u884c\u7f29\u8fdb\u6216' + e = exception.Forbidden(message=risky_info) + self.assertValidJsonRendering(e) + self.assertNotIn(risky_info, six.text_type(e)) diff --git a/keystone-moon/keystone/tests/unit/test_hacking_checks.py b/keystone-moon/keystone/tests/unit/test_hacking_checks.py new file mode 100644 index 00000000..b9b047b3 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_hacking_checks.py @@ -0,0 +1,143 @@ +# 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 textwrap + +import mock +import pep8 +import testtools + +from keystone.hacking import checks +from keystone.tests.unit.ksfixtures import hacking as hacking_fixtures + + +class BaseStyleCheck(testtools.TestCase): + + def setUp(self): + super(BaseStyleCheck, self).setUp() + self.code_ex = self.useFixture(self.get_fixture()) + self.addCleanup(delattr, self, 'code_ex') + + def get_checker(self): + """Returns the checker to be used for tests in this class.""" + raise NotImplemented('subclasses must provide a real implementation') + + def get_fixture(self): + return hacking_fixtures.HackingCode() + + # We are patching pep8 so that only the check under test is actually + # installed. + @mock.patch('pep8._checks', + {'physical_line': {}, 'logical_line': {}, 'tree': {}}) + def run_check(self, code): + pep8.register_check(self.get_checker()) + + lines = textwrap.dedent(code).strip().splitlines(True) + + checker = pep8.Checker(lines=lines) + checker.check_all() + checker.report._deferred_print.sort() + return checker.report._deferred_print + + def assert_has_errors(self, code, expected_errors=None): + actual_errors = [e[:3] for e in self.run_check(code)] + self.assertEqual(expected_errors or [], actual_errors) + + +class TestCheckForMutableDefaultArgs(BaseStyleCheck): + + def get_checker(self): + return checks.CheckForMutableDefaultArgs + + def test(self): + code = self.code_ex.mutable_default_args['code'] + errors = self.code_ex.mutable_default_args['expected_errors'] + self.assert_has_errors(code, expected_errors=errors) + + +class TestBlockCommentsBeginWithASpace(BaseStyleCheck): + + def get_checker(self): + return checks.block_comments_begin_with_a_space + + def test(self): + code = self.code_ex.comments_begin_with_space['code'] + errors = self.code_ex.comments_begin_with_space['expected_errors'] + self.assert_has_errors(code, expected_errors=errors) + + +class TestAssertingNoneEquality(BaseStyleCheck): + + def get_checker(self): + return checks.CheckForAssertingNoneEquality + + def test(self): + code = self.code_ex.asserting_none_equality['code'] + errors = self.code_ex.asserting_none_equality['expected_errors'] + self.assert_has_errors(code, expected_errors=errors) + + +class TestCheckForDebugLoggingIssues(BaseStyleCheck): + + def get_checker(self): + return checks.CheckForLoggingIssues + + def test_for_translations(self): + fixture = self.code_ex.assert_no_translations_for_debug_logging + code = fixture['code'] + errors = fixture['expected_errors'] + self.assert_has_errors(code, expected_errors=errors) + + +class TestCheckForNonDebugLoggingIssues(BaseStyleCheck): + + def get_checker(self): + return checks.CheckForLoggingIssues + + def get_fixture(self): + return hacking_fixtures.HackingLogging() + + def test_for_translations(self): + for example in self.code_ex.examples: + code = self.code_ex.shared_imports + example['code'] + errors = example['expected_errors'] + self.assert_has_errors(code, expected_errors=errors) + + def assert_has_errors(self, code, expected_errors=None): + # pull out the parts of the error that we'll match against + actual_errors = (e[:3] for e in self.run_check(code)) + # adjust line numbers to make the fixure data more readable. + import_lines = len(self.code_ex.shared_imports.split('\n')) - 1 + actual_errors = [(e[0] - import_lines, e[1], e[2]) + for e in actual_errors] + self.assertEqual(expected_errors or [], actual_errors) + + +class TestCheckOsloNamespaceImports(BaseStyleCheck): + def get_checker(self): + return checks.check_oslo_namespace_imports + + def test(self): + code = self.code_ex.oslo_namespace_imports['code'] + errors = self.code_ex.oslo_namespace_imports['expected_errors'] + self.assert_has_errors(code, expected_errors=errors) + + +class TestDictConstructorWithSequenceCopy(BaseStyleCheck): + + def get_checker(self): + return checks.dict_constructor_with_sequence_copy + + def test(self): + code = self.code_ex.dict_constructor['code'] + errors = self.code_ex.dict_constructor['expected_errors'] + self.assert_has_errors(code, expected_errors=errors) diff --git a/keystone-moon/keystone/tests/unit/test_ipv6.py b/keystone-moon/keystone/tests/unit/test_ipv6.py new file mode 100644 index 00000000..e3d467fb --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_ipv6.py @@ -0,0 +1,51 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 oslo_config import cfg + +from keystone.common import environment +from keystone.tests import unit as tests +from keystone.tests.unit.ksfixtures import appserver + + +CONF = cfg.CONF + + +class IPv6TestCase(tests.TestCase): + + def setUp(self): + self.skip_if_no_ipv6() + super(IPv6TestCase, self).setUp() + self.load_backends() + + def test_ipv6_ok(self): + """Make sure both public and admin API work with ipv6.""" + paste_conf = self._paste_config('keystone') + + # Verify Admin + with appserver.AppServer(paste_conf, appserver.ADMIN, host="::1"): + conn = environment.httplib.HTTPConnection( + '::1', CONF.eventlet_server.admin_port) + conn.request('GET', '/') + resp = conn.getresponse() + self.assertEqual(300, resp.status) + + # Verify Public + with appserver.AppServer(paste_conf, appserver.MAIN, host="::1"): + conn = environment.httplib.HTTPConnection( + '::1', CONF.eventlet_server.public_port) + conn.request('GET', '/') + resp = conn.getresponse() + self.assertEqual(300, resp.status) diff --git a/keystone-moon/keystone/tests/unit/test_kvs.py b/keystone-moon/keystone/tests/unit/test_kvs.py new file mode 100644 index 00000000..4d80ea33 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_kvs.py @@ -0,0 +1,581 @@ +# Copyright 2013 Metacloud, Inc. +# +# 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 time +import uuid + +from dogpile.cache import api +from dogpile.cache import proxy +from dogpile.cache import util +import mock +import six +from testtools import matchers + +from keystone.common.kvs.backends import inmemdb +from keystone.common.kvs.backends import memcached +from keystone.common.kvs import core +from keystone import exception +from keystone.tests import unit as tests + +NO_VALUE = api.NO_VALUE + + +class MutexFixture(object): + def __init__(self, storage_dict, key, timeout): + self.database = storage_dict + self.key = '_lock' + key + + def acquire(self, wait=True): + while True: + try: + self.database[self.key] = 1 + return True + except KeyError: + return False + + def release(self): + self.database.pop(self.key, None) + + +class KVSBackendFixture(inmemdb.MemoryBackend): + def __init__(self, arguments): + class InmemTestDB(dict): + def __setitem__(self, key, value): + if key in self: + raise KeyError('Key %s already exists' % key) + super(InmemTestDB, self).__setitem__(key, value) + + self._db = InmemTestDB() + self.lock_timeout = arguments.pop('lock_timeout', 5) + self.test_arg = arguments.pop('test_arg', None) + + def get_mutex(self, key): + return MutexFixture(self._db, key, self.lock_timeout) + + @classmethod + def key_mangler(cls, key): + return 'KVSBackend_' + key + + +class KVSBackendForcedKeyMangleFixture(KVSBackendFixture): + use_backend_key_mangler = True + + @classmethod + def key_mangler(cls, key): + return 'KVSBackendForcedKeyMangle_' + key + + +class RegionProxyFixture(proxy.ProxyBackend): + """A test dogpile.cache proxy that does nothing.""" + + +class RegionProxy2Fixture(proxy.ProxyBackend): + """A test dogpile.cache proxy that does nothing.""" + + +class TestMemcacheDriver(api.CacheBackend): + """A test dogpile.cache backend that conforms to the mixin-mechanism for + overriding set and set_multi methods on dogpile memcached drivers. + """ + class test_client(object): + # FIXME(morganfainberg): Convert this test client over to using mock + # and/or mock.MagicMock as appropriate + + def __init__(self): + self.__name__ = 'TestingMemcacheDriverClientObject' + self.set_arguments_passed = None + self.keys_values = {} + self.lock_set_time = None + self.lock_expiry = None + + def set(self, key, value, **set_arguments): + self.keys_values.clear() + self.keys_values[key] = value + self.set_arguments_passed = set_arguments + + def set_multi(self, mapping, **set_arguments): + self.keys_values.clear() + self.keys_values = mapping + self.set_arguments_passed = set_arguments + + def add(self, key, value, expiry_time): + # NOTE(morganfainberg): `add` is used in this case for the + # memcache lock testing. If further testing is required around the + # actual memcache `add` interface, this method should be + # expanded to work more like the actual memcache `add` function + if self.lock_expiry is not None and self.lock_set_time is not None: + if time.time() - self.lock_set_time < self.lock_expiry: + return False + self.lock_expiry = expiry_time + self.lock_set_time = time.time() + return True + + def delete(self, key): + # NOTE(morganfainberg): `delete` is used in this case for the + # memcache lock testing. If further testing is required around the + # actual memcache `delete` interface, this method should be + # expanded to work more like the actual memcache `delete` function. + self.lock_expiry = None + self.lock_set_time = None + return True + + def __init__(self, arguments): + self.client = self.test_client() + self.set_arguments = {} + # NOTE(morganfainberg): This is the same logic as the dogpile backend + # since we need to mirror that functionality for the `set_argument` + # values to appear on the actual backend. + if 'memcached_expire_time' in arguments: + self.set_arguments['time'] = arguments['memcached_expire_time'] + + def set(self, key, value): + self.client.set(key, value, **self.set_arguments) + + def set_multi(self, mapping): + self.client.set_multi(mapping, **self.set_arguments) + + +class KVSTest(tests.TestCase): + def setUp(self): + super(KVSTest, self).setUp() + self.key_foo = 'foo_' + uuid.uuid4().hex + self.value_foo = uuid.uuid4().hex + self.key_bar = 'bar_' + uuid.uuid4().hex + self.value_bar = {'complex_data_structure': uuid.uuid4().hex} + self.addCleanup(memcached.VALID_DOGPILE_BACKENDS.pop, + 'TestDriver', + None) + memcached.VALID_DOGPILE_BACKENDS['TestDriver'] = TestMemcacheDriver + + def _get_kvs_region(self, name=None): + if name is None: + name = uuid.uuid4().hex + return core.get_key_value_store(name) + + def test_kvs_basic_configuration(self): + # Test that the most basic configuration options pass through to the + # backend. + region_one = uuid.uuid4().hex + region_two = uuid.uuid4().hex + test_arg = 100 + kvs = self._get_kvs_region(region_one) + kvs.configure('openstack.kvs.Memory') + + self.assertIsInstance(kvs._region.backend, inmemdb.MemoryBackend) + self.assertEqual(region_one, kvs._region.name) + + kvs = self._get_kvs_region(region_two) + kvs.configure('openstack.kvs.KVSBackendFixture', + test_arg=test_arg) + + self.assertEqual(region_two, kvs._region.name) + self.assertEqual(test_arg, kvs._region.backend.test_arg) + + def test_kvs_proxy_configuration(self): + # Test that proxies are applied correctly and in the correct (reverse) + # order to the kvs region. + kvs = self._get_kvs_region() + kvs.configure( + 'openstack.kvs.Memory', + proxy_list=['keystone.tests.unit.test_kvs.RegionProxyFixture', + 'keystone.tests.unit.test_kvs.RegionProxy2Fixture']) + + self.assertIsInstance(kvs._region.backend, RegionProxyFixture) + self.assertIsInstance(kvs._region.backend.proxied, RegionProxy2Fixture) + self.assertIsInstance(kvs._region.backend.proxied.proxied, + inmemdb.MemoryBackend) + + def test_kvs_key_mangler_fallthrough_default(self): + # Test to make sure we default to the standard dogpile sha1 hashing + # key_mangler + kvs = self._get_kvs_region() + kvs.configure('openstack.kvs.Memory') + + self.assertIs(kvs._region.key_mangler, util.sha1_mangle_key) + # The backend should also have the keymangler set the same as the + # region now. + self.assertIs(kvs._region.backend.key_mangler, util.sha1_mangle_key) + + def test_kvs_key_mangler_configuration_backend(self): + kvs = self._get_kvs_region() + kvs.configure('openstack.kvs.KVSBackendFixture') + expected = KVSBackendFixture.key_mangler(self.key_foo) + self.assertEqual(expected, kvs._region.key_mangler(self.key_foo)) + + def test_kvs_key_mangler_configuration_forced_backend(self): + kvs = self._get_kvs_region() + kvs.configure('openstack.kvs.KVSBackendForcedKeyMangleFixture', + key_mangler=util.sha1_mangle_key) + expected = KVSBackendForcedKeyMangleFixture.key_mangler(self.key_foo) + self.assertEqual(expected, kvs._region.key_mangler(self.key_foo)) + + def test_kvs_key_mangler_configuration_disabled(self): + # Test that no key_mangler is set if enable_key_mangler is false + self.config_fixture.config(group='kvs', enable_key_mangler=False) + kvs = self._get_kvs_region() + kvs.configure('openstack.kvs.Memory') + + self.assertIsNone(kvs._region.key_mangler) + self.assertIsNone(kvs._region.backend.key_mangler) + + def test_kvs_key_mangler_set_on_backend(self): + def test_key_mangler(key): + return key + + kvs = self._get_kvs_region() + kvs.configure('openstack.kvs.Memory') + self.assertIs(kvs._region.backend.key_mangler, util.sha1_mangle_key) + kvs._set_key_mangler(test_key_mangler) + self.assertIs(kvs._region.backend.key_mangler, test_key_mangler) + + def test_kvs_basic_get_set_delete(self): + # Test the basic get/set/delete actions on the KVS region + kvs = self._get_kvs_region() + kvs.configure('openstack.kvs.Memory') + + # Not found should be raised if the key doesn't exist + self.assertRaises(exception.NotFound, kvs.get, key=self.key_bar) + kvs.set(self.key_bar, self.value_bar) + returned_value = kvs.get(self.key_bar) + # The returned value should be the same value as the value in .set + self.assertEqual(self.value_bar, returned_value) + # The value should not be the exact object used in .set + self.assertIsNot(returned_value, self.value_bar) + kvs.delete(self.key_bar) + # Second delete should raise NotFound + self.assertRaises(exception.NotFound, kvs.delete, key=self.key_bar) + + def _kvs_multi_get_set_delete(self, kvs): + keys = [self.key_foo, self.key_bar] + expected = [self.value_foo, self.value_bar] + + kvs.set_multi({self.key_foo: self.value_foo, + self.key_bar: self.value_bar}) + # Returned value from get_multi should be a list of the values of the + # keys + self.assertEqual(expected, kvs.get_multi(keys)) + # Delete both keys + kvs.delete_multi(keys) + # make sure that NotFound is properly raised when trying to get the now + # deleted keys + self.assertRaises(exception.NotFound, kvs.get_multi, keys=keys) + self.assertRaises(exception.NotFound, kvs.get, key=self.key_foo) + self.assertRaises(exception.NotFound, kvs.get, key=self.key_bar) + # Make sure get_multi raises NotFound if one of the keys isn't found + kvs.set(self.key_foo, self.value_foo) + self.assertRaises(exception.NotFound, kvs.get_multi, keys=keys) + + def test_kvs_multi_get_set_delete(self): + kvs = self._get_kvs_region() + kvs.configure('openstack.kvs.Memory') + + self._kvs_multi_get_set_delete(kvs) + + def test_kvs_locking_context_handler(self): + # Make sure we're creating the correct key/value pairs for the backend + # distributed locking mutex. + self.config_fixture.config(group='kvs', enable_key_mangler=False) + kvs = self._get_kvs_region() + kvs.configure('openstack.kvs.KVSBackendFixture') + + lock_key = '_lock' + self.key_foo + self.assertNotIn(lock_key, kvs._region.backend._db) + with core.KeyValueStoreLock(kvs._mutex(self.key_foo), self.key_foo): + self.assertIn(lock_key, kvs._region.backend._db) + self.assertIs(kvs._region.backend._db[lock_key], 1) + + self.assertNotIn(lock_key, kvs._region.backend._db) + + def test_kvs_locking_context_handler_locking_disabled(self): + # Make sure no creation of key/value pairs for the backend + # distributed locking mutex occurs if locking is disabled. + self.config_fixture.config(group='kvs', enable_key_mangler=False) + kvs = self._get_kvs_region() + kvs.configure('openstack.kvs.KVSBackendFixture', locking=False) + lock_key = '_lock' + self.key_foo + self.assertNotIn(lock_key, kvs._region.backend._db) + with core.KeyValueStoreLock(kvs._mutex(self.key_foo), self.key_foo, + False): + self.assertNotIn(lock_key, kvs._region.backend._db) + + self.assertNotIn(lock_key, kvs._region.backend._db) + + def test_kvs_with_lock_action_context_manager_timeout(self): + kvs = self._get_kvs_region() + lock_timeout = 5 + kvs.configure('openstack.kvs.Memory', lock_timeout=lock_timeout) + + def do_with_lock_action_timeout(kvs_region, key, offset): + with kvs_region.get_lock(key) as lock_in_use: + self.assertTrue(lock_in_use.active) + # Subtract the offset from the acquire_time. If this puts the + # acquire_time difference from time.time() at >= lock_timeout + # this should raise a LockTimeout exception. This is because + # there is a built-in 1-second overlap where the context + # manager thinks the lock is expired but the lock is still + # active. This is to help mitigate race conditions on the + # time-check itself. + lock_in_use.acquire_time -= offset + with kvs_region._action_with_lock(key, lock_in_use): + pass + + # This should succeed, we are not timed-out here. + do_with_lock_action_timeout(kvs, key=uuid.uuid4().hex, offset=2) + # Try it now with an offset equal to the lock_timeout + self.assertRaises(core.LockTimeout, + do_with_lock_action_timeout, + kvs_region=kvs, + key=uuid.uuid4().hex, + offset=lock_timeout) + # Final test with offset significantly greater than the lock_timeout + self.assertRaises(core.LockTimeout, + do_with_lock_action_timeout, + kvs_region=kvs, + key=uuid.uuid4().hex, + offset=100) + + def test_kvs_with_lock_action_mismatched_keys(self): + kvs = self._get_kvs_region() + kvs.configure('openstack.kvs.Memory') + + def do_with_lock_action(kvs_region, lock_key, target_key): + with kvs_region.get_lock(lock_key) as lock_in_use: + self.assertTrue(lock_in_use.active) + with kvs_region._action_with_lock(target_key, lock_in_use): + pass + + # Ensure we raise a ValueError if the lock key mismatches from the + # target key. + self.assertRaises(ValueError, + do_with_lock_action, + kvs_region=kvs, + lock_key=self.key_foo, + target_key=self.key_bar) + + def test_kvs_with_lock_action_context_manager(self): + # Make sure we're creating the correct key/value pairs for the backend + # distributed locking mutex. + self.config_fixture.config(group='kvs', enable_key_mangler=False) + kvs = self._get_kvs_region() + kvs.configure('openstack.kvs.KVSBackendFixture') + + lock_key = '_lock' + self.key_foo + self.assertNotIn(lock_key, kvs._region.backend._db) + with kvs.get_lock(self.key_foo) as lock: + with kvs._action_with_lock(self.key_foo, lock): + self.assertTrue(lock.active) + self.assertIn(lock_key, kvs._region.backend._db) + self.assertIs(kvs._region.backend._db[lock_key], 1) + + self.assertNotIn(lock_key, kvs._region.backend._db) + + def test_kvs_with_lock_action_context_manager_no_lock(self): + # Make sure we're not locking unless an actual lock is passed into the + # context manager + self.config_fixture.config(group='kvs', enable_key_mangler=False) + kvs = self._get_kvs_region() + kvs.configure('openstack.kvs.KVSBackendFixture') + + lock_key = '_lock' + self.key_foo + lock = None + self.assertNotIn(lock_key, kvs._region.backend._db) + with kvs._action_with_lock(self.key_foo, lock): + self.assertNotIn(lock_key, kvs._region.backend._db) + + self.assertNotIn(lock_key, kvs._region.backend._db) + + def test_kvs_backend_registration_does_not_reregister_backends(self): + # SetUp registers the test backends. Running this again would raise an + # exception if re-registration of the backends occurred. + kvs = self._get_kvs_region() + kvs.configure('openstack.kvs.Memory') + core._register_backends() + + def test_kvs_memcached_manager_valid_dogpile_memcached_backend(self): + kvs = self._get_kvs_region() + kvs.configure('openstack.kvs.Memcached', + memcached_backend='TestDriver') + self.assertIsInstance(kvs._region.backend.driver, + TestMemcacheDriver) + + def test_kvs_memcached_manager_invalid_dogpile_memcached_backend(self): + # Invalid dogpile memcache backend should raise ValueError + kvs = self._get_kvs_region() + self.assertRaises(ValueError, + kvs.configure, + backing_store='openstack.kvs.Memcached', + memcached_backend=uuid.uuid4().hex) + + def test_kvs_memcache_manager_no_expiry_keys(self): + # Make sure the memcache backend recalculates the no-expiry keys + # correctly when a key-mangler is set on it. + + def new_mangler(key): + return '_mangled_key_' + key + + kvs = self._get_kvs_region() + no_expiry_keys = set(['test_key']) + kvs.configure('openstack.kvs.Memcached', + memcached_backend='TestDriver', + no_expiry_keys=no_expiry_keys) + calculated_keys = set([kvs._region.key_mangler(key) + for key in no_expiry_keys]) + self.assertIs(kvs._region.backend.key_mangler, util.sha1_mangle_key) + self.assertSetEqual(calculated_keys, + kvs._region.backend.no_expiry_hashed_keys) + self.assertSetEqual(no_expiry_keys, + kvs._region.backend.raw_no_expiry_keys) + calculated_keys = set([new_mangler(key) for key in no_expiry_keys]) + kvs._region.backend.key_mangler = new_mangler + self.assertSetEqual(calculated_keys, + kvs._region.backend.no_expiry_hashed_keys) + self.assertSetEqual(no_expiry_keys, + kvs._region.backend.raw_no_expiry_keys) + + def test_kvs_memcache_key_mangler_set_to_none(self): + kvs = self._get_kvs_region() + no_expiry_keys = set(['test_key']) + kvs.configure('openstack.kvs.Memcached', + memcached_backend='TestDriver', + no_expiry_keys=no_expiry_keys) + self.assertIs(kvs._region.backend.key_mangler, util.sha1_mangle_key) + kvs._region.backend.key_mangler = None + self.assertSetEqual(kvs._region.backend.raw_no_expiry_keys, + kvs._region.backend.no_expiry_hashed_keys) + self.assertIsNone(kvs._region.backend.key_mangler) + + def test_noncallable_key_mangler_set_on_driver_raises_type_error(self): + kvs = self._get_kvs_region() + kvs.configure('openstack.kvs.Memcached', + memcached_backend='TestDriver') + self.assertRaises(TypeError, + setattr, + kvs._region.backend, + 'key_mangler', + 'Non-Callable') + + def test_kvs_memcache_set_arguments_and_memcache_expires_ttl(self): + # Test the "set_arguments" (arguments passed on all set calls) logic + # and the no-expiry-key modifications of set_arguments for the explicit + # memcache TTL. + self.config_fixture.config(group='kvs', enable_key_mangler=False) + kvs = self._get_kvs_region() + memcache_expire_time = 86400 + + expected_set_args = {'time': memcache_expire_time} + expected_no_expiry_args = {} + + expected_foo_keys = [self.key_foo] + expected_bar_keys = [self.key_bar] + + mapping_foo = {self.key_foo: self.value_foo} + mapping_bar = {self.key_bar: self.value_bar} + + kvs.configure(backing_store='openstack.kvs.Memcached', + memcached_backend='TestDriver', + memcached_expire_time=memcache_expire_time, + some_other_arg=uuid.uuid4().hex, + no_expiry_keys=[self.key_bar]) + # Ensure the set_arguments are correct + self.assertDictEqual( + kvs._region.backend._get_set_arguments_driver_attr(), + expected_set_args) + + # Set a key that would have an expiry and verify the correct result + # occurred and that the correct set_arguments were passed. + kvs.set(self.key_foo, self.value_foo) + self.assertDictEqual( + kvs._region.backend.driver.client.set_arguments_passed, + expected_set_args) + self.assertEqual(expected_foo_keys, + kvs._region.backend.driver.client.keys_values.keys()) + self.assertEqual( + self.value_foo, + kvs._region.backend.driver.client.keys_values[self.key_foo][0]) + + # Set a key that would not have an expiry and verify the correct result + # occurred and that the correct set_arguments were passed. + kvs.set(self.key_bar, self.value_bar) + self.assertDictEqual( + kvs._region.backend.driver.client.set_arguments_passed, + expected_no_expiry_args) + self.assertEqual(expected_bar_keys, + kvs._region.backend.driver.client.keys_values.keys()) + self.assertEqual( + self.value_bar, + kvs._region.backend.driver.client.keys_values[self.key_bar][0]) + + # set_multi a dict that would have an expiry and verify the correct + # result occurred and that the correct set_arguments were passed. + kvs.set_multi(mapping_foo) + self.assertDictEqual( + kvs._region.backend.driver.client.set_arguments_passed, + expected_set_args) + self.assertEqual(expected_foo_keys, + kvs._region.backend.driver.client.keys_values.keys()) + self.assertEqual( + self.value_foo, + kvs._region.backend.driver.client.keys_values[self.key_foo][0]) + + # set_multi a dict that would not have an expiry and verify the correct + # result occurred and that the correct set_arguments were passed. + kvs.set_multi(mapping_bar) + self.assertDictEqual( + kvs._region.backend.driver.client.set_arguments_passed, + expected_no_expiry_args) + self.assertEqual(expected_bar_keys, + kvs._region.backend.driver.client.keys_values.keys()) + self.assertEqual( + self.value_bar, + kvs._region.backend.driver.client.keys_values[self.key_bar][0]) + + def test_memcached_lock_max_lock_attempts(self): + kvs = self._get_kvs_region() + max_lock_attempts = 1 + test_key = uuid.uuid4().hex + + kvs.configure(backing_store='openstack.kvs.Memcached', + memcached_backend='TestDriver', + max_lock_attempts=max_lock_attempts) + + self.assertEqual(max_lock_attempts, + kvs._region.backend.max_lock_attempts) + # Simple Lock success test + with kvs.get_lock(test_key) as lock: + kvs.set(test_key, 'testing', lock) + + def lock_within_a_lock(key): + with kvs.get_lock(key) as first_lock: + kvs.set(test_key, 'lock', first_lock) + with kvs.get_lock(key) as second_lock: + kvs.set(key, 'lock-within-a-lock', second_lock) + + self.assertRaises(exception.UnexpectedError, + lock_within_a_lock, + key=test_key) + + +class TestMemcachedBackend(tests.TestCase): + + @mock.patch('keystone.common.kvs.backends.memcached._', six.text_type) + def test_invalid_backend_fails_initialization(self): + raises_valueerror = matchers.Raises(matchers.MatchesException( + ValueError, r'.*FakeBackend.*')) + + options = { + 'url': 'needed to get to the focus of this test (the backend)', + 'memcached_backend': 'FakeBackend', + } + self.assertThat(lambda: memcached.MemcachedBackend(options), + raises_valueerror) diff --git a/keystone-moon/keystone/tests/unit/test_ldap_livetest.py b/keystone-moon/keystone/tests/unit/test_ldap_livetest.py new file mode 100644 index 00000000..5b449362 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_ldap_livetest.py @@ -0,0 +1,229 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 subprocess +import uuid + +import ldap +import ldap.modlist +from oslo_config import cfg + +from keystone import exception +from keystone.identity.backends import ldap as identity_ldap +from keystone.tests import unit as tests +from keystone.tests.unit import test_backend_ldap + + +CONF = cfg.CONF + + +def create_object(dn, attrs): + conn = ldap.initialize(CONF.ldap.url) + conn.simple_bind_s(CONF.ldap.user, CONF.ldap.password) + ldif = ldap.modlist.addModlist(attrs) + conn.add_s(dn, ldif) + conn.unbind_s() + + +class LiveLDAPIdentity(test_backend_ldap.LDAPIdentity): + + def setUp(self): + self._ldap_skip_live() + super(LiveLDAPIdentity, self).setUp() + + def _ldap_skip_live(self): + self.skip_if_env_not_set('ENABLE_LDAP_LIVE_TEST') + + def clear_database(self): + devnull = open('/dev/null', 'w') + subprocess.call(['ldapdelete', + '-x', + '-D', CONF.ldap.user, + '-H', CONF.ldap.url, + '-w', CONF.ldap.password, + '-r', CONF.ldap.suffix], + stderr=devnull) + + if CONF.ldap.suffix.startswith('ou='): + tree_dn_attrs = {'objectclass': 'organizationalUnit', + 'ou': 'openstack'} + else: + tree_dn_attrs = {'objectclass': ['dcObject', 'organizationalUnit'], + 'dc': 'openstack', + 'ou': 'openstack'} + create_object(CONF.ldap.suffix, tree_dn_attrs) + create_object(CONF.ldap.user_tree_dn, + {'objectclass': 'organizationalUnit', + 'ou': 'Users'}) + create_object(CONF.ldap.role_tree_dn, + {'objectclass': 'organizationalUnit', + 'ou': 'Roles'}) + create_object(CONF.ldap.project_tree_dn, + {'objectclass': 'organizationalUnit', + 'ou': 'Projects'}) + create_object(CONF.ldap.group_tree_dn, + {'objectclass': 'organizationalUnit', + 'ou': 'UserGroups'}) + + def config_files(self): + config_files = super(LiveLDAPIdentity, self).config_files() + config_files.append(tests.dirs.tests_conf('backend_liveldap.conf')) + return config_files + + def config_overrides(self): + super(LiveLDAPIdentity, self).config_overrides() + self.config_fixture.config( + group='identity', + driver='keystone.identity.backends.ldap.Identity') + + def test_build_tree(self): + """Regression test for building the tree names + """ + # logic is different from the fake backend. + user_api = identity_ldap.UserApi(CONF) + self.assertTrue(user_api) + self.assertEqual(user_api.tree_dn, CONF.ldap.user_tree_dn) + + def tearDown(self): + tests.TestCase.tearDown(self) + + def test_ldap_dereferencing(self): + alt_users_ldif = {'objectclass': ['top', 'organizationalUnit'], + 'ou': 'alt_users'} + alt_fake_user_ldif = {'objectclass': ['person', 'inetOrgPerson'], + 'cn': 'alt_fake1', + 'sn': 'alt_fake1'} + aliased_users_ldif = {'objectclass': ['alias', 'extensibleObject'], + 'aliasedobjectname': "ou=alt_users,%s" % + CONF.ldap.suffix} + create_object("ou=alt_users,%s" % CONF.ldap.suffix, alt_users_ldif) + create_object("%s=alt_fake1,ou=alt_users,%s" % + (CONF.ldap.user_id_attribute, CONF.ldap.suffix), + alt_fake_user_ldif) + create_object("ou=alt_users,%s" % CONF.ldap.user_tree_dn, + aliased_users_ldif) + + self.config_fixture.config(group='ldap', + query_scope='sub', + alias_dereferencing='never') + self.identity_api = identity_ldap.Identity() + self.assertRaises(exception.UserNotFound, + self.identity_api.get_user, + 'alt_fake1') + + self.config_fixture.config(group='ldap', + alias_dereferencing='searching') + self.identity_api = identity_ldap.Identity() + user_ref = self.identity_api.get_user('alt_fake1') + self.assertEqual('alt_fake1', user_ref['id']) + + self.config_fixture.config(group='ldap', alias_dereferencing='always') + self.identity_api = identity_ldap.Identity() + user_ref = self.identity_api.get_user('alt_fake1') + self.assertEqual('alt_fake1', user_ref['id']) + + # FakeLDAP does not correctly process filters, so this test can only be + # run against a live LDAP server + def test_list_groups_for_user_filtered(self): + domain = self._get_domain_fixture() + test_groups = [] + test_users = [] + GROUP_COUNT = 3 + USER_COUNT = 2 + + for x in range(0, USER_COUNT): + new_user = {'name': uuid.uuid4().hex, 'password': uuid.uuid4().hex, + 'enabled': True, 'domain_id': domain['id']} + new_user = self.identity_api.create_user(new_user) + test_users.append(new_user) + positive_user = test_users[0] + negative_user = test_users[1] + + for x in range(0, USER_COUNT): + group_refs = self.identity_api.list_groups_for_user( + test_users[x]['id']) + self.assertEqual(0, len(group_refs)) + + for x in range(0, GROUP_COUNT): + new_group = {'domain_id': domain['id'], + 'name': uuid.uuid4().hex} + new_group = self.identity_api.create_group(new_group) + test_groups.append(new_group) + + group_refs = self.identity_api.list_groups_for_user( + positive_user['id']) + self.assertEqual(x, len(group_refs)) + + self.identity_api.add_user_to_group( + positive_user['id'], + new_group['id']) + group_refs = self.identity_api.list_groups_for_user( + positive_user['id']) + self.assertEqual(x + 1, len(group_refs)) + + group_refs = self.identity_api.list_groups_for_user( + negative_user['id']) + self.assertEqual(0, len(group_refs)) + + self.config_fixture.config(group='ldap', group_filter='(dn=xx)') + self.reload_backends(CONF.identity.default_domain_id) + group_refs = self.identity_api.list_groups_for_user( + positive_user['id']) + self.assertEqual(0, len(group_refs)) + group_refs = self.identity_api.list_groups_for_user( + negative_user['id']) + self.assertEqual(0, len(group_refs)) + + self.config_fixture.config(group='ldap', + group_filter='(objectclass=*)') + self.reload_backends(CONF.identity.default_domain_id) + group_refs = self.identity_api.list_groups_for_user( + positive_user['id']) + self.assertEqual(GROUP_COUNT, len(group_refs)) + group_refs = self.identity_api.list_groups_for_user( + negative_user['id']) + self.assertEqual(0, len(group_refs)) + + def test_user_enable_attribute_mask(self): + self.config_fixture.config( + group='ldap', + user_enabled_emulation=False, + user_enabled_attribute='employeeType') + super(LiveLDAPIdentity, self).test_user_enable_attribute_mask() + + def test_create_project_case_sensitivity(self): + # The attribute used for the live LDAP tests is case insensitive. + + def call_super(): + (super(LiveLDAPIdentity, self). + test_create_project_case_sensitivity()) + + self.assertRaises(exception.Conflict, call_super) + + def test_create_user_case_sensitivity(self): + # The attribute used for the live LDAP tests is case insensitive. + + def call_super(): + super(LiveLDAPIdentity, self).test_create_user_case_sensitivity() + + self.assertRaises(exception.Conflict, call_super) + + def test_project_update_missing_attrs_with_a_falsey_value(self): + # The description attribute doesn't allow an empty value. + + def call_super(): + (super(LiveLDAPIdentity, self). + test_project_update_missing_attrs_with_a_falsey_value()) + + self.assertRaises(ldap.INVALID_SYNTAX, call_super) diff --git a/keystone-moon/keystone/tests/unit/test_ldap_pool_livetest.py b/keystone-moon/keystone/tests/unit/test_ldap_pool_livetest.py new file mode 100644 index 00000000..02fa8145 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_ldap_pool_livetest.py @@ -0,0 +1,208 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 + +import ldappool +from oslo_config import cfg + +from keystone.common.ldap import core as ldap_core +from keystone.identity.backends import ldap +from keystone.tests import unit as tests +from keystone.tests.unit import fakeldap +from keystone.tests.unit import test_backend_ldap_pool +from keystone.tests.unit import test_ldap_livetest + + +CONF = cfg.CONF + + +class LiveLDAPPoolIdentity(test_backend_ldap_pool.LdapPoolCommonTestMixin, + test_ldap_livetest.LiveLDAPIdentity): + """Executes existing LDAP live test with pooled LDAP handler to make + sure it works without any error. + + Also executes common pool specific tests via Mixin class. + """ + + def setUp(self): + super(LiveLDAPPoolIdentity, self).setUp() + self.addCleanup(self.cleanup_pools) + # storing to local variable to avoid long references + self.conn_pools = ldap_core.PooledLDAPHandler.connection_pools + + def config_files(self): + config_files = super(LiveLDAPPoolIdentity, self).config_files() + config_files.append(tests.dirs. + tests_conf('backend_pool_liveldap.conf')) + return config_files + + def config_overrides(self): + super(LiveLDAPPoolIdentity, self).config_overrides() + self.config_fixture.config( + group='identity', + driver='keystone.identity.backends.ldap.Identity') + + def test_assert_connector_used_not_fake_ldap_pool(self): + handler = ldap_core._get_connection(CONF.ldap.url, use_pool=True) + self.assertNotEqual(type(handler.Connector), + type(fakeldap.FakeLdapPool)) + self.assertEqual(type(ldappool.StateConnector), + type(handler.Connector)) + + def test_async_search_and_result3(self): + self.config_fixture.config(group='ldap', page_size=1) + self.test_user_enable_attribute_mask() + + def test_pool_size_expands_correctly(self): + + who = CONF.ldap.user + cred = CONF.ldap.password + # get related connection manager instance + ldappool_cm = self.conn_pools[CONF.ldap.url] + + def _get_conn(): + return ldappool_cm.connection(who, cred) + + with _get_conn() as c1: # 1 + self.assertEqual(1, len(ldappool_cm)) + self.assertTrue(c1.connected, True) + self.assertTrue(c1.active, True) + with _get_conn() as c2: # conn2 + self.assertEqual(2, len(ldappool_cm)) + self.assertTrue(c2.connected) + self.assertTrue(c2.active) + + self.assertEqual(2, len(ldappool_cm)) + # c2 went out of context, its connected but not active + self.assertTrue(c2.connected) + self.assertFalse(c2.active) + with _get_conn() as c3: # conn3 + self.assertEqual(2, len(ldappool_cm)) + self.assertTrue(c3.connected) + self.assertTrue(c3.active) + self.assertTrue(c3 is c2) # same connection is reused + self.assertTrue(c2.active) + with _get_conn() as c4: # conn4 + self.assertEqual(3, len(ldappool_cm)) + self.assertTrue(c4.connected) + self.assertTrue(c4.active) + + def test_password_change_with_auth_pool_disabled(self): + self.config_fixture.config(group='ldap', use_auth_pool=False) + old_password = self.user_sna['password'] + + self.test_password_change_with_pool() + + self.assertRaises(AssertionError, + self.identity_api.authenticate, + context={}, + user_id=self.user_sna['id'], + password=old_password) + + def _create_user_and_authenticate(self, password): + user_dict = { + 'domain_id': CONF.identity.default_domain_id, + 'name': uuid.uuid4().hex, + 'password': password} + user = self.identity_api.create_user(user_dict) + + self.identity_api.authenticate( + context={}, + user_id=user['id'], + password=password) + + return self.identity_api.get_user(user['id']) + + def _get_auth_conn_pool_cm(self): + pool_url = ldap_core.PooledLDAPHandler.auth_pool_prefix + CONF.ldap.url + return self.conn_pools[pool_url] + + def _do_password_change_for_one_user(self, password, new_password): + self.config_fixture.config(group='ldap', use_auth_pool=True) + self.cleanup_pools() + self.load_backends() + + user1 = self._create_user_and_authenticate(password) + auth_cm = self._get_auth_conn_pool_cm() + self.assertEqual(1, len(auth_cm)) + user2 = self._create_user_and_authenticate(password) + self.assertEqual(1, len(auth_cm)) + user3 = self._create_user_and_authenticate(password) + self.assertEqual(1, len(auth_cm)) + user4 = self._create_user_and_authenticate(password) + self.assertEqual(1, len(auth_cm)) + user5 = self._create_user_and_authenticate(password) + self.assertEqual(1, len(auth_cm)) + + # connection pool size remains 1 even for different user ldap bind + # as there is only one active connection at a time + + user_api = ldap.UserApi(CONF) + u1_dn = user_api._id_to_dn_string(user1['id']) + u2_dn = user_api._id_to_dn_string(user2['id']) + u3_dn = user_api._id_to_dn_string(user3['id']) + u4_dn = user_api._id_to_dn_string(user4['id']) + u5_dn = user_api._id_to_dn_string(user5['id']) + + # now create multiple active connections for end user auth case which + # will force to keep them in pool. After that, modify one of user + # password. Need to make sure that user connection is in middle + # of pool list. + auth_cm = self._get_auth_conn_pool_cm() + with auth_cm.connection(u1_dn, password) as _: + with auth_cm.connection(u2_dn, password) as _: + with auth_cm.connection(u3_dn, password) as _: + with auth_cm.connection(u4_dn, password) as _: + with auth_cm.connection(u5_dn, password) as _: + self.assertEqual(5, len(auth_cm)) + _.unbind_s() + + user3['password'] = new_password + self.identity_api.update_user(user3['id'], user3) + + return user3 + + def test_password_change_with_auth_pool_enabled_long_lifetime(self): + self.config_fixture.config(group='ldap', + auth_pool_connection_lifetime=600) + old_password = 'my_password' + new_password = 'new_password' + user = self._do_password_change_for_one_user(old_password, + new_password) + user.pop('password') + + # with long connection lifetime auth_pool can bind to old password + # successfully which is not desired if password change is frequent + # use case in a deployment. + # This can happen in multiple concurrent connections case only. + user_ref = self.identity_api.authenticate( + context={}, user_id=user['id'], password=old_password) + + self.assertDictEqual(user_ref, user) + + def test_password_change_with_auth_pool_enabled_no_lifetime(self): + self.config_fixture.config(group='ldap', + auth_pool_connection_lifetime=0) + + old_password = 'my_password' + new_password = 'new_password' + user = self._do_password_change_for_one_user(old_password, + new_password) + # now as connection lifetime is zero, so authentication + # with old password will always fail. + self.assertRaises(AssertionError, + self.identity_api.authenticate, + context={}, user_id=user['id'], + password=old_password) diff --git a/keystone-moon/keystone/tests/unit/test_ldap_tls_livetest.py b/keystone-moon/keystone/tests/unit/test_ldap_tls_livetest.py new file mode 100644 index 00000000..d79c2bad --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_ldap_tls_livetest.py @@ -0,0 +1,122 @@ +# Copyright 2013 OpenStack Foundation +# Copyright 2013 IBM Corp. +# +# 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 ldap +import ldap.modlist +from oslo_config import cfg + +from keystone import exception +from keystone import identity +from keystone.tests import unit as tests +from keystone.tests.unit import test_ldap_livetest + + +CONF = cfg.CONF + + +def create_object(dn, attrs): + conn = ldap.initialize(CONF.ldap.url) + conn.simple_bind_s(CONF.ldap.user, CONF.ldap.password) + ldif = ldap.modlist.addModlist(attrs) + conn.add_s(dn, ldif) + conn.unbind_s() + + +class LiveTLSLDAPIdentity(test_ldap_livetest.LiveLDAPIdentity): + + def _ldap_skip_live(self): + self.skip_if_env_not_set('ENABLE_TLS_LDAP_LIVE_TEST') + + def config_files(self): + config_files = super(LiveTLSLDAPIdentity, self).config_files() + config_files.append(tests.dirs.tests_conf('backend_tls_liveldap.conf')) + return config_files + + def config_overrides(self): + super(LiveTLSLDAPIdentity, self).config_overrides() + self.config_fixture.config( + group='identity', + driver='keystone.identity.backends.ldap.Identity') + + def test_tls_certfile_demand_option(self): + self.config_fixture.config(group='ldap', + use_tls=True, + tls_cacertdir=None, + tls_req_cert='demand') + self.identity_api = identity.backends.ldap.Identity() + + user = {'name': 'fake1', + 'password': 'fakepass1', + 'tenants': ['bar']} + user = self.identity_api.create_user('user') + user_ref = self.identity_api.get_user(user['id']) + self.assertEqual(user['id'], user_ref['id']) + + user['password'] = 'fakepass2' + self.identity_api.update_user(user['id'], user) + + self.identity_api.delete_user(user['id']) + self.assertRaises(exception.UserNotFound, self.identity_api.get_user, + user['id']) + + def test_tls_certdir_demand_option(self): + self.config_fixture.config(group='ldap', + use_tls=True, + tls_cacertdir=None, + tls_req_cert='demand') + self.identity_api = identity.backends.ldap.Identity() + + user = {'id': 'fake1', + 'name': 'fake1', + 'password': 'fakepass1', + 'tenants': ['bar']} + self.identity_api.create_user('fake1', user) + user_ref = self.identity_api.get_user('fake1') + self.assertEqual('fake1', user_ref['id']) + + user['password'] = 'fakepass2' + self.identity_api.update_user('fake1', user) + + self.identity_api.delete_user('fake1') + self.assertRaises(exception.UserNotFound, self.identity_api.get_user, + 'fake1') + + def test_tls_bad_certfile(self): + self.config_fixture.config( + group='ldap', + use_tls=True, + tls_req_cert='demand', + tls_cacertfile='/etc/keystone/ssl/certs/mythicalcert.pem', + tls_cacertdir=None) + self.identity_api = identity.backends.ldap.Identity() + + user = {'name': 'fake1', + 'password': 'fakepass1', + 'tenants': ['bar']} + self.assertRaises(IOError, self.identity_api.create_user, user) + + def test_tls_bad_certdir(self): + self.config_fixture.config( + group='ldap', + use_tls=True, + tls_cacertfile=None, + tls_req_cert='demand', + tls_cacertdir='/etc/keystone/ssl/mythicalcertdir') + self.identity_api = identity.backends.ldap.Identity() + + user = {'name': 'fake1', + 'password': 'fakepass1', + 'tenants': ['bar']} + self.assertRaises(IOError, self.identity_api.create_user, user) diff --git a/keystone-moon/keystone/tests/unit/test_middleware.py b/keystone-moon/keystone/tests/unit/test_middleware.py new file mode 100644 index 00000000..3a26dd24 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_middleware.py @@ -0,0 +1,119 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 oslo_config import cfg +import webob + +from keystone import middleware +from keystone.tests import unit as tests + + +CONF = cfg.CONF + + +def make_request(**kwargs): + accept = kwargs.pop('accept', None) + method = kwargs.pop('method', 'GET') + body = kwargs.pop('body', None) + req = webob.Request.blank('/', **kwargs) + req.method = method + if body is not None: + req.body = body + if accept is not None: + req.accept = accept + return req + + +def make_response(**kwargs): + body = kwargs.pop('body', None) + return webob.Response(body) + + +class TokenAuthMiddlewareTest(tests.TestCase): + def test_request(self): + req = make_request() + req.headers[middleware.AUTH_TOKEN_HEADER] = 'MAGIC' + middleware.TokenAuthMiddleware(None).process_request(req) + context = req.environ[middleware.CONTEXT_ENV] + self.assertEqual('MAGIC', context['token_id']) + + +class AdminTokenAuthMiddlewareTest(tests.TestCase): + def test_request_admin(self): + req = make_request() + req.headers[middleware.AUTH_TOKEN_HEADER] = CONF.admin_token + middleware.AdminTokenAuthMiddleware(None).process_request(req) + context = req.environ[middleware.CONTEXT_ENV] + self.assertTrue(context['is_admin']) + + def test_request_non_admin(self): + req = make_request() + req.headers[middleware.AUTH_TOKEN_HEADER] = 'NOT-ADMIN' + middleware.AdminTokenAuthMiddleware(None).process_request(req) + context = req.environ[middleware.CONTEXT_ENV] + self.assertFalse(context['is_admin']) + + +class PostParamsMiddlewareTest(tests.TestCase): + def test_request_with_params(self): + req = make_request(body="arg1=one", method='POST') + middleware.PostParamsMiddleware(None).process_request(req) + params = req.environ[middleware.PARAMS_ENV] + self.assertEqual({"arg1": "one"}, params) + + +class JsonBodyMiddlewareTest(tests.TestCase): + def test_request_with_params(self): + req = make_request(body='{"arg1": "one", "arg2": ["a"]}', + content_type='application/json', + method='POST') + middleware.JsonBodyMiddleware(None).process_request(req) + params = req.environ[middleware.PARAMS_ENV] + self.assertEqual({"arg1": "one", "arg2": ["a"]}, params) + + def test_malformed_json(self): + req = make_request(body='{"arg1": "on', + content_type='application/json', + method='POST') + resp = middleware.JsonBodyMiddleware(None).process_request(req) + self.assertEqual(400, resp.status_int) + + def test_not_dict_body(self): + req = make_request(body='42', + content_type='application/json', + method='POST') + resp = middleware.JsonBodyMiddleware(None).process_request(req) + self.assertEqual(400, resp.status_int) + self.assertTrue('valid JSON object' in resp.json['error']['message']) + + def test_no_content_type(self): + req = make_request(body='{"arg1": "one", "arg2": ["a"]}', + method='POST') + middleware.JsonBodyMiddleware(None).process_request(req) + params = req.environ[middleware.PARAMS_ENV] + self.assertEqual({"arg1": "one", "arg2": ["a"]}, params) + + def test_unrecognized_content_type(self): + req = make_request(body='{"arg1": "one", "arg2": ["a"]}', + content_type='text/plain', + method='POST') + resp = middleware.JsonBodyMiddleware(None).process_request(req) + self.assertEqual(400, resp.status_int) + + def test_unrecognized_content_type_without_body(self): + req = make_request(content_type='text/plain', + method='GET') + middleware.JsonBodyMiddleware(None).process_request(req) + params = req.environ.get(middleware.PARAMS_ENV, {}) + self.assertEqual({}, params) diff --git a/keystone-moon/keystone/tests/unit/test_no_admin_token_auth.py b/keystone-moon/keystone/tests/unit/test_no_admin_token_auth.py new file mode 100644 index 00000000..9f67fbd7 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_no_admin_token_auth.py @@ -0,0 +1,59 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 os + +import webtest + +from keystone.tests import unit as tests + + +class TestNoAdminTokenAuth(tests.TestCase): + def setUp(self): + super(TestNoAdminTokenAuth, self).setUp() + self.load_backends() + + self._generate_paste_config() + + self.admin_app = webtest.TestApp( + self.loadapp(tests.dirs.tmp('no_admin_token_auth'), name='admin'), + extra_environ=dict(REMOTE_ADDR='127.0.0.1')) + self.addCleanup(setattr, self, 'admin_app', None) + + def _generate_paste_config(self): + # Generate a file, based on keystone-paste.ini, that doesn't include + # admin_token_auth in the pipeline + + with open(tests.dirs.etc('keystone-paste.ini'), 'r') as f: + contents = f.read() + + new_contents = contents.replace(' admin_token_auth ', ' ') + + filename = tests.dirs.tmp('no_admin_token_auth-paste.ini') + with open(filename, 'w') as f: + f.write(new_contents) + self.addCleanup(os.remove, filename) + + def test_request_no_admin_token_auth(self): + # This test verifies that if the admin_token_auth middleware isn't + # in the paste pipeline that users can still make requests. + + # Note(blk-u): Picked /v2.0/tenants because it's an operation that + # requires is_admin in the context, any operation that requires + # is_admin would work for this test. + REQ_PATH = '/v2.0/tenants' + + # If the following does not raise, then the test is successful. + self.admin_app.get(REQ_PATH, headers={'X-Auth-Token': 'NotAdminToken'}, + status=401) diff --git a/keystone-moon/keystone/tests/unit/test_policy.py b/keystone-moon/keystone/tests/unit/test_policy.py new file mode 100644 index 00000000..2c0c3995 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_policy.py @@ -0,0 +1,228 @@ +# Copyright 2011 Piston Cloud Computing, Inc. +# All Rights Reserved. + +# 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 mock +from oslo_policy import policy as common_policy +import six +from six.moves.urllib import request as urlrequest +from testtools import matchers + +from keystone import exception +from keystone.policy.backends import rules +from keystone.tests import unit as tests +from keystone.tests.unit.ksfixtures import temporaryfile + + +class BasePolicyTestCase(tests.TestCase): + def setUp(self): + super(BasePolicyTestCase, self).setUp() + rules.reset() + self.addCleanup(rules.reset) + self.addCleanup(self.clear_cache_safely) + + def clear_cache_safely(self): + if rules._ENFORCER: + rules._ENFORCER.clear() + + +class PolicyFileTestCase(BasePolicyTestCase): + def setUp(self): + # self.tmpfilename should exist before setUp super is called + # this is to ensure it is available for the config_fixture in + # the config_overrides call. + self.tempfile = self.useFixture(temporaryfile.SecureTempFile()) + self.tmpfilename = self.tempfile.file_name + super(PolicyFileTestCase, self).setUp() + self.target = {} + + def config_overrides(self): + super(PolicyFileTestCase, self).config_overrides() + self.config_fixture.config(group='oslo_policy', + policy_file=self.tmpfilename) + + def test_modified_policy_reloads(self): + action = "example:test" + empty_credentials = {} + with open(self.tmpfilename, "w") as policyfile: + policyfile.write("""{"example:test": []}""") + rules.enforce(empty_credentials, action, self.target) + with open(self.tmpfilename, "w") as policyfile: + policyfile.write("""{"example:test": ["false:false"]}""") + rules._ENFORCER.clear() + self.assertRaises(exception.ForbiddenAction, rules.enforce, + empty_credentials, action, self.target) + + def test_invalid_policy_raises_error(self): + action = "example:test" + empty_credentials = {} + invalid_json = '{"example:test": [],}' + with open(self.tmpfilename, "w") as policyfile: + policyfile.write(invalid_json) + self.assertRaises(ValueError, rules.enforce, + empty_credentials, action, self.target) + + +class PolicyTestCase(BasePolicyTestCase): + def setUp(self): + super(PolicyTestCase, self).setUp() + # NOTE(vish): preload rules to circumvent reloading from file + rules.init() + self.rules = { + "true": [], + "example:allowed": [], + "example:denied": [["false:false"]], + "example:get_http": [["http:http://www.example.com"]], + "example:my_file": [["role:compute_admin"], + ["project_id:%(project_id)s"]], + "example:early_and_fail": [["false:false", "rule:true"]], + "example:early_or_success": [["rule:true"], ["false:false"]], + "example:lowercase_admin": [["role:admin"], ["role:sysadmin"]], + "example:uppercase_admin": [["role:ADMIN"], ["role:sysadmin"]], + } + + # NOTE(vish): then overload underlying policy engine + self._set_rules() + self.credentials = {} + self.target = {} + + def _set_rules(self): + these_rules = common_policy.Rules.from_dict(self.rules) + rules._ENFORCER.set_rules(these_rules) + + def test_enforce_nonexistent_action_throws(self): + action = "example:noexist" + self.assertRaises(exception.ForbiddenAction, rules.enforce, + self.credentials, action, self.target) + + def test_enforce_bad_action_throws(self): + action = "example:denied" + self.assertRaises(exception.ForbiddenAction, rules.enforce, + self.credentials, action, self.target) + + def test_enforce_good_action(self): + action = "example:allowed" + rules.enforce(self.credentials, action, self.target) + + def test_enforce_http_true(self): + + def fakeurlopen(url, post_data): + return six.StringIO("True") + + action = "example:get_http" + target = {} + with mock.patch.object(urlrequest, 'urlopen', fakeurlopen): + result = rules.enforce(self.credentials, action, target) + self.assertTrue(result) + + def test_enforce_http_false(self): + + def fakeurlopen(url, post_data): + return six.StringIO("False") + + action = "example:get_http" + target = {} + with mock.patch.object(urlrequest, 'urlopen', fakeurlopen): + self.assertRaises(exception.ForbiddenAction, rules.enforce, + self.credentials, action, target) + + def test_templatized_enforcement(self): + target_mine = {'project_id': 'fake'} + target_not_mine = {'project_id': 'another'} + credentials = {'project_id': 'fake', 'roles': []} + action = "example:my_file" + rules.enforce(credentials, action, target_mine) + self.assertRaises(exception.ForbiddenAction, rules.enforce, + credentials, action, target_not_mine) + + def test_early_AND_enforcement(self): + action = "example:early_and_fail" + self.assertRaises(exception.ForbiddenAction, rules.enforce, + self.credentials, action, self.target) + + def test_early_OR_enforcement(self): + action = "example:early_or_success" + rules.enforce(self.credentials, action, self.target) + + def test_ignore_case_role_check(self): + lowercase_action = "example:lowercase_admin" + uppercase_action = "example:uppercase_admin" + # NOTE(dprince) we mix case in the Admin role here to ensure + # case is ignored + admin_credentials = {'roles': ['AdMiN']} + rules.enforce(admin_credentials, lowercase_action, self.target) + rules.enforce(admin_credentials, uppercase_action, self.target) + + +class DefaultPolicyTestCase(BasePolicyTestCase): + def setUp(self): + super(DefaultPolicyTestCase, self).setUp() + rules.init() + + self.rules = { + "default": [], + "example:exist": [["false:false"]] + } + self._set_rules('default') + self.credentials = {} + + # FIXME(gyee): latest Oslo policy Enforcer class reloads the rules in + # its enforce() method even though rules has been initialized via + # set_rules(). To make it easier to do our tests, we're going to + # monkeypatch load_roles() so it does nothing. This seem like a bug in + # Oslo policy as we shoudn't have to reload the rules if they have + # already been set using set_rules(). + self._old_load_rules = rules._ENFORCER.load_rules + self.addCleanup(setattr, rules._ENFORCER, 'load_rules', + self._old_load_rules) + rules._ENFORCER.load_rules = lambda *args, **kwargs: None + + def _set_rules(self, default_rule): + these_rules = common_policy.Rules.from_dict(self.rules, default_rule) + rules._ENFORCER.set_rules(these_rules) + + def test_policy_called(self): + self.assertRaises(exception.ForbiddenAction, rules.enforce, + self.credentials, "example:exist", {}) + + def test_not_found_policy_calls_default(self): + rules.enforce(self.credentials, "example:noexist", {}) + + def test_default_not_found(self): + new_default_rule = "default_noexist" + # FIXME(gyee): need to overwrite the Enforcer's default_rule first + # as it is recreating the rules with its own default_rule instead + # of the default_rule passed in from set_rules(). I think this is a + # bug in Oslo policy. + rules._ENFORCER.default_rule = new_default_rule + self._set_rules(new_default_rule) + self.assertRaises(exception.ForbiddenAction, rules.enforce, + self.credentials, "example:noexist", {}) + + +class PolicyJsonTestCase(tests.TestCase): + + def _load_entries(self, filename): + return set(json.load(open(filename))) + + def test_json_examples_have_matching_entries(self): + policy_keys = self._load_entries(tests.dirs.etc('policy.json')) + cloud_policy_keys = self._load_entries( + tests.dirs.etc('policy.v3cloudsample.json')) + + diffs = set(policy_keys).difference(set(cloud_policy_keys)) + + self.assertThat(diffs, matchers.Equals(set())) diff --git a/keystone-moon/keystone/tests/unit/test_revoke.py b/keystone-moon/keystone/tests/unit/test_revoke.py new file mode 100644 index 00000000..727eff78 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_revoke.py @@ -0,0 +1,637 @@ +# 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 datetime +import uuid + +import mock +from oslo_utils import timeutils +from testtools import matchers + +from keystone.contrib.revoke import model +from keystone import exception +from keystone.tests import unit as tests +from keystone.tests.unit import test_backend_sql +from keystone.token import provider + + +def _new_id(): + return uuid.uuid4().hex + + +def _future_time(): + expire_delta = datetime.timedelta(seconds=1000) + future_time = timeutils.utcnow() + expire_delta + return future_time + + +def _past_time(): + expire_delta = datetime.timedelta(days=-1000) + past_time = timeutils.utcnow() + expire_delta + return past_time + + +def _sample_blank_token(): + issued_delta = datetime.timedelta(minutes=-2) + issued_at = timeutils.utcnow() + issued_delta + token_data = model.blank_token_data(issued_at) + return token_data + + +def _matches(event, token_values): + """See if the token matches the revocation event. + + Used as a secondary check on the logic to Check + By Tree Below: This is abrute force approach to checking. + Compare each attribute from the event with the corresponding + value from the token. If the event does not have a value for + the attribute, a match is still possible. If the event has a + value for the attribute, and it does not match the token, no match + is possible, so skip the remaining checks. + + :param event one revocation event to match + :param token_values dictionary with set of values taken from the + token + :returns if the token matches the revocation event, indicating the + token has been revoked + """ + + # The token has three attributes that can match the user_id + if event.user_id is not None: + for attribute_name in ['user_id', 'trustor_id', 'trustee_id']: + if event.user_id == token_values[attribute_name]: + break + else: + return False + + # The token has two attributes that can match the domain_id + if event.domain_id is not None: + for attribute_name in ['identity_domain_id', 'assignment_domain_id']: + if event.domain_id == token_values[attribute_name]: + break + else: + return False + + if event.domain_scope_id is not None: + if event.domain_scope_id != token_values['assignment_domain_id']: + return False + + # If any one check does not match, the while token does + # not match the event. The numerous return False indicate + # that the token is still valid and short-circuits the + # rest of the logic. + attribute_names = ['project_id', + 'expires_at', 'trust_id', 'consumer_id', + 'access_token_id', 'audit_id', 'audit_chain_id'] + for attribute_name in attribute_names: + if getattr(event, attribute_name) is not None: + if (getattr(event, attribute_name) != + token_values[attribute_name]): + return False + + if event.role_id is not None: + roles = token_values['roles'] + for role in roles: + if event.role_id == role: + break + else: + return False + if token_values['issued_at'] > event.issued_before: + return False + return True + + +class RevokeTests(object): + def test_list(self): + self.revoke_api.revoke_by_user(user_id=1) + self.assertEqual(1, len(self.revoke_api.list_events())) + + self.revoke_api.revoke_by_user(user_id=2) + self.assertEqual(2, len(self.revoke_api.list_events())) + + def test_list_since(self): + self.revoke_api.revoke_by_user(user_id=1) + self.revoke_api.revoke_by_user(user_id=2) + past = timeutils.utcnow() - datetime.timedelta(seconds=1000) + self.assertEqual(2, len(self.revoke_api.list_events(past))) + future = timeutils.utcnow() + datetime.timedelta(seconds=1000) + self.assertEqual(0, len(self.revoke_api.list_events(future))) + + def test_past_expiry_are_removed(self): + user_id = 1 + self.revoke_api.revoke_by_expiration(user_id, _future_time()) + self.assertEqual(1, len(self.revoke_api.list_events())) + event = model.RevokeEvent() + event.revoked_at = _past_time() + self.revoke_api.revoke(event) + self.assertEqual(1, len(self.revoke_api.list_events())) + + @mock.patch.object(timeutils, 'utcnow') + def test_expired_events_removed_validate_token_success(self, mock_utcnow): + def _sample_token_values(): + token = _sample_blank_token() + token['expires_at'] = timeutils.isotime(_future_time(), + subsecond=True) + return token + + now = datetime.datetime.utcnow() + now_plus_2h = now + datetime.timedelta(hours=2) + mock_utcnow.return_value = now + + # Build a token and validate it. This will seed the cache for the + # future 'synchronize' call. + token_values = _sample_token_values() + + user_id = _new_id() + self.revoke_api.revoke_by_user(user_id) + token_values['user_id'] = user_id + self.assertRaises(exception.TokenNotFound, + self.revoke_api.check_token, + token_values) + + # Move our clock forward by 2h, build a new token and validate it. + # 'synchronize' should now be exercised and remove old expired events + mock_utcnow.return_value = now_plus_2h + self.revoke_api.revoke_by_expiration(_new_id(), now_plus_2h) + # should no longer throw an exception + self.revoke_api.check_token(token_values) + + def test_revoke_by_expiration_project_and_domain_fails(self): + user_id = _new_id() + expires_at = timeutils.isotime(_future_time(), subsecond=True) + domain_id = _new_id() + project_id = _new_id() + self.assertThat( + lambda: self.revoke_api.revoke_by_expiration( + user_id, expires_at, domain_id=domain_id, + project_id=project_id), + matchers.raises(exception.UnexpectedError)) + + +class SqlRevokeTests(test_backend_sql.SqlTests, RevokeTests): + def config_overrides(self): + super(SqlRevokeTests, self).config_overrides() + self.config_fixture.config( + group='revoke', + driver='keystone.contrib.revoke.backends.sql.Revoke') + self.config_fixture.config( + group='token', + provider='keystone.token.providers.pki.Provider', + revoke_by_id=False) + + +class KvsRevokeTests(tests.TestCase, RevokeTests): + def config_overrides(self): + super(KvsRevokeTests, self).config_overrides() + self.config_fixture.config( + group='revoke', + driver='keystone.contrib.revoke.backends.kvs.Revoke') + self.config_fixture.config( + group='token', + provider='keystone.token.providers.pki.Provider', + revoke_by_id=False) + + def setUp(self): + super(KvsRevokeTests, self).setUp() + self.load_backends() + + +class RevokeTreeTests(tests.TestCase): + def setUp(self): + super(RevokeTreeTests, self).setUp() + self.events = [] + self.tree = model.RevokeTree() + self._sample_data() + + def _sample_data(self): + user_ids = [] + project_ids = [] + role_ids = [] + for i in range(0, 3): + user_ids.append(_new_id()) + project_ids.append(_new_id()) + role_ids.append(_new_id()) + + project_tokens = [] + i = len(project_tokens) + project_tokens.append(_sample_blank_token()) + project_tokens[i]['user_id'] = user_ids[0] + project_tokens[i]['project_id'] = project_ids[0] + project_tokens[i]['roles'] = [role_ids[1]] + + i = len(project_tokens) + project_tokens.append(_sample_blank_token()) + project_tokens[i]['user_id'] = user_ids[1] + project_tokens[i]['project_id'] = project_ids[0] + project_tokens[i]['roles'] = [role_ids[0]] + + i = len(project_tokens) + project_tokens.append(_sample_blank_token()) + project_tokens[i]['user_id'] = user_ids[0] + project_tokens[i]['project_id'] = project_ids[1] + project_tokens[i]['roles'] = [role_ids[0]] + + token_to_revoke = _sample_blank_token() + token_to_revoke['user_id'] = user_ids[0] + token_to_revoke['project_id'] = project_ids[0] + token_to_revoke['roles'] = [role_ids[0]] + + self.project_tokens = project_tokens + self.user_ids = user_ids + self.project_ids = project_ids + self.role_ids = role_ids + self.token_to_revoke = token_to_revoke + + def _assertTokenRevoked(self, token_data): + self.assertTrue(any([_matches(e, token_data) for e in self.events])) + return self.assertTrue(self.tree.is_revoked(token_data), + 'Token should be revoked') + + def _assertTokenNotRevoked(self, token_data): + self.assertFalse(any([_matches(e, token_data) for e in self.events])) + return self.assertFalse(self.tree.is_revoked(token_data), + 'Token should not be revoked') + + def _revoke_by_user(self, user_id): + return self.tree.add_event( + model.RevokeEvent(user_id=user_id)) + + def _revoke_by_audit_id(self, audit_id): + event = self.tree.add_event( + model.RevokeEvent(audit_id=audit_id)) + self.events.append(event) + return event + + def _revoke_by_audit_chain_id(self, audit_chain_id, project_id=None, + domain_id=None): + event = self.tree.add_event( + model.RevokeEvent(audit_chain_id=audit_chain_id, + project_id=project_id, + domain_id=domain_id) + ) + self.events.append(event) + return event + + def _revoke_by_expiration(self, user_id, expires_at, project_id=None, + domain_id=None): + event = self.tree.add_event( + model.RevokeEvent(user_id=user_id, + expires_at=expires_at, + project_id=project_id, + domain_id=domain_id)) + self.events.append(event) + return event + + def _revoke_by_grant(self, role_id, user_id=None, + domain_id=None, project_id=None): + event = self.tree.add_event( + model.RevokeEvent(user_id=user_id, + role_id=role_id, + domain_id=domain_id, + project_id=project_id)) + self.events.append(event) + return event + + def _revoke_by_user_and_project(self, user_id, project_id): + event = self.tree.add_event( + model.RevokeEvent(project_id=project_id, + user_id=user_id)) + self.events.append(event) + return event + + def _revoke_by_project_role_assignment(self, project_id, role_id): + event = self.tree.add_event( + model.RevokeEvent(project_id=project_id, + role_id=role_id)) + self.events.append(event) + return event + + def _revoke_by_domain_role_assignment(self, domain_id, role_id): + event = self.tree.add_event( + model.RevokeEvent(domain_id=domain_id, + role_id=role_id)) + self.events.append(event) + return event + + def _revoke_by_domain(self, domain_id): + event = self.tree.add_event(model.RevokeEvent(domain_id=domain_id)) + self.events.append(event) + + def _user_field_test(self, field_name): + user_id = _new_id() + event = self._revoke_by_user(user_id) + self.events.append(event) + token_data_u1 = _sample_blank_token() + token_data_u1[field_name] = user_id + self._assertTokenRevoked(token_data_u1) + token_data_u2 = _sample_blank_token() + token_data_u2[field_name] = _new_id() + self._assertTokenNotRevoked(token_data_u2) + self.tree.remove_event(event) + self.events.remove(event) + self._assertTokenNotRevoked(token_data_u1) + + def test_revoke_by_user(self): + self._user_field_test('user_id') + + def test_revoke_by_user_matches_trustee(self): + self._user_field_test('trustee_id') + + def test_revoke_by_user_matches_trustor(self): + self._user_field_test('trustor_id') + + def test_by_user_expiration(self): + future_time = _future_time() + + user_id = 1 + event = self._revoke_by_expiration(user_id, future_time) + token_data_1 = _sample_blank_token() + token_data_1['user_id'] = user_id + token_data_1['expires_at'] = future_time.replace(microsecond=0) + self._assertTokenRevoked(token_data_1) + + token_data_2 = _sample_blank_token() + token_data_2['user_id'] = user_id + expire_delta = datetime.timedelta(seconds=2000) + future_time = timeutils.utcnow() + expire_delta + token_data_2['expires_at'] = future_time + self._assertTokenNotRevoked(token_data_2) + + self.remove_event(event) + self._assertTokenNotRevoked(token_data_1) + + def test_revoke_by_audit_id(self): + audit_id = provider.audit_info(parent_audit_id=None)[0] + token_data_1 = _sample_blank_token() + # Audit ID and Audit Chain ID are populated with the same value + # if the token is an original token + token_data_1['audit_id'] = audit_id + token_data_1['audit_chain_id'] = audit_id + event = self._revoke_by_audit_id(audit_id) + self._assertTokenRevoked(token_data_1) + + audit_id_2 = provider.audit_info(parent_audit_id=audit_id)[0] + token_data_2 = _sample_blank_token() + token_data_2['audit_id'] = audit_id_2 + token_data_2['audit_chain_id'] = audit_id + self._assertTokenNotRevoked(token_data_2) + + self.remove_event(event) + self._assertTokenNotRevoked(token_data_1) + + def test_revoke_by_audit_chain_id(self): + audit_id = provider.audit_info(parent_audit_id=None)[0] + token_data_1 = _sample_blank_token() + # Audit ID and Audit Chain ID are populated with the same value + # if the token is an original token + token_data_1['audit_id'] = audit_id + token_data_1['audit_chain_id'] = audit_id + event = self._revoke_by_audit_chain_id(audit_id) + self._assertTokenRevoked(token_data_1) + + audit_id_2 = provider.audit_info(parent_audit_id=audit_id)[0] + token_data_2 = _sample_blank_token() + token_data_2['audit_id'] = audit_id_2 + token_data_2['audit_chain_id'] = audit_id + self._assertTokenRevoked(token_data_2) + + self.remove_event(event) + self._assertTokenNotRevoked(token_data_1) + self._assertTokenNotRevoked(token_data_2) + + def test_by_user_project(self): + # When a user has a project-scoped token and the project-scoped token + # is revoked then the token is revoked. + + user_id = _new_id() + project_id = _new_id() + + future_time = _future_time() + + token_data = _sample_blank_token() + token_data['user_id'] = user_id + token_data['project_id'] = project_id + token_data['expires_at'] = future_time.replace(microsecond=0) + + self._revoke_by_expiration(user_id, future_time, project_id=project_id) + self._assertTokenRevoked(token_data) + + def test_by_user_domain(self): + # When a user has a domain-scoped token and the domain-scoped token + # is revoked then the token is revoked. + + user_id = _new_id() + domain_id = _new_id() + + future_time = _future_time() + + token_data = _sample_blank_token() + token_data['user_id'] = user_id + token_data['assignment_domain_id'] = domain_id + token_data['expires_at'] = future_time.replace(microsecond=0) + + self._revoke_by_expiration(user_id, future_time, domain_id=domain_id) + self._assertTokenRevoked(token_data) + + def remove_event(self, event): + self.events.remove(event) + self.tree.remove_event(event) + + def test_by_project_grant(self): + token_to_revoke = self.token_to_revoke + tokens = self.project_tokens + + self._assertTokenNotRevoked(token_to_revoke) + for token in tokens: + self._assertTokenNotRevoked(token) + + event = self._revoke_by_grant(role_id=self.role_ids[0], + user_id=self.user_ids[0], + project_id=self.project_ids[0]) + + self._assertTokenRevoked(token_to_revoke) + for token in tokens: + self._assertTokenNotRevoked(token) + + self.remove_event(event) + + self._assertTokenNotRevoked(token_to_revoke) + for token in tokens: + self._assertTokenNotRevoked(token) + + token_to_revoke['roles'] = [self.role_ids[0], + self.role_ids[1], + self.role_ids[2]] + + event = self._revoke_by_grant(role_id=self.role_ids[0], + user_id=self.user_ids[0], + project_id=self.project_ids[0]) + self._assertTokenRevoked(token_to_revoke) + self.remove_event(event) + self._assertTokenNotRevoked(token_to_revoke) + + event = self._revoke_by_grant(role_id=self.role_ids[1], + user_id=self.user_ids[0], + project_id=self.project_ids[0]) + self._assertTokenRevoked(token_to_revoke) + self.remove_event(event) + self._assertTokenNotRevoked(token_to_revoke) + + self._revoke_by_grant(role_id=self.role_ids[0], + user_id=self.user_ids[0], + project_id=self.project_ids[0]) + self._revoke_by_grant(role_id=self.role_ids[1], + user_id=self.user_ids[0], + project_id=self.project_ids[0]) + self._revoke_by_grant(role_id=self.role_ids[2], + user_id=self.user_ids[0], + project_id=self.project_ids[0]) + self._assertTokenRevoked(token_to_revoke) + + def test_by_project_and_user_and_role(self): + user_id1 = _new_id() + user_id2 = _new_id() + project_id = _new_id() + self.events.append(self._revoke_by_user(user_id1)) + self.events.append( + self._revoke_by_user_and_project(user_id2, project_id)) + token_data = _sample_blank_token() + token_data['user_id'] = user_id2 + token_data['project_id'] = project_id + self._assertTokenRevoked(token_data) + + def test_by_domain_user(self): + # If revoke a domain, then a token for a user in the domain is revoked + + user_id = _new_id() + domain_id = _new_id() + + token_data = _sample_blank_token() + token_data['user_id'] = user_id + token_data['identity_domain_id'] = domain_id + + self._revoke_by_domain(domain_id) + + self._assertTokenRevoked(token_data) + + def test_by_domain_project(self): + # If revoke a domain, then a token scoped to a project in the domain + # is revoked. + + user_id = _new_id() + user_domain_id = _new_id() + + project_id = _new_id() + project_domain_id = _new_id() + + token_data = _sample_blank_token() + token_data['user_id'] = user_id + token_data['identity_domain_id'] = user_domain_id + token_data['project_id'] = project_id + token_data['assignment_domain_id'] = project_domain_id + + self._revoke_by_domain(project_domain_id) + + self._assertTokenRevoked(token_data) + + def test_by_domain_domain(self): + # If revoke a domain, then a token scoped to the domain is revoked. + + user_id = _new_id() + user_domain_id = _new_id() + + domain_id = _new_id() + + token_data = _sample_blank_token() + token_data['user_id'] = user_id + token_data['identity_domain_id'] = user_domain_id + token_data['assignment_domain_id'] = domain_id + + self._revoke_by_domain(domain_id) + + self._assertTokenRevoked(token_data) + + def _assertEmpty(self, collection): + return self.assertEqual(0, len(collection), "collection not empty") + + def _assertEventsMatchIteration(self, turn): + self.assertEqual(1, len(self.tree.revoke_map)) + self.assertEqual(turn + 1, len(self.tree.revoke_map + ['trust_id=*'] + ['consumer_id=*'] + ['access_token_id=*'] + ['audit_id=*'] + ['audit_chain_id=*'])) + # two different functions add domain_ids, +1 for None + self.assertEqual(2 * turn + 1, len(self.tree.revoke_map + ['trust_id=*'] + ['consumer_id=*'] + ['access_token_id=*'] + ['audit_id=*'] + ['audit_chain_id=*'] + ['expires_at=*'])) + # two different functions add project_ids, +1 for None + self.assertEqual(2 * turn + 1, len(self.tree.revoke_map + ['trust_id=*'] + ['consumer_id=*'] + ['access_token_id=*'] + ['audit_id=*'] + ['audit_chain_id=*'] + ['expires_at=*'] + ['domain_id=*'])) + # 10 users added + self.assertEqual(turn, len(self.tree.revoke_map + ['trust_id=*'] + ['consumer_id=*'] + ['access_token_id=*'] + ['audit_id=*'] + ['audit_chain_id=*'] + ['expires_at=*'] + ['domain_id=*'] + ['project_id=*'])) + + def test_cleanup(self): + events = self.events + self._assertEmpty(self.tree.revoke_map) + expiry_base_time = _future_time() + for i in range(0, 10): + events.append( + self._revoke_by_user(_new_id())) + + args = (_new_id(), + expiry_base_time + datetime.timedelta(seconds=i)) + events.append( + self._revoke_by_expiration(*args)) + + self.assertEqual(i + 2, len(self.tree.revoke_map + ['trust_id=*'] + ['consumer_id=*'] + ['access_token_id=*'] + ['audit_id=*'] + ['audit_chain_id=*']), + 'adding %s to %s' % (args, + self.tree.revoke_map)) + + events.append( + self._revoke_by_project_role_assignment(_new_id(), _new_id())) + events.append( + self._revoke_by_domain_role_assignment(_new_id(), _new_id())) + events.append( + self._revoke_by_domain_role_assignment(_new_id(), _new_id())) + events.append( + self._revoke_by_user_and_project(_new_id(), _new_id())) + self._assertEventsMatchIteration(i + 1) + + for event in self.events: + self.tree.remove_event(event) + self._assertEmpty(self.tree.revoke_map) diff --git a/keystone-moon/keystone/tests/unit/test_singular_plural.py b/keystone-moon/keystone/tests/unit/test_singular_plural.py new file mode 100644 index 00000000..b07ea8d5 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_singular_plural.py @@ -0,0 +1,48 @@ +# Copyright 2012 Red Hat, Inc. +# +# 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 ast + +from keystone.contrib.admin_crud import core as admin_crud_core +from keystone.contrib.s3 import core as s3_core +from keystone.contrib.user_crud import core as user_crud_core +from keystone.identity import core as identity_core +from keystone import service + + +class TestSingularPlural(object): + def test_keyword_arg_condition_or_methods(self): + """Raise if we see a keyword arg called 'condition' or 'methods'.""" + modules = [admin_crud_core, s3_core, + user_crud_core, identity_core, service] + for module in modules: + filename = module.__file__ + if filename.endswith(".pyc"): + # In Python 2, the .py and .pyc files are in the same dir. + filename = filename[:-1] + with open(filename) as fil: + source = fil.read() + module = ast.parse(source, filename) + last_stmt_or_expr = None + for node in ast.walk(module): + if isinstance(node, ast.stmt) or isinstance(node, ast.expr): + # keyword nodes don't have line numbers, so we need to + # get that information from the parent stmt or expr. + last_stmt_or_expr = node + elif isinstance(node, ast.keyword): + for bad_word in ["condition", "methods"]: + if node.arg == bad_word: + raise AssertionError( + "Suspicious name '%s' at %s line %s" % + (bad_word, filename, last_stmt_or_expr.lineno)) diff --git a/keystone-moon/keystone/tests/unit/test_sql_livetest.py b/keystone-moon/keystone/tests/unit/test_sql_livetest.py new file mode 100644 index 00000000..96ee6c70 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_sql_livetest.py @@ -0,0 +1,73 @@ +# Copyright 2013 Red Hat, Inc +# +# 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 keystone.tests import unit as tests +from keystone.tests.unit import test_sql_migrate_extensions +from keystone.tests.unit import test_sql_upgrade + + +class PostgresqlMigrateTests(test_sql_upgrade.SqlUpgradeTests): + def setUp(self): + self.skip_if_env_not_set('ENABLE_LIVE_POSTGRES_TEST') + super(PostgresqlMigrateTests, self).setUp() + + def config_files(self): + files = super(PostgresqlMigrateTests, self).config_files() + files.append(tests.dirs.tests_conf("backend_postgresql.conf")) + return files + + +class MysqlMigrateTests(test_sql_upgrade.SqlUpgradeTests): + def setUp(self): + self.skip_if_env_not_set('ENABLE_LIVE_MYSQL_TEST') + super(MysqlMigrateTests, self).setUp() + + def config_files(self): + files = super(MysqlMigrateTests, self).config_files() + files.append(tests.dirs.tests_conf("backend_mysql.conf")) + return files + + +class PostgresqlRevokeExtensionsTests( + test_sql_migrate_extensions.RevokeExtension): + def setUp(self): + self.skip_if_env_not_set('ENABLE_LIVE_POSTGRES_TEST') + super(PostgresqlRevokeExtensionsTests, self).setUp() + + def config_files(self): + files = super(PostgresqlRevokeExtensionsTests, self).config_files() + files.append(tests.dirs.tests_conf("backend_postgresql.conf")) + return files + + +class MysqlRevokeExtensionsTests(test_sql_migrate_extensions.RevokeExtension): + def setUp(self): + self.skip_if_env_not_set('ENABLE_LIVE_MYSQL_TEST') + super(MysqlRevokeExtensionsTests, self).setUp() + + def config_files(self): + files = super(MysqlRevokeExtensionsTests, self).config_files() + files.append(tests.dirs.tests_conf("backend_mysql.conf")) + return files + + +class Db2MigrateTests(test_sql_upgrade.SqlUpgradeTests): + def setUp(self): + self.skip_if_env_not_set('ENABLE_LIVE_DB2_TEST') + super(Db2MigrateTests, self).setUp() + + def config_files(self): + files = super(Db2MigrateTests, self).config_files() + files.append(tests.dirs.tests_conf("backend_db2.conf")) + return files diff --git a/keystone-moon/keystone/tests/unit/test_sql_migrate_extensions.py b/keystone-moon/keystone/tests/unit/test_sql_migrate_extensions.py new file mode 100644 index 00000000..edfb91d7 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_sql_migrate_extensions.py @@ -0,0 +1,380 @@ +# Copyright 2012 OpenStack Foundation +# +# 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. +""" +To run these tests against a live database: + +1. Modify the file `keystone/tests/unit/config_files/backend_sql.conf` to use + the connection for your live database. +2. Set up a blank, live database. +3. Run the tests using:: + + tox -e py27 -- keystone.tests.unit.test_sql_migrate_extensions + +WARNING:: + + Your database will be wiped. + + Do not do this against a Database with valuable data as + all data will be lost. +""" + +import sqlalchemy +import uuid + +from oslo_db import exception as db_exception +from oslo_db.sqlalchemy import utils + +from keystone.contrib import endpoint_filter +from keystone.contrib import endpoint_policy +from keystone.contrib import example +from keystone.contrib import federation +from keystone.contrib import oauth1 +from keystone.contrib import revoke +from keystone.tests.unit import test_sql_upgrade + + +class SqlUpgradeExampleExtension(test_sql_upgrade.SqlMigrateBase): + def repo_package(self): + return example + + def test_upgrade(self): + self.assertTableDoesNotExist('example') + self.upgrade(1, repository=self.repo_path) + self.assertTableColumns('example', ['id', 'type', 'extra']) + + def test_downgrade(self): + self.upgrade(1, repository=self.repo_path) + self.assertTableColumns('example', ['id', 'type', 'extra']) + self.downgrade(0, repository=self.repo_path) + self.assertTableDoesNotExist('example') + + +class SqlUpgradeOAuth1Extension(test_sql_upgrade.SqlMigrateBase): + def repo_package(self): + return oauth1 + + def upgrade(self, version): + super(SqlUpgradeOAuth1Extension, self).upgrade( + version, repository=self.repo_path) + + def downgrade(self, version): + super(SqlUpgradeOAuth1Extension, self).downgrade( + version, repository=self.repo_path) + + def _assert_v1_3_tables(self): + self.assertTableColumns('consumer', + ['id', + 'description', + 'secret', + 'extra']) + self.assertTableColumns('request_token', + ['id', + 'request_secret', + 'verifier', + 'authorizing_user_id', + 'requested_project_id', + 'requested_roles', + 'consumer_id', + 'expires_at']) + self.assertTableColumns('access_token', + ['id', + 'access_secret', + 'authorizing_user_id', + 'project_id', + 'requested_roles', + 'consumer_id', + 'expires_at']) + + def _assert_v4_later_tables(self): + self.assertTableColumns('consumer', + ['id', + 'description', + 'secret', + 'extra']) + self.assertTableColumns('request_token', + ['id', + 'request_secret', + 'verifier', + 'authorizing_user_id', + 'requested_project_id', + 'role_ids', + 'consumer_id', + 'expires_at']) + self.assertTableColumns('access_token', + ['id', + 'access_secret', + 'authorizing_user_id', + 'project_id', + 'role_ids', + 'consumer_id', + 'expires_at']) + + def test_upgrade(self): + self.assertTableDoesNotExist('consumer') + self.assertTableDoesNotExist('request_token') + self.assertTableDoesNotExist('access_token') + self.upgrade(1) + self._assert_v1_3_tables() + + # NOTE(blk-u): Migrations 2-3 don't modify the tables in a way that we + # can easily test for. + + self.upgrade(4) + self._assert_v4_later_tables() + + self.upgrade(5) + self._assert_v4_later_tables() + + def test_downgrade(self): + self.upgrade(5) + self._assert_v4_later_tables() + self.downgrade(3) + self._assert_v1_3_tables() + self.downgrade(1) + self._assert_v1_3_tables() + self.downgrade(0) + self.assertTableDoesNotExist('consumer') + self.assertTableDoesNotExist('request_token') + self.assertTableDoesNotExist('access_token') + + +class EndpointFilterExtension(test_sql_upgrade.SqlMigrateBase): + def repo_package(self): + return endpoint_filter + + def upgrade(self, version): + super(EndpointFilterExtension, self).upgrade( + version, repository=self.repo_path) + + def downgrade(self, version): + super(EndpointFilterExtension, self).downgrade( + version, repository=self.repo_path) + + def _assert_v1_tables(self): + self.assertTableColumns('project_endpoint', + ['endpoint_id', 'project_id']) + self.assertTableDoesNotExist('endpoint_group') + self.assertTableDoesNotExist('project_endpoint_group') + + def _assert_v2_tables(self): + self.assertTableColumns('project_endpoint', + ['endpoint_id', 'project_id']) + self.assertTableColumns('endpoint_group', + ['id', 'name', 'description', 'filters']) + self.assertTableColumns('project_endpoint_group', + ['endpoint_group_id', 'project_id']) + + def test_upgrade(self): + self.assertTableDoesNotExist('project_endpoint') + self.upgrade(1) + self._assert_v1_tables() + self.assertTableColumns('project_endpoint', + ['endpoint_id', 'project_id']) + self.upgrade(2) + self._assert_v2_tables() + + def test_downgrade(self): + self.upgrade(2) + self._assert_v2_tables() + self.downgrade(1) + self._assert_v1_tables() + self.downgrade(0) + self.assertTableDoesNotExist('project_endpoint') + + +class EndpointPolicyExtension(test_sql_upgrade.SqlMigrateBase): + def repo_package(self): + return endpoint_policy + + def test_upgrade(self): + self.assertTableDoesNotExist('policy_association') + self.upgrade(1, repository=self.repo_path) + self.assertTableColumns('policy_association', + ['id', 'policy_id', 'endpoint_id', + 'service_id', 'region_id']) + + def test_downgrade(self): + self.upgrade(1, repository=self.repo_path) + self.assertTableColumns('policy_association', + ['id', 'policy_id', 'endpoint_id', + 'service_id', 'region_id']) + self.downgrade(0, repository=self.repo_path) + self.assertTableDoesNotExist('policy_association') + + +class FederationExtension(test_sql_upgrade.SqlMigrateBase): + """Test class for ensuring the Federation SQL.""" + + def setUp(self): + super(FederationExtension, self).setUp() + self.identity_provider = 'identity_provider' + self.federation_protocol = 'federation_protocol' + self.service_provider = 'service_provider' + self.mapping = 'mapping' + + def repo_package(self): + return federation + + def insert_dict(self, session, table_name, d): + """Naively inserts key-value pairs into a table, given a dictionary.""" + table = sqlalchemy.Table(table_name, self.metadata, autoload=True) + insert = table.insert().values(**d) + session.execute(insert) + session.commit() + + def test_upgrade(self): + self.assertTableDoesNotExist(self.identity_provider) + self.assertTableDoesNotExist(self.federation_protocol) + self.assertTableDoesNotExist(self.mapping) + + self.upgrade(1, repository=self.repo_path) + self.assertTableColumns(self.identity_provider, + ['id', + 'enabled', + 'description']) + + self.assertTableColumns(self.federation_protocol, + ['id', + 'idp_id', + 'mapping_id']) + + self.upgrade(2, repository=self.repo_path) + self.assertTableColumns(self.mapping, + ['id', 'rules']) + + federation_protocol = utils.get_table( + self.engine, + 'federation_protocol') + with self.engine.begin() as conn: + conn.execute(federation_protocol.insert(), id=0, idp_id=1) + self.upgrade(3, repository=self.repo_path) + federation_protocol = utils.get_table( + self.engine, + 'federation_protocol') + self.assertFalse(federation_protocol.c.mapping_id.nullable) + + def test_downgrade(self): + self.upgrade(3, repository=self.repo_path) + self.assertTableColumns(self.identity_provider, + ['id', 'enabled', 'description']) + self.assertTableColumns(self.federation_protocol, + ['id', 'idp_id', 'mapping_id']) + self.assertTableColumns(self.mapping, + ['id', 'rules']) + + self.downgrade(2, repository=self.repo_path) + federation_protocol = utils.get_table( + self.engine, + 'federation_protocol') + self.assertTrue(federation_protocol.c.mapping_id.nullable) + + self.downgrade(0, repository=self.repo_path) + self.assertTableDoesNotExist(self.identity_provider) + self.assertTableDoesNotExist(self.federation_protocol) + self.assertTableDoesNotExist(self.mapping) + + def test_fixup_service_provider_attributes(self): + self.upgrade(6, repository=self.repo_path) + self.assertTableColumns(self.service_provider, + ['id', 'description', 'enabled', 'auth_url', + 'sp_url']) + + session = self.Session() + sp1 = {'id': uuid.uuid4().hex, + 'auth_url': None, + 'sp_url': uuid.uuid4().hex, + 'description': uuid.uuid4().hex, + 'enabled': True} + sp2 = {'id': uuid.uuid4().hex, + 'auth_url': uuid.uuid4().hex, + 'sp_url': None, + 'description': uuid.uuid4().hex, + 'enabled': True} + sp3 = {'id': uuid.uuid4().hex, + 'auth_url': None, + 'sp_url': None, + 'description': uuid.uuid4().hex, + 'enabled': True} + + # Insert with 'auth_url' or 'sp_url' set to null must fail + self.assertRaises(db_exception.DBError, + self.insert_dict, + session, + self.service_provider, + sp1) + self.assertRaises(db_exception.DBError, + self.insert_dict, + session, + self.service_provider, + sp2) + self.assertRaises(db_exception.DBError, + self.insert_dict, + session, + self.service_provider, + sp3) + + session.close() + self.downgrade(5, repository=self.repo_path) + self.assertTableColumns(self.service_provider, + ['id', 'description', 'enabled', 'auth_url', + 'sp_url']) + session = self.Session() + self.metadata.clear() + + # Before the migration, the table should accept null values + self.insert_dict(session, self.service_provider, sp1) + self.insert_dict(session, self.service_provider, sp2) + self.insert_dict(session, self.service_provider, sp3) + + # Check if null values are updated to empty string when migrating + session.close() + self.upgrade(6, repository=self.repo_path) + sp_table = sqlalchemy.Table(self.service_provider, + self.metadata, + autoload=True) + session = self.Session() + self.metadata.clear() + + sp = session.query(sp_table).filter(sp_table.c.id == sp1['id'])[0] + self.assertEqual('', sp.auth_url) + + sp = session.query(sp_table).filter(sp_table.c.id == sp2['id'])[0] + self.assertEqual('', sp.sp_url) + + sp = session.query(sp_table).filter(sp_table.c.id == sp3['id'])[0] + self.assertEqual('', sp.auth_url) + self.assertEqual('', sp.sp_url) + +_REVOKE_COLUMN_NAMES = ['id', 'domain_id', 'project_id', 'user_id', 'role_id', + 'trust_id', 'consumer_id', 'access_token_id', + 'issued_before', 'expires_at', 'revoked_at'] + + +class RevokeExtension(test_sql_upgrade.SqlMigrateBase): + + def repo_package(self): + return revoke + + def test_upgrade(self): + self.assertTableDoesNotExist('revocation_event') + self.upgrade(1, repository=self.repo_path) + self.assertTableColumns('revocation_event', + _REVOKE_COLUMN_NAMES) + + def test_downgrade(self): + self.upgrade(1, repository=self.repo_path) + self.assertTableColumns('revocation_event', + _REVOKE_COLUMN_NAMES) + self.downgrade(0, repository=self.repo_path) + self.assertTableDoesNotExist('revocation_event') diff --git a/keystone-moon/keystone/tests/unit/test_sql_upgrade.py b/keystone-moon/keystone/tests/unit/test_sql_upgrade.py new file mode 100644 index 00000000..e50bad56 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_sql_upgrade.py @@ -0,0 +1,957 @@ +# Copyright 2012 OpenStack Foundation +# +# 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. +""" +To run these tests against a live database: + +1. Modify the file ``keystone/tests/unit/config_files/backend_sql.conf`` to use + the connection for your live database. +2. Set up a blank, live database +3. Run the tests using:: + + tox -e py27 -- keystone.tests.unit.test_sql_upgrade + +WARNING:: + + Your database will be wiped. + + Do not do this against a database with valuable data as + all data will be lost. +""" + +import copy +import json +import uuid + +from migrate.versioning import api as versioning_api +from oslo_config import cfg +from oslo_db import exception as db_exception +from oslo_db.sqlalchemy import migration +from oslo_db.sqlalchemy import session as db_session +import six +from sqlalchemy.engine import reflection +import sqlalchemy.exc +from sqlalchemy import schema + +from keystone.common import sql +from keystone.common.sql import migrate_repo +from keystone.common.sql import migration_helpers +from keystone.contrib import federation +from keystone.contrib import revoke +from keystone import exception +from keystone.tests import unit as tests +from keystone.tests.unit import default_fixtures +from keystone.tests.unit.ksfixtures import database + + +CONF = cfg.CONF +DEFAULT_DOMAIN_ID = CONF.identity.default_domain_id + +# NOTE(morganfainberg): This should be updated when each DB migration collapse +# is done to mirror the expected structure of the DB in the format of +# { : [, , ...], ... } +INITIAL_TABLE_STRUCTURE = { + 'credential': [ + 'id', 'user_id', 'project_id', 'blob', 'type', 'extra', + ], + 'domain': [ + 'id', 'name', 'enabled', 'extra', + ], + 'endpoint': [ + 'id', 'legacy_endpoint_id', 'interface', 'region', 'service_id', 'url', + 'enabled', 'extra', + ], + 'group': [ + 'id', 'domain_id', 'name', 'description', 'extra', + ], + 'policy': [ + 'id', 'type', 'blob', 'extra', + ], + 'project': [ + 'id', 'name', 'extra', 'description', 'enabled', 'domain_id', + ], + 'role': [ + 'id', 'name', 'extra', + ], + 'service': [ + 'id', 'type', 'extra', 'enabled', + ], + 'token': [ + 'id', 'expires', 'extra', 'valid', 'trust_id', 'user_id', + ], + 'trust': [ + 'id', 'trustor_user_id', 'trustee_user_id', 'project_id', + 'impersonation', 'deleted_at', 'expires_at', 'remaining_uses', 'extra', + ], + 'trust_role': [ + 'trust_id', 'role_id', + ], + 'user': [ + 'id', 'name', 'extra', 'password', 'enabled', 'domain_id', + 'default_project_id', + ], + 'user_group_membership': [ + 'user_id', 'group_id', + ], + 'region': [ + 'id', 'description', 'parent_region_id', 'extra', + ], + 'assignment': [ + 'type', 'actor_id', 'target_id', 'role_id', 'inherited', + ], +} + + +INITIAL_EXTENSION_TABLE_STRUCTURE = { + 'revocation_event': [ + 'id', 'domain_id', 'project_id', 'user_id', 'role_id', + 'trust_id', 'consumer_id', 'access_token_id', + 'issued_before', 'expires_at', 'revoked_at', 'audit_id', + 'audit_chain_id', + ], +} + +EXTENSIONS = {'federation': federation, + 'revoke': revoke} + + +class SqlMigrateBase(tests.SQLDriverOverrides, tests.TestCase): + def initialize_sql(self): + self.metadata = sqlalchemy.MetaData() + self.metadata.bind = self.engine + + def config_files(self): + config_files = super(SqlMigrateBase, self).config_files() + config_files.append(tests.dirs.tests_conf('backend_sql.conf')) + return config_files + + def repo_package(self): + return sql + + def setUp(self): + super(SqlMigrateBase, self).setUp() + database.initialize_sql_session() + conn_str = CONF.database.connection + if (conn_str != tests.IN_MEM_DB_CONN_STRING and + conn_str.startswith('sqlite') and + conn_str[10:] == tests.DEFAULT_TEST_DB_FILE): + # Override the default with a DB that is specific to the migration + # tests only if the DB Connection string is the same as the global + # default. This is required so that no conflicts occur due to the + # global default DB already being under migrate control. This is + # only needed if the DB is not-in-memory + db_file = tests.dirs.tmp('keystone_migrate_test.db') + self.config_fixture.config( + group='database', + connection='sqlite:///%s' % db_file) + + # create and share a single sqlalchemy engine for testing + self.engine = sql.get_engine() + self.Session = db_session.get_maker(self.engine, autocommit=False) + + self.initialize_sql() + self.repo_path = migration_helpers.find_migrate_repo( + self.repo_package()) + self.schema = versioning_api.ControlledSchema.create( + self.engine, + self.repo_path, self.initial_db_version) + + # auto-detect the highest available schema version in the migrate_repo + self.max_version = self.schema.repository.version().version + + def tearDown(self): + sqlalchemy.orm.session.Session.close_all() + meta = sqlalchemy.MetaData() + meta.bind = self.engine + meta.reflect(self.engine) + + with self.engine.begin() as conn: + inspector = reflection.Inspector.from_engine(self.engine) + metadata = schema.MetaData() + tbs = [] + all_fks = [] + + for table_name in inspector.get_table_names(): + fks = [] + for fk in inspector.get_foreign_keys(table_name): + if not fk['name']: + continue + fks.append( + schema.ForeignKeyConstraint((), (), name=fk['name'])) + table = schema.Table(table_name, metadata, *fks) + tbs.append(table) + all_fks.extend(fks) + + for fkc in all_fks: + conn.execute(schema.DropConstraint(fkc)) + + for table in tbs: + conn.execute(schema.DropTable(table)) + + sql.cleanup() + super(SqlMigrateBase, self).tearDown() + + def select_table(self, name): + table = sqlalchemy.Table(name, + self.metadata, + autoload=True) + s = sqlalchemy.select([table]) + return s + + def assertTableExists(self, table_name): + try: + self.select_table(table_name) + except sqlalchemy.exc.NoSuchTableError: + raise AssertionError('Table "%s" does not exist' % table_name) + + def assertTableDoesNotExist(self, table_name): + """Asserts that a given table exists cannot be selected by name.""" + # Switch to a different metadata otherwise you might still + # detect renamed or dropped tables + try: + temp_metadata = sqlalchemy.MetaData() + temp_metadata.bind = self.engine + sqlalchemy.Table(table_name, temp_metadata, autoload=True) + except sqlalchemy.exc.NoSuchTableError: + pass + else: + raise AssertionError('Table "%s" already exists' % table_name) + + def upgrade(self, *args, **kwargs): + self._migrate(*args, **kwargs) + + def downgrade(self, *args, **kwargs): + self._migrate(*args, downgrade=True, **kwargs) + + def _migrate(self, version, repository=None, downgrade=False, + current_schema=None): + repository = repository or self.repo_path + err = '' + version = versioning_api._migrate_version(self.schema, + version, + not downgrade, + err) + if not current_schema: + current_schema = self.schema + changeset = current_schema.changeset(version) + for ver, change in changeset: + self.schema.runchange(ver, change, changeset.step) + self.assertEqual(self.schema.version, version) + + def assertTableColumns(self, table_name, expected_cols): + """Asserts that the table contains the expected set of columns.""" + self.initialize_sql() + table = self.select_table(table_name) + actual_cols = [col.name for col in table.columns] + # Check if the columns are equal, but allow for a different order, + # which might occur after an upgrade followed by a downgrade + self.assertItemsEqual(expected_cols, actual_cols, + '%s table' % table_name) + + @property + def initial_db_version(self): + return getattr(self, '_initial_db_version', 0) + + +class SqlUpgradeTests(SqlMigrateBase): + + _initial_db_version = migrate_repo.DB_INIT_VERSION + + def test_blank_db_to_start(self): + self.assertTableDoesNotExist('user') + + def test_start_version_db_init_version(self): + version = migration.db_version(sql.get_engine(), self.repo_path, + migrate_repo.DB_INIT_VERSION) + self.assertEqual( + migrate_repo.DB_INIT_VERSION, + version, + 'DB is not at version %s' % migrate_repo.DB_INIT_VERSION) + + def test_two_steps_forward_one_step_back(self): + """You should be able to cleanly undo and re-apply all upgrades. + + Upgrades are run in the following order:: + + Starting with the initial version defined at + keystone.common.migrate_repo.DB_INIT_VERSION + + INIT +1 -> INIT +2 -> INIT +1 -> INIT +2 -> INIT +3 -> INIT +2 ... + ^---------------------^ ^---------------------^ + + Downgrade to the DB_INIT_VERSION does not occur based on the + requirement that the base version be DB_INIT_VERSION + 1 before + migration can occur. Downgrade below DB_INIT_VERSION + 1 is no longer + supported. + + DB_INIT_VERSION is the number preceding the release schema version from + two releases prior. Example, Juno releases with the DB_INIT_VERSION + being 35 where Havana (Havana was two releases before Juno) release + schema version is 36. + + The migrate utility requires the db must be initialized under version + control with the revision directly before the first version to be + applied. + + """ + for x in range(migrate_repo.DB_INIT_VERSION + 1, + self.max_version + 1): + self.upgrade(x) + downgrade_ver = x - 1 + # Don't actually downgrade to the init version. This will raise + # a not-implemented error. + if downgrade_ver != migrate_repo.DB_INIT_VERSION: + self.downgrade(x - 1) + self.upgrade(x) + + def test_upgrade_add_initial_tables(self): + self.upgrade(migrate_repo.DB_INIT_VERSION + 1) + self.check_initial_table_structure() + + def check_initial_table_structure(self): + for table in INITIAL_TABLE_STRUCTURE: + self.assertTableColumns(table, INITIAL_TABLE_STRUCTURE[table]) + + # Ensure the default domain was properly created. + default_domain = migration_helpers.get_default_domain() + + meta = sqlalchemy.MetaData() + meta.bind = self.engine + + domain_table = sqlalchemy.Table('domain', meta, autoload=True) + + session = self.Session() + q = session.query(domain_table) + refs = q.all() + + self.assertEqual(1, len(refs)) + for k in default_domain.keys(): + self.assertEqual(default_domain[k], getattr(refs[0], k)) + + def test_downgrade_to_db_init_version(self): + self.upgrade(self.max_version) + + if self.engine.name == 'mysql': + self._mysql_check_all_tables_innodb() + + self.downgrade(migrate_repo.DB_INIT_VERSION + 1) + self.check_initial_table_structure() + + meta = sqlalchemy.MetaData() + meta.bind = self.engine + meta.reflect(self.engine) + + initial_table_set = set(INITIAL_TABLE_STRUCTURE.keys()) + table_set = set(meta.tables.keys()) + # explicitly remove the migrate_version table, this is not controlled + # by the migration scripts and should be exempt from this check. + table_set.remove('migrate_version') + + self.assertSetEqual(initial_table_set, table_set) + # Downgrade to before Icehouse's release schema version (044) is not + # supported. A NotImplementedError should be raised when attempting to + # downgrade. + self.assertRaises(NotImplementedError, self.downgrade, + migrate_repo.DB_INIT_VERSION) + + def insert_dict(self, session, table_name, d, table=None): + """Naively inserts key-value pairs into a table, given a dictionary.""" + if table is None: + this_table = sqlalchemy.Table(table_name, self.metadata, + autoload=True) + else: + this_table = table + insert = this_table.insert().values(**d) + session.execute(insert) + session.commit() + + def test_id_mapping(self): + self.upgrade(50) + self.assertTableDoesNotExist('id_mapping') + self.upgrade(51) + self.assertTableExists('id_mapping') + self.downgrade(50) + self.assertTableDoesNotExist('id_mapping') + + def test_region_url_upgrade(self): + self.upgrade(52) + self.assertTableColumns('region', + ['id', 'description', 'parent_region_id', + 'extra', 'url']) + + def test_region_url_downgrade(self): + self.upgrade(52) + self.downgrade(51) + self.assertTableColumns('region', + ['id', 'description', 'parent_region_id', + 'extra']) + + def test_region_url_cleanup(self): + # make sure that the url field is dropped in the downgrade + self.upgrade(52) + session = self.Session() + beta = { + 'id': uuid.uuid4().hex, + 'description': uuid.uuid4().hex, + 'parent_region_id': uuid.uuid4().hex, + 'url': uuid.uuid4().hex + } + acme = { + 'id': uuid.uuid4().hex, + 'description': uuid.uuid4().hex, + 'parent_region_id': uuid.uuid4().hex, + 'url': None + } + self.insert_dict(session, 'region', beta) + self.insert_dict(session, 'region', acme) + region_table = sqlalchemy.Table('region', self.metadata, autoload=True) + self.assertEqual(2, session.query(region_table).count()) + session.close() + self.downgrade(51) + session = self.Session() + self.metadata.clear() + region_table = sqlalchemy.Table('region', self.metadata, autoload=True) + self.assertEqual(2, session.query(region_table).count()) + region = session.query(region_table)[0] + self.assertRaises(AttributeError, getattr, region, 'url') + + def test_endpoint_region_upgrade_columns(self): + self.upgrade(53) + self.assertTableColumns('endpoint', + ['id', 'legacy_endpoint_id', 'interface', + 'service_id', 'url', 'extra', 'enabled', + 'region_id']) + region_table = sqlalchemy.Table('region', self.metadata, autoload=True) + self.assertEqual(255, region_table.c.id.type.length) + self.assertEqual(255, region_table.c.parent_region_id.type.length) + endpoint_table = sqlalchemy.Table('endpoint', + self.metadata, + autoload=True) + self.assertEqual(255, endpoint_table.c.region_id.type.length) + + def test_endpoint_region_downgrade_columns(self): + self.upgrade(53) + self.downgrade(52) + self.assertTableColumns('endpoint', + ['id', 'legacy_endpoint_id', 'interface', + 'service_id', 'url', 'extra', 'enabled', + 'region']) + region_table = sqlalchemy.Table('region', self.metadata, autoload=True) + self.assertEqual(64, region_table.c.id.type.length) + self.assertEqual(64, region_table.c.parent_region_id.type.length) + endpoint_table = sqlalchemy.Table('endpoint', + self.metadata, + autoload=True) + self.assertEqual(255, endpoint_table.c.region.type.length) + + def test_endpoint_region_migration(self): + self.upgrade(52) + session = self.Session() + _small_region_name = '0' * 30 + _long_region_name = '0' * 255 + _clashing_region_name = '0' * 70 + + def add_service(): + service_id = uuid.uuid4().hex + + service = { + 'id': service_id, + 'type': uuid.uuid4().hex + } + + self.insert_dict(session, 'service', service) + + return service_id + + def add_endpoint(service_id, region): + endpoint_id = uuid.uuid4().hex + + endpoint = { + 'id': endpoint_id, + 'interface': uuid.uuid4().hex[:8], + 'service_id': service_id, + 'url': uuid.uuid4().hex, + 'region': region + } + self.insert_dict(session, 'endpoint', endpoint) + + return endpoint_id + + _service_id_ = add_service() + add_endpoint(_service_id_, region=_long_region_name) + add_endpoint(_service_id_, region=_long_region_name) + add_endpoint(_service_id_, region=_clashing_region_name) + add_endpoint(_service_id_, region=_small_region_name) + add_endpoint(_service_id_, region=None) + + # upgrade to 53 + session.close() + self.upgrade(53) + session = self.Session() + self.metadata.clear() + + region_table = sqlalchemy.Table('region', self.metadata, autoload=True) + self.assertEqual(1, session.query(region_table). + filter_by(id=_long_region_name).count()) + self.assertEqual(1, session.query(region_table). + filter_by(id=_clashing_region_name).count()) + self.assertEqual(1, session.query(region_table). + filter_by(id=_small_region_name).count()) + + endpoint_table = sqlalchemy.Table('endpoint', + self.metadata, + autoload=True) + self.assertEqual(5, session.query(endpoint_table).count()) + self.assertEqual(2, session.query(endpoint_table). + filter_by(region_id=_long_region_name).count()) + self.assertEqual(1, session.query(endpoint_table). + filter_by(region_id=_clashing_region_name).count()) + self.assertEqual(1, session.query(endpoint_table). + filter_by(region_id=_small_region_name).count()) + + # downgrade to 52 + session.close() + self.downgrade(52) + session = self.Session() + self.metadata.clear() + + region_table = sqlalchemy.Table('region', self.metadata, autoload=True) + self.assertEqual(1, session.query(region_table).count()) + self.assertEqual(1, session.query(region_table). + filter_by(id=_small_region_name).count()) + + endpoint_table = sqlalchemy.Table('endpoint', + self.metadata, + autoload=True) + self.assertEqual(5, session.query(endpoint_table).count()) + self.assertEqual(2, session.query(endpoint_table). + filter_by(region=_long_region_name).count()) + self.assertEqual(1, session.query(endpoint_table). + filter_by(region=_clashing_region_name).count()) + self.assertEqual(1, session.query(endpoint_table). + filter_by(region=_small_region_name).count()) + + def test_add_actor_id_index(self): + self.upgrade(53) + self.upgrade(54) + table = sqlalchemy.Table('assignment', self.metadata, autoload=True) + index_data = [(idx.name, idx.columns.keys()) for idx in table.indexes] + self.assertIn(('ix_actor_id', ['actor_id']), index_data) + + def test_token_user_id_and_trust_id_index_upgrade(self): + self.upgrade(54) + self.upgrade(55) + table = sqlalchemy.Table('token', self.metadata, autoload=True) + index_data = [(idx.name, idx.columns.keys()) for idx in table.indexes] + self.assertIn(('ix_token_user_id', ['user_id']), index_data) + self.assertIn(('ix_token_trust_id', ['trust_id']), index_data) + + def test_token_user_id_and_trust_id_index_downgrade(self): + self.upgrade(55) + self.downgrade(54) + table = sqlalchemy.Table('token', self.metadata, autoload=True) + index_data = [(idx.name, idx.columns.keys()) for idx in table.indexes] + self.assertNotIn(('ix_token_user_id', ['user_id']), index_data) + self.assertNotIn(('ix_token_trust_id', ['trust_id']), index_data) + + def test_remove_actor_id_index(self): + self.upgrade(54) + self.downgrade(53) + table = sqlalchemy.Table('assignment', self.metadata, autoload=True) + index_data = [(idx.name, idx.columns.keys()) for idx in table.indexes] + self.assertNotIn(('ix_actor_id', ['actor_id']), index_data) + + def test_project_parent_id_upgrade(self): + self.upgrade(61) + self.assertTableColumns('project', + ['id', 'name', 'extra', 'description', + 'enabled', 'domain_id', 'parent_id']) + + def test_project_parent_id_downgrade(self): + self.upgrade(61) + self.downgrade(60) + self.assertTableColumns('project', + ['id', 'name', 'extra', 'description', + 'enabled', 'domain_id']) + + def test_project_parent_id_cleanup(self): + # make sure that the parent_id field is dropped in the downgrade + self.upgrade(61) + session = self.Session() + domain = {'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'enabled': True} + acme = { + 'id': uuid.uuid4().hex, + 'description': uuid.uuid4().hex, + 'domain_id': domain['id'], + 'name': uuid.uuid4().hex, + 'parent_id': None + } + beta = { + 'id': uuid.uuid4().hex, + 'description': uuid.uuid4().hex, + 'domain_id': domain['id'], + 'name': uuid.uuid4().hex, + 'parent_id': acme['id'] + } + self.insert_dict(session, 'domain', domain) + self.insert_dict(session, 'project', acme) + self.insert_dict(session, 'project', beta) + proj_table = sqlalchemy.Table('project', self.metadata, autoload=True) + self.assertEqual(2, session.query(proj_table).count()) + session.close() + self.downgrade(60) + session = self.Session() + self.metadata.clear() + proj_table = sqlalchemy.Table('project', self.metadata, autoload=True) + self.assertEqual(2, session.query(proj_table).count()) + project = session.query(proj_table)[0] + self.assertRaises(AttributeError, getattr, project, 'parent_id') + + def test_drop_assignment_role_fk(self): + self.upgrade(61) + self.assertTrue(self.does_fk_exist('assignment', 'role_id')) + self.upgrade(62) + if self.engine.name != 'sqlite': + # sqlite does not support FK deletions (or enforcement) + self.assertFalse(self.does_fk_exist('assignment', 'role_id')) + self.downgrade(61) + self.assertTrue(self.does_fk_exist('assignment', 'role_id')) + + def does_fk_exist(self, table, fk_column): + inspector = reflection.Inspector.from_engine(self.engine) + for fk in inspector.get_foreign_keys(table): + if fk_column in fk['constrained_columns']: + return True + return False + + def test_drop_region_url_upgrade(self): + self.upgrade(63) + self.assertTableColumns('region', + ['id', 'description', 'parent_region_id', + 'extra']) + + def test_drop_region_url_downgrade(self): + self.upgrade(63) + self.downgrade(62) + self.assertTableColumns('region', + ['id', 'description', 'parent_region_id', + 'extra', 'url']) + + def test_drop_domain_fk(self): + self.upgrade(63) + self.assertTrue(self.does_fk_exist('group', 'domain_id')) + self.assertTrue(self.does_fk_exist('user', 'domain_id')) + self.upgrade(64) + if self.engine.name != 'sqlite': + # sqlite does not support FK deletions (or enforcement) + self.assertFalse(self.does_fk_exist('group', 'domain_id')) + self.assertFalse(self.does_fk_exist('user', 'domain_id')) + self.downgrade(63) + self.assertTrue(self.does_fk_exist('group', 'domain_id')) + self.assertTrue(self.does_fk_exist('user', 'domain_id')) + + def test_add_domain_config(self): + whitelisted_table = 'whitelisted_config' + sensitive_table = 'sensitive_config' + self.upgrade(64) + self.assertTableDoesNotExist(whitelisted_table) + self.assertTableDoesNotExist(sensitive_table) + self.upgrade(65) + self.assertTableColumns(whitelisted_table, + ['domain_id', 'group', 'option', 'value']) + self.assertTableColumns(sensitive_table, + ['domain_id', 'group', 'option', 'value']) + self.downgrade(64) + self.assertTableDoesNotExist(whitelisted_table) + self.assertTableDoesNotExist(sensitive_table) + + def test_fixup_service_name_value_upgrade(self): + """Update service name data from `extra` to empty string.""" + def add_service(**extra_data): + service_id = uuid.uuid4().hex + + service = { + 'id': service_id, + 'type': uuid.uuid4().hex, + 'extra': json.dumps(extra_data), + } + + self.insert_dict(session, 'service', service) + + return service_id + + self.upgrade(65) + session = self.Session() + + # Services with extra values having a random attribute and + # different combinations of name + random_attr_name = uuid.uuid4().hex + random_attr_value = uuid.uuid4().hex + random_attr_str = "%s='%s'" % (random_attr_name, random_attr_value) + random_attr_no_name = {random_attr_name: random_attr_value} + random_attr_no_name_str = "%s='%s'" % (random_attr_name, + random_attr_value) + random_attr_name_value = {random_attr_name: random_attr_value, + 'name': 'myname'} + random_attr_name_value_str = 'name=myname,%s' % random_attr_str + random_attr_name_empty = {random_attr_name: random_attr_value, + 'name': ''} + random_attr_name_empty_str = 'name=,%s' % random_attr_str + random_attr_name_none = {random_attr_name: random_attr_value, + 'name': None} + random_attr_name_none_str = 'name=None,%s' % random_attr_str + + services = [ + (add_service(**random_attr_no_name), + random_attr_name_empty, random_attr_no_name_str), + (add_service(**random_attr_name_value), + random_attr_name_value, random_attr_name_value_str), + (add_service(**random_attr_name_empty), + random_attr_name_empty, random_attr_name_empty_str), + (add_service(**random_attr_name_none), + random_attr_name_empty, random_attr_name_none_str), + ] + + session.close() + self.upgrade(66) + session = self.Session() + + # Verify that the services have the expected values. + self.metadata.clear() + service_table = sqlalchemy.Table('service', self.metadata, + autoload=True) + + def fetch_service_extra(service_id): + cols = [service_table.c.extra] + f = service_table.c.id == service_id + s = sqlalchemy.select(cols).where(f) + service = session.execute(s).fetchone() + return json.loads(service.extra) + + for service_id, exp_extra, msg in services: + extra = fetch_service_extra(service_id) + self.assertDictEqual(exp_extra, extra, msg) + + def populate_user_table(self, with_pass_enab=False, + with_pass_enab_domain=False): + # Populate the appropriate fields in the user + # table, depending on the parameters: + # + # Default: id, name, extra + # pass_enab: Add password, enabled as well + # pass_enab_domain: Add password, enabled and domain as well + # + this_table = sqlalchemy.Table("user", + self.metadata, + autoload=True) + for user in default_fixtures.USERS: + extra = copy.deepcopy(user) + extra.pop('id') + extra.pop('name') + + if with_pass_enab: + password = extra.pop('password', None) + enabled = extra.pop('enabled', True) + ins = this_table.insert().values( + {'id': user['id'], + 'name': user['name'], + 'password': password, + 'enabled': bool(enabled), + 'extra': json.dumps(extra)}) + else: + if with_pass_enab_domain: + password = extra.pop('password', None) + enabled = extra.pop('enabled', True) + extra.pop('domain_id') + ins = this_table.insert().values( + {'id': user['id'], + 'name': user['name'], + 'domain_id': user['domain_id'], + 'password': password, + 'enabled': bool(enabled), + 'extra': json.dumps(extra)}) + else: + ins = this_table.insert().values( + {'id': user['id'], + 'name': user['name'], + 'extra': json.dumps(extra)}) + self.engine.execute(ins) + + def populate_tenant_table(self, with_desc_enab=False, + with_desc_enab_domain=False): + # Populate the appropriate fields in the tenant or + # project table, depending on the parameters + # + # Default: id, name, extra + # desc_enab: Add description, enabled as well + # desc_enab_domain: Add description, enabled and domain as well, + # plus use project instead of tenant + # + if with_desc_enab_domain: + # By this time tenants are now projects + this_table = sqlalchemy.Table("project", + self.metadata, + autoload=True) + else: + this_table = sqlalchemy.Table("tenant", + self.metadata, + autoload=True) + + for tenant in default_fixtures.TENANTS: + extra = copy.deepcopy(tenant) + extra.pop('id') + extra.pop('name') + + if with_desc_enab: + desc = extra.pop('description', None) + enabled = extra.pop('enabled', True) + ins = this_table.insert().values( + {'id': tenant['id'], + 'name': tenant['name'], + 'description': desc, + 'enabled': bool(enabled), + 'extra': json.dumps(extra)}) + else: + if with_desc_enab_domain: + desc = extra.pop('description', None) + enabled = extra.pop('enabled', True) + extra.pop('domain_id') + ins = this_table.insert().values( + {'id': tenant['id'], + 'name': tenant['name'], + 'domain_id': tenant['domain_id'], + 'description': desc, + 'enabled': bool(enabled), + 'extra': json.dumps(extra)}) + else: + ins = this_table.insert().values( + {'id': tenant['id'], + 'name': tenant['name'], + 'extra': json.dumps(extra)}) + self.engine.execute(ins) + + def _mysql_check_all_tables_innodb(self): + database = self.engine.url.database + + connection = self.engine.connect() + # sanity check + total = connection.execute("SELECT count(*) " + "from information_schema.TABLES " + "where TABLE_SCHEMA='%(database)s'" % + dict(database=database)) + self.assertTrue(total.scalar() > 0, "No tables found. Wrong schema?") + + noninnodb = connection.execute("SELECT table_name " + "from information_schema.TABLES " + "where TABLE_SCHEMA='%(database)s' " + "and ENGINE!='InnoDB' " + "and TABLE_NAME!='migrate_version'" % + dict(database=database)) + names = [x[0] for x in noninnodb] + self.assertEqual([], names, + "Non-InnoDB tables exist") + + connection.close() + + +class VersionTests(SqlMigrateBase): + + _initial_db_version = migrate_repo.DB_INIT_VERSION + + def test_core_initial(self): + """Get the version before migrated, it's the initial DB version.""" + version = migration_helpers.get_db_version() + self.assertEqual(migrate_repo.DB_INIT_VERSION, version) + + def test_core_max(self): + """When get the version after upgrading, it's the new version.""" + self.upgrade(self.max_version) + version = migration_helpers.get_db_version() + self.assertEqual(self.max_version, version) + + def test_extension_not_controlled(self): + """When get the version before controlling, raises DbMigrationError.""" + self.assertRaises(db_exception.DbMigrationError, + migration_helpers.get_db_version, + extension='federation') + + def test_extension_initial(self): + """When get the initial version of an extension, it's 0.""" + for name, extension in six.iteritems(EXTENSIONS): + abs_path = migration_helpers.find_migrate_repo(extension) + migration.db_version_control(sql.get_engine(), abs_path) + version = migration_helpers.get_db_version(extension=name) + self.assertEqual(0, version, + 'Migrate version for %s is not 0' % name) + + def test_extension_migrated(self): + """When get the version after migrating an extension, it's not 0.""" + for name, extension in six.iteritems(EXTENSIONS): + abs_path = migration_helpers.find_migrate_repo(extension) + migration.db_version_control(sql.get_engine(), abs_path) + migration.db_sync(sql.get_engine(), abs_path) + version = migration_helpers.get_db_version(extension=name) + self.assertTrue( + version > 0, + "Version for %s didn't change after migrated?" % name) + + def test_extension_downgraded(self): + """When get the version after downgrading an extension, it is 0.""" + for name, extension in six.iteritems(EXTENSIONS): + abs_path = migration_helpers.find_migrate_repo(extension) + migration.db_version_control(sql.get_engine(), abs_path) + migration.db_sync(sql.get_engine(), abs_path) + version = migration_helpers.get_db_version(extension=name) + self.assertTrue( + version > 0, + "Version for %s didn't change after migrated?" % name) + migration.db_sync(sql.get_engine(), abs_path, version=0) + version = migration_helpers.get_db_version(extension=name) + self.assertEqual(0, version, + 'Migrate version for %s is not 0' % name) + + def test_unexpected_extension(self): + """The version for an extension that doesn't exist raises ImportError. + + """ + + extension_name = uuid.uuid4().hex + self.assertRaises(ImportError, + migration_helpers.get_db_version, + extension=extension_name) + + def test_unversioned_extension(self): + """The version for extensions without migrations raise an exception. + + """ + + self.assertRaises(exception.MigrationNotProvided, + migration_helpers.get_db_version, + extension='admin_crud') + + def test_initial_with_extension_version_None(self): + """When performing a default migration, also migrate extensions.""" + migration_helpers.sync_database_to_version(extension=None, + version=None) + for table in INITIAL_EXTENSION_TABLE_STRUCTURE: + self.assertTableColumns(table, + INITIAL_EXTENSION_TABLE_STRUCTURE[table]) + + def test_initial_with_extension_version_max(self): + """When migrating to max version, do not migrate extensions.""" + migration_helpers.sync_database_to_version(extension=None, + version=self.max_version) + for table in INITIAL_EXTENSION_TABLE_STRUCTURE: + self.assertTableDoesNotExist(table) diff --git a/keystone-moon/keystone/tests/unit/test_ssl.py b/keystone-moon/keystone/tests/unit/test_ssl.py new file mode 100644 index 00000000..c5f443b0 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_ssl.py @@ -0,0 +1,176 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 os +import ssl + +from oslo_config import cfg + +from keystone.common import environment +from keystone.tests import unit as tests +from keystone.tests.unit.ksfixtures import appserver + + +CONF = cfg.CONF + +CERTDIR = tests.dirs.root('examples', 'pki', 'certs') +KEYDIR = tests.dirs.root('examples', 'pki', 'private') +CERT = os.path.join(CERTDIR, 'ssl_cert.pem') +KEY = os.path.join(KEYDIR, 'ssl_key.pem') +CA = os.path.join(CERTDIR, 'cacert.pem') +CLIENT = os.path.join(CERTDIR, 'middleware.pem') + + +class SSLTestCase(tests.TestCase): + def setUp(self): + super(SSLTestCase, self).setUp() + # NOTE(jamespage): + # Deal with more secure certificate chain verification + # introduced in python 2.7.9 under PEP-0476 + # https://github.com/python/peps/blob/master/pep-0476.txt + self.context = None + if hasattr(ssl, '_create_unverified_context'): + self.context = ssl._create_unverified_context() + self.load_backends() + + def get_HTTPSConnection(self, *args): + """Simple helper to configure HTTPSConnection objects.""" + if self.context: + return environment.httplib.HTTPSConnection( + *args, + context=self.context + ) + else: + return environment.httplib.HTTPSConnection(*args) + + def test_1way_ssl_ok(self): + """Make sure both public and admin API work with 1-way SSL.""" + paste_conf = self._paste_config('keystone') + ssl_kwargs = dict(cert=CERT, key=KEY, ca=CA) + + # Verify Admin + with appserver.AppServer(paste_conf, appserver.ADMIN, **ssl_kwargs): + conn = self.get_HTTPSConnection( + '127.0.0.1', CONF.eventlet_server.admin_port) + conn.request('GET', '/') + resp = conn.getresponse() + self.assertEqual(300, resp.status) + + # Verify Public + with appserver.AppServer(paste_conf, appserver.MAIN, **ssl_kwargs): + conn = self.get_HTTPSConnection( + '127.0.0.1', CONF.eventlet_server.public_port) + conn.request('GET', '/') + resp = conn.getresponse() + self.assertEqual(300, resp.status) + + def test_2way_ssl_ok(self): + """Make sure both public and admin API work with 2-way SSL. + + Requires client certificate. + """ + paste_conf = self._paste_config('keystone') + ssl_kwargs = dict(cert=CERT, key=KEY, ca=CA, cert_required=True) + + # Verify Admin + with appserver.AppServer(paste_conf, appserver.ADMIN, **ssl_kwargs): + conn = self.get_HTTPSConnection( + '127.0.0.1', CONF.eventlet_server.admin_port, CLIENT, CLIENT) + conn.request('GET', '/') + resp = conn.getresponse() + self.assertEqual(300, resp.status) + + # Verify Public + with appserver.AppServer(paste_conf, appserver.MAIN, **ssl_kwargs): + conn = self.get_HTTPSConnection( + '127.0.0.1', CONF.eventlet_server.public_port, CLIENT, CLIENT) + conn.request('GET', '/') + resp = conn.getresponse() + self.assertEqual(300, resp.status) + + def test_1way_ssl_with_ipv6_ok(self): + """Make sure both public and admin API work with 1-way ipv6 & SSL.""" + self.skip_if_no_ipv6() + + paste_conf = self._paste_config('keystone') + ssl_kwargs = dict(cert=CERT, key=KEY, ca=CA, host="::1") + + # Verify Admin + with appserver.AppServer(paste_conf, appserver.ADMIN, **ssl_kwargs): + conn = self.get_HTTPSConnection( + '::1', CONF.eventlet_server.admin_port) + conn.request('GET', '/') + resp = conn.getresponse() + self.assertEqual(300, resp.status) + + # Verify Public + with appserver.AppServer(paste_conf, appserver.MAIN, **ssl_kwargs): + conn = self.get_HTTPSConnection( + '::1', CONF.eventlet_server.public_port) + conn.request('GET', '/') + resp = conn.getresponse() + self.assertEqual(300, resp.status) + + def test_2way_ssl_with_ipv6_ok(self): + """Make sure both public and admin API work with 2-way ipv6 & SSL. + + Requires client certificate. + """ + self.skip_if_no_ipv6() + + paste_conf = self._paste_config('keystone') + ssl_kwargs = dict(cert=CERT, key=KEY, ca=CA, + cert_required=True, host="::1") + + # Verify Admin + with appserver.AppServer(paste_conf, appserver.ADMIN, **ssl_kwargs): + conn = self.get_HTTPSConnection( + '::1', CONF.eventlet_server.admin_port, CLIENT, CLIENT) + conn.request('GET', '/') + resp = conn.getresponse() + self.assertEqual(300, resp.status) + + # Verify Public + with appserver.AppServer(paste_conf, appserver.MAIN, **ssl_kwargs): + conn = self.get_HTTPSConnection( + '::1', CONF.eventlet_server.public_port, CLIENT, CLIENT) + conn.request('GET', '/') + resp = conn.getresponse() + self.assertEqual(300, resp.status) + + def test_2way_ssl_fail(self): + """Expect to fail when client does not present proper certificate.""" + paste_conf = self._paste_config('keystone') + ssl_kwargs = dict(cert=CERT, key=KEY, ca=CA, cert_required=True) + + # Verify Admin + with appserver.AppServer(paste_conf, appserver.ADMIN, **ssl_kwargs): + conn = self.get_HTTPSConnection( + '127.0.0.1', CONF.eventlet_server.admin_port) + try: + conn.request('GET', '/') + self.fail('Admin API shoulda failed with SSL handshake!') + except ssl.SSLError: + pass + + # Verify Public + with appserver.AppServer(paste_conf, appserver.MAIN, **ssl_kwargs): + conn = self.get_HTTPSConnection( + '127.0.0.1', CONF.eventlet_server.public_port) + try: + conn.request('GET', '/') + self.fail('Public API shoulda failed with SSL handshake!') + except ssl.SSLError: + pass diff --git a/keystone-moon/keystone/tests/unit/test_token_bind.py b/keystone-moon/keystone/tests/unit/test_token_bind.py new file mode 100644 index 00000000..7dc7ccca --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_token_bind.py @@ -0,0 +1,198 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 +import uuid + +from keystone.common import wsgi +from keystone import exception +from keystone.models import token_model +from keystone.tests import unit as tests +from keystone.tests.unit import test_token_provider + + +KERBEROS_BIND = 'USER@REALM' +ANY = 'any' + + +class BindTest(tests.TestCase): + """Test binding tokens to a Principal. + + Even though everything in this file references kerberos the same concepts + will apply to all future binding mechanisms. + """ + + def setUp(self): + super(BindTest, self).setUp() + self.TOKEN_BIND_KERB = copy.deepcopy( + test_token_provider.SAMPLE_V3_TOKEN) + self.TOKEN_BIND_KERB['token']['bind'] = {'kerberos': KERBEROS_BIND} + self.TOKEN_BIND_UNKNOWN = copy.deepcopy( + test_token_provider.SAMPLE_V3_TOKEN) + self.TOKEN_BIND_UNKNOWN['token']['bind'] = {'FOO': 'BAR'} + self.TOKEN_BIND_NONE = copy.deepcopy( + test_token_provider.SAMPLE_V3_TOKEN) + + self.ALL_TOKENS = [self.TOKEN_BIND_KERB, self.TOKEN_BIND_UNKNOWN, + self.TOKEN_BIND_NONE] + + def assert_kerberos_bind(self, tokens, bind_level, + use_kerberos=True, success=True): + if not isinstance(tokens, dict): + for token in tokens: + self.assert_kerberos_bind(token, bind_level, + use_kerberos=use_kerberos, + success=success) + elif use_kerberos == ANY: + for val in (True, False): + self.assert_kerberos_bind(tokens, bind_level, + use_kerberos=val, success=success) + else: + context = {'environment': {}} + self.config_fixture.config(group='token', + enforce_token_bind=bind_level) + + if use_kerberos: + context['environment']['REMOTE_USER'] = KERBEROS_BIND + context['environment']['AUTH_TYPE'] = 'Negotiate' + + # NOTE(morganfainberg): This assumes a V3 token. + token_ref = token_model.KeystoneToken( + token_id=uuid.uuid4().hex, + token_data=tokens) + + if not success: + self.assertRaises(exception.Unauthorized, + wsgi.validate_token_bind, + context, token_ref) + else: + wsgi.validate_token_bind(context, token_ref) + + # DISABLED + + def test_bind_disabled_with_kerb_user(self): + self.assert_kerberos_bind(self.ALL_TOKENS, + bind_level='disabled', + use_kerberos=ANY, + success=True) + + # PERMISSIVE + + def test_bind_permissive_with_kerb_user(self): + self.assert_kerberos_bind(self.TOKEN_BIND_KERB, + bind_level='permissive', + use_kerberos=True, + success=True) + + def test_bind_permissive_with_regular_token(self): + self.assert_kerberos_bind(self.TOKEN_BIND_NONE, + bind_level='permissive', + use_kerberos=ANY, + success=True) + + def test_bind_permissive_without_kerb_user(self): + self.assert_kerberos_bind(self.TOKEN_BIND_KERB, + bind_level='permissive', + use_kerberos=False, + success=False) + + def test_bind_permissive_with_unknown_bind(self): + self.assert_kerberos_bind(self.TOKEN_BIND_UNKNOWN, + bind_level='permissive', + use_kerberos=ANY, + success=True) + + # STRICT + + def test_bind_strict_with_regular_token(self): + self.assert_kerberos_bind(self.TOKEN_BIND_NONE, + bind_level='strict', + use_kerberos=ANY, + success=True) + + def test_bind_strict_with_kerb_user(self): + self.assert_kerberos_bind(self.TOKEN_BIND_KERB, + bind_level='strict', + use_kerberos=True, + success=True) + + def test_bind_strict_without_kerb_user(self): + self.assert_kerberos_bind(self.TOKEN_BIND_KERB, + bind_level='strict', + use_kerberos=False, + success=False) + + def test_bind_strict_with_unknown_bind(self): + self.assert_kerberos_bind(self.TOKEN_BIND_UNKNOWN, + bind_level='strict', + use_kerberos=ANY, + success=False) + + # REQUIRED + + def test_bind_required_with_regular_token(self): + self.assert_kerberos_bind(self.TOKEN_BIND_NONE, + bind_level='required', + use_kerberos=ANY, + success=False) + + def test_bind_required_with_kerb_user(self): + self.assert_kerberos_bind(self.TOKEN_BIND_KERB, + bind_level='required', + use_kerberos=True, + success=True) + + def test_bind_required_without_kerb_user(self): + self.assert_kerberos_bind(self.TOKEN_BIND_KERB, + bind_level='required', + use_kerberos=False, + success=False) + + def test_bind_required_with_unknown_bind(self): + self.assert_kerberos_bind(self.TOKEN_BIND_UNKNOWN, + bind_level='required', + use_kerberos=ANY, + success=False) + + # NAMED + + def test_bind_named_with_regular_token(self): + self.assert_kerberos_bind(self.TOKEN_BIND_NONE, + bind_level='kerberos', + use_kerberos=ANY, + success=False) + + def test_bind_named_with_kerb_user(self): + self.assert_kerberos_bind(self.TOKEN_BIND_KERB, + bind_level='kerberos', + use_kerberos=True, + success=True) + + def test_bind_named_without_kerb_user(self): + self.assert_kerberos_bind(self.TOKEN_BIND_KERB, + bind_level='kerberos', + use_kerberos=False, + success=False) + + def test_bind_named_with_unknown_bind(self): + self.assert_kerberos_bind(self.TOKEN_BIND_UNKNOWN, + bind_level='kerberos', + use_kerberos=ANY, + success=False) + + def test_bind_named_with_unknown_scheme(self): + self.assert_kerberos_bind(self.ALL_TOKENS, + bind_level='unknown', + use_kerberos=ANY, + success=False) diff --git a/keystone-moon/keystone/tests/unit/test_token_provider.py b/keystone-moon/keystone/tests/unit/test_token_provider.py new file mode 100644 index 00000000..dc08664f --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_token_provider.py @@ -0,0 +1,836 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 datetime + +from oslo_config import cfg +from oslo_utils import timeutils + +from keystone.common import dependency +from keystone import exception +from keystone.tests import unit as tests +from keystone.tests.unit.ksfixtures import database +from keystone import token +from keystone.token.providers import pki +from keystone.token.providers import uuid + + +CONF = cfg.CONF + +FUTURE_DELTA = datetime.timedelta(seconds=CONF.token.expiration) +CURRENT_DATE = timeutils.utcnow() + +SAMPLE_V2_TOKEN = { + "access": { + "trust": { + "id": "abc123", + "trustee_user_id": "123456", + "trustor_user_id": "333333", + "impersonation": False + }, + "serviceCatalog": [ + { + "endpoints": [ + { + "adminURL": "http://localhost:8774/v1.1/01257", + "id": "51934fe63a5b4ac0a32664f64eb462c3", + "internalURL": "http://localhost:8774/v1.1/01257", + "publicURL": "http://localhost:8774/v1.1/01257", + "region": "RegionOne" + } + ], + "endpoints_links": [], + "name": "nova", + "type": "compute" + }, + { + "endpoints": [ + { + "adminURL": "http://localhost:9292", + "id": "aaa17a539e364297a7845d67c7c7cc4b", + "internalURL": "http://localhost:9292", + "publicURL": "http://localhost:9292", + "region": "RegionOne" + } + ], + "endpoints_links": [], + "name": "glance", + "type": "image" + }, + { + "endpoints": [ + { + "adminURL": "http://localhost:8776/v1/01257", + "id": "077d82df25304abeac2294004441db5a", + "internalURL": "http://localhost:8776/v1/01257", + "publicURL": "http://localhost:8776/v1/01257", + "region": "RegionOne" + } + ], + "endpoints_links": [], + "name": "volume", + "type": "volume" + }, + { + "endpoints": [ + { + "adminURL": "http://localhost:8773/services/Admin", + "id": "b06997fd08414903ad458836efaa9067", + "internalURL": "http://localhost:8773/services/Cloud", + "publicURL": "http://localhost:8773/services/Cloud", + "region": "RegionOne" + } + ], + "endpoints_links": [], + "name": "ec2", + "type": "ec2" + }, + { + "endpoints": [ + { + "adminURL": "http://localhost:8080/v1", + "id": "7bd0c643e05a4a2ab40902b2fa0dd4e6", + "internalURL": "http://localhost:8080/v1/AUTH_01257", + "publicURL": "http://localhost:8080/v1/AUTH_01257", + "region": "RegionOne" + } + ], + "endpoints_links": [], + "name": "swift", + "type": "object-store" + }, + { + "endpoints": [ + { + "adminURL": "http://localhost:35357/v2.0", + "id": "02850c5d1d094887bdc46e81e1e15dc7", + "internalURL": "http://localhost:5000/v2.0", + "publicURL": "http://localhost:5000/v2.0", + "region": "RegionOne" + } + ], + "endpoints_links": [], + "name": "keystone", + "type": "identity" + } + ], + "token": { + "expires": "2013-05-22T00:02:43.941430Z", + "id": "ce4fc2d36eea4cc9a36e666ac2f1029a", + "issued_at": "2013-05-21T00:02:43.941473Z", + "tenant": { + "enabled": True, + "id": "01257", + "name": "service" + } + }, + "user": { + "id": "f19ddbe2c53c46f189fe66d0a7a9c9ce", + "name": "nova", + "roles": [ + { + "name": "_member_" + }, + { + "name": "admin" + } + ], + "roles_links": [], + "username": "nova" + } + } +} + +SAMPLE_V3_TOKEN = { + "token": { + "catalog": [ + { + "endpoints": [ + { + "id": "02850c5d1d094887bdc46e81e1e15dc7", + "interface": "admin", + "region": "RegionOne", + "url": "http://localhost:35357/v2.0" + }, + { + "id": "446e244b75034a9ab4b0811e82d0b7c8", + "interface": "internal", + "region": "RegionOne", + "url": "http://localhost:5000/v2.0" + }, + { + "id": "47fa3d9f499240abb5dfcf2668f168cd", + "interface": "public", + "region": "RegionOne", + "url": "http://localhost:5000/v2.0" + } + ], + "id": "26d7541715a44a4d9adad96f9872b633", + "type": "identity", + }, + { + "endpoints": [ + { + "id": "aaa17a539e364297a7845d67c7c7cc4b", + "interface": "admin", + "region": "RegionOne", + "url": "http://localhost:9292" + }, + { + "id": "4fa9620e42394cb1974736dce0856c71", + "interface": "internal", + "region": "RegionOne", + "url": "http://localhost:9292" + }, + { + "id": "9673687f9bc441d88dec37942bfd603b", + "interface": "public", + "region": "RegionOne", + "url": "http://localhost:9292" + } + ], + "id": "d27a41843f4e4b0e8cf6dac4082deb0d", + "type": "image", + }, + { + "endpoints": [ + { + "id": "7bd0c643e05a4a2ab40902b2fa0dd4e6", + "interface": "admin", + "region": "RegionOne", + "url": "http://localhost:8080/v1" + }, + { + "id": "43bef154594d4ccb8e49014d20624e1d", + "interface": "internal", + "region": "RegionOne", + "url": "http://localhost:8080/v1/AUTH_01257" + }, + { + "id": "e63b5f5d7aa3493690189d0ff843b9b3", + "interface": "public", + "region": "RegionOne", + "url": "http://localhost:8080/v1/AUTH_01257" + } + ], + "id": "a669e152f1104810a4b6701aade721bb", + "type": "object-store", + }, + { + "endpoints": [ + { + "id": "51934fe63a5b4ac0a32664f64eb462c3", + "interface": "admin", + "region": "RegionOne", + "url": "http://localhost:8774/v1.1/01257" + }, + { + "id": "869b535eea0d42e483ae9da0d868ebad", + "interface": "internal", + "region": "RegionOne", + "url": "http://localhost:8774/v1.1/01257" + }, + { + "id": "93583824c18f4263a2245ca432b132a6", + "interface": "public", + "region": "RegionOne", + "url": "http://localhost:8774/v1.1/01257" + } + ], + "id": "7f32cc2af6c9476e82d75f80e8b3bbb8", + "type": "compute", + }, + { + "endpoints": [ + { + "id": "b06997fd08414903ad458836efaa9067", + "interface": "admin", + "region": "RegionOne", + "url": "http://localhost:8773/services/Admin" + }, + { + "id": "411f7de7c9a8484c9b46c254fb2676e2", + "interface": "internal", + "region": "RegionOne", + "url": "http://localhost:8773/services/Cloud" + }, + { + "id": "f21c93f3da014785854b4126d0109c49", + "interface": "public", + "region": "RegionOne", + "url": "http://localhost:8773/services/Cloud" + } + ], + "id": "b08c9c7d4ef543eba5eeb766f72e5aa1", + "type": "ec2", + }, + { + "endpoints": [ + { + "id": "077d82df25304abeac2294004441db5a", + "interface": "admin", + "region": "RegionOne", + "url": "http://localhost:8776/v1/01257" + }, + { + "id": "875bf282362c40219665278b4fd11467", + "interface": "internal", + "region": "RegionOne", + "url": "http://localhost:8776/v1/01257" + }, + { + "id": "cd229aa6df0640dc858a8026eb7e640c", + "interface": "public", + "region": "RegionOne", + "url": "http://localhost:8776/v1/01257" + } + ], + "id": "5db21b82617f4a95816064736a7bec22", + "type": "volume", + } + ], + "expires_at": "2013-05-22T00:02:43.941430Z", + "issued_at": "2013-05-21T00:02:43.941473Z", + "methods": [ + "password" + ], + "project": { + "domain": { + "id": "default", + "name": "Default" + }, + "id": "01257", + "name": "service" + }, + "roles": [ + { + "id": "9fe2ff9ee4384b1894a90878d3e92bab", + "name": "_member_" + }, + { + "id": "53bff13443bd4450b97f978881d47b18", + "name": "admin" + } + ], + "user": { + "domain": { + "id": "default", + "name": "Default" + }, + "id": "f19ddbe2c53c46f189fe66d0a7a9c9ce", + "name": "nova" + }, + "OS-TRUST:trust": { + "id": "abc123", + "trustee_user_id": "123456", + "trustor_user_id": "333333", + "impersonation": False + } + } +} + +SAMPLE_V2_TOKEN_WITH_EMBEDED_VERSION = { + "access": { + "trust": { + "id": "abc123", + "trustee_user_id": "123456", + "trustor_user_id": "333333", + "impersonation": False + }, + "serviceCatalog": [ + { + "endpoints": [ + { + "adminURL": "http://localhost:8774/v1.1/01257", + "id": "51934fe63a5b4ac0a32664f64eb462c3", + "internalURL": "http://localhost:8774/v1.1/01257", + "publicURL": "http://localhost:8774/v1.1/01257", + "region": "RegionOne" + } + ], + "endpoints_links": [], + "name": "nova", + "type": "compute" + }, + { + "endpoints": [ + { + "adminURL": "http://localhost:9292", + "id": "aaa17a539e364297a7845d67c7c7cc4b", + "internalURL": "http://localhost:9292", + "publicURL": "http://localhost:9292", + "region": "RegionOne" + } + ], + "endpoints_links": [], + "name": "glance", + "type": "image" + }, + { + "endpoints": [ + { + "adminURL": "http://localhost:8776/v1/01257", + "id": "077d82df25304abeac2294004441db5a", + "internalURL": "http://localhost:8776/v1/01257", + "publicURL": "http://localhost:8776/v1/01257", + "region": "RegionOne" + } + ], + "endpoints_links": [], + "name": "volume", + "type": "volume" + }, + { + "endpoints": [ + { + "adminURL": "http://localhost:8773/services/Admin", + "id": "b06997fd08414903ad458836efaa9067", + "internalURL": "http://localhost:8773/services/Cloud", + "publicURL": "http://localhost:8773/services/Cloud", + "region": "RegionOne" + } + ], + "endpoints_links": [], + "name": "ec2", + "type": "ec2" + }, + { + "endpoints": [ + { + "adminURL": "http://localhost:8080/v1", + "id": "7bd0c643e05a4a2ab40902b2fa0dd4e6", + "internalURL": "http://localhost:8080/v1/AUTH_01257", + "publicURL": "http://localhost:8080/v1/AUTH_01257", + "region": "RegionOne" + } + ], + "endpoints_links": [], + "name": "swift", + "type": "object-store" + }, + { + "endpoints": [ + { + "adminURL": "http://localhost:35357/v2.0", + "id": "02850c5d1d094887bdc46e81e1e15dc7", + "internalURL": "http://localhost:5000/v2.0", + "publicURL": "http://localhost:5000/v2.0", + "region": "RegionOne" + } + ], + "endpoints_links": [], + "name": "keystone", + "type": "identity" + } + ], + "token": { + "expires": "2013-05-22T00:02:43.941430Z", + "id": "ce4fc2d36eea4cc9a36e666ac2f1029a", + "issued_at": "2013-05-21T00:02:43.941473Z", + "tenant": { + "enabled": True, + "id": "01257", + "name": "service" + } + }, + "user": { + "id": "f19ddbe2c53c46f189fe66d0a7a9c9ce", + "name": "nova", + "roles": [ + { + "name": "_member_" + }, + { + "name": "admin" + } + ], + "roles_links": [], + "username": "nova" + } + }, + 'token_version': 'v2.0' +} +SAMPLE_V3_TOKEN_WITH_EMBEDED_VERSION = { + "token": { + "catalog": [ + { + "endpoints": [ + { + "id": "02850c5d1d094887bdc46e81e1e15dc7", + "interface": "admin", + "region": "RegionOne", + "url": "http://localhost:35357/v2.0" + }, + { + "id": "446e244b75034a9ab4b0811e82d0b7c8", + "interface": "internal", + "region": "RegionOne", + "url": "http://localhost:5000/v2.0" + }, + { + "id": "47fa3d9f499240abb5dfcf2668f168cd", + "interface": "public", + "region": "RegionOne", + "url": "http://localhost:5000/v2.0" + } + ], + "id": "26d7541715a44a4d9adad96f9872b633", + "type": "identity", + }, + { + "endpoints": [ + { + "id": "aaa17a539e364297a7845d67c7c7cc4b", + "interface": "admin", + "region": "RegionOne", + "url": "http://localhost:9292" + }, + { + "id": "4fa9620e42394cb1974736dce0856c71", + "interface": "internal", + "region": "RegionOne", + "url": "http://localhost:9292" + }, + { + "id": "9673687f9bc441d88dec37942bfd603b", + "interface": "public", + "region": "RegionOne", + "url": "http://localhost:9292" + } + ], + "id": "d27a41843f4e4b0e8cf6dac4082deb0d", + "type": "image", + }, + { + "endpoints": [ + { + "id": "7bd0c643e05a4a2ab40902b2fa0dd4e6", + "interface": "admin", + "region": "RegionOne", + "url": "http://localhost:8080/v1" + }, + { + "id": "43bef154594d4ccb8e49014d20624e1d", + "interface": "internal", + "region": "RegionOne", + "url": "http://localhost:8080/v1/AUTH_01257" + }, + { + "id": "e63b5f5d7aa3493690189d0ff843b9b3", + "interface": "public", + "region": "RegionOne", + "url": "http://localhost:8080/v1/AUTH_01257" + } + ], + "id": "a669e152f1104810a4b6701aade721bb", + "type": "object-store", + }, + { + "endpoints": [ + { + "id": "51934fe63a5b4ac0a32664f64eb462c3", + "interface": "admin", + "region": "RegionOne", + "url": "http://localhost:8774/v1.1/01257" + }, + { + "id": "869b535eea0d42e483ae9da0d868ebad", + "interface": "internal", + "region": "RegionOne", + "url": "http://localhost:8774/v1.1/01257" + }, + { + "id": "93583824c18f4263a2245ca432b132a6", + "interface": "public", + "region": "RegionOne", + "url": "http://localhost:8774/v1.1/01257" + } + ], + "id": "7f32cc2af6c9476e82d75f80e8b3bbb8", + "type": "compute", + }, + { + "endpoints": [ + { + "id": "b06997fd08414903ad458836efaa9067", + "interface": "admin", + "region": "RegionOne", + "url": "http://localhost:8773/services/Admin" + }, + { + "id": "411f7de7c9a8484c9b46c254fb2676e2", + "interface": "internal", + "region": "RegionOne", + "url": "http://localhost:8773/services/Cloud" + }, + { + "id": "f21c93f3da014785854b4126d0109c49", + "interface": "public", + "region": "RegionOne", + "url": "http://localhost:8773/services/Cloud" + } + ], + "id": "b08c9c7d4ef543eba5eeb766f72e5aa1", + "type": "ec2", + }, + { + "endpoints": [ + { + "id": "077d82df25304abeac2294004441db5a", + "interface": "admin", + "region": "RegionOne", + "url": "http://localhost:8776/v1/01257" + }, + { + "id": "875bf282362c40219665278b4fd11467", + "interface": "internal", + "region": "RegionOne", + "url": "http://localhost:8776/v1/01257" + }, + { + "id": "cd229aa6df0640dc858a8026eb7e640c", + "interface": "public", + "region": "RegionOne", + "url": "http://localhost:8776/v1/01257" + } + ], + "id": "5db21b82617f4a95816064736a7bec22", + "type": "volume", + } + ], + "expires_at": "2013-05-22T00:02:43.941430Z", + "issued_at": "2013-05-21T00:02:43.941473Z", + "methods": [ + "password" + ], + "project": { + "domain": { + "id": "default", + "name": "Default" + }, + "id": "01257", + "name": "service" + }, + "roles": [ + { + "id": "9fe2ff9ee4384b1894a90878d3e92bab", + "name": "_member_" + }, + { + "id": "53bff13443bd4450b97f978881d47b18", + "name": "admin" + } + ], + "user": { + "domain": { + "id": "default", + "name": "Default" + }, + "id": "f19ddbe2c53c46f189fe66d0a7a9c9ce", + "name": "nova" + }, + "OS-TRUST:trust": { + "id": "abc123", + "trustee_user_id": "123456", + "trustor_user_id": "333333", + "impersonation": False + } + }, + 'token_version': 'v3.0' +} + + +def create_v2_token(): + return { + "access": { + "token": { + "expires": timeutils.isotime(timeutils.utcnow() + + FUTURE_DELTA), + "issued_at": "2013-05-21T00:02:43.941473Z", + "tenant": { + "enabled": True, + "id": "01257", + "name": "service" + } + } + } + } + + +SAMPLE_V2_TOKEN_EXPIRED = { + "access": { + "token": { + "expires": timeutils.isotime(CURRENT_DATE), + "issued_at": "2013-05-21T00:02:43.941473Z", + "tenant": { + "enabled": True, + "id": "01257", + "name": "service" + } + } + } +} + + +def create_v3_token(): + return { + "token": { + 'methods': [], + "expires_at": timeutils.isotime(timeutils.utcnow() + FUTURE_DELTA), + "issued_at": "2013-05-21T00:02:43.941473Z", + } + } + + +SAMPLE_V3_TOKEN_EXPIRED = { + "token": { + "expires_at": timeutils.isotime(CURRENT_DATE), + "issued_at": "2013-05-21T00:02:43.941473Z", + } +} + +SAMPLE_MALFORMED_TOKEN = { + "token": { + "bogus": { + "no expiration data": None + } + } +} + + +class TestTokenProvider(tests.TestCase): + def setUp(self): + super(TestTokenProvider, self).setUp() + self.useFixture(database.Database()) + self.load_backends() + + def test_get_token_version(self): + self.assertEqual( + token.provider.V2, + self.token_provider_api.get_token_version(SAMPLE_V2_TOKEN)) + self.assertEqual( + token.provider.V2, + self.token_provider_api.get_token_version( + SAMPLE_V2_TOKEN_WITH_EMBEDED_VERSION)) + self.assertEqual( + token.provider.V3, + self.token_provider_api.get_token_version(SAMPLE_V3_TOKEN)) + self.assertEqual( + token.provider.V3, + self.token_provider_api.get_token_version( + SAMPLE_V3_TOKEN_WITH_EMBEDED_VERSION)) + self.assertRaises(exception.UnsupportedTokenVersionException, + self.token_provider_api.get_token_version, + 'bogus') + + def test_supported_token_providers(self): + # test default config + + dependency.reset() + self.assertIsInstance(token.provider.Manager().driver, + uuid.Provider) + + dependency.reset() + self.config_fixture.config( + group='token', + provider='keystone.token.providers.uuid.Provider') + token.provider.Manager() + + dependency.reset() + self.config_fixture.config( + group='token', + provider='keystone.token.providers.pki.Provider') + token.provider.Manager() + + dependency.reset() + self.config_fixture.config( + group='token', + provider='keystone.token.providers.pkiz.Provider') + token.provider.Manager() + + def test_unsupported_token_provider(self): + self.config_fixture.config(group='token', + provider='my.package.MyProvider') + self.assertRaises(ImportError, + token.provider.Manager) + + def test_provider_token_expiration_validation(self): + self.assertRaises(exception.TokenNotFound, + self.token_provider_api._is_valid_token, + SAMPLE_V2_TOKEN_EXPIRED) + self.assertRaises(exception.TokenNotFound, + self.token_provider_api._is_valid_token, + SAMPLE_V3_TOKEN_EXPIRED) + self.assertRaises(exception.TokenNotFound, + self.token_provider_api._is_valid_token, + SAMPLE_MALFORMED_TOKEN) + self.assertIsNone( + self.token_provider_api._is_valid_token(create_v2_token())) + self.assertIsNone( + self.token_provider_api._is_valid_token(create_v3_token())) + + +# NOTE(ayoung): renamed to avoid automatic test detection +class PKIProviderTests(object): + + def setUp(self): + super(PKIProviderTests, self).setUp() + + from keystoneclient.common import cms + self.cms = cms + + from keystone.common import environment + self.environment = environment + + old_cms_subprocess = cms.subprocess + self.addCleanup(setattr, cms, 'subprocess', old_cms_subprocess) + + old_env_subprocess = environment.subprocess + self.addCleanup(setattr, environment, 'subprocess', old_env_subprocess) + + self.cms.subprocess = self.target_subprocess + self.environment.subprocess = self.target_subprocess + + reload(pki) # force module reload so the imports get re-evaluated + + def test_get_token_id_error_handling(self): + # cause command-line failure + self.config_fixture.config(group='signing', + keyfile='--please-break-me') + + provider = pki.Provider() + token_data = {} + self.assertRaises(exception.UnexpectedError, + provider._get_token_id, + token_data) + + +class TestPKIProviderWithEventlet(PKIProviderTests, tests.TestCase): + + def setUp(self): + # force keystoneclient.common.cms to use eventlet's subprocess + from eventlet.green import subprocess + self.target_subprocess = subprocess + + super(TestPKIProviderWithEventlet, self).setUp() + + +class TestPKIProviderWithStdlib(PKIProviderTests, tests.TestCase): + + def setUp(self): + # force keystoneclient.common.cms to use the stdlib subprocess + import subprocess + self.target_subprocess = subprocess + + super(TestPKIProviderWithStdlib, self).setUp() diff --git a/keystone-moon/keystone/tests/unit/test_url_middleware.py b/keystone-moon/keystone/tests/unit/test_url_middleware.py new file mode 100644 index 00000000..1b3872b5 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_url_middleware.py @@ -0,0 +1,53 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 webob + +from keystone import middleware +from keystone.tests import unit as tests + + +class FakeApp(object): + """Fakes a WSGI app URL normalized.""" + def __call__(self, env, start_response): + resp = webob.Response() + resp.body = 'SUCCESS' + return resp(env, start_response) + + +class UrlMiddlewareTest(tests.TestCase): + def setUp(self): + self.middleware = middleware.NormalizingFilter(FakeApp()) + self.response_status = None + self.response_headers = None + super(UrlMiddlewareTest, self).setUp() + + def start_fake_response(self, status, headers): + self.response_status = int(status.split(' ', 1)[0]) + self.response_headers = dict(headers) + + def test_trailing_slash_normalization(self): + """Tests /v2.0/tokens and /v2.0/tokens/ normalized URLs match.""" + req1 = webob.Request.blank('/v2.0/tokens') + req2 = webob.Request.blank('/v2.0/tokens/') + self.middleware(req1.environ, self.start_fake_response) + self.middleware(req2.environ, self.start_fake_response) + self.assertEqual(req1.path_url, req2.path_url) + self.assertEqual('http://localhost/v2.0/tokens', req1.path_url) + + def test_rewrite_empty_path(self): + """Tests empty path is rewritten to root.""" + req = webob.Request.blank('') + self.middleware(req.environ, self.start_fake_response) + self.assertEqual('http://localhost/', req.path_url) diff --git a/keystone-moon/keystone/tests/unit/test_v2.py b/keystone-moon/keystone/tests/unit/test_v2.py new file mode 100644 index 00000000..8c7c3792 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_v2.py @@ -0,0 +1,1500 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 time +import uuid + +from keystoneclient.common import cms +from oslo_config import cfg +import six +from testtools import matchers + +from keystone.common import extension as keystone_extension +from keystone.tests.unit import ksfixtures +from keystone.tests.unit import rest + + +CONF = cfg.CONF + + +class CoreApiTests(object): + def assertValidError(self, error): + self.assertIsNotNone(error.get('code')) + self.assertIsNotNone(error.get('title')) + self.assertIsNotNone(error.get('message')) + + def assertValidVersion(self, version): + self.assertIsNotNone(version) + self.assertIsNotNone(version.get('id')) + self.assertIsNotNone(version.get('status')) + self.assertIsNotNone(version.get('updated')) + + def assertValidExtension(self, extension): + self.assertIsNotNone(extension) + self.assertIsNotNone(extension.get('name')) + self.assertIsNotNone(extension.get('namespace')) + self.assertIsNotNone(extension.get('alias')) + self.assertIsNotNone(extension.get('updated')) + + def assertValidExtensionLink(self, link): + self.assertIsNotNone(link.get('rel')) + self.assertIsNotNone(link.get('type')) + self.assertIsNotNone(link.get('href')) + + def assertValidTenant(self, tenant): + self.assertIsNotNone(tenant.get('id')) + self.assertIsNotNone(tenant.get('name')) + + def assertValidUser(self, user): + self.assertIsNotNone(user.get('id')) + self.assertIsNotNone(user.get('name')) + + def assertValidRole(self, tenant): + self.assertIsNotNone(tenant.get('id')) + self.assertIsNotNone(tenant.get('name')) + + def test_public_not_found(self): + r = self.public_request( + path='/%s' % uuid.uuid4().hex, + expected_status=404) + self.assertValidErrorResponse(r) + + def test_admin_not_found(self): + r = self.admin_request( + path='/%s' % uuid.uuid4().hex, + expected_status=404) + self.assertValidErrorResponse(r) + + def test_public_multiple_choice(self): + r = self.public_request(path='/', expected_status=300) + self.assertValidMultipleChoiceResponse(r) + + def test_admin_multiple_choice(self): + r = self.admin_request(path='/', expected_status=300) + self.assertValidMultipleChoiceResponse(r) + + def test_public_version(self): + r = self.public_request(path='/v2.0/') + self.assertValidVersionResponse(r) + + def test_admin_version(self): + r = self.admin_request(path='/v2.0/') + self.assertValidVersionResponse(r) + + def test_public_extensions(self): + r = self.public_request(path='/v2.0/extensions') + self.assertValidExtensionListResponse( + r, keystone_extension.PUBLIC_EXTENSIONS) + + def test_admin_extensions(self): + r = self.admin_request(path='/v2.0/extensions') + self.assertValidExtensionListResponse( + r, keystone_extension.ADMIN_EXTENSIONS) + + def test_admin_extensions_404(self): + self.admin_request(path='/v2.0/extensions/invalid-extension', + expected_status=404) + + def test_public_osksadm_extension_404(self): + self.public_request(path='/v2.0/extensions/OS-KSADM', + expected_status=404) + + def test_admin_osksadm_extension(self): + r = self.admin_request(path='/v2.0/extensions/OS-KSADM') + self.assertValidExtensionResponse( + r, keystone_extension.ADMIN_EXTENSIONS) + + def test_authenticate(self): + r = self.public_request( + method='POST', + path='/v2.0/tokens', + body={ + 'auth': { + 'passwordCredentials': { + 'username': self.user_foo['name'], + 'password': self.user_foo['password'], + }, + 'tenantId': self.tenant_bar['id'], + }, + }, + expected_status=200) + self.assertValidAuthenticationResponse(r, require_service_catalog=True) + + def test_authenticate_unscoped(self): + r = self.public_request( + method='POST', + path='/v2.0/tokens', + body={ + 'auth': { + 'passwordCredentials': { + 'username': self.user_foo['name'], + 'password': self.user_foo['password'], + }, + }, + }, + expected_status=200) + self.assertValidAuthenticationResponse(r) + + def test_get_tenants_for_token(self): + r = self.public_request(path='/v2.0/tenants', + token=self.get_scoped_token()) + self.assertValidTenantListResponse(r) + + def test_validate_token(self): + token = self.get_scoped_token() + r = self.admin_request( + path='/v2.0/tokens/%(token_id)s' % { + 'token_id': token, + }, + token=token) + self.assertValidAuthenticationResponse(r) + + def test_invalid_token_404(self): + token = self.get_scoped_token() + self.admin_request( + path='/v2.0/tokens/%(token_id)s' % { + 'token_id': 'invalid', + }, + token=token, + expected_status=404) + + def test_validate_token_service_role(self): + self.md_foobar = self.assignment_api.add_role_to_user_and_project( + self.user_foo['id'], + self.tenant_service['id'], + self.role_service['id']) + + token = self.get_scoped_token(tenant_id='service') + r = self.admin_request( + path='/v2.0/tokens/%s' % token, + token=token) + self.assertValidAuthenticationResponse(r) + + def test_remove_role_revokes_token(self): + self.md_foobar = self.assignment_api.add_role_to_user_and_project( + self.user_foo['id'], + self.tenant_service['id'], + self.role_service['id']) + + token = self.get_scoped_token(tenant_id='service') + r = self.admin_request( + path='/v2.0/tokens/%s' % token, + token=token) + self.assertValidAuthenticationResponse(r) + + self.assignment_api.remove_role_from_user_and_project( + self.user_foo['id'], + self.tenant_service['id'], + self.role_service['id']) + + r = self.admin_request( + path='/v2.0/tokens/%s' % token, + token=token, + expected_status=401) + + def test_validate_token_belongs_to(self): + token = self.get_scoped_token() + path = ('/v2.0/tokens/%s?belongsTo=%s' % (token, + self.tenant_bar['id'])) + r = self.admin_request(path=path, token=token) + self.assertValidAuthenticationResponse(r, require_service_catalog=True) + + def test_validate_token_no_belongs_to_still_returns_catalog(self): + token = self.get_scoped_token() + path = ('/v2.0/tokens/%s' % token) + r = self.admin_request(path=path, token=token) + self.assertValidAuthenticationResponse(r, require_service_catalog=True) + + def test_validate_token_head(self): + """The same call as above, except using HEAD. + + There's no response to validate here, but this is included for the + sake of completely covering the core API. + + """ + token = self.get_scoped_token() + self.admin_request( + method='HEAD', + path='/v2.0/tokens/%(token_id)s' % { + 'token_id': token, + }, + token=token, + expected_status=200) + + def test_endpoints(self): + token = self.get_scoped_token() + r = self.admin_request( + path='/v2.0/tokens/%(token_id)s/endpoints' % { + 'token_id': token, + }, + token=token) + self.assertValidEndpointListResponse(r) + + def test_get_tenant(self): + token = self.get_scoped_token() + r = self.admin_request( + path='/v2.0/tenants/%(tenant_id)s' % { + 'tenant_id': self.tenant_bar['id'], + }, + token=token) + self.assertValidTenantResponse(r) + + def test_get_tenant_by_name(self): + token = self.get_scoped_token() + r = self.admin_request( + path='/v2.0/tenants?name=%(tenant_name)s' % { + 'tenant_name': self.tenant_bar['name'], + }, + token=token) + self.assertValidTenantResponse(r) + + def test_get_user_roles_with_tenant(self): + token = self.get_scoped_token() + r = self.admin_request( + path='/v2.0/tenants/%(tenant_id)s/users/%(user_id)s/roles' % { + 'tenant_id': self.tenant_bar['id'], + 'user_id': self.user_foo['id'], + }, + token=token) + self.assertValidRoleListResponse(r) + + def test_get_user(self): + token = self.get_scoped_token() + r = self.admin_request( + path='/v2.0/users/%(user_id)s' % { + 'user_id': self.user_foo['id'], + }, + token=token) + self.assertValidUserResponse(r) + + def test_get_user_by_name(self): + token = self.get_scoped_token() + r = self.admin_request( + path='/v2.0/users?name=%(user_name)s' % { + 'user_name': self.user_foo['name'], + }, + token=token) + self.assertValidUserResponse(r) + + def test_create_update_user_invalid_enabled_type(self): + # Enforce usage of boolean for 'enabled' field + token = self.get_scoped_token() + + # Test CREATE request + r = self.admin_request( + method='POST', + path='/v2.0/users', + body={ + 'user': { + 'name': uuid.uuid4().hex, + 'password': uuid.uuid4().hex, + 'enabled': "False", + }, + }, + token=token, + expected_status=400) + self.assertValidErrorResponse(r) + + r = self.admin_request( + method='POST', + path='/v2.0/users', + body={ + 'user': { + 'name': uuid.uuid4().hex, + 'password': uuid.uuid4().hex, + # In JSON, 0|1 are not booleans + 'enabled': 0, + }, + }, + token=token, + expected_status=400) + self.assertValidErrorResponse(r) + + # Test UPDATE request + path = '/v2.0/users/%(user_id)s' % { + 'user_id': self.user_foo['id'], + } + + r = self.admin_request( + method='PUT', + path=path, + body={ + 'user': { + 'enabled': "False", + }, + }, + token=token, + expected_status=400) + self.assertValidErrorResponse(r) + + r = self.admin_request( + method='PUT', + path=path, + body={ + 'user': { + # In JSON, 0|1 are not booleans + 'enabled': 1, + }, + }, + token=token, + expected_status=400) + self.assertValidErrorResponse(r) + + def test_create_update_user_valid_enabled_type(self): + # Enforce usage of boolean for 'enabled' field + token = self.get_scoped_token() + + # Test CREATE request + self.admin_request(method='POST', + path='/v2.0/users', + body={ + 'user': { + 'name': uuid.uuid4().hex, + 'password': uuid.uuid4().hex, + 'enabled': False, + }, + }, + token=token, + expected_status=200) + + def test_error_response(self): + """This triggers assertValidErrorResponse by convention.""" + self.public_request(path='/v2.0/tenants', expected_status=401) + + def test_invalid_parameter_error_response(self): + token = self.get_scoped_token() + bad_body = { + 'OS-KSADM:service%s' % uuid.uuid4().hex: { + 'name': uuid.uuid4().hex, + 'type': uuid.uuid4().hex, + }, + } + res = self.admin_request(method='POST', + path='/v2.0/OS-KSADM/services', + body=bad_body, + token=token, + expected_status=400) + self.assertValidErrorResponse(res) + res = self.admin_request(method='POST', + path='/v2.0/users', + body=bad_body, + token=token, + expected_status=400) + self.assertValidErrorResponse(res) + + def _get_user_id(self, r): + """Helper method to return user ID from a response. + + This needs to be overridden by child classes + based on their content type. + + """ + raise NotImplementedError() + + def _get_role_id(self, r): + """Helper method to return a role ID from a response. + + This needs to be overridden by child classes + based on their content type. + + """ + raise NotImplementedError() + + def _get_role_name(self, r): + """Helper method to return role NAME from a response. + + This needs to be overridden by child classes + based on their content type. + + """ + raise NotImplementedError() + + def _get_project_id(self, r): + """Helper method to return project ID from a response. + + This needs to be overridden by child classes + based on their content type. + + """ + raise NotImplementedError() + + def assertNoRoles(self, r): + """Helper method to assert No Roles + + This needs to be overridden by child classes + based on their content type. + + """ + raise NotImplementedError() + + def test_update_user_tenant(self): + token = self.get_scoped_token() + + # Create a new user + r = self.admin_request( + method='POST', + path='/v2.0/users', + body={ + 'user': { + 'name': uuid.uuid4().hex, + 'password': uuid.uuid4().hex, + 'tenantId': self.tenant_bar['id'], + 'enabled': True, + }, + }, + token=token, + expected_status=200) + + user_id = self._get_user_id(r.result) + + # Check if member_role is in tenant_bar + r = self.admin_request( + path='/v2.0/tenants/%(project_id)s/users/%(user_id)s/roles' % { + 'project_id': self.tenant_bar['id'], + 'user_id': user_id + }, + token=token, + expected_status=200) + self.assertEqual(CONF.member_role_name, self._get_role_name(r.result)) + + # Create a new tenant + r = self.admin_request( + method='POST', + path='/v2.0/tenants', + body={ + 'tenant': { + 'name': 'test_update_user', + 'description': 'A description ...', + 'enabled': True, + }, + }, + token=token, + expected_status=200) + + project_id = self._get_project_id(r.result) + + # Update user's tenant + r = self.admin_request( + method='PUT', + path='/v2.0/users/%(user_id)s' % { + 'user_id': user_id, + }, + body={ + 'user': { + 'tenantId': project_id, + }, + }, + token=token, + expected_status=200) + + # 'member_role' should be in new_tenant + r = self.admin_request( + path='/v2.0/tenants/%(project_id)s/users/%(user_id)s/roles' % { + 'project_id': project_id, + 'user_id': user_id + }, + token=token, + expected_status=200) + self.assertEqual('_member_', self._get_role_name(r.result)) + + # 'member_role' should not be in tenant_bar any more + r = self.admin_request( + path='/v2.0/tenants/%(project_id)s/users/%(user_id)s/roles' % { + 'project_id': self.tenant_bar['id'], + 'user_id': user_id + }, + token=token, + expected_status=200) + self.assertNoRoles(r.result) + + def test_update_user_with_invalid_tenant(self): + token = self.get_scoped_token() + + # Create a new user + r = self.admin_request( + method='POST', + path='/v2.0/users', + body={ + 'user': { + 'name': 'test_invalid_tenant', + 'password': uuid.uuid4().hex, + 'tenantId': self.tenant_bar['id'], + 'enabled': True, + }, + }, + token=token, + expected_status=200) + user_id = self._get_user_id(r.result) + + # Update user with an invalid tenant + r = self.admin_request( + method='PUT', + path='/v2.0/users/%(user_id)s' % { + 'user_id': user_id, + }, + body={ + 'user': { + 'tenantId': 'abcde12345heha', + }, + }, + token=token, + expected_status=404) + + def test_update_user_with_invalid_tenant_no_prev_tenant(self): + token = self.get_scoped_token() + + # Create a new user + r = self.admin_request( + method='POST', + path='/v2.0/users', + body={ + 'user': { + 'name': 'test_invalid_tenant', + 'password': uuid.uuid4().hex, + 'enabled': True, + }, + }, + token=token, + expected_status=200) + user_id = self._get_user_id(r.result) + + # Update user with an invalid tenant + r = self.admin_request( + method='PUT', + path='/v2.0/users/%(user_id)s' % { + 'user_id': user_id, + }, + body={ + 'user': { + 'tenantId': 'abcde12345heha', + }, + }, + token=token, + expected_status=404) + + def test_update_user_with_old_tenant(self): + token = self.get_scoped_token() + + # Create a new user + r = self.admin_request( + method='POST', + path='/v2.0/users', + body={ + 'user': { + 'name': uuid.uuid4().hex, + 'password': uuid.uuid4().hex, + 'tenantId': self.tenant_bar['id'], + 'enabled': True, + }, + }, + token=token, + expected_status=200) + + user_id = self._get_user_id(r.result) + + # Check if member_role is in tenant_bar + r = self.admin_request( + path='/v2.0/tenants/%(project_id)s/users/%(user_id)s/roles' % { + 'project_id': self.tenant_bar['id'], + 'user_id': user_id + }, + token=token, + expected_status=200) + self.assertEqual(CONF.member_role_name, self._get_role_name(r.result)) + + # Update user's tenant with old tenant id + r = self.admin_request( + method='PUT', + path='/v2.0/users/%(user_id)s' % { + 'user_id': user_id, + }, + body={ + 'user': { + 'tenantId': self.tenant_bar['id'], + }, + }, + token=token, + expected_status=200) + + # 'member_role' should still be in tenant_bar + r = self.admin_request( + path='/v2.0/tenants/%(project_id)s/users/%(user_id)s/roles' % { + 'project_id': self.tenant_bar['id'], + 'user_id': user_id + }, + token=token, + expected_status=200) + self.assertEqual('_member_', self._get_role_name(r.result)) + + def test_authenticating_a_user_with_no_password(self): + token = self.get_scoped_token() + + username = uuid.uuid4().hex + + # create the user + self.admin_request( + method='POST', + path='/v2.0/users', + body={ + 'user': { + 'name': username, + 'enabled': True, + }, + }, + token=token) + + # fail to authenticate + r = self.public_request( + method='POST', + path='/v2.0/tokens', + body={ + 'auth': { + 'passwordCredentials': { + 'username': username, + 'password': 'password', + }, + }, + }, + expected_status=401) + self.assertValidErrorResponse(r) + + def test_www_authenticate_header(self): + r = self.public_request( + path='/v2.0/tenants', + expected_status=401) + self.assertEqual('Keystone uri="http://localhost"', + r.headers.get('WWW-Authenticate')) + + def test_www_authenticate_header_host(self): + test_url = 'http://%s:4187' % uuid.uuid4().hex + self.config_fixture.config(public_endpoint=test_url) + r = self.public_request( + path='/v2.0/tenants', + expected_status=401) + self.assertEqual('Keystone uri="%s"' % test_url, + r.headers.get('WWW-Authenticate')) + + +class LegacyV2UsernameTests(object): + """Tests to show the broken username behavior in V2. + + The V2 API is documented to use `username` instead of `name`. The + API forced used to use name and left the username to fall into the + `extra` field. + + These tests ensure this behavior works so fixes to `username`/`name` + will be backward compatible. + """ + + def create_user(self, **user_attrs): + """Creates a users and returns the response object. + + :param user_attrs: attributes added to the request body (optional) + """ + token = self.get_scoped_token() + body = { + 'user': { + 'name': uuid.uuid4().hex, + 'enabled': True, + }, + } + body['user'].update(user_attrs) + + return self.admin_request( + method='POST', + path='/v2.0/users', + token=token, + body=body, + expected_status=200) + + def test_create_with_extra_username(self): + """The response for creating a user will contain the extra fields.""" + fake_username = uuid.uuid4().hex + r = self.create_user(username=fake_username) + + self.assertValidUserResponse(r) + + user = self.get_user_from_response(r) + self.assertEqual(fake_username, user.get('username')) + + def test_get_returns_username_from_extra(self): + """The response for getting a user will contain the extra fields.""" + token = self.get_scoped_token() + + fake_username = uuid.uuid4().hex + r = self.create_user(username=fake_username) + + id_ = self.get_user_attribute_from_response(r, 'id') + r = self.admin_request(path='/v2.0/users/%s' % id_, token=token) + + self.assertValidUserResponse(r) + + user = self.get_user_from_response(r) + self.assertEqual(fake_username, user.get('username')) + + def test_update_returns_new_username_when_adding_username(self): + """The response for updating a user will contain the extra fields. + + This is specifically testing for updating a username when a value + was not previously set. + """ + token = self.get_scoped_token() + + r = self.create_user() + + id_ = self.get_user_attribute_from_response(r, 'id') + name = self.get_user_attribute_from_response(r, 'name') + enabled = self.get_user_attribute_from_response(r, 'enabled') + r = self.admin_request( + method='PUT', + path='/v2.0/users/%s' % id_, + token=token, + body={ + 'user': { + 'name': name, + 'username': 'new_username', + 'enabled': enabled, + }, + }, + expected_status=200) + + self.assertValidUserResponse(r) + + user = self.get_user_from_response(r) + self.assertEqual('new_username', user.get('username')) + + def test_update_returns_new_username_when_updating_username(self): + """The response for updating a user will contain the extra fields. + + This tests updating a username that was previously set. + """ + token = self.get_scoped_token() + + r = self.create_user(username='original_username') + + id_ = self.get_user_attribute_from_response(r, 'id') + name = self.get_user_attribute_from_response(r, 'name') + enabled = self.get_user_attribute_from_response(r, 'enabled') + r = self.admin_request( + method='PUT', + path='/v2.0/users/%s' % id_, + token=token, + body={ + 'user': { + 'name': name, + 'username': 'new_username', + 'enabled': enabled, + }, + }, + expected_status=200) + + self.assertValidUserResponse(r) + + user = self.get_user_from_response(r) + self.assertEqual('new_username', user.get('username')) + + def test_username_is_always_returned_create(self): + """Username is set as the value of name if no username is provided. + + This matches the v2.0 spec where we really should be using username + and not name. + """ + r = self.create_user() + + self.assertValidUserResponse(r) + + user = self.get_user_from_response(r) + self.assertEqual(user.get('name'), user.get('username')) + + def test_username_is_always_returned_get(self): + """Username is set as the value of name if no username is provided. + + This matches the v2.0 spec where we really should be using username + and not name. + """ + token = self.get_scoped_token() + + r = self.create_user() + + id_ = self.get_user_attribute_from_response(r, 'id') + r = self.admin_request(path='/v2.0/users/%s' % id_, token=token) + + self.assertValidUserResponse(r) + + user = self.get_user_from_response(r) + self.assertEqual(user.get('name'), user.get('username')) + + def test_username_is_always_returned_get_by_name(self): + """Username is set as the value of name if no username is provided. + + This matches the v2.0 spec where we really should be using username + and not name. + """ + token = self.get_scoped_token() + + r = self.create_user() + + name = self.get_user_attribute_from_response(r, 'name') + r = self.admin_request(path='/v2.0/users?name=%s' % name, token=token) + + self.assertValidUserResponse(r) + + user = self.get_user_from_response(r) + self.assertEqual(user.get('name'), user.get('username')) + + def test_username_is_always_returned_update_no_username_provided(self): + """Username is set as the value of name if no username is provided. + + This matches the v2.0 spec where we really should be using username + and not name. + """ + token = self.get_scoped_token() + + r = self.create_user() + + id_ = self.get_user_attribute_from_response(r, 'id') + name = self.get_user_attribute_from_response(r, 'name') + enabled = self.get_user_attribute_from_response(r, 'enabled') + r = self.admin_request( + method='PUT', + path='/v2.0/users/%s' % id_, + token=token, + body={ + 'user': { + 'name': name, + 'enabled': enabled, + }, + }, + expected_status=200) + + self.assertValidUserResponse(r) + + user = self.get_user_from_response(r) + self.assertEqual(user.get('name'), user.get('username')) + + def test_updated_username_is_returned(self): + """Username is set as the value of name if no username is provided. + + This matches the v2.0 spec where we really should be using username + and not name. + """ + token = self.get_scoped_token() + + r = self.create_user() + + id_ = self.get_user_attribute_from_response(r, 'id') + name = self.get_user_attribute_from_response(r, 'name') + enabled = self.get_user_attribute_from_response(r, 'enabled') + r = self.admin_request( + method='PUT', + path='/v2.0/users/%s' % id_, + token=token, + body={ + 'user': { + 'name': name, + 'enabled': enabled, + }, + }, + expected_status=200) + + self.assertValidUserResponse(r) + + user = self.get_user_from_response(r) + self.assertEqual(user.get('name'), user.get('username')) + + def test_username_can_be_used_instead_of_name_create(self): + token = self.get_scoped_token() + + r = self.admin_request( + method='POST', + path='/v2.0/users', + token=token, + body={ + 'user': { + 'username': uuid.uuid4().hex, + 'enabled': True, + }, + }, + expected_status=200) + + self.assertValidUserResponse(r) + + user = self.get_user_from_response(r) + self.assertEqual(user.get('name'), user.get('username')) + + def test_username_can_be_used_instead_of_name_update(self): + token = self.get_scoped_token() + + r = self.create_user() + + id_ = self.get_user_attribute_from_response(r, 'id') + new_username = uuid.uuid4().hex + enabled = self.get_user_attribute_from_response(r, 'enabled') + r = self.admin_request( + method='PUT', + path='/v2.0/users/%s' % id_, + token=token, + body={ + 'user': { + 'username': new_username, + 'enabled': enabled, + }, + }, + expected_status=200) + + self.assertValidUserResponse(r) + + user = self.get_user_from_response(r) + self.assertEqual(new_username, user.get('name')) + self.assertEqual(user.get('name'), user.get('username')) + + +class RestfulTestCase(rest.RestfulTestCase): + + def setUp(self): + super(RestfulTestCase, self).setUp() + + # TODO(termie): add an admin user to the fixtures and use that user + # override the fixtures, for now + self.assignment_api.add_role_to_user_and_project( + self.user_foo['id'], + self.tenant_bar['id'], + self.role_admin['id']) + + +class V2TestCase(RestfulTestCase, CoreApiTests, LegacyV2UsernameTests): + def _get_user_id(self, r): + return r['user']['id'] + + def _get_role_name(self, r): + return r['roles'][0]['name'] + + def _get_role_id(self, r): + return r['roles'][0]['id'] + + def _get_project_id(self, r): + return r['tenant']['id'] + + def _get_token_id(self, r): + return r.result['access']['token']['id'] + + def assertNoRoles(self, r): + self.assertEqual([], r['roles']) + + def assertValidErrorResponse(self, r): + self.assertIsNotNone(r.result.get('error')) + self.assertValidError(r.result['error']) + self.assertEqual(r.result['error']['code'], r.status_code) + + def assertValidExtension(self, extension, expected): + super(V2TestCase, self).assertValidExtension(extension) + descriptions = [ext['description'] for ext in six.itervalues(expected)] + description = extension.get('description') + self.assertIsNotNone(description) + self.assertIn(description, descriptions) + self.assertIsNotNone(extension.get('links')) + self.assertNotEmpty(extension.get('links')) + for link in extension.get('links'): + self.assertValidExtensionLink(link) + + def assertValidExtensionListResponse(self, r, expected): + self.assertIsNotNone(r.result.get('extensions')) + self.assertIsNotNone(r.result['extensions'].get('values')) + self.assertNotEmpty(r.result['extensions'].get('values')) + for extension in r.result['extensions']['values']: + self.assertValidExtension(extension, expected) + + def assertValidExtensionResponse(self, r, expected): + self.assertValidExtension(r.result.get('extension'), expected) + + def assertValidUser(self, user): + super(V2TestCase, self).assertValidUser(user) + self.assertNotIn('default_project_id', user) + if 'tenantId' in user: + # NOTE(morganfainberg): tenantId should never be "None", it gets + # filtered out of the object if it is there. This is suspenders + # and a belt check to avoid unintended regressions. + self.assertIsNotNone(user.get('tenantId')) + + def assertValidAuthenticationResponse(self, r, + require_service_catalog=False): + self.assertIsNotNone(r.result.get('access')) + self.assertIsNotNone(r.result['access'].get('token')) + self.assertIsNotNone(r.result['access'].get('user')) + + # validate token + self.assertIsNotNone(r.result['access']['token'].get('id')) + self.assertIsNotNone(r.result['access']['token'].get('expires')) + tenant = r.result['access']['token'].get('tenant') + if tenant is not None: + # validate tenant + self.assertIsNotNone(tenant.get('id')) + self.assertIsNotNone(tenant.get('name')) + + # validate user + self.assertIsNotNone(r.result['access']['user'].get('id')) + self.assertIsNotNone(r.result['access']['user'].get('name')) + + if require_service_catalog: + # roles are only provided with a service catalog + roles = r.result['access']['user'].get('roles') + self.assertNotEmpty(roles) + for role in roles: + self.assertIsNotNone(role.get('name')) + + serviceCatalog = r.result['access'].get('serviceCatalog') + # validate service catalog + if require_service_catalog: + self.assertIsNotNone(serviceCatalog) + if serviceCatalog is not None: + self.assertIsInstance(serviceCatalog, list) + if require_service_catalog: + self.assertNotEmpty(serviceCatalog) + for service in r.result['access']['serviceCatalog']: + # validate service + self.assertIsNotNone(service.get('name')) + self.assertIsNotNone(service.get('type')) + + # services contain at least one endpoint + self.assertIsNotNone(service.get('endpoints')) + self.assertNotEmpty(service['endpoints']) + for endpoint in service['endpoints']: + # validate service endpoint + self.assertIsNotNone(endpoint.get('publicURL')) + + def assertValidTenantListResponse(self, r): + self.assertIsNotNone(r.result.get('tenants')) + self.assertNotEmpty(r.result['tenants']) + for tenant in r.result['tenants']: + self.assertValidTenant(tenant) + self.assertIsNotNone(tenant.get('enabled')) + self.assertIn(tenant.get('enabled'), [True, False]) + + def assertValidUserResponse(self, r): + self.assertIsNotNone(r.result.get('user')) + self.assertValidUser(r.result['user']) + + def assertValidTenantResponse(self, r): + self.assertIsNotNone(r.result.get('tenant')) + self.assertValidTenant(r.result['tenant']) + + def assertValidRoleListResponse(self, r): + self.assertIsNotNone(r.result.get('roles')) + self.assertNotEmpty(r.result['roles']) + for role in r.result['roles']: + self.assertValidRole(role) + + def assertValidVersion(self, version): + super(V2TestCase, self).assertValidVersion(version) + + self.assertIsNotNone(version.get('links')) + self.assertNotEmpty(version.get('links')) + for link in version.get('links'): + self.assertIsNotNone(link.get('rel')) + self.assertIsNotNone(link.get('href')) + + self.assertIsNotNone(version.get('media-types')) + self.assertNotEmpty(version.get('media-types')) + for media in version.get('media-types'): + self.assertIsNotNone(media.get('base')) + self.assertIsNotNone(media.get('type')) + + def assertValidMultipleChoiceResponse(self, r): + self.assertIsNotNone(r.result.get('versions')) + self.assertIsNotNone(r.result['versions'].get('values')) + self.assertNotEmpty(r.result['versions']['values']) + for version in r.result['versions']['values']: + self.assertValidVersion(version) + + def assertValidVersionResponse(self, r): + self.assertValidVersion(r.result.get('version')) + + def assertValidEndpointListResponse(self, r): + self.assertIsNotNone(r.result.get('endpoints')) + self.assertNotEmpty(r.result['endpoints']) + for endpoint in r.result['endpoints']: + self.assertIsNotNone(endpoint.get('id')) + self.assertIsNotNone(endpoint.get('name')) + self.assertIsNotNone(endpoint.get('type')) + self.assertIsNotNone(endpoint.get('publicURL')) + self.assertIsNotNone(endpoint.get('internalURL')) + self.assertIsNotNone(endpoint.get('adminURL')) + + def get_user_from_response(self, r): + return r.result.get('user') + + def get_user_attribute_from_response(self, r, attribute_name): + return r.result['user'][attribute_name] + + def test_service_crud_requires_auth(self): + """Service CRUD should 401 without an X-Auth-Token (bug 1006822).""" + # values here don't matter because we should 401 before they're checked + service_path = '/v2.0/OS-KSADM/services/%s' % uuid.uuid4().hex + service_body = { + 'OS-KSADM:service': { + 'name': uuid.uuid4().hex, + 'type': uuid.uuid4().hex, + }, + } + + r = self.admin_request(method='GET', + path='/v2.0/OS-KSADM/services', + expected_status=401) + self.assertValidErrorResponse(r) + + r = self.admin_request(method='POST', + path='/v2.0/OS-KSADM/services', + body=service_body, + expected_status=401) + self.assertValidErrorResponse(r) + + r = self.admin_request(method='GET', + path=service_path, + expected_status=401) + self.assertValidErrorResponse(r) + + r = self.admin_request(method='DELETE', + path=service_path, + expected_status=401) + self.assertValidErrorResponse(r) + + def test_user_role_list_requires_auth(self): + """User role list should 401 without an X-Auth-Token (bug 1006815).""" + # values here don't matter because we should 401 before they're checked + path = '/v2.0/tenants/%(tenant_id)s/users/%(user_id)s/roles' % { + 'tenant_id': uuid.uuid4().hex, + 'user_id': uuid.uuid4().hex, + } + + r = self.admin_request(path=path, expected_status=401) + self.assertValidErrorResponse(r) + + def test_fetch_revocation_list_nonadmin_fails(self): + self.admin_request( + method='GET', + path='/v2.0/tokens/revoked', + expected_status=401) + + def test_fetch_revocation_list_admin_200(self): + token = self.get_scoped_token() + r = self.admin_request( + method='GET', + path='/v2.0/tokens/revoked', + token=token, + expected_status=200) + self.assertValidRevocationListResponse(r) + + def assertValidRevocationListResponse(self, response): + self.assertIsNotNone(response.result['signed']) + + def _fetch_parse_revocation_list(self): + + token1 = self.get_scoped_token() + + # TODO(morganfainberg): Because this is making a restful call to the + # app a change to UTCNOW via mock.patch will not affect the returned + # token. The only surefire way to ensure there is not a transient bug + # based upon when the second token is issued is with a sleep. This + # issue all stems from the limited resolution (no microseconds) on the + # expiry time of tokens and the way revocation events utilizes token + # expiry to revoke individual tokens. This is a stop-gap until all + # associated issues with resolution on expiration and revocation events + # are resolved. + time.sleep(1) + + token2 = self.get_scoped_token() + + self.admin_request(method='DELETE', + path='/v2.0/tokens/%s' % token2, + token=token1) + + r = self.admin_request( + method='GET', + path='/v2.0/tokens/revoked', + token=token1, + expected_status=200) + signed_text = r.result['signed'] + + data_json = cms.cms_verify(signed_text, CONF.signing.certfile, + CONF.signing.ca_certs) + + data = json.loads(data_json) + + return (data, token2) + + def test_fetch_revocation_list_md5(self): + """If the server is configured for md5, then the revocation list has + tokens hashed with MD5. + """ + + # The default hash algorithm is md5. + hash_algorithm = 'md5' + + (data, token) = self._fetch_parse_revocation_list() + token_hash = cms.cms_hash_token(token, mode=hash_algorithm) + self.assertThat(token_hash, matchers.Equals(data['revoked'][0]['id'])) + + def test_fetch_revocation_list_sha256(self): + """If the server is configured for sha256, then the revocation list has + tokens hashed with SHA256 + """ + + hash_algorithm = 'sha256' + self.config_fixture.config(group='token', + hash_algorithm=hash_algorithm) + + (data, token) = self._fetch_parse_revocation_list() + token_hash = cms.cms_hash_token(token, mode=hash_algorithm) + self.assertThat(token_hash, matchers.Equals(data['revoked'][0]['id'])) + + def test_create_update_user_invalid_enabled_type(self): + # Enforce usage of boolean for 'enabled' field + token = self.get_scoped_token() + + # Test CREATE request + r = self.admin_request( + method='POST', + path='/v2.0/users', + body={ + 'user': { + 'name': uuid.uuid4().hex, + 'password': uuid.uuid4().hex, + # In JSON, "true|false" are not boolean + 'enabled': "true", + }, + }, + token=token, + expected_status=400) + self.assertValidErrorResponse(r) + + # Test UPDATE request + r = self.admin_request( + method='PUT', + path='/v2.0/users/%(user_id)s' % { + 'user_id': self.user_foo['id'], + }, + body={ + 'user': { + # In JSON, "true|false" are not boolean + 'enabled': "true", + }, + }, + token=token, + expected_status=400) + self.assertValidErrorResponse(r) + + def test_authenticating_a_user_with_an_OSKSADM_password(self): + token = self.get_scoped_token() + + username = uuid.uuid4().hex + password = uuid.uuid4().hex + + # create the user + r = self.admin_request( + method='POST', + path='/v2.0/users', + body={ + 'user': { + 'name': username, + 'OS-KSADM:password': password, + 'enabled': True, + }, + }, + token=token) + + # successfully authenticate + self.public_request( + method='POST', + path='/v2.0/tokens', + body={ + 'auth': { + 'passwordCredentials': { + 'username': username, + 'password': password, + }, + }, + }, + expected_status=200) + + # ensure password doesn't leak + user_id = r.result['user']['id'] + r = self.admin_request( + method='GET', + path='/v2.0/users/%s' % user_id, + token=token, + expected_status=200) + self.assertNotIn('OS-KSADM:password', r.result['user']) + + def test_updating_a_user_with_an_OSKSADM_password(self): + token = self.get_scoped_token() + + user_id = self.user_foo['id'] + password = uuid.uuid4().hex + + # update the user + self.admin_request( + method='PUT', + path='/v2.0/users/%s/OS-KSADM/password' % user_id, + body={ + 'user': { + 'password': password, + }, + }, + token=token, + expected_status=200) + + # successfully authenticate + self.public_request( + method='POST', + path='/v2.0/tokens', + body={ + 'auth': { + 'passwordCredentials': { + 'username': self.user_foo['name'], + 'password': password, + }, + }, + }, + expected_status=200) + + +class RevokeApiTestCase(V2TestCase): + def config_overrides(self): + super(RevokeApiTestCase, self).config_overrides() + self.config_fixture.config( + group='revoke', + driver='keystone.contrib.revoke.backends.kvs.Revoke') + self.config_fixture.config( + group='token', + provider='keystone.token.providers.pki.Provider', + revoke_by_id=False) + + def test_fetch_revocation_list_admin_200(self): + self.skipTest('Revoke API disables revocation_list.') + + def test_fetch_revocation_list_md5(self): + self.skipTest('Revoke API disables revocation_list.') + + def test_fetch_revocation_list_sha256(self): + self.skipTest('Revoke API disables revocation_list.') + + +class TestFernetTokenProviderV2(RestfulTestCase): + + def setUp(self): + super(TestFernetTokenProviderV2, self).setUp() + self.useFixture(ksfixtures.KeyRepository(self.config_fixture)) + + # Used by RestfulTestCase + def _get_token_id(self, r): + return r.result['access']['token']['id'] + + def new_project_ref(self): + return {'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'description': uuid.uuid4().hex, + 'domain_id': 'default', + 'enabled': True} + + def config_overrides(self): + super(TestFernetTokenProviderV2, self).config_overrides() + self.config_fixture.config( + group='token', + provider='keystone.token.providers.fernet.Provider') + + def test_authenticate_unscoped_token(self): + unscoped_token = self.get_unscoped_token() + # Fernet token must be of length 255 per usability requirements + self.assertLess(len(unscoped_token), 255) + + def test_validate_unscoped_token(self): + # Grab an admin token to validate with + project_ref = self.new_project_ref() + self.resource_api.create_project(project_ref['id'], project_ref) + self.assignment_api.add_role_to_user_and_project(self.user_foo['id'], + project_ref['id'], + self.role_admin['id']) + admin_token = self.get_scoped_token(tenant_id=project_ref['id']) + unscoped_token = self.get_unscoped_token() + path = ('/v2.0/tokens/%s' % unscoped_token) + self.admin_request( + method='GET', + path=path, + token=admin_token, + expected_status=200) + + def test_authenticate_scoped_token(self): + project_ref = self.new_project_ref() + self.resource_api.create_project(project_ref['id'], project_ref) + self.assignment_api.add_role_to_user_and_project( + self.user_foo['id'], project_ref['id'], self.role_service['id']) + token = self.get_scoped_token(tenant_id=project_ref['id']) + # Fernet token must be of length 255 per usability requirements + self.assertLess(len(token), 255) + + def test_validate_scoped_token(self): + project_ref = self.new_project_ref() + self.resource_api.create_project(project_ref['id'], project_ref) + self.assignment_api.add_role_to_user_and_project(self.user_foo['id'], + project_ref['id'], + self.role_admin['id']) + project2_ref = self.new_project_ref() + self.resource_api.create_project(project2_ref['id'], project2_ref) + self.assignment_api.add_role_to_user_and_project( + self.user_foo['id'], project2_ref['id'], self.role_member['id']) + admin_token = self.get_scoped_token(tenant_id=project_ref['id']) + member_token = self.get_scoped_token(tenant_id=project2_ref['id']) + path = ('/v2.0/tokens/%s?belongsTo=%s' % (member_token, + project2_ref['id'])) + # Validate token belongs to project + self.admin_request( + method='GET', + path=path, + token=admin_token, + expected_status=200) + + def test_token_authentication_and_validation(self): + """Test token authentication for Fernet token provider. + + Verify that token authentication returns validate response code and + valid token belongs to project. + """ + project_ref = self.new_project_ref() + self.resource_api.create_project(project_ref['id'], project_ref) + unscoped_token = self.get_unscoped_token() + self.assignment_api.add_role_to_user_and_project(self.user_foo['id'], + project_ref['id'], + self.role_admin['id']) + r = self.public_request( + method='POST', + path='/v2.0/tokens', + body={ + 'auth': { + 'tenantName': project_ref['name'], + 'token': { + 'id': unscoped_token.encode('ascii') + } + } + }, + expected_status=200) + + token_id = self._get_token_id(r) + path = ('/v2.0/tokens/%s?belongsTo=%s' % (token_id, project_ref['id'])) + # Validate token belongs to project + self.admin_request( + method='GET', + path=path, + token=CONF.admin_token, + expected_status=200) diff --git a/keystone-moon/keystone/tests/unit/test_v2_controller.py b/keystone-moon/keystone/tests/unit/test_v2_controller.py new file mode 100644 index 00000000..6c1edd0a --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_v2_controller.py @@ -0,0 +1,95 @@ +# Copyright 2014 IBM Corp. +# +# 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 keystone.assignment import controllers as assignment_controllers +from keystone.resource import controllers as resource_controllers +from keystone.tests import unit as tests +from keystone.tests.unit import default_fixtures +from keystone.tests.unit.ksfixtures import database + + +_ADMIN_CONTEXT = {'is_admin': True, 'query_string': {}} + + +class TenantTestCase(tests.TestCase): + """Tests for the V2 Tenant controller. + + These tests exercise :class:`keystone.assignment.controllers.Tenant`. + + """ + def setUp(self): + super(TenantTestCase, self).setUp() + self.useFixture(database.Database()) + self.load_backends() + self.load_fixtures(default_fixtures) + self.tenant_controller = resource_controllers.Tenant() + self.assignment_tenant_controller = ( + assignment_controllers.TenantAssignment()) + self.assignment_role_controller = ( + assignment_controllers.RoleAssignmentV2()) + + def test_get_project_users_no_user(self): + """get_project_users when user doesn't exist. + + When a user that's not known to `identity` has a role on a project, + then `get_project_users` just skips that user. + + """ + project_id = self.tenant_bar['id'] + + orig_project_users = ( + self.assignment_tenant_controller.get_project_users(_ADMIN_CONTEXT, + project_id)) + + # Assign a role to a user that doesn't exist to the `bar` project. + + user_id = uuid.uuid4().hex + self.assignment_role_controller.add_role_to_user( + _ADMIN_CONTEXT, user_id, self.role_other['id'], project_id) + + new_project_users = ( + self.assignment_tenant_controller.get_project_users(_ADMIN_CONTEXT, + project_id)) + + # The new user isn't included in the result, so no change. + # asserting that the expected values appear in the list, + # without asserting the order of the results + self.assertEqual(sorted(orig_project_users), sorted(new_project_users)) + + def test_list_projects_default_domain(self): + """Test that list projects only returns those in the default domain.""" + + domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'enabled': True} + self.resource_api.create_domain(domain['id'], domain) + project1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'domain_id': domain['id']} + self.resource_api.create_project(project1['id'], project1) + # Check the real total number of projects, we should have the above + # plus those in the default features + refs = self.resource_api.list_projects() + self.assertEqual(len(default_fixtures.TENANTS) + 1, len(refs)) + + # Now list all projects using the v2 API - we should only get + # back those in the default features, since only those are in the + # default domain. + refs = self.tenant_controller.get_all_projects(_ADMIN_CONTEXT) + self.assertEqual(len(default_fixtures.TENANTS), len(refs['tenants'])) + for tenant in default_fixtures.TENANTS: + tenant_copy = tenant.copy() + tenant_copy.pop('domain_id') + self.assertIn(tenant_copy, refs['tenants']) diff --git a/keystone-moon/keystone/tests/unit/test_v2_keystoneclient.py b/keystone-moon/keystone/tests/unit/test_v2_keystoneclient.py new file mode 100644 index 00000000..7abc5bc4 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_v2_keystoneclient.py @@ -0,0 +1,1045 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 datetime +import uuid + +from keystoneclient import exceptions as client_exceptions +from keystoneclient.v2_0 import client as ks_client +import mock +from oslo_config import cfg +from oslo_serialization import jsonutils +from oslo_utils import timeutils +import webob + +from keystone.tests import unit as tests +from keystone.tests.unit import default_fixtures +from keystone.tests.unit.ksfixtures import appserver +from keystone.tests.unit.ksfixtures import database + + +CONF = cfg.CONF +DEFAULT_DOMAIN_ID = CONF.identity.default_domain_id + + +class ClientDrivenTestCase(tests.TestCase): + + def setUp(self): + super(ClientDrivenTestCase, self).setUp() + + # FIXME(morganfainberg): Since we are running tests through the + # controllers and some internal api drivers are SQL-only, the correct + # approach is to ensure we have the correct backing store. The + # credential api makes some very SQL specific assumptions that should + # be addressed allowing for non-SQL based testing to occur. + self.useFixture(database.Database()) + self.load_backends() + + self.load_fixtures(default_fixtures) + + # TODO(termie): add an admin user to the fixtures and use that user + # override the fixtures, for now + self.assignment_api.add_role_to_user_and_project( + self.user_foo['id'], + self.tenant_bar['id'], + self.role_admin['id']) + + conf = self._paste_config('keystone') + fixture = self.useFixture(appserver.AppServer(conf, appserver.MAIN)) + self.public_server = fixture.server + fixture = self.useFixture(appserver.AppServer(conf, appserver.ADMIN)) + self.admin_server = fixture.server + + self.addCleanup(self.cleanup_instance('public_server', 'admin_server')) + + def _public_url(self): + public_port = self.public_server.socket_info['socket'][1] + return "http://localhost:%s/v2.0" % public_port + + def _admin_url(self): + admin_port = self.admin_server.socket_info['socket'][1] + return "http://localhost:%s/v2.0" % admin_port + + def _client(self, admin=False, **kwargs): + url = self._admin_url() if admin else self._public_url() + kc = ks_client.Client(endpoint=url, + auth_url=self._public_url(), + **kwargs) + kc.authenticate() + # have to manually overwrite the management url after authentication + kc.management_url = url + return kc + + def get_client(self, user_ref=None, tenant_ref=None, admin=False): + if user_ref is None: + user_ref = self.user_foo + if tenant_ref is None: + for user in default_fixtures.USERS: + # The fixture ID is no longer used as the ID in the database + # The fixture ID, however, is still used as part of the + # attribute name when storing the created object on the test + # case. This means that we need to use the fixture ID below to + # find the actial object so that we can get the ID as stored + # in the database to compare against. + if (getattr(self, 'user_%s' % user['id'])['id'] == + user_ref['id']): + tenant_id = user['tenants'][0] + else: + tenant_id = tenant_ref['id'] + + return self._client(username=user_ref['name'], + password=user_ref['password'], + tenant_id=tenant_id, + admin=admin) + + def test_authenticate_tenant_name_and_tenants(self): + client = self.get_client() + tenants = client.tenants.list() + self.assertEqual(self.tenant_bar['id'], tenants[0].id) + + def test_authenticate_tenant_id_and_tenants(self): + client = self._client(username=self.user_foo['name'], + password=self.user_foo['password'], + tenant_id='bar') + tenants = client.tenants.list() + self.assertEqual(self.tenant_bar['id'], tenants[0].id) + + def test_authenticate_invalid_tenant_id(self): + self.assertRaises(client_exceptions.Unauthorized, + self._client, + username=self.user_foo['name'], + password=self.user_foo['password'], + tenant_id='baz') + + def test_authenticate_token_no_tenant(self): + client = self.get_client() + token = client.auth_token + token_client = self._client(token=token) + tenants = token_client.tenants.list() + self.assertEqual(self.tenant_bar['id'], tenants[0].id) + + def test_authenticate_token_tenant_id(self): + client = self.get_client() + token = client.auth_token + token_client = self._client(token=token, tenant_id='bar') + tenants = token_client.tenants.list() + self.assertEqual(self.tenant_bar['id'], tenants[0].id) + + def test_authenticate_token_invalid_tenant_id(self): + client = self.get_client() + token = client.auth_token + self.assertRaises(client_exceptions.Unauthorized, + self._client, token=token, + tenant_id=uuid.uuid4().hex) + + def test_authenticate_token_invalid_tenant_name(self): + client = self.get_client() + token = client.auth_token + self.assertRaises(client_exceptions.Unauthorized, + self._client, token=token, + tenant_name=uuid.uuid4().hex) + + def test_authenticate_token_tenant_name(self): + client = self.get_client() + token = client.auth_token + token_client = self._client(token=token, tenant_name='BAR') + tenants = token_client.tenants.list() + self.assertEqual(self.tenant_bar['id'], tenants[0].id) + self.assertEqual(self.tenant_bar['id'], tenants[0].id) + + def test_authenticate_and_delete_token(self): + client = self.get_client(admin=True) + token = client.auth_token + token_client = self._client(token=token) + tenants = token_client.tenants.list() + self.assertEqual(self.tenant_bar['id'], tenants[0].id) + + client.tokens.delete(token_client.auth_token) + + self.assertRaises(client_exceptions.Unauthorized, + token_client.tenants.list) + + def test_authenticate_no_password(self): + user_ref = self.user_foo.copy() + user_ref['password'] = None + self.assertRaises(client_exceptions.AuthorizationFailure, + self.get_client, + user_ref) + + def test_authenticate_no_username(self): + user_ref = self.user_foo.copy() + user_ref['name'] = None + self.assertRaises(client_exceptions.AuthorizationFailure, + self.get_client, + user_ref) + + def test_authenticate_disabled_tenant(self): + admin_client = self.get_client(admin=True) + + tenant = { + 'name': uuid.uuid4().hex, + 'description': uuid.uuid4().hex, + 'enabled': False, + } + tenant_ref = admin_client.tenants.create( + tenant_name=tenant['name'], + description=tenant['description'], + enabled=tenant['enabled']) + tenant['id'] = tenant_ref.id + + user = { + 'name': uuid.uuid4().hex, + 'password': uuid.uuid4().hex, + 'email': uuid.uuid4().hex, + 'tenant_id': tenant['id'], + } + user_ref = admin_client.users.create( + name=user['name'], + password=user['password'], + email=user['email'], + tenant_id=user['tenant_id']) + user['id'] = user_ref.id + + # password authentication + self.assertRaises( + client_exceptions.Unauthorized, + self._client, + username=user['name'], + password=user['password'], + tenant_id=tenant['id']) + + # token authentication + client = self._client( + username=user['name'], + password=user['password']) + self.assertRaises( + client_exceptions.Unauthorized, + self._client, + token=client.auth_token, + tenant_id=tenant['id']) + + # FIXME(ja): this test should require the "keystone:admin" roled + # (probably the role set via --keystone_admin_role flag) + # FIXME(ja): add a test that admin endpoint is only sent to admin user + # FIXME(ja): add a test that admin endpoint returns unauthorized if not + # admin + def test_tenant_create_update_and_delete(self): + tenant_name = 'original_tenant' + tenant_description = 'My original tenant!' + tenant_enabled = True + client = self.get_client(admin=True) + + # create, get, and list a tenant + tenant = client.tenants.create(tenant_name=tenant_name, + description=tenant_description, + enabled=tenant_enabled) + self.assertEqual(tenant_name, tenant.name) + self.assertEqual(tenant_description, tenant.description) + self.assertEqual(tenant_enabled, tenant.enabled) + + tenant = client.tenants.get(tenant_id=tenant.id) + self.assertEqual(tenant_name, tenant.name) + self.assertEqual(tenant_description, tenant.description) + self.assertEqual(tenant_enabled, tenant.enabled) + + tenant = [t for t in client.tenants.list() if t.id == tenant.id].pop() + self.assertEqual(tenant_name, tenant.name) + self.assertEqual(tenant_description, tenant.description) + self.assertEqual(tenant_enabled, tenant.enabled) + + # update, get, and list a tenant + tenant_name = 'updated_tenant' + tenant_description = 'Updated tenant!' + tenant_enabled = False + tenant = client.tenants.update(tenant_id=tenant.id, + tenant_name=tenant_name, + enabled=tenant_enabled, + description=tenant_description) + self.assertEqual(tenant_name, tenant.name) + self.assertEqual(tenant_description, tenant.description) + self.assertEqual(tenant_enabled, tenant.enabled) + + tenant = client.tenants.get(tenant_id=tenant.id) + self.assertEqual(tenant_name, tenant.name) + self.assertEqual(tenant_description, tenant.description) + self.assertEqual(tenant_enabled, tenant.enabled) + + tenant = [t for t in client.tenants.list() if t.id == tenant.id].pop() + self.assertEqual(tenant_name, tenant.name) + self.assertEqual(tenant_description, tenant.description) + self.assertEqual(tenant_enabled, tenant.enabled) + + # delete, get, and list a tenant + client.tenants.delete(tenant=tenant.id) + self.assertRaises(client_exceptions.NotFound, client.tenants.get, + tenant.id) + self.assertFalse([t for t in client.tenants.list() + if t.id == tenant.id]) + + def test_tenant_create_update_and_delete_unicode(self): + tenant_name = u'original \u540d\u5b57' + tenant_description = 'My original tenant!' + tenant_enabled = True + client = self.get_client(admin=True) + + # create, get, and list a tenant + tenant = client.tenants.create(tenant_name, + description=tenant_description, + enabled=tenant_enabled) + self.assertEqual(tenant_name, tenant.name) + self.assertEqual(tenant_description, tenant.description) + self.assertIs(tenant.enabled, tenant_enabled) + + tenant = client.tenants.get(tenant.id) + self.assertEqual(tenant_name, tenant.name) + self.assertEqual(tenant_description, tenant.description) + self.assertIs(tenant.enabled, tenant_enabled) + + # multiple tenants exist due to fixtures, so find the one we're testing + tenant = [t for t in client.tenants.list() if t.id == tenant.id].pop() + self.assertEqual(tenant_name, tenant.name) + self.assertEqual(tenant_description, tenant.description) + self.assertIs(tenant.enabled, tenant_enabled) + + # update, get, and list a tenant + tenant_name = u'updated \u540d\u5b57' + tenant_description = 'Updated tenant!' + tenant_enabled = False + tenant = client.tenants.update(tenant.id, + tenant_name=tenant_name, + enabled=tenant_enabled, + description=tenant_description) + self.assertEqual(tenant_name, tenant.name) + self.assertEqual(tenant_description, tenant.description) + self.assertIs(tenant.enabled, tenant_enabled) + + tenant = client.tenants.get(tenant.id) + self.assertEqual(tenant_name, tenant.name) + self.assertEqual(tenant_description, tenant.description) + self.assertIs(tenant.enabled, tenant_enabled) + + tenant = [t for t in client.tenants.list() if t.id == tenant.id].pop() + self.assertEqual(tenant_name, tenant.name) + self.assertEqual(tenant_description, tenant.description) + self.assertIs(tenant.enabled, tenant_enabled) + + # delete, get, and list a tenant + client.tenants.delete(tenant.id) + self.assertRaises(client_exceptions.NotFound, client.tenants.get, + tenant.id) + self.assertFalse([t for t in client.tenants.list() + if t.id == tenant.id]) + + def test_tenant_create_no_name(self): + client = self.get_client(admin=True) + self.assertRaises(client_exceptions.BadRequest, + client.tenants.create, + tenant_name="") + + def test_tenant_delete_404(self): + client = self.get_client(admin=True) + self.assertRaises(client_exceptions.NotFound, + client.tenants.delete, + tenant=uuid.uuid4().hex) + + def test_tenant_get_404(self): + client = self.get_client(admin=True) + self.assertRaises(client_exceptions.NotFound, + client.tenants.get, + tenant_id=uuid.uuid4().hex) + + def test_tenant_update_404(self): + client = self.get_client(admin=True) + self.assertRaises(client_exceptions.NotFound, + client.tenants.update, + tenant_id=uuid.uuid4().hex) + + def test_tenant_list(self): + client = self.get_client() + tenants = client.tenants.list() + self.assertEqual(1, len(tenants)) + + # Admin endpoint should return *all* tenants + client = self.get_client(admin=True) + tenants = client.tenants.list() + self.assertEqual(len(default_fixtures.TENANTS), len(tenants)) + + def test_invalid_password(self): + good_client = self._client(username=self.user_foo['name'], + password=self.user_foo['password']) + good_client.tenants.list() + + self.assertRaises(client_exceptions.Unauthorized, + self._client, + username=self.user_foo['name'], + password=uuid.uuid4().hex) + + def test_invalid_user_and_password(self): + self.assertRaises(client_exceptions.Unauthorized, + self._client, + username=uuid.uuid4().hex, + password=uuid.uuid4().hex) + + def test_change_password_invalidates_token(self): + admin_client = self.get_client(admin=True) + + username = uuid.uuid4().hex + password = uuid.uuid4().hex + user = admin_client.users.create(name=username, password=password, + email=uuid.uuid4().hex) + + # auth as user should work before a password change + client = self._client(username=username, password=password) + + # auth as user with a token should work before a password change + self._client(token=client.auth_token) + + # administrative password reset + admin_client.users.update_password( + user=user.id, + password=uuid.uuid4().hex) + + # auth as user with original password should not work after change + self.assertRaises(client_exceptions.Unauthorized, + self._client, + username=username, + password=password) + + # authenticate with an old token should not work after change + self.assertRaises(client_exceptions.Unauthorized, + self._client, + token=client.auth_token) + + def test_user_change_own_password_invalidates_token(self): + # bootstrap a user as admin + client = self.get_client(admin=True) + username = uuid.uuid4().hex + password = uuid.uuid4().hex + client.users.create(name=username, password=password, + email=uuid.uuid4().hex) + + # auth as user should work before a password change + client = self._client(username=username, password=password) + + # auth as user with a token should work before a password change + self._client(token=client.auth_token) + + # change the user's own password + # TODO(dolphm): This should NOT raise an HTTPError at all, but rather + # this should succeed with a 2xx. This 500 does not prevent the test + # from demonstrating the desired consequences below, though. + self.assertRaises(client_exceptions.HTTPError, + client.users.update_own_password, + password, uuid.uuid4().hex) + + # auth as user with original password should not work after change + self.assertRaises(client_exceptions.Unauthorized, + self._client, + username=username, + password=password) + + # auth as user with an old token should not work after change + self.assertRaises(client_exceptions.Unauthorized, + self._client, + token=client.auth_token) + + def test_disable_tenant_invalidates_token(self): + admin_client = self.get_client(admin=True) + foo_client = self.get_client(self.user_foo) + tenant_bar = admin_client.tenants.get(self.tenant_bar['id']) + + # Disable the tenant. + tenant_bar.update(enabled=False) + + # Test that the token has been removed. + self.assertRaises(client_exceptions.Unauthorized, + foo_client.tokens.authenticate, + token=foo_client.auth_token) + + # Test that the user access has been disabled. + self.assertRaises(client_exceptions.Unauthorized, + self.get_client, + self.user_foo) + + def test_delete_tenant_invalidates_token(self): + admin_client = self.get_client(admin=True) + foo_client = self.get_client(self.user_foo) + tenant_bar = admin_client.tenants.get(self.tenant_bar['id']) + + # Delete the tenant. + tenant_bar.delete() + + # Test that the token has been removed. + self.assertRaises(client_exceptions.Unauthorized, + foo_client.tokens.authenticate, + token=foo_client.auth_token) + + # Test that the user access has been disabled. + self.assertRaises(client_exceptions.Unauthorized, + self.get_client, + self.user_foo) + + def test_disable_user_invalidates_token(self): + admin_client = self.get_client(admin=True) + foo_client = self.get_client(self.user_foo) + + admin_client.users.update_enabled(user=self.user_foo['id'], + enabled=False) + + self.assertRaises(client_exceptions.Unauthorized, + foo_client.tokens.authenticate, + token=foo_client.auth_token) + + self.assertRaises(client_exceptions.Unauthorized, + self.get_client, + self.user_foo) + + def test_delete_user_invalidates_token(self): + admin_client = self.get_client(admin=True) + client = self.get_client(admin=False) + + username = uuid.uuid4().hex + password = uuid.uuid4().hex + user_id = admin_client.users.create( + name=username, password=password, email=uuid.uuid4().hex).id + + token_id = client.tokens.authenticate( + username=username, password=password).id + + # token should be usable before the user is deleted + client.tokens.authenticate(token=token_id) + + admin_client.users.delete(user=user_id) + + # authenticate with a token should not work after the user is deleted + self.assertRaises(client_exceptions.Unauthorized, + client.tokens.authenticate, + token=token_id) + + @mock.patch.object(timeutils, 'utcnow') + def test_token_expiry_maintained(self, mock_utcnow): + now = datetime.datetime.utcnow() + mock_utcnow.return_value = now + foo_client = self.get_client(self.user_foo) + + orig_token = foo_client.service_catalog.catalog['token'] + mock_utcnow.return_value = now + datetime.timedelta(seconds=1) + reauthenticated_token = foo_client.tokens.authenticate( + token=foo_client.auth_token) + + self.assertCloseEnoughForGovernmentWork( + timeutils.parse_isotime(orig_token['expires']), + timeutils.parse_isotime(reauthenticated_token.expires)) + + def test_user_create_update_delete(self): + test_username = 'new_user' + client = self.get_client(admin=True) + user = client.users.create(name=test_username, + password='password', + email='user1@test.com') + self.assertEqual(test_username, user.name) + + user = client.users.get(user=user.id) + self.assertEqual(test_username, user.name) + + user = client.users.update(user=user, + name=test_username, + email='user2@test.com') + self.assertEqual('user2@test.com', user.email) + + # NOTE(termie): update_enabled doesn't return anything, probably a bug + client.users.update_enabled(user=user, enabled=False) + user = client.users.get(user.id) + self.assertFalse(user.enabled) + + self.assertRaises(client_exceptions.Unauthorized, + self._client, + username=test_username, + password='password') + client.users.update_enabled(user, True) + + user = client.users.update_password(user=user, password='password2') + + self._client(username=test_username, + password='password2') + + user = client.users.update_tenant(user=user, tenant='bar') + # TODO(ja): once keystonelight supports default tenant + # when you login without specifying tenant, the + # token should be scoped to tenant 'bar' + + client.users.delete(user.id) + self.assertRaises(client_exceptions.NotFound, client.users.get, + user.id) + + # Test creating a user with a tenant (auto-add to tenant) + user2 = client.users.create(name=test_username, + password='password', + email='user1@test.com', + tenant_id='bar') + self.assertEqual(test_username, user2.name) + + def test_update_default_tenant_to_existing_value(self): + client = self.get_client(admin=True) + + user = client.users.create( + name=uuid.uuid4().hex, + password=uuid.uuid4().hex, + email=uuid.uuid4().hex, + tenant_id=self.tenant_bar['id']) + + # attempting to update the tenant with the existing value should work + user = client.users.update_tenant( + user=user, tenant=self.tenant_bar['id']) + + def test_user_create_no_string_password(self): + client = self.get_client(admin=True) + self.assertRaises(client_exceptions.BadRequest, + client.users.create, + name='test_user', + password=12345, + email=uuid.uuid4().hex) + + def test_user_create_no_name(self): + client = self.get_client(admin=True) + self.assertRaises(client_exceptions.BadRequest, + client.users.create, + name="", + password=uuid.uuid4().hex, + email=uuid.uuid4().hex) + + def test_user_create_404(self): + client = self.get_client(admin=True) + self.assertRaises(client_exceptions.NotFound, + client.users.create, + name=uuid.uuid4().hex, + password=uuid.uuid4().hex, + email=uuid.uuid4().hex, + tenant_id=uuid.uuid4().hex) + + def test_user_get_404(self): + client = self.get_client(admin=True) + self.assertRaises(client_exceptions.NotFound, + client.users.get, + user=uuid.uuid4().hex) + + def test_user_list_404(self): + client = self.get_client(admin=True) + self.assertRaises(client_exceptions.NotFound, + client.users.list, + tenant_id=uuid.uuid4().hex) + + def test_user_update_404(self): + client = self.get_client(admin=True) + self.assertRaises(client_exceptions.NotFound, + client.users.update, + user=uuid.uuid4().hex) + + def test_user_update_tenant(self): + client = self.get_client(admin=True) + tenant_id = uuid.uuid4().hex + user = client.users.update(user=self.user_foo['id'], + tenant_id=tenant_id) + self.assertEqual(tenant_id, user.tenant_id) + + def test_user_update_password_404(self): + client = self.get_client(admin=True) + self.assertRaises(client_exceptions.NotFound, + client.users.update_password, + user=uuid.uuid4().hex, + password=uuid.uuid4().hex) + + def test_user_delete_404(self): + client = self.get_client(admin=True) + self.assertRaises(client_exceptions.NotFound, + client.users.delete, + user=uuid.uuid4().hex) + + def test_user_list(self): + client = self.get_client(admin=True) + users = client.users.list() + self.assertTrue(len(users) > 0) + user = users[0] + self.assertRaises(AttributeError, lambda: user.password) + + def test_user_get(self): + client = self.get_client(admin=True) + user = client.users.get(user=self.user_foo['id']) + self.assertRaises(AttributeError, lambda: user.password) + + def test_role_get(self): + client = self.get_client(admin=True) + role = client.roles.get(role=self.role_admin['id']) + self.assertEqual(self.role_admin['id'], role.id) + + def test_role_crud(self): + test_role = 'new_role' + client = self.get_client(admin=True) + role = client.roles.create(name=test_role) + self.assertEqual(test_role, role.name) + + role = client.roles.get(role=role.id) + self.assertEqual(test_role, role.name) + + client.roles.delete(role=role.id) + + self.assertRaises(client_exceptions.NotFound, + client.roles.delete, + role=role.id) + self.assertRaises(client_exceptions.NotFound, + client.roles.get, + role=role.id) + + def test_role_create_no_name(self): + client = self.get_client(admin=True) + self.assertRaises(client_exceptions.BadRequest, + client.roles.create, + name="") + + def test_role_get_404(self): + client = self.get_client(admin=True) + self.assertRaises(client_exceptions.NotFound, + client.roles.get, + role=uuid.uuid4().hex) + + def test_role_delete_404(self): + client = self.get_client(admin=True) + self.assertRaises(client_exceptions.NotFound, + client.roles.delete, + role=uuid.uuid4().hex) + + def test_role_list_404(self): + client = self.get_client(admin=True) + self.assertRaises(client_exceptions.NotFound, + client.roles.roles_for_user, + user=uuid.uuid4().hex, + tenant=uuid.uuid4().hex) + self.assertRaises(client_exceptions.NotFound, + client.roles.roles_for_user, + user=self.user_foo['id'], + tenant=uuid.uuid4().hex) + self.assertRaises(client_exceptions.NotFound, + client.roles.roles_for_user, + user=uuid.uuid4().hex, + tenant=self.tenant_bar['id']) + + def test_role_list(self): + client = self.get_client(admin=True) + roles = client.roles.list() + # TODO(devcamcar): This assert should be more specific. + self.assertTrue(len(roles) > 0) + + def test_service_crud(self): + client = self.get_client(admin=True) + + service_name = uuid.uuid4().hex + service_type = uuid.uuid4().hex + service_desc = uuid.uuid4().hex + + # create & read + service = client.services.create(name=service_name, + service_type=service_type, + description=service_desc) + self.assertEqual(service_name, service.name) + self.assertEqual(service_type, service.type) + self.assertEqual(service_desc, service.description) + + service = client.services.get(id=service.id) + self.assertEqual(service_name, service.name) + self.assertEqual(service_type, service.type) + self.assertEqual(service_desc, service.description) + + service = [x for x in client.services.list() if x.id == service.id][0] + self.assertEqual(service_name, service.name) + self.assertEqual(service_type, service.type) + self.assertEqual(service_desc, service.description) + + # update is not supported in API v2... + + # delete & read + client.services.delete(id=service.id) + self.assertRaises(client_exceptions.NotFound, + client.services.get, + id=service.id) + services = [x for x in client.services.list() if x.id == service.id] + self.assertEqual(0, len(services)) + + def test_service_delete_404(self): + client = self.get_client(admin=True) + self.assertRaises(client_exceptions.NotFound, + client.services.delete, + id=uuid.uuid4().hex) + + def test_service_get_404(self): + client = self.get_client(admin=True) + self.assertRaises(client_exceptions.NotFound, + client.services.get, + id=uuid.uuid4().hex) + + def test_endpoint_delete_404(self): + client = self.get_client(admin=True) + self.assertRaises(client_exceptions.NotFound, + client.endpoints.delete, + id=uuid.uuid4().hex) + + def test_admin_requires_adminness(self): + # FIXME(ja): this should be Unauthorized + exception = client_exceptions.ClientException + + two = self.get_client(self.user_two, admin=True) # non-admin user + + # USER CRUD + self.assertRaises(exception, + two.users.list) + self.assertRaises(exception, + two.users.get, + user=self.user_two['id']) + self.assertRaises(exception, + two.users.create, + name='oops', + password='password', + email='oops@test.com') + self.assertRaises(exception, + two.users.delete, + user=self.user_foo['id']) + + # TENANT CRUD + self.assertRaises(exception, + two.tenants.list) + self.assertRaises(exception, + two.tenants.get, + tenant_id=self.tenant_bar['id']) + self.assertRaises(exception, + two.tenants.create, + tenant_name='oops', + description="shouldn't work!", + enabled=True) + self.assertRaises(exception, + two.tenants.delete, + tenant=self.tenant_baz['id']) + + # ROLE CRUD + self.assertRaises(exception, + two.roles.get, + role=self.role_admin['id']) + self.assertRaises(exception, + two.roles.list) + self.assertRaises(exception, + two.roles.create, + name='oops') + self.assertRaises(exception, + two.roles.delete, + role=self.role_admin['id']) + + # TODO(ja): MEMBERSHIP CRUD + # TODO(ja): determine what else todo + + def test_tenant_add_and_remove_user(self): + client = self.get_client(admin=True) + client.roles.add_user_role(tenant=self.tenant_bar['id'], + user=self.user_two['id'], + role=self.role_other['id']) + user_refs = client.tenants.list_users(tenant=self.tenant_bar['id']) + self.assertIn(self.user_two['id'], [x.id for x in user_refs]) + client.roles.remove_user_role(tenant=self.tenant_bar['id'], + user=self.user_two['id'], + role=self.role_other['id']) + roles = client.roles.roles_for_user(user=self.user_foo['id'], + tenant=self.tenant_bar['id']) + self.assertNotIn(self.role_other['id'], roles) + user_refs = client.tenants.list_users(tenant=self.tenant_bar['id']) + self.assertNotIn(self.user_two['id'], [x.id for x in user_refs]) + + def test_user_role_add_404(self): + client = self.get_client(admin=True) + self.assertRaises(client_exceptions.NotFound, + client.roles.add_user_role, + tenant=uuid.uuid4().hex, + user=self.user_foo['id'], + role=self.role_member['id']) + self.assertRaises(client_exceptions.NotFound, + client.roles.add_user_role, + tenant=self.tenant_baz['id'], + user=self.user_foo['id'], + role=uuid.uuid4().hex) + + def test_user_role_add_no_user(self): + # If add_user_role and user doesn't exist, doesn't fail. + client = self.get_client(admin=True) + client.roles.add_user_role(tenant=self.tenant_baz['id'], + user=uuid.uuid4().hex, + role=self.role_member['id']) + + def test_user_role_remove_404(self): + client = self.get_client(admin=True) + self.assertRaises(client_exceptions.NotFound, + client.roles.remove_user_role, + tenant=uuid.uuid4().hex, + user=self.user_foo['id'], + role=self.role_member['id']) + self.assertRaises(client_exceptions.NotFound, + client.roles.remove_user_role, + tenant=self.tenant_baz['id'], + user=uuid.uuid4().hex, + role=self.role_member['id']) + self.assertRaises(client_exceptions.NotFound, + client.roles.remove_user_role, + tenant=self.tenant_baz['id'], + user=self.user_foo['id'], + role=uuid.uuid4().hex) + self.assertRaises(client_exceptions.NotFound, + client.roles.remove_user_role, + tenant=self.tenant_baz['id'], + user=self.user_foo['id'], + role=self.role_member['id']) + + def test_tenant_list_marker(self): + client = self.get_client() + + # Add two arbitrary tenants to user for testing purposes + for i in range(2): + tenant_id = uuid.uuid4().hex + tenant = {'name': 'tenant-%s' % tenant_id, 'id': tenant_id, + 'domain_id': DEFAULT_DOMAIN_ID} + self.resource_api.create_project(tenant_id, tenant) + self.assignment_api.add_user_to_project(tenant_id, + self.user_foo['id']) + + tenants = client.tenants.list() + self.assertEqual(3, len(tenants)) + + tenants_marker = client.tenants.list(marker=tenants[0].id) + self.assertEqual(2, len(tenants_marker)) + self.assertEqual(tenants_marker[0].name, tenants[1].name) + self.assertEqual(tenants_marker[1].name, tenants[2].name) + + def test_tenant_list_marker_not_found(self): + client = self.get_client() + self.assertRaises(client_exceptions.BadRequest, + client.tenants.list, marker=uuid.uuid4().hex) + + def test_tenant_list_limit(self): + client = self.get_client() + + # Add two arbitrary tenants to user for testing purposes + for i in range(2): + tenant_id = uuid.uuid4().hex + tenant = {'name': 'tenant-%s' % tenant_id, 'id': tenant_id, + 'domain_id': DEFAULT_DOMAIN_ID} + self.resource_api.create_project(tenant_id, tenant) + self.assignment_api.add_user_to_project(tenant_id, + self.user_foo['id']) + + tenants = client.tenants.list() + self.assertEqual(3, len(tenants)) + + tenants_limited = client.tenants.list(limit=2) + self.assertEqual(2, len(tenants_limited)) + self.assertEqual(tenants[0].name, tenants_limited[0].name) + self.assertEqual(tenants[1].name, tenants_limited[1].name) + + def test_tenant_list_limit_bad_value(self): + client = self.get_client() + self.assertRaises(client_exceptions.BadRequest, + client.tenants.list, limit='a') + self.assertRaises(client_exceptions.BadRequest, + client.tenants.list, limit=-1) + + def test_roles_get_by_user(self): + client = self.get_client(admin=True) + roles = client.roles.roles_for_user(user=self.user_foo['id'], + tenant=self.tenant_bar['id']) + self.assertTrue(len(roles) > 0) + + def test_user_can_update_passwd(self): + client = self.get_client(self.user_two) + + token_id = client.auth_token + new_password = uuid.uuid4().hex + + # TODO(derekh): Update to use keystoneclient when available + class FakeResponse(object): + def start_fake_response(self, status, headers): + self.response_status = int(status.split(' ', 1)[0]) + self.response_headers = dict(headers) + responseobject = FakeResponse() + + req = webob.Request.blank( + '/v2.0/OS-KSCRUD/users/%s' % self.user_two['id'], + headers={'X-Auth-Token': token_id}) + req.method = 'PATCH' + req.body = ('{"user":{"password":"%s","original_password":"%s"}}' % + (new_password, self.user_two['password'])) + self.public_server.application(req.environ, + responseobject.start_fake_response) + + self.user_two['password'] = new_password + self.get_client(self.user_two) + + def test_user_cannot_update_other_users_passwd(self): + client = self.get_client(self.user_two) + + token_id = client.auth_token + new_password = uuid.uuid4().hex + + # TODO(derekh): Update to use keystoneclient when available + class FakeResponse(object): + def start_fake_response(self, status, headers): + self.response_status = int(status.split(' ', 1)[0]) + self.response_headers = dict(headers) + responseobject = FakeResponse() + + req = webob.Request.blank( + '/v2.0/OS-KSCRUD/users/%s' % self.user_foo['id'], + headers={'X-Auth-Token': token_id}) + req.method = 'PATCH' + req.body = ('{"user":{"password":"%s","original_password":"%s"}}' % + (new_password, self.user_two['password'])) + self.public_server.application(req.environ, + responseobject.start_fake_response) + self.assertEqual(403, responseobject.response_status) + + self.user_two['password'] = new_password + self.assertRaises(client_exceptions.Unauthorized, + self.get_client, self.user_two) + + def test_tokens_after_user_update_passwd(self): + client = self.get_client(self.user_two) + + token_id = client.auth_token + new_password = uuid.uuid4().hex + + # TODO(derekh): Update to use keystoneclient when available + class FakeResponse(object): + def start_fake_response(self, status, headers): + self.response_status = int(status.split(' ', 1)[0]) + self.response_headers = dict(headers) + responseobject = FakeResponse() + + req = webob.Request.blank( + '/v2.0/OS-KSCRUD/users/%s' % self.user_two['id'], + headers={'X-Auth-Token': token_id}) + req.method = 'PATCH' + req.body = ('{"user":{"password":"%s","original_password":"%s"}}' % + (new_password, self.user_two['password'])) + + rv = self.public_server.application( + req.environ, + responseobject.start_fake_response) + response_json = jsonutils.loads(rv.pop()) + new_token_id = response_json['access']['token']['id'] + + self.assertRaises(client_exceptions.Unauthorized, client.tenants.list) + client.auth_token = new_token_id + client.tenants.list() diff --git a/keystone-moon/keystone/tests/unit/test_v2_keystoneclient_sql.py b/keystone-moon/keystone/tests/unit/test_v2_keystoneclient_sql.py new file mode 100644 index 00000000..0fb60fd9 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_v2_keystoneclient_sql.py @@ -0,0 +1,344 @@ +# Copyright 2012 OpenStack Foundation +# +# 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.contrib.ec2 import utils as ec2_utils +from keystoneclient import exceptions as client_exceptions + +from keystone.tests import unit as tests +from keystone.tests.unit import test_v2_keystoneclient + + +class ClientDrivenSqlTestCase(test_v2_keystoneclient.ClientDrivenTestCase): + def config_files(self): + config_files = super(ClientDrivenSqlTestCase, self).config_files() + config_files.append(tests.dirs.tests_conf('backend_sql.conf')) + return config_files + + def setUp(self): + super(ClientDrivenSqlTestCase, self).setUp() + self.default_client = self.get_client() + self.addCleanup(self.cleanup_instance('default_client')) + + def test_endpoint_crud(self): + client = self.get_client(admin=True) + + service = client.services.create(name=uuid.uuid4().hex, + service_type=uuid.uuid4().hex, + description=uuid.uuid4().hex) + + endpoint_region = uuid.uuid4().hex + invalid_service_id = uuid.uuid4().hex + endpoint_publicurl = uuid.uuid4().hex + endpoint_internalurl = uuid.uuid4().hex + endpoint_adminurl = uuid.uuid4().hex + + # a non-existent service ID should trigger a 400 + self.assertRaises(client_exceptions.BadRequest, + client.endpoints.create, + region=endpoint_region, + service_id=invalid_service_id, + publicurl=endpoint_publicurl, + adminurl=endpoint_adminurl, + internalurl=endpoint_internalurl) + + endpoint = client.endpoints.create(region=endpoint_region, + service_id=service.id, + publicurl=endpoint_publicurl, + adminurl=endpoint_adminurl, + internalurl=endpoint_internalurl) + + self.assertEqual(endpoint_region, endpoint.region) + self.assertEqual(service.id, endpoint.service_id) + self.assertEqual(endpoint_publicurl, endpoint.publicurl) + self.assertEqual(endpoint_internalurl, endpoint.internalurl) + self.assertEqual(endpoint_adminurl, endpoint.adminurl) + + client.endpoints.delete(id=endpoint.id) + self.assertRaises(client_exceptions.NotFound, client.endpoints.delete, + id=endpoint.id) + + def _send_ec2_auth_request(self, credentials, client=None): + if not client: + client = self.default_client + url = '%s/ec2tokens' % self.default_client.auth_url + (resp, token) = client.request( + url=url, method='POST', + body={'credentials': credentials}) + return resp, token + + def _generate_default_user_ec2_credentials(self): + cred = self. default_client.ec2.create( + user_id=self.user_foo['id'], + tenant_id=self.tenant_bar['id']) + return self._generate_user_ec2_credentials(cred.access, cred.secret) + + def _generate_user_ec2_credentials(self, access, secret): + signer = ec2_utils.Ec2Signer(secret) + credentials = {'params': {'SignatureVersion': '2'}, + 'access': access, + 'verb': 'GET', + 'host': 'localhost', + 'path': '/service/cloud'} + signature = signer.generate(credentials) + return credentials, signature + + def test_ec2_auth_success(self): + credentials, signature = self._generate_default_user_ec2_credentials() + credentials['signature'] = signature + resp, token = self._send_ec2_auth_request(credentials) + self.assertEqual(200, resp.status_code) + self.assertIn('access', token) + + def test_ec2_auth_success_trust(self): + # Add "other" role user_foo and create trust delegating it to user_two + self.assignment_api.add_role_to_user_and_project( + self.user_foo['id'], + self.tenant_bar['id'], + self.role_other['id']) + trust_id = 'atrust123' + trust = {'trustor_user_id': self.user_foo['id'], + 'trustee_user_id': self.user_two['id'], + 'project_id': self.tenant_bar['id'], + 'impersonation': True} + roles = [self.role_other] + self.trust_api.create_trust(trust_id, trust, roles) + + # Create a client for user_two, scoped to the trust + client = self.get_client(self.user_two) + ret = client.authenticate(trust_id=trust_id, + tenant_id=self.tenant_bar['id']) + self.assertTrue(ret) + self.assertTrue(client.auth_ref.trust_scoped) + self.assertEqual(trust_id, client.auth_ref.trust_id) + + # Create an ec2 keypair using the trust client impersonating user_foo + cred = client.ec2.create(user_id=self.user_foo['id'], + tenant_id=self.tenant_bar['id']) + credentials, signature = self._generate_user_ec2_credentials( + cred.access, cred.secret) + credentials['signature'] = signature + resp, token = self._send_ec2_auth_request(credentials) + self.assertEqual(200, resp.status_code) + self.assertEqual(trust_id, token['access']['trust']['id']) + # TODO(shardy) we really want to check the roles and trustee + # but because of where the stubbing happens we don't seem to + # hit the necessary code in controllers.py _authenticate_token + # so although all is OK via a real request, it incorrect in + # this test.. + + def test_ec2_auth_failure(self): + credentials, signature = self._generate_default_user_ec2_credentials() + credentials['signature'] = uuid.uuid4().hex + self.assertRaises(client_exceptions.Unauthorized, + self._send_ec2_auth_request, + credentials) + + def test_ec2_credential_crud(self): + creds = self.default_client.ec2.list(user_id=self.user_foo['id']) + self.assertEqual([], creds) + + cred = self.default_client.ec2.create(user_id=self.user_foo['id'], + tenant_id=self.tenant_bar['id']) + creds = self.default_client.ec2.list(user_id=self.user_foo['id']) + self.assertEqual(creds, [cred]) + got = self.default_client.ec2.get(user_id=self.user_foo['id'], + access=cred.access) + self.assertEqual(cred, got) + + self.default_client.ec2.delete(user_id=self.user_foo['id'], + access=cred.access) + creds = self.default_client.ec2.list(user_id=self.user_foo['id']) + self.assertEqual([], creds) + + def test_ec2_credential_crud_non_admin(self): + na_client = self.get_client(self.user_two) + creds = na_client.ec2.list(user_id=self.user_two['id']) + self.assertEqual([], creds) + + cred = na_client.ec2.create(user_id=self.user_two['id'], + tenant_id=self.tenant_baz['id']) + creds = na_client.ec2.list(user_id=self.user_two['id']) + self.assertEqual(creds, [cred]) + got = na_client.ec2.get(user_id=self.user_two['id'], + access=cred.access) + self.assertEqual(cred, got) + + na_client.ec2.delete(user_id=self.user_two['id'], + access=cred.access) + creds = na_client.ec2.list(user_id=self.user_two['id']) + self.assertEqual([], creds) + + def test_ec2_list_credentials(self): + cred_1 = self.default_client.ec2.create( + user_id=self.user_foo['id'], + tenant_id=self.tenant_bar['id']) + cred_2 = self.default_client.ec2.create( + user_id=self.user_foo['id'], + tenant_id=self.tenant_service['id']) + cred_3 = self.default_client.ec2.create( + user_id=self.user_foo['id'], + tenant_id=self.tenant_mtu['id']) + two = self.get_client(self.user_two) + cred_4 = two.ec2.create(user_id=self.user_two['id'], + tenant_id=self.tenant_bar['id']) + creds = self.default_client.ec2.list(user_id=self.user_foo['id']) + self.assertEqual(3, len(creds)) + self.assertEqual(sorted([cred_1, cred_2, cred_3], + key=lambda x: x.access), + sorted(creds, key=lambda x: x.access)) + self.assertNotIn(cred_4, creds) + + def test_ec2_credentials_create_404(self): + self.assertRaises(client_exceptions.NotFound, + self.default_client.ec2.create, + user_id=uuid.uuid4().hex, + tenant_id=self.tenant_bar['id']) + self.assertRaises(client_exceptions.NotFound, + self.default_client.ec2.create, + user_id=self.user_foo['id'], + tenant_id=uuid.uuid4().hex) + + def test_ec2_credentials_delete_404(self): + self.assertRaises(client_exceptions.NotFound, + self.default_client.ec2.delete, + user_id=uuid.uuid4().hex, + access=uuid.uuid4().hex) + + def test_ec2_credentials_get_404(self): + self.assertRaises(client_exceptions.NotFound, + self.default_client.ec2.get, + user_id=uuid.uuid4().hex, + access=uuid.uuid4().hex) + + def test_ec2_credentials_list_404(self): + self.assertRaises(client_exceptions.NotFound, + self.default_client.ec2.list, + user_id=uuid.uuid4().hex) + + def test_ec2_credentials_list_user_forbidden(self): + two = self.get_client(self.user_two) + self.assertRaises(client_exceptions.Forbidden, two.ec2.list, + user_id=self.user_foo['id']) + + def test_ec2_credentials_get_user_forbidden(self): + cred = self.default_client.ec2.create(user_id=self.user_foo['id'], + tenant_id=self.tenant_bar['id']) + + two = self.get_client(self.user_two) + self.assertRaises(client_exceptions.Forbidden, two.ec2.get, + user_id=self.user_foo['id'], access=cred.access) + + self.default_client.ec2.delete(user_id=self.user_foo['id'], + access=cred.access) + + def test_ec2_credentials_delete_user_forbidden(self): + cred = self.default_client.ec2.create(user_id=self.user_foo['id'], + tenant_id=self.tenant_bar['id']) + + two = self.get_client(self.user_two) + self.assertRaises(client_exceptions.Forbidden, two.ec2.delete, + user_id=self.user_foo['id'], access=cred.access) + + self.default_client.ec2.delete(user_id=self.user_foo['id'], + access=cred.access) + + def test_endpoint_create_nonexistent_service(self): + client = self.get_client(admin=True) + self.assertRaises(client_exceptions.BadRequest, + client.endpoints.create, + region=uuid.uuid4().hex, + service_id=uuid.uuid4().hex, + publicurl=uuid.uuid4().hex, + adminurl=uuid.uuid4().hex, + internalurl=uuid.uuid4().hex) + + def test_endpoint_delete_404(self): + client = self.get_client(admin=True) + self.assertRaises(client_exceptions.NotFound, + client.endpoints.delete, + id=uuid.uuid4().hex) + + def test_policy_crud(self): + # FIXME(dolph): this test was written prior to the v3 implementation of + # the client and essentially refers to a non-existent + # policy manager in the v2 client. this test needs to be + # moved to a test suite running against the v3 api + self.skipTest('Written prior to v3 client; needs refactor') + + client = self.get_client(admin=True) + + policy_blob = uuid.uuid4().hex + policy_type = uuid.uuid4().hex + service = client.services.create( + name=uuid.uuid4().hex, + service_type=uuid.uuid4().hex, + description=uuid.uuid4().hex) + endpoint = client.endpoints.create( + service_id=service.id, + region=uuid.uuid4().hex, + adminurl=uuid.uuid4().hex, + internalurl=uuid.uuid4().hex, + publicurl=uuid.uuid4().hex) + + # create + policy = client.policies.create( + blob=policy_blob, + type=policy_type, + endpoint=endpoint.id) + self.assertEqual(policy_blob, policy.policy) + self.assertEqual(policy_type, policy.type) + self.assertEqual(endpoint.id, policy.endpoint_id) + + policy = client.policies.get(policy=policy.id) + self.assertEqual(policy_blob, policy.policy) + self.assertEqual(policy_type, policy.type) + self.assertEqual(endpoint.id, policy.endpoint_id) + + endpoints = [x for x in client.endpoints.list() if x.id == endpoint.id] + endpoint = endpoints[0] + self.assertEqual(policy_blob, policy.policy) + self.assertEqual(policy_type, policy.type) + self.assertEqual(endpoint.id, policy.endpoint_id) + + # update + policy_blob = uuid.uuid4().hex + policy_type = uuid.uuid4().hex + endpoint = client.endpoints.create( + service_id=service.id, + region=uuid.uuid4().hex, + adminurl=uuid.uuid4().hex, + internalurl=uuid.uuid4().hex, + publicurl=uuid.uuid4().hex) + + policy = client.policies.update( + policy=policy.id, + blob=policy_blob, + type=policy_type, + endpoint=endpoint.id) + + policy = client.policies.get(policy=policy.id) + self.assertEqual(policy_blob, policy.policy) + self.assertEqual(policy_type, policy.type) + self.assertEqual(endpoint.id, policy.endpoint_id) + + # delete + client.policies.delete(policy=policy.id) + self.assertRaises( + client_exceptions.NotFound, + client.policies.get, + policy=policy.id) + policies = [x for x in client.policies.list() if x.id == policy.id] + self.assertEqual(0, len(policies)) diff --git a/keystone-moon/keystone/tests/unit/test_v3.py b/keystone-moon/keystone/tests/unit/test_v3.py new file mode 100644 index 00000000..f6d6ed93 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_v3.py @@ -0,0 +1,1283 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 datetime +import uuid + +from oslo_config import cfg +from oslo_serialization import jsonutils +from oslo_utils import timeutils +import six +from testtools import matchers + +from keystone import auth +from keystone.common import authorization +from keystone.common import cache +from keystone import exception +from keystone import middleware +from keystone.policy.backends import rules +from keystone.tests import unit as tests +from keystone.tests.unit import rest + + +CONF = cfg.CONF +DEFAULT_DOMAIN_ID = 'default' + +TIME_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ' + + +class AuthTestMixin(object): + """To hold auth building helper functions.""" + def build_auth_scope(self, project_id=None, project_name=None, + project_domain_id=None, project_domain_name=None, + domain_id=None, domain_name=None, trust_id=None, + unscoped=None): + scope_data = {} + if unscoped: + scope_data['unscoped'] = {} + if project_id or project_name: + scope_data['project'] = {} + if project_id: + scope_data['project']['id'] = project_id + else: + scope_data['project']['name'] = project_name + if project_domain_id or project_domain_name: + project_domain_json = {} + if project_domain_id: + project_domain_json['id'] = project_domain_id + else: + project_domain_json['name'] = project_domain_name + scope_data['project']['domain'] = project_domain_json + if domain_id or domain_name: + scope_data['domain'] = {} + if domain_id: + scope_data['domain']['id'] = domain_id + else: + scope_data['domain']['name'] = domain_name + if trust_id: + scope_data['OS-TRUST:trust'] = {} + scope_data['OS-TRUST:trust']['id'] = trust_id + return scope_data + + def build_password_auth(self, user_id=None, username=None, + user_domain_id=None, user_domain_name=None, + password=None): + password_data = {'user': {}} + if user_id: + password_data['user']['id'] = user_id + else: + password_data['user']['name'] = username + if user_domain_id or user_domain_name: + password_data['user']['domain'] = {} + if user_domain_id: + password_data['user']['domain']['id'] = user_domain_id + else: + password_data['user']['domain']['name'] = user_domain_name + password_data['user']['password'] = password + return password_data + + def build_token_auth(self, token): + return {'id': token} + + def build_authentication_request(self, token=None, user_id=None, + username=None, user_domain_id=None, + user_domain_name=None, password=None, + kerberos=False, **kwargs): + """Build auth dictionary. + + It will create an auth dictionary based on all the arguments + that it receives. + """ + auth_data = {} + auth_data['identity'] = {'methods': []} + if kerberos: + auth_data['identity']['methods'].append('kerberos') + auth_data['identity']['kerberos'] = {} + if token: + auth_data['identity']['methods'].append('token') + auth_data['identity']['token'] = self.build_token_auth(token) + if user_id or username: + auth_data['identity']['methods'].append('password') + auth_data['identity']['password'] = self.build_password_auth( + user_id, username, user_domain_id, user_domain_name, password) + if kwargs: + auth_data['scope'] = self.build_auth_scope(**kwargs) + return {'auth': auth_data} + + +class RestfulTestCase(tests.SQLDriverOverrides, rest.RestfulTestCase, + AuthTestMixin): + def config_files(self): + config_files = super(RestfulTestCase, self).config_files() + config_files.append(tests.dirs.tests_conf('backend_sql.conf')) + return config_files + + def get_extensions(self): + extensions = set(['revoke']) + if hasattr(self, 'EXTENSION_NAME'): + extensions.add(self.EXTENSION_NAME) + return extensions + + def generate_paste_config(self): + new_paste_file = None + try: + new_paste_file = tests.generate_paste_config(self.EXTENSION_TO_ADD) + except AttributeError: + # no need to report this error here, as most tests will not have + # EXTENSION_TO_ADD defined. + pass + finally: + return new_paste_file + + def remove_generated_paste_config(self): + try: + tests.remove_generated_paste_config(self.EXTENSION_TO_ADD) + except AttributeError: + pass + + def setUp(self, app_conf='keystone'): + """Setup for v3 Restful Test Cases. + + """ + new_paste_file = self.generate_paste_config() + self.addCleanup(self.remove_generated_paste_config) + if new_paste_file: + app_conf = 'config:%s' % (new_paste_file) + + super(RestfulTestCase, self).setUp(app_conf=app_conf) + + self.empty_context = {'environment': {}} + + # Initialize the policy engine and allow us to write to a temp + # file in each test to create the policies + rules.reset() + + # drop the policy rules + self.addCleanup(rules.reset) + + def load_backends(self): + # ensure the cache region instance is setup + cache.configure_cache_region(cache.REGION) + + super(RestfulTestCase, self).load_backends() + + def load_fixtures(self, fixtures): + self.load_sample_data() + + def _populate_default_domain(self): + if CONF.database.connection == tests.IN_MEM_DB_CONN_STRING: + # NOTE(morganfainberg): If an in-memory db is being used, be sure + # to populate the default domain, this is typically done by + # a migration, but the in-mem db uses model definitions to create + # the schema (no migrations are run). + try: + self.resource_api.get_domain(DEFAULT_DOMAIN_ID) + except exception.DomainNotFound: + domain = {'description': (u'Owns users and tenants (i.e. ' + u'projects) available on Identity ' + u'API v2.'), + 'enabled': True, + 'id': DEFAULT_DOMAIN_ID, + 'name': u'Default'} + self.resource_api.create_domain(DEFAULT_DOMAIN_ID, domain) + + def load_sample_data(self): + self._populate_default_domain() + self.domain_id = uuid.uuid4().hex + self.domain = self.new_domain_ref() + self.domain['id'] = self.domain_id + self.resource_api.create_domain(self.domain_id, self.domain) + + self.project_id = uuid.uuid4().hex + self.project = self.new_project_ref( + domain_id=self.domain_id) + self.project['id'] = self.project_id + self.resource_api.create_project(self.project_id, self.project) + + self.user = self.new_user_ref(domain_id=self.domain_id) + password = self.user['password'] + self.user = self.identity_api.create_user(self.user) + self.user['password'] = password + self.user_id = self.user['id'] + + self.default_domain_project_id = uuid.uuid4().hex + self.default_domain_project = self.new_project_ref( + domain_id=DEFAULT_DOMAIN_ID) + self.default_domain_project['id'] = self.default_domain_project_id + self.resource_api.create_project(self.default_domain_project_id, + self.default_domain_project) + + self.default_domain_user = self.new_user_ref( + domain_id=DEFAULT_DOMAIN_ID) + password = self.default_domain_user['password'] + self.default_domain_user = ( + self.identity_api.create_user(self.default_domain_user)) + self.default_domain_user['password'] = password + self.default_domain_user_id = self.default_domain_user['id'] + + # create & grant policy.json's default role for admin_required + self.role_id = uuid.uuid4().hex + self.role = self.new_role_ref() + self.role['id'] = self.role_id + self.role['name'] = 'admin' + self.role_api.create_role(self.role_id, self.role) + self.assignment_api.add_role_to_user_and_project( + self.user_id, self.project_id, self.role_id) + self.assignment_api.add_role_to_user_and_project( + self.default_domain_user_id, self.default_domain_project_id, + self.role_id) + self.assignment_api.add_role_to_user_and_project( + self.default_domain_user_id, self.project_id, + self.role_id) + + self.region_id = uuid.uuid4().hex + self.region = self.new_region_ref() + self.region['id'] = self.region_id + self.catalog_api.create_region( + self.region.copy()) + + self.service_id = uuid.uuid4().hex + self.service = self.new_service_ref() + self.service['id'] = self.service_id + self.catalog_api.create_service( + self.service_id, + self.service.copy()) + + self.endpoint_id = uuid.uuid4().hex + self.endpoint = self.new_endpoint_ref(service_id=self.service_id) + self.endpoint['id'] = self.endpoint_id + self.endpoint['region_id'] = self.region['id'] + self.catalog_api.create_endpoint( + self.endpoint_id, + self.endpoint.copy()) + # The server adds 'enabled' and defaults to True. + self.endpoint['enabled'] = True + + def new_ref(self): + """Populates a ref with attributes common to all API entities.""" + return { + 'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'description': uuid.uuid4().hex, + 'enabled': True} + + def new_region_ref(self): + ref = self.new_ref() + # Region doesn't have name or enabled. + del ref['name'] + del ref['enabled'] + ref['parent_region_id'] = None + return ref + + def new_service_ref(self): + ref = self.new_ref() + ref['type'] = uuid.uuid4().hex + return ref + + def new_endpoint_ref(self, service_id, interface='public', **kwargs): + ref = self.new_ref() + del ref['enabled'] # enabled is optional + ref['interface'] = interface + ref['service_id'] = service_id + ref['url'] = 'https://' + uuid.uuid4().hex + '.com' + ref['region_id'] = self.region_id + ref.update(kwargs) + return ref + + def new_domain_ref(self): + ref = self.new_ref() + return ref + + def new_project_ref(self, domain_id, parent_id=None): + ref = self.new_ref() + ref['domain_id'] = domain_id + ref['parent_id'] = parent_id + return ref + + def new_user_ref(self, domain_id, project_id=None): + ref = self.new_ref() + ref['domain_id'] = domain_id + ref['email'] = uuid.uuid4().hex + ref['password'] = uuid.uuid4().hex + if project_id: + ref['default_project_id'] = project_id + return ref + + def new_group_ref(self, domain_id): + ref = self.new_ref() + ref['domain_id'] = domain_id + return ref + + def new_credential_ref(self, user_id, project_id=None, cred_type=None): + ref = dict() + ref['id'] = uuid.uuid4().hex + ref['user_id'] = user_id + if cred_type == 'ec2': + ref['type'] = 'ec2' + ref['blob'] = {'blah': 'test'} + else: + ref['type'] = 'cert' + ref['blob'] = uuid.uuid4().hex + if project_id: + ref['project_id'] = project_id + return ref + + def new_role_ref(self): + ref = self.new_ref() + # Roles don't have a description or the enabled flag + del ref['description'] + del ref['enabled'] + return ref + + def new_policy_ref(self): + ref = self.new_ref() + ref['blob'] = uuid.uuid4().hex + ref['type'] = uuid.uuid4().hex + return ref + + def new_trust_ref(self, trustor_user_id, trustee_user_id, project_id=None, + impersonation=None, expires=None, role_ids=None, + role_names=None, remaining_uses=None, + allow_redelegation=False): + ref = dict() + ref['id'] = uuid.uuid4().hex + ref['trustor_user_id'] = trustor_user_id + ref['trustee_user_id'] = trustee_user_id + ref['impersonation'] = impersonation or False + ref['project_id'] = project_id + ref['remaining_uses'] = remaining_uses + ref['allow_redelegation'] = allow_redelegation + + if isinstance(expires, six.string_types): + ref['expires_at'] = expires + elif isinstance(expires, dict): + ref['expires_at'] = timeutils.strtime( + timeutils.utcnow() + datetime.timedelta(**expires), + fmt=TIME_FORMAT) + elif expires is None: + pass + else: + raise NotImplementedError('Unexpected value for "expires"') + + role_ids = role_ids or [] + role_names = role_names or [] + if role_ids or role_names: + ref['roles'] = [] + for role_id in role_ids: + ref['roles'].append({'id': role_id}) + for role_name in role_names: + ref['roles'].append({'name': role_name}) + + return ref + + def create_new_default_project_for_user(self, user_id, domain_id, + enable_project=True): + ref = self.new_project_ref(domain_id=domain_id) + ref['enabled'] = enable_project + r = self.post('/projects', body={'project': ref}) + project = self.assertValidProjectResponse(r, ref) + # set the user's preferred project + body = {'user': {'default_project_id': project['id']}} + r = self.patch('/users/%(user_id)s' % { + 'user_id': user_id}, + body=body) + self.assertValidUserResponse(r) + + return project + + def get_scoped_token(self): + """Convenience method so that we can test authenticated requests.""" + r = self.admin_request( + method='POST', + path='/v3/auth/tokens', + body={ + 'auth': { + 'identity': { + 'methods': ['password'], + 'password': { + 'user': { + 'name': self.user['name'], + 'password': self.user['password'], + 'domain': { + 'id': self.user['domain_id'] + } + } + } + }, + 'scope': { + 'project': { + 'id': self.project['id'], + } + } + } + }) + return r.headers.get('X-Subject-Token') + + def get_requested_token(self, auth): + """Request the specific token we want.""" + + r = self.v3_authenticate_token(auth) + return r.headers.get('X-Subject-Token') + + def v3_authenticate_token(self, auth, expected_status=201): + return self.admin_request(method='POST', + path='/v3/auth/tokens', + body=auth, + expected_status=expected_status) + + def v3_noauth_request(self, path, **kwargs): + # request does not require auth token header + path = '/v3' + path + return self.admin_request(path=path, **kwargs) + + def v3_request(self, path, **kwargs): + # check to see if caller requires token for the API call. + if kwargs.pop('noauth', None): + return self.v3_noauth_request(path, **kwargs) + + # Check if the caller has passed in auth details for + # use in requesting the token + auth_arg = kwargs.pop('auth', None) + if auth_arg: + token = self.get_requested_token(auth_arg) + else: + token = kwargs.pop('token', None) + if not token: + token = self.get_scoped_token() + path = '/v3' + path + + return self.admin_request(path=path, token=token, **kwargs) + + def get(self, path, **kwargs): + r = self.v3_request(method='GET', path=path, **kwargs) + if 'expected_status' not in kwargs: + self.assertResponseStatus(r, 200) + return r + + def head(self, path, **kwargs): + r = self.v3_request(method='HEAD', path=path, **kwargs) + if 'expected_status' not in kwargs: + self.assertResponseStatus(r, 204) + self.assertEqual('', r.body) + return r + + def post(self, path, **kwargs): + r = self.v3_request(method='POST', path=path, **kwargs) + if 'expected_status' not in kwargs: + self.assertResponseStatus(r, 201) + return r + + def put(self, path, **kwargs): + r = self.v3_request(method='PUT', path=path, **kwargs) + if 'expected_status' not in kwargs: + self.assertResponseStatus(r, 204) + return r + + def patch(self, path, **kwargs): + r = self.v3_request(method='PATCH', path=path, **kwargs) + if 'expected_status' not in kwargs: + self.assertResponseStatus(r, 200) + return r + + def delete(self, path, **kwargs): + r = self.v3_request(method='DELETE', path=path, **kwargs) + if 'expected_status' not in kwargs: + self.assertResponseStatus(r, 204) + return r + + def assertValidErrorResponse(self, r): + resp = r.result + self.assertIsNotNone(resp.get('error')) + self.assertIsNotNone(resp['error'].get('code')) + self.assertIsNotNone(resp['error'].get('title')) + self.assertIsNotNone(resp['error'].get('message')) + self.assertEqual(int(resp['error']['code']), r.status_code) + + def assertValidListLinks(self, links, resource_url=None): + self.assertIsNotNone(links) + self.assertIsNotNone(links.get('self')) + self.assertThat(links['self'], matchers.StartsWith('http://localhost')) + + if resource_url: + self.assertThat(links['self'], matchers.EndsWith(resource_url)) + + self.assertIn('next', links) + if links['next'] is not None: + self.assertThat(links['next'], + matchers.StartsWith('http://localhost')) + + self.assertIn('previous', links) + if links['previous'] is not None: + self.assertThat(links['previous'], + matchers.StartsWith('http://localhost')) + + def assertValidListResponse(self, resp, key, entity_validator, ref=None, + expected_length=None, keys_to_check=None, + resource_url=None): + """Make assertions common to all API list responses. + + If a reference is provided, it's ID will be searched for in the + response, and asserted to be equal. + + """ + entities = resp.result.get(key) + self.assertIsNotNone(entities) + + if expected_length is not None: + self.assertEqual(expected_length, len(entities)) + elif ref is not None: + # we're at least expecting the ref + self.assertNotEmpty(entities) + + # collections should have relational links + self.assertValidListLinks(resp.result.get('links'), + resource_url=resource_url) + + for entity in entities: + self.assertIsNotNone(entity) + self.assertValidEntity(entity, keys_to_check=keys_to_check) + entity_validator(entity) + if ref: + entity = [x for x in entities if x['id'] == ref['id']][0] + self.assertValidEntity(entity, ref=ref, + keys_to_check=keys_to_check) + entity_validator(entity, ref) + return entities + + def assertValidResponse(self, resp, key, entity_validator, *args, + **kwargs): + """Make assertions common to all API responses.""" + entity = resp.result.get(key) + self.assertIsNotNone(entity) + keys = kwargs.pop('keys_to_check', None) + self.assertValidEntity(entity, keys_to_check=keys, *args, **kwargs) + entity_validator(entity, *args, **kwargs) + return entity + + def assertValidEntity(self, entity, ref=None, keys_to_check=None): + """Make assertions common to all API entities. + + If a reference is provided, the entity will also be compared against + the reference. + """ + if keys_to_check is not None: + keys = keys_to_check + else: + keys = ['name', 'description', 'enabled'] + + for k in ['id'] + keys: + msg = '%s unexpectedly None in %s' % (k, entity) + self.assertIsNotNone(entity.get(k), msg) + + self.assertIsNotNone(entity.get('links')) + self.assertIsNotNone(entity['links'].get('self')) + self.assertThat(entity['links']['self'], + matchers.StartsWith('http://localhost')) + self.assertIn(entity['id'], entity['links']['self']) + + if ref: + for k in keys: + msg = '%s not equal: %s != %s' % (k, ref[k], entity[k]) + self.assertEqual(ref[k], entity[k]) + + return entity + + def assertDictContainsSubset(self, expected, actual): + """"Asserts if dictionary actual is a superset of expected. + + Tests whether the key/value pairs in dictionary actual are a superset + of those in expected. + + """ + for k, v in expected.iteritems(): + self.assertIn(k, actual) + if isinstance(v, dict): + self.assertDictContainsSubset(v, actual[k]) + else: + self.assertEqual(v, actual[k]) + + # auth validation + + def assertValidISO8601ExtendedFormatDatetime(self, dt): + try: + return timeutils.parse_strtime(dt, fmt=TIME_FORMAT) + except Exception: + msg = '%s is not a valid ISO 8601 extended format date time.' % dt + raise AssertionError(msg) + self.assertIsInstance(dt, datetime.datetime) + + def assertValidTokenResponse(self, r, user=None): + self.assertTrue(r.headers.get('X-Subject-Token')) + token = r.result['token'] + + self.assertIsNotNone(token.get('expires_at')) + expires_at = self.assertValidISO8601ExtendedFormatDatetime( + token['expires_at']) + self.assertIsNotNone(token.get('issued_at')) + issued_at = self.assertValidISO8601ExtendedFormatDatetime( + token['issued_at']) + self.assertTrue(issued_at < expires_at) + + self.assertIn('user', token) + self.assertIn('id', token['user']) + self.assertIn('name', token['user']) + self.assertIn('domain', token['user']) + self.assertIn('id', token['user']['domain']) + + if user is not None: + self.assertEqual(user['id'], token['user']['id']) + self.assertEqual(user['name'], token['user']['name']) + self.assertEqual(user['domain_id'], token['user']['domain']['id']) + + return token + + def assertValidUnscopedTokenResponse(self, r, *args, **kwargs): + token = self.assertValidTokenResponse(r, *args, **kwargs) + + self.assertNotIn('roles', token) + self.assertNotIn('catalog', token) + self.assertNotIn('project', token) + self.assertNotIn('domain', token) + + return token + + def assertValidScopedTokenResponse(self, r, *args, **kwargs): + require_catalog = kwargs.pop('require_catalog', True) + endpoint_filter = kwargs.pop('endpoint_filter', False) + ep_filter_assoc = kwargs.pop('ep_filter_assoc', 0) + token = self.assertValidTokenResponse(r, *args, **kwargs) + + if require_catalog: + endpoint_num = 0 + self.assertIn('catalog', token) + + if isinstance(token['catalog'], list): + # only test JSON + for service in token['catalog']: + for endpoint in service['endpoints']: + self.assertNotIn('enabled', endpoint) + self.assertNotIn('legacy_endpoint_id', endpoint) + self.assertNotIn('service_id', endpoint) + endpoint_num += 1 + + # sub test for the OS-EP-FILTER extension enabled + if endpoint_filter: + self.assertEqual(ep_filter_assoc, endpoint_num) + else: + self.assertNotIn('catalog', token) + + self.assertIn('roles', token) + self.assertTrue(token['roles']) + for role in token['roles']: + self.assertIn('id', role) + self.assertIn('name', role) + + return token + + def assertValidProjectScopedTokenResponse(self, r, *args, **kwargs): + token = self.assertValidScopedTokenResponse(r, *args, **kwargs) + + self.assertIn('project', token) + self.assertIn('id', token['project']) + self.assertIn('name', token['project']) + self.assertIn('domain', token['project']) + self.assertIn('id', token['project']['domain']) + self.assertIn('name', token['project']['domain']) + + self.assertEqual(self.role_id, token['roles'][0]['id']) + + return token + + def assertValidProjectTrustScopedTokenResponse(self, r, *args, **kwargs): + token = self.assertValidProjectScopedTokenResponse(r, *args, **kwargs) + + trust = token.get('OS-TRUST:trust') + self.assertIsNotNone(trust) + self.assertIsNotNone(trust.get('id')) + self.assertIsInstance(trust.get('impersonation'), bool) + self.assertIsNotNone(trust.get('trustor_user')) + self.assertIsNotNone(trust.get('trustee_user')) + self.assertIsNotNone(trust['trustor_user'].get('id')) + self.assertIsNotNone(trust['trustee_user'].get('id')) + + def assertValidDomainScopedTokenResponse(self, r, *args, **kwargs): + token = self.assertValidScopedTokenResponse(r, *args, **kwargs) + + self.assertIn('domain', token) + self.assertIn('id', token['domain']) + self.assertIn('name', token['domain']) + + return token + + def assertEqualTokens(self, a, b): + """Assert that two tokens are equal. + + Compare two tokens except for their ids. This also truncates + the time in the comparison. + """ + def normalize(token): + del token['token']['expires_at'] + del token['token']['issued_at'] + return token + + a_expires_at = self.assertValidISO8601ExtendedFormatDatetime( + a['token']['expires_at']) + b_expires_at = self.assertValidISO8601ExtendedFormatDatetime( + b['token']['expires_at']) + self.assertCloseEnoughForGovernmentWork(a_expires_at, b_expires_at) + + a_issued_at = self.assertValidISO8601ExtendedFormatDatetime( + a['token']['issued_at']) + b_issued_at = self.assertValidISO8601ExtendedFormatDatetime( + b['token']['issued_at']) + self.assertCloseEnoughForGovernmentWork(a_issued_at, b_issued_at) + + return self.assertDictEqual(normalize(a), normalize(b)) + + # catalog validation + + def assertValidCatalogResponse(self, resp, *args, **kwargs): + self.assertEqual(set(['catalog', 'links']), set(resp.json.keys())) + self.assertValidCatalog(resp.json['catalog']) + self.assertIn('links', resp.json) + self.assertIsInstance(resp.json['links'], dict) + self.assertEqual(['self'], resp.json['links'].keys()) + self.assertEqual( + 'http://localhost/v3/auth/catalog', + resp.json['links']['self']) + + def assertValidCatalog(self, entity): + self.assertIsInstance(entity, list) + self.assertTrue(len(entity) > 0) + for service in entity: + self.assertIsNotNone(service.get('id')) + self.assertIsNotNone(service.get('name')) + self.assertIsNotNone(service.get('type')) + self.assertNotIn('enabled', service) + self.assertTrue(len(service['endpoints']) > 0) + for endpoint in service['endpoints']: + self.assertIsNotNone(endpoint.get('id')) + self.assertIsNotNone(endpoint.get('interface')) + self.assertIsNotNone(endpoint.get('url')) + self.assertNotIn('enabled', endpoint) + self.assertNotIn('legacy_endpoint_id', endpoint) + self.assertNotIn('service_id', endpoint) + + # region validation + + def assertValidRegionListResponse(self, resp, *args, **kwargs): + # NOTE(jaypipes): I have to pass in a blank keys_to_check parameter + # below otherwise the base assertValidEntity method + # tries to find a "name" and an "enabled" key in the + # returned ref dicts. The issue is, I don't understand + # how the service and endpoint entity assertions below + # actually work (they don't raise assertions), since + # AFAICT, the service and endpoint tables don't have + # a "name" column either... :( + return self.assertValidListResponse( + resp, + 'regions', + self.assertValidRegion, + keys_to_check=[], + *args, + **kwargs) + + def assertValidRegionResponse(self, resp, *args, **kwargs): + return self.assertValidResponse( + resp, + 'region', + self.assertValidRegion, + keys_to_check=[], + *args, + **kwargs) + + def assertValidRegion(self, entity, ref=None): + self.assertIsNotNone(entity.get('description')) + if ref: + self.assertEqual(ref['description'], entity['description']) + return entity + + # service validation + + def assertValidServiceListResponse(self, resp, *args, **kwargs): + return self.assertValidListResponse( + resp, + 'services', + self.assertValidService, + *args, + **kwargs) + + def assertValidServiceResponse(self, resp, *args, **kwargs): + return self.assertValidResponse( + resp, + 'service', + self.assertValidService, + *args, + **kwargs) + + def assertValidService(self, entity, ref=None): + self.assertIsNotNone(entity.get('type')) + self.assertIsInstance(entity.get('enabled'), bool) + if ref: + self.assertEqual(ref['type'], entity['type']) + return entity + + # endpoint validation + + def assertValidEndpointListResponse(self, resp, *args, **kwargs): + return self.assertValidListResponse( + resp, + 'endpoints', + self.assertValidEndpoint, + *args, + **kwargs) + + def assertValidEndpointResponse(self, resp, *args, **kwargs): + return self.assertValidResponse( + resp, + 'endpoint', + self.assertValidEndpoint, + *args, + **kwargs) + + def assertValidEndpoint(self, entity, ref=None): + self.assertIsNotNone(entity.get('interface')) + self.assertIsNotNone(entity.get('service_id')) + self.assertIsInstance(entity['enabled'], bool) + + # this is intended to be an unexposed implementation detail + self.assertNotIn('legacy_endpoint_id', entity) + + if ref: + self.assertEqual(ref['interface'], entity['interface']) + self.assertEqual(ref['service_id'], entity['service_id']) + if ref.get('region') is not None: + self.assertEqual(ref['region_id'], entity.get('region_id')) + + return entity + + # domain validation + + def assertValidDomainListResponse(self, resp, *args, **kwargs): + return self.assertValidListResponse( + resp, + 'domains', + self.assertValidDomain, + *args, + **kwargs) + + def assertValidDomainResponse(self, resp, *args, **kwargs): + return self.assertValidResponse( + resp, + 'domain', + self.assertValidDomain, + *args, + **kwargs) + + def assertValidDomain(self, entity, ref=None): + if ref: + pass + return entity + + # project validation + + def assertValidProjectListResponse(self, resp, *args, **kwargs): + return self.assertValidListResponse( + resp, + 'projects', + self.assertValidProject, + *args, + **kwargs) + + def assertValidProjectResponse(self, resp, *args, **kwargs): + return self.assertValidResponse( + resp, + 'project', + self.assertValidProject, + *args, + **kwargs) + + def assertValidProject(self, entity, ref=None): + self.assertIsNotNone(entity.get('domain_id')) + if ref: + self.assertEqual(ref['domain_id'], entity['domain_id']) + return entity + + # user validation + + def assertValidUserListResponse(self, resp, *args, **kwargs): + return self.assertValidListResponse( + resp, + 'users', + self.assertValidUser, + *args, + **kwargs) + + def assertValidUserResponse(self, resp, *args, **kwargs): + return self.assertValidResponse( + resp, + 'user', + self.assertValidUser, + *args, + **kwargs) + + def assertValidUser(self, entity, ref=None): + self.assertIsNotNone(entity.get('domain_id')) + self.assertIsNotNone(entity.get('email')) + self.assertIsNone(entity.get('password')) + self.assertNotIn('tenantId', entity) + if ref: + self.assertEqual(ref['domain_id'], entity['domain_id']) + self.assertEqual(ref['email'], entity['email']) + if 'default_project_id' in ref: + self.assertIsNotNone(ref['default_project_id']) + self.assertEqual(ref['default_project_id'], + entity['default_project_id']) + return entity + + # group validation + + def assertValidGroupListResponse(self, resp, *args, **kwargs): + return self.assertValidListResponse( + resp, + 'groups', + self.assertValidGroup, + *args, + **kwargs) + + def assertValidGroupResponse(self, resp, *args, **kwargs): + return self.assertValidResponse( + resp, + 'group', + self.assertValidGroup, + *args, + **kwargs) + + def assertValidGroup(self, entity, ref=None): + self.assertIsNotNone(entity.get('name')) + if ref: + self.assertEqual(ref['name'], entity['name']) + return entity + + # credential validation + + def assertValidCredentialListResponse(self, resp, *args, **kwargs): + return self.assertValidListResponse( + resp, + 'credentials', + self.assertValidCredential, + keys_to_check=['blob', 'user_id', 'type'], + *args, + **kwargs) + + def assertValidCredentialResponse(self, resp, *args, **kwargs): + return self.assertValidResponse( + resp, + 'credential', + self.assertValidCredential, + keys_to_check=['blob', 'user_id', 'type'], + *args, + **kwargs) + + def assertValidCredential(self, entity, ref=None): + self.assertIsNotNone(entity.get('user_id')) + self.assertIsNotNone(entity.get('blob')) + self.assertIsNotNone(entity.get('type')) + if ref: + self.assertEqual(ref['user_id'], entity['user_id']) + self.assertEqual(ref['blob'], entity['blob']) + self.assertEqual(ref['type'], entity['type']) + self.assertEqual(ref.get('project_id'), entity.get('project_id')) + return entity + + # role validation + + def assertValidRoleListResponse(self, resp, *args, **kwargs): + return self.assertValidListResponse( + resp, + 'roles', + self.assertValidRole, + keys_to_check=['name'], + *args, + **kwargs) + + def assertValidRoleResponse(self, resp, *args, **kwargs): + return self.assertValidResponse( + resp, + 'role', + self.assertValidRole, + keys_to_check=['name'], + *args, + **kwargs) + + def assertValidRole(self, entity, ref=None): + self.assertIsNotNone(entity.get('name')) + if ref: + self.assertEqual(ref['name'], entity['name']) + return entity + + # role assignment validation + + def assertValidRoleAssignmentListResponse(self, resp, expected_length=None, + resource_url=None): + entities = resp.result.get('role_assignments') + + if expected_length: + self.assertEqual(expected_length, len(entities)) + + # Collections should have relational links + self.assertValidListLinks(resp.result.get('links'), + resource_url=resource_url) + + for entity in entities: + self.assertIsNotNone(entity) + self.assertValidRoleAssignment(entity) + return entities + + def assertValidRoleAssignment(self, entity, ref=None): + # A role should be present + self.assertIsNotNone(entity.get('role')) + self.assertIsNotNone(entity['role'].get('id')) + + # Only one of user or group should be present + if entity.get('user'): + self.assertIsNone(entity.get('group')) + self.assertIsNotNone(entity['user'].get('id')) + else: + self.assertIsNotNone(entity.get('group')) + self.assertIsNotNone(entity['group'].get('id')) + + # A scope should be present and have only one of domain or project + self.assertIsNotNone(entity.get('scope')) + + if entity['scope'].get('project'): + self.assertIsNone(entity['scope'].get('domain')) + self.assertIsNotNone(entity['scope']['project'].get('id')) + else: + self.assertIsNotNone(entity['scope'].get('domain')) + self.assertIsNotNone(entity['scope']['domain'].get('id')) + + # An assignment link should be present + self.assertIsNotNone(entity.get('links')) + self.assertIsNotNone(entity['links'].get('assignment')) + + if ref: + links = ref.pop('links') + try: + self.assertDictContainsSubset(ref, entity) + self.assertIn(links['assignment'], + entity['links']['assignment']) + finally: + if links: + ref['links'] = links + + def assertRoleAssignmentInListResponse(self, resp, ref, expected=1): + + found_count = 0 + for entity in resp.result.get('role_assignments'): + try: + self.assertValidRoleAssignment(entity, ref=ref) + except Exception: + # It doesn't match, so let's go onto the next one + pass + else: + found_count += 1 + self.assertEqual(expected, found_count) + + def assertRoleAssignmentNotInListResponse(self, resp, ref): + self.assertRoleAssignmentInListResponse(resp, ref=ref, expected=0) + + # policy validation + + def assertValidPolicyListResponse(self, resp, *args, **kwargs): + return self.assertValidListResponse( + resp, + 'policies', + self.assertValidPolicy, + *args, + **kwargs) + + def assertValidPolicyResponse(self, resp, *args, **kwargs): + return self.assertValidResponse( + resp, + 'policy', + self.assertValidPolicy, + *args, + **kwargs) + + def assertValidPolicy(self, entity, ref=None): + self.assertIsNotNone(entity.get('blob')) + self.assertIsNotNone(entity.get('type')) + if ref: + self.assertEqual(ref['blob'], entity['blob']) + self.assertEqual(ref['type'], entity['type']) + return entity + + # trust validation + + def assertValidTrustListResponse(self, resp, *args, **kwargs): + return self.assertValidListResponse( + resp, + 'trusts', + self.assertValidTrustSummary, + keys_to_check=['trustor_user_id', + 'trustee_user_id', + 'impersonation'], + *args, + **kwargs) + + def assertValidTrustResponse(self, resp, *args, **kwargs): + return self.assertValidResponse( + resp, + 'trust', + self.assertValidTrust, + keys_to_check=['trustor_user_id', + 'trustee_user_id', + 'impersonation'], + *args, + **kwargs) + + def assertValidTrustSummary(self, entity, ref=None): + return self.assertValidTrust(entity, ref, summary=True) + + def assertValidTrust(self, entity, ref=None, summary=False): + self.assertIsNotNone(entity.get('trustor_user_id')) + self.assertIsNotNone(entity.get('trustee_user_id')) + self.assertIsNotNone(entity.get('impersonation')) + + self.assertIn('expires_at', entity) + if entity['expires_at'] is not None: + self.assertValidISO8601ExtendedFormatDatetime(entity['expires_at']) + + if summary: + # Trust list contains no roles, but getting a specific + # trust by ID provides the detailed response containing roles + self.assertNotIn('roles', entity) + self.assertIn('project_id', entity) + else: + for role in entity['roles']: + self.assertIsNotNone(role) + self.assertValidEntity(role, keys_to_check=['name']) + self.assertValidRole(role) + + self.assertValidListLinks(entity.get('roles_links')) + + # always disallow role xor project_id (neither or both is allowed) + has_roles = bool(entity.get('roles')) + has_project = bool(entity.get('project_id')) + self.assertFalse(has_roles ^ has_project) + + if ref: + self.assertEqual(ref['trustor_user_id'], entity['trustor_user_id']) + self.assertEqual(ref['trustee_user_id'], entity['trustee_user_id']) + self.assertEqual(ref['project_id'], entity['project_id']) + if entity.get('expires_at') or ref.get('expires_at'): + entity_exp = self.assertValidISO8601ExtendedFormatDatetime( + entity['expires_at']) + ref_exp = self.assertValidISO8601ExtendedFormatDatetime( + ref['expires_at']) + self.assertCloseEnoughForGovernmentWork(entity_exp, ref_exp) + else: + self.assertEqual(ref.get('expires_at'), + entity.get('expires_at')) + + return entity + + def build_external_auth_request(self, remote_user, + remote_domain=None, auth_data=None, + kerberos=False): + context = {'environment': {'REMOTE_USER': remote_user, + 'AUTH_TYPE': 'Negotiate'}} + if remote_domain: + context['environment']['REMOTE_DOMAIN'] = remote_domain + if not auth_data: + auth_data = self.build_authentication_request( + kerberos=kerberos)['auth'] + no_context = None + auth_info = auth.controllers.AuthInfo.create(no_context, auth_data) + auth_context = {'extras': {}, 'method_names': []} + return context, auth_info, auth_context + + +class VersionTestCase(RestfulTestCase): + def test_get_version(self): + pass + + +# NOTE(gyee): test AuthContextMiddleware here instead of test_middleware.py +# because we need the token +class AuthContextMiddlewareTestCase(RestfulTestCase): + def _mock_request_object(self, token_id): + + class fake_req(object): + headers = {middleware.AUTH_TOKEN_HEADER: token_id} + environ = {} + + return fake_req() + + def test_auth_context_build_by_middleware(self): + # test to make sure AuthContextMiddleware successful build the auth + # context from the incoming auth token + admin_token = self.get_scoped_token() + req = self._mock_request_object(admin_token) + application = None + middleware.AuthContextMiddleware(application).process_request(req) + self.assertEqual( + self.user['id'], + req.environ.get(authorization.AUTH_CONTEXT_ENV)['user_id']) + + def test_auth_context_override(self): + overridden_context = 'OVERRIDDEN_CONTEXT' + # this token should not be used + token = uuid.uuid4().hex + req = self._mock_request_object(token) + req.environ[authorization.AUTH_CONTEXT_ENV] = overridden_context + application = None + middleware.AuthContextMiddleware(application).process_request(req) + # make sure overridden context take precedence + self.assertEqual(overridden_context, + req.environ.get(authorization.AUTH_CONTEXT_ENV)) + + def test_admin_token_auth_context(self): + # test to make sure AuthContextMiddleware does not attempt to build + # auth context if the incoming auth token is the special admin token + req = self._mock_request_object(CONF.admin_token) + application = None + middleware.AuthContextMiddleware(application).process_request(req) + self.assertDictEqual(req.environ.get(authorization.AUTH_CONTEXT_ENV), + {}) + + +class JsonHomeTestMixin(object): + """JSON Home test + + Mixin this class to provide a test for the JSON-Home response for an + extension. + + The base class must set JSON_HOME_DATA to a dict of relationship URLs + (rels) to the JSON-Home data for the relationship. The rels and associated + data must be in the response. + + """ + def test_get_json_home(self): + resp = self.get('/', convert=False, + headers={'Accept': 'application/json-home'}) + self.assertThat(resp.headers['Content-Type'], + matchers.Equals('application/json-home')) + resp_data = jsonutils.loads(resp.body) + + # Check that the example relationships are present. + for rel in self.JSON_HOME_DATA: + self.assertThat(resp_data['resources'][rel], + matchers.Equals(self.JSON_HOME_DATA[rel])) diff --git a/keystone-moon/keystone/tests/unit/test_v3_assignment.py b/keystone-moon/keystone/tests/unit/test_v3_assignment.py new file mode 100644 index 00000000..add14bfb --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_v3_assignment.py @@ -0,0 +1,2943 @@ +# 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 random +import six +import uuid + +from oslo_config import cfg + +from keystone.common import controller +from keystone import exception +from keystone.tests import unit as tests +from keystone.tests.unit import test_v3 + + +CONF = cfg.CONF + + +def _build_role_assignment_query_url(effective=False, **filters): + '''Build and return a role assignment query url with provided params. + + Available filters are: domain_id, project_id, user_id, group_id, role_id + and inherited_to_projects. + + ''' + + query_params = '?effective' if effective else '' + + for k, v in six.iteritems(filters): + query_params += '?' if not query_params else '&' + + if k == 'inherited_to_projects': + query_params += 'scope.OS-INHERIT:inherited_to=projects' + else: + if k in ['domain_id', 'project_id']: + query_params += 'scope.' + elif k not in ['user_id', 'group_id', 'role_id']: + raise ValueError('Invalid key \'%s\' in provided filters.' % k) + + query_params += '%s=%s' % (k.replace('_', '.'), v) + + return '/role_assignments%s' % query_params + + +def _build_role_assignment_link(**attribs): + """Build and return a role assignment link with provided attributes. + + Provided attributes are expected to contain: domain_id or project_id, + user_id or group_id, role_id and, optionally, inherited_to_projects. + + """ + + if attribs.get('domain_id'): + link = '/domains/' + attribs['domain_id'] + else: + link = '/projects/' + attribs['project_id'] + + if attribs.get('user_id'): + link += '/users/' + attribs['user_id'] + else: + link += '/groups/' + attribs['group_id'] + + link += '/roles/' + attribs['role_id'] + + if attribs.get('inherited_to_projects'): + return '/OS-INHERIT%s/inherited_to_projects' % link + + return link + + +def _build_role_assignment_entity(link=None, **attribs): + """Build and return a role assignment entity with provided attributes. + + Provided attributes are expected to contain: domain_id or project_id, + user_id or group_id, role_id and, optionally, inherited_to_projects. + + """ + + entity = {'links': {'assignment': ( + link or _build_role_assignment_link(**attribs))}} + + if attribs.get('domain_id'): + entity['scope'] = {'domain': {'id': attribs['domain_id']}} + else: + entity['scope'] = {'project': {'id': attribs['project_id']}} + + if attribs.get('user_id'): + entity['user'] = {'id': attribs['user_id']} + + if attribs.get('group_id'): + entity['links']['membership'] = ('/groups/%s/users/%s' % + (attribs['group_id'], + attribs['user_id'])) + else: + entity['group'] = {'id': attribs['group_id']} + + entity['role'] = {'id': attribs['role_id']} + + if attribs.get('inherited_to_projects'): + entity['scope']['OS-INHERIT:inherited_to'] = 'projects' + + return entity + + +class AssignmentTestCase(test_v3.RestfulTestCase): + """Test domains, projects, roles and role assignments.""" + + def setUp(self): + super(AssignmentTestCase, self).setUp() + + self.group = self.new_group_ref( + domain_id=self.domain_id) + self.group = self.identity_api.create_group(self.group) + self.group_id = self.group['id'] + + self.credential_id = uuid.uuid4().hex + self.credential = self.new_credential_ref( + user_id=self.user['id'], + project_id=self.project_id) + self.credential['id'] = self.credential_id + self.credential_api.create_credential( + self.credential_id, + self.credential) + + # Domain CRUD tests + + def test_create_domain(self): + """Call ``POST /domains``.""" + ref = self.new_domain_ref() + r = self.post( + '/domains', + body={'domain': ref}) + return self.assertValidDomainResponse(r, ref) + + def test_create_domain_case_sensitivity(self): + """Call `POST /domains`` twice with upper() and lower() cased name.""" + ref = self.new_domain_ref() + + # ensure the name is lowercase + ref['name'] = ref['name'].lower() + r = self.post( + '/domains', + body={'domain': ref}) + self.assertValidDomainResponse(r, ref) + + # ensure the name is uppercase + ref['name'] = ref['name'].upper() + r = self.post( + '/domains', + body={'domain': ref}) + self.assertValidDomainResponse(r, ref) + + def test_create_domain_400(self): + """Call ``POST /domains``.""" + self.post('/domains', body={'domain': {}}, expected_status=400) + + def test_list_domains(self): + """Call ``GET /domains``.""" + resource_url = '/domains' + r = self.get(resource_url) + self.assertValidDomainListResponse(r, ref=self.domain, + resource_url=resource_url) + + def test_get_domain(self): + """Call ``GET /domains/{domain_id}``.""" + r = self.get('/domains/%(domain_id)s' % { + 'domain_id': self.domain_id}) + self.assertValidDomainResponse(r, self.domain) + + def test_update_domain(self): + """Call ``PATCH /domains/{domain_id}``.""" + ref = self.new_domain_ref() + del ref['id'] + r = self.patch('/domains/%(domain_id)s' % { + 'domain_id': self.domain_id}, + body={'domain': ref}) + self.assertValidDomainResponse(r, ref) + + def test_disable_domain(self): + """Call ``PATCH /domains/{domain_id}`` (set enabled=False).""" + # Create a 2nd set of entities in a 2nd domain + self.domain2 = self.new_domain_ref() + self.resource_api.create_domain(self.domain2['id'], self.domain2) + + self.project2 = self.new_project_ref( + domain_id=self.domain2['id']) + self.resource_api.create_project(self.project2['id'], self.project2) + + self.user2 = self.new_user_ref( + domain_id=self.domain2['id'], + project_id=self.project2['id']) + password = self.user2['password'] + self.user2 = self.identity_api.create_user(self.user2) + self.user2['password'] = password + + self.assignment_api.add_user_to_project(self.project2['id'], + self.user2['id']) + + # First check a user in that domain can authenticate, via + # Both v2 and v3 + body = { + 'auth': { + 'passwordCredentials': { + 'userId': self.user2['id'], + 'password': self.user2['password'] + }, + 'tenantId': self.project2['id'] + } + } + self.admin_request(path='/v2.0/tokens', method='POST', body=body) + + auth_data = self.build_authentication_request( + user_id=self.user2['id'], + password=self.user2['password'], + project_id=self.project2['id']) + self.v3_authenticate_token(auth_data) + + # Now disable the domain + self.domain2['enabled'] = False + r = self.patch('/domains/%(domain_id)s' % { + 'domain_id': self.domain2['id']}, + body={'domain': {'enabled': False}}) + self.assertValidDomainResponse(r, self.domain2) + + # Make sure the user can no longer authenticate, via + # either API + body = { + 'auth': { + 'passwordCredentials': { + 'userId': self.user2['id'], + 'password': self.user2['password'] + }, + 'tenantId': self.project2['id'] + } + } + self.admin_request( + path='/v2.0/tokens', method='POST', body=body, expected_status=401) + + # Try looking up in v3 by name and id + auth_data = self.build_authentication_request( + user_id=self.user2['id'], + password=self.user2['password'], + project_id=self.project2['id']) + self.v3_authenticate_token(auth_data, expected_status=401) + + auth_data = self.build_authentication_request( + username=self.user2['name'], + user_domain_id=self.domain2['id'], + password=self.user2['password'], + project_id=self.project2['id']) + self.v3_authenticate_token(auth_data, expected_status=401) + + def test_delete_enabled_domain_fails(self): + """Call ``DELETE /domains/{domain_id}`` (when domain enabled).""" + + # Try deleting an enabled domain, which should fail + self.delete('/domains/%(domain_id)s' % { + 'domain_id': self.domain['id']}, + expected_status=exception.ForbiddenAction.code) + + def test_delete_domain(self): + """Call ``DELETE /domains/{domain_id}``. + + The sample data set up already has a user, group, project + and credential that is part of self.domain. Since the user + we will authenticate with is in this domain, we create a + another set of entities in a second domain. Deleting this + second domain should delete all these new entities. In addition, + all the entities in the regular self.domain should be unaffected + by the delete. + + Test Plan: + + - Create domain2 and a 2nd set of entities + - Disable domain2 + - Delete domain2 + - Check entities in domain2 have been deleted + - Check entities in self.domain are unaffected + + """ + + # Create a 2nd set of entities in a 2nd domain + self.domain2 = self.new_domain_ref() + self.resource_api.create_domain(self.domain2['id'], self.domain2) + + self.project2 = self.new_project_ref( + domain_id=self.domain2['id']) + self.resource_api.create_project(self.project2['id'], self.project2) + + self.user2 = self.new_user_ref( + domain_id=self.domain2['id'], + project_id=self.project2['id']) + self.user2 = self.identity_api.create_user(self.user2) + + self.group2 = self.new_group_ref( + domain_id=self.domain2['id']) + self.group2 = self.identity_api.create_group(self.group2) + + self.credential2 = self.new_credential_ref( + user_id=self.user2['id'], + project_id=self.project2['id']) + self.credential_api.create_credential( + self.credential2['id'], + self.credential2) + + # Now disable the new domain and delete it + self.domain2['enabled'] = False + r = self.patch('/domains/%(domain_id)s' % { + 'domain_id': self.domain2['id']}, + body={'domain': {'enabled': False}}) + self.assertValidDomainResponse(r, self.domain2) + self.delete('/domains/%(domain_id)s' % { + 'domain_id': self.domain2['id']}) + + # Check all the domain2 relevant entities are gone + self.assertRaises(exception.DomainNotFound, + self.resource_api.get_domain, + self.domain2['id']) + self.assertRaises(exception.ProjectNotFound, + self.resource_api.get_project, + self.project2['id']) + self.assertRaises(exception.GroupNotFound, + self.identity_api.get_group, + self.group2['id']) + self.assertRaises(exception.UserNotFound, + self.identity_api.get_user, + self.user2['id']) + self.assertRaises(exception.CredentialNotFound, + self.credential_api.get_credential, + self.credential2['id']) + + # ...and that all self.domain entities are still here + r = self.resource_api.get_domain(self.domain['id']) + self.assertDictEqual(r, self.domain) + r = self.resource_api.get_project(self.project['id']) + self.assertDictEqual(r, self.project) + r = self.identity_api.get_group(self.group['id']) + self.assertDictEqual(r, self.group) + r = self.identity_api.get_user(self.user['id']) + self.user.pop('password') + self.assertDictEqual(r, self.user) + r = self.credential_api.get_credential(self.credential['id']) + self.assertDictEqual(r, self.credential) + + def test_delete_default_domain_fails(self): + # Attempting to delete the default domain results in 403 Forbidden. + + # Need to disable it first. + self.patch('/domains/%(domain_id)s' % { + 'domain_id': CONF.identity.default_domain_id}, + body={'domain': {'enabled': False}}) + + self.delete('/domains/%(domain_id)s' % { + 'domain_id': CONF.identity.default_domain_id}, + expected_status=exception.ForbiddenAction.code) + + def test_delete_new_default_domain_fails(self): + # If change the default domain ID, deleting the new default domain + # results in a 403 Forbidden. + + # Create a new domain that's not the default + new_domain = self.new_domain_ref() + new_domain_id = new_domain['id'] + self.resource_api.create_domain(new_domain_id, new_domain) + + # Disable the new domain so can delete it later. + self.patch('/domains/%(domain_id)s' % { + 'domain_id': new_domain_id}, + body={'domain': {'enabled': False}}) + + # Change the default domain + self.config_fixture.config(group='identity', + default_domain_id=new_domain_id) + + # Attempt to delete the new domain + + self.delete('/domains/%(domain_id)s' % {'domain_id': new_domain_id}, + expected_status=exception.ForbiddenAction.code) + + def test_delete_old_default_domain(self): + # If change the default domain ID, deleting the old default domain + # works. + + # Create a new domain that's not the default + new_domain = self.new_domain_ref() + new_domain_id = new_domain['id'] + self.resource_api.create_domain(new_domain_id, new_domain) + + old_default_domain_id = CONF.identity.default_domain_id + + # Disable the default domain so we can delete it later. + self.patch('/domains/%(domain_id)s' % { + 'domain_id': old_default_domain_id}, + body={'domain': {'enabled': False}}) + + # Change the default domain + self.config_fixture.config(group='identity', + default_domain_id=new_domain_id) + + # Delete the old default domain + + self.delete( + '/domains/%(domain_id)s' % {'domain_id': old_default_domain_id}) + + def test_token_revoked_once_domain_disabled(self): + """Test token from a disabled domain has been invalidated. + + Test that a token that was valid for an enabled domain + becomes invalid once that domain is disabled. + + """ + + self.domain = self.new_domain_ref() + self.resource_api.create_domain(self.domain['id'], self.domain) + + self.user2 = self.new_user_ref(domain_id=self.domain['id']) + password = self.user2['password'] + self.user2 = self.identity_api.create_user(self.user2) + self.user2['password'] = password + + # build a request body + auth_body = self.build_authentication_request( + user_id=self.user2['id'], + password=self.user2['password']) + + # sends a request for the user's token + token_resp = self.post('/auth/tokens', body=auth_body) + + subject_token = token_resp.headers.get('x-subject-token') + + # validates the returned token and it should be valid. + self.head('/auth/tokens', + headers={'x-subject-token': subject_token}, + expected_status=200) + + # now disable the domain + self.domain['enabled'] = False + url = "/domains/%(domain_id)s" % {'domain_id': self.domain['id']} + self.patch(url, + body={'domain': {'enabled': False}}, + expected_status=200) + + # validates the same token again and it should be 'not found' + # as the domain has already been disabled. + self.head('/auth/tokens', + headers={'x-subject-token': subject_token}, + expected_status=404) + + def test_delete_domain_hierarchy(self): + """Call ``DELETE /domains/{domain_id}``.""" + domain = self.new_domain_ref() + self.resource_api.create_domain(domain['id'], domain) + + root_project = self.new_project_ref( + domain_id=domain['id']) + self.resource_api.create_project(root_project['id'], root_project) + + leaf_project = self.new_project_ref( + domain_id=domain['id'], + parent_id=root_project['id']) + self.resource_api.create_project(leaf_project['id'], leaf_project) + + # Need to disable it first. + self.patch('/domains/%(domain_id)s' % { + 'domain_id': domain['id']}, + body={'domain': {'enabled': False}}) + + self.delete( + '/domains/%(domain_id)s' % { + 'domain_id': domain['id']}) + + self.assertRaises(exception.DomainNotFound, + self.resource_api.get_domain, + domain['id']) + + self.assertRaises(exception.ProjectNotFound, + self.resource_api.get_project, + root_project['id']) + + self.assertRaises(exception.ProjectNotFound, + self.resource_api.get_project, + leaf_project['id']) + + def test_forbid_operations_on_federated_domain(self): + """Make sure one cannot operate on federated domain. + + This includes operations like create, update, delete + on domain identified by id and name where difference variations of + id 'Federated' are used. + + """ + def create_domains(): + for variation in ('Federated', 'FEDERATED', + 'federated', 'fEderated'): + domain = self.new_domain_ref() + domain['id'] = variation + yield domain + + for domain in create_domains(): + self.assertRaises( + AssertionError, self.assignment_api.create_domain, + domain['id'], domain) + self.assertRaises( + AssertionError, self.assignment_api.update_domain, + domain['id'], domain) + self.assertRaises( + exception.DomainNotFound, self.assignment_api.delete_domain, + domain['id']) + + # swap 'name' with 'id' and try again, expecting the request to + # gracefully fail + domain['id'], domain['name'] = domain['name'], domain['id'] + self.assertRaises( + AssertionError, self.assignment_api.create_domain, + domain['id'], domain) + self.assertRaises( + AssertionError, self.assignment_api.update_domain, + domain['id'], domain) + self.assertRaises( + exception.DomainNotFound, self.assignment_api.delete_domain, + domain['id']) + + def test_forbid_operations_on_defined_federated_domain(self): + """Make sure one cannot operate on a user-defined federated domain. + + This includes operations like create, update, delete. + + """ + + non_default_name = 'beta_federated_domain' + self.config_fixture.config(group='federation', + federated_domain_name=non_default_name) + domain = self.new_domain_ref() + domain['name'] = non_default_name + self.assertRaises(AssertionError, + self.assignment_api.create_domain, + domain['id'], domain) + self.assertRaises(exception.DomainNotFound, + self.assignment_api.delete_domain, + domain['id']) + self.assertRaises(AssertionError, + self.assignment_api.update_domain, + domain['id'], domain) + + def test_set_federated_domain_when_config_empty(self): + """Make sure we are operable even if config value is not properly + set. + + This includes operations like create, update, delete. + + """ + federated_name = 'Federated' + self.config_fixture.config(group='federation', + federated_domain_name='') + domain = self.new_domain_ref() + domain['id'] = federated_name + self.assertRaises(AssertionError, + self.assignment_api.create_domain, + domain['id'], domain) + self.assertRaises(exception.DomainNotFound, + self.assignment_api.delete_domain, + domain['id']) + self.assertRaises(AssertionError, + self.assignment_api.update_domain, + domain['id'], domain) + + # swap id with name + domain['id'], domain['name'] = domain['name'], domain['id'] + self.assertRaises(AssertionError, + self.assignment_api.create_domain, + domain['id'], domain) + self.assertRaises(exception.DomainNotFound, + self.assignment_api.delete_domain, + domain['id']) + self.assertRaises(AssertionError, + self.assignment_api.update_domain, + domain['id'], domain) + + # Project CRUD tests + + def test_list_projects(self): + """Call ``GET /projects``.""" + resource_url = '/projects' + r = self.get(resource_url) + self.assertValidProjectListResponse(r, ref=self.project, + resource_url=resource_url) + + def test_create_project(self): + """Call ``POST /projects``.""" + ref = self.new_project_ref(domain_id=self.domain_id) + r = self.post( + '/projects', + body={'project': ref}) + self.assertValidProjectResponse(r, ref) + + def test_create_project_400(self): + """Call ``POST /projects``.""" + self.post('/projects', body={'project': {}}, expected_status=400) + + def _create_projects_hierarchy(self, hierarchy_size=1): + """Creates a project hierarchy with specified size. + + :param hierarchy_size: the desired hierarchy size, default is 1 - + a project with one child. + + :returns projects: a list of the projects in the created hierarchy. + + """ + resp = self.get( + '/projects/%(project_id)s' % { + 'project_id': self.project_id}) + + projects = [resp.result] + + for i in range(hierarchy_size): + new_ref = self.new_project_ref( + domain_id=self.domain_id, + parent_id=projects[i]['project']['id']) + resp = self.post('/projects', + body={'project': new_ref}) + self.assertValidProjectResponse(resp, new_ref) + + projects.append(resp.result) + + return projects + + def test_create_hierarchical_project(self): + """Call ``POST /projects``.""" + self._create_projects_hierarchy() + + def test_get_project(self): + """Call ``GET /projects/{project_id}``.""" + r = self.get( + '/projects/%(project_id)s' % { + 'project_id': self.project_id}) + self.assertValidProjectResponse(r, self.project) + + def test_get_project_with_parents_as_ids(self): + """Call ``GET /projects/{project_id}?parents_as_ids``.""" + projects = self._create_projects_hierarchy(hierarchy_size=2) + + # Query for projects[2] parents_as_ids + r = self.get( + '/projects/%(project_id)s?parents_as_ids' % { + 'project_id': projects[2]['project']['id']}) + + self.assertValidProjectResponse(r, projects[2]['project']) + parents_as_ids = r.result['project']['parents'] + + # Assert parents_as_ids is a structured dictionary correctly + # representing the hierarchy. The request was made using projects[2] + # id, hence its parents should be projects[1] and projects[0]. It + # should have the following structure: + # { + # projects[1]: { + # projects[0]: None + # } + # } + expected_dict = { + projects[1]['project']['id']: { + projects[0]['project']['id']: None + } + } + self.assertDictEqual(expected_dict, parents_as_ids) + + # Query for projects[0] parents_as_ids + r = self.get( + '/projects/%(project_id)s?parents_as_ids' % { + 'project_id': projects[0]['project']['id']}) + + self.assertValidProjectResponse(r, projects[0]['project']) + parents_as_ids = r.result['project']['parents'] + + # projects[0] has no parents, parents_as_ids must be None + self.assertIsNone(parents_as_ids) + + def test_get_project_with_parents_as_list(self): + """Call ``GET /projects/{project_id}?parents_as_list``.""" + projects = self._create_projects_hierarchy(hierarchy_size=2) + + r = self.get( + '/projects/%(project_id)s?parents_as_list' % { + 'project_id': projects[1]['project']['id']}) + + self.assertEqual(1, len(r.result['project']['parents'])) + self.assertValidProjectResponse(r, projects[1]['project']) + self.assertIn(projects[0], r.result['project']['parents']) + self.assertNotIn(projects[2], r.result['project']['parents']) + + def test_get_project_with_parents_as_list_and_parents_as_ids(self): + """Call ``GET /projects/{project_id}?parents_as_list&parents_as_ids``. + + """ + projects = self._create_projects_hierarchy(hierarchy_size=2) + + self.get( + '/projects/%(project_id)s?parents_as_list&parents_as_ids' % { + 'project_id': projects[1]['project']['id']}, + expected_status=400) + + def test_get_project_with_subtree_as_ids(self): + """Call ``GET /projects/{project_id}?subtree_as_ids``. + + This test creates a more complex hierarchy to test if the structured + dictionary returned by using the ``subtree_as_ids`` query param + correctly represents the hierarchy. + + The hierarchy contains 5 projects with the following structure:: + + +--A--+ + | | + +--B--+ C + | | + D E + + + """ + projects = self._create_projects_hierarchy(hierarchy_size=2) + + # Add another child to projects[0] - it will be projects[3] + new_ref = self.new_project_ref( + domain_id=self.domain_id, + parent_id=projects[0]['project']['id']) + resp = self.post('/projects', + body={'project': new_ref}) + self.assertValidProjectResponse(resp, new_ref) + projects.append(resp.result) + + # Add another child to projects[1] - it will be projects[4] + new_ref = self.new_project_ref( + domain_id=self.domain_id, + parent_id=projects[1]['project']['id']) + resp = self.post('/projects', + body={'project': new_ref}) + self.assertValidProjectResponse(resp, new_ref) + projects.append(resp.result) + + # Query for projects[0] subtree_as_ids + r = self.get( + '/projects/%(project_id)s?subtree_as_ids' % { + 'project_id': projects[0]['project']['id']}) + self.assertValidProjectResponse(r, projects[0]['project']) + subtree_as_ids = r.result['project']['subtree'] + + # The subtree hierarchy from projects[0] should have the following + # structure: + # { + # projects[1]: { + # projects[2]: None, + # projects[4]: None + # }, + # projects[3]: None + # } + expected_dict = { + projects[1]['project']['id']: { + projects[2]['project']['id']: None, + projects[4]['project']['id']: None + }, + projects[3]['project']['id']: None + } + self.assertDictEqual(expected_dict, subtree_as_ids) + + # Now query for projects[1] subtree_as_ids + r = self.get( + '/projects/%(project_id)s?subtree_as_ids' % { + 'project_id': projects[1]['project']['id']}) + self.assertValidProjectResponse(r, projects[1]['project']) + subtree_as_ids = r.result['project']['subtree'] + + # The subtree hierarchy from projects[1] should have the following + # structure: + # { + # projects[2]: None, + # projects[4]: None + # } + expected_dict = { + projects[2]['project']['id']: None, + projects[4]['project']['id']: None + } + self.assertDictEqual(expected_dict, subtree_as_ids) + + # Now query for projects[3] subtree_as_ids + r = self.get( + '/projects/%(project_id)s?subtree_as_ids' % { + 'project_id': projects[3]['project']['id']}) + self.assertValidProjectResponse(r, projects[3]['project']) + subtree_as_ids = r.result['project']['subtree'] + + # projects[3] has no subtree, subtree_as_ids must be None + self.assertIsNone(subtree_as_ids) + + def test_get_project_with_subtree_as_list(self): + """Call ``GET /projects/{project_id}?subtree_as_list``.""" + projects = self._create_projects_hierarchy(hierarchy_size=2) + + r = self.get( + '/projects/%(project_id)s?subtree_as_list' % { + 'project_id': projects[1]['project']['id']}) + + self.assertEqual(1, len(r.result['project']['subtree'])) + self.assertValidProjectResponse(r, projects[1]['project']) + self.assertNotIn(projects[0], r.result['project']['subtree']) + self.assertIn(projects[2], r.result['project']['subtree']) + + def test_get_project_with_subtree_as_list_and_subtree_as_ids(self): + """Call ``GET /projects/{project_id}?subtree_as_list&subtree_as_ids``. + + """ + projects = self._create_projects_hierarchy(hierarchy_size=2) + + self.get( + '/projects/%(project_id)s?subtree_as_list&subtree_as_ids' % { + 'project_id': projects[1]['project']['id']}, + expected_status=400) + + def test_update_project(self): + """Call ``PATCH /projects/{project_id}``.""" + ref = self.new_project_ref(domain_id=self.domain_id) + del ref['id'] + r = self.patch( + '/projects/%(project_id)s' % { + 'project_id': self.project_id}, + body={'project': ref}) + self.assertValidProjectResponse(r, ref) + + def test_update_project_domain_id(self): + """Call ``PATCH /projects/{project_id}`` with domain_id.""" + project = self.new_project_ref(domain_id=self.domain['id']) + self.resource_api.create_project(project['id'], project) + project['domain_id'] = CONF.identity.default_domain_id + r = self.patch('/projects/%(project_id)s' % { + 'project_id': project['id']}, + body={'project': project}, + expected_status=exception.ValidationError.code) + self.config_fixture.config(domain_id_immutable=False) + project['domain_id'] = self.domain['id'] + r = self.patch('/projects/%(project_id)s' % { + 'project_id': project['id']}, + body={'project': project}) + self.assertValidProjectResponse(r, project) + + def test_update_project_parent_id(self): + """Call ``PATCH /projects/{project_id}``.""" + projects = self._create_projects_hierarchy() + leaf_project = projects[1]['project'] + leaf_project['parent_id'] = None + self.patch( + '/projects/%(project_id)s' % { + 'project_id': leaf_project['id']}, + body={'project': leaf_project}, + expected_status=403) + + def test_disable_leaf_project(self): + """Call ``PATCH /projects/{project_id}``.""" + projects = self._create_projects_hierarchy() + leaf_project = projects[1]['project'] + leaf_project['enabled'] = False + r = self.patch( + '/projects/%(project_id)s' % { + 'project_id': leaf_project['id']}, + body={'project': leaf_project}) + self.assertEqual( + leaf_project['enabled'], r.result['project']['enabled']) + + def test_disable_not_leaf_project(self): + """Call ``PATCH /projects/{project_id}``.""" + projects = self._create_projects_hierarchy() + root_project = projects[0]['project'] + root_project['enabled'] = False + self.patch( + '/projects/%(project_id)s' % { + 'project_id': root_project['id']}, + body={'project': root_project}, + expected_status=403) + + def test_delete_project(self): + """Call ``DELETE /projects/{project_id}`` + + As well as making sure the delete succeeds, we ensure + that any credentials that reference this projects are + also deleted, while other credentials are unaffected. + + """ + # First check the credential for this project is present + r = self.credential_api.get_credential(self.credential['id']) + self.assertDictEqual(r, self.credential) + # Create a second credential with a different project + self.project2 = self.new_project_ref( + domain_id=self.domain['id']) + self.resource_api.create_project(self.project2['id'], self.project2) + self.credential2 = self.new_credential_ref( + user_id=self.user['id'], + project_id=self.project2['id']) + self.credential_api.create_credential( + self.credential2['id'], + self.credential2) + + # Now delete the project + self.delete( + '/projects/%(project_id)s' % { + 'project_id': self.project_id}) + + # Deleting the project should have deleted any credentials + # that reference this project + self.assertRaises(exception.CredentialNotFound, + self.credential_api.get_credential, + credential_id=self.credential['id']) + # But the credential for project2 is unaffected + r = self.credential_api.get_credential(self.credential2['id']) + self.assertDictEqual(r, self.credential2) + + def test_delete_not_leaf_project(self): + """Call ``DELETE /projects/{project_id}``.""" + self._create_projects_hierarchy() + self.delete( + '/projects/%(project_id)s' % { + 'project_id': self.project_id}, + expected_status=403) + + # Role CRUD tests + + def test_create_role(self): + """Call ``POST /roles``.""" + ref = self.new_role_ref() + r = self.post( + '/roles', + body={'role': ref}) + return self.assertValidRoleResponse(r, ref) + + def test_create_role_400(self): + """Call ``POST /roles``.""" + self.post('/roles', body={'role': {}}, expected_status=400) + + def test_list_roles(self): + """Call ``GET /roles``.""" + resource_url = '/roles' + r = self.get(resource_url) + self.assertValidRoleListResponse(r, ref=self.role, + resource_url=resource_url) + + def test_get_role(self): + """Call ``GET /roles/{role_id}``.""" + r = self.get('/roles/%(role_id)s' % { + 'role_id': self.role_id}) + self.assertValidRoleResponse(r, self.role) + + def test_update_role(self): + """Call ``PATCH /roles/{role_id}``.""" + ref = self.new_role_ref() + del ref['id'] + r = self.patch('/roles/%(role_id)s' % { + 'role_id': self.role_id}, + body={'role': ref}) + self.assertValidRoleResponse(r, ref) + + def test_delete_role(self): + """Call ``DELETE /roles/{role_id}``.""" + self.delete('/roles/%(role_id)s' % { + 'role_id': self.role_id}) + + # Role Grants tests + + def test_crud_user_project_role_grants(self): + collection_url = ( + '/projects/%(project_id)s/users/%(user_id)s/roles' % { + 'project_id': self.project['id'], + 'user_id': self.user['id']}) + member_url = '%(collection_url)s/%(role_id)s' % { + 'collection_url': collection_url, + 'role_id': self.role_id} + + self.put(member_url) + self.head(member_url) + r = self.get(collection_url) + self.assertValidRoleListResponse(r, ref=self.role, + resource_url=collection_url) + + # FIXME(gyee): this test is no longer valid as user + # have no role in the project. Can't get a scoped token + # self.delete(member_url) + # r = self.get(collection_url) + # self.assertValidRoleListResponse(r, expected_length=0) + # self.assertIn(collection_url, r.result['links']['self']) + + def test_crud_user_project_role_grants_no_user(self): + """Grant role on a project to a user that doesn't exist, 404 result. + + When grant a role on a project to a user that doesn't exist, the server + returns 404 Not Found for the user. + + """ + + user_id = uuid.uuid4().hex + + collection_url = ( + '/projects/%(project_id)s/users/%(user_id)s/roles' % { + 'project_id': self.project['id'], 'user_id': user_id}) + member_url = '%(collection_url)s/%(role_id)s' % { + 'collection_url': collection_url, + 'role_id': self.role_id} + + self.put(member_url, expected_status=404) + + def test_crud_user_domain_role_grants(self): + collection_url = ( + '/domains/%(domain_id)s/users/%(user_id)s/roles' % { + 'domain_id': self.domain_id, + 'user_id': self.user['id']}) + member_url = '%(collection_url)s/%(role_id)s' % { + 'collection_url': collection_url, + 'role_id': self.role_id} + + self.put(member_url) + self.head(member_url) + r = self.get(collection_url) + self.assertValidRoleListResponse(r, ref=self.role, + resource_url=collection_url) + + self.delete(member_url) + r = self.get(collection_url) + self.assertValidRoleListResponse(r, expected_length=0, + resource_url=collection_url) + + def test_crud_user_domain_role_grants_no_user(self): + """Grant role on a domain to a user that doesn't exist, 404 result. + + When grant a role on a domain to a user that doesn't exist, the server + returns 404 Not Found for the user. + + """ + + user_id = uuid.uuid4().hex + + collection_url = ( + '/domains/%(domain_id)s/users/%(user_id)s/roles' % { + 'domain_id': self.domain_id, 'user_id': user_id}) + member_url = '%(collection_url)s/%(role_id)s' % { + 'collection_url': collection_url, + 'role_id': self.role_id} + + self.put(member_url, expected_status=404) + + def test_crud_group_project_role_grants(self): + collection_url = ( + '/projects/%(project_id)s/groups/%(group_id)s/roles' % { + 'project_id': self.project_id, + 'group_id': self.group_id}) + member_url = '%(collection_url)s/%(role_id)s' % { + 'collection_url': collection_url, + 'role_id': self.role_id} + + self.put(member_url) + self.head(member_url) + r = self.get(collection_url) + self.assertValidRoleListResponse(r, ref=self.role, + resource_url=collection_url) + + self.delete(member_url) + r = self.get(collection_url) + self.assertValidRoleListResponse(r, expected_length=0, + resource_url=collection_url) + + def test_crud_group_project_role_grants_no_group(self): + """Grant role on a project to a group that doesn't exist, 404 result. + + When grant a role on a project to a group that doesn't exist, the + server returns 404 Not Found for the group. + + """ + + group_id = uuid.uuid4().hex + + collection_url = ( + '/projects/%(project_id)s/groups/%(group_id)s/roles' % { + 'project_id': self.project_id, + 'group_id': group_id}) + member_url = '%(collection_url)s/%(role_id)s' % { + 'collection_url': collection_url, + 'role_id': self.role_id} + + self.put(member_url, expected_status=404) + + def test_crud_group_domain_role_grants(self): + collection_url = ( + '/domains/%(domain_id)s/groups/%(group_id)s/roles' % { + 'domain_id': self.domain_id, + 'group_id': self.group_id}) + member_url = '%(collection_url)s/%(role_id)s' % { + 'collection_url': collection_url, + 'role_id': self.role_id} + + self.put(member_url) + self.head(member_url) + r = self.get(collection_url) + self.assertValidRoleListResponse(r, ref=self.role, + resource_url=collection_url) + + self.delete(member_url) + r = self.get(collection_url) + self.assertValidRoleListResponse(r, expected_length=0, + resource_url=collection_url) + + def test_crud_group_domain_role_grants_no_group(self): + """Grant role on a domain to a group that doesn't exist, 404 result. + + When grant a role on a domain to a group that doesn't exist, the server + returns 404 Not Found for the group. + + """ + + group_id = uuid.uuid4().hex + + collection_url = ( + '/domains/%(domain_id)s/groups/%(group_id)s/roles' % { + 'domain_id': self.domain_id, + 'group_id': group_id}) + member_url = '%(collection_url)s/%(role_id)s' % { + 'collection_url': collection_url, + 'role_id': self.role_id} + + self.put(member_url, expected_status=404) + + def _create_new_user_and_assign_role_on_project(self): + """Create a new user and assign user a role on a project.""" + # Create a new user + new_user = self.new_user_ref(domain_id=self.domain_id) + user_ref = self.identity_api.create_user(new_user) + # Assign the user a role on the project + collection_url = ( + '/projects/%(project_id)s/users/%(user_id)s/roles' % { + 'project_id': self.project_id, + 'user_id': user_ref['id']}) + member_url = ('%(collection_url)s/%(role_id)s' % { + 'collection_url': collection_url, + 'role_id': self.role_id}) + self.put(member_url, expected_status=204) + # Check the user has the role assigned + self.head(member_url, expected_status=204) + return member_url, user_ref + + def test_delete_user_before_removing_role_assignment_succeeds(self): + """Call ``DELETE`` on the user before the role assignment.""" + member_url, user = self._create_new_user_and_assign_role_on_project() + # Delete the user from identity backend + self.identity_api.driver.delete_user(user['id']) + # Clean up the role assignment + self.delete(member_url, expected_status=204) + # Make sure the role is gone + self.head(member_url, expected_status=404) + + def test_delete_user_and_check_role_assignment_fails(self): + """Call ``DELETE`` on the user and check the role assignment.""" + member_url, user = self._create_new_user_and_assign_role_on_project() + # Delete the user from identity backend + self.identity_api.delete_user(user['id']) + # We should get a 404 when looking for the user in the identity + # backend because we're not performing a delete operation on the role. + self.head(member_url, expected_status=404) + + def test_token_revoked_once_group_role_grant_revoked(self): + """Test token is revoked when group role grant is revoked + + When a role granted to a group is revoked for a given scope, + all tokens related to this scope and belonging to one of the members + of this group should be revoked. + + The revocation should be independently to the presence + of the revoke API. + """ + # creates grant from group on project. + self.assignment_api.create_grant(role_id=self.role['id'], + project_id=self.project['id'], + group_id=self.group['id']) + + # adds user to the group. + self.identity_api.add_user_to_group(user_id=self.user['id'], + group_id=self.group['id']) + + # creates a token for the user + auth_body = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + project_id=self.project['id']) + token_resp = self.post('/auth/tokens', body=auth_body) + token = token_resp.headers.get('x-subject-token') + + # validates the returned token; it should be valid. + self.head('/auth/tokens', + headers={'x-subject-token': token}, + expected_status=200) + + # revokes the grant from group on project. + self.assignment_api.delete_grant(role_id=self.role['id'], + project_id=self.project['id'], + group_id=self.group['id']) + + # validates the same token again; it should not longer be valid. + self.head('/auth/tokens', + headers={'x-subject-token': token}, + expected_status=404) + + # Role Assignments tests + + def test_get_role_assignments(self): + """Call ``GET /role_assignments``. + + The sample data set up already has a user, group and project + that is part of self.domain. We use these plus a new user + we create as our data set, making sure we ignore any + role assignments that are already in existence. + + Since we don't yet support a first class entity for role + assignments, we are only testing the LIST API. To create + and delete the role assignments we use the old grant APIs. + + Test Plan: + + - Create extra user for tests + - Get a list of all existing role assignments + - Add a new assignment for each of the four combinations, i.e. + group+domain, user+domain, group+project, user+project, using + the same role each time + - Get a new list of all role assignments, checking these four new + ones have been added + - Then delete the four we added + - Get a new list of all role assignments, checking the four have + been removed + + """ + + # Since the default fixtures already assign some roles to the + # user it creates, we also need a new user that will not have any + # existing assignments + self.user1 = self.new_user_ref( + domain_id=self.domain['id']) + self.user1 = self.identity_api.create_user(self.user1) + + collection_url = '/role_assignments' + r = self.get(collection_url) + self.assertValidRoleAssignmentListResponse(r, + resource_url=collection_url) + existing_assignments = len(r.result.get('role_assignments')) + + # Now add one of each of the four types of assignment, making sure + # that we get them all back. + gd_entity = _build_role_assignment_entity(domain_id=self.domain_id, + group_id=self.group_id, + role_id=self.role_id) + self.put(gd_entity['links']['assignment']) + r = self.get(collection_url) + self.assertValidRoleAssignmentListResponse( + r, + expected_length=existing_assignments + 1, + resource_url=collection_url) + self.assertRoleAssignmentInListResponse(r, gd_entity) + + ud_entity = _build_role_assignment_entity(domain_id=self.domain_id, + user_id=self.user1['id'], + role_id=self.role_id) + self.put(ud_entity['links']['assignment']) + r = self.get(collection_url) + self.assertValidRoleAssignmentListResponse( + r, + expected_length=existing_assignments + 2, + resource_url=collection_url) + self.assertRoleAssignmentInListResponse(r, ud_entity) + + gp_entity = _build_role_assignment_entity(project_id=self.project_id, + group_id=self.group_id, + role_id=self.role_id) + self.put(gp_entity['links']['assignment']) + r = self.get(collection_url) + self.assertValidRoleAssignmentListResponse( + r, + expected_length=existing_assignments + 3, + resource_url=collection_url) + self.assertRoleAssignmentInListResponse(r, gp_entity) + + up_entity = _build_role_assignment_entity(project_id=self.project_id, + user_id=self.user1['id'], + role_id=self.role_id) + self.put(up_entity['links']['assignment']) + r = self.get(collection_url) + self.assertValidRoleAssignmentListResponse( + r, + expected_length=existing_assignments + 4, + resource_url=collection_url) + self.assertRoleAssignmentInListResponse(r, up_entity) + + # Now delete the four we added and make sure they are removed + # from the collection. + + self.delete(gd_entity['links']['assignment']) + self.delete(ud_entity['links']['assignment']) + self.delete(gp_entity['links']['assignment']) + self.delete(up_entity['links']['assignment']) + r = self.get(collection_url) + self.assertValidRoleAssignmentListResponse( + r, + expected_length=existing_assignments, + resource_url=collection_url) + self.assertRoleAssignmentNotInListResponse(r, gd_entity) + self.assertRoleAssignmentNotInListResponse(r, ud_entity) + self.assertRoleAssignmentNotInListResponse(r, gp_entity) + self.assertRoleAssignmentNotInListResponse(r, up_entity) + + def test_get_effective_role_assignments(self): + """Call ``GET /role_assignments?effective``. + + Test Plan: + + - Create two extra user for tests + - Add these users to a group + - Add a role assignment for the group on a domain + - Get a list of all role assignments, checking one has been added + - Then get a list of all effective role assignments - the group + assignment should have turned into assignments on the domain + for each of the group members. + + """ + self.user1 = self.new_user_ref( + domain_id=self.domain['id']) + password = self.user1['password'] + self.user1 = self.identity_api.create_user(self.user1) + self.user1['password'] = password + self.user2 = self.new_user_ref( + domain_id=self.domain['id']) + password = self.user2['password'] + self.user2 = self.identity_api.create_user(self.user2) + self.user2['password'] = password + self.identity_api.add_user_to_group(self.user1['id'], self.group['id']) + self.identity_api.add_user_to_group(self.user2['id'], self.group['id']) + + collection_url = '/role_assignments' + r = self.get(collection_url) + self.assertValidRoleAssignmentListResponse(r, + resource_url=collection_url) + existing_assignments = len(r.result.get('role_assignments')) + + gd_entity = _build_role_assignment_entity(domain_id=self.domain_id, + group_id=self.group_id, + role_id=self.role_id) + self.put(gd_entity['links']['assignment']) + r = self.get(collection_url) + self.assertValidRoleAssignmentListResponse( + r, + expected_length=existing_assignments + 1, + resource_url=collection_url) + self.assertRoleAssignmentInListResponse(r, gd_entity) + + # Now re-read the collection asking for effective roles - this + # should mean the group assignment is translated into the two + # member user assignments + collection_url = '/role_assignments?effective' + r = self.get(collection_url) + self.assertValidRoleAssignmentListResponse( + r, + expected_length=existing_assignments + 2, + resource_url=collection_url) + ud_entity = _build_role_assignment_entity( + link=gd_entity['links']['assignment'], domain_id=self.domain_id, + user_id=self.user1['id'], role_id=self.role_id) + self.assertRoleAssignmentInListResponse(r, ud_entity) + ud_entity = _build_role_assignment_entity( + link=gd_entity['links']['assignment'], domain_id=self.domain_id, + user_id=self.user2['id'], role_id=self.role_id) + self.assertRoleAssignmentInListResponse(r, ud_entity) + + def test_check_effective_values_for_role_assignments(self): + """Call ``GET /role_assignments?effective=value``. + + Check the various ways of specifying the 'effective' + query parameter. If the 'effective' query parameter + is included then this should always be treated as meaning 'True' + unless it is specified as: + + {url}?effective=0 + + This is by design to match the agreed way of handling + policy checking on query/filter parameters. + + Test Plan: + + - Create two extra user for tests + - Add these users to a group + - Add a role assignment for the group on a domain + - Get a list of all role assignments, checking one has been added + - Then issue various request with different ways of defining + the 'effective' query parameter. As we have tested the + correctness of the data coming back when we get effective roles + in other tests, here we just use the count of entities to + know if we are getting effective roles or not + + """ + self.user1 = self.new_user_ref( + domain_id=self.domain['id']) + password = self.user1['password'] + self.user1 = self.identity_api.create_user(self.user1) + self.user1['password'] = password + self.user2 = self.new_user_ref( + domain_id=self.domain['id']) + password = self.user2['password'] + self.user2 = self.identity_api.create_user(self.user2) + self.user2['password'] = password + self.identity_api.add_user_to_group(self.user1['id'], self.group['id']) + self.identity_api.add_user_to_group(self.user2['id'], self.group['id']) + + collection_url = '/role_assignments' + r = self.get(collection_url) + self.assertValidRoleAssignmentListResponse(r, + resource_url=collection_url) + existing_assignments = len(r.result.get('role_assignments')) + + gd_entity = _build_role_assignment_entity(domain_id=self.domain_id, + group_id=self.group_id, + role_id=self.role_id) + self.put(gd_entity['links']['assignment']) + r = self.get(collection_url) + self.assertValidRoleAssignmentListResponse( + r, + expected_length=existing_assignments + 1, + resource_url=collection_url) + self.assertRoleAssignmentInListResponse(r, gd_entity) + + # Now re-read the collection asking for effective roles, + # using the most common way of defining "effective'. This + # should mean the group assignment is translated into the two + # member user assignments + collection_url = '/role_assignments?effective' + r = self.get(collection_url) + self.assertValidRoleAssignmentListResponse( + r, + expected_length=existing_assignments + 2, + resource_url=collection_url) + # Now set 'effective' to false explicitly - should get + # back the regular roles + collection_url = '/role_assignments?effective=0' + r = self.get(collection_url) + self.assertValidRoleAssignmentListResponse( + r, + expected_length=existing_assignments + 1, + resource_url=collection_url) + # Now try setting 'effective' to 'False' explicitly- this is + # NOT supported as a way of setting a query or filter + # parameter to false by design. Hence we should get back + # effective roles. + collection_url = '/role_assignments?effective=False' + r = self.get(collection_url) + self.assertValidRoleAssignmentListResponse( + r, + expected_length=existing_assignments + 2, + resource_url=collection_url) + # Now set 'effective' to True explicitly + collection_url = '/role_assignments?effective=True' + r = self.get(collection_url) + self.assertValidRoleAssignmentListResponse( + r, + expected_length=existing_assignments + 2, + resource_url=collection_url) + + def test_filtered_role_assignments(self): + """Call ``GET /role_assignments?filters``. + + Test Plan: + + - Create extra users, group, role and project for tests + - Make the following assignments: + Give group1, role1 on project1 and domain + Give user1, role2 on project1 and domain + Make User1 a member of Group1 + - Test a series of single filter list calls, checking that + the correct results are obtained + - Test a multi-filtered list call + - Test listing all effective roles for a given user + - Test the equivalent of the list of roles in a project scoped + token (all effective roles for a user on a project) + + """ + + # Since the default fixtures already assign some roles to the + # user it creates, we also need a new user that will not have any + # existing assignments + self.user1 = self.new_user_ref( + domain_id=self.domain['id']) + password = self.user1['password'] + self.user1 = self.identity_api.create_user(self.user1) + self.user1['password'] = password + self.user2 = self.new_user_ref( + domain_id=self.domain['id']) + password = self.user2['password'] + self.user2 = self.identity_api.create_user(self.user2) + self.user2['password'] = password + self.group1 = self.new_group_ref( + domain_id=self.domain['id']) + self.group1 = self.identity_api.create_group(self.group1) + self.identity_api.add_user_to_group(self.user1['id'], + self.group1['id']) + self.identity_api.add_user_to_group(self.user2['id'], + self.group1['id']) + self.project1 = self.new_project_ref( + domain_id=self.domain['id']) + self.resource_api.create_project(self.project1['id'], self.project1) + self.role1 = self.new_role_ref() + self.role_api.create_role(self.role1['id'], self.role1) + self.role2 = self.new_role_ref() + self.role_api.create_role(self.role2['id'], self.role2) + + # Now add one of each of the four types of assignment + + gd_entity = _build_role_assignment_entity(domain_id=self.domain_id, + group_id=self.group1['id'], + role_id=self.role1['id']) + self.put(gd_entity['links']['assignment']) + + ud_entity = _build_role_assignment_entity(domain_id=self.domain_id, + user_id=self.user1['id'], + role_id=self.role2['id']) + self.put(ud_entity['links']['assignment']) + + gp_entity = _build_role_assignment_entity( + project_id=self.project1['id'], group_id=self.group1['id'], + role_id=self.role1['id']) + self.put(gp_entity['links']['assignment']) + + up_entity = _build_role_assignment_entity( + project_id=self.project1['id'], user_id=self.user1['id'], + role_id=self.role2['id']) + self.put(up_entity['links']['assignment']) + + # Now list by various filters to make sure we get back the right ones + + collection_url = ('/role_assignments?scope.project.id=%s' % + self.project1['id']) + r = self.get(collection_url) + self.assertValidRoleAssignmentListResponse(r, + expected_length=2, + resource_url=collection_url) + self.assertRoleAssignmentInListResponse(r, up_entity) + self.assertRoleAssignmentInListResponse(r, gp_entity) + + collection_url = ('/role_assignments?scope.domain.id=%s' % + self.domain['id']) + r = self.get(collection_url) + self.assertValidRoleAssignmentListResponse(r, + expected_length=2, + resource_url=collection_url) + self.assertRoleAssignmentInListResponse(r, ud_entity) + self.assertRoleAssignmentInListResponse(r, gd_entity) + + collection_url = '/role_assignments?user.id=%s' % self.user1['id'] + r = self.get(collection_url) + self.assertValidRoleAssignmentListResponse(r, + expected_length=2, + resource_url=collection_url) + self.assertRoleAssignmentInListResponse(r, up_entity) + self.assertRoleAssignmentInListResponse(r, ud_entity) + + collection_url = '/role_assignments?group.id=%s' % self.group1['id'] + r = self.get(collection_url) + self.assertValidRoleAssignmentListResponse(r, + expected_length=2, + resource_url=collection_url) + self.assertRoleAssignmentInListResponse(r, gd_entity) + self.assertRoleAssignmentInListResponse(r, gp_entity) + + collection_url = '/role_assignments?role.id=%s' % self.role1['id'] + r = self.get(collection_url) + self.assertValidRoleAssignmentListResponse(r, + expected_length=2, + resource_url=collection_url) + self.assertRoleAssignmentInListResponse(r, gd_entity) + self.assertRoleAssignmentInListResponse(r, gp_entity) + + # Let's try combining two filers together.... + + collection_url = ( + '/role_assignments?user.id=%(user_id)s' + '&scope.project.id=%(project_id)s' % { + 'user_id': self.user1['id'], + 'project_id': self.project1['id']}) + r = self.get(collection_url) + self.assertValidRoleAssignmentListResponse(r, + expected_length=1, + resource_url=collection_url) + self.assertRoleAssignmentInListResponse(r, up_entity) + + # Now for a harder one - filter for user with effective + # roles - this should return role assignment that were directly + # assigned as well as by virtue of group membership + + collection_url = ('/role_assignments?effective&user.id=%s' % + self.user1['id']) + r = self.get(collection_url) + self.assertValidRoleAssignmentListResponse(r, + expected_length=4, + resource_url=collection_url) + # Should have the two direct roles... + self.assertRoleAssignmentInListResponse(r, up_entity) + self.assertRoleAssignmentInListResponse(r, ud_entity) + # ...and the two via group membership... + gp1_link = _build_role_assignment_link(project_id=self.project1['id'], + group_id=self.group1['id'], + role_id=self.role1['id']) + gd1_link = _build_role_assignment_link(domain_id=self.domain_id, + group_id=self.group1['id'], + role_id=self.role1['id']) + + up1_entity = _build_role_assignment_entity( + link=gp1_link, project_id=self.project1['id'], + user_id=self.user1['id'], role_id=self.role1['id']) + ud1_entity = _build_role_assignment_entity( + link=gd1_link, domain_id=self.domain_id, user_id=self.user1['id'], + role_id=self.role1['id']) + self.assertRoleAssignmentInListResponse(r, up1_entity) + self.assertRoleAssignmentInListResponse(r, ud1_entity) + + # ...and for the grand-daddy of them all, simulate the request + # that would generate the list of effective roles in a project + # scoped token. + + collection_url = ( + '/role_assignments?effective&user.id=%(user_id)s' + '&scope.project.id=%(project_id)s' % { + 'user_id': self.user1['id'], + 'project_id': self.project1['id']}) + r = self.get(collection_url) + self.assertValidRoleAssignmentListResponse(r, + expected_length=2, + resource_url=collection_url) + # Should have one direct role and one from group membership... + self.assertRoleAssignmentInListResponse(r, up_entity) + self.assertRoleAssignmentInListResponse(r, up1_entity) + + +class RoleAssignmentBaseTestCase(test_v3.RestfulTestCase): + """Base class for testing /v3/role_assignments API behavior.""" + + MAX_HIERARCHY_BREADTH = 3 + MAX_HIERARCHY_DEPTH = CONF.max_project_tree_depth - 1 + + def load_sample_data(self): + """Creates sample data to be used on tests. + + Created data are i) a role and ii) a domain containing: a project + hierarchy and 3 users within 3 groups. + + """ + def create_project_hierarchy(parent_id, depth): + "Creates a random project hierarchy." + if depth == 0: + return + + breadth = random.randint(1, self.MAX_HIERARCHY_BREADTH) + + subprojects = [] + for i in range(breadth): + subprojects.append(self.new_project_ref( + domain_id=self.domain_id, parent_id=parent_id)) + self.assignment_api.create_project(subprojects[-1]['id'], + subprojects[-1]) + + new_parent = subprojects[random.randint(0, breadth - 1)] + create_project_hierarchy(new_parent['id'], depth - 1) + + super(RoleAssignmentBaseTestCase, self).load_sample_data() + + # Create a domain + self.domain = self.new_domain_ref() + self.domain_id = self.domain['id'] + self.assignment_api.create_domain(self.domain_id, self.domain) + + # Create a project hierarchy + self.project = self.new_project_ref(domain_id=self.domain_id) + self.project_id = self.project['id'] + self.assignment_api.create_project(self.project_id, self.project) + + # Create a random project hierarchy + create_project_hierarchy(self.project_id, + random.randint(1, self.MAX_HIERARCHY_DEPTH)) + + # Create 3 users + self.user_ids = [] + for i in range(3): + user = self.new_user_ref(domain_id=self.domain_id) + user = self.identity_api.create_user(user) + self.user_ids.append(user['id']) + + # Create 3 groups + self.group_ids = [] + for i in range(3): + group = self.new_group_ref(domain_id=self.domain_id) + group = self.identity_api.create_group(group) + self.group_ids.append(group['id']) + + # Put 2 members on each group + self.identity_api.add_user_to_group(user_id=self.user_ids[i], + group_id=group['id']) + self.identity_api.add_user_to_group(user_id=self.user_ids[i % 2], + group_id=group['id']) + + self.assignment_api.create_grant(user_id=self.user_id, + project_id=self.project_id, + role_id=self.role_id) + + # Create a role + self.role = self.new_role_ref() + self.role_id = self.role['id'] + self.assignment_api.create_role(self.role_id, self.role) + + # Set default user and group to be used on tests + self.default_user_id = self.user_ids[0] + self.default_group_id = self.group_ids[0] + + def get_role_assignments(self, expected_status=200, **filters): + """Returns the result from querying role assignment API + queried URL. + + Calls GET /v3/role_assignments? and returns its result, where + is the HTTP query parameters form of effective option plus + filters, if provided. Queried URL is returned as well. + + :returns: a tuple containing the list role assignments API response and + queried URL. + + """ + + query_url = self._get_role_assignments_query_url(**filters) + response = self.get(query_url, expected_status=expected_status) + + return (response, query_url) + + def _get_role_assignments_query_url(self, **filters): + """Returns non-effective role assignments query URL from given filters. + + :param filters: query parameters are created with the provided filters + on role assignments attributes. Valid filters are: + role_id, domain_id, project_id, group_id, user_id and + inherited_to_projects. + + :returns: role assignments query URL. + + """ + return _build_role_assignment_query_url(**filters) + + +class RoleAssignmentFailureTestCase(RoleAssignmentBaseTestCase): + """Class for testing invalid query params on /v3/role_assignments API. + + Querying domain and project, or user and group results in a HTTP 400, since + a role assignment must contain only a single pair of (actor, target). In + addition, since filtering on role assignments applies only to the final + result, effective mode cannot be combined with i) group or ii) domain and + inherited, because it would always result in an empty list. + + """ + + def test_get_role_assignments_by_domain_and_project(self): + self.get_role_assignments(domain_id=self.domain_id, + project_id=self.project_id, + expected_status=400) + + def test_get_role_assignments_by_user_and_group(self): + self.get_role_assignments(user_id=self.default_user_id, + group_id=self.default_group_id, + expected_status=400) + + def test_get_role_assignments_by_effective_and_inherited(self): + self.config_fixture.config(group='os_inherit', enabled=True) + + self.get_role_assignments(domain_id=self.domain_id, effective=True, + inherited_to_projects=True, + expected_status=400) + + def test_get_role_assignments_by_effective_and_group(self): + self.get_role_assignments(effective=True, + group_id=self.default_group_id, + expected_status=400) + + +class RoleAssignmentDirectTestCase(RoleAssignmentBaseTestCase): + """Class for testing direct assignments on /v3/role_assignments API. + + Direct assignments on a domain or project have effect on them directly, + instead of on their project hierarchy, i.e they are non-inherited. In + addition, group direct assignments are not expanded to group's users. + + Tests on this class make assertions on the representation and API filtering + of direct assignments. + + """ + + def _test_get_role_assignments(self, **filters): + """Generic filtering test method. + + According to the provided filters, this method: + - creates a new role assignment; + - asserts that list role assignments API reponds correctly; + - deletes the created role assignment. + + :param filters: filters to be considered when listing role assignments. + Valid filters are: role_id, domain_id, project_id, + group_id, user_id and inherited_to_projects. + + """ + + # Fills default assignment with provided filters + test_assignment = self._set_default_assignment_attributes(**filters) + + # Create new role assignment for this test + self.assignment_api.create_grant(**test_assignment) + + # Get expected role assignments + expected_assignments = self._list_expected_role_assignments( + **test_assignment) + + # Get role assignments from API + response, query_url = self.get_role_assignments(**test_assignment) + self.assertValidRoleAssignmentListResponse(response, + resource_url=query_url) + self.assertEqual(len(expected_assignments), + len(response.result.get('role_assignments'))) + + # Assert that expected role assignments were returned by the API call + for assignment in expected_assignments: + self.assertRoleAssignmentInListResponse(response, assignment) + + # Delete created role assignment + self.assignment_api.delete_grant(**test_assignment) + + def _set_default_assignment_attributes(self, **attribs): + """Inserts default values for missing attributes of role assignment. + + If no actor, target or role are provided, they will default to values + from sample data. + + :param attribs: info from a role assignment entity. Valid attributes + are: role_id, domain_id, project_id, group_id, user_id + and inherited_to_projects. + + """ + if not any(target in attribs + for target in ('domain_id', 'projects_id')): + attribs['project_id'] = self.project_id + + if not any(actor in attribs for actor in ('user_id', 'group_id')): + attribs['user_id'] = self.default_user_id + + if 'role_id' not in attribs: + attribs['role_id'] = self.role_id + + return attribs + + def _list_expected_role_assignments(self, **filters): + """Given the filters, it returns expected direct role assignments. + + :param filters: filters that will be considered when listing role + assignments. Valid filters are: role_id, domain_id, + project_id, group_id, user_id and + inherited_to_projects. + + :returns: the list of the expected role assignments. + + """ + return [_build_role_assignment_entity(**filters)] + + # Test cases below call the generic test method, providing different filter + # combinations. Filters are provided as specified in the method name, after + # 'by'. For example, test_get_role_assignments_by_project_user_and_role + # calls the generic test method with project_id, user_id and role_id. + + def test_get_role_assignments_by_domain(self, **filters): + self._test_get_role_assignments(domain_id=self.domain_id, **filters) + + def test_get_role_assignments_by_project(self, **filters): + self._test_get_role_assignments(project_id=self.project_id, **filters) + + def test_get_role_assignments_by_user(self, **filters): + self._test_get_role_assignments(user_id=self.default_user_id, + **filters) + + def test_get_role_assignments_by_group(self, **filters): + self._test_get_role_assignments(group_id=self.default_group_id, + **filters) + + def test_get_role_assignments_by_role(self, **filters): + self._test_get_role_assignments(role_id=self.role_id, **filters) + + def test_get_role_assignments_by_domain_and_user(self, **filters): + self.test_get_role_assignments_by_domain(user_id=self.default_user_id, + **filters) + + def test_get_role_assignments_by_domain_and_group(self, **filters): + self.test_get_role_assignments_by_domain( + group_id=self.default_group_id, **filters) + + def test_get_role_assignments_by_project_and_user(self, **filters): + self.test_get_role_assignments_by_project(user_id=self.default_user_id, + **filters) + + def test_get_role_assignments_by_project_and_group(self, **filters): + self.test_get_role_assignments_by_project( + group_id=self.default_group_id, **filters) + + def test_get_role_assignments_by_domain_user_and_role(self, **filters): + self.test_get_role_assignments_by_domain_and_user(role_id=self.role_id, + **filters) + + def test_get_role_assignments_by_domain_group_and_role(self, **filters): + self.test_get_role_assignments_by_domain_and_group( + role_id=self.role_id, **filters) + + def test_get_role_assignments_by_project_user_and_role(self, **filters): + self.test_get_role_assignments_by_project_and_user( + role_id=self.role_id, **filters) + + def test_get_role_assignments_by_project_group_and_role(self, **filters): + self.test_get_role_assignments_by_project_and_group( + role_id=self.role_id, **filters) + + +class RoleAssignmentInheritedTestCase(RoleAssignmentDirectTestCase): + """Class for testing inherited assignments on /v3/role_assignments API. + + Inherited assignments on a domain or project have no effect on them + directly, but on the projects under them instead. + + Tests on this class do not make assertions on the effect of inherited + assignments, but in their representation and API filtering. + + """ + + def config_overrides(self): + super(RoleAssignmentBaseTestCase, self).config_overrides() + self.config_fixture.config(group='os_inherit', enabled=True) + + def _test_get_role_assignments(self, **filters): + """Adds inherited_to_project filter to expected entity in tests.""" + super(RoleAssignmentInheritedTestCase, + self)._test_get_role_assignments(inherited_to_projects=True, + **filters) + + +class RoleAssignmentEffectiveTestCase(RoleAssignmentInheritedTestCase): + """Class for testing inheritance effects on /v3/role_assignments API. + + Inherited assignments on a domain or project have no effect on them + directly, but on the projects under them instead. + + Tests on this class make assertions on the effect of inherited assignments + and API filtering. + + """ + + def _get_role_assignments_query_url(self, **filters): + """Returns effective role assignments query URL from given filters. + + For test methods in this class, effetive will always be true. As in + effective mode, inherited_to_projects, group_id, domain_id and + project_id will always be desconsidered from provided filters. + + :param filters: query parameters are created with the provided filters. + Valid filters are: role_id, domain_id, project_id, + group_id, user_id and inherited_to_projects. + + :returns: role assignments query URL. + + """ + query_filters = filters.copy() + query_filters.pop('inherited_to_projects') + + query_filters.pop('group_id', None) + query_filters.pop('domain_id', None) + query_filters.pop('project_id', None) + + return _build_role_assignment_query_url(effective=True, + **query_filters) + + def _list_expected_role_assignments(self, **filters): + """Given the filters, it returns expected direct role assignments. + + :param filters: filters that will be considered when listing role + assignments. Valid filters are: role_id, domain_id, + project_id, group_id, user_id and + inherited_to_projects. + + :returns: the list of the expected role assignments. + + """ + # Get assignment link, to be put on 'links': {'assignment': link} + assignment_link = _build_role_assignment_link(**filters) + + # Expand group membership + user_ids = [None] + if filters.get('group_id'): + user_ids = [user['id'] for user in + self.identity_api.list_users_in_group( + filters['group_id'])] + else: + user_ids = [self.default_user_id] + + # Expand role inheritance + project_ids = [None] + if filters.get('domain_id'): + project_ids = [project['id'] for project in + self.assignment_api.list_projects_in_domain( + filters.pop('domain_id'))] + else: + project_ids = [project['id'] for project in + self.assignment_api.list_projects_in_subtree( + self.project_id)] + + # Compute expected role assignments + assignments = [] + for project_id in project_ids: + filters['project_id'] = project_id + for user_id in user_ids: + filters['user_id'] = user_id + assignments.append(_build_role_assignment_entity( + link=assignment_link, **filters)) + + return assignments + + +class AssignmentInheritanceTestCase(test_v3.RestfulTestCase): + """Test inheritance crud and its effects.""" + + def config_overrides(self): + super(AssignmentInheritanceTestCase, self).config_overrides() + self.config_fixture.config(group='os_inherit', enabled=True) + + def test_get_token_from_inherited_user_domain_role_grants(self): + # Create a new user to ensure that no grant is loaded from sample data + user = self.new_user_ref(domain_id=self.domain_id) + password = user['password'] + user = self.identity_api.create_user(user) + user['password'] = password + + # Define domain and project authentication data + domain_auth_data = self.build_authentication_request( + user_id=user['id'], + password=user['password'], + domain_id=self.domain_id) + project_auth_data = self.build_authentication_request( + user_id=user['id'], + password=user['password'], + project_id=self.project_id) + + # Check the user cannot get a domain nor a project token + self.v3_authenticate_token(domain_auth_data, expected_status=401) + self.v3_authenticate_token(project_auth_data, expected_status=401) + + # Grant non-inherited role for user on domain + non_inher_ud_link = _build_role_assignment_link( + domain_id=self.domain_id, user_id=user['id'], role_id=self.role_id) + self.put(non_inher_ud_link) + + # Check the user can get only a domain token + self.v3_authenticate_token(domain_auth_data) + self.v3_authenticate_token(project_auth_data, expected_status=401) + + # Create inherited role + inherited_role = {'id': uuid.uuid4().hex, 'name': 'inherited'} + self.role_api.create_role(inherited_role['id'], inherited_role) + + # Grant inherited role for user on domain + inher_ud_link = _build_role_assignment_link( + domain_id=self.domain_id, user_id=user['id'], + role_id=inherited_role['id'], inherited_to_projects=True) + self.put(inher_ud_link) + + # Check the user can get both a domain and a project token + self.v3_authenticate_token(domain_auth_data) + self.v3_authenticate_token(project_auth_data) + + # Delete inherited grant + self.delete(inher_ud_link) + + # Check the user can only get a domain token + self.v3_authenticate_token(domain_auth_data) + self.v3_authenticate_token(project_auth_data, expected_status=401) + + # Delete non-inherited grant + self.delete(non_inher_ud_link) + + # Check the user cannot get a domain token anymore + self.v3_authenticate_token(domain_auth_data, expected_status=401) + + def test_get_token_from_inherited_group_domain_role_grants(self): + # Create a new group and put a new user in it to + # ensure that no grant is loaded from sample data + user = self.new_user_ref(domain_id=self.domain_id) + password = user['password'] + user = self.identity_api.create_user(user) + user['password'] = password + + group = self.new_group_ref(domain_id=self.domain['id']) + group = self.identity_api.create_group(group) + self.identity_api.add_user_to_group(user['id'], group['id']) + + # Define domain and project authentication data + domain_auth_data = self.build_authentication_request( + user_id=user['id'], + password=user['password'], + domain_id=self.domain_id) + project_auth_data = self.build_authentication_request( + user_id=user['id'], + password=user['password'], + project_id=self.project_id) + + # Check the user cannot get a domain nor a project token + self.v3_authenticate_token(domain_auth_data, expected_status=401) + self.v3_authenticate_token(project_auth_data, expected_status=401) + + # Grant non-inherited role for user on domain + non_inher_gd_link = _build_role_assignment_link( + domain_id=self.domain_id, user_id=user['id'], role_id=self.role_id) + self.put(non_inher_gd_link) + + # Check the user can get only a domain token + self.v3_authenticate_token(domain_auth_data) + self.v3_authenticate_token(project_auth_data, expected_status=401) + + # Create inherited role + inherited_role = {'id': uuid.uuid4().hex, 'name': 'inherited'} + self.role_api.create_role(inherited_role['id'], inherited_role) + + # Grant inherited role for user on domain + inher_gd_link = _build_role_assignment_link( + domain_id=self.domain_id, user_id=user['id'], + role_id=inherited_role['id'], inherited_to_projects=True) + self.put(inher_gd_link) + + # Check the user can get both a domain and a project token + self.v3_authenticate_token(domain_auth_data) + self.v3_authenticate_token(project_auth_data) + + # Delete inherited grant + self.delete(inher_gd_link) + + # Check the user can only get a domain token + self.v3_authenticate_token(domain_auth_data) + self.v3_authenticate_token(project_auth_data, expected_status=401) + + # Delete non-inherited grant + self.delete(non_inher_gd_link) + + # Check the user cannot get a domain token anymore + self.v3_authenticate_token(domain_auth_data, expected_status=401) + + def test_crud_user_inherited_domain_role_grants(self): + role_list = [] + for _ in range(2): + role = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.role_api.create_role(role['id'], role) + role_list.append(role) + + # Create a non-inherited role as a spoiler + self.assignment_api.create_grant( + role_list[1]['id'], user_id=self.user['id'], + domain_id=self.domain_id) + + base_collection_url = ( + '/OS-INHERIT/domains/%(domain_id)s/users/%(user_id)s/roles' % { + 'domain_id': self.domain_id, + 'user_id': self.user['id']}) + member_url = '%(collection_url)s/%(role_id)s/inherited_to_projects' % { + 'collection_url': base_collection_url, + 'role_id': role_list[0]['id']} + collection_url = base_collection_url + '/inherited_to_projects' + + self.put(member_url) + + # Check we can read it back + self.head(member_url) + r = self.get(collection_url) + self.assertValidRoleListResponse(r, ref=role_list[0], + resource_url=collection_url) + + # Now delete and check its gone + self.delete(member_url) + r = self.get(collection_url) + self.assertValidRoleListResponse(r, expected_length=0, + resource_url=collection_url) + + def test_list_role_assignments_for_inherited_domain_grants(self): + """Call ``GET /role_assignments with inherited domain grants``. + + Test Plan: + + - Create 4 roles + - Create a domain with a user and two projects + - Assign two direct roles to project1 + - Assign a spoiler role to project2 + - Issue the URL to add inherited role to the domain + - Issue the URL to check it is indeed on the domain + - Issue the URL to check effective roles on project1 - this + should return 3 roles. + + """ + role_list = [] + for _ in range(4): + role = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.role_api.create_role(role['id'], role) + role_list.append(role) + + domain = self.new_domain_ref() + self.resource_api.create_domain(domain['id'], domain) + user1 = self.new_user_ref( + domain_id=domain['id']) + password = user1['password'] + user1 = self.identity_api.create_user(user1) + user1['password'] = password + project1 = self.new_project_ref( + domain_id=domain['id']) + self.resource_api.create_project(project1['id'], project1) + project2 = self.new_project_ref( + domain_id=domain['id']) + self.resource_api.create_project(project2['id'], project2) + # Add some roles to the project + self.assignment_api.add_role_to_user_and_project( + user1['id'], project1['id'], role_list[0]['id']) + self.assignment_api.add_role_to_user_and_project( + user1['id'], project1['id'], role_list[1]['id']) + # ..and one on a different project as a spoiler + self.assignment_api.add_role_to_user_and_project( + user1['id'], project2['id'], role_list[2]['id']) + + # Now create our inherited role on the domain + base_collection_url = ( + '/OS-INHERIT/domains/%(domain_id)s/users/%(user_id)s/roles' % { + 'domain_id': domain['id'], + 'user_id': user1['id']}) + member_url = '%(collection_url)s/%(role_id)s/inherited_to_projects' % { + 'collection_url': base_collection_url, + 'role_id': role_list[3]['id']} + collection_url = base_collection_url + '/inherited_to_projects' + + self.put(member_url) + self.head(member_url) + r = self.get(collection_url) + self.assertValidRoleListResponse(r, ref=role_list[3], + resource_url=collection_url) + + # Now use the list domain role assignments api to check if this + # is included + collection_url = ( + '/role_assignments?user.id=%(user_id)s' + '&scope.domain.id=%(domain_id)s' % { + 'user_id': user1['id'], + 'domain_id': domain['id']}) + r = self.get(collection_url) + self.assertValidRoleAssignmentListResponse(r, + expected_length=1, + resource_url=collection_url) + ud_entity = _build_role_assignment_entity( + domain_id=domain['id'], user_id=user1['id'], + role_id=role_list[3]['id'], inherited_to_projects=True) + self.assertRoleAssignmentInListResponse(r, ud_entity) + + # Now ask for effective list role assignments - the role should + # turn into a project role, along with the two direct roles that are + # on the project + collection_url = ( + '/role_assignments?effective&user.id=%(user_id)s' + '&scope.project.id=%(project_id)s' % { + 'user_id': user1['id'], + 'project_id': project1['id']}) + r = self.get(collection_url) + self.assertValidRoleAssignmentListResponse(r, + expected_length=3, + resource_url=collection_url) + # An effective role for an inherited role will be a project + # entity, with a domain link to the inherited assignment + ud_url = _build_role_assignment_link( + domain_id=domain['id'], user_id=user1['id'], + role_id=role_list[3]['id'], inherited_to_projects=True) + up_entity = _build_role_assignment_entity(link=ud_url, + project_id=project1['id'], + user_id=user1['id'], + role_id=role_list[3]['id'], + inherited_to_projects=True) + self.assertRoleAssignmentInListResponse(r, up_entity) + + def test_list_role_assignments_for_disabled_inheritance_extension(self): + """Call ``GET /role_assignments with inherited domain grants``. + + Test Plan: + + - Issue the URL to add inherited role to the domain + - Issue the URL to check effective roles on project include the + inherited role + - Disable the extension + - Re-check the effective roles, proving the inherited role no longer + shows up. + + """ + + role_list = [] + for _ in range(4): + role = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.role_api.create_role(role['id'], role) + role_list.append(role) + + domain = self.new_domain_ref() + self.resource_api.create_domain(domain['id'], domain) + user1 = self.new_user_ref( + domain_id=domain['id']) + password = user1['password'] + user1 = self.identity_api.create_user(user1) + user1['password'] = password + project1 = self.new_project_ref( + domain_id=domain['id']) + self.resource_api.create_project(project1['id'], project1) + project2 = self.new_project_ref( + domain_id=domain['id']) + self.resource_api.create_project(project2['id'], project2) + # Add some roles to the project + self.assignment_api.add_role_to_user_and_project( + user1['id'], project1['id'], role_list[0]['id']) + self.assignment_api.add_role_to_user_and_project( + user1['id'], project1['id'], role_list[1]['id']) + # ..and one on a different project as a spoiler + self.assignment_api.add_role_to_user_and_project( + user1['id'], project2['id'], role_list[2]['id']) + + # Now create our inherited role on the domain + base_collection_url = ( + '/OS-INHERIT/domains/%(domain_id)s/users/%(user_id)s/roles' % { + 'domain_id': domain['id'], + 'user_id': user1['id']}) + member_url = '%(collection_url)s/%(role_id)s/inherited_to_projects' % { + 'collection_url': base_collection_url, + 'role_id': role_list[3]['id']} + collection_url = base_collection_url + '/inherited_to_projects' + + self.put(member_url) + self.head(member_url) + r = self.get(collection_url) + self.assertValidRoleListResponse(r, ref=role_list[3], + resource_url=collection_url) + + # Get effective list role assignments - the role should + # turn into a project role, along with the two direct roles that are + # on the project + collection_url = ( + '/role_assignments?effective&user.id=%(user_id)s' + '&scope.project.id=%(project_id)s' % { + 'user_id': user1['id'], + 'project_id': project1['id']}) + r = self.get(collection_url) + self.assertValidRoleAssignmentListResponse(r, + expected_length=3, + resource_url=collection_url) + + ud_url = _build_role_assignment_link( + domain_id=domain['id'], user_id=user1['id'], + role_id=role_list[3]['id'], inherited_to_projects=True) + up_entity = _build_role_assignment_entity(link=ud_url, + project_id=project1['id'], + user_id=user1['id'], + role_id=role_list[3]['id'], + inherited_to_projects=True) + + self.assertRoleAssignmentInListResponse(r, up_entity) + + # Disable the extension and re-check the list, the role inherited + # from the project should no longer show up + self.config_fixture.config(group='os_inherit', enabled=False) + r = self.get(collection_url) + self.assertValidRoleAssignmentListResponse(r, + expected_length=2, + resource_url=collection_url) + + self.assertRoleAssignmentNotInListResponse(r, up_entity) + + def test_list_role_assignments_for_inherited_group_domain_grants(self): + """Call ``GET /role_assignments with inherited group domain grants``. + + Test Plan: + + - Create 4 roles + - Create a domain with a user and two projects + - Assign two direct roles to project1 + - Assign a spoiler role to project2 + - Issue the URL to add inherited role to the domain + - Issue the URL to check it is indeed on the domain + - Issue the URL to check effective roles on project1 - this + should return 3 roles. + + """ + role_list = [] + for _ in range(4): + role = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.role_api.create_role(role['id'], role) + role_list.append(role) + + domain = self.new_domain_ref() + self.resource_api.create_domain(domain['id'], domain) + user1 = self.new_user_ref( + domain_id=domain['id']) + password = user1['password'] + user1 = self.identity_api.create_user(user1) + user1['password'] = password + user2 = self.new_user_ref( + domain_id=domain['id']) + password = user2['password'] + user2 = self.identity_api.create_user(user2) + user2['password'] = password + group1 = self.new_group_ref( + domain_id=domain['id']) + group1 = self.identity_api.create_group(group1) + self.identity_api.add_user_to_group(user1['id'], + group1['id']) + self.identity_api.add_user_to_group(user2['id'], + group1['id']) + project1 = self.new_project_ref( + domain_id=domain['id']) + self.resource_api.create_project(project1['id'], project1) + project2 = self.new_project_ref( + domain_id=domain['id']) + self.resource_api.create_project(project2['id'], project2) + # Add some roles to the project + self.assignment_api.add_role_to_user_and_project( + user1['id'], project1['id'], role_list[0]['id']) + self.assignment_api.add_role_to_user_and_project( + user1['id'], project1['id'], role_list[1]['id']) + # ..and one on a different project as a spoiler + self.assignment_api.add_role_to_user_and_project( + user1['id'], project2['id'], role_list[2]['id']) + + # Now create our inherited role on the domain + base_collection_url = ( + '/OS-INHERIT/domains/%(domain_id)s/groups/%(group_id)s/roles' % { + 'domain_id': domain['id'], + 'group_id': group1['id']}) + member_url = '%(collection_url)s/%(role_id)s/inherited_to_projects' % { + 'collection_url': base_collection_url, + 'role_id': role_list[3]['id']} + collection_url = base_collection_url + '/inherited_to_projects' + + self.put(member_url) + self.head(member_url) + r = self.get(collection_url) + self.assertValidRoleListResponse(r, ref=role_list[3], + resource_url=collection_url) + + # Now use the list domain role assignments api to check if this + # is included + collection_url = ( + '/role_assignments?group.id=%(group_id)s' + '&scope.domain.id=%(domain_id)s' % { + 'group_id': group1['id'], + 'domain_id': domain['id']}) + r = self.get(collection_url) + self.assertValidRoleAssignmentListResponse(r, + expected_length=1, + resource_url=collection_url) + gd_entity = _build_role_assignment_entity( + domain_id=domain['id'], group_id=group1['id'], + role_id=role_list[3]['id'], inherited_to_projects=True) + self.assertRoleAssignmentInListResponse(r, gd_entity) + + # Now ask for effective list role assignments - the role should + # turn into a user project role, along with the two direct roles + # that are on the project + collection_url = ( + '/role_assignments?effective&user.id=%(user_id)s' + '&scope.project.id=%(project_id)s' % { + 'user_id': user1['id'], + 'project_id': project1['id']}) + r = self.get(collection_url) + self.assertValidRoleAssignmentListResponse(r, + expected_length=3, + resource_url=collection_url) + # An effective role for an inherited role will be a project + # entity, with a domain link to the inherited assignment + up_entity = _build_role_assignment_entity( + link=gd_entity['links']['assignment'], project_id=project1['id'], + user_id=user1['id'], role_id=role_list[3]['id'], + inherited_to_projects=True) + self.assertRoleAssignmentInListResponse(r, up_entity) + + def test_filtered_role_assignments_for_inherited_grants(self): + """Call ``GET /role_assignments?scope.OS-INHERIT:inherited_to``. + + Test Plan: + + - Create 5 roles + - Create a domain with a user, group and two projects + - Assign three direct spoiler roles to projects + - Issue the URL to add an inherited user role to the domain + - Issue the URL to add an inherited group role to the domain + - Issue the URL to filter by inherited roles - this should + return just the 2 inherited roles. + + """ + role_list = [] + for _ in range(5): + role = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.role_api.create_role(role['id'], role) + role_list.append(role) + + domain = self.new_domain_ref() + self.resource_api.create_domain(domain['id'], domain) + user1 = self.new_user_ref( + domain_id=domain['id']) + password = user1['password'] + user1 = self.identity_api.create_user(user1) + user1['password'] = password + group1 = self.new_group_ref( + domain_id=domain['id']) + group1 = self.identity_api.create_group(group1) + project1 = self.new_project_ref( + domain_id=domain['id']) + self.resource_api.create_project(project1['id'], project1) + project2 = self.new_project_ref( + domain_id=domain['id']) + self.resource_api.create_project(project2['id'], project2) + # Add some spoiler roles to the projects + self.assignment_api.add_role_to_user_and_project( + user1['id'], project1['id'], role_list[0]['id']) + self.assignment_api.add_role_to_user_and_project( + user1['id'], project2['id'], role_list[1]['id']) + # Create a non-inherited role as a spoiler + self.assignment_api.create_grant( + role_list[2]['id'], user_id=user1['id'], domain_id=domain['id']) + + # Now create two inherited roles on the domain, one for a user + # and one for a domain + base_collection_url = ( + '/OS-INHERIT/domains/%(domain_id)s/users/%(user_id)s/roles' % { + 'domain_id': domain['id'], + 'user_id': user1['id']}) + member_url = '%(collection_url)s/%(role_id)s/inherited_to_projects' % { + 'collection_url': base_collection_url, + 'role_id': role_list[3]['id']} + collection_url = base_collection_url + '/inherited_to_projects' + + self.put(member_url) + self.head(member_url) + r = self.get(collection_url) + self.assertValidRoleListResponse(r, ref=role_list[3], + resource_url=collection_url) + + base_collection_url = ( + '/OS-INHERIT/domains/%(domain_id)s/groups/%(group_id)s/roles' % { + 'domain_id': domain['id'], + 'group_id': group1['id']}) + member_url = '%(collection_url)s/%(role_id)s/inherited_to_projects' % { + 'collection_url': base_collection_url, + 'role_id': role_list[4]['id']} + collection_url = base_collection_url + '/inherited_to_projects' + + self.put(member_url) + self.head(member_url) + r = self.get(collection_url) + self.assertValidRoleListResponse(r, ref=role_list[4], + resource_url=collection_url) + + # Now use the list role assignments api to get a list of inherited + # roles on the domain - should get back the two roles + collection_url = ( + '/role_assignments?scope.OS-INHERIT:inherited_to=projects') + r = self.get(collection_url) + self.assertValidRoleAssignmentListResponse(r, + expected_length=2, + resource_url=collection_url) + ud_entity = _build_role_assignment_entity( + domain_id=domain['id'], user_id=user1['id'], + role_id=role_list[3]['id'], inherited_to_projects=True) + gd_entity = _build_role_assignment_entity( + domain_id=domain['id'], group_id=group1['id'], + role_id=role_list[4]['id'], inherited_to_projects=True) + self.assertRoleAssignmentInListResponse(r, ud_entity) + self.assertRoleAssignmentInListResponse(r, gd_entity) + + def _setup_hierarchical_projects_scenario(self): + """Creates basic hierarchical projects scenario. + + This basic scenario contains a root with one leaf project and + two roles with the following names: non-inherited and inherited. + + """ + # Create project hierarchy + root = self.new_project_ref(domain_id=self.domain['id']) + leaf = self.new_project_ref(domain_id=self.domain['id'], + parent_id=root['id']) + + self.resource_api.create_project(root['id'], root) + self.resource_api.create_project(leaf['id'], leaf) + + # Create 'non-inherited' and 'inherited' roles + non_inherited_role = {'id': uuid.uuid4().hex, 'name': 'non-inherited'} + self.role_api.create_role(non_inherited_role['id'], non_inherited_role) + inherited_role = {'id': uuid.uuid4().hex, 'name': 'inherited'} + self.role_api.create_role(inherited_role['id'], inherited_role) + + return (root['id'], leaf['id'], + non_inherited_role['id'], inherited_role['id']) + + def test_get_token_from_inherited_user_project_role_grants(self): + # Create default scenario + root_id, leaf_id, non_inherited_role_id, inherited_role_id = ( + self._setup_hierarchical_projects_scenario()) + + # Define root and leaf projects authentication data + root_project_auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + project_id=root_id) + leaf_project_auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + project_id=leaf_id) + + # Check the user cannot get a token on root nor leaf project + self.v3_authenticate_token(root_project_auth_data, expected_status=401) + self.v3_authenticate_token(leaf_project_auth_data, expected_status=401) + + # Grant non-inherited role for user on leaf project + non_inher_up_link = _build_role_assignment_link( + project_id=leaf_id, user_id=self.user['id'], + role_id=non_inherited_role_id) + self.put(non_inher_up_link) + + # Check the user can only get a token on leaf project + self.v3_authenticate_token(root_project_auth_data, expected_status=401) + self.v3_authenticate_token(leaf_project_auth_data) + + # Grant inherited role for user on root project + inher_up_link = _build_role_assignment_link( + project_id=root_id, user_id=self.user['id'], + role_id=inherited_role_id, inherited_to_projects=True) + self.put(inher_up_link) + + # Check the user still can get a token only on leaf project + self.v3_authenticate_token(root_project_auth_data, expected_status=401) + self.v3_authenticate_token(leaf_project_auth_data) + + # Delete non-inherited grant + self.delete(non_inher_up_link) + + # Check the inherited role still applies for leaf project + self.v3_authenticate_token(root_project_auth_data, expected_status=401) + self.v3_authenticate_token(leaf_project_auth_data) + + # Delete inherited grant + self.delete(inher_up_link) + + # Check the user cannot get a token on leaf project anymore + self.v3_authenticate_token(leaf_project_auth_data, expected_status=401) + + def test_get_token_from_inherited_group_project_role_grants(self): + # Create default scenario + root_id, leaf_id, non_inherited_role_id, inherited_role_id = ( + self._setup_hierarchical_projects_scenario()) + + # Create group and add user to it + group = self.new_group_ref(domain_id=self.domain['id']) + group = self.identity_api.create_group(group) + self.identity_api.add_user_to_group(self.user['id'], group['id']) + + # Define root and leaf projects authentication data + root_project_auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + project_id=root_id) + leaf_project_auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + project_id=leaf_id) + + # Check the user cannot get a token on root nor leaf project + self.v3_authenticate_token(root_project_auth_data, expected_status=401) + self.v3_authenticate_token(leaf_project_auth_data, expected_status=401) + + # Grant non-inherited role for group on leaf project + non_inher_gp_link = _build_role_assignment_link( + project_id=leaf_id, group_id=group['id'], + role_id=non_inherited_role_id) + self.put(non_inher_gp_link) + + # Check the user can only get a token on leaf project + self.v3_authenticate_token(root_project_auth_data, expected_status=401) + self.v3_authenticate_token(leaf_project_auth_data) + + # Grant inherited role for group on root project + inher_gp_link = _build_role_assignment_link( + project_id=root_id, group_id=group['id'], + role_id=inherited_role_id, inherited_to_projects=True) + self.put(inher_gp_link) + + # Check the user still can get a token only on leaf project + self.v3_authenticate_token(root_project_auth_data, expected_status=401) + self.v3_authenticate_token(leaf_project_auth_data) + + # Delete no-inherited grant + self.delete(non_inher_gp_link) + + # Check the inherited role still applies for leaf project + self.v3_authenticate_token(leaf_project_auth_data) + + # Delete inherited grant + self.delete(inher_gp_link) + + # Check the user cannot get a token on leaf project anymore + self.v3_authenticate_token(leaf_project_auth_data, expected_status=401) + + def test_get_role_assignments_for_project_hierarchy(self): + """Call ``GET /role_assignments``. + + Test Plan: + + - Create 2 roles + - Create a hierarchy of projects with one root and one leaf project + - Issue the URL to add a non-inherited user role to the root project + - Issue the URL to add an inherited user role to the root project + - Issue the URL to get all role assignments - this should return just + 2 roles (non-inherited and inherited) in the root project. + + """ + # Create default scenario + root_id, leaf_id, non_inherited_role_id, inherited_role_id = ( + self._setup_hierarchical_projects_scenario()) + + # Grant non-inherited role + non_inher_up_entity = _build_role_assignment_entity( + project_id=root_id, user_id=self.user['id'], + role_id=non_inherited_role_id) + self.put(non_inher_up_entity['links']['assignment']) + + # Grant inherited role + inher_up_entity = _build_role_assignment_entity( + project_id=root_id, user_id=self.user['id'], + role_id=inherited_role_id, inherited_to_projects=True) + self.put(inher_up_entity['links']['assignment']) + + # Get role assignments + collection_url = '/role_assignments' + r = self.get(collection_url) + self.assertValidRoleAssignmentListResponse(r, + resource_url=collection_url) + + # Assert that the user has non-inherited role on root project + self.assertRoleAssignmentInListResponse(r, non_inher_up_entity) + + # Assert that the user has inherited role on root project + self.assertRoleAssignmentInListResponse(r, inher_up_entity) + + # Assert that the user does not have non-inherited role on leaf project + non_inher_up_entity = _build_role_assignment_entity( + project_id=leaf_id, user_id=self.user['id'], + role_id=non_inherited_role_id) + self.assertRoleAssignmentNotInListResponse(r, non_inher_up_entity) + + # Assert that the user does not have inherited role on leaf project + inher_up_entity['scope']['project']['id'] = leaf_id + self.assertRoleAssignmentNotInListResponse(r, inher_up_entity) + + def test_get_effective_role_assignments_for_project_hierarchy(self): + """Call ``GET /role_assignments?effective``. + + Test Plan: + + - Create 2 roles + - Create a hierarchy of projects with one root and one leaf project + - Issue the URL to add a non-inherited user role to the root project + - Issue the URL to add an inherited user role to the root project + - Issue the URL to get effective role assignments - this should return + 1 role (non-inherited) on the root project and 1 role (inherited) on + the leaf project. + + """ + # Create default scenario + root_id, leaf_id, non_inherited_role_id, inherited_role_id = ( + self._setup_hierarchical_projects_scenario()) + + # Grant non-inherited role + non_inher_up_entity = _build_role_assignment_entity( + project_id=root_id, user_id=self.user['id'], + role_id=non_inherited_role_id) + self.put(non_inher_up_entity['links']['assignment']) + + # Grant inherited role + inher_up_entity = _build_role_assignment_entity( + project_id=root_id, user_id=self.user['id'], + role_id=inherited_role_id, inherited_to_projects=True) + self.put(inher_up_entity['links']['assignment']) + + # Get effective role assignments + collection_url = '/role_assignments?effective' + r = self.get(collection_url) + self.assertValidRoleAssignmentListResponse(r, + resource_url=collection_url) + + # Assert that the user has non-inherited role on root project + self.assertRoleAssignmentInListResponse(r, non_inher_up_entity) + + # Assert that the user does not have inherited role on root project + self.assertRoleAssignmentNotInListResponse(r, inher_up_entity) + + # Assert that the user does not have non-inherited role on leaf project + non_inher_up_entity = _build_role_assignment_entity( + project_id=leaf_id, user_id=self.user['id'], + role_id=non_inherited_role_id) + self.assertRoleAssignmentNotInListResponse(r, non_inher_up_entity) + + # Assert that the user has inherited role on leaf project + inher_up_entity['scope']['project']['id'] = leaf_id + self.assertRoleAssignmentInListResponse(r, inher_up_entity) + + def test_get_inherited_role_assignments_for_project_hierarchy(self): + """Call ``GET /role_assignments?scope.OS-INHERIT:inherited_to``. + + Test Plan: + + - Create 2 roles + - Create a hierarchy of projects with one root and one leaf project + - Issue the URL to add a non-inherited user role to the root project + - Issue the URL to add an inherited user role to the root project + - Issue the URL to filter inherited to projects role assignments - this + should return 1 role (inherited) on the root project. + + """ + # Create default scenario + root_id, leaf_id, non_inherited_role_id, inherited_role_id = ( + self._setup_hierarchical_projects_scenario()) + + # Grant non-inherited role + non_inher_up_entity = _build_role_assignment_entity( + project_id=root_id, user_id=self.user['id'], + role_id=non_inherited_role_id) + self.put(non_inher_up_entity['links']['assignment']) + + # Grant inherited role + inher_up_entity = _build_role_assignment_entity( + project_id=root_id, user_id=self.user['id'], + role_id=inherited_role_id, inherited_to_projects=True) + self.put(inher_up_entity['links']['assignment']) + + # Get inherited role assignments + collection_url = ('/role_assignments' + '?scope.OS-INHERIT:inherited_to=projects') + r = self.get(collection_url) + self.assertValidRoleAssignmentListResponse(r, + resource_url=collection_url) + + # Assert that the user does not have non-inherited role on root project + self.assertRoleAssignmentNotInListResponse(r, non_inher_up_entity) + + # Assert that the user has inherited role on root project + self.assertRoleAssignmentInListResponse(r, inher_up_entity) + + # Assert that the user does not have non-inherited role on leaf project + non_inher_up_entity = _build_role_assignment_entity( + project_id=leaf_id, user_id=self.user['id'], + role_id=non_inherited_role_id) + self.assertRoleAssignmentNotInListResponse(r, non_inher_up_entity) + + # Assert that the user does not have inherited role on leaf project + inher_up_entity['scope']['project']['id'] = leaf_id + self.assertRoleAssignmentNotInListResponse(r, inher_up_entity) + + +class AssignmentInheritanceDisabledTestCase(test_v3.RestfulTestCase): + """Test inheritance crud and its effects.""" + + def config_overrides(self): + super(AssignmentInheritanceDisabledTestCase, self).config_overrides() + self.config_fixture.config(group='os_inherit', enabled=False) + + def test_crud_inherited_role_grants_failed_if_disabled(self): + role = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.role_api.create_role(role['id'], role) + + base_collection_url = ( + '/OS-INHERIT/domains/%(domain_id)s/users/%(user_id)s/roles' % { + 'domain_id': self.domain_id, + 'user_id': self.user['id']}) + member_url = '%(collection_url)s/%(role_id)s/inherited_to_projects' % { + 'collection_url': base_collection_url, + 'role_id': role['id']} + collection_url = base_collection_url + '/inherited_to_projects' + + self.put(member_url, expected_status=404) + self.head(member_url, expected_status=404) + self.get(collection_url, expected_status=404) + self.delete(member_url, expected_status=404) + + +class AssignmentV3toV2MethodsTestCase(tests.TestCase): + """Test domain V3 to V2 conversion methods.""" + + def test_v2controller_filter_domain_id(self): + # V2.0 is not domain aware, ensure domain_id is popped off the ref. + other_data = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref = {'domain_id': domain_id, + 'other_data': other_data} + + ref_no_domain = {'other_data': other_data} + expected_ref = ref_no_domain.copy() + + updated_ref = controller.V2Controller.filter_domain_id(ref) + self.assertIs(ref, updated_ref) + self.assertDictEqual(ref, expected_ref) + # Make sure we don't error/muck up data if domain_id isn't present + updated_ref = controller.V2Controller.filter_domain_id(ref_no_domain) + self.assertIs(ref_no_domain, updated_ref) + self.assertDictEqual(ref_no_domain, expected_ref) + + def test_v3controller_filter_domain_id(self): + # No data should be filtered out in this case. + other_data = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref = {'domain_id': domain_id, + 'other_data': other_data} + + expected_ref = ref.copy() + updated_ref = controller.V3Controller.filter_domain_id(ref) + self.assertIs(ref, updated_ref) + self.assertDictEqual(ref, expected_ref) + + def test_v2controller_filter_domain(self): + other_data = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + non_default_domain_ref = {'domain': {'id': domain_id}, + 'other_data': other_data} + default_domain_ref = {'domain': {'id': 'default'}, + 'other_data': other_data} + updated_ref = controller.V2Controller.filter_domain(default_domain_ref) + self.assertNotIn('domain', updated_ref) + self.assertRaises(exception.Unauthorized, + controller.V2Controller.filter_domain, + non_default_domain_ref) diff --git a/keystone-moon/keystone/tests/unit/test_v3_auth.py b/keystone-moon/keystone/tests/unit/test_v3_auth.py new file mode 100644 index 00000000..ec079170 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_v3_auth.py @@ -0,0 +1,4494 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 +import datetime +import json +import operator +import uuid + +from keystoneclient.common import cms +import mock +from oslo_config import cfg +from oslo_utils import timeutils +import six +from testtools import matchers +from testtools import testcase + +from keystone import auth +from keystone import exception +from keystone.policy.backends import rules +from keystone.tests import unit as tests +from keystone.tests.unit import ksfixtures +from keystone.tests.unit import test_v3 + + +CONF = cfg.CONF + + +class TestAuthInfo(test_v3.AuthTestMixin, testcase.TestCase): + def setUp(self): + super(TestAuthInfo, self).setUp() + auth.controllers.load_auth_methods() + + def test_missing_auth_methods(self): + auth_data = {'identity': {}} + auth_data['identity']['token'] = {'id': uuid.uuid4().hex} + self.assertRaises(exception.ValidationError, + auth.controllers.AuthInfo.create, + None, + auth_data) + + def test_unsupported_auth_method(self): + auth_data = {'methods': ['abc']} + auth_data['abc'] = {'test': 'test'} + auth_data = {'identity': auth_data} + self.assertRaises(exception.AuthMethodNotSupported, + auth.controllers.AuthInfo.create, + None, + auth_data) + + def test_missing_auth_method_data(self): + auth_data = {'methods': ['password']} + auth_data = {'identity': auth_data} + self.assertRaises(exception.ValidationError, + auth.controllers.AuthInfo.create, + None, + auth_data) + + def test_project_name_no_domain(self): + auth_data = self.build_authentication_request( + username='test', + password='test', + project_name='abc')['auth'] + self.assertRaises(exception.ValidationError, + auth.controllers.AuthInfo.create, + None, + auth_data) + + def test_both_project_and_domain_in_scope(self): + auth_data = self.build_authentication_request( + user_id='test', + password='test', + project_name='test', + domain_name='test')['auth'] + self.assertRaises(exception.ValidationError, + auth.controllers.AuthInfo.create, + None, + auth_data) + + def test_get_method_names_duplicates(self): + auth_data = self.build_authentication_request( + token='test', + user_id='test', + password='test')['auth'] + auth_data['identity']['methods'] = ['password', 'token', + 'password', 'password'] + context = None + auth_info = auth.controllers.AuthInfo.create(context, auth_data) + self.assertEqual(auth_info.get_method_names(), + ['password', 'token']) + + def test_get_method_data_invalid_method(self): + auth_data = self.build_authentication_request( + user_id='test', + password='test')['auth'] + context = None + auth_info = auth.controllers.AuthInfo.create(context, auth_data) + + method_name = uuid.uuid4().hex + self.assertRaises(exception.ValidationError, + auth_info.get_method_data, + method_name) + + +class TokenAPITests(object): + # Why is this not just setUP? Because TokenAPITests is not a test class + # itself. If TokenAPITests became a subclass of the testcase, it would get + # called by the enumerate-tests-in-file code. The way the functions get + # resolved in Python for multiple inheritance means that a setUp in this + # would get skipped by the testrunner. + def doSetUp(self): + auth_data = self.build_authentication_request( + username=self.user['name'], + user_domain_id=self.domain_id, + password=self.user['password']) + resp = self.v3_authenticate_token(auth_data) + self.token_data = resp.result + self.token = resp.headers.get('X-Subject-Token') + self.headers = {'X-Subject-Token': resp.headers.get('X-Subject-Token')} + + def test_default_fixture_scope_token(self): + self.assertIsNotNone(self.get_scoped_token()) + + def verify_token(self, *args, **kwargs): + return cms.verify_token(*args, **kwargs) + + def test_v3_token_id(self): + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password']) + resp = self.v3_authenticate_token(auth_data) + token_data = resp.result + token_id = resp.headers.get('X-Subject-Token') + self.assertIn('expires_at', token_data['token']) + + decoded_token = self.verify_token(token_id, CONF.signing.certfile, + CONF.signing.ca_certs) + decoded_token_dict = json.loads(decoded_token) + + token_resp_dict = json.loads(resp.body) + + self.assertEqual(decoded_token_dict, token_resp_dict) + # should be able to validate hash PKI token as well + hash_token_id = cms.cms_hash_token(token_id) + headers = {'X-Subject-Token': hash_token_id} + resp = self.get('/auth/tokens', headers=headers) + expected_token_data = resp.result + self.assertDictEqual(expected_token_data, token_data) + + def test_v3_v2_intermix_non_default_domain_failed(self): + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password']) + token = self.get_requested_token(auth_data) + + # now validate the v3 token with v2 API + path = '/v2.0/tokens/%s' % (token) + self.admin_request(path=path, + token='ADMIN', + method='GET', + expected_status=401) + + def test_v3_v2_intermix_new_default_domain(self): + # If the default_domain_id config option is changed, then should be + # able to validate a v3 token with user in the new domain. + + # 1) Create a new domain for the user. + new_domain_id = uuid.uuid4().hex + new_domain = { + 'description': uuid.uuid4().hex, + 'enabled': True, + 'id': new_domain_id, + 'name': uuid.uuid4().hex, + } + + self.resource_api.create_domain(new_domain_id, new_domain) + + # 2) Create user in new domain. + new_user_password = uuid.uuid4().hex + new_user = { + 'name': uuid.uuid4().hex, + 'domain_id': new_domain_id, + 'password': new_user_password, + 'email': uuid.uuid4().hex, + } + + new_user = self.identity_api.create_user(new_user) + + # 3) Update the default_domain_id config option to the new domain + + self.config_fixture.config(group='identity', + default_domain_id=new_domain_id) + + # 4) Get a token using v3 api. + + auth_data = self.build_authentication_request( + user_id=new_user['id'], + password=new_user_password) + token = self.get_requested_token(auth_data) + + # 5) Authenticate token using v2 api. + + path = '/v2.0/tokens/%s' % (token) + self.admin_request(path=path, + token='ADMIN', + method='GET') + + def test_v3_v2_intermix_domain_scoped_token_failed(self): + # grant the domain role to user + path = '/domains/%s/users/%s/roles/%s' % ( + self.domain['id'], self.user['id'], self.role['id']) + self.put(path=path) + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + domain_id=self.domain['id']) + token = self.get_requested_token(auth_data) + + # now validate the v3 token with v2 API + path = '/v2.0/tokens/%s' % (token) + self.admin_request(path=path, + token='ADMIN', + method='GET', + expected_status=401) + + def test_v3_v2_intermix_non_default_project_failed(self): + auth_data = self.build_authentication_request( + user_id=self.default_domain_user['id'], + password=self.default_domain_user['password'], + project_id=self.project['id']) + token = self.get_requested_token(auth_data) + + # now validate the v3 token with v2 API + path = '/v2.0/tokens/%s' % (token) + self.admin_request(path=path, + token='ADMIN', + method='GET', + expected_status=401) + + def test_v3_v2_unscoped_token_intermix(self): + auth_data = self.build_authentication_request( + user_id=self.default_domain_user['id'], + password=self.default_domain_user['password']) + resp = self.v3_authenticate_token(auth_data) + token_data = resp.result + token = resp.headers.get('X-Subject-Token') + + # now validate the v3 token with v2 API + path = '/v2.0/tokens/%s' % (token) + resp = self.admin_request(path=path, + token='ADMIN', + method='GET') + v2_token = resp.result + self.assertEqual(v2_token['access']['user']['id'], + token_data['token']['user']['id']) + # v2 token time has not fraction of second precision so + # just need to make sure the non fraction part agrees + self.assertIn(v2_token['access']['token']['expires'][:-1], + token_data['token']['expires_at']) + + def test_v3_v2_token_intermix(self): + # FIXME(gyee): PKI tokens are not interchangeable because token + # data is baked into the token itself. + auth_data = self.build_authentication_request( + user_id=self.default_domain_user['id'], + password=self.default_domain_user['password'], + project_id=self.default_domain_project['id']) + resp = self.v3_authenticate_token(auth_data) + token_data = resp.result + token = resp.headers.get('X-Subject-Token') + + # now validate the v3 token with v2 API + path = '/v2.0/tokens/%s' % (token) + resp = self.admin_request(path=path, + token='ADMIN', + method='GET') + v2_token = resp.result + self.assertEqual(v2_token['access']['user']['id'], + token_data['token']['user']['id']) + # v2 token time has not fraction of second precision so + # just need to make sure the non fraction part agrees + self.assertIn(v2_token['access']['token']['expires'][:-1], + token_data['token']['expires_at']) + self.assertEqual(v2_token['access']['user']['roles'][0]['id'], + token_data['token']['roles'][0]['id']) + + def test_v3_v2_hashed_pki_token_intermix(self): + auth_data = self.build_authentication_request( + user_id=self.default_domain_user['id'], + password=self.default_domain_user['password'], + project_id=self.default_domain_project['id']) + resp = self.v3_authenticate_token(auth_data) + token_data = resp.result + token = resp.headers.get('X-Subject-Token') + + # should be able to validate a hash PKI token in v2 too + token = cms.cms_hash_token(token) + path = '/v2.0/tokens/%s' % (token) + resp = self.admin_request(path=path, + token='ADMIN', + method='GET') + v2_token = resp.result + self.assertEqual(v2_token['access']['user']['id'], + token_data['token']['user']['id']) + # v2 token time has not fraction of second precision so + # just need to make sure the non fraction part agrees + self.assertIn(v2_token['access']['token']['expires'][:-1], + token_data['token']['expires_at']) + self.assertEqual(v2_token['access']['user']['roles'][0]['id'], + token_data['token']['roles'][0]['id']) + + def test_v2_v3_unscoped_token_intermix(self): + body = { + 'auth': { + 'passwordCredentials': { + 'userId': self.user['id'], + 'password': self.user['password'] + } + }} + resp = self.admin_request(path='/v2.0/tokens', + method='POST', + body=body) + v2_token_data = resp.result + v2_token = v2_token_data['access']['token']['id'] + headers = {'X-Subject-Token': v2_token} + resp = self.get('/auth/tokens', headers=headers) + token_data = resp.result + self.assertEqual(v2_token_data['access']['user']['id'], + token_data['token']['user']['id']) + # v2 token time has not fraction of second precision so + # just need to make sure the non fraction part agrees + self.assertIn(v2_token_data['access']['token']['expires'][-1], + token_data['token']['expires_at']) + + def test_v2_v3_token_intermix(self): + body = { + 'auth': { + 'passwordCredentials': { + 'userId': self.user['id'], + 'password': self.user['password'] + }, + 'tenantId': self.project['id'] + }} + resp = self.admin_request(path='/v2.0/tokens', + method='POST', + body=body) + v2_token_data = resp.result + v2_token = v2_token_data['access']['token']['id'] + headers = {'X-Subject-Token': v2_token} + resp = self.get('/auth/tokens', headers=headers) + token_data = resp.result + self.assertEqual(v2_token_data['access']['user']['id'], + token_data['token']['user']['id']) + # v2 token time has not fraction of second precision so + # just need to make sure the non fraction part agrees + self.assertIn(v2_token_data['access']['token']['expires'][-1], + token_data['token']['expires_at']) + self.assertEqual(v2_token_data['access']['user']['roles'][0]['name'], + token_data['token']['roles'][0]['name']) + + v2_issued_at = timeutils.parse_isotime( + v2_token_data['access']['token']['issued_at']) + v3_issued_at = timeutils.parse_isotime( + token_data['token']['issued_at']) + + self.assertEqual(v2_issued_at, v3_issued_at) + + def test_rescoping_token(self): + expires = self.token_data['token']['expires_at'] + auth_data = self.build_authentication_request( + token=self.token, + project_id=self.project_id) + r = self.v3_authenticate_token(auth_data) + self.assertValidProjectScopedTokenResponse(r) + # make sure expires stayed the same + self.assertEqual(expires, r.result['token']['expires_at']) + + def test_check_token(self): + self.head('/auth/tokens', headers=self.headers, expected_status=200) + + def test_validate_token(self): + r = self.get('/auth/tokens', headers=self.headers) + self.assertValidUnscopedTokenResponse(r) + + def test_validate_token_nocatalog(self): + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + project_id=self.project['id']) + headers = {'X-Subject-Token': self.get_requested_token(auth_data)} + r = self.get('/auth/tokens?nocatalog', headers=headers) + self.assertValidProjectScopedTokenResponse(r, require_catalog=False) + + +class AllowRescopeScopedTokenDisabledTests(test_v3.RestfulTestCase): + def config_overrides(self): + super(AllowRescopeScopedTokenDisabledTests, self).config_overrides() + self.config_fixture.config( + group='token', + allow_rescope_scoped_token=False) + + def test_rescoping_v3_to_v3_disabled(self): + self.v3_authenticate_token( + self.build_authentication_request( + token=self.get_scoped_token(), + project_id=self.project_id), + expected_status=403) + + def _v2_token(self): + body = { + 'auth': { + "tenantId": self.project['id'], + 'passwordCredentials': { + 'userId': self.user['id'], + 'password': self.user['password'] + } + }} + resp = self.admin_request(path='/v2.0/tokens', + method='POST', + body=body) + v2_token_data = resp.result + return v2_token_data + + def _v2_token_from_token(self, token): + body = { + 'auth': { + "tenantId": self.project['id'], + "token": token + }} + self.admin_request(path='/v2.0/tokens', + method='POST', + body=body, + expected_status=403) + + def test_rescoping_v2_to_v3_disabled(self): + token = self._v2_token() + self.v3_authenticate_token( + self.build_authentication_request( + token=token['access']['token']['id'], + project_id=self.project_id), + expected_status=403) + + def test_rescoping_v3_to_v2_disabled(self): + token = {'id': self.get_scoped_token()} + self._v2_token_from_token(token) + + def test_rescoping_v2_to_v2_disabled(self): + token = self._v2_token() + self._v2_token_from_token(token['access']['token']) + + def test_rescoped_domain_token_disabled(self): + + self.domainA = self.new_domain_ref() + self.assignment_api.create_domain(self.domainA['id'], self.domainA) + self.assignment_api.create_grant(self.role['id'], + user_id=self.user['id'], + domain_id=self.domainA['id']) + unscoped_token = self.get_requested_token( + self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'])) + # Get a domain-scoped token from the unscoped token + domain_scoped_token = self.get_requested_token( + self.build_authentication_request( + token=unscoped_token, + domain_id=self.domainA['id'])) + self.v3_authenticate_token( + self.build_authentication_request( + token=domain_scoped_token, + project_id=self.project_id), + expected_status=403) + + +class TestPKITokenAPIs(test_v3.RestfulTestCase, TokenAPITests): + def config_overrides(self): + super(TestPKITokenAPIs, self).config_overrides() + self.config_fixture.config( + group='token', + provider='keystone.token.providers.pki.Provider') + + def setUp(self): + super(TestPKITokenAPIs, self).setUp() + self.doSetUp() + + +class TestPKIZTokenAPIs(test_v3.RestfulTestCase, TokenAPITests): + + def verify_token(self, *args, **kwargs): + return cms.pkiz_verify(*args, **kwargs) + + def config_overrides(self): + super(TestPKIZTokenAPIs, self).config_overrides() + self.config_fixture.config( + group='token', + provider='keystone.token.providers.pkiz.Provider') + + def setUp(self): + super(TestPKIZTokenAPIs, self).setUp() + self.doSetUp() + + +class TestUUIDTokenAPIs(test_v3.RestfulTestCase, TokenAPITests): + def config_overrides(self): + super(TestUUIDTokenAPIs, self).config_overrides() + self.config_fixture.config( + group='token', + provider='keystone.token.providers.uuid.Provider') + + def setUp(self): + super(TestUUIDTokenAPIs, self).setUp() + self.doSetUp() + + def test_v3_token_id(self): + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password']) + resp = self.v3_authenticate_token(auth_data) + token_data = resp.result + token_id = resp.headers.get('X-Subject-Token') + self.assertIn('expires_at', token_data['token']) + self.assertFalse(cms.is_asn1_token(token_id)) + + def test_v3_v2_hashed_pki_token_intermix(self): + # this test is only applicable for PKI tokens + # skipping it for UUID tokens + pass + + +class TestTokenRevokeSelfAndAdmin(test_v3.RestfulTestCase): + """Test token revoke using v3 Identity API by token owner and admin.""" + + def load_sample_data(self): + """Load Sample Data for Test Cases. + + Two domains, domainA and domainB + Two users in domainA, userNormalA and userAdminA + One user in domainB, userAdminB + + """ + super(TestTokenRevokeSelfAndAdmin, self).load_sample_data() + # DomainA setup + self.domainA = self.new_domain_ref() + self.resource_api.create_domain(self.domainA['id'], self.domainA) + + self.userAdminA = self.new_user_ref(domain_id=self.domainA['id']) + password = self.userAdminA['password'] + self.userAdminA = self.identity_api.create_user(self.userAdminA) + self.userAdminA['password'] = password + + self.userNormalA = self.new_user_ref( + domain_id=self.domainA['id']) + password = self.userNormalA['password'] + self.userNormalA = self.identity_api.create_user(self.userNormalA) + self.userNormalA['password'] = password + + self.assignment_api.create_grant(self.role['id'], + user_id=self.userAdminA['id'], + domain_id=self.domainA['id']) + + def config_overrides(self): + super(TestTokenRevokeSelfAndAdmin, self).config_overrides() + self.config_fixture.config( + group='oslo_policy', + policy_file=tests.dirs.etc('policy.v3cloudsample.json')) + + def test_user_revokes_own_token(self): + user_token = self.get_requested_token( + self.build_authentication_request( + user_id=self.userNormalA['id'], + password=self.userNormalA['password'], + user_domain_id=self.domainA['id'])) + self.assertNotEmpty(user_token) + headers = {'X-Subject-Token': user_token} + + adminA_token = self.get_requested_token( + self.build_authentication_request( + user_id=self.userAdminA['id'], + password=self.userAdminA['password'], + domain_name=self.domainA['name'])) + + self.head('/auth/tokens', headers=headers, expected_status=200, + token=adminA_token) + self.head('/auth/tokens', headers=headers, expected_status=200, + token=user_token) + self.delete('/auth/tokens', headers=headers, expected_status=204, + token=user_token) + # invalid X-Auth-Token and invalid X-Subject-Token (401) + self.head('/auth/tokens', headers=headers, expected_status=401, + token=user_token) + # invalid X-Auth-Token and invalid X-Subject-Token (401) + self.delete('/auth/tokens', headers=headers, expected_status=401, + token=user_token) + # valid X-Auth-Token and invalid X-Subject-Token (404) + self.delete('/auth/tokens', headers=headers, expected_status=404, + token=adminA_token) + # valid X-Auth-Token and invalid X-Subject-Token (404) + self.head('/auth/tokens', headers=headers, expected_status=404, + token=adminA_token) + + def test_adminA_revokes_userA_token(self): + user_token = self.get_requested_token( + self.build_authentication_request( + user_id=self.userNormalA['id'], + password=self.userNormalA['password'], + user_domain_id=self.domainA['id'])) + self.assertNotEmpty(user_token) + headers = {'X-Subject-Token': user_token} + + adminA_token = self.get_requested_token( + self.build_authentication_request( + user_id=self.userAdminA['id'], + password=self.userAdminA['password'], + domain_name=self.domainA['name'])) + + self.head('/auth/tokens', headers=headers, expected_status=200, + token=adminA_token) + self.head('/auth/tokens', headers=headers, expected_status=200, + token=user_token) + self.delete('/auth/tokens', headers=headers, expected_status=204, + token=adminA_token) + # invalid X-Auth-Token and invalid X-Subject-Token (401) + self.head('/auth/tokens', headers=headers, expected_status=401, + token=user_token) + # valid X-Auth-Token and invalid X-Subject-Token (404) + self.delete('/auth/tokens', headers=headers, expected_status=404, + token=adminA_token) + # valid X-Auth-Token and invalid X-Subject-Token (404) + self.head('/auth/tokens', headers=headers, expected_status=404, + token=adminA_token) + + def test_adminB_fails_revoking_userA_token(self): + # DomainB setup + self.domainB = self.new_domain_ref() + self.resource_api.create_domain(self.domainB['id'], self.domainB) + self.userAdminB = self.new_user_ref(domain_id=self.domainB['id']) + password = self.userAdminB['password'] + self.userAdminB = self.identity_api.create_user(self.userAdminB) + self.userAdminB['password'] = password + self.assignment_api.create_grant(self.role['id'], + user_id=self.userAdminB['id'], + domain_id=self.domainB['id']) + + user_token = self.get_requested_token( + self.build_authentication_request( + user_id=self.userNormalA['id'], + password=self.userNormalA['password'], + user_domain_id=self.domainA['id'])) + headers = {'X-Subject-Token': user_token} + + adminB_token = self.get_requested_token( + self.build_authentication_request( + user_id=self.userAdminB['id'], + password=self.userAdminB['password'], + domain_name=self.domainB['name'])) + + self.head('/auth/tokens', headers=headers, expected_status=403, + token=adminB_token) + self.delete('/auth/tokens', headers=headers, expected_status=403, + token=adminB_token) + + +class TestTokenRevokeById(test_v3.RestfulTestCase): + """Test token revocation on the v3 Identity API.""" + + def config_overrides(self): + super(TestTokenRevokeById, self).config_overrides() + self.config_fixture.config( + group='revoke', + driver='keystone.contrib.revoke.backends.kvs.Revoke') + self.config_fixture.config( + group='token', + provider='keystone.token.providers.pki.Provider', + revoke_by_id=False) + + def setUp(self): + """Setup for Token Revoking Test Cases. + + As well as the usual housekeeping, create a set of domains, + users, groups, roles and projects for the subsequent tests: + + - Two domains: A & B + - Three users (1, 2 and 3) + - Three groups (1, 2 and 3) + - Two roles (1 and 2) + - DomainA owns user1, domainB owns user2 and user3 + - DomainA owns group1 and group2, domainB owns group3 + - User1 and user2 are members of group1 + - User3 is a member of group2 + - Two projects: A & B, both in domainA + - Group1 has role1 on Project A and B, meaning that user1 and user2 + will get these roles by virtue of membership + - User1, 2 and 3 have role1 assigned to projectA + - Group1 has role1 on Project A and B, meaning that user1 and user2 + will get role1 (duplicated) by virtue of membership + - User1 has role2 assigned to domainA + + """ + super(TestTokenRevokeById, self).setUp() + + # Start by creating a couple of domains and projects + self.domainA = self.new_domain_ref() + self.resource_api.create_domain(self.domainA['id'], self.domainA) + self.domainB = self.new_domain_ref() + self.resource_api.create_domain(self.domainB['id'], self.domainB) + self.projectA = self.new_project_ref(domain_id=self.domainA['id']) + self.resource_api.create_project(self.projectA['id'], self.projectA) + self.projectB = self.new_project_ref(domain_id=self.domainA['id']) + self.resource_api.create_project(self.projectB['id'], self.projectB) + + # Now create some users + self.user1 = self.new_user_ref( + domain_id=self.domainA['id']) + password = self.user1['password'] + self.user1 = self.identity_api.create_user(self.user1) + self.user1['password'] = password + + self.user2 = self.new_user_ref( + domain_id=self.domainB['id']) + password = self.user2['password'] + self.user2 = self.identity_api.create_user(self.user2) + self.user2['password'] = password + + self.user3 = self.new_user_ref( + domain_id=self.domainB['id']) + password = self.user3['password'] + self.user3 = self.identity_api.create_user(self.user3) + self.user3['password'] = password + + self.group1 = self.new_group_ref( + domain_id=self.domainA['id']) + self.group1 = self.identity_api.create_group(self.group1) + + self.group2 = self.new_group_ref( + domain_id=self.domainA['id']) + self.group2 = self.identity_api.create_group(self.group2) + + self.group3 = self.new_group_ref( + domain_id=self.domainB['id']) + self.group3 = self.identity_api.create_group(self.group3) + + self.identity_api.add_user_to_group(self.user1['id'], + self.group1['id']) + self.identity_api.add_user_to_group(self.user2['id'], + self.group1['id']) + self.identity_api.add_user_to_group(self.user3['id'], + self.group2['id']) + + self.role1 = self.new_role_ref() + self.role_api.create_role(self.role1['id'], self.role1) + self.role2 = self.new_role_ref() + self.role_api.create_role(self.role2['id'], self.role2) + + self.assignment_api.create_grant(self.role2['id'], + user_id=self.user1['id'], + domain_id=self.domainA['id']) + self.assignment_api.create_grant(self.role1['id'], + user_id=self.user1['id'], + project_id=self.projectA['id']) + self.assignment_api.create_grant(self.role1['id'], + user_id=self.user2['id'], + project_id=self.projectA['id']) + self.assignment_api.create_grant(self.role1['id'], + user_id=self.user3['id'], + project_id=self.projectA['id']) + self.assignment_api.create_grant(self.role1['id'], + group_id=self.group1['id'], + project_id=self.projectA['id']) + + def test_unscoped_token_remains_valid_after_role_assignment(self): + unscoped_token = self.get_requested_token( + self.build_authentication_request( + user_id=self.user1['id'], + password=self.user1['password'])) + + scoped_token = self.get_requested_token( + self.build_authentication_request( + token=unscoped_token, + project_id=self.projectA['id'])) + + # confirm both tokens are valid + self.head('/auth/tokens', + headers={'X-Subject-Token': unscoped_token}, + expected_status=200) + self.head('/auth/tokens', + headers={'X-Subject-Token': scoped_token}, + expected_status=200) + + # create a new role + role = self.new_role_ref() + self.role_api.create_role(role['id'], role) + + # assign a new role + self.put( + '/projects/%(project_id)s/users/%(user_id)s/roles/%(role_id)s' % { + 'project_id': self.projectA['id'], + 'user_id': self.user1['id'], + 'role_id': role['id']}) + + # both tokens should remain valid + self.head('/auth/tokens', + headers={'X-Subject-Token': unscoped_token}, + expected_status=200) + self.head('/auth/tokens', + headers={'X-Subject-Token': scoped_token}, + expected_status=200) + + def test_deleting_user_grant_revokes_token(self): + """Test deleting a user grant revokes token. + + Test Plan: + + - Get a token for user1, scoped to ProjectA + - Delete the grant user1 has on ProjectA + - Check token is no longer valid + + """ + auth_data = self.build_authentication_request( + user_id=self.user1['id'], + password=self.user1['password'], + project_id=self.projectA['id']) + token = self.get_requested_token(auth_data) + # Confirm token is valid + self.head('/auth/tokens', + headers={'X-Subject-Token': token}, + expected_status=200) + # Delete the grant, which should invalidate the token + grant_url = ( + '/projects/%(project_id)s/users/%(user_id)s/' + 'roles/%(role_id)s' % { + 'project_id': self.projectA['id'], + 'user_id': self.user1['id'], + 'role_id': self.role1['id']}) + self.delete(grant_url) + self.head('/auth/tokens', + headers={'X-Subject-Token': token}, + expected_status=404) + + def role_data_fixtures(self): + self.projectC = self.new_project_ref(domain_id=self.domainA['id']) + self.resource_api.create_project(self.projectC['id'], self.projectC) + self.user4 = self.new_user_ref(domain_id=self.domainB['id']) + password = self.user4['password'] + self.user4 = self.identity_api.create_user(self.user4) + self.user4['password'] = password + self.user5 = self.new_user_ref( + domain_id=self.domainA['id']) + password = self.user5['password'] + self.user5 = self.identity_api.create_user(self.user5) + self.user5['password'] = password + self.user6 = self.new_user_ref( + domain_id=self.domainA['id']) + password = self.user6['password'] + self.user6 = self.identity_api.create_user(self.user6) + self.user6['password'] = password + self.identity_api.add_user_to_group(self.user5['id'], + self.group1['id']) + self.assignment_api.create_grant(self.role1['id'], + group_id=self.group1['id'], + project_id=self.projectB['id']) + self.assignment_api.create_grant(self.role2['id'], + user_id=self.user4['id'], + project_id=self.projectC['id']) + self.assignment_api.create_grant(self.role1['id'], + user_id=self.user6['id'], + project_id=self.projectA['id']) + self.assignment_api.create_grant(self.role1['id'], + user_id=self.user6['id'], + domain_id=self.domainA['id']) + + def test_deleting_role_revokes_token(self): + """Test deleting a role revokes token. + + Add some additional test data, namely: + - A third project (project C) + - Three additional users - user4 owned by domainB and user5 and 6 + owned by domainA (different domain ownership should not affect + the test results, just provided to broaden test coverage) + - User5 is a member of group1 + - Group1 gets an additional assignment - role1 on projectB as + well as its existing role1 on projectA + - User4 has role2 on Project C + - User6 has role1 on projectA and domainA + - This allows us to create 5 tokens by virtue of different types + of role assignment: + - user1, scoped to ProjectA by virtue of user role1 assignment + - user5, scoped to ProjectB by virtue of group role1 assignment + - user4, scoped to ProjectC by virtue of user role2 assignment + - user6, scoped to ProjectA by virtue of user role1 assignment + - user6, scoped to DomainA by virtue of user role1 assignment + - role1 is then deleted + - Check the tokens on Project A and B, and DomainA are revoked, + but not the one for Project C + + """ + + self.role_data_fixtures() + + # Now we are ready to start issuing requests + auth_data = self.build_authentication_request( + user_id=self.user1['id'], + password=self.user1['password'], + project_id=self.projectA['id']) + tokenA = self.get_requested_token(auth_data) + auth_data = self.build_authentication_request( + user_id=self.user5['id'], + password=self.user5['password'], + project_id=self.projectB['id']) + tokenB = self.get_requested_token(auth_data) + auth_data = self.build_authentication_request( + user_id=self.user4['id'], + password=self.user4['password'], + project_id=self.projectC['id']) + tokenC = self.get_requested_token(auth_data) + auth_data = self.build_authentication_request( + user_id=self.user6['id'], + password=self.user6['password'], + project_id=self.projectA['id']) + tokenD = self.get_requested_token(auth_data) + auth_data = self.build_authentication_request( + user_id=self.user6['id'], + password=self.user6['password'], + domain_id=self.domainA['id']) + tokenE = self.get_requested_token(auth_data) + # Confirm tokens are valid + self.head('/auth/tokens', + headers={'X-Subject-Token': tokenA}, + expected_status=200) + self.head('/auth/tokens', + headers={'X-Subject-Token': tokenB}, + expected_status=200) + self.head('/auth/tokens', + headers={'X-Subject-Token': tokenC}, + expected_status=200) + self.head('/auth/tokens', + headers={'X-Subject-Token': tokenD}, + expected_status=200) + self.head('/auth/tokens', + headers={'X-Subject-Token': tokenE}, + expected_status=200) + + # Delete the role, which should invalidate the tokens + role_url = '/roles/%s' % self.role1['id'] + self.delete(role_url) + + # Check the tokens that used role1 is invalid + self.head('/auth/tokens', + headers={'X-Subject-Token': tokenA}, + expected_status=404) + self.head('/auth/tokens', + headers={'X-Subject-Token': tokenB}, + expected_status=404) + self.head('/auth/tokens', + headers={'X-Subject-Token': tokenD}, + expected_status=404) + self.head('/auth/tokens', + headers={'X-Subject-Token': tokenE}, + expected_status=404) + + # ...but the one using role2 is still valid + self.head('/auth/tokens', + headers={'X-Subject-Token': tokenC}, + expected_status=200) + + def test_domain_user_role_assignment_maintains_token(self): + """Test user-domain role assignment maintains existing token. + + Test Plan: + + - Get a token for user1, scoped to ProjectA + - Create a grant for user1 on DomainB + - Check token is still valid + + """ + auth_data = self.build_authentication_request( + user_id=self.user1['id'], + password=self.user1['password'], + project_id=self.projectA['id']) + token = self.get_requested_token(auth_data) + # Confirm token is valid + self.head('/auth/tokens', + headers={'X-Subject-Token': token}, + expected_status=200) + # Assign a role, which should not affect the token + grant_url = ( + '/domains/%(domain_id)s/users/%(user_id)s/' + 'roles/%(role_id)s' % { + 'domain_id': self.domainB['id'], + 'user_id': self.user1['id'], + 'role_id': self.role1['id']}) + self.put(grant_url) + self.head('/auth/tokens', + headers={'X-Subject-Token': token}, + expected_status=200) + + def test_disabling_project_revokes_token(self): + token = self.get_requested_token( + self.build_authentication_request( + user_id=self.user3['id'], + password=self.user3['password'], + project_id=self.projectA['id'])) + + # confirm token is valid + self.head('/auth/tokens', + headers={'X-Subject-Token': token}, + expected_status=200) + + # disable the project, which should invalidate the token + self.patch( + '/projects/%(project_id)s' % {'project_id': self.projectA['id']}, + body={'project': {'enabled': False}}) + + # user should no longer have access to the project + self.head('/auth/tokens', + headers={'X-Subject-Token': token}, + expected_status=404) + self.v3_authenticate_token( + self.build_authentication_request( + user_id=self.user3['id'], + password=self.user3['password'], + project_id=self.projectA['id']), + expected_status=401) + + def test_deleting_project_revokes_token(self): + token = self.get_requested_token( + self.build_authentication_request( + user_id=self.user3['id'], + password=self.user3['password'], + project_id=self.projectA['id'])) + + # confirm token is valid + self.head('/auth/tokens', + headers={'X-Subject-Token': token}, + expected_status=200) + + # delete the project, which should invalidate the token + self.delete( + '/projects/%(project_id)s' % {'project_id': self.projectA['id']}) + + # user should no longer have access to the project + self.head('/auth/tokens', + headers={'X-Subject-Token': token}, + expected_status=404) + self.v3_authenticate_token( + self.build_authentication_request( + user_id=self.user3['id'], + password=self.user3['password'], + project_id=self.projectA['id']), + expected_status=401) + + def test_deleting_group_grant_revokes_tokens(self): + """Test deleting a group grant revokes tokens. + + Test Plan: + + - Get a token for user1, scoped to ProjectA + - Get a token for user2, scoped to ProjectA + - Get a token for user3, scoped to ProjectA + - Delete the grant group1 has on ProjectA + - Check tokens for user1 & user2 are no longer valid, + since user1 and user2 are members of group1 + - Check token for user3 is still valid + + """ + auth_data = self.build_authentication_request( + user_id=self.user1['id'], + password=self.user1['password'], + project_id=self.projectA['id']) + token1 = self.get_requested_token(auth_data) + auth_data = self.build_authentication_request( + user_id=self.user2['id'], + password=self.user2['password'], + project_id=self.projectA['id']) + token2 = self.get_requested_token(auth_data) + auth_data = self.build_authentication_request( + user_id=self.user3['id'], + password=self.user3['password'], + project_id=self.projectA['id']) + token3 = self.get_requested_token(auth_data) + # Confirm tokens are valid + self.head('/auth/tokens', + headers={'X-Subject-Token': token1}, + expected_status=200) + self.head('/auth/tokens', + headers={'X-Subject-Token': token2}, + expected_status=200) + self.head('/auth/tokens', + headers={'X-Subject-Token': token3}, + expected_status=200) + # Delete the group grant, which should invalidate the + # tokens for user1 and user2 + grant_url = ( + '/projects/%(project_id)s/groups/%(group_id)s/' + 'roles/%(role_id)s' % { + 'project_id': self.projectA['id'], + 'group_id': self.group1['id'], + 'role_id': self.role1['id']}) + self.delete(grant_url) + self.head('/auth/tokens', + headers={'X-Subject-Token': token1}, + expected_status=404) + self.head('/auth/tokens', + headers={'X-Subject-Token': token2}, + expected_status=404) + # But user3's token should still be valid + self.head('/auth/tokens', + headers={'X-Subject-Token': token3}, + expected_status=200) + + def test_domain_group_role_assignment_maintains_token(self): + """Test domain-group role assignment maintains existing token. + + Test Plan: + + - Get a token for user1, scoped to ProjectA + - Create a grant for group1 on DomainB + - Check token is still longer valid + + """ + auth_data = self.build_authentication_request( + user_id=self.user1['id'], + password=self.user1['password'], + project_id=self.projectA['id']) + token = self.get_requested_token(auth_data) + # Confirm token is valid + self.head('/auth/tokens', + headers={'X-Subject-Token': token}, + expected_status=200) + # Delete the grant, which should invalidate the token + grant_url = ( + '/domains/%(domain_id)s/groups/%(group_id)s/' + 'roles/%(role_id)s' % { + 'domain_id': self.domainB['id'], + 'group_id': self.group1['id'], + 'role_id': self.role1['id']}) + self.put(grant_url) + self.head('/auth/tokens', + headers={'X-Subject-Token': token}, + expected_status=200) + + def test_group_membership_changes_revokes_token(self): + """Test add/removal to/from group revokes token. + + Test Plan: + + - Get a token for user1, scoped to ProjectA + - Get a token for user2, scoped to ProjectA + - Remove user1 from group1 + - Check token for user1 is no longer valid + - Check token for user2 is still valid, even though + user2 is also part of group1 + - Add user2 to group2 + - Check token for user2 is now no longer valid + + """ + auth_data = self.build_authentication_request( + user_id=self.user1['id'], + password=self.user1['password'], + project_id=self.projectA['id']) + token1 = self.get_requested_token(auth_data) + auth_data = self.build_authentication_request( + user_id=self.user2['id'], + password=self.user2['password'], + project_id=self.projectA['id']) + token2 = self.get_requested_token(auth_data) + # Confirm tokens are valid + self.head('/auth/tokens', + headers={'X-Subject-Token': token1}, + expected_status=200) + self.head('/auth/tokens', + headers={'X-Subject-Token': token2}, + expected_status=200) + # Remove user1 from group1, which should invalidate + # the token + self.delete('/groups/%(group_id)s/users/%(user_id)s' % { + 'group_id': self.group1['id'], + 'user_id': self.user1['id']}) + self.head('/auth/tokens', + headers={'X-Subject-Token': token1}, + expected_status=404) + # But user2's token should still be valid + self.head('/auth/tokens', + headers={'X-Subject-Token': token2}, + expected_status=200) + # Adding user2 to a group should not invalidate token + self.put('/groups/%(group_id)s/users/%(user_id)s' % { + 'group_id': self.group2['id'], + 'user_id': self.user2['id']}) + self.head('/auth/tokens', + headers={'X-Subject-Token': token2}, + expected_status=200) + + def test_removing_role_assignment_does_not_affect_other_users(self): + """Revoking a role from one user should not affect other users.""" + user1_token = self.get_requested_token( + self.build_authentication_request( + user_id=self.user1['id'], + password=self.user1['password'], + project_id=self.projectA['id'])) + + user3_token = self.get_requested_token( + self.build_authentication_request( + user_id=self.user3['id'], + password=self.user3['password'], + project_id=self.projectA['id'])) + + # delete relationships between user1 and projectA from setUp + self.delete( + '/projects/%(project_id)s/users/%(user_id)s/roles/%(role_id)s' % { + 'project_id': self.projectA['id'], + 'user_id': self.user1['id'], + 'role_id': self.role1['id']}) + self.delete( + '/projects/%(project_id)s/groups/%(group_id)s/roles/%(role_id)s' % + {'project_id': self.projectA['id'], + 'group_id': self.group1['id'], + 'role_id': self.role1['id']}) + + # authorization for the first user should now fail + self.head('/auth/tokens', + headers={'X-Subject-Token': user1_token}, + expected_status=404) + self.v3_authenticate_token( + self.build_authentication_request( + user_id=self.user1['id'], + password=self.user1['password'], + project_id=self.projectA['id']), + expected_status=401) + + # authorization for the second user should still succeed + self.head('/auth/tokens', + headers={'X-Subject-Token': user3_token}, + expected_status=200) + self.v3_authenticate_token( + self.build_authentication_request( + user_id=self.user3['id'], + password=self.user3['password'], + project_id=self.projectA['id'])) + + def test_deleting_project_deletes_grants(self): + # This is to make it a little bit more pretty with PEP8 + role_path = ('/projects/%(project_id)s/users/%(user_id)s/' + 'roles/%(role_id)s') + role_path = role_path % {'user_id': self.user['id'], + 'project_id': self.projectA['id'], + 'role_id': self.role['id']} + + # grant the user a role on the project + self.put(role_path) + + # delete the project, which should remove the roles + self.delete( + '/projects/%(project_id)s' % {'project_id': self.projectA['id']}) + + # Make sure that we get a NotFound(404) when heading that role. + self.head(role_path, expected_status=404) + + def get_v2_token(self, token=None, project_id=None): + body = {'auth': {}, } + + if token: + body['auth']['token'] = { + 'id': token + } + else: + body['auth']['passwordCredentials'] = { + 'username': self.default_domain_user['name'], + 'password': self.default_domain_user['password'], + } + + if project_id: + body['auth']['tenantId'] = project_id + + r = self.admin_request(method='POST', path='/v2.0/tokens', body=body) + return r.json_body['access']['token']['id'] + + def test_revoke_v2_token_no_check(self): + # Test that a V2 token can be revoked without validating it first. + + token = self.get_v2_token() + + self.delete('/auth/tokens', + headers={'X-Subject-Token': token}, + expected_status=204) + + self.head('/auth/tokens', + headers={'X-Subject-Token': token}, + expected_status=404) + + def test_revoke_token_from_token(self): + # Test that a scoped token can be requested from an unscoped token, + # the scoped token can be revoked, and the unscoped token remains + # valid. + + unscoped_token = self.get_requested_token( + self.build_authentication_request( + user_id=self.user1['id'], + password=self.user1['password'])) + + # Get a project-scoped token from the unscoped token + project_scoped_token = self.get_requested_token( + self.build_authentication_request( + token=unscoped_token, + project_id=self.projectA['id'])) + + # Get a domain-scoped token from the unscoped token + domain_scoped_token = self.get_requested_token( + self.build_authentication_request( + token=unscoped_token, + domain_id=self.domainA['id'])) + + # revoke the project-scoped token. + self.delete('/auth/tokens', + headers={'X-Subject-Token': project_scoped_token}, + expected_status=204) + + # The project-scoped token is invalidated. + self.head('/auth/tokens', + headers={'X-Subject-Token': project_scoped_token}, + expected_status=404) + + # The unscoped token should still be valid. + self.head('/auth/tokens', + headers={'X-Subject-Token': unscoped_token}, + expected_status=200) + + # The domain-scoped token should still be valid. + self.head('/auth/tokens', + headers={'X-Subject-Token': domain_scoped_token}, + expected_status=200) + + # revoke the domain-scoped token. + self.delete('/auth/tokens', + headers={'X-Subject-Token': domain_scoped_token}, + expected_status=204) + + # The domain-scoped token is invalid. + self.head('/auth/tokens', + headers={'X-Subject-Token': domain_scoped_token}, + expected_status=404) + + # The unscoped token should still be valid. + self.head('/auth/tokens', + headers={'X-Subject-Token': unscoped_token}, + expected_status=200) + + def test_revoke_token_from_token_v2(self): + # Test that a scoped token can be requested from an unscoped token, + # the scoped token can be revoked, and the unscoped token remains + # valid. + + # FIXME(blk-u): This isn't working correctly. The scoped token should + # be revoked. See bug 1347318. + + unscoped_token = self.get_v2_token() + + # Get a project-scoped token from the unscoped token + project_scoped_token = self.get_v2_token( + token=unscoped_token, project_id=self.default_domain_project['id']) + + # revoke the project-scoped token. + self.delete('/auth/tokens', + headers={'X-Subject-Token': project_scoped_token}, + expected_status=204) + + # The project-scoped token is invalidated. + self.head('/auth/tokens', + headers={'X-Subject-Token': project_scoped_token}, + expected_status=404) + + # The unscoped token should still be valid. + self.head('/auth/tokens', + headers={'X-Subject-Token': unscoped_token}, + expected_status=200) + + +class TestTokenRevokeApi(TestTokenRevokeById): + EXTENSION_NAME = 'revoke' + EXTENSION_TO_ADD = 'revoke_extension' + + """Test token revocation on the v3 Identity API.""" + def config_overrides(self): + super(TestTokenRevokeApi, self).config_overrides() + self.config_fixture.config( + group='revoke', + driver='keystone.contrib.revoke.backends.kvs.Revoke') + self.config_fixture.config( + group='token', + provider='keystone.token.providers.pki.Provider', + revoke_by_id=False) + + def assertValidDeletedProjectResponse(self, events_response, project_id): + events = events_response['events'] + self.assertEqual(1, len(events)) + self.assertEqual(project_id, events[0]['project_id']) + self.assertIsNotNone(events[0]['issued_before']) + self.assertIsNotNone(events_response['links']) + del (events_response['events'][0]['issued_before']) + del (events_response['links']) + expected_response = {'events': [{'project_id': project_id}]} + self.assertEqual(expected_response, events_response) + + def assertDomainInList(self, events_response, domain_id): + events = events_response['events'] + self.assertEqual(1, len(events)) + self.assertEqual(domain_id, events[0]['domain_id']) + self.assertIsNotNone(events[0]['issued_before']) + self.assertIsNotNone(events_response['links']) + del (events_response['events'][0]['issued_before']) + del (events_response['links']) + expected_response = {'events': [{'domain_id': domain_id}]} + self.assertEqual(expected_response, events_response) + + def assertValidRevokedTokenResponse(self, events_response, **kwargs): + events = events_response['events'] + self.assertEqual(1, len(events)) + for k, v in six.iteritems(kwargs): + self.assertEqual(v, events[0].get(k)) + self.assertIsNotNone(events[0]['issued_before']) + self.assertIsNotNone(events_response['links']) + del (events_response['events'][0]['issued_before']) + del (events_response['links']) + + expected_response = {'events': [kwargs]} + self.assertEqual(expected_response, events_response) + + def test_revoke_token(self): + scoped_token = self.get_scoped_token() + headers = {'X-Subject-Token': scoped_token} + response = self.get('/auth/tokens', headers=headers, + expected_status=200).json_body['token'] + + self.delete('/auth/tokens', headers=headers, expected_status=204) + self.head('/auth/tokens', headers=headers, expected_status=404) + events_response = self.get('/OS-REVOKE/events', + expected_status=200).json_body + self.assertValidRevokedTokenResponse(events_response, + audit_id=response['audit_ids'][0]) + + def test_revoke_v2_token(self): + token = self.get_v2_token() + headers = {'X-Subject-Token': token} + response = self.get('/auth/tokens', headers=headers, + expected_status=200).json_body['token'] + self.delete('/auth/tokens', headers=headers, expected_status=204) + self.head('/auth/tokens', headers=headers, expected_status=404) + events_response = self.get('/OS-REVOKE/events', + expected_status=200).json_body + + self.assertValidRevokedTokenResponse( + events_response, + audit_id=response['audit_ids'][0]) + + def test_revoke_by_id_false_410(self): + self.get('/auth/tokens/OS-PKI/revoked', expected_status=410) + + def test_list_delete_project_shows_in_event_list(self): + self.role_data_fixtures() + events = self.get('/OS-REVOKE/events', + expected_status=200).json_body['events'] + self.assertEqual([], events) + self.delete( + '/projects/%(project_id)s' % {'project_id': self.projectA['id']}) + events_response = self.get('/OS-REVOKE/events', + expected_status=200).json_body + + self.assertValidDeletedProjectResponse(events_response, + self.projectA['id']) + + def test_disable_domain_shows_in_event_list(self): + events = self.get('/OS-REVOKE/events', + expected_status=200).json_body['events'] + self.assertEqual([], events) + disable_body = {'domain': {'enabled': False}} + self.patch( + '/domains/%(project_id)s' % {'project_id': self.domainA['id']}, + body=disable_body) + + events = self.get('/OS-REVOKE/events', + expected_status=200).json_body + + self.assertDomainInList(events, self.domainA['id']) + + def assertEventDataInList(self, events, **kwargs): + found = False + for e in events: + for key, value in six.iteritems(kwargs): + try: + if e[key] != value: + break + except KeyError: + # Break the loop and present a nice error instead of + # KeyError + break + else: + # If the value of the event[key] matches the value of the kwarg + # for each item in kwargs, the event was fully matched and + # the assertTrue below should succeed. + found = True + self.assertTrue(found, + 'event with correct values not in list, expected to ' + 'find event with key-value pairs. Expected: ' + '"%(expected)s" Events: "%(events)s"' % + {'expected': ','.join( + ["'%s=%s'" % (k, v) for k, v in six.iteritems( + kwargs)]), + 'events': events}) + + def test_list_delete_token_shows_in_event_list(self): + self.role_data_fixtures() + events = self.get('/OS-REVOKE/events', + expected_status=200).json_body['events'] + self.assertEqual([], events) + + scoped_token = self.get_scoped_token() + headers = {'X-Subject-Token': scoped_token} + auth_req = self.build_authentication_request(token=scoped_token) + response = self.v3_authenticate_token(auth_req) + token2 = response.json_body['token'] + headers2 = {'X-Subject-Token': response.headers['X-Subject-Token']} + + response = self.v3_authenticate_token(auth_req) + response.json_body['token'] + headers3 = {'X-Subject-Token': response.headers['X-Subject-Token']} + + self.head('/auth/tokens', headers=headers, expected_status=200) + self.head('/auth/tokens', headers=headers2, expected_status=200) + self.head('/auth/tokens', headers=headers3, expected_status=200) + + self.delete('/auth/tokens', headers=headers, expected_status=204) + # NOTE(ayoung): not deleting token3, as it should be deleted + # by previous + events_response = self.get('/OS-REVOKE/events', + expected_status=200).json_body + events = events_response['events'] + self.assertEqual(1, len(events)) + self.assertEventDataInList( + events, + audit_id=token2['audit_ids'][1]) + self.head('/auth/tokens', headers=headers, expected_status=404) + self.head('/auth/tokens', headers=headers2, expected_status=200) + self.head('/auth/tokens', headers=headers3, expected_status=200) + + def test_list_with_filter(self): + + self.role_data_fixtures() + events = self.get('/OS-REVOKE/events', + expected_status=200).json_body['events'] + self.assertEqual(0, len(events)) + + scoped_token = self.get_scoped_token() + headers = {'X-Subject-Token': scoped_token} + auth = self.build_authentication_request(token=scoped_token) + headers2 = {'X-Subject-Token': self.get_requested_token(auth)} + self.delete('/auth/tokens', headers=headers, expected_status=204) + self.delete('/auth/tokens', headers=headers2, expected_status=204) + + events = self.get('/OS-REVOKE/events', + expected_status=200).json_body['events'] + + self.assertEqual(2, len(events)) + future = timeutils.isotime(timeutils.utcnow() + + datetime.timedelta(seconds=1000)) + + events = self.get('/OS-REVOKE/events?since=%s' % (future), + expected_status=200).json_body['events'] + self.assertEqual(0, len(events)) + + +class TestAuthExternalDisabled(test_v3.RestfulTestCase): + def config_overrides(self): + super(TestAuthExternalDisabled, self).config_overrides() + self.config_fixture.config( + group='auth', + methods=['password', 'token']) + + def test_remote_user_disabled(self): + api = auth.controllers.Auth() + remote_user = '%s@%s' % (self.user['name'], self.domain['name']) + context, auth_info, auth_context = self.build_external_auth_request( + remote_user) + self.assertRaises(exception.Unauthorized, + api.authenticate, + context, + auth_info, + auth_context) + + +class TestAuthExternalLegacyDefaultDomain(test_v3.RestfulTestCase): + content_type = 'json' + + def config_overrides(self): + super(TestAuthExternalLegacyDefaultDomain, self).config_overrides() + self.auth_plugin_config_override( + methods=['external', 'password', 'token'], + external='keystone.auth.plugins.external.LegacyDefaultDomain', + password='keystone.auth.plugins.password.Password', + token='keystone.auth.plugins.token.Token') + + def test_remote_user_no_realm(self): + self.config_fixture.config(group='auth', methods='external') + api = auth.controllers.Auth() + context, auth_info, auth_context = self.build_external_auth_request( + self.default_domain_user['name']) + api.authenticate(context, auth_info, auth_context) + self.assertEqual(auth_context['user_id'], + self.default_domain_user['id']) + + def test_remote_user_no_domain(self): + api = auth.controllers.Auth() + context, auth_info, auth_context = self.build_external_auth_request( + self.user['name']) + self.assertRaises(exception.Unauthorized, + api.authenticate, + context, + auth_info, + auth_context) + + +class TestAuthExternalLegacyDomain(test_v3.RestfulTestCase): + content_type = 'json' + + def config_overrides(self): + super(TestAuthExternalLegacyDomain, self).config_overrides() + self.auth_plugin_config_override( + methods=['external', 'password', 'token'], + external='keystone.auth.plugins.external.LegacyDomain', + password='keystone.auth.plugins.password.Password', + token='keystone.auth.plugins.token.Token') + + def test_remote_user_with_realm(self): + api = auth.controllers.Auth() + remote_user = '%s@%s' % (self.user['name'], self.domain['name']) + context, auth_info, auth_context = self.build_external_auth_request( + remote_user) + + api.authenticate(context, auth_info, auth_context) + self.assertEqual(auth_context['user_id'], self.user['id']) + + # Now test to make sure the user name can, itself, contain the + # '@' character. + user = {'name': 'myname@mydivision'} + self.identity_api.update_user(self.user['id'], user) + remote_user = '%s@%s' % (user['name'], self.domain['name']) + context, auth_info, auth_context = self.build_external_auth_request( + remote_user) + + api.authenticate(context, auth_info, auth_context) + self.assertEqual(auth_context['user_id'], self.user['id']) + + def test_project_id_scoped_with_remote_user(self): + self.config_fixture.config(group='token', bind=['kerberos']) + auth_data = self.build_authentication_request( + project_id=self.project['id']) + remote_user = '%s@%s' % (self.user['name'], self.domain['name']) + self.admin_app.extra_environ.update({'REMOTE_USER': remote_user, + 'AUTH_TYPE': 'Negotiate'}) + r = self.v3_authenticate_token(auth_data) + token = self.assertValidProjectScopedTokenResponse(r) + self.assertEqual(token['bind']['kerberos'], self.user['name']) + + def test_unscoped_bind_with_remote_user(self): + self.config_fixture.config(group='token', bind=['kerberos']) + auth_data = self.build_authentication_request() + remote_user = '%s@%s' % (self.user['name'], self.domain['name']) + self.admin_app.extra_environ.update({'REMOTE_USER': remote_user, + 'AUTH_TYPE': 'Negotiate'}) + r = self.v3_authenticate_token(auth_data) + token = self.assertValidUnscopedTokenResponse(r) + self.assertEqual(token['bind']['kerberos'], self.user['name']) + + +class TestAuthExternalDomain(test_v3.RestfulTestCase): + content_type = 'json' + + def config_overrides(self): + super(TestAuthExternalDomain, self).config_overrides() + self.kerberos = False + self.auth_plugin_config_override( + methods=['external', 'password', 'token'], + external='keystone.auth.plugins.external.Domain', + password='keystone.auth.plugins.password.Password', + token='keystone.auth.plugins.token.Token') + + def test_remote_user_with_realm(self): + api = auth.controllers.Auth() + remote_user = self.user['name'] + remote_domain = self.domain['name'] + context, auth_info, auth_context = self.build_external_auth_request( + remote_user, remote_domain=remote_domain, kerberos=self.kerberos) + + api.authenticate(context, auth_info, auth_context) + self.assertEqual(auth_context['user_id'], self.user['id']) + + # Now test to make sure the user name can, itself, contain the + # '@' character. + user = {'name': 'myname@mydivision'} + self.identity_api.update_user(self.user['id'], user) + remote_user = user['name'] + context, auth_info, auth_context = self.build_external_auth_request( + remote_user, remote_domain=remote_domain, kerberos=self.kerberos) + + api.authenticate(context, auth_info, auth_context) + self.assertEqual(auth_context['user_id'], self.user['id']) + + def test_project_id_scoped_with_remote_user(self): + self.config_fixture.config(group='token', bind=['kerberos']) + auth_data = self.build_authentication_request( + project_id=self.project['id'], + kerberos=self.kerberos) + remote_user = self.user['name'] + remote_domain = self.domain['name'] + self.admin_app.extra_environ.update({'REMOTE_USER': remote_user, + 'REMOTE_DOMAIN': remote_domain, + 'AUTH_TYPE': 'Negotiate'}) + r = self.v3_authenticate_token(auth_data) + token = self.assertValidProjectScopedTokenResponse(r) + self.assertEqual(token['bind']['kerberos'], self.user['name']) + + def test_unscoped_bind_with_remote_user(self): + self.config_fixture.config(group='token', bind=['kerberos']) + auth_data = self.build_authentication_request(kerberos=self.kerberos) + remote_user = self.user['name'] + remote_domain = self.domain['name'] + self.admin_app.extra_environ.update({'REMOTE_USER': remote_user, + 'REMOTE_DOMAIN': remote_domain, + 'AUTH_TYPE': 'Negotiate'}) + r = self.v3_authenticate_token(auth_data) + token = self.assertValidUnscopedTokenResponse(r) + self.assertEqual(token['bind']['kerberos'], self.user['name']) + + +class TestAuthKerberos(TestAuthExternalDomain): + + def config_overrides(self): + super(TestAuthKerberos, self).config_overrides() + self.kerberos = True + self.auth_plugin_config_override( + methods=['kerberos', 'password', 'token'], + kerberos='keystone.auth.plugins.external.KerberosDomain', + password='keystone.auth.plugins.password.Password', + token='keystone.auth.plugins.token.Token') + + +class TestAuth(test_v3.RestfulTestCase): + + def test_unscoped_token_with_user_id(self): + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password']) + r = self.v3_authenticate_token(auth_data) + self.assertValidUnscopedTokenResponse(r) + + def test_unscoped_token_with_user_domain_id(self): + auth_data = self.build_authentication_request( + username=self.user['name'], + user_domain_id=self.domain['id'], + password=self.user['password']) + r = self.v3_authenticate_token(auth_data) + self.assertValidUnscopedTokenResponse(r) + + def test_unscoped_token_with_user_domain_name(self): + auth_data = self.build_authentication_request( + username=self.user['name'], + user_domain_name=self.domain['name'], + password=self.user['password']) + r = self.v3_authenticate_token(auth_data) + self.assertValidUnscopedTokenResponse(r) + + def test_project_id_scoped_token_with_user_id(self): + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + project_id=self.project['id']) + r = self.v3_authenticate_token(auth_data) + self.assertValidProjectScopedTokenResponse(r) + + def _second_project_as_default(self): + ref = self.new_project_ref(domain_id=self.domain_id) + r = self.post('/projects', body={'project': ref}) + project = self.assertValidProjectResponse(r, ref) + + # grant the user a role on the project + self.put( + '/projects/%(project_id)s/users/%(user_id)s/roles/%(role_id)s' % { + 'user_id': self.user['id'], + 'project_id': project['id'], + 'role_id': self.role['id']}) + + # set the user's preferred project + body = {'user': {'default_project_id': project['id']}} + r = self.patch('/users/%(user_id)s' % { + 'user_id': self.user['id']}, + body=body) + self.assertValidUserResponse(r) + + return project + + def test_default_project_id_scoped_token_with_user_id(self): + project = self._second_project_as_default() + + # attempt to authenticate without requesting a project + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password']) + r = self.v3_authenticate_token(auth_data) + self.assertValidProjectScopedTokenResponse(r) + self.assertEqual(r.result['token']['project']['id'], project['id']) + + def test_default_project_id_scoped_token_with_user_id_no_catalog(self): + project = self._second_project_as_default() + + # attempt to authenticate without requesting a project + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password']) + r = self.post('/auth/tokens?nocatalog', body=auth_data, noauth=True) + self.assertValidProjectScopedTokenResponse(r, require_catalog=False) + self.assertEqual(r.result['token']['project']['id'], project['id']) + + def test_explicit_unscoped_token(self): + self._second_project_as_default() + + # attempt to authenticate without requesting a project + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + unscoped="unscoped") + r = self.post('/auth/tokens', body=auth_data, noauth=True) + + self.assertIsNone(r.result['token'].get('project')) + self.assertIsNone(r.result['token'].get('domain')) + self.assertIsNone(r.result['token'].get('scope')) + + def test_implicit_project_id_scoped_token_with_user_id_no_catalog(self): + # attempt to authenticate without requesting a project + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + project_id=self.project['id']) + r = self.post('/auth/tokens?nocatalog', body=auth_data, noauth=True) + self.assertValidProjectScopedTokenResponse(r, require_catalog=False) + self.assertEqual(r.result['token']['project']['id'], + self.project['id']) + + def test_auth_catalog_attributes(self): + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + project_id=self.project['id']) + r = self.v3_authenticate_token(auth_data) + + catalog = r.result['token']['catalog'] + self.assertEqual(1, len(catalog)) + catalog = catalog[0] + + self.assertEqual(self.service['id'], catalog['id']) + self.assertEqual(self.service['name'], catalog['name']) + self.assertEqual(self.service['type'], catalog['type']) + + endpoint = catalog['endpoints'] + self.assertEqual(1, len(endpoint)) + endpoint = endpoint[0] + + self.assertEqual(self.endpoint['id'], endpoint['id']) + self.assertEqual(self.endpoint['interface'], endpoint['interface']) + self.assertEqual(self.endpoint['region_id'], endpoint['region_id']) + self.assertEqual(self.endpoint['url'], endpoint['url']) + + def _check_disabled_endpoint_result(self, catalog, disabled_endpoint_id): + endpoints = catalog[0]['endpoints'] + endpoint_ids = [ep['id'] for ep in endpoints] + self.assertEqual([self.endpoint_id], endpoint_ids) + + def test_auth_catalog_disabled_service(self): + """On authenticate, get a catalog that excludes disabled services.""" + # although the child endpoint is enabled, the service is disabled + self.assertTrue(self.endpoint['enabled']) + self.catalog_api.update_service( + self.endpoint['service_id'], {'enabled': False}) + service = self.catalog_api.get_service(self.endpoint['service_id']) + self.assertFalse(service['enabled']) + + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + project_id=self.project['id']) + r = self.v3_authenticate_token(auth_data) + + self.assertEqual([], r.result['token']['catalog']) + + def test_auth_catalog_disabled_endpoint(self): + """On authenticate, get a catalog that excludes disabled endpoints.""" + + # Create a disabled endpoint that's like the enabled one. + disabled_endpoint_ref = copy.copy(self.endpoint) + disabled_endpoint_id = uuid.uuid4().hex + disabled_endpoint_ref.update({ + 'id': disabled_endpoint_id, + 'enabled': False, + 'interface': 'internal' + }) + self.catalog_api.create_endpoint(disabled_endpoint_id, + disabled_endpoint_ref) + + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + project_id=self.project['id']) + r = self.v3_authenticate_token(auth_data) + + self._check_disabled_endpoint_result(r.result['token']['catalog'], + disabled_endpoint_id) + + def test_project_id_scoped_token_with_user_id_401(self): + project = self.new_project_ref(domain_id=self.domain_id) + self.resource_api.create_project(project['id'], project) + + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + project_id=project['id']) + self.v3_authenticate_token(auth_data, expected_status=401) + + def test_user_and_group_roles_scoped_token(self): + """Test correct roles are returned in scoped token. + + Test Plan: + + - Create a domain, with 1 project, 2 users (user1 and user2) + and 2 groups (group1 and group2) + - Make user1 a member of group1, user2 a member of group2 + - Create 8 roles, assigning them to each of the 8 combinations + of users/groups on domain/project + - Get a project scoped token for user1, checking that the right + two roles are returned (one directly assigned, one by virtue + of group membership) + - Repeat this for a domain scoped token + - Make user1 also a member of group2 + - Get another scoped token making sure the additional role + shows up + - User2 is just here as a spoiler, to make sure we don't get + any roles uniquely assigned to it returned in any of our + tokens + + """ + + domainA = self.new_domain_ref() + self.resource_api.create_domain(domainA['id'], domainA) + projectA = self.new_project_ref(domain_id=domainA['id']) + self.resource_api.create_project(projectA['id'], projectA) + + user1 = self.new_user_ref( + domain_id=domainA['id']) + password = user1['password'] + user1 = self.identity_api.create_user(user1) + user1['password'] = password + + user2 = self.new_user_ref( + domain_id=domainA['id']) + password = user2['password'] + user2 = self.identity_api.create_user(user2) + user2['password'] = password + + group1 = self.new_group_ref( + domain_id=domainA['id']) + group1 = self.identity_api.create_group(group1) + + group2 = self.new_group_ref( + domain_id=domainA['id']) + group2 = self.identity_api.create_group(group2) + + self.identity_api.add_user_to_group(user1['id'], + group1['id']) + self.identity_api.add_user_to_group(user2['id'], + group2['id']) + + # Now create all the roles and assign them + role_list = [] + for _ in range(8): + role = self.new_role_ref() + self.role_api.create_role(role['id'], role) + role_list.append(role) + + self.assignment_api.create_grant(role_list[0]['id'], + user_id=user1['id'], + domain_id=domainA['id']) + self.assignment_api.create_grant(role_list[1]['id'], + user_id=user1['id'], + project_id=projectA['id']) + self.assignment_api.create_grant(role_list[2]['id'], + user_id=user2['id'], + domain_id=domainA['id']) + self.assignment_api.create_grant(role_list[3]['id'], + user_id=user2['id'], + project_id=projectA['id']) + self.assignment_api.create_grant(role_list[4]['id'], + group_id=group1['id'], + domain_id=domainA['id']) + self.assignment_api.create_grant(role_list[5]['id'], + group_id=group1['id'], + project_id=projectA['id']) + self.assignment_api.create_grant(role_list[6]['id'], + group_id=group2['id'], + domain_id=domainA['id']) + self.assignment_api.create_grant(role_list[7]['id'], + group_id=group2['id'], + project_id=projectA['id']) + + # First, get a project scoped token - which should + # contain the direct user role and the one by virtue + # of group membership + auth_data = self.build_authentication_request( + user_id=user1['id'], + password=user1['password'], + project_id=projectA['id']) + r = self.v3_authenticate_token(auth_data) + token = self.assertValidScopedTokenResponse(r) + roles_ids = [] + for ref in token['roles']: + roles_ids.append(ref['id']) + self.assertEqual(2, len(token['roles'])) + self.assertIn(role_list[1]['id'], roles_ids) + self.assertIn(role_list[5]['id'], roles_ids) + + # Now the same thing for a domain scoped token + auth_data = self.build_authentication_request( + user_id=user1['id'], + password=user1['password'], + domain_id=domainA['id']) + r = self.v3_authenticate_token(auth_data) + token = self.assertValidScopedTokenResponse(r) + roles_ids = [] + for ref in token['roles']: + roles_ids.append(ref['id']) + self.assertEqual(2, len(token['roles'])) + self.assertIn(role_list[0]['id'], roles_ids) + self.assertIn(role_list[4]['id'], roles_ids) + + # Finally, add user1 to the 2nd group, and get a new + # scoped token - the extra role should now be included + # by virtue of the 2nd group + self.identity_api.add_user_to_group(user1['id'], + group2['id']) + auth_data = self.build_authentication_request( + user_id=user1['id'], + password=user1['password'], + project_id=projectA['id']) + r = self.v3_authenticate_token(auth_data) + token = self.assertValidScopedTokenResponse(r) + roles_ids = [] + for ref in token['roles']: + roles_ids.append(ref['id']) + self.assertEqual(3, len(token['roles'])) + self.assertIn(role_list[1]['id'], roles_ids) + self.assertIn(role_list[5]['id'], roles_ids) + self.assertIn(role_list[7]['id'], roles_ids) + + def test_auth_token_cross_domain_group_and_project(self): + """Verify getting a token in cross domain group/project roles.""" + # create domain, project and group and grant roles to user + domain1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(domain1['id'], domain1) + project1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, + 'domain_id': domain1['id']} + self.resource_api.create_project(project1['id'], project1) + user_foo = self.new_user_ref(domain_id=test_v3.DEFAULT_DOMAIN_ID) + password = user_foo['password'] + user_foo = self.identity_api.create_user(user_foo) + user_foo['password'] = password + role_member = {'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex} + self.role_api.create_role(role_member['id'], role_member) + role_admin = {'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex} + self.role_api.create_role(role_admin['id'], role_admin) + role_foo_domain1 = {'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex} + self.role_api.create_role(role_foo_domain1['id'], role_foo_domain1) + role_group_domain1 = {'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex} + self.role_api.create_role(role_group_domain1['id'], role_group_domain1) + self.assignment_api.add_user_to_project(project1['id'], + user_foo['id']) + new_group = {'domain_id': domain1['id'], 'name': uuid.uuid4().hex} + new_group = self.identity_api.create_group(new_group) + self.identity_api.add_user_to_group(user_foo['id'], + new_group['id']) + self.assignment_api.create_grant( + user_id=user_foo['id'], + project_id=project1['id'], + role_id=role_member['id']) + self.assignment_api.create_grant( + group_id=new_group['id'], + project_id=project1['id'], + role_id=role_admin['id']) + self.assignment_api.create_grant( + user_id=user_foo['id'], + domain_id=domain1['id'], + role_id=role_foo_domain1['id']) + self.assignment_api.create_grant( + group_id=new_group['id'], + domain_id=domain1['id'], + role_id=role_group_domain1['id']) + + # Get a scoped token for the project + auth_data = self.build_authentication_request( + username=user_foo['name'], + user_domain_id=test_v3.DEFAULT_DOMAIN_ID, + password=user_foo['password'], + project_name=project1['name'], + project_domain_id=domain1['id']) + + r = self.v3_authenticate_token(auth_data) + scoped_token = self.assertValidScopedTokenResponse(r) + project = scoped_token["project"] + roles_ids = [] + for ref in scoped_token['roles']: + roles_ids.append(ref['id']) + self.assertEqual(project1['id'], project["id"]) + self.assertIn(role_member['id'], roles_ids) + self.assertIn(role_admin['id'], roles_ids) + self.assertNotIn(role_foo_domain1['id'], roles_ids) + self.assertNotIn(role_group_domain1['id'], roles_ids) + + def test_project_id_scoped_token_with_user_domain_id(self): + auth_data = self.build_authentication_request( + username=self.user['name'], + user_domain_id=self.domain['id'], + password=self.user['password'], + project_id=self.project['id']) + r = self.v3_authenticate_token(auth_data) + self.assertValidProjectScopedTokenResponse(r) + + def test_project_id_scoped_token_with_user_domain_name(self): + auth_data = self.build_authentication_request( + username=self.user['name'], + user_domain_name=self.domain['name'], + password=self.user['password'], + project_id=self.project['id']) + r = self.v3_authenticate_token(auth_data) + self.assertValidProjectScopedTokenResponse(r) + + def test_domain_id_scoped_token_with_user_id(self): + path = '/domains/%s/users/%s/roles/%s' % ( + self.domain['id'], self.user['id'], self.role['id']) + self.put(path=path) + + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + domain_id=self.domain['id']) + r = self.v3_authenticate_token(auth_data) + self.assertValidDomainScopedTokenResponse(r) + + def test_domain_id_scoped_token_with_user_domain_id(self): + path = '/domains/%s/users/%s/roles/%s' % ( + self.domain['id'], self.user['id'], self.role['id']) + self.put(path=path) + + auth_data = self.build_authentication_request( + username=self.user['name'], + user_domain_id=self.domain['id'], + password=self.user['password'], + domain_id=self.domain['id']) + r = self.v3_authenticate_token(auth_data) + self.assertValidDomainScopedTokenResponse(r) + + def test_domain_id_scoped_token_with_user_domain_name(self): + path = '/domains/%s/users/%s/roles/%s' % ( + self.domain['id'], self.user['id'], self.role['id']) + self.put(path=path) + + auth_data = self.build_authentication_request( + username=self.user['name'], + user_domain_name=self.domain['name'], + password=self.user['password'], + domain_id=self.domain['id']) + r = self.v3_authenticate_token(auth_data) + self.assertValidDomainScopedTokenResponse(r) + + def test_domain_name_scoped_token_with_user_id(self): + path = '/domains/%s/users/%s/roles/%s' % ( + self.domain['id'], self.user['id'], self.role['id']) + self.put(path=path) + + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + domain_name=self.domain['name']) + r = self.v3_authenticate_token(auth_data) + self.assertValidDomainScopedTokenResponse(r) + + def test_domain_name_scoped_token_with_user_domain_id(self): + path = '/domains/%s/users/%s/roles/%s' % ( + self.domain['id'], self.user['id'], self.role['id']) + self.put(path=path) + + auth_data = self.build_authentication_request( + username=self.user['name'], + user_domain_id=self.domain['id'], + password=self.user['password'], + domain_name=self.domain['name']) + r = self.v3_authenticate_token(auth_data) + self.assertValidDomainScopedTokenResponse(r) + + def test_domain_name_scoped_token_with_user_domain_name(self): + path = '/domains/%s/users/%s/roles/%s' % ( + self.domain['id'], self.user['id'], self.role['id']) + self.put(path=path) + + auth_data = self.build_authentication_request( + username=self.user['name'], + user_domain_name=self.domain['name'], + password=self.user['password'], + domain_name=self.domain['name']) + r = self.v3_authenticate_token(auth_data) + self.assertValidDomainScopedTokenResponse(r) + + def test_domain_scope_token_with_group_role(self): + group = self.new_group_ref( + domain_id=self.domain_id) + group = self.identity_api.create_group(group) + + # add user to group + self.identity_api.add_user_to_group(self.user['id'], group['id']) + + # grant the domain role to group + path = '/domains/%s/groups/%s/roles/%s' % ( + self.domain['id'], group['id'], self.role['id']) + self.put(path=path) + + # now get a domain-scoped token + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + domain_id=self.domain['id']) + r = self.v3_authenticate_token(auth_data) + self.assertValidDomainScopedTokenResponse(r) + + def test_domain_scope_token_with_name(self): + # grant the domain role to user + path = '/domains/%s/users/%s/roles/%s' % ( + self.domain['id'], self.user['id'], self.role['id']) + self.put(path=path) + # now get a domain-scoped token + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + domain_name=self.domain['name']) + r = self.v3_authenticate_token(auth_data) + self.assertValidDomainScopedTokenResponse(r) + + def test_domain_scope_failed(self): + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + domain_id=self.domain['id']) + self.v3_authenticate_token(auth_data, expected_status=401) + + def test_auth_with_id(self): + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password']) + r = self.v3_authenticate_token(auth_data) + self.assertValidUnscopedTokenResponse(r) + + token = r.headers.get('X-Subject-Token') + + # test token auth + auth_data = self.build_authentication_request(token=token) + r = self.v3_authenticate_token(auth_data) + self.assertValidUnscopedTokenResponse(r) + + def get_v2_token(self, tenant_id=None): + body = { + 'auth': { + 'passwordCredentials': { + 'username': self.default_domain_user['name'], + 'password': self.default_domain_user['password'], + }, + }, + } + r = self.admin_request(method='POST', path='/v2.0/tokens', body=body) + return r + + def test_validate_v2_unscoped_token_with_v3_api(self): + v2_token = self.get_v2_token().result['access']['token']['id'] + auth_data = self.build_authentication_request(token=v2_token) + r = self.v3_authenticate_token(auth_data) + self.assertValidUnscopedTokenResponse(r) + + def test_validate_v2_scoped_token_with_v3_api(self): + v2_response = self.get_v2_token( + tenant_id=self.default_domain_project['id']) + result = v2_response.result + v2_token = result['access']['token']['id'] + auth_data = self.build_authentication_request( + token=v2_token, + project_id=self.default_domain_project['id']) + r = self.v3_authenticate_token(auth_data) + self.assertValidScopedTokenResponse(r) + + def test_invalid_user_id(self): + auth_data = self.build_authentication_request( + user_id=uuid.uuid4().hex, + password=self.user['password']) + self.v3_authenticate_token(auth_data, expected_status=401) + + def test_invalid_user_name(self): + auth_data = self.build_authentication_request( + username=uuid.uuid4().hex, + user_domain_id=self.domain['id'], + password=self.user['password']) + self.v3_authenticate_token(auth_data, expected_status=401) + + def test_invalid_domain_id(self): + auth_data = self.build_authentication_request( + username=self.user['name'], + user_domain_id=uuid.uuid4().hex, + password=self.user['password']) + self.v3_authenticate_token(auth_data, expected_status=401) + + def test_invalid_domain_name(self): + auth_data = self.build_authentication_request( + username=self.user['name'], + user_domain_name=uuid.uuid4().hex, + password=self.user['password']) + self.v3_authenticate_token(auth_data, expected_status=401) + + def test_invalid_password(self): + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=uuid.uuid4().hex) + self.v3_authenticate_token(auth_data, expected_status=401) + + def test_remote_user_no_realm(self): + CONF.auth.methods = 'external' + api = auth.controllers.Auth() + context, auth_info, auth_context = self.build_external_auth_request( + self.default_domain_user['name']) + api.authenticate(context, auth_info, auth_context) + self.assertEqual(auth_context['user_id'], + self.default_domain_user['id']) + # Now test to make sure the user name can, itself, contain the + # '@' character. + user = {'name': 'myname@mydivision'} + self.identity_api.update_user(self.default_domain_user['id'], user) + context, auth_info, auth_context = self.build_external_auth_request( + user["name"]) + api.authenticate(context, auth_info, auth_context) + self.assertEqual(auth_context['user_id'], + self.default_domain_user['id']) + + def test_remote_user_no_domain(self): + api = auth.controllers.Auth() + context, auth_info, auth_context = self.build_external_auth_request( + self.user['name']) + self.assertRaises(exception.Unauthorized, + api.authenticate, + context, + auth_info, + auth_context) + + def test_remote_user_and_password(self): + # both REMOTE_USER and password methods must pass. + # note that they do not have to match + api = auth.controllers.Auth() + auth_data = self.build_authentication_request( + user_domain_id=self.default_domain_user['domain_id'], + username=self.default_domain_user['name'], + password=self.default_domain_user['password'])['auth'] + context, auth_info, auth_context = self.build_external_auth_request( + self.default_domain_user['name'], auth_data=auth_data) + + api.authenticate(context, auth_info, auth_context) + + def test_remote_user_and_explicit_external(self): + # both REMOTE_USER and password methods must pass. + # note that they do not have to match + auth_data = self.build_authentication_request( + user_domain_id=self.domain['id'], + username=self.user['name'], + password=self.user['password'])['auth'] + auth_data['identity']['methods'] = ["password", "external"] + auth_data['identity']['external'] = {} + api = auth.controllers.Auth() + auth_info = auth.controllers.AuthInfo(None, auth_data) + auth_context = {'extras': {}, 'method_names': []} + self.assertRaises(exception.Unauthorized, + api.authenticate, + self.empty_context, + auth_info, + auth_context) + + def test_remote_user_bad_password(self): + # both REMOTE_USER and password methods must pass. + api = auth.controllers.Auth() + auth_data = self.build_authentication_request( + user_domain_id=self.domain['id'], + username=self.user['name'], + password='badpassword')['auth'] + context, auth_info, auth_context = self.build_external_auth_request( + self.default_domain_user['name'], auth_data=auth_data) + self.assertRaises(exception.Unauthorized, + api.authenticate, + context, + auth_info, + auth_context) + + def test_bind_not_set_with_remote_user(self): + self.config_fixture.config(group='token', bind=[]) + auth_data = self.build_authentication_request() + remote_user = self.default_domain_user['name'] + self.admin_app.extra_environ.update({'REMOTE_USER': remote_user, + 'AUTH_TYPE': 'Negotiate'}) + r = self.v3_authenticate_token(auth_data) + token = self.assertValidUnscopedTokenResponse(r) + self.assertNotIn('bind', token) + + # TODO(ayoung): move to TestPKITokenAPIs; it will be run for both formats + def test_verify_with_bound_token(self): + self.config_fixture.config(group='token', bind='kerberos') + auth_data = self.build_authentication_request( + project_id=self.project['id']) + remote_user = self.default_domain_user['name'] + self.admin_app.extra_environ.update({'REMOTE_USER': remote_user, + 'AUTH_TYPE': 'Negotiate'}) + + token = self.get_requested_token(auth_data) + headers = {'X-Subject-Token': token} + r = self.get('/auth/tokens', headers=headers, token=token) + token = self.assertValidProjectScopedTokenResponse(r) + self.assertEqual(token['bind']['kerberos'], + self.default_domain_user['name']) + + def test_auth_with_bind_token(self): + self.config_fixture.config(group='token', bind=['kerberos']) + + auth_data = self.build_authentication_request() + remote_user = self.default_domain_user['name'] + self.admin_app.extra_environ.update({'REMOTE_USER': remote_user, + 'AUTH_TYPE': 'Negotiate'}) + r = self.v3_authenticate_token(auth_data) + + # the unscoped token should have bind information in it + token = self.assertValidUnscopedTokenResponse(r) + self.assertEqual(token['bind']['kerberos'], remote_user) + + token = r.headers.get('X-Subject-Token') + + # using unscoped token with remote user succeeds + auth_params = {'token': token, 'project_id': self.project_id} + auth_data = self.build_authentication_request(**auth_params) + r = self.v3_authenticate_token(auth_data) + token = self.assertValidProjectScopedTokenResponse(r) + + # the bind information should be carried over from the original token + self.assertEqual(token['bind']['kerberos'], remote_user) + + def test_v2_v3_bind_token_intermix(self): + self.config_fixture.config(group='token', bind='kerberos') + + # we need our own user registered to the default domain because of + # the way external auth works. + remote_user = self.default_domain_user['name'] + self.admin_app.extra_environ.update({'REMOTE_USER': remote_user, + 'AUTH_TYPE': 'Negotiate'}) + body = {'auth': {}} + resp = self.admin_request(path='/v2.0/tokens', + method='POST', + body=body) + + v2_token_data = resp.result + + bind = v2_token_data['access']['token']['bind'] + self.assertEqual(bind['kerberos'], self.default_domain_user['name']) + + v2_token_id = v2_token_data['access']['token']['id'] + # NOTE(gyee): self.get() will try to obtain an auth token if one + # is not provided. When REMOTE_USER is present in the request + # environment, the external user auth plugin is used in conjunction + # with the password auth for the admin user. Therefore, we need to + # cleanup the REMOTE_USER information from the previous call. + del self.admin_app.extra_environ['REMOTE_USER'] + headers = {'X-Subject-Token': v2_token_id} + resp = self.get('/auth/tokens', headers=headers) + token_data = resp.result + + self.assertDictEqual(v2_token_data['access']['token']['bind'], + token_data['token']['bind']) + + def test_authenticating_a_user_with_no_password(self): + user = self.new_user_ref(domain_id=self.domain['id']) + user.pop('password', None) # can't have a password for this test + user = self.identity_api.create_user(user) + + auth_data = self.build_authentication_request( + user_id=user['id'], + password='password') + + self.v3_authenticate_token(auth_data, expected_status=401) + + def test_disabled_default_project_result_in_unscoped_token(self): + # create a disabled project to work with + project = self.create_new_default_project_for_user( + self.user['id'], self.domain_id, enable_project=False) + + # assign a role to user for the new project + self.assignment_api.add_role_to_user_and_project(self.user['id'], + project['id'], + self.role_id) + + # attempt to authenticate without requesting a project + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password']) + r = self.v3_authenticate_token(auth_data) + self.assertValidUnscopedTokenResponse(r) + + def test_disabled_default_project_domain_result_in_unscoped_token(self): + domain_ref = self.new_domain_ref() + r = self.post('/domains', body={'domain': domain_ref}) + domain = self.assertValidDomainResponse(r, domain_ref) + + project = self.create_new_default_project_for_user( + self.user['id'], domain['id']) + + # assign a role to user for the new project + self.assignment_api.add_role_to_user_and_project(self.user['id'], + project['id'], + self.role_id) + + # now disable the project domain + body = {'domain': {'enabled': False}} + r = self.patch('/domains/%(domain_id)s' % {'domain_id': domain['id']}, + body=body) + self.assertValidDomainResponse(r) + + # attempt to authenticate without requesting a project + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password']) + r = self.v3_authenticate_token(auth_data) + self.assertValidUnscopedTokenResponse(r) + + def test_no_access_to_default_project_result_in_unscoped_token(self): + # create a disabled project to work with + self.create_new_default_project_for_user(self.user['id'], + self.domain_id) + + # attempt to authenticate without requesting a project + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password']) + r = self.v3_authenticate_token(auth_data) + self.assertValidUnscopedTokenResponse(r) + + def test_disabled_scope_project_domain_result_in_401(self): + # create a disabled domain + domain = self.new_domain_ref() + domain['enabled'] = False + self.resource_api.create_domain(domain['id'], domain) + + # create a project in the disabled domain + project = self.new_project_ref(domain_id=domain['id']) + self.resource_api.create_project(project['id'], project) + + # assign some role to self.user for the project in the disabled domain + self.assignment_api.add_role_to_user_and_project( + self.user['id'], + project['id'], + self.role_id) + + # user should not be able to auth with project_id + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + project_id=project['id']) + self.v3_authenticate_token(auth_data, expected_status=401) + + # user should not be able to auth with project_name & domain + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + project_name=project['name'], + project_domain_id=domain['id']) + self.v3_authenticate_token(auth_data, expected_status=401) + + def test_auth_methods_with_different_identities_fails(self): + # get the token for a user. This is self.user which is different from + # self.default_domain_user. + token = self.get_scoped_token() + # try both password and token methods with different identities and it + # should fail + auth_data = self.build_authentication_request( + token=token, + user_id=self.default_domain_user['id'], + password=self.default_domain_user['password']) + self.v3_authenticate_token(auth_data, expected_status=401) + + +class TestAuthJSONExternal(test_v3.RestfulTestCase): + content_type = 'json' + + def config_overrides(self): + super(TestAuthJSONExternal, self).config_overrides() + self.config_fixture.config(group='auth', methods='') + + def auth_plugin_config_override(self, methods=None, **method_classes): + self.config_fixture.config(group='auth', methods='') + + def test_remote_user_no_method(self): + api = auth.controllers.Auth() + context, auth_info, auth_context = self.build_external_auth_request( + self.default_domain_user['name']) + self.assertRaises(exception.Unauthorized, + api.authenticate, + context, + auth_info, + auth_context) + + +class TestTrustOptional(test_v3.RestfulTestCase): + def config_overrides(self): + super(TestTrustOptional, self).config_overrides() + self.config_fixture.config(group='trust', enabled=False) + + def test_trusts_404(self): + self.get('/OS-TRUST/trusts', body={'trust': {}}, expected_status=404) + self.post('/OS-TRUST/trusts', body={'trust': {}}, expected_status=404) + + def test_auth_with_scope_in_trust_403(self): + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + trust_id=uuid.uuid4().hex) + self.v3_authenticate_token(auth_data, expected_status=403) + + +class TestTrustRedelegation(test_v3.RestfulTestCase): + """Redelegation valid and secure + + Redelegation is a hierarchical structure of trusts between initial trustor + and a group of users allowed to impersonate trustor and act in his name. + Hierarchy is created in a process of trusting already trusted permissions + and organized as an adjacency list using 'redelegated_trust_id' field. + Redelegation is valid if each subsequent trust in a chain passes 'not more' + permissions than being redelegated. + + Trust constraints are: + * roles - set of roles trusted by trustor + * expiration_time + * allow_redelegation - a flag + * redelegation_count - decreasing value restricting length of trust chain + * remaining_uses - DISALLOWED when allow_redelegation == True + + Trust becomes invalid in case: + * trust roles were revoked from trustor + * one of the users in the delegation chain was disabled or deleted + * expiration time passed + * one of the parent trusts has become invalid + * one of the parent trusts was deleted + + """ + + def config_overrides(self): + super(TestTrustRedelegation, self).config_overrides() + self.config_fixture.config( + group='trust', + enabled=True, + allow_redelegation=True, + max_redelegation_count=10 + ) + + def setUp(self): + super(TestTrustRedelegation, self).setUp() + # Create a trustee to delegate stuff to + trustee_user_ref = self.new_user_ref(domain_id=self.domain_id) + self.trustee_user = self.identity_api.create_user(trustee_user_ref) + self.trustee_user['password'] = trustee_user_ref['password'] + + # trustor->trustee + self.redelegated_trust_ref = self.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user['id'], + project_id=self.project_id, + impersonation=True, + expires=dict(minutes=1), + role_ids=[self.role_id], + allow_redelegation=True) + + # trustor->trustee (no redelegation) + self.chained_trust_ref = self.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user['id'], + project_id=self.project_id, + impersonation=True, + role_ids=[self.role_id], + allow_redelegation=True) + + def _get_trust_token(self, trust): + trust_id = trust['id'] + auth_data = self.build_authentication_request( + user_id=self.trustee_user['id'], + password=self.trustee_user['password'], + trust_id=trust_id) + trust_token = self.get_requested_token(auth_data) + return trust_token + + def test_depleted_redelegation_count_error(self): + self.redelegated_trust_ref['redelegation_count'] = 0 + r = self.post('/OS-TRUST/trusts', + body={'trust': self.redelegated_trust_ref}) + trust = self.assertValidTrustResponse(r) + trust_token = self._get_trust_token(trust) + + # Attempt to create a redelegated trust. + self.post('/OS-TRUST/trusts', + body={'trust': self.chained_trust_ref}, + token=trust_token, + expected_status=403) + + def test_modified_redelegation_count_error(self): + r = self.post('/OS-TRUST/trusts', + body={'trust': self.redelegated_trust_ref}) + trust = self.assertValidTrustResponse(r) + trust_token = self._get_trust_token(trust) + + # Attempt to create a redelegated trust with incorrect + # redelegation_count. + correct = trust['redelegation_count'] - 1 + incorrect = correct - 1 + self.chained_trust_ref['redelegation_count'] = incorrect + self.post('/OS-TRUST/trusts', + body={'trust': self.chained_trust_ref}, + token=trust_token, + expected_status=403) + + def test_max_redelegation_count_constraint(self): + incorrect = CONF.trust.max_redelegation_count + 1 + self.redelegated_trust_ref['redelegation_count'] = incorrect + self.post('/OS-TRUST/trusts', + body={'trust': self.redelegated_trust_ref}, + expected_status=403) + + def test_redelegation_expiry(self): + r = self.post('/OS-TRUST/trusts', + body={'trust': self.redelegated_trust_ref}) + trust = self.assertValidTrustResponse(r) + trust_token = self._get_trust_token(trust) + + # Attempt to create a redelegated trust supposed to last longer + # than the parent trust: let's give it 10 minutes (>1 minute). + too_long_live_chained_trust_ref = self.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user['id'], + project_id=self.project_id, + impersonation=True, + expires=dict(minutes=10), + role_ids=[self.role_id]) + self.post('/OS-TRUST/trusts', + body={'trust': too_long_live_chained_trust_ref}, + token=trust_token, + expected_status=403) + + def test_redelegation_remaining_uses(self): + r = self.post('/OS-TRUST/trusts', + body={'trust': self.redelegated_trust_ref}) + trust = self.assertValidTrustResponse(r) + trust_token = self._get_trust_token(trust) + + # Attempt to create a redelegated trust with remaining_uses defined. + # It must fail according to specification: remaining_uses must be + # omitted for trust redelegation. Any number here. + self.chained_trust_ref['remaining_uses'] = 5 + self.post('/OS-TRUST/trusts', + body={'trust': self.chained_trust_ref}, + token=trust_token, + expected_status=403) + + def test_roles_subset(self): + # Build second role + role = self.new_role_ref() + self.assignment_api.create_role(role['id'], role) + # assign a new role to the user + self.assignment_api.create_grant(role_id=role['id'], + user_id=self.user_id, + project_id=self.project_id) + + # Create first trust with extended set of roles + ref = self.redelegated_trust_ref + ref['roles'].append({'id': role['id']}) + r = self.post('/OS-TRUST/trusts', + body={'trust': ref}) + trust = self.assertValidTrustResponse(r) + # Trust created with exact set of roles (checked by role id) + role_id_set = set(r['id'] for r in ref['roles']) + trust_role_id_set = set(r['id'] for r in trust['roles']) + self.assertEqual(role_id_set, trust_role_id_set) + + trust_token = self._get_trust_token(trust) + + # Chain second trust with roles subset + r = self.post('/OS-TRUST/trusts', + body={'trust': self.chained_trust_ref}, + token=trust_token) + trust2 = self.assertValidTrustResponse(r) + # First trust contains roles superset + # Second trust contains roles subset + role_id_set1 = set(r['id'] for r in trust['roles']) + role_id_set2 = set(r['id'] for r in trust2['roles']) + self.assertThat(role_id_set1, matchers.GreaterThan(role_id_set2)) + + def test_redelegate_with_role_by_name(self): + # For role by name testing + ref = self.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user['id'], + project_id=self.project_id, + impersonation=True, + expires=dict(minutes=1), + role_names=[self.role['name']], + allow_redelegation=True) + r = self.post('/OS-TRUST/trusts', + body={'trust': ref}) + trust = self.assertValidTrustResponse(r) + # Ensure we can get a token with this trust + trust_token = self._get_trust_token(trust) + # Chain second trust with roles subset + ref = self.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user['id'], + project_id=self.project_id, + impersonation=True, + role_names=[self.role['name']], + allow_redelegation=True) + r = self.post('/OS-TRUST/trusts', + body={'trust': ref}, + token=trust_token) + trust = self.assertValidTrustResponse(r) + # Ensure we can get a token with this trust + self._get_trust_token(trust) + + def test_redelegate_new_role_fails(self): + r = self.post('/OS-TRUST/trusts', + body={'trust': self.redelegated_trust_ref}) + trust = self.assertValidTrustResponse(r) + trust_token = self._get_trust_token(trust) + + # Build second trust with a role not in parent's roles + role = self.new_role_ref() + self.assignment_api.create_role(role['id'], role) + # assign a new role to the user + self.assignment_api.create_grant(role_id=role['id'], + user_id=self.user_id, + project_id=self.project_id) + + # Try to chain a trust with the role not from parent trust + self.chained_trust_ref['roles'] = [{'id': role['id']}] + + # Bypass policy enforcement + with mock.patch.object(rules, 'enforce', return_value=True): + self.post('/OS-TRUST/trusts', + body={'trust': self.chained_trust_ref}, + token=trust_token, + expected_status=403) + + def test_redelegation_terminator(self): + r = self.post('/OS-TRUST/trusts', + body={'trust': self.redelegated_trust_ref}) + trust = self.assertValidTrustResponse(r) + trust_token = self._get_trust_token(trust) + + # Build second trust - the terminator + ref = dict(self.chained_trust_ref, + redelegation_count=1, + allow_redelegation=False) + + r = self.post('/OS-TRUST/trusts', + body={'trust': ref}, + token=trust_token) + + trust = self.assertValidTrustResponse(r) + # Check that allow_redelegation == False caused redelegation_count + # to be set to 0, while allow_redelegation is removed + self.assertNotIn('allow_redelegation', trust) + self.assertEqual(trust['redelegation_count'], 0) + trust_token = self._get_trust_token(trust) + + # Build third trust, same as second + self.post('/OS-TRUST/trusts', + body={'trust': ref}, + token=trust_token, + expected_status=403) + + +class TestTrustChain(test_v3.RestfulTestCase): + + def config_overrides(self): + super(TestTrustChain, self).config_overrides() + self.config_fixture.config( + group='trust', + enabled=True, + allow_redelegation=True, + max_redelegation_count=10 + ) + + def setUp(self): + super(TestTrustChain, self).setUp() + # Create trust chain + self.user_chain = list() + self.trust_chain = list() + for _ in xrange(3): + user_ref = self.new_user_ref(domain_id=self.domain_id) + user = self.identity_api.create_user(user_ref) + user['password'] = user_ref['password'] + self.user_chain.append(user) + + # trustor->trustee + trustee = self.user_chain[0] + trust_ref = self.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=trustee['id'], + project_id=self.project_id, + impersonation=True, + expires=dict(minutes=1), + role_ids=[self.role_id]) + trust_ref.update( + allow_redelegation=True, + redelegation_count=3) + + r = self.post('/OS-TRUST/trusts', + body={'trust': trust_ref}) + + trust = self.assertValidTrustResponse(r) + auth_data = self.build_authentication_request( + user_id=trustee['id'], + password=trustee['password'], + trust_id=trust['id']) + trust_token = self.get_requested_token(auth_data) + self.trust_chain.append(trust) + + for trustee in self.user_chain[1:]: + trust_ref = self.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=trustee['id'], + project_id=self.project_id, + impersonation=True, + role_ids=[self.role_id]) + trust_ref.update( + allow_redelegation=True) + r = self.post('/OS-TRUST/trusts', + body={'trust': trust_ref}, + token=trust_token) + trust = self.assertValidTrustResponse(r) + auth_data = self.build_authentication_request( + user_id=trustee['id'], + password=trustee['password'], + trust_id=trust['id']) + trust_token = self.get_requested_token(auth_data) + self.trust_chain.append(trust) + + trustee = self.user_chain[-1] + trust = self.trust_chain[-1] + auth_data = self.build_authentication_request( + user_id=trustee['id'], + password=trustee['password'], + trust_id=trust['id']) + + self.last_token = self.get_requested_token(auth_data) + + def assert_user_authenticate(self, user): + auth_data = self.build_authentication_request( + user_id=user['id'], + password=user['password'] + ) + r = self.v3_authenticate_token(auth_data) + self.assertValidTokenResponse(r) + + def assert_trust_tokens_revoked(self, trust_id): + trustee = self.user_chain[0] + auth_data = self.build_authentication_request( + user_id=trustee['id'], + password=trustee['password'] + ) + r = self.v3_authenticate_token(auth_data) + self.assertValidTokenResponse(r) + + revocation_response = self.get('/OS-REVOKE/events') + revocation_events = revocation_response.json_body['events'] + found = False + for event in revocation_events: + if event.get('OS-TRUST:trust_id') == trust_id: + found = True + self.assertTrue(found, 'event with trust_id %s not found in list' % + trust_id) + + def test_delete_trust_cascade(self): + self.assert_user_authenticate(self.user_chain[0]) + self.delete('/OS-TRUST/trusts/%(trust_id)s' % { + 'trust_id': self.trust_chain[0]['id']}, + expected_status=204) + + headers = {'X-Subject-Token': self.last_token} + self.head('/auth/tokens', headers=headers, expected_status=404) + self.assert_trust_tokens_revoked(self.trust_chain[0]['id']) + + def test_delete_broken_chain(self): + self.assert_user_authenticate(self.user_chain[0]) + self.delete('/OS-TRUST/trusts/%(trust_id)s' % { + 'trust_id': self.trust_chain[1]['id']}, + expected_status=204) + + self.delete('/OS-TRUST/trusts/%(trust_id)s' % { + 'trust_id': self.trust_chain[0]['id']}, + expected_status=204) + + def test_trustor_roles_revoked(self): + self.assert_user_authenticate(self.user_chain[0]) + + self.assignment_api.remove_role_from_user_and_project( + self.user_id, self.project_id, self.role_id + ) + + auth_data = self.build_authentication_request( + token=self.last_token, + trust_id=self.trust_chain[-1]['id']) + self.v3_authenticate_token(auth_data, expected_status=404) + + def test_intermediate_user_disabled(self): + self.assert_user_authenticate(self.user_chain[0]) + + disabled = self.user_chain[0] + disabled['enabled'] = False + self.identity_api.update_user(disabled['id'], disabled) + + # Bypass policy enforcement + with mock.patch.object(rules, 'enforce', return_value=True): + headers = {'X-Subject-Token': self.last_token} + self.head('/auth/tokens', headers=headers, expected_status=403) + + def test_intermediate_user_deleted(self): + self.assert_user_authenticate(self.user_chain[0]) + + self.identity_api.delete_user(self.user_chain[0]['id']) + + # Bypass policy enforcement + with mock.patch.object(rules, 'enforce', return_value=True): + headers = {'X-Subject-Token': self.last_token} + self.head('/auth/tokens', headers=headers, expected_status=403) + + +class TestTrustAuth(test_v3.RestfulTestCase): + EXTENSION_NAME = 'revoke' + EXTENSION_TO_ADD = 'revoke_extension' + + def config_overrides(self): + super(TestTrustAuth, self).config_overrides() + self.config_fixture.config( + group='revoke', + driver='keystone.contrib.revoke.backends.kvs.Revoke') + self.config_fixture.config( + group='token', + provider='keystone.token.providers.pki.Provider', + revoke_by_id=False) + self.config_fixture.config(group='trust', enabled=True) + + def setUp(self): + super(TestTrustAuth, self).setUp() + + # create a trustee to delegate stuff to + self.trustee_user = self.new_user_ref(domain_id=self.domain_id) + password = self.trustee_user['password'] + self.trustee_user = self.identity_api.create_user(self.trustee_user) + self.trustee_user['password'] = password + self.trustee_user_id = self.trustee_user['id'] + + def test_create_trust_400(self): + # The server returns a 403 Forbidden rather than a 400, see bug 1133435 + self.post('/OS-TRUST/trusts', body={'trust': {}}, expected_status=403) + + def test_create_unscoped_trust(self): + ref = self.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user_id) + r = self.post('/OS-TRUST/trusts', body={'trust': ref}) + self.assertValidTrustResponse(r, ref) + + def test_create_trust_no_roles(self): + ref = self.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user_id, + project_id=self.project_id) + self.post('/OS-TRUST/trusts', body={'trust': ref}, expected_status=403) + + def _initialize_test_consume_trust(self, count): + # Make sure remaining_uses is decremented as we consume the trust + ref = self.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user_id, + project_id=self.project_id, + remaining_uses=count, + role_ids=[self.role_id]) + r = self.post('/OS-TRUST/trusts', body={'trust': ref}) + # make sure the trust exists + trust = self.assertValidTrustResponse(r, ref) + r = self.get( + '/OS-TRUST/trusts/%(trust_id)s' % {'trust_id': trust['id']}, + expected_status=200) + # get a token for the trustee + auth_data = self.build_authentication_request( + user_id=self.trustee_user['id'], + password=self.trustee_user['password']) + r = self.v3_authenticate_token(auth_data) + token = r.headers.get('X-Subject-Token') + # get a trust token, consume one use + auth_data = self.build_authentication_request( + token=token, + trust_id=trust['id']) + r = self.v3_authenticate_token(auth_data) + return trust + + def test_consume_trust_once(self): + trust = self._initialize_test_consume_trust(2) + # check decremented value + r = self.get( + '/OS-TRUST/trusts/%(trust_id)s' % {'trust_id': trust['id']}, + expected_status=200) + trust = r.result.get('trust') + self.assertIsNotNone(trust) + self.assertEqual(trust['remaining_uses'], 1) + + def test_create_one_time_use_trust(self): + trust = self._initialize_test_consume_trust(1) + # No more uses, the trust is made unavailable + self.get( + '/OS-TRUST/trusts/%(trust_id)s' % {'trust_id': trust['id']}, + expected_status=404) + # this time we can't get a trust token + auth_data = self.build_authentication_request( + user_id=self.trustee_user['id'], + password=self.trustee_user['password'], + trust_id=trust['id']) + self.v3_authenticate_token(auth_data, expected_status=401) + + def test_create_trust_with_bad_values_for_remaining_uses(self): + # negative values for the remaining_uses parameter are forbidden + self._create_trust_with_bad_remaining_use(bad_value=-1) + # 0 is a forbidden value as well + self._create_trust_with_bad_remaining_use(bad_value=0) + # as are non integer values + self._create_trust_with_bad_remaining_use(bad_value="a bad value") + self._create_trust_with_bad_remaining_use(bad_value=7.2) + + def _create_trust_with_bad_remaining_use(self, bad_value): + ref = self.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user_id, + project_id=self.project_id, + remaining_uses=bad_value, + role_ids=[self.role_id]) + self.post('/OS-TRUST/trusts', + body={'trust': ref}, + expected_status=400) + + def test_invalid_trust_request_without_impersonation(self): + ref = self.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user_id, + project_id=self.project_id, + role_ids=[self.role_id]) + + del ref['impersonation'] + + self.post('/OS-TRUST/trusts', + body={'trust': ref}, + expected_status=400) + + def test_invalid_trust_request_without_trustee(self): + ref = self.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user_id, + project_id=self.project_id, + role_ids=[self.role_id]) + + del ref['trustee_user_id'] + + self.post('/OS-TRUST/trusts', + body={'trust': ref}, + expected_status=400) + + def test_create_unlimited_use_trust(self): + # by default trusts are unlimited in terms of tokens that can be + # generated from them, this test creates such a trust explicitly + ref = self.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user_id, + project_id=self.project_id, + remaining_uses=None, + role_ids=[self.role_id]) + r = self.post('/OS-TRUST/trusts', body={'trust': ref}) + trust = self.assertValidTrustResponse(r, ref) + + r = self.get( + '/OS-TRUST/trusts/%(trust_id)s' % {'trust_id': trust['id']}, + expected_status=200) + auth_data = self.build_authentication_request( + user_id=self.trustee_user['id'], + password=self.trustee_user['password']) + r = self.v3_authenticate_token(auth_data) + token = r.headers.get('X-Subject-Token') + auth_data = self.build_authentication_request( + token=token, + trust_id=trust['id']) + r = self.v3_authenticate_token(auth_data) + r = self.get( + '/OS-TRUST/trusts/%(trust_id)s' % {'trust_id': trust['id']}, + expected_status=200) + trust = r.result.get('trust') + self.assertIsNone(trust['remaining_uses']) + + def test_trust_crud(self): + ref = self.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user_id, + project_id=self.project_id, + role_ids=[self.role_id]) + r = self.post('/OS-TRUST/trusts', body={'trust': ref}) + trust = self.assertValidTrustResponse(r, ref) + + r = self.get( + '/OS-TRUST/trusts/%(trust_id)s' % {'trust_id': trust['id']}, + expected_status=200) + self.assertValidTrustResponse(r, ref) + + # validate roles on the trust + r = self.get( + '/OS-TRUST/trusts/%(trust_id)s/roles' % { + 'trust_id': trust['id']}, + expected_status=200) + roles = self.assertValidRoleListResponse(r, self.role) + self.assertIn(self.role['id'], [x['id'] for x in roles]) + self.head( + '/OS-TRUST/trusts/%(trust_id)s/roles/%(role_id)s' % { + 'trust_id': trust['id'], + 'role_id': self.role['id']}, + expected_status=200) + r = self.get( + '/OS-TRUST/trusts/%(trust_id)s/roles/%(role_id)s' % { + 'trust_id': trust['id'], + 'role_id': self.role['id']}, + expected_status=200) + self.assertValidRoleResponse(r, self.role) + + r = self.get('/OS-TRUST/trusts', expected_status=200) + self.assertValidTrustListResponse(r, trust) + + # trusts are immutable + self.patch( + '/OS-TRUST/trusts/%(trust_id)s' % {'trust_id': trust['id']}, + body={'trust': ref}, + expected_status=404) + + self.delete( + '/OS-TRUST/trusts/%(trust_id)s' % {'trust_id': trust['id']}, + expected_status=204) + + self.get( + '/OS-TRUST/trusts/%(trust_id)s' % {'trust_id': trust['id']}, + expected_status=404) + + def test_create_trust_trustee_404(self): + ref = self.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=uuid.uuid4().hex, + project_id=self.project_id, + role_ids=[self.role_id]) + self.post('/OS-TRUST/trusts', body={'trust': ref}, expected_status=404) + + def test_create_trust_trustor_trustee_backwards(self): + ref = self.new_trust_ref( + trustor_user_id=self.trustee_user_id, + trustee_user_id=self.user_id, + project_id=self.project_id, + role_ids=[self.role_id]) + self.post('/OS-TRUST/trusts', body={'trust': ref}, expected_status=403) + + def test_create_trust_project_404(self): + ref = self.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user_id, + project_id=uuid.uuid4().hex, + role_ids=[self.role_id]) + self.post('/OS-TRUST/trusts', body={'trust': ref}, expected_status=404) + + def test_create_trust_role_id_404(self): + ref = self.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user_id, + project_id=self.project_id, + role_ids=[uuid.uuid4().hex]) + self.post('/OS-TRUST/trusts', body={'trust': ref}, expected_status=404) + + def test_create_trust_role_name_404(self): + ref = self.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user_id, + project_id=self.project_id, + role_names=[uuid.uuid4().hex]) + self.post('/OS-TRUST/trusts', body={'trust': ref}, expected_status=404) + + def test_create_expired_trust(self): + ref = self.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user_id, + project_id=self.project_id, + expires=dict(seconds=-1), + role_ids=[self.role_id]) + r = self.post('/OS-TRUST/trusts', body={'trust': ref}) + trust = self.assertValidTrustResponse(r, ref) + + self.get('/OS-TRUST/trusts/%(trust_id)s' % { + 'trust_id': trust['id']}, + expected_status=404) + + auth_data = self.build_authentication_request( + user_id=self.trustee_user['id'], + password=self.trustee_user['password'], + trust_id=trust['id']) + self.v3_authenticate_token(auth_data, expected_status=401) + + def test_v3_v2_intermix_trustor_not_in_default_domain_failed(self): + ref = self.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.default_domain_user_id, + project_id=self.project_id, + impersonation=False, + expires=dict(minutes=1), + role_ids=[self.role_id]) + + r = self.post('/OS-TRUST/trusts', body={'trust': ref}) + trust = self.assertValidTrustResponse(r) + + auth_data = self.build_authentication_request( + user_id=self.default_domain_user['id'], + password=self.default_domain_user['password'], + trust_id=trust['id']) + r = self.v3_authenticate_token(auth_data) + self.assertValidProjectTrustScopedTokenResponse( + r, self.default_domain_user) + + token = r.headers.get('X-Subject-Token') + + # now validate the v3 token with v2 API + path = '/v2.0/tokens/%s' % (token) + self.admin_request( + path=path, token='ADMIN', method='GET', expected_status=401) + + def test_v3_v2_intermix_trustor_not_in_default_domaini_failed(self): + ref = self.new_trust_ref( + trustor_user_id=self.default_domain_user_id, + trustee_user_id=self.trustee_user_id, + project_id=self.default_domain_project_id, + impersonation=False, + expires=dict(minutes=1), + role_ids=[self.role_id]) + + auth_data = self.build_authentication_request( + user_id=self.default_domain_user['id'], + password=self.default_domain_user['password'], + project_id=self.default_domain_project_id) + token = self.get_requested_token(auth_data) + + r = self.post('/OS-TRUST/trusts', body={'trust': ref}, token=token) + trust = self.assertValidTrustResponse(r) + + auth_data = self.build_authentication_request( + user_id=self.trustee_user['id'], + password=self.trustee_user['password'], + trust_id=trust['id']) + r = self.v3_authenticate_token(auth_data) + self.assertValidProjectTrustScopedTokenResponse( + r, self.trustee_user) + token = r.headers.get('X-Subject-Token') + + # now validate the v3 token with v2 API + path = '/v2.0/tokens/%s' % (token) + self.admin_request( + path=path, token='ADMIN', method='GET', expected_status=401) + + def test_v3_v2_intermix_project_not_in_default_domaini_failed(self): + # create a trustee in default domain to delegate stuff to + trustee_user = self.new_user_ref(domain_id=test_v3.DEFAULT_DOMAIN_ID) + password = trustee_user['password'] + trustee_user = self.identity_api.create_user(trustee_user) + trustee_user['password'] = password + trustee_user_id = trustee_user['id'] + + ref = self.new_trust_ref( + trustor_user_id=self.default_domain_user_id, + trustee_user_id=trustee_user_id, + project_id=self.project_id, + impersonation=False, + expires=dict(minutes=1), + role_ids=[self.role_id]) + + auth_data = self.build_authentication_request( + user_id=self.default_domain_user['id'], + password=self.default_domain_user['password'], + project_id=self.default_domain_project_id) + token = self.get_requested_token(auth_data) + + r = self.post('/OS-TRUST/trusts', body={'trust': ref}, token=token) + trust = self.assertValidTrustResponse(r) + + auth_data = self.build_authentication_request( + user_id=trustee_user['id'], + password=trustee_user['password'], + trust_id=trust['id']) + r = self.v3_authenticate_token(auth_data) + self.assertValidProjectTrustScopedTokenResponse( + r, trustee_user) + token = r.headers.get('X-Subject-Token') + + # now validate the v3 token with v2 API + path = '/v2.0/tokens/%s' % (token) + self.admin_request( + path=path, token='ADMIN', method='GET', expected_status=401) + + def test_v3_v2_intermix(self): + # create a trustee in default domain to delegate stuff to + trustee_user = self.new_user_ref(domain_id=test_v3.DEFAULT_DOMAIN_ID) + password = trustee_user['password'] + trustee_user = self.identity_api.create_user(trustee_user) + trustee_user['password'] = password + trustee_user_id = trustee_user['id'] + + ref = self.new_trust_ref( + trustor_user_id=self.default_domain_user_id, + trustee_user_id=trustee_user_id, + project_id=self.default_domain_project_id, + impersonation=False, + expires=dict(minutes=1), + role_ids=[self.role_id]) + auth_data = self.build_authentication_request( + user_id=self.default_domain_user['id'], + password=self.default_domain_user['password'], + project_id=self.default_domain_project_id) + token = self.get_requested_token(auth_data) + + r = self.post('/OS-TRUST/trusts', body={'trust': ref}, token=token) + trust = self.assertValidTrustResponse(r) + + auth_data = self.build_authentication_request( + user_id=trustee_user['id'], + password=trustee_user['password'], + trust_id=trust['id']) + r = self.v3_authenticate_token(auth_data) + self.assertValidProjectTrustScopedTokenResponse( + r, trustee_user) + token = r.headers.get('X-Subject-Token') + + # now validate the v3 token with v2 API + path = '/v2.0/tokens/%s' % (token) + self.admin_request( + path=path, token='ADMIN', method='GET', expected_status=200) + + def test_exercise_trust_scoped_token_without_impersonation(self): + ref = self.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user_id, + project_id=self.project_id, + impersonation=False, + expires=dict(minutes=1), + role_ids=[self.role_id]) + + r = self.post('/OS-TRUST/trusts', body={'trust': ref}) + trust = self.assertValidTrustResponse(r) + + auth_data = self.build_authentication_request( + user_id=self.trustee_user['id'], + password=self.trustee_user['password'], + trust_id=trust['id']) + r = self.v3_authenticate_token(auth_data) + self.assertValidProjectTrustScopedTokenResponse(r, self.trustee_user) + self.assertEqual(r.result['token']['user']['id'], + self.trustee_user['id']) + self.assertEqual(r.result['token']['user']['name'], + self.trustee_user['name']) + self.assertEqual(r.result['token']['user']['domain']['id'], + self.domain['id']) + self.assertEqual(r.result['token']['user']['domain']['name'], + self.domain['name']) + self.assertEqual(r.result['token']['project']['id'], + self.project['id']) + self.assertEqual(r.result['token']['project']['name'], + self.project['name']) + + def test_exercise_trust_scoped_token_with_impersonation(self): + ref = self.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user_id, + project_id=self.project_id, + impersonation=True, + expires=dict(minutes=1), + role_ids=[self.role_id]) + + r = self.post('/OS-TRUST/trusts', body={'trust': ref}) + trust = self.assertValidTrustResponse(r) + + auth_data = self.build_authentication_request( + user_id=self.trustee_user['id'], + password=self.trustee_user['password'], + trust_id=trust['id']) + r = self.v3_authenticate_token(auth_data) + self.assertValidProjectTrustScopedTokenResponse(r, self.user) + self.assertEqual(r.result['token']['user']['id'], self.user['id']) + self.assertEqual(r.result['token']['user']['name'], self.user['name']) + self.assertEqual(r.result['token']['user']['domain']['id'], + self.domain['id']) + self.assertEqual(r.result['token']['user']['domain']['name'], + self.domain['name']) + self.assertEqual(r.result['token']['project']['id'], + self.project['id']) + self.assertEqual(r.result['token']['project']['name'], + self.project['name']) + + def test_impersonation_token_cannot_create_new_trust(self): + ref = self.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user_id, + project_id=self.project_id, + impersonation=True, + expires=dict(minutes=1), + role_ids=[self.role_id]) + + r = self.post('/OS-TRUST/trusts', body={'trust': ref}) + trust = self.assertValidTrustResponse(r) + + auth_data = self.build_authentication_request( + user_id=self.trustee_user['id'], + password=self.trustee_user['password'], + trust_id=trust['id']) + + trust_token = self.get_requested_token(auth_data) + + # Build second trust + ref = self.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user_id, + project_id=self.project_id, + impersonation=True, + expires=dict(minutes=1), + role_ids=[self.role_id]) + + self.post('/OS-TRUST/trusts', + body={'trust': ref}, + token=trust_token, + expected_status=403) + + def test_trust_deleted_grant(self): + # create a new role + role = self.new_role_ref() + self.role_api.create_role(role['id'], role) + + grant_url = ( + '/projects/%(project_id)s/users/%(user_id)s/' + 'roles/%(role_id)s' % { + 'project_id': self.project_id, + 'user_id': self.user_id, + 'role_id': role['id']}) + + # assign a new role + self.put(grant_url) + + # create a trust that delegates the new role + ref = self.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user_id, + project_id=self.project_id, + impersonation=False, + expires=dict(minutes=1), + role_ids=[role['id']]) + + r = self.post('/OS-TRUST/trusts', body={'trust': ref}) + trust = self.assertValidTrustResponse(r) + + # delete the grant + self.delete(grant_url) + + # attempt to get a trust token with the deleted grant + # and ensure it's unauthorized + auth_data = self.build_authentication_request( + user_id=self.trustee_user['id'], + password=self.trustee_user['password'], + trust_id=trust['id']) + r = self.v3_authenticate_token(auth_data, expected_status=403) + + def test_trust_chained(self): + """Test that a trust token can't be used to execute another trust. + + To do this, we create an A->B->C hierarchy of trusts, then attempt to + execute the trusts in series (C->B->A). + + """ + # create a sub-trustee user + sub_trustee_user = self.new_user_ref( + domain_id=test_v3.DEFAULT_DOMAIN_ID) + password = sub_trustee_user['password'] + sub_trustee_user = self.identity_api.create_user(sub_trustee_user) + sub_trustee_user['password'] = password + sub_trustee_user_id = sub_trustee_user['id'] + + # create a new role + role = self.new_role_ref() + self.role_api.create_role(role['id'], role) + + # assign the new role to trustee + self.put( + '/projects/%(project_id)s/users/%(user_id)s/roles/%(role_id)s' % { + 'project_id': self.project_id, + 'user_id': self.trustee_user_id, + 'role_id': role['id']}) + + # create a trust from trustor -> trustee + ref = self.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user_id, + project_id=self.project_id, + impersonation=True, + expires=dict(minutes=1), + role_ids=[self.role_id]) + r = self.post('/OS-TRUST/trusts', body={'trust': ref}) + trust1 = self.assertValidTrustResponse(r) + + # authenticate as trustee so we can create a second trust + auth_data = self.build_authentication_request( + user_id=self.trustee_user_id, + password=self.trustee_user['password'], + project_id=self.project_id) + token = self.get_requested_token(auth_data) + + # create a trust from trustee -> sub-trustee + ref = self.new_trust_ref( + trustor_user_id=self.trustee_user_id, + trustee_user_id=sub_trustee_user_id, + project_id=self.project_id, + impersonation=True, + expires=dict(minutes=1), + role_ids=[role['id']]) + r = self.post('/OS-TRUST/trusts', token=token, body={'trust': ref}) + trust2 = self.assertValidTrustResponse(r) + + # authenticate as sub-trustee and get a trust token + auth_data = self.build_authentication_request( + user_id=sub_trustee_user['id'], + password=sub_trustee_user['password'], + trust_id=trust2['id']) + trust_token = self.get_requested_token(auth_data) + + # attempt to get the second trust using a trust token + auth_data = self.build_authentication_request( + token=trust_token, + trust_id=trust1['id']) + r = self.v3_authenticate_token(auth_data, expected_status=403) + + def assertTrustTokensRevoked(self, trust_id): + revocation_response = self.get('/OS-REVOKE/events', + expected_status=200) + revocation_events = revocation_response.json_body['events'] + found = False + for event in revocation_events: + if event.get('OS-TRUST:trust_id') == trust_id: + found = True + self.assertTrue(found, 'event with trust_id %s not found in list' % + trust_id) + + def test_delete_trust_revokes_tokens(self): + ref = self.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user_id, + project_id=self.project_id, + impersonation=False, + expires=dict(minutes=1), + role_ids=[self.role_id]) + r = self.post('/OS-TRUST/trusts', body={'trust': ref}) + trust = self.assertValidTrustResponse(r) + trust_id = trust['id'] + auth_data = self.build_authentication_request( + user_id=self.trustee_user['id'], + password=self.trustee_user['password'], + trust_id=trust_id) + r = self.v3_authenticate_token(auth_data) + self.assertValidProjectTrustScopedTokenResponse( + r, self.trustee_user) + trust_token = r.headers['X-Subject-Token'] + self.delete('/OS-TRUST/trusts/%(trust_id)s' % { + 'trust_id': trust_id}, + expected_status=204) + headers = {'X-Subject-Token': trust_token} + self.head('/auth/tokens', headers=headers, expected_status=404) + self.assertTrustTokensRevoked(trust_id) + + def disable_user(self, user): + user['enabled'] = False + self.identity_api.update_user(user['id'], user) + + def test_trust_get_token_fails_if_trustor_disabled(self): + ref = self.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user_id, + project_id=self.project_id, + impersonation=False, + expires=dict(minutes=1), + role_ids=[self.role_id]) + + r = self.post('/OS-TRUST/trusts', body={'trust': ref}) + + trust = self.assertValidTrustResponse(r, ref) + + auth_data = self.build_authentication_request( + user_id=self.trustee_user['id'], + password=self.trustee_user['password'], + trust_id=trust['id']) + self.v3_authenticate_token(auth_data, expected_status=201) + + self.disable_user(self.user) + + auth_data = self.build_authentication_request( + user_id=self.trustee_user['id'], + password=self.trustee_user['password'], + trust_id=trust['id']) + self.v3_authenticate_token(auth_data, expected_status=403) + + def test_trust_get_token_fails_if_trustee_disabled(self): + ref = self.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user_id, + project_id=self.project_id, + impersonation=False, + expires=dict(minutes=1), + role_ids=[self.role_id]) + + r = self.post('/OS-TRUST/trusts', body={'trust': ref}) + + trust = self.assertValidTrustResponse(r, ref) + + auth_data = self.build_authentication_request( + user_id=self.trustee_user['id'], + password=self.trustee_user['password'], + trust_id=trust['id']) + self.v3_authenticate_token(auth_data, expected_status=201) + + self.disable_user(self.trustee_user) + + auth_data = self.build_authentication_request( + user_id=self.trustee_user['id'], + password=self.trustee_user['password'], + trust_id=trust['id']) + self.v3_authenticate_token(auth_data, expected_status=401) + + def test_delete_trust(self): + ref = self.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user_id, + project_id=self.project_id, + impersonation=False, + expires=dict(minutes=1), + role_ids=[self.role_id]) + + r = self.post('/OS-TRUST/trusts', body={'trust': ref}) + + trust = self.assertValidTrustResponse(r, ref) + + self.delete('/OS-TRUST/trusts/%(trust_id)s' % { + 'trust_id': trust['id']}, + expected_status=204) + + self.get('/OS-TRUST/trusts/%(trust_id)s' % { + 'trust_id': trust['id']}, + expected_status=404) + + self.get('/OS-TRUST/trusts/%(trust_id)s' % { + 'trust_id': trust['id']}, + expected_status=404) + + auth_data = self.build_authentication_request( + user_id=self.trustee_user['id'], + password=self.trustee_user['password'], + trust_id=trust['id']) + self.v3_authenticate_token(auth_data, expected_status=401) + + def test_list_trusts(self): + ref = self.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user_id, + project_id=self.project_id, + impersonation=False, + expires=dict(minutes=1), + role_ids=[self.role_id]) + + for i in range(3): + r = self.post('/OS-TRUST/trusts', body={'trust': ref}) + self.assertValidTrustResponse(r, ref) + + r = self.get('/OS-TRUST/trusts', expected_status=200) + trusts = r.result['trusts'] + self.assertEqual(3, len(trusts)) + self.assertValidTrustListResponse(r) + + r = self.get('/OS-TRUST/trusts?trustor_user_id=%s' % + self.user_id, expected_status=200) + trusts = r.result['trusts'] + self.assertEqual(3, len(trusts)) + self.assertValidTrustListResponse(r) + + r = self.get('/OS-TRUST/trusts?trustee_user_id=%s' % + self.user_id, expected_status=200) + trusts = r.result['trusts'] + self.assertEqual(0, len(trusts)) + + def test_change_password_invalidates_trust_tokens(self): + ref = self.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user_id, + project_id=self.project_id, + impersonation=True, + expires=dict(minutes=1), + role_ids=[self.role_id]) + + r = self.post('/OS-TRUST/trusts', body={'trust': ref}) + trust = self.assertValidTrustResponse(r) + + auth_data = self.build_authentication_request( + user_id=self.trustee_user['id'], + password=self.trustee_user['password'], + trust_id=trust['id']) + r = self.v3_authenticate_token(auth_data) + + self.assertValidProjectTrustScopedTokenResponse(r, self.user) + trust_token = r.headers.get('X-Subject-Token') + + self.get('/OS-TRUST/trusts?trustor_user_id=%s' % + self.user_id, expected_status=200, + token=trust_token) + + self.assertValidUserResponse( + self.patch('/users/%s' % self.trustee_user['id'], + body={'user': {'password': uuid.uuid4().hex}}, + expected_status=200)) + + self.get('/OS-TRUST/trusts?trustor_user_id=%s' % + self.user_id, expected_status=401, + token=trust_token) + + def test_trustee_can_do_role_ops(self): + ref = self.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user_id, + project_id=self.project_id, + impersonation=True, + role_ids=[self.role_id]) + + r = self.post('/OS-TRUST/trusts', body={'trust': ref}) + trust = self.assertValidTrustResponse(r) + + auth_data = self.build_authentication_request( + user_id=self.trustee_user['id'], + password=self.trustee_user['password']) + + r = self.get( + '/OS-TRUST/trusts/%(trust_id)s/roles' % { + 'trust_id': trust['id']}, + auth=auth_data) + self.assertValidRoleListResponse(r, self.role) + + self.head( + '/OS-TRUST/trusts/%(trust_id)s/roles/%(role_id)s' % { + 'trust_id': trust['id'], + 'role_id': self.role['id']}, + auth=auth_data, + expected_status=200) + + r = self.get( + '/OS-TRUST/trusts/%(trust_id)s/roles/%(role_id)s' % { + 'trust_id': trust['id'], + 'role_id': self.role['id']}, + auth=auth_data, + expected_status=200) + self.assertValidRoleResponse(r, self.role) + + def test_do_not_consume_remaining_uses_when_get_token_fails(self): + ref = self.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user_id, + project_id=self.project_id, + impersonation=False, + expires=dict(minutes=1), + role_ids=[self.role_id], + remaining_uses=3) + r = self.post('/OS-TRUST/trusts', body={'trust': ref}) + + new_trust = r.result.get('trust') + trust_id = new_trust.get('id') + # Pass in another user's ID as the trustee, the result being a failed + # token authenticate and the remaining_uses of the trust should not be + # decremented. + auth_data = self.build_authentication_request( + user_id=self.default_domain_user['id'], + password=self.default_domain_user['password'], + trust_id=trust_id) + self.v3_authenticate_token(auth_data, expected_status=403) + + r = self.get('/OS-TRUST/trusts/%s' % trust_id) + self.assertEqual(3, r.result.get('trust').get('remaining_uses')) + + +class TestAPIProtectionWithoutAuthContextMiddleware(test_v3.RestfulTestCase): + def test_api_protection_with_no_auth_context_in_env(self): + auth_data = self.build_authentication_request( + user_id=self.default_domain_user['id'], + password=self.default_domain_user['password'], + project_id=self.project['id']) + token = self.get_requested_token(auth_data) + auth_controller = auth.controllers.Auth() + # all we care is that auth context is not in the environment and + # 'token_id' is used to build the auth context instead + context = {'subject_token_id': token, + 'token_id': token, + 'query_string': {}, + 'environment': {}} + r = auth_controller.validate_token(context) + self.assertEqual(200, r.status_code) + + +class TestAuthContext(tests.TestCase): + def setUp(self): + super(TestAuthContext, self).setUp() + self.auth_context = auth.controllers.AuthContext() + + def test_pick_lowest_expires_at(self): + expires_at_1 = timeutils.isotime(timeutils.utcnow()) + expires_at_2 = timeutils.isotime(timeutils.utcnow() + + datetime.timedelta(seconds=10)) + # make sure auth_context picks the lowest value + self.auth_context['expires_at'] = expires_at_1 + self.auth_context['expires_at'] = expires_at_2 + self.assertEqual(expires_at_1, self.auth_context['expires_at']) + + def test_identity_attribute_conflict(self): + for identity_attr in auth.controllers.AuthContext.IDENTITY_ATTRIBUTES: + self.auth_context[identity_attr] = uuid.uuid4().hex + if identity_attr == 'expires_at': + # 'expires_at' is a special case. Will test it in a separate + # test case. + continue + self.assertRaises(exception.Unauthorized, + operator.setitem, + self.auth_context, + identity_attr, + uuid.uuid4().hex) + + def test_identity_attribute_conflict_with_none_value(self): + for identity_attr in auth.controllers.AuthContext.IDENTITY_ATTRIBUTES: + self.auth_context[identity_attr] = None + + if identity_attr == 'expires_at': + # 'expires_at' is a special case and is tested above. + self.auth_context['expires_at'] = uuid.uuid4().hex + continue + + self.assertRaises(exception.Unauthorized, + operator.setitem, + self.auth_context, + identity_attr, + uuid.uuid4().hex) + + def test_non_identity_attribute_conflict_override(self): + # for attributes Keystone doesn't know about, make sure they can be + # freely manipulated + attr_name = uuid.uuid4().hex + attr_val_1 = uuid.uuid4().hex + attr_val_2 = uuid.uuid4().hex + self.auth_context[attr_name] = attr_val_1 + self.auth_context[attr_name] = attr_val_2 + self.assertEqual(attr_val_2, self.auth_context[attr_name]) + + +class TestAuthSpecificData(test_v3.RestfulTestCase): + + def test_get_catalog_project_scoped_token(self): + """Call ``GET /auth/catalog`` with a project-scoped token.""" + r = self.get( + '/auth/catalog', + expected_status=200) + self.assertValidCatalogResponse(r) + + def test_get_catalog_domain_scoped_token(self): + """Call ``GET /auth/catalog`` with a domain-scoped token.""" + # grant a domain role to a user + self.put(path='/domains/%s/users/%s/roles/%s' % ( + self.domain['id'], self.user['id'], self.role['id'])) + + self.get( + '/auth/catalog', + auth=self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + domain_id=self.domain['id']), + expected_status=403) + + def test_get_catalog_unscoped_token(self): + """Call ``GET /auth/catalog`` with an unscoped token.""" + self.get( + '/auth/catalog', + auth=self.build_authentication_request( + user_id=self.default_domain_user['id'], + password=self.default_domain_user['password']), + expected_status=403) + + def test_get_catalog_no_token(self): + """Call ``GET /auth/catalog`` without a token.""" + self.get( + '/auth/catalog', + noauth=True, + expected_status=401) + + def test_get_projects_project_scoped_token(self): + r = self.get('/auth/projects', expected_status=200) + self.assertThat(r.json['projects'], matchers.HasLength(1)) + self.assertValidProjectListResponse(r) + + def test_get_domains_project_scoped_token(self): + self.put(path='/domains/%s/users/%s/roles/%s' % ( + self.domain['id'], self.user['id'], self.role['id'])) + + r = self.get('/auth/domains', expected_status=200) + self.assertThat(r.json['domains'], matchers.HasLength(1)) + self.assertValidDomainListResponse(r) + + +class TestFernetTokenProvider(test_v3.RestfulTestCase): + def setUp(self): + super(TestFernetTokenProvider, self).setUp() + self.useFixture(ksfixtures.KeyRepository(self.config_fixture)) + + def _make_auth_request(self, auth_data): + resp = self.post('/auth/tokens', body=auth_data, expected_status=201) + token = resp.headers.get('X-Subject-Token') + self.assertLess(len(token), 255) + return token + + def _get_unscoped_token(self): + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password']) + return self._make_auth_request(auth_data) + + def _get_project_scoped_token(self): + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + project_id=self.project_id) + return self._make_auth_request(auth_data) + + def _get_domain_scoped_token(self): + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + domain_id=self.domain_id) + return self._make_auth_request(auth_data) + + def _get_trust_scoped_token(self, trustee_user, trust): + auth_data = self.build_authentication_request( + user_id=trustee_user['id'], + password=trustee_user['password'], + trust_id=trust['id']) + return self._make_auth_request(auth_data) + + def _validate_token(self, token, expected_status=200): + return self.get( + '/auth/tokens', + headers={'X-Subject-Token': token}, + expected_status=expected_status) + + def _revoke_token(self, token, expected_status=204): + return self.delete( + '/auth/tokens', + headers={'X-Subject-Token': token}, + expected_status=expected_status) + + def _set_user_enabled(self, user, enabled=True): + user['enabled'] = enabled + self.identity_api.update_user(user['id'], user) + + def _create_trust(self): + # Create a trustee user + trustee_user_ref = self.new_user_ref(domain_id=self.domain_id) + trustee_user = self.identity_api.create_user(trustee_user_ref) + trustee_user['password'] = trustee_user_ref['password'] + ref = self.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=trustee_user['id'], + project_id=self.project_id, + impersonation=True, + role_ids=[self.role_id]) + + # Create a trust + r = self.post('/OS-TRUST/trusts', body={'trust': ref}) + trust = self.assertValidTrustResponse(r) + return (trustee_user, trust) + + def config_overrides(self): + super(TestFernetTokenProvider, self).config_overrides() + self.config_fixture.config( + group='token', + provider='keystone.token.providers.fernet.Provider') + + def test_validate_unscoped_token(self): + unscoped_token = self._get_unscoped_token() + self._validate_token(unscoped_token) + + def test_validate_tampered_unscoped_token_fails(self): + unscoped_token = self._get_unscoped_token() + tampered_token = (unscoped_token[:50] + uuid.uuid4().hex + + unscoped_token[50 + 32:]) + self._validate_token(tampered_token, expected_status=401) + + def test_revoke_unscoped_token(self): + unscoped_token = self._get_unscoped_token() + self._validate_token(unscoped_token) + self._revoke_token(unscoped_token) + self._validate_token(unscoped_token, expected_status=404) + + def test_unscoped_token_is_invalid_after_disabling_user(self): + unscoped_token = self._get_unscoped_token() + # Make sure the token is valid + self._validate_token(unscoped_token) + # Disable the user + self._set_user_enabled(self.user, enabled=False) + # Ensure validating a token for a disabled user fails + self.assertRaises(exception.TokenNotFound, + self.token_provider_api.validate_token, + unscoped_token) + + def test_unscoped_token_is_invalid_after_enabling_disabled_user(self): + unscoped_token = self._get_unscoped_token() + # Make sure the token is valid + self._validate_token(unscoped_token) + # Disable the user + self._set_user_enabled(self.user, enabled=False) + # Ensure validating a token for a disabled user fails + self.assertRaises(exception.TokenNotFound, + self.token_provider_api.validate_token, + unscoped_token) + # Enable the user + self._set_user_enabled(self.user) + # Ensure validating a token for a re-enabled user fails + self.assertRaises(exception.TokenNotFound, + self.token_provider_api.validate_token, + unscoped_token) + + def test_unscoped_token_is_invalid_after_disabling_user_domain(self): + unscoped_token = self._get_unscoped_token() + # Make sure the token is valid + self._validate_token(unscoped_token) + # Disable the user's domain + self.domain['enabled'] = False + self.resource_api.update_domain(self.domain['id'], self.domain) + # Ensure validating a token for a disabled user fails + self.assertRaises(exception.TokenNotFound, + self.token_provider_api.validate_token, + unscoped_token) + + def test_unscoped_token_is_invalid_after_changing_user_password(self): + unscoped_token = self._get_unscoped_token() + # Make sure the token is valid + self._validate_token(unscoped_token) + # Change user's password + self.user['password'] = 'Password1' + self.identity_api.update_user(self.user['id'], self.user) + # Ensure updating user's password revokes existing user's tokens + self.assertRaises(exception.TokenNotFound, + self.token_provider_api.validate_token, + unscoped_token) + + def test_validate_project_scoped_token(self): + project_scoped_token = self._get_project_scoped_token() + self._validate_token(project_scoped_token) + + def test_validate_domain_scoped_token(self): + # Grant user access to domain + self.assignment_api.create_grant(self.role['id'], + user_id=self.user['id'], + domain_id=self.domain['id']) + domain_scoped_token = self._get_domain_scoped_token() + resp = self._validate_token(domain_scoped_token) + resp_json = json.loads(resp.body) + self.assertIsNotNone(resp_json['token']['catalog']) + self.assertIsNotNone(resp_json['token']['roles']) + self.assertIsNotNone(resp_json['token']['domain']) + + def test_validate_tampered_project_scoped_token_fails(self): + project_scoped_token = self._get_project_scoped_token() + tampered_token = (project_scoped_token[:50] + uuid.uuid4().hex + + project_scoped_token[50 + 32:]) + self._validate_token(tampered_token, expected_status=401) + + def test_revoke_project_scoped_token(self): + project_scoped_token = self._get_project_scoped_token() + self._validate_token(project_scoped_token) + self._revoke_token(project_scoped_token) + self._validate_token(project_scoped_token, expected_status=404) + + def test_project_scoped_token_is_invalid_after_disabling_user(self): + project_scoped_token = self._get_project_scoped_token() + # Make sure the token is valid + self._validate_token(project_scoped_token) + # Disable the user + self._set_user_enabled(self.user, enabled=False) + # Ensure validating a token for a disabled user fails + self.assertRaises(exception.TokenNotFound, + self.token_provider_api.validate_token, + project_scoped_token) + + def test_domain_scoped_token_is_invalid_after_disabling_user(self): + # Grant user access to domain + self.assignment_api.create_grant(self.role['id'], + user_id=self.user['id'], + domain_id=self.domain['id']) + domain_scoped_token = self._get_domain_scoped_token() + # Make sure the token is valid + self._validate_token(domain_scoped_token) + # Disable user + self._set_user_enabled(self.user, enabled=False) + # Ensure validating a token for a disabled user fails + self.assertRaises(exception.TokenNotFound, + self.token_provider_api.validate_token, + domain_scoped_token) + + def test_domain_scoped_token_is_invalid_after_deleting_grant(self): + # Grant user access to domain + self.assignment_api.create_grant(self.role['id'], + user_id=self.user['id'], + domain_id=self.domain['id']) + domain_scoped_token = self._get_domain_scoped_token() + # Make sure the token is valid + self._validate_token(domain_scoped_token) + # Delete access to domain + self.assignment_api.delete_grant(self.role['id'], + user_id=self.user['id'], + domain_id=self.domain['id']) + # Ensure validating a token for a disabled user fails + self.assertRaises(exception.TokenNotFound, + self.token_provider_api.validate_token, + domain_scoped_token) + + def test_project_scoped_token_invalid_after_changing_user_password(self): + project_scoped_token = self._get_project_scoped_token() + # Make sure the token is valid + self._validate_token(project_scoped_token) + # Update user's password + self.user['password'] = 'Password1' + self.identity_api.update_user(self.user['id'], self.user) + # Ensure updating user's password revokes existing tokens + self.assertRaises(exception.TokenNotFound, + self.token_provider_api.validate_token, + project_scoped_token) + + def test_project_scoped_token_invalid_after_disabling_project(self): + project_scoped_token = self._get_project_scoped_token() + # Make sure the token is valid + self._validate_token(project_scoped_token) + # Disable project + self.project['enabled'] = False + self.resource_api.update_project(self.project['id'], self.project) + # Ensure validating a token for a disabled project fails + self.assertRaises(exception.TokenNotFound, + self.token_provider_api.validate_token, + project_scoped_token) + + def test_domain_scoped_token_invalid_after_disabling_domain(self): + # Grant user access to domain + self.assignment_api.create_grant(self.role['id'], + user_id=self.user['id'], + domain_id=self.domain['id']) + domain_scoped_token = self._get_domain_scoped_token() + # Make sure the token is valid + self._validate_token(domain_scoped_token) + # Disable domain + self.domain['enabled'] = False + self.resource_api.update_domain(self.domain['id'], self.domain) + # Ensure validating a token for a disabled domain fails + self.assertRaises(exception.TokenNotFound, + self.token_provider_api.validate_token, + domain_scoped_token) + + def test_rescope_unscoped_token_with_trust(self): + trustee_user, trust = self._create_trust() + trust_scoped_token = self._get_trust_scoped_token(trustee_user, trust) + self.assertLess(len(trust_scoped_token), 255) + + def test_validate_a_trust_scoped_token(self): + trustee_user, trust = self._create_trust() + trust_scoped_token = self._get_trust_scoped_token(trustee_user, trust) + # Validate a trust scoped token + self._validate_token(trust_scoped_token) + + def test_validate_tampered_trust_scoped_token_fails(self): + trustee_user, trust = self._create_trust() + trust_scoped_token = self._get_trust_scoped_token(trustee_user, trust) + # Get a trust scoped token + tampered_token = (trust_scoped_token[:50] + uuid.uuid4().hex + + trust_scoped_token[50 + 32:]) + self._validate_token(tampered_token, expected_status=401) + + def test_revoke_trust_scoped_token(self): + trustee_user, trust = self._create_trust() + trust_scoped_token = self._get_trust_scoped_token(trustee_user, trust) + # Validate a trust scoped token + self._validate_token(trust_scoped_token) + self._revoke_token(trust_scoped_token) + self._validate_token(trust_scoped_token, expected_status=404) + + def test_trust_scoped_token_is_invalid_after_disabling_trustee(self): + trustee_user, trust = self._create_trust() + trust_scoped_token = self._get_trust_scoped_token(trustee_user, trust) + # Validate a trust scoped token + self._validate_token(trust_scoped_token) + + # Disable trustee + trustee_update_ref = dict(enabled=False) + self.identity_api.update_user(trustee_user['id'], trustee_update_ref) + # Ensure validating a token for a disabled user fails + self.assertRaises(exception.TokenNotFound, + self.token_provider_api.validate_token, + trust_scoped_token) + + def test_trust_scoped_token_invalid_after_changing_trustee_password(self): + trustee_user, trust = self._create_trust() + trust_scoped_token = self._get_trust_scoped_token(trustee_user, trust) + # Validate a trust scoped token + self._validate_token(trust_scoped_token) + # Change trustee's password + trustee_update_ref = dict(password='Password1') + self.identity_api.update_user(trustee_user['id'], trustee_update_ref) + # Ensure updating trustee's password revokes existing tokens + self.assertRaises(exception.TokenNotFound, + self.token_provider_api.validate_token, + trust_scoped_token) + + def test_trust_scoped_token_is_invalid_after_disabling_trustor(self): + trustee_user, trust = self._create_trust() + trust_scoped_token = self._get_trust_scoped_token(trustee_user, trust) + # Validate a trust scoped token + self._validate_token(trust_scoped_token) + + # Disable the trustor + trustor_update_ref = dict(enabled=False) + self.identity_api.update_user(self.user['id'], trustor_update_ref) + # Ensure validating a token for a disabled user fails + self.assertRaises(exception.TokenNotFound, + self.token_provider_api.validate_token, + trust_scoped_token) + + def test_trust_scoped_token_invalid_after_changing_trustor_password(self): + trustee_user, trust = self._create_trust() + trust_scoped_token = self._get_trust_scoped_token(trustee_user, trust) + # Validate a trust scoped token + self._validate_token(trust_scoped_token) + + # Change trustor's password + trustor_update_ref = dict(password='Password1') + self.identity_api.update_user(self.user['id'], trustor_update_ref) + # Ensure updating trustor's password revokes existing user's tokens + self.assertRaises(exception.TokenNotFound, + self.token_provider_api.validate_token, + trust_scoped_token) + + def test_trust_scoped_token_invalid_after_disabled_trustor_domain(self): + trustee_user, trust = self._create_trust() + trust_scoped_token = self._get_trust_scoped_token(trustee_user, trust) + # Validate a trust scoped token + self._validate_token(trust_scoped_token) + + # Disable trustor's domain + self.domain['enabled'] = False + self.resource_api.update_domain(self.domain['id'], self.domain) + + trustor_update_ref = dict(password='Password1') + self.identity_api.update_user(self.user['id'], trustor_update_ref) + # Ensure updating trustor's password revokes existing user's tokens + self.assertRaises(exception.TokenNotFound, + self.token_provider_api.validate_token, + trust_scoped_token) + + def test_v2_validate_unscoped_token_returns_401(self): + """Test raised exception when validating unscoped token. + + Test that validating an unscoped token in v2.0 of a v3 user of a + non-default domain returns unauthorized. + """ + unscoped_token = self._get_unscoped_token() + self.assertRaises(exception.Unauthorized, + self.token_provider_api.validate_v2_token, + unscoped_token) + + def test_v2_validate_domain_scoped_token_returns_401(self): + """Test raised exception when validating a domain scoped token. + + Test that validating an domain scoped token in v2.0 + returns unauthorized. + """ + + # Grant user access to domain + self.assignment_api.create_grant(self.role['id'], + user_id=self.user['id'], + domain_id=self.domain['id']) + + scoped_token = self._get_domain_scoped_token() + self.assertRaises(exception.Unauthorized, + self.token_provider_api.validate_v2_token, + scoped_token) + + def test_v2_validate_trust_scoped_token(self): + """Test raised exception when validating a trust scoped token. + + Test that validating an trust scoped token in v2.0 returns + unauthorized. + """ + + trustee_user, trust = self._create_trust() + trust_scoped_token = self._get_trust_scoped_token(trustee_user, trust) + self.assertRaises(exception.Unauthorized, + self.token_provider_api.validate_v2_token, + trust_scoped_token) + + +class TestAuthFernetTokenProvider(TestAuth): + def setUp(self): + super(TestAuthFernetTokenProvider, self).setUp() + self.useFixture(ksfixtures.KeyRepository(self.config_fixture)) + + def config_overrides(self): + super(TestAuthFernetTokenProvider, self).config_overrides() + self.config_fixture.config( + group='token', + provider='keystone.token.providers.fernet.Provider') + + def test_verify_with_bound_token(self): + self.config_fixture.config(group='token', bind='kerberos') + auth_data = self.build_authentication_request( + project_id=self.project['id']) + remote_user = self.default_domain_user['name'] + self.admin_app.extra_environ.update({'REMOTE_USER': remote_user, + 'AUTH_TYPE': 'Negotiate'}) + # Bind not current supported by Fernet, see bug 1433311. + self.v3_authenticate_token(auth_data, expected_status=501) + + def test_v2_v3_bind_token_intermix(self): + self.config_fixture.config(group='token', bind='kerberos') + + # we need our own user registered to the default domain because of + # the way external auth works. + remote_user = self.default_domain_user['name'] + self.admin_app.extra_environ.update({'REMOTE_USER': remote_user, + 'AUTH_TYPE': 'Negotiate'}) + body = {'auth': {}} + # Bind not current supported by Fernet, see bug 1433311. + self.admin_request(path='/v2.0/tokens', + method='POST', + body=body, + expected_status=501) + + def test_auth_with_bind_token(self): + self.config_fixture.config(group='token', bind=['kerberos']) + + auth_data = self.build_authentication_request() + remote_user = self.default_domain_user['name'] + self.admin_app.extra_environ.update({'REMOTE_USER': remote_user, + 'AUTH_TYPE': 'Negotiate'}) + # Bind not current supported by Fernet, see bug 1433311. + self.v3_authenticate_token(auth_data, expected_status=501) diff --git a/keystone-moon/keystone/tests/unit/test_v3_catalog.py b/keystone-moon/keystone/tests/unit/test_v3_catalog.py new file mode 100644 index 00000000..d231b2e1 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_v3_catalog.py @@ -0,0 +1,746 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 +import uuid + +from keystone import catalog +from keystone.tests import unit as tests +from keystone.tests.unit.ksfixtures import database +from keystone.tests.unit import test_v3 + + +class CatalogTestCase(test_v3.RestfulTestCase): + """Test service & endpoint CRUD.""" + + # region crud tests + + def test_create_region_with_id(self): + """Call ``PUT /regions/{region_id}`` w/o an ID in the request body.""" + ref = self.new_region_ref() + region_id = ref.pop('id') + r = self.put( + '/regions/%s' % region_id, + body={'region': ref}, + expected_status=201) + self.assertValidRegionResponse(r, ref) + # Double-check that the region ID was kept as-is and not + # populated with a UUID, as is the case with POST /v3/regions + self.assertEqual(region_id, r.json['region']['id']) + + def test_create_region_with_matching_ids(self): + """Call ``PUT /regions/{region_id}`` with an ID in the request body.""" + ref = self.new_region_ref() + region_id = ref['id'] + r = self.put( + '/regions/%s' % region_id, + body={'region': ref}, + expected_status=201) + self.assertValidRegionResponse(r, ref) + # Double-check that the region ID was kept as-is and not + # populated with a UUID, as is the case with POST /v3/regions + self.assertEqual(region_id, r.json['region']['id']) + + def test_create_region_with_duplicate_id(self): + """Call ``PUT /regions/{region_id}``.""" + ref = dict(description="my region") + self.put( + '/regions/myregion', + body={'region': ref}, expected_status=201) + # Create region again with duplicate id + self.put( + '/regions/myregion', + body={'region': ref}, expected_status=409) + + def test_create_region(self): + """Call ``POST /regions`` with an ID in the request body.""" + # the ref will have an ID defined on it + ref = self.new_region_ref() + r = self.post( + '/regions', + body={'region': ref}) + self.assertValidRegionResponse(r, ref) + + # we should be able to get the region, having defined the ID ourselves + r = self.get( + '/regions/%(region_id)s' % { + 'region_id': ref['id']}) + self.assertValidRegionResponse(r, ref) + + def test_create_region_with_empty_id(self): + """Call ``POST /regions`` with an empty ID in the request body.""" + ref = self.new_region_ref() + ref['id'] = '' + + r = self.post( + '/regions', + body={'region': ref}, expected_status=201) + self.assertValidRegionResponse(r, ref) + self.assertNotEmpty(r.result['region'].get('id')) + + def test_create_region_without_id(self): + """Call ``POST /regions`` without an ID in the request body.""" + ref = self.new_region_ref() + + # instead of defining the ID ourselves... + del ref['id'] + + # let the service define the ID + r = self.post( + '/regions', + body={'region': ref}, + expected_status=201) + self.assertValidRegionResponse(r, ref) + + def test_create_region_without_description(self): + """Call ``POST /regions`` without description in the request body.""" + ref = self.new_region_ref() + + del ref['description'] + + r = self.post( + '/regions', + body={'region': ref}, + expected_status=201) + # Create the description in the reference to compare to since the + # response should now have a description, even though we didn't send + # it with the original reference. + ref['description'] = '' + self.assertValidRegionResponse(r, ref) + + def test_create_regions_with_same_description_string(self): + """Call ``POST /regions`` with same description in the request bodies. + """ + # NOTE(lbragstad): Make sure we can create two regions that have the + # same description. + ref1 = self.new_region_ref() + ref2 = self.new_region_ref() + + region_desc = 'Some Region Description' + + ref1['description'] = region_desc + ref2['description'] = region_desc + + resp1 = self.post( + '/regions', + body={'region': ref1}, + expected_status=201) + self.assertValidRegionResponse(resp1, ref1) + + resp2 = self.post( + '/regions', + body={'region': ref2}, + expected_status=201) + self.assertValidRegionResponse(resp2, ref2) + + def test_create_regions_without_descriptions(self): + """Call ``POST /regions`` with no description in the request bodies. + """ + # NOTE(lbragstad): Make sure we can create two regions that have + # no description in the request body. The description should be + # populated by Catalog Manager. + ref1 = self.new_region_ref() + ref2 = self.new_region_ref() + + del ref1['description'] + del ref2['description'] + + resp1 = self.post( + '/regions', + body={'region': ref1}, + expected_status=201) + + resp2 = self.post( + '/regions', + body={'region': ref2}, + expected_status=201) + # Create the descriptions in the references to compare to since the + # responses should now have descriptions, even though we didn't send + # a description with the original references. + ref1['description'] = '' + ref2['description'] = '' + self.assertValidRegionResponse(resp1, ref1) + self.assertValidRegionResponse(resp2, ref2) + + def test_create_region_with_conflicting_ids(self): + """Call ``PUT /regions/{region_id}`` with conflicting region IDs.""" + # the region ref is created with an ID + ref = self.new_region_ref() + + # but instead of using that ID, make up a new, conflicting one + self.put( + '/regions/%s' % uuid.uuid4().hex, + body={'region': ref}, + expected_status=400) + + def test_list_regions(self): + """Call ``GET /regions``.""" + r = self.get('/regions') + self.assertValidRegionListResponse(r, ref=self.region) + + def _create_region_with_parent_id(self, parent_id=None): + ref = self.new_region_ref() + ref['parent_region_id'] = parent_id + return self.post( + '/regions', + body={'region': ref}) + + def test_list_regions_filtered_by_parent_region_id(self): + """Call ``GET /regions?parent_region_id={parent_region_id}``.""" + new_region = self._create_region_with_parent_id() + parent_id = new_region.result['region']['id'] + + new_region = self._create_region_with_parent_id(parent_id) + new_region = self._create_region_with_parent_id(parent_id) + + r = self.get('/regions?parent_region_id=%s' % parent_id) + + for region in r.result['regions']: + self.assertEqual(parent_id, region['parent_region_id']) + + def test_get_region(self): + """Call ``GET /regions/{region_id}``.""" + r = self.get('/regions/%(region_id)s' % { + 'region_id': self.region_id}) + self.assertValidRegionResponse(r, self.region) + + def test_update_region(self): + """Call ``PATCH /regions/{region_id}``.""" + region = self.new_region_ref() + del region['id'] + r = self.patch('/regions/%(region_id)s' % { + 'region_id': self.region_id}, + body={'region': region}) + self.assertValidRegionResponse(r, region) + + def test_delete_region(self): + """Call ``DELETE /regions/{region_id}``.""" + + ref = self.new_region_ref() + r = self.post( + '/regions', + body={'region': ref}) + self.assertValidRegionResponse(r, ref) + + self.delete('/regions/%(region_id)s' % { + 'region_id': ref['id']}) + + # service crud tests + + def test_create_service(self): + """Call ``POST /services``.""" + ref = self.new_service_ref() + r = self.post( + '/services', + body={'service': ref}) + self.assertValidServiceResponse(r, ref) + + def test_create_service_no_name(self): + """Call ``POST /services``.""" + ref = self.new_service_ref() + del ref['name'] + r = self.post( + '/services', + body={'service': ref}) + ref['name'] = '' + self.assertValidServiceResponse(r, ref) + + def test_create_service_no_enabled(self): + """Call ``POST /services``.""" + ref = self.new_service_ref() + del ref['enabled'] + r = self.post( + '/services', + body={'service': ref}) + ref['enabled'] = True + self.assertValidServiceResponse(r, ref) + self.assertIs(True, r.result['service']['enabled']) + + def test_create_service_enabled_false(self): + """Call ``POST /services``.""" + ref = self.new_service_ref() + ref['enabled'] = False + r = self.post( + '/services', + body={'service': ref}) + self.assertValidServiceResponse(r, ref) + self.assertIs(False, r.result['service']['enabled']) + + def test_create_service_enabled_true(self): + """Call ``POST /services``.""" + ref = self.new_service_ref() + ref['enabled'] = True + r = self.post( + '/services', + body={'service': ref}) + self.assertValidServiceResponse(r, ref) + self.assertIs(True, r.result['service']['enabled']) + + def test_create_service_enabled_str_true(self): + """Call ``POST /services``.""" + ref = self.new_service_ref() + ref['enabled'] = 'True' + self.post('/services', body={'service': ref}, expected_status=400) + + def test_create_service_enabled_str_false(self): + """Call ``POST /services``.""" + ref = self.new_service_ref() + ref['enabled'] = 'False' + self.post('/services', body={'service': ref}, expected_status=400) + + def test_create_service_enabled_str_random(self): + """Call ``POST /services``.""" + ref = self.new_service_ref() + ref['enabled'] = 'puppies' + self.post('/services', body={'service': ref}, expected_status=400) + + def test_list_services(self): + """Call ``GET /services``.""" + r = self.get('/services') + self.assertValidServiceListResponse(r, ref=self.service) + + def _create_random_service(self): + ref = self.new_service_ref() + ref['enabled'] = True + response = self.post( + '/services', + body={'service': ref}) + return response.json['service'] + + def test_filter_list_services_by_type(self): + """Call ``GET /services?type=``.""" + target_ref = self._create_random_service() + + # create unrelated services + self._create_random_service() + self._create_random_service() + + response = self.get('/services?type=' + target_ref['type']) + self.assertValidServiceListResponse(response, ref=target_ref) + + filtered_service_list = response.json['services'] + self.assertEqual(1, len(filtered_service_list)) + + filtered_service = filtered_service_list[0] + self.assertEqual(target_ref['type'], filtered_service['type']) + + def test_filter_list_services_by_name(self): + """Call ``GET /services?name=``.""" + target_ref = self._create_random_service() + + # create unrelated services + self._create_random_service() + self._create_random_service() + + response = self.get('/services?name=' + target_ref['name']) + self.assertValidServiceListResponse(response, ref=target_ref) + + filtered_service_list = response.json['services'] + self.assertEqual(1, len(filtered_service_list)) + + filtered_service = filtered_service_list[0] + self.assertEqual(target_ref['name'], filtered_service['name']) + + def test_get_service(self): + """Call ``GET /services/{service_id}``.""" + r = self.get('/services/%(service_id)s' % { + 'service_id': self.service_id}) + self.assertValidServiceResponse(r, self.service) + + def test_update_service(self): + """Call ``PATCH /services/{service_id}``.""" + service = self.new_service_ref() + del service['id'] + r = self.patch('/services/%(service_id)s' % { + 'service_id': self.service_id}, + body={'service': service}) + self.assertValidServiceResponse(r, service) + + def test_delete_service(self): + """Call ``DELETE /services/{service_id}``.""" + self.delete('/services/%(service_id)s' % { + 'service_id': self.service_id}) + + # endpoint crud tests + + def test_list_endpoints(self): + """Call ``GET /endpoints``.""" + r = self.get('/endpoints') + self.assertValidEndpointListResponse(r, ref=self.endpoint) + + def test_create_endpoint_no_enabled(self): + """Call ``POST /endpoints``.""" + ref = self.new_endpoint_ref(service_id=self.service_id) + r = self.post( + '/endpoints', + body={'endpoint': ref}) + ref['enabled'] = True + self.assertValidEndpointResponse(r, ref) + + def test_create_endpoint_enabled_true(self): + """Call ``POST /endpoints`` with enabled: true.""" + ref = self.new_endpoint_ref(service_id=self.service_id, + enabled=True) + r = self.post( + '/endpoints', + body={'endpoint': ref}) + self.assertValidEndpointResponse(r, ref) + + def test_create_endpoint_enabled_false(self): + """Call ``POST /endpoints`` with enabled: false.""" + ref = self.new_endpoint_ref(service_id=self.service_id, + enabled=False) + r = self.post( + '/endpoints', + body={'endpoint': ref}) + self.assertValidEndpointResponse(r, ref) + + def test_create_endpoint_enabled_str_true(self): + """Call ``POST /endpoints`` with enabled: 'True'.""" + ref = self.new_endpoint_ref(service_id=self.service_id, + enabled='True') + self.post( + '/endpoints', + body={'endpoint': ref}, + expected_status=400) + + def test_create_endpoint_enabled_str_false(self): + """Call ``POST /endpoints`` with enabled: 'False'.""" + ref = self.new_endpoint_ref(service_id=self.service_id, + enabled='False') + self.post( + '/endpoints', + body={'endpoint': ref}, + expected_status=400) + + def test_create_endpoint_enabled_str_random(self): + """Call ``POST /endpoints`` with enabled: 'puppies'.""" + ref = self.new_endpoint_ref(service_id=self.service_id, + enabled='puppies') + self.post( + '/endpoints', + body={'endpoint': ref}, + expected_status=400) + + def test_create_endpoint_with_invalid_region_id(self): + """Call ``POST /endpoints``.""" + ref = self.new_endpoint_ref(service_id=self.service_id) + ref["region_id"] = uuid.uuid4().hex + self.post('/endpoints', body={'endpoint': ref}, expected_status=400) + + def test_create_endpoint_with_region(self): + """EndpointV3 creates the region before creating the endpoint, if + endpoint is provided with 'region' and no 'region_id' + """ + ref = self.new_endpoint_ref(service_id=self.service_id) + ref["region"] = uuid.uuid4().hex + ref.pop('region_id') + self.post('/endpoints', body={'endpoint': ref}, expected_status=201) + # Make sure the region is created + self.get('/regions/%(region_id)s' % { + 'region_id': ref["region"]}) + + def test_create_endpoint_with_no_region(self): + """EndpointV3 allows to creates the endpoint without region.""" + ref = self.new_endpoint_ref(service_id=self.service_id) + ref.pop('region_id') + self.post('/endpoints', body={'endpoint': ref}, expected_status=201) + + def test_create_endpoint_with_empty_url(self): + """Call ``POST /endpoints``.""" + ref = self.new_endpoint_ref(service_id=self.service_id) + ref["url"] = '' + self.post('/endpoints', body={'endpoint': ref}, expected_status=400) + + def test_get_endpoint(self): + """Call ``GET /endpoints/{endpoint_id}``.""" + r = self.get( + '/endpoints/%(endpoint_id)s' % { + 'endpoint_id': self.endpoint_id}) + self.assertValidEndpointResponse(r, self.endpoint) + + def test_update_endpoint(self): + """Call ``PATCH /endpoints/{endpoint_id}``.""" + ref = self.new_endpoint_ref(service_id=self.service_id) + del ref['id'] + r = self.patch( + '/endpoints/%(endpoint_id)s' % { + 'endpoint_id': self.endpoint_id}, + body={'endpoint': ref}) + ref['enabled'] = True + self.assertValidEndpointResponse(r, ref) + + def test_update_endpoint_enabled_true(self): + """Call ``PATCH /endpoints/{endpoint_id}`` with enabled: True.""" + r = self.patch( + '/endpoints/%(endpoint_id)s' % { + 'endpoint_id': self.endpoint_id}, + body={'endpoint': {'enabled': True}}) + self.assertValidEndpointResponse(r, self.endpoint) + + def test_update_endpoint_enabled_false(self): + """Call ``PATCH /endpoints/{endpoint_id}`` with enabled: False.""" + r = self.patch( + '/endpoints/%(endpoint_id)s' % { + 'endpoint_id': self.endpoint_id}, + body={'endpoint': {'enabled': False}}) + exp_endpoint = copy.copy(self.endpoint) + exp_endpoint['enabled'] = False + self.assertValidEndpointResponse(r, exp_endpoint) + + def test_update_endpoint_enabled_str_true(self): + """Call ``PATCH /endpoints/{endpoint_id}`` with enabled: 'True'.""" + self.patch( + '/endpoints/%(endpoint_id)s' % { + 'endpoint_id': self.endpoint_id}, + body={'endpoint': {'enabled': 'True'}}, + expected_status=400) + + def test_update_endpoint_enabled_str_false(self): + """Call ``PATCH /endpoints/{endpoint_id}`` with enabled: 'False'.""" + self.patch( + '/endpoints/%(endpoint_id)s' % { + 'endpoint_id': self.endpoint_id}, + body={'endpoint': {'enabled': 'False'}}, + expected_status=400) + + def test_update_endpoint_enabled_str_random(self): + """Call ``PATCH /endpoints/{endpoint_id}`` with enabled: 'kitties'.""" + self.patch( + '/endpoints/%(endpoint_id)s' % { + 'endpoint_id': self.endpoint_id}, + body={'endpoint': {'enabled': 'kitties'}}, + expected_status=400) + + def test_delete_endpoint(self): + """Call ``DELETE /endpoints/{endpoint_id}``.""" + self.delete( + '/endpoints/%(endpoint_id)s' % { + 'endpoint_id': self.endpoint_id}) + + def test_create_endpoint_on_v2(self): + # clear the v3 endpoint so we only have endpoints created on v2 + self.delete( + '/endpoints/%(endpoint_id)s' % { + 'endpoint_id': self.endpoint_id}) + + # create a v3 endpoint ref, and then tweak it back to a v2-style ref + ref = self.new_endpoint_ref(service_id=self.service['id']) + del ref['id'] + del ref['interface'] + ref['publicurl'] = ref.pop('url') + ref['internalurl'] = None + ref['region'] = ref['region_id'] + del ref['region_id'] + # don't set adminurl to ensure it's absence is handled like internalurl + + # create the endpoint on v2 (using a v3 token) + r = self.admin_request( + method='POST', + path='/v2.0/endpoints', + token=self.get_scoped_token(), + body={'endpoint': ref}) + endpoint_v2 = r.result['endpoint'] + + # test the endpoint on v3 + r = self.get('/endpoints') + endpoints = self.assertValidEndpointListResponse(r) + self.assertEqual(1, len(endpoints)) + endpoint_v3 = endpoints.pop() + + # these attributes are identical between both APIs + self.assertEqual(ref['region'], endpoint_v3['region_id']) + self.assertEqual(ref['service_id'], endpoint_v3['service_id']) + self.assertEqual(ref['description'], endpoint_v3['description']) + + # a v2 endpoint is not quite the same concept as a v3 endpoint, so they + # receive different identifiers + self.assertNotEqual(endpoint_v2['id'], endpoint_v3['id']) + + # v2 has a publicurl; v3 has a url + interface type + self.assertEqual(ref['publicurl'], endpoint_v3['url']) + self.assertEqual('public', endpoint_v3['interface']) + + # tests for bug 1152632 -- these attributes were being returned by v3 + self.assertNotIn('publicurl', endpoint_v3) + self.assertNotIn('adminurl', endpoint_v3) + self.assertNotIn('internalurl', endpoint_v3) + + # test for bug 1152635 -- this attribute was being returned by v3 + self.assertNotIn('legacy_endpoint_id', endpoint_v3) + + self.assertEqual(endpoint_v2['region'], endpoint_v3['region_id']) + + +class TestCatalogAPISQL(tests.TestCase): + """Tests for the catalog Manager against the SQL backend. + + """ + + def setUp(self): + super(TestCatalogAPISQL, self).setUp() + self.useFixture(database.Database()) + self.catalog_api = catalog.Manager() + + self.service_id = uuid.uuid4().hex + service = {'id': self.service_id, 'name': uuid.uuid4().hex} + self.catalog_api.create_service(self.service_id, service) + + endpoint = self.new_endpoint_ref(service_id=self.service_id) + self.catalog_api.create_endpoint(endpoint['id'], endpoint) + + def config_overrides(self): + super(TestCatalogAPISQL, self).config_overrides() + self.config_fixture.config( + group='catalog', + driver='keystone.catalog.backends.sql.Catalog') + + def new_endpoint_ref(self, service_id): + return { + 'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'description': uuid.uuid4().hex, + 'interface': uuid.uuid4().hex[:8], + 'service_id': service_id, + 'url': uuid.uuid4().hex, + 'region': uuid.uuid4().hex, + } + + def test_get_catalog_ignores_endpoints_with_invalid_urls(self): + user_id = uuid.uuid4().hex + tenant_id = uuid.uuid4().hex + + # the only endpoint in the catalog is the one created in setUp + catalog = self.catalog_api.get_v3_catalog(user_id, tenant_id) + self.assertEqual(1, len(catalog[0]['endpoints'])) + # it's also the only endpoint in the backend + self.assertEqual(1, len(self.catalog_api.list_endpoints())) + + # create a new, invalid endpoint - malformed type declaration + ref = self.new_endpoint_ref(self.service_id) + ref['url'] = 'http://keystone/%(tenant_id)' + self.catalog_api.create_endpoint(ref['id'], ref) + + # create a new, invalid endpoint - nonexistent key + ref = self.new_endpoint_ref(self.service_id) + ref['url'] = 'http://keystone/%(you_wont_find_me)s' + self.catalog_api.create_endpoint(ref['id'], ref) + + # verify that the invalid endpoints don't appear in the catalog + catalog = self.catalog_api.get_v3_catalog(user_id, tenant_id) + self.assertEqual(1, len(catalog[0]['endpoints'])) + # all three appear in the backend + self.assertEqual(3, len(self.catalog_api.list_endpoints())) + + def test_get_catalog_always_returns_service_name(self): + user_id = uuid.uuid4().hex + tenant_id = uuid.uuid4().hex + + # create a service, with a name + named_svc = { + 'id': uuid.uuid4().hex, + 'type': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + } + self.catalog_api.create_service(named_svc['id'], named_svc) + endpoint = self.new_endpoint_ref(service_id=named_svc['id']) + self.catalog_api.create_endpoint(endpoint['id'], endpoint) + + # create a service, with no name + unnamed_svc = { + 'id': uuid.uuid4().hex, + 'type': uuid.uuid4().hex + } + self.catalog_api.create_service(unnamed_svc['id'], unnamed_svc) + endpoint = self.new_endpoint_ref(service_id=unnamed_svc['id']) + self.catalog_api.create_endpoint(endpoint['id'], endpoint) + + catalog = self.catalog_api.get_v3_catalog(user_id, tenant_id) + + named_endpoint = [ep for ep in catalog + if ep['type'] == named_svc['type']][0] + self.assertEqual(named_svc['name'], named_endpoint['name']) + + unnamed_endpoint = [ep for ep in catalog + if ep['type'] == unnamed_svc['type']][0] + self.assertEqual('', unnamed_endpoint['name']) + + +# TODO(dstanek): this needs refactoring with the test above, but we are in a +# crunch so that will happen in a future patch. +class TestCatalogAPISQLRegions(tests.TestCase): + """Tests for the catalog Manager against the SQL backend. + + """ + + def setUp(self): + super(TestCatalogAPISQLRegions, self).setUp() + self.useFixture(database.Database()) + self.catalog_api = catalog.Manager() + + def config_overrides(self): + super(TestCatalogAPISQLRegions, self).config_overrides() + self.config_fixture.config( + group='catalog', + driver='keystone.catalog.backends.sql.Catalog') + + def new_endpoint_ref(self, service_id): + return { + 'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'description': uuid.uuid4().hex, + 'interface': uuid.uuid4().hex[:8], + 'service_id': service_id, + 'url': uuid.uuid4().hex, + 'region_id': uuid.uuid4().hex, + } + + def test_get_catalog_returns_proper_endpoints_with_no_region(self): + service_id = uuid.uuid4().hex + service = {'id': service_id, 'name': uuid.uuid4().hex} + self.catalog_api.create_service(service_id, service) + + endpoint = self.new_endpoint_ref(service_id=service_id) + del endpoint['region_id'] + self.catalog_api.create_endpoint(endpoint['id'], endpoint) + + user_id = uuid.uuid4().hex + tenant_id = uuid.uuid4().hex + + catalog = self.catalog_api.get_v3_catalog(user_id, tenant_id) + self.assertValidCatalogEndpoint( + catalog[0]['endpoints'][0], ref=endpoint) + + def test_get_catalog_returns_proper_endpoints_with_region(self): + service_id = uuid.uuid4().hex + service = {'id': service_id, 'name': uuid.uuid4().hex} + self.catalog_api.create_service(service_id, service) + + endpoint = self.new_endpoint_ref(service_id=service_id) + self.catalog_api.create_region({'id': endpoint['region_id']}) + self.catalog_api.create_endpoint(endpoint['id'], endpoint) + + endpoint = self.catalog_api.get_endpoint(endpoint['id']) + user_id = uuid.uuid4().hex + tenant_id = uuid.uuid4().hex + + catalog = self.catalog_api.get_v3_catalog(user_id, tenant_id) + self.assertValidCatalogEndpoint( + catalog[0]['endpoints'][0], ref=endpoint) + + def assertValidCatalogEndpoint(self, entity, ref=None): + keys = ['description', 'id', 'interface', 'name', 'region_id', 'url'] + for k in keys: + self.assertEqual(ref.get(k), entity[k], k) + self.assertEqual(entity['region_id'], entity['region']) diff --git a/keystone-moon/keystone/tests/unit/test_v3_controller.py b/keystone-moon/keystone/tests/unit/test_v3_controller.py new file mode 100644 index 00000000..3ac4ba5a --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_v3_controller.py @@ -0,0 +1,52 @@ +# Copyright 2014 CERN. +# +# 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 + +import six +from testtools import matchers + +from keystone.common import controller +from keystone import exception +from keystone.tests import unit as tests + + +class V3ControllerTestCase(tests.TestCase): + """Tests for the V3Controller class.""" + def setUp(self): + super(V3ControllerTestCase, self).setUp() + + class ControllerUnderTest(controller.V3Controller): + _mutable_parameters = frozenset(['hello', 'world']) + + self.api = ControllerUnderTest() + + def test_check_immutable_params(self): + """Pass valid parameters to the method and expect no failure.""" + ref = { + 'hello': uuid.uuid4().hex, + 'world': uuid.uuid4().hex + } + self.api.check_immutable_params(ref) + + def test_check_immutable_params_fail(self): + """Pass invalid parameter to the method and expect failure.""" + ref = {uuid.uuid4().hex: uuid.uuid4().hex for _ in range(3)} + + ex = self.assertRaises(exception.ImmutableAttributeError, + self.api.check_immutable_params, ref) + ex_msg = six.text_type(ex) + self.assertThat(ex_msg, matchers.Contains(self.api.__class__.__name__)) + for key in ref.keys(): + self.assertThat(ex_msg, matchers.Contains(key)) diff --git a/keystone-moon/keystone/tests/unit/test_v3_credential.py b/keystone-moon/keystone/tests/unit/test_v3_credential.py new file mode 100644 index 00000000..d792b216 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_v3_credential.py @@ -0,0 +1,406 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 hashlib +import json +import uuid + +from keystoneclient.contrib.ec2 import utils as ec2_utils +from oslo_config import cfg + +from keystone import exception +from keystone.tests.unit import test_v3 + + +CONF = cfg.CONF + + +class CredentialBaseTestCase(test_v3.RestfulTestCase): + def _create_dict_blob_credential(self): + blob = {"access": uuid.uuid4().hex, + "secret": uuid.uuid4().hex} + credential_id = hashlib.sha256(blob['access']).hexdigest() + credential = self.new_credential_ref( + user_id=self.user['id'], + project_id=self.project_id) + credential['id'] = credential_id + + # Store the blob as a dict *not* JSON ref bug #1259584 + # This means we can test the dict->json workaround, added + # as part of the bugfix for backwards compatibility works. + credential['blob'] = blob + credential['type'] = 'ec2' + # Create direct via the DB API to avoid validation failure + self.credential_api.create_credential( + credential_id, + credential) + expected_blob = json.dumps(blob) + return expected_blob, credential_id + + +class CredentialTestCase(CredentialBaseTestCase): + """Test credential CRUD.""" + def setUp(self): + + super(CredentialTestCase, self).setUp() + + self.credential_id = uuid.uuid4().hex + self.credential = self.new_credential_ref( + user_id=self.user['id'], + project_id=self.project_id) + self.credential['id'] = self.credential_id + self.credential_api.create_credential( + self.credential_id, + self.credential) + + def test_credential_api_delete_credentials_for_project(self): + self.credential_api.delete_credentials_for_project(self.project_id) + # Test that the credential that we created in .setUp no longer exists + # once we delete all credentials for self.project_id + self.assertRaises(exception.CredentialNotFound, + self.credential_api.get_credential, + credential_id=self.credential_id) + + def test_credential_api_delete_credentials_for_user(self): + self.credential_api.delete_credentials_for_user(self.user_id) + # Test that the credential that we created in .setUp no longer exists + # once we delete all credentials for self.user_id + self.assertRaises(exception.CredentialNotFound, + self.credential_api.get_credential, + credential_id=self.credential_id) + + def test_list_credentials(self): + """Call ``GET /credentials``.""" + r = self.get('/credentials') + self.assertValidCredentialListResponse(r, ref=self.credential) + + def test_list_credentials_filtered_by_user_id(self): + """Call ``GET /credentials?user_id={user_id}``.""" + credential = self.new_credential_ref( + user_id=uuid.uuid4().hex) + self.credential_api.create_credential( + credential['id'], credential) + + r = self.get('/credentials?user_id=%s' % self.user['id']) + self.assertValidCredentialListResponse(r, ref=self.credential) + for cred in r.result['credentials']: + self.assertEqual(self.user['id'], cred['user_id']) + + def test_create_credential(self): + """Call ``POST /credentials``.""" + ref = self.new_credential_ref(user_id=self.user['id']) + r = self.post( + '/credentials', + body={'credential': ref}) + self.assertValidCredentialResponse(r, ref) + + def test_get_credential(self): + """Call ``GET /credentials/{credential_id}``.""" + r = self.get( + '/credentials/%(credential_id)s' % { + 'credential_id': self.credential_id}) + self.assertValidCredentialResponse(r, self.credential) + + def test_update_credential(self): + """Call ``PATCH /credentials/{credential_id}``.""" + ref = self.new_credential_ref( + user_id=self.user['id'], + project_id=self.project_id) + del ref['id'] + r = self.patch( + '/credentials/%(credential_id)s' % { + 'credential_id': self.credential_id}, + body={'credential': ref}) + self.assertValidCredentialResponse(r, ref) + + def test_delete_credential(self): + """Call ``DELETE /credentials/{credential_id}``.""" + self.delete( + '/credentials/%(credential_id)s' % { + 'credential_id': self.credential_id}) + + def test_create_ec2_credential(self): + """Call ``POST /credentials`` for creating ec2 credential.""" + ref = self.new_credential_ref(user_id=self.user['id'], + project_id=self.project_id) + blob = {"access": uuid.uuid4().hex, + "secret": uuid.uuid4().hex} + ref['blob'] = json.dumps(blob) + ref['type'] = 'ec2' + r = self.post( + '/credentials', + body={'credential': ref}) + self.assertValidCredentialResponse(r, ref) + # Assert credential id is same as hash of access key id for + # ec2 credentials + self.assertEqual(r.result['credential']['id'], + hashlib.sha256(blob['access']).hexdigest()) + # Create second ec2 credential with the same access key id and check + # for conflict. + self.post( + '/credentials', + body={'credential': ref}, expected_status=409) + + def test_get_ec2_dict_blob(self): + """Ensure non-JSON blob data is correctly converted.""" + expected_blob, credential_id = self._create_dict_blob_credential() + + r = self.get( + '/credentials/%(credential_id)s' % { + 'credential_id': credential_id}) + self.assertEqual(expected_blob, r.result['credential']['blob']) + + def test_list_ec2_dict_blob(self): + """Ensure non-JSON blob data is correctly converted.""" + expected_blob, credential_id = self._create_dict_blob_credential() + + list_r = self.get('/credentials') + list_creds = list_r.result['credentials'] + list_ids = [r['id'] for r in list_creds] + self.assertIn(credential_id, list_ids) + for r in list_creds: + if r['id'] == credential_id: + self.assertEqual(expected_blob, r['blob']) + + def test_create_non_ec2_credential(self): + """Call ``POST /credentials`` for creating non-ec2 credential.""" + ref = self.new_credential_ref(user_id=self.user['id']) + blob = {"access": uuid.uuid4().hex, + "secret": uuid.uuid4().hex} + ref['blob'] = json.dumps(blob) + r = self.post( + '/credentials', + body={'credential': ref}) + self.assertValidCredentialResponse(r, ref) + # Assert credential id is not same as hash of access key id for + # non-ec2 credentials + self.assertNotEqual(r.result['credential']['id'], + hashlib.sha256(blob['access']).hexdigest()) + + def test_create_ec2_credential_with_missing_project_id(self): + """Call ``POST /credentials`` for creating ec2 + credential with missing project_id. + """ + ref = self.new_credential_ref(user_id=self.user['id']) + blob = {"access": uuid.uuid4().hex, + "secret": uuid.uuid4().hex} + ref['blob'] = json.dumps(blob) + ref['type'] = 'ec2' + # Assert 400 status for bad request with missing project_id + self.post( + '/credentials', + body={'credential': ref}, expected_status=400) + + def test_create_ec2_credential_with_invalid_blob(self): + """Call ``POST /credentials`` for creating ec2 + credential with invalid blob. + """ + ref = self.new_credential_ref(user_id=self.user['id'], + project_id=self.project_id) + ref['blob'] = '{"abc":"def"d}' + ref['type'] = 'ec2' + # Assert 400 status for bad request containing invalid + # blob + response = self.post( + '/credentials', + body={'credential': ref}, expected_status=400) + self.assertValidErrorResponse(response) + + def test_create_credential_with_admin_token(self): + # Make sure we can create credential with the static admin token + ref = self.new_credential_ref(user_id=self.user['id']) + r = self.post( + '/credentials', + body={'credential': ref}, + token=CONF.admin_token) + self.assertValidCredentialResponse(r, ref) + + +class TestCredentialTrustScoped(test_v3.RestfulTestCase): + """Test credential with trust scoped token.""" + def setUp(self): + super(TestCredentialTrustScoped, self).setUp() + + self.trustee_user = self.new_user_ref(domain_id=self.domain_id) + password = self.trustee_user['password'] + self.trustee_user = self.identity_api.create_user(self.trustee_user) + self.trustee_user['password'] = password + self.trustee_user_id = self.trustee_user['id'] + + def config_overrides(self): + super(TestCredentialTrustScoped, self).config_overrides() + self.config_fixture.config(group='trust', enabled=True) + + def test_trust_scoped_ec2_credential(self): + """Call ``POST /credentials`` for creating ec2 credential.""" + # Create the trust + ref = self.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user_id, + project_id=self.project_id, + impersonation=True, + expires=dict(minutes=1), + role_ids=[self.role_id]) + del ref['id'] + r = self.post('/OS-TRUST/trusts', body={'trust': ref}) + trust = self.assertValidTrustResponse(r) + + # Get a trust scoped token + auth_data = self.build_authentication_request( + user_id=self.trustee_user['id'], + password=self.trustee_user['password'], + trust_id=trust['id']) + r = self.v3_authenticate_token(auth_data) + self.assertValidProjectTrustScopedTokenResponse(r, self.user) + trust_id = r.result['token']['OS-TRUST:trust']['id'] + token_id = r.headers.get('X-Subject-Token') + + # Create the credential with the trust scoped token + ref = self.new_credential_ref(user_id=self.user['id'], + project_id=self.project_id) + blob = {"access": uuid.uuid4().hex, + "secret": uuid.uuid4().hex} + ref['blob'] = json.dumps(blob) + ref['type'] = 'ec2' + r = self.post( + '/credentials', + body={'credential': ref}, + token=token_id) + + # We expect the response blob to contain the trust_id + ret_ref = ref.copy() + ret_blob = blob.copy() + ret_blob['trust_id'] = trust_id + ret_ref['blob'] = json.dumps(ret_blob) + self.assertValidCredentialResponse(r, ref=ret_ref) + + # Assert credential id is same as hash of access key id for + # ec2 credentials + self.assertEqual(r.result['credential']['id'], + hashlib.sha256(blob['access']).hexdigest()) + + # Create second ec2 credential with the same access key id and check + # for conflict. + self.post( + '/credentials', + body={'credential': ref}, + token=token_id, + expected_status=409) + + +class TestCredentialEc2(CredentialBaseTestCase): + """Test v3 credential compatibility with ec2tokens.""" + def setUp(self): + super(TestCredentialEc2, self).setUp() + + def _validate_signature(self, access, secret): + """Test signature validation with the access/secret provided.""" + signer = ec2_utils.Ec2Signer(secret) + params = {'SignatureMethod': 'HmacSHA256', + 'SignatureVersion': '2', + 'AWSAccessKeyId': access} + request = {'host': 'foo', + 'verb': 'GET', + 'path': '/bar', + 'params': params} + signature = signer.generate(request) + + # Now make a request to validate the signed dummy request via the + # ec2tokens API. This proves the v3 ec2 credentials actually work. + sig_ref = {'access': access, + 'signature': signature, + 'host': 'foo', + 'verb': 'GET', + 'path': '/bar', + 'params': params} + r = self.post( + '/ec2tokens', + body={'ec2Credentials': sig_ref}, + expected_status=200) + self.assertValidTokenResponse(r) + + def test_ec2_credential_signature_validate(self): + """Test signature validation with a v3 ec2 credential.""" + ref = self.new_credential_ref( + user_id=self.user['id'], + project_id=self.project_id) + blob = {"access": uuid.uuid4().hex, + "secret": uuid.uuid4().hex} + ref['blob'] = json.dumps(blob) + ref['type'] = 'ec2' + r = self.post( + '/credentials', + body={'credential': ref}) + self.assertValidCredentialResponse(r, ref) + # Assert credential id is same as hash of access key id + self.assertEqual(r.result['credential']['id'], + hashlib.sha256(blob['access']).hexdigest()) + + cred_blob = json.loads(r.result['credential']['blob']) + self.assertEqual(blob, cred_blob) + self._validate_signature(access=cred_blob['access'], + secret=cred_blob['secret']) + + def test_ec2_credential_signature_validate_legacy(self): + """Test signature validation with a legacy v3 ec2 credential.""" + cred_json, credential_id = self._create_dict_blob_credential() + cred_blob = json.loads(cred_json) + self._validate_signature(access=cred_blob['access'], + secret=cred_blob['secret']) + + def _get_ec2_cred_uri(self): + return '/users/%s/credentials/OS-EC2' % self.user_id + + def _get_ec2_cred(self): + uri = self._get_ec2_cred_uri() + r = self.post(uri, body={'tenant_id': self.project_id}) + return r.result['credential'] + + def test_ec2_create_credential(self): + """Test ec2 credential creation.""" + ec2_cred = self._get_ec2_cred() + self.assertEqual(self.user_id, ec2_cred['user_id']) + self.assertEqual(self.project_id, ec2_cred['tenant_id']) + self.assertIsNone(ec2_cred['trust_id']) + self._validate_signature(access=ec2_cred['access'], + secret=ec2_cred['secret']) + + return ec2_cred + + def test_ec2_get_credential(self): + ec2_cred = self._get_ec2_cred() + uri = '/'.join([self._get_ec2_cred_uri(), ec2_cred['access']]) + r = self.get(uri) + self.assertDictEqual(ec2_cred, r.result['credential']) + + def test_ec2_list_credentials(self): + """Test ec2 credential listing.""" + self._get_ec2_cred() + uri = self._get_ec2_cred_uri() + r = self.get(uri) + cred_list = r.result['credentials'] + self.assertEqual(1, len(cred_list)) + + def test_ec2_delete_credential(self): + """Test ec2 credential deletion.""" + ec2_cred = self._get_ec2_cred() + uri = '/'.join([self._get_ec2_cred_uri(), ec2_cred['access']]) + cred_from_credential_api = ( + self.credential_api + .list_credentials_for_user(self.user_id)) + self.assertEqual(1, len(cred_from_credential_api)) + self.delete(uri) + self.assertRaises(exception.CredentialNotFound, + self.credential_api.get_credential, + cred_from_credential_api[0]['id']) diff --git a/keystone-moon/keystone/tests/unit/test_v3_domain_config.py b/keystone-moon/keystone/tests/unit/test_v3_domain_config.py new file mode 100644 index 00000000..6f96f0e7 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_v3_domain_config.py @@ -0,0 +1,210 @@ +# 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 +import uuid + +from oslo_config import cfg + +from keystone import exception +from keystone.tests.unit import test_v3 + + +CONF = cfg.CONF + + +class DomainConfigTestCase(test_v3.RestfulTestCase): + """Test domain config support.""" + + def setUp(self): + super(DomainConfigTestCase, self).setUp() + + self.domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(self.domain['id'], self.domain) + self.config = {'ldap': {'url': uuid.uuid4().hex, + 'user_tree_dn': uuid.uuid4().hex}, + 'identity': {'driver': uuid.uuid4().hex}} + + def test_create_config(self): + """Call ``PUT /domains/{domain_id}/config``.""" + url = '/domains/%(domain_id)s/config' % { + 'domain_id': self.domain['id']} + r = self.put(url, body={'config': self.config}, + expected_status=201) + res = self.domain_config_api.get_config(self.domain['id']) + self.assertEqual(self.config, r.result['config']) + self.assertEqual(self.config, res) + + def test_create_config_twice(self): + """Check multiple creates don't throw error""" + self.put('/domains/%(domain_id)s/config' % { + 'domain_id': self.domain['id']}, + body={'config': self.config}, + expected_status=201) + self.put('/domains/%(domain_id)s/config' % { + 'domain_id': self.domain['id']}, + body={'config': self.config}, + expected_status=200) + + def test_delete_config(self): + """Call ``DELETE /domains{domain_id}/config``.""" + self.domain_config_api.create_config(self.domain['id'], self.config) + self.delete('/domains/%(domain_id)s/config' % { + 'domain_id': self.domain['id']}) + self.get('/domains/%(domain_id)s/config' % { + 'domain_id': self.domain['id']}, + expected_status=exception.DomainConfigNotFound.code) + + def test_delete_config_by_group(self): + """Call ``DELETE /domains{domain_id}/config/{group}``.""" + self.domain_config_api.create_config(self.domain['id'], self.config) + self.delete('/domains/%(domain_id)s/config/ldap' % { + 'domain_id': self.domain['id']}) + res = self.domain_config_api.get_config(self.domain['id']) + self.assertNotIn('ldap', res) + + def test_get_head_config(self): + """Call ``GET & HEAD for /domains{domain_id}/config``.""" + self.domain_config_api.create_config(self.domain['id'], self.config) + url = '/domains/%(domain_id)s/config' % { + 'domain_id': self.domain['id']} + r = self.get(url) + self.assertEqual(self.config, r.result['config']) + self.head(url, expected_status=200) + + def test_get_config_by_group(self): + """Call ``GET & HEAD /domains{domain_id}/config/{group}``.""" + self.domain_config_api.create_config(self.domain['id'], self.config) + url = '/domains/%(domain_id)s/config/ldap' % { + 'domain_id': self.domain['id']} + r = self.get(url) + self.assertEqual({'ldap': self.config['ldap']}, r.result['config']) + self.head(url, expected_status=200) + + def test_get_config_by_option(self): + """Call ``GET & HEAD /domains{domain_id}/config/{group}/{option}``.""" + self.domain_config_api.create_config(self.domain['id'], self.config) + url = '/domains/%(domain_id)s/config/ldap/url' % { + 'domain_id': self.domain['id']} + r = self.get(url) + self.assertEqual({'url': self.config['ldap']['url']}, + r.result['config']) + self.head(url, expected_status=200) + + def test_get_non_existant_config(self): + """Call ``GET /domains{domain_id}/config when no config defined``.""" + self.get('/domains/%(domain_id)s/config' % { + 'domain_id': self.domain['id']}, expected_status=404) + + def test_get_non_existant_config_group(self): + """Call ``GET /domains{domain_id}/config/{group_not_exist}``.""" + config = {'ldap': {'url': uuid.uuid4().hex}} + self.domain_config_api.create_config(self.domain['id'], config) + self.get('/domains/%(domain_id)s/config/identity' % { + 'domain_id': self.domain['id']}, expected_status=404) + + def test_get_non_existant_config_option(self): + """Call ``GET /domains{domain_id}/config/group/{option_not_exist}``.""" + config = {'ldap': {'url': uuid.uuid4().hex}} + self.domain_config_api.create_config(self.domain['id'], config) + self.get('/domains/%(domain_id)s/config/ldap/user_tree_dn' % { + 'domain_id': self.domain['id']}, expected_status=404) + + def test_update_config(self): + """Call ``PATCH /domains/{domain_id}/config``.""" + self.domain_config_api.create_config(self.domain['id'], self.config) + new_config = {'ldap': {'url': uuid.uuid4().hex}, + 'identity': {'driver': uuid.uuid4().hex}} + r = self.patch('/domains/%(domain_id)s/config' % { + 'domain_id': self.domain['id']}, + body={'config': new_config}) + res = self.domain_config_api.get_config(self.domain['id']) + expected_config = copy.deepcopy(self.config) + expected_config['ldap']['url'] = new_config['ldap']['url'] + expected_config['identity']['driver'] = ( + new_config['identity']['driver']) + self.assertEqual(expected_config, r.result['config']) + self.assertEqual(expected_config, res) + + def test_update_config_group(self): + """Call ``PATCH /domains/{domain_id}/config/{group}``.""" + self.domain_config_api.create_config(self.domain['id'], self.config) + new_config = {'ldap': {'url': uuid.uuid4().hex, + 'user_filter': uuid.uuid4().hex}} + r = self.patch('/domains/%(domain_id)s/config/ldap' % { + 'domain_id': self.domain['id']}, + body={'config': new_config}) + res = self.domain_config_api.get_config(self.domain['id']) + expected_config = copy.deepcopy(self.config) + expected_config['ldap']['url'] = new_config['ldap']['url'] + expected_config['ldap']['user_filter'] = ( + new_config['ldap']['user_filter']) + self.assertEqual(expected_config, r.result['config']) + self.assertEqual(expected_config, res) + + def test_update_config_invalid_group(self): + """Call ``PATCH /domains/{domain_id}/config/{invalid_group}``.""" + self.domain_config_api.create_config(self.domain['id'], self.config) + + # Trying to update a group that is neither whitelisted or sensitive + # should result in Forbidden. + invalid_group = uuid.uuid4().hex + new_config = {invalid_group: {'url': uuid.uuid4().hex, + 'user_filter': uuid.uuid4().hex}} + self.patch('/domains/%(domain_id)s/config/%(invalid_group)s' % { + 'domain_id': self.domain['id'], 'invalid_group': invalid_group}, + body={'config': new_config}, + expected_status=403) + # Trying to update a valid group, but one that is not in the current + # config should result in NotFound + config = {'ldap': {'suffix': uuid.uuid4().hex}} + self.domain_config_api.create_config(self.domain['id'], config) + new_config = {'identity': {'driver': uuid.uuid4().hex}} + self.patch('/domains/%(domain_id)s/config/identity' % { + 'domain_id': self.domain['id']}, + body={'config': new_config}, + expected_status=404) + + def test_update_config_option(self): + """Call ``PATCH /domains/{domain_id}/config/{group}/{option}``.""" + self.domain_config_api.create_config(self.domain['id'], self.config) + new_config = {'url': uuid.uuid4().hex} + r = self.patch('/domains/%(domain_id)s/config/ldap/url' % { + 'domain_id': self.domain['id']}, + body={'config': new_config}) + res = self.domain_config_api.get_config(self.domain['id']) + expected_config = copy.deepcopy(self.config) + expected_config['ldap']['url'] = new_config['url'] + self.assertEqual(expected_config, r.result['config']) + self.assertEqual(expected_config, res) + + def test_update_config_invalid_option(self): + """Call ``PATCH /domains/{domain_id}/config/{group}/{invalid}``.""" + self.domain_config_api.create_config(self.domain['id'], self.config) + invalid_option = uuid.uuid4().hex + new_config = {'ldap': {invalid_option: uuid.uuid4().hex}} + # Trying to update an option that is neither whitelisted or sensitive + # should result in Forbidden. + self.patch( + '/domains/%(domain_id)s/config/ldap/%(invalid_option)s' % { + 'domain_id': self.domain['id'], + 'invalid_option': invalid_option}, + body={'config': new_config}, + expected_status=403) + # Trying to update a valid option, but one that is not in the current + # config should result in NotFound + new_config = {'suffix': uuid.uuid4().hex} + self.patch( + '/domains/%(domain_id)s/config/ldap/suffix' % { + 'domain_id': self.domain['id']}, + body={'config': new_config}, + expected_status=404) diff --git a/keystone-moon/keystone/tests/unit/test_v3_endpoint_policy.py b/keystone-moon/keystone/tests/unit/test_v3_endpoint_policy.py new file mode 100644 index 00000000..437fb155 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_v3_endpoint_policy.py @@ -0,0 +1,251 @@ +# Copyright 2014 IBM Corp. +# +# 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 testtools import matchers + +from keystone.tests.unit import test_v3 + + +class TestExtensionCase(test_v3.RestfulTestCase): + + EXTENSION_NAME = 'endpoint_policy' + EXTENSION_TO_ADD = 'endpoint_policy_extension' + + +class EndpointPolicyTestCase(TestExtensionCase): + """Test endpoint policy CRUD. + + In general, the controller layer of the endpoint policy extension is really + just marshalling the data around the underlying manager calls. Given that + the manager layer is tested in depth by the backend tests, the tests we + execute here concentrate on ensuring we are correctly passing and + presenting the data. + + """ + + def setUp(self): + super(EndpointPolicyTestCase, self).setUp() + self.policy = self.new_policy_ref() + self.policy_api.create_policy(self.policy['id'], self.policy) + self.service = self.new_service_ref() + self.catalog_api.create_service(self.service['id'], self.service) + self.endpoint = self.new_endpoint_ref(self.service['id'], enabled=True) + self.catalog_api.create_endpoint(self.endpoint['id'], self.endpoint) + self.region = self.new_region_ref() + self.catalog_api.create_region(self.region) + + def assert_head_and_get_return_same_response(self, url, expected_status): + self.get(url, expected_status=expected_status) + self.head(url, expected_status=expected_status) + + # endpoint policy crud tests + def _crud_test(self, url): + # Test when the resource does not exist also ensures + # that there is not a false negative after creation. + + self.assert_head_and_get_return_same_response(url, expected_status=404) + + self.put(url, expected_status=204) + + # test that the new resource is accessible. + self.assert_head_and_get_return_same_response(url, expected_status=204) + + self.delete(url, expected_status=204) + + # test that the deleted resource is no longer accessible + self.assert_head_and_get_return_same_response(url, expected_status=404) + + def test_crud_for_policy_for_explicit_endpoint(self): + """PUT, HEAD and DELETE for explicit endpoint policy.""" + + url = ('/policies/%(policy_id)s/OS-ENDPOINT-POLICY' + '/endpoints/%(endpoint_id)s') % { + 'policy_id': self.policy['id'], + 'endpoint_id': self.endpoint['id']} + self._crud_test(url) + + def test_crud_for_policy_for_service(self): + """PUT, HEAD and DELETE for service endpoint policy.""" + + url = ('/policies/%(policy_id)s/OS-ENDPOINT-POLICY' + '/services/%(service_id)s') % { + 'policy_id': self.policy['id'], + 'service_id': self.service['id']} + self._crud_test(url) + + def test_crud_for_policy_for_region_and_service(self): + """PUT, HEAD and DELETE for region and service endpoint policy.""" + + url = ('/policies/%(policy_id)s/OS-ENDPOINT-POLICY' + '/services/%(service_id)s/regions/%(region_id)s') % { + 'policy_id': self.policy['id'], + 'service_id': self.service['id'], + 'region_id': self.region['id']} + self._crud_test(url) + + def test_get_policy_for_endpoint(self): + """GET /endpoints/{endpoint_id}/policy.""" + + self.put('/policies/%(policy_id)s/OS-ENDPOINT-POLICY' + '/endpoints/%(endpoint_id)s' % { + 'policy_id': self.policy['id'], + 'endpoint_id': self.endpoint['id']}, + expected_status=204) + + self.head('/endpoints/%(endpoint_id)s/OS-ENDPOINT-POLICY' + '/policy' % { + 'endpoint_id': self.endpoint['id']}, + expected_status=200) + + r = self.get('/endpoints/%(endpoint_id)s/OS-ENDPOINT-POLICY' + '/policy' % { + 'endpoint_id': self.endpoint['id']}, + expected_status=200) + self.assertValidPolicyResponse(r, ref=self.policy) + + def test_list_endpoints_for_policy(self): + """GET /policies/%(policy_id}/endpoints.""" + + self.put('/policies/%(policy_id)s/OS-ENDPOINT-POLICY' + '/endpoints/%(endpoint_id)s' % { + 'policy_id': self.policy['id'], + 'endpoint_id': self.endpoint['id']}, + expected_status=204) + + r = self.get('/policies/%(policy_id)s/OS-ENDPOINT-POLICY' + '/endpoints' % { + 'policy_id': self.policy['id']}, + expected_status=200) + self.assertValidEndpointListResponse(r, ref=self.endpoint) + self.assertThat(r.result.get('endpoints'), matchers.HasLength(1)) + + def test_endpoint_association_cleanup_when_endpoint_deleted(self): + url = ('/policies/%(policy_id)s/OS-ENDPOINT-POLICY' + '/endpoints/%(endpoint_id)s') % { + 'policy_id': self.policy['id'], + 'endpoint_id': self.endpoint['id']} + + self.put(url, expected_status=204) + self.head(url, expected_status=204) + + self.delete('/endpoints/%(endpoint_id)s' % { + 'endpoint_id': self.endpoint['id']}) + + self.head(url, expected_status=404) + + def test_region_service_association_cleanup_when_region_deleted(self): + url = ('/policies/%(policy_id)s/OS-ENDPOINT-POLICY' + '/services/%(service_id)s/regions/%(region_id)s') % { + 'policy_id': self.policy['id'], + 'service_id': self.service['id'], + 'region_id': self.region['id']} + + self.put(url, expected_status=204) + self.head(url, expected_status=204) + + self.delete('/regions/%(region_id)s' % { + 'region_id': self.region['id']}) + + self.head(url, expected_status=404) + + def test_region_service_association_cleanup_when_service_deleted(self): + url = ('/policies/%(policy_id)s/OS-ENDPOINT-POLICY' + '/services/%(service_id)s/regions/%(region_id)s') % { + 'policy_id': self.policy['id'], + 'service_id': self.service['id'], + 'region_id': self.region['id']} + + self.put(url, expected_status=204) + self.head(url, expected_status=204) + + self.delete('/services/%(service_id)s' % { + 'service_id': self.service['id']}) + + self.head(url, expected_status=404) + + def test_service_association_cleanup_when_service_deleted(self): + url = ('/policies/%(policy_id)s/OS-ENDPOINT-POLICY' + '/services/%(service_id)s') % { + 'policy_id': self.policy['id'], + 'service_id': self.service['id']} + + self.put(url, expected_status=204) + self.get(url, expected_status=204) + + self.delete('/policies/%(policy_id)s' % { + 'policy_id': self.policy['id']}) + + self.head(url, expected_status=404) + + def test_service_association_cleanup_when_policy_deleted(self): + url = ('/policies/%(policy_id)s/OS-ENDPOINT-POLICY' + '/services/%(service_id)s') % { + 'policy_id': self.policy['id'], + 'service_id': self.service['id']} + + self.put(url, expected_status=204) + self.get(url, expected_status=204) + + self.delete('/services/%(service_id)s' % { + 'service_id': self.service['id']}) + + self.head(url, expected_status=404) + + +class JsonHomeTests(TestExtensionCase, test_v3.JsonHomeTestMixin): + EXTENSION_LOCATION = ('http://docs.openstack.org/api/openstack-identity/3/' + 'ext/OS-ENDPOINT-POLICY/1.0/rel') + PARAM_LOCATION = 'http://docs.openstack.org/api/openstack-identity/3/param' + + JSON_HOME_DATA = { + EXTENSION_LOCATION + '/endpoint_policy': { + 'href-template': '/endpoints/{endpoint_id}/OS-ENDPOINT-POLICY/' + 'policy', + 'href-vars': { + 'endpoint_id': PARAM_LOCATION + '/endpoint_id', + }, + }, + EXTENSION_LOCATION + '/policy_endpoints': { + 'href-template': '/policies/{policy_id}/OS-ENDPOINT-POLICY/' + 'endpoints', + 'href-vars': { + 'policy_id': PARAM_LOCATION + '/policy_id', + }, + }, + EXTENSION_LOCATION + '/endpoint_policy_association': { + 'href-template': '/policies/{policy_id}/OS-ENDPOINT-POLICY/' + 'endpoints/{endpoint_id}', + 'href-vars': { + 'policy_id': PARAM_LOCATION + '/policy_id', + 'endpoint_id': PARAM_LOCATION + '/endpoint_id', + }, + }, + EXTENSION_LOCATION + '/service_policy_association': { + 'href-template': '/policies/{policy_id}/OS-ENDPOINT-POLICY/' + 'services/{service_id}', + 'href-vars': { + 'policy_id': PARAM_LOCATION + '/policy_id', + 'service_id': PARAM_LOCATION + '/service_id', + }, + }, + EXTENSION_LOCATION + '/region_and_service_policy_association': { + 'href-template': '/policies/{policy_id}/OS-ENDPOINT-POLICY/' + 'services/{service_id}/regions/{region_id}', + 'href-vars': { + 'policy_id': PARAM_LOCATION + '/policy_id', + 'service_id': PARAM_LOCATION + '/service_id', + 'region_id': PARAM_LOCATION + '/region_id', + }, + }, + } diff --git a/keystone-moon/keystone/tests/unit/test_v3_federation.py b/keystone-moon/keystone/tests/unit/test_v3_federation.py new file mode 100644 index 00000000..3b6f4d8b --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_v3_federation.py @@ -0,0 +1,3296 @@ +# 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 os +import random +import subprocess +import uuid + +from lxml import etree +import mock +from oslo_config import cfg +from oslo_log import log +from oslo_serialization import jsonutils +from oslotest import mockpatch +import saml2 +from saml2 import saml +from saml2 import sigver +from six.moves import urllib +import xmldsig + +from keystone.auth import controllers as auth_controllers +from keystone.auth.plugins import mapped +from keystone.contrib import federation +from keystone.contrib.federation import controllers as federation_controllers +from keystone.contrib.federation import idp as keystone_idp +from keystone.contrib.federation import utils as mapping_utils +from keystone import exception +from keystone import notifications +from keystone.tests.unit import core +from keystone.tests.unit import federation_fixtures +from keystone.tests.unit import ksfixtures +from keystone.tests.unit import mapping_fixtures +from keystone.tests.unit import test_v3 +from keystone.token.providers import common as token_common + + +CONF = cfg.CONF +LOG = log.getLogger(__name__) +ROOTDIR = os.path.dirname(os.path.abspath(__file__)) +XMLDIR = os.path.join(ROOTDIR, 'saml2/') + + +def dummy_validator(*args, **kwargs): + pass + + +class FederationTests(test_v3.RestfulTestCase): + + EXTENSION_NAME = 'federation' + EXTENSION_TO_ADD = 'federation_extension' + + +class FederatedSetupMixin(object): + + ACTION = 'authenticate' + IDP = 'ORG_IDP' + PROTOCOL = 'saml2' + AUTH_METHOD = 'saml2' + USER = 'user@ORGANIZATION' + ASSERTION_PREFIX = 'PREFIX_' + IDP_WITH_REMOTE = 'ORG_IDP_REMOTE' + REMOTE_ID = 'entityID_IDP' + REMOTE_ID_ATTR = uuid.uuid4().hex + + UNSCOPED_V3_SAML2_REQ = { + "identity": { + "methods": [AUTH_METHOD], + AUTH_METHOD: { + "identity_provider": IDP, + "protocol": PROTOCOL + } + } + } + + def _check_domains_are_valid(self, token): + self.assertEqual('Federated', token['user']['domain']['id']) + self.assertEqual('Federated', token['user']['domain']['name']) + + def _project(self, project): + return (project['id'], project['name']) + + def _roles(self, roles): + return set([(r['id'], r['name']) for r in roles]) + + def _check_projects_and_roles(self, token, roles, projects): + """Check whether the projects and the roles match.""" + token_roles = token.get('roles') + if token_roles is None: + raise AssertionError('Roles not found in the token') + token_roles = self._roles(token_roles) + roles_ref = self._roles(roles) + self.assertEqual(token_roles, roles_ref) + + token_projects = token.get('project') + if token_projects is None: + raise AssertionError('Projects not found in the token') + token_projects = self._project(token_projects) + projects_ref = self._project(projects) + self.assertEqual(token_projects, projects_ref) + + def _check_scoped_token_attributes(self, token): + def xor_project_domain(iterable): + return sum(('project' in iterable, 'domain' in iterable)) % 2 + + for obj in ('user', 'catalog', 'expires_at', 'issued_at', + 'methods', 'roles'): + self.assertIn(obj, token) + # Check for either project or domain + if not xor_project_domain(token.keys()): + raise AssertionError("You must specify either" + "project or domain.") + + self.assertIn('OS-FEDERATION', token['user']) + os_federation = token['user']['OS-FEDERATION'] + self.assertEqual(self.IDP, os_federation['identity_provider']['id']) + self.assertEqual(self.PROTOCOL, os_federation['protocol']['id']) + + def _issue_unscoped_token(self, + idp=None, + assertion='EMPLOYEE_ASSERTION', + environment=None): + api = federation_controllers.Auth() + context = {'environment': environment or {}} + self._inject_assertion(context, assertion) + if idp is None: + idp = self.IDP + r = api.federated_authentication(context, idp, self.PROTOCOL) + return r + + def idp_ref(self, id=None): + idp = { + 'id': id or uuid.uuid4().hex, + 'enabled': True, + 'description': uuid.uuid4().hex + } + return idp + + def proto_ref(self, mapping_id=None): + proto = { + 'id': uuid.uuid4().hex, + 'mapping_id': mapping_id or uuid.uuid4().hex + } + return proto + + def mapping_ref(self, rules=None): + return { + 'id': uuid.uuid4().hex, + 'rules': rules or self.rules['rules'] + } + + def _scope_request(self, unscoped_token_id, scope, scope_id): + return { + 'auth': { + 'identity': { + 'methods': [ + self.AUTH_METHOD + ], + self.AUTH_METHOD: { + 'id': unscoped_token_id + } + }, + 'scope': { + scope: { + 'id': scope_id + } + } + } + } + + def _inject_assertion(self, context, variant, query_string=None): + assertion = getattr(mapping_fixtures, variant) + context['environment'].update(assertion) + context['query_string'] = query_string or [] + + def load_federation_sample_data(self): + """Inject additional data.""" + + # Create and add domains + self.domainA = self.new_domain_ref() + self.resource_api.create_domain(self.domainA['id'], + self.domainA) + + self.domainB = self.new_domain_ref() + self.resource_api.create_domain(self.domainB['id'], + self.domainB) + + self.domainC = self.new_domain_ref() + self.resource_api.create_domain(self.domainC['id'], + self.domainC) + + self.domainD = self.new_domain_ref() + self.resource_api.create_domain(self.domainD['id'], + self.domainD) + + # Create and add projects + self.proj_employees = self.new_project_ref( + domain_id=self.domainA['id']) + self.resource_api.create_project(self.proj_employees['id'], + self.proj_employees) + self.proj_customers = self.new_project_ref( + domain_id=self.domainA['id']) + self.resource_api.create_project(self.proj_customers['id'], + self.proj_customers) + + self.project_all = self.new_project_ref( + domain_id=self.domainA['id']) + self.resource_api.create_project(self.project_all['id'], + self.project_all) + + self.project_inherited = self.new_project_ref( + domain_id=self.domainD['id']) + self.resource_api.create_project(self.project_inherited['id'], + self.project_inherited) + + # Create and add groups + self.group_employees = self.new_group_ref( + domain_id=self.domainA['id']) + self.group_employees = ( + self.identity_api.create_group(self.group_employees)) + + self.group_customers = self.new_group_ref( + domain_id=self.domainA['id']) + self.group_customers = ( + self.identity_api.create_group(self.group_customers)) + + self.group_admins = self.new_group_ref( + domain_id=self.domainA['id']) + self.group_admins = self.identity_api.create_group(self.group_admins) + + # Create and add roles + self.role_employee = self.new_role_ref() + self.role_api.create_role(self.role_employee['id'], self.role_employee) + self.role_customer = self.new_role_ref() + self.role_api.create_role(self.role_customer['id'], self.role_customer) + + self.role_admin = self.new_role_ref() + self.role_api.create_role(self.role_admin['id'], self.role_admin) + + # Employees can access + # * proj_employees + # * project_all + self.assignment_api.create_grant(self.role_employee['id'], + group_id=self.group_employees['id'], + project_id=self.proj_employees['id']) + self.assignment_api.create_grant(self.role_employee['id'], + group_id=self.group_employees['id'], + project_id=self.project_all['id']) + # Customers can access + # * proj_customers + self.assignment_api.create_grant(self.role_customer['id'], + group_id=self.group_customers['id'], + project_id=self.proj_customers['id']) + + # Admins can access: + # * proj_customers + # * proj_employees + # * project_all + self.assignment_api.create_grant(self.role_admin['id'], + group_id=self.group_admins['id'], + project_id=self.proj_customers['id']) + self.assignment_api.create_grant(self.role_admin['id'], + group_id=self.group_admins['id'], + project_id=self.proj_employees['id']) + self.assignment_api.create_grant(self.role_admin['id'], + group_id=self.group_admins['id'], + project_id=self.project_all['id']) + + self.assignment_api.create_grant(self.role_customer['id'], + group_id=self.group_customers['id'], + domain_id=self.domainA['id']) + + # Customers can access: + # * domain A + self.assignment_api.create_grant(self.role_customer['id'], + group_id=self.group_customers['id'], + domain_id=self.domainA['id']) + + # Customers can access projects via inheritance: + # * domain D + self.assignment_api.create_grant(self.role_customer['id'], + group_id=self.group_customers['id'], + domain_id=self.domainD['id'], + inherited_to_projects=True) + + # Employees can access: + # * domain A + # * domain B + + self.assignment_api.create_grant(self.role_employee['id'], + group_id=self.group_employees['id'], + domain_id=self.domainA['id']) + self.assignment_api.create_grant(self.role_employee['id'], + group_id=self.group_employees['id'], + domain_id=self.domainB['id']) + + # Admins can access: + # * domain A + # * domain B + # * domain C + self.assignment_api.create_grant(self.role_admin['id'], + group_id=self.group_admins['id'], + domain_id=self.domainA['id']) + self.assignment_api.create_grant(self.role_admin['id'], + group_id=self.group_admins['id'], + domain_id=self.domainB['id']) + + self.assignment_api.create_grant(self.role_admin['id'], + group_id=self.group_admins['id'], + domain_id=self.domainC['id']) + self.rules = { + 'rules': [ + { + 'local': [ + { + 'group': { + 'id': self.group_employees['id'] + } + }, + { + 'user': { + 'name': '{0}' + } + } + ], + 'remote': [ + { + 'type': 'UserName' + }, + { + 'type': 'orgPersonType', + 'any_one_of': [ + 'Employee' + ] + } + ] + }, + { + 'local': [ + { + 'group': { + 'id': self.group_employees['id'] + } + }, + { + 'user': { + 'name': '{0}' + } + } + ], + 'remote': [ + { + 'type': self.ASSERTION_PREFIX + 'UserName' + }, + { + 'type': self.ASSERTION_PREFIX + 'orgPersonType', + 'any_one_of': [ + 'SuperEmployee' + ] + } + ] + }, + { + 'local': [ + { + 'group': { + 'id': self.group_customers['id'] + } + }, + { + 'user': { + 'name': '{0}' + } + } + ], + 'remote': [ + { + 'type': 'UserName' + }, + { + 'type': 'orgPersonType', + 'any_one_of': [ + 'Customer' + ] + } + ] + }, + { + 'local': [ + { + 'group': { + 'id': self.group_admins['id'] + } + }, + { + 'group': { + 'id': self.group_employees['id'] + } + }, + { + 'group': { + 'id': self.group_customers['id'] + } + }, + + { + 'user': { + 'name': '{0}' + } + } + ], + 'remote': [ + { + 'type': 'UserName' + }, + { + 'type': 'orgPersonType', + 'any_one_of': [ + 'Admin', + 'Chief' + ] + } + ] + }, + { + 'local': [ + { + 'group': { + 'id': uuid.uuid4().hex + } + }, + { + 'group': { + 'id': self.group_customers['id'] + } + }, + { + 'user': { + 'name': '{0}' + } + } + ], + 'remote': [ + { + 'type': 'UserName', + }, + { + 'type': 'FirstName', + 'any_one_of': [ + 'Jill' + ] + }, + { + 'type': 'LastName', + 'any_one_of': [ + 'Smith' + ] + } + ] + }, + { + 'local': [ + { + 'group': { + 'id': 'this_group_no_longer_exists' + } + }, + { + 'user': { + 'name': '{0}' + } + } + ], + 'remote': [ + { + 'type': 'UserName', + }, + { + 'type': 'Email', + 'any_one_of': [ + 'testacct@example.com' + ] + }, + { + 'type': 'orgPersonType', + 'any_one_of': [ + 'Tester' + ] + } + ] + }, + # rules with local group names + { + "local": [ + { + 'user': { + 'name': '{0}' + } + }, + { + "group": { + "name": self.group_customers['name'], + "domain": { + "name": self.domainA['name'] + } + } + } + ], + "remote": [ + { + 'type': 'UserName', + }, + { + "type": "orgPersonType", + "any_one_of": [ + "CEO", + "CTO" + ], + } + ] + }, + { + "local": [ + { + 'user': { + 'name': '{0}' + } + }, + { + "group": { + "name": self.group_admins['name'], + "domain": { + "id": self.domainA['id'] + } + } + } + ], + "remote": [ + { + "type": "UserName", + }, + { + "type": "orgPersonType", + "any_one_of": [ + "Managers" + ] + } + ] + }, + { + "local": [ + { + "user": { + "name": "{0}" + } + }, + { + "group": { + "name": "NON_EXISTING", + "domain": { + "id": self.domainA['id'] + } + } + } + ], + "remote": [ + { + "type": "UserName", + }, + { + "type": "UserName", + "any_one_of": [ + "IamTester" + ] + } + ] + }, + { + "local": [ + { + "user": { + "type": "local", + "name": self.user['name'], + "domain": { + "id": self.user['domain_id'] + } + } + }, + { + "group": { + "id": self.group_customers['id'] + } + } + ], + "remote": [ + { + "type": "UserType", + "any_one_of": [ + "random" + ] + } + ] + }, + { + "local": [ + { + "user": { + "type": "local", + "name": self.user['name'], + "domain": { + "id": uuid.uuid4().hex + } + } + } + ], + "remote": [ + { + "type": "Position", + "any_one_of": [ + "DirectorGeneral" + ] + } + ] + } + ] + } + + # Add IDP + self.idp = self.idp_ref(id=self.IDP) + self.federation_api.create_idp(self.idp['id'], + self.idp) + # Add IDP with remote + self.idp_with_remote = self.idp_ref(id=self.IDP_WITH_REMOTE) + self.idp_with_remote['remote_id'] = self.REMOTE_ID + self.federation_api.create_idp(self.idp_with_remote['id'], + self.idp_with_remote) + # Add a mapping + self.mapping = self.mapping_ref() + self.federation_api.create_mapping(self.mapping['id'], + self.mapping) + # Add protocols + self.proto_saml = self.proto_ref(mapping_id=self.mapping['id']) + self.proto_saml['id'] = self.PROTOCOL + self.federation_api.create_protocol(self.idp['id'], + self.proto_saml['id'], + self.proto_saml) + # Add protocols IDP with remote + self.federation_api.create_protocol(self.idp_with_remote['id'], + self.proto_saml['id'], + self.proto_saml) + # Generate fake tokens + context = {'environment': {}} + + self.tokens = {} + VARIANTS = ('EMPLOYEE_ASSERTION', 'CUSTOMER_ASSERTION', + 'ADMIN_ASSERTION') + api = auth_controllers.Auth() + for variant in VARIANTS: + self._inject_assertion(context, variant) + r = api.authenticate_for_token(context, self.UNSCOPED_V3_SAML2_REQ) + self.tokens[variant] = r.headers.get('X-Subject-Token') + + self.TOKEN_SCOPE_PROJECT_FROM_NONEXISTENT_TOKEN = self._scope_request( + uuid.uuid4().hex, 'project', self.proj_customers['id']) + + self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_EMPLOYEE = self._scope_request( + self.tokens['EMPLOYEE_ASSERTION'], 'project', + self.proj_employees['id']) + + self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_ADMIN = self._scope_request( + self.tokens['ADMIN_ASSERTION'], 'project', + self.proj_employees['id']) + + self.TOKEN_SCOPE_PROJECT_CUSTOMER_FROM_ADMIN = self._scope_request( + self.tokens['ADMIN_ASSERTION'], 'project', + self.proj_customers['id']) + + self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_CUSTOMER = self._scope_request( + self.tokens['CUSTOMER_ASSERTION'], 'project', + self.proj_employees['id']) + + self.TOKEN_SCOPE_PROJECT_INHERITED_FROM_CUSTOMER = self._scope_request( + self.tokens['CUSTOMER_ASSERTION'], 'project', + self.project_inherited['id']) + + self.TOKEN_SCOPE_DOMAIN_A_FROM_CUSTOMER = self._scope_request( + self.tokens['CUSTOMER_ASSERTION'], 'domain', self.domainA['id']) + + self.TOKEN_SCOPE_DOMAIN_B_FROM_CUSTOMER = self._scope_request( + self.tokens['CUSTOMER_ASSERTION'], 'domain', + self.domainB['id']) + + self.TOKEN_SCOPE_DOMAIN_D_FROM_CUSTOMER = self._scope_request( + self.tokens['CUSTOMER_ASSERTION'], 'domain', self.domainD['id']) + + self.TOKEN_SCOPE_DOMAIN_A_FROM_ADMIN = self._scope_request( + self.tokens['ADMIN_ASSERTION'], 'domain', self.domainA['id']) + + self.TOKEN_SCOPE_DOMAIN_B_FROM_ADMIN = self._scope_request( + self.tokens['ADMIN_ASSERTION'], 'domain', self.domainB['id']) + + self.TOKEN_SCOPE_DOMAIN_C_FROM_ADMIN = self._scope_request( + self.tokens['ADMIN_ASSERTION'], 'domain', + self.domainC['id']) + + +class FederatedIdentityProviderTests(FederationTests): + """A test class for Identity Providers.""" + + idp_keys = ['description', 'enabled'] + + default_body = {'description': None, 'enabled': True} + + def base_url(self, suffix=None): + if suffix is not None: + return '/OS-FEDERATION/identity_providers/' + str(suffix) + return '/OS-FEDERATION/identity_providers' + + def _fetch_attribute_from_response(self, resp, parameter, + assert_is_not_none=True): + """Fetch single attribute from TestResponse object.""" + result = resp.result.get(parameter) + if assert_is_not_none: + self.assertIsNotNone(result) + return result + + def _create_and_decapsulate_response(self, body=None): + """Create IdP and fetch it's random id along with entity.""" + default_resp = self._create_default_idp(body=body) + idp = self._fetch_attribute_from_response(default_resp, + 'identity_provider') + self.assertIsNotNone(idp) + idp_id = idp.get('id') + return (idp_id, idp) + + def _get_idp(self, idp_id): + """Fetch IdP entity based on its id.""" + url = self.base_url(suffix=idp_id) + resp = self.get(url) + return resp + + def _create_default_idp(self, body=None): + """Create default IdP.""" + url = self.base_url(suffix=uuid.uuid4().hex) + if body is None: + body = self._http_idp_input() + resp = self.put(url, body={'identity_provider': body}, + expected_status=201) + return resp + + def _http_idp_input(self, **kwargs): + """Create default input for IdP data.""" + body = None + if 'body' not in kwargs: + body = self.default_body.copy() + body['description'] = uuid.uuid4().hex + else: + body = kwargs['body'] + return body + + def _assign_protocol_to_idp(self, idp_id=None, proto=None, url=None, + mapping_id=None, validate=True, **kwargs): + if url is None: + url = self.base_url(suffix='%(idp_id)s/protocols/%(protocol_id)s') + if idp_id is None: + idp_id, _ = self._create_and_decapsulate_response() + if proto is None: + proto = uuid.uuid4().hex + if mapping_id is None: + mapping_id = uuid.uuid4().hex + body = {'mapping_id': mapping_id} + url = url % {'idp_id': idp_id, 'protocol_id': proto} + resp = self.put(url, body={'protocol': body}, **kwargs) + if validate: + self.assertValidResponse(resp, 'protocol', dummy_validator, + keys_to_check=['id', 'mapping_id'], + ref={'id': proto, + 'mapping_id': mapping_id}) + return (resp, idp_id, proto) + + def _get_protocol(self, idp_id, protocol_id): + url = "%s/protocols/%s" % (idp_id, protocol_id) + url = self.base_url(suffix=url) + r = self.get(url) + return r + + def test_create_idp(self): + """Creates the IdentityProvider entity.""" + + keys_to_check = self.idp_keys + body = self._http_idp_input() + resp = self._create_default_idp(body=body) + self.assertValidResponse(resp, 'identity_provider', dummy_validator, + keys_to_check=keys_to_check, + ref=body) + + def test_create_idp_remote(self): + """Creates the IdentityProvider entity associated to a remote_id.""" + + keys_to_check = list(self.idp_keys) + keys_to_check.append('remote_id') + body = self.default_body.copy() + body['description'] = uuid.uuid4().hex + body['remote_id'] = uuid.uuid4().hex + resp = self._create_default_idp(body=body) + self.assertValidResponse(resp, 'identity_provider', dummy_validator, + keys_to_check=keys_to_check, + ref=body) + + def test_list_idps(self, iterations=5): + """Lists all available IdentityProviders. + + This test collects ids of created IdPs and + intersects it with the list of all available IdPs. + List of all IdPs can be a superset of IdPs created in this test, + because other tests also create IdPs. + + """ + def get_id(resp): + r = self._fetch_attribute_from_response(resp, + 'identity_provider') + return r.get('id') + + ids = [] + for _ in range(iterations): + id = get_id(self._create_default_idp()) + ids.append(id) + ids = set(ids) + + keys_to_check = self.idp_keys + url = self.base_url() + resp = self.get(url) + self.assertValidListResponse(resp, 'identity_providers', + dummy_validator, + keys_to_check=keys_to_check) + entities = self._fetch_attribute_from_response(resp, + 'identity_providers') + entities_ids = set([e['id'] for e in entities]) + ids_intersection = entities_ids.intersection(ids) + self.assertEqual(ids_intersection, ids) + + def test_check_idp_uniqueness(self): + """Add same IdP twice. + + Expect HTTP 409 code for the latter call. + + """ + url = self.base_url(suffix=uuid.uuid4().hex) + body = self._http_idp_input() + self.put(url, body={'identity_provider': body}, + expected_status=201) + self.put(url, body={'identity_provider': body}, + expected_status=409) + + def test_get_idp(self): + """Create and later fetch IdP.""" + body = self._http_idp_input() + default_resp = self._create_default_idp(body=body) + default_idp = self._fetch_attribute_from_response(default_resp, + 'identity_provider') + idp_id = default_idp.get('id') + url = self.base_url(suffix=idp_id) + resp = self.get(url) + self.assertValidResponse(resp, 'identity_provider', + dummy_validator, keys_to_check=body.keys(), + ref=body) + + def test_get_nonexisting_idp(self): + """Fetch nonexisting IdP entity. + + Expected HTTP 404 status code. + + """ + idp_id = uuid.uuid4().hex + self.assertIsNotNone(idp_id) + + url = self.base_url(suffix=idp_id) + self.get(url, expected_status=404) + + def test_delete_existing_idp(self): + """Create and later delete IdP. + + Expect HTTP 404 for the GET IdP call. + """ + default_resp = self._create_default_idp() + default_idp = self._fetch_attribute_from_response(default_resp, + 'identity_provider') + idp_id = default_idp.get('id') + self.assertIsNotNone(idp_id) + url = self.base_url(suffix=idp_id) + self.delete(url) + self.get(url, expected_status=404) + + def test_delete_nonexisting_idp(self): + """Delete nonexisting IdP. + + Expect HTTP 404 for the GET IdP call. + """ + idp_id = uuid.uuid4().hex + url = self.base_url(suffix=idp_id) + self.delete(url, expected_status=404) + + def test_update_idp_mutable_attributes(self): + """Update IdP's mutable parameters.""" + default_resp = self._create_default_idp() + default_idp = self._fetch_attribute_from_response(default_resp, + 'identity_provider') + idp_id = default_idp.get('id') + url = self.base_url(suffix=idp_id) + self.assertIsNotNone(idp_id) + + _enabled = not default_idp.get('enabled') + body = {'remote_id': uuid.uuid4().hex, + 'description': uuid.uuid4().hex, + 'enabled': _enabled} + + body = {'identity_provider': body} + resp = self.patch(url, body=body) + updated_idp = self._fetch_attribute_from_response(resp, + 'identity_provider') + body = body['identity_provider'] + for key in body.keys(): + self.assertEqual(body[key], updated_idp.get(key)) + + resp = self.get(url) + updated_idp = self._fetch_attribute_from_response(resp, + 'identity_provider') + for key in body.keys(): + self.assertEqual(body[key], updated_idp.get(key)) + + def test_update_idp_immutable_attributes(self): + """Update IdP's immutable parameters. + + Expect HTTP 403 code. + + """ + default_resp = self._create_default_idp() + default_idp = self._fetch_attribute_from_response(default_resp, + 'identity_provider') + idp_id = default_idp.get('id') + self.assertIsNotNone(idp_id) + + body = self._http_idp_input() + body['id'] = uuid.uuid4().hex + body['protocols'] = [uuid.uuid4().hex, uuid.uuid4().hex] + + url = self.base_url(suffix=idp_id) + self.patch(url, body={'identity_provider': body}, expected_status=403) + + def test_update_nonexistent_idp(self): + """Update nonexistent IdP + + Expect HTTP 404 code. + + """ + idp_id = uuid.uuid4().hex + url = self.base_url(suffix=idp_id) + body = self._http_idp_input() + body['enabled'] = False + body = {'identity_provider': body} + + self.patch(url, body=body, expected_status=404) + + def test_assign_protocol_to_idp(self): + """Assign a protocol to existing IdP.""" + + self._assign_protocol_to_idp(expected_status=201) + + def test_protocol_composite_pk(self): + """Test whether Keystone let's add two entities with identical + names, however attached to different IdPs. + + 1. Add IdP and assign it protocol with predefined name + 2. Add another IdP and assign it a protocol with same name. + + Expect HTTP 201 code + + """ + url = self.base_url(suffix='%(idp_id)s/protocols/%(protocol_id)s') + + kwargs = {'expected_status': 201} + self._assign_protocol_to_idp(proto='saml2', + url=url, **kwargs) + + self._assign_protocol_to_idp(proto='saml2', + url=url, **kwargs) + + def test_protocol_idp_pk_uniqueness(self): + """Test whether Keystone checks for unique idp/protocol values. + + Add same protocol twice, expect Keystone to reject a latter call and + return HTTP 409 code. + + """ + url = self.base_url(suffix='%(idp_id)s/protocols/%(protocol_id)s') + + kwargs = {'expected_status': 201} + resp, idp_id, proto = self._assign_protocol_to_idp(proto='saml2', + url=url, **kwargs) + kwargs = {'expected_status': 409} + resp, idp_id, proto = self._assign_protocol_to_idp(idp_id=idp_id, + proto='saml2', + validate=False, + url=url, **kwargs) + + def test_assign_protocol_to_nonexistent_idp(self): + """Assign protocol to IdP that doesn't exist. + + Expect HTTP 404 code. + + """ + + idp_id = uuid.uuid4().hex + kwargs = {'expected_status': 404} + self._assign_protocol_to_idp(proto='saml2', + idp_id=idp_id, + validate=False, + **kwargs) + + def test_get_protocol(self): + """Create and later fetch protocol tied to IdP.""" + + resp, idp_id, proto = self._assign_protocol_to_idp(expected_status=201) + proto_id = self._fetch_attribute_from_response(resp, 'protocol')['id'] + url = "%s/protocols/%s" % (idp_id, proto_id) + url = self.base_url(suffix=url) + + resp = self.get(url) + + reference = {'id': proto_id} + self.assertValidResponse(resp, 'protocol', + dummy_validator, + keys_to_check=reference.keys(), + ref=reference) + + def test_list_protocols(self): + """Create set of protocols and later list them. + + Compare input and output id sets. + + """ + resp, idp_id, proto = self._assign_protocol_to_idp(expected_status=201) + iterations = random.randint(0, 16) + protocol_ids = [] + for _ in range(iterations): + resp, _, proto = self._assign_protocol_to_idp(idp_id=idp_id, + expected_status=201) + proto_id = self._fetch_attribute_from_response(resp, 'protocol') + proto_id = proto_id['id'] + protocol_ids.append(proto_id) + + url = "%s/protocols" % idp_id + url = self.base_url(suffix=url) + resp = self.get(url) + self.assertValidListResponse(resp, 'protocols', + dummy_validator, + keys_to_check=['id']) + entities = self._fetch_attribute_from_response(resp, 'protocols') + entities = set([entity['id'] for entity in entities]) + protocols_intersection = entities.intersection(protocol_ids) + self.assertEqual(protocols_intersection, set(protocol_ids)) + + def test_update_protocols_attribute(self): + """Update protocol's attribute.""" + + resp, idp_id, proto = self._assign_protocol_to_idp(expected_status=201) + new_mapping_id = uuid.uuid4().hex + + url = "%s/protocols/%s" % (idp_id, proto) + url = self.base_url(suffix=url) + body = {'mapping_id': new_mapping_id} + resp = self.patch(url, body={'protocol': body}) + self.assertValidResponse(resp, 'protocol', dummy_validator, + keys_to_check=['id', 'mapping_id'], + ref={'id': proto, + 'mapping_id': new_mapping_id} + ) + + def test_delete_protocol(self): + """Delete protocol. + + Expect HTTP 404 code for the GET call after the protocol is deleted. + + """ + url = self.base_url(suffix='/%(idp_id)s/' + 'protocols/%(protocol_id)s') + resp, idp_id, proto = self._assign_protocol_to_idp(expected_status=201) + url = url % {'idp_id': idp_id, + 'protocol_id': proto} + self.delete(url) + self.get(url, expected_status=404) + + +class MappingCRUDTests(FederationTests): + """A class for testing CRUD operations for Mappings.""" + + MAPPING_URL = '/OS-FEDERATION/mappings/' + + def assertValidMappingListResponse(self, resp, *args, **kwargs): + return self.assertValidListResponse( + resp, + 'mappings', + self.assertValidMapping, + keys_to_check=[], + *args, + **kwargs) + + def assertValidMappingResponse(self, resp, *args, **kwargs): + return self.assertValidResponse( + resp, + 'mapping', + self.assertValidMapping, + keys_to_check=[], + *args, + **kwargs) + + def assertValidMapping(self, entity, ref=None): + self.assertIsNotNone(entity.get('id')) + self.assertIsNotNone(entity.get('rules')) + if ref: + self.assertEqual(jsonutils.loads(entity['rules']), ref['rules']) + return entity + + def _create_default_mapping_entry(self): + url = self.MAPPING_URL + uuid.uuid4().hex + resp = self.put(url, + body={'mapping': mapping_fixtures.MAPPING_LARGE}, + expected_status=201) + return resp + + def _get_id_from_response(self, resp): + r = resp.result.get('mapping') + return r.get('id') + + def test_mapping_create(self): + resp = self._create_default_mapping_entry() + self.assertValidMappingResponse(resp, mapping_fixtures.MAPPING_LARGE) + + def test_mapping_list(self): + url = self.MAPPING_URL + self._create_default_mapping_entry() + resp = self.get(url) + entities = resp.result.get('mappings') + self.assertIsNotNone(entities) + self.assertResponseStatus(resp, 200) + self.assertValidListLinks(resp.result.get('links')) + self.assertEqual(1, len(entities)) + + def test_mapping_delete(self): + url = self.MAPPING_URL + '%(mapping_id)s' + resp = self._create_default_mapping_entry() + mapping_id = self._get_id_from_response(resp) + url = url % {'mapping_id': str(mapping_id)} + resp = self.delete(url) + self.assertResponseStatus(resp, 204) + self.get(url, expected_status=404) + + def test_mapping_get(self): + url = self.MAPPING_URL + '%(mapping_id)s' + resp = self._create_default_mapping_entry() + mapping_id = self._get_id_from_response(resp) + url = url % {'mapping_id': mapping_id} + resp = self.get(url) + self.assertValidMappingResponse(resp, mapping_fixtures.MAPPING_LARGE) + + def test_mapping_update(self): + url = self.MAPPING_URL + '%(mapping_id)s' + resp = self._create_default_mapping_entry() + mapping_id = self._get_id_from_response(resp) + url = url % {'mapping_id': mapping_id} + resp = self.patch(url, + body={'mapping': mapping_fixtures.MAPPING_SMALL}) + self.assertValidMappingResponse(resp, mapping_fixtures.MAPPING_SMALL) + resp = self.get(url) + self.assertValidMappingResponse(resp, mapping_fixtures.MAPPING_SMALL) + + def test_delete_mapping_dne(self): + url = self.MAPPING_URL + uuid.uuid4().hex + self.delete(url, expected_status=404) + + def test_get_mapping_dne(self): + url = self.MAPPING_URL + uuid.uuid4().hex + self.get(url, expected_status=404) + + def test_create_mapping_bad_requirements(self): + url = self.MAPPING_URL + uuid.uuid4().hex + self.put(url, expected_status=400, + body={'mapping': mapping_fixtures.MAPPING_BAD_REQ}) + + def test_create_mapping_no_rules(self): + url = self.MAPPING_URL + uuid.uuid4().hex + self.put(url, expected_status=400, + body={'mapping': mapping_fixtures.MAPPING_NO_RULES}) + + def test_create_mapping_no_remote_objects(self): + url = self.MAPPING_URL + uuid.uuid4().hex + self.put(url, expected_status=400, + body={'mapping': mapping_fixtures.MAPPING_NO_REMOTE}) + + def test_create_mapping_bad_value(self): + url = self.MAPPING_URL + uuid.uuid4().hex + self.put(url, expected_status=400, + body={'mapping': mapping_fixtures.MAPPING_BAD_VALUE}) + + def test_create_mapping_missing_local(self): + url = self.MAPPING_URL + uuid.uuid4().hex + self.put(url, expected_status=400, + body={'mapping': mapping_fixtures.MAPPING_MISSING_LOCAL}) + + def test_create_mapping_missing_type(self): + url = self.MAPPING_URL + uuid.uuid4().hex + self.put(url, expected_status=400, + body={'mapping': mapping_fixtures.MAPPING_MISSING_TYPE}) + + def test_create_mapping_wrong_type(self): + url = self.MAPPING_URL + uuid.uuid4().hex + self.put(url, expected_status=400, + body={'mapping': mapping_fixtures.MAPPING_WRONG_TYPE}) + + def test_create_mapping_extra_remote_properties_not_any_of(self): + url = self.MAPPING_URL + uuid.uuid4().hex + mapping = mapping_fixtures.MAPPING_EXTRA_REMOTE_PROPS_NOT_ANY_OF + self.put(url, expected_status=400, body={'mapping': mapping}) + + def test_create_mapping_extra_remote_properties_any_one_of(self): + url = self.MAPPING_URL + uuid.uuid4().hex + mapping = mapping_fixtures.MAPPING_EXTRA_REMOTE_PROPS_ANY_ONE_OF + self.put(url, expected_status=400, body={'mapping': mapping}) + + def test_create_mapping_extra_remote_properties_just_type(self): + url = self.MAPPING_URL + uuid.uuid4().hex + mapping = mapping_fixtures.MAPPING_EXTRA_REMOTE_PROPS_JUST_TYPE + self.put(url, expected_status=400, body={'mapping': mapping}) + + def test_create_mapping_empty_map(self): + url = self.MAPPING_URL + uuid.uuid4().hex + self.put(url, expected_status=400, + body={'mapping': {}}) + + def test_create_mapping_extra_rules_properties(self): + url = self.MAPPING_URL + uuid.uuid4().hex + self.put(url, expected_status=400, + body={'mapping': mapping_fixtures.MAPPING_EXTRA_RULES_PROPS}) + + def test_create_mapping_with_blacklist_and_whitelist(self): + """Test for adding whitelist and blacklist in the rule + + Server should respond with HTTP 400 error upon discovering both + ``whitelist`` and ``blacklist`` keywords in the same rule. + + """ + url = self.MAPPING_URL + uuid.uuid4().hex + mapping = mapping_fixtures.MAPPING_GROUPS_WHITELIST_AND_BLACKLIST + self.put(url, expected_status=400, body={'mapping': mapping}) + + +class MappingRuleEngineTests(FederationTests): + """A class for testing the mapping rule engine.""" + + def assertValidMappedUserObject(self, mapped_properties, + user_type='ephemeral', + domain_id=None): + """Check whether mapped properties object has 'user' within. + + According to today's rules, RuleProcessor does not have to issue user's + id or name. What's actually required is user's type and for ephemeral + users that would be service domain named 'Federated'. + """ + self.assertIn('user', mapped_properties, + message='Missing user object in mapped properties') + user = mapped_properties['user'] + self.assertIn('type', user) + self.assertEqual(user_type, user['type']) + self.assertIn('domain', user) + domain = user['domain'] + domain_name_or_id = domain.get('id') or domain.get('name') + domain_ref = domain_id or federation.FEDERATED_DOMAIN_KEYWORD + self.assertEqual(domain_ref, domain_name_or_id) + + def test_rule_engine_any_one_of_and_direct_mapping(self): + """Should return user's name and group id EMPLOYEE_GROUP_ID. + + The ADMIN_ASSERTION should successfully have a match in MAPPING_LARGE. + They will test the case where `any_one_of` is valid, and there is + a direct mapping for the users name. + + """ + + mapping = mapping_fixtures.MAPPING_LARGE + assertion = mapping_fixtures.ADMIN_ASSERTION + rp = mapping_utils.RuleProcessor(mapping['rules']) + values = rp.process(assertion) + + fn = assertion.get('FirstName') + ln = assertion.get('LastName') + full_name = '%s %s' % (fn, ln) + group_ids = values.get('group_ids') + user_name = values.get('user', {}).get('name') + + self.assertIn(mapping_fixtures.EMPLOYEE_GROUP_ID, group_ids) + self.assertEqual(full_name, user_name) + + def test_rule_engine_no_regex_match(self): + """Should deny authorization, the email of the tester won't match. + + This will not match since the email in the assertion will fail + the regex test. It is set to match any @example.com address. + But the incoming value is set to eviltester@example.org. + RuleProcessor should return list of empty group_ids. + + """ + + mapping = mapping_fixtures.MAPPING_LARGE + assertion = mapping_fixtures.BAD_TESTER_ASSERTION + rp = mapping_utils.RuleProcessor(mapping['rules']) + mapped_properties = rp.process(assertion) + + self.assertValidMappedUserObject(mapped_properties) + self.assertIsNone(mapped_properties['user'].get('name')) + self.assertListEqual(list(), mapped_properties['group_ids']) + + def test_rule_engine_regex_many_groups(self): + """Should return group CONTRACTOR_GROUP_ID. + + The TESTER_ASSERTION should successfully have a match in + MAPPING_TESTER_REGEX. This will test the case where many groups + are in the assertion, and a regex value is used to try and find + a match. + + """ + + mapping = mapping_fixtures.MAPPING_TESTER_REGEX + assertion = mapping_fixtures.TESTER_ASSERTION + rp = mapping_utils.RuleProcessor(mapping['rules']) + values = rp.process(assertion) + + self.assertValidMappedUserObject(values) + user_name = assertion.get('UserName') + group_ids = values.get('group_ids') + name = values.get('user', {}).get('name') + + self.assertEqual(user_name, name) + self.assertIn(mapping_fixtures.TESTER_GROUP_ID, group_ids) + + def test_rule_engine_any_one_of_many_rules(self): + """Should return group CONTRACTOR_GROUP_ID. + + The CONTRACTOR_ASSERTION should successfully have a match in + MAPPING_SMALL. This will test the case where many rules + must be matched, including an `any_one_of`, and a direct + mapping. + + """ + + mapping = mapping_fixtures.MAPPING_SMALL + assertion = mapping_fixtures.CONTRACTOR_ASSERTION + rp = mapping_utils.RuleProcessor(mapping['rules']) + values = rp.process(assertion) + + self.assertValidMappedUserObject(values) + user_name = assertion.get('UserName') + group_ids = values.get('group_ids') + name = values.get('user', {}).get('name') + + self.assertEqual(user_name, name) + self.assertIn(mapping_fixtures.CONTRACTOR_GROUP_ID, group_ids) + + def test_rule_engine_not_any_of_and_direct_mapping(self): + """Should return user's name and email. + + The CUSTOMER_ASSERTION should successfully have a match in + MAPPING_LARGE. This will test the case where a requirement + has `not_any_of`, and direct mapping to a username, no group. + + """ + + mapping = mapping_fixtures.MAPPING_LARGE + assertion = mapping_fixtures.CUSTOMER_ASSERTION + rp = mapping_utils.RuleProcessor(mapping['rules']) + values = rp.process(assertion) + + self.assertValidMappedUserObject(values) + user_name = assertion.get('UserName') + group_ids = values.get('group_ids') + name = values.get('user', {}).get('name') + + self.assertEqual(user_name, name) + self.assertEqual([], group_ids,) + + def test_rule_engine_not_any_of_many_rules(self): + """Should return group EMPLOYEE_GROUP_ID. + + The EMPLOYEE_ASSERTION should successfully have a match in + MAPPING_SMALL. This will test the case where many remote + rules must be matched, including a `not_any_of`. + + """ + + mapping = mapping_fixtures.MAPPING_SMALL + assertion = mapping_fixtures.EMPLOYEE_ASSERTION + rp = mapping_utils.RuleProcessor(mapping['rules']) + values = rp.process(assertion) + + self.assertValidMappedUserObject(values) + user_name = assertion.get('UserName') + group_ids = values.get('group_ids') + name = values.get('user', {}).get('name') + + self.assertEqual(user_name, name) + self.assertIn(mapping_fixtures.EMPLOYEE_GROUP_ID, group_ids) + + def test_rule_engine_not_any_of_regex_verify_pass(self): + """Should return group DEVELOPER_GROUP_ID. + + The DEVELOPER_ASSERTION should successfully have a match in + MAPPING_DEVELOPER_REGEX. This will test the case where many + remote rules must be matched, including a `not_any_of`, with + regex set to True. + + """ + + mapping = mapping_fixtures.MAPPING_DEVELOPER_REGEX + assertion = mapping_fixtures.DEVELOPER_ASSERTION + rp = mapping_utils.RuleProcessor(mapping['rules']) + values = rp.process(assertion) + + self.assertValidMappedUserObject(values) + user_name = assertion.get('UserName') + group_ids = values.get('group_ids') + name = values.get('user', {}).get('name') + + self.assertEqual(user_name, name) + self.assertIn(mapping_fixtures.DEVELOPER_GROUP_ID, group_ids) + + def test_rule_engine_not_any_of_regex_verify_fail(self): + """Should deny authorization. + + The email in the assertion will fail the regex test. + It is set to reject any @example.org address, but the + incoming value is set to evildeveloper@example.org. + RuleProcessor should return list of empty group_ids. + + """ + + mapping = mapping_fixtures.MAPPING_DEVELOPER_REGEX + assertion = mapping_fixtures.BAD_DEVELOPER_ASSERTION + rp = mapping_utils.RuleProcessor(mapping['rules']) + mapped_properties = rp.process(assertion) + + self.assertValidMappedUserObject(mapped_properties) + self.assertIsNone(mapped_properties['user'].get('name')) + self.assertListEqual(list(), mapped_properties['group_ids']) + + def _rule_engine_regex_match_and_many_groups(self, assertion): + """Should return group DEVELOPER_GROUP_ID and TESTER_GROUP_ID. + + A helper function injecting assertion passed as an argument. + Expect DEVELOPER_GROUP_ID and TESTER_GROUP_ID in the results. + + """ + + mapping = mapping_fixtures.MAPPING_LARGE + rp = mapping_utils.RuleProcessor(mapping['rules']) + values = rp.process(assertion) + + user_name = assertion.get('UserName') + group_ids = values.get('group_ids') + name = values.get('user', {}).get('name') + + self.assertValidMappedUserObject(values) + self.assertEqual(user_name, name) + self.assertIn(mapping_fixtures.DEVELOPER_GROUP_ID, group_ids) + self.assertIn(mapping_fixtures.TESTER_GROUP_ID, group_ids) + + def test_rule_engine_regex_match_and_many_groups(self): + """Should return group DEVELOPER_GROUP_ID and TESTER_GROUP_ID. + + The TESTER_ASSERTION should successfully have a match in + MAPPING_LARGE. This will test a successful regex match + for an `any_one_of` evaluation type, and will have many + groups returned. + + """ + self._rule_engine_regex_match_and_many_groups( + mapping_fixtures.TESTER_ASSERTION) + + def test_rule_engine_discards_nonstring_objects(self): + """Check whether RuleProcessor discards non string objects. + + Despite the fact that assertion is malformed and contains + non string objects, RuleProcessor should correctly discard them and + successfully have a match in MAPPING_LARGE. + + """ + self._rule_engine_regex_match_and_many_groups( + mapping_fixtures.MALFORMED_TESTER_ASSERTION) + + def test_rule_engine_fails_after_discarding_nonstring(self): + """Check whether RuleProcessor discards non string objects. + + Expect RuleProcessor to discard non string object, which + is required for a correct rule match. RuleProcessor will result with + empty list of groups. + + """ + mapping = mapping_fixtures.MAPPING_SMALL + rp = mapping_utils.RuleProcessor(mapping['rules']) + assertion = mapping_fixtures.CONTRACTOR_MALFORMED_ASSERTION + mapped_properties = rp.process(assertion) + self.assertValidMappedUserObject(mapped_properties) + self.assertIsNone(mapped_properties['user'].get('name')) + self.assertListEqual(list(), mapped_properties['group_ids']) + + def test_rule_engine_returns_group_names(self): + """Check whether RuleProcessor returns group names with their domains. + + RuleProcessor should return 'group_names' entry with a list of + dictionaries with two entries 'name' and 'domain' identifying group by + its name and domain. + + """ + mapping = mapping_fixtures.MAPPING_GROUP_NAMES + rp = mapping_utils.RuleProcessor(mapping['rules']) + assertion = mapping_fixtures.EMPLOYEE_ASSERTION + mapped_properties = rp.process(assertion) + self.assertIsNotNone(mapped_properties) + self.assertValidMappedUserObject(mapped_properties) + reference = { + mapping_fixtures.DEVELOPER_GROUP_NAME: + { + "name": mapping_fixtures.DEVELOPER_GROUP_NAME, + "domain": { + "name": mapping_fixtures.DEVELOPER_GROUP_DOMAIN_NAME + } + }, + mapping_fixtures.TESTER_GROUP_NAME: + { + "name": mapping_fixtures.TESTER_GROUP_NAME, + "domain": { + "id": mapping_fixtures.DEVELOPER_GROUP_DOMAIN_ID + } + } + } + for rule in mapped_properties['group_names']: + self.assertDictEqual(reference.get(rule.get('name')), rule) + + def test_rule_engine_whitelist_and_direct_groups_mapping(self): + """Should return user's groups Developer and Contractor. + + The EMPLOYEE_ASSERTION_MULTIPLE_GROUPS should successfully have a match + in MAPPING_GROUPS_WHITELIST. It will test the case where 'whitelist' + correctly filters out Manager and only allows Developer and Contractor. + + """ + + mapping = mapping_fixtures.MAPPING_GROUPS_WHITELIST + assertion = mapping_fixtures.EMPLOYEE_ASSERTION_MULTIPLE_GROUPS + rp = mapping_utils.RuleProcessor(mapping['rules']) + mapped_properties = rp.process(assertion) + self.assertIsNotNone(mapped_properties) + + reference = { + mapping_fixtures.DEVELOPER_GROUP_NAME: + { + "name": mapping_fixtures.DEVELOPER_GROUP_NAME, + "domain": { + "id": mapping_fixtures.DEVELOPER_GROUP_DOMAIN_ID + } + }, + mapping_fixtures.CONTRACTOR_GROUP_NAME: + { + "name": mapping_fixtures.CONTRACTOR_GROUP_NAME, + "domain": { + "id": mapping_fixtures.DEVELOPER_GROUP_DOMAIN_ID + } + } + } + for rule in mapped_properties['group_names']: + self.assertDictEqual(reference.get(rule.get('name')), rule) + + self.assertEqual('tbo', mapped_properties['user']['name']) + self.assertEqual([], mapped_properties['group_ids']) + + def test_rule_engine_blacklist_and_direct_groups_mapping(self): + """Should return user's group Developer. + + The EMPLOYEE_ASSERTION_MULTIPLE_GROUPS should successfully have a match + in MAPPING_GROUPS_BLACKLIST. It will test the case where 'blacklist' + correctly filters out Manager and Developer and only allows Contractor. + + """ + + mapping = mapping_fixtures.MAPPING_GROUPS_BLACKLIST + assertion = mapping_fixtures.EMPLOYEE_ASSERTION_MULTIPLE_GROUPS + rp = mapping_utils.RuleProcessor(mapping['rules']) + mapped_properties = rp.process(assertion) + self.assertIsNotNone(mapped_properties) + + reference = { + mapping_fixtures.CONTRACTOR_GROUP_NAME: + { + "name": mapping_fixtures.CONTRACTOR_GROUP_NAME, + "domain": { + "id": mapping_fixtures.DEVELOPER_GROUP_DOMAIN_ID + } + } + } + for rule in mapped_properties['group_names']: + self.assertDictEqual(reference.get(rule.get('name')), rule) + self.assertEqual('tbo', mapped_properties['user']['name']) + self.assertEqual([], mapped_properties['group_ids']) + + def test_rule_engine_blacklist_and_direct_groups_mapping_multiples(self): + """Tests matching multiple values before the blacklist. + + Verifies that the local indexes are correct when matching multiple + remote values for a field when the field occurs before the blacklist + entry in the remote rules. + + """ + + mapping = mapping_fixtures.MAPPING_GROUPS_BLACKLIST_MULTIPLES + assertion = mapping_fixtures.EMPLOYEE_ASSERTION_MULTIPLE_GROUPS + rp = mapping_utils.RuleProcessor(mapping['rules']) + mapped_properties = rp.process(assertion) + self.assertIsNotNone(mapped_properties) + + reference = { + mapping_fixtures.CONTRACTOR_GROUP_NAME: + { + "name": mapping_fixtures.CONTRACTOR_GROUP_NAME, + "domain": { + "id": mapping_fixtures.DEVELOPER_GROUP_DOMAIN_ID + } + } + } + for rule in mapped_properties['group_names']: + self.assertDictEqual(reference.get(rule.get('name')), rule) + self.assertEqual('tbo', mapped_properties['user']['name']) + self.assertEqual([], mapped_properties['group_ids']) + + def test_rule_engine_whitelist_direct_group_mapping_missing_domain(self): + """Test if the local rule is rejected upon missing domain value + + This is a variation with a ``whitelist`` filter. + + """ + mapping = mapping_fixtures.MAPPING_GROUPS_WHITELIST_MISSING_DOMAIN + assertion = mapping_fixtures.EMPLOYEE_ASSERTION_MULTIPLE_GROUPS + rp = mapping_utils.RuleProcessor(mapping['rules']) + self.assertRaises(exception.ValidationError, rp.process, assertion) + + def test_rule_engine_blacklist_direct_group_mapping_missing_domain(self): + """Test if the local rule is rejected upon missing domain value + + This is a variation with a ``blacklist`` filter. + + """ + mapping = mapping_fixtures.MAPPING_GROUPS_BLACKLIST_MISSING_DOMAIN + assertion = mapping_fixtures.EMPLOYEE_ASSERTION_MULTIPLE_GROUPS + rp = mapping_utils.RuleProcessor(mapping['rules']) + self.assertRaises(exception.ValidationError, rp.process, assertion) + + def test_rule_engine_no_groups_allowed(self): + """Should return user mapped to no groups. + + The EMPLOYEE_ASSERTION should successfully have a match + in MAPPING_GROUPS_WHITELIST, but 'whitelist' should filter out + the group values from the assertion and thus map to no groups. + + """ + mapping = mapping_fixtures.MAPPING_GROUPS_WHITELIST + assertion = mapping_fixtures.EMPLOYEE_ASSERTION + rp = mapping_utils.RuleProcessor(mapping['rules']) + mapped_properties = rp.process(assertion) + self.assertIsNotNone(mapped_properties) + self.assertListEqual(mapped_properties['group_names'], []) + self.assertListEqual(mapped_properties['group_ids'], []) + self.assertEqual('tbo', mapped_properties['user']['name']) + + def test_mapping_federated_domain_specified(self): + """Test mapping engine when domain 'ephemeral' is explicitely set. + + For that, we use mapping rule MAPPING_EPHEMERAL_USER and assertion + EMPLOYEE_ASSERTION + + """ + mapping = mapping_fixtures.MAPPING_EPHEMERAL_USER + rp = mapping_utils.RuleProcessor(mapping['rules']) + assertion = mapping_fixtures.EMPLOYEE_ASSERTION + mapped_properties = rp.process(assertion) + self.assertIsNotNone(mapped_properties) + self.assertValidMappedUserObject(mapped_properties) + + def test_create_user_object_with_bad_mapping(self): + """Test if user object is created even with bad mapping. + + User objects will be created by mapping engine always as long as there + is corresponding local rule. This test shows, that even with assertion + where no group names nor ids are matched, but there is 'blind' rule for + mapping user, such object will be created. + + In this test MAPPING_EHPEMERAL_USER expects UserName set to jsmith + whereas value from assertion is 'tbo'. + + """ + mapping = mapping_fixtures.MAPPING_EPHEMERAL_USER + rp = mapping_utils.RuleProcessor(mapping['rules']) + assertion = mapping_fixtures.CONTRACTOR_ASSERTION + mapped_properties = rp.process(assertion) + self.assertIsNotNone(mapped_properties) + self.assertValidMappedUserObject(mapped_properties) + + self.assertNotIn('id', mapped_properties['user']) + self.assertNotIn('name', mapped_properties['user']) + + def test_set_ephemeral_domain_to_ephemeral_users(self): + """Test auto assigning service domain to ephemeral users. + + Test that ephemeral users will always become members of federated + service domain. The check depends on ``type`` value which must be set + to ``ephemeral`` in case of ephemeral user. + + """ + mapping = mapping_fixtures.MAPPING_EPHEMERAL_USER_LOCAL_DOMAIN + rp = mapping_utils.RuleProcessor(mapping['rules']) + assertion = mapping_fixtures.CONTRACTOR_ASSERTION + mapped_properties = rp.process(assertion) + self.assertIsNotNone(mapped_properties) + self.assertValidMappedUserObject(mapped_properties) + + def test_local_user_local_domain(self): + """Test that local users can have non-service domains assigned.""" + mapping = mapping_fixtures.MAPPING_LOCAL_USER_LOCAL_DOMAIN + rp = mapping_utils.RuleProcessor(mapping['rules']) + assertion = mapping_fixtures.CONTRACTOR_ASSERTION + mapped_properties = rp.process(assertion) + self.assertIsNotNone(mapped_properties) + self.assertValidMappedUserObject( + mapped_properties, user_type='local', + domain_id=mapping_fixtures.LOCAL_DOMAIN) + + def test_user_identifications_name(self): + """Test varius mapping options and how users are identified. + + This test calls mapped.setup_username() for propagating user object. + + Test plan: + - Check if the user has proper domain ('federated') set + - Check if the user has property type set ('ephemeral') + - Check if user's name is properly mapped from the assertion + - Check if user's id is properly set and equal to name, as it was not + explicitely specified in the mapping. + + """ + mapping = mapping_fixtures.MAPPING_USER_IDS + rp = mapping_utils.RuleProcessor(mapping['rules']) + assertion = mapping_fixtures.CONTRACTOR_ASSERTION + mapped_properties = rp.process(assertion) + self.assertIsNotNone(mapped_properties) + self.assertValidMappedUserObject(mapped_properties) + mapped.setup_username({}, mapped_properties) + self.assertEqual('jsmith', mapped_properties['user']['id']) + self.assertEqual('jsmith', mapped_properties['user']['name']) + + def test_user_identifications_name_and_federated_domain(self): + """Test varius mapping options and how users are identified. + + This test calls mapped.setup_username() for propagating user object. + + Test plan: + - Check if the user has proper domain ('federated') set + - Check if the user has propert type set ('ephemeral') + - Check if user's name is properly mapped from the assertion + - Check if user's id is properly set and equal to name, as it was not + explicitely specified in the mapping. + + """ + mapping = mapping_fixtures.MAPPING_USER_IDS + rp = mapping_utils.RuleProcessor(mapping['rules']) + assertion = mapping_fixtures.EMPLOYEE_ASSERTION + mapped_properties = rp.process(assertion) + self.assertIsNotNone(mapped_properties) + self.assertValidMappedUserObject(mapped_properties) + mapped.setup_username({}, mapped_properties) + self.assertEqual('tbo', mapped_properties['user']['name']) + self.assertEqual('tbo', mapped_properties['user']['id']) + + def test_user_identification_id(self): + """Test varius mapping options and how users are identified. + + This test calls mapped.setup_username() for propagating user object. + + Test plan: + - Check if the user has proper domain ('federated') set + - Check if the user has propert type set ('ephemeral') + - Check if user's id is properly mapped from the assertion + - Check if user's name is properly set and equal to id, as it was not + explicitely specified in the mapping. + + """ + mapping = mapping_fixtures.MAPPING_USER_IDS + rp = mapping_utils.RuleProcessor(mapping['rules']) + assertion = mapping_fixtures.ADMIN_ASSERTION + mapped_properties = rp.process(assertion) + context = {'environment': {}} + self.assertIsNotNone(mapped_properties) + self.assertValidMappedUserObject(mapped_properties) + mapped.setup_username(context, mapped_properties) + self.assertEqual('bob', mapped_properties['user']['name']) + self.assertEqual('bob', mapped_properties['user']['id']) + + def test_user_identification_id_and_name(self): + """Test varius mapping options and how users are identified. + + This test calls mapped.setup_username() for propagating user object. + + Test plan: + - Check if the user has proper domain ('federated') set + - Check if the user has proper type set ('ephemeral') + - Check if user's name is properly mapped from the assertion + - Check if user's id is properly set and and equal to value hardcoded + in the mapping + + """ + mapping = mapping_fixtures.MAPPING_USER_IDS + rp = mapping_utils.RuleProcessor(mapping['rules']) + assertion = mapping_fixtures.CUSTOMER_ASSERTION + mapped_properties = rp.process(assertion) + context = {'environment': {}} + self.assertIsNotNone(mapped_properties) + self.assertValidMappedUserObject(mapped_properties) + mapped.setup_username(context, mapped_properties) + self.assertEqual('bwilliams', mapped_properties['user']['name']) + self.assertEqual('abc123', mapped_properties['user']['id']) + + +class FederatedTokenTests(FederationTests, FederatedSetupMixin): + + def auth_plugin_config_override(self): + methods = ['saml2'] + method_classes = {'saml2': 'keystone.auth.plugins.saml2.Saml2'} + super(FederatedTokenTests, self).auth_plugin_config_override( + methods, **method_classes) + + def setUp(self): + super(FederatedTokenTests, self).setUp() + self._notifications = [] + + def fake_saml_notify(action, context, user_id, group_ids, + identity_provider, protocol, token_id, outcome): + note = { + 'action': action, + 'user_id': user_id, + 'identity_provider': identity_provider, + 'protocol': protocol, + 'send_notification_called': True} + self._notifications.append(note) + + self.useFixture(mockpatch.PatchObject( + notifications, + 'send_saml_audit_notification', + fake_saml_notify)) + + def _assert_last_notify(self, action, identity_provider, protocol, + user_id=None): + self.assertTrue(self._notifications) + note = self._notifications[-1] + if user_id: + self.assertEqual(note['user_id'], user_id) + self.assertEqual(note['action'], action) + self.assertEqual(note['identity_provider'], identity_provider) + self.assertEqual(note['protocol'], protocol) + self.assertTrue(note['send_notification_called']) + + def load_fixtures(self, fixtures): + super(FederationTests, self).load_fixtures(fixtures) + self.load_federation_sample_data() + + def test_issue_unscoped_token_notify(self): + self._issue_unscoped_token() + self._assert_last_notify(self.ACTION, self.IDP, self.PROTOCOL) + + def test_issue_unscoped_token(self): + r = self._issue_unscoped_token() + self.assertIsNotNone(r.headers.get('X-Subject-Token')) + + def test_issue_unscoped_token_disabled_idp(self): + """Checks if authentication works with disabled identity providers. + + Test plan: + 1) Disable default IdP + 2) Try issuing unscoped token for that IdP + 3) Expect server to forbid authentication + + """ + enabled_false = {'enabled': False} + self.federation_api.update_idp(self.IDP, enabled_false) + self.assertRaises(exception.Forbidden, + self._issue_unscoped_token) + + def test_issue_unscoped_token_group_names_in_mapping(self): + r = self._issue_unscoped_token(assertion='ANOTHER_CUSTOMER_ASSERTION') + ref_groups = set([self.group_customers['id'], self.group_admins['id']]) + token_resp = r.json_body + token_groups = token_resp['token']['user']['OS-FEDERATION']['groups'] + token_groups = set([group['id'] for group in token_groups]) + self.assertEqual(ref_groups, token_groups) + + def test_issue_unscoped_tokens_nonexisting_group(self): + self.assertRaises(exception.MissingGroups, + self._issue_unscoped_token, + assertion='ANOTHER_TESTER_ASSERTION') + + def test_issue_unscoped_token_with_remote_no_attribute(self): + r = self._issue_unscoped_token(idp=self.IDP_WITH_REMOTE, + environment={ + self.REMOTE_ID_ATTR: self.REMOTE_ID + }) + self.assertIsNotNone(r.headers.get('X-Subject-Token')) + + def test_issue_unscoped_token_with_remote(self): + self.config_fixture.config(group='federation', + remote_id_attribute=self.REMOTE_ID_ATTR) + r = self._issue_unscoped_token(idp=self.IDP_WITH_REMOTE, + environment={ + self.REMOTE_ID_ATTR: self.REMOTE_ID + }) + self.assertIsNotNone(r.headers.get('X-Subject-Token')) + + def test_issue_unscoped_token_with_remote_different(self): + self.config_fixture.config(group='federation', + remote_id_attribute=self.REMOTE_ID_ATTR) + self.assertRaises(exception.Forbidden, + self._issue_unscoped_token, + idp=self.IDP_WITH_REMOTE, + environment={ + self.REMOTE_ID_ATTR: uuid.uuid4().hex + }) + + def test_issue_unscoped_token_with_remote_unavailable(self): + self.config_fixture.config(group='federation', + remote_id_attribute=self.REMOTE_ID_ATTR) + self.assertRaises(exception.ValidationError, + self._issue_unscoped_token, + idp=self.IDP_WITH_REMOTE, + environment={ + uuid.uuid4().hex: uuid.uuid4().hex + }) + + def test_issue_unscoped_token_with_remote_user_as_empty_string(self): + # make sure that REMOTE_USER set as the empty string won't interfere + r = self._issue_unscoped_token(environment={'REMOTE_USER': ''}) + self.assertIsNotNone(r.headers.get('X-Subject-Token')) + + def test_issue_unscoped_token_no_groups(self): + self.assertRaises(exception.Unauthorized, + self._issue_unscoped_token, + assertion='BAD_TESTER_ASSERTION') + + def test_issue_unscoped_token_malformed_environment(self): + """Test whether non string objects are filtered out. + + Put non string objects into the environment, inject + correct assertion and try to get an unscoped token. + Expect server not to fail on using split() method on + non string objects and return token id in the HTTP header. + + """ + api = auth_controllers.Auth() + context = { + 'environment': { + 'malformed_object': object(), + 'another_bad_idea': tuple(xrange(10)), + 'yet_another_bad_param': dict(zip(uuid.uuid4().hex, + range(32))) + } + } + self._inject_assertion(context, 'EMPLOYEE_ASSERTION') + r = api.authenticate_for_token(context, self.UNSCOPED_V3_SAML2_REQ) + self.assertIsNotNone(r.headers.get('X-Subject-Token')) + + def test_scope_to_project_once_notify(self): + r = self.v3_authenticate_token( + self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_EMPLOYEE) + user_id = r.json['token']['user']['id'] + self._assert_last_notify(self.ACTION, self.IDP, self.PROTOCOL, user_id) + + def test_scope_to_project_once(self): + r = self.v3_authenticate_token( + self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_EMPLOYEE) + token_resp = r.result['token'] + project_id = token_resp['project']['id'] + self.assertEqual(project_id, self.proj_employees['id']) + self._check_scoped_token_attributes(token_resp) + roles_ref = [self.role_employee] + projects_ref = self.proj_employees + self._check_projects_and_roles(token_resp, roles_ref, projects_ref) + + def test_scope_token_with_idp_disabled(self): + """Scope token issued by disabled IdP. + + Try scoping the token issued by an IdP which is disabled now. Expect + server to refuse scoping operation. + + This test confirms correct behaviour when IdP was enabled and unscoped + token was issued, but disabled before user tries to scope the token. + Here we assume the unscoped token was already issued and start from + the moment where IdP is being disabled and unscoped token is being + used. + + Test plan: + 1) Disable IdP + 2) Try scoping unscoped token + + """ + enabled_false = {'enabled': False} + self.federation_api.update_idp(self.IDP, enabled_false) + self.v3_authenticate_token( + self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_CUSTOMER, + expected_status=403) + + def test_scope_to_bad_project(self): + """Scope unscoped token with a project we don't have access to.""" + + self.v3_authenticate_token( + self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_CUSTOMER, + expected_status=401) + + def test_scope_to_project_multiple_times(self): + """Try to scope the unscoped token multiple times. + + The new tokens should be scoped to: + + * Customers' project + * Employees' project + + """ + + bodies = (self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_ADMIN, + self.TOKEN_SCOPE_PROJECT_CUSTOMER_FROM_ADMIN) + project_ids = (self.proj_employees['id'], + self.proj_customers['id']) + for body, project_id_ref in zip(bodies, project_ids): + r = self.v3_authenticate_token(body) + token_resp = r.result['token'] + project_id = token_resp['project']['id'] + self.assertEqual(project_id, project_id_ref) + self._check_scoped_token_attributes(token_resp) + + def test_scope_to_project_with_only_inherited_roles(self): + """Try to scope token whose only roles are inherited.""" + self.config_fixture.config(group='os_inherit', enabled=True) + r = self.v3_authenticate_token( + self.TOKEN_SCOPE_PROJECT_INHERITED_FROM_CUSTOMER) + token_resp = r.result['token'] + project_id = token_resp['project']['id'] + self.assertEqual(project_id, self.project_inherited['id']) + self._check_scoped_token_attributes(token_resp) + roles_ref = [self.role_customer] + projects_ref = self.project_inherited + self._check_projects_and_roles(token_resp, roles_ref, projects_ref) + + def test_scope_token_from_nonexistent_unscoped_token(self): + """Try to scope token from non-existent unscoped token.""" + self.v3_authenticate_token( + self.TOKEN_SCOPE_PROJECT_FROM_NONEXISTENT_TOKEN, + expected_status=404) + + def test_issue_token_from_rules_without_user(self): + api = auth_controllers.Auth() + context = {'environment': {}} + self._inject_assertion(context, 'BAD_TESTER_ASSERTION') + self.assertRaises(exception.Unauthorized, + api.authenticate_for_token, + context, self.UNSCOPED_V3_SAML2_REQ) + + def test_issue_token_with_nonexistent_group(self): + """Inject assertion that matches rule issuing bad group id. + + Expect server to find out that some groups are missing in the + backend and raise exception.MappedGroupNotFound exception. + + """ + self.assertRaises(exception.MappedGroupNotFound, + self._issue_unscoped_token, + assertion='CONTRACTOR_ASSERTION') + + def test_scope_to_domain_once(self): + r = self.v3_authenticate_token(self.TOKEN_SCOPE_DOMAIN_A_FROM_CUSTOMER) + token_resp = r.result['token'] + domain_id = token_resp['domain']['id'] + self.assertEqual(self.domainA['id'], domain_id) + self._check_scoped_token_attributes(token_resp) + + def test_scope_to_domain_multiple_tokens(self): + """Issue multiple tokens scoping to different domains. + + The new tokens should be scoped to: + + * domainA + * domainB + * domainC + + """ + bodies = (self.TOKEN_SCOPE_DOMAIN_A_FROM_ADMIN, + self.TOKEN_SCOPE_DOMAIN_B_FROM_ADMIN, + self.TOKEN_SCOPE_DOMAIN_C_FROM_ADMIN) + domain_ids = (self.domainA['id'], + self.domainB['id'], + self.domainC['id']) + + for body, domain_id_ref in zip(bodies, domain_ids): + r = self.v3_authenticate_token(body) + token_resp = r.result['token'] + domain_id = token_resp['domain']['id'] + self.assertEqual(domain_id_ref, domain_id) + self._check_scoped_token_attributes(token_resp) + + def test_scope_to_domain_with_only_inherited_roles_fails(self): + """Try to scope to a domain that has no direct roles.""" + self.v3_authenticate_token( + self.TOKEN_SCOPE_DOMAIN_D_FROM_CUSTOMER, + expected_status=401) + + def test_list_projects(self): + urls = ('/OS-FEDERATION/projects', '/auth/projects') + + token = (self.tokens['CUSTOMER_ASSERTION'], + self.tokens['EMPLOYEE_ASSERTION'], + self.tokens['ADMIN_ASSERTION']) + + self.config_fixture.config(group='os_inherit', enabled=True) + projects_refs = (set([self.proj_customers['id'], + self.project_inherited['id']]), + set([self.proj_employees['id'], + self.project_all['id']]), + set([self.proj_employees['id'], + self.project_all['id'], + self.proj_customers['id'], + self.project_inherited['id']])) + + for token, projects_ref in zip(token, projects_refs): + for url in urls: + r = self.get(url, token=token) + projects_resp = r.result['projects'] + projects = set(p['id'] for p in projects_resp) + self.assertEqual(projects_ref, projects, + 'match failed for url %s' % url) + + def test_list_domains(self): + urls = ('/OS-FEDERATION/domains', '/auth/domains') + + tokens = (self.tokens['CUSTOMER_ASSERTION'], + self.tokens['EMPLOYEE_ASSERTION'], + self.tokens['ADMIN_ASSERTION']) + + # NOTE(henry-nash): domain D does not appear in the expected results + # since it only had inherited roles (which only apply to projects + # within the domain) + + domain_refs = (set([self.domainA['id']]), + set([self.domainA['id'], + self.domainB['id']]), + set([self.domainA['id'], + self.domainB['id'], + self.domainC['id']])) + + for token, domains_ref in zip(tokens, domain_refs): + for url in urls: + r = self.get(url, token=token) + domains_resp = r.result['domains'] + domains = set(p['id'] for p in domains_resp) + self.assertEqual(domains_ref, domains, + 'match failed for url %s' % url) + + def test_full_workflow(self): + """Test 'standard' workflow for granting access tokens. + + * Issue unscoped token + * List available projects based on groups + * Scope token to one of available projects + + """ + + r = self._issue_unscoped_token() + employee_unscoped_token_id = r.headers.get('X-Subject-Token') + r = self.get('/OS-FEDERATION/projects', + token=employee_unscoped_token_id) + projects = r.result['projects'] + random_project = random.randint(0, len(projects)) - 1 + project = projects[random_project] + + v3_scope_request = self._scope_request(employee_unscoped_token_id, + 'project', project['id']) + + r = self.v3_authenticate_token(v3_scope_request) + token_resp = r.result['token'] + project_id = token_resp['project']['id'] + self.assertEqual(project['id'], project_id) + self._check_scoped_token_attributes(token_resp) + + def test_workflow_with_groups_deletion(self): + """Test full workflow with groups deletion before token scoping. + + The test scenario is as follows: + - Create group ``group`` + - Create and assign roles to ``group`` and ``project_all`` + - Patch mapping rules for existing IdP so it issues group id + - Issue unscoped token with ``group``'s id + - Delete group ``group`` + - Scope token to ``project_all`` + - Expect HTTP 500 response + + """ + # create group and role + group = self.new_group_ref( + domain_id=self.domainA['id']) + group = self.identity_api.create_group(group) + role = self.new_role_ref() + self.role_api.create_role(role['id'], role) + + # assign role to group and project_admins + self.assignment_api.create_grant(role['id'], + group_id=group['id'], + project_id=self.project_all['id']) + + rules = { + 'rules': [ + { + 'local': [ + { + 'group': { + 'id': group['id'] + } + }, + { + 'user': { + 'name': '{0}' + } + } + ], + 'remote': [ + { + 'type': 'UserName' + }, + { + 'type': 'LastName', + 'any_one_of': [ + 'Account' + ] + } + ] + } + ] + } + + self.federation_api.update_mapping(self.mapping['id'], rules) + + r = self._issue_unscoped_token(assertion='TESTER_ASSERTION') + token_id = r.headers.get('X-Subject-Token') + + # delete group + self.identity_api.delete_group(group['id']) + + # scope token to project_all, expect HTTP 500 + scoped_token = self._scope_request( + token_id, 'project', + self.project_all['id']) + + self.v3_authenticate_token(scoped_token, expected_status=500) + + def test_lists_with_missing_group_in_backend(self): + """Test a mapping that points to a group that does not exist + + For explicit mappings, we expect the group to exist in the backend, + but for lists, specifically blacklists, a missing group is expected + as many groups will be specified by the IdP that are not Keystone + groups. + + The test scenario is as follows: + - Create group ``EXISTS`` + - Set mapping rules for existing IdP with a blacklist + that passes through as REMOTE_USER_GROUPS + - Issue unscoped token with on group ``EXISTS`` id in it + + """ + domain_id = self.domainA['id'] + domain_name = self.domainA['name'] + group = self.new_group_ref(domain_id=domain_id) + group['name'] = 'EXISTS' + group = self.identity_api.create_group(group) + rules = { + 'rules': [ + { + "local": [ + { + "user": { + "name": "{0}", + "id": "{0}" + } + } + ], + "remote": [ + { + "type": "REMOTE_USER" + } + ] + }, + { + "local": [ + { + "groups": "{0}", + "domain": {"name": domain_name} + } + ], + "remote": [ + { + "type": "REMOTE_USER_GROUPS", + "blacklist": ["noblacklist"] + } + ] + } + ] + } + self.federation_api.update_mapping(self.mapping['id'], rules) + + r = self._issue_unscoped_token(assertion='UNMATCHED_GROUP_ASSERTION') + assigned_group_ids = r.json['token']['user']['OS-FEDERATION']['groups'] + self.assertEqual(1, len(assigned_group_ids)) + self.assertEqual(group['id'], assigned_group_ids[0]['id']) + + def test_assertion_prefix_parameter(self): + """Test parameters filtering based on the prefix. + + With ``assertion_prefix`` set to fixed, non default value, + issue an unscoped token from assertion EMPLOYEE_ASSERTION_PREFIXED. + Expect server to return unscoped token. + + """ + self.config_fixture.config(group='federation', + assertion_prefix=self.ASSERTION_PREFIX) + r = self._issue_unscoped_token(assertion='EMPLOYEE_ASSERTION_PREFIXED') + self.assertIsNotNone(r.headers.get('X-Subject-Token')) + + def test_assertion_prefix_parameter_expect_fail(self): + """Test parameters filtering based on the prefix. + + With ``assertion_prefix`` default value set to empty string + issue an unscoped token from assertion EMPLOYEE_ASSERTION. + Next, configure ``assertion_prefix`` to value ``UserName``. + Try issuing unscoped token with EMPLOYEE_ASSERTION. + Expect server to raise exception.Unathorized exception. + + """ + r = self._issue_unscoped_token() + self.assertIsNotNone(r.headers.get('X-Subject-Token')) + self.config_fixture.config(group='federation', + assertion_prefix='UserName') + + self.assertRaises(exception.Unauthorized, + self._issue_unscoped_token) + + def test_v2_auth_with_federation_token_fails(self): + """Test that using a federation token with v2 auth fails. + + If an admin sets up a federated Keystone environment, and a user + incorrectly configures a service (like Nova) to only use v2 auth, the + returned message should be informative. + + """ + r = self._issue_unscoped_token() + token_id = r.headers.get('X-Subject-Token') + self.assertRaises(exception.Unauthorized, + self.token_provider_api.validate_v2_token, + token_id=token_id) + + def test_unscoped_token_has_user_domain(self): + r = self._issue_unscoped_token() + self._check_domains_are_valid(r.json_body['token']) + + def test_scoped_token_has_user_domain(self): + r = self.v3_authenticate_token( + self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_EMPLOYEE) + self._check_domains_are_valid(r.result['token']) + + def test_issue_unscoped_token_for_local_user(self): + r = self._issue_unscoped_token(assertion='LOCAL_USER_ASSERTION') + token_resp = r.json_body['token'] + self.assertListEqual(['saml2'], token_resp['methods']) + self.assertEqual(self.user['id'], token_resp['user']['id']) + self.assertEqual(self.user['name'], token_resp['user']['name']) + self.assertEqual(self.domain['id'], token_resp['user']['domain']['id']) + # Make sure the token is not scoped + self.assertNotIn('project', token_resp) + self.assertNotIn('domain', token_resp) + + def test_issue_token_for_local_user_user_not_found(self): + self.assertRaises(exception.Unauthorized, + self._issue_unscoped_token, + assertion='ANOTHER_LOCAL_USER_ASSERTION') + + +class FernetFederatedTokenTests(FederationTests, FederatedSetupMixin): + AUTH_METHOD = 'token' + + def load_fixtures(self, fixtures): + super(FernetFederatedTokenTests, self).load_fixtures(fixtures) + self.load_federation_sample_data() + + def auth_plugin_config_override(self): + methods = ['saml2', 'token', 'password'] + method_classes = dict( + password='keystone.auth.plugins.password.Password', + token='keystone.auth.plugins.token.Token', + saml2='keystone.auth.plugins.saml2.Saml2') + super(FernetFederatedTokenTests, + self).auth_plugin_config_override(methods, **method_classes) + self.config_fixture.config( + group='token', + provider='keystone.token.providers.fernet.Provider') + self.useFixture(ksfixtures.KeyRepository(self.config_fixture)) + + def test_federated_unscoped_token(self): + resp = self._issue_unscoped_token() + self.assertEqual(186, len(resp.headers['X-Subject-Token'])) + + def test_federated_unscoped_token_with_multiple_groups(self): + assertion = 'ANOTHER_CUSTOMER_ASSERTION' + resp = self._issue_unscoped_token(assertion=assertion) + self.assertEqual(204, len(resp.headers['X-Subject-Token'])) + + def test_validate_federated_unscoped_token(self): + resp = self._issue_unscoped_token() + unscoped_token = resp.headers.get('X-Subject-Token') + # assert that the token we received is valid + self.get('/auth/tokens/', headers={'X-Subject-Token': unscoped_token}) + + def test_fernet_full_workflow(self): + """Test 'standard' workflow for granting Fernet access tokens. + + * Issue unscoped token + * List available projects based on groups + * Scope token to one of available projects + + """ + resp = self._issue_unscoped_token() + unscoped_token = resp.headers.get('X-Subject-Token') + resp = self.get('/OS-FEDERATION/projects', + token=unscoped_token) + projects = resp.result['projects'] + random_project = random.randint(0, len(projects)) - 1 + project = projects[random_project] + + v3_scope_request = self._scope_request(unscoped_token, + 'project', project['id']) + + resp = self.v3_authenticate_token(v3_scope_request) + token_resp = resp.result['token'] + project_id = token_resp['project']['id'] + self.assertEqual(project['id'], project_id) + self._check_scoped_token_attributes(token_resp) + + +class FederatedTokenTestsMethodToken(FederatedTokenTests): + """Test federation operation with unified scoping auth method. + + Test all the operations with auth method set to ``token`` as a new, unified + way for scoping all the tokens. + + """ + AUTH_METHOD = 'token' + + def auth_plugin_config_override(self): + methods = ['saml2', 'token'] + method_classes = dict( + token='keystone.auth.plugins.token.Token', + saml2='keystone.auth.plugins.saml2.Saml2') + super(FederatedTokenTests, + self).auth_plugin_config_override(methods, **method_classes) + + +class JsonHomeTests(FederationTests, test_v3.JsonHomeTestMixin): + JSON_HOME_DATA = { + 'http://docs.openstack.org/api/openstack-identity/3/ext/OS-FEDERATION/' + '1.0/rel/identity_provider': { + 'href-template': '/OS-FEDERATION/identity_providers/{idp_id}', + 'href-vars': { + 'idp_id': 'http://docs.openstack.org/api/openstack-identity/3/' + 'ext/OS-FEDERATION/1.0/param/idp_id' + }, + }, + } + + +def _is_xmlsec1_installed(): + p = subprocess.Popen( + ['which', 'xmlsec1'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + + # invert the return code + return not bool(p.wait()) + + +def _load_xml(filename): + with open(os.path.join(XMLDIR, filename), 'r') as xml: + return xml.read() + + +class SAMLGenerationTests(FederationTests): + + SP_AUTH_URL = ('http://beta.com:5000/v3/OS-FEDERATION/identity_providers' + '/BETA/protocols/saml2/auth') + ISSUER = 'https://acme.com/FIM/sps/openstack/saml20' + RECIPIENT = 'http://beta.com/Shibboleth.sso/SAML2/POST' + SUBJECT = 'test_user' + ROLES = ['admin', 'member'] + PROJECT = 'development' + SAML_GENERATION_ROUTE = '/auth/OS-FEDERATION/saml2' + ASSERTION_VERSION = "2.0" + SERVICE_PROVDIER_ID = 'ACME' + + def sp_ref(self): + ref = { + 'auth_url': self.SP_AUTH_URL, + 'enabled': True, + 'description': uuid.uuid4().hex, + 'sp_url': self.RECIPIENT, + + } + return ref + + def setUp(self): + super(SAMLGenerationTests, self).setUp() + self.signed_assertion = saml2.create_class_from_xml_string( + saml.Assertion, _load_xml('signed_saml2_assertion.xml')) + self.sp = self.sp_ref() + self.federation_api.create_sp(self.SERVICE_PROVDIER_ID, self.sp) + + def test_samlize_token_values(self): + """Test the SAML generator produces a SAML object. + + Test the SAML generator directly by passing known arguments, the result + should be a SAML object that consistently includes attributes based on + the known arguments that were passed in. + + """ + with mock.patch.object(keystone_idp, '_sign_assertion', + return_value=self.signed_assertion): + generator = keystone_idp.SAMLGenerator() + response = generator.samlize_token(self.ISSUER, self.RECIPIENT, + self.SUBJECT, self.ROLES, + self.PROJECT) + + assertion = response.assertion + self.assertIsNotNone(assertion) + self.assertIsInstance(assertion, saml.Assertion) + issuer = response.issuer + self.assertEqual(self.RECIPIENT, response.destination) + self.assertEqual(self.ISSUER, issuer.text) + + user_attribute = assertion.attribute_statement[0].attribute[0] + self.assertEqual(self.SUBJECT, user_attribute.attribute_value[0].text) + + role_attribute = assertion.attribute_statement[0].attribute[1] + for attribute_value in role_attribute.attribute_value: + self.assertIn(attribute_value.text, self.ROLES) + + project_attribute = assertion.attribute_statement[0].attribute[2] + self.assertEqual(self.PROJECT, + project_attribute.attribute_value[0].text) + + def test_verify_assertion_object(self): + """Test that the Assertion object is built properly. + + The Assertion doesn't need to be signed in this test, so + _sign_assertion method is patched and doesn't alter the assertion. + + """ + with mock.patch.object(keystone_idp, '_sign_assertion', + side_effect=lambda x: x): + generator = keystone_idp.SAMLGenerator() + response = generator.samlize_token(self.ISSUER, self.RECIPIENT, + self.SUBJECT, self.ROLES, + self.PROJECT) + assertion = response.assertion + self.assertEqual(self.ASSERTION_VERSION, assertion.version) + + def test_valid_saml_xml(self): + """Test the generated SAML object can become valid XML. + + Test the generator directly by passing known arguments, the result + should be a SAML object that consistently includes attributes based on + the known arguments that were passed in. + + """ + with mock.patch.object(keystone_idp, '_sign_assertion', + return_value=self.signed_assertion): + generator = keystone_idp.SAMLGenerator() + response = generator.samlize_token(self.ISSUER, self.RECIPIENT, + self.SUBJECT, self.ROLES, + self.PROJECT) + + saml_str = response.to_string() + response = etree.fromstring(saml_str) + issuer = response[0] + assertion = response[2] + + self.assertEqual(self.RECIPIENT, response.get('Destination')) + self.assertEqual(self.ISSUER, issuer.text) + + user_attribute = assertion[4][0] + self.assertEqual(self.SUBJECT, user_attribute[0].text) + + role_attribute = assertion[4][1] + for attribute_value in role_attribute: + self.assertIn(attribute_value.text, self.ROLES) + + project_attribute = assertion[4][2] + self.assertEqual(self.PROJECT, project_attribute[0].text) + + def test_assertion_using_explicit_namespace_prefixes(self): + def mocked_subprocess_check_output(*popenargs, **kwargs): + # the last option is the assertion file to be signed + filename = popenargs[0][-1] + with open(filename, 'r') as f: + assertion_content = f.read() + # since we are not testing the signature itself, we can return + # the assertion as is without signing it + return assertion_content + + with mock.patch('subprocess.check_output', + side_effect=mocked_subprocess_check_output): + generator = keystone_idp.SAMLGenerator() + response = generator.samlize_token(self.ISSUER, self.RECIPIENT, + self.SUBJECT, self.ROLES, + self.PROJECT) + assertion_xml = response.assertion.to_string() + # make sure we have the proper tag and prefix for the assertion + # namespace + self.assertIn(' + + Test Plan: + + - Attempt to get all entities back by passing a two-term attribute + - Attempt to piggyback filter to damage DB (e.g. drop table) + + """ + self._set_policy({"identity:list_users": [], + "identity:list_groups": [], + "identity:create_group": []}) + + url_by_name = "/users?name=anything' or 'x'='x" + r = self.get(url_by_name, auth=self.auth) + + self.assertEqual(0, len(r.result.get('users'))) + + # See if we can add a SQL command...use the group table instead of the + # user table since 'user' is reserved word for SQLAlchemy. + group = self.new_group_ref(domain_id=self.domainB['id']) + group = self.identity_api.create_group(group) + + url_by_name = "/users?name=x'; drop table group" + r = self.get(url_by_name, auth=self.auth) + + # Check group table is still there... + url_by_name = "/groups" + r = self.get(url_by_name, auth=self.auth) + self.assertTrue(len(r.result.get('groups')) > 0) + + +class IdentityTestListLimitCase(IdentityTestFilteredCase): + """Test list limiting enforcement on the v3 Identity API.""" + content_type = 'json' + + def setUp(self): + """Setup for Identity Limit Test Cases.""" + + super(IdentityTestListLimitCase, self).setUp() + + self._set_policy({"identity:list_users": [], + "identity:list_groups": [], + "identity:list_projects": [], + "identity:list_services": [], + "identity:list_policies": []}) + + # Create 10 entries for each of the entities we are going to test + self.ENTITY_TYPES = ['user', 'group', 'project'] + self.entity_lists = {} + for entity in self.ENTITY_TYPES: + self.entity_lists[entity] = self._create_test_data(entity, 10) + # Make sure we clean up when finished + self.addCleanup(self.clean_up_entity, entity) + + self.service_list = [] + self.addCleanup(self.clean_up_service) + for _ in range(10): + new_entity = {'id': uuid.uuid4().hex, 'type': uuid.uuid4().hex} + service = self.catalog_api.create_service(new_entity['id'], + new_entity) + self.service_list.append(service) + + self.policy_list = [] + self.addCleanup(self.clean_up_policy) + for _ in range(10): + new_entity = {'id': uuid.uuid4().hex, 'type': uuid.uuid4().hex, + 'blob': uuid.uuid4().hex} + policy = self.policy_api.create_policy(new_entity['id'], + new_entity) + self.policy_list.append(policy) + + def clean_up_entity(self, entity): + """Clean up entity test data from Identity Limit Test Cases.""" + + self._delete_test_data(entity, self.entity_lists[entity]) + + def clean_up_service(self): + """Clean up service test data from Identity Limit Test Cases.""" + + for service in self.service_list: + self.catalog_api.delete_service(service['id']) + + def clean_up_policy(self): + """Clean up policy test data from Identity Limit Test Cases.""" + + for policy in self.policy_list: + self.policy_api.delete_policy(policy['id']) + + def _test_entity_list_limit(self, entity, driver): + """GET / (limited) + + Test Plan: + + - For the specified type of entity: + - Update policy for no protection on api + - Add a bunch of entities + - Set the global list limit to 5, and check that getting all + - entities only returns 5 + - Set the driver list_limit to 4, and check that now only 4 are + - returned + + """ + if entity == 'policy': + plural = 'policies' + else: + plural = '%ss' % entity + + self.config_fixture.config(list_limit=5) + self.config_fixture.config(group=driver, list_limit=None) + r = self.get('/%s' % plural, auth=self.auth) + self.assertEqual(5, len(r.result.get(plural))) + self.assertIs(r.result.get('truncated'), True) + + self.config_fixture.config(group=driver, list_limit=4) + r = self.get('/%s' % plural, auth=self.auth) + self.assertEqual(4, len(r.result.get(plural))) + self.assertIs(r.result.get('truncated'), True) + + def test_users_list_limit(self): + self._test_entity_list_limit('user', 'identity') + + def test_groups_list_limit(self): + self._test_entity_list_limit('group', 'identity') + + def test_projects_list_limit(self): + self._test_entity_list_limit('project', 'resource') + + def test_services_list_limit(self): + self._test_entity_list_limit('service', 'catalog') + + def test_non_driver_list_limit(self): + """Check list can be limited without driver level support. + + Policy limiting is not done at the driver level (since it + really isn't worth doing it there). So use this as a test + for ensuring the controller level will successfully limit + in this case. + + """ + self._test_entity_list_limit('policy', 'policy') + + def test_no_limit(self): + """Check truncated attribute not set when list not limited.""" + + r = self.get('/services', auth=self.auth) + self.assertEqual(10, len(r.result.get('services'))) + self.assertIsNone(r.result.get('truncated')) + + def test_at_limit(self): + """Check truncated attribute not set when list at max size.""" + + # Test this by overriding the general limit with a higher + # driver-specific limit (allowing all entities to be returned + # in the collection), which should result in a non truncated list + self.config_fixture.config(list_limit=5) + self.config_fixture.config(group='catalog', list_limit=10) + r = self.get('/services', auth=self.auth) + self.assertEqual(10, len(r.result.get('services'))) + self.assertIsNone(r.result.get('truncated')) diff --git a/keystone-moon/keystone/tests/unit/test_v3_identity.py b/keystone-moon/keystone/tests/unit/test_v3_identity.py new file mode 100644 index 00000000..ac077297 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_v3_identity.py @@ -0,0 +1,584 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 oslo_config import cfg +from testtools import matchers + +from keystone.common import controller +from keystone import exception +from keystone.tests import unit as tests +from keystone.tests.unit import test_v3 + + +CONF = cfg.CONF + + +class IdentityTestCase(test_v3.RestfulTestCase): + """Test users and groups.""" + + def setUp(self): + super(IdentityTestCase, self).setUp() + + self.group = self.new_group_ref( + domain_id=self.domain_id) + self.group = self.identity_api.create_group(self.group) + self.group_id = self.group['id'] + + self.credential_id = uuid.uuid4().hex + self.credential = self.new_credential_ref( + user_id=self.user['id'], + project_id=self.project_id) + self.credential['id'] = self.credential_id + self.credential_api.create_credential( + self.credential_id, + self.credential) + + # user crud tests + + def test_create_user(self): + """Call ``POST /users``.""" + ref = self.new_user_ref(domain_id=self.domain_id) + r = self.post( + '/users', + body={'user': ref}) + return self.assertValidUserResponse(r, ref) + + def test_create_user_without_domain(self): + """Call ``POST /users`` without specifying domain. + + According to the identity-api specification, if you do not + explicitly specific the domain_id in the entity, it should + take the domain scope of the token as the domain_id. + + """ + # Create a user with a role on the domain so we can get a + # domain scoped token + domain = self.new_domain_ref() + self.resource_api.create_domain(domain['id'], domain) + user = self.new_user_ref(domain_id=domain['id']) + password = user['password'] + user = self.identity_api.create_user(user) + user['password'] = password + self.assignment_api.create_grant( + role_id=self.role_id, user_id=user['id'], + domain_id=domain['id']) + + ref = self.new_user_ref(domain_id=domain['id']) + ref_nd = ref.copy() + ref_nd.pop('domain_id') + auth = self.build_authentication_request( + user_id=user['id'], + password=user['password'], + domain_id=domain['id']) + r = self.post('/users', body={'user': ref_nd}, auth=auth) + self.assertValidUserResponse(r, ref) + + # Now try the same thing without a domain token - which should fail + ref = self.new_user_ref(domain_id=domain['id']) + ref_nd = ref.copy() + ref_nd.pop('domain_id') + auth = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + project_id=self.project['id']) + r = self.post('/users', body={'user': ref_nd}, auth=auth) + # TODO(henry-nash): Due to bug #1283539 we currently automatically + # use the default domain_id if a domain scoped token is not being + # used. Change the code below to expect a failure once this bug is + # fixed. + ref['domain_id'] = CONF.identity.default_domain_id + return self.assertValidUserResponse(r, ref) + + def test_create_user_400(self): + """Call ``POST /users``.""" + self.post('/users', body={'user': {}}, expected_status=400) + + def test_list_users(self): + """Call ``GET /users``.""" + resource_url = '/users' + r = self.get(resource_url) + self.assertValidUserListResponse(r, ref=self.user, + resource_url=resource_url) + + def test_list_users_with_multiple_backends(self): + """Call ``GET /users`` when multiple backends is enabled. + + In this scenario, the controller requires a domain to be specified + either as a filter or by using a domain scoped token. + + """ + self.config_fixture.config(group='identity', + domain_specific_drivers_enabled=True) + + # Create a user with a role on the domain so we can get a + # domain scoped token + domain = self.new_domain_ref() + self.resource_api.create_domain(domain['id'], domain) + user = self.new_user_ref(domain_id=domain['id']) + password = user['password'] + user = self.identity_api.create_user(user) + user['password'] = password + self.assignment_api.create_grant( + role_id=self.role_id, user_id=user['id'], + domain_id=domain['id']) + + ref = self.new_user_ref(domain_id=domain['id']) + ref_nd = ref.copy() + ref_nd.pop('domain_id') + auth = self.build_authentication_request( + user_id=user['id'], + password=user['password'], + domain_id=domain['id']) + + # First try using a domain scoped token + resource_url = '/users' + r = self.get(resource_url, auth=auth) + self.assertValidUserListResponse(r, ref=user, + resource_url=resource_url) + + # Now try with an explicit filter + resource_url = ('/users?domain_id=%(domain_id)s' % + {'domain_id': domain['id']}) + r = self.get(resource_url) + self.assertValidUserListResponse(r, ref=user, + resource_url=resource_url) + + # Now try the same thing without a domain token or filter, + # which should fail + r = self.get('/users', expected_status=exception.Unauthorized.code) + + def test_list_users_with_static_admin_token_and_multiple_backends(self): + # domain-specific operations with the bootstrap ADMIN token is + # disallowed when domain-specific drivers are enabled + self.config_fixture.config(group='identity', + domain_specific_drivers_enabled=True) + self.get('/users', token=CONF.admin_token, + expected_status=exception.Unauthorized.code) + + def test_list_users_no_default_project(self): + """Call ``GET /users`` making sure no default_project_id.""" + user = self.new_user_ref(self.domain_id) + user = self.identity_api.create_user(user) + resource_url = '/users' + r = self.get(resource_url) + self.assertValidUserListResponse(r, ref=user, + resource_url=resource_url) + + def test_get_user(self): + """Call ``GET /users/{user_id}``.""" + r = self.get('/users/%(user_id)s' % { + 'user_id': self.user['id']}) + self.assertValidUserResponse(r, self.user) + + def test_get_user_with_default_project(self): + """Call ``GET /users/{user_id}`` making sure of default_project_id.""" + user = self.new_user_ref(domain_id=self.domain_id, + project_id=self.project_id) + user = self.identity_api.create_user(user) + r = self.get('/users/%(user_id)s' % {'user_id': user['id']}) + self.assertValidUserResponse(r, user) + + def test_add_user_to_group(self): + """Call ``PUT /groups/{group_id}/users/{user_id}``.""" + self.put('/groups/%(group_id)s/users/%(user_id)s' % { + 'group_id': self.group_id, 'user_id': self.user['id']}) + + def test_list_groups_for_user(self): + """Call ``GET /users/{user_id}/groups``.""" + + self.user1 = self.new_user_ref( + domain_id=self.domain['id']) + password = self.user1['password'] + self.user1 = self.identity_api.create_user(self.user1) + self.user1['password'] = password + self.user2 = self.new_user_ref( + domain_id=self.domain['id']) + password = self.user2['password'] + self.user2 = self.identity_api.create_user(self.user2) + self.user2['password'] = password + self.put('/groups/%(group_id)s/users/%(user_id)s' % { + 'group_id': self.group_id, 'user_id': self.user1['id']}) + + # Scenarios below are written to test the default policy configuration + + # One should be allowed to list one's own groups + auth = self.build_authentication_request( + user_id=self.user1['id'], + password=self.user1['password']) + resource_url = ('/users/%(user_id)s/groups' % + {'user_id': self.user1['id']}) + r = self.get(resource_url, auth=auth) + self.assertValidGroupListResponse(r, ref=self.group, + resource_url=resource_url) + + # Administrator is allowed to list others' groups + resource_url = ('/users/%(user_id)s/groups' % + {'user_id': self.user1['id']}) + r = self.get(resource_url) + self.assertValidGroupListResponse(r, ref=self.group, + resource_url=resource_url) + + # Ordinary users should not be allowed to list other's groups + auth = self.build_authentication_request( + user_id=self.user2['id'], + password=self.user2['password']) + r = self.get('/users/%(user_id)s/groups' % { + 'user_id': self.user1['id']}, auth=auth, + expected_status=exception.ForbiddenAction.code) + + def test_check_user_in_group(self): + """Call ``HEAD /groups/{group_id}/users/{user_id}``.""" + self.put('/groups/%(group_id)s/users/%(user_id)s' % { + 'group_id': self.group_id, 'user_id': self.user['id']}) + self.head('/groups/%(group_id)s/users/%(user_id)s' % { + 'group_id': self.group_id, 'user_id': self.user['id']}) + + def test_list_users_in_group(self): + """Call ``GET /groups/{group_id}/users``.""" + self.put('/groups/%(group_id)s/users/%(user_id)s' % { + 'group_id': self.group_id, 'user_id': self.user['id']}) + resource_url = ('/groups/%(group_id)s/users' % + {'group_id': self.group_id}) + r = self.get(resource_url) + self.assertValidUserListResponse(r, ref=self.user, + resource_url=resource_url) + self.assertIn('/groups/%(group_id)s/users' % { + 'group_id': self.group_id}, r.result['links']['self']) + + def test_remove_user_from_group(self): + """Call ``DELETE /groups/{group_id}/users/{user_id}``.""" + self.put('/groups/%(group_id)s/users/%(user_id)s' % { + 'group_id': self.group_id, 'user_id': self.user['id']}) + self.delete('/groups/%(group_id)s/users/%(user_id)s' % { + 'group_id': self.group_id, 'user_id': self.user['id']}) + + def test_update_user(self): + """Call ``PATCH /users/{user_id}``.""" + user = self.new_user_ref(domain_id=self.domain_id) + del user['id'] + r = self.patch('/users/%(user_id)s' % { + 'user_id': self.user['id']}, + body={'user': user}) + self.assertValidUserResponse(r, user) + + def test_admin_password_reset(self): + # bootstrap a user as admin + user_ref = self.new_user_ref(domain_id=self.domain['id']) + password = user_ref['password'] + user_ref = self.identity_api.create_user(user_ref) + + # auth as user should work before a password change + old_password_auth = self.build_authentication_request( + user_id=user_ref['id'], + password=password) + r = self.v3_authenticate_token(old_password_auth, expected_status=201) + old_token = r.headers.get('X-Subject-Token') + + # auth as user with a token should work before a password change + old_token_auth = self.build_authentication_request(token=old_token) + self.v3_authenticate_token(old_token_auth, expected_status=201) + + # administrative password reset + new_password = uuid.uuid4().hex + self.patch('/users/%s' % user_ref['id'], + body={'user': {'password': new_password}}, + expected_status=200) + + # auth as user with original password should not work after change + self.v3_authenticate_token(old_password_auth, expected_status=401) + + # auth as user with an old token should not work after change + self.v3_authenticate_token(old_token_auth, expected_status=404) + + # new password should work + new_password_auth = self.build_authentication_request( + user_id=user_ref['id'], + password=new_password) + self.v3_authenticate_token(new_password_auth, expected_status=201) + + def test_update_user_domain_id(self): + """Call ``PATCH /users/{user_id}`` with domain_id.""" + user = self.new_user_ref(domain_id=self.domain['id']) + user = self.identity_api.create_user(user) + user['domain_id'] = CONF.identity.default_domain_id + r = self.patch('/users/%(user_id)s' % { + 'user_id': user['id']}, + body={'user': user}, + expected_status=exception.ValidationError.code) + self.config_fixture.config(domain_id_immutable=False) + user['domain_id'] = self.domain['id'] + r = self.patch('/users/%(user_id)s' % { + 'user_id': user['id']}, + body={'user': user}) + self.assertValidUserResponse(r, user) + + def test_delete_user(self): + """Call ``DELETE /users/{user_id}``. + + As well as making sure the delete succeeds, we ensure + that any credentials that reference this user are + also deleted, while other credentials are unaffected. + In addition, no tokens should remain valid for this user. + + """ + # First check the credential for this user is present + r = self.credential_api.get_credential(self.credential['id']) + self.assertDictEqual(r, self.credential) + # Create a second credential with a different user + self.user2 = self.new_user_ref( + domain_id=self.domain['id'], + project_id=self.project['id']) + self.user2 = self.identity_api.create_user(self.user2) + self.credential2 = self.new_credential_ref( + user_id=self.user2['id'], + project_id=self.project['id']) + self.credential_api.create_credential( + self.credential2['id'], + self.credential2) + # Create a token for this user which we can check later + # gets deleted + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + project_id=self.project['id']) + token = self.get_requested_token(auth_data) + # Confirm token is valid for now + self.head('/auth/tokens', + headers={'X-Subject-Token': token}, + expected_status=200) + + # Now delete the user + self.delete('/users/%(user_id)s' % { + 'user_id': self.user['id']}) + + # Deleting the user should have deleted any credentials + # that reference this project + self.assertRaises(exception.CredentialNotFound, + self.credential_api.get_credential, + self.credential['id']) + # And the no tokens we remain valid + tokens = self.token_provider_api._persistence._list_tokens( + self.user['id']) + self.assertEqual(0, len(tokens)) + # But the credential for user2 is unaffected + r = self.credential_api.get_credential(self.credential2['id']) + self.assertDictEqual(r, self.credential2) + + # group crud tests + + def test_create_group(self): + """Call ``POST /groups``.""" + ref = self.new_group_ref(domain_id=self.domain_id) + r = self.post( + '/groups', + body={'group': ref}) + return self.assertValidGroupResponse(r, ref) + + def test_create_group_400(self): + """Call ``POST /groups``.""" + self.post('/groups', body={'group': {}}, expected_status=400) + + def test_list_groups(self): + """Call ``GET /groups``.""" + resource_url = '/groups' + r = self.get(resource_url) + self.assertValidGroupListResponse(r, ref=self.group, + resource_url=resource_url) + + def test_get_group(self): + """Call ``GET /groups/{group_id}``.""" + r = self.get('/groups/%(group_id)s' % { + 'group_id': self.group_id}) + self.assertValidGroupResponse(r, self.group) + + def test_update_group(self): + """Call ``PATCH /groups/{group_id}``.""" + group = self.new_group_ref(domain_id=self.domain_id) + del group['id'] + r = self.patch('/groups/%(group_id)s' % { + 'group_id': self.group_id}, + body={'group': group}) + self.assertValidGroupResponse(r, group) + + def test_update_group_domain_id(self): + """Call ``PATCH /groups/{group_id}`` with domain_id.""" + group = self.new_group_ref(domain_id=self.domain['id']) + group = self.identity_api.create_group(group) + group['domain_id'] = CONF.identity.default_domain_id + r = self.patch('/groups/%(group_id)s' % { + 'group_id': group['id']}, + body={'group': group}, + expected_status=exception.ValidationError.code) + self.config_fixture.config(domain_id_immutable=False) + group['domain_id'] = self.domain['id'] + r = self.patch('/groups/%(group_id)s' % { + 'group_id': group['id']}, + body={'group': group}) + self.assertValidGroupResponse(r, group) + + def test_delete_group(self): + """Call ``DELETE /groups/{group_id}``.""" + self.delete('/groups/%(group_id)s' % { + 'group_id': self.group_id}) + + +class IdentityV3toV2MethodsTestCase(tests.TestCase): + """Test users V3 to V2 conversion methods.""" + + def setUp(self): + super(IdentityV3toV2MethodsTestCase, self).setUp() + self.load_backends() + self.user_id = uuid.uuid4().hex + self.default_project_id = uuid.uuid4().hex + self.tenant_id = uuid.uuid4().hex + self.domain_id = uuid.uuid4().hex + # User with only default_project_id in ref + self.user1 = {'id': self.user_id, + 'name': self.user_id, + 'default_project_id': self.default_project_id, + 'domain_id': self.domain_id} + # User without default_project_id or tenantId in ref + self.user2 = {'id': self.user_id, + 'name': self.user_id, + 'domain_id': self.domain_id} + # User with both tenantId and default_project_id in ref + self.user3 = {'id': self.user_id, + 'name': self.user_id, + 'default_project_id': self.default_project_id, + 'tenantId': self.tenant_id, + 'domain_id': self.domain_id} + # User with only tenantId in ref + self.user4 = {'id': self.user_id, + 'name': self.user_id, + 'tenantId': self.tenant_id, + 'domain_id': self.domain_id} + + # Expected result if the user is meant to have a tenantId element + self.expected_user = {'id': self.user_id, + 'name': self.user_id, + 'username': self.user_id, + 'tenantId': self.default_project_id} + + # Expected result if the user is not meant to have a tenantId element + self.expected_user_no_tenant_id = {'id': self.user_id, + 'name': self.user_id, + 'username': self.user_id} + + def test_v3_to_v2_user_method(self): + + updated_user1 = controller.V2Controller.v3_to_v2_user(self.user1) + self.assertIs(self.user1, updated_user1) + self.assertDictEqual(self.user1, self.expected_user) + updated_user2 = controller.V2Controller.v3_to_v2_user(self.user2) + self.assertIs(self.user2, updated_user2) + self.assertDictEqual(self.user2, self.expected_user_no_tenant_id) + updated_user3 = controller.V2Controller.v3_to_v2_user(self.user3) + self.assertIs(self.user3, updated_user3) + self.assertDictEqual(self.user3, self.expected_user) + updated_user4 = controller.V2Controller.v3_to_v2_user(self.user4) + self.assertIs(self.user4, updated_user4) + self.assertDictEqual(self.user4, self.expected_user_no_tenant_id) + + def test_v3_to_v2_user_method_list(self): + user_list = [self.user1, self.user2, self.user3, self.user4] + updated_list = controller.V2Controller.v3_to_v2_user(user_list) + + self.assertEqual(len(updated_list), len(user_list)) + + for i, ref in enumerate(updated_list): + # Order should not change. + self.assertIs(ref, user_list[i]) + + self.assertDictEqual(self.user1, self.expected_user) + self.assertDictEqual(self.user2, self.expected_user_no_tenant_id) + self.assertDictEqual(self.user3, self.expected_user) + self.assertDictEqual(self.user4, self.expected_user_no_tenant_id) + + +class UserSelfServiceChangingPasswordsTestCase(test_v3.RestfulTestCase): + + def setUp(self): + super(UserSelfServiceChangingPasswordsTestCase, self).setUp() + self.user_ref = self.new_user_ref(domain_id=self.domain['id']) + password = self.user_ref['password'] + self.user_ref = self.identity_api.create_user(self.user_ref) + self.user_ref['password'] = password + self.token = self.get_request_token(self.user_ref['password'], 201) + + def get_request_token(self, password, expected_status): + auth_data = self.build_authentication_request( + user_id=self.user_ref['id'], + password=password) + r = self.v3_authenticate_token(auth_data, + expected_status=expected_status) + return r.headers.get('X-Subject-Token') + + def change_password(self, expected_status, **kwargs): + """Returns a test response for a change password request.""" + return self.post('/users/%s/password' % self.user_ref['id'], + body={'user': kwargs}, + token=self.token, + expected_status=expected_status) + + def test_changing_password(self): + # original password works + token_id = self.get_request_token(self.user_ref['password'], + expected_status=201) + # original token works + old_token_auth = self.build_authentication_request(token=token_id) + self.v3_authenticate_token(old_token_auth, expected_status=201) + + # change password + new_password = uuid.uuid4().hex + self.change_password(password=new_password, + original_password=self.user_ref['password'], + expected_status=204) + + # old password fails + self.get_request_token(self.user_ref['password'], expected_status=401) + + # old token fails + self.v3_authenticate_token(old_token_auth, expected_status=404) + + # new password works + self.get_request_token(new_password, expected_status=201) + + def test_changing_password_with_missing_original_password_fails(self): + r = self.change_password(password=uuid.uuid4().hex, + expected_status=400) + self.assertThat(r.result['error']['message'], + matchers.Contains('original_password')) + + def test_changing_password_with_missing_password_fails(self): + r = self.change_password(original_password=self.user_ref['password'], + expected_status=400) + self.assertThat(r.result['error']['message'], + matchers.Contains('password')) + + def test_changing_password_with_incorrect_password_fails(self): + self.change_password(password=uuid.uuid4().hex, + original_password=uuid.uuid4().hex, + expected_status=401) + + def test_changing_password_with_disabled_user_fails(self): + # disable the user account + self.user_ref['enabled'] = False + self.patch('/users/%s' % self.user_ref['id'], + body={'user': self.user_ref}) + + self.change_password(password=uuid.uuid4().hex, + original_password=self.user_ref['password'], + expected_status=401) diff --git a/keystone-moon/keystone/tests/unit/test_v3_oauth1.py b/keystone-moon/keystone/tests/unit/test_v3_oauth1.py new file mode 100644 index 00000000..608162d8 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_v3_oauth1.py @@ -0,0 +1,891 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 +import uuid + +from oslo_config import cfg +from oslo_serialization import jsonutils +from pycadf import cadftaxonomy +from six.moves import urllib + +from keystone.contrib import oauth1 +from keystone.contrib.oauth1 import controllers +from keystone.contrib.oauth1 import core +from keystone import exception +from keystone.tests.unit.common import test_notifications +from keystone.tests.unit.ksfixtures import temporaryfile +from keystone.tests.unit import test_v3 + + +CONF = cfg.CONF + + +class OAuth1Tests(test_v3.RestfulTestCase): + + EXTENSION_NAME = 'oauth1' + EXTENSION_TO_ADD = 'oauth1_extension' + + CONSUMER_URL = '/OS-OAUTH1/consumers' + + def setUp(self): + super(OAuth1Tests, self).setUp() + + # Now that the app has been served, we can query CONF values + self.base_url = 'http://localhost/v3' + self.controller = controllers.OAuthControllerV3() + + def _create_single_consumer(self): + ref = {'description': uuid.uuid4().hex} + resp = self.post( + self.CONSUMER_URL, + body={'consumer': ref}) + return resp.result['consumer'] + + def _create_request_token(self, consumer, project_id): + endpoint = '/OS-OAUTH1/request_token' + client = oauth1.Client(consumer['key'], + client_secret=consumer['secret'], + signature_method=oauth1.SIG_HMAC, + callback_uri="oob") + headers = {'requested_project_id': project_id} + url, headers, body = client.sign(self.base_url + endpoint, + http_method='POST', + headers=headers) + return endpoint, headers + + def _create_access_token(self, consumer, token): + endpoint = '/OS-OAUTH1/access_token' + client = oauth1.Client(consumer['key'], + client_secret=consumer['secret'], + resource_owner_key=token.key, + resource_owner_secret=token.secret, + signature_method=oauth1.SIG_HMAC, + verifier=token.verifier) + url, headers, body = client.sign(self.base_url + endpoint, + http_method='POST') + headers.update({'Content-Type': 'application/json'}) + return endpoint, headers + + def _get_oauth_token(self, consumer, token): + client = oauth1.Client(consumer['key'], + client_secret=consumer['secret'], + resource_owner_key=token.key, + resource_owner_secret=token.secret, + signature_method=oauth1.SIG_HMAC) + endpoint = '/auth/tokens' + url, headers, body = client.sign(self.base_url + endpoint, + http_method='POST') + headers.update({'Content-Type': 'application/json'}) + ref = {'auth': {'identity': {'oauth1': {}, 'methods': ['oauth1']}}} + return endpoint, headers, ref + + def _authorize_request_token(self, request_id): + return '/OS-OAUTH1/authorize/%s' % (request_id) + + +class ConsumerCRUDTests(OAuth1Tests): + + def _consumer_create(self, description=None, description_flag=True, + **kwargs): + if description_flag: + ref = {'description': description} + else: + ref = {} + if kwargs: + ref.update(kwargs) + resp = self.post( + self.CONSUMER_URL, + body={'consumer': ref}) + consumer = resp.result['consumer'] + consumer_id = consumer['id'] + self.assertEqual(description, consumer['description']) + self.assertIsNotNone(consumer_id) + self.assertIsNotNone(consumer['secret']) + return consumer + + def test_consumer_create(self): + description = uuid.uuid4().hex + self._consumer_create(description=description) + + def test_consumer_create_none_desc_1(self): + self._consumer_create() + + def test_consumer_create_none_desc_2(self): + self._consumer_create(description_flag=False) + + def test_consumer_create_normalize_field(self): + # If create a consumer with a field with : or - in the name, + # the name is normalized by converting those chars to _. + field_name = 'some:weird-field' + field_value = uuid.uuid4().hex + extra_fields = {field_name: field_value} + consumer = self._consumer_create(**extra_fields) + normalized_field_name = 'some_weird_field' + self.assertEqual(field_value, consumer[normalized_field_name]) + + def test_consumer_delete(self): + consumer = self._create_single_consumer() + consumer_id = consumer['id'] + resp = self.delete(self.CONSUMER_URL + '/%s' % consumer_id) + self.assertResponseStatus(resp, 204) + + def test_consumer_get(self): + consumer = self._create_single_consumer() + consumer_id = consumer['id'] + resp = self.get(self.CONSUMER_URL + '/%s' % consumer_id) + self_url = ['http://localhost/v3', self.CONSUMER_URL, + '/', consumer_id] + self_url = ''.join(self_url) + self.assertEqual(self_url, resp.result['consumer']['links']['self']) + self.assertEqual(consumer_id, resp.result['consumer']['id']) + + def test_consumer_list(self): + self._consumer_create() + resp = self.get(self.CONSUMER_URL) + entities = resp.result['consumers'] + self.assertIsNotNone(entities) + self_url = ['http://localhost/v3', self.CONSUMER_URL] + self_url = ''.join(self_url) + self.assertEqual(self_url, resp.result['links']['self']) + self.assertValidListLinks(resp.result['links']) + + def test_consumer_update(self): + consumer = self._create_single_consumer() + original_id = consumer['id'] + original_description = consumer['description'] + update_description = original_description + '_new' + + update_ref = {'description': update_description} + update_resp = self.patch(self.CONSUMER_URL + '/%s' % original_id, + body={'consumer': update_ref}) + consumer = update_resp.result['consumer'] + self.assertEqual(update_description, consumer['description']) + self.assertEqual(original_id, consumer['id']) + + def test_consumer_update_bad_secret(self): + consumer = self._create_single_consumer() + original_id = consumer['id'] + update_ref = copy.deepcopy(consumer) + update_ref['description'] = uuid.uuid4().hex + update_ref['secret'] = uuid.uuid4().hex + self.patch(self.CONSUMER_URL + '/%s' % original_id, + body={'consumer': update_ref}, + expected_status=400) + + def test_consumer_update_bad_id(self): + consumer = self._create_single_consumer() + original_id = consumer['id'] + original_description = consumer['description'] + update_description = original_description + "_new" + + update_ref = copy.deepcopy(consumer) + update_ref['description'] = update_description + update_ref['id'] = update_description + self.patch(self.CONSUMER_URL + '/%s' % original_id, + body={'consumer': update_ref}, + expected_status=400) + + def test_consumer_update_normalize_field(self): + # If update a consumer with a field with : or - in the name, + # the name is normalized by converting those chars to _. + field1_name = 'some:weird-field' + field1_orig_value = uuid.uuid4().hex + + extra_fields = {field1_name: field1_orig_value} + consumer = self._consumer_create(**extra_fields) + consumer_id = consumer['id'] + + field1_new_value = uuid.uuid4().hex + + field2_name = 'weird:some-field' + field2_value = uuid.uuid4().hex + + update_ref = {field1_name: field1_new_value, + field2_name: field2_value} + + update_resp = self.patch(self.CONSUMER_URL + '/%s' % consumer_id, + body={'consumer': update_ref}) + consumer = update_resp.result['consumer'] + + normalized_field1_name = 'some_weird_field' + self.assertEqual(field1_new_value, consumer[normalized_field1_name]) + + normalized_field2_name = 'weird_some_field' + self.assertEqual(field2_value, consumer[normalized_field2_name]) + + def test_consumer_create_no_description(self): + resp = self.post(self.CONSUMER_URL, body={'consumer': {}}) + consumer = resp.result['consumer'] + consumer_id = consumer['id'] + self.assertIsNone(consumer['description']) + self.assertIsNotNone(consumer_id) + self.assertIsNotNone(consumer['secret']) + + def test_consumer_get_bad_id(self): + self.get(self.CONSUMER_URL + '/%(consumer_id)s' + % {'consumer_id': uuid.uuid4().hex}, + expected_status=404) + + +class OAuthFlowTests(OAuth1Tests): + + def auth_plugin_config_override(self): + methods = ['password', 'token', 'oauth1'] + method_classes = { + 'password': 'keystone.auth.plugins.password.Password', + 'token': 'keystone.auth.plugins.token.Token', + 'oauth1': 'keystone.auth.plugins.oauth1.OAuth', + } + super(OAuthFlowTests, self).auth_plugin_config_override( + methods, **method_classes) + + def test_oauth_flow(self): + consumer = self._create_single_consumer() + consumer_id = consumer['id'] + consumer_secret = consumer['secret'] + self.consumer = {'key': consumer_id, 'secret': consumer_secret} + self.assertIsNotNone(self.consumer['secret']) + + url, headers = self._create_request_token(self.consumer, + self.project_id) + content = self.post( + url, headers=headers, + response_content_type='application/x-www-urlformencoded') + credentials = urllib.parse.parse_qs(content.result) + request_key = credentials['oauth_token'][0] + request_secret = credentials['oauth_token_secret'][0] + self.request_token = oauth1.Token(request_key, request_secret) + self.assertIsNotNone(self.request_token.key) + + url = self._authorize_request_token(request_key) + body = {'roles': [{'id': self.role_id}]} + resp = self.put(url, body=body, expected_status=200) + self.verifier = resp.result['token']['oauth_verifier'] + self.assertTrue(all(i in core.VERIFIER_CHARS for i in self.verifier)) + self.assertEqual(8, len(self.verifier)) + + self.request_token.set_verifier(self.verifier) + url, headers = self._create_access_token(self.consumer, + self.request_token) + content = self.post( + url, headers=headers, + response_content_type='application/x-www-urlformencoded') + credentials = urllib.parse.parse_qs(content.result) + access_key = credentials['oauth_token'][0] + access_secret = credentials['oauth_token_secret'][0] + self.access_token = oauth1.Token(access_key, access_secret) + self.assertIsNotNone(self.access_token.key) + + url, headers, body = self._get_oauth_token(self.consumer, + self.access_token) + content = self.post(url, headers=headers, body=body) + self.keystone_token_id = content.headers['X-Subject-Token'] + self.keystone_token = content.result['token'] + self.assertIsNotNone(self.keystone_token_id) + + +class AccessTokenCRUDTests(OAuthFlowTests): + def test_delete_access_token_dne(self): + self.delete('/users/%(user)s/OS-OAUTH1/access_tokens/%(auth)s' + % {'user': self.user_id, + 'auth': uuid.uuid4().hex}, + expected_status=404) + + def test_list_no_access_tokens(self): + resp = self.get('/users/%(user_id)s/OS-OAUTH1/access_tokens' + % {'user_id': self.user_id}) + entities = resp.result['access_tokens'] + self.assertEqual([], entities) + self.assertValidListLinks(resp.result['links']) + + def test_get_single_access_token(self): + self.test_oauth_flow() + url = '/users/%(user_id)s/OS-OAUTH1/access_tokens/%(key)s' % { + 'user_id': self.user_id, + 'key': self.access_token.key + } + resp = self.get(url) + entity = resp.result['access_token'] + self.assertEqual(self.access_token.key, entity['id']) + self.assertEqual(self.consumer['key'], entity['consumer_id']) + self.assertEqual('http://localhost/v3' + url, entity['links']['self']) + + def test_get_access_token_dne(self): + self.get('/users/%(user_id)s/OS-OAUTH1/access_tokens/%(key)s' + % {'user_id': self.user_id, + 'key': uuid.uuid4().hex}, + expected_status=404) + + def test_list_all_roles_in_access_token(self): + self.test_oauth_flow() + resp = self.get('/users/%(id)s/OS-OAUTH1/access_tokens/%(key)s/roles' + % {'id': self.user_id, + 'key': self.access_token.key}) + entities = resp.result['roles'] + self.assertTrue(entities) + self.assertValidListLinks(resp.result['links']) + + def test_get_role_in_access_token(self): + self.test_oauth_flow() + url = ('/users/%(id)s/OS-OAUTH1/access_tokens/%(key)s/roles/%(role)s' + % {'id': self.user_id, 'key': self.access_token.key, + 'role': self.role_id}) + resp = self.get(url) + entity = resp.result['role'] + self.assertEqual(self.role_id, entity['id']) + + def test_get_role_in_access_token_dne(self): + self.test_oauth_flow() + url = ('/users/%(id)s/OS-OAUTH1/access_tokens/%(key)s/roles/%(role)s' + % {'id': self.user_id, 'key': self.access_token.key, + 'role': uuid.uuid4().hex}) + self.get(url, expected_status=404) + + def test_list_and_delete_access_tokens(self): + self.test_oauth_flow() + # List access_tokens should be > 0 + resp = self.get('/users/%(user_id)s/OS-OAUTH1/access_tokens' + % {'user_id': self.user_id}) + entities = resp.result['access_tokens'] + self.assertTrue(entities) + self.assertValidListLinks(resp.result['links']) + + # Delete access_token + resp = self.delete('/users/%(user)s/OS-OAUTH1/access_tokens/%(auth)s' + % {'user': self.user_id, + 'auth': self.access_token.key}) + self.assertResponseStatus(resp, 204) + + # List access_token should be 0 + resp = self.get('/users/%(user_id)s/OS-OAUTH1/access_tokens' + % {'user_id': self.user_id}) + entities = resp.result['access_tokens'] + self.assertEqual([], entities) + self.assertValidListLinks(resp.result['links']) + + +class AuthTokenTests(OAuthFlowTests): + + def test_keystone_token_is_valid(self): + self.test_oauth_flow() + headers = {'X-Subject-Token': self.keystone_token_id, + 'X-Auth-Token': self.keystone_token_id} + r = self.get('/auth/tokens', headers=headers) + self.assertValidTokenResponse(r, self.user) + + # now verify the oauth section + oauth_section = r.result['token']['OS-OAUTH1'] + self.assertEqual(self.access_token.key, + oauth_section['access_token_id']) + self.assertEqual(self.consumer['key'], oauth_section['consumer_id']) + + # verify the roles section + roles_list = r.result['token']['roles'] + # we can just verify the 0th role since we are only assigning one role + self.assertEqual(self.role_id, roles_list[0]['id']) + + # verify that the token can perform delegated tasks + ref = self.new_user_ref(domain_id=self.domain_id) + r = self.admin_request(path='/v3/users', headers=headers, + method='POST', body={'user': ref}) + self.assertValidUserResponse(r, ref) + + def test_delete_access_token_also_revokes_token(self): + self.test_oauth_flow() + + # Delete access token + resp = self.delete('/users/%(user)s/OS-OAUTH1/access_tokens/%(auth)s' + % {'user': self.user_id, + 'auth': self.access_token.key}) + self.assertResponseStatus(resp, 204) + + # Check Keystone Token no longer exists + headers = {'X-Subject-Token': self.keystone_token_id, + 'X-Auth-Token': self.keystone_token_id} + self.get('/auth/tokens', headers=headers, + expected_status=404) + + def test_deleting_consumer_also_deletes_tokens(self): + self.test_oauth_flow() + + # Delete consumer + consumer_id = self.consumer['key'] + resp = self.delete('/OS-OAUTH1/consumers/%(consumer_id)s' + % {'consumer_id': consumer_id}) + self.assertResponseStatus(resp, 204) + + # List access_token should be 0 + resp = self.get('/users/%(user_id)s/OS-OAUTH1/access_tokens' + % {'user_id': self.user_id}) + entities = resp.result['access_tokens'] + self.assertEqual([], entities) + + # Check Keystone Token no longer exists + headers = {'X-Subject-Token': self.keystone_token_id, + 'X-Auth-Token': self.keystone_token_id} + self.head('/auth/tokens', headers=headers, + expected_status=404) + + def test_change_user_password_also_deletes_tokens(self): + self.test_oauth_flow() + + # delegated keystone token exists + headers = {'X-Subject-Token': self.keystone_token_id, + 'X-Auth-Token': self.keystone_token_id} + r = self.get('/auth/tokens', headers=headers) + self.assertValidTokenResponse(r, self.user) + + user = {'password': uuid.uuid4().hex} + r = self.patch('/users/%(user_id)s' % { + 'user_id': self.user['id']}, + body={'user': user}) + + headers = {'X-Subject-Token': self.keystone_token_id, + 'X-Auth-Token': self.keystone_token_id} + self.admin_request(path='/auth/tokens', headers=headers, + method='GET', expected_status=404) + + def test_deleting_project_also_invalidates_tokens(self): + self.test_oauth_flow() + + # delegated keystone token exists + headers = {'X-Subject-Token': self.keystone_token_id, + 'X-Auth-Token': self.keystone_token_id} + r = self.get('/auth/tokens', headers=headers) + self.assertValidTokenResponse(r, self.user) + + r = self.delete('/projects/%(project_id)s' % { + 'project_id': self.project_id}) + + headers = {'X-Subject-Token': self.keystone_token_id, + 'X-Auth-Token': self.keystone_token_id} + self.admin_request(path='/auth/tokens', headers=headers, + method='GET', expected_status=404) + + def test_token_chaining_is_not_allowed(self): + self.test_oauth_flow() + + # attempt to re-authenticate (token chain) with the given token + path = '/v3/auth/tokens/' + auth_data = self.build_authentication_request( + token=self.keystone_token_id) + + self.admin_request( + path=path, + body=auth_data, + token=self.keystone_token_id, + method='POST', + expected_status=403) + + def test_delete_keystone_tokens_by_consumer_id(self): + self.test_oauth_flow() + self.token_provider_api._persistence.get_token(self.keystone_token_id) + self.token_provider_api._persistence.delete_tokens( + self.user_id, + consumer_id=self.consumer['key']) + self.assertRaises(exception.TokenNotFound, + self.token_provider_api._persistence.get_token, + self.keystone_token_id) + + def _create_trust_get_token(self): + ref = self.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.user_id, + project_id=self.project_id, + impersonation=True, + expires=dict(minutes=1), + role_ids=[self.role_id]) + del ref['id'] + + r = self.post('/OS-TRUST/trusts', body={'trust': ref}) + trust = self.assertValidTrustResponse(r) + + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + trust_id=trust['id']) + + return self.get_requested_token(auth_data) + + def _approve_request_token_url(self): + consumer = self._create_single_consumer() + consumer_id = consumer['id'] + consumer_secret = consumer['secret'] + self.consumer = {'key': consumer_id, 'secret': consumer_secret} + self.assertIsNotNone(self.consumer['secret']) + + url, headers = self._create_request_token(self.consumer, + self.project_id) + content = self.post( + url, headers=headers, + response_content_type='application/x-www-urlformencoded') + credentials = urllib.parse.parse_qs(content.result) + request_key = credentials['oauth_token'][0] + request_secret = credentials['oauth_token_secret'][0] + self.request_token = oauth1.Token(request_key, request_secret) + self.assertIsNotNone(self.request_token.key) + + url = self._authorize_request_token(request_key) + + return url + + def test_oauth_token_cannot_create_new_trust(self): + self.test_oauth_flow() + ref = self.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.user_id, + project_id=self.project_id, + impersonation=True, + expires=dict(minutes=1), + role_ids=[self.role_id]) + del ref['id'] + + self.post('/OS-TRUST/trusts', + body={'trust': ref}, + token=self.keystone_token_id, + expected_status=403) + + def test_oauth_token_cannot_authorize_request_token(self): + self.test_oauth_flow() + url = self._approve_request_token_url() + body = {'roles': [{'id': self.role_id}]} + self.put(url, body=body, token=self.keystone_token_id, + expected_status=403) + + def test_oauth_token_cannot_list_request_tokens(self): + self._set_policy({"identity:list_access_tokens": [], + "identity:create_consumer": [], + "identity:authorize_request_token": []}) + self.test_oauth_flow() + url = '/users/%s/OS-OAUTH1/access_tokens' % self.user_id + self.get(url, token=self.keystone_token_id, + expected_status=403) + + def _set_policy(self, new_policy): + self.tempfile = self.useFixture(temporaryfile.SecureTempFile()) + self.tmpfilename = self.tempfile.file_name + self.config_fixture.config(group='oslo_policy', + policy_file=self.tmpfilename) + with open(self.tmpfilename, "w") as policyfile: + policyfile.write(jsonutils.dumps(new_policy)) + + def test_trust_token_cannot_authorize_request_token(self): + trust_token = self._create_trust_get_token() + url = self._approve_request_token_url() + body = {'roles': [{'id': self.role_id}]} + self.put(url, body=body, token=trust_token, expected_status=403) + + def test_trust_token_cannot_list_request_tokens(self): + self._set_policy({"identity:list_access_tokens": [], + "identity:create_trust": []}) + trust_token = self._create_trust_get_token() + url = '/users/%s/OS-OAUTH1/access_tokens' % self.user_id + self.get(url, token=trust_token, expected_status=403) + + +class MaliciousOAuth1Tests(OAuth1Tests): + + def test_bad_consumer_secret(self): + consumer = self._create_single_consumer() + consumer_id = consumer['id'] + consumer = {'key': consumer_id, 'secret': uuid.uuid4().hex} + url, headers = self._create_request_token(consumer, self.project_id) + self.post(url, headers=headers, expected_status=401) + + def test_bad_request_token_key(self): + consumer = self._create_single_consumer() + consumer_id = consumer['id'] + consumer_secret = consumer['secret'] + consumer = {'key': consumer_id, 'secret': consumer_secret} + url, headers = self._create_request_token(consumer, self.project_id) + self.post( + url, headers=headers, + response_content_type='application/x-www-urlformencoded') + url = self._authorize_request_token(uuid.uuid4().hex) + body = {'roles': [{'id': self.role_id}]} + self.put(url, body=body, expected_status=404) + + def test_bad_consumer_id(self): + consumer = self._create_single_consumer() + consumer_id = uuid.uuid4().hex + consumer_secret = consumer['secret'] + consumer = {'key': consumer_id, 'secret': consumer_secret} + url, headers = self._create_request_token(consumer, self.project_id) + self.post(url, headers=headers, expected_status=404) + + def test_bad_requested_project_id(self): + consumer = self._create_single_consumer() + consumer_id = consumer['id'] + consumer_secret = consumer['secret'] + consumer = {'key': consumer_id, 'secret': consumer_secret} + project_id = uuid.uuid4().hex + url, headers = self._create_request_token(consumer, project_id) + self.post(url, headers=headers, expected_status=404) + + def test_bad_verifier(self): + consumer = self._create_single_consumer() + consumer_id = consumer['id'] + consumer_secret = consumer['secret'] + consumer = {'key': consumer_id, 'secret': consumer_secret} + + url, headers = self._create_request_token(consumer, self.project_id) + content = self.post( + url, headers=headers, + response_content_type='application/x-www-urlformencoded') + credentials = urllib.parse.parse_qs(content.result) + request_key = credentials['oauth_token'][0] + request_secret = credentials['oauth_token_secret'][0] + request_token = oauth1.Token(request_key, request_secret) + + url = self._authorize_request_token(request_key) + body = {'roles': [{'id': self.role_id}]} + resp = self.put(url, body=body, expected_status=200) + verifier = resp.result['token']['oauth_verifier'] + self.assertIsNotNone(verifier) + + request_token.set_verifier(uuid.uuid4().hex) + url, headers = self._create_access_token(consumer, request_token) + self.post(url, headers=headers, expected_status=401) + + def test_bad_authorizing_roles(self): + consumer = self._create_single_consumer() + consumer_id = consumer['id'] + consumer_secret = consumer['secret'] + consumer = {'key': consumer_id, 'secret': consumer_secret} + + url, headers = self._create_request_token(consumer, self.project_id) + content = self.post( + url, headers=headers, + response_content_type='application/x-www-urlformencoded') + credentials = urllib.parse.parse_qs(content.result) + request_key = credentials['oauth_token'][0] + + self.assignment_api.remove_role_from_user_and_project( + self.user_id, self.project_id, self.role_id) + url = self._authorize_request_token(request_key) + body = {'roles': [{'id': self.role_id}]} + self.admin_request(path=url, method='PUT', + body=body, expected_status=404) + + def test_expired_authorizing_request_token(self): + self.config_fixture.config(group='oauth1', request_token_duration=-1) + + consumer = self._create_single_consumer() + consumer_id = consumer['id'] + consumer_secret = consumer['secret'] + self.consumer = {'key': consumer_id, 'secret': consumer_secret} + self.assertIsNotNone(self.consumer['key']) + + url, headers = self._create_request_token(self.consumer, + self.project_id) + content = self.post( + url, headers=headers, + response_content_type='application/x-www-urlformencoded') + credentials = urllib.parse.parse_qs(content.result) + request_key = credentials['oauth_token'][0] + request_secret = credentials['oauth_token_secret'][0] + self.request_token = oauth1.Token(request_key, request_secret) + self.assertIsNotNone(self.request_token.key) + + url = self._authorize_request_token(request_key) + body = {'roles': [{'id': self.role_id}]} + self.put(url, body=body, expected_status=401) + + def test_expired_creating_keystone_token(self): + self.config_fixture.config(group='oauth1', access_token_duration=-1) + consumer = self._create_single_consumer() + consumer_id = consumer['id'] + consumer_secret = consumer['secret'] + self.consumer = {'key': consumer_id, 'secret': consumer_secret} + self.assertIsNotNone(self.consumer['key']) + + url, headers = self._create_request_token(self.consumer, + self.project_id) + content = self.post( + url, headers=headers, + response_content_type='application/x-www-urlformencoded') + credentials = urllib.parse.parse_qs(content.result) + request_key = credentials['oauth_token'][0] + request_secret = credentials['oauth_token_secret'][0] + self.request_token = oauth1.Token(request_key, request_secret) + self.assertIsNotNone(self.request_token.key) + + url = self._authorize_request_token(request_key) + body = {'roles': [{'id': self.role_id}]} + resp = self.put(url, body=body, expected_status=200) + self.verifier = resp.result['token']['oauth_verifier'] + + self.request_token.set_verifier(self.verifier) + url, headers = self._create_access_token(self.consumer, + self.request_token) + content = self.post( + url, headers=headers, + response_content_type='application/x-www-urlformencoded') + credentials = urllib.parse.parse_qs(content.result) + access_key = credentials['oauth_token'][0] + access_secret = credentials['oauth_token_secret'][0] + self.access_token = oauth1.Token(access_key, access_secret) + self.assertIsNotNone(self.access_token.key) + + url, headers, body = self._get_oauth_token(self.consumer, + self.access_token) + self.post(url, headers=headers, body=body, expected_status=401) + + def test_missing_oauth_headers(self): + endpoint = '/OS-OAUTH1/request_token' + client = oauth1.Client(uuid.uuid4().hex, + client_secret=uuid.uuid4().hex, + signature_method=oauth1.SIG_HMAC, + callback_uri="oob") + headers = {'requested_project_id': uuid.uuid4().hex} + _url, headers, _body = client.sign(self.base_url + endpoint, + http_method='POST', + headers=headers) + + # NOTE(stevemar): To simulate this error, we remove the Authorization + # header from the post request. + del headers['Authorization'] + self.post(endpoint, headers=headers, expected_status=500) + + +class OAuthNotificationTests(OAuth1Tests, + test_notifications.BaseNotificationTest): + + def test_create_consumer(self): + consumer_ref = self._create_single_consumer() + self._assert_notify_sent(consumer_ref['id'], + test_notifications.CREATED_OPERATION, + 'OS-OAUTH1:consumer') + self._assert_last_audit(consumer_ref['id'], + test_notifications.CREATED_OPERATION, + 'OS-OAUTH1:consumer', + cadftaxonomy.SECURITY_ACCOUNT) + + def test_update_consumer(self): + consumer_ref = self._create_single_consumer() + update_ref = {'consumer': {'description': uuid.uuid4().hex}} + self.oauth_api.update_consumer(consumer_ref['id'], update_ref) + self._assert_notify_sent(consumer_ref['id'], + test_notifications.UPDATED_OPERATION, + 'OS-OAUTH1:consumer') + self._assert_last_audit(consumer_ref['id'], + test_notifications.UPDATED_OPERATION, + 'OS-OAUTH1:consumer', + cadftaxonomy.SECURITY_ACCOUNT) + + def test_delete_consumer(self): + consumer_ref = self._create_single_consumer() + self.oauth_api.delete_consumer(consumer_ref['id']) + self._assert_notify_sent(consumer_ref['id'], + test_notifications.DELETED_OPERATION, + 'OS-OAUTH1:consumer') + self._assert_last_audit(consumer_ref['id'], + test_notifications.DELETED_OPERATION, + 'OS-OAUTH1:consumer', + cadftaxonomy.SECURITY_ACCOUNT) + + def test_oauth_flow_notifications(self): + """Test to ensure notifications are sent for oauth tokens + + This test is very similar to test_oauth_flow, however + there are additional checks in this test for ensuring that + notifications for request token creation, and access token + creation/deletion are emitted. + """ + + consumer = self._create_single_consumer() + consumer_id = consumer['id'] + consumer_secret = consumer['secret'] + self.consumer = {'key': consumer_id, 'secret': consumer_secret} + self.assertIsNotNone(self.consumer['secret']) + + url, headers = self._create_request_token(self.consumer, + self.project_id) + content = self.post( + url, headers=headers, + response_content_type='application/x-www-urlformencoded') + credentials = urllib.parse.parse_qs(content.result) + request_key = credentials['oauth_token'][0] + request_secret = credentials['oauth_token_secret'][0] + self.request_token = oauth1.Token(request_key, request_secret) + self.assertIsNotNone(self.request_token.key) + + # Test to ensure the create request token notification is sent + self._assert_notify_sent(request_key, + test_notifications.CREATED_OPERATION, + 'OS-OAUTH1:request_token') + self._assert_last_audit(request_key, + test_notifications.CREATED_OPERATION, + 'OS-OAUTH1:request_token', + cadftaxonomy.SECURITY_CREDENTIAL) + + url = self._authorize_request_token(request_key) + body = {'roles': [{'id': self.role_id}]} + resp = self.put(url, body=body, expected_status=200) + self.verifier = resp.result['token']['oauth_verifier'] + self.assertTrue(all(i in core.VERIFIER_CHARS for i in self.verifier)) + self.assertEqual(8, len(self.verifier)) + + self.request_token.set_verifier(self.verifier) + url, headers = self._create_access_token(self.consumer, + self.request_token) + content = self.post( + url, headers=headers, + response_content_type='application/x-www-urlformencoded') + credentials = urllib.parse.parse_qs(content.result) + access_key = credentials['oauth_token'][0] + access_secret = credentials['oauth_token_secret'][0] + self.access_token = oauth1.Token(access_key, access_secret) + self.assertIsNotNone(self.access_token.key) + + # Test to ensure the create access token notification is sent + self._assert_notify_sent(access_key, + test_notifications.CREATED_OPERATION, + 'OS-OAUTH1:access_token') + self._assert_last_audit(access_key, + test_notifications.CREATED_OPERATION, + 'OS-OAUTH1:access_token', + cadftaxonomy.SECURITY_CREDENTIAL) + + resp = self.delete('/users/%(user)s/OS-OAUTH1/access_tokens/%(auth)s' + % {'user': self.user_id, + 'auth': self.access_token.key}) + self.assertResponseStatus(resp, 204) + + # Test to ensure the delete access token notification is sent + self._assert_notify_sent(access_key, + test_notifications.DELETED_OPERATION, + 'OS-OAUTH1:access_token') + self._assert_last_audit(access_key, + test_notifications.DELETED_OPERATION, + 'OS-OAUTH1:access_token', + cadftaxonomy.SECURITY_CREDENTIAL) + + +class OAuthCADFNotificationTests(OAuthNotificationTests): + + def setUp(self): + """Repeat the tests for CADF notifications """ + super(OAuthCADFNotificationTests, self).setUp() + self.config_fixture.config(notification_format='cadf') + + +class JsonHomeTests(OAuth1Tests, test_v3.JsonHomeTestMixin): + JSON_HOME_DATA = { + 'http://docs.openstack.org/api/openstack-identity/3/ext/OS-OAUTH1/1.0/' + 'rel/consumers': { + 'href': '/OS-OAUTH1/consumers', + }, + } diff --git a/keystone-moon/keystone/tests/unit/test_v3_os_revoke.py b/keystone-moon/keystone/tests/unit/test_v3_os_revoke.py new file mode 100644 index 00000000..5710d973 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_v3_os_revoke.py @@ -0,0 +1,135 @@ +# 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 datetime +import uuid + +from oslo_utils import timeutils +import six +from testtools import matchers + +from keystone.contrib.revoke import model +from keystone.tests.unit import test_v3 +from keystone.token import provider + + +def _future_time_string(): + expire_delta = datetime.timedelta(seconds=1000) + future_time = timeutils.utcnow() + expire_delta + return timeutils.isotime(future_time) + + +class OSRevokeTests(test_v3.RestfulTestCase, test_v3.JsonHomeTestMixin): + EXTENSION_NAME = 'revoke' + EXTENSION_TO_ADD = 'revoke_extension' + + JSON_HOME_DATA = { + 'http://docs.openstack.org/api/openstack-identity/3/ext/OS-REVOKE/1.0/' + 'rel/events': { + 'href': '/OS-REVOKE/events', + }, + } + + def test_get_empty_list(self): + resp = self.get('/OS-REVOKE/events') + self.assertEqual([], resp.json_body['events']) + + def _blank_event(self): + return {} + + # The two values will be the same with the exception of + # 'issued_before' which is set when the event is recorded. + def assertReportedEventMatchesRecorded(self, event, sample, before_time): + after_time = timeutils.utcnow() + event_issued_before = timeutils.normalize_time( + timeutils.parse_isotime(event['issued_before'])) + self.assertTrue( + before_time <= event_issued_before, + 'invalid event issued_before time; %s is not later than %s.' % ( + timeutils.isotime(event_issued_before, subsecond=True), + timeutils.isotime(before_time, subsecond=True))) + self.assertTrue( + event_issued_before <= after_time, + 'invalid event issued_before time; %s is not earlier than %s.' % ( + timeutils.isotime(event_issued_before, subsecond=True), + timeutils.isotime(after_time, subsecond=True))) + del (event['issued_before']) + self.assertEqual(sample, event) + + def test_revoked_list_self_url(self): + revoked_list_url = '/OS-REVOKE/events' + resp = self.get(revoked_list_url) + links = resp.json_body['links'] + self.assertThat(links['self'], matchers.EndsWith(revoked_list_url)) + + def test_revoked_token_in_list(self): + user_id = uuid.uuid4().hex + expires_at = provider.default_expire_time() + sample = self._blank_event() + sample['user_id'] = six.text_type(user_id) + sample['expires_at'] = six.text_type(timeutils.isotime(expires_at)) + before_time = timeutils.utcnow() + self.revoke_api.revoke_by_expiration(user_id, expires_at) + resp = self.get('/OS-REVOKE/events') + events = resp.json_body['events'] + self.assertEqual(1, len(events)) + self.assertReportedEventMatchesRecorded(events[0], sample, before_time) + + def test_disabled_project_in_list(self): + project_id = uuid.uuid4().hex + sample = dict() + sample['project_id'] = six.text_type(project_id) + before_time = timeutils.utcnow() + self.revoke_api.revoke( + model.RevokeEvent(project_id=project_id)) + + resp = self.get('/OS-REVOKE/events') + events = resp.json_body['events'] + self.assertEqual(1, len(events)) + self.assertReportedEventMatchesRecorded(events[0], sample, before_time) + + def test_disabled_domain_in_list(self): + domain_id = uuid.uuid4().hex + sample = dict() + sample['domain_id'] = six.text_type(domain_id) + before_time = timeutils.utcnow() + self.revoke_api.revoke( + model.RevokeEvent(domain_id=domain_id)) + + resp = self.get('/OS-REVOKE/events') + events = resp.json_body['events'] + self.assertEqual(1, len(events)) + self.assertReportedEventMatchesRecorded(events[0], sample, before_time) + + def test_list_since_invalid(self): + self.get('/OS-REVOKE/events?since=blah', expected_status=400) + + def test_list_since_valid(self): + resp = self.get('/OS-REVOKE/events?since=2013-02-27T18:30:59.999999Z') + events = resp.json_body['events'] + self.assertEqual(0, len(events)) + + def test_since_future_time_no_events(self): + domain_id = uuid.uuid4().hex + sample = dict() + sample['domain_id'] = six.text_type(domain_id) + + self.revoke_api.revoke( + model.RevokeEvent(domain_id=domain_id)) + + resp = self.get('/OS-REVOKE/events') + events = resp.json_body['events'] + self.assertEqual(1, len(events)) + + resp = self.get('/OS-REVOKE/events?since=%s' % _future_time_string()) + events = resp.json_body['events'] + self.assertEqual([], events) diff --git a/keystone-moon/keystone/tests/unit/test_v3_policy.py b/keystone-moon/keystone/tests/unit/test_v3_policy.py new file mode 100644 index 00000000..538fc565 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_v3_policy.py @@ -0,0 +1,68 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 keystone.tests.unit import test_v3 + + +class PolicyTestCase(test_v3.RestfulTestCase): + """Test policy CRUD.""" + + def setUp(self): + super(PolicyTestCase, self).setUp() + self.policy_id = uuid.uuid4().hex + self.policy = self.new_policy_ref() + self.policy['id'] = self.policy_id + self.policy_api.create_policy( + self.policy_id, + self.policy.copy()) + + # policy crud tests + + def test_create_policy(self): + """Call ``POST /policies``.""" + ref = self.new_policy_ref() + r = self.post( + '/policies', + body={'policy': ref}) + return self.assertValidPolicyResponse(r, ref) + + def test_list_policies(self): + """Call ``GET /policies``.""" + r = self.get('/policies') + self.assertValidPolicyListResponse(r, ref=self.policy) + + def test_get_policy(self): + """Call ``GET /policies/{policy_id}``.""" + r = self.get( + '/policies/%(policy_id)s' % { + 'policy_id': self.policy_id}) + self.assertValidPolicyResponse(r, self.policy) + + def test_update_policy(self): + """Call ``PATCH /policies/{policy_id}``.""" + policy = self.new_policy_ref() + policy['id'] = self.policy_id + r = self.patch( + '/policies/%(policy_id)s' % { + 'policy_id': self.policy_id}, + body={'policy': policy}) + self.assertValidPolicyResponse(r, policy) + + def test_delete_policy(self): + """Call ``DELETE /policies/{policy_id}``.""" + self.delete( + '/policies/%(policy_id)s' % { + 'policy_id': self.policy_id}) diff --git a/keystone-moon/keystone/tests/unit/test_v3_protection.py b/keystone-moon/keystone/tests/unit/test_v3_protection.py new file mode 100644 index 00000000..2b2c96d1 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_v3_protection.py @@ -0,0 +1,1170 @@ +# Copyright 2012 OpenStack Foundation +# Copyright 2013 IBM Corp. +# +# 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 oslo_config import cfg +from oslo_serialization import jsonutils + +from keystone import exception +from keystone.policy.backends import rules +from keystone.tests import unit as tests +from keystone.tests.unit.ksfixtures import temporaryfile +from keystone.tests.unit import test_v3 + + +CONF = cfg.CONF +DEFAULT_DOMAIN_ID = CONF.identity.default_domain_id + + +class IdentityTestProtectedCase(test_v3.RestfulTestCase): + """Test policy enforcement on the v3 Identity API.""" + + def setUp(self): + """Setup for Identity Protection Test Cases. + + As well as the usual housekeeping, create a set of domains, + users, roles and projects for the subsequent tests: + + - Three domains: A,B & C. C is disabled. + - DomainA has user1, DomainB has user2 and user3 + - DomainA has group1 and group2, DomainB has group3 + - User1 has two roles on DomainA + - User2 has one role on DomainA + + Remember that there will also be a fourth domain in existence, + the default domain. + + """ + # Ensure that test_v3.RestfulTestCase doesn't load its own + # sample data, which would make checking the results of our + # tests harder + super(IdentityTestProtectedCase, self).setUp() + self.tempfile = self.useFixture(temporaryfile.SecureTempFile()) + self.tmpfilename = self.tempfile.file_name + self.config_fixture.config(group='oslo_policy', + policy_file=self.tmpfilename) + + # A default auth request we can use - un-scoped user token + self.auth = self.build_authentication_request( + user_id=self.user1['id'], + password=self.user1['password']) + + def load_sample_data(self): + self._populate_default_domain() + # Start by creating a couple of domains + self.domainA = self.new_domain_ref() + self.resource_api.create_domain(self.domainA['id'], self.domainA) + self.domainB = self.new_domain_ref() + self.resource_api.create_domain(self.domainB['id'], self.domainB) + self.domainC = self.new_domain_ref() + self.domainC['enabled'] = False + self.resource_api.create_domain(self.domainC['id'], self.domainC) + + # Now create some users, one in domainA and two of them in domainB + self.user1 = self.new_user_ref(domain_id=self.domainA['id']) + password = uuid.uuid4().hex + self.user1['password'] = password + self.user1 = self.identity_api.create_user(self.user1) + self.user1['password'] = password + + self.user2 = self.new_user_ref(domain_id=self.domainB['id']) + password = uuid.uuid4().hex + self.user2['password'] = password + self.user2 = self.identity_api.create_user(self.user2) + self.user2['password'] = password + + self.user3 = self.new_user_ref(domain_id=self.domainB['id']) + password = uuid.uuid4().hex + self.user3['password'] = password + self.user3 = self.identity_api.create_user(self.user3) + self.user3['password'] = password + + self.group1 = self.new_group_ref(domain_id=self.domainA['id']) + self.group1 = self.identity_api.create_group(self.group1) + + self.group2 = self.new_group_ref(domain_id=self.domainA['id']) + self.group2 = self.identity_api.create_group(self.group2) + + self.group3 = self.new_group_ref(domain_id=self.domainB['id']) + self.group3 = self.identity_api.create_group(self.group3) + + self.role = self.new_role_ref() + self.role_api.create_role(self.role['id'], self.role) + self.role1 = self.new_role_ref() + self.role_api.create_role(self.role1['id'], self.role1) + self.assignment_api.create_grant(self.role['id'], + user_id=self.user1['id'], + domain_id=self.domainA['id']) + self.assignment_api.create_grant(self.role['id'], + user_id=self.user2['id'], + domain_id=self.domainA['id']) + self.assignment_api.create_grant(self.role1['id'], + user_id=self.user1['id'], + domain_id=self.domainA['id']) + + def _get_id_list_from_ref_list(self, ref_list): + result_list = [] + for x in ref_list: + result_list.append(x['id']) + return result_list + + def _set_policy(self, new_policy): + with open(self.tmpfilename, "w") as policyfile: + policyfile.write(jsonutils.dumps(new_policy)) + + def test_list_users_unprotected(self): + """GET /users (unprotected) + + Test Plan: + + - Update policy so api is unprotected + - Use an un-scoped token to make sure we can get back all + the users independent of domain + + """ + self._set_policy({"identity:list_users": []}) + r = self.get('/users', auth=self.auth) + id_list = self._get_id_list_from_ref_list(r.result.get('users')) + self.assertIn(self.user1['id'], id_list) + self.assertIn(self.user2['id'], id_list) + self.assertIn(self.user3['id'], id_list) + + def test_list_users_filtered_by_domain(self): + """GET /users?domain_id=mydomain (filtered) + + Test Plan: + + - Update policy so api is unprotected + - Use an un-scoped token to make sure we can filter the + users by domainB, getting back the 2 users in that domain + + """ + self._set_policy({"identity:list_users": []}) + url_by_name = '/users?domain_id=%s' % self.domainB['id'] + r = self.get(url_by_name, auth=self.auth) + # We should get back two users, those in DomainB + id_list = self._get_id_list_from_ref_list(r.result.get('users')) + self.assertIn(self.user2['id'], id_list) + self.assertIn(self.user3['id'], id_list) + + def test_get_user_protected_match_id(self): + """GET /users/{id} (match payload) + + Test Plan: + + - Update policy to protect api by user_id + - List users with user_id of user1 as filter, to check that + this will correctly match user_id in the flattened + payload + + """ + # TODO(henry-nash, ayoung): It would be good to expand this + # test for further test flattening, e.g. protect on, say, an + # attribute of an object being created + new_policy = {"identity:get_user": [["user_id:%(user_id)s"]]} + self._set_policy(new_policy) + url_by_name = '/users/%s' % self.user1['id'] + r = self.get(url_by_name, auth=self.auth) + self.assertEqual(self.user1['id'], r.result['user']['id']) + + def test_get_user_protected_match_target(self): + """GET /users/{id} (match target) + + Test Plan: + + - Update policy to protect api by domain_id + - Try and read a user who is in DomainB with a token scoped + to Domain A - this should fail + - Retry this for a user who is in Domain A, which should succeed. + - Finally, try getting a user that does not exist, which should + still return UserNotFound + + """ + new_policy = {'identity:get_user': + [["domain_id:%(target.user.domain_id)s"]]} + self._set_policy(new_policy) + self.auth = self.build_authentication_request( + user_id=self.user1['id'], + password=self.user1['password'], + domain_id=self.domainA['id']) + url_by_name = '/users/%s' % self.user2['id'] + r = self.get(url_by_name, auth=self.auth, + expected_status=exception.ForbiddenAction.code) + + url_by_name = '/users/%s' % self.user1['id'] + r = self.get(url_by_name, auth=self.auth) + self.assertEqual(self.user1['id'], r.result['user']['id']) + + url_by_name = '/users/%s' % uuid.uuid4().hex + r = self.get(url_by_name, auth=self.auth, + expected_status=exception.UserNotFound.code) + + def test_revoke_grant_protected_match_target(self): + """DELETE /domains/{id}/users/{id}/roles/{id} (match target) + + Test Plan: + + - Update policy to protect api by domain_id of entities in + the grant + - Try and delete the existing grant that has a user who is + from a different domain - this should fail. + - Retry this for a user who is in Domain A, which should succeed. + + """ + new_policy = {'identity:revoke_grant': + [["domain_id:%(target.user.domain_id)s"]]} + self._set_policy(new_policy) + collection_url = ( + '/domains/%(domain_id)s/users/%(user_id)s/roles' % { + 'domain_id': self.domainA['id'], + 'user_id': self.user2['id']}) + member_url = '%(collection_url)s/%(role_id)s' % { + 'collection_url': collection_url, + 'role_id': self.role['id']} + + self.auth = self.build_authentication_request( + user_id=self.user1['id'], + password=self.user1['password'], + domain_id=self.domainA['id']) + self.delete(member_url, auth=self.auth, + expected_status=exception.ForbiddenAction.code) + + collection_url = ( + '/domains/%(domain_id)s/users/%(user_id)s/roles' % { + 'domain_id': self.domainA['id'], + 'user_id': self.user1['id']}) + member_url = '%(collection_url)s/%(role_id)s' % { + 'collection_url': collection_url, + 'role_id': self.role1['id']} + self.delete(member_url, auth=self.auth) + + def test_list_users_protected_by_domain(self): + """GET /users?domain_id=mydomain (protected) + + Test Plan: + + - Update policy to protect api by domain_id + - List groups using a token scoped to domainA with a filter + specifying domainA - we should only get back the one user + that is in domainA. + - Try and read the users from domainB - this should fail since + we don't have a token scoped for domainB + + """ + new_policy = {"identity:list_users": ["domain_id:%(domain_id)s"]} + self._set_policy(new_policy) + self.auth = self.build_authentication_request( + user_id=self.user1['id'], + password=self.user1['password'], + domain_id=self.domainA['id']) + url_by_name = '/users?domain_id=%s' % self.domainA['id'] + r = self.get(url_by_name, auth=self.auth) + # We should only get back one user, the one in DomainA + id_list = self._get_id_list_from_ref_list(r.result.get('users')) + self.assertEqual(1, len(id_list)) + self.assertIn(self.user1['id'], id_list) + + # Now try for domainB, which should fail + url_by_name = '/users?domain_id=%s' % self.domainB['id'] + r = self.get(url_by_name, auth=self.auth, + expected_status=exception.ForbiddenAction.code) + + def test_list_groups_protected_by_domain(self): + """GET /groups?domain_id=mydomain (protected) + + Test Plan: + + - Update policy to protect api by domain_id + - List groups using a token scoped to domainA and make sure + we only get back the two groups that are in domainA + - Try and read the groups from domainB - this should fail since + we don't have a token scoped for domainB + + """ + new_policy = {"identity:list_groups": ["domain_id:%(domain_id)s"]} + self._set_policy(new_policy) + self.auth = self.build_authentication_request( + user_id=self.user1['id'], + password=self.user1['password'], + domain_id=self.domainA['id']) + url_by_name = '/groups?domain_id=%s' % self.domainA['id'] + r = self.get(url_by_name, auth=self.auth) + # We should only get back two groups, the ones in DomainA + id_list = self._get_id_list_from_ref_list(r.result.get('groups')) + self.assertEqual(2, len(id_list)) + self.assertIn(self.group1['id'], id_list) + self.assertIn(self.group2['id'], id_list) + + # Now try for domainB, which should fail + url_by_name = '/groups?domain_id=%s' % self.domainB['id'] + r = self.get(url_by_name, auth=self.auth, + expected_status=exception.ForbiddenAction.code) + + def test_list_groups_protected_by_domain_and_filtered(self): + """GET /groups?domain_id=mydomain&name=myname (protected) + + Test Plan: + + - Update policy to protect api by domain_id + - List groups using a token scoped to domainA with a filter + specifying both domainA and the name of group. + - We should only get back the group in domainA that matches + the name + + """ + new_policy = {"identity:list_groups": ["domain_id:%(domain_id)s"]} + self._set_policy(new_policy) + self.auth = self.build_authentication_request( + user_id=self.user1['id'], + password=self.user1['password'], + domain_id=self.domainA['id']) + url_by_name = '/groups?domain_id=%s&name=%s' % ( + self.domainA['id'], self.group2['name']) + r = self.get(url_by_name, auth=self.auth) + # We should only get back one user, the one in DomainA that matches + # the name supplied + id_list = self._get_id_list_from_ref_list(r.result.get('groups')) + self.assertEqual(1, len(id_list)) + self.assertIn(self.group2['id'], id_list) + + +class IdentityTestPolicySample(test_v3.RestfulTestCase): + """Test policy enforcement of the policy.json file.""" + + def load_sample_data(self): + self._populate_default_domain() + + self.just_a_user = self.new_user_ref( + domain_id=CONF.identity.default_domain_id) + password = uuid.uuid4().hex + self.just_a_user['password'] = password + self.just_a_user = self.identity_api.create_user(self.just_a_user) + self.just_a_user['password'] = password + + self.another_user = self.new_user_ref( + domain_id=CONF.identity.default_domain_id) + password = uuid.uuid4().hex + self.another_user['password'] = password + self.another_user = self.identity_api.create_user(self.another_user) + self.another_user['password'] = password + + self.admin_user = self.new_user_ref( + domain_id=CONF.identity.default_domain_id) + password = uuid.uuid4().hex + self.admin_user['password'] = password + self.admin_user = self.identity_api.create_user(self.admin_user) + self.admin_user['password'] = password + + self.role = self.new_role_ref() + self.role_api.create_role(self.role['id'], self.role) + self.admin_role = {'id': uuid.uuid4().hex, 'name': 'admin'} + self.role_api.create_role(self.admin_role['id'], self.admin_role) + + # Create and assign roles to the project + self.project = self.new_project_ref( + domain_id=CONF.identity.default_domain_id) + self.resource_api.create_project(self.project['id'], self.project) + self.assignment_api.create_grant(self.role['id'], + user_id=self.just_a_user['id'], + project_id=self.project['id']) + self.assignment_api.create_grant(self.role['id'], + user_id=self.another_user['id'], + project_id=self.project['id']) + self.assignment_api.create_grant(self.admin_role['id'], + user_id=self.admin_user['id'], + project_id=self.project['id']) + + def test_user_validate_same_token(self): + # Given a non-admin user token, the token can be used to validate + # itself. + # This is GET /v3/auth/tokens, with X-Auth-Token == X-Subject-Token + # FIXME(blk-u): This test fails, a user can't validate their own token, + # see bug 1421825. + + auth = self.build_authentication_request( + user_id=self.just_a_user['id'], + password=self.just_a_user['password']) + token = self.get_requested_token(auth) + + # FIXME(blk-u): remove expected_status=403. + self.get('/auth/tokens', token=token, + headers={'X-Subject-Token': token}, expected_status=403) + + def test_user_validate_user_token(self): + # A user can validate one of their own tokens. + # This is GET /v3/auth/tokens + # FIXME(blk-u): This test fails, a user can't validate their own token, + # see bug 1421825. + + auth = self.build_authentication_request( + user_id=self.just_a_user['id'], + password=self.just_a_user['password']) + token1 = self.get_requested_token(auth) + token2 = self.get_requested_token(auth) + + # FIXME(blk-u): remove expected_status=403. + self.get('/auth/tokens', token=token1, + headers={'X-Subject-Token': token2}, expected_status=403) + + def test_user_validate_other_user_token_rejected(self): + # A user cannot validate another user's token. + # This is GET /v3/auth/tokens + + user1_auth = self.build_authentication_request( + user_id=self.just_a_user['id'], + password=self.just_a_user['password']) + user1_token = self.get_requested_token(user1_auth) + + user2_auth = self.build_authentication_request( + user_id=self.another_user['id'], + password=self.another_user['password']) + user2_token = self.get_requested_token(user2_auth) + + self.get('/auth/tokens', token=user1_token, + headers={'X-Subject-Token': user2_token}, expected_status=403) + + def test_admin_validate_user_token(self): + # An admin can validate a user's token. + # This is GET /v3/auth/tokens + + admin_auth = self.build_authentication_request( + user_id=self.admin_user['id'], + password=self.admin_user['password'], + project_id=self.project['id']) + admin_token = self.get_requested_token(admin_auth) + + user_auth = self.build_authentication_request( + user_id=self.just_a_user['id'], + password=self.just_a_user['password']) + user_token = self.get_requested_token(user_auth) + + self.get('/auth/tokens', token=admin_token, + headers={'X-Subject-Token': user_token}) + + def test_user_check_same_token(self): + # Given a non-admin user token, the token can be used to check + # itself. + # This is HEAD /v3/auth/tokens, with X-Auth-Token == X-Subject-Token + # FIXME(blk-u): This test fails, a user can't check the same token, + # see bug 1421825. + + auth = self.build_authentication_request( + user_id=self.just_a_user['id'], + password=self.just_a_user['password']) + token = self.get_requested_token(auth) + + # FIXME(blk-u): change to expected_status=200 + self.head('/auth/tokens', token=token, + headers={'X-Subject-Token': token}, expected_status=403) + + def test_user_check_user_token(self): + # A user can check one of their own tokens. + # This is HEAD /v3/auth/tokens + # FIXME(blk-u): This test fails, a user can't check the same token, + # see bug 1421825. + + auth = self.build_authentication_request( + user_id=self.just_a_user['id'], + password=self.just_a_user['password']) + token1 = self.get_requested_token(auth) + token2 = self.get_requested_token(auth) + + # FIXME(blk-u): change to expected_status=200 + self.head('/auth/tokens', token=token1, + headers={'X-Subject-Token': token2}, expected_status=403) + + def test_user_check_other_user_token_rejected(self): + # A user cannot check another user's token. + # This is HEAD /v3/auth/tokens + + user1_auth = self.build_authentication_request( + user_id=self.just_a_user['id'], + password=self.just_a_user['password']) + user1_token = self.get_requested_token(user1_auth) + + user2_auth = self.build_authentication_request( + user_id=self.another_user['id'], + password=self.another_user['password']) + user2_token = self.get_requested_token(user2_auth) + + self.head('/auth/tokens', token=user1_token, + headers={'X-Subject-Token': user2_token}, + expected_status=403) + + def test_admin_check_user_token(self): + # An admin can check a user's token. + # This is HEAD /v3/auth/tokens + + admin_auth = self.build_authentication_request( + user_id=self.admin_user['id'], + password=self.admin_user['password'], + project_id=self.project['id']) + admin_token = self.get_requested_token(admin_auth) + + user_auth = self.build_authentication_request( + user_id=self.just_a_user['id'], + password=self.just_a_user['password']) + user_token = self.get_requested_token(user_auth) + + self.head('/auth/tokens', token=admin_token, + headers={'X-Subject-Token': user_token}, expected_status=200) + + def test_user_revoke_same_token(self): + # Given a non-admin user token, the token can be used to revoke + # itself. + # This is DELETE /v3/auth/tokens, with X-Auth-Token == X-Subject-Token + # FIXME(blk-u): This test fails, a user can't revoke the same token, + # see bug 1421825. + + auth = self.build_authentication_request( + user_id=self.just_a_user['id'], + password=self.just_a_user['password']) + token = self.get_requested_token(auth) + + # FIXME(blk-u): remove expected_status=403 + self.delete('/auth/tokens', token=token, + headers={'X-Subject-Token': token}, expected_status=403) + + def test_user_revoke_user_token(self): + # A user can revoke one of their own tokens. + # This is DELETE /v3/auth/tokens + # FIXME(blk-u): This test fails, a user can't revoke the same token, + # see bug 1421825. + + auth = self.build_authentication_request( + user_id=self.just_a_user['id'], + password=self.just_a_user['password']) + token1 = self.get_requested_token(auth) + token2 = self.get_requested_token(auth) + + # FIXME(blk-u): remove expected_status=403 + self.delete('/auth/tokens', token=token1, + headers={'X-Subject-Token': token2}, expected_status=403) + + def test_user_revoke_other_user_token_rejected(self): + # A user cannot revoke another user's token. + # This is DELETE /v3/auth/tokens + + user1_auth = self.build_authentication_request( + user_id=self.just_a_user['id'], + password=self.just_a_user['password']) + user1_token = self.get_requested_token(user1_auth) + + user2_auth = self.build_authentication_request( + user_id=self.another_user['id'], + password=self.another_user['password']) + user2_token = self.get_requested_token(user2_auth) + + self.delete('/auth/tokens', token=user1_token, + headers={'X-Subject-Token': user2_token}, + expected_status=403) + + def test_admin_revoke_user_token(self): + # An admin can revoke a user's token. + # This is DELETE /v3/auth/tokens + + admin_auth = self.build_authentication_request( + user_id=self.admin_user['id'], + password=self.admin_user['password'], + project_id=self.project['id']) + admin_token = self.get_requested_token(admin_auth) + + user_auth = self.build_authentication_request( + user_id=self.just_a_user['id'], + password=self.just_a_user['password']) + user_token = self.get_requested_token(user_auth) + + self.delete('/auth/tokens', token=admin_token, + headers={'X-Subject-Token': user_token}) + + +class IdentityTestv3CloudPolicySample(test_v3.RestfulTestCase): + """Test policy enforcement of the sample v3 cloud policy file.""" + + def setUp(self): + """Setup for v3 Cloud Policy Sample Test Cases. + + The following data is created: + + - Three domains: domainA, domainB and admin_domain + - One project, which name is 'project' + - domainA has three users: domain_admin_user, project_admin_user and + just_a_user: + + - domain_admin_user has role 'admin' on domainA, + - project_admin_user has role 'admin' on the project, + - just_a_user has a non-admin role on both domainA and the project. + - admin_domain has user cloud_admin_user, with an 'admin' role + on admin_domain. + + We test various api protection rules from the cloud sample policy + file to make sure the sample is valid and that we correctly enforce it. + + """ + # Ensure that test_v3.RestfulTestCase doesn't load its own + # sample data, which would make checking the results of our + # tests harder + super(IdentityTestv3CloudPolicySample, self).setUp() + + # Finally, switch to the v3 sample policy file + self.addCleanup(rules.reset) + rules.reset() + self.config_fixture.config( + group='oslo_policy', + policy_file=tests.dirs.etc('policy.v3cloudsample.json')) + + def load_sample_data(self): + # Start by creating a couple of domains + self._populate_default_domain() + self.domainA = self.new_domain_ref() + self.resource_api.create_domain(self.domainA['id'], self.domainA) + self.domainB = self.new_domain_ref() + self.resource_api.create_domain(self.domainB['id'], self.domainB) + self.admin_domain = {'id': 'admin_domain_id', 'name': 'Admin_domain'} + self.resource_api.create_domain(self.admin_domain['id'], + self.admin_domain) + + # And our users + self.cloud_admin_user = self.new_user_ref( + domain_id=self.admin_domain['id']) + password = uuid.uuid4().hex + self.cloud_admin_user['password'] = password + self.cloud_admin_user = ( + self.identity_api.create_user(self.cloud_admin_user)) + self.cloud_admin_user['password'] = password + self.just_a_user = self.new_user_ref(domain_id=self.domainA['id']) + password = uuid.uuid4().hex + self.just_a_user['password'] = password + self.just_a_user = self.identity_api.create_user(self.just_a_user) + self.just_a_user['password'] = password + self.domain_admin_user = self.new_user_ref( + domain_id=self.domainA['id']) + password = uuid.uuid4().hex + self.domain_admin_user['password'] = password + self.domain_admin_user = ( + self.identity_api.create_user(self.domain_admin_user)) + self.domain_admin_user['password'] = password + self.project_admin_user = self.new_user_ref( + domain_id=self.domainA['id']) + password = uuid.uuid4().hex + self.project_admin_user['password'] = password + self.project_admin_user = ( + self.identity_api.create_user(self.project_admin_user)) + self.project_admin_user['password'] = password + + # The admin role and another plain role + self.admin_role = {'id': uuid.uuid4().hex, 'name': 'admin'} + self.role_api.create_role(self.admin_role['id'], self.admin_role) + self.role = self.new_role_ref() + self.role_api.create_role(self.role['id'], self.role) + + # The cloud admin just gets the admin role + self.assignment_api.create_grant(self.admin_role['id'], + user_id=self.cloud_admin_user['id'], + domain_id=self.admin_domain['id']) + + # Assign roles to the domain + self.assignment_api.create_grant(self.admin_role['id'], + user_id=self.domain_admin_user['id'], + domain_id=self.domainA['id']) + self.assignment_api.create_grant(self.role['id'], + user_id=self.just_a_user['id'], + domain_id=self.domainA['id']) + + # Create and assign roles to the project + self.project = self.new_project_ref(domain_id=self.domainA['id']) + self.resource_api.create_project(self.project['id'], self.project) + self.assignment_api.create_grant(self.admin_role['id'], + user_id=self.project_admin_user['id'], + project_id=self.project['id']) + self.assignment_api.create_grant(self.role['id'], + user_id=self.just_a_user['id'], + project_id=self.project['id']) + + def _stati(self, expected_status): + # Return the expected return codes for APIs with and without data + # with any specified status overriding the normal values + if expected_status is None: + return (200, 201, 204) + else: + return (expected_status, expected_status, expected_status) + + def _test_user_management(self, domain_id, expected=None): + status_OK, status_created, status_no_data = self._stati(expected) + entity_url = '/users/%s' % self.just_a_user['id'] + list_url = '/users?domain_id=%s' % domain_id + + self.get(entity_url, auth=self.auth, + expected_status=status_OK) + self.get(list_url, auth=self.auth, + expected_status=status_OK) + user = {'description': 'Updated'} + self.patch(entity_url, auth=self.auth, body={'user': user}, + expected_status=status_OK) + self.delete(entity_url, auth=self.auth, + expected_status=status_no_data) + + user_ref = self.new_user_ref(domain_id=domain_id) + self.post('/users', auth=self.auth, body={'user': user_ref}, + expected_status=status_created) + + def _test_project_management(self, domain_id, expected=None): + status_OK, status_created, status_no_data = self._stati(expected) + entity_url = '/projects/%s' % self.project['id'] + list_url = '/projects?domain_id=%s' % domain_id + + self.get(entity_url, auth=self.auth, + expected_status=status_OK) + self.get(list_url, auth=self.auth, + expected_status=status_OK) + project = {'description': 'Updated'} + self.patch(entity_url, auth=self.auth, body={'project': project}, + expected_status=status_OK) + self.delete(entity_url, auth=self.auth, + expected_status=status_no_data) + + proj_ref = self.new_project_ref(domain_id=domain_id) + self.post('/projects', auth=self.auth, body={'project': proj_ref}, + expected_status=status_created) + + def _test_domain_management(self, expected=None): + status_OK, status_created, status_no_data = self._stati(expected) + entity_url = '/domains/%s' % self.domainB['id'] + list_url = '/domains' + + self.get(entity_url, auth=self.auth, + expected_status=status_OK) + self.get(list_url, auth=self.auth, + expected_status=status_OK) + domain = {'description': 'Updated', 'enabled': False} + self.patch(entity_url, auth=self.auth, body={'domain': domain}, + expected_status=status_OK) + self.delete(entity_url, auth=self.auth, + expected_status=status_no_data) + + domain_ref = self.new_domain_ref() + self.post('/domains', auth=self.auth, body={'domain': domain_ref}, + expected_status=status_created) + + def _test_grants(self, target, entity_id, expected=None): + status_OK, status_created, status_no_data = self._stati(expected) + a_role = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.role_api.create_role(a_role['id'], a_role) + + collection_url = ( + '/%(target)s/%(target_id)s/users/%(user_id)s/roles' % { + 'target': target, + 'target_id': entity_id, + 'user_id': self.just_a_user['id']}) + member_url = '%(collection_url)s/%(role_id)s' % { + 'collection_url': collection_url, + 'role_id': a_role['id']} + + self.put(member_url, auth=self.auth, + expected_status=status_no_data) + self.head(member_url, auth=self.auth, + expected_status=status_no_data) + self.get(collection_url, auth=self.auth, + expected_status=status_OK) + self.delete(member_url, auth=self.auth, + expected_status=status_no_data) + + def test_user_management(self): + # First, authenticate with a user that does not have the domain + # admin role - shouldn't be able to do much. + self.auth = self.build_authentication_request( + user_id=self.just_a_user['id'], + password=self.just_a_user['password'], + domain_id=self.domainA['id']) + + self._test_user_management( + self.domainA['id'], expected=exception.ForbiddenAction.code) + + # Now, authenticate with a user that does have the domain admin role + self.auth = self.build_authentication_request( + user_id=self.domain_admin_user['id'], + password=self.domain_admin_user['password'], + domain_id=self.domainA['id']) + + self._test_user_management(self.domainA['id']) + + def test_user_management_by_cloud_admin(self): + # Test users management with a cloud admin. This user should + # be able to manage users in any domain. + self.auth = self.build_authentication_request( + user_id=self.cloud_admin_user['id'], + password=self.cloud_admin_user['password'], + domain_id=self.admin_domain['id']) + + self._test_user_management(self.domainA['id']) + + def test_project_management(self): + # First, authenticate with a user that does not have the project + # admin role - shouldn't be able to do much. + self.auth = self.build_authentication_request( + user_id=self.just_a_user['id'], + password=self.just_a_user['password'], + domain_id=self.domainA['id']) + + self._test_project_management( + self.domainA['id'], expected=exception.ForbiddenAction.code) + + # ...but should still be able to list projects of which they are + # a member + url = '/users/%s/projects' % self.just_a_user['id'] + self.get(url, auth=self.auth) + + # Now, authenticate with a user that does have the domain admin role + self.auth = self.build_authentication_request( + user_id=self.domain_admin_user['id'], + password=self.domain_admin_user['password'], + domain_id=self.domainA['id']) + + self._test_project_management(self.domainA['id']) + + def test_project_management_by_cloud_admin(self): + self.auth = self.build_authentication_request( + user_id=self.cloud_admin_user['id'], + password=self.cloud_admin_user['password'], + domain_id=self.admin_domain['id']) + + # Check whether cloud admin can operate a domain + # other than its own domain or not + self._test_project_management(self.domainA['id']) + + def test_domain_grants(self): + self.auth = self.build_authentication_request( + user_id=self.just_a_user['id'], + password=self.just_a_user['password'], + domain_id=self.domainA['id']) + + self._test_grants('domains', self.domainA['id'], + expected=exception.ForbiddenAction.code) + + # Now, authenticate with a user that does have the domain admin role + self.auth = self.build_authentication_request( + user_id=self.domain_admin_user['id'], + password=self.domain_admin_user['password'], + domain_id=self.domainA['id']) + + self._test_grants('domains', self.domainA['id']) + + # Check that with such a token we cannot modify grants on a + # different domain + self._test_grants('domains', self.domainB['id'], + expected=exception.ForbiddenAction.code) + + def test_domain_grants_by_cloud_admin(self): + # Test domain grants with a cloud admin. This user should be + # able to manage roles on any domain. + self.auth = self.build_authentication_request( + user_id=self.cloud_admin_user['id'], + password=self.cloud_admin_user['password'], + domain_id=self.admin_domain['id']) + + self._test_grants('domains', self.domainA['id']) + + def test_project_grants(self): + self.auth = self.build_authentication_request( + user_id=self.just_a_user['id'], + password=self.just_a_user['password'], + project_id=self.project['id']) + + self._test_grants('projects', self.project['id'], + expected=exception.ForbiddenAction.code) + + # Now, authenticate with a user that does have the project + # admin role + self.auth = self.build_authentication_request( + user_id=self.project_admin_user['id'], + password=self.project_admin_user['password'], + project_id=self.project['id']) + + self._test_grants('projects', self.project['id']) + + def test_project_grants_by_domain_admin(self): + # Test project grants with a domain admin. This user should be + # able to manage roles on any project in its own domain. + self.auth = self.build_authentication_request( + user_id=self.domain_admin_user['id'], + password=self.domain_admin_user['password'], + domain_id=self.domainA['id']) + + self._test_grants('projects', self.project['id']) + + def test_cloud_admin(self): + self.auth = self.build_authentication_request( + user_id=self.domain_admin_user['id'], + password=self.domain_admin_user['password'], + domain_id=self.domainA['id']) + + self._test_domain_management( + expected=exception.ForbiddenAction.code) + + self.auth = self.build_authentication_request( + user_id=self.cloud_admin_user['id'], + password=self.cloud_admin_user['password'], + domain_id=self.admin_domain['id']) + + self._test_domain_management() + + def test_list_user_credentials(self): + self.credential_user = self.new_credential_ref(self.just_a_user['id']) + self.credential_api.create_credential(self.credential_user['id'], + self.credential_user) + self.credential_admin = self.new_credential_ref( + self.cloud_admin_user['id']) + self.credential_api.create_credential(self.credential_admin['id'], + self.credential_admin) + + self.auth = self.build_authentication_request( + user_id=self.just_a_user['id'], + password=self.just_a_user['password']) + url = '/credentials?user_id=%s' % self.just_a_user['id'] + self.get(url, auth=self.auth) + url = '/credentials?user_id=%s' % self.cloud_admin_user['id'] + self.get(url, auth=self.auth, + expected_status=exception.ForbiddenAction.code) + url = '/credentials' + self.get(url, auth=self.auth, + expected_status=exception.ForbiddenAction.code) + + def test_get_and_delete_ec2_credentials(self): + """Tests getting and deleting ec2 credentials through the ec2 API.""" + another_user = self.new_user_ref(domain_id=self.domainA['id']) + password = another_user['password'] + another_user = self.identity_api.create_user(another_user) + + # create a credential for just_a_user + just_user_auth = self.build_authentication_request( + user_id=self.just_a_user['id'], + password=self.just_a_user['password'], + project_id=self.project['id']) + url = '/users/%s/credentials/OS-EC2' % self.just_a_user['id'] + r = self.post(url, body={'tenant_id': self.project['id']}, + auth=just_user_auth) + + # another normal user can't get the credential + another_user_auth = self.build_authentication_request( + user_id=another_user['id'], + password=password) + another_user_url = '/users/%s/credentials/OS-EC2/%s' % ( + another_user['id'], r.result['credential']['access']) + self.get(another_user_url, auth=another_user_auth, + expected_status=exception.ForbiddenAction.code) + + # the owner can get the credential + just_user_url = '/users/%s/credentials/OS-EC2/%s' % ( + self.just_a_user['id'], r.result['credential']['access']) + self.get(just_user_url, auth=just_user_auth) + + # another normal user can't delete the credential + self.delete(another_user_url, auth=another_user_auth, + expected_status=exception.ForbiddenAction.code) + + # the owner can get the credential + self.delete(just_user_url, auth=just_user_auth) + + def test_user_validate_same_token(self): + # Given a non-admin user token, the token can be used to validate + # itself. + # This is GET /v3/auth/tokens, with X-Auth-Token == X-Subject-Token + # FIXME(blk-u): This test fails, a user can't validate their own token, + # see bug 1421825. + + auth = self.build_authentication_request( + user_id=self.just_a_user['id'], + password=self.just_a_user['password']) + token = self.get_requested_token(auth) + + # FIXME(blk-u): remove expected_status=403. + self.get('/auth/tokens', token=token, + headers={'X-Subject-Token': token}, expected_status=403) + + def test_user_validate_user_token(self): + # A user can validate one of their own tokens. + # This is GET /v3/auth/tokens + # FIXME(blk-u): This test fails, a user can't validate their own token, + # see bug 1421825. + + auth = self.build_authentication_request( + user_id=self.just_a_user['id'], + password=self.just_a_user['password']) + token1 = self.get_requested_token(auth) + token2 = self.get_requested_token(auth) + + # FIXME(blk-u): remove expected_status=403. + self.get('/auth/tokens', token=token1, + headers={'X-Subject-Token': token2}, expected_status=403) + + def test_user_validate_other_user_token_rejected(self): + # A user cannot validate another user's token. + # This is GET /v3/auth/tokens + + user1_auth = self.build_authentication_request( + user_id=self.just_a_user['id'], + password=self.just_a_user['password']) + user1_token = self.get_requested_token(user1_auth) + + user2_auth = self.build_authentication_request( + user_id=self.cloud_admin_user['id'], + password=self.cloud_admin_user['password']) + user2_token = self.get_requested_token(user2_auth) + + self.get('/auth/tokens', token=user1_token, + headers={'X-Subject-Token': user2_token}, expected_status=403) + + def test_admin_validate_user_token(self): + # An admin can validate a user's token. + # This is GET /v3/auth/tokens + + admin_auth = self.build_authentication_request( + user_id=self.cloud_admin_user['id'], + password=self.cloud_admin_user['password'], + domain_id=self.admin_domain['id']) + admin_token = self.get_requested_token(admin_auth) + + user_auth = self.build_authentication_request( + user_id=self.just_a_user['id'], + password=self.just_a_user['password']) + user_token = self.get_requested_token(user_auth) + + self.get('/auth/tokens', token=admin_token, + headers={'X-Subject-Token': user_token}) + + def test_user_check_same_token(self): + # Given a non-admin user token, the token can be used to check + # itself. + # This is HEAD /v3/auth/tokens, with X-Auth-Token == X-Subject-Token + + auth = self.build_authentication_request( + user_id=self.just_a_user['id'], + password=self.just_a_user['password']) + token = self.get_requested_token(auth) + + self.head('/auth/tokens', token=token, + headers={'X-Subject-Token': token}, expected_status=200) + + def test_user_check_user_token(self): + # A user can check one of their own tokens. + # This is HEAD /v3/auth/tokens + + auth = self.build_authentication_request( + user_id=self.just_a_user['id'], + password=self.just_a_user['password']) + token1 = self.get_requested_token(auth) + token2 = self.get_requested_token(auth) + + self.head('/auth/tokens', token=token1, + headers={'X-Subject-Token': token2}, expected_status=200) + + def test_user_check_other_user_token_rejected(self): + # A user cannot check another user's token. + # This is HEAD /v3/auth/tokens + + user1_auth = self.build_authentication_request( + user_id=self.just_a_user['id'], + password=self.just_a_user['password']) + user1_token = self.get_requested_token(user1_auth) + + user2_auth = self.build_authentication_request( + user_id=self.cloud_admin_user['id'], + password=self.cloud_admin_user['password']) + user2_token = self.get_requested_token(user2_auth) + + self.head('/auth/tokens', token=user1_token, + headers={'X-Subject-Token': user2_token}, + expected_status=403) + + def test_admin_check_user_token(self): + # An admin can check a user's token. + # This is HEAD /v3/auth/tokens + + admin_auth = self.build_authentication_request( + user_id=self.domain_admin_user['id'], + password=self.domain_admin_user['password'], + domain_id=self.domainA['id']) + admin_token = self.get_requested_token(admin_auth) + + user_auth = self.build_authentication_request( + user_id=self.just_a_user['id'], + password=self.just_a_user['password']) + user_token = self.get_requested_token(user_auth) + + self.head('/auth/tokens', token=admin_token, + headers={'X-Subject-Token': user_token}, expected_status=200) + + def test_user_revoke_same_token(self): + # Given a non-admin user token, the token can be used to revoke + # itself. + # This is DELETE /v3/auth/tokens, with X-Auth-Token == X-Subject-Token + + auth = self.build_authentication_request( + user_id=self.just_a_user['id'], + password=self.just_a_user['password']) + token = self.get_requested_token(auth) + + self.delete('/auth/tokens', token=token, + headers={'X-Subject-Token': token}) + + def test_user_revoke_user_token(self): + # A user can revoke one of their own tokens. + # This is DELETE /v3/auth/tokens + + auth = self.build_authentication_request( + user_id=self.just_a_user['id'], + password=self.just_a_user['password']) + token1 = self.get_requested_token(auth) + token2 = self.get_requested_token(auth) + + self.delete('/auth/tokens', token=token1, + headers={'X-Subject-Token': token2}) + + def test_user_revoke_other_user_token_rejected(self): + # A user cannot revoke another user's token. + # This is DELETE /v3/auth/tokens + + user1_auth = self.build_authentication_request( + user_id=self.just_a_user['id'], + password=self.just_a_user['password']) + user1_token = self.get_requested_token(user1_auth) + + user2_auth = self.build_authentication_request( + user_id=self.cloud_admin_user['id'], + password=self.cloud_admin_user['password']) + user2_token = self.get_requested_token(user2_auth) + + self.delete('/auth/tokens', token=user1_token, + headers={'X-Subject-Token': user2_token}, + expected_status=403) + + def test_admin_revoke_user_token(self): + # An admin can revoke a user's token. + # This is DELETE /v3/auth/tokens + + admin_auth = self.build_authentication_request( + user_id=self.domain_admin_user['id'], + password=self.domain_admin_user['password'], + domain_id=self.domainA['id']) + admin_token = self.get_requested_token(admin_auth) + + user_auth = self.build_authentication_request( + user_id=self.just_a_user['id'], + password=self.just_a_user['password']) + user_token = self.get_requested_token(user_auth) + + self.delete('/auth/tokens', token=admin_token, + headers={'X-Subject-Token': user_token}) diff --git a/keystone-moon/keystone/tests/unit/test_validation.py b/keystone-moon/keystone/tests/unit/test_validation.py new file mode 100644 index 00000000..f83cabcb --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_validation.py @@ -0,0 +1,1563 @@ +# -*- coding: utf-8 -*- +# 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 + +import testtools + +from keystone.assignment import schema as assignment_schema +from keystone.catalog import schema as catalog_schema +from keystone.common import validation +from keystone.common.validation import parameter_types +from keystone.common.validation import validators +from keystone.contrib.endpoint_filter import schema as endpoint_filter_schema +from keystone.contrib.federation import schema as federation_schema +from keystone.credential import schema as credential_schema +from keystone import exception +from keystone.policy import schema as policy_schema +from keystone.resource import schema as resource_schema +from keystone.trust import schema as trust_schema + +"""Example model to validate create requests against. Assume that this is +the only backend for the create and validate schemas. This is just an +example to show how a backend can be used to construct a schema. In +Keystone, schemas are built according to the Identity API and the backends +available in Keystone. This example does not mean that all schema in +Keystone were strictly based on the SQL backends. + +class Entity(sql.ModelBase): + __tablename__ = 'entity' + attributes = ['id', 'name', 'domain_id', 'description'] + id = sql.Column(sql.String(64), primary_key=True) + name = sql.Column(sql.String(255), nullable=False) + description = sql.Column(sql.Text(), nullable=True) + enabled = sql.Column(sql.Boolean, default=True, nullable=False) + url = sql.Column(sql.String(225), nullable=True) + email = sql.Column(sql.String(64), nullable=True) +""" + +# Test schema to validate create requests against + +_entity_properties = { + 'name': parameter_types.name, + 'description': validation.nullable(parameter_types.description), + 'enabled': parameter_types.boolean, + 'url': validation.nullable(parameter_types.url), + 'email': validation.nullable(parameter_types.email), + 'id_string': validation.nullable(parameter_types.id_string) +} + +entity_create = { + 'type': 'object', + 'properties': _entity_properties, + 'required': ['name'], + 'additionalProperties': True, +} + +entity_update = { + 'type': 'object', + 'properties': _entity_properties, + 'minProperties': 1, + 'additionalProperties': True, +} + +_VALID_ENABLED_FORMATS = [True, False] + +_INVALID_ENABLED_FORMATS = ['some string', 1, 0, 'True', 'False'] + +_VALID_URLS = ['https://example.com', 'http://EXAMPLE.com/v3', + 'http://localhost', 'http://127.0.0.1:5000', + 'http://1.1.1.1', 'http://255.255.255.255', + 'http://[::1]', 'http://[::1]:35357', + 'http://[1::8]', 'http://[fe80::8%25eth0]', + 'http://[::1.2.3.4]', 'http://[2001:DB8::1.2.3.4]', + 'http://[::a:1.2.3.4]', 'http://[a::b:1.2.3.4]', + 'http://[1:2:3:4:5:6:7:8]', 'http://[1:2:3:4:5:6:1.2.3.4]', + 'http://[abcd:efAB:CDEF:1111:9999::]'] + +_INVALID_URLS = [False, 'this is not a URL', 1234, 'www.example.com', + 'localhost', 'http//something.com', + 'https//something.com'] + +_VALID_FILTERS = [{'interface': 'admin'}, + {'region': 'US-WEST', + 'interface': 'internal'}] + +_INVALID_FILTERS = ['some string', 1, 0, True, False] + + +class EntityValidationTestCase(testtools.TestCase): + + def setUp(self): + super(EntityValidationTestCase, self).setUp() + self.resource_name = 'some resource name' + self.description = 'Some valid description' + self.valid_enabled = True + self.valid_url = 'http://example.com' + self.valid_email = 'joe@example.com' + self.create_schema_validator = validators.SchemaValidator( + entity_create) + self.update_schema_validator = validators.SchemaValidator( + entity_update) + + def test_create_entity_with_all_valid_parameters_validates(self): + """Validate all parameter values against test schema.""" + request_to_validate = {'name': self.resource_name, + 'description': self.description, + 'enabled': self.valid_enabled, + 'url': self.valid_url, + 'email': self.valid_email} + self.create_schema_validator.validate(request_to_validate) + + def test_create_entity_with_only_required_valid_parameters_validates(self): + """Validate correct for only parameters values against test schema.""" + request_to_validate = {'name': self.resource_name} + self.create_schema_validator.validate(request_to_validate) + + def test_create_entity_with_name_too_long_raises_exception(self): + """Validate long names. + + Validate that an exception is raised when validating a string of 255+ + characters passed in as a name. + """ + invalid_name = 'a' * 256 + request_to_validate = {'name': invalid_name} + self.assertRaises(exception.SchemaValidationError, + self.create_schema_validator.validate, + request_to_validate) + + def test_create_entity_with_name_too_short_raises_exception(self): + """Validate short names. + + Test that an exception is raised when passing a string of length + zero as a name parameter. + """ + request_to_validate = {'name': ''} + self.assertRaises(exception.SchemaValidationError, + self.create_schema_validator.validate, + request_to_validate) + + def test_create_entity_with_unicode_name_validates(self): + """Test that we successfully validate a unicode string.""" + request_to_validate = {'name': u'αβγδ'} + self.create_schema_validator.validate(request_to_validate) + + def test_create_entity_with_invalid_enabled_format_raises_exception(self): + """Validate invalid enabled formats. + + Test that an exception is raised when passing invalid boolean-like + values as `enabled`. + """ + for format in _INVALID_ENABLED_FORMATS: + request_to_validate = {'name': self.resource_name, + 'enabled': format} + self.assertRaises(exception.SchemaValidationError, + self.create_schema_validator.validate, + request_to_validate) + + def test_create_entity_with_valid_enabled_formats_validates(self): + """Validate valid enabled formats. + + Test that we have successful validation on boolean values for + `enabled`. + """ + for valid_enabled in _VALID_ENABLED_FORMATS: + request_to_validate = {'name': self.resource_name, + 'enabled': valid_enabled} + # Make sure validation doesn't raise a validation exception + self.create_schema_validator.validate(request_to_validate) + + def test_create_entity_with_valid_urls_validates(self): + """Test that proper urls are successfully validated.""" + for valid_url in _VALID_URLS: + request_to_validate = {'name': self.resource_name, + 'url': valid_url} + self.create_schema_validator.validate(request_to_validate) + + def test_create_entity_with_invalid_urls_fails(self): + """Test that an exception is raised when validating improper urls.""" + for invalid_url in _INVALID_URLS: + request_to_validate = {'name': self.resource_name, + 'url': invalid_url} + self.assertRaises(exception.SchemaValidationError, + self.create_schema_validator.validate, + request_to_validate) + + def test_create_entity_with_valid_email_validates(self): + """Validate email address + + Test that we successfully validate properly formatted email + addresses. + """ + request_to_validate = {'name': self.resource_name, + 'email': self.valid_email} + self.create_schema_validator.validate(request_to_validate) + + def test_create_entity_with_invalid_email_fails(self): + """Validate invalid email address. + + Test that an exception is raised when validating improperly + formatted email addresses. + """ + request_to_validate = {'name': self.resource_name, + 'email': 'some invalid email value'} + self.assertRaises(exception.SchemaValidationError, + self.create_schema_validator.validate, + request_to_validate) + + def test_create_entity_with_valid_id_strings(self): + """Validate acceptable id strings.""" + valid_id_strings = [str(uuid.uuid4()), uuid.uuid4().hex, 'default'] + for valid_id in valid_id_strings: + request_to_validate = {'name': self.resource_name, + 'id_string': valid_id} + self.create_schema_validator.validate(request_to_validate) + + def test_create_entity_with_invalid_id_strings(self): + """Exception raised when using invalid id strings.""" + long_string = 'A' * 65 + invalid_id_strings = ['', long_string, 'this,should,fail'] + for invalid_id in invalid_id_strings: + request_to_validate = {'name': self.resource_name, + 'id_string': invalid_id} + self.assertRaises(exception.SchemaValidationError, + self.create_schema_validator.validate, + request_to_validate) + + def test_create_entity_with_null_id_string(self): + """Validate that None is an acceptable optional string type.""" + request_to_validate = {'name': self.resource_name, + 'id_string': None} + self.create_schema_validator.validate(request_to_validate) + + def test_create_entity_with_null_string_succeeds(self): + """Exception raised when passing None on required id strings.""" + request_to_validate = {'name': self.resource_name, + 'id_string': None} + self.create_schema_validator.validate(request_to_validate) + + def test_update_entity_with_no_parameters_fails(self): + """At least one parameter needs to be present for an update.""" + request_to_validate = {} + self.assertRaises(exception.SchemaValidationError, + self.update_schema_validator.validate, + request_to_validate) + + def test_update_entity_with_all_parameters_valid_validates(self): + """Simulate updating an entity by ID.""" + request_to_validate = {'name': self.resource_name, + 'description': self.description, + 'enabled': self.valid_enabled, + 'url': self.valid_url, + 'email': self.valid_email} + self.update_schema_validator.validate(request_to_validate) + + def test_update_entity_with_a_valid_required_parameter_validates(self): + """Succeed if a valid required parameter is provided.""" + request_to_validate = {'name': self.resource_name} + self.update_schema_validator.validate(request_to_validate) + + def test_update_entity_with_invalid_required_parameter_fails(self): + """Fail if a provided required parameter is invalid.""" + request_to_validate = {'name': 'a' * 256} + self.assertRaises(exception.SchemaValidationError, + self.update_schema_validator.validate, + request_to_validate) + + def test_update_entity_with_a_null_optional_parameter_validates(self): + """Optional parameters can be null to removed the value.""" + request_to_validate = {'email': None} + self.update_schema_validator.validate(request_to_validate) + + def test_update_entity_with_a_required_null_parameter_fails(self): + """The `name` parameter can't be null.""" + request_to_validate = {'name': None} + self.assertRaises(exception.SchemaValidationError, + self.update_schema_validator.validate, + request_to_validate) + + def test_update_entity_with_a_valid_optional_parameter_validates(self): + """Succeeds with only a single valid optional parameter.""" + request_to_validate = {'email': self.valid_email} + self.update_schema_validator.validate(request_to_validate) + + def test_update_entity_with_invalid_optional_parameter_fails(self): + """Fails when an optional parameter is invalid.""" + request_to_validate = {'email': 0} + self.assertRaises(exception.SchemaValidationError, + self.update_schema_validator.validate, + request_to_validate) + + +class ProjectValidationTestCase(testtools.TestCase): + """Test for V3 Project API validation.""" + + def setUp(self): + super(ProjectValidationTestCase, self).setUp() + + self.project_name = 'My Project' + + create = resource_schema.project_create + update = resource_schema.project_update + self.create_project_validator = validators.SchemaValidator(create) + self.update_project_validator = validators.SchemaValidator(update) + + def test_validate_project_request(self): + """Test that we validate a project with `name` in request.""" + request_to_validate = {'name': self.project_name} + self.create_project_validator.validate(request_to_validate) + + def test_validate_project_request_without_name_fails(self): + """Validate project request fails without name.""" + request_to_validate = {'enabled': True} + self.assertRaises(exception.SchemaValidationError, + self.create_project_validator.validate, + request_to_validate) + + def test_validate_project_request_with_enabled(self): + """Validate `enabled` as boolean-like values for projects.""" + for valid_enabled in _VALID_ENABLED_FORMATS: + request_to_validate = {'name': self.project_name, + 'enabled': valid_enabled} + self.create_project_validator.validate(request_to_validate) + + def test_validate_project_request_with_invalid_enabled_fails(self): + """Exception is raised when `enabled` isn't a boolean-like value.""" + for invalid_enabled in _INVALID_ENABLED_FORMATS: + request_to_validate = {'name': self.project_name, + 'enabled': invalid_enabled} + self.assertRaises(exception.SchemaValidationError, + self.create_project_validator.validate, + request_to_validate) + + def test_validate_project_request_with_valid_description(self): + """Test that we validate `description` in create project requests.""" + request_to_validate = {'name': self.project_name, + 'description': 'My Project'} + self.create_project_validator.validate(request_to_validate) + + def test_validate_project_request_with_invalid_description_fails(self): + """Exception is raised when `description` as a non-string value.""" + request_to_validate = {'name': self.project_name, + 'description': False} + self.assertRaises(exception.SchemaValidationError, + self.create_project_validator.validate, + request_to_validate) + + def test_validate_project_request_with_name_too_long(self): + """Exception is raised when `name` is too long.""" + long_project_name = 'a' * 65 + request_to_validate = {'name': long_project_name} + self.assertRaises(exception.SchemaValidationError, + self.create_project_validator.validate, + request_to_validate) + + def test_validate_project_request_with_name_too_short(self): + """Exception raised when `name` is too short.""" + request_to_validate = {'name': ''} + self.assertRaises(exception.SchemaValidationError, + self.create_project_validator.validate, + request_to_validate) + + def test_validate_project_request_with_valid_parent_id(self): + """Test that we validate `parent_id` in create project requests.""" + # parent_id is nullable + request_to_validate = {'name': self.project_name, + 'parent_id': None} + self.create_project_validator.validate(request_to_validate) + request_to_validate = {'name': self.project_name, + 'parent_id': uuid.uuid4().hex} + self.create_project_validator.validate(request_to_validate) + + def test_validate_project_request_with_invalid_parent_id_fails(self): + """Exception is raised when `parent_id` as a non-id value.""" + request_to_validate = {'name': self.project_name, + 'parent_id': False} + self.assertRaises(exception.SchemaValidationError, + self.create_project_validator.validate, + request_to_validate) + request_to_validate = {'name': self.project_name, + 'parent_id': 'fake project'} + self.assertRaises(exception.SchemaValidationError, + self.create_project_validator.validate, + request_to_validate) + + def test_validate_project_update_request(self): + """Test that we validate a project update request.""" + request_to_validate = {'domain_id': uuid.uuid4().hex} + self.update_project_validator.validate(request_to_validate) + + def test_validate_project_update_request_with_no_parameters_fails(self): + """Exception is raised when updating project without parameters.""" + request_to_validate = {} + self.assertRaises(exception.SchemaValidationError, + self.update_project_validator.validate, + request_to_validate) + + def test_validate_project_update_request_with_name_too_long_fails(self): + """Exception raised when updating a project with `name` too long.""" + long_project_name = 'a' * 65 + request_to_validate = {'name': long_project_name} + self.assertRaises(exception.SchemaValidationError, + self.update_project_validator.validate, + request_to_validate) + + def test_validate_project_update_request_with_name_too_short_fails(self): + """Exception raised when updating a project with `name` too short.""" + request_to_validate = {'name': ''} + self.assertRaises(exception.SchemaValidationError, + self.update_project_validator.validate, + request_to_validate) + + def test_validate_project_update_request_with_null_domain_id_fails(self): + request_to_validate = {'domain_id': None} + self.assertRaises(exception.SchemaValidationError, + self.update_project_validator.validate, + request_to_validate) + + +class DomainValidationTestCase(testtools.TestCase): + """Test for V3 Domain API validation.""" + + def setUp(self): + super(DomainValidationTestCase, self).setUp() + + self.domain_name = 'My Domain' + + create = resource_schema.domain_create + update = resource_schema.domain_update + self.create_domain_validator = validators.SchemaValidator(create) + self.update_domain_validator = validators.SchemaValidator(update) + + def test_validate_domain_request(self): + """Make sure we successfully validate a create domain request.""" + request_to_validate = {'name': self.domain_name} + self.create_domain_validator.validate(request_to_validate) + + def test_validate_domain_request_without_name_fails(self): + """Make sure we raise an exception when `name` isn't included.""" + request_to_validate = {'enabled': True} + self.assertRaises(exception.SchemaValidationError, + self.create_domain_validator.validate, + request_to_validate) + + def test_validate_domain_request_with_enabled(self): + """Validate `enabled` as boolean-like values for domains.""" + for valid_enabled in _VALID_ENABLED_FORMATS: + request_to_validate = {'name': self.domain_name, + 'enabled': valid_enabled} + self.create_domain_validator.validate(request_to_validate) + + def test_validate_domain_request_with_invalid_enabled_fails(self): + """Exception is raised when `enabled` isn't a boolean-like value.""" + for invalid_enabled in _INVALID_ENABLED_FORMATS: + request_to_validate = {'name': self.domain_name, + 'enabled': invalid_enabled} + self.assertRaises(exception.SchemaValidationError, + self.create_domain_validator.validate, + request_to_validate) + + def test_validate_domain_request_with_valid_description(self): + """Test that we validate `description` in create domain requests.""" + request_to_validate = {'name': self.domain_name, + 'description': 'My Domain'} + self.create_domain_validator.validate(request_to_validate) + + def test_validate_domain_request_with_invalid_description_fails(self): + """Exception is raised when `description` is a non-string value.""" + request_to_validate = {'name': self.domain_name, + 'description': False} + self.assertRaises(exception.SchemaValidationError, + self.create_domain_validator.validate, + request_to_validate) + + def test_validate_domain_request_with_name_too_long(self): + """Exception is raised when `name` is too long.""" + long_domain_name = 'a' * 65 + request_to_validate = {'name': long_domain_name} + self.assertRaises(exception.SchemaValidationError, + self.create_domain_validator.validate, + request_to_validate) + + def test_validate_domain_request_with_name_too_short(self): + """Exception raised when `name` is too short.""" + request_to_validate = {'name': ''} + self.assertRaises(exception.SchemaValidationError, + self.create_domain_validator.validate, + request_to_validate) + + def test_validate_domain_update_request(self): + """Test that we validate a domain update request.""" + request_to_validate = {'domain_id': uuid.uuid4().hex} + self.update_domain_validator.validate(request_to_validate) + + def test_validate_domain_update_request_with_no_parameters_fails(self): + """Exception is raised when updating a domain without parameters.""" + request_to_validate = {} + self.assertRaises(exception.SchemaValidationError, + self.update_domain_validator.validate, + request_to_validate) + + def test_validate_domain_update_request_with_name_too_long_fails(self): + """Exception raised when updating a domain with `name` too long.""" + long_domain_name = 'a' * 65 + request_to_validate = {'name': long_domain_name} + self.assertRaises(exception.SchemaValidationError, + self.update_domain_validator.validate, + request_to_validate) + + def test_validate_domain_update_request_with_name_too_short_fails(self): + """Exception raised when updating a domain with `name` too short.""" + request_to_validate = {'name': ''} + self.assertRaises(exception.SchemaValidationError, + self.update_domain_validator.validate, + request_to_validate) + + +class RoleValidationTestCase(testtools.TestCase): + """Test for V3 Role API validation.""" + + def setUp(self): + super(RoleValidationTestCase, self).setUp() + + self.role_name = 'My Role' + + create = assignment_schema.role_create + update = assignment_schema.role_update + self.create_role_validator = validators.SchemaValidator(create) + self.update_role_validator = validators.SchemaValidator(update) + + def test_validate_role_request(self): + """Test we can successfully validate a create role request.""" + request_to_validate = {'name': self.role_name} + self.create_role_validator.validate(request_to_validate) + + def test_validate_role_create_without_name_raises_exception(self): + """Test that we raise an exception when `name` isn't included.""" + request_to_validate = {'enabled': True} + self.assertRaises(exception.SchemaValidationError, + self.create_role_validator.validate, + request_to_validate) + + def test_validate_role_create_when_name_is_not_string_fails(self): + """Exception is raised on role create with a non-string `name`.""" + request_to_validate = {'name': True} + self.assertRaises(exception.SchemaValidationError, + self.create_role_validator.validate, + request_to_validate) + request_to_validate = {'name': 24} + self.assertRaises(exception.SchemaValidationError, + self.create_role_validator.validate, + request_to_validate) + + def test_validate_role_update_request(self): + """Test that we validate a role update request.""" + request_to_validate = {'name': 'My New Role'} + self.update_role_validator.validate(request_to_validate) + + def test_validate_role_update_fails_with_invalid_name_fails(self): + """Exception when validating an update request with invalid `name`.""" + request_to_validate = {'name': True} + self.assertRaises(exception.SchemaValidationError, + self.update_role_validator.validate, + request_to_validate) + + request_to_validate = {'name': 24} + self.assertRaises(exception.SchemaValidationError, + self.update_role_validator.validate, + request_to_validate) + + +class PolicyValidationTestCase(testtools.TestCase): + """Test for V3 Policy API validation.""" + + def setUp(self): + super(PolicyValidationTestCase, self).setUp() + + create = policy_schema.policy_create + update = policy_schema.policy_update + self.create_policy_validator = validators.SchemaValidator(create) + self.update_policy_validator = validators.SchemaValidator(update) + + def test_validate_policy_succeeds(self): + """Test that we validate a create policy request.""" + request_to_validate = {'blob': 'some blob information', + 'type': 'application/json'} + self.create_policy_validator.validate(request_to_validate) + + def test_validate_policy_without_blob_fails(self): + """Exception raised without `blob` in request.""" + request_to_validate = {'type': 'application/json'} + self.assertRaises(exception.SchemaValidationError, + self.create_policy_validator.validate, + request_to_validate) + + def test_validate_policy_without_type_fails(self): + """Exception raised without `type` in request.""" + request_to_validate = {'blob': 'some blob information'} + self.assertRaises(exception.SchemaValidationError, + self.create_policy_validator.validate, + request_to_validate) + + def test_validate_policy_create_with_extra_parameters_succeeds(self): + """Validate policy create with extra parameters.""" + request_to_validate = {'blob': 'some blob information', + 'type': 'application/json', + 'extra': 'some extra stuff'} + self.create_policy_validator.validate(request_to_validate) + + def test_validate_policy_create_with_invalid_type_fails(self): + """Exception raised when `blob` and `type` are boolean.""" + for prop in ['blob', 'type']: + request_to_validate = {prop: False} + self.assertRaises(exception.SchemaValidationError, + self.create_policy_validator.validate, + request_to_validate) + + def test_validate_policy_update_without_parameters_fails(self): + """Exception raised when updating policy without parameters.""" + request_to_validate = {} + self.assertRaises(exception.SchemaValidationError, + self.update_policy_validator.validate, + request_to_validate) + + def test_validate_policy_update_with_extra_parameters_succeeds(self): + """Validate policy update request with extra parameters.""" + request_to_validate = {'blob': 'some blob information', + 'type': 'application/json', + 'extra': 'some extra stuff'} + self.update_policy_validator.validate(request_to_validate) + + def test_validate_policy_update_succeeds(self): + """Test that we validate a policy update request.""" + request_to_validate = {'blob': 'some blob information', + 'type': 'application/json'} + self.update_policy_validator.validate(request_to_validate) + + def test_validate_policy_update_with_invalid_type_fails(self): + """Exception raised when invalid `type` on policy update.""" + for prop in ['blob', 'type']: + request_to_validate = {prop: False} + self.assertRaises(exception.SchemaValidationError, + self.update_policy_validator.validate, + request_to_validate) + + +class CredentialValidationTestCase(testtools.TestCase): + """Test for V3 Credential API validation.""" + + def setUp(self): + super(CredentialValidationTestCase, self).setUp() + + create = credential_schema.credential_create + update = credential_schema.credential_update + self.create_credential_validator = validators.SchemaValidator(create) + self.update_credential_validator = validators.SchemaValidator(update) + + def test_validate_credential_succeeds(self): + """Test that we validate a credential request.""" + request_to_validate = {'blob': 'some string', + 'project_id': uuid.uuid4().hex, + 'type': 'ec2', + 'user_id': uuid.uuid4().hex} + self.create_credential_validator.validate(request_to_validate) + + def test_validate_credential_without_blob_fails(self): + """Exception raised without `blob` in create request.""" + request_to_validate = {'type': 'ec2', + 'user_id': uuid.uuid4().hex} + self.assertRaises(exception.SchemaValidationError, + self.create_credential_validator.validate, + request_to_validate) + + def test_validate_credential_without_user_id_fails(self): + """Exception raised without `user_id` in create request.""" + request_to_validate = {'blob': 'some credential blob', + 'type': 'ec2'} + self.assertRaises(exception.SchemaValidationError, + self.create_credential_validator.validate, + request_to_validate) + + def test_validate_credential_without_type_fails(self): + """Exception raised without `type` in create request.""" + request_to_validate = {'blob': 'some credential blob', + 'user_id': uuid.uuid4().hex} + self.assertRaises(exception.SchemaValidationError, + self.create_credential_validator.validate, + request_to_validate) + + def test_validate_credential_ec2_without_project_id_fails(self): + """Validate `project_id` is required for ec2. + + Test that a SchemaValidationError is raised when type is ec2 + and no `project_id` is provided in create request. + """ + request_to_validate = {'blob': 'some credential blob', + 'type': 'ec2', + 'user_id': uuid.uuid4().hex} + self.assertRaises(exception.SchemaValidationError, + self.create_credential_validator.validate, + request_to_validate) + + def test_validate_credential_with_project_id_succeeds(self): + """Test that credential request works for all types.""" + cred_types = ['ec2', 'cert', uuid.uuid4().hex] + + for c_type in cred_types: + request_to_validate = {'blob': 'some blob', + 'project_id': uuid.uuid4().hex, + 'type': c_type, + 'user_id': uuid.uuid4().hex} + # Make sure an exception isn't raised + self.create_credential_validator.validate(request_to_validate) + + def test_validate_credential_non_ec2_without_project_id_succeeds(self): + """Validate `project_id` is not required for non-ec2. + + Test that create request without `project_id` succeeds for any + non-ec2 credential. + """ + cred_types = ['cert', uuid.uuid4().hex] + + for c_type in cred_types: + request_to_validate = {'blob': 'some blob', + 'type': c_type, + 'user_id': uuid.uuid4().hex} + # Make sure an exception isn't raised + self.create_credential_validator.validate(request_to_validate) + + def test_validate_credential_with_extra_parameters_succeeds(self): + """Validate create request with extra parameters.""" + request_to_validate = {'blob': 'some string', + 'extra': False, + 'project_id': uuid.uuid4().hex, + 'type': 'ec2', + 'user_id': uuid.uuid4().hex} + self.create_credential_validator.validate(request_to_validate) + + def test_validate_credential_update_succeeds(self): + """Test that a credential request is properly validated.""" + request_to_validate = {'blob': 'some string', + 'project_id': uuid.uuid4().hex, + 'type': 'ec2', + 'user_id': uuid.uuid4().hex} + self.update_credential_validator.validate(request_to_validate) + + def test_validate_credential_update_without_parameters_fails(self): + """Exception is raised on update without parameters.""" + request_to_validate = {} + self.assertRaises(exception.SchemaValidationError, + self.update_credential_validator.validate, + request_to_validate) + + def test_validate_credential_update_with_extra_parameters_succeeds(self): + """Validate credential update with extra parameters.""" + request_to_validate = {'blob': 'some string', + 'extra': False, + 'project_id': uuid.uuid4().hex, + 'type': 'ec2', + 'user_id': uuid.uuid4().hex} + self.update_credential_validator.validate(request_to_validate) + + +class RegionValidationTestCase(testtools.TestCase): + """Test for V3 Region API validation.""" + + def setUp(self): + super(RegionValidationTestCase, self).setUp() + + self.region_name = 'My Region' + + create = catalog_schema.region_create + update = catalog_schema.region_update + self.create_region_validator = validators.SchemaValidator(create) + self.update_region_validator = validators.SchemaValidator(update) + + def test_validate_region_request(self): + """Test that we validate a basic region request.""" + # Create_region doesn't take any parameters in the request so let's + # make sure we cover that case. + request_to_validate = {} + self.create_region_validator.validate(request_to_validate) + + def test_validate_region_create_request_with_parameters(self): + """Test that we validate a region request with parameters.""" + request_to_validate = {'id': 'us-east', + 'description': 'US East Region', + 'parent_region_id': 'US Region'} + self.create_region_validator.validate(request_to_validate) + + def test_validate_region_create_with_uuid(self): + """Test that we validate a region request with a UUID as the id.""" + request_to_validate = {'id': uuid.uuid4().hex, + 'description': 'US East Region', + 'parent_region_id': uuid.uuid4().hex} + self.create_region_validator.validate(request_to_validate) + + def test_validate_region_create_succeeds_with_extra_parameters(self): + """Validate create region request with extra values.""" + request_to_validate = {'other_attr': uuid.uuid4().hex} + self.create_region_validator.validate(request_to_validate) + + def test_validate_region_update_succeeds(self): + """Test that we validate a region update request.""" + request_to_validate = {'id': 'us-west', + 'description': 'US West Region', + 'parent_region_id': 'us-region'} + self.update_region_validator.validate(request_to_validate) + + def test_validate_region_update_succeeds_with_extra_parameters(self): + """Validate extra attributes in the region update request.""" + request_to_validate = {'other_attr': uuid.uuid4().hex} + self.update_region_validator.validate(request_to_validate) + + def test_validate_region_update_fails_with_no_parameters(self): + """Exception raised when passing no parameters in a region update.""" + # An update request should consist of at least one value to update + request_to_validate = {} + self.assertRaises(exception.SchemaValidationError, + self.update_region_validator.validate, + request_to_validate) + + +class ServiceValidationTestCase(testtools.TestCase): + """Test for V3 Service API validation.""" + + def setUp(self): + super(ServiceValidationTestCase, self).setUp() + + create = catalog_schema.service_create + update = catalog_schema.service_update + self.create_service_validator = validators.SchemaValidator(create) + self.update_service_validator = validators.SchemaValidator(update) + + def test_validate_service_create_succeeds(self): + """Test that we validate a service create request.""" + request_to_validate = {'name': 'Nova', + 'description': 'OpenStack Compute Service', + 'enabled': True, + 'type': 'compute'} + self.create_service_validator.validate(request_to_validate) + + def test_validate_service_create_succeeds_with_required_parameters(self): + """Validate a service create request with the required parameters.""" + # The only parameter type required for service creation is 'type' + request_to_validate = {'type': 'compute'} + self.create_service_validator.validate(request_to_validate) + + def test_validate_service_create_fails_without_type(self): + """Exception raised when trying to create a service without `type`.""" + request_to_validate = {'name': 'Nova'} + self.assertRaises(exception.SchemaValidationError, + self.create_service_validator.validate, + request_to_validate) + + def test_validate_service_create_succeeds_with_extra_parameters(self): + """Test that extra parameters pass validation on create service.""" + request_to_validate = {'other_attr': uuid.uuid4().hex, + 'type': uuid.uuid4().hex} + self.create_service_validator.validate(request_to_validate) + + def test_validate_service_create_succeeds_with_valid_enabled(self): + """Validate boolean values as enabled values on service create.""" + for valid_enabled in _VALID_ENABLED_FORMATS: + request_to_validate = {'enabled': valid_enabled, + 'type': uuid.uuid4().hex} + self.create_service_validator.validate(request_to_validate) + + def test_validate_service_create_fails_with_invalid_enabled(self): + """Exception raised when boolean-like parameters as `enabled` + + On service create, make sure an exception is raised if `enabled` is + not a boolean value. + """ + for invalid_enabled in _INVALID_ENABLED_FORMATS: + request_to_validate = {'enabled': invalid_enabled, + 'type': uuid.uuid4().hex} + self.assertRaises(exception.SchemaValidationError, + self.create_service_validator.validate, + request_to_validate) + + def test_validate_service_create_fails_when_name_too_long(self): + """Exception raised when `name` is greater than 255 characters.""" + long_name = 'a' * 256 + request_to_validate = {'type': 'compute', + 'name': long_name} + self.assertRaises(exception.SchemaValidationError, + self.create_service_validator.validate, + request_to_validate) + + def test_validate_service_create_fails_when_name_too_short(self): + """Exception is raised when `name` is too short.""" + request_to_validate = {'type': 'compute', + 'name': ''} + self.assertRaises(exception.SchemaValidationError, + self.create_service_validator.validate, + request_to_validate) + + def test_validate_service_create_fails_when_type_too_long(self): + """Exception is raised when `type` is too long.""" + long_type_name = 'a' * 256 + request_to_validate = {'type': long_type_name} + self.assertRaises(exception.SchemaValidationError, + self.create_service_validator.validate, + request_to_validate) + + def test_validate_service_create_fails_when_type_too_short(self): + """Exception is raised when `type` is too short.""" + request_to_validate = {'type': ''} + self.assertRaises(exception.SchemaValidationError, + self.create_service_validator.validate, + request_to_validate) + + def test_validate_service_update_request_succeeds(self): + """Test that we validate a service update request.""" + request_to_validate = {'name': 'Cinder', + 'type': 'volume', + 'description': 'OpenStack Block Storage', + 'enabled': False} + self.update_service_validator.validate(request_to_validate) + + def test_validate_service_update_fails_with_no_parameters(self): + """Exception raised when updating a service without values.""" + request_to_validate = {} + self.assertRaises(exception.SchemaValidationError, + self.update_service_validator.validate, + request_to_validate) + + def test_validate_service_update_succeeds_with_extra_parameters(self): + """Validate updating a service with extra parameters.""" + request_to_validate = {'other_attr': uuid.uuid4().hex} + self.update_service_validator.validate(request_to_validate) + + def test_validate_service_update_succeeds_with_valid_enabled(self): + """Validate boolean formats as `enabled` on service update.""" + for valid_enabled in _VALID_ENABLED_FORMATS: + request_to_validate = {'enabled': valid_enabled} + self.update_service_validator.validate(request_to_validate) + + def test_validate_service_update_fails_with_invalid_enabled(self): + """Exception raised when boolean-like values as `enabled`.""" + for invalid_enabled in _INVALID_ENABLED_FORMATS: + request_to_validate = {'enabled': invalid_enabled} + self.assertRaises(exception.SchemaValidationError, + self.update_service_validator.validate, + request_to_validate) + + def test_validate_service_update_fails_with_name_too_long(self): + """Exception is raised when `name` is too long on update.""" + long_name = 'a' * 256 + request_to_validate = {'name': long_name} + self.assertRaises(exception.SchemaValidationError, + self.update_service_validator.validate, + request_to_validate) + + def test_validate_service_update_fails_with_name_too_short(self): + """Exception is raised when `name` is too short on update.""" + request_to_validate = {'name': ''} + self.assertRaises(exception.SchemaValidationError, + self.update_service_validator.validate, + request_to_validate) + + def test_validate_service_update_fails_with_type_too_long(self): + """Exception is raised when `type` is too long on update.""" + long_type_name = 'a' * 256 + request_to_validate = {'type': long_type_name} + self.assertRaises(exception.SchemaValidationError, + self.update_service_validator.validate, + request_to_validate) + + def test_validate_service_update_fails_with_type_too_short(self): + """Exception is raised when `type` is too short on update.""" + request_to_validate = {'type': ''} + self.assertRaises(exception.SchemaValidationError, + self.update_service_validator.validate, + request_to_validate) + + +class EndpointValidationTestCase(testtools.TestCase): + """Test for V3 Endpoint API validation.""" + + def setUp(self): + super(EndpointValidationTestCase, self).setUp() + + create = catalog_schema.endpoint_create + update = catalog_schema.endpoint_update + self.create_endpoint_validator = validators.SchemaValidator(create) + self.update_endpoint_validator = validators.SchemaValidator(update) + + def test_validate_endpoint_request_succeeds(self): + """Test that we validate an endpoint request.""" + request_to_validate = {'enabled': True, + 'interface': 'admin', + 'region_id': uuid.uuid4().hex, + 'service_id': uuid.uuid4().hex, + 'url': 'https://service.example.com:5000/'} + self.create_endpoint_validator.validate(request_to_validate) + + def test_validate_endpoint_create_succeeds_with_required_parameters(self): + """Validate an endpoint request with only the required parameters.""" + # According to the Identity V3 API endpoint creation requires + # 'service_id', 'interface', and 'url' + request_to_validate = {'service_id': uuid.uuid4().hex, + 'interface': 'public', + 'url': 'https://service.example.com:5000/'} + self.create_endpoint_validator.validate(request_to_validate) + + def test_validate_endpoint_create_succeeds_with_valid_enabled(self): + """Validate an endpoint with boolean values. + + Validate boolean values as `enabled` in endpoint create requests. + """ + for valid_enabled in _VALID_ENABLED_FORMATS: + request_to_validate = {'enabled': valid_enabled, + 'service_id': uuid.uuid4().hex, + 'interface': 'public', + 'url': 'https://service.example.com:5000/'} + self.create_endpoint_validator.validate(request_to_validate) + + def test_validate_create_endpoint_fails_with_invalid_enabled(self): + """Exception raised when boolean-like values as `enabled`.""" + for invalid_enabled in _INVALID_ENABLED_FORMATS: + request_to_validate = {'enabled': invalid_enabled, + 'service_id': uuid.uuid4().hex, + 'interface': 'public', + 'url': 'https://service.example.com:5000/'} + self.assertRaises(exception.SchemaValidationError, + self.create_endpoint_validator.validate, + request_to_validate) + + def test_validate_endpoint_create_succeeds_with_extra_parameters(self): + """Test that extra parameters pass validation on create endpoint.""" + request_to_validate = {'other_attr': uuid.uuid4().hex, + 'service_id': uuid.uuid4().hex, + 'interface': 'public', + 'url': 'https://service.example.com:5000/'} + self.create_endpoint_validator.validate(request_to_validate) + + def test_validate_endpoint_create_fails_without_service_id(self): + """Exception raised when `service_id` isn't in endpoint request.""" + request_to_validate = {'interface': 'public', + 'url': 'https://service.example.com:5000/'} + self.assertRaises(exception.SchemaValidationError, + self.create_endpoint_validator.validate, + request_to_validate) + + def test_validate_endpoint_create_fails_without_interface(self): + """Exception raised when `interface` isn't in endpoint request.""" + request_to_validate = {'service_id': uuid.uuid4().hex, + 'url': 'https://service.example.com:5000/'} + self.assertRaises(exception.SchemaValidationError, + self.create_endpoint_validator.validate, + request_to_validate) + + def test_validate_endpoint_create_fails_without_url(self): + """Exception raised when `url` isn't in endpoint request.""" + request_to_validate = {'service_id': uuid.uuid4().hex, + 'interface': 'public'} + self.assertRaises(exception.SchemaValidationError, + self.create_endpoint_validator.validate, + request_to_validate) + + def test_validate_endpoint_create_succeeds_with_url(self): + """Validate `url` attribute in endpoint create request.""" + request_to_validate = {'service_id': uuid.uuid4().hex, + 'interface': 'public'} + for url in _VALID_URLS: + request_to_validate['url'] = url + self.create_endpoint_validator.validate(request_to_validate) + + def test_validate_endpoint_create_fails_with_invalid_url(self): + """Exception raised when passing invalid `url` in request.""" + request_to_validate = {'service_id': uuid.uuid4().hex, + 'interface': 'public'} + for url in _INVALID_URLS: + request_to_validate['url'] = url + self.assertRaises(exception.SchemaValidationError, + self.create_endpoint_validator.validate, + request_to_validate) + + def test_validate_endpoint_create_fails_with_invalid_interface(self): + """Exception raised with invalid `interface`.""" + request_to_validate = {'interface': uuid.uuid4().hex, + 'service_id': uuid.uuid4().hex, + 'url': 'https://service.example.com:5000/'} + self.assertRaises(exception.SchemaValidationError, + self.create_endpoint_validator.validate, + request_to_validate) + + def test_validate_endpoint_update_fails_with_invalid_enabled(self): + """Exception raised when `enabled` is boolean-like value.""" + for invalid_enabled in _INVALID_ENABLED_FORMATS: + request_to_validate = {'enabled': invalid_enabled} + self.assertRaises(exception.SchemaValidationError, + self.update_endpoint_validator.validate, + request_to_validate) + + def test_validate_endpoint_update_succeeds_with_valid_enabled(self): + """Validate `enabled` as boolean values.""" + for valid_enabled in _VALID_ENABLED_FORMATS: + request_to_validate = {'enabled': valid_enabled} + self.update_endpoint_validator.validate(request_to_validate) + + def test_validate_endpoint_update_fails_with_invalid_interface(self): + """Exception raised when invalid `interface` on endpoint update.""" + request_to_validate = {'interface': uuid.uuid4().hex, + 'service_id': uuid.uuid4().hex, + 'url': 'https://service.example.com:5000/'} + self.assertRaises(exception.SchemaValidationError, + self.update_endpoint_validator.validate, + request_to_validate) + + def test_validate_endpoint_update_request_succeeds(self): + """Test that we validate an endpoint update request.""" + request_to_validate = {'enabled': True, + 'interface': 'admin', + 'region_id': uuid.uuid4().hex, + 'service_id': uuid.uuid4().hex, + 'url': 'https://service.example.com:5000/'} + self.update_endpoint_validator.validate(request_to_validate) + + def test_validate_endpoint_update_fails_with_no_parameters(self): + """Exception raised when no parameters on endpoint update.""" + request_to_validate = {} + self.assertRaises(exception.SchemaValidationError, + self.update_endpoint_validator.validate, + request_to_validate) + + def test_validate_endpoint_update_succeeds_with_extra_parameters(self): + """Test that extra parameters pass validation on update endpoint.""" + request_to_validate = {'enabled': True, + 'interface': 'admin', + 'region_id': uuid.uuid4().hex, + 'service_id': uuid.uuid4().hex, + 'url': 'https://service.example.com:5000/', + 'other_attr': uuid.uuid4().hex} + self.update_endpoint_validator.validate(request_to_validate) + + def test_validate_endpoint_update_succeeds_with_url(self): + """Validate `url` attribute in endpoint update request.""" + request_to_validate = {'service_id': uuid.uuid4().hex, + 'interface': 'public'} + for url in _VALID_URLS: + request_to_validate['url'] = url + self.update_endpoint_validator.validate(request_to_validate) + + def test_validate_endpoint_update_fails_with_invalid_url(self): + """Exception raised when passing invalid `url` in request.""" + request_to_validate = {'service_id': uuid.uuid4().hex, + 'interface': 'public'} + for url in _INVALID_URLS: + request_to_validate['url'] = url + self.assertRaises(exception.SchemaValidationError, + self.update_endpoint_validator.validate, + request_to_validate) + + +class EndpointGroupValidationTestCase(testtools.TestCase): + """Test for V3 Endpoint Group API validation.""" + + def setUp(self): + super(EndpointGroupValidationTestCase, self).setUp() + + create = endpoint_filter_schema.endpoint_group_create + update = endpoint_filter_schema.endpoint_group_update + self.create_endpoint_grp_validator = validators.SchemaValidator(create) + self.update_endpoint_grp_validator = validators.SchemaValidator(update) + + def test_validate_endpoint_group_request_succeeds(self): + """Test that we validate an endpoint group request.""" + request_to_validate = {'description': 'endpoint group description', + 'filters': {'interface': 'admin'}, + 'name': 'endpoint_group_name'} + self.create_endpoint_grp_validator.validate(request_to_validate) + + def test_validate_endpoint_group_create_succeeds_with_req_parameters(self): + """Validate required endpoint group parameters. + + This test ensure that validation succeeds with only the required + parameters passed for creating an endpoint group. + """ + request_to_validate = {'filters': {'interface': 'admin'}, + 'name': 'endpoint_group_name'} + self.create_endpoint_grp_validator.validate(request_to_validate) + + def test_validate_endpoint_group_create_succeeds_with_valid_filters(self): + """Validate dict values as `filters` in endpoint group create requests. + """ + request_to_validate = {'description': 'endpoint group description', + 'name': 'endpoint_group_name'} + for valid_filters in _VALID_FILTERS: + request_to_validate['filters'] = valid_filters + self.create_endpoint_grp_validator.validate(request_to_validate) + + def test_validate_create_endpoint_group_fails_with_invalid_filters(self): + """Validate invalid `filters` value in endpoint group parameters. + + This test ensures that exception is raised when non-dict values is + used as `filters` in endpoint group create request. + """ + request_to_validate = {'description': 'endpoint group description', + 'name': 'endpoint_group_name'} + for invalid_filters in _INVALID_FILTERS: + request_to_validate['filters'] = invalid_filters + self.assertRaises(exception.SchemaValidationError, + self.create_endpoint_grp_validator.validate, + request_to_validate) + + def test_validate_endpoint_group_create_fails_without_name(self): + """Exception raised when `name` isn't in endpoint group request.""" + request_to_validate = {'description': 'endpoint group description', + 'filters': {'interface': 'admin'}} + self.assertRaises(exception.SchemaValidationError, + self.create_endpoint_grp_validator.validate, + request_to_validate) + + def test_validate_endpoint_group_create_fails_without_filters(self): + """Exception raised when `filters` isn't in endpoint group request.""" + request_to_validate = {'description': 'endpoint group description', + 'name': 'endpoint_group_name'} + self.assertRaises(exception.SchemaValidationError, + self.create_endpoint_grp_validator.validate, + request_to_validate) + + def test_validate_endpoint_group_update_request_succeeds(self): + """Test that we validate an endpoint group update request.""" + request_to_validate = {'description': 'endpoint group description', + 'filters': {'interface': 'admin'}, + 'name': 'endpoint_group_name'} + self.update_endpoint_grp_validator.validate(request_to_validate) + + def test_validate_endpoint_group_update_fails_with_no_parameters(self): + """Exception raised when no parameters on endpoint group update.""" + request_to_validate = {} + self.assertRaises(exception.SchemaValidationError, + self.update_endpoint_grp_validator.validate, + request_to_validate) + + def test_validate_endpoint_group_update_succeeds_with_name(self): + """Validate request with only `name` in endpoint group update. + + This test ensures that passing only a `name` passes validation + on update endpoint group request. + """ + request_to_validate = {'name': 'endpoint_group_name'} + self.update_endpoint_grp_validator.validate(request_to_validate) + + def test_validate_endpoint_group_update_succeeds_with_valid_filters(self): + """Validate `filters` as dict values.""" + for valid_filters in _VALID_FILTERS: + request_to_validate = {'filters': valid_filters} + self.update_endpoint_grp_validator.validate(request_to_validate) + + def test_validate_endpoint_group_update_fails_with_invalid_filters(self): + """Exception raised when passing invalid `filters` in request.""" + for invalid_filters in _INVALID_FILTERS: + request_to_validate = {'filters': invalid_filters} + self.assertRaises(exception.SchemaValidationError, + self.update_endpoint_grp_validator.validate, + request_to_validate) + + +class TrustValidationTestCase(testtools.TestCase): + """Test for V3 Trust API validation.""" + + _valid_roles = ['member', uuid.uuid4().hex, str(uuid.uuid4())] + _invalid_roles = [False, True, 123, None] + + def setUp(self): + super(TrustValidationTestCase, self).setUp() + + create = trust_schema.trust_create + self.create_trust_validator = validators.SchemaValidator(create) + + def test_validate_trust_succeeds(self): + """Test that we can validate a trust request.""" + request_to_validate = {'trustor_user_id': uuid.uuid4().hex, + 'trustee_user_id': uuid.uuid4().hex, + 'impersonation': False} + self.create_trust_validator.validate(request_to_validate) + + def test_validate_trust_with_all_parameters_succeeds(self): + """Test that we can validate a trust request with all parameters.""" + request_to_validate = {'trustor_user_id': uuid.uuid4().hex, + 'trustee_user_id': uuid.uuid4().hex, + 'impersonation': False, + 'project_id': uuid.uuid4().hex, + 'roles': [uuid.uuid4().hex, uuid.uuid4().hex], + 'expires_at': 'some timestamp', + 'remaining_uses': 2} + self.create_trust_validator.validate(request_to_validate) + + def test_validate_trust_without_trustor_id_fails(self): + """Validate trust request fails without `trustor_id`.""" + request_to_validate = {'trustee_user_id': uuid.uuid4().hex, + 'impersonation': False} + self.assertRaises(exception.SchemaValidationError, + self.create_trust_validator.validate, + request_to_validate) + + def test_validate_trust_without_trustee_id_fails(self): + """Validate trust request fails without `trustee_id`.""" + request_to_validate = {'trusor_user_id': uuid.uuid4().hex, + 'impersonation': False} + self.assertRaises(exception.SchemaValidationError, + self.create_trust_validator.validate, + request_to_validate) + + def test_validate_trust_without_impersonation_fails(self): + """Validate trust request fails without `impersonation`.""" + request_to_validate = {'trustee_user_id': uuid.uuid4().hex, + 'trustor_user_id': uuid.uuid4().hex} + self.assertRaises(exception.SchemaValidationError, + self.create_trust_validator.validate, + request_to_validate) + + def test_validate_trust_with_extra_parameters_succeeds(self): + """Test that we can validate a trust request with extra parameters.""" + request_to_validate = {'trustor_user_id': uuid.uuid4().hex, + 'trustee_user_id': uuid.uuid4().hex, + 'impersonation': False, + 'project_id': uuid.uuid4().hex, + 'roles': [uuid.uuid4().hex, uuid.uuid4().hex], + 'expires_at': 'some timestamp', + 'remaining_uses': 2, + 'extra': 'something extra!'} + self.create_trust_validator.validate(request_to_validate) + + def test_validate_trust_with_invalid_impersonation_fails(self): + """Validate trust request with invalid `impersonation` fails.""" + request_to_validate = {'trustor_user_id': uuid.uuid4().hex, + 'trustee_user_id': uuid.uuid4().hex, + 'impersonation': 2} + self.assertRaises(exception.SchemaValidationError, + self.create_trust_validator.validate, + request_to_validate) + + def test_validate_trust_with_null_remaining_uses_succeeds(self): + """Validate trust request with null `remaining_uses`.""" + request_to_validate = {'trustor_user_id': uuid.uuid4().hex, + 'trustee_user_id': uuid.uuid4().hex, + 'impersonation': False, + 'remaining_uses': None} + self.create_trust_validator.validate(request_to_validate) + + def test_validate_trust_with_remaining_uses_succeeds(self): + """Validate trust request with `remaining_uses` succeeds.""" + request_to_validate = {'trustor_user_id': uuid.uuid4().hex, + 'trustee_user_id': uuid.uuid4().hex, + 'impersonation': False, + 'remaining_uses': 2} + self.create_trust_validator.validate(request_to_validate) + + def test_validate_trust_with_invalid_expires_at_fails(self): + """Validate trust request with invalid `expires_at` fails.""" + request_to_validate = {'trustor_user_id': uuid.uuid4().hex, + 'trustee_user_id': uuid.uuid4().hex, + 'impersonation': False, + 'expires_at': 3} + self.assertRaises(exception.SchemaValidationError, + self.create_trust_validator.validate, + request_to_validate) + + def test_validate_trust_with_role_types_succeeds(self): + """Validate trust request with `roles` succeeds.""" + for role in self._valid_roles: + request_to_validate = {'trustor_user_id': uuid.uuid4().hex, + 'trustee_user_id': uuid.uuid4().hex, + 'impersonation': False, + 'roles': [role]} + self.create_trust_validator.validate(request_to_validate) + + def test_validate_trust_with_invalid_role_type_fails(self): + """Validate trust request with invalid `roles` fails.""" + for role in self._invalid_roles: + request_to_validate = {'trustor_user_id': uuid.uuid4().hex, + 'trustee_user_id': uuid.uuid4().hex, + 'impersonation': False, + 'roles': role} + self.assertRaises(exception.SchemaValidationError, + self.create_trust_validator.validate, + request_to_validate) + + def test_validate_trust_with_list_of_valid_roles_succeeds(self): + """Validate trust request with a list of valid `roles`.""" + request_to_validate = {'trustor_user_id': uuid.uuid4().hex, + 'trustee_user_id': uuid.uuid4().hex, + 'impersonation': False, + 'roles': self._valid_roles} + self.create_trust_validator.validate(request_to_validate) + + +class ServiceProviderValidationTestCase(testtools.TestCase): + """Test for V3 Service Provider API validation.""" + + def setUp(self): + super(ServiceProviderValidationTestCase, self).setUp() + + self.valid_auth_url = 'https://' + uuid.uuid4().hex + '.com' + self.valid_sp_url = 'https://' + uuid.uuid4().hex + '.com' + + create = federation_schema.service_provider_create + update = federation_schema.service_provider_update + self.create_sp_validator = validators.SchemaValidator(create) + self.update_sp_validator = validators.SchemaValidator(update) + + def test_validate_sp_request(self): + """Test that we validate `auth_url` and `sp_url` in request.""" + request_to_validate = { + 'auth_url': self.valid_auth_url, + 'sp_url': self.valid_sp_url + } + self.create_sp_validator.validate(request_to_validate) + + def test_validate_sp_request_with_invalid_auth_url_fails(self): + """Validate request fails with invalid `auth_url`.""" + request_to_validate = { + 'auth_url': uuid.uuid4().hex, + 'sp_url': self.valid_sp_url + } + self.assertRaises(exception.SchemaValidationError, + self.create_sp_validator.validate, + request_to_validate) + + def test_validate_sp_request_with_invalid_sp_url_fails(self): + """Validate request fails with invalid `sp_url`.""" + request_to_validate = { + 'auth_url': self.valid_auth_url, + 'sp_url': uuid.uuid4().hex, + } + self.assertRaises(exception.SchemaValidationError, + self.create_sp_validator.validate, + request_to_validate) + + def test_validate_sp_request_without_auth_url_fails(self): + """Validate request fails without `auth_url`.""" + request_to_validate = { + 'sp_url': self.valid_sp_url + } + self.assertRaises(exception.SchemaValidationError, + self.create_sp_validator.validate, + request_to_validate) + request_to_validate = { + 'auth_url': None, + 'sp_url': self.valid_sp_url + } + self.assertRaises(exception.SchemaValidationError, + self.create_sp_validator.validate, + request_to_validate) + + def test_validate_sp_request_without_sp_url_fails(self): + """Validate request fails without `sp_url`.""" + request_to_validate = { + 'auth_url': self.valid_auth_url, + } + self.assertRaises(exception.SchemaValidationError, + self.create_sp_validator.validate, + request_to_validate) + request_to_validate = { + 'auth_url': self.valid_auth_url, + 'sp_url': None, + } + self.assertRaises(exception.SchemaValidationError, + self.create_sp_validator.validate, + request_to_validate) + + def test_validate_sp_request_with_enabled(self): + """Validate `enabled` as boolean-like values.""" + for valid_enabled in _VALID_ENABLED_FORMATS: + request_to_validate = { + 'auth_url': self.valid_auth_url, + 'sp_url': self.valid_sp_url, + 'enabled': valid_enabled + } + self.create_sp_validator.validate(request_to_validate) + + def test_validate_sp_request_with_invalid_enabled_fails(self): + """Exception is raised when `enabled` isn't a boolean-like value.""" + for invalid_enabled in _INVALID_ENABLED_FORMATS: + request_to_validate = { + 'auth_url': self.valid_auth_url, + 'sp_url': self.valid_sp_url, + 'enabled': invalid_enabled + } + self.assertRaises(exception.SchemaValidationError, + self.create_sp_validator.validate, + request_to_validate) + + def test_validate_sp_request_with_valid_description(self): + """Test that we validate `description` in create requests.""" + request_to_validate = { + 'auth_url': self.valid_auth_url, + 'sp_url': self.valid_sp_url, + 'description': 'My Service Provider' + } + self.create_sp_validator.validate(request_to_validate) + + def test_validate_sp_request_with_invalid_description_fails(self): + """Exception is raised when `description` as a non-string value.""" + request_to_validate = { + 'auth_url': self.valid_auth_url, + 'sp_url': self.valid_sp_url, + 'description': False + } + self.assertRaises(exception.SchemaValidationError, + self.create_sp_validator.validate, + request_to_validate) + + def test_validate_sp_request_with_extra_field_fails(self): + """Exception raised when passing extra fields in the body.""" + # 'id' can't be passed in the body since it is passed in the URL + request_to_validate = { + 'id': 'ACME', + 'auth_url': self.valid_auth_url, + 'sp_url': self.valid_sp_url, + 'description': 'My Service Provider' + } + self.assertRaises(exception.SchemaValidationError, + self.create_sp_validator.validate, + request_to_validate) + + def test_validate_sp_update_request(self): + """Test that we validate a update request.""" + request_to_validate = {'description': uuid.uuid4().hex} + self.update_sp_validator.validate(request_to_validate) + + def test_validate_sp_update_request_with_no_parameters_fails(self): + """Exception is raised when updating without parameters.""" + request_to_validate = {} + self.assertRaises(exception.SchemaValidationError, + self.update_sp_validator.validate, + request_to_validate) + + def test_validate_sp_update_request_with_invalid_auth_url_fails(self): + """Exception raised when updating with invalid `auth_url`.""" + request_to_validate = {'auth_url': uuid.uuid4().hex} + self.assertRaises(exception.SchemaValidationError, + self.update_sp_validator.validate, + request_to_validate) + request_to_validate = {'auth_url': None} + self.assertRaises(exception.SchemaValidationError, + self.update_sp_validator.validate, + request_to_validate) + + def test_validate_sp_update_request_with_invalid_sp_url_fails(self): + """Exception raised when updating with invalid `sp_url`.""" + request_to_validate = {'sp_url': uuid.uuid4().hex} + self.assertRaises(exception.SchemaValidationError, + self.update_sp_validator.validate, + request_to_validate) + request_to_validate = {'sp_url': None} + self.assertRaises(exception.SchemaValidationError, + self.update_sp_validator.validate, + request_to_validate) diff --git a/keystone-moon/keystone/tests/unit/test_versions.py b/keystone-moon/keystone/tests/unit/test_versions.py new file mode 100644 index 00000000..6fe692ad --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_versions.py @@ -0,0 +1,1051 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 +import functools +import random + +import mock +from oslo_config import cfg +from oslo_serialization import jsonutils +from testtools import matchers as tt_matchers + +from keystone.common import json_home +from keystone import controllers +from keystone.tests import unit as tests + + +CONF = cfg.CONF + +v2_MEDIA_TYPES = [ + { + "base": "application/json", + "type": "application/" + "vnd.openstack.identity-v2.0+json" + } +] + +v2_HTML_DESCRIPTION = { + "rel": "describedby", + "type": "text/html", + "href": "http://docs.openstack.org/" +} + + +v2_EXPECTED_RESPONSE = { + "id": "v2.0", + "status": "stable", + "updated": "2014-04-17T00:00:00Z", + "links": [ + { + "rel": "self", + "href": "", # Will get filled in after initialization + }, + v2_HTML_DESCRIPTION + ], + "media-types": v2_MEDIA_TYPES +} + +v2_VERSION_RESPONSE = { + "version": v2_EXPECTED_RESPONSE +} + +v3_MEDIA_TYPES = [ + { + "base": "application/json", + "type": "application/" + "vnd.openstack.identity-v3+json" + } +] + +v3_EXPECTED_RESPONSE = { + "id": "v3.0", + "status": "stable", + "updated": "2013-03-06T00:00:00Z", + "links": [ + { + "rel": "self", + "href": "", # Will get filled in after initialization + } + ], + "media-types": v3_MEDIA_TYPES +} + +v3_VERSION_RESPONSE = { + "version": v3_EXPECTED_RESPONSE +} + +VERSIONS_RESPONSE = { + "versions": { + "values": [ + v3_EXPECTED_RESPONSE, + v2_EXPECTED_RESPONSE + ] + } +} + +_build_ec2tokens_relation = functools.partial( + json_home.build_v3_extension_resource_relation, extension_name='OS-EC2', + extension_version='1.0') + +REVOCATIONS_RELATION = json_home.build_v3_extension_resource_relation( + 'OS-PKI', '1.0', 'revocations') + +_build_simple_cert_relation = functools.partial( + json_home.build_v3_extension_resource_relation, + extension_name='OS-SIMPLE-CERT', extension_version='1.0') + +_build_trust_relation = functools.partial( + json_home.build_v3_extension_resource_relation, extension_name='OS-TRUST', + extension_version='1.0') + +_build_federation_rel = functools.partial( + json_home.build_v3_extension_resource_relation, + extension_name='OS-FEDERATION', + extension_version='1.0') + +_build_oauth1_rel = functools.partial( + json_home.build_v3_extension_resource_relation, + extension_name='OS-OAUTH1', extension_version='1.0') + +_build_ep_policy_rel = functools.partial( + json_home.build_v3_extension_resource_relation, + extension_name='OS-ENDPOINT-POLICY', extension_version='1.0') + +_build_ep_filter_rel = functools.partial( + json_home.build_v3_extension_resource_relation, + extension_name='OS-EP-FILTER', extension_version='1.0') + +TRUST_ID_PARAMETER_RELATION = json_home.build_v3_extension_parameter_relation( + 'OS-TRUST', '1.0', 'trust_id') + +IDP_ID_PARAMETER_RELATION = json_home.build_v3_extension_parameter_relation( + 'OS-FEDERATION', '1.0', 'idp_id') + +PROTOCOL_ID_PARAM_RELATION = json_home.build_v3_extension_parameter_relation( + 'OS-FEDERATION', '1.0', 'protocol_id') + +MAPPING_ID_PARAM_RELATION = json_home.build_v3_extension_parameter_relation( + 'OS-FEDERATION', '1.0', 'mapping_id') + +SP_ID_PARAMETER_RELATION = json_home.build_v3_extension_parameter_relation( + 'OS-FEDERATION', '1.0', 'sp_id') + +CONSUMER_ID_PARAMETER_RELATION = ( + json_home.build_v3_extension_parameter_relation( + 'OS-OAUTH1', '1.0', 'consumer_id')) + +REQUEST_TOKEN_ID_PARAMETER_RELATION = ( + json_home.build_v3_extension_parameter_relation( + 'OS-OAUTH1', '1.0', 'request_token_id')) + +ACCESS_TOKEN_ID_PARAMETER_RELATION = ( + json_home.build_v3_extension_parameter_relation( + 'OS-OAUTH1', '1.0', 'access_token_id')) + +ENDPOINT_GROUP_ID_PARAMETER_RELATION = ( + json_home.build_v3_extension_parameter_relation( + 'OS-EP-FILTER', '1.0', 'endpoint_group_id')) + +BASE_IDP_PROTOCOL = '/OS-FEDERATION/identity_providers/{idp_id}/protocols' +BASE_EP_POLICY = '/policies/{policy_id}/OS-ENDPOINT-POLICY' +BASE_EP_FILTER = '/OS-EP-FILTER/endpoint_groups/{endpoint_group_id}' +BASE_ACCESS_TOKEN = ( + '/users/{user_id}/OS-OAUTH1/access_tokens/{access_token_id}') + +# TODO(stevemar): Use BASE_IDP_PROTOCOL when bug 1420125 is resolved. +FEDERATED_AUTH_URL = ('/OS-FEDERATION/identity_providers/{identity_provider}' + '/protocols/{protocol}/auth') + +V3_JSON_HOME_RESOURCES_INHERIT_DISABLED = { + json_home.build_v3_resource_relation('auth_tokens'): { + 'href': '/auth/tokens'}, + json_home.build_v3_resource_relation('auth_catalog'): { + 'href': '/auth/catalog'}, + json_home.build_v3_resource_relation('auth_projects'): { + 'href': '/auth/projects'}, + json_home.build_v3_resource_relation('auth_domains'): { + 'href': '/auth/domains'}, + json_home.build_v3_resource_relation('credential'): { + 'href-template': '/credentials/{credential_id}', + 'href-vars': { + 'credential_id': + json_home.build_v3_parameter_relation('credential_id')}}, + json_home.build_v3_resource_relation('credentials'): { + 'href': '/credentials'}, + json_home.build_v3_resource_relation('domain'): { + 'href-template': '/domains/{domain_id}', + 'href-vars': {'domain_id': json_home.Parameters.DOMAIN_ID, }}, + json_home.build_v3_resource_relation('domain_group_role'): { + 'href-template': + '/domains/{domain_id}/groups/{group_id}/roles/{role_id}', + 'href-vars': { + 'domain_id': json_home.Parameters.DOMAIN_ID, + 'group_id': json_home.Parameters.GROUP_ID, + 'role_id': json_home.Parameters.ROLE_ID, }}, + json_home.build_v3_resource_relation('domain_group_roles'): { + 'href-template': '/domains/{domain_id}/groups/{group_id}/roles', + 'href-vars': { + 'domain_id': json_home.Parameters.DOMAIN_ID, + 'group_id': json_home.Parameters.GROUP_ID}}, + json_home.build_v3_resource_relation('domain_user_role'): { + 'href-template': + '/domains/{domain_id}/users/{user_id}/roles/{role_id}', + 'href-vars': { + 'domain_id': json_home.Parameters.DOMAIN_ID, + 'role_id': json_home.Parameters.ROLE_ID, + 'user_id': json_home.Parameters.USER_ID, }}, + json_home.build_v3_resource_relation('domain_user_roles'): { + 'href-template': '/domains/{domain_id}/users/{user_id}/roles', + 'href-vars': { + 'domain_id': json_home.Parameters.DOMAIN_ID, + 'user_id': json_home.Parameters.USER_ID, }}, + json_home.build_v3_resource_relation('domains'): {'href': '/domains'}, + json_home.build_v3_resource_relation('endpoint'): { + 'href-template': '/endpoints/{endpoint_id}', + 'href-vars': { + 'endpoint_id': + json_home.build_v3_parameter_relation('endpoint_id'), }}, + json_home.build_v3_resource_relation('endpoints'): { + 'href': '/endpoints'}, + _build_ec2tokens_relation(resource_name='ec2tokens'): { + 'href': '/ec2tokens'}, + _build_ec2tokens_relation(resource_name='user_credential'): { + 'href-template': '/users/{user_id}/credentials/OS-EC2/{credential_id}', + 'href-vars': { + 'credential_id': json_home.build_v3_extension_parameter_relation( + 'OS-EC2', '1.0', 'credential_id'), + 'user_id': json_home.Parameters.USER_ID, }}, + _build_ec2tokens_relation(resource_name='user_credentials'): { + 'href-template': '/users/{user_id}/credentials/OS-EC2', + 'href-vars': { + 'user_id': json_home.Parameters.USER_ID, }}, + REVOCATIONS_RELATION: { + 'href': '/auth/tokens/OS-PKI/revoked'}, + 'http://docs.openstack.org/api/openstack-identity/3/ext/OS-REVOKE/1.0/rel/' + 'events': { + 'href': '/OS-REVOKE/events'}, + _build_simple_cert_relation(resource_name='ca_certificate'): { + 'href': '/OS-SIMPLE-CERT/ca'}, + _build_simple_cert_relation(resource_name='certificates'): { + 'href': '/OS-SIMPLE-CERT/certificates'}, + _build_trust_relation(resource_name='trust'): + { + 'href-template': '/OS-TRUST/trusts/{trust_id}', + 'href-vars': {'trust_id': TRUST_ID_PARAMETER_RELATION, }}, + _build_trust_relation(resource_name='trust_role'): { + 'href-template': '/OS-TRUST/trusts/{trust_id}/roles/{role_id}', + 'href-vars': { + 'role_id': json_home.Parameters.ROLE_ID, + 'trust_id': TRUST_ID_PARAMETER_RELATION, }}, + _build_trust_relation(resource_name='trust_roles'): { + 'href-template': '/OS-TRUST/trusts/{trust_id}/roles', + 'href-vars': {'trust_id': TRUST_ID_PARAMETER_RELATION, }}, + _build_trust_relation(resource_name='trusts'): { + 'href': '/OS-TRUST/trusts'}, + 'http://docs.openstack.org/api/openstack-identity/3/ext/s3tokens/1.0/rel/' + 's3tokens': { + 'href': '/s3tokens'}, + json_home.build_v3_resource_relation('group'): { + 'href-template': '/groups/{group_id}', + 'href-vars': { + 'group_id': json_home.Parameters.GROUP_ID, }}, + json_home.build_v3_resource_relation('group_user'): { + 'href-template': '/groups/{group_id}/users/{user_id}', + 'href-vars': { + 'group_id': json_home.Parameters.GROUP_ID, + 'user_id': json_home.Parameters.USER_ID, }}, + json_home.build_v3_resource_relation('group_users'): { + 'href-template': '/groups/{group_id}/users', + 'href-vars': {'group_id': json_home.Parameters.GROUP_ID, }}, + json_home.build_v3_resource_relation('groups'): {'href': '/groups'}, + json_home.build_v3_resource_relation('policies'): { + 'href': '/policies'}, + json_home.build_v3_resource_relation('policy'): { + 'href-template': '/policies/{policy_id}', + 'href-vars': { + 'policy_id': + json_home.build_v3_parameter_relation('policy_id'), }}, + json_home.build_v3_resource_relation('project'): { + 'href-template': '/projects/{project_id}', + 'href-vars': { + 'project_id': json_home.Parameters.PROJECT_ID, }}, + json_home.build_v3_resource_relation('project_group_role'): { + 'href-template': + '/projects/{project_id}/groups/{group_id}/roles/{role_id}', + 'href-vars': { + 'group_id': json_home.Parameters.GROUP_ID, + 'project_id': json_home.Parameters.PROJECT_ID, + 'role_id': json_home.Parameters.ROLE_ID, }}, + json_home.build_v3_resource_relation('project_group_roles'): { + 'href-template': '/projects/{project_id}/groups/{group_id}/roles', + 'href-vars': { + 'group_id': json_home.Parameters.GROUP_ID, + 'project_id': json_home.Parameters.PROJECT_ID, }}, + json_home.build_v3_resource_relation('project_user_role'): { + 'href-template': + '/projects/{project_id}/users/{user_id}/roles/{role_id}', + 'href-vars': { + 'project_id': json_home.Parameters.PROJECT_ID, + 'role_id': json_home.Parameters.ROLE_ID, + 'user_id': json_home.Parameters.USER_ID, }}, + json_home.build_v3_resource_relation('project_user_roles'): { + 'href-template': '/projects/{project_id}/users/{user_id}/roles', + 'href-vars': { + 'project_id': json_home.Parameters.PROJECT_ID, + 'user_id': json_home.Parameters.USER_ID, }}, + json_home.build_v3_resource_relation('projects'): { + 'href': '/projects'}, + json_home.build_v3_resource_relation('region'): { + 'href-template': '/regions/{region_id}', + 'href-vars': { + 'region_id': + json_home.build_v3_parameter_relation('region_id'), }}, + json_home.build_v3_resource_relation('regions'): {'href': '/regions'}, + json_home.build_v3_resource_relation('role'): { + 'href-template': '/roles/{role_id}', + 'href-vars': { + 'role_id': json_home.Parameters.ROLE_ID, }}, + json_home.build_v3_resource_relation('role_assignments'): { + 'href': '/role_assignments'}, + json_home.build_v3_resource_relation('roles'): {'href': '/roles'}, + json_home.build_v3_resource_relation('service'): { + 'href-template': '/services/{service_id}', + 'href-vars': { + 'service_id': + json_home.build_v3_parameter_relation('service_id')}}, + json_home.build_v3_resource_relation('services'): { + 'href': '/services'}, + json_home.build_v3_resource_relation('user'): { + 'href-template': '/users/{user_id}', + 'href-vars': { + 'user_id': json_home.Parameters.USER_ID, }}, + json_home.build_v3_resource_relation('user_change_password'): { + 'href-template': '/users/{user_id}/password', + 'href-vars': {'user_id': json_home.Parameters.USER_ID, }}, + json_home.build_v3_resource_relation('user_groups'): { + 'href-template': '/users/{user_id}/groups', + 'href-vars': {'user_id': json_home.Parameters.USER_ID, }}, + json_home.build_v3_resource_relation('user_projects'): { + 'href-template': '/users/{user_id}/projects', + 'href-vars': {'user_id': json_home.Parameters.USER_ID, }}, + json_home.build_v3_resource_relation('users'): {'href': '/users'}, + _build_federation_rel(resource_name='domains'): { + 'href': '/OS-FEDERATION/domains'}, + _build_federation_rel(resource_name='websso'): { + 'href-template': '/auth/OS-FEDERATION/websso/{protocol_id}', + 'href-vars': { + 'protocol_id': PROTOCOL_ID_PARAM_RELATION, }}, + _build_federation_rel(resource_name='projects'): { + 'href': '/OS-FEDERATION/projects'}, + _build_federation_rel(resource_name='saml2'): { + 'href': '/auth/OS-FEDERATION/saml2'}, + _build_federation_rel(resource_name='metadata'): { + 'href': '/OS-FEDERATION/saml2/metadata'}, + _build_federation_rel(resource_name='identity_providers'): { + 'href': '/OS-FEDERATION/identity_providers'}, + _build_federation_rel(resource_name='service_providers'): { + 'href': '/OS-FEDERATION/service_providers'}, + _build_federation_rel(resource_name='mappings'): { + 'href': '/OS-FEDERATION/mappings'}, + _build_federation_rel(resource_name='identity_provider'): + { + 'href-template': '/OS-FEDERATION/identity_providers/{idp_id}', + 'href-vars': {'idp_id': IDP_ID_PARAMETER_RELATION, }}, + _build_federation_rel(resource_name='service_provider'): + { + 'href-template': '/OS-FEDERATION/service_providers/{sp_id}', + 'href-vars': {'sp_id': SP_ID_PARAMETER_RELATION, }}, + _build_federation_rel(resource_name='mapping'): + { + 'href-template': '/OS-FEDERATION/mappings/{mapping_id}', + 'href-vars': {'mapping_id': MAPPING_ID_PARAM_RELATION, }}, + _build_federation_rel(resource_name='identity_provider_protocol'): { + 'href-template': BASE_IDP_PROTOCOL + '/{protocol_id}', + 'href-vars': { + 'idp_id': IDP_ID_PARAMETER_RELATION, + 'protocol_id': PROTOCOL_ID_PARAM_RELATION, }}, + _build_federation_rel(resource_name='identity_provider_protocols'): { + 'href-template': BASE_IDP_PROTOCOL, + 'href-vars': { + 'idp_id': IDP_ID_PARAMETER_RELATION}}, + # TODO(stevemar): Update href-vars when bug 1420125 is resolved. + _build_federation_rel(resource_name='identity_provider_protocol_auth'): { + 'href-template': FEDERATED_AUTH_URL, + 'href-vars': { + 'identity_provider': IDP_ID_PARAMETER_RELATION, + 'protocol': PROTOCOL_ID_PARAM_RELATION, }}, + _build_oauth1_rel(resource_name='access_tokens'): { + 'href': '/OS-OAUTH1/access_token'}, + _build_oauth1_rel(resource_name='request_tokens'): { + 'href': '/OS-OAUTH1/request_token'}, + _build_oauth1_rel(resource_name='consumers'): { + 'href': '/OS-OAUTH1/consumers'}, + _build_oauth1_rel(resource_name='authorize_request_token'): + { + 'href-template': '/OS-OAUTH1/authorize/{request_token_id}', + 'href-vars': {'request_token_id': + REQUEST_TOKEN_ID_PARAMETER_RELATION, }}, + _build_oauth1_rel(resource_name='consumer'): + { + 'href-template': '/OS-OAUTH1/consumers/{consumer_id}', + 'href-vars': {'consumer_id': CONSUMER_ID_PARAMETER_RELATION, }}, + _build_oauth1_rel(resource_name='user_access_token'): + { + 'href-template': BASE_ACCESS_TOKEN, + 'href-vars': {'user_id': json_home.Parameters.USER_ID, + 'access_token_id': + ACCESS_TOKEN_ID_PARAMETER_RELATION, }}, + _build_oauth1_rel(resource_name='user_access_tokens'): + { + 'href-template': '/users/{user_id}/OS-OAUTH1/access_tokens', + 'href-vars': {'user_id': json_home.Parameters.USER_ID, }}, + _build_oauth1_rel(resource_name='user_access_token_role'): + { + 'href-template': BASE_ACCESS_TOKEN + '/roles/{role_id}', + 'href-vars': {'user_id': json_home.Parameters.USER_ID, + 'role_id': json_home.Parameters.ROLE_ID, + 'access_token_id': + ACCESS_TOKEN_ID_PARAMETER_RELATION, }}, + _build_oauth1_rel(resource_name='user_access_token_roles'): + { + 'href-template': BASE_ACCESS_TOKEN + '/roles', + 'href-vars': {'user_id': json_home.Parameters.USER_ID, + 'access_token_id': + ACCESS_TOKEN_ID_PARAMETER_RELATION, }}, + _build_ep_policy_rel(resource_name='endpoint_policy'): + { + 'href-template': '/endpoints/{endpoint_id}/OS-ENDPOINT-POLICY/policy', + 'href-vars': {'endpoint_id': json_home.Parameters.ENDPOINT_ID, }}, + _build_ep_policy_rel(resource_name='endpoint_policy_association'): + { + 'href-template': BASE_EP_POLICY + '/endpoints/{endpoint_id}', + 'href-vars': {'endpoint_id': json_home.Parameters.ENDPOINT_ID, + 'policy_id': json_home.Parameters.POLICY_ID, }}, + _build_ep_policy_rel(resource_name='policy_endpoints'): + { + 'href-template': BASE_EP_POLICY + '/endpoints', + 'href-vars': {'policy_id': json_home.Parameters.POLICY_ID, }}, + _build_ep_policy_rel( + resource_name='region_and_service_policy_association'): + { + 'href-template': (BASE_EP_POLICY + + '/services/{service_id}/regions/{region_id}'), + 'href-vars': {'policy_id': json_home.Parameters.POLICY_ID, + 'service_id': json_home.Parameters.SERVICE_ID, + 'region_id': json_home.Parameters.REGION_ID, }}, + _build_ep_policy_rel(resource_name='service_policy_association'): + { + 'href-template': BASE_EP_POLICY + '/services/{service_id}', + 'href-vars': {'policy_id': json_home.Parameters.POLICY_ID, + 'service_id': json_home.Parameters.SERVICE_ID, }}, + _build_ep_filter_rel(resource_name='endpoint_group'): + { + 'href-template': '/OS-EP-FILTER/endpoint_groups/{endpoint_group_id}', + 'href-vars': {'endpoint_group_id': + ENDPOINT_GROUP_ID_PARAMETER_RELATION, }}, + _build_ep_filter_rel( + resource_name='endpoint_group_to_project_association'): + { + 'href-template': BASE_EP_FILTER + '/projects/{project_id}', + 'href-vars': {'endpoint_group_id': + ENDPOINT_GROUP_ID_PARAMETER_RELATION, + 'project_id': json_home.Parameters.PROJECT_ID, }}, + _build_ep_filter_rel(resource_name='endpoint_groups'): + {'href': '/OS-EP-FILTER/endpoint_groups'}, + _build_ep_filter_rel(resource_name='endpoint_projects'): + { + 'href-template': '/OS-EP-FILTER/endpoints/{endpoint_id}/projects', + 'href-vars': {'endpoint_id': json_home.Parameters.ENDPOINT_ID, }}, + _build_ep_filter_rel(resource_name='endpoints_in_endpoint_group'): + { + 'href-template': BASE_EP_FILTER + '/endpoints', + 'href-vars': {'endpoint_group_id': + ENDPOINT_GROUP_ID_PARAMETER_RELATION, }}, + _build_ep_filter_rel(resource_name='project_endpoint'): + { + 'href-template': ('/OS-EP-FILTER/projects/{project_id}' + '/endpoints/{endpoint_id}'), + 'href-vars': {'endpoint_id': json_home.Parameters.ENDPOINT_ID, + 'project_id': json_home.Parameters.PROJECT_ID, }}, + _build_ep_filter_rel(resource_name='project_endpoints'): + { + 'href-template': '/OS-EP-FILTER/projects/{project_id}/endpoints', + 'href-vars': {'project_id': json_home.Parameters.PROJECT_ID, }}, + _build_ep_filter_rel( + resource_name='projects_associated_with_endpoint_group'): + { + 'href-template': BASE_EP_FILTER + '/projects', + 'href-vars': {'endpoint_group_id': + ENDPOINT_GROUP_ID_PARAMETER_RELATION, }}, + json_home.build_v3_resource_relation('domain_config'): { + 'href-template': + '/domains/{domain_id}/config', + 'href-vars': { + 'domain_id': json_home.Parameters.DOMAIN_ID}, + 'hints': {'status': 'experimental'}}, + json_home.build_v3_resource_relation('domain_config_group'): { + 'href-template': + '/domains/{domain_id}/config/{group}', + 'href-vars': { + 'domain_id': json_home.Parameters.DOMAIN_ID, + 'group': json_home.build_v3_parameter_relation('config_group')}, + 'hints': {'status': 'experimental'}}, + json_home.build_v3_resource_relation('domain_config_option'): { + 'href-template': + '/domains/{domain_id}/config/{group}/{option}', + 'href-vars': { + 'domain_id': json_home.Parameters.DOMAIN_ID, + 'group': json_home.build_v3_parameter_relation('config_group'), + 'option': json_home.build_v3_parameter_relation('config_option')}, + 'hints': {'status': 'experimental'}}, +} + + +# with os-inherit enabled, there's some more resources. + +build_os_inherit_relation = functools.partial( + json_home.build_v3_extension_resource_relation, + extension_name='OS-INHERIT', extension_version='1.0') + +V3_JSON_HOME_RESOURCES_INHERIT_ENABLED = dict( + V3_JSON_HOME_RESOURCES_INHERIT_DISABLED) +V3_JSON_HOME_RESOURCES_INHERIT_ENABLED.update( + ( + ( + build_os_inherit_relation( + resource_name='domain_user_role_inherited_to_projects'), + { + 'href-template': '/OS-INHERIT/domains/{domain_id}/users/' + '{user_id}/roles/{role_id}/inherited_to_projects', + 'href-vars': { + 'domain_id': json_home.Parameters.DOMAIN_ID, + 'role_id': json_home.Parameters.ROLE_ID, + 'user_id': json_home.Parameters.USER_ID, + }, + } + ), + ( + build_os_inherit_relation( + resource_name='domain_group_role_inherited_to_projects'), + { + 'href-template': '/OS-INHERIT/domains/{domain_id}/groups/' + '{group_id}/roles/{role_id}/inherited_to_projects', + 'href-vars': { + 'domain_id': json_home.Parameters.DOMAIN_ID, + 'group_id': json_home.Parameters.GROUP_ID, + 'role_id': json_home.Parameters.ROLE_ID, + }, + } + ), + ( + build_os_inherit_relation( + resource_name='domain_user_roles_inherited_to_projects'), + { + 'href-template': '/OS-INHERIT/domains/{domain_id}/users/' + '{user_id}/roles/inherited_to_projects', + 'href-vars': { + 'domain_id': json_home.Parameters.DOMAIN_ID, + 'user_id': json_home.Parameters.USER_ID, + }, + } + ), + ( + build_os_inherit_relation( + resource_name='domain_group_roles_inherited_to_projects'), + { + 'href-template': '/OS-INHERIT/domains/{domain_id}/groups/' + '{group_id}/roles/inherited_to_projects', + 'href-vars': { + 'domain_id': json_home.Parameters.DOMAIN_ID, + 'group_id': json_home.Parameters.GROUP_ID, + }, + } + ), + ( + build_os_inherit_relation( + resource_name='project_user_role_inherited_to_projects'), + { + 'href-template': '/OS-INHERIT/projects/{project_id}/users/' + '{user_id}/roles/{role_id}/inherited_to_projects', + 'href-vars': { + 'project_id': json_home.Parameters.PROJECT_ID, + 'role_id': json_home.Parameters.ROLE_ID, + 'user_id': json_home.Parameters.USER_ID, + }, + } + ), + ( + build_os_inherit_relation( + resource_name='project_group_role_inherited_to_projects'), + { + 'href-template': '/OS-INHERIT/projects/{project_id}/groups/' + '{group_id}/roles/{role_id}/inherited_to_projects', + 'href-vars': { + 'project_id': json_home.Parameters.PROJECT_ID, + 'group_id': json_home.Parameters.GROUP_ID, + 'role_id': json_home.Parameters.ROLE_ID, + }, + } + ), + ) +) + + +class _VersionsEqual(tt_matchers.MatchesListwise): + def __init__(self, expected): + super(_VersionsEqual, self).__init__([ + tt_matchers.KeysEqual(expected), + tt_matchers.KeysEqual(expected['versions']), + tt_matchers.HasLength(len(expected['versions']['values'])), + tt_matchers.ContainsAll(expected['versions']['values']), + ]) + + def match(self, other): + return super(_VersionsEqual, self).match([ + other, + other['versions'], + other['versions']['values'], + other['versions']['values'], + ]) + + +class VersionTestCase(tests.TestCase): + def setUp(self): + super(VersionTestCase, self).setUp() + self.load_backends() + self.public_app = self.loadapp('keystone', 'main') + self.admin_app = self.loadapp('keystone', 'admin') + + self.config_fixture.config( + public_endpoint='http://localhost:%(public_port)d', + admin_endpoint='http://localhost:%(admin_port)d') + + def config_overrides(self): + super(VersionTestCase, self).config_overrides() + port = random.randint(10000, 30000) + self.config_fixture.config(group='eventlet_server', public_port=port, + admin_port=port) + + def _paste_in_port(self, response, port): + for link in response['links']: + if link['rel'] == 'self': + link['href'] = port + + def test_public_versions(self): + client = tests.TestClient(self.public_app) + resp = client.get('/') + self.assertEqual(300, resp.status_int) + data = jsonutils.loads(resp.body) + expected = VERSIONS_RESPONSE + for version in expected['versions']['values']: + if version['id'] == 'v3.0': + self._paste_in_port( + version, 'http://localhost:%s/v3/' % + CONF.eventlet_server.public_port) + elif version['id'] == 'v2.0': + self._paste_in_port( + version, 'http://localhost:%s/v2.0/' % + CONF.eventlet_server.public_port) + self.assertThat(data, _VersionsEqual(expected)) + + def test_admin_versions(self): + client = tests.TestClient(self.admin_app) + resp = client.get('/') + self.assertEqual(300, resp.status_int) + data = jsonutils.loads(resp.body) + expected = VERSIONS_RESPONSE + for version in expected['versions']['values']: + if version['id'] == 'v3.0': + self._paste_in_port( + version, 'http://localhost:%s/v3/' % + CONF.eventlet_server.admin_port) + elif version['id'] == 'v2.0': + self._paste_in_port( + version, 'http://localhost:%s/v2.0/' % + CONF.eventlet_server.admin_port) + self.assertThat(data, _VersionsEqual(expected)) + + def test_use_site_url_if_endpoint_unset(self): + self.config_fixture.config(public_endpoint=None, admin_endpoint=None) + + for app in (self.public_app, self.admin_app): + client = tests.TestClient(app) + resp = client.get('/') + self.assertEqual(300, resp.status_int) + data = jsonutils.loads(resp.body) + expected = VERSIONS_RESPONSE + for version in expected['versions']['values']: + # localhost happens to be the site url for tests + if version['id'] == 'v3.0': + self._paste_in_port( + version, 'http://localhost/v3/') + elif version['id'] == 'v2.0': + self._paste_in_port( + version, 'http://localhost/v2.0/') + self.assertThat(data, _VersionsEqual(expected)) + + def test_public_version_v2(self): + client = tests.TestClient(self.public_app) + resp = client.get('/v2.0/') + self.assertEqual(200, resp.status_int) + data = jsonutils.loads(resp.body) + expected = v2_VERSION_RESPONSE + self._paste_in_port(expected['version'], + 'http://localhost:%s/v2.0/' % + CONF.eventlet_server.public_port) + self.assertEqual(expected, data) + + def test_admin_version_v2(self): + client = tests.TestClient(self.admin_app) + resp = client.get('/v2.0/') + self.assertEqual(200, resp.status_int) + data = jsonutils.loads(resp.body) + expected = v2_VERSION_RESPONSE + self._paste_in_port(expected['version'], + 'http://localhost:%s/v2.0/' % + CONF.eventlet_server.admin_port) + self.assertEqual(expected, data) + + def test_use_site_url_if_endpoint_unset_v2(self): + self.config_fixture.config(public_endpoint=None, admin_endpoint=None) + for app in (self.public_app, self.admin_app): + client = tests.TestClient(app) + resp = client.get('/v2.0/') + self.assertEqual(200, resp.status_int) + data = jsonutils.loads(resp.body) + expected = v2_VERSION_RESPONSE + self._paste_in_port(expected['version'], 'http://localhost/v2.0/') + self.assertEqual(data, expected) + + def test_public_version_v3(self): + client = tests.TestClient(self.public_app) + resp = client.get('/v3/') + self.assertEqual(200, resp.status_int) + data = jsonutils.loads(resp.body) + expected = v3_VERSION_RESPONSE + self._paste_in_port(expected['version'], + 'http://localhost:%s/v3/' % + CONF.eventlet_server.public_port) + self.assertEqual(expected, data) + + def test_admin_version_v3(self): + client = tests.TestClient(self.public_app) + resp = client.get('/v3/') + self.assertEqual(200, resp.status_int) + data = jsonutils.loads(resp.body) + expected = v3_VERSION_RESPONSE + self._paste_in_port(expected['version'], + 'http://localhost:%s/v3/' % + CONF.eventlet_server.admin_port) + self.assertEqual(expected, data) + + def test_use_site_url_if_endpoint_unset_v3(self): + self.config_fixture.config(public_endpoint=None, admin_endpoint=None) + for app in (self.public_app, self.admin_app): + client = tests.TestClient(app) + resp = client.get('/v3/') + self.assertEqual(200, resp.status_int) + data = jsonutils.loads(resp.body) + expected = v3_VERSION_RESPONSE + self._paste_in_port(expected['version'], 'http://localhost/v3/') + self.assertEqual(expected, data) + + @mock.patch.object(controllers, '_VERSIONS', ['v3']) + def test_v2_disabled(self): + client = tests.TestClient(self.public_app) + # request to /v2.0 should fail + resp = client.get('/v2.0/') + self.assertEqual(404, resp.status_int) + + # request to /v3 should pass + resp = client.get('/v3/') + self.assertEqual(200, resp.status_int) + data = jsonutils.loads(resp.body) + expected = v3_VERSION_RESPONSE + self._paste_in_port(expected['version'], + 'http://localhost:%s/v3/' % + CONF.eventlet_server.public_port) + self.assertEqual(expected, data) + + # only v3 information should be displayed by requests to / + v3_only_response = { + "versions": { + "values": [ + v3_EXPECTED_RESPONSE + ] + } + } + self._paste_in_port(v3_only_response['versions']['values'][0], + 'http://localhost:%s/v3/' % + CONF.eventlet_server.public_port) + resp = client.get('/') + self.assertEqual(300, resp.status_int) + data = jsonutils.loads(resp.body) + self.assertEqual(v3_only_response, data) + + @mock.patch.object(controllers, '_VERSIONS', ['v2.0']) + def test_v3_disabled(self): + client = tests.TestClient(self.public_app) + # request to /v3 should fail + resp = client.get('/v3/') + self.assertEqual(404, resp.status_int) + + # request to /v2.0 should pass + resp = client.get('/v2.0/') + self.assertEqual(200, resp.status_int) + data = jsonutils.loads(resp.body) + expected = v2_VERSION_RESPONSE + self._paste_in_port(expected['version'], + 'http://localhost:%s/v2.0/' % + CONF.eventlet_server.public_port) + self.assertEqual(expected, data) + + # only v2 information should be displayed by requests to / + v2_only_response = { + "versions": { + "values": [ + v2_EXPECTED_RESPONSE + ] + } + } + self._paste_in_port(v2_only_response['versions']['values'][0], + 'http://localhost:%s/v2.0/' % + CONF.eventlet_server.public_port) + resp = client.get('/') + self.assertEqual(300, resp.status_int) + data = jsonutils.loads(resp.body) + self.assertEqual(v2_only_response, data) + + def _test_json_home(self, path, exp_json_home_data): + client = tests.TestClient(self.public_app) + resp = client.get(path, headers={'Accept': 'application/json-home'}) + + self.assertThat(resp.status, tt_matchers.Equals('200 OK')) + self.assertThat(resp.headers['Content-Type'], + tt_matchers.Equals('application/json-home')) + + self.assertThat(jsonutils.loads(resp.body), + tt_matchers.Equals(exp_json_home_data)) + + def test_json_home_v3(self): + # If the request is /v3 and the Accept header is application/json-home + # then the server responds with a JSON Home document. + + exp_json_home_data = { + 'resources': V3_JSON_HOME_RESOURCES_INHERIT_DISABLED} + + self._test_json_home('/v3', exp_json_home_data) + + def test_json_home_root(self): + # If the request is / and the Accept header is application/json-home + # then the server responds with a JSON Home document. + + exp_json_home_data = copy.deepcopy({ + 'resources': V3_JSON_HOME_RESOURCES_INHERIT_DISABLED}) + json_home.translate_urls(exp_json_home_data, '/v3') + + self._test_json_home('/', exp_json_home_data) + + def test_accept_type_handling(self): + # Accept headers with multiple types and qvalues are handled. + + def make_request(accept_types=None): + client = tests.TestClient(self.public_app) + headers = None + if accept_types: + headers = {'Accept': accept_types} + resp = client.get('/v3', headers=headers) + self.assertThat(resp.status, tt_matchers.Equals('200 OK')) + return resp.headers['Content-Type'] + + JSON = controllers.MimeTypes.JSON + JSON_HOME = controllers.MimeTypes.JSON_HOME + + JSON_MATCHER = tt_matchers.Equals(JSON) + JSON_HOME_MATCHER = tt_matchers.Equals(JSON_HOME) + + # Default is JSON. + self.assertThat(make_request(), JSON_MATCHER) + + # Can request JSON and get JSON. + self.assertThat(make_request(JSON), JSON_MATCHER) + + # Can request JSONHome and get JSONHome. + self.assertThat(make_request(JSON_HOME), JSON_HOME_MATCHER) + + # If request JSON, JSON Home get JSON. + accept_types = '%s, %s' % (JSON, JSON_HOME) + self.assertThat(make_request(accept_types), JSON_MATCHER) + + # If request JSON Home, JSON get JSON. + accept_types = '%s, %s' % (JSON_HOME, JSON) + self.assertThat(make_request(accept_types), JSON_MATCHER) + + # If request JSON Home, JSON;q=0.5 get JSON Home. + accept_types = '%s, %s;q=0.5' % (JSON_HOME, JSON) + self.assertThat(make_request(accept_types), JSON_HOME_MATCHER) + + # If request some unknown mime-type, get JSON. + self.assertThat(make_request(self.getUniqueString()), JSON_MATCHER) + + @mock.patch.object(controllers, '_VERSIONS', []) + def test_no_json_home_document_returned_when_v3_disabled(self): + json_home_document = controllers.request_v3_json_home('some_prefix') + expected_document = {'resources': {}} + self.assertEqual(expected_document, json_home_document) + + def test_extension_property_method_returns_none(self): + extension_obj = controllers.Extensions() + extensions_property = extension_obj.extensions + self.assertIsNone(extensions_property) + + +class VersionSingleAppTestCase(tests.TestCase): + """Tests running with a single application loaded. + + These are important because when Keystone is running in Apache httpd + there's only one application loaded for each instance. + + """ + + def setUp(self): + super(VersionSingleAppTestCase, self).setUp() + self.load_backends() + + self.config_fixture.config( + public_endpoint='http://localhost:%(public_port)d', + admin_endpoint='http://localhost:%(admin_port)d') + + def config_overrides(self): + super(VersionSingleAppTestCase, self).config_overrides() + port = random.randint(10000, 30000) + self.config_fixture.config(group='eventlet_server', public_port=port, + admin_port=port) + + def _paste_in_port(self, response, port): + for link in response['links']: + if link['rel'] == 'self': + link['href'] = port + + def _test_version(self, app_name): + app = self.loadapp('keystone', app_name) + client = tests.TestClient(app) + resp = client.get('/') + self.assertEqual(300, resp.status_int) + data = jsonutils.loads(resp.body) + expected = VERSIONS_RESPONSE + for version in expected['versions']['values']: + if version['id'] == 'v3.0': + self._paste_in_port( + version, 'http://localhost:%s/v3/' % + CONF.eventlet_server.public_port) + elif version['id'] == 'v2.0': + self._paste_in_port( + version, 'http://localhost:%s/v2.0/' % + CONF.eventlet_server.public_port) + self.assertThat(data, _VersionsEqual(expected)) + + def test_public(self): + self._test_version('main') + + def test_admin(self): + self._test_version('admin') + + +class VersionInheritEnabledTestCase(tests.TestCase): + def setUp(self): + super(VersionInheritEnabledTestCase, self).setUp() + self.load_backends() + self.public_app = self.loadapp('keystone', 'main') + self.admin_app = self.loadapp('keystone', 'admin') + + self.config_fixture.config( + public_endpoint='http://localhost:%(public_port)d', + admin_endpoint='http://localhost:%(admin_port)d') + + def config_overrides(self): + super(VersionInheritEnabledTestCase, self).config_overrides() + port = random.randint(10000, 30000) + self.config_fixture.config(group='eventlet_server', public_port=port, + admin_port=port) + + self.config_fixture.config(group='os_inherit', enabled=True) + + def test_json_home_v3(self): + # If the request is /v3 and the Accept header is application/json-home + # then the server responds with a JSON Home document. + + client = tests.TestClient(self.public_app) + resp = client.get('/v3/', headers={'Accept': 'application/json-home'}) + + self.assertThat(resp.status, tt_matchers.Equals('200 OK')) + self.assertThat(resp.headers['Content-Type'], + tt_matchers.Equals('application/json-home')) + + exp_json_home_data = { + 'resources': V3_JSON_HOME_RESOURCES_INHERIT_ENABLED} + + self.assertThat(jsonutils.loads(resp.body), + tt_matchers.Equals(exp_json_home_data)) + + +class VersionBehindSslTestCase(tests.TestCase): + def setUp(self): + super(VersionBehindSslTestCase, self).setUp() + self.load_backends() + self.public_app = self.loadapp('keystone', 'main') + + def config_overrides(self): + super(VersionBehindSslTestCase, self).config_overrides() + self.config_fixture.config( + secure_proxy_ssl_header='HTTP_X_FORWARDED_PROTO') + + def _paste_in_port(self, response, port): + for link in response['links']: + if link['rel'] == 'self': + link['href'] = port + + def _get_expected(self, host): + expected = VERSIONS_RESPONSE + for version in expected['versions']['values']: + if version['id'] == 'v3.0': + self._paste_in_port(version, host + 'v3/') + elif version['id'] == 'v2.0': + self._paste_in_port(version, host + 'v2.0/') + return expected + + def test_versions_without_headers(self): + client = tests.TestClient(self.public_app) + host_name = 'host-%d' % random.randint(10, 30) + host_port = random.randint(10000, 30000) + host = 'http://%s:%s/' % (host_name, host_port) + resp = client.get(host) + self.assertEqual(300, resp.status_int) + data = jsonutils.loads(resp.body) + expected = self._get_expected(host) + self.assertThat(data, _VersionsEqual(expected)) + + def test_versions_with_header(self): + client = tests.TestClient(self.public_app) + host_name = 'host-%d' % random.randint(10, 30) + host_port = random.randint(10000, 30000) + resp = client.get('http://%s:%s/' % (host_name, host_port), + headers={'X-Forwarded-Proto': 'https'}) + self.assertEqual(300, resp.status_int) + data = jsonutils.loads(resp.body) + expected = self._get_expected('https://%s:%s/' % (host_name, + host_port)) + self.assertThat(data, _VersionsEqual(expected)) diff --git a/keystone-moon/keystone/tests/unit/test_wsgi.py b/keystone-moon/keystone/tests/unit/test_wsgi.py new file mode 100644 index 00000000..1785dd00 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_wsgi.py @@ -0,0 +1,427 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 gettext +import socket +import uuid + +import mock +import oslo_i18n +from oslo_serialization import jsonutils +import six +from testtools import matchers +import webob + +from keystone.common import environment +from keystone.common import wsgi +from keystone import exception +from keystone.tests import unit as tests + + +class FakeApp(wsgi.Application): + def index(self, context): + return {'a': 'b'} + + +class FakeAttributeCheckerApp(wsgi.Application): + def index(self, context): + return context['query_string'] + + def assert_attribute(self, body, attr): + """Asserts that the given request has a certain attribute.""" + ref = jsonutils.loads(body) + self._require_attribute(ref, attr) + + def assert_attributes(self, body, attr): + """Asserts that the given request has a certain set attributes.""" + ref = jsonutils.loads(body) + self._require_attributes(ref, attr) + + +class BaseWSGITest(tests.TestCase): + def setUp(self): + self.app = FakeApp() + super(BaseWSGITest, self).setUp() + + def _make_request(self, url='/'): + req = webob.Request.blank(url) + args = {'action': 'index', 'controller': None} + req.environ['wsgiorg.routing_args'] = [None, args] + return req + + +class ApplicationTest(BaseWSGITest): + def test_response_content_type(self): + req = self._make_request() + resp = req.get_response(self.app) + self.assertEqual(resp.content_type, 'application/json') + + def test_query_string_available(self): + class FakeApp(wsgi.Application): + def index(self, context): + return context['query_string'] + req = self._make_request(url='/?1=2') + resp = req.get_response(FakeApp()) + self.assertEqual(jsonutils.loads(resp.body), {'1': '2'}) + + def test_headers_available(self): + class FakeApp(wsgi.Application): + def index(self, context): + return context['headers'] + + app = FakeApp() + req = self._make_request(url='/?1=2') + req.headers['X-Foo'] = "bar" + resp = req.get_response(app) + self.assertIn('X-Foo', eval(resp.body)) + + def test_render_response(self): + data = {'attribute': 'value'} + body = b'{"attribute": "value"}' + + resp = wsgi.render_response(body=data) + self.assertEqual('200 OK', resp.status) + self.assertEqual(200, resp.status_int) + self.assertEqual(body, resp.body) + self.assertEqual('X-Auth-Token', resp.headers.get('Vary')) + self.assertEqual(str(len(body)), resp.headers.get('Content-Length')) + + def test_render_response_custom_status(self): + resp = wsgi.render_response(status=(501, 'Not Implemented')) + self.assertEqual('501 Not Implemented', resp.status) + self.assertEqual(501, resp.status_int) + + def test_successful_require_attribute(self): + app = FakeAttributeCheckerApp() + req = self._make_request(url='/?1=2') + resp = req.get_response(app) + app.assert_attribute(resp.body, '1') + + def test_require_attribute_fail_if_attribute_not_present(self): + app = FakeAttributeCheckerApp() + req = self._make_request(url='/?1=2') + resp = req.get_response(app) + self.assertRaises(exception.ValidationError, + app.assert_attribute, resp.body, 'a') + + def test_successful_require_multiple_attributes(self): + app = FakeAttributeCheckerApp() + req = self._make_request(url='/?a=1&b=2') + resp = req.get_response(app) + app.assert_attributes(resp.body, ['a', 'b']) + + def test_attribute_missing_from_request(self): + app = FakeAttributeCheckerApp() + req = self._make_request(url='/?a=1&b=2') + resp = req.get_response(app) + ex = self.assertRaises(exception.ValidationError, + app.assert_attributes, + resp.body, ['a', 'missing_attribute']) + self.assertThat(six.text_type(ex), + matchers.Contains('missing_attribute')) + + def test_no_required_attributes_present(self): + app = FakeAttributeCheckerApp() + req = self._make_request(url='/') + resp = req.get_response(app) + + ex = self.assertRaises(exception.ValidationError, + app.assert_attributes, resp.body, + ['missing_attribute1', 'missing_attribute2']) + self.assertThat(six.text_type(ex), + matchers.Contains('missing_attribute1')) + self.assertThat(six.text_type(ex), + matchers.Contains('missing_attribute2')) + + def test_render_response_custom_headers(self): + resp = wsgi.render_response(headers=[('Custom-Header', 'Some-Value')]) + self.assertEqual('Some-Value', resp.headers.get('Custom-Header')) + self.assertEqual('X-Auth-Token', resp.headers.get('Vary')) + + def test_render_response_no_body(self): + resp = wsgi.render_response() + self.assertEqual('204 No Content', resp.status) + self.assertEqual(204, resp.status_int) + self.assertEqual(b'', resp.body) + self.assertEqual('0', resp.headers.get('Content-Length')) + self.assertIsNone(resp.headers.get('Content-Type')) + + def test_render_response_head_with_body(self): + resp = wsgi.render_response({'id': uuid.uuid4().hex}, method='HEAD') + self.assertEqual(200, resp.status_int) + self.assertEqual(b'', resp.body) + self.assertNotEqual(resp.headers.get('Content-Length'), '0') + self.assertEqual('application/json', resp.headers.get('Content-Type')) + + def test_application_local_config(self): + class FakeApp(wsgi.Application): + def __init__(self, *args, **kwargs): + self.kwargs = kwargs + + app = FakeApp.factory({}, testkey="test") + self.assertIn("testkey", app.kwargs) + self.assertEqual("test", app.kwargs["testkey"]) + + def test_render_exception(self): + e = exception.Unauthorized(message=u'\u7f51\u7edc') + resp = wsgi.render_exception(e) + self.assertEqual(401, resp.status_int) + + def test_render_exception_host(self): + e = exception.Unauthorized(message=u'\u7f51\u7edc') + context = {'host_url': 'http://%s:5000' % uuid.uuid4().hex} + resp = wsgi.render_exception(e, context=context) + + self.assertEqual(401, resp.status_int) + + +class ExtensionRouterTest(BaseWSGITest): + def test_extensionrouter_local_config(self): + class FakeRouter(wsgi.ExtensionRouter): + def __init__(self, *args, **kwargs): + self.kwargs = kwargs + + factory = FakeRouter.factory({}, testkey="test") + app = factory(self.app) + self.assertIn("testkey", app.kwargs) + self.assertEqual("test", app.kwargs["testkey"]) + + +class MiddlewareTest(BaseWSGITest): + def test_middleware_request(self): + class FakeMiddleware(wsgi.Middleware): + def process_request(self, req): + req.environ['fake_request'] = True + return req + req = self._make_request() + resp = FakeMiddleware(None)(req) + self.assertIn('fake_request', resp.environ) + + def test_middleware_response(self): + class FakeMiddleware(wsgi.Middleware): + def process_response(self, request, response): + response.environ = {} + response.environ['fake_response'] = True + return response + req = self._make_request() + resp = FakeMiddleware(self.app)(req) + self.assertIn('fake_response', resp.environ) + + def test_middleware_bad_request(self): + class FakeMiddleware(wsgi.Middleware): + def process_response(self, request, response): + raise exception.Unauthorized() + + req = self._make_request() + req.environ['REMOTE_ADDR'] = '127.0.0.1' + resp = FakeMiddleware(self.app)(req) + self.assertEqual(exception.Unauthorized.code, resp.status_int) + + def test_middleware_type_error(self): + class FakeMiddleware(wsgi.Middleware): + def process_response(self, request, response): + raise TypeError() + + req = self._make_request() + req.environ['REMOTE_ADDR'] = '127.0.0.1' + resp = FakeMiddleware(self.app)(req) + # This is a validationerror type + self.assertEqual(exception.ValidationError.code, resp.status_int) + + def test_middleware_exception_error(self): + + exception_str = b'EXCEPTIONERROR' + + class FakeMiddleware(wsgi.Middleware): + def process_response(self, request, response): + raise exception.UnexpectedError(exception_str) + + def do_request(): + req = self._make_request() + resp = FakeMiddleware(self.app)(req) + self.assertEqual(exception.UnexpectedError.code, resp.status_int) + return resp + + # Exception data should not be in the message when debug is False + self.config_fixture.config(debug=False) + self.assertNotIn(exception_str, do_request().body) + + # Exception data should be in the message when debug is True + self.config_fixture.config(debug=True) + self.assertIn(exception_str, do_request().body) + + def test_middleware_local_config(self): + class FakeMiddleware(wsgi.Middleware): + def __init__(self, *args, **kwargs): + self.kwargs = kwargs + + factory = FakeMiddleware.factory({}, testkey="test") + app = factory(self.app) + self.assertIn("testkey", app.kwargs) + self.assertEqual("test", app.kwargs["testkey"]) + + +class LocalizedResponseTest(tests.TestCase): + def test_request_match_default(self): + # The default language if no Accept-Language is provided is None + req = webob.Request.blank('/') + self.assertIsNone(wsgi.best_match_language(req)) + + @mock.patch.object(oslo_i18n, 'get_available_languages') + def test_request_match_language_expected(self, mock_gal): + # If Accept-Language is a supported language, best_match_language() + # returns it. + + language = uuid.uuid4().hex + mock_gal.return_value = [language] + + req = webob.Request.blank('/', headers={'Accept-Language': language}) + self.assertEqual(language, wsgi.best_match_language(req)) + + @mock.patch.object(oslo_i18n, 'get_available_languages') + def test_request_match_language_unexpected(self, mock_gal): + # If Accept-Language is a language we do not support, + # best_match_language() returns None. + + supported_language = uuid.uuid4().hex + mock_gal.return_value = [supported_language] + + request_language = uuid.uuid4().hex + req = webob.Request.blank( + '/', headers={'Accept-Language': request_language}) + self.assertIsNone(wsgi.best_match_language(req)) + + def test_static_translated_string_is_lazy_translatable(self): + # Statically created message strings are an object that can get + # lazy-translated rather than a regular string. + self.assertNotEqual(type(exception.Unauthorized.message_format), + six.text_type) + + @mock.patch.object(oslo_i18n, 'get_available_languages') + def test_get_localized_response(self, mock_gal): + # If the request has the Accept-Language set to a supported language + # and an exception is raised by the application that is translatable + # then the response will have the translated message. + + language = uuid.uuid4().hex + mock_gal.return_value = [language] + + # The arguments for the xlated message format have to match the args + # for the chosen exception (exception.NotFound) + xlated_msg_fmt = "Xlated NotFound, %(target)s." + + # Fake out gettext.translation() to return a translator for our + # expected language and a passthrough translator for other langs. + + def fake_translation(*args, **kwargs): + class IdentityTranslator(object): + def ugettext(self, msgid): + return msgid + + gettext = ugettext + + class LangTranslator(object): + def ugettext(self, msgid): + if msgid == exception.NotFound.message_format: + return xlated_msg_fmt + return msgid + + gettext = ugettext + + if language in kwargs.get('languages', []): + return LangTranslator() + return IdentityTranslator() + + with mock.patch.object(gettext, 'translation', + side_effect=fake_translation) as xlation_mock: + target = uuid.uuid4().hex + + # Fake app raises NotFound exception to simulate Keystone raising. + + class FakeApp(wsgi.Application): + def index(self, context): + raise exception.NotFound(target=target) + + # Make the request with Accept-Language on the app, expect an error + # response with the translated message. + + req = webob.Request.blank('/') + args = {'action': 'index', 'controller': None} + req.environ['wsgiorg.routing_args'] = [None, args] + req.headers['Accept-Language'] = language + resp = req.get_response(FakeApp()) + + # Assert that the translated message appears in the response. + + exp_msg = xlated_msg_fmt % dict(target=target) + self.assertThat(resp.json['error']['message'], + matchers.Equals(exp_msg)) + self.assertThat(xlation_mock.called, matchers.Equals(True)) + + +class ServerTest(tests.TestCase): + + def setUp(self): + super(ServerTest, self).setUp() + self.host = '127.0.0.1' + self.port = '1234' + + @mock.patch('eventlet.listen') + @mock.patch('socket.getaddrinfo') + def test_keepalive_unset(self, mock_getaddrinfo, mock_listen): + mock_getaddrinfo.return_value = [(1, 2, 3, 4, 5)] + mock_sock_dup = mock_listen.return_value.dup.return_value + + server = environment.Server(mock.MagicMock(), host=self.host, + port=self.port) + server.start() + self.addCleanup(server.stop) + self.assertTrue(mock_listen.called) + self.assertFalse(mock_sock_dup.setsockopt.called) + + @mock.patch('eventlet.listen') + @mock.patch('socket.getaddrinfo') + def test_keepalive_set(self, mock_getaddrinfo, mock_listen): + mock_getaddrinfo.return_value = [(1, 2, 3, 4, 5)] + mock_sock_dup = mock_listen.return_value.dup.return_value + + server = environment.Server(mock.MagicMock(), host=self.host, + port=self.port, keepalive=True) + server.start() + self.addCleanup(server.stop) + mock_sock_dup.setsockopt.assert_called_once_with(socket.SOL_SOCKET, + socket.SO_KEEPALIVE, + 1) + self.assertTrue(mock_listen.called) + + @mock.patch('eventlet.listen') + @mock.patch('socket.getaddrinfo') + def test_keepalive_and_keepidle_set(self, mock_getaddrinfo, mock_listen): + mock_getaddrinfo.return_value = [(1, 2, 3, 4, 5)] + mock_sock_dup = mock_listen.return_value.dup.return_value + + server = environment.Server(mock.MagicMock(), host=self.host, + port=self.port, keepalive=True, + keepidle=1) + server.start() + self.addCleanup(server.stop) + + self.assertEqual(2, mock_sock_dup.setsockopt.call_count) + + # Test the last set of call args i.e. for the keepidle + mock_sock_dup.setsockopt.assert_called_with(socket.IPPROTO_TCP, + socket.TCP_KEEPIDLE, + 1) + + self.assertTrue(mock_listen.called) diff --git a/keystone-moon/keystone/tests/unit/tests/__init__.py b/keystone-moon/keystone/tests/unit/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/tests/unit/tests/test_core.py b/keystone-moon/keystone/tests/unit/tests/test_core.py new file mode 100644 index 00000000..86c91a8d --- /dev/null +++ b/keystone-moon/keystone/tests/unit/tests/test_core.py @@ -0,0 +1,62 @@ +# Copyright 2014 IBM Corp. +# +# 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 sys +import warnings + +from oslo_log import log +from sqlalchemy import exc +from testtools import matchers + +from keystone.tests import unit as tests + + +LOG = log.getLogger(__name__) + + +class BaseTestTestCase(tests.BaseTestCase): + + def test_unexpected_exit(self): + # if a test calls sys.exit it raises rather than exiting. + self.assertThat(lambda: sys.exit(), + matchers.raises(tests.UnexpectedExit)) + + +class TestTestCase(tests.TestCase): + + def test_bad_log(self): + # If the arguments are invalid for the string in a log it raises an + # exception during testing. + self.assertThat( + lambda: LOG.warn('String %(p1)s %(p2)s', {'p1': 'something'}), + matchers.raises(tests.BadLog)) + + def test_sa_warning(self): + self.assertThat( + lambda: warnings.warn('test sa warning error', exc.SAWarning), + matchers.raises(exc.SAWarning)) + + def test_deprecations(self): + # If any deprecation warnings occur during testing it's raised as + # exception. + + def use_deprecated(): + # DeprecationWarning: BaseException.message has been deprecated as + # of Python 2.6 + try: + raise Exception('something') + except Exception as e: + e.message + + self.assertThat(use_deprecated, matchers.raises(DeprecationWarning)) diff --git a/keystone-moon/keystone/tests/unit/tests/test_utils.py b/keystone-moon/keystone/tests/unit/tests/test_utils.py new file mode 100644 index 00000000..22c485c0 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/tests/test_utils.py @@ -0,0 +1,37 @@ +# 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 testtools import matchers +from testtools import testcase + +from keystone.tests.unit import utils + + +class TestWipDecorator(testcase.TestCase): + + def test_raises_SkipError_when_broken_test_fails(self): + + @utils.wip('waiting on bug #000000') + def test(): + raise Exception('i expected a failure - this is a WIP') + + e = self.assertRaises(testcase.TestSkipped, test) + self.assertThat(str(e), matchers.Contains('#000000')) + + def test_raises_AssertionError_when_test_passes(self): + + @utils.wip('waiting on bug #000000') + def test(): + pass # literally + + e = self.assertRaises(AssertionError, test) + self.assertThat(str(e), matchers.Contains('#000000')) diff --git a/keystone-moon/keystone/tests/unit/token/__init__.py b/keystone-moon/keystone/tests/unit/token/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/tests/unit/token/test_fernet_provider.py b/keystone-moon/keystone/tests/unit/token/test_fernet_provider.py new file mode 100644 index 00000000..23fc0214 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/token/test_fernet_provider.py @@ -0,0 +1,183 @@ +# 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 datetime +import uuid + +from oslo_utils import timeutils + +from keystone.common import config +from keystone import exception +from keystone.tests import unit as tests +from keystone.tests.unit import ksfixtures +from keystone.token import provider +from keystone.token.providers import fernet +from keystone.token.providers.fernet import token_formatters + + +CONF = config.CONF + + +class TestFernetTokenProvider(tests.TestCase): + def setUp(self): + super(TestFernetTokenProvider, self).setUp() + self.useFixture(ksfixtures.KeyRepository(self.config_fixture)) + self.provider = fernet.Provider() + + def test_get_token_id_raises_not_implemented(self): + """Test that an exception is raised when calling _get_token_id.""" + token_data = {} + self.assertRaises(exception.NotImplemented, + self.provider._get_token_id, token_data) + + def test_invalid_v3_token_raises_401(self): + self.assertRaises( + exception.Unauthorized, + self.provider.validate_v3_token, + uuid.uuid4().hex) + + def test_invalid_v2_token_raises_401(self): + self.assertRaises( + exception.Unauthorized, + self.provider.validate_v2_token, + uuid.uuid4().hex) + + +class TestPayloads(tests.TestCase): + def test_uuid_hex_to_byte_conversions(self): + payload_cls = token_formatters.BasePayload + + expected_hex_uuid = uuid.uuid4().hex + uuid_obj = uuid.UUID(expected_hex_uuid) + expected_uuid_in_bytes = uuid_obj.bytes + actual_uuid_in_bytes = payload_cls.convert_uuid_hex_to_bytes( + expected_hex_uuid) + self.assertEqual(expected_uuid_in_bytes, actual_uuid_in_bytes) + actual_hex_uuid = payload_cls.convert_uuid_bytes_to_hex( + expected_uuid_in_bytes) + self.assertEqual(expected_hex_uuid, actual_hex_uuid) + + def test_time_string_to_int_conversions(self): + payload_cls = token_formatters.BasePayload + + expected_time_str = timeutils.isotime() + time_obj = timeutils.parse_isotime(expected_time_str) + expected_time_int = ( + (timeutils.normalize_time(time_obj) - + datetime.datetime.utcfromtimestamp(0)).total_seconds()) + + actual_time_int = payload_cls._convert_time_string_to_int( + expected_time_str) + self.assertEqual(expected_time_int, actual_time_int) + + actual_time_str = payload_cls._convert_int_to_time_string( + actual_time_int) + self.assertEqual(expected_time_str, actual_time_str) + + def test_unscoped_payload(self): + exp_user_id = uuid.uuid4().hex + exp_methods = ['password'] + exp_expires_at = timeutils.isotime(timeutils.utcnow()) + exp_audit_ids = [provider.random_urlsafe_str()] + + payload = token_formatters.UnscopedPayload.assemble( + exp_user_id, exp_methods, exp_expires_at, exp_audit_ids) + + (user_id, methods, expires_at, audit_ids) = ( + token_formatters.UnscopedPayload.disassemble(payload)) + + self.assertEqual(exp_user_id, user_id) + self.assertEqual(exp_methods, methods) + self.assertEqual(exp_expires_at, expires_at) + self.assertEqual(exp_audit_ids, audit_ids) + + def test_project_scoped_payload(self): + exp_user_id = uuid.uuid4().hex + exp_methods = ['password'] + exp_project_id = uuid.uuid4().hex + exp_expires_at = timeutils.isotime(timeutils.utcnow()) + exp_audit_ids = [provider.random_urlsafe_str()] + + payload = token_formatters.ProjectScopedPayload.assemble( + exp_user_id, exp_methods, exp_project_id, exp_expires_at, + exp_audit_ids) + + (user_id, methods, project_id, expires_at, audit_ids) = ( + token_formatters.ProjectScopedPayload.disassemble(payload)) + + self.assertEqual(exp_user_id, user_id) + self.assertEqual(exp_methods, methods) + self.assertEqual(exp_project_id, project_id) + self.assertEqual(exp_expires_at, expires_at) + self.assertEqual(exp_audit_ids, audit_ids) + + def test_domain_scoped_payload(self): + exp_user_id = uuid.uuid4().hex + exp_methods = ['password'] + exp_domain_id = uuid.uuid4().hex + exp_expires_at = timeutils.isotime(timeutils.utcnow()) + exp_audit_ids = [provider.random_urlsafe_str()] + + payload = token_formatters.DomainScopedPayload.assemble( + exp_user_id, exp_methods, exp_domain_id, exp_expires_at, + exp_audit_ids) + + (user_id, methods, domain_id, expires_at, audit_ids) = ( + token_formatters.DomainScopedPayload.disassemble(payload)) + + self.assertEqual(exp_user_id, user_id) + self.assertEqual(exp_methods, methods) + self.assertEqual(exp_domain_id, domain_id) + self.assertEqual(exp_expires_at, expires_at) + self.assertEqual(exp_audit_ids, audit_ids) + + def test_domain_scoped_payload_with_default_domain(self): + exp_user_id = uuid.uuid4().hex + exp_methods = ['password'] + exp_domain_id = CONF.identity.default_domain_id + exp_expires_at = timeutils.isotime(timeutils.utcnow()) + exp_audit_ids = [provider.random_urlsafe_str()] + + payload = token_formatters.DomainScopedPayload.assemble( + exp_user_id, exp_methods, exp_domain_id, exp_expires_at, + exp_audit_ids) + + (user_id, methods, domain_id, expires_at, audit_ids) = ( + token_formatters.DomainScopedPayload.disassemble(payload)) + + self.assertEqual(exp_user_id, user_id) + self.assertEqual(exp_methods, methods) + self.assertEqual(exp_domain_id, domain_id) + self.assertEqual(exp_expires_at, expires_at) + self.assertEqual(exp_audit_ids, audit_ids) + + def test_trust_scoped_payload(self): + exp_user_id = uuid.uuid4().hex + exp_methods = ['password'] + exp_project_id = uuid.uuid4().hex + exp_expires_at = timeutils.isotime(timeutils.utcnow()) + exp_audit_ids = [provider.random_urlsafe_str()] + exp_trust_id = uuid.uuid4().hex + + payload = token_formatters.TrustScopedPayload.assemble( + exp_user_id, exp_methods, exp_project_id, exp_expires_at, + exp_audit_ids, exp_trust_id) + + (user_id, methods, project_id, expires_at, audit_ids, trust_id) = ( + token_formatters.TrustScopedPayload.disassemble(payload)) + + self.assertEqual(exp_user_id, user_id) + self.assertEqual(exp_methods, methods) + self.assertEqual(exp_project_id, project_id) + self.assertEqual(exp_expires_at, expires_at) + self.assertEqual(exp_audit_ids, audit_ids) + self.assertEqual(exp_trust_id, trust_id) diff --git a/keystone-moon/keystone/tests/unit/token/test_provider.py b/keystone-moon/keystone/tests/unit/token/test_provider.py new file mode 100644 index 00000000..e5910690 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/token/test_provider.py @@ -0,0 +1,29 @@ +# 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 urllib + +from keystone.tests import unit +from keystone.token import provider + + +class TestRandomStrings(unit.BaseTestCase): + def test_strings_are_url_safe(self): + s = provider.random_urlsafe_str() + self.assertEqual(s, urllib.quote_plus(s)) + + def test_strings_can_be_converted_to_bytes(self): + s = provider.random_urlsafe_str() + self.assertTrue(isinstance(s, basestring)) + + b = provider.random_urlsafe_str_to_bytes(s) + self.assertTrue(isinstance(b, bytes)) diff --git a/keystone-moon/keystone/tests/unit/token/test_token_data_helper.py b/keystone-moon/keystone/tests/unit/token/test_token_data_helper.py new file mode 100644 index 00000000..a12a22d4 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/token/test_token_data_helper.py @@ -0,0 +1,55 @@ +# 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 uuid + +from testtools import matchers + +from keystone import exception +from keystone.tests import unit as tests +from keystone.token.providers import common + + +class TestTokenDataHelper(tests.TestCase): + def setUp(self): + super(TestTokenDataHelper, self).setUp() + self.load_backends() + self.v3_data_helper = common.V3TokenDataHelper() + + def test_v3_token_data_helper_populate_audit_info_string(self): + token_data = {} + audit_info = base64.urlsafe_b64encode(uuid.uuid4().bytes)[:-2] + self.v3_data_helper._populate_audit_info(token_data, audit_info) + self.assertIn(audit_info, token_data['audit_ids']) + self.assertThat(token_data['audit_ids'], matchers.HasLength(2)) + + def test_v3_token_data_helper_populate_audit_info_none(self): + token_data = {} + self.v3_data_helper._populate_audit_info(token_data, audit_info=None) + self.assertThat(token_data['audit_ids'], matchers.HasLength(1)) + self.assertNotIn(None, token_data['audit_ids']) + + def test_v3_token_data_helper_populate_audit_info_list(self): + token_data = {} + audit_info = [base64.urlsafe_b64encode(uuid.uuid4().bytes)[:-2], + base64.urlsafe_b64encode(uuid.uuid4().bytes)[:-2]] + self.v3_data_helper._populate_audit_info(token_data, audit_info) + self.assertEqual(audit_info, token_data['audit_ids']) + + def test_v3_token_data_helper_populate_audit_info_invalid(self): + token_data = {} + audit_info = dict() + self.assertRaises(exception.UnexpectedError, + self.v3_data_helper._populate_audit_info, + token_data=token_data, + audit_info=audit_info) diff --git a/keystone-moon/keystone/tests/unit/token/test_token_model.py b/keystone-moon/keystone/tests/unit/token/test_token_model.py new file mode 100644 index 00000000..b2474289 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/token/test_token_model.py @@ -0,0 +1,262 @@ +# 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 +import uuid + +from oslo_config import cfg +from oslo_utils import timeutils + +from keystone import exception +from keystone.models import token_model +from keystone.tests.unit import core +from keystone.tests.unit import test_token_provider + + +CONF = cfg.CONF + + +class TestKeystoneTokenModel(core.TestCase): + def setUp(self): + super(TestKeystoneTokenModel, self).setUp() + self.load_backends() + self.v2_sample_token = copy.deepcopy( + test_token_provider.SAMPLE_V2_TOKEN) + self.v3_sample_token = copy.deepcopy( + test_token_provider.SAMPLE_V3_TOKEN) + + def test_token_model_v3(self): + token_data = token_model.KeystoneToken(uuid.uuid4().hex, + self.v3_sample_token) + self.assertIs(token_model.V3, token_data.version) + expires = timeutils.normalize_time(timeutils.parse_isotime( + self.v3_sample_token['token']['expires_at'])) + issued = timeutils.normalize_time(timeutils.parse_isotime( + self.v3_sample_token['token']['issued_at'])) + self.assertEqual(expires, token_data.expires) + self.assertEqual(issued, token_data.issued) + self.assertEqual(self.v3_sample_token['token']['user']['id'], + token_data.user_id) + self.assertEqual(self.v3_sample_token['token']['user']['name'], + token_data.user_name) + self.assertEqual(self.v3_sample_token['token']['user']['domain']['id'], + token_data.user_domain_id) + self.assertEqual( + self.v3_sample_token['token']['user']['domain']['name'], + token_data.user_domain_name) + self.assertEqual( + self.v3_sample_token['token']['project']['domain']['id'], + token_data.project_domain_id) + self.assertEqual( + self.v3_sample_token['token']['project']['domain']['name'], + token_data.project_domain_name) + self.assertEqual(self.v3_sample_token['token']['OS-TRUST:trust']['id'], + token_data.trust_id) + self.assertEqual( + self.v3_sample_token['token']['OS-TRUST:trust']['trustor_user_id'], + token_data.trustor_user_id) + self.assertEqual( + self.v3_sample_token['token']['OS-TRUST:trust']['trustee_user_id'], + token_data.trustee_user_id) + # Project Scoped Token + self.assertRaises(exception.UnexpectedError, getattr, token_data, + 'domain_id') + self.assertRaises(exception.UnexpectedError, getattr, token_data, + 'domain_name') + self.assertFalse(token_data.domain_scoped) + self.assertEqual(self.v3_sample_token['token']['project']['id'], + token_data.project_id) + self.assertEqual(self.v3_sample_token['token']['project']['name'], + token_data.project_name) + self.assertTrue(token_data.project_scoped) + self.assertTrue(token_data.scoped) + self.assertTrue(token_data.trust_scoped) + self.assertEqual( + [r['id'] for r in self.v3_sample_token['token']['roles']], + token_data.role_ids) + self.assertEqual( + [r['name'] for r in self.v3_sample_token['token']['roles']], + token_data.role_names) + token_data.pop('project') + self.assertFalse(token_data.project_scoped) + self.assertFalse(token_data.scoped) + self.assertRaises(exception.UnexpectedError, getattr, token_data, + 'project_id') + self.assertRaises(exception.UnexpectedError, getattr, token_data, + 'project_name') + self.assertFalse(token_data.project_scoped) + domain_id = uuid.uuid4().hex + domain_name = uuid.uuid4().hex + token_data['domain'] = {'id': domain_id, + 'name': domain_name} + self.assertEqual(domain_id, token_data.domain_id) + self.assertEqual(domain_name, token_data.domain_name) + self.assertTrue(token_data.domain_scoped) + + token_data['audit_ids'] = [uuid.uuid4().hex] + self.assertEqual(token_data.audit_id, + token_data['audit_ids'][0]) + self.assertEqual(token_data.audit_chain_id, + token_data['audit_ids'][0]) + token_data['audit_ids'].append(uuid.uuid4().hex) + self.assertEqual(token_data.audit_chain_id, + token_data['audit_ids'][1]) + del token_data['audit_ids'] + self.assertIsNone(token_data.audit_id) + self.assertIsNone(token_data.audit_chain_id) + + def test_token_model_v3_federated_user(self): + token_data = token_model.KeystoneToken(token_id=uuid.uuid4().hex, + token_data=self.v3_sample_token) + federation_data = {'identity_provider': {'id': uuid.uuid4().hex}, + 'protocol': {'id': 'saml2'}, + 'groups': [{'id': uuid.uuid4().hex} + for x in range(1, 5)]} + + self.assertFalse(token_data.is_federated_user) + self.assertEqual([], token_data.federation_group_ids) + self.assertIsNone(token_data.federation_protocol_id) + self.assertIsNone(token_data.federation_idp_id) + + token_data['user'][token_model.federation.FEDERATION] = federation_data + + self.assertTrue(token_data.is_federated_user) + self.assertEqual([x['id'] for x in federation_data['groups']], + token_data.federation_group_ids) + self.assertEqual(federation_data['protocol']['id'], + token_data.federation_protocol_id) + self.assertEqual(federation_data['identity_provider']['id'], + token_data.federation_idp_id) + + def test_token_model_v2_federated_user(self): + token_data = token_model.KeystoneToken(token_id=uuid.uuid4().hex, + token_data=self.v2_sample_token) + federation_data = {'identity_provider': {'id': uuid.uuid4().hex}, + 'protocol': {'id': 'saml2'}, + 'groups': [{'id': uuid.uuid4().hex} + for x in range(1, 5)]} + self.assertFalse(token_data.is_federated_user) + self.assertEqual([], token_data.federation_group_ids) + self.assertIsNone(token_data.federation_protocol_id) + self.assertIsNone(token_data.federation_idp_id) + + token_data['user'][token_model.federation.FEDERATION] = federation_data + + # Federated users should not exist in V2, the data should remain empty + self.assertFalse(token_data.is_federated_user) + self.assertEqual([], token_data.federation_group_ids) + self.assertIsNone(token_data.federation_protocol_id) + self.assertIsNone(token_data.federation_idp_id) + + def test_token_model_v2(self): + token_data = token_model.KeystoneToken(uuid.uuid4().hex, + self.v2_sample_token) + self.assertIs(token_model.V2, token_data.version) + expires = timeutils.normalize_time(timeutils.parse_isotime( + self.v2_sample_token['access']['token']['expires'])) + issued = timeutils.normalize_time(timeutils.parse_isotime( + self.v2_sample_token['access']['token']['issued_at'])) + self.assertEqual(expires, token_data.expires) + self.assertEqual(issued, token_data.issued) + self.assertEqual(self.v2_sample_token['access']['user']['id'], + token_data.user_id) + self.assertEqual(self.v2_sample_token['access']['user']['name'], + token_data.user_name) + self.assertEqual(CONF.identity.default_domain_id, + token_data.user_domain_id) + self.assertEqual('Default', token_data.user_domain_name) + self.assertEqual(CONF.identity.default_domain_id, + token_data.project_domain_id) + self.assertEqual('Default', + token_data.project_domain_name) + self.assertEqual(self.v2_sample_token['access']['trust']['id'], + token_data.trust_id) + self.assertEqual( + self.v2_sample_token['access']['trust']['trustor_user_id'], + token_data.trustor_user_id) + self.assertEqual( + self.v2_sample_token['access']['trust']['impersonation'], + token_data.trust_impersonation) + self.assertEqual( + self.v2_sample_token['access']['trust']['trustee_user_id'], + token_data.trustee_user_id) + # Project Scoped Token + self.assertEqual( + self.v2_sample_token['access']['token']['tenant']['id'], + token_data.project_id) + self.assertEqual( + self.v2_sample_token['access']['token']['tenant']['name'], + token_data.project_name) + self.assertTrue(token_data.project_scoped) + self.assertTrue(token_data.scoped) + self.assertTrue(token_data.trust_scoped) + self.assertEqual( + [r['name'] + for r in self.v2_sample_token['access']['user']['roles']], + token_data.role_names) + token_data['token'].pop('tenant') + self.assertFalse(token_data.scoped) + self.assertFalse(token_data.project_scoped) + self.assertFalse(token_data.domain_scoped) + self.assertRaises(exception.UnexpectedError, getattr, token_data, + 'project_id') + self.assertRaises(exception.UnexpectedError, getattr, token_data, + 'project_name') + self.assertRaises(exception.UnexpectedError, getattr, token_data, + 'project_domain_id') + self.assertRaises(exception.UnexpectedError, getattr, token_data, + 'project_domain_id') + # No Domain Scoped tokens in V2 + self.assertRaises(NotImplementedError, getattr, token_data, + 'domain_id') + self.assertRaises(NotImplementedError, getattr, token_data, + 'domain_name') + token_data['domain'] = {'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex} + self.assertRaises(NotImplementedError, getattr, token_data, + 'domain_id') + self.assertRaises(NotImplementedError, getattr, token_data, + 'domain_name') + self.assertFalse(token_data.domain_scoped) + + token_data['token']['audit_ids'] = [uuid.uuid4().hex] + self.assertEqual(token_data.audit_chain_id, + token_data['token']['audit_ids'][0]) + token_data['token']['audit_ids'].append(uuid.uuid4().hex) + self.assertEqual(token_data.audit_chain_id, + token_data['token']['audit_ids'][1]) + self.assertEqual(token_data.audit_id, + token_data['token']['audit_ids'][0]) + del token_data['token']['audit_ids'] + self.assertIsNone(token_data.audit_id) + self.assertIsNone(token_data.audit_chain_id) + + def test_token_model_unknown(self): + self.assertRaises(exception.UnsupportedTokenVersionException, + token_model.KeystoneToken, + token_id=uuid.uuid4().hex, + token_data={'bogus_data': uuid.uuid4().hex}) + + def test_token_model_dual_scoped_token(self): + domain = {'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex} + self.v2_sample_token['access']['domain'] = domain + self.v3_sample_token['token']['domain'] = domain + + # V2 Tokens Cannot be domain scoped, this should work + token_model.KeystoneToken(token_id=uuid.uuid4().hex, + token_data=self.v2_sample_token) + + self.assertRaises(exception.UnexpectedError, + token_model.KeystoneToken, + token_id=uuid.uuid4().hex, + token_data=self.v3_sample_token) diff --git a/keystone-moon/keystone/tests/unit/utils.py b/keystone-moon/keystone/tests/unit/utils.py new file mode 100644 index 00000000..17d1de81 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/utils.py @@ -0,0 +1,89 @@ +# 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. + +"""Useful utilities for tests.""" + +import functools +import os +import time +import uuid + +from oslo_log import log +import six +from testtools import testcase + + +LOG = log.getLogger(__name__) + +TZ = None + + +def timezone(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + tz_original = os.environ.get('TZ') + try: + if TZ: + os.environ['TZ'] = TZ + time.tzset() + return func(*args, **kwargs) + finally: + if TZ: + if tz_original: + os.environ['TZ'] = tz_original + else: + if 'TZ' in os.environ: + del os.environ['TZ'] + time.tzset() + return wrapper + + +def new_uuid(): + """Return a string UUID.""" + return uuid.uuid4().hex + + +def wip(message): + """Mark a test as work in progress. + + Based on code by Nat Pryce: + https://gist.github.com/npryce/997195#file-wip-py + + The test will always be run. If the test fails then a TestSkipped + exception is raised. If the test passes an AssertionError exception + is raised so that the developer knows they made the test pass. This + is a reminder to remove the decorator. + + :param message: a string message to help clarify why the test is + marked as a work in progress + + usage: + >>> @wip('waiting on bug #000000') + >>> def test(): + >>> pass + + """ + + def _wip(f): + @six.wraps(f) + def run_test(*args, **kwargs): + try: + f(*args, **kwargs) + except Exception: + raise testcase.TestSkipped('work in progress test failed: ' + + message) + + raise AssertionError('work in progress test passed: ' + message) + + return run_test + + return _wip diff --git a/keystone-moon/keystone/token/__init__.py b/keystone-moon/keystone/token/__init__.py new file mode 100644 index 00000000..a73e19f9 --- /dev/null +++ b/keystone-moon/keystone/token/__init__.py @@ -0,0 +1,18 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 keystone.token import controllers # noqa +from keystone.token import persistence # noqa +from keystone.token import provider # noqa +from keystone.token import routers # noqa diff --git a/keystone-moon/keystone/token/controllers.py b/keystone-moon/keystone/token/controllers.py new file mode 100644 index 00000000..3304acb5 --- /dev/null +++ b/keystone-moon/keystone/token/controllers.py @@ -0,0 +1,523 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 datetime +import sys + +from keystoneclient.common import cms +from oslo_config import cfg +from oslo_log import log +from oslo_serialization import jsonutils +from oslo_utils import timeutils +import six + +from keystone.common import controller +from keystone.common import dependency +from keystone.common import wsgi +from keystone import exception +from keystone.i18n import _ +from keystone.models import token_model +from keystone.token import provider + + +CONF = cfg.CONF +LOG = log.getLogger(__name__) + + +class ExternalAuthNotApplicable(Exception): + """External authentication is not applicable.""" + pass + + +@dependency.requires('assignment_api', 'catalog_api', 'identity_api', + 'resource_api', 'role_api', 'token_provider_api', + 'trust_api') +class Auth(controller.V2Controller): + + @controller.v2_deprecated + def ca_cert(self, context, auth=None): + ca_file = open(CONF.signing.ca_certs, 'r') + data = ca_file.read() + ca_file.close() + return data + + @controller.v2_deprecated + def signing_cert(self, context, auth=None): + cert_file = open(CONF.signing.certfile, 'r') + data = cert_file.read() + cert_file.close() + return data + + @controller.v2_deprecated + def authenticate(self, context, auth=None): + """Authenticate credentials and return a token. + + Accept auth as a dict that looks like:: + + { + "auth":{ + "passwordCredentials":{ + "username":"test_user", + "password":"mypass" + }, + "tenantName":"customer-x" + } + } + + In this case, tenant is optional, if not provided the token will be + considered "unscoped" and can later be used to get a scoped token. + + Alternatively, this call accepts auth with only a token and tenant + that will return a token that is scoped to that tenant. + """ + + if auth is None: + raise exception.ValidationError(attribute='auth', + target='request body') + + if "token" in auth: + # Try to authenticate using a token + auth_info = self._authenticate_token( + context, auth) + else: + # Try external authentication + try: + auth_info = self._authenticate_external( + context, auth) + except ExternalAuthNotApplicable: + # Try local authentication + auth_info = self._authenticate_local( + context, auth) + + user_ref, tenant_ref, metadata_ref, expiry, bind, audit_id = auth_info + # Validate that the auth info is valid and nothing is disabled + try: + self.identity_api.assert_user_enabled( + user_id=user_ref['id'], user=user_ref) + if tenant_ref: + self.resource_api.assert_project_enabled( + project_id=tenant_ref['id'], project=tenant_ref) + except AssertionError as e: + six.reraise(exception.Unauthorized, exception.Unauthorized(e), + sys.exc_info()[2]) + # NOTE(morganfainberg): Make sure the data is in correct form since it + # might be consumed external to Keystone and this is a v2.0 controller. + # The user_ref is encoded into the auth_token_data which is returned as + # part of the token data. The token provider doesn't care about the + # format. + user_ref = self.v3_to_v2_user(user_ref) + if tenant_ref: + tenant_ref = self.filter_domain_id(tenant_ref) + auth_token_data = self._get_auth_token_data(user_ref, + tenant_ref, + metadata_ref, + expiry, + audit_id) + + if tenant_ref: + catalog_ref = self.catalog_api.get_catalog( + user_ref['id'], tenant_ref['id']) + else: + catalog_ref = {} + + auth_token_data['id'] = 'placeholder' + if bind: + auth_token_data['bind'] = bind + + roles_ref = [] + for role_id in metadata_ref.get('roles', []): + role_ref = self.role_api.get_role(role_id) + roles_ref.append(dict(name=role_ref['name'])) + + (token_id, token_data) = self.token_provider_api.issue_v2_token( + auth_token_data, roles_ref=roles_ref, catalog_ref=catalog_ref) + + # NOTE(wanghong): We consume a trust use only when we are using trusts + # and have successfully issued a token. + if CONF.trust.enabled and 'trust_id' in auth: + self.trust_api.consume_use(auth['trust_id']) + + return token_data + + def _restrict_scope(self, token_model_ref): + # A trust token cannot be used to get another token + if token_model_ref.trust_scoped: + raise exception.Forbidden() + if not CONF.token.allow_rescope_scoped_token: + # Do not allow conversion from scoped tokens. + if token_model_ref.project_scoped or token_model_ref.domain_scoped: + raise exception.Forbidden(action=_("rescope a scoped token")) + + def _authenticate_token(self, context, auth): + """Try to authenticate using an already existing token. + + Returns auth_token_data, (user_ref, tenant_ref, metadata_ref) + """ + if 'token' not in auth: + raise exception.ValidationError( + attribute='token', target='auth') + + if "id" not in auth['token']: + raise exception.ValidationError( + attribute="id", target="token") + + old_token = auth['token']['id'] + if len(old_token) > CONF.max_token_size: + raise exception.ValidationSizeError(attribute='token', + size=CONF.max_token_size) + + try: + token_model_ref = token_model.KeystoneToken( + token_id=old_token, + token_data=self.token_provider_api.validate_token(old_token)) + except exception.NotFound as e: + raise exception.Unauthorized(e) + + wsgi.validate_token_bind(context, token_model_ref) + + self._restrict_scope(token_model_ref) + user_id = token_model_ref.user_id + tenant_id = self._get_project_id_from_auth(auth) + + if not CONF.trust.enabled and 'trust_id' in auth: + raise exception.Forbidden('Trusts are disabled.') + elif CONF.trust.enabled and 'trust_id' in auth: + trust_ref = self.trust_api.get_trust(auth['trust_id']) + if trust_ref is None: + raise exception.Forbidden() + if user_id != trust_ref['trustee_user_id']: + raise exception.Forbidden() + if (trust_ref['project_id'] and + tenant_id != trust_ref['project_id']): + raise exception.Forbidden() + if ('expires' in trust_ref) and (trust_ref['expires']): + expiry = trust_ref['expires'] + if expiry < timeutils.parse_isotime(timeutils.isotime()): + raise exception.Forbidden() + user_id = trust_ref['trustor_user_id'] + trustor_user_ref = self.identity_api.get_user( + trust_ref['trustor_user_id']) + if not trustor_user_ref['enabled']: + raise exception.Forbidden() + trustee_user_ref = self.identity_api.get_user( + trust_ref['trustee_user_id']) + if not trustee_user_ref['enabled']: + raise exception.Forbidden() + + if trust_ref['impersonation'] is True: + current_user_ref = trustor_user_ref + else: + current_user_ref = trustee_user_ref + + else: + current_user_ref = self.identity_api.get_user(user_id) + + metadata_ref = {} + tenant_ref, metadata_ref['roles'] = self._get_project_roles_and_ref( + user_id, tenant_id) + + expiry = token_model_ref.expires + if CONF.trust.enabled and 'trust_id' in auth: + trust_id = auth['trust_id'] + trust_roles = [] + for role in trust_ref['roles']: + if 'roles' not in metadata_ref: + raise exception.Forbidden() + if role['id'] in metadata_ref['roles']: + trust_roles.append(role['id']) + else: + raise exception.Forbidden() + if 'expiry' in trust_ref and trust_ref['expiry']: + trust_expiry = timeutils.parse_isotime(trust_ref['expiry']) + if trust_expiry < expiry: + expiry = trust_expiry + metadata_ref['roles'] = trust_roles + metadata_ref['trustee_user_id'] = trust_ref['trustee_user_id'] + metadata_ref['trust_id'] = trust_id + + bind = token_model_ref.bind + audit_id = token_model_ref.audit_chain_id + + return (current_user_ref, tenant_ref, metadata_ref, expiry, bind, + audit_id) + + def _authenticate_local(self, context, auth): + """Try to authenticate against the identity backend. + + Returns auth_token_data, (user_ref, tenant_ref, metadata_ref) + """ + if 'passwordCredentials' not in auth: + raise exception.ValidationError( + attribute='passwordCredentials', target='auth') + + if "password" not in auth['passwordCredentials']: + raise exception.ValidationError( + attribute='password', target='passwordCredentials') + + password = auth['passwordCredentials']['password'] + if password and len(password) > CONF.identity.max_password_length: + raise exception.ValidationSizeError( + attribute='password', size=CONF.identity.max_password_length) + + if (not auth['passwordCredentials'].get("userId") and + not auth['passwordCredentials'].get("username")): + raise exception.ValidationError( + attribute='username or userId', + target='passwordCredentials') + + user_id = auth['passwordCredentials'].get('userId') + if user_id and len(user_id) > CONF.max_param_size: + raise exception.ValidationSizeError(attribute='userId', + size=CONF.max_param_size) + + username = auth['passwordCredentials'].get('username', '') + + if username: + if len(username) > CONF.max_param_size: + raise exception.ValidationSizeError(attribute='username', + size=CONF.max_param_size) + try: + user_ref = self.identity_api.get_user_by_name( + username, CONF.identity.default_domain_id) + user_id = user_ref['id'] + except exception.UserNotFound as e: + raise exception.Unauthorized(e) + + try: + user_ref = self.identity_api.authenticate( + context, + user_id=user_id, + password=password) + except AssertionError as e: + raise exception.Unauthorized(e.args[0]) + + metadata_ref = {} + tenant_id = self._get_project_id_from_auth(auth) + tenant_ref, metadata_ref['roles'] = self._get_project_roles_and_ref( + user_id, tenant_id) + + expiry = provider.default_expire_time() + bind = None + audit_id = None + return (user_ref, tenant_ref, metadata_ref, expiry, bind, audit_id) + + def _authenticate_external(self, context, auth): + """Try to authenticate an external user via REMOTE_USER variable. + + Returns auth_token_data, (user_ref, tenant_ref, metadata_ref) + """ + environment = context.get('environment', {}) + if not environment.get('REMOTE_USER'): + raise ExternalAuthNotApplicable() + + username = environment['REMOTE_USER'] + try: + user_ref = self.identity_api.get_user_by_name( + username, CONF.identity.default_domain_id) + user_id = user_ref['id'] + except exception.UserNotFound as e: + raise exception.Unauthorized(e) + + metadata_ref = {} + tenant_id = self._get_project_id_from_auth(auth) + tenant_ref, metadata_ref['roles'] = self._get_project_roles_and_ref( + user_id, tenant_id) + + expiry = provider.default_expire_time() + bind = None + if ('kerberos' in CONF.token.bind and + environment.get('AUTH_TYPE', '').lower() == 'negotiate'): + bind = {'kerberos': username} + audit_id = None + + return (user_ref, tenant_ref, metadata_ref, expiry, bind, audit_id) + + def _get_auth_token_data(self, user, tenant, metadata, expiry, audit_id): + return dict(user=user, + tenant=tenant, + metadata=metadata, + expires=expiry, + parent_audit_id=audit_id) + + def _get_project_id_from_auth(self, auth): + """Extract tenant information from auth dict. + + Returns a valid tenant_id if it exists, or None if not specified. + """ + tenant_id = auth.get('tenantId') + if tenant_id and len(tenant_id) > CONF.max_param_size: + raise exception.ValidationSizeError(attribute='tenantId', + size=CONF.max_param_size) + + tenant_name = auth.get('tenantName') + if tenant_name and len(tenant_name) > CONF.max_param_size: + raise exception.ValidationSizeError(attribute='tenantName', + size=CONF.max_param_size) + + if tenant_name: + try: + tenant_ref = self.resource_api.get_project_by_name( + tenant_name, CONF.identity.default_domain_id) + tenant_id = tenant_ref['id'] + except exception.ProjectNotFound as e: + raise exception.Unauthorized(e) + return tenant_id + + def _get_project_roles_and_ref(self, user_id, tenant_id): + """Returns the project roles for this user, and the project ref.""" + + tenant_ref = None + role_list = [] + if tenant_id: + try: + tenant_ref = self.resource_api.get_project(tenant_id) + role_list = self.assignment_api.get_roles_for_user_and_project( + user_id, tenant_id) + except exception.ProjectNotFound: + pass + + if not role_list: + msg = _('User %(u_id)s is unauthorized for tenant %(t_id)s') + msg = msg % {'u_id': user_id, 't_id': tenant_id} + LOG.warning(msg) + raise exception.Unauthorized(msg) + + return (tenant_ref, role_list) + + def _get_token_ref(self, token_id, belongs_to=None): + """Returns a token if a valid one exists. + + Optionally, limited to a token owned by a specific tenant. + + """ + token_ref = token_model.KeystoneToken( + token_id=token_id, + token_data=self.token_provider_api.validate_token(token_id)) + if belongs_to: + if not token_ref.project_scoped: + raise exception.Unauthorized( + _('Token does not belong to specified tenant.')) + if token_ref.project_id != belongs_to: + raise exception.Unauthorized( + _('Token does not belong to specified tenant.')) + return token_ref + + @controller.v2_deprecated + @controller.protected() + def validate_token_head(self, context, token_id): + """Check that a token is valid. + + Optionally, also ensure that it is owned by a specific tenant. + + Identical to ``validate_token``, except does not return a response. + + The code in ``keystone.common.wsgi.render_response`` will remove + the content body. + + """ + belongs_to = context['query_string'].get('belongsTo') + return self.token_provider_api.validate_v2_token(token_id, belongs_to) + + @controller.v2_deprecated + @controller.protected() + def validate_token(self, context, token_id): + """Check that a token is valid. + + Optionally, also ensure that it is owned by a specific tenant. + + Returns metadata about the token along any associated roles. + + """ + belongs_to = context['query_string'].get('belongsTo') + # TODO(ayoung) validate against revocation API + return self.token_provider_api.validate_v2_token(token_id, belongs_to) + + @controller.v2_deprecated + def delete_token(self, context, token_id): + """Delete a token, effectively invalidating it for authz.""" + # TODO(termie): this stuff should probably be moved to middleware + self.assert_admin(context) + self.token_provider_api.revoke_token(token_id) + + @controller.v2_deprecated + @controller.protected() + def revocation_list(self, context, auth=None): + if not CONF.token.revoke_by_id: + raise exception.Gone() + tokens = self.token_provider_api.list_revoked_tokens() + + for t in tokens: + expires = t['expires'] + if expires and isinstance(expires, datetime.datetime): + t['expires'] = timeutils.isotime(expires) + data = {'revoked': tokens} + json_data = jsonutils.dumps(data) + signed_text = cms.cms_sign_text(json_data, + CONF.signing.certfile, + CONF.signing.keyfile) + + return {'signed': signed_text} + + @controller.v2_deprecated + def endpoints(self, context, token_id): + """Return a list of endpoints available to the token.""" + self.assert_admin(context) + + token_ref = self._get_token_ref(token_id) + + catalog_ref = None + if token_ref.project_id: + catalog_ref = self.catalog_api.get_catalog( + token_ref.user_id, + token_ref.project_id) + + return Auth.format_endpoint_list(catalog_ref) + + @classmethod + def format_endpoint_list(cls, catalog_ref): + """Formats a list of endpoints according to Identity API v2. + + The v2.0 API wants an endpoint list to look like:: + + { + 'endpoints': [ + { + 'id': $endpoint_id, + 'name': $SERVICE[name], + 'type': $SERVICE, + 'tenantId': $tenant_id, + 'region': $REGION, + } + ], + 'endpoints_links': [], + } + + """ + if not catalog_ref: + return {} + + endpoints = [] + for region_name, region_ref in six.iteritems(catalog_ref): + for service_type, service_ref in six.iteritems(region_ref): + endpoints.append({ + 'id': service_ref.get('id'), + 'name': service_ref.get('name'), + 'type': service_type, + 'region': region_name, + 'publicURL': service_ref.get('publicURL'), + 'internalURL': service_ref.get('internalURL'), + 'adminURL': service_ref.get('adminURL'), + }) + + return {'endpoints': endpoints, 'endpoints_links': []} diff --git a/keystone-moon/keystone/token/persistence/__init__.py b/keystone-moon/keystone/token/persistence/__init__.py new file mode 100644 index 00000000..29ad5653 --- /dev/null +++ b/keystone-moon/keystone/token/persistence/__init__.py @@ -0,0 +1,16 @@ +# 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 keystone.token.persistence.core import * # noqa + + +__all__ = ['Manager', 'Driver', 'backends'] diff --git a/keystone-moon/keystone/token/persistence/backends/__init__.py b/keystone-moon/keystone/token/persistence/backends/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/token/persistence/backends/kvs.py b/keystone-moon/keystone/token/persistence/backends/kvs.py new file mode 100644 index 00000000..b4807bf1 --- /dev/null +++ b/keystone-moon/keystone/token/persistence/backends/kvs.py @@ -0,0 +1,357 @@ +# Copyright 2013 Metacloud, Inc. +# Copyright 2012 OpenStack Foundation +# +# 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 __future__ import absolute_import +import copy + +from oslo_config import cfg +from oslo_log import log +from oslo_utils import timeutils +import six + +from keystone.common import kvs +from keystone import exception +from keystone.i18n import _, _LE, _LW +from keystone import token +from keystone.token import provider + + +CONF = cfg.CONF +LOG = log.getLogger(__name__) + + +class Token(token.persistence.Driver): + """KeyValueStore backend for tokens. + + This is the base implementation for any/all key-value-stores (e.g. + memcached) for the Token backend. It is recommended to only use the base + in-memory implementation for testing purposes. + """ + + revocation_key = 'revocation-list' + kvs_backend = 'openstack.kvs.Memory' + + def __init__(self, backing_store=None, **kwargs): + super(Token, self).__init__() + self._store = kvs.get_key_value_store('token-driver') + if backing_store is not None: + self.kvs_backend = backing_store + if not self._store.is_configured: + # Do not re-configure the backend if the store has been initialized + self._store.configure(backing_store=self.kvs_backend, **kwargs) + if self.__class__ == Token: + # NOTE(morganfainberg): Only warn if the base KVS implementation + # is instantiated. + LOG.warn(_LW('It is recommended to only use the base ' + 'key-value-store implementation for the token driver ' + 'for testing purposes. Please use ' + 'keystone.token.persistence.backends.memcache.Token ' + 'or keystone.token.persistence.backends.sql.Token ' + 'instead.')) + + def _prefix_token_id(self, token_id): + return 'token-%s' % token_id.encode('utf-8') + + def _prefix_user_id(self, user_id): + return 'usertokens-%s' % user_id.encode('utf-8') + + def _get_key_or_default(self, key, default=None): + try: + return self._store.get(key) + except exception.NotFound: + return default + + def _get_key(self, key): + return self._store.get(key) + + def _set_key(self, key, value, lock=None): + self._store.set(key, value, lock) + + def _delete_key(self, key): + return self._store.delete(key) + + def get_token(self, token_id): + ptk = self._prefix_token_id(token_id) + try: + token_ref = self._get_key(ptk) + except exception.NotFound: + raise exception.TokenNotFound(token_id=token_id) + + return token_ref + + def create_token(self, token_id, data): + """Create a token by id and data. + + It is assumed the caller has performed data validation on the "data" + parameter. + """ + data_copy = copy.deepcopy(data) + ptk = self._prefix_token_id(token_id) + if not data_copy.get('expires'): + data_copy['expires'] = provider.default_expire_time() + if not data_copy.get('user_id'): + data_copy['user_id'] = data_copy['user']['id'] + + # NOTE(morganfainberg): for ease of manipulating the data without + # concern about the backend, always store the value(s) in the + # index as the isotime (string) version so this is where the string is + # built. + expires_str = timeutils.isotime(data_copy['expires'], subsecond=True) + + self._set_key(ptk, data_copy) + user_id = data['user']['id'] + user_key = self._prefix_user_id(user_id) + self._update_user_token_list(user_key, token_id, expires_str) + if CONF.trust.enabled and data.get('trust_id'): + # NOTE(morganfainberg): If trusts are enabled and this is a trust + # scoped token, we add the token to the trustee list as well. This + # allows password changes of the trustee to also expire the token. + # There is no harm in placing the token in multiple lists, as + # _list_tokens is smart enough to handle almost any case of + # valid/invalid/expired for a given token. + token_data = data_copy['token_data'] + if data_copy['token_version'] == token.provider.V2: + trustee_user_id = token_data['access']['trust'][ + 'trustee_user_id'] + elif data_copy['token_version'] == token.provider.V3: + trustee_user_id = token_data['OS-TRUST:trust'][ + 'trustee_user_id'] + else: + raise exception.UnsupportedTokenVersionException( + _('Unknown token version %s') % + data_copy.get('token_version')) + + trustee_key = self._prefix_user_id(trustee_user_id) + self._update_user_token_list(trustee_key, token_id, expires_str) + + return data_copy + + def _get_user_token_list_with_expiry(self, user_key): + """Return a list of tuples in the format (token_id, token_expiry) for + the user_key. + """ + return self._get_key_or_default(user_key, default=[]) + + def _get_user_token_list(self, user_key): + """Return a list of token_ids for the user_key.""" + token_list = self._get_user_token_list_with_expiry(user_key) + # Each element is a tuple of (token_id, token_expiry). Most code does + # not care about the expiry, it is stripped out and only a + # list of token_ids are returned. + return [t[0] for t in token_list] + + def _update_user_token_list(self, user_key, token_id, expires_isotime_str): + current_time = self._get_current_time() + revoked_token_list = set([t['id'] for t in + self.list_revoked_tokens()]) + + with self._store.get_lock(user_key) as lock: + filtered_list = [] + token_list = self._get_user_token_list_with_expiry(user_key) + for item in token_list: + try: + item_id, expires = self._format_token_index_item(item) + except (ValueError, TypeError): + # NOTE(morganfainberg): Skip on expected errors + # possibilities from the `_format_token_index_item` method. + continue + + if expires < current_time: + LOG.debug(('Token `%(token_id)s` is expired, removing ' + 'from `%(user_key)s`.'), + {'token_id': item_id, 'user_key': user_key}) + continue + + if item_id in revoked_token_list: + # NOTE(morganfainberg): If the token has been revoked, it + # can safely be removed from this list. This helps to keep + # the user_token_list as reasonably small as possible. + LOG.debug(('Token `%(token_id)s` is revoked, removing ' + 'from `%(user_key)s`.'), + {'token_id': item_id, 'user_key': user_key}) + continue + filtered_list.append(item) + filtered_list.append((token_id, expires_isotime_str)) + self._set_key(user_key, filtered_list, lock) + return filtered_list + + def _get_current_time(self): + return timeutils.normalize_time(timeutils.utcnow()) + + def _add_to_revocation_list(self, data, lock): + filtered_list = [] + revoked_token_data = {} + + current_time = self._get_current_time() + expires = data['expires'] + + if isinstance(expires, six.string_types): + expires = timeutils.parse_isotime(expires) + + expires = timeutils.normalize_time(expires) + + if expires < current_time: + LOG.warning(_LW('Token `%s` is expired, not adding to the ' + 'revocation list.'), data['id']) + return + + revoked_token_data['expires'] = timeutils.isotime(expires, + subsecond=True) + revoked_token_data['id'] = data['id'] + + token_list = self._get_key_or_default(self.revocation_key, default=[]) + if not isinstance(token_list, list): + # NOTE(morganfainberg): In the case that the revocation list is not + # in a format we understand, reinitialize it. This is an attempt to + # not allow the revocation list to be completely broken if + # somehow the key is changed outside of keystone (e.g. memcache + # that is shared by multiple applications). Logging occurs at error + # level so that the cloud administrators have some awareness that + # the revocation_list needed to be cleared out. In all, this should + # be recoverable. Keystone cannot control external applications + # from changing a key in some backends, however, it is possible to + # gracefully handle and notify of this event. + LOG.error(_LE('Reinitializing revocation list due to error ' + 'in loading revocation list from backend. ' + 'Expected `list` type got `%(type)s`. Old ' + 'revocation list data: %(list)r'), + {'type': type(token_list), 'list': token_list}) + token_list = [] + + # NOTE(morganfainberg): on revocation, cleanup the expired entries, try + # to keep the list of tokens revoked at the minimum. + for token_data in token_list: + try: + expires_at = timeutils.normalize_time( + timeutils.parse_isotime(token_data['expires'])) + except ValueError: + LOG.warning(_LW('Removing `%s` from revocation list due to ' + 'invalid expires data in revocation list.'), + token_data.get('id', 'INVALID_TOKEN_DATA')) + continue + if expires_at > current_time: + filtered_list.append(token_data) + filtered_list.append(revoked_token_data) + self._set_key(self.revocation_key, filtered_list, lock) + + def delete_token(self, token_id): + # Test for existence + with self._store.get_lock(self.revocation_key) as lock: + data = self.get_token(token_id) + ptk = self._prefix_token_id(token_id) + result = self._delete_key(ptk) + self._add_to_revocation_list(data, lock) + return result + + def delete_tokens(self, user_id, tenant_id=None, trust_id=None, + consumer_id=None): + return super(Token, self).delete_tokens( + user_id=user_id, + tenant_id=tenant_id, + trust_id=trust_id, + consumer_id=consumer_id, + ) + + def _format_token_index_item(self, item): + try: + token_id, expires = item + except (TypeError, ValueError): + LOG.debug(('Invalid token entry expected tuple of ' + '`(, )` got: `%(item)r`'), + dict(item=item)) + raise + + try: + expires = timeutils.normalize_time( + timeutils.parse_isotime(expires)) + except ValueError: + LOG.debug(('Invalid expires time on token `%(token_id)s`:' + ' %(expires)r'), + dict(token_id=token_id, expires=expires)) + raise + return token_id, expires + + def _token_match_tenant(self, token_ref, tenant_id): + if token_ref.get('tenant'): + return token_ref['tenant'].get('id') == tenant_id + return False + + def _token_match_trust(self, token_ref, trust_id): + if not token_ref.get('trust_id'): + return False + return token_ref['trust_id'] == trust_id + + def _token_match_consumer(self, token_ref, consumer_id): + try: + oauth = token_ref['token_data']['token']['OS-OAUTH1'] + return oauth.get('consumer_id') == consumer_id + except KeyError: + return False + + def _list_tokens(self, user_id, tenant_id=None, trust_id=None, + consumer_id=None): + # This function is used to generate the list of tokens that should be + # revoked when revoking by token identifiers. This approach will be + # deprecated soon, probably in the Juno release. Setting revoke_by_id + # to False indicates that this kind of recording should not be + # performed. In order to test the revocation events, tokens shouldn't + # be deleted from the backends. This check ensures that tokens are + # still recorded. + if not CONF.token.revoke_by_id: + return [] + tokens = [] + user_key = self._prefix_user_id(user_id) + token_list = self._get_user_token_list_with_expiry(user_key) + current_time = self._get_current_time() + for item in token_list: + try: + token_id, expires = self._format_token_index_item(item) + except (TypeError, ValueError): + # NOTE(morganfainberg): Skip on expected error possibilities + # from the `_format_token_index_item` method. + continue + + if expires < current_time: + continue + + try: + token_ref = self.get_token(token_id) + except exception.TokenNotFound: + # NOTE(morganfainberg): Token doesn't exist, skip it. + continue + if token_ref: + if tenant_id is not None: + if not self._token_match_tenant(token_ref, tenant_id): + continue + if trust_id is not None: + if not self._token_match_trust(token_ref, trust_id): + continue + if consumer_id is not None: + if not self._token_match_consumer(token_ref, consumer_id): + continue + + tokens.append(token_id) + return tokens + + def list_revoked_tokens(self): + revoked_token_list = self._get_key_or_default(self.revocation_key, + default=[]) + if isinstance(revoked_token_list, list): + return revoked_token_list + return [] + + def flush_expired_tokens(self): + """Archive or delete tokens that have expired.""" + raise exception.NotImplemented() diff --git a/keystone-moon/keystone/token/persistence/backends/memcache.py b/keystone-moon/keystone/token/persistence/backends/memcache.py new file mode 100644 index 00000000..03f27eaf --- /dev/null +++ b/keystone-moon/keystone/token/persistence/backends/memcache.py @@ -0,0 +1,33 @@ +# Copyright 2013 Metacloud, Inc. +# Copyright 2012 OpenStack Foundation +# +# 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 oslo_config import cfg + +from keystone.token.persistence.backends import kvs + + +CONF = cfg.CONF + + +class Token(kvs.Token): + kvs_backend = 'openstack.kvs.Memcached' + memcached_backend = 'memcached' + + def __init__(self, *args, **kwargs): + kwargs['memcached_backend'] = self.memcached_backend + kwargs['no_expiry_keys'] = [self.revocation_key] + kwargs['memcached_expire_time'] = CONF.token.expiration + kwargs['url'] = CONF.memcache.servers + super(Token, self).__init__(*args, **kwargs) diff --git a/keystone-moon/keystone/token/persistence/backends/memcache_pool.py b/keystone-moon/keystone/token/persistence/backends/memcache_pool.py new file mode 100644 index 00000000..55f9e8ae --- /dev/null +++ b/keystone-moon/keystone/token/persistence/backends/memcache_pool.py @@ -0,0 +1,28 @@ +# 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 oslo_config import cfg + +from keystone.token.persistence.backends import memcache + + +CONF = cfg.CONF + + +class Token(memcache.Token): + memcached_backend = 'pooled_memcached' + + def __init__(self, *args, **kwargs): + for arg in ('dead_retry', 'socket_timeout', 'pool_maxsize', + 'pool_unused_timeout', 'pool_connection_get_timeout'): + kwargs[arg] = getattr(CONF.memcache, arg) + super(Token, self).__init__(*args, **kwargs) diff --git a/keystone-moon/keystone/token/persistence/backends/sql.py b/keystone-moon/keystone/token/persistence/backends/sql.py new file mode 100644 index 00000000..fc70fb92 --- /dev/null +++ b/keystone-moon/keystone/token/persistence/backends/sql.py @@ -0,0 +1,279 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 +import functools + +from oslo_config import cfg +from oslo_log import log +from oslo_utils import timeutils + +from keystone.common import sql +from keystone import exception +from keystone.i18n import _LI +from keystone import token +from keystone.token import provider + + +CONF = cfg.CONF +LOG = log.getLogger(__name__) + + +class TokenModel(sql.ModelBase, sql.DictBase): + __tablename__ = 'token' + attributes = ['id', 'expires', 'user_id', 'trust_id'] + id = sql.Column(sql.String(64), primary_key=True) + expires = sql.Column(sql.DateTime(), default=None) + extra = sql.Column(sql.JsonBlob()) + valid = sql.Column(sql.Boolean(), default=True, nullable=False) + user_id = sql.Column(sql.String(64)) + trust_id = sql.Column(sql.String(64)) + __table_args__ = ( + sql.Index('ix_token_expires', 'expires'), + sql.Index('ix_token_expires_valid', 'expires', 'valid'), + sql.Index('ix_token_user_id', 'user_id'), + sql.Index('ix_token_trust_id', 'trust_id') + ) + + +def _expiry_range_batched(session, upper_bound_func, batch_size): + """Returns the stop point of the next batch for expiration. + + Return the timestamp of the next token that is `batch_size` rows from + being the oldest expired token. + """ + + # This expiry strategy splits the tokens into roughly equal sized batches + # to be deleted. It does this by finding the timestamp of a token + # `batch_size` rows from the oldest token and yielding that to the caller. + # It's expected that the caller will then delete all rows with a timestamp + # equal to or older than the one yielded. This may delete slightly more + # tokens than the batch_size, but that should be ok in almost all cases. + LOG.debug('Token expiration batch size: %d', batch_size) + query = session.query(TokenModel.expires) + query = query.filter(TokenModel.expires < upper_bound_func()) + query = query.order_by(TokenModel.expires) + query = query.offset(batch_size - 1) + query = query.limit(1) + while True: + try: + next_expiration = query.one()[0] + except sql.NotFound: + # There are less than `batch_size` rows remaining, so fall + # through to the normal delete + break + yield next_expiration + yield upper_bound_func() + + +def _expiry_range_all(session, upper_bound_func): + """Expires all tokens in one pass.""" + + yield upper_bound_func() + + +class Token(token.persistence.Driver): + # Public interface + def get_token(self, token_id): + if token_id is None: + raise exception.TokenNotFound(token_id=token_id) + session = sql.get_session() + token_ref = session.query(TokenModel).get(token_id) + if not token_ref or not token_ref.valid: + raise exception.TokenNotFound(token_id=token_id) + return token_ref.to_dict() + + def create_token(self, token_id, data): + data_copy = copy.deepcopy(data) + if not data_copy.get('expires'): + data_copy['expires'] = provider.default_expire_time() + if not data_copy.get('user_id'): + data_copy['user_id'] = data_copy['user']['id'] + + token_ref = TokenModel.from_dict(data_copy) + token_ref.valid = True + session = sql.get_session() + with session.begin(): + session.add(token_ref) + return token_ref.to_dict() + + def delete_token(self, token_id): + session = sql.get_session() + with session.begin(): + token_ref = session.query(TokenModel).get(token_id) + if not token_ref or not token_ref.valid: + raise exception.TokenNotFound(token_id=token_id) + token_ref.valid = False + + def delete_tokens(self, user_id, tenant_id=None, trust_id=None, + consumer_id=None): + """Deletes all tokens in one session + + The user_id will be ignored if the trust_id is specified. user_id + will always be specified. + If using a trust, the token's user_id is set to the trustee's user ID + or the trustor's user ID, so will use trust_id to query the tokens. + + """ + session = sql.get_session() + with session.begin(): + now = timeutils.utcnow() + query = session.query(TokenModel) + query = query.filter_by(valid=True) + query = query.filter(TokenModel.expires > now) + if trust_id: + query = query.filter(TokenModel.trust_id == trust_id) + else: + query = query.filter(TokenModel.user_id == user_id) + + for token_ref in query.all(): + if tenant_id: + token_ref_dict = token_ref.to_dict() + if not self._tenant_matches(tenant_id, token_ref_dict): + continue + if consumer_id: + token_ref_dict = token_ref.to_dict() + if not self._consumer_matches(consumer_id, token_ref_dict): + continue + + token_ref.valid = False + + def _tenant_matches(self, tenant_id, token_ref_dict): + return ((tenant_id is None) or + (token_ref_dict.get('tenant') and + token_ref_dict['tenant'].get('id') == tenant_id)) + + def _consumer_matches(self, consumer_id, ref): + if consumer_id is None: + return True + else: + try: + oauth = ref['token_data']['token'].get('OS-OAUTH1', {}) + return oauth and oauth['consumer_id'] == consumer_id + except KeyError: + return False + + def _list_tokens_for_trust(self, trust_id): + session = sql.get_session() + tokens = [] + now = timeutils.utcnow() + query = session.query(TokenModel) + query = query.filter(TokenModel.expires > now) + query = query.filter(TokenModel.trust_id == trust_id) + + token_references = query.filter_by(valid=True) + for token_ref in token_references: + token_ref_dict = token_ref.to_dict() + tokens.append(token_ref_dict['id']) + return tokens + + def _list_tokens_for_user(self, user_id, tenant_id=None): + session = sql.get_session() + tokens = [] + now = timeutils.utcnow() + query = session.query(TokenModel) + query = query.filter(TokenModel.expires > now) + query = query.filter(TokenModel.user_id == user_id) + + token_references = query.filter_by(valid=True) + for token_ref in token_references: + token_ref_dict = token_ref.to_dict() + if self._tenant_matches(tenant_id, token_ref_dict): + tokens.append(token_ref['id']) + return tokens + + def _list_tokens_for_consumer(self, user_id, consumer_id): + tokens = [] + session = sql.get_session() + with session.begin(): + now = timeutils.utcnow() + query = session.query(TokenModel) + query = query.filter(TokenModel.expires > now) + query = query.filter(TokenModel.user_id == user_id) + token_references = query.filter_by(valid=True) + + for token_ref in token_references: + token_ref_dict = token_ref.to_dict() + if self._consumer_matches(consumer_id, token_ref_dict): + tokens.append(token_ref_dict['id']) + return tokens + + def _list_tokens(self, user_id, tenant_id=None, trust_id=None, + consumer_id=None): + if not CONF.token.revoke_by_id: + return [] + if trust_id: + return self._list_tokens_for_trust(trust_id) + if consumer_id: + return self._list_tokens_for_consumer(user_id, consumer_id) + else: + return self._list_tokens_for_user(user_id, tenant_id) + + def list_revoked_tokens(self): + session = sql.get_session() + tokens = [] + now = timeutils.utcnow() + query = session.query(TokenModel.id, TokenModel.expires) + query = query.filter(TokenModel.expires > now) + token_references = query.filter_by(valid=False) + for token_ref in token_references: + record = { + 'id': token_ref[0], + 'expires': token_ref[1], + } + tokens.append(record) + return tokens + + def _expiry_range_strategy(self, dialect): + """Choose a token range expiration strategy + + Based on the DB dialect, select an expiry range callable that is + appropriate. + """ + + # DB2 and MySQL can both benefit from a batched strategy. On DB2 the + # transaction log can fill up and on MySQL w/Galera, large + # transactions can exceed the maximum write set size. + if dialect == 'ibm_db_sa': + # Limit of 100 is known to not fill a transaction log + # of default maximum size while not significantly + # impacting the performance of large token purges on + # systems where the maximum transaction log size has + # been increased beyond the default. + return functools.partial(_expiry_range_batched, + batch_size=100) + elif dialect == 'mysql': + # We want somewhat more than 100, since Galera replication delay is + # at least RTT*2. This can be a significant amount of time if + # doing replication across a WAN. + return functools.partial(_expiry_range_batched, + batch_size=1000) + return _expiry_range_all + + def flush_expired_tokens(self): + session = sql.get_session() + dialect = session.bind.dialect.name + expiry_range_func = self._expiry_range_strategy(dialect) + query = session.query(TokenModel.expires) + total_removed = 0 + upper_bound_func = timeutils.utcnow + for expiry_time in expiry_range_func(session, upper_bound_func): + delete_query = query.filter(TokenModel.expires <= + expiry_time) + row_count = delete_query.delete(synchronize_session=False) + total_removed += row_count + LOG.debug('Removed %d total expired tokens', total_removed) + + session.flush() + LOG.info(_LI('Total expired tokens removed: %d'), total_removed) diff --git a/keystone-moon/keystone/token/persistence/core.py b/keystone-moon/keystone/token/persistence/core.py new file mode 100644 index 00000000..19f0df35 --- /dev/null +++ b/keystone-moon/keystone/token/persistence/core.py @@ -0,0 +1,361 @@ +# Copyright 2012 OpenStack Foundation +# +# 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. + +"""Main entry point into the Token persistence service.""" + +import abc +import copy + +from oslo_config import cfg +from oslo_log import log +from oslo_utils import timeutils +import six + +from keystone.common import cache +from keystone.common import dependency +from keystone.common import manager +from keystone import exception +from keystone.i18n import _LW + + +CONF = cfg.CONF +LOG = log.getLogger(__name__) +MEMOIZE = cache.get_memoization_decorator(section='token') +REVOCATION_MEMOIZE = cache.get_memoization_decorator( + section='token', expiration_section='revoke') + + +@dependency.requires('assignment_api', 'identity_api', 'resource_api', + 'token_provider_api', 'trust_api') +class PersistenceManager(manager.Manager): + """Default pivot point for the Token backend. + + See :mod:`keystone.common.manager.Manager` for more details on how this + dynamically calls the backend. + + """ + + def __init__(self): + super(PersistenceManager, self).__init__(CONF.token.driver) + + def _assert_valid(self, token_id, token_ref): + """Raise TokenNotFound if the token is expired.""" + current_time = timeutils.normalize_time(timeutils.utcnow()) + expires = token_ref.get('expires') + if not expires or current_time > timeutils.normalize_time(expires): + raise exception.TokenNotFound(token_id=token_id) + + def get_token(self, token_id): + if not token_id: + # NOTE(morganfainberg): There are cases when the + # context['token_id'] will in-fact be None. This also saves + # a round-trip to the backend if we don't have a token_id. + raise exception.TokenNotFound(token_id='') + unique_id = self.token_provider_api.unique_id(token_id) + token_ref = self._get_token(unique_id) + # NOTE(morganfainberg): Lift expired checking to the manager, there is + # no reason to make the drivers implement this check. With caching, + # self._get_token could return an expired token. Make sure we behave + # as expected and raise TokenNotFound on those instances. + self._assert_valid(token_id, token_ref) + return token_ref + + @MEMOIZE + def _get_token(self, token_id): + # Only ever use the "unique" id in the cache key. + return self.driver.get_token(token_id) + + def create_token(self, token_id, data): + unique_id = self.token_provider_api.unique_id(token_id) + data_copy = copy.deepcopy(data) + data_copy['id'] = unique_id + ret = self.driver.create_token(unique_id, data_copy) + if MEMOIZE.should_cache(ret): + # NOTE(morganfainberg): when doing a cache set, you must pass the + # same arguments through, the same as invalidate (this includes + # "self"). First argument is always the value to be cached + self._get_token.set(ret, self, unique_id) + return ret + + def delete_token(self, token_id): + if not CONF.token.revoke_by_id: + return + unique_id = self.token_provider_api.unique_id(token_id) + self.driver.delete_token(unique_id) + self._invalidate_individual_token_cache(unique_id) + self.invalidate_revocation_list() + + def delete_tokens(self, user_id, tenant_id=None, trust_id=None, + consumer_id=None): + if not CONF.token.revoke_by_id: + return + token_list = self.driver._list_tokens(user_id, tenant_id, trust_id, + consumer_id) + self.driver.delete_tokens(user_id, tenant_id, trust_id, consumer_id) + for token_id in token_list: + unique_id = self.token_provider_api.unique_id(token_id) + self._invalidate_individual_token_cache(unique_id) + self.invalidate_revocation_list() + + @REVOCATION_MEMOIZE + def list_revoked_tokens(self): + return self.driver.list_revoked_tokens() + + def invalidate_revocation_list(self): + # NOTE(morganfainberg): Note that ``self`` needs to be passed to + # invalidate() because of the way the invalidation method works on + # determining cache-keys. + self.list_revoked_tokens.invalidate(self) + + def delete_tokens_for_domain(self, domain_id): + """Delete all tokens for a given domain. + + It will delete all the project-scoped tokens for the projects + that are owned by the given domain, as well as any tokens issued + to users that are owned by this domain. + + However, deletion of domain_scoped tokens will still need to be + implemented as stated in TODO below. + """ + if not CONF.token.revoke_by_id: + return + projects = self.resource_api.list_projects() + for project in projects: + if project['domain_id'] == domain_id: + for user_id in self.assignment_api.list_user_ids_for_project( + project['id']): + self.delete_tokens_for_user(user_id, project['id']) + # TODO(morganfainberg): implement deletion of domain_scoped tokens. + + users = self.identity_api.list_users(domain_id) + user_ids = (user['id'] for user in users) + self.delete_tokens_for_users(user_ids) + + def delete_tokens_for_user(self, user_id, project_id=None): + """Delete all tokens for a given user or user-project combination. + + This method adds in the extra logic for handling trust-scoped token + revocations in a single call instead of needing to explicitly handle + trusts in the caller's logic. + """ + if not CONF.token.revoke_by_id: + return + self.delete_tokens(user_id, tenant_id=project_id) + for trust in self.trust_api.list_trusts_for_trustee(user_id): + # Ensure we revoke tokens associated to the trust / project + # user_id combination. + self.delete_tokens(user_id, trust_id=trust['id'], + tenant_id=project_id) + for trust in self.trust_api.list_trusts_for_trustor(user_id): + # Ensure we revoke tokens associated to the trust / project / + # user_id combination where the user_id is the trustor. + + # NOTE(morganfainberg): This revocation is a bit coarse, but it + # covers a number of cases such as disabling of the trustor user, + # deletion of the trustor user (for any number of reasons). It + # might make sense to refine this and be more surgical on the + # deletions (e.g. don't revoke tokens for the trusts when the + # trustor changes password). For now, to maintain previous + # functionality, this will continue to be a bit overzealous on + # revocations. + self.delete_tokens(trust['trustee_user_id'], trust_id=trust['id'], + tenant_id=project_id) + + def delete_tokens_for_users(self, user_ids, project_id=None): + """Delete all tokens for a list of user_ids. + + :param user_ids: list of user identifiers + :param project_id: optional project identifier + """ + if not CONF.token.revoke_by_id: + return + for user_id in user_ids: + self.delete_tokens_for_user(user_id, project_id=project_id) + + def _invalidate_individual_token_cache(self, token_id): + # NOTE(morganfainberg): invalidate takes the exact same arguments as + # the normal method, this means we need to pass "self" in (which gets + # stripped off). + + # FIXME(morganfainberg): Does this cache actually need to be + # invalidated? We maintain a cached revocation list, which should be + # consulted before accepting a token as valid. For now we will + # do the explicit individual token invalidation. + self._get_token.invalidate(self, token_id) + self.token_provider_api.invalidate_individual_token_cache(token_id) + + +# NOTE(morganfainberg): @dependency.optional() is required here to ensure the +# class-level optional dependency control attribute is populated as empty +# this is because of the override of .__getattr__ and ensures that if the +# optional dependency injector changes attributes, this class doesn't break. +@dependency.optional() +@dependency.requires('token_provider_api') +@dependency.provider('token_api') +class Manager(object): + """The token_api provider. + + This class is a proxy class to the token_provider_api's persistence + manager. + """ + def __init__(self): + # NOTE(morganfainberg): __init__ is required for dependency processing. + super(Manager, self).__init__() + + def __getattr__(self, item): + """Forward calls to the `token_provider_api` persistence manager.""" + + # NOTE(morganfainberg): Prevent infinite recursion, raise an + # AttributeError for 'token_provider_api' ensuring that the dep + # injection doesn't infinitely try and lookup self.token_provider_api + # on _process_dependencies. This doesn't need an exception string as + # it should only ever be hit on instantiation. + if item == 'token_provider_api': + raise AttributeError() + + f = getattr(self.token_provider_api._persistence, item) + LOG.warning(_LW('`token_api.%s` is deprecated as of Juno in favor of ' + 'utilizing methods on `token_provider_api` and may be ' + 'removed in Kilo.'), item) + setattr(self, item, f) + return f + + +@six.add_metaclass(abc.ABCMeta) +class Driver(object): + """Interface description for a Token driver.""" + + @abc.abstractmethod + def get_token(self, token_id): + """Get a token by id. + + :param token_id: identity of the token + :type token_id: string + :returns: token_ref + :raises: keystone.exception.TokenNotFound + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def create_token(self, token_id, data): + """Create a token by id and data. + + :param token_id: identity of the token + :type token_id: string + :param data: dictionary with additional reference information + + :: + + { + expires='' + id=token_id, + user=user_ref, + tenant=tenant_ref, + metadata=metadata_ref + } + + :type data: dict + :returns: token_ref or None. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def delete_token(self, token_id): + """Deletes a token by id. + + :param token_id: identity of the token + :type token_id: string + :returns: None. + :raises: keystone.exception.TokenNotFound + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def delete_tokens(self, user_id, tenant_id=None, trust_id=None, + consumer_id=None): + """Deletes tokens by user. + + If the tenant_id is not None, only delete the tokens by user id under + the specified tenant. + + If the trust_id is not None, it will be used to query tokens and the + user_id will be ignored. + + If the consumer_id is not None, only delete the tokens by consumer id + that match the specified consumer id. + + :param user_id: identity of user + :type user_id: string + :param tenant_id: identity of the tenant + :type tenant_id: string + :param trust_id: identity of the trust + :type trust_id: string + :param consumer_id: identity of the consumer + :type consumer_id: string + :returns: None. + :raises: keystone.exception.TokenNotFound + + """ + if not CONF.token.revoke_by_id: + return + token_list = self._list_tokens(user_id, + tenant_id=tenant_id, + trust_id=trust_id, + consumer_id=consumer_id) + + for token in token_list: + try: + self.delete_token(token) + except exception.NotFound: + pass + + @abc.abstractmethod + def _list_tokens(self, user_id, tenant_id=None, trust_id=None, + consumer_id=None): + """Returns a list of current token_id's for a user + + This is effectively a private method only used by the ``delete_tokens`` + method and should not be called by anything outside of the + ``token_api`` manager or the token driver itself. + + :param user_id: identity of the user + :type user_id: string + :param tenant_id: identity of the tenant + :type tenant_id: string + :param trust_id: identity of the trust + :type trust_id: string + :param consumer_id: identity of the consumer + :type consumer_id: string + :returns: list of token_id's + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_revoked_tokens(self): + """Returns a list of all revoked tokens + + :returns: list of token_id's + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def flush_expired_tokens(self): + """Archive or delete tokens that have expired. + """ + raise exception.NotImplemented() # pragma: no cover diff --git a/keystone-moon/keystone/token/provider.py b/keystone-moon/keystone/token/provider.py new file mode 100644 index 00000000..fb41d4bb --- /dev/null +++ b/keystone-moon/keystone/token/provider.py @@ -0,0 +1,584 @@ +# Copyright 2012 OpenStack Foundation +# +# 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. + +"""Token provider interface.""" + +import abc +import base64 +import datetime +import sys +import uuid + +from keystoneclient.common import cms +from oslo_config import cfg +from oslo_log import log +from oslo_utils import timeutils +import six + +from keystone.common import cache +from keystone.common import dependency +from keystone.common import manager +from keystone import exception +from keystone.i18n import _, _LE +from keystone.models import token_model +from keystone import notifications +from keystone.token import persistence + + +CONF = cfg.CONF +LOG = log.getLogger(__name__) +MEMOIZE = cache.get_memoization_decorator(section='token') + +# NOTE(morganfainberg): This is for compatibility in case someone was relying +# on the old location of the UnsupportedTokenVersionException for their code. +UnsupportedTokenVersionException = exception.UnsupportedTokenVersionException + +# supported token versions +V2 = token_model.V2 +V3 = token_model.V3 +VERSIONS = token_model.VERSIONS + + +def base64_encode(s): + """Encode a URL-safe string.""" + return base64.urlsafe_b64encode(s).rstrip('=') + + +def random_urlsafe_str(): + """Generate a random URL-safe string.""" + # chop the padding (==) off the end of the encoding to save space + return base64.urlsafe_b64encode(uuid.uuid4().bytes)[:-2] + + +def random_urlsafe_str_to_bytes(s): + """Convert a string generated by ``random_urlsafe_str()`` to bytes.""" + # restore the padding (==) at the end of the string + return base64.urlsafe_b64decode(s + '==') + + +def default_expire_time(): + """Determine when a fresh token should expire. + + Expiration time varies based on configuration (see ``[token] expiration``). + + :returns: a naive UTC datetime.datetime object + + """ + expire_delta = datetime.timedelta(seconds=CONF.token.expiration) + return timeutils.utcnow() + expire_delta + + +def audit_info(parent_audit_id): + """Build the audit data for a token. + + If ``parent_audit_id`` is None, the list will be one element in length + containing a newly generated audit_id. + + If ``parent_audit_id`` is supplied, the list will be two elements in length + containing a newly generated audit_id and the ``parent_audit_id``. The + ``parent_audit_id`` will always be element index 1 in the resulting + list. + + :param parent_audit_id: the audit of the original token in the chain + :type parent_audit_id: str + :returns: Keystone token audit data + """ + audit_id = random_urlsafe_str() + if parent_audit_id is not None: + return [audit_id, parent_audit_id] + return [audit_id] + + +@dependency.provider('token_provider_api') +@dependency.requires('assignment_api', 'revoke_api') +class Manager(manager.Manager): + """Default pivot point for the token provider backend. + + See :mod:`keystone.common.manager.Manager` for more details on how this + dynamically calls the backend. + + """ + + V2 = V2 + V3 = V3 + VERSIONS = VERSIONS + INVALIDATE_PROJECT_TOKEN_PERSISTENCE = 'invalidate_project_tokens' + INVALIDATE_USER_TOKEN_PERSISTENCE = 'invalidate_user_tokens' + _persistence_manager = None + + def __init__(self): + super(Manager, self).__init__(CONF.token.provider) + self._register_callback_listeners() + + def _register_callback_listeners(self): + # This is used by the @dependency.provider decorator to register the + # provider (token_provider_api) manager to listen for trust deletions. + callbacks = { + notifications.ACTIONS.deleted: [ + ['OS-TRUST:trust', self._trust_deleted_event_callback], + ['user', self._delete_user_tokens_callback], + ['domain', self._delete_domain_tokens_callback], + ], + notifications.ACTIONS.disabled: [ + ['user', self._delete_user_tokens_callback], + ['domain', self._delete_domain_tokens_callback], + ['project', self._delete_project_tokens_callback], + ], + notifications.ACTIONS.internal: [ + [notifications.INVALIDATE_USER_TOKEN_PERSISTENCE, + self._delete_user_tokens_callback], + [notifications.INVALIDATE_USER_PROJECT_TOKEN_PERSISTENCE, + self._delete_user_project_tokens_callback], + [notifications.INVALIDATE_USER_OAUTH_CONSUMER_TOKENS, + self._delete_user_oauth_consumer_tokens_callback], + ] + } + + for event, cb_info in six.iteritems(callbacks): + for resource_type, callback_fns in cb_info: + notifications.register_event_callback(event, resource_type, + callback_fns) + + @property + def _needs_persistence(self): + return self.driver.needs_persistence() + + @property + def _persistence(self): + # NOTE(morganfainberg): This should not be handled via __init__ to + # avoid dependency injection oddities circular dependencies (where + # the provider manager requires the token persistence manager, which + # requires the token provider manager). + if self._persistence_manager is None: + self._persistence_manager = persistence.PersistenceManager() + return self._persistence_manager + + def unique_id(self, token_id): + """Return a unique ID for a token. + + The returned value is useful as the primary key of a database table, + memcache store, or other lookup table. + + :returns: Given a PKI token, returns it's hashed value. Otherwise, + returns the passed-in value (such as a UUID token ID or an + existing hash). + """ + return cms.cms_hash_token(token_id, mode=CONF.token.hash_algorithm) + + def _create_token(self, token_id, token_data): + try: + if isinstance(token_data['expires'], six.string_types): + token_data['expires'] = timeutils.normalize_time( + timeutils.parse_isotime(token_data['expires'])) + self._persistence.create_token(token_id, token_data) + except Exception: + exc_info = sys.exc_info() + # an identical token may have been created already. + # if so, return the token_data as it is also identical + try: + self._persistence.get_token(token_id) + except exception.TokenNotFound: + six.reraise(*exc_info) + + def validate_token(self, token_id, belongs_to=None): + unique_id = self.unique_id(token_id) + # NOTE(morganfainberg): Ensure we never use the long-form token_id + # (PKI) as part of the cache_key. + token = self._validate_token(unique_id) + self._token_belongs_to(token, belongs_to) + self._is_valid_token(token) + return token + + def check_revocation_v2(self, token): + try: + token_data = token['access'] + except KeyError: + raise exception.TokenNotFound(_('Failed to validate token')) + + token_values = self.revoke_api.model.build_token_values_v2( + token_data, CONF.identity.default_domain_id) + self.revoke_api.check_token(token_values) + + def validate_v2_token(self, token_id, belongs_to=None): + unique_id = self.unique_id(token_id) + if self._needs_persistence: + # NOTE(morganfainberg): Ensure we never use the long-form token_id + # (PKI) as part of the cache_key. + token_ref = self._persistence.get_token(unique_id) + else: + token_ref = token_id + token = self._validate_v2_token(token_ref) + self._token_belongs_to(token, belongs_to) + self._is_valid_token(token) + return token + + def check_revocation_v3(self, token): + try: + token_data = token['token'] + except KeyError: + raise exception.TokenNotFound(_('Failed to validate token')) + token_values = self.revoke_api.model.build_token_values(token_data) + self.revoke_api.check_token(token_values) + + def check_revocation(self, token): + version = self.driver.get_token_version(token) + if version == V2: + return self.check_revocation_v2(token) + else: + return self.check_revocation_v3(token) + + def validate_v3_token(self, token_id): + unique_id = self.unique_id(token_id) + # NOTE(lbragstad): Only go to persistent storage if we have a token to + # fetch from the backend. If the Fernet token provider is being used + # this step isn't necessary. The Fernet token reference is persisted in + # the token_id, so in this case set the token_ref as the identifier of + # the token. + if not self._needs_persistence: + token_ref = token_id + else: + # NOTE(morganfainberg): Ensure we never use the long-form token_id + # (PKI) as part of the cache_key. + token_ref = self._persistence.get_token(unique_id) + token = self._validate_v3_token(token_ref) + self._is_valid_token(token) + return token + + @MEMOIZE + def _validate_token(self, token_id): + if not self._needs_persistence: + return self.driver.validate_v3_token(token_id) + token_ref = self._persistence.get_token(token_id) + version = self.driver.get_token_version(token_ref) + if version == self.V3: + return self.driver.validate_v3_token(token_ref) + elif version == self.V2: + return self.driver.validate_v2_token(token_ref) + raise exception.UnsupportedTokenVersionException() + + @MEMOIZE + def _validate_v2_token(self, token_id): + return self.driver.validate_v2_token(token_id) + + @MEMOIZE + def _validate_v3_token(self, token_id): + return self.driver.validate_v3_token(token_id) + + def _is_valid_token(self, token): + """Verify the token is valid format and has not expired.""" + + current_time = timeutils.normalize_time(timeutils.utcnow()) + + try: + # Get the data we need from the correct location (V2 and V3 tokens + # differ in structure, Try V3 first, fall back to V2 second) + token_data = token.get('token', token.get('access')) + expires_at = token_data.get('expires_at', + token_data.get('expires')) + if not expires_at: + expires_at = token_data['token']['expires'] + expiry = timeutils.normalize_time( + timeutils.parse_isotime(expires_at)) + except Exception: + LOG.exception(_LE('Unexpected error or malformed token ' + 'determining token expiry: %s'), token) + raise exception.TokenNotFound(_('Failed to validate token')) + + if current_time < expiry: + self.check_revocation(token) + # Token has not expired and has not been revoked. + return None + else: + raise exception.TokenNotFound(_('Failed to validate token')) + + def _token_belongs_to(self, token, belongs_to): + """Check if the token belongs to the right tenant. + + This is only used on v2 tokens. The structural validity of the token + will have already been checked before this method is called. + + """ + if belongs_to: + token_data = token['access']['token'] + if ('tenant' not in token_data or + token_data['tenant']['id'] != belongs_to): + raise exception.Unauthorized() + + def issue_v2_token(self, token_ref, roles_ref=None, catalog_ref=None): + token_id, token_data = self.driver.issue_v2_token( + token_ref, roles_ref, catalog_ref) + + if self._needs_persistence: + data = dict(key=token_id, + id=token_id, + expires=token_data['access']['token']['expires'], + user=token_ref['user'], + tenant=token_ref['tenant'], + metadata=token_ref['metadata'], + token_data=token_data, + bind=token_ref.get('bind'), + trust_id=token_ref['metadata'].get('trust_id'), + token_version=self.V2) + self._create_token(token_id, data) + + return token_id, token_data + + def issue_v3_token(self, user_id, method_names, expires_at=None, + project_id=None, domain_id=None, auth_context=None, + trust=None, metadata_ref=None, include_catalog=True, + parent_audit_id=None): + token_id, token_data = self.driver.issue_v3_token( + user_id, method_names, expires_at, project_id, domain_id, + auth_context, trust, metadata_ref, include_catalog, + parent_audit_id) + + if metadata_ref is None: + metadata_ref = {} + + if 'project' in token_data['token']: + # project-scoped token, fill in the v2 token data + # all we care are the role IDs + + # FIXME(gyee): is there really a need to store roles in metadata? + role_ids = [r['id'] for r in token_data['token']['roles']] + metadata_ref = {'roles': role_ids} + + if trust: + metadata_ref.setdefault('trust_id', trust['id']) + metadata_ref.setdefault('trustee_user_id', + trust['trustee_user_id']) + + data = dict(key=token_id, + id=token_id, + expires=token_data['token']['expires_at'], + user=token_data['token']['user'], + tenant=token_data['token'].get('project'), + metadata=metadata_ref, + token_data=token_data, + trust_id=trust['id'] if trust else None, + token_version=self.V3) + if self._needs_persistence: + self._create_token(token_id, data) + return token_id, token_data + + def invalidate_individual_token_cache(self, token_id): + # NOTE(morganfainberg): invalidate takes the exact same arguments as + # the normal method, this means we need to pass "self" in (which gets + # stripped off). + + # FIXME(morganfainberg): Does this cache actually need to be + # invalidated? We maintain a cached revocation list, which should be + # consulted before accepting a token as valid. For now we will + # do the explicit individual token invalidation. + + self._validate_token.invalidate(self, token_id) + self._validate_v2_token.invalidate(self, token_id) + self._validate_v3_token.invalidate(self, token_id) + + def revoke_token(self, token_id, revoke_chain=False): + revoke_by_expires = False + project_id = None + domain_id = None + + token_ref = token_model.KeystoneToken( + token_id=token_id, + token_data=self.validate_token(token_id)) + + user_id = token_ref.user_id + expires_at = token_ref.expires + audit_id = token_ref.audit_id + audit_chain_id = token_ref.audit_chain_id + if token_ref.project_scoped: + project_id = token_ref.project_id + if token_ref.domain_scoped: + domain_id = token_ref.domain_id + + if audit_id is None and not revoke_chain: + LOG.debug('Received token with no audit_id.') + revoke_by_expires = True + + if audit_chain_id is None and revoke_chain: + LOG.debug('Received token with no audit_chain_id.') + revoke_by_expires = True + + if revoke_by_expires: + self.revoke_api.revoke_by_expiration(user_id, expires_at, + project_id=project_id, + domain_id=domain_id) + elif revoke_chain: + self.revoke_api.revoke_by_audit_chain_id(audit_chain_id, + project_id=project_id, + domain_id=domain_id) + else: + self.revoke_api.revoke_by_audit_id(audit_id) + + if CONF.token.revoke_by_id and self._needs_persistence: + self._persistence.delete_token(token_id=token_id) + + def list_revoked_tokens(self): + return self._persistence.list_revoked_tokens() + + def _trust_deleted_event_callback(self, service, resource_type, operation, + payload): + if CONF.token.revoke_by_id: + trust_id = payload['resource_info'] + trust = self.trust_api.get_trust(trust_id, deleted=True) + self._persistence.delete_tokens(user_id=trust['trustor_user_id'], + trust_id=trust_id) + + def _delete_user_tokens_callback(self, service, resource_type, operation, + payload): + if CONF.token.revoke_by_id: + user_id = payload['resource_info'] + self._persistence.delete_tokens_for_user(user_id) + + def _delete_domain_tokens_callback(self, service, resource_type, + operation, payload): + if CONF.token.revoke_by_id: + domain_id = payload['resource_info'] + self._persistence.delete_tokens_for_domain(domain_id=domain_id) + + def _delete_user_project_tokens_callback(self, service, resource_type, + operation, payload): + if CONF.token.revoke_by_id: + user_id = payload['resource_info']['user_id'] + project_id = payload['resource_info']['project_id'] + self._persistence.delete_tokens_for_user(user_id=user_id, + project_id=project_id) + + def _delete_project_tokens_callback(self, service, resource_type, + operation, payload): + if CONF.token.revoke_by_id: + project_id = payload['resource_info'] + self._persistence.delete_tokens_for_users( + self.assignment_api.list_user_ids_for_project(project_id), + project_id=project_id) + + def _delete_user_oauth_consumer_tokens_callback(self, service, + resource_type, operation, + payload): + if CONF.token.revoke_by_id: + user_id = payload['resource_info']['user_id'] + consumer_id = payload['resource_info']['consumer_id'] + self._persistence.delete_tokens(user_id=user_id, + consumer_id=consumer_id) + + +@six.add_metaclass(abc.ABCMeta) +class Provider(object): + """Interface description for a Token provider.""" + + @abc.abstractmethod + def needs_persistence(self): + """Determine if the token should be persisted. + + If the token provider requires that the token be persisted to a + backend this should return True, otherwise return False. + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def get_token_version(self, token_data): + """Return the version of the given token data. + + If the given token data is unrecognizable, + UnsupportedTokenVersionException is raised. + + :param token_data: token_data + :type token_data: dict + :returns: token version string + :raises: keystone.token.provider.UnsupportedTokenVersionException + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def issue_v2_token(self, token_ref, roles_ref=None, catalog_ref=None): + """Issue a V2 token. + + :param token_ref: token data to generate token from + :type token_ref: dict + :param roles_ref: optional roles list + :type roles_ref: dict + :param catalog_ref: optional catalog information + :type catalog_ref: dict + :returns: (token_id, token_data) + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def issue_v3_token(self, user_id, method_names, expires_at=None, + project_id=None, domain_id=None, auth_context=None, + trust=None, metadata_ref=None, include_catalog=True, + parent_audit_id=None): + """Issue a V3 Token. + + :param user_id: identity of the user + :type user_id: string + :param method_names: names of authentication methods + :type method_names: list + :param expires_at: optional time the token will expire + :type expires_at: string + :param project_id: optional project identity + :type project_id: string + :param domain_id: optional domain identity + :type domain_id: string + :param auth_context: optional context from the authorization plugins + :type auth_context: dict + :param trust: optional trust reference + :type trust: dict + :param metadata_ref: optional metadata reference + :type metadata_ref: dict + :param include_catalog: optional, include the catalog in token data + :type include_catalog: boolean + :param parent_audit_id: optional, the audit id of the parent token + :type parent_audit_id: string + :returns: (token_id, token_data) + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def validate_v2_token(self, token_ref): + """Validate the given V2 token and return the token data. + + Must raise Unauthorized exception if unable to validate token. + + :param token_ref: the token reference + :type token_ref: dict + :returns: token data + :raises: keystone.exception.TokenNotFound + + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def validate_v3_token(self, token_ref): + """Validate the given V3 token and return the token_data. + + :param token_ref: the token reference + :type token_ref: dict + :returns: token data + :raises: keystone.exception.TokenNotFound + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def _get_token_id(self, token_data): + """Generate the token_id based upon the data in token_data. + + :param token_data: token information + :type token_data: dict + returns: token identifier + """ + raise exception.NotImplemented() # pragma: no cover diff --git a/keystone-moon/keystone/token/providers/__init__.py b/keystone-moon/keystone/token/providers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/token/providers/common.py b/keystone-moon/keystone/token/providers/common.py new file mode 100644 index 00000000..717e1495 --- /dev/null +++ b/keystone-moon/keystone/token/providers/common.py @@ -0,0 +1,709 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 oslo_config import cfg +from oslo_log import log +from oslo_serialization import jsonutils +from oslo_utils import timeutils +import six +from six.moves.urllib import parse + +from keystone.common import controller as common_controller +from keystone.common import dependency +from keystone.contrib import federation +from keystone import exception +from keystone.i18n import _, _LE +from keystone.openstack.common import versionutils +from keystone import token +from keystone.token import provider + + +LOG = log.getLogger(__name__) +CONF = cfg.CONF + + +@dependency.requires('catalog_api', 'resource_api') +class V2TokenDataHelper(object): + """Creates V2 token data.""" + + def v3_to_v2_token(self, token_id, v3_token_data): + token_data = {} + # Build v2 token + v3_token = v3_token_data['token'] + + token = {} + token['id'] = token_id + token['expires'] = v3_token.get('expires_at') + token['issued_at'] = v3_token.get('issued_at') + token['audit_ids'] = v3_token.get('audit_ids') + + if 'project' in v3_token: + # v3 token_data does not contain all tenant attributes + tenant = self.resource_api.get_project( + v3_token['project']['id']) + token['tenant'] = common_controller.V2Controller.filter_domain_id( + tenant) + token_data['token'] = token + + # Build v2 user + v3_user = v3_token['user'] + user = common_controller.V2Controller.v3_to_v2_user(v3_user) + + # Set user roles + user['roles'] = [] + role_ids = [] + for role in v3_token.get('roles', []): + # Filter role id since it's not included in v2 token response + role_ids.append(role.pop('id')) + user['roles'].append(role) + user['roles_links'] = [] + + token_data['user'] = user + + # Get and build v2 service catalog + token_data['serviceCatalog'] = [] + if 'tenant' in token: + catalog_ref = self.catalog_api.get_catalog( + user['id'], token['tenant']['id']) + if catalog_ref: + token_data['serviceCatalog'] = self.format_catalog(catalog_ref) + + # Build v2 metadata + metadata = {} + metadata['roles'] = role_ids + # Setting is_admin to keep consistency in v2 response + metadata['is_admin'] = 0 + token_data['metadata'] = metadata + + return {'access': token_data} + + @classmethod + def format_token(cls, token_ref, roles_ref=None, catalog_ref=None, + trust_ref=None): + audit_info = None + user_ref = token_ref['user'] + metadata_ref = token_ref['metadata'] + if roles_ref is None: + roles_ref = [] + expires = token_ref.get('expires', provider.default_expire_time()) + if expires is not None: + if not isinstance(expires, six.text_type): + expires = timeutils.isotime(expires) + + token_data = token_ref.get('token_data') + if token_data: + token_audit = token_data.get( + 'access', token_data).get('token', {}).get('audit_ids') + audit_info = token_audit + + if audit_info is None: + audit_info = provider.audit_info(token_ref.get('parent_audit_id')) + + o = {'access': {'token': {'id': token_ref['id'], + 'expires': expires, + 'issued_at': timeutils.strtime(), + 'audit_ids': audit_info + }, + 'user': {'id': user_ref['id'], + 'name': user_ref['name'], + 'username': user_ref['name'], + 'roles': roles_ref, + 'roles_links': metadata_ref.get('roles_links', + []) + } + } + } + if 'bind' in token_ref: + o['access']['token']['bind'] = token_ref['bind'] + if 'tenant' in token_ref and token_ref['tenant']: + token_ref['tenant']['enabled'] = True + o['access']['token']['tenant'] = token_ref['tenant'] + if catalog_ref is not None: + o['access']['serviceCatalog'] = V2TokenDataHelper.format_catalog( + catalog_ref) + if metadata_ref: + if 'is_admin' in metadata_ref: + o['access']['metadata'] = {'is_admin': + metadata_ref['is_admin']} + else: + o['access']['metadata'] = {'is_admin': 0} + if 'roles' in metadata_ref: + o['access']['metadata']['roles'] = metadata_ref['roles'] + if CONF.trust.enabled and trust_ref: + o['access']['trust'] = {'trustee_user_id': + trust_ref['trustee_user_id'], + 'id': trust_ref['id'], + 'trustor_user_id': + trust_ref['trustor_user_id'], + 'impersonation': + trust_ref['impersonation'] + } + return o + + @classmethod + def format_catalog(cls, catalog_ref): + """Munge catalogs from internal to output format + Internal catalogs look like:: + + {$REGION: { + {$SERVICE: { + $key1: $value1, + ... + } + } + } + + The legacy api wants them to look like:: + + [{'name': $SERVICE[name], + 'type': $SERVICE, + 'endpoints': [{ + 'tenantId': $tenant_id, + ... + 'region': $REGION, + }], + 'endpoints_links': [], + }] + + """ + if not catalog_ref: + return [] + + services = {} + for region, region_ref in six.iteritems(catalog_ref): + for service, service_ref in six.iteritems(region_ref): + new_service_ref = services.get(service, {}) + new_service_ref['name'] = service_ref.pop('name') + new_service_ref['type'] = service + new_service_ref['endpoints_links'] = [] + service_ref['region'] = region + + endpoints_ref = new_service_ref.get('endpoints', []) + endpoints_ref.append(service_ref) + + new_service_ref['endpoints'] = endpoints_ref + services[service] = new_service_ref + + return services.values() + + +@dependency.requires('assignment_api', 'catalog_api', 'federation_api', + 'identity_api', 'resource_api', 'role_api', 'trust_api') +class V3TokenDataHelper(object): + """Token data helper.""" + def __init__(self): + # Keep __init__ around to ensure dependency injection works. + super(V3TokenDataHelper, self).__init__() + + def _get_filtered_domain(self, domain_id): + domain_ref = self.resource_api.get_domain(domain_id) + return {'id': domain_ref['id'], 'name': domain_ref['name']} + + def _get_filtered_project(self, project_id): + project_ref = self.resource_api.get_project(project_id) + filtered_project = { + 'id': project_ref['id'], + 'name': project_ref['name']} + filtered_project['domain'] = self._get_filtered_domain( + project_ref['domain_id']) + return filtered_project + + def _populate_scope(self, token_data, domain_id, project_id): + if 'domain' in token_data or 'project' in token_data: + # scope already exist, no need to populate it again + return + + if domain_id: + token_data['domain'] = self._get_filtered_domain(domain_id) + if project_id: + token_data['project'] = self._get_filtered_project(project_id) + + def _get_roles_for_user(self, user_id, domain_id, project_id): + roles = [] + if domain_id: + roles = self.assignment_api.get_roles_for_user_and_domain( + user_id, domain_id) + if project_id: + roles = self.assignment_api.get_roles_for_user_and_project( + user_id, project_id) + return [self.role_api.get_role(role_id) for role_id in roles] + + def _populate_roles_for_groups(self, group_ids, + project_id=None, domain_id=None, + user_id=None): + def _check_roles(roles, user_id, project_id, domain_id): + # User was granted roles so simply exit this function. + if roles: + return + if project_id: + msg = _('User %(user_id)s has no access ' + 'to project %(project_id)s') % { + 'user_id': user_id, + 'project_id': project_id} + elif domain_id: + msg = _('User %(user_id)s has no access ' + 'to domain %(domain_id)s') % { + 'user_id': user_id, + 'domain_id': domain_id} + # Since no roles were found a user is not authorized to + # perform any operations. Raise an exception with + # appropriate error message. + raise exception.Unauthorized(msg) + + roles = self.assignment_api.get_roles_for_groups(group_ids, + project_id, + domain_id) + _check_roles(roles, user_id, project_id, domain_id) + return roles + + def _populate_user(self, token_data, user_id, trust): + if 'user' in token_data: + # no need to repopulate user if it already exists + return + + user_ref = self.identity_api.get_user(user_id) + if CONF.trust.enabled and trust and 'OS-TRUST:trust' not in token_data: + trustor_user_ref = (self.identity_api.get_user( + trust['trustor_user_id'])) + try: + self.identity_api.assert_user_enabled(trust['trustor_user_id']) + except AssertionError: + raise exception.Forbidden(_('Trustor is disabled.')) + if trust['impersonation']: + user_ref = trustor_user_ref + token_data['OS-TRUST:trust'] = ( + { + 'id': trust['id'], + 'trustor_user': {'id': trust['trustor_user_id']}, + 'trustee_user': {'id': trust['trustee_user_id']}, + 'impersonation': trust['impersonation'] + }) + filtered_user = { + 'id': user_ref['id'], + 'name': user_ref['name'], + 'domain': self._get_filtered_domain(user_ref['domain_id'])} + token_data['user'] = filtered_user + + def _populate_oauth_section(self, token_data, access_token): + if access_token: + access_token_id = access_token['id'] + consumer_id = access_token['consumer_id'] + token_data['OS-OAUTH1'] = ({'access_token_id': access_token_id, + 'consumer_id': consumer_id}) + + def _populate_roles(self, token_data, user_id, domain_id, project_id, + trust, access_token): + if 'roles' in token_data: + # no need to repopulate roles + return + + if access_token: + filtered_roles = [] + authed_role_ids = jsonutils.loads(access_token['role_ids']) + all_roles = self.role_api.list_roles() + for role in all_roles: + for authed_role in authed_role_ids: + if authed_role == role['id']: + filtered_roles.append({'id': role['id'], + 'name': role['name']}) + token_data['roles'] = filtered_roles + return + + if CONF.trust.enabled and trust: + token_user_id = trust['trustor_user_id'] + token_project_id = trust['project_id'] + # trusts do not support domains yet + token_domain_id = None + else: + token_user_id = user_id + token_project_id = project_id + token_domain_id = domain_id + + if token_domain_id or token_project_id: + roles = self._get_roles_for_user(token_user_id, + token_domain_id, + token_project_id) + filtered_roles = [] + if CONF.trust.enabled and trust: + for trust_role in trust['roles']: + match_roles = [x for x in roles + if x['id'] == trust_role['id']] + if match_roles: + filtered_roles.append(match_roles[0]) + else: + raise exception.Forbidden( + _('Trustee has no delegated roles.')) + else: + for role in roles: + filtered_roles.append({'id': role['id'], + 'name': role['name']}) + + # user has no project or domain roles, therefore access denied + if not filtered_roles: + if token_project_id: + msg = _('User %(user_id)s has no access ' + 'to project %(project_id)s') % { + 'user_id': user_id, + 'project_id': token_project_id} + else: + msg = _('User %(user_id)s has no access ' + 'to domain %(domain_id)s') % { + 'user_id': user_id, + 'domain_id': token_domain_id} + LOG.debug(msg) + raise exception.Unauthorized(msg) + + token_data['roles'] = filtered_roles + + def _populate_service_catalog(self, token_data, user_id, + domain_id, project_id, trust): + if 'catalog' in token_data: + # no need to repopulate service catalog + return + + if CONF.trust.enabled and trust: + user_id = trust['trustor_user_id'] + if project_id or domain_id: + service_catalog = self.catalog_api.get_v3_catalog( + user_id, project_id) + # TODO(ayoung): Enforce Endpoints for trust + token_data['catalog'] = service_catalog + + def _populate_service_providers(self, token_data): + if 'service_providers' in token_data: + return + + service_providers = self.federation_api.get_enabled_service_providers() + if service_providers: + token_data['service_providers'] = service_providers + + def _populate_token_dates(self, token_data, expires=None, trust=None, + issued_at=None): + if not expires: + expires = provider.default_expire_time() + if not isinstance(expires, six.string_types): + expires = timeutils.isotime(expires, subsecond=True) + token_data['expires_at'] = expires + token_data['issued_at'] = (issued_at or + timeutils.isotime(subsecond=True)) + + def _populate_audit_info(self, token_data, audit_info=None): + if audit_info is None or isinstance(audit_info, six.string_types): + token_data['audit_ids'] = provider.audit_info(audit_info) + elif isinstance(audit_info, list): + token_data['audit_ids'] = audit_info + else: + msg = (_('Invalid audit info data type: %(data)s (%(type)s)') % + {'data': audit_info, 'type': type(audit_info)}) + LOG.error(msg) + raise exception.UnexpectedError(msg) + + def get_token_data(self, user_id, method_names, extras=None, + domain_id=None, project_id=None, expires=None, + trust=None, token=None, include_catalog=True, + bind=None, access_token=None, issued_at=None, + audit_info=None): + if extras is None: + extras = {} + if extras: + versionutils.deprecated( + what='passing token data with "extras"', + as_of=versionutils.deprecated.KILO, + in_favor_of='well-defined APIs') + token_data = {'methods': method_names, + 'extras': extras} + + # We've probably already written these to the token + if token: + for x in ('roles', 'user', 'catalog', 'project', 'domain'): + if x in token: + token_data[x] = token[x] + + if CONF.trust.enabled and trust: + if user_id != trust['trustee_user_id']: + raise exception.Forbidden(_('User is not a trustee.')) + + if bind: + token_data['bind'] = bind + + self._populate_scope(token_data, domain_id, project_id) + self._populate_user(token_data, user_id, trust) + self._populate_roles(token_data, user_id, domain_id, project_id, trust, + access_token) + self._populate_audit_info(token_data, audit_info) + + if include_catalog: + self._populate_service_catalog(token_data, user_id, domain_id, + project_id, trust) + self._populate_service_providers(token_data) + self._populate_token_dates(token_data, expires=expires, trust=trust, + issued_at=issued_at) + self._populate_oauth_section(token_data, access_token) + return {'token': token_data} + + +@dependency.requires('catalog_api', 'identity_api', 'oauth_api', + 'resource_api', 'role_api', 'trust_api') +class BaseProvider(provider.Provider): + def __init__(self, *args, **kwargs): + super(BaseProvider, self).__init__(*args, **kwargs) + self.v3_token_data_helper = V3TokenDataHelper() + self.v2_token_data_helper = V2TokenDataHelper() + + def get_token_version(self, token_data): + if token_data and isinstance(token_data, dict): + if 'token_version' in token_data: + if token_data['token_version'] in token.provider.VERSIONS: + return token_data['token_version'] + # FIXME(morganfainberg): deprecate the following logic in future + # revisions. It is better to just specify the token_version in + # the token_data itself. This way we can support future versions + # that might have the same fields. + if 'access' in token_data: + return token.provider.V2 + if 'token' in token_data and 'methods' in token_data['token']: + return token.provider.V3 + raise exception.UnsupportedTokenVersionException() + + def issue_v2_token(self, token_ref, roles_ref=None, + catalog_ref=None): + metadata_ref = token_ref['metadata'] + trust_ref = None + if CONF.trust.enabled and metadata_ref and 'trust_id' in metadata_ref: + trust_ref = self.trust_api.get_trust(metadata_ref['trust_id']) + + token_data = self.v2_token_data_helper.format_token( + token_ref, roles_ref, catalog_ref, trust_ref) + token_id = self._get_token_id(token_data) + token_data['access']['token']['id'] = token_id + return token_id, token_data + + def _is_mapped_token(self, auth_context): + return (federation.IDENTITY_PROVIDER in auth_context and + federation.PROTOCOL in auth_context) + + def issue_v3_token(self, user_id, method_names, expires_at=None, + project_id=None, domain_id=None, auth_context=None, + trust=None, metadata_ref=None, include_catalog=True, + parent_audit_id=None): + # for V2, trust is stashed in metadata_ref + if (CONF.trust.enabled and not trust and metadata_ref and + 'trust_id' in metadata_ref): + trust = self.trust_api.get_trust(metadata_ref['trust_id']) + + token_ref = None + if auth_context and self._is_mapped_token(auth_context): + token_ref = self._handle_mapped_tokens( + auth_context, project_id, domain_id) + + access_token = None + if 'oauth1' in method_names: + access_token_id = auth_context['access_token_id'] + access_token = self.oauth_api.get_access_token(access_token_id) + + token_data = self.v3_token_data_helper.get_token_data( + user_id, + method_names, + auth_context.get('extras') if auth_context else None, + domain_id=domain_id, + project_id=project_id, + expires=expires_at, + trust=trust, + bind=auth_context.get('bind') if auth_context else None, + token=token_ref, + include_catalog=include_catalog, + access_token=access_token, + audit_info=parent_audit_id) + + token_id = self._get_token_id(token_data) + return token_id, token_data + + def _handle_mapped_tokens(self, auth_context, project_id, domain_id): + def get_federated_domain(): + return (CONF.federation.federated_domain_name or + federation.FEDERATED_DOMAIN_KEYWORD) + + federated_domain = get_federated_domain() + user_id = auth_context['user_id'] + group_ids = auth_context['group_ids'] + idp = auth_context[federation.IDENTITY_PROVIDER] + protocol = auth_context[federation.PROTOCOL] + token_data = { + 'user': { + 'id': user_id, + 'name': parse.unquote(user_id), + federation.FEDERATION: { + 'identity_provider': {'id': idp}, + 'protocol': {'id': protocol} + }, + 'domain': { + 'id': federated_domain, + 'name': federated_domain + } + } + } + + if project_id or domain_id: + roles = self.v3_token_data_helper._populate_roles_for_groups( + group_ids, project_id, domain_id, user_id) + token_data.update({'roles': roles}) + else: + token_data['user'][federation.FEDERATION].update({ + 'groups': [{'id': x} for x in group_ids] + }) + return token_data + + def _verify_token_ref(self, token_ref): + """Verify and return the given token_ref.""" + if not token_ref: + raise exception.Unauthorized() + return token_ref + + def _assert_is_not_federation_token(self, token_ref): + """Make sure we aren't using v2 auth on a federation token.""" + token_data = token_ref.get('token_data') + if (token_data and self.get_token_version(token_data) == + token.provider.V3): + if 'OS-FEDERATION' in token_data['token']['user']: + msg = _('Attempting to use OS-FEDERATION token with V2 ' + 'Identity Service, use V3 Authentication') + raise exception.Unauthorized(msg) + + def _assert_default_domain(self, token_ref): + """Make sure we are operating on default domain only.""" + if (token_ref.get('token_data') and + self.get_token_version(token_ref.get('token_data')) == + token.provider.V3): + # this is a V3 token + msg = _('Non-default domain is not supported') + # user in a non-default is prohibited + if (token_ref['token_data']['token']['user']['domain']['id'] != + CONF.identity.default_domain_id): + raise exception.Unauthorized(msg) + # domain scoping is prohibited + if token_ref['token_data']['token'].get('domain'): + raise exception.Unauthorized( + _('Domain scoped token is not supported')) + # project in non-default domain is prohibited + if token_ref['token_data']['token'].get('project'): + project = token_ref['token_data']['token']['project'] + project_domain_id = project['domain']['id'] + # scoped to project in non-default domain is prohibited + if project_domain_id != CONF.identity.default_domain_id: + raise exception.Unauthorized(msg) + # if token is scoped to trust, both trustor and trustee must + # be in the default domain. Furthermore, the delegated project + # must also be in the default domain + metadata_ref = token_ref['metadata'] + if CONF.trust.enabled and 'trust_id' in metadata_ref: + trust_ref = self.trust_api.get_trust(metadata_ref['trust_id']) + trustee_user_ref = self.identity_api.get_user( + trust_ref['trustee_user_id']) + if (trustee_user_ref['domain_id'] != + CONF.identity.default_domain_id): + raise exception.Unauthorized(msg) + trustor_user_ref = self.identity_api.get_user( + trust_ref['trustor_user_id']) + if (trustor_user_ref['domain_id'] != + CONF.identity.default_domain_id): + raise exception.Unauthorized(msg) + project_ref = self.resource_api.get_project( + trust_ref['project_id']) + if (project_ref['domain_id'] != + CONF.identity.default_domain_id): + raise exception.Unauthorized(msg) + + def validate_v2_token(self, token_ref): + try: + self._assert_is_not_federation_token(token_ref) + self._assert_default_domain(token_ref) + # FIXME(gyee): performance or correctness? Should we return the + # cached token or reconstruct it? Obviously if we are going with + # the cached token, any role, project, or domain name changes + # will not be reflected. One may argue that with PKI tokens, + # we are essentially doing cached token validation anyway. + # Lets go with the cached token strategy. Since token + # management layer is now pluggable, one can always provide + # their own implementation to suit their needs. + token_data = token_ref.get('token_data') + if (not token_data or + self.get_token_version(token_data) != + token.provider.V2): + # token is created by old v2 logic + metadata_ref = token_ref['metadata'] + roles_ref = [] + for role_id in metadata_ref.get('roles', []): + roles_ref.append(self.role_api.get_role(role_id)) + + # Get a service catalog if possible + # This is needed for on-behalf-of requests + catalog_ref = None + if token_ref.get('tenant'): + catalog_ref = self.catalog_api.get_catalog( + token_ref['user']['id'], + token_ref['tenant']['id']) + + trust_ref = None + if CONF.trust.enabled and 'trust_id' in metadata_ref: + trust_ref = self.trust_api.get_trust( + metadata_ref['trust_id']) + + token_data = self.v2_token_data_helper.format_token( + token_ref, roles_ref, catalog_ref, trust_ref) + + trust_id = token_data['access'].get('trust', {}).get('id') + if trust_id: + # token trust validation + self.trust_api.get_trust(trust_id) + + return token_data + except exception.ValidationError as e: + LOG.exception(_LE('Failed to validate token')) + raise exception.TokenNotFound(e) + + def validate_v3_token(self, token_ref): + # FIXME(gyee): performance or correctness? Should we return the + # cached token or reconstruct it? Obviously if we are going with + # the cached token, any role, project, or domain name changes + # will not be reflected. One may argue that with PKI tokens, + # we are essentially doing cached token validation anyway. + # Lets go with the cached token strategy. Since token + # management layer is now pluggable, one can always provide + # their own implementation to suit their needs. + + trust_id = token_ref.get('trust_id') + if trust_id: + # token trust validation + self.trust_api.get_trust(trust_id) + + token_data = token_ref.get('token_data') + if not token_data or 'token' not in token_data: + # token ref is created by V2 API + project_id = None + project_ref = token_ref.get('tenant') + if project_ref: + project_id = project_ref['id'] + + issued_at = token_ref['token_data']['access']['token']['issued_at'] + audit = token_ref['token_data']['access']['token'].get('audit_ids') + + token_data = self.v3_token_data_helper.get_token_data( + token_ref['user']['id'], + ['password', 'token'], + project_id=project_id, + bind=token_ref.get('bind'), + expires=token_ref['expires'], + issued_at=issued_at, + audit_info=audit) + return token_data diff --git a/keystone-moon/keystone/token/providers/fernet/__init__.py b/keystone-moon/keystone/token/providers/fernet/__init__.py new file mode 100644 index 00000000..953ef624 --- /dev/null +++ b/keystone-moon/keystone/token/providers/fernet/__init__.py @@ -0,0 +1,13 @@ +# 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 keystone.token.providers.fernet.core import * # noqa diff --git a/keystone-moon/keystone/token/providers/fernet/core.py b/keystone-moon/keystone/token/providers/fernet/core.py new file mode 100644 index 00000000..b1da263b --- /dev/null +++ b/keystone-moon/keystone/token/providers/fernet/core.py @@ -0,0 +1,267 @@ +# 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 oslo_config import cfg +from oslo_log import log + +from keystone.common import dependency +from keystone.contrib import federation +from keystone import exception +from keystone.i18n import _ +from keystone.token import provider +from keystone.token.providers import common +from keystone.token.providers.fernet import token_formatters as tf + + +CONF = cfg.CONF +LOG = log.getLogger(__name__) + + +@dependency.requires('trust_api') +class Provider(common.BaseProvider): + def __init__(self, *args, **kwargs): + super(Provider, self).__init__(*args, **kwargs) + + self.token_formatter = tf.TokenFormatter() + + def needs_persistence(self): + """Should the token be written to a backend.""" + return False + + def issue_v2_token(self, token_ref, roles_ref=None, catalog_ref=None): + """Issue a V2 formatted token. + + :param token_ref: reference describing the token + :param roles_ref: reference describing the roles for the token + :param catalog_ref: reference describing the token's catalog + :returns: tuple containing the ID of the token and the token data + + """ + # TODO(lbragstad): Currently, Fernet tokens don't support bind in the + # token format. Raise a 501 if we're dealing with bind. + if token_ref.get('bind'): + raise exception.NotImplemented() + + user_id = token_ref['user']['id'] + # Default to password since methods not provided by token_ref + method_names = ['password'] + project_id = None + # Verify that tenant is not None in token_ref + if token_ref.get('tenant'): + project_id = token_ref['tenant']['id'] + + parent_audit_id = token_ref.get('parent_audit_id') + # If parent_audit_id is defined then a token authentication was made + if parent_audit_id: + method_names.append('token') + + audit_ids = provider.audit_info(parent_audit_id) + + # Get v3 token data and exclude building v3 specific catalog. This is + # due to the fact that the V2TokenDataHelper.format_token() method + # doesn't build any of the token_reference from other Keystone APIs. + # Instead, it builds it from what is persisted in the token reference. + # Here we are going to leverage the V3TokenDataHelper.get_token_data() + # method written for V3 because it goes through and populates the token + # reference dynamically. Once we have a V3 token reference, we can + # attempt to convert it to a V2 token response. + v3_token_data = self.v3_token_data_helper.get_token_data( + user_id, + method_names, + project_id=project_id, + token=token_ref, + include_catalog=False, + audit_info=audit_ids) + + expires_at = v3_token_data['token']['expires_at'] + token_id = self.token_formatter.create_token(user_id, expires_at, + audit_ids, + methods=method_names, + project_id=project_id) + # Convert v3 to v2 token data and build v2 catalog + token_data = self.v2_token_data_helper.v3_to_v2_token(token_id, + v3_token_data) + + return token_id, token_data + + def _build_federated_info(self, token_data): + """Extract everything needed for federated tokens. + + This dictionary is passed to the FederatedPayload token formatter, + which unpacks the values and builds the Fernet token. + + """ + group_ids = token_data.get('user', {}).get( + federation.FEDERATION, {}).get('groups') + idp_id = token_data.get('user', {}).get( + federation.FEDERATION, {}).get('identity_provider', {}).get('id') + protocol_id = token_data.get('user', {}).get( + federation.FEDERATION, {}).get('protocol', {}).get('id') + if not group_ids: + group_ids = list() + federated_dict = dict(group_ids=group_ids, idp_id=idp_id, + protocol_id=protocol_id) + return federated_dict + + def _rebuild_federated_info(self, federated_dict, user_id): + """Format federated information into the token reference. + + The federated_dict is passed back from the FederatedPayload token + formatter. The responsibility of this method is to format the + information passed back from the token formatter into the token + reference before constructing the token data from the + V3TokenDataHelper. + + """ + g_ids = federated_dict['group_ids'] + idp_id = federated_dict['idp_id'] + protocol_id = federated_dict['protocol_id'] + federated_info = dict(groups=g_ids, + identity_provider=dict(id=idp_id), + protocol=dict(id=protocol_id)) + token_dict = {'user': {federation.FEDERATION: federated_info}} + token_dict['user']['id'] = user_id + token_dict['user']['name'] = user_id + return token_dict + + def issue_v3_token(self, user_id, method_names, expires_at=None, + project_id=None, domain_id=None, auth_context=None, + trust=None, metadata_ref=None, include_catalog=True, + parent_audit_id=None): + """Issue a V3 formatted token. + + Here is where we need to detect what is given to us, and what kind of + token the user is expecting. Depending on the outcome of that, we can + pass all the information to be packed to the proper token format + handler. + + :param user_id: ID of the user + :param method_names: method of authentication + :param expires_at: token expiration time + :param project_id: ID of the project being scoped to + :param domain_id: ID of the domain being scoped to + :param auth_context: authentication context + :param trust: ID of the trust + :param metadata_ref: metadata reference + :param include_catalog: return the catalog in the response if True, + otherwise don't return the catalog + :param parent_audit_id: ID of the patent audit entity + :returns: tuple containing the id of the token and the token data + + """ + # TODO(lbragstad): Currently, Fernet tokens don't support bind in the + # token format. Raise a 501 if we're dealing with bind. + if auth_context.get('bind'): + raise exception.NotImplemented() + + token_ref = None + # NOTE(lbragstad): This determines if we are dealing with a federated + # token or not. The groups for the user will be in the returned token + # reference. + federated_dict = None + if auth_context and self._is_mapped_token(auth_context): + token_ref = self._handle_mapped_tokens( + auth_context, project_id, domain_id) + federated_dict = self._build_federated_info(token_ref) + + token_data = self.v3_token_data_helper.get_token_data( + user_id, + method_names, + auth_context.get('extras') if auth_context else None, + domain_id=domain_id, + project_id=project_id, + expires=expires_at, + trust=trust, + bind=auth_context.get('bind') if auth_context else None, + token=token_ref, + include_catalog=include_catalog, + audit_info=parent_audit_id) + + token = self.token_formatter.create_token( + user_id, + token_data['token']['expires_at'], + token_data['token']['audit_ids'], + methods=method_names, + domain_id=domain_id, + project_id=project_id, + trust_id=token_data['token'].get('OS-TRUST:trust', {}).get('id'), + federated_info=federated_dict) + return token, token_data + + def validate_v2_token(self, token_ref): + """Validate a V2 formatted token. + + :param token_ref: reference describing the token to validate + :returns: the token data + :raises keystone.exception.Unauthorized: if v3 token is used + + """ + (user_id, methods, + audit_ids, domain_id, + project_id, trust_id, + federated_info, created_at, + expires_at) = self.token_formatter.validate_token(token_ref) + + if trust_id or domain_id or federated_info: + msg = _('This is not a v2.0 Fernet token. Use v3 for trust, ' + 'domain, or federated tokens.') + raise exception.Unauthorized(msg) + + v3_token_data = self.v3_token_data_helper.get_token_data( + user_id, + methods, + project_id=project_id, + expires=expires_at, + issued_at=created_at, + token=token_ref, + include_catalog=False, + audit_info=audit_ids) + return self.v2_token_data_helper.v3_to_v2_token(token_ref, + v3_token_data) + + def validate_v3_token(self, token): + """Validate a V3 formatted token. + + :param token: a string describing the token to validate + :returns: the token data + :raises keystone.exception.Unauthorized: if token format version isn't + supported + + """ + (user_id, methods, audit_ids, domain_id, project_id, trust_id, + federated_info, created_at, expires_at) = ( + self.token_formatter.validate_token(token)) + + token_dict = None + if federated_info: + token_dict = self._rebuild_federated_info(federated_info, user_id) + trust_ref = self.trust_api.get_trust(trust_id) + + return self.v3_token_data_helper.get_token_data( + user_id, + method_names=methods, + domain_id=domain_id, + project_id=project_id, + issued_at=created_at, + expires=expires_at, + trust=trust_ref, + token=token_dict, + audit_info=audit_ids) + + def _get_token_id(self, token_data): + """Generate the token_id based upon the data in token_data. + + :param token_data: token information + :type token_data: dict + :raises keystone.exception.NotImplemented: when called + """ + raise exception.NotImplemented() diff --git a/keystone-moon/keystone/token/providers/fernet/token_formatters.py b/keystone-moon/keystone/token/providers/fernet/token_formatters.py new file mode 100644 index 00000000..50960923 --- /dev/null +++ b/keystone-moon/keystone/token/providers/fernet/token_formatters.py @@ -0,0 +1,545 @@ +# 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 datetime +import struct +import uuid + +from cryptography import fernet +import msgpack +from oslo_config import cfg +from oslo_log import log +from oslo_utils import timeutils +import six +from six.moves import urllib + +from keystone.auth import plugins as auth_plugins +from keystone import exception +from keystone.i18n import _ +from keystone.token import provider +from keystone.token.providers.fernet import utils + + +CONF = cfg.CONF +LOG = log.getLogger(__name__) + +# Fernet byte indexes as as computed by pypi/keyless_fernet and defined in +# https://github.com/fernet/spec +TIMESTAMP_START = 1 +TIMESTAMP_END = 9 + + +class TokenFormatter(object): + """Packs and unpacks payloads into tokens for transport.""" + + @property + def crypto(self): + """Return a cryptography instance. + + You can extend this class with a custom crypto @property to provide + your own token encoding / decoding. For example, using a different + cryptography library (e.g. ``python-keyczar``) or to meet arbitrary + security requirements. + + This @property just needs to return an object that implements + ``encrypt(plaintext)`` and ``decrypt(ciphertext)``. + + """ + keys = utils.load_keys() + + if not keys: + raise exception.KeysNotFound() + + fernet_instances = [fernet.Fernet(key) for key in utils.load_keys()] + return fernet.MultiFernet(fernet_instances) + + def pack(self, payload): + """Pack a payload for transport as a token.""" + # base64 padding (if any) is not URL-safe + return urllib.parse.quote(self.crypto.encrypt(payload)) + + def unpack(self, token): + """Unpack a token, and validate the payload.""" + token = urllib.parse.unquote(six.binary_type(token)) + + try: + return self.crypto.decrypt(token) + except fernet.InvalidToken as e: + raise exception.Unauthorized(six.text_type(e)) + + @classmethod + def creation_time(cls, fernet_token): + """Returns the creation time of a valid Fernet token.""" + # tokens may be transmitted as Unicode, but they're just ASCII + # (pypi/cryptography will refuse to operate on Unicode input) + fernet_token = six.binary_type(fernet_token) + + # the base64 padding on fernet tokens is made URL-safe + fernet_token = urllib.parse.unquote(fernet_token) + + # fernet tokens are base64 encoded and the padding made URL-safe + token_bytes = base64.urlsafe_b64decode(fernet_token) + + # slice into the byte array to get just the timestamp + timestamp_bytes = token_bytes[TIMESTAMP_START:TIMESTAMP_END] + + # convert those bytes to an integer + # (it's a 64-bit "unsigned long long int" in C) + timestamp_int = struct.unpack(">Q", timestamp_bytes)[0] + + # and with an integer, it's trivial to produce a datetime object + created_at = datetime.datetime.utcfromtimestamp(timestamp_int) + + return created_at + + def create_token(self, user_id, expires_at, audit_ids, methods=None, + domain_id=None, project_id=None, trust_id=None, + federated_info=None): + """Given a set of payload attributes, generate a Fernet token.""" + if trust_id: + version = TrustScopedPayload.version + payload = TrustScopedPayload.assemble( + user_id, + methods, + project_id, + expires_at, + audit_ids, + trust_id) + elif federated_info: + version = FederatedPayload.version + payload = FederatedPayload.assemble( + user_id, + methods, + expires_at, + audit_ids, + federated_info) + elif project_id: + version = ProjectScopedPayload.version + payload = ProjectScopedPayload.assemble( + user_id, + methods, + project_id, + expires_at, + audit_ids) + elif domain_id: + version = DomainScopedPayload.version + payload = DomainScopedPayload.assemble( + user_id, + methods, + domain_id, + expires_at, + audit_ids) + else: + version = UnscopedPayload.version + payload = UnscopedPayload.assemble( + user_id, + methods, + expires_at, + audit_ids) + + versioned_payload = (version,) + payload + serialized_payload = msgpack.packb(versioned_payload) + token = self.pack(serialized_payload) + + return token + + def validate_token(self, token): + """Validates a Fernet token and returns the payload attributes.""" + # Convert v2 unicode token to a string + if not isinstance(token, six.binary_type): + token = token.encode('ascii') + + serialized_payload = self.unpack(token) + versioned_payload = msgpack.unpackb(serialized_payload) + version, payload = versioned_payload[0], versioned_payload[1:] + + # depending on the formatter, these may or may not be defined + domain_id = None + project_id = None + trust_id = None + federated_info = None + + if version == UnscopedPayload.version: + (user_id, methods, expires_at, audit_ids) = ( + UnscopedPayload.disassemble(payload)) + elif version == DomainScopedPayload.version: + (user_id, methods, domain_id, expires_at, audit_ids) = ( + DomainScopedPayload.disassemble(payload)) + elif version == ProjectScopedPayload.version: + (user_id, methods, project_id, expires_at, audit_ids) = ( + ProjectScopedPayload.disassemble(payload)) + elif version == TrustScopedPayload.version: + (user_id, methods, project_id, expires_at, audit_ids, trust_id) = ( + TrustScopedPayload.disassemble(payload)) + elif version == FederatedPayload.version: + (user_id, methods, expires_at, audit_ids, federated_info) = ( + FederatedPayload.disassemble(payload)) + else: + # If the token_format is not recognized, raise Unauthorized. + raise exception.Unauthorized(_( + 'This is not a recognized Fernet payload version: %s') % + version) + + # rather than appearing in the payload, the creation time is encoded + # into the token format itself + created_at = TokenFormatter.creation_time(token) + created_at = timeutils.isotime(at=created_at, subsecond=True) + expires_at = timeutils.parse_isotime(expires_at) + expires_at = timeutils.isotime(at=expires_at, subsecond=True) + + return (user_id, methods, audit_ids, domain_id, project_id, trust_id, + federated_info, created_at, expires_at) + + +class BasePayload(object): + # each payload variant should have a unique version + version = None + + @classmethod + def assemble(cls, *args): + """Assemble the payload of a token. + + :param args: whatever data should go into the payload + :returns: the payload of a token + + """ + raise NotImplementedError() + + @classmethod + def disassemble(cls, payload): + """Disassemble an unscoped payload into the component data. + + :param payload: this variant of payload + :returns: a tuple of the payloads component data + + """ + raise NotImplementedError() + + @classmethod + def convert_uuid_hex_to_bytes(cls, uuid_string): + """Compress UUID formatted strings to bytes. + + :param uuid_string: uuid string to compress to bytes + :returns: a byte representation of the uuid + + """ + # TODO(lbragstad): Wrap this in an exception. Not sure what the case + # would be where we couldn't handle what we've been given but incase + # the integrity of the token has been compromised. + uuid_obj = uuid.UUID(uuid_string) + return uuid_obj.bytes + + @classmethod + def convert_uuid_bytes_to_hex(cls, uuid_byte_string): + """Generate uuid.hex format based on byte string. + + :param uuid_byte_string: uuid string to generate from + :returns: uuid hex formatted string + + """ + # TODO(lbragstad): Wrap this in an exception. Not sure what the case + # would be where we couldn't handle what we've been given but incase + # the integrity of the token has been compromised. + uuid_obj = uuid.UUID(bytes=uuid_byte_string) + return uuid_obj.hex + + @classmethod + def _convert_time_string_to_int(cls, time_string): + """Convert a time formatted string to a timestamp integer. + + :param time_string: time formatted string + :returns: an integer timestamp + + """ + time_object = timeutils.parse_isotime(time_string) + return (timeutils.normalize_time(time_object) - + datetime.datetime.utcfromtimestamp(0)).total_seconds() + + @classmethod + def _convert_int_to_time_string(cls, time_int): + """Convert a timestamp integer to a string. + + :param time_int: integer representing timestamp + :returns: a time formatted strings + + """ + time_object = datetime.datetime.utcfromtimestamp(int(time_int)) + return timeutils.isotime(time_object) + + @classmethod + def attempt_convert_uuid_hex_to_bytes(cls, value): + """Attempt to convert value to bytes or return value. + + :param value: value to attempt to convert to bytes + :returns: uuid value in bytes or value + + """ + try: + return cls.convert_uuid_hex_to_bytes(value) + except ValueError: + # this might not be a UUID, depending on the situation (i.e. + # federation) + return value + + @classmethod + def attempt_convert_uuid_bytes_to_hex(cls, value): + """Attempt to convert value to hex or return value. + + :param value: value to attempt to convert to hex + :returns: uuid value in hex or value + + """ + try: + return cls.convert_uuid_bytes_to_hex(value) + except ValueError: + return value + + +class UnscopedPayload(BasePayload): + version = 0 + + @classmethod + def assemble(cls, user_id, methods, expires_at, audit_ids): + """Assemble the payload of an unscoped token. + + :param user_id: identifier of the user in the token request + :param methods: list of authentication methods used + :param expires_at: datetime of the token's expiration + :param audit_ids: list of the token's audit IDs + :returns: the payload of an unscoped token + + """ + b_user_id = cls.convert_uuid_hex_to_bytes(user_id) + methods = auth_plugins.convert_method_list_to_integer(methods) + expires_at_int = cls._convert_time_string_to_int(expires_at) + b_audit_ids = list(map(provider.random_urlsafe_str_to_bytes, + audit_ids)) + return (b_user_id, methods, expires_at_int, b_audit_ids) + + @classmethod + def disassemble(cls, payload): + """Disassemble an unscoped payload into the component data. + + :param payload: the payload of an unscoped token + :return: a tuple containing the user_id, auth methods, expires_at, and + audit_ids + + """ + user_id = cls.convert_uuid_bytes_to_hex(payload[0]) + methods = auth_plugins.convert_integer_to_method_list(payload[1]) + expires_at_str = cls._convert_int_to_time_string(payload[2]) + audit_ids = list(map(provider.base64_encode, payload[3])) + return (user_id, methods, expires_at_str, audit_ids) + + +class DomainScopedPayload(BasePayload): + version = 1 + + @classmethod + def assemble(cls, user_id, methods, domain_id, expires_at, audit_ids): + """Assemble the payload of a domain-scoped token. + + :param user_id: ID of the user in the token request + :param methods: list of authentication methods used + :param domain_id: ID of the domain to scope to + :param expires_at: datetime of the token's expiration + :param audit_ids: list of the token's audit IDs + :returns: the payload of a domain-scoped token + + """ + b_user_id = cls.convert_uuid_hex_to_bytes(user_id) + methods = auth_plugins.convert_method_list_to_integer(methods) + try: + b_domain_id = cls.convert_uuid_hex_to_bytes(domain_id) + except ValueError: + # the default domain ID is configurable, and probably isn't a UUID + if domain_id == CONF.identity.default_domain_id: + b_domain_id = domain_id + else: + raise + expires_at_int = cls._convert_time_string_to_int(expires_at) + b_audit_ids = list(map(provider.random_urlsafe_str_to_bytes, + audit_ids)) + return (b_user_id, methods, b_domain_id, expires_at_int, b_audit_ids) + + @classmethod + def disassemble(cls, payload): + """Disassemble a payload into the component data. + + :param payload: the payload of a token + :return: a tuple containing the user_id, auth methods, domain_id, + expires_at_str, and audit_ids + + """ + user_id = cls.convert_uuid_bytes_to_hex(payload[0]) + methods = auth_plugins.convert_integer_to_method_list(payload[1]) + try: + domain_id = cls.convert_uuid_bytes_to_hex(payload[2]) + except ValueError: + # the default domain ID is configurable, and probably isn't a UUID + if payload[2] == CONF.identity.default_domain_id: + domain_id = payload[2] + else: + raise + expires_at_str = cls._convert_int_to_time_string(payload[3]) + audit_ids = list(map(provider.base64_encode, payload[4])) + + return (user_id, methods, domain_id, expires_at_str, audit_ids) + + +class ProjectScopedPayload(BasePayload): + version = 2 + + @classmethod + def assemble(cls, user_id, methods, project_id, expires_at, audit_ids): + """Assemble the payload of a project-scoped token. + + :param user_id: ID of the user in the token request + :param methods: list of authentication methods used + :param project_id: ID of the project to scope to + :param expires_at: datetime of the token's expiration + :param audit_ids: list of the token's audit IDs + :returns: the payload of a project-scoped token + + """ + b_user_id = cls.convert_uuid_hex_to_bytes(user_id) + methods = auth_plugins.convert_method_list_to_integer(methods) + b_project_id = cls.convert_uuid_hex_to_bytes(project_id) + expires_at_int = cls._convert_time_string_to_int(expires_at) + b_audit_ids = list(map(provider.random_urlsafe_str_to_bytes, + audit_ids)) + return (b_user_id, methods, b_project_id, expires_at_int, b_audit_ids) + + @classmethod + def disassemble(cls, payload): + """Disassemble a payload into the component data. + + :param payload: the payload of a token + :return: a tuple containing the user_id, auth methods, project_id, + expires_at_str, and audit_ids + + """ + user_id = cls.convert_uuid_bytes_to_hex(payload[0]) + methods = auth_plugins.convert_integer_to_method_list(payload[1]) + project_id = cls.convert_uuid_bytes_to_hex(payload[2]) + expires_at_str = cls._convert_int_to_time_string(payload[3]) + audit_ids = list(map(provider.base64_encode, payload[4])) + + return (user_id, methods, project_id, expires_at_str, audit_ids) + + +class TrustScopedPayload(BasePayload): + version = 3 + + @classmethod + def assemble(cls, user_id, methods, project_id, expires_at, audit_ids, + trust_id): + """Assemble the payload of a trust-scoped token. + + :param user_id: ID of the user in the token request + :param methods: list of authentication methods used + :param project_id: ID of the project to scope to + :param expires_at: datetime of the token's expiration + :param audit_ids: list of the token's audit IDs + :param trust_id: ID of the trust in effect + :returns: the payload of a trust-scoped token + + """ + b_user_id = cls.convert_uuid_hex_to_bytes(user_id) + methods = auth_plugins.convert_method_list_to_integer(methods) + b_project_id = cls.convert_uuid_hex_to_bytes(project_id) + b_trust_id = cls.convert_uuid_hex_to_bytes(trust_id) + expires_at_int = cls._convert_time_string_to_int(expires_at) + b_audit_ids = list(map(provider.random_urlsafe_str_to_bytes, + audit_ids)) + + return (b_user_id, methods, b_project_id, expires_at_int, b_audit_ids, + b_trust_id) + + @classmethod + def disassemble(cls, payload): + """Validate a trust-based payload. + + :param token_string: a string representing the token + :returns: a tuple containing the user_id, auth methods, project_id, + expires_at_str, audit_ids, and trust_id + + """ + user_id = cls.convert_uuid_bytes_to_hex(payload[0]) + methods = auth_plugins.convert_integer_to_method_list(payload[1]) + project_id = cls.convert_uuid_bytes_to_hex(payload[2]) + expires_at_str = cls._convert_int_to_time_string(payload[3]) + audit_ids = list(map(provider.base64_encode, payload[4])) + trust_id = cls.convert_uuid_bytes_to_hex(payload[5]) + + return (user_id, methods, project_id, expires_at_str, audit_ids, + trust_id) + + +class FederatedPayload(BasePayload): + version = 4 + + @classmethod + def assemble(cls, user_id, methods, expires_at, audit_ids, federated_info): + """Assemble the payload of a federated token. + + :param user_id: ID of the user in the token request + :param methods: list of authentication methods used + :param expires_at: datetime of the token's expiration + :param audit_ids: list of the token's audit IDs + :param federated_info: dictionary containing group IDs, the identity + provider ID, protocol ID, and federated domain + ID + :returns: the payload of a federated token + + """ + def pack_group_ids(group_dict): + return cls.convert_uuid_hex_to_bytes(group_dict['id']) + + b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id) + methods = auth_plugins.convert_method_list_to_integer(methods) + b_group_ids = map(pack_group_ids, federated_info['group_ids']) + b_idp_id = cls.attempt_convert_uuid_hex_to_bytes( + federated_info['idp_id']) + protocol_id = federated_info['protocol_id'] + expires_at_int = cls._convert_time_string_to_int(expires_at) + b_audit_ids = map(provider.random_urlsafe_str_to_bytes, audit_ids) + + return (b_user_id, methods, b_group_ids, b_idp_id, protocol_id, + expires_at_int, b_audit_ids) + + @classmethod + def disassemble(cls, payload): + """Validate a federated paylod. + + :param token_string: a string representing the token + :return: a tuple containing the user_id, auth methods, audit_ids, and + a dictionary containing federated information such as the the + group IDs, the identity provider ID, the protocol ID, and the + federated domain ID + + """ + def unpack_group_ids(group_id_in_bytes): + group_id = cls.convert_uuid_bytes_to_hex(group_id_in_bytes) + return {'id': group_id} + + user_id = cls.attempt_convert_uuid_bytes_to_hex(payload[0]) + methods = auth_plugins.convert_integer_to_method_list(payload[1]) + group_ids = map(unpack_group_ids, payload[2]) + idp_id = cls.attempt_convert_uuid_bytes_to_hex(payload[3]) + protocol_id = payload[4] + expires_at_str = cls._convert_int_to_time_string(payload[5]) + audit_ids = map(provider.base64_encode, payload[6]) + federated_info = dict(group_ids=group_ids, idp_id=idp_id, + protocol_id=protocol_id) + return (user_id, methods, expires_at_str, audit_ids, federated_info) diff --git a/keystone-moon/keystone/token/providers/fernet/utils.py b/keystone-moon/keystone/token/providers/fernet/utils.py new file mode 100644 index 00000000..56624ee5 --- /dev/null +++ b/keystone-moon/keystone/token/providers/fernet/utils.py @@ -0,0 +1,243 @@ +# 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 os +import stat + +from cryptography import fernet +from oslo_config import cfg +from oslo_log import log + +from keystone.i18n import _LE, _LW, _LI + + +LOG = log.getLogger(__name__) + +CONF = cfg.CONF + + +def validate_key_repository(): + """Validate permissions on the key repository directory.""" + # NOTE(lbragstad): We shouldn't need to check if the directory was passed + # in as None because we don't set allow_no_values to True. + + # ensure current user has full access to the key repository + if (not os.access(CONF.fernet_tokens.key_repository, os.R_OK) or not + os.access(CONF.fernet_tokens.key_repository, os.W_OK) or not + os.access(CONF.fernet_tokens.key_repository, os.X_OK)): + LOG.error( + _LE('Either [fernet_tokens] key_repository does not exist or ' + 'Keystone does not have sufficient permission to access it: ' + '%s'), CONF.fernet_tokens.key_repository) + return False + + # ensure the key repository isn't world-readable + stat_info = os.stat(CONF.fernet_tokens.key_repository) + if stat_info.st_mode & stat.S_IROTH or stat_info.st_mode & stat.S_IXOTH: + LOG.warning(_LW( + '[fernet_tokens] key_repository is world readable: %s'), + CONF.fernet_tokens.key_repository) + + return True + + +def _convert_to_integers(id_value): + """Cast user and group system identifiers to integers.""" + # NOTE(lbragstad) os.chown() will raise a TypeError here if + # keystone_user_id and keystone_group_id are not integers. Let's + # cast them to integers if we can because it's possible to pass non-integer + # values into the fernet_setup utility. + try: + id_int = int(id_value) + except ValueError as e: + msg = ('Unable to convert Keystone user or group ID. Error: %s', e) + LOG.error(msg) + raise + + return id_int + + +def create_key_directory(keystone_user_id=None, keystone_group_id=None): + """If the configured key directory does not exist, attempt to create it.""" + if not os.access(CONF.fernet_tokens.key_repository, os.F_OK): + LOG.info(_LI( + '[fernet_tokens] key_repository does not appear to exist; ' + 'attempting to create it')) + + try: + os.makedirs(CONF.fernet_tokens.key_repository, 0o700) + except OSError: + LOG.error(_LE( + 'Failed to create [fernet_tokens] key_repository: either it ' + 'already exists or you don\'t have sufficient permissions to ' + 'create it')) + + if keystone_user_id and keystone_group_id: + os.chown( + CONF.fernet_tokens.key_repository, + keystone_user_id, + keystone_group_id) + elif keystone_user_id or keystone_group_id: + LOG.warning(_LW( + 'Unable to change the ownership of [fernet_tokens] ' + 'key_repository without a keystone user ID and keystone group ' + 'ID both being provided: %s') % + CONF.fernet_tokens.key_repository) + + +def _create_new_key(keystone_user_id, keystone_group_id): + """Securely create a new encryption key. + + Create a new key that is readable by the Keystone group and Keystone user. + """ + key = fernet.Fernet.generate_key() + + # This ensures the key created is not world-readable + old_umask = os.umask(0o177) + if keystone_user_id and keystone_group_id: + old_egid = os.getegid() + old_euid = os.geteuid() + os.setegid(keystone_group_id) + os.seteuid(keystone_user_id) + elif keystone_user_id or keystone_group_id: + LOG.warning(_LW( + 'Unable to change the ownership of the new key without a keystone ' + 'user ID and keystone group ID both being provided: %s') % + CONF.fernet_tokens.key_repository) + # Determine the file name of the new key + key_file = os.path.join(CONF.fernet_tokens.key_repository, '0') + try: + with open(key_file, 'w') as f: + f.write(key) + finally: + # After writing the key, set the umask back to it's original value. Do + # the same with group and user identifiers if a Keystone group or user + # was supplied. + os.umask(old_umask) + if keystone_user_id and keystone_group_id: + os.seteuid(old_euid) + os.setegid(old_egid) + + LOG.info(_LI('Created a new key: %s'), key_file) + + +def initialize_key_repository(keystone_user_id=None, keystone_group_id=None): + """Create a key repository and bootstrap it with a key. + + :param keystone_user_id: User ID of the Keystone user. + :param keystone_group_id: Group ID of the Keystone user. + + """ + # make sure we have work to do before proceeding + if os.access(os.path.join(CONF.fernet_tokens.key_repository, '0'), + os.F_OK): + LOG.info(_LI('Key repository is already initialized; aborting.')) + return + + # bootstrap an existing key + _create_new_key(keystone_user_id, keystone_group_id) + + # ensure that we end up with a primary and secondary key + rotate_keys(keystone_user_id, keystone_group_id) + + +def rotate_keys(keystone_user_id=None, keystone_group_id=None): + """Create a new primary key and revoke excess active keys. + + :param keystone_user_id: User ID of the Keystone user. + :param keystone_group_id: Group ID of the Keystone user. + + Key rotation utilizes the following behaviors: + + - The highest key number is used as the primary key (used for encryption). + - All keys can be used for decryption. + - New keys are always created as key "0," which serves as a placeholder + before promoting it to be the primary key. + + This strategy allows you to safely perform rotation on one node in a + cluster, before syncing the results of the rotation to all other nodes + (during both key rotation and synchronization, all nodes must recognize all + primary keys). + + """ + # read the list of key files + key_files = dict() + for filename in os.listdir(CONF.fernet_tokens.key_repository): + path = os.path.join(CONF.fernet_tokens.key_repository, str(filename)) + if os.path.isfile(path): + key_files[int(filename)] = path + + LOG.info(_LI('Starting key rotation with %(count)s key files: %(list)s'), { + 'count': len(key_files), + 'list': key_files.values()}) + + # determine the number of the new primary key + current_primary_key = max(key_files.keys()) + LOG.info(_LI('Current primary key is: %s'), current_primary_key) + new_primary_key = current_primary_key + 1 + LOG.info(_LI('Next primary key will be: %s'), new_primary_key) + + # promote the next primary key to be the primary + os.rename( + os.path.join(CONF.fernet_tokens.key_repository, '0'), + os.path.join(CONF.fernet_tokens.key_repository, str(new_primary_key))) + key_files.pop(0) + key_files[new_primary_key] = os.path.join( + CONF.fernet_tokens.key_repository, + str(new_primary_key)) + LOG.info(_LI('Promoted key 0 to be the primary: %s'), new_primary_key) + + # add a new key to the rotation, which will be the *next* primary + _create_new_key(keystone_user_id, keystone_group_id) + + # check for bad configuration + if CONF.fernet_tokens.max_active_keys < 1: + LOG.warning(_LW( + '[fernet_tokens] max_active_keys must be at least 1 to maintain a ' + 'primary key.')) + CONF.fernet_tokens.max_active_keys = 1 + + # purge excess keys + keys = sorted(key_files.keys()) + excess_keys = ( + keys[:len(key_files) - CONF.fernet_tokens.max_active_keys + 1]) + LOG.info(_LI('Excess keys to purge: %s'), excess_keys) + for i in excess_keys: + os.remove(key_files[i]) + + +def load_keys(): + """Load keys from disk into a list. + + The first key in the list is the primary key used for encryption. All + other keys are active secondary keys that can be used for decrypting + tokens. + + """ + if not validate_key_repository(): + return [] + + # build a dictionary of key_number:encryption_key pairs + keys = dict() + for filename in os.listdir(CONF.fernet_tokens.key_repository): + path = os.path.join(CONF.fernet_tokens.key_repository, str(filename)) + if os.path.isfile(path): + with open(path, 'r') as key_file: + keys[int(filename)] = key_file.read() + + LOG.info(_LI( + 'Loaded %(count)s encryption keys from: %(dir)s'), { + 'count': len(keys), + 'dir': CONF.fernet_tokens.key_repository}) + + # return the encryption_keys, sorted by key number, descending + return [keys[x] for x in sorted(keys.keys(), reverse=True)] diff --git a/keystone-moon/keystone/token/providers/pki.py b/keystone-moon/keystone/token/providers/pki.py new file mode 100644 index 00000000..61b42817 --- /dev/null +++ b/keystone-moon/keystone/token/providers/pki.py @@ -0,0 +1,53 @@ +# Copyright 2013 OpenStack Foundation +# +# 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. + +"""Keystone PKI Token Provider""" + +from keystoneclient.common import cms +from oslo_config import cfg +from oslo_log import log +from oslo_serialization import jsonutils + +from keystone.common import environment +from keystone.common import utils +from keystone import exception +from keystone.i18n import _, _LE +from keystone.token.providers import common + + +CONF = cfg.CONF + +LOG = log.getLogger(__name__) + + +class Provider(common.BaseProvider): + def _get_token_id(self, token_data): + try: + # force conversion to a string as the keystone client cms code + # produces unicode. This can be removed if the client returns + # str() + # TODO(ayoung): Make to a byte_str for Python3 + token_json = jsonutils.dumps(token_data, cls=utils.PKIEncoder) + token_id = str(cms.cms_sign_token(token_json, + CONF.signing.certfile, + CONF.signing.keyfile)) + return token_id + except environment.subprocess.CalledProcessError: + LOG.exception(_LE('Unable to sign token')) + raise exception.UnexpectedError(_( + 'Unable to sign token.')) + + def needs_persistence(self): + """Should the token be written to a backend.""" + return True diff --git a/keystone-moon/keystone/token/providers/pkiz.py b/keystone-moon/keystone/token/providers/pkiz.py new file mode 100644 index 00000000..b6f2944d --- /dev/null +++ b/keystone-moon/keystone/token/providers/pkiz.py @@ -0,0 +1,51 @@ +# 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. + +"""Keystone Compressed PKI Token Provider""" + +from keystoneclient.common import cms +from oslo_config import cfg +from oslo_log import log +from oslo_serialization import jsonutils + +from keystone.common import environment +from keystone.common import utils +from keystone import exception +from keystone.i18n import _ +from keystone.token.providers import common + + +CONF = cfg.CONF + +LOG = log.getLogger(__name__) +ERROR_MESSAGE = _('Unable to sign token.') + + +class Provider(common.BaseProvider): + def _get_token_id(self, token_data): + try: + # force conversion to a string as the keystone client cms code + # produces unicode. This can be removed if the client returns + # str() + # TODO(ayoung): Make to a byte_str for Python3 + token_json = jsonutils.dumps(token_data, cls=utils.PKIEncoder) + token_id = str(cms.pkiz_sign(token_json, + CONF.signing.certfile, + CONF.signing.keyfile)) + return token_id + except environment.subprocess.CalledProcessError: + LOG.exception(ERROR_MESSAGE) + raise exception.UnexpectedError(ERROR_MESSAGE) + + def needs_persistence(self): + """Should the token be written to a backend.""" + return True diff --git a/keystone-moon/keystone/token/providers/uuid.py b/keystone-moon/keystone/token/providers/uuid.py new file mode 100644 index 00000000..15118d82 --- /dev/null +++ b/keystone-moon/keystone/token/providers/uuid.py @@ -0,0 +1,33 @@ +# Copyright 2013 OpenStack Foundation +# +# 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. + +"""Keystone UUID Token Provider""" + +from __future__ import absolute_import + +import uuid + +from keystone.token.providers import common + + +class Provider(common.BaseProvider): + def __init__(self, *args, **kwargs): + super(Provider, self).__init__(*args, **kwargs) + + def _get_token_id(self, token_data): + return uuid.uuid4().hex + + def needs_persistence(self): + """Should the token be written to a backend.""" + return True diff --git a/keystone-moon/keystone/token/routers.py b/keystone-moon/keystone/token/routers.py new file mode 100644 index 00000000..bcd40ee4 --- /dev/null +++ b/keystone-moon/keystone/token/routers.py @@ -0,0 +1,59 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 keystone.common import wsgi +from keystone.token import controllers + + +class Router(wsgi.ComposableRouter): + def add_routes(self, mapper): + token_controller = controllers.Auth() + mapper.connect('/tokens', + controller=token_controller, + action='authenticate', + conditions=dict(method=['POST'])) + mapper.connect('/tokens/revoked', + controller=token_controller, + action='revocation_list', + conditions=dict(method=['GET'])) + mapper.connect('/tokens/{token_id}', + controller=token_controller, + action='validate_token', + conditions=dict(method=['GET'])) + # NOTE(morganfainberg): For policy enforcement reasons, the + # ``validate_token_head`` method is still used for HEAD requests. + # The controller method makes the same call as the validate_token + # call and lets wsgi.render_response remove the body data. + mapper.connect('/tokens/{token_id}', + controller=token_controller, + action='validate_token_head', + conditions=dict(method=['HEAD'])) + mapper.connect('/tokens/{token_id}', + controller=token_controller, + action='delete_token', + conditions=dict(method=['DELETE'])) + mapper.connect('/tokens/{token_id}/endpoints', + controller=token_controller, + action='endpoints', + conditions=dict(method=['GET'])) + + # Certificates used to verify auth tokens + mapper.connect('/certificates/ca', + controller=token_controller, + action='ca_cert', + conditions=dict(method=['GET'])) + + mapper.connect('/certificates/signing', + controller=token_controller, + action='signing_cert', + conditions=dict(method=['GET'])) diff --git a/keystone-moon/keystone/trust/__init__.py b/keystone-moon/keystone/trust/__init__.py new file mode 100644 index 00000000..e5ee61fb --- /dev/null +++ b/keystone-moon/keystone/trust/__init__.py @@ -0,0 +1,17 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 keystone.trust import controllers # noqa +from keystone.trust.core import * # noqa +from keystone.trust import routers # noqa diff --git a/keystone-moon/keystone/trust/backends/__init__.py b/keystone-moon/keystone/trust/backends/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/trust/backends/sql.py b/keystone-moon/keystone/trust/backends/sql.py new file mode 100644 index 00000000..4f5ee2e5 --- /dev/null +++ b/keystone-moon/keystone/trust/backends/sql.py @@ -0,0 +1,180 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 time + +from oslo_log import log +from oslo_utils import timeutils + +from keystone.common import sql +from keystone import exception +from keystone import trust + + +LOG = log.getLogger(__name__) +# The maximum number of iterations that will be attempted for optimistic +# locking on consuming a limited-use trust. +MAXIMUM_CONSUME_ATTEMPTS = 10 + + +class TrustModel(sql.ModelBase, sql.DictBase): + __tablename__ = 'trust' + attributes = ['id', 'trustor_user_id', 'trustee_user_id', + 'project_id', 'impersonation', 'expires_at', + 'remaining_uses', 'deleted_at'] + id = sql.Column(sql.String(64), primary_key=True) + # user id of owner + trustor_user_id = sql.Column(sql.String(64), nullable=False,) + # user_id of user allowed to consume this preauth + trustee_user_id = sql.Column(sql.String(64), nullable=False) + project_id = sql.Column(sql.String(64)) + impersonation = sql.Column(sql.Boolean, nullable=False) + deleted_at = sql.Column(sql.DateTime) + expires_at = sql.Column(sql.DateTime) + remaining_uses = sql.Column(sql.Integer, nullable=True) + extra = sql.Column(sql.JsonBlob()) + + +class TrustRole(sql.ModelBase): + __tablename__ = 'trust_role' + attributes = ['trust_id', 'role_id'] + trust_id = sql.Column(sql.String(64), primary_key=True, nullable=False) + role_id = sql.Column(sql.String(64), primary_key=True, nullable=False) + + +class Trust(trust.Driver): + @sql.handle_conflicts(conflict_type='trust') + def create_trust(self, trust_id, trust, roles): + with sql.transaction() as session: + ref = TrustModel.from_dict(trust) + ref['id'] = trust_id + if ref.get('expires_at') and ref['expires_at'].tzinfo is not None: + ref['expires_at'] = timeutils.normalize_time(ref['expires_at']) + session.add(ref) + added_roles = [] + for role in roles: + trust_role = TrustRole() + trust_role.trust_id = trust_id + trust_role.role_id = role['id'] + added_roles.append({'id': role['id']}) + session.add(trust_role) + trust_dict = ref.to_dict() + trust_dict['roles'] = added_roles + return trust_dict + + def _add_roles(self, trust_id, session, trust_dict): + roles = [] + for role in session.query(TrustRole).filter_by(trust_id=trust_id): + roles.append({'id': role.role_id}) + trust_dict['roles'] = roles + + @sql.handle_conflicts(conflict_type='trust') + def consume_use(self, trust_id): + + for attempt in range(MAXIMUM_CONSUME_ATTEMPTS): + with sql.transaction() as session: + try: + query_result = (session.query(TrustModel.remaining_uses). + filter_by(id=trust_id). + filter_by(deleted_at=None).one()) + except sql.NotFound: + raise exception.TrustNotFound(trust_id=trust_id) + + remaining_uses = query_result.remaining_uses + + if remaining_uses is None: + # unlimited uses, do nothing + break + elif remaining_uses > 0: + # NOTE(morganfainberg): use an optimistic locking method + # to ensure we only ever update a trust that has the + # expected number of remaining uses. + rows_affected = ( + session.query(TrustModel). + filter_by(id=trust_id). + filter_by(deleted_at=None). + filter_by(remaining_uses=remaining_uses). + update({'remaining_uses': (remaining_uses - 1)}, + synchronize_session=False)) + if rows_affected == 1: + # Successfully consumed a single limited-use trust. + # Since trust_id is the PK on the Trust table, there is + # no case we should match more than 1 row in the + # update. We either update 1 row or 0 rows. + break + else: + raise exception.TrustUseLimitReached(trust_id=trust_id) + # NOTE(morganfainberg): Ensure we have a yield point for eventlet + # here. This should cost us nothing otherwise. This can be removed + # if/when oslo_db cleanly handles yields on db calls. + time.sleep(0) + else: + # NOTE(morganfainberg): In the case the for loop is not prematurely + # broken out of, this else block is executed. This means the trust + # was not unlimited nor was it consumed (we hit the maximum + # iteration limit). This is just an indicator that we were unable + # to get the optimistic lock rather than silently failing or + # incorrectly indicating a trust was consumed. + raise exception.TrustConsumeMaximumAttempt(trust_id=trust_id) + + def get_trust(self, trust_id, deleted=False): + session = sql.get_session() + query = session.query(TrustModel).filter_by(id=trust_id) + if not deleted: + query = query.filter_by(deleted_at=None) + ref = query.first() + if ref is None: + return None + if ref.expires_at is not None and not deleted: + now = timeutils.utcnow() + if now > ref.expires_at: + return None + # Do not return trusts that can't be used anymore + if ref.remaining_uses is not None and not deleted: + if ref.remaining_uses <= 0: + return None + trust_dict = ref.to_dict() + + self._add_roles(trust_id, session, trust_dict) + return trust_dict + + @sql.handle_conflicts(conflict_type='trust') + def list_trusts(self): + session = sql.get_session() + trusts = session.query(TrustModel).filter_by(deleted_at=None) + return [trust_ref.to_dict() for trust_ref in trusts] + + @sql.handle_conflicts(conflict_type='trust') + def list_trusts_for_trustee(self, trustee_user_id): + session = sql.get_session() + trusts = (session.query(TrustModel). + filter_by(deleted_at=None). + filter_by(trustee_user_id=trustee_user_id)) + return [trust_ref.to_dict() for trust_ref in trusts] + + @sql.handle_conflicts(conflict_type='trust') + def list_trusts_for_trustor(self, trustor_user_id): + session = sql.get_session() + trusts = (session.query(TrustModel). + filter_by(deleted_at=None). + filter_by(trustor_user_id=trustor_user_id)) + return [trust_ref.to_dict() for trust_ref in trusts] + + @sql.handle_conflicts(conflict_type='trust') + def delete_trust(self, trust_id): + with sql.transaction() as session: + trust_ref = session.query(TrustModel).get(trust_id) + if not trust_ref: + raise exception.TrustNotFound(trust_id=trust_id) + trust_ref.deleted_at = timeutils.utcnow() diff --git a/keystone-moon/keystone/trust/controllers.py b/keystone-moon/keystone/trust/controllers.py new file mode 100644 index 00000000..60e34ccd --- /dev/null +++ b/keystone-moon/keystone/trust/controllers.py @@ -0,0 +1,287 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 oslo_config import cfg +from oslo_log import log +from oslo_utils import timeutils +import six + +from keystone import assignment +from keystone.common import controller +from keystone.common import dependency +from keystone.common import validation +from keystone import exception +from keystone.i18n import _ +from keystone.models import token_model +from keystone import notifications +from keystone.openstack.common import versionutils +from keystone.trust import schema + + +CONF = cfg.CONF + +LOG = log.getLogger(__name__) + + +def _trustor_trustee_only(trust, user_id): + if (user_id != trust.get('trustee_user_id') and + user_id != trust.get('trustor_user_id')): + raise exception.Forbidden() + + +def _admin_trustor_only(context, trust, user_id): + if user_id != trust.get('trustor_user_id') and not context['is_admin']: + raise exception.Forbidden() + + +@dependency.requires('assignment_api', 'identity_api', 'role_api', + 'token_provider_api', 'trust_api') +class TrustV3(controller.V3Controller): + collection_name = "trusts" + member_name = "trust" + + @classmethod + def base_url(cls, context, path=None): + """Construct a path and pass it to V3Controller.base_url method.""" + + # NOTE(stevemar): Overriding path to /OS-TRUST/trusts so that + # V3Controller.base_url handles setting the self link correctly. + path = '/OS-TRUST/' + cls.collection_name + return super(TrustV3, cls).base_url(context, path=path) + + def _get_user_id(self, context): + if 'token_id' in context: + token_id = context['token_id'] + token_data = self.token_provider_api.validate_token(token_id) + token_ref = token_model.KeystoneToken(token_id=token_id, + token_data=token_data) + return token_ref.user_id + return None + + def get_trust(self, context, trust_id): + user_id = self._get_user_id(context) + trust = self.trust_api.get_trust(trust_id) + if not trust: + raise exception.TrustNotFound(trust_id=trust_id) + _trustor_trustee_only(trust, user_id) + self._fill_in_roles(context, trust, + self.role_api.list_roles()) + return TrustV3.wrap_member(context, trust) + + def _fill_in_roles(self, context, trust, all_roles): + if trust.get('expires_at') is not None: + trust['expires_at'] = (timeutils.isotime + (trust['expires_at'], + subsecond=True)) + + if 'roles' not in trust: + trust['roles'] = [] + trust_full_roles = [] + for trust_role in trust['roles']: + if isinstance(trust_role, six.string_types): + trust_role = {'id': trust_role} + matching_roles = [x for x in all_roles + if x['id'] == trust_role['id']] + if matching_roles: + full_role = assignment.controllers.RoleV3.wrap_member( + context, matching_roles[0])['role'] + trust_full_roles.append(full_role) + trust['roles'] = trust_full_roles + trust['roles_links'] = { + 'self': (self.base_url(context) + "/%s/roles" % trust['id']), + 'next': None, + 'previous': None} + + def _normalize_role_list(self, trust, all_roles): + trust_roles = [] + all_role_names = {r['name']: r for r in all_roles} + for role in trust.get('roles', []): + if 'id' in role: + trust_roles.append({'id': role['id']}) + elif 'name' in role: + rolename = role['name'] + if rolename in all_role_names: + trust_roles.append({'id': + all_role_names[rolename]['id']}) + else: + raise exception.RoleNotFound("role %s is not defined" % + rolename) + else: + raise exception.ValidationError(attribute='id or name', + target='roles') + return trust_roles + + @controller.protected() + @validation.validated(schema.trust_create, 'trust') + def create_trust(self, context, trust=None): + """Create a new trust. + + The user creating the trust must be the trustor. + + """ + if not trust: + raise exception.ValidationError(attribute='trust', + target='request') + + auth_context = context.get('environment', + {}).get('KEYSTONE_AUTH_CONTEXT', {}) + + # Check if delegated via trust + if auth_context.get('is_delegated_auth'): + # Redelegation case + src_trust_id = auth_context['trust_id'] + if not src_trust_id: + raise exception.Forbidden( + _('Redelegation allowed for delegated by trust only')) + + redelegated_trust = self.trust_api.get_trust(src_trust_id) + else: + redelegated_trust = None + + if trust.get('project_id'): + self._require_role(trust) + self._require_user_is_trustor(context, trust) + self._require_trustee_exists(trust['trustee_user_id']) + all_roles = self.role_api.list_roles() + # Normalize roles + normalized_roles = self._normalize_role_list(trust, all_roles) + trust['roles'] = normalized_roles + self._require_trustor_has_role_in_project(trust) + trust['expires_at'] = self._parse_expiration_date( + trust.get('expires_at')) + trust_id = uuid.uuid4().hex + initiator = notifications._get_request_audit_info(context) + new_trust = self.trust_api.create_trust(trust_id, trust, + normalized_roles, + redelegated_trust, + initiator) + self._fill_in_roles(context, new_trust, all_roles) + return TrustV3.wrap_member(context, new_trust) + + def _require_trustee_exists(self, trustee_user_id): + self.identity_api.get_user(trustee_user_id) + + def _require_user_is_trustor(self, context, trust): + user_id = self._get_user_id(context) + if user_id != trust.get('trustor_user_id'): + raise exception.Forbidden( + _("The authenticated user should match the trustor.")) + + def _require_role(self, trust): + if not trust.get('roles'): + raise exception.Forbidden( + _('At least one role should be specified.')) + + def _get_user_role(self, trust): + if not self._attribute_is_empty(trust, 'project_id'): + return self.assignment_api.get_roles_for_user_and_project( + trust['trustor_user_id'], trust['project_id']) + else: + return [] + + def _require_trustor_has_role_in_project(self, trust): + user_roles = self._get_user_role(trust) + for trust_role in trust['roles']: + matching_roles = [x for x in user_roles + if x == trust_role['id']] + if not matching_roles: + raise exception.RoleNotFound(role_id=trust_role['id']) + + def _parse_expiration_date(self, expiration_date): + if expiration_date is None: + return None + if not expiration_date.endswith('Z'): + expiration_date += 'Z' + try: + return timeutils.parse_isotime(expiration_date) + except ValueError: + raise exception.ValidationTimeStampError() + + def _check_role_for_trust(self, context, trust_id, role_id): + """Checks if a role has been assigned to a trust.""" + trust = self.trust_api.get_trust(trust_id) + if not trust: + raise exception.TrustNotFound(trust_id=trust_id) + user_id = self._get_user_id(context) + _trustor_trustee_only(trust, user_id) + if not any(role['id'] == role_id for role in trust['roles']): + raise exception.RoleNotFound(role_id=role_id) + + @controller.protected() + def list_trusts(self, context): + query = context['query_string'] + trusts = [] + if not query: + self.assert_admin(context) + trusts += self.trust_api.list_trusts() + if 'trustor_user_id' in query: + user_id = query['trustor_user_id'] + calling_user_id = self._get_user_id(context) + if user_id != calling_user_id: + raise exception.Forbidden() + trusts += (self.trust_api. + list_trusts_for_trustor(user_id)) + if 'trustee_user_id' in query: + user_id = query['trustee_user_id'] + calling_user_id = self._get_user_id(context) + if user_id != calling_user_id: + raise exception.Forbidden() + trusts += self.trust_api.list_trusts_for_trustee(user_id) + for trust in trusts: + # get_trust returns roles, list_trusts does not + # It seems in some circumstances, roles does not + # exist in the query response, so check first + if 'roles' in trust: + del trust['roles'] + if trust.get('expires_at') is not None: + trust['expires_at'] = (timeutils.isotime + (trust['expires_at'], + subsecond=True)) + return TrustV3.wrap_collection(context, trusts) + + @controller.protected() + def delete_trust(self, context, trust_id): + trust = self.trust_api.get_trust(trust_id) + if not trust: + raise exception.TrustNotFound(trust_id=trust_id) + + user_id = self._get_user_id(context) + _admin_trustor_only(context, trust, user_id) + initiator = notifications._get_request_audit_info(context) + self.trust_api.delete_trust(trust_id, initiator) + + @controller.protected() + def list_roles_for_trust(self, context, trust_id): + trust = self.get_trust(context, trust_id)['trust'] + if not trust: + raise exception.TrustNotFound(trust_id=trust_id) + user_id = self._get_user_id(context) + _trustor_trustee_only(trust, user_id) + return {'roles': trust['roles'], + 'links': trust['roles_links']} + + @versionutils.deprecated( + versionutils.deprecated.KILO, + remove_in=+2) + def check_role_for_trust(self, context, trust_id, role_id): + return self._check_role_for_trust(self, context, trust_id, role_id) + + @controller.protected() + def get_role_for_trust(self, context, trust_id, role_id): + """Get a role that has been assigned to a trust.""" + self._check_role_for_trust(context, trust_id, role_id) + role = self.role_api.get_role(role_id) + return assignment.controllers.RoleV3.wrap_member(context, role) diff --git a/keystone-moon/keystone/trust/core.py b/keystone-moon/keystone/trust/core.py new file mode 100644 index 00000000..de6b6d85 --- /dev/null +++ b/keystone-moon/keystone/trust/core.py @@ -0,0 +1,251 @@ +# Copyright 2012 OpenStack Foundation +# +# 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. + +"""Main entry point into the Identity service.""" + +import abc + +from oslo_config import cfg +from oslo_log import log +import six + +from keystone.common import dependency +from keystone.common import manager +from keystone import exception +from keystone.i18n import _ +from keystone import notifications + + +CONF = cfg.CONF + +LOG = log.getLogger(__name__) + + +@dependency.requires('identity_api') +@dependency.provider('trust_api') +class Manager(manager.Manager): + """Default pivot point for the Trust backend. + + See :mod:`keystone.common.manager.Manager` for more details on how this + dynamically calls the backend. + + """ + _TRUST = "OS-TRUST:trust" + + def __init__(self): + super(Manager, self).__init__(CONF.trust.driver) + + @staticmethod + def _validate_redelegation(redelegated_trust, trust): + # Validate against: + # 0 < redelegation_count <= max_redelegation_count + max_redelegation_count = CONF.trust.max_redelegation_count + redelegation_depth = redelegated_trust.get('redelegation_count', 0) + if not (0 < redelegation_depth <= max_redelegation_count): + raise exception.Forbidden( + _('Remaining redelegation depth of %(redelegation_depth)d' + ' out of allowed range of [0..%(max_count)d]'), + redelegation_depth=redelegation_depth, + max_count=max_redelegation_count) + + # remaining_uses is None + remaining_uses = trust.get('remaining_uses') + if remaining_uses is not None: + raise exception.Forbidden( + _('Field "remaining_uses" is set to %(value)s' + ' while it must not be set in order to redelegate a trust'), + value=remaining_uses) + + # expiry times + trust_expiry = trust.get('expires_at') + redelegated_expiry = redelegated_trust['expires_at'] + if trust_expiry: + # redelegated trust is from backend and has no tzinfo + if redelegated_expiry < trust_expiry.replace(tzinfo=None): + raise exception.Forbidden( + _('Requested expiration time is more ' + 'than redelegated trust can provide')) + else: + trust['expires_at'] = redelegated_expiry + + # trust roles is a subset of roles of the redelegated trust + parent_roles = set(role['id'] + for role in redelegated_trust['roles']) + if not all(role['id'] in parent_roles for role in trust['roles']): + raise exception.Forbidden( + _('Some of requested roles are not in redelegated trust')) + + def get_trust_pedigree(self, trust_id): + trust = self.driver.get_trust(trust_id) + trust_chain = [trust] + if trust and trust.get('redelegated_trust_id'): + trusts = self.driver.list_trusts_for_trustor( + trust['trustor_user_id']) + while trust_chain[-1].get('redelegated_trust_id'): + for t in trusts: + if t['id'] == trust_chain[-1]['redelegated_trust_id']: + trust_chain.append(t) + break + + return trust_chain + + def get_trust(self, trust_id, deleted=False): + trust = self.driver.get_trust(trust_id, deleted) + + if trust and trust.get('redelegated_trust_id') and not deleted: + trust_chain = self.get_trust_pedigree(trust_id) + + for parent, child in zip(trust_chain[1:], trust_chain): + self._validate_redelegation(parent, child) + try: + self.identity_api.assert_user_enabled( + parent['trustee_user_id']) + except (AssertionError, exception.NotFound): + raise exception.Forbidden( + _('One of the trust agents is disabled or deleted')) + + return trust + + def create_trust(self, trust_id, trust, roles, redelegated_trust=None, + initiator=None): + """Create a new trust. + + :returns: a new trust + """ + # Default for initial trust in chain is max_redelegation_count + max_redelegation_count = CONF.trust.max_redelegation_count + requested_count = trust.get('redelegation_count') + redelegatable = (trust.pop('allow_redelegation', False) + and requested_count != 0) + if not redelegatable: + trust['redelegation_count'] = requested_count = 0 + remaining_uses = trust.get('remaining_uses') + if remaining_uses is not None and remaining_uses <= 0: + msg = _('remaining_uses must be a positive integer or null.') + raise exception.ValidationError(msg) + else: + # Validate requested redelegation depth + if requested_count and requested_count > max_redelegation_count: + raise exception.Forbidden( + _('Requested redelegation depth of %(requested_count)d ' + 'is greater than allowed %(max_count)d'), + requested_count=requested_count, + max_count=max_redelegation_count) + # Decline remaining_uses + if 'remaining_uses' in trust: + exception.ValidationError(_('remaining_uses must not be set ' + 'if redelegation is allowed')) + + if redelegated_trust: + trust['redelegated_trust_id'] = redelegated_trust['id'] + remaining_count = redelegated_trust['redelegation_count'] - 1 + + # Validate depth consistency + if (redelegatable and requested_count and + requested_count != remaining_count): + msg = _('Modifying "redelegation_count" upon redelegation is ' + 'forbidden. Omitting this parameter is advised.') + raise exception.Forbidden(msg) + trust.setdefault('redelegation_count', remaining_count) + + # Check entire trust pedigree validity + pedigree = self.get_trust_pedigree(redelegated_trust['id']) + for t in pedigree: + self._validate_redelegation(t, trust) + + trust.setdefault('redelegation_count', max_redelegation_count) + ref = self.driver.create_trust(trust_id, trust, roles) + + notifications.Audit.created(self._TRUST, trust_id, initiator=initiator) + + return ref + + def delete_trust(self, trust_id, initiator=None): + """Remove a trust. + + :raises: keystone.exception.TrustNotFound + + Recursively remove given and redelegated trusts + """ + trust = self.driver.get_trust(trust_id) + if not trust: + raise exception.TrustNotFound(trust_id) + + trusts = self.driver.list_trusts_for_trustor( + trust['trustor_user_id']) + + for t in trusts: + if t.get('redelegated_trust_id') == trust_id: + # recursive call to make sure all notifications are sent + try: + self.delete_trust(t['id']) + except exception.TrustNotFound: + # if trust was deleted by concurrent process + # consistency must not suffer + pass + + # end recursion + self.driver.delete_trust(trust_id) + + notifications.Audit.deleted(self._TRUST, trust_id, initiator) + + +@six.add_metaclass(abc.ABCMeta) +class Driver(object): + + @abc.abstractmethod + def create_trust(self, trust_id, trust, roles): + """Create a new trust. + + :returns: a new trust + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def get_trust(self, trust_id, deleted=False): + """Get a trust by the trust id. + + :param trust_id: the trust identifier + :type trust_id: string + :param deleted: return the trust even if it is deleted, expired, or + has no consumptions left + :type deleted: bool + """ + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_trusts(self): + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_trusts_for_trustee(self, trustee): + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def list_trusts_for_trustor(self, trustor): + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def delete_trust(self, trust_id): + raise exception.NotImplemented() # pragma: no cover + + @abc.abstractmethod + def consume_use(self, trust_id): + """Consume one use when a trust was created with a limitation on its + uses, provided there are still uses available. + + :raises: keystone.exception.TrustUseLimitReached, + keystone.exception.TrustNotFound + """ + raise exception.NotImplemented() # pragma: no cover diff --git a/keystone-moon/keystone/trust/routers.py b/keystone-moon/keystone/trust/routers.py new file mode 100644 index 00000000..3a6243cc --- /dev/null +++ b/keystone-moon/keystone/trust/routers.py @@ -0,0 +1,67 @@ +# Copyright 2012 OpenStack Foundation +# +# 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. +"""WSGI Routers for the Trust service.""" + +import functools + +from keystone.common import json_home +from keystone.common import wsgi +from keystone.trust import controllers + + +_build_resource_relation = functools.partial( + json_home.build_v3_extension_resource_relation, extension_name='OS-TRUST', + extension_version='1.0') + +TRUST_ID_PARAMETER_RELATION = json_home.build_v3_extension_parameter_relation( + 'OS-TRUST', '1.0', 'trust_id') + + +class Routers(wsgi.RoutersBase): + + def append_v3_routers(self, mapper, routers): + trust_controller = controllers.TrustV3() + + self._add_resource( + mapper, trust_controller, + path='/OS-TRUST/trusts', + get_action='list_trusts', + post_action='create_trust', + rel=_build_resource_relation(resource_name='trusts')) + self._add_resource( + mapper, trust_controller, + path='/OS-TRUST/trusts/{trust_id}', + get_action='get_trust', + delete_action='delete_trust', + rel=_build_resource_relation(resource_name='trust'), + path_vars={ + 'trust_id': TRUST_ID_PARAMETER_RELATION, + }) + self._add_resource( + mapper, trust_controller, + path='/OS-TRUST/trusts/{trust_id}/roles', + get_action='list_roles_for_trust', + rel=_build_resource_relation(resource_name='trust_roles'), + path_vars={ + 'trust_id': TRUST_ID_PARAMETER_RELATION, + }) + self._add_resource( + mapper, trust_controller, + path='/OS-TRUST/trusts/{trust_id}/roles/{role_id}', + get_head_action='get_role_for_trust', + rel=_build_resource_relation(resource_name='trust_role'), + path_vars={ + 'trust_id': TRUST_ID_PARAMETER_RELATION, + 'role_id': json_home.Parameters.ROLE_ID, + }) diff --git a/keystone-moon/keystone/trust/schema.py b/keystone-moon/keystone/trust/schema.py new file mode 100644 index 00000000..087cd1e9 --- /dev/null +++ b/keystone-moon/keystone/trust/schema.py @@ -0,0 +1,46 @@ +# 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 keystone.common import validation +from keystone.common.validation import parameter_types + + +_trust_properties = { + 'trustor_user_id': parameter_types.id_string, + 'trustee_user_id': parameter_types.id_string, + 'impersonation': parameter_types.boolean, + 'project_id': validation.nullable(parameter_types.id_string), + 'remaining_uses': { + 'type': ['integer', 'null'], + 'minimum': 1 + }, + 'expires_at': { + 'type': ['null', 'string'] + }, + 'allow_redelegation': { + 'type': ['boolean', 'null'] + }, + 'redelegation_count': { + 'type': ['integer', 'null'], + 'minimum': 0 + }, + # TODO(lbragstad): Need to find a better way to do this. We should be + # checking that a role is a list of IDs and/or names. + 'roles': validation.add_array_type(parameter_types.id_string) +} + +trust_create = { + 'type': 'object', + 'properties': _trust_properties, + 'required': ['trustor_user_id', 'trustee_user_id', 'impersonation'], + 'additionalProperties': True +} -- cgit 1.2.3-korg