From: Tobias Urdin Date: Sun, 26 Jun 2022 23:14:21 +0000 (+0000) Subject: rgw/qa: Add QA suite for Keystone service token X-Git-Tag: v17.2.8~589^2 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=a4618b1b2752f01ee88fda212d49b298f12d62f9;p=ceph.git rgw/qa: Add QA suite for Keystone service token This adds a QA suite for the service token auth in RGW. The suite runs a workunit that is a bash script that spawns a fake Keystone server which is then used as an auth backend for RGW to test the feature. A python script is then executed that runs a battery of tests against RadosGW which talks to the fake Keystone server running in the background. Signed-off-by: Tobias Urdin (cherry picked from commit 567024d363e1474d4a836965ae5241541a54fc67) --- diff --git a/qa/suites/rgw/service-token/% b/qa/suites/rgw/service-token/% new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/qa/suites/rgw/service-token/.qa b/qa/suites/rgw/service-token/.qa new file mode 120000 index 000000000000..a602a0353e75 --- /dev/null +++ b/qa/suites/rgw/service-token/.qa @@ -0,0 +1 @@ +../.qa/ \ No newline at end of file diff --git a/qa/suites/rgw/service-token/clusters/.qa b/qa/suites/rgw/service-token/clusters/.qa new file mode 120000 index 000000000000..a602a0353e75 --- /dev/null +++ b/qa/suites/rgw/service-token/clusters/.qa @@ -0,0 +1 @@ +../.qa/ \ No newline at end of file diff --git a/qa/suites/rgw/service-token/clusters/fixed-1.yaml b/qa/suites/rgw/service-token/clusters/fixed-1.yaml new file mode 120000 index 000000000000..02df5dd0cd04 --- /dev/null +++ b/qa/suites/rgw/service-token/clusters/fixed-1.yaml @@ -0,0 +1 @@ +.qa/clusters/fixed-1.yaml \ No newline at end of file diff --git a/qa/suites/rgw/service-token/frontend b/qa/suites/rgw/service-token/frontend new file mode 120000 index 000000000000..926a53e83834 --- /dev/null +++ b/qa/suites/rgw/service-token/frontend @@ -0,0 +1 @@ +.qa/rgw_frontend \ No newline at end of file diff --git a/qa/suites/rgw/service-token/overrides.yaml b/qa/suites/rgw/service-token/overrides.yaml new file mode 100644 index 000000000000..c727ec3fdd3a --- /dev/null +++ b/qa/suites/rgw/service-token/overrides.yaml @@ -0,0 +1,22 @@ +overrides: + ceph: + conf: + client: + setuser: ceph + setgroup: ceph + debug rgw: 20 + rgw keystone api version: 3 + rgw keystone url: http://localhost:5000 + rgw keystone accepted roles: admin,Member + rgw keystone implicit tenants: true + rgw keystone accepted admin roles: admin + rgw swift enforce content length: true + rgw swift account in url: true + rgw swift versioning enabled: true + rgw keystone admin domain: Default + rgw keystone admin user: admin + rgw keystone admin password: ADMIN + rgw keystone admin project: admin + rgw keystone service token enabled: true + rgw keystone service token accepted roles: admin + rgw keystone expired token cache expiration: 10 diff --git a/qa/suites/rgw/service-token/tasks/.qa b/qa/suites/rgw/service-token/tasks/.qa new file mode 120000 index 000000000000..a602a0353e75 --- /dev/null +++ b/qa/suites/rgw/service-token/tasks/.qa @@ -0,0 +1 @@ +../.qa/ \ No newline at end of file diff --git a/qa/suites/rgw/service-token/tasks/service-token.yaml b/qa/suites/rgw/service-token/tasks/service-token.yaml new file mode 100644 index 000000000000..8aef1985b24c --- /dev/null +++ b/qa/suites/rgw/service-token/tasks/service-token.yaml @@ -0,0 +1,11 @@ +tasks: +- install: +- ceph: +- rgw: + client.0: + port: 8000 +- workunit: + basedir: qa/workunits/rgw + clients: + client.0: + - keystone-service-token.sh diff --git a/qa/suites/rgw/service-token/ubuntu_latest.yaml b/qa/suites/rgw/service-token/ubuntu_latest.yaml new file mode 120000 index 000000000000..3a09f9abb05c --- /dev/null +++ b/qa/suites/rgw/service-token/ubuntu_latest.yaml @@ -0,0 +1 @@ +.qa/distros/supported/ubuntu_latest.yaml \ No newline at end of file diff --git a/qa/workunits/rgw/keystone-fake-server.py b/qa/workunits/rgw/keystone-fake-server.py new file mode 100755 index 000000000000..c05ad7bfdcc7 --- /dev/null +++ b/qa/workunits/rgw/keystone-fake-server.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 Binero +# +# Author: Tobias Urdin +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Library Public License as published by +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Library Public License for more details. + +from datetime import datetime, timedelta +import logging +import json +from http.server import BaseHTTPRequestHandler, HTTPServer + + +DEFAULT_DOMAIN = { + 'id': 'default', + 'name': 'Default', +} + + +PROJECTS = { + 'admin': { + 'domain': DEFAULT_DOMAIN, + 'id': 'a6944d763bf64ee6a275f1263fae0352', + 'name': 'admin', + }, + 'deadbeef': { + 'domain': DEFAULT_DOMAIN, + 'id': 'b4221c214dd64ee6a464g2153fae3813', + 'name': 'deadbeef', + }, +} + + +USERS = { + 'admin': { + 'domain': DEFAULT_DOMAIN, + 'id': '51cc68287d524c759f47c811e6463340', + 'name': 'admin', + }, + 'deadbeef': { + 'domain': DEFAULT_DOMAIN, + 'id': '99gg485738df758349jf8d848g774392', + 'name': 'deadbeef', + }, +} + + +USERROLES = { + 'admin': [ + { + 'id': '51cc68287d524c759f47c811e6463340', + 'name': 'admin', + } + ], + 'deadbeef': [ + { + 'id': '98bd32184f854f393a72b932g5334124', + 'name': 'Member', + } + ], +} + + +TOKENS = { + 'admin-token-1': { + 'username': 'admin', + 'project': 'admin', + 'expired': False, + }, + 'user-token-1': { + 'username': 'deadbeef', + 'project': 'deadbeef', + 'expired': False, + }, + 'user-token-2': { + 'username': 'deadbeef', + 'project': 'deadbeef', + 'expired': True, + }, +} + + +def _generate_token_result(username, project, expired=False): + userdata = USERS[username] + projectdata = PROJECTS[project] + userroles = USERROLES[username] + + if expired: + then = datetime.now() - timedelta(hours=2) + issued_at = then.strftime('%Y-%m-%dT%H:%M:%SZ') + expires_at = (then + timedelta(hours=1)).strftime('%Y-%m-%dT%H:%M:%SZ') + else: + now = datetime.now() + issued_at = now.strftime('%Y-%m-%dT%H:%M:%SZ') + expires_at = (now + timedelta(seconds=10)).strftime('%Y-%m-%dT%H:%M:%SZ') + + result = { + 'token': { + 'audit_ids': ['3T2dc1CGQxyJsHdDu1xkcw'], + 'catalog': [], + 'expires_at': expires_at, + 'is_domain': False, + 'issued_at': issued_at, + 'methods': ['password'], + 'project': projectdata, + 'roles': userroles, + 'user': userdata, + } + } + + return result + + +COUNTERS = { + 'get_total': 0, + 'post_total': 0, +} + + +class HTTPRequestHandler(BaseHTTPRequestHandler): + def do_GET(self): + # This is not part of the Keystone API + if self.path == '/stats': + self._handle_stats() + return + + if str(self.path).startswith('/v3/auth/tokens'): + self._handle_get_auth() + else: + self.send_response(403) + self.end_headers() + + def do_POST(self): + if self.path == '/v3/auth/tokens': + self._handle_post_auth() + else: + self.send_response(400) + self.end_headers() + + def _get_data(self): + length = int(self.headers.get('content-length')) + data = self.rfile.read(length).decode('utf8') + return json.loads(data) + + def _set_data(self, data): + jdata = json.dumps(data) + self.wfile.write(jdata.encode('utf8')) + + def _handle_stats(self): + self.send_response(200) + self.end_headers() + self._set_data(COUNTERS) + + def _handle_get_auth(self): + logging.info('Increasing get_total counter from %d -> %d' % (COUNTERS['get_total'], COUNTERS['get_total']+1)) + COUNTERS['get_total'] += 1 + auth_token = self.headers.get('X-Subject-Token', None) + if auth_token and auth_token in TOKENS: + tokendata = TOKENS[auth_token] + if tokendata['expired'] and 'allow_expired=1' not in self.path: + self.send_response(404) + self.end_headers() + else: + self.send_response(200) + self.send_header('Content-Type', 'application/json') + self.end_headers() + result = _generate_token_result(tokendata['username'], tokendata['project'], tokendata['expired']) + self._set_data(result) + else: + self.send_response(404) + self.end_headers() + + def _handle_post_auth(self): + logging.info('Increasing post_total counter from %d -> %d' % (COUNTERS['post_total'], COUNTERS['post_total']+1)) + COUNTERS['post_total'] += 1 + data = self._get_data() + user = data['auth']['identity']['password']['user'] + if user['name'] == 'admin' and user['password'] == 'ADMIN': + self.send_response(201) + self.send_header('Content-Type', 'application/json') + self.send_header('X-Subject-Token', 'admin-token-1') + self.end_headers() + tokendata = TOKENS['admin-token-1'] + result = _generate_token_result(tokendata['username'], tokendata['project'], tokendata['expired']) + self._set_data(result) + else: + self.send_response(401) + self.end_headers() + + +def main(): + logging.basicConfig(level=logging.DEBUG) + logging.info('Starting keystone-fake-server') + server = HTTPServer(('localhost', 5000), HTTPRequestHandler) + server.serve_forever() + + +if __name__ == '__main__': + main() diff --git a/qa/workunits/rgw/keystone-service-token.sh b/qa/workunits/rgw/keystone-service-token.sh new file mode 100755 index 000000000000..fc39731ca951 --- /dev/null +++ b/qa/workunits/rgw/keystone-service-token.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# +# Copyright (C) 2022 Binero +# +# Author: Tobias Urdin +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Library Public License as published by +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Library Public License for more details. + +source $CEPH_ROOT/qa/standalone/ceph-helpers.sh + +trap cleanup EXIT + +function cleanup() { + kill $KEYSTONE_FAKE_SERVER_PID + wait +} + +function run() { + $CEPH_ROOT/qa/workunits/rgw//keystone-fake-server.py & + KEYSTONE_FAKE_SERVER_PID=$! + # Give fake Keystone server some seconds to startup + sleep 5 + $CEPH_ROOT/qa/workunits/rgw/test-keystone-service-token.py +} + +main keystone-service-token "$@" diff --git a/qa/workunits/rgw/test-keystone-service-token.py b/qa/workunits/rgw/test-keystone-service-token.py new file mode 100755 index 000000000000..2c7f21e93992 --- /dev/null +++ b/qa/workunits/rgw/test-keystone-service-token.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 Binero +# +# Author: Tobias Urdin +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Library Public License as published by +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Library Public License for more details. + +import sys +import requests +import time + + +# b4221c214dd64ee6a464g2153fae3813 is ID of deadbeef project +SWIFT_URL = 'http://localhost:8000/swift/v1/AUTH_b4221c214dd64ee6a464g2153fae3813' +KEYSTONE_URL = 'http://localhost:5000' + + +def get_stats(): + stats_url = '%s/stats' % KEYSTONE_URL + return requests.get(stats_url) + + +def test_list_containers(): + # Loop five list container requests with same token + for i in range(0, 5): + r = requests.get(SWIFT_URL, headers={'X-Auth-Token': 'user-token-1'}) + if r.status_code != 204: + print('FAILED, status code is %d not 204' % r.status_code) + sys.exit(1) + + # Get stats from fake Keystone server + r = get_stats() + if r.status_code != 200: + print('FAILED, status code is %d not 200' % r.status_code) + sys.exit(1) + stats = r.json() + + # Verify admin token was cached + if stats['post_total'] != 1: + print('FAILED, post_total stat is %d not 1' % stats['post_total']) + sys.exit(1) + + # Verify user token was cached + if stats['get_total'] != 1: + print('FAILED, get_total stat is %d not 1' % stats['get_total']) + sys.exit(1) + + print('Wait for cache to be invalid') + time.sleep(11) + + r = requests.get(SWIFT_URL, headers={'X-Auth-Token': 'user-token-1'}) + if r.status_code != 204: + print('FAILED, status code is %d not 204' % r.status_code) + sys.exit(1) + + # Get stats from fake Keystone server + r = get_stats() + if r.status_code != 200: + print('FAILED, status code is %d not 200' % r.status_code) + sys.exit(1) + stats = r.json() + + if stats['post_total'] != 2: + print('FAILED, post_total stat is %d not 2' % stats['post_total']) + sys.exit(1) + + if stats['get_total'] != 2: + print('FAILED, get_total stat is %d not 2' % stats['get_total']) + sys.exit(1) + + +def test_expired_token(): + # Try listing containers with an expired token + for i in range(0, 3): + r = requests.get(SWIFT_URL, headers={'X-Auth-Token': 'user-token-2'}) + if r.status_code != 401: + print('FAILED, status code is %d not 401' % r.status_code) + sys.exit(1) + + # Get stats from fake Keystone server + r = get_stats() + if r.status_code != 200: + print('FAILED, status code is %d not 200' % r.status_code) + sys.exit(1) + stats = r.json() + + # Verify admin token was cached + if stats['post_total'] != 2: + print('FAILED, post_total stat is %d not 2' % stats['post_total']) + sys.exit(1) + + # Verify we got to fake Keystone server since expired tokens is not cached + if stats['get_total'] != 5: + print('FAILED, get_total stat is %d not 5' % stats['get_total']) + sys.exit(1) + + +def test_expired_token_with_service_token(): + # Try listing containers with an expired token but with a service token + for i in range(0, 3): + r = requests.get(SWIFT_URL, headers={'X-Auth-Token': 'user-token-2', 'X-Service-Token': 'admin-token-1'}) + if r.status_code != 204: + print('FAILED, status code is %d not 204' % r.status_code) + sys.exit(1) + + # Get stats from fake Keystone server + r = get_stats() + if r.status_code != 200: + print('FAILED, status code is %d not 200' % r.status_code) + sys.exit(1) + stats = r.json() + + # Verify admin token was cached + if stats['post_total'] != 2: + print('FAILED, post_total stat is %d not 2' % stats['post_total']) + sys.exit(1) + + # Verify we got to fake Keystone server since expired tokens is not cached + if stats['get_total'] != 7: + print('FAILED, get_total stat is %d not 7' % stats['get_total']) + sys.exit(1) + + print('Wait for cache to be invalid') + time.sleep(11) + + r = requests.get(SWIFT_URL, headers={'X-Auth-Token': 'user-token-2', 'X-Service-Token': 'admin-token-1'}) + if r.status_code != 204: + print('FAILED, status code is %d not 204' % r.status_code) + sys.exit(1) + + # Get stats from fake Keystone server + r = get_stats() + if r.status_code != 200: + print('FAILED, status code is %d not 200' % r.status_code) + sys.exit(1) + stats = r.json() + + if stats['post_total'] != 3: + print('FAILED, post_total stat is %d not 3' % stats['post_total']) + sys.exit(1) + + if stats['get_total'] != 9: + print('FAILED, get_total stat is %d not 9' % stats['get_total']) + sys.exit(1) + + +def test_expired_token_with_invalid_service_token(): + print('Wait for cache to be invalid') + time.sleep(11) + + # Test with a token that doesn't have allowed role as service token + for i in range(0, 3): + r = requests.get(SWIFT_URL, headers={'X-Auth-Token': 'user-token-2', 'X-Service-Token': 'user-token-1'}) + if r.status_code != 401: + print('FAILED, status code is %d not 401' % r.status_code) + sys.exit(1) + + # Make sure we get user-token-1 cached + r = requests.get(SWIFT_URL, headers={'X-Auth-Token': 'user-token-1'}) + if r.status_code != 204: + print('FAILED, status code is %d not 204' % r.status_code) + sys.exit(1) + + # Test that a cached token (that is invalid as service token) cannot be used as service token + for i in range(0, 3): + r = requests.get(SWIFT_URL, headers={'X-Auth-Token': 'user-token-2', 'X-Service-Token': 'user-token-1'}) + if r.status_code != 401: + print('FAILED, status code is %d not 401' % r.status_code) + sys.exit(1) + + +def main(): + test_list_containers() + test_expired_token() + test_expired_token_with_service_token() + test_expired_token_with_invalid_service_token() + + +if __name__ == '__main__': + main()