diff options
author | Milo Casagrande <milo.casagrande@linaro.org> | 2014-07-25 16:21:02 +0200 |
---|---|---|
committer | Milo Casagrande <milo.casagrande@linaro.org> | 2014-07-25 16:21:02 +0200 |
commit | 4d9b1da6a04551b3d6c5506822e1810aeea3fb9d (patch) | |
tree | add225a061d58535223dac86371927caed6488d1 /app | |
parent | e47e7d7d840372ed8b8648c458146ffc25557b1f (diff) |
Work on the token model.
* Update the token model, add unit tests.
Change-Id: Id466c02afe084ac076bb6a58f1fd0d672d65aee0
Diffstat (limited to 'app')
-rw-r--r-- | app/models/__init__.py | 7 | ||||
-rw-r--r-- | app/models/tests/test_token_model.py | 105 | ||||
-rw-r--r-- | app/models/token.py | 205 | ||||
-rw-r--r-- | app/tests/__init__.py | 1 |
4 files changed, 312 insertions, 6 deletions
diff --git a/app/models/__init__.py b/app/models/__init__.py index ace1c9c..1d9e197 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -30,8 +30,11 @@ DEFCONFIG_KEY = 'defconfig' DIRNAME_KEY = 'dirname' DTB_ADDR_KEY = 'dtb_addr' DTB_KEY = 'dtb' +EMAIL_KEY = 'email' ENDIANNESS_KEY = 'endian' ERRORS_KEY = 'errors' +EXPIRED_KEY = 'expired' +EXPIRES_KEY = 'expires_on' FIELD_KEY = 'field' GIT_BRANCH_KEY = 'git_branch' GIT_COMMIT_KEY = 'git_commit' @@ -39,6 +42,7 @@ GIT_DESCRIBE_KEY = 'git_describe' GIT_URL_KEY = 'git_url' ID_KEY = '_id' INITRD_ADDR_KEY = 'initrd_addr' +IP_ADDRESS_KEY = 'ip_address' JOB_ID_KEY = 'job_id' JOB_KEY = 'job' KERNEL_IMAGE_KEY = 'kernel_image' @@ -48,12 +52,15 @@ LOAD_ADDR_KEY = 'load_addr' METADATA_KEY = 'metadata' NOT_FIELD_KEY = 'nfield' PRIVATE_KEY = 'private' +PROPERTIES_KEY = 'properties' SKIP_KEY = 'skip' SORT_KEY = 'sort' SORT_ORDER_KEY = 'sort_order' STATUS_KEY = 'status' TIME_KEY = 'time' +TOKEN_KEY = 'token' UPDATED_KEY = 'updated' +USERNAME_KEY = 'username' WARNINGS_KEY = 'warnings' # Job and/or build status. diff --git a/app/models/tests/test_token_model.py b/app/models/tests/test_token_model.py new file mode 100644 index 0000000..e8a7cf5 --- /dev/null +++ b/app/models/tests/test_token_model.py @@ -0,0 +1,105 @@ +# Copyright (C) 2014 Linaro Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import unittest + +from datetime import date + +from models.base import BaseDocument +from models.token import Token + +from types import DictionaryType + + +class TestTokenModel(unittest.TestCase): + + def test_token_model_not_base_document(self): + token_obj = Token() + self.assertNotIsInstance(token_obj, BaseDocument) + + def test_properties_len(self): + token_obj = Token() + self.assertEqual(len(token_obj.properties), 16) + + def test_not_expired(self): + token_obj = Token() + self.assertFalse(token_obj.expired) + + def test_token_not_none(self): + token_obj = Token() + self.assertIsNotNone(token_obj.token) + + def test_unique_token(self): + token_obj1 = Token() + token_obj2 = Token() + + self.assertNotEqual(token_obj1.token, token_obj2.token) + + def test_token_creation_date(self): + token_obj = Token() + today = date.today() + + self.assertIsInstance(token_obj.created_on, date) + self.assertEqual(today, token_obj.created_on) + + def test_token_to_dict_is_dict(self): + token_obj = Token() + + self.assertIsInstance(token_obj.to_dict(), DictionaryType) + + def test_token_is_admin(self): + token_obj = Token() + token_obj.is_admin = 1 + + self.assertEqual(token_obj.is_admin, 1) + self.assertEqual(token_obj.can_create_token, 1) + self.assertEqual(token_obj.is_superuser, 1) + self.assertEqual(token_obj.is_get_token, 1) + self.assertEqual(token_obj.is_delete_token, 1) + self.assertEqual(token_obj.is_post_token, 1) + + def test_token_is_superuser(self): + token_obj = Token() + token_obj.is_superuser = 1 + + self.assertEqual(token_obj.is_admin, 0) + self.assertEqual(token_obj.can_create_token, 0) + self.assertEqual(token_obj.is_superuser, 1) + self.assertEqual(token_obj.is_get_token, 1) + self.assertEqual(token_obj.is_delete_token, 1) + self.assertEqual(token_obj.is_post_token, 1) + + def test_token_wrong_numeric_value(self): + token_obj = Token() + + def _call_setter(value): + token_obj.is_admin = value + + self.assertRaises(ValueError, _call_setter, 2) + + def test_token_wrong_type(self): + token_obj = Token() + + def _call_setter(value): + token_obj.is_admin = value + + self.assertRaises(TypeError, _call_setter, "1") + + def test_token_with_boolean(self): + token_obj = Token() + token_obj.is_admin = True + + self.assertTrue(token_obj.is_admin) + self.assertEqual(token_obj.is_admin, 1) diff --git a/app/models/token.py b/app/models/token.py index 20931b7..c7c55a5 100644 --- a/app/models/token.py +++ b/app/models/token.py @@ -13,21 +13,64 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -"""The API token class to store token in the DB.""" +"""The API token model to store token in the DB.""" TOKEN_COLLECTION = 'api-token' +from datetime import date +from types import ( + IntType, + BooleanType, +) +from uuid import uuid4 -class APIToken(object): +from models import ( + CREATED_KEY, + EMAIL_KEY, + EXPIRED_KEY, + EXPIRES_KEY, + IP_ADDRESS_KEY, + PROPERTIES_KEY, + TOKEN_KEY, + USERNAME_KEY, +) + + +class Token(object): + """This is an API token as stored on the DB. + + A token can be: + - basic: All permissions have to be set (GET, POST, DELETE) + - superuser: All methods are permitted, but cannot be used to create new + tokens + - admin: All operations permitted, can create new tokens. + + A token can be restricted to be used based on an IP address. + + Each token object has a properity called `properties`, a list of integers + (0, 1) that describe the token. When the object is initialized, the list + is all set to 0, and it has a default length of 16. Not all slots are used + and will be left for future expansion. + + The property lists (index - property description) is as follows: + - 0: if the token is an admin token + - 1: if the token is a superuser token + - 2: if the token can perform GET + - 3: if the token can perfrom POST + - 4: if the token can perform DELETE + - 5: if the token is IP restricted + - 6: if the token can create new tokens + """ def __init__(self): - self._token = None - self._created = None - self._expires = None + self._token = str(uuid4()) + self._created_on = date.today() + self._expires_on = None self._expired = False self._username = None self._email = None - self._properties = [] + self._ip_address = None + self._properties = [0 for _ in range(0, 16)] @property def token(self): @@ -36,3 +79,153 @@ class APIToken(object): @token.setter def token(self, value): self._token = value + + @property + def properties(self): + return self._properties + + @property + def created_on(self): + return self._created_on + + @property + def expires_on(self): + return self._expires_on + + @expires_on.setter + def expires_on(self, value): + self._expires_on = value + + @property + def ip_address(self): + return self._ip_address + + @ip_address.setter + def ip_address(self, value): + # TODO: need IP address checking logic + # use netaddr (0.7.2) for py 2.7, py >3.3 is part of the stdlib + self._ip_address = value + + @property + def expired(self): + return self._expired + + @expired.setter + def expired(self, value): + self._expired = value + + @property + def is_admin(self): + return self._properties[0] + + @is_admin.setter + def is_admin(self, value): + value = self._check_properties_value(value) + + self._properties[0] = value + # Admin tokens can GET, POST and DELETE, are superuser and can create + # new tokens. + self._properties[1] = value + self._properties[2] = value + self._properties[3] = value + self._properties[4] = value + self._properties[6] = value + + @property + def is_superuser(self): + return self._properties[1] + + @is_superuser.setter + def is_superuser(self, value): + value = self._check_properties_value(value) + + # Force admin to zero, and also if can create new tokens, regardless + # of what is passed. A super user cannot create new tokens. + self._properties[0] = 0 + self._properties[6] = 0 + + self._properties[1] = value + self._properties[2] = value + self._properties[3] = value + self._properties[4] = value + + @property + def is_get_token(self): + return self._properties[2] + + @is_get_token.setter + def is_get_token(self, value): + value = self._check_properties_value(value) + self._properties[2] = value + + @property + def is_post_token(self): + return self._properties[3] + + @is_post_token.setter + def is_post_token(self, value): + value = self._check_properties_value(value) + self._properties[3] = value + + @property + def is_delete_token(self): + return self._properties[4] + + @is_delete_token.setter + def is_delete_token(self, value): + value = self._check_properties_value(value) + self._properties[4] = value + + @property + def is_ip_restricted(self): + return self._properties[5] + + @is_ip_restricted.setter + def is_ip_restricted(self, value): + value = self._check_properties_value(value) + self._properties[5] = value + + @property + def can_create_token(self): + return self._properties[6] + + @can_create_token.setter + def can_create_token(self, value): + value = self._check_properties_value(value) + self._properties[6] = value + + @staticmethod + def _check_properties_value(value): + """Make sure the value passed for the properties list is valid. + + A properties value must be an integer or a boolean, either 0 or 1. + + :param value: The value to check. + :return The value converted into an int. + :raise TypeError if the value is not IntType or BooleanType; ValueError + if it is > 1. + """ + if not isinstance(value, (IntType, BooleanType)): + raise TypeError("Wrong value passed, must be int or bool") + + value = abs(int(value)) + if not (value == 0 or value == 1): + raise ValueError("Value must be 0 or 1") + + return value + + def to_dict(self): + """Return a dictionary view of the object. + + :return The object as a dictionary. + """ + return { + CREATED_KEY: self._created_on, + EMAIL_KEY: self._email, + EXPIRED_KEY: self._expired, + EXPIRES_KEY: self._expires_on, + IP_ADDRESS_KEY: self._ip_address, + PROPERTIES_KEY: self._properties, + TOKEN_KEY: self._token, + USERNAME_KEY: self._username, + } diff --git a/app/tests/__init__.py b/app/tests/__init__.py index d88f731..c505fa8 100644 --- a/app/tests/__init__.py +++ b/app/tests/__init__.py @@ -27,6 +27,7 @@ def test_modules(): 'handlers.tests.test_subscription_handler', 'models.tests.test_boot_model', 'models.tests.test_models', + 'models.tests.test_token_model', 'utils.tests.test_bootimport', 'utils.tests.test_docimport', 'utils.tests.test_meta_parser', |