diff --git a/changelog.d/3268.added.md b/changelog.d/3268.added.md new file mode 100644 index 0000000000..0084758571 --- /dev/null +++ b/changelog.d/3268.added.md @@ -0,0 +1 @@ +Add database model for JWT refresh tokens diff --git a/python/nav/models/api.py b/python/nav/models/api.py index d3ac6213c9..bfc2739a70 100644 --- a/python/nav/models/api.py +++ b/python/nav/models/api.py @@ -66,3 +66,25 @@ def get_absolute_url(self): class Meta(object): db_table = 'apitoken' + + +class JWTRefreshToken(models.Model): + + name = VarcharField(unique=True) + description = models.TextField(null=True, blank=True) + expires = models.DateTimeField() + activates = models.DateTimeField() + hash = VarcharField() + + def __str__(self): + return self.name + + def is_active(self) -> bool: + """True if token is active. A token is considered active when + `activates` is in the past and `expires` is in the future. + """ + now = datetime.now() + return now >= self.activates and now < self.expires + + class Meta(object): + db_table = 'jwtrefreshtoken' diff --git a/python/nav/models/sql/changes/sc.05.13.0001.sql b/python/nav/models/sql/changes/sc.05.13.0001.sql new file mode 100644 index 0000000000..ae6cfa972d --- /dev/null +++ b/python/nav/models/sql/changes/sc.05.13.0001.sql @@ -0,0 +1,8 @@ +CREATE TABLE manage.JWTRefreshToken ( + id SERIAL PRIMARY KEY, + name VARCHAR NOT NULL UNIQUE, + description VARCHAR, + expires TIMESTAMP NOT NULL, + activates TIMESTAMP NOT NULL, + hash VARCHAR NOT NULL +); diff --git a/tests/unittests/models/jwtrefreshtoken_test.py b/tests/unittests/models/jwtrefreshtoken_test.py new file mode 100644 index 0000000000..d6602b6a98 --- /dev/null +++ b/tests/unittests/models/jwtrefreshtoken_test.py @@ -0,0 +1,58 @@ +from datetime import datetime, timedelta + +from nav.models.api import JWTRefreshToken + + +class TestIsActive: + def test_should_return_false_if_token_activates_in_the_future(self): + now = datetime.now() + token = JWTRefreshToken( + name="testtoken", + hash="dummyhash", + expires=now + timedelta(hours=1), + activates=now + timedelta(hours=1), + ) + assert not token.is_active() + + def test_should_return_false_if_token_expires_in_the_past(self): + now = datetime.now() + token = JWTRefreshToken( + name="testtoken", + hash="dummyhash", + expires=now - timedelta(hours=1), + activates=now - timedelta(hours=1), + ) + assert not token.is_active() + + def test_should_return_true_if_token_activates_in_the_past_and_expires_in_the_future( + self, + ): + now = datetime.now() + token = JWTRefreshToken( + name="testtoken", + hash="dummyhash", + expires=now + timedelta(hours=1), + activates=now - timedelta(hours=1), + ) + assert token.is_active() + + def test_should_return_true_if_token_activates_now_and_expires_in_the_future(self): + now = datetime.now() + token = JWTRefreshToken( + name="testtoken", + hash="dummyhash", + expires=now + timedelta(hours=1), + activates=now, + ) + assert token.is_active() + + +def test_string_representation_should_match_name(): + now = datetime.now() + token = JWTRefreshToken( + name="testtoken", + hash="dummyhash", + expires=now + timedelta(hours=1), + activates=now - timedelta(hours=1), + ) + assert str(token) == token.name