diff --git a/doc/api_samples/versions/versions-get-resp.json b/doc/api_samples/versions/versions-get-resp.json index e6a31ff004..f32b72c513 100644 --- a/doc/api_samples/versions/versions-get-resp.json +++ b/doc/api_samples/versions/versions-get-resp.json @@ -22,7 +22,7 @@ } ], "status": "CURRENT", - "version": "2.9", + "version": "2.10", "min_version": "2.1", "updated": "2013-07-23T11:33:21Z" } diff --git a/doc/v3/api_samples/keypairs/v2.10/keypairs-get-resp.json b/doc/v3/api_samples/keypairs/v2.10/keypairs-get-resp.json new file mode 100644 index 0000000000..2970994fad --- /dev/null +++ b/doc/v3/api_samples/keypairs/v2.10/keypairs-get-resp.json @@ -0,0 +1,14 @@ +{ + "keypair": { + "fingerprint": "44:fe:29:6e:23:14:b9:53:5b:65:82:58:1c:fe:5a:c3", + "name": "keypair-6638abdb-c4e8-407c-ba88-c8dd7cc3c4f1", + "type": "ssh", + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC1HTrHCbb9NawNLSV8N6tSa8i637+EC2dA+lsdHHfQlT54t+N0nHhJPlKWDLhc579j87vp6RDFriFJ/smsTnDnf64O12z0kBaJpJPH2zXrBkZFK6q2rmxydURzX/z0yLSCP77SFJ0fdXWH2hMsAusflGyryHGX20n+mZK6mDrxVzGxEz228dwQ5G7Az5OoZDWygH2pqPvKjkifRw0jwUKf3BbkP0QvANACOk26cv16mNFpFJfI1N3OC5lUsZQtKGR01ptJoWijYKccqhkAKuo902tg/qup58J5kflNm7I61sy1mJon6SGqNUSfoQagqtBH6vd/tU1jnlwZ03uUroAL Generated-by-Nova\n", + "user_id": "fake", + "deleted": false, + "created_at": "2014-05-07T12:06:13.681238", + "updated_at": null, + "deleted_at": null, + "id": 1 + } +} diff --git a/doc/v3/api_samples/keypairs/v2.10/keypairs-import-post-req.json b/doc/v3/api_samples/keypairs/v2.10/keypairs-import-post-req.json new file mode 100644 index 0000000000..fb18f269c9 --- /dev/null +++ b/doc/v3/api_samples/keypairs/v2.10/keypairs-import-post-req.json @@ -0,0 +1,8 @@ +{ + "keypair": { + "name": "keypair-d20a3d59-9433-4b79-8726-20b431d89c78", + "type": "ssh", + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated-by-Nova", + "user_id": "fake" + } +} diff --git a/doc/v3/api_samples/keypairs/v2.10/keypairs-import-post-resp.json b/doc/v3/api_samples/keypairs/v2.10/keypairs-import-post-resp.json new file mode 100644 index 0000000000..ca1a70b334 --- /dev/null +++ b/doc/v3/api_samples/keypairs/v2.10/keypairs-import-post-resp.json @@ -0,0 +1,9 @@ +{ + "keypair": { + "fingerprint": "1e:2c:9b:56:79:4b:45:77:f9:ca:7a:98:2c:b0:d5:3c", + "name": "keypair-803a1926-af78-4b05-902a-1d6f7a8d9d3e", + "type": "ssh", + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDx8nkQv/zgGgB4rMYmIf+6A4l6Rr+o/6lHBQdW5aYd44bd8JttDCE/F/pNRr0lRE+PiqSPO8nDPHw0010JeMH9gYgnnFlyY3/OcJ02RhIPyyxYpv9FhY+2YiUkpwFOcLImyrxEsYXpD/0d3ac30bNH6Sw9JD9UZHYcpSxsIbECHw== Generated-by-Nova", + "user_id": "fake" + } +} \ No newline at end of file diff --git a/doc/v3/api_samples/keypairs/v2.10/keypairs-list-resp.json b/doc/v3/api_samples/keypairs/v2.10/keypairs-list-resp.json new file mode 100644 index 0000000000..3d5fe045a0 --- /dev/null +++ b/doc/v3/api_samples/keypairs/v2.10/keypairs-list-resp.json @@ -0,0 +1,12 @@ +{ + "keypairs": [ + { + "keypair": { + "fingerprint": "7e:eb:ab:24:ba:d1:e1:88:ae:9a:fb:66:53:df:d3:bd", + "name": "keypair-50ca852e-273f-4cdc-8949-45feba200837", + "type": "ssh", + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCkF3MX59OrlBs3dH5CU7lNmvpbrgZxSpyGjlnE8Flkirnc/Up22lpjznoxqeoTAwTW034k7Dz6aYIrZGmQwe2TkE084yqvlj45Dkyoj95fW/sZacm0cZNuL69EObEGHdprfGJQajrpz22NQoCD8TFB8Wv+8om9NH9Le6s+WPe98WC77KLw8qgfQsbIey+JawPWl4O67ZdL5xrypuRjfIPWjgy/VH85IXg/Z/GONZ2nxHgSShMkwqSFECAC5L3PHB+0+/12M/iikdatFSVGjpuHvkLOs3oe7m6HlOfluSJ85BzLWBbvva93qkGmLg4ZAc8rPh2O+YIsBUHNLLMM/oQp Generated-by-Nova\n" + } + } + ] +} \ No newline at end of file diff --git a/doc/v3/api_samples/keypairs/v2.10/keypairs-post-req.json b/doc/v3/api_samples/keypairs/v2.10/keypairs-post-req.json new file mode 100644 index 0000000000..005a3f5045 --- /dev/null +++ b/doc/v3/api_samples/keypairs/v2.10/keypairs-post-req.json @@ -0,0 +1,7 @@ +{ + "keypair": { + "name": "keypair-ab9ff2e6-a6d7-4915-a241-044c369c07f9", + "type": "ssh", + "user_id": "fake" + } +} diff --git a/doc/v3/api_samples/keypairs/v2.10/keypairs-post-resp.json b/doc/v3/api_samples/keypairs/v2.10/keypairs-post-resp.json new file mode 100644 index 0000000000..394960868b --- /dev/null +++ b/doc/v3/api_samples/keypairs/v2.10/keypairs-post-resp.json @@ -0,0 +1,10 @@ +{ + "keypair": { + "fingerprint": "7e:eb:ab:24:ba:d1:e1:88:ae:9a:fb:66:53:df:d3:bd", + "name": "keypair-ab9ff2e6-a6d7-4915-a241-044c369c07f9", + "type": "ssh", + "private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEApBdzF+fTq5QbN3R+QlO5TZr6W64GcUqcho5ZxPBZZIq53P1K\ndtpaY856ManqEwME1tN+JOw8+mmCK2RpkMHtk5BNPOMqr5Y+OQ5MqI/eX1v7GWnJ\ntHGTbi+vRDmxBh3aa3xiUGo66c9tjUKAg/ExQfFr/vKJvTR/S3urPlj3vfFgu+yi\n8PKoH0LGyHsviWsD1peDuu2XS+ca8qbkY3yD1o4Mv1R/OSF4P2fxjjWdp8R4EkoT\nJMKkhRAgAuS9zxwftPv9djP4opHWrRUlRo6bh75CzrN6Hu5uh5Tn5bkifOQcy1gW\n772vd6pBpi4OGQHPKz4djvmCLAVBzSyzDP6EKQIDAQABAoIBAQCB+tU/ZXKlIe+h\nMNTmoz1QfOe+AY625Rwx9cakGqMk4kKyC62VkgcxshfXCToSjzyhEuyEQOFYloT2\n7FY2xXb0gcS861Efv0pQlcQhbbz/GnQ/wC13ktPu3zTdPTm9l54xsFiMTGmYVaf4\n0mnMmhyjmKIsVGDJEDGZUD/oZj7wJGOFha5M4FZrZlJIrEZC0rGGlcC0kGF2no6B\nj1Mu7HjyK3pTKf4dlp+jeRikUF5Pct+qT+rcv2rZ3fl3inxtlLEwZeFPbp/njf/U\nIGxFzZsuLmiFlsJar6M5nEckTB3p25maWWaR8/0jvJRgsPnuoUrUoGDq87DMKCdk\nlw6by9fRAoGBANhnS9ko7Of+ntqIFR7xOG9p/oPATztgHkFxe4GbQ0leaDRTx3vE\ndQmUCnn24xtyVECaI9a4IV+LP1npw8niWUJ4pjgdAlkF4cCTu9sN+cBO15SfdACI\nzD1DaaHmpFCAWlpTo68VWlvWll6i2ncCkRJR1+q/C/yQz7asvl4AakElAoGBAMId\nxqMT2Sy9xLuHsrAoMUvBOkwaMYZH+IAb4DvUDjVIiKWjmonrmopS5Lpb+ALBKqZe\neVfD6HwWQqGwCFItToaEkZvrNfTapoNCHWWg001D49765UV5lMrArDbM1vXtFfM4\nDRYM6+Y6o/6QH8EBgXtyBxcYthIDBM3wBJa67xG1AoGAKTm8fFlMkIG0N4N3Kpbf\nnnH915GaRoBwIx2AXtd6QQ7oIRfYx95MQY/fUw7SgxcLr+btbulTCkWXwwRClUI2\nqPAdElGMcfMp56r9PaTy8EzUyu55heSJrB4ckIhEw0VAcTa/1wnlVduSd+LkZYmq\no2fOD11n5iycNXvBJF1F4LUCgYAMaRbwCi7SW3eefbiA5rDwJPRzNSGBckyC9EVL\nzezynyaNYH5a3wNMYKxa9dJPasYtSND9OXs9o7ay26xMhLUGiKc+jrUuaGRI9Asp\nGjUoNXT2JphN7s4CgHsCLep4YqYKnMTJah4S5CDj/5boIg6DM/EcGupZEHRYLkY8\n1MrAGQKBgQCi9yeC39ctLUNn+Ix604gttWWChdt3ozufTZ7HybJOSRA9Gh3iD5gm\nzlz0xqpGShKpOY2k+ftvja0poMdGeJLt84P3r2q01IgI7w0LmOj5m0W10dHysH27\nBWpCnHdBJMxnBsMRPoM4MKkmKWD9l5PSTCTWtkIpsyuDCko6D9UwZA==\n-----END RSA PRIVATE KEY-----\n", + "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCkF3MX59OrlBs3dH5CU7lNmvpbrgZxSpyGjlnE8Flkirnc/Up22lpjznoxqeoTAwTW034k7Dz6aYIrZGmQwe2TkE084yqvlj45Dkyoj95fW/sZacm0cZNuL69EObEGHdprfGJQajrpz22NQoCD8TFB8Wv+8om9NH9Le6s+WPe98WC77KLw8qgfQsbIey+JawPWl4O67ZdL5xrypuRjfIPWjgy/VH85IXg/Z/GONZ2nxHgSShMkwqSFECAC5L3PHB+0+/12M/iikdatFSVGjpuHvkLOs3oe7m6HlOfluSJ85BzLWBbvva93qkGmLg4ZAc8rPh2O+YIsBUHNLLMM/oQp Generated-by-Nova\n", + "user_id": "fake" + } +} diff --git a/etc/nova/policy.json b/etc/nova/policy.json index 338b7d6e24..a644b203b9 100644 --- a/etc/nova/policy.json +++ b/etc/nova/policy.json @@ -302,10 +302,10 @@ "os_compute_api:ips:show": "rule:admin_or_owner", "os_compute_api:os-keypairs:discoverable": "", "os_compute_api:os-keypairs": "", - "os_compute_api:os-keypairs:index": "", - "os_compute_api:os-keypairs:show": "", - "os_compute_api:os-keypairs:create": "", - "os_compute_api:os-keypairs:delete": "", + "os_compute_api:os-keypairs:index": "rule:admin_api or user_id:%(user_id)s", + "os_compute_api:os-keypairs:show": "rule:admin_api or user_id:%(user_id)s", + "os_compute_api:os-keypairs:create": "rule:admin_api or user_id:%(user_id)s", + "os_compute_api:os-keypairs:delete": "rule:admin_api or user_id:%(user_id)s", "os_compute_api:limits:discoverable": "", "os_compute_api:limits": "", "os_compute_api:os-lock-server:discoverable": "", diff --git a/nova/api/openstack/api_version_request.py b/nova/api/openstack/api_version_request.py index 6e2db74213..c8d60c1a2d 100644 --- a/nova/api/openstack/api_version_request.py +++ b/nova/api/openstack/api_version_request.py @@ -48,6 +48,8 @@ REST_API_VERSION_HISTORY = """REST API Version History: * 2.7 - Check flavor type before add tenant access. * 2.8 - Add new protocol for VM console (mks) * 2.9 - Exposes lock information in server details. + * 2.10 - Allow admins to query, create and delete keypairs owned by any + user. """ # The minimum and maximum versions of the API supported @@ -56,7 +58,7 @@ REST_API_VERSION_HISTORY = """REST API Version History: # Note(cyeoh): This only applies for the v2.1 API once microversions # support is fully merged. It does not affect the V2 API. _MIN_API_VERSION = "2.1" -_MAX_API_VERSION = "2.9" +_MAX_API_VERSION = "2.10" DEFAULT_API_VERSION = _MIN_API_VERSION diff --git a/nova/api/openstack/compute/plugins/v3/keypairs.py b/nova/api/openstack/compute/plugins/v3/keypairs.py index f41fc29eec..64d40dcf1f 100644 --- a/nova/api/openstack/compute/plugins/v3/keypairs.py +++ b/nova/api/openstack/compute/plugins/v3/keypairs.py @@ -51,7 +51,26 @@ class KeypairController(wsgi.Controller): clean[attr] = keypair[attr] return clean - @wsgi.Controller.api_version("2.2") + @wsgi.Controller.api_version("2.10") + @wsgi.response(201) + @extensions.expected_errors((400, 403, 409)) + @validation.schema(keypairs.create_v210) + def create(self, req, body): + """Create or import keypair. + + A policy check restricts users from creating keys for other users + + params: keypair object with: + name (required) - string + public_key (optional) - string + type (optional) - string + user_id (optional) - string + """ + # handle optional user-id for admin only + user_id = body['keypair'].get('user_id') + return self._create(req, body, type=True, user_id=user_id) + + @wsgi.Controller.api_version("2.2", "2.9") # noqa @wsgi.response(201) @extensions.expected_errors((400, 403, 409)) @validation.schema(keypairs.create_v22) @@ -89,24 +108,26 @@ class KeypairController(wsgi.Controller): """ return self._create(req, body) - def _create(self, req, body, **keypair_filters): + def _create(self, req, body, user_id=None, **keypair_filters): context = req.environ['nova.context'] - authorize(context, action='create') - params = body['keypair'] name = params['name'] key_type = params.get('type', keypair_obj.KEYPAIR_TYPE_SSH) + user_id = user_id or context.user_id + authorize(context, action='create', + target={'user_id': user_id, + 'project_id': context.project_id}) try: if 'public_key' in params: keypair = self.api.import_key_pair(context, - context.user_id, name, + user_id, name, params['public_key'], key_type) keypair = self._filter_keypair(keypair, user_id=True, **keypair_filters) else: keypair, private_key = self.api.create_key_pair( - context, context.user_id, name, key_type) + context, user_id, name, key_type) keypair = self._filter_keypair(keypair, user_id=True, **keypair_filters) keypair['private_key'] = private_key @@ -127,22 +148,46 @@ class KeypairController(wsgi.Controller): def delete(self, req, id): self._delete(req, id) - @wsgi.Controller.api_version("2.2") # noqa + @wsgi.Controller.api_version("2.2", "2.9") # noqa @wsgi.response(204) @extensions.expected_errors(404) def delete(self, req, id): self._delete(req, id) - def _delete(self, req, id): + @wsgi.Controller.api_version("2.10") # noqa + @wsgi.response(204) + @extensions.expected_errors(404) + def delete(self, req, id): + # handle optional user-id for admin only + user_id = self._get_user_id(req) + self._delete(req, id, user_id=user_id) + + def _delete(self, req, id, user_id=None): """Delete a keypair with a given name.""" context = req.environ['nova.context'] - authorize(context, action='delete') + # handle optional user-id for admin only + user_id = user_id or context.user_id + authorize(context, action='delete', + target={'user_id': user_id, + 'project_id': context.project_id}) try: - self.api.delete_key_pair(context, context.user_id, id) + self.api.delete_key_pair(context, user_id, id) except exception.KeypairNotFound as exc: raise webob.exc.HTTPNotFound(explanation=exc.format_message()) - @wsgi.Controller.api_version("2.2") + def _get_user_id(self, req): + if 'user_id' in req.GET.keys(): + user_id = req.GET.getall('user_id')[0] + return user_id + + @wsgi.Controller.api_version("2.10") + @extensions.expected_errors(404) + def show(self, req, id): + # handle optional user-id for admin only + user_id = self._get_user_id(req) + return self._show(req, id, type=True, user_id=user_id) + + @wsgi.Controller.api_version("2.2", "2.9") # noqa @extensions.expected_errors(404) def show(self, req, id): return self._show(req, id, type=True) @@ -152,15 +197,18 @@ class KeypairController(wsgi.Controller): def show(self, req, id): return self._show(req, id) - def _show(self, req, id, **keypair_filters): + def _show(self, req, id, user_id=None, **keypair_filters): """Return data for the given key name.""" context = req.environ['nova.context'] - authorize(context, action='show') + user_id = user_id or context.user_id + authorize(context, action='show', + target={'user_id': user_id, + 'project_id': context.project_id}) try: # The return object needs to be a dict in order to pop the 'type' # field, if the api_version < 2.2. - keypair = self.api.get_key_pair(context, context.user_id, id) + keypair = self.api.get_key_pair(context, user_id, id) keypair = self._filter_keypair(keypair, created_at=True, deleted=True, deleted_at=True, id=True, user_id=True, @@ -172,7 +220,14 @@ class KeypairController(wsgi.Controller): # behaviors in this keypair resource. return {'keypair': keypair} - @wsgi.Controller.api_version("2.2") + @wsgi.Controller.api_version("2.10") + @extensions.expected_errors(()) + def index(self, req): + # handle optional user-id for admin only + user_id = self._get_user_id(req) + return self._index(req, type=True, user_id=user_id) + + @wsgi.Controller.api_version("2.2", "2.9") # noqa @extensions.expected_errors(()) def index(self, req): return self._index(req, type=True) @@ -182,11 +237,14 @@ class KeypairController(wsgi.Controller): def index(self, req): return self._index(req) - def _index(self, req, **keypair_filters): + def _index(self, req, user_id=None, **keypair_filters): """List of keypairs for a user.""" context = req.environ['nova.context'] - authorize(context, action='index') - key_pairs = self.api.get_key_pairs(context, context.user_id) + user_id = user_id or context.user_id + authorize(context, action='index', + target={'user_id': user_id, + 'project_id': context.project_id}) + key_pairs = self.api.get_key_pairs(context, user_id) rval = [] for key_pair in key_pairs: rval.append({'keypair': self._filter_keypair(key_pair, diff --git a/nova/api/openstack/compute/schemas/v3/keypairs.py b/nova/api/openstack/compute/schemas/v3/keypairs.py index c31192376b..f8a0575692 100644 --- a/nova/api/openstack/compute/schemas/v3/keypairs.py +++ b/nova/api/openstack/compute/schemas/v3/keypairs.py @@ -53,6 +53,28 @@ create_v22 = { 'additionalProperties': False, } +create_v210 = { + 'type': 'object', + 'properties': { + 'keypair': { + 'type': 'object', + 'properties': { + 'name': parameter_types.name, + 'type': { + 'type': 'string', + 'enum': ['ssh', 'x509'] + }, + 'public_key': {'type': 'string'}, + 'user_id': {'type': 'string'}, + }, + 'required': ['name'], + 'additionalProperties': False, + }, + }, + 'required': ['keypair'], + 'additionalProperties': False, +} + server_create = { 'key_name': parameter_types.name, } diff --git a/nova/api/openstack/rest_api_version_history.rst b/nova/api/openstack/rest_api_version_history.rst index 0961e1e5c4..8ea86f340f 100644 --- a/nova/api/openstack/rest_api_version_history.rst +++ b/nova/api/openstack/rest_api_version_history.rst @@ -107,3 +107,13 @@ user documentation. Add a new ``locked`` attribute to the detailed view of servers. ``locked`` will be ``true`` if anyone is currently holding a lock on the server, ``false`` otherwise. + +2.10 +--- + + Added user_id parameter to os-keypairs plugin, as well as a new property + in the request body, for the create operation. + + Administrators will be able to list, get details and delete keypairs owned by + users other than themselves and to create new keypairs on behalf of their + users. diff --git a/nova/tests/functional/api_samples/versions/versions-get-resp.json.tpl b/nova/tests/functional/api_samples/versions/versions-get-resp.json.tpl index e6a31ff004..f32b72c513 100644 --- a/nova/tests/functional/api_samples/versions/versions-get-resp.json.tpl +++ b/nova/tests/functional/api_samples/versions/versions-get-resp.json.tpl @@ -22,7 +22,7 @@ } ], "status": "CURRENT", - "version": "2.9", + "version": "2.10", "min_version": "2.1", "updated": "2013-07-23T11:33:21Z" } diff --git a/nova/tests/functional/api_samples_test_base.py b/nova/tests/functional/api_samples_test_base.py index e78c103dc3..fcf3c6057b 100644 --- a/nova/tests/functional/api_samples_test_base.py +++ b/nova/tests/functional/api_samples_test_base.py @@ -304,6 +304,7 @@ class ApiSampleTestBase(integrated_helpers._IntegratedTestBase): 'compute_host': self.compute.host, 'text': text, 'int': '[0-9]+', + 'user_id': text, } def _get_response(self, url, method, body=None, strip_version=False, diff --git a/nova/tests/functional/v3/api_samples/keypairs/v2.10/keypairs-get-resp.json.tpl b/nova/tests/functional/v3/api_samples/keypairs/v2.10/keypairs-get-resp.json.tpl new file mode 100644 index 0000000000..d18ae16351 --- /dev/null +++ b/nova/tests/functional/v3/api_samples/keypairs/v2.10/keypairs-get-resp.json.tpl @@ -0,0 +1,14 @@ +{ + "keypair": { + "public_key": "%(public_key)s", + "name": "%(keypair_name)s", + "type": "%(keypair_type)s", + "fingerprint": "%(fingerprint)s", + "user_id": "%(user_id)s", + "deleted": false, + "created_at": "%(strtime)s", + "updated_at": null, + "deleted_at": null, + "id": 1 + } +} diff --git a/nova/tests/functional/v3/api_samples/keypairs/v2.10/keypairs-import-post-req.json.tpl b/nova/tests/functional/v3/api_samples/keypairs/v2.10/keypairs-import-post-req.json.tpl new file mode 100644 index 0000000000..03e60c0133 --- /dev/null +++ b/nova/tests/functional/v3/api_samples/keypairs/v2.10/keypairs-import-post-req.json.tpl @@ -0,0 +1,8 @@ +{ + "keypair": { + "name": "%(keypair_name)s", + "type": "%(keypair_type)s", + "public_key": "%(public_key)s", + "user_id": "%(user_id)s" + } +} diff --git a/nova/tests/functional/v3/api_samples/keypairs/v2.10/keypairs-import-post-resp.json.tpl b/nova/tests/functional/v3/api_samples/keypairs/v2.10/keypairs-import-post-resp.json.tpl new file mode 100644 index 0000000000..30d3fa969d --- /dev/null +++ b/nova/tests/functional/v3/api_samples/keypairs/v2.10/keypairs-import-post-resp.json.tpl @@ -0,0 +1,9 @@ +{ + "keypair": { + "fingerprint": "%(fingerprint)s", + "name": "%(keypair_name)s", + "type": "%(keypair_type)s", + "public_key": "%(public_key)s", + "user_id": "%(user_id)s" + } +} diff --git a/nova/tests/functional/v3/api_samples/keypairs/v2.10/keypairs-list-resp.json.tpl b/nova/tests/functional/v3/api_samples/keypairs/v2.10/keypairs-list-resp.json.tpl new file mode 100644 index 0000000000..8e0963bc7a --- /dev/null +++ b/nova/tests/functional/v3/api_samples/keypairs/v2.10/keypairs-list-resp.json.tpl @@ -0,0 +1,12 @@ +{ + "keypairs": [ + { + "keypair": { + "fingerprint": "%(fingerprint)s", + "name": "%(keypair_name)s", + "type": "%(keypair_type)s", + "public_key": "%(public_key)s" + } + } + ] +} diff --git a/nova/tests/functional/v3/api_samples/keypairs/v2.10/keypairs-post-req.json.tpl b/nova/tests/functional/v3/api_samples/keypairs/v2.10/keypairs-post-req.json.tpl new file mode 100644 index 0000000000..f6a6d47b56 --- /dev/null +++ b/nova/tests/functional/v3/api_samples/keypairs/v2.10/keypairs-post-req.json.tpl @@ -0,0 +1,7 @@ +{ + "keypair": { + "name": "%(keypair_name)s", + "type": "%(keypair_type)s", + "user_id": "%(user_id)s" + } +} diff --git a/nova/tests/functional/v3/api_samples/keypairs/v2.10/keypairs-post-resp.json.tpl b/nova/tests/functional/v3/api_samples/keypairs/v2.10/keypairs-post-resp.json.tpl new file mode 100644 index 0000000000..ee5eb23f77 --- /dev/null +++ b/nova/tests/functional/v3/api_samples/keypairs/v2.10/keypairs-post-resp.json.tpl @@ -0,0 +1,10 @@ +{ + "keypair": { + "fingerprint": "%(fingerprint)s", + "name": "%(keypair_name)s", + "type": "%(keypair_type)s", + "private_key": "%(private_key)s", + "public_key": "%(public_key)s", + "user_id": "%(user_id)s" + } +} diff --git a/nova/tests/functional/v3/test_keypairs.py b/nova/tests/functional/v3/test_keypairs.py index 98216287fd..838b6738de 100644 --- a/nova/tests/functional/v3/test_keypairs.py +++ b/nova/tests/functional/v3/test_keypairs.py @@ -169,3 +169,66 @@ class KeyPairsV22SampleJsonTest(KeyPairsSampleJsonTest): def test_keypairs_import_key_post_invalid_combination(self): self._check_keypairs_import_key_post_invalid( keypair_type=keypair_obj.KEYPAIR_TYPE_X509) + + +class KeyPairsV210SampleJsonTest(KeyPairsSampleJsonTest): + ADMIN_API = True + request_api_version = '2.10' + expected_post_status_code = 201 + expected_delete_status_code = 204 + scenarios = [('v2_10', {})] + _api_version = 'v2' + + def test_keypair_create_for_user(self): + subs = { + 'keypair_type': keypair_obj.KEYPAIR_TYPE_SSH, + 'public_key': fake_crypto.get_ssh_public_key(), + 'user_id': "fake" + } + self._check_keypairs_post(**subs) + + def test_keypairs_post(self): + return self._check_keypairs_post( + keypair_type=keypair_obj.KEYPAIR_TYPE_SSH, + user_id="admin") + + def test_keypairs_import_key_post(self): + # NOTE(claudiub): overrides the method with the same name in + # KeypairsSampleJsonTest, since the API sample expects a keypair_type. + public_key = fake_crypto.get_ssh_public_key() + self._check_keypairs_import_key_post( + public_key, keypair_type=keypair_obj.KEYPAIR_TYPE_SSH, + user_id="fake") + + def test_keypairs_delete_for_user(self): + # Delete a keypair on behalf of a user + subs = { + 'keypair_type': keypair_obj.KEYPAIR_TYPE_SSH, + 'public_key': fake_crypto.get_ssh_public_key(), + 'user_id': "fake" + } + key_name = self._check_keypairs_post(**subs) + response = self._do_delete('os-keypairs/%s?user_id=fake' % key_name, + api_version=self.request_api_version) + self.assertEqual(self.expected_delete_status_code, + response.status_code) + + +class KeyPairsV210SampleJsonTestNotAdmin(KeyPairsV210SampleJsonTest): + ADMIN_API = False + + def test_keypairs_post(self): + return self._check_keypairs_post( + keypair_type=keypair_obj.KEYPAIR_TYPE_SSH, + user_id="fake") + + def test_keypairs_post_for_other_user(self): + key_name = 'keypair-' + str(uuid.uuid4()) + subs = dict(keypair_name=key_name, + keypair_type=keypair_obj.KEYPAIR_TYPE_SSH, + user_id='fake1') + response = self._do_post('os-keypairs', 'keypairs-post-req', subs, + api_version=self.request_api_version, + ) + + self.assertEqual(403, response.status_code) diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_keypairs.py b/nova/tests/unit/api/openstack/compute/contrib/test_keypairs.py index 4f0dfec37f..9c47b8f7b4 100644 --- a/nova/tests/unit/api/openstack/compute/contrib/test_keypairs.py +++ b/nova/tests/unit/api/openstack/compute/contrib/test_keypairs.py @@ -13,6 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. +import mock from oslo_serialization import jsonutils import webob @@ -399,6 +400,105 @@ class KeypairsTestV22(KeypairsTestV21): self.assertEqual('ssh', res_dict['keypair']['type']) +class KeypairsTestV210(KeypairsTestV22): + wsgi_api_version = '2.10' + + def test_keypair_list_other_user(self): + req = fakes.HTTPRequest.blank(self.base_url + + '/os-keypairs?user_id=foo', + version=self.wsgi_api_version, + use_admin_context=True) + with mock.patch.object(self.controller.api, 'get_key_pairs') as mock_g: + self.controller.index(req) + userid = mock_g.call_args_list[0][0][1] + self.assertEqual('foo', userid) + + def test_keypair_list_other_user_not_admin(self): + req = fakes.HTTPRequest.blank(self.base_url + + '/os-keypairs?user_id=foo', + version=self.wsgi_api_version) + with mock.patch.object(self.controller.api, 'get_key_pairs'): + self.assertRaises(exception.PolicyNotAuthorized, + self.controller.index, req) + + def test_keypair_show_other_user(self): + req = fakes.HTTPRequest.blank(self.base_url + + '/os-keypairs/FAKE?user_id=foo', + version=self.wsgi_api_version, + use_admin_context=True) + with mock.patch.object(self.controller.api, 'get_key_pair') as mock_g: + self.controller.show(req, 'FAKE') + userid = mock_g.call_args_list[0][0][1] + self.assertEqual('foo', userid) + + def test_keypair_show_other_user_not_admin(self): + req = fakes.HTTPRequest.blank(self.base_url + + '/os-keypairs/FAKE?user_id=foo', + version=self.wsgi_api_version) + with mock.patch.object(self.controller.api, 'get_key_pair'): + self.assertRaises(exception.PolicyNotAuthorized, + self.controller.show, req, 'FAKE') + + def test_keypair_delete_other_user(self): + req = fakes.HTTPRequest.blank(self.base_url + + '/os-keypairs/FAKE?user_id=foo', + version=self.wsgi_api_version, + use_admin_context=True) + with mock.patch.object(self.controller.api, + 'delete_key_pair') as mock_g: + self.controller.delete(req, 'FAKE') + userid = mock_g.call_args_list[0][0][1] + self.assertEqual('foo', userid) + + def test_keypair_delete_other_user_not_admin(self): + req = fakes.HTTPRequest.blank(self.base_url + + '/os-keypairs/FAKE?user_id=foo', + version=self.wsgi_api_version) + with mock.patch.object(self.controller.api, 'delete_key_pair'): + self.assertRaises(exception.PolicyNotAuthorized, + self.controller.delete, req, 'FAKE') + + def test_keypair_create_other_user(self): + req = fakes.HTTPRequest.blank(self.base_url + + '/os-keypairs', + version=self.wsgi_api_version, + use_admin_context=True) + body = {'keypair': {'name': 'create_test', + 'user_id': '8861f37f-034e-4ca8-8abe-6d13c074574a'}} + with mock.patch.object(self.controller.api, + 'create_key_pair', + return_value=(mock.MagicMock(), 1)) as mock_g: + res = self.controller.create(req, body=body) + userid = mock_g.call_args_list[0][0][1] + self.assertEqual('8861f37f-034e-4ca8-8abe-6d13c074574a', userid) + self.assertIn('keypair', res) + + def test_keypair_import_other_user(self): + req = fakes.HTTPRequest.blank(self.base_url + + '/os-keypairs', + version=self.wsgi_api_version, + use_admin_context=True) + body = {'keypair': {'name': 'create_test', + 'user_id': '8861f37f-034e-4ca8-8abe-6d13c074574a', + 'public_key': 'public_key'}} + with mock.patch.object(self.controller.api, + 'import_key_pair') as mock_g: + res = self.controller.create(req, body=body) + userid = mock_g.call_args_list[0][0][1] + self.assertEqual('8861f37f-034e-4ca8-8abe-6d13c074574a', userid) + self.assertIn('keypair', res) + + def test_keypair_create_other_user_not_admin(self): + req = fakes.HTTPRequest.blank(self.base_url + + '/os-keypairs', + version=self.wsgi_api_version) + body = {'keypair': {'name': 'create_test', + 'user_id': '8861f37f-034e-4ca8-8abe-6d13c074574a'}} + self.assertRaises(exception.PolicyNotAuthorized, + self.controller.create, + req, body=body) + + class KeypairPolicyTestV2(KeypairPolicyTestV21): KeyPairController = keypairs_v2.KeypairController() policy_path = 'compute_extension:keypairs' diff --git a/nova/tests/unit/api/openstack/compute/test_versions.py b/nova/tests/unit/api/openstack/compute/test_versions.py index 0103882afa..86229c043a 100644 --- a/nova/tests/unit/api/openstack/compute/test_versions.py +++ b/nova/tests/unit/api/openstack/compute/test_versions.py @@ -65,7 +65,7 @@ EXP_VERSIONS = { "v2.1": { "id": "v2.1", "status": "CURRENT", - "version": "2.9", + "version": "2.10", "min_version": "2.1", "updated": "2013-07-23T11:33:21Z", "links": [ @@ -114,7 +114,7 @@ class VersionsTestV20(test.NoDBTestCase): { "id": "v2.1", "status": "CURRENT", - "version": "2.9", + "version": "2.10", "min_version": "2.1", "updated": "2013-07-23T11:33:21Z", "links": [ diff --git a/nova/tests/unit/fake_policy.py b/nova/tests/unit/fake_policy.py index ed5de175e8..be87db5816 100644 --- a/nova/tests/unit/fake_policy.py +++ b/nova/tests/unit/fake_policy.py @@ -266,10 +266,14 @@ policy_data = """ "compute_extension:keypairs:delete": "", "os_compute_api:os-keypairs": "", - "os_compute_api:os-keypairs:index": "", - "os_compute_api:os-keypairs:show": "", - "os_compute_api:os-keypairs:create": "", - "os_compute_api:os-keypairs:delete": "", + "os_compute_api:os-keypairs:index": + "rule:admin_api or user_id:%(user_id)s", + "os_compute_api:os-keypairs:show": + "rule:admin_api or user_id:%(user_id)s", + "os_compute_api:os-keypairs:create": + "rule:admin_api or user_id:%(user_id)s", + "os_compute_api:os-keypairs:delete": + "rule:admin_api or user_id:%(user_id)s", "os_compute_api:os-lock-server:lock": "", "os_compute_api:os-lock-server:unlock": "", "os_compute_api:os-lock-server:unlock:unlock_override": "",