cornflow 1.1.4__py3-none-any.whl → 1.1.5__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.
cornflow/app.py CHANGED
@@ -1,6 +1,7 @@
1
1
  """
2
2
  Main file with the creation of the app logic
3
3
  """
4
+
4
5
  # Full imports
5
6
  import os
6
7
  import click
@@ -13,6 +14,8 @@ from flask_cors import CORS
13
14
  from flask_migrate import Migrate
14
15
  from flask_restful import Api
15
16
  from logging.config import dictConfig
17
+ from werkzeug.middleware.dispatcher import DispatcherMiddleware
18
+ from werkzeug.exceptions import NotFound
16
19
 
17
20
  # Module imports
18
21
  from cornflow.commands import (
@@ -112,6 +115,11 @@ def create_app(env_name="development", dataconn=None):
112
115
  app.cli.add_command(register_deployed_dags)
113
116
  app.cli.add_command(register_dag_permissions)
114
117
 
118
+ if app.config["APPLICATION_ROOT"] != "/" and app.config["EXTERNAL_APP"] == 0:
119
+ app.wsgi_app = DispatcherMiddleware(
120
+ NotFound(), {app.config["APPLICATION_ROOT"]: app.wsgi_app}
121
+ )
122
+
115
123
  return app
116
124
 
117
125
 
cornflow/config.py CHANGED
@@ -5,6 +5,12 @@ from apispec.ext.marshmallow import MarshmallowPlugin
5
5
 
6
6
 
7
7
  class DefaultConfig(object):
8
+ """
9
+ Default configuration class
10
+ """
11
+
12
+ APPLICATION_ROOT = os.getenv("APPLICATION_ROOT", "/")
13
+ EXTERNAL_APP = int(os.getenv("EXTERNAL_APP", 0))
8
14
  SERVICE_NAME = os.getenv("SERVICE_NAME", "Cornflow")
9
15
  SECRET_TOKEN_KEY = os.getenv("SECRET_KEY")
10
16
  SECRET_BI_KEY = os.getenv("SECRET_BI_KEY")
@@ -22,6 +28,11 @@ class DefaultConfig(object):
22
28
  SIGNUP_ACTIVATED = int(os.getenv("SIGNUP_ACTIVATED", 1))
23
29
  CORNFLOW_SERVICE_USER = os.getenv("CORNFLOW_SERVICE_USER", "service_user")
24
30
 
31
+ # If service user is allow to log with username and password
32
+ SERVICE_USER_ALLOW_PASSWORD_LOGIN = int(
33
+ os.getenv("SERVICE_USER_ALLOW_PASSWORD_LOGIN", 1)
34
+ )
35
+
25
36
  # Open deployment (all dags accessible to all users)
26
37
  OPEN_DEPLOYMENT = os.getenv("OPEN_DEPLOYMENT", 1)
27
38
 
@@ -84,14 +95,17 @@ class DefaultConfig(object):
84
95
 
85
96
 
86
97
  class Development(DefaultConfig):
87
-
88
- """ """
98
+ """
99
+ Configuration class for development
100
+ """
89
101
 
90
102
  ENV = "development"
91
103
 
92
104
 
93
105
  class Testing(DefaultConfig):
94
- """ """
106
+ """
107
+ Configuration class for testing
108
+ """
95
109
 
96
110
  ENV = "testing"
97
111
  SQLALCHEMY_TRACK_MODIFICATIONS = False
@@ -109,8 +123,26 @@ class Testing(DefaultConfig):
109
123
  LOG_LEVEL = int(os.getenv("LOG_LEVEL", 10))
110
124
 
111
125
 
126
+ class TestingOpenAuth(Testing):
127
+ """
128
+ Configuration class for testing some edge cases with Open Auth login
129
+ """
130
+
131
+ AUTH_TYPE = 0
132
+
133
+
134
+ class TestingApplicationRoot(Testing):
135
+ """
136
+ Configuration class for testing with application root
137
+ """
138
+
139
+ APPLICATION_ROOT = "/test"
140
+
141
+
112
142
  class Production(DefaultConfig):
113
- """ """
143
+ """
144
+ Configuration class for production
145
+ """
114
146
 
115
147
  ENV = "production"
116
148
  SQLALCHEMY_TRACK_MODIFICATIONS = False
@@ -121,4 +153,10 @@ class Production(DefaultConfig):
121
153
  PROPAGATE_EXCEPTIONS = True
122
154
 
123
155
 
124
- app_config = {"development": Development, "testing": Testing, "production": Production}
156
+ app_config = {
157
+ "development": Development,
158
+ "testing": Testing,
159
+ "production": Production,
160
+ "testing-oauth": TestingOpenAuth,
161
+ "testing-root": TestingApplicationRoot,
162
+ }
@@ -38,6 +38,7 @@ class LoginBaseEndpoint(BaseMetaResource):
38
38
  def __init__(self):
39
39
  super().__init__()
40
40
  self.ldap_class = LDAPBase
41
+ self.user_role_association = UserRoleModel
41
42
 
42
43
  def log_in(self, **kwargs):
43
44
  """
@@ -136,58 +137,95 @@ class LoginBaseEndpoint(BaseMetaResource):
136
137
 
137
138
  return user
138
139
 
139
- def auth_oid_authenticate(self, token):
140
+ def auth_oid_authenticate(
141
+ self, token: str = None, username: str = None, password: str = None
142
+ ):
140
143
  """
141
- Method in charge of performing the log in with the token issued by an Open ID provider
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.
142
146
 
143
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
144
150
  :return: the user object or it raises an error if it has not been possible to log in
145
151
  :rtype: :class:`UserModel`
146
152
  """
147
- oid_provider = int(current_app.config["OID_PROVIDER"])
148
153
 
149
- client_id = current_app.config["OID_CLIENT_ID"]
150
- tenant_id = current_app.config["OID_TENANT_ID"]
151
- issuer = current_app.config["OID_ISSUER"]
154
+ if token:
152
155
 
153
- if client_id is None or tenant_id is None or issuer is None:
154
- raise ConfigurationError("The OID provider configuration is not valid")
156
+ oid_provider = int(current_app.config["OID_PROVIDER"])
155
157
 
156
- if oid_provider == OID_AZURE:
157
- decoded_token = self.auth_class().validate_oid_token(
158
- token, client_id, tenant_id, issuer, oid_provider
159
- )
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"]
160
161
 
161
- elif oid_provider == OID_GOOGLE:
162
- raise EndpointNotImplemented("The selected OID provider is not implemented")
163
- elif oid_provider == OID_NONE:
164
- raise EndpointNotImplemented("The OID provider configuration is not valid")
165
- else:
166
- raise EndpointNotImplemented("The OID provider configuration is not valid")
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")
167
164
 
168
- username = decoded_token["preferred_username"]
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
169
 
170
- user = self.data_model.get_one_object(username=username)
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
+ )
171
182
 
172
- if not user:
173
- current_app.logger.info(
174
- f"OpenID user {username} does not exist and is created"
175
- )
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", "")
176
187
 
177
- data = {"username": username, "email": username}
188
+ user = self.data_model.get_one_object(username=username)
178
189
 
179
- user = self.data_model(data=data)
180
- user.save()
190
+ if not user:
191
+ current_app.logger.info(
192
+ f"OpenID user {username} does not exist and is created"
193
+ )
181
194
 
182
- self.user_role_association(user.id)
195
+ data = {
196
+ "username": username,
197
+ "email": email,
198
+ "first_name": first_name,
199
+ "last_name": last_name,
200
+ }
183
201
 
184
- user_role = self.user_role_association(
185
- {"user_id": user.id, "role_id": int(current_app.config["DEFAULT_ROLE"])}
186
- )
202
+ user = self.data_model(data=data)
203
+ user.save()
187
204
 
188
- user_role.save()
205
+ user_role = self.user_role_association(
206
+ {
207
+ "user_id": user.id,
208
+ "role_id": int(current_app.config["DEFAULT_ROLE"]),
209
+ }
210
+ )
189
211
 
190
- return user
212
+ user_role.save()
213
+
214
+ return user
215
+ elif (
216
+ username
217
+ and password
218
+ and current_app.config["SERVICE_USER_ALLOW_PASSWORD_LOGIN"] == 1
219
+ ):
220
+
221
+ user = self.auth_db_authenticate(username, password)
222
+
223
+ if user.is_service_user():
224
+ return user
225
+ else:
226
+ raise InvalidUsage("Invalid request")
227
+ else:
228
+ raise InvalidUsage("Invalid request")
191
229
 
192
230
 
193
231
  def check_last_password_change(user):
cornflow/schemas/user.py CHANGED
@@ -1,12 +1,14 @@
1
1
  """
2
2
  This file contains the schemas used for the users defined in the application
3
3
  """
4
- from marshmallow import fields, Schema
4
+
5
+ from marshmallow import fields, Schema, validates_schema, ValidationError
5
6
  from .instance import InstanceSchema
6
7
 
7
8
 
8
9
  class UserSchema(Schema):
9
10
  """ """
11
+
10
12
  id = fields.Int(dump_only=True)
11
13
  first_name = fields.Str()
12
14
  last_name = fields.Str()
@@ -66,9 +68,23 @@ class LoginEndpointRequest(Schema):
66
68
  class LoginOpenAuthRequest(Schema):
67
69
  """
68
70
  This is the schema used by the login endpoint with Open ID protocol
71
+ Validates that either a token is provided, or both username and password are present
69
72
  """
70
73
 
71
- token = fields.Str(required=True)
74
+ token = fields.Str(required=False)
75
+ username = fields.Str(required=False)
76
+ password = fields.Str(required=False)
77
+
78
+ @validates_schema
79
+ def validate_fields(self, data, **kwargs):
80
+ if data.get("token") is None:
81
+ if not data.get("username") or not data.get("password"):
82
+ raise ValidationError(
83
+ "A token needs to be provided when using Open ID authentication"
84
+ )
85
+ else:
86
+ if data.get("username") or data.get("password"):
87
+ raise ValidationError("The login needs to be done with a token only")
72
88
 
73
89
 
74
90
  class SignupRequest(Schema):
@@ -14,6 +14,8 @@ from datetime import datetime, timedelta
14
14
  from flask import request, g, current_app, Request
15
15
  from functools import wraps
16
16
  from typing import Union, Tuple
17
+
18
+ from jwt import DecodeError
17
19
  from werkzeug.datastructures import Headers
18
20
 
19
21
  # Imports from internal modules
@@ -103,7 +105,8 @@ class Auth:
103
105
  )
104
106
 
105
107
  payload = {
106
- "exp": datetime.utcnow() + timedelta(hours=float(current_app.config["TOKEN_DURATION"])),
108
+ "exp": datetime.utcnow()
109
+ + timedelta(hours=float(current_app.config["TOKEN_DURATION"])),
107
110
  "iat": datetime.utcnow(),
108
111
  "sub": user_id,
109
112
  }
@@ -314,7 +317,10 @@ class Auth:
314
317
  :return: the key identifier
315
318
  :rtype: str
316
319
  """
317
- headers = jwt.get_unverified_header(token)
320
+ try:
321
+ headers = jwt.get_unverified_header(token)
322
+ except DecodeError as err:
323
+ raise InvalidCredentials("Token is not valid")
318
324
  if not headers:
319
325
  raise InvalidCredentials("Token is missing the headers")
320
326
  try:
@@ -346,9 +352,9 @@ class Auth:
346
352
  try:
347
353
  response = requests.get(discovery_url)
348
354
  response.raise_for_status()
349
- except requests.exceptions.HTTPError as error:
355
+ except requests.exceptions.HTTPError:
350
356
  raise CommunicationError(
351
- f"Error getting issuer discovery meta from {discovery_url}", error
357
+ f"Error getting issuer discovery meta from {discovery_url}"
352
358
  )
353
359
  return response.json()
354
360
 
@@ -2,6 +2,7 @@
2
2
  This file contains the different exceptions created to report errors and the handler that registers them
3
3
  on a flask REST API server
4
4
  """
5
+
5
6
  from flask import jsonify
6
7
  from webargs.flaskparser import parser
7
8
  from cornflow_client.constants import AirflowError
@@ -123,9 +124,11 @@ class ConfigurationError(InvalidUsage):
123
124
 
124
125
 
125
126
  INTERNAL_SERVER_ERROR_MESSAGE = "500 Internal Server Error"
126
- INTERNAL_SERVER_ERROR_MESSAGE_DETAIL = "The server encountered an internal error and was unable " \
127
- "to complete your request. Either the server is overloaded or " \
128
- "there is an error in the application."
127
+ INTERNAL_SERVER_ERROR_MESSAGE_DETAIL = (
128
+ "The server encountered an internal error and was unable "
129
+ "to complete your request. Either the server is overloaded or "
130
+ "there is an error in the application."
131
+ )
129
132
 
130
133
 
131
134
  def initialize_errorhandlers(app):
@@ -146,6 +149,7 @@ def initialize_errorhandlers(app):
146
149
  @app.errorhandler(InvalidData)
147
150
  @app.errorhandler(InvalidPatch)
148
151
  @app.errorhandler(ConfigurationError)
152
+ @app.errorhandler(CommunicationError)
149
153
  def handle_invalid_usage(error):
150
154
  """
151
155
  Method to handle the error given by the different exceptions.
@@ -187,10 +191,7 @@ def initialize_errorhandlers(app):
187
191
  status_code = error.code or status_code
188
192
  error_msg = f"{status_code} {error.name or INTERNAL_SERVER_ERROR_MESSAGE}"
189
193
  error_str = f"{error_msg}. {str(error.description or '') or INTERNAL_SERVER_ERROR_MESSAGE_DETAIL}"
190
- response_dict = {
191
- "message": error_msg,
192
- "error": error_str
193
- }
194
+ response_dict = {"message": error_msg, "error": error_str}
194
195
  response = jsonify(response_dict)
195
196
 
196
197
  elif app.config["ENV"] == "production":
@@ -202,7 +203,7 @@ def initialize_errorhandlers(app):
202
203
 
203
204
  response_dict = {
204
205
  "message": INTERNAL_SERVER_ERROR_MESSAGE,
205
- "error": INTERNAL_SERVER_ERROR_MESSAGE_DETAIL
206
+ "error": INTERNAL_SERVER_ERROR_MESSAGE_DETAIL,
206
207
  }
207
208
  response = jsonify(response_dict)
208
209
  else: