From 16e11b0beb46f1d18979ec7d05ce5fc3e526e072 Mon Sep 17 00:00:00 2001 From: Michael Krotscheck Date: Tue, 9 Dec 2014 16:26:32 -0800 Subject: [PATCH] Added cron plugin to clean old access tokens. This cron plugin runs every hour and deletes any access tokens whose expiration date has passed for more than a week. Tests provided. Change-Id: Ief9b25b684364f3ecdf58b2725fd2bb4228a6720 --- setup.cfg | 1 + storyboard/plugin/oauth/__init__.py | 0 storyboard/plugin/oauth/cleaner.py | 59 +++++++++++++ storyboard/tests/plugin/cron/test_manager.py | 9 +- storyboard/tests/plugin/oauth/__init__.py | 0 storyboard/tests/plugin/oauth/test_cleaner.py | 85 +++++++++++++++++++ 6 files changed, 149 insertions(+), 5 deletions(-) create mode 100644 storyboard/plugin/oauth/__init__.py create mode 100644 storyboard/plugin/oauth/cleaner.py create mode 100644 storyboard/tests/plugin/oauth/__init__.py create mode 100644 storyboard/tests/plugin/oauth/test_cleaner.py diff --git a/setup.cfg b/setup.cfg index d348230d..9c79842c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -42,6 +42,7 @@ storyboard.worker.task = storyboard.plugin.user_preferences = storyboard.plugin.cron = cron-management = storyboard.plugin.cron.manager:CronManager + token-cleaner = storyboard.plugin.oauth.cleaner:TokenCleaner [build_sphinx] source-dir = doc/source diff --git a/storyboard/plugin/oauth/__init__.py b/storyboard/plugin/oauth/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/storyboard/plugin/oauth/cleaner.py b/storyboard/plugin/oauth/cleaner.py new file mode 100644 index 00000000..6561fdd7 --- /dev/null +++ b/storyboard/plugin/oauth/cleaner.py @@ -0,0 +1,59 @@ +# Copyright (c) 2014 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. See the License for the specific language governing permissions and +# limitations under the License. + +from datetime import datetime +from datetime import timedelta + +import storyboard.db.api.base as api_base +from storyboard.db.models import AccessToken +from storyboard.plugin.cron.base import CronPluginBase + + +class TokenCleaner(CronPluginBase): + """A Cron Plugin which checks periodically for expired auth tokens and + removes them from the database. By default it only cleans up expired + tokens that are more than a week old, to permit some historical debugging + forensics. + """ + + def enabled(self): + """Indicate whether this plugin is enabled. This indicates whether + this plugin alone is runnable, as opposed to the entire cron system. + """ + return True + + def interval(self): + """This plugin executes on startup, and once every hour after that. + + :return: "? * * * *" + """ + return "? * * * *" + + def run(self, start_time, end_time): + """Remove all oauth tokens that are more than a week old. + + :param start_time: The last time the plugin was run. + :param end_time: The current timestamp. + """ + # Calculate last week. + lastweek = datetime.utcnow() - timedelta(weeks=1) + + # Build the query. + query = api_base.model_query(AccessToken) + + # Apply the filter. + query = query.filter(AccessToken.expires_at < lastweek) + + # Batch delete. + query.delete() diff --git a/storyboard/tests/plugin/cron/test_manager.py b/storyboard/tests/plugin/cron/test_manager.py index 91e775de..6c0ba9c8 100644 --- a/storyboard/tests/plugin/cron/test_manager.py +++ b/storyboard/tests/plugin/cron/test_manager.py @@ -245,8 +245,8 @@ class TestCronManager(base.TestCase): manager = cronmanager.CronManager(CONF, tabfile=self.tabfile) manager.execute() - # We're expecting 1 in-branch plugins. - self.assertCronLength(1, command='storyboard-cron') + # We're expecting 2 in-branch plugins. + self.assertCronLength(2, command='storyboard-cron') def test_execute_update(self): """Test that execute() method updates plugins.""" @@ -311,9 +311,8 @@ class TestCronManager(base.TestCase): manager.execute() # Check a new crontab to see what we find. - cron = crontab.CronTab(tabfile=self.tabfile) self.assertCronLength(0, command=plugin_command) - self.assertCronLength(1, command='storyboard-cron') + self.assertCronLength(2, command='storyboard-cron') # Cleanup after ourselves. manager.remove() @@ -344,7 +343,7 @@ class TestCronManager(base.TestCase): manager.execute() # Check a new crontab to see what we find. - self.assertCronLength(1, command='storyboard-cron') + self.assertCronLength(2, command='storyboard-cron') # Cleanup after ourselves. manager.remove() diff --git a/storyboard/tests/plugin/oauth/__init__.py b/storyboard/tests/plugin/oauth/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/storyboard/tests/plugin/oauth/test_cleaner.py b/storyboard/tests/plugin/oauth/test_cleaner.py new file mode 100644 index 00000000..d757b1a8 --- /dev/null +++ b/storyboard/tests/plugin/oauth/test_cleaner.py @@ -0,0 +1,85 @@ +# Copyright (c) 2014 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. See the License for the specific language governing permissions and +# limitations under the License. + +from datetime import datetime +from datetime import timedelta + +from oslo.config import cfg +import storyboard.db.api.base as db_api +from storyboard.db.models import AccessToken +from storyboard.plugin.oauth.cleaner import TokenCleaner +import storyboard.tests.base as base +from storyboard.tests.mock_data import load_data + + +CONF = cfg.CONF + + +class TestTokenCleaner(base.FunctionalTest): + """Test cases for our OAuth Token cleaner plugin.""" + + def setUp(self): + super(TestTokenCleaner, self).setUp() + + def tearDown(self): + super(TestTokenCleaner, self).tearDown() + + def test_enabled(self): + """This plugin must always be enabled. The only time it's not enabled + is when cron has been disabled. + """ + plugin = TokenCleaner(CONF) + self.assertTrue(plugin.enabled()) + + def test_interval(self): + """Assert that the cron manager runs every 5 minutes.""" + plugin = TokenCleaner(CONF) + self.assertEqual("? * * * *", plugin.interval()) + + def test_token_removal(self): + """Assert that the plugin deletes tokens whose expiration date passed + over a week ago. + """ + + # Start with a clean database. + db_api.model_query(AccessToken).delete() + self.assertEqual(0, db_api.model_query(AccessToken).count()) + + # Build 100 tokens, each one day older than the other, with 24 hour + # expiration dates. I subtract 5 seconds here because the time it + # takes to execute the script may, or may not, result in an + # 8-day-old-token to remain valid. + for i in range(0, 100): + created_at = datetime.utcnow() - timedelta(days=i) + expires_in = (60 * 60 * 24) - 5 # Minus five seconds, see above. + expires_at = created_at + timedelta(seconds=expires_in) + + load_data([ + AccessToken( + user_id=1, + created_at=created_at.strftime('%Y-%m-%d %H:%M:%S'), + expires_in=expires_in, + expires_at=expires_at.strftime('%Y-%m-%d %H:%M:%S'), + access_token='test_token_%s' % (i,)) + ]) + + # Make sure we have 100 tokens. + self.assertEqual(100, db_api.model_query(AccessToken).count()) + + # Run the plugin. + plugin = TokenCleaner(CONF) + plugin.execute() + + # Make sure we have 8 tokens left (since one plugin starts today). + self.assertEqual(8, db_api.model_query(AccessToken).count())