cornflow 1.1.5__py3-none-any.whl → 1.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. airflow_config/airflow_local_settings.py +1 -1
  2. cornflow/app.py +8 -3
  3. cornflow/cli/migrations.py +23 -3
  4. cornflow/cli/service.py +17 -15
  5. cornflow/cli/utils.py +16 -1
  6. cornflow/config.py +10 -11
  7. cornflow/endpoints/__init__.py +7 -1
  8. cornflow/endpoints/alarms.py +66 -2
  9. cornflow/endpoints/login.py +81 -63
  10. cornflow/endpoints/meta_resource.py +11 -3
  11. cornflow/models/base_data_model.py +4 -32
  12. cornflow/models/meta_models.py +28 -22
  13. cornflow/models/user.py +7 -10
  14. cornflow/schemas/alarms.py +8 -0
  15. cornflow/schemas/query.py +2 -1
  16. cornflow/schemas/user.py +5 -20
  17. cornflow/shared/authentication/auth.py +201 -264
  18. cornflow/shared/const.py +3 -14
  19. cornflow/tests/const.py +1 -0
  20. cornflow/tests/custom_test_case.py +77 -26
  21. cornflow/tests/unit/test_actions.py +2 -2
  22. cornflow/tests/unit/test_alarms.py +55 -1
  23. cornflow/tests/unit/test_apiview.py +108 -3
  24. cornflow/tests/unit/test_cases.py +20 -29
  25. cornflow/tests/unit/test_cli.py +6 -5
  26. cornflow/tests/unit/test_dags.py +5 -6
  27. cornflow/tests/unit/test_instances.py +14 -2
  28. cornflow/tests/unit/test_instances_file.py +1 -1
  29. cornflow/tests/unit/test_licenses.py +1 -1
  30. cornflow/tests/unit/test_log_in.py +230 -207
  31. cornflow/tests/unit/test_permissions.py +8 -8
  32. cornflow/tests/unit/test_roles.py +48 -10
  33. cornflow/tests/unit/test_tables.py +7 -7
  34. cornflow/tests/unit/test_token.py +19 -5
  35. cornflow/tests/unit/test_users.py +22 -6
  36. {cornflow-1.1.5.dist-info → cornflow-1.2.0.dist-info}/METADATA +13 -12
  37. {cornflow-1.1.5.dist-info → cornflow-1.2.0.dist-info}/RECORD +40 -40
  38. {cornflow-1.1.5.dist-info → cornflow-1.2.0.dist-info}/WHEEL +1 -1
  39. {cornflow-1.1.5.dist-info → cornflow-1.2.0.dist-info}/entry_points.txt +0 -0
  40. {cornflow-1.1.5.dist-info → cornflow-1.2.0.dist-info}/top_level.txt +0 -0
@@ -19,5 +19,5 @@ STATE_COLORS = {
19
19
  from airflow.www.utils import UIAlert
20
20
 
21
21
  DASHBOARD_UIALERTS = [
22
- UIAlert("Welcome! This is the backend of your Cornflow environment. Airflow™ is a platform created by the community to programmatically author, schedule and monitor workflows."),
22
+ UIAlert("Welcome! This is the backend of your cornflow environment. Airflow™ is a platform created by the community to programmatically author, schedule and monitor workflows."),
23
23
  ]
cornflow/app.py CHANGED
@@ -37,7 +37,7 @@ from cornflow.endpoints.signup import SignUpEndpoint
37
37
  from cornflow.shared import db, bcrypt
38
38
  from cornflow.shared.compress import init_compress
39
39
  from cornflow.shared.const import AUTH_DB, AUTH_LDAP, AUTH_OID
40
- from cornflow.shared.exceptions import initialize_errorhandlers
40
+ from cornflow.shared.exceptions import initialize_errorhandlers, ConfigurationError
41
41
  from cornflow.shared.log_config import log_config
42
42
 
43
43
 
@@ -62,11 +62,11 @@ def create_app(env_name="development", dataconn=None):
62
62
  CORS(app)
63
63
  bcrypt.init_app(app)
64
64
  db.init_app(app)
65
- migrate = Migrate(app=app, db=db)
65
+ Migrate(app=app, db=db)
66
66
 
67
67
  if "sqlite" in app.config["SQLALCHEMY_DATABASE_URI"]:
68
68
 
69
- def _fk_pragma_on_connect(dbapi_con, con_record):
69
+ def _fk_pragma_on_connect(dbapi_con, _con_record):
70
70
  dbapi_con.execute("pragma foreign_keys=ON")
71
71
 
72
72
  with app.app_context():
@@ -100,6 +100,11 @@ def create_app(env_name="development", dataconn=None):
100
100
  api.add_resource(LoginEndpoint, "/login/", endpoint="login")
101
101
  elif auth_type == AUTH_OID:
102
102
  api.add_resource(LoginOpenAuthEndpoint, "/login/", endpoint="login")
103
+ else:
104
+ raise ConfigurationError(
105
+ error="Invalid authentication type",
106
+ log_txt="Error while configuring authentication. The authentication type is not valid."
107
+ )
103
108
 
104
109
  initialize_errorhandlers(app)
105
110
  init_compress(app)
@@ -3,7 +3,7 @@ import os.path
3
3
 
4
4
  import click
5
5
  from cornflow.shared import db
6
- from flask_migrate import Migrate, migrate, upgrade, init
6
+ from flask_migrate import Migrate, migrate, upgrade, downgrade, init
7
7
 
8
8
  from .utils import get_app
9
9
 
@@ -28,7 +28,27 @@ def migrate_migrations():
28
28
 
29
29
 
30
30
  @migrations.command(name="upgrade", help="Apply migrations")
31
- def upgrade_migrations():
31
+ @click.option(
32
+ "-r", "--revision", type=str, help="The revision to upgrade to", default="head"
33
+ )
34
+ def upgrade_migrations(revision="head"):
35
+ app = get_app()
36
+ external = int(os.getenv("EXTERNAL_APP", 0))
37
+ if external == 0:
38
+ path = "./cornflow/migrations"
39
+ else:
40
+ path = f"./{os.getenv('EXTERNAL_APP_MODULE', 'external_app')}/migrations"
41
+
42
+ with app.app_context():
43
+ migration_client = Migrate(app=app, db=db, directory=path)
44
+ upgrade(revision=revision)
45
+
46
+
47
+ @migrations.command(name="downgrade", help="Downgrade migrations")
48
+ @click.option(
49
+ "-r", "--revision", type=str, help="The revision to downgrade to", default="-1"
50
+ )
51
+ def downgrade_migrations(revision="-1"):
32
52
  app = get_app()
33
53
  external = int(os.getenv("EXTERNAL_APP", 0))
34
54
  if external == 0:
@@ -38,7 +58,7 @@ def upgrade_migrations():
38
58
 
39
59
  with app.app_context():
40
60
  migration_client = Migrate(app=app, db=db, directory=path)
41
- upgrade()
61
+ downgrade(revision=revision)
42
62
 
43
63
 
44
64
  @migrations.command(
cornflow/cli/service.py CHANGED
@@ -6,6 +6,7 @@ from logging import error
6
6
 
7
7
 
8
8
  import click
9
+ from .utils import get_db_conn
9
10
  import cornflow
10
11
  from cornflow.app import create_app
11
12
  from cornflow.commands import (
@@ -16,7 +17,14 @@ from cornflow.commands import (
16
17
  update_schemas_command,
17
18
  update_dag_registry_command,
18
19
  )
19
- from cornflow.shared.const import AUTH_DB, ADMIN_ROLE, SERVICE_ROLE
20
+ from cornflow.shared.const import (
21
+ AUTH_DB,
22
+ AUTH_LDAP,
23
+ AUTH_OID,
24
+ ADMIN_ROLE,
25
+ SERVICE_ROLE,
26
+ PLANNER_ROLE,
27
+ )
20
28
  from cornflow.shared import db
21
29
  from cryptography.fernet import Fernet
22
30
  from flask_migrate import Migrate, upgrade
@@ -49,15 +57,8 @@ def init_cornflow_service():
49
57
  os.environ["SECRET_KEY"] = os.getenv("FERNET_KEY", Fernet.generate_key().decode())
50
58
 
51
59
  # Cornflow db defaults
52
- cornflow_db_host = os.getenv("CORNFLOW_DB_HOST", "cornflow_db")
53
- cornflow_db_port = os.getenv("CORNFLOW_DB_PORT", "5432")
54
- cornflow_db_user = os.getenv("CORNFLOW_DB_USER", "cornflow")
55
- cornflow_db_password = os.getenv("CORNFLOW_DB_PASSWORD", "cornflow")
56
- cornflow_db = os.getenv("CORNFLOW_DB", "cornflow")
57
- cornflow_db_conn = os.getenv(
58
- "cornflow_db_conn",
59
- f"postgresql://{cornflow_db_user}:{cornflow_db_password}@{cornflow_db_host}:{cornflow_db_port}/{cornflow_db}",
60
- )
60
+ os.environ["DEFAULT_POSTGRES"] = "1"
61
+ cornflow_db_conn = get_db_conn()
61
62
  os.environ["DATABASE_URL"] = cornflow_db_conn
62
63
 
63
64
  # Platform auth config and service users
@@ -83,11 +84,11 @@ def init_cornflow_service():
83
84
  os.environ["SIGNUP_ACTIVATED"] = str(signup_activated)
84
85
  user_access_all_objects = os.getenv("USER_ACCESS_ALL_OBJECTS", 0)
85
86
  os.environ["USER_ACCESS_ALL_OBJECTS"] = str(user_access_all_objects)
86
- default_role = os.getenv("DEFAULT_ROLE", 2)
87
+ default_role = int(os.getenv("DEFAULT_ROLE", PLANNER_ROLE))
87
88
  os.environ["DEFAULT_ROLE"] = str(default_role)
88
89
 
89
90
  # Check LDAP parameters for active directory and show message
90
- if os.getenv("AUTH_TYPE") == 2:
91
+ if os.getenv("AUTH_TYPE") == AUTH_LDAP:
91
92
  print(
92
93
  "WARNING: Cornflow will be deployed with LDAP Authorization. Please review your ldap auth configuration."
93
94
  )
@@ -129,10 +130,11 @@ def init_cornflow_service():
129
130
  app = create_app(environment, cornflow_db_conn)
130
131
  with app.app_context():
131
132
  path = f"{os.path.dirname(cornflow.__file__)}/migrations"
132
- migrate = Migrate(app=app, db=db, directory=path)
133
+ Migrate(app=app, db=db, directory=path)
133
134
  upgrade()
134
135
  access_init_command(verbose=False)
135
- if auth == 1 or auth == 0:
136
+ if auth == AUTH_DB or auth == AUTH_OID:
137
+ # create cornflow admin user
136
138
  create_user_with_role(
137
139
  cornflow_admin_user,
138
140
  cornflow_admin_email,
@@ -188,7 +190,7 @@ def init_cornflow_service():
188
190
  migrate = Migrate(app=app, db=db, directory=path)
189
191
  upgrade()
190
192
  access_init_command(verbose=False)
191
- if auth == 1 or auth == 0:
193
+ if auth == AUTH_DB or auth == AUTH_OID:
192
194
  # create cornflow admin user
193
195
  create_user_with_role(
194
196
  cornflow_admin_user,
cornflow/cli/utils.py CHANGED
@@ -6,7 +6,7 @@ import warnings
6
6
 
7
7
  def get_app():
8
8
  env = os.getenv("FLASK_ENV", "development")
9
- data_conn = os.getenv("DATABASE_URL", "sqlite:///cornflow.db")
9
+ data_conn = get_db_conn()
10
10
  if env == "production":
11
11
  warnings.filterwarnings("ignore")
12
12
  external = int(os.getenv("EXTERNAL_APP", 0))
@@ -24,3 +24,18 @@ def get_app():
24
24
  app = create_app(env, data_conn)
25
25
 
26
26
  return app
27
+
28
+
29
+ def get_db_conn():
30
+ if int(os.getenv("DEFAULT_POSTGRES", 0)) == 0:
31
+ return os.getenv("DATABASE_URL", "sqlite:///cornflow.db")
32
+ else:
33
+ cornflow_db_host = os.getenv("CORNFLOW_DB_HOST", "cornflow_db")
34
+ cornflow_db_port = os.getenv("CORNFLOW_DB_PORT", "5432")
35
+ cornflow_db_user = os.getenv("CORNFLOW_DB_USER", "cornflow")
36
+ cornflow_db_password = os.getenv("CORNFLOW_DB_PASSWORD", "cornflow")
37
+ cornflow_db = os.getenv("CORNFLOW_DB", "cornflow")
38
+ return os.getenv(
39
+ "cornflow_db_conn",
40
+ f"postgresql://{cornflow_db_user}:{cornflow_db_password}@{cornflow_db_host}:{cornflow_db_port}/{cornflow_db}",
41
+ )
cornflow/config.py CHANGED
@@ -1,5 +1,5 @@
1
1
  import os
2
- from .shared.const import AUTH_DB, PLANNER_ROLE
2
+ from .shared.const import AUTH_DB, PLANNER_ROLE, AUTH_OID
3
3
  from apispec import APISpec
4
4
  from apispec.ext.marshmallow import MarshmallowPlugin
5
5
 
@@ -28,7 +28,7 @@ class DefaultConfig(object):
28
28
  SIGNUP_ACTIVATED = int(os.getenv("SIGNUP_ACTIVATED", 1))
29
29
  CORNFLOW_SERVICE_USER = os.getenv("CORNFLOW_SERVICE_USER", "service_user")
30
30
 
31
- # If service user is allow to log with username and password
31
+ # If service user is allowed to log with username and password
32
32
  SERVICE_USER_ALLOW_PASSWORD_LOGIN = int(
33
33
  os.getenv("SERVICE_USER_ALLOW_PASSWORD_LOGIN", 1)
34
34
  )
@@ -59,15 +59,13 @@ class DefaultConfig(object):
59
59
  LDAP_PROTOCOL_VERSION = int(os.getenv("LDAP_PROTOCOL_VERSION", 3))
60
60
  LDAP_USE_TLS = os.getenv("LDAP_USE_TLS", "False")
61
61
 
62
- # OpenID login -> Default Azure
63
- OID_PROVIDER = os.getenv("OID_PROVIDER", 0)
64
- OID_CLIENT_ID = os.getenv("OID_CLIENT_ID")
65
- OID_TENANT_ID = os.getenv("OID_TENANT_ID")
66
- OID_ISSUER = os.getenv("OID_ISSUER")
62
+ # OpenID Connect configuration
63
+ OID_PROVIDER = os.getenv("OID_PROVIDER")
64
+ OID_EXPECTED_AUDIENCE = os.getenv("OID_EXPECTED_AUDIENCE")
67
65
 
68
66
  # APISPEC:
69
67
  APISPEC_SPEC = APISpec(
70
- title="Cornflow API docs",
68
+ title="cornflow API docs",
71
69
  version="v1",
72
70
  plugins=[MarshmallowPlugin()],
73
71
  openapi_version="2.0.0",
@@ -127,8 +125,9 @@ class TestingOpenAuth(Testing):
127
125
  """
128
126
  Configuration class for testing some edge cases with Open Auth login
129
127
  """
130
-
131
- AUTH_TYPE = 0
128
+ AUTH_TYPE = AUTH_OID
129
+ OID_PROVIDER = "https://test-provider.example.com"
130
+ OID_EXPECTED_AUDIENCE = "test-audience-id"
132
131
 
133
132
 
134
133
  class TestingApplicationRoot(Testing):
@@ -158,5 +157,5 @@ app_config = {
158
157
  "testing": Testing,
159
158
  "production": Production,
160
159
  "testing-oauth": TestingOpenAuth,
161
- "testing-root": TestingApplicationRoot,
160
+ "testing-root": TestingApplicationRoot
162
161
  }
@@ -3,8 +3,9 @@ Initialization file for the endpoints module
3
3
  All references to endpoints should be imported from here
4
4
  The login resource gets created on app startup as it depends on configuration
5
5
  """
6
+
6
7
  from .action import ActionListEndpoint
7
- from .alarms import AlarmsEndpoint
8
+ from .alarms import AlarmsEndpoint, AlarmDetailEndpoint
8
9
  from .apiview import ApiViewListEndpoint
9
10
  from .case import (
10
11
  CaseEndpoint,
@@ -225,6 +226,11 @@ alarms_resources = [
225
226
  urls="/alarms/",
226
227
  endpoint="alarms",
227
228
  ),
229
+ dict(
230
+ resource=AlarmDetailEndpoint,
231
+ urls="/alarms/<int:idx>/",
232
+ endpoint="alarms-detail",
233
+ ),
228
234
  dict(
229
235
  resource=MainAlarmsEndpoint,
230
236
  urls="/main-alarms/",
@@ -1,15 +1,19 @@
1
1
  # Imports from libraries
2
+ from flask import current_app
2
3
  from flask_apispec import doc, marshal_with, use_kwargs
3
4
 
4
5
  # Import from internal modules
5
6
  from cornflow.endpoints.meta_resource import BaseMetaResource
6
7
  from cornflow.models import AlarmsModel
7
8
  from cornflow.schemas.alarms import (
9
+ AlarmEditRequest,
8
10
  AlarmsResponse,
9
11
  AlarmsPostRequest,
10
- QueryFiltersAlarms
12
+ QueryFiltersAlarms,
11
13
  )
12
14
  from cornflow.shared.authentication import Auth, authenticate
15
+ from cornflow.shared.exceptions import AirflowError, ObjectDoesNotExist, InvalidData
16
+ from cornflow.shared.const import SERVICE_ROLE
13
17
 
14
18
 
15
19
  class AlarmsEndpoint(BaseMetaResource):
@@ -56,4 +60,64 @@ class AlarmsEndpoint(BaseMetaResource):
56
60
  and an integer with the HTTP status code.
57
61
  :rtype: Tuple(dict, integer)
58
62
  """
59
- return self.post_list(data=kwargs)
63
+ return self.post_list(data=kwargs)
64
+
65
+
66
+ class AlarmDetailEndpointBase(BaseMetaResource):
67
+ """
68
+ Endpoint used to get the information of a certain alarm. But not the data!
69
+ """
70
+
71
+ def __init__(self):
72
+ super().__init__()
73
+ self.data_model = AlarmsModel
74
+ self.unique = ["id"]
75
+
76
+
77
+ class AlarmDetailEndpoint(AlarmDetailEndpointBase):
78
+ @doc(description="Get details of an alarm", tags=["None"], inherit=False)
79
+ @authenticate(auth_class=Auth())
80
+ @marshal_with(AlarmsResponse)
81
+ @BaseMetaResource.get_data_or_404
82
+ def get(self, idx):
83
+ """
84
+ API method to get an execution created by the user and its related info.
85
+ It requires authentication to be passed in the form of a token that has to be linked to
86
+ an existing session (login) made by a user.
87
+
88
+ :param str idx: ID of the execution.
89
+ :return: A dictionary with a message (error if authentication failed, or the execution does not exist or
90
+ the data of the execution) and an integer with the HTTP status code.
91
+ :rtype: Tuple(dict, integer)
92
+ """
93
+ current_app.logger.info(
94
+ f"User {self.get_user()} gets details of execution {idx}"
95
+ )
96
+ return self.get_detail(idx=idx)
97
+
98
+ @doc(description="Edit an execution", tags=["Executions"], inherit=False)
99
+ @authenticate(auth_class=Auth())
100
+ @use_kwargs(AlarmEditRequest, location="json")
101
+ def put(self, idx, **data):
102
+ """
103
+ Edit an existing alarm
104
+
105
+ :param string idx: ID of the alarm.
106
+ :return: A dictionary with a message (error if authentication failed, or the alarm does not exist or
107
+ a message) and an integer with the HTTP status code.
108
+ :rtype: Tuple(dict, integer)
109
+ """
110
+ current_app.logger.info(f"User {self.get_user()} edits alarm {idx}")
111
+ return self.put_detail(data, track_user=False, idx=idx)
112
+
113
+ @doc(description="Disable an alarm", tags=["None"])
114
+ @authenticate(auth_class=Auth())
115
+ def delete(self, idx):
116
+ """
117
+ :param int alarm_id: Alarm id.
118
+ :return:
119
+ :rtype: Tuple(dict, integer)
120
+ """
121
+
122
+ current_app.logger.info(f"Alarm {idx} was disabled by user {self.get_user()}")
123
+ return self.disable_detail(idx=idx)
@@ -1,16 +1,17 @@
1
1
  """
2
- External endpoint for the user to login to the cornflow webserver
2
+ External endpoint for the user to log in to the cornflow webserver
3
3
  """
4
4
 
5
+ from datetime import datetime, timezone, timedelta
6
+
5
7
  # Partial imports
6
- from flask import current_app
8
+ from flask import current_app, request
7
9
  from flask_apispec import use_kwargs, doc
8
10
  from sqlalchemy.exc import IntegrityError, DBAPIError
9
- from datetime import datetime, timedelta
10
11
 
11
12
  # Import from internal modules
12
13
  from cornflow.endpoints.meta_resource import BaseMetaResource
13
- from cornflow.models import UserModel, UserRoleModel
14
+ from cornflow.models import UserModel, UserRoleModel, PermissionsDAG
14
15
  from cornflow.schemas.user import LoginEndpointRequest, LoginOpenAuthRequest
15
16
  from cornflow.shared import db
16
17
  from cornflow.shared.authentication import Auth, LDAPBase
@@ -18,15 +19,11 @@ from cornflow.shared.const import (
18
19
  AUTH_DB,
19
20
  AUTH_LDAP,
20
21
  AUTH_OID,
21
- OID_AZURE,
22
- OID_GOOGLE,
23
- OID_NONE,
24
22
  )
25
23
  from cornflow.shared.exceptions import (
26
24
  ConfigurationError,
27
25
  InvalidCredentials,
28
26
  InvalidUsage,
29
- EndpointNotImplemented,
30
27
  )
31
28
 
32
29
 
@@ -45,7 +42,7 @@ class LoginBaseEndpoint(BaseMetaResource):
45
42
  This method is in charge of performing the log in of the user
46
43
 
47
44
  :param kwargs: keyword arguments passed for the login, these can be username, password or a token
48
- :return: the response of the login or it raises an error. The correct response is a dict
45
+ :return: the response of the login, or it raises an error. The correct response is a dict
49
46
  with the newly issued token and the user id, and a status code of 200
50
47
  :rtype: dict
51
48
  """
@@ -55,10 +52,37 @@ class LoginBaseEndpoint(BaseMetaResource):
55
52
  if auth_type == AUTH_DB:
56
53
  user = self.auth_db_authenticate(**kwargs)
57
54
  response.update({"change_password": check_last_password_change(user)})
55
+ current_app.logger.info(
56
+ f"User {user.id} logged in successfully using database authentication"
57
+ )
58
58
  elif auth_type == AUTH_LDAP:
59
59
  user = self.auth_ldap_authenticate(**kwargs)
60
+ current_app.logger.info(
61
+ f"User {user.id} logged in successfully using LDAP authentication"
62
+ )
60
63
  elif auth_type == AUTH_OID:
61
- user = self.auth_oid_authenticate(**kwargs)
64
+ if kwargs.get("username") and kwargs.get("password"):
65
+ if not current_app.config.get("SERVICE_USER_ALLOW_PASSWORD_LOGIN", 0):
66
+ raise InvalidUsage(
67
+ "Must provide a token in Authorization header. Cannot log in with username and password",
68
+ 400,
69
+ )
70
+ user = self.auth_oid_authenticate(
71
+ username=kwargs["username"], password=kwargs["password"]
72
+ )
73
+ current_app.logger.info(
74
+ f"Service user {user.id} logged in successfully using password"
75
+ )
76
+ token = self.auth_class.generate_token(user.id)
77
+ else:
78
+ token = self.auth_class().get_token_from_header(request.headers)
79
+ user = self.auth_oid_authenticate(token=token)
80
+ current_app.logger.info(
81
+ f"User {user.id} logged in successfully using OpenID authentication"
82
+ )
83
+
84
+ response.update({"token": token, "id": user.id})
85
+ return response, 200
62
86
  else:
63
87
  raise ConfigurationError()
64
88
 
@@ -77,7 +101,7 @@ class LoginBaseEndpoint(BaseMetaResource):
77
101
 
78
102
  :param str username: the username of the user to log in
79
103
  :param str password: the password of the user to log in
80
- :return: the user object or it raises an error if it has not been possible to log in
104
+ :return: the user object, or it raises an error if it has not been possible to log in
81
105
  :rtype: :class:`UserModel`
82
106
  """
83
107
  user = self.data_model.get_one_object(username=username)
@@ -96,7 +120,7 @@ class LoginBaseEndpoint(BaseMetaResource):
96
120
 
97
121
  :param str username: the username of the user to log in
98
122
  :param str password: the password of the user to log in
99
- :return: the user object or it raises an error if it has not been possible to log in
123
+ :return: the user object, or it raises an error if it has not been possible to log in
100
124
  :rtype: :class:`UserModel`
101
125
  """
102
126
  ldap_obj = self.ldap_class(current_app.config)
@@ -141,49 +165,20 @@ class LoginBaseEndpoint(BaseMetaResource):
141
165
  self, token: str = None, username: str = None, password: str = None
142
166
  ):
143
167
  """
144
- Method in charge of performing the log in with the token issued by an Open ID provider.
145
- It has an exception and thus accepts username and password for service users if needed.
168
+ Method in charge of performing the authentication using OpenID Connect tokens.
169
+ Supports any OIDC provider configured via provider_url.
146
170
 
147
- :param str token: the token that the user has obtained from the Open ID provider
148
- :param str username: the username of the user to log in
149
- :param str password: the password of the user to log in
150
- :return: the user object or it raises an error if it has not been possible to log in
171
+ :param str token: the JWT token from the OIDC provider
172
+ :param str username: username for service users
173
+ :param str password: password for service users
174
+ :return: the user object, or it raises an error if it has not been possible to log in
151
175
  :rtype: :class:`UserModel`
152
176
  """
153
-
154
177
  if token:
155
178
 
156
- oid_provider = int(current_app.config["OID_PROVIDER"])
157
-
158
- client_id = current_app.config["OID_CLIENT_ID"]
159
- tenant_id = current_app.config["OID_TENANT_ID"]
160
- issuer = current_app.config["OID_ISSUER"]
161
-
162
- if client_id is None or tenant_id is None or issuer is None:
163
- raise ConfigurationError("The OID provider configuration is not valid")
164
-
165
- if oid_provider == OID_AZURE:
166
- decoded_token = self.auth_class().validate_oid_token(
167
- token, client_id, tenant_id, issuer, oid_provider
168
- )
169
-
170
- elif oid_provider == OID_GOOGLE:
171
- raise EndpointNotImplemented(
172
- "The selected OID provider is not implemented"
173
- )
174
- elif oid_provider == OID_NONE:
175
- raise EndpointNotImplemented(
176
- "The OID provider configuration is not valid"
177
- )
178
- else:
179
- raise EndpointNotImplemented(
180
- "The OID provider configuration is not valid"
181
- )
179
+ decoded_token = self.auth_class().decode_token(token)
182
180
 
183
- username = decoded_token["preferred_username"]
184
- email = decoded_token.get("email", f"{username}@test.org")
185
- first_name = decoded_token.get("given_name", "")
186
- last_name = decoded_token.get("family_name", "")
181
+ username = decoded_token.get("sub")
187
182
 
188
183
  user = self.data_model.get_one_object(username=username)
189
184
 
@@ -192,6 +187,10 @@ class LoginBaseEndpoint(BaseMetaResource):
192
187
  f"OpenID user {username} does not exist and is created"
193
188
  )
194
189
 
190
+ email = decoded_token.get("email", f"{username}@cornflow.org")
191
+ first_name = decoded_token.get("given_name", "")
192
+ last_name = decoded_token.get("family_name", "")
193
+
195
194
  data = {
196
195
  "username": username,
197
196
  "email": email,
@@ -208,33 +207,52 @@ class LoginBaseEndpoint(BaseMetaResource):
208
207
  "role_id": int(current_app.config["DEFAULT_ROLE"]),
209
208
  }
210
209
  )
211
-
212
210
  user_role.save()
211
+ if int(current_app.config["OPEN_DEPLOYMENT"]) == 1:
212
+ PermissionsDAG.add_all_permissions_to_user(user.id)
213
213
 
214
214
  return user
215
- elif (
216
- username
217
- and password
218
- and current_app.config["SERVICE_USER_ALLOW_PASSWORD_LOGIN"] == 1
219
- ):
220
215
 
216
+ elif username and password:
221
217
  user = self.auth_db_authenticate(username, password)
222
-
223
218
  if user.is_service_user():
224
219
  return user
225
- else:
226
- raise InvalidUsage("Invalid request")
220
+ raise InvalidUsage("Invalid request")
227
221
  else:
228
222
  raise InvalidUsage("Invalid request")
229
223
 
230
224
 
231
225
  def check_last_password_change(user):
226
+ """
227
+ Check if the user needs to change their password based on the password rotation time.
228
+
229
+ :param user: The user object to check
230
+ :return: True if password needs to be changed, False otherwise
231
+ :rtype: bool
232
+ """
232
233
  if user.pwd_last_change:
233
- if (
234
- user.pwd_last_change
235
- + timedelta(days=int(current_app.config["PWD_ROTATION_TIME"]))
236
- < datetime.utcnow()
237
- ):
234
+ # Handle the case where pwd_last_change is already a datetime object
235
+ if isinstance(user.pwd_last_change, datetime):
236
+ # If it's a naive datetime (no timezone info), make it timezone-aware
237
+ if user.pwd_last_change.tzinfo is None:
238
+ last_change = user.pwd_last_change.replace(tzinfo=timezone.utc)
239
+ else:
240
+ # Already timezone-aware
241
+ last_change = user.pwd_last_change
242
+ else:
243
+ # It's a timestamp (integer), convert to datetime
244
+ last_change = datetime.fromtimestamp(user.pwd_last_change, timezone.utc)
245
+
246
+ # Get current time with UTC timezone for proper comparison
247
+ current_time = datetime.now(timezone.utc)
248
+
249
+ # Calculate the expiration time based on the password rotation setting
250
+ expiration_time = last_change + timedelta(
251
+ days=int(current_app.config["PWD_ROTATION_TIME"])
252
+ )
253
+
254
+ # Compare the timezone-aware datetimes
255
+ if expiration_time < current_time:
238
256
  return True
239
257
  return False
240
258
 
@@ -13,6 +13,7 @@ from cornflow.shared.const import ALL_DEFAULT_ROLES
13
13
  from cornflow.shared.exceptions import InvalidUsage, ObjectDoesNotExist, NoPermission
14
14
 
15
15
 
16
+
16
17
  class BaseMetaResource(Resource, MethodResource):
17
18
  """
18
19
  The base resource from all methods inherit from.
@@ -175,11 +176,18 @@ class BaseMetaResource(Resource, MethodResource):
175
176
  METHODS USED FOR ACTIVATING / DISABLING RECORDS IN CASE WE DO NOT WANT TO DELETE THEM STRAIGHT AWAY
176
177
  """
177
178
 
178
- def disable_detail(self):
179
+ def disable_detail(self, idx):
179
180
  """
180
- Method not implemented yet
181
+ Method to DISABLE an object from the database
182
+
183
+ :param idx: the idx which identifies the object
184
+ :return: the object and a status code.
181
185
  """
182
- raise NotImplemented
186
+ row = self.data_model.query.get(idx)
187
+ if row is None:
188
+ raise ObjectDoesNotExist(f"Object with id {idx} not found.")
189
+ row.disable()
190
+ return {"message": "Object marked as disabled"}, 200
183
191
 
184
192
  def activate_detail(self, **kwargs):
185
193
  """