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.
- airflow_config/airflow_local_settings.py +1 -1
- cornflow/app.py +8 -3
- cornflow/cli/migrations.py +23 -3
- cornflow/cli/service.py +17 -15
- cornflow/cli/utils.py +16 -1
- cornflow/config.py +10 -11
- cornflow/endpoints/__init__.py +7 -1
- cornflow/endpoints/alarms.py +66 -2
- cornflow/endpoints/login.py +81 -63
- cornflow/endpoints/meta_resource.py +11 -3
- cornflow/models/base_data_model.py +4 -32
- cornflow/models/meta_models.py +28 -22
- cornflow/models/user.py +7 -10
- cornflow/schemas/alarms.py +8 -0
- 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/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_dags.py +5 -6
- 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_tables.py +7 -7
- cornflow/tests/unit/test_token.py +19 -5
- cornflow/tests/unit/test_users.py +22 -6
- {cornflow-1.1.5.dist-info → cornflow-1.2.0.dist-info}/METADATA +13 -12
- {cornflow-1.1.5.dist-info → cornflow-1.2.0.dist-info}/RECORD +40 -40
- {cornflow-1.1.5.dist-info → cornflow-1.2.0.dist-info}/WHEEL +1 -1
- {cornflow-1.1.5.dist-info → cornflow-1.2.0.dist-info}/entry_points.txt +0 -0
- {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
|
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
|
-
|
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,
|
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)
|
cornflow/cli/migrations.py
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
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
|
-
|
53
|
-
|
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",
|
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") ==
|
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
|
-
|
133
|
+
Migrate(app=app, db=db, directory=path)
|
133
134
|
upgrade()
|
134
135
|
access_init_command(verbose=False)
|
135
|
-
if auth ==
|
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 ==
|
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 =
|
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
|
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
|
63
|
-
OID_PROVIDER = os.getenv("OID_PROVIDER"
|
64
|
-
|
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="
|
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
|
-
|
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
|
}
|
cornflow/endpoints/__init__.py
CHANGED
@@ -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/",
|
cornflow/endpoints/alarms.py
CHANGED
@@ -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)
|
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
|
"""
|