]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
rgw/qa: Add QA suite for Keystone service token 54445/head
authorTobias Urdin <tobias.urdin@binero.se>
Sun, 26 Jun 2022 23:14:21 +0000 (23:14 +0000)
committerTobias Urdin <tobias.urdin@binero.se>
Thu, 9 Nov 2023 23:13:50 +0000 (23:13 +0000)
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 <tobias.urdin@binero.com>
(cherry picked from commit 567024d363e1474d4a836965ae5241541a54fc67)

12 files changed:
qa/suites/rgw/service-token/% [new file with mode: 0644]
qa/suites/rgw/service-token/.qa [new symlink]
qa/suites/rgw/service-token/clusters/.qa [new symlink]
qa/suites/rgw/service-token/clusters/fixed-1.yaml [new symlink]
qa/suites/rgw/service-token/frontend [new symlink]
qa/suites/rgw/service-token/overrides.yaml [new file with mode: 0644]
qa/suites/rgw/service-token/tasks/.qa [new symlink]
qa/suites/rgw/service-token/tasks/service-token.yaml [new file with mode: 0644]
qa/suites/rgw/service-token/ubuntu_latest.yaml [new symlink]
qa/workunits/rgw/keystone-fake-server.py [new file with mode: 0755]
qa/workunits/rgw/keystone-service-token.sh [new file with mode: 0755]
qa/workunits/rgw/test-keystone-service-token.py [new file with mode: 0755]

diff --git a/qa/suites/rgw/service-token/% b/qa/suites/rgw/service-token/%
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/qa/suites/rgw/service-token/.qa b/qa/suites/rgw/service-token/.qa
new file mode 120000 (symlink)
index 0000000..a602a03
--- /dev/null
@@ -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 (symlink)
index 0000000..a602a03
--- /dev/null
@@ -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 (symlink)
index 0000000..02df5dd
--- /dev/null
@@ -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 (symlink)
index 0000000..926a53e
--- /dev/null
@@ -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 (file)
index 0000000..c727ec3
--- /dev/null
@@ -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 (symlink)
index 0000000..a602a03
--- /dev/null
@@ -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 (file)
index 0000000..8aef198
--- /dev/null
@@ -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 (symlink)
index 0000000..3a09f9a
--- /dev/null
@@ -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 (executable)
index 0000000..c05ad7b
--- /dev/null
@@ -0,0 +1,208 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022 Binero
+#
+# Author: Tobias Urdin <tobias.urdin@binero.com>
+#
+# 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 (executable)
index 0000000..fc39731
--- /dev/null
@@ -0,0 +1,34 @@
+#!/usr/bin/env bash
+#
+# Copyright (C) 2022 Binero
+#
+# Author: Tobias Urdin <tobias.urdin@binero.com>
+#
+# 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 (executable)
index 0000000..2c7f21e
--- /dev/null
@@ -0,0 +1,189 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2022 Binero
+#
+# Author: Tobias Urdin <tobias.urdin@binero.com>
+#
+# 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()