-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add django signal projector, improve roles, added token-based roles s…
…ervice, several fixes, improvements and cleanup
- Loading branch information
1 parent
d867cbd
commit b3bc0f1
Showing
15 changed files
with
293 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import datetime | ||
from typing import Type | ||
|
||
from django.dispatch import Signal | ||
|
||
from fractal.core.event_sourcing.event import SendingEvent | ||
from fractal.core.event_sourcing.event_projector import EventProjector | ||
from fractal.core.event_sourcing.message import Message | ||
|
||
fractal_event_signal = Signal(providing_args=["message"]) | ||
|
||
|
||
class DjangoSignalEventProjector(EventProjector): | ||
def project(self, id: str, event: Type[SendingEvent]): | ||
message = Message( | ||
id=id, | ||
occurred_on=datetime.datetime.now(tz=datetime.timezone.utc), | ||
event_type=event.__class__.__name__, | ||
event=event, | ||
object_id=str(event.object_id), | ||
) | ||
fractal_event_signal.send(sender=self.__class__, message=message) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,4 +2,5 @@ | |
|
||
|
||
class Role(EnumModel): | ||
pass | ||
OWNER = "role.owner" | ||
ADMIN = "role.admin" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
from fractal.core.exceptions import DomainException | ||
|
||
|
||
class TokenInvalidException(DomainException): | ||
code = "TOKEN_INVALID" | ||
status_code = 403 | ||
|
||
|
||
class TokenExpiredException(DomainException): | ||
code = "TOKEN_EXPIRED" | ||
status_code = 403 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
import json | ||
import uuid | ||
from abc import abstractmethod | ||
from calendar import timegm | ||
from datetime import datetime | ||
from typing import Dict | ||
|
||
import jwt | ||
from jwt import DecodeError, ExpiredSignatureError | ||
|
||
from fractal.contrib.tokens.exceptions import ( | ||
TokenExpiredException, | ||
TokenInvalidException, | ||
) | ||
from fractal.contrib.tokens.settings import ( | ||
ACCESS_TOKEN_EXPIRATION_SECONDS, | ||
REFRESH_TOKEN_EXPIRATION_SECONDS, | ||
) | ||
from fractal.core.services import Service | ||
from fractal.core.utils.application_context import ApplicationContext | ||
|
||
|
||
class TokenService(Service): | ||
@abstractmethod | ||
def generate( | ||
self, | ||
payload: Dict, | ||
token_type: str = "access", | ||
seconds_valid: int = ACCESS_TOKEN_EXPIRATION_SECONDS, | ||
) -> str: | ||
raise NotImplementedError | ||
|
||
def _prepare( | ||
self, payload: Dict, token_type: str, seconds_valid: int, issuer: str | ||
) -> Dict: | ||
utcnow = timegm(datetime.utcnow().utctimetuple()) | ||
if not seconds_valid: | ||
seconds_valid = ( | ||
REFRESH_TOKEN_EXPIRATION_SECONDS | ||
if token_type == "refresh" | ||
else ACCESS_TOKEN_EXPIRATION_SECONDS | ||
) | ||
payload.update( | ||
{ | ||
"iat": utcnow, | ||
"nbf": utcnow, | ||
"jti": str(uuid.uuid4()), | ||
"iss": issuer, | ||
"exp": utcnow + seconds_valid, | ||
"typ": token_type, | ||
} | ||
) | ||
return payload | ||
|
||
@abstractmethod | ||
def verify(self, token: str): | ||
raise NotImplementedError | ||
|
||
|
||
class DummyJsonTokenService(TokenService): | ||
def generate( | ||
self, | ||
payload: Dict, | ||
token_type: str = "access", | ||
seconds_valid: int = ACCESS_TOKEN_EXPIRATION_SECONDS, | ||
) -> str: | ||
return json.dumps(payload) | ||
|
||
def verify(self, token: str): | ||
return json.loads(token) | ||
|
||
|
||
class SymmetricJwtTokenService(TokenService): | ||
def __init__(self, issuer: str, secret: str): | ||
self.issuer = issuer | ||
self.secret = secret | ||
self.algorithm = "HS256" | ||
|
||
@classmethod | ||
def install(cls, context: ApplicationContext): | ||
app_name, app_env, app_domain, secret_key = context.get_parameters( | ||
["app_name", "app_env", "app_domain", "secret_key"] | ||
) | ||
yield cls( | ||
f"{app_name}@{app_env}.{app_domain}", | ||
secret_key, | ||
) | ||
|
||
def generate( | ||
self, | ||
payload: Dict, | ||
token_type: str = "access", | ||
seconds_valid: int = ACCESS_TOKEN_EXPIRATION_SECONDS, | ||
) -> str: | ||
return jwt.encode( | ||
self._prepare(payload, token_type, seconds_valid, self.issuer), | ||
self.secret, | ||
algorithm=self.algorithm, | ||
) | ||
|
||
def verify(self, token: str): | ||
try: | ||
payload = jwt.decode(token, self.secret, algorithms=self.algorithm) | ||
except DecodeError: | ||
raise TokenInvalidException("The supplied token is invalid!") | ||
except ExpiredSignatureError: | ||
raise TokenExpiredException("The supplied token is expired!") | ||
if payload["typ"] != "access": | ||
raise TokenInvalidException("The supplied token is invalid!") | ||
return payload | ||
|
||
|
||
class AsymmetricJwtTokenService(TokenService): | ||
def __init__(self, issuer: str, private_key: str, public_key: str): | ||
self.issuer = issuer | ||
self.private_key = private_key | ||
self.public_key = public_key | ||
self.algorithm = "RS256" | ||
|
||
@classmethod | ||
def install(cls, context: ApplicationContext): | ||
app_name, app_env, app_domain, private_key, public_key = context.get_parameters( | ||
["app_name", "app_env", "app_domain", "private_key", "public_key"] | ||
) | ||
yield cls( | ||
f"{app_name}@{app_env}.{app_domain}", | ||
private_key, | ||
public_key, | ||
) | ||
|
||
def generate( | ||
self, | ||
payload: Dict, | ||
token_type: str = "access", | ||
seconds_valid: int = ACCESS_TOKEN_EXPIRATION_SECONDS, | ||
) -> str: | ||
return jwt.encode( | ||
self._prepare(payload, token_type, seconds_valid, self.issuer), | ||
self.private_key, | ||
algorithm=self.algorithm, | ||
) | ||
|
||
def verify(self, token: str): | ||
try: | ||
payload = jwt.decode(token, self.public_key, algorithms=self.algorithm) | ||
except DecodeError: | ||
raise TokenInvalidException("The supplied token is invalid!") | ||
except ExpiredSignatureError: | ||
raise TokenExpiredException("The supplied token is expired!") | ||
if payload["typ"] not in ["access", "refresh"]: | ||
raise TokenInvalidException("The supplied token is invalid!") | ||
return payload |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
REFRESH_TOKEN_EXPIRATION_SECONDS = 60 * 60 * 24 # 1 day | ||
ACCESS_TOKEN_EXPIRATION_SECONDS = 60 * 10 # 10 minutes |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.