From 1b8a2e0a928b142b453dd76dc1afed0b26ee6eae Mon Sep 17 00:00:00 2001 From: Vladik Romanovsky Date: Mon, 20 Apr 2015 13:21:37 -0700 Subject: [PATCH] Adding user_id handling to keypair index, show and create api calls Administering an openstack cluster I found the need to see what keypairs a user had. I found this bug that was requesting the same thing and decided to implement it. This is the update to the api server to handle a query param passed to return a keypair list for a specific user-id. Only a user with admin privileges is allowed to make this call. Allowing the administrators to be able to list and get details of keypairs which owned by users other than themselves, as well as creating new and deleting keypairs on behalf of their users. DocImpact: This adds API microversion Implements blueprint admin-query-any-keypair APIImpact UpgradeImpact: Policy rules of the index, create, delete and show operations has been updated to support the change. os_compute_api:os-keypairs:{index, show, create, delete}: "rule:admin_api or user_id:%(user_id)s Co-Authored-By: Dan Smith Co-Authored-By: Dan Radez Closes-Bug: #1182965 Change-Id: I45846f770628e8f24a8c137dcdc46baa64c50801 --- .../versions/versions-get-resp.json | 2 +- .../keypairs/v2.10/keypairs-get-resp.json | 14 +++ .../v2.10/keypairs-import-post-req.json | 8 ++ .../v2.10/keypairs-import-post-resp.json | 9 ++ .../keypairs/v2.10/keypairs-list-resp.json | 12 +++ .../keypairs/v2.10/keypairs-post-req.json | 7 ++ .../keypairs/v2.10/keypairs-post-resp.json | 10 ++ etc/nova/policy.json | 8 +- nova/api/openstack/api_version_request.py | 4 +- .../openstack/compute/plugins/v3/keypairs.py | 94 ++++++++++++---- .../openstack/compute/schemas/v3/keypairs.py | 22 ++++ .../openstack/rest_api_version_history.rst | 10 ++ .../versions/versions-get-resp.json.tpl | 2 +- .../tests/functional/api_samples_test_base.py | 1 + .../keypairs/v2.10/keypairs-get-resp.json.tpl | 14 +++ .../v2.10/keypairs-import-post-req.json.tpl | 8 ++ .../v2.10/keypairs-import-post-resp.json.tpl | 9 ++ .../v2.10/keypairs-list-resp.json.tpl | 12 +++ .../keypairs/v2.10/keypairs-post-req.json.tpl | 7 ++ .../v2.10/keypairs-post-resp.json.tpl | 10 ++ nova/tests/functional/v3/test_keypairs.py | 63 +++++++++++ .../compute/contrib/test_keypairs.py | 100 ++++++++++++++++++ .../api/openstack/compute/test_versions.py | 4 +- nova/tests/unit/fake_policy.py | 12 ++- 24 files changed, 411 insertions(+), 31 deletions(-) create mode 100644 doc/v3/api_samples/keypairs/v2.10/keypairs-get-resp.json create mode 100644 doc/v3/api_samples/keypairs/v2.10/keypairs-import-post-req.json create mode 100644 doc/v3/api_samples/keypairs/v2.10/keypairs-import-post-resp.json create mode 100644 doc/v3/api_samples/keypairs/v2.10/keypairs-list-resp.json create mode 100644 doc/v3/api_samples/keypairs/v2.10/keypairs-post-req.json create mode 100644 doc/v3/api_samples/keypairs/v2.10/keypairs-post-resp.json create mode 100644 nova/tests/functional/v3/api_samples/keypairs/v2.10/keypairs-get-resp.json.tpl create mode 100644 nova/tests/functional/v3/api_samples/keypairs/v2.10/keypairs-import-post-req.json.tpl create mode 100644 nova/tests/functional/v3/api_samples/keypairs/v2.10/keypairs-import-post-resp.json.tpl create mode 100644 nova/tests/functional/v3/api_samples/keypairs/v2.10/keypairs-list-resp.json.tpl create mode 100644 nova/tests/functional/v3/api_samples/keypairs/v2.10/keypairs-post-req.json.tpl create mode 100644 nova/tests/functional/v3/api_samples/keypairs/v2.10/keypairs-post-resp.json.tpl diff --git a/doc/api_samples/versions/versions-get-resp.json b/doc/api_samples/versions/versions-get-resp.json index e6a31ff00458..f32b72c5136a 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 000000000000..2970994fadf2 --- /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 000000000000..fb18f269c9d6 --- /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 000000000000..ca1a70b334c1 --- /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 000000000000..3d5fe045a023 --- /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 000000000000..005a3f504530 --- /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 000000000000..394960868bf1 --- /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 338b7d6e24f1..a644b203b97d 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 6e2db74213fb..c8d60c1a2d7b 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 f41fc29eec89..64d40dcf1f41 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 c31192376bb0..f8a057569207 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 0961e1e5c498..8ea86f340f9f 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 e6a31ff00458..f32b72c5136a 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 e78c103dc3aa..fcf3c6057b9f 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 000000000000..d18ae163519d --- /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 000000000000..03e60c013312 --- /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 000000000000..30d3fa969daf --- /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 000000000000..8e0963bc7a39 --- /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 000000000000..f6a6d47b56ec --- /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 000000000000..ee5eb23f77dd --- /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 98216287fd31..838b6738de44 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 4f0dfec37fbf..9c47b8f7b4c8 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 0103882afa09..86229c043a9c 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 ed5de175e850..be87db5816d2 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": "",