1111from django .contrib .auth .models import AbstractUser
1212from django .core .exceptions import ValidationError
1313from django .core .validators import FileExtensionValidator
14+ from packaging .requirements import Requirement , InvalidRequirement
1415from rest_framework import permissions , serializers
1516from rest_framework .decorators import api_view , permission_classes
1617from rest_framework .request import Request
1718from rest_framework .response import Response
1819
1920from api .models import Program
2021from api .use_cases .functions .upload import UploadFunctionData , FunctionUploadUseCase
21- from api .utils import encrypt_env_vars
22+ from api .utils import encrypt_env_vars , check_whitelisted
2223from api .v1 .endpoint_decorator import endpoint
2324from api .v1 .endpoint_handle_exceptions import endpoint_handle_exceptions
2425from api .v1 .views .serializer_utils import SanitizedCharField
@@ -31,7 +32,6 @@ def get_upload_path(instance, filename):
3132 return f"{ instance .author .username } /{ instance .id } /{ filename } "
3233
3334
34- # TODO: review validation inheritances such as dependencies
3535class InputSerializer (serializers .Serializer ):
3636 """
3737 Validates and sanitizes query parameters for the jobs list endpoint.
@@ -71,13 +71,67 @@ def _normalize_dependency(self, raw_dependency):
7171
7272 return dependency_name + dependency_version
7373
74+ def _parse_dependency (self , dep : Any ):
75+ if not isinstance (dep , dict ) and not isinstance (dep , str ):
76+ raise ValidationError (
77+ "'dependencies' should be an array with strings or dict."
78+ )
79+
80+ if isinstance (dep , str ):
81+ dep_string = dep
82+ else :
83+ dep_name = list (dep .keys ())
84+ if len (dep_name ) > 1 or len (dep_name ) == 0 :
85+ raise ValidationError (
86+ "'dependencies' should be an array with dict containing one dependency only."
87+ )
88+ dep_name = str (dep_name [0 ])
89+ dep_version = str (list (dep .values ())[0 ])
90+
91+ # if starts with a number then prefix ==
92+ try :
93+ if int (dep_version [0 ]) >= 0 :
94+ dep_version = f"=={ dep_version } "
95+ except ValueError :
96+ pass
97+
98+ dep_string = dep_name + dep_version
99+
100+ requirement = Requirement (dep_string )
101+ req_specifier_list = list (requirement .specifier )
102+ req_specifier_first = next (iter (req_specifier_list ), None )
103+
104+ if len (req_specifier_list ) > 1 or (
105+ req_specifier_first and req_specifier_first .operator != "=="
106+ ):
107+ raise ValidationError (
108+ "'dependencies' needs one fixed version using the '==' operator."
109+ )
110+
111+ return requirement
112+
74113 def validate_dependencies (self , value : list ):
75114 """
76115 Validates the function title
77116 """
78117 if not isinstance (value , list ):
79118 raise serializers .ValidationError ("'dependencies' should be a list." )
80119
120+ if len (value ) == 0 :
121+ return []
122+
123+ try :
124+ required_deps = [self ._parse_dependency (dep ) for dep in value ]
125+ except InvalidRequirement as invalid_requirement :
126+ raise ValidationError (
127+ "Error while parsing dependencies."
128+ ) from invalid_requirement
129+
130+ try :
131+ check_whitelisted (required_deps )
132+ except ValueError as value_error :
133+ raise ValidationError (value_error .args [0 ]) from value_error
134+
81135 return [self ._normalize_dependency (dep ) for dep in value ]
82136
83137 def validate_artifact (self , value ):
@@ -121,7 +175,8 @@ def validate(self, attrs):
121175 else :
122176 if len (title_split ) > 1 :
123177 raise ValidationError (
124- "Qiskit Function title is malformed. It cannot contain title with slash and provider."
178+ "Qiskit Function title is malformed. "
179+ "It cannot contain title with slash and provider."
125180 )
126181
127182 if env_vars :
0 commit comments