103103from  kolibri .core .serializers  import  HexOnlyUUIDField 
104104from  kolibri .core .tasks .exceptions  import  JobRunning 
105105from  kolibri .core .utils .pagination  import  ValuesViewsetPageNumberPagination 
106+ from  kolibri .core .utils .token_generator  import  TokenGenerator 
106107from  kolibri .core .utils .urls  import  reverse_path 
107108from  kolibri .plugins .app .utils  import  interface 
108109from  kolibri .utils .urls  import  validator 
@@ -1145,73 +1146,98 @@ def post(self, request):
11451146        return  Response ()
11461147
11471148
1148- @method_decorator ([ensure_csrf_cookie ], name = "dispatch" ) 
1149- class  SessionViewSet (viewsets .ViewSet ):
1150-     def  _check_os_user (self , request , username ):
1151-         auth_token  =  request .COOKIES .get (APP_AUTH_TOKEN_COOKIE_NAME )
1152-         if  auth_token :
1153-             try :
1154-                 user  =  FacilityUser .objects .get_or_create_os_user (auth_token )
1155-                 if  user  is  not   None  and  user .username  ==  username :
1156-                     return  user 
1157-             except  ValidationError  as  e :
1158-                 logger .error (e )
1149+ class  CreateSessionSerializer (serializers .Serializer ):
1150+     username  =  serializers .CharField (required = False , default = None )
1151+     user_id  =  HexOnlyUUIDField (required = False , default = None )
1152+     password  =  serializers .CharField (
1153+         default = "" ,
1154+         write_only = True ,
1155+         required = False ,
1156+         allow_blank = True ,
1157+     )
1158+     facility  =  serializers .PrimaryKeyRelatedField (
1159+         queryset = Facility .objects .all (),
1160+         default = Facility .get_default_facility ,
1161+         required = False ,
1162+     )
1163+     auth_token  =  serializers .CharField (required = False , default = None )
11591164
1160-     def  create (self , request ):
1161-         username  =  request .data .get ("username" , "" )
1162-         password  =  request .data .get ("password" , "" )
1163-         facility_id  =  request .data .get ("facility" , None )
1165+     def  validate (self , attrs ):
1166+         username  =  attrs .get ("username" )
1167+         password  =  attrs .get ("password" )
1168+         facility  =  attrs .get ("facility" )
1169+         user_id  =  attrs .get ("user_id" )
1170+         auth_token  =  attrs .get ("auth_token" )
11641171
1165-         # Only enforce this when running in an app 
1166-         if  (
1167-             interface .enabled 
1168-             and  not  allow_other_browsers_to_connect ()
1169-             and  not  valid_app_key_on_request (request )
1170-         ):
1171-             return  Response (
1172-                 [{"id" : error_constants .INVALID_CREDENTIALS , "metadata" : {}}],
1173-                 status = status .HTTP_401_UNAUTHORIZED ,
1174-             )
1172+         request  =  self .context .get ("request" )
11751173
11761174        user  =  None 
1175+ 
1176+         # OS User authentication 
11771177        if  interface .enabled  and  valid_app_key_on_request (request ):
11781178            # If we are in app context, then try to get the automatically created OS User 
11791179            # if it matches the username, without needing a password. 
11801180            user  =  self ._check_os_user (request , username )
1181+ 
1182+         # user_id/auth_token authentication 
1183+         if  user  is  None  and  user_id  and  auth_token :
1184+             if  TokenGenerator ().check_token (user_id , auth_token ):
1185+                 user  =  FacilityUser .objects .filter (
1186+                     id = user_id , facility = facility 
1187+                 ).first ()
1188+ 
1189+         # username/password authentication 
11811190        if  user  is  None :
11821191            # Otherwise attempt full authentication 
1183-             user  =  authenticate (
1184-                 username = username , password = password , facility = facility_id 
1185-             )
1192+             user  =  authenticate (username = username , password = password , facility = facility )
1193+ 
11861194        if  user  is  not   None  and  user .is_active :
1187-             # Correct password, and the user is marked "active" 
1188-             login (request , user )
1189-             # Success! 
1190-             return  self .get_session_response (request )
1191-         # Otherwise, try to give a helpful error message 
1195+             attrs ["user" ] =  user 
1196+             return  attrs 
1197+ 
1198+         # Otherwise, throw a meaningful validation error 
1199+         self ._throw_validation_error (username , password , facility )
1200+ 
1201+     def  _check_os_user (self , request , username ):
1202+         app_auth_token  =  request .COOKIES .get (APP_AUTH_TOKEN_COOKIE_NAME )
1203+         if  app_auth_token :
1204+             try :
1205+                 user  =  FacilityUser .objects .get_or_create_os_user (app_auth_token )
1206+                 if  user  is  not   None  and  user .username  ==  username :
1207+                     return  user 
1208+             except  ValidationError  as  e :
1209+                 logger .error (e )
1210+ 
1211+     def  _throw_validation_error (self , username , password , facility ):
1212+         """ 
1213+         Throw a RestValidationError with a helpful error message 
1214+         depending on what went wrong with authentication. 
1215+         """ 
11921216        # Find the FacilityUser we're looking for 
11931217        try :
11941218            unauthenticated_user  =  FacilityUser .objects .get (
1195-                 username__iexact = username , facility = facility_id 
1219+                 username__iexact = username , facility = facility 
11961220            )
11971221        except  (ValueError , ObjectDoesNotExist ):
1198-             return  Response (
1199-                 [
1200-                     {
1201-                         "id" : error_constants .NOT_FOUND ,
1202-                         "metadata" : {
1203-                             "field" : "username" ,
1204-                             "message" : "Username not found." ,
1205-                         },
1206-                     }
1207-                 ],
1208-                 status = status .HTTP_400_BAD_REQUEST ,
1222+             raise  RestValidationError (
1223+                 detail = {
1224+                     "username" : [
1225+                         {
1226+                             "id" : error_constants .NOT_FOUND ,
1227+                             "metadata" : {
1228+                                 "field" : "username" ,
1229+                                 "message" : "Username not found." ,
1230+                             },
1231+                         }
1232+                     ]
1233+                 }
12091234            )
12101235        except  FacilityUser .MultipleObjectsReturned :
12111236            # Handle case of multiple matching usernames 
12121237            unauthenticated_user  =  FacilityUser .objects .filter (
1213-                 username__exact = username , facility = facility_id 
1238+                 username__exact = username , facility = facility 
12141239            ).first ()
1240+ 
12151241        if  unauthenticated_user .password  ==  NOT_SPECIFIED  and  not  hasattr (
12161242            unauthenticated_user , "os_user" 
12171243        ):
@@ -1220,42 +1246,92 @@ def create(self, request):
12201246            # it is enabled again. 
12211247            # Alternatively, they may have been created as an OSUser for automatic login with an 
12221248            # authentication token. If this is the case, then we do not allow for the password to be set. 
1223-             return  Response (
1224-                 [
1225-                     {
1226-                         "id" : error_constants .PASSWORD_NOT_SPECIFIED ,
1227-                         "metadata" : {
1228-                             "field" : "password" ,
1229-                             "message" : "Username is valid, but password needs to be set before login." ,
1230-                         },
1231-                     }
1232-                 ],
1233-                 status = status .HTTP_400_BAD_REQUEST ,
1249+             raise  RestValidationError (
1250+                 detail = {
1251+                     "password" : [
1252+                         {
1253+                             "id" : error_constants .PASSWORD_NOT_SPECIFIED ,
1254+                             "metadata" : {
1255+                                 "field" : "password" ,
1256+                                 "message" : "Username is valid, but password needs to be set before login." ,
1257+                             },
1258+                         }
1259+                     ]
1260+                 }
12341261            )
1262+ 
12351263        if  (
12361264            not  password 
12371265            and  FacilityUser .objects .filter (
1238-                 username__iexact = username , facility = facility_id 
1266+                 username__iexact = username , facility = facility 
12391267            ).exists ()
12401268        ):
12411269            # Password was missing, but username is valid, prompt to give password 
1242-             return  Response (
1243-                 [
1270+             raise  RestValidationError (
1271+                 detail = {
1272+                     "password" : [
1273+                         {
1274+                             "id" : error_constants .MISSING_PASSWORD ,
1275+                             "metadata" : {
1276+                                 "field" : "password" ,
1277+                                 "message" : "Username is valid, but password is missing." ,
1278+                             },
1279+                         }
1280+                     ]
1281+                 }
1282+             )
1283+ 
1284+         # If no other error message was raised, then throw a generic invalid credentials message 
1285+         raise  RestValidationError (
1286+             detail = {
1287+                 "non_field_errors" : [
12441288                    {
1245-                         "id" : error_constants .MISSING_PASSWORD ,
1246-                         "metadata" : {
1247-                             "field" : "password" ,
1248-                             "message" : "Username is valid, but password is missing." ,
1249-                         },
1289+                         "id" : error_constants .INVALID_CREDENTIALS ,
1290+                         "metadata" : {},
12501291                    }
1251-                 ],
1252-                 status = status .HTTP_400_BAD_REQUEST ,
1292+                 ]
1293+             }
1294+         )
1295+ 
1296+ 
1297+ @method_decorator ([ensure_csrf_cookie ], name = "dispatch" ) 
1298+ class  SessionViewSet (viewsets .ViewSet ):
1299+     def  create (self , request ):
1300+         # Only enforce this when running in an app 
1301+         if  (
1302+             interface .enabled 
1303+             and  not  allow_other_browsers_to_connect ()
1304+             and  not  valid_app_key_on_request (request )
1305+         ):
1306+             return  Response (
1307+                 [{"id" : error_constants .INVALID_CREDENTIALS , "metadata" : {}}],
1308+                 status = status .HTTP_401_UNAUTHORIZED ,
12531309            )
1254-         # Respond with error 
1255-         return  Response (
1256-             [{"id" : error_constants .INVALID_CREDENTIALS , "metadata" : {}}],
1257-             status = status .HTTP_401_UNAUTHORIZED ,
1310+ 
1311+         serializer  =  CreateSessionSerializer (
1312+             data = request .data , context = {"request" : request }
12581313        )
1314+         if  serializer .is_valid ():
1315+             user  =  serializer .validated_data ["user" ]
1316+             login (request , user )
1317+             return  self .get_session_response (request )
1318+ 
1319+         errors  =  serializer .errors 
1320+         return  self ._get_error_response (errors )
1321+ 
1322+     def  _get_error_response (self , errors ):
1323+         """ 
1324+         Helper method to construct a standardized error response. 
1325+         """ 
1326+         error_list  =  []
1327+         response_status  =  status .HTTP_400_BAD_REQUEST 
1328+         for  field , field_errors  in  errors .items ():
1329+             for  error  in  field_errors :
1330+                 error_list .append (error )
1331+                 if  error .get ("id" ) ==  error_constants .INVALID_CREDENTIALS :
1332+                     response_status  =  status .HTTP_401_UNAUTHORIZED 
1333+ 
1334+         return  Response (error_list , status = response_status )
12591335
12601336    def  destroy (self , request , pk = None ):
12611337        logout (request )
0 commit comments