cornflow 2.0.0a11__py3-none-any.whl → 2.0.0a12__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.
- airflow_config/airflow_local_settings.py +1 -1
- cornflow/app.py +8 -3
- cornflow/cli/migrations.py +23 -3
- cornflow/cli/service.py +18 -18
- cornflow/cli/utils.py +16 -1
- cornflow/commands/dag.py +1 -1
- cornflow/config.py +13 -8
- cornflow/endpoints/__init__.py +8 -2
- cornflow/endpoints/alarms.py +66 -2
- cornflow/endpoints/data_check.py +53 -26
- cornflow/endpoints/execution.py +387 -132
- cornflow/endpoints/login.py +81 -63
- cornflow/endpoints/meta_resource.py +11 -3
- cornflow/migrations/versions/999b98e24225.py +34 -0
- cornflow/models/base_data_model.py +4 -32
- cornflow/models/execution.py +2 -3
- cornflow/models/meta_models.py +28 -22
- cornflow/models/user.py +7 -10
- cornflow/schemas/alarms.py +8 -0
- cornflow/schemas/execution.py +1 -1
- cornflow/schemas/query.py +2 -1
- cornflow/schemas/user.py +5 -20
- cornflow/shared/authentication/auth.py +201 -264
- cornflow/shared/const.py +3 -14
- cornflow/shared/databricks.py +5 -1
- cornflow/tests/const.py +1 -0
- cornflow/tests/custom_test_case.py +77 -26
- cornflow/tests/unit/test_actions.py +2 -2
- cornflow/tests/unit/test_alarms.py +55 -1
- cornflow/tests/unit/test_apiview.py +108 -3
- cornflow/tests/unit/test_cases.py +20 -29
- cornflow/tests/unit/test_cli.py +6 -5
- cornflow/tests/unit/test_commands.py +3 -3
- cornflow/tests/unit/test_dags.py +5 -6
- cornflow/tests/unit/test_executions.py +443 -123
- cornflow/tests/unit/test_instances.py +14 -2
- cornflow/tests/unit/test_instances_file.py +1 -1
- cornflow/tests/unit/test_licenses.py +1 -1
- cornflow/tests/unit/test_log_in.py +230 -207
- cornflow/tests/unit/test_permissions.py +8 -8
- cornflow/tests/unit/test_roles.py +48 -10
- cornflow/tests/unit/test_schemas.py +1 -1
- cornflow/tests/unit/test_tables.py +7 -7
- cornflow/tests/unit/test_token.py +19 -5
- cornflow/tests/unit/test_users.py +22 -6
- cornflow/tests/unit/tools.py +75 -10
- {cornflow-2.0.0a11.dist-info → cornflow-2.0.0a12.dist-info}/METADATA +16 -15
- {cornflow-2.0.0a11.dist-info → cornflow-2.0.0a12.dist-info}/RECORD +51 -51
- {cornflow-2.0.0a11.dist-info → cornflow-2.0.0a12.dist-info}/WHEEL +1 -1
- cornflow/endpoints/execution_databricks.py +0 -808
- {cornflow-2.0.0a11.dist-info → cornflow-2.0.0a12.dist-info}/entry_points.txt +0 -0
- {cornflow-2.0.0a11.dist-info → cornflow-2.0.0a12.dist-info}/top_level.txt +0 -0
cornflow/endpoints/login.py
CHANGED
@@ -1,16 +1,17 @@
|
|
1
1
|
"""
|
2
|
-
External endpoint for the user to
|
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
|
-
|
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
|
145
|
-
|
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
|
148
|
-
:param str username:
|
149
|
-
:param str password:
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
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
|
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
|
-
|
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
|
"""
|
@@ -0,0 +1,34 @@
|
|
1
|
+
"""
|
2
|
+
Rename dag_run_id column to run_id in executions table
|
3
|
+
|
4
|
+
Revision ID: 999b98e24225
|
5
|
+
Revises: 991b98e24225
|
6
|
+
Create Date: 2024-04-08 12:00:00.000000
|
7
|
+
|
8
|
+
"""
|
9
|
+
|
10
|
+
from alembic import op
|
11
|
+
import sqlalchemy as sa
|
12
|
+
|
13
|
+
|
14
|
+
# revision identifiers, used by Alembic.
|
15
|
+
revision = "abc123456789"
|
16
|
+
down_revision = "991b98e24225"
|
17
|
+
branch_labels = None
|
18
|
+
depends_on = None
|
19
|
+
|
20
|
+
|
21
|
+
def upgrade():
|
22
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
23
|
+
with op.batch_alter_table("executions", schema=None) as batch_op:
|
24
|
+
batch_op.alter_column("dag_run_id", new_column_name="run_id")
|
25
|
+
|
26
|
+
# ### end Alembic commands ###
|
27
|
+
|
28
|
+
|
29
|
+
def downgrade():
|
30
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
31
|
+
with op.batch_alter_table("executions", schema=None) as batch_op:
|
32
|
+
batch_op.alter_column("run_id", new_column_name="dag_run_id")
|
33
|
+
|
34
|
+
# ### end Alembic commands ###
|
@@ -1,14 +1,13 @@
|
|
1
1
|
"""
|
2
2
|
|
3
3
|
"""
|
4
|
-
|
4
|
+
|
5
5
|
from flask import current_app
|
6
6
|
from sqlalchemy import desc
|
7
7
|
from sqlalchemy.dialects.postgresql import JSON
|
8
8
|
from sqlalchemy.dialects.postgresql import TEXT
|
9
9
|
from sqlalchemy.ext.declarative import declared_attr
|
10
10
|
|
11
|
-
# Import from internal modules
|
12
11
|
from cornflow.models.meta_models import TraceAttributesModel
|
13
12
|
from cornflow.shared import db
|
14
13
|
from cornflow.shared.utils import hash_json_256
|
@@ -39,7 +38,7 @@ class BaseDataModel(TraceAttributesModel):
|
|
39
38
|
|
40
39
|
def __init__(self, data):
|
41
40
|
self.user_id = data.get("user_id")
|
42
|
-
self.data = data.get("data")
|
41
|
+
self.data = data.get("data")
|
43
42
|
self.data_hash = hash_json_256(self.data)
|
44
43
|
self.name = data.get("name")
|
45
44
|
self.description = data.get("description")
|
@@ -50,23 +49,16 @@ class BaseDataModel(TraceAttributesModel):
|
|
50
49
|
@classmethod
|
51
50
|
def get_all_objects(
|
52
51
|
cls,
|
53
|
-
user,
|
54
52
|
schema=None,
|
55
53
|
creation_date_gte=None,
|
56
54
|
creation_date_lte=None,
|
57
55
|
deletion_date_gte=None,
|
58
56
|
deletion_date_lte=None,
|
59
|
-
id=None,
|
60
57
|
update_date_gte=None,
|
61
58
|
update_date_lte=None,
|
62
|
-
user_name=None,
|
63
|
-
last_name=None,
|
64
|
-
email=None,
|
65
|
-
role_id=None,
|
66
|
-
url_rule=None,
|
67
|
-
description=None,
|
68
59
|
offset=0,
|
69
60
|
limit=10,
|
61
|
+
user=None,
|
70
62
|
):
|
71
63
|
"""
|
72
64
|
Query to get all objects from a user
|
@@ -77,13 +69,6 @@ class BaseDataModel(TraceAttributesModel):
|
|
77
69
|
:param string creation_date_lte: created_at needs to be smaller or equal to this
|
78
70
|
:param string deletion_date_gte: deletion_at needs to be larger or equal to this
|
79
71
|
:param string deletion_date_lte: deletion_at needs to be smaller or equal to this
|
80
|
-
:param string id: user id
|
81
|
-
:param string user_name: user first name
|
82
|
-
:param string last_name: user last name
|
83
|
-
:param string email: user email
|
84
|
-
:param int role_id: user role id
|
85
|
-
:param string url_rule: url_rule
|
86
|
-
:param string description: description
|
87
72
|
:param string update_date_gte: update_at needs to be larger or equal to this
|
88
73
|
:param string update_date_lte: update_at needs to be smaller or equal to this
|
89
74
|
:param int offset: query offset for pagination
|
@@ -114,20 +99,7 @@ class BaseDataModel(TraceAttributesModel):
|
|
114
99
|
if update_date_gte:
|
115
100
|
query = query.filter(cls.update_at >= update_date_gte)
|
116
101
|
if update_date_lte:
|
117
|
-
query = query.filter(cls.
|
118
|
-
if user_name:
|
119
|
-
query = query.filter(cls.user_name == user_name)
|
120
|
-
if last_name:
|
121
|
-
query = query.filter(cls.last_name == last_name)
|
122
|
-
if email:
|
123
|
-
query = query.filter(cls.email == email)
|
124
|
-
if role_id:
|
125
|
-
query = query.filter(cls.role_id == role_id)
|
126
|
-
if url_rule:
|
127
|
-
query = query.filter(cls.url_rule == url_rule)
|
128
|
-
if description:
|
129
|
-
query = query.filter(cls.description == description)
|
130
|
-
# if airflow they also return total_entries = query.count(), for some reason
|
102
|
+
query = query.filter(cls.update_at <= update_date_lte)
|
131
103
|
|
132
104
|
return query.order_by(desc(cls.created_at)).offset(offset).limit(limit).all()
|
133
105
|
|
cornflow/models/execution.py
CHANGED
@@ -55,7 +55,7 @@ class ExecutionModel(BaseDataModel):
|
|
55
55
|
db.String(256), db.ForeignKey("instances.id"), nullable=False
|
56
56
|
)
|
57
57
|
config = db.Column(JSON, nullable=False)
|
58
|
-
|
58
|
+
run_id = db.Column(db.String(256), nullable=True)
|
59
59
|
log_text = db.Column(TEXT, nullable=True)
|
60
60
|
log_json = db.Column(JSON, nullable=True)
|
61
61
|
state = db.Column(db.SmallInteger, default=DEFAULT_EXECUTION_CODE, nullable=False)
|
@@ -78,8 +78,7 @@ class ExecutionModel(BaseDataModel):
|
|
78
78
|
+ str(self.instance_id)
|
79
79
|
).encode()
|
80
80
|
).hexdigest()
|
81
|
-
|
82
|
-
self.dag_run_id = data.get("dag_run_id")
|
81
|
+
self.run_id = data.get("run_id")
|
83
82
|
self.state = data.get("state", DEFAULT_EXECUTION_CODE)
|
84
83
|
self.state_message = EXECUTION_STATE_MESSAGE_DICT[self.state]
|
85
84
|
self.config = data.get("config")
|
cornflow/models/meta_models.py
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
"""
|
2
2
|
This file contains the base abstract models from which the rest of the models inherit
|
3
3
|
"""
|
4
|
-
|
5
|
-
from datetime import datetime
|
4
|
+
|
5
|
+
from datetime import datetime, timezone
|
6
|
+
from typing import Dict, List
|
7
|
+
|
6
8
|
from flask import current_app
|
7
9
|
from sqlalchemy.exc import DBAPIError, IntegrityError
|
8
|
-
from typing import Dict, List
|
9
10
|
|
10
|
-
# Imports from internal modules
|
11
11
|
from cornflow.shared import db
|
12
12
|
from cornflow.shared.exceptions import InvalidData
|
13
13
|
|
@@ -33,7 +33,9 @@ class EmptyBaseModel(db.Model):
|
|
33
33
|
|
34
34
|
try:
|
35
35
|
db.session.commit()
|
36
|
-
current_app.logger.debug(
|
36
|
+
current_app.logger.debug(
|
37
|
+
f"Transaction type: {action}, performed correctly on {self}"
|
38
|
+
)
|
37
39
|
except IntegrityError as err:
|
38
40
|
db.session.rollback()
|
39
41
|
current_app.logger.error(f"Integrity error on {action} data: {err}")
|
@@ -99,7 +101,10 @@ class EmptyBaseModel(db.Model):
|
|
99
101
|
action = "bulk create"
|
100
102
|
try:
|
101
103
|
db.session.commit()
|
102
|
-
current_app.logger.debug(
|
104
|
+
current_app.logger.debug(
|
105
|
+
f"Transaction type: {action}, performed correctly on {cls}"
|
106
|
+
)
|
107
|
+
|
103
108
|
except IntegrityError as err:
|
104
109
|
db.session.rollback()
|
105
110
|
current_app.logger.error(f"Integrity error on {action} data: {err}")
|
@@ -120,7 +125,10 @@ class EmptyBaseModel(db.Model):
|
|
120
125
|
action = "bulk create update"
|
121
126
|
try:
|
122
127
|
db.session.commit()
|
123
|
-
current_app.logger.debug(
|
128
|
+
current_app.logger.debug(
|
129
|
+
f"Transaction type: {action}, performed correctly on {cls}"
|
130
|
+
)
|
131
|
+
|
124
132
|
except IntegrityError as err:
|
125
133
|
db.session.rollback()
|
126
134
|
current_app.logger.error(f"Integrity error on {action} data: {err}")
|
@@ -136,12 +144,7 @@ class EmptyBaseModel(db.Model):
|
|
136
144
|
return instances
|
137
145
|
|
138
146
|
@classmethod
|
139
|
-
def get_all_objects(
|
140
|
-
cls,
|
141
|
-
offset=0,
|
142
|
-
limit=None,
|
143
|
-
**kwargs
|
144
|
-
):
|
147
|
+
def get_all_objects(cls, offset=0, limit=None, **kwargs):
|
145
148
|
"""
|
146
149
|
Method to get all the objects from the database applying the filters passed as keyword arguments
|
147
150
|
|
@@ -154,7 +157,9 @@ class EmptyBaseModel(db.Model):
|
|
154
157
|
"""
|
155
158
|
if "user" in kwargs:
|
156
159
|
kwargs.pop("user")
|
157
|
-
query = cls.query.filter_by(**kwargs)
|
160
|
+
query = cls.query.filter_by(**kwargs)
|
161
|
+
if offset:
|
162
|
+
query = query.offset(offset)
|
158
163
|
if limit:
|
159
164
|
query = query.limit(limit)
|
160
165
|
return query
|
@@ -208,8 +213,8 @@ class TraceAttributesModel(EmptyBaseModel):
|
|
208
213
|
deleted_at = db.Column(db.DateTime, nullable=True)
|
209
214
|
|
210
215
|
def __init__(self):
|
211
|
-
self.created_at = datetime.
|
212
|
-
self.updated_at = datetime.
|
216
|
+
self.created_at = datetime.now(timezone.utc)
|
217
|
+
self.updated_at = datetime.now(timezone.utc)
|
213
218
|
self.deleted_at = None
|
214
219
|
|
215
220
|
def update(self, data):
|
@@ -220,11 +225,11 @@ class TraceAttributesModel(EmptyBaseModel):
|
|
220
225
|
:return: None
|
221
226
|
:rtype: None
|
222
227
|
"""
|
223
|
-
self.updated_at = datetime.
|
228
|
+
self.updated_at = datetime.now(timezone.utc)
|
224
229
|
super().update(data)
|
225
230
|
|
226
231
|
def pre_update(self, data):
|
227
|
-
self.updated_at = datetime.
|
232
|
+
self.updated_at = datetime.now(timezone.utc)
|
228
233
|
super().pre_update(data)
|
229
234
|
|
230
235
|
def disable(self):
|
@@ -234,7 +239,7 @@ class TraceAttributesModel(EmptyBaseModel):
|
|
234
239
|
:return: None
|
235
240
|
:rtype: None
|
236
241
|
"""
|
237
|
-
self.deleted_at = datetime.
|
242
|
+
self.deleted_at = datetime.now(timezone.utc)
|
238
243
|
db.session.add(self)
|
239
244
|
self.commit_changes("disabling")
|
240
245
|
|
@@ -245,7 +250,7 @@ class TraceAttributesModel(EmptyBaseModel):
|
|
245
250
|
:return: None
|
246
251
|
:rtype: None
|
247
252
|
"""
|
248
|
-
self.updated_at = datetime.
|
253
|
+
self.updated_at = datetime.now(timezone.utc)
|
249
254
|
self.deleted_at = None
|
250
255
|
db.session.add(self)
|
251
256
|
self.commit_changes("activating")
|
@@ -261,7 +266,7 @@ class TraceAttributesModel(EmptyBaseModel):
|
|
261
266
|
update_date_lte=None,
|
262
267
|
offset=0,
|
263
268
|
limit=None,
|
264
|
-
**kwargs
|
269
|
+
**kwargs,
|
265
270
|
):
|
266
271
|
"""
|
267
272
|
Method to get all the objects from the database applying the filters passed as keyword arguments
|
@@ -300,7 +305,8 @@ class TraceAttributesModel(EmptyBaseModel):
|
|
300
305
|
if creation_date_lte:
|
301
306
|
query = query.filter(cls.created_at <= creation_date_lte)
|
302
307
|
|
303
|
-
|
308
|
+
if offset:
|
309
|
+
query = query.offset(offset)
|
304
310
|
if limit:
|
305
311
|
query = query.limit(limit)
|
306
312
|
return query
|
cornflow/models/user.py
CHANGED
@@ -1,10 +1,11 @@
|
|
1
1
|
"""
|
2
2
|
This file contains the UserModel
|
3
3
|
"""
|
4
|
+
|
4
5
|
# Imports from external libraries
|
5
6
|
import random
|
6
7
|
import string
|
7
|
-
from datetime import datetime
|
8
|
+
from datetime import datetime, timezone, timedelta
|
8
9
|
|
9
10
|
# Imports from internal modules
|
10
11
|
from cornflow.models.meta_models import TraceAttributesModel
|
@@ -55,9 +56,7 @@ class UserModel(TraceAttributesModel):
|
|
55
56
|
pwd_last_change = db.Column(db.DateTime, nullable=True)
|
56
57
|
email = db.Column(db.String(128), nullable=False, unique=True)
|
57
58
|
|
58
|
-
user_roles = db.relationship(
|
59
|
-
"UserRoleModel", cascade="all,delete", backref="users"
|
60
|
-
)
|
59
|
+
user_roles = db.relationship("UserRoleModel", cascade="all,delete", backref="users")
|
61
60
|
|
62
61
|
instances = db.relationship(
|
63
62
|
"InstanceModel",
|
@@ -95,15 +94,14 @@ class UserModel(TraceAttributesModel):
|
|
95
94
|
self.first_name = data.get("first_name")
|
96
95
|
self.last_name = data.get("last_name")
|
97
96
|
self.username = data.get("username")
|
98
|
-
self.pwd_last_change = datetime.
|
97
|
+
self.pwd_last_change = datetime.now(timezone.utc)
|
99
98
|
# TODO: handle better None passwords that can be found when using ldap
|
100
99
|
check_pass, msg = check_password_pattern(data.get("password"))
|
101
100
|
if check_pass:
|
102
101
|
self.password = self.__generate_hash(data.get("password"))
|
103
102
|
else:
|
104
103
|
raise InvalidCredentials(
|
105
|
-
msg,
|
106
|
-
log_txt="Error while trying to create a new user. " + msg
|
104
|
+
msg, log_txt="Error while trying to create a new user. " + msg
|
107
105
|
)
|
108
106
|
|
109
107
|
check_email, msg = check_email_pattern(data.get("email"))
|
@@ -111,8 +109,7 @@ class UserModel(TraceAttributesModel):
|
|
111
109
|
self.email = data.get("email")
|
112
110
|
else:
|
113
111
|
raise InvalidCredentials(
|
114
|
-
msg,
|
115
|
-
log_txt="Error while trying to create a new user. " + msg
|
112
|
+
msg, log_txt="Error while trying to create a new user. " + msg
|
116
113
|
)
|
117
114
|
|
118
115
|
def update(self, data):
|
@@ -126,7 +123,7 @@ class UserModel(TraceAttributesModel):
|
|
126
123
|
if new_password:
|
127
124
|
new_password = self.__generate_hash(new_password)
|
128
125
|
data["password"] = new_password
|
129
|
-
data["pwd_last_change"] = datetime.
|
126
|
+
data["pwd_last_change"] = datetime.now(timezone.utc)
|
130
127
|
super().update(data)
|
131
128
|
|
132
129
|
def comes_from_external_provider(self):
|
cornflow/schemas/alarms.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
"""
|
2
2
|
This file contains the schemas used for the table alarms defined in the application None
|
3
3
|
"""
|
4
|
+
|
4
5
|
from marshmallow import fields, Schema
|
5
6
|
|
6
7
|
|
@@ -18,3 +19,10 @@ class AlarmsResponse(AlarmsPostRequest):
|
|
18
19
|
class QueryFiltersAlarms(Schema):
|
19
20
|
schema = fields.Str(required=False)
|
20
21
|
criticality = fields.Number(required=False)
|
22
|
+
|
23
|
+
|
24
|
+
class AlarmEditRequest(Schema):
|
25
|
+
name = fields.Str(required=False)
|
26
|
+
criticality = fields.Number(required=False)
|
27
|
+
description = fields.Str(required=False)
|
28
|
+
schema = fields.Str(required=False)
|
cornflow/schemas/execution.py
CHANGED
@@ -43,7 +43,7 @@ class ExecutionSchema(Schema):
|
|
43
43
|
name = fields.Str()
|
44
44
|
description = fields.Str()
|
45
45
|
schema = fields.Str(required=False)
|
46
|
-
|
46
|
+
run_id = fields.Str(required=False, dump_only=True)
|
47
47
|
config = fields.Nested(ConfigSchema, required=True)
|
48
48
|
data = fields.Raw(dump_only=True)
|
49
49
|
checks = fields.Raw(required=False, allow_none=True)
|
cornflow/schemas/query.py
CHANGED
@@ -1,13 +1,14 @@
|
|
1
1
|
"""
|
2
2
|
This file contains the schemas used to query the results of a GET request
|
3
3
|
"""
|
4
|
+
|
4
5
|
from marshmallow import fields, Schema
|
5
6
|
|
6
7
|
|
7
8
|
class BaseQueryFilters(Schema):
|
8
9
|
"""This is the schema of the base query arguments"""
|
9
10
|
|
10
|
-
limit = fields.Int(required=False, dump_default=
|
11
|
+
limit = fields.Int(required=False, dump_default=10)
|
11
12
|
offset = fields.Int(required=False, dump_default=0)
|
12
13
|
creation_date_gte = fields.DateTime(required=False)
|
13
14
|
creation_date_lte = fields.DateTime(required=False)
|