Skip to content

Commit

Permalink
Merge pull request #493 from baobabsoluciones/release/v1.0.9
Browse files Browse the repository at this point in the history
Release/v1.0.9
  • Loading branch information
ggsdc authored Dec 27, 2023
2 parents 3fb9f82 + 66f4c85 commit efcde90
Show file tree
Hide file tree
Showing 11 changed files with 316 additions and 37 deletions.
2 changes: 1 addition & 1 deletion cornflow-server/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ ENV DEBIAN_FRONTEND noninteractive
ENV TERM linux

# CORNFLOW vars
ARG CORNFLOW_VERSION=1.0.8
ARG CORNFLOW_VERSION=1.0.9

# install linux pkg
RUN apt update -y && apt-get install -y --no-install-recommends \
Expand Down
10 changes: 10 additions & 0 deletions cornflow-server/changelog.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
version 1.0.9
--------------

- released: 2023-12-27
- description: added new authentication for BI endpoints where the token does not expire
- changelog:
- Added new auth method.
- Added new token generation that can be used only through the cli.
- Added new token decodification that doe snot check for expiry date on token.

version 1.0.8
--------------

Expand Down
1 change: 1 addition & 0 deletions cornflow-server/cornflow/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"""

import click

from cornflow.cli.actions import actions
from cornflow.cli.config import config
from cornflow.cli.migrations import migrations
Expand Down
3 changes: 1 addition & 2 deletions cornflow-server/cornflow/cli/actions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import os

import click

from cornflow.cli.utils import get_app
from cornflow.commands import register_actions_command
from .arguments import verbose
Expand Down
49 changes: 47 additions & 2 deletions cornflow-server/cornflow/cli/users.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import click

from cornflow.cli.arguments import username, password, email, verbose
from cornflow.cli.utils import get_app
from cornflow.commands import create_user_with_role
from cornflow.shared.const import SERVICE_ROLE
from cornflow.models import UserModel
from cornflow.shared.authentication.auth import BIAuth
from cornflow.shared.const import SERVICE_ROLE, VIEWER_ROLE
from cornflow.shared.exceptions import (
ObjectDoesNotExist,
NoPermission,
)


@click.group(name="users", help="Commands to manage the users")
Expand All @@ -26,7 +33,45 @@ def create():
def create_service_user(username, password, email, verbose):
app = get_app()
with app.app_context():

create_user_with_role(
username, email, password, "service user", SERVICE_ROLE, verbose=verbose
)


@create.command(name="viewer", help="Create a viewer user")
@username
@password
@email
@verbose
def create_viewer_user(username, password, email, verbose):
app = get_app()
with app.app_context():
create_user_with_role(
username, email, password, "viewer user", VIEWER_ROLE, verbose=verbose
)


@create.command(
name="token",
help="Creates a token for a user that is never going to expire. This token can only be used on BI endpoints",
)
@click.option(
"--idx", "-i", type=int, help="The id of the user to generate the token for"
)
@username
@password
def create_unexpiring_token(idx, username, password):
app = get_app()
with app.app_context():
user = UserModel.get_one_object(id=idx)
asking_user = UserModel.get_one_user_by_username(username)

if not asking_user.check_hash(password) or not asking_user.is_service_user():
raise NoPermission("The asking user has no permissions to generate tokens")

if not user:
raise ObjectDoesNotExist("User does not exist")

token = BIAuth.generate_token(idx)
click.echo(token)
return True
6 changes: 4 additions & 2 deletions cornflow-server/cornflow/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@

class DefaultConfig(object):
SERVICE_NAME = os.getenv("SERVICE_NAME", "Cornflow")
SECRET_KEY = os.getenv("SECRET_KEY")
SECRET_TOKEN_KEY = os.getenv("SECRET_KEY")
SECRET_BI_KEY = os.getenv("SECRET_BI_KEY")
SQLALCHEMY_DATABASE_URI = os.getenv("DATABASE_URL", "sqlite:///cornflow.db")
AIRFLOW_URL = os.getenv("AIRFLOW_URL")
AIRFLOW_USER = os.getenv("AIRFLOW_USER")
Expand Down Expand Up @@ -91,7 +92,8 @@ class Testing(DefaultConfig):
DEBUG = False
TESTING = True
PROPAGATE_EXCEPTIONS = True
SECRET_KEY = "TESTINGSECRETKEY"
SECRET_TOKEN_KEY = "TESTINGSECRETKEY"
SECRET_BI_KEY = "THISISANOTHERKEY"
SQLALCHEMY_DATABASE_URI = os.getenv("DATABASE_URL", "sqlite:///cornflow_test.db")
AIRFLOW_URL = os.getenv("AIRFLOW_URL", "http://localhost:8080")
PRESERVE_CONTEXT_ON_EXCEPTION = False
Expand Down
91 changes: 72 additions & 19 deletions cornflow-server/cornflow/shared/authentication/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,7 @@ def generate_token(user_id: int = None) -> str:
if user_id is None:
err = "The user id passed to generate the token is not valid."
raise InvalidUsage(
err,
log_txt="Error while trying to generate token. " + err
err, log_txt="Error while trying to generate token. " + err
)

payload = {
Expand All @@ -109,7 +108,9 @@ def generate_token(user_id: int = None) -> str:
"sub": user_id,
}

return jwt.encode(payload, current_app.config["SECRET_KEY"], algorithm="HS256")
return jwt.encode(
payload, current_app.config["SECRET_TOKEN_KEY"], algorithm="HS256"
)

@staticmethod
def decode_token(token: str = None) -> dict:
Expand All @@ -123,27 +124,26 @@ def decode_token(token: str = None) -> dict:
if token is None:
err = "The provided token is not valid."
raise InvalidUsage(
err,
log_txt="Error while trying to decode token. " + err
err, log_txt="Error while trying to decode token. " + err
)
try:
payload = jwt.decode(
token, current_app.config["SECRET_KEY"], algorithms="HS256"
token, current_app.config["SECRET_TOKEN_KEY"], algorithms="HS256"
)
return {"user_id": payload["sub"]}
except jwt.ExpiredSignatureError:
raise InvalidCredentials(
"The token has expired, please login again",
log_txt="Error while trying to decode token. The token has expired."
log_txt="Error while trying to decode token. The token has expired.",
)
except jwt.InvalidTokenError:
raise InvalidCredentials(
"Invalid token, please try again with a new token",
log_txt="Error while trying to decode token. The token is invalid."
log_txt="Error while trying to decode token. The token is invalid.",
)

def validate_oid_token(
self, token: str, client_id: str, tenant_id: str, issuer: str, provider: int
self, token: str, client_id: str, tenant_id: str, issuer: str, provider: int
) -> dict:
"""
This method takes a token issued by an OID provider, the relevant information about the OID provider
Expand Down Expand Up @@ -172,12 +172,12 @@ def validate_oid_token(
except jwt.ExpiredSignatureError:
raise InvalidCredentials(
"The token has expired, please login again",
log_txt="Error while trying to validate a token. The token has expired. "
log_txt="Error while trying to validate a token. The token has expired.",
)
except jwt.InvalidTokenError:
raise InvalidCredentials(
"Invalid token, please try again with a new token",
log_txt="Error while trying to validate a token. The token is not valid. "
log_txt="Error while trying to validate a token. The token is not valid.",
)

@staticmethod
Expand All @@ -191,12 +191,14 @@ def get_token_from_header(headers: Headers = None) -> str:
:rtype: str
"""
if headers is None:
raise InvalidUsage(log_txt="Error while trying to get a token from header. The header is invalid.")
raise InvalidUsage(
log_txt="Error while trying to get a token from header. The header is invalid."
)

if "Authorization" not in headers:
raise InvalidCredentials(
"Auth token is not available",
log_txt="Error while trying to get a token from header. The auth token is not available."
log_txt="Error while trying to get a token from header. The auth token is not available.",
)
auth_header = headers.get("Authorization")
if not auth_header:
Expand All @@ -206,8 +208,7 @@ def get_token_from_header(headers: Headers = None) -> str:
except Exception as e:
err = f"The authorization header has a bad syntax: {e}"
raise InvalidCredentials(
err,
log_txt=f"Error while trying to get a token from header. " + err
err, log_txt=f"Error while trying to get a token from header. " + err
)

def get_user_from_header(self, headers: Headers = None) -> UserModel:
Expand All @@ -222,8 +223,7 @@ def get_user_from_header(self, headers: Headers = None) -> UserModel:
if headers is None:
err = "Headers are missing from the request. Authentication was not possible to perform."
raise InvalidUsage(
err,
log_txt="Error while trying to get user from header. " + err
err, log_txt="Error while trying to get user from header. " + err
)
token = self.get_token_from_header(headers)
data = self.decode_token(token)
Expand All @@ -232,8 +232,7 @@ def get_user_from_header(self, headers: Headers = None) -> UserModel:
if user is None:
err = "User does not exist, invalid token."
raise ObjectDoesNotExist(
err,
log_txt="Error while trying to get user from header. " + err
err, log_txt="Error while trying to get user from header. " + err
)
return user

Expand Down Expand Up @@ -460,3 +459,57 @@ def _get_public_key(self, token: str, tenant_id: str, provider: int):
kid = self._get_key_id(token)
jwk = self._get_jwk(kid, tenant_id, provider)
return self._rsa_pem_from_jwk(jwk)


class BIAuth(Auth):
def __init__(self, user_model=UserModel):
super().__init__(user_model)

@staticmethod
def decode_token(token: str = None) -> dict:
"""
Decodes a given JSON Web token and extracts the sub from it to give it back.
:param str token: the given JSON Web Token
:return: the sub field of the token as the user_id
:rtype: dict
"""
if token is None:
err = "The provided token is not valid."
raise InvalidUsage(
err, log_txt="Error while trying to decode token. " + err
)
try:
payload = jwt.decode(
token, current_app.config["SECRET_BI_KEY"], algorithms="HS256"
)
return {"user_id": payload["sub"]}
except jwt.InvalidTokenError:
raise InvalidCredentials(
"Invalid token, please try again with a new token",
log_txt="Error while trying to decode token. The token is invalid.",
)

@staticmethod
def generate_token(user_id: int = None) -> str:
"""
Generates a token given a user_id with a duration of one day
:param int user_id: user code to be encoded in the token to identify the user afterward.
:return: the generated token
:rtype: str
"""
if user_id is None:
err = "The user id passed to generate the token is not valid."
raise InvalidUsage(
err, log_txt="Error while trying to generate token. " + err
)

payload = {
"iat": datetime.utcnow(),
"sub": user_id,
}

return jwt.encode(
payload, current_app.config["SECRET_BI_KEY"], algorithm="HS256"
)
4 changes: 1 addition & 3 deletions cornflow-server/cornflow/tests/custom_test_case.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,6 @@ def create_user(self, data):

@staticmethod
def assign_role(user_id, role_id):

if UserRoleModel.check_if_role_assigned(user_id, role_id):
user_role = UserRoleModel.query.filter_by(
user_id=user_id, role_id=role_id
Expand Down Expand Up @@ -288,7 +287,6 @@ def patch_row(
self.assertEqual(payload_to_check["solution"], row.json["solution"])

def delete_row(self, url):

response = self.client.delete(
url, follow_redirects=True, headers=self.get_header_with_auth(self.token)
)
Expand Down Expand Up @@ -733,7 +731,7 @@ def test_token(self):
self.assertEqual(str, type(self.response.json["token"]))
decoded_token = jwt.decode(
self.response.json["token"],
current_app.config["SECRET_KEY"],
current_app.config["SECRET_TOKEN_KEY"],
algorithms="HS256",
)

Expand Down
Loading

0 comments on commit efcde90

Please sign in to comment.