aboutsummaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorMilo Casagrande <milo.casagrande@linaro.org>2014-07-25 16:21:02 +0200
committerMilo Casagrande <milo.casagrande@linaro.org>2014-07-25 16:21:02 +0200
commit4d9b1da6a04551b3d6c5506822e1810aeea3fb9d (patch)
treeadd225a061d58535223dac86371927caed6488d1 /app
parente47e7d7d840372ed8b8648c458146ffc25557b1f (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__.py7
-rw-r--r--app/models/tests/test_token_model.py105
-rw-r--r--app/models/token.py205
-rw-r--r--app/tests/__init__.py1
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',