def prepare_user(self): ''' Goal: prepare a user service instance - by creating one. Returns user:UserInstance Exceptions: Raises: ValueError: "Coudn't prepare a test user." is caught in cases when one tries to create a user, which already exists -> excepted with a discharge of a user and a resubmission. "Still cound't preare a test user." - raised when the discharge,resubmission hasn't helped. ''' user=UserService() payload={ 'user_data':{ 'password':'******', **self.dummy_identification_payload['signup'] }, 'key_data':{ 'public_key':5, 'private_key':{'iv':'str','data':'str'} } } try: assert user.signup(**payload), ValueError('Couldn\'t prepare a test user.') except: self.discharge_user() assert user.signup(**payload), ValueError('Still couldn\'t prepare a test user.') return user
def test_invalid_login_invalid_activity(self): ''' Goal: test a request aimed at the signup, providing identification data for already existing user. Verifies, that the verification token would be denied according to the activity state update. ''' preaccess_token = self.preaccess_token('login') self.prepare_user() user = UserService(username='******') verification_token = self.verification_token( identification_data=self.dummy_identification_payload['login'], preaccess=preaccess_token.value, activity=user.activity_state) time.sleep(5) user.update_activity() client = self.app.test_client() response = client.post( '/api/tokens/grant', headers={'Authorization': f'Bearer {verification_token.value}'}, json=self.dummy_auth_payload['login']) self.assertEqual(response.status_code, 401)
def discharge_user(self): ''' Goal: remove/delete a test user instance. Returns:None Exceptions: Raises: ValueError - in cases,when one tries to discharge a user instance, which is not related to any user. ''' user=UserService(username='******') assert user.remove(), ValueError('Coudn\'t find a test user to remove.')
def test_valid_login(self): ''' Goal: test a request aimed at the signup, providing valid json,header /w absent user. ''' self.prepare_user() preaccess_token = self.preaccess_token('login') verification_token = self.verification_token( identification_data=self.dummy_identification_payload['login'], preaccess=preaccess_token.value, activity=(UserService(username='******')).activity_state) client = self.app.test_client() response = client.post( '/api/tokens/grant', headers={'Authorization': f'Bearer {verification_token.value}'}, json=self.dummy_auth_payload['login']) self.assertEqual(response.status_code, 201) self.assertIn('grant_token', response.json) self.discharge_user()
def validate(*args,**kwargs): #Exceptions assert len(args)>1, Exception('No headers has been found.') assert isinstance((headers:=args[1]),dict), TypeError('Headers must be a dictionary.') assert (True if (authorization:=kwargs.pop('authorization',{token_type:{}}))=={token_type:{}} else (True if authorization.get(token_type,None) is None else False)), Exception(f'"{token_type}" is already in the authorization chain.') #Validation #Initialize lambda functions for the 1.: search = lambda head,raw: match.group() if (match:=re.search(f'(?<={head})([^\s]+)',raw)) else None token_raw_data = lambda l,d: search('Bearer ',d) if l=='Authorization' else (search(f'{token_type}_token=',d) if l=='Cookie' else d) locate = lambda l,h: {'location':l, 'object':Token(raw_data=token_raw_data(l,raw) ) } if (raw:=h.get(l)) else {'location':l,'object':None} #Initialize lambda functions for the 3.2: find_case = lambda cases: next(iter(tupled)) if (tupled:=tuple(filter(lambda case: case is not None,cases))) else None map_cases = lambda **credentials: map(lambda identification: case if (case:=UserService(**{identification:credentials[identification]})).id else None,credentials) resolve_user = lambda **identification: find_case(map_cases(**identification)) either = auth_field_case if (auth_field_case:=locate('Authorization',headers)).get('object') else (cookie_field_case if (cookie_field_case:=locate('Cookie',headers)).get('object') else {'location':None,'object':None}) #Validation:1. if (token:=locate(location,headers) if location else either)['object'] and token['object'].is_valid and token['object']['token_type']==token_type: #Validation:2. #Validation:3 ~> valid_ownership|valid_verification|valid_preaccess. valid_preaccess = lambda t : {'valid':True,'status_code':200} if any(True for route in ('signup','login') if t['object']['route']==route) else {'valid':False,'status_code':401} valid_verification = lambda t : {'valid':True,'status_code':200} if \ ( None!=t['object']['activity'] ==\ ( (None if (resolve_user(username=(identification:=t['object']['identification_data'])['username'],email=identification['email'])) else 0 )\ if (preaccess:=Token(raw_data=t['object']['preaccess']))['route']=='signup' else\ (resolved_user.activity_state if (resolved_user:=resolve_user(username=(identification:=t['object']['identification_data']['identification']),email=identification)) else None) ) \ )\ else {'valid':False,'status_code':401} valid_ownership = lambda t: {'valid':True,'status_code':200, 'owner':owner} if\ None != (owner:=UserService(id=t['object']['user_id'])).activity_state == t['object']['activity']\ else {'valid':False,'status_code':401, 'owner':None} authorization[token_type] = {'token':token,\ **(valid_ownership(token) if token_type in ('grant','access','confirmation')\ else (valid_verification(token) if token_type=='verification'\ else (valid_preaccess(token) if token_type=='preaccess'\ else {'valid':True,'status_code':200}) ) )}
def test_invalid_login_absent(self): ''' Goal: test a request aimed at the login, providing data for a user that doesn't exist , has been discharged after the generation of a verification token. Verifies, that the verification token would be denied according to the activity state update or better say absense of such. ''' preaccess_token = self.preaccess_token('login') self.prepare_user() verification_token = self.verification_token( identification_data=self.dummy_identification_payload['login'], preaccess=preaccess_token.value, activity=(UserService(username='******')).activity_state) self.discharge_user() client = self.app.test_client() response = client.post( '/api/tokens/grant', headers={'Authorization': f'Bearer {verification_token.value}'}, json=self.dummy_auth_payload['login']) self.assertEqual(response.status_code, 401)
def accept(self, headers, data, **kwargs): ''' Goal : Mail verification_token to an email , based on the provided <identification_data> - thus allowing a user to proceed to verify themselves. Arguments:headers:dict, data:dict, kwargs:key-word-argument. headers : meant to contain all headers data , in this particular case - a cookie field as a sign of authority, must contain a preaccess_token = {route:["signup"/"login"],token_type:"preaccess",dnt}. Note: This argument is used in the authorized decorator - to perform proper authorization process, the result of which is stored in the kwargs. To know more about the authorized decorator - view a separate documentation for the authorized method in the chathouse/utilities/security/validation/headers.py. data : meant to store any data that's passed into the request - json body. The could be 2 cases: signup:<identification_data>:{email:str,username:str,name:str,about:str} or login:<identification_data>={identification:str} Note: This argument is used in the verification process of the incoming request data, which is handled by the derived class template - which on itself is a result of create_a_template function, meant to return a proper template instance according to the route. To know more about the create_a_template - view a separate documentation for the create_a_template function in the ./template.py. kwargs: meant to store any data passed into the accept method, from the initial requrest, up to the authorization steps. In particular stores the authorization key , which on it's own stores shall store more nested information related to a token_type. In this instance the token_type is the preaccess one - so kwargs shall store:{ authorization:{ preaccess:{ valid:bool, status:int, token:{ object:None|Token, location:str } } } } Full verification: 0.Verify the preaccess_token; If invalid respond with 401; Otherwise resume with the next steps. 1.Extract the route value from the token and create a proper template. 2.Validate the data against the proper template. 3.Resolve the user's email and activity using the resolve function, which returns a : dictionary of {'email':str,'activity':int} | None , based on the route and identification data. If the email has been resolved : Proceed to send the email and respond accordingly with 201. Otherwise: Respond accordingly with 400. Lambda functions: [Note each lambda function shall be called from bottom up.]s find_case: Goal: return the very first case of existing UserService from the submited search cases. Arguments: cases:map of cases:UserService|None. Returns: very first UserService instance. [Note at this point there must be a UserService instance, with the help of explicit validation with any(). But if there isn't one - error will not arise due to the tuple verification] map_cases: Goal:create a map object of iterated UserService instances for email and username cases. Iteration itself checks if the instance has an id -> returns the UserService if instance has an id else None. Arguments: data:key word argument. Returns: map object. resolve: Goal: resolve email based on the identification data , by verify if the such data is appropriate according to a certain route. If the route is signup: Get respective map_cases, then : If not even one case (not any function) is valid return: {'email':as email from the provided data, 'activity': as 0} Otherwise return None Otherwise: Get respective map_cases, then convert cases into a tuple: If there is a case, which isn't None - then return the very next element of the filtered cases , where every case must not be a None - thus returning: {'email':resolved user's email, 'activity':current activity_dnt state of the resolved user}. Otherwise return None. Returns: {email:str,activir:int}|None. Generation: verification_token={identification_data:<identification_data>,token_type:"verification", activity:<current_activity_dnt_state>, dnt,preaccess:<preaccess_token>}. Returns: If verified: 201,{success:True,email_sent:True} Otherwise: Failed to verify the token: 401,{success:False,message:"Unauthorized."} Failed to validate the json body: 400,{success:False,message:"Incorrect json data."} ''' #Code #Exceptions assert all( map(lambda argument: isinstance(argument, dict), (headers, data)) ), TypeError( 'Arguments , such as : headers and data - must be dictionaries') #Lambda functions find_case = lambda cases: next(iter(tupled)) if (tupled := (tuple( filter(lambda case: case is not None, cases)))) else None map_cases = lambda **credentials: map( lambda identification: case if (case := UserService(**{ identification: credentials[identification] })).id else None, credentials) resolve = lambda route,data: ( {'email':data['email'],'activity':0} if not any(map_cases(email=data['email'],username=data['username'])) else None ) if route=='signup' \ else ({'email':case.email, 'activity': case.activity_state } if any( (cases:=tuple( map_cases(email=data['identification'],username=data['identification']) ) ) ) and (case:=find_case(cases)) else None) #Step 0. if not kwargs['authorization']['preaccess']['valid']: return { 'success': 'False', 'message': 'Unauthorized!', 'reason': 'Invalid preaccess token.' }, 401 #Step 1. template = create_a_template(route := kwargs['authorization'] ['preaccess']['token']['object']['route']) #Step 2. #Step 3. if template.validate(**data) and (resolution := resolve( route, (data := template.data))) is not None: verification_token = Token( payload_data={ 'identification_data': data, 'token_type': 'verification', 'activity': resolution['activity'], 'preaccess': kwargs['authorization']['preaccess']['token'] ['object'].value, 'exp': current_app.config['VERIFICATION_EXP'] }) MailService.send( recipients=[resolution['email']], body= f"There has been a request to {route}, using this email. If you wish to proceed with the verification , follow this url http://127.0.0.1:5000/verify/{verification_token.value} .\nOtherwise please ignore this email.", subject="Verification email.") return {'success': 'True', 'message': 'Email has been sent.'}, 201
def accept(self,headers,data,**kwargs): ''' Goal : establish/create a chat instance and join the chat on the behalf of the requester and the other participant, if provided. Arguments:headers:dict, data:dict, kwargs:key-word-argument. headers : meant to contain all headers data , in this particular case - an Authorization field as a sign of authority, must contain a "Bearer <token> , which is the access_token: access_token={user_id:int, token_type: "access":str, activity:int , dnt:float} Note: This argument is used in the authorized decorator - to perform proper authorization process, the result of which is stored in the kwargs. To know more about the authorized decorator - view a separate documentation for the authorized method in the chathouse/utilities/security/validation/headers.py. data : meant to store any data that's passed into the request - json body, there could 2 cases: With another participant: { name:str, participnat_id:<int> } or without one: { name:str } Note: This argument is used in the verification process of the incoming request data, which is handled by the derived class template - which on itself is a result of create_a_template function, meant to return a proper template instance according to the route. To know more about the create_a_template - view a separate documentation for the create_a_template function in the ./template.py. kwargs: meant to store any data passed into the accept method, from the initial requrest, up to the authorization steps. In particular stores the authorization key , which on it's own stores shall store more nested information related to a token_type. In this instance the token_type is the access one - so kwargs shall store:{ authorization:{ access:{ valid:bool, status:int, owner:UserService|None, token:{ object:None|Token, location:str|None } } } } Lambda functions: resolve: Goal: resolve a UserService instance with existing inner user instance, based on the provided provided identification. Arguments:credentials:key-word-argument. Expecting a key/value pair such as id/<int>. Actions: If there is a UserService instance with inner existing user instance: Such participant exists - return ther UserService instance Otherwise return None Returns: UserService instance | None Full verification and actions: 0.Verify the access_token , which on it's own - verifies ownership - makes sure of the existance of a user with the user_id - establishing a UserService, and verifies the provided activity with the current one related to the UserService : If 0. is invalid - disconnect the client. [Note, on the response the user reconnects ,after they try to reestablish a new access_token] Otherwise proceed to the next steps. 1.At this point the access_token is valid - validate the incoming data: Set up a template - using a custom create_template, which builds and returns a Template instance. Validate the data against the template: If the verification has been successful and: If the product of the validation / validated doesn't contain a participant_id set other_participant as None and resume the condition to end up successful. -> step 2. Otherwise If the product of the validation / validated contains a participant_id and the resolution has returned a UserService - thus has found such user - follow the next step -> step 2. 2. Owner starts a chat -> creates an instance of a ChatService , If everything has gone well: 2.1 On the behalf of each "soon to be participant" - join the chat. If at any point a UserService wasn't able to join the chat: The provided payload - was invalid : the request contained the credentials of users that already were related to the chat. 2.1[-] Discharge/Remove the created chat, and mention this to requester/owner. 2.2 Otherwise proceed to the notify each participant of the chat - using the participations property. 2.[-] Otherwise: The chat couldn't be created - notify the owner of the request/access_token about the failure 1.[-] Otherwise - the resolution wasn't able to figure out the user based on the credentials proceed to the mention about the failure. 1.[-] Otherwise proceed to mention about the failure. Exceptions: Raises: TypeError - if headers and data arguments are not dictionaries. ''' #Code: #Exceptions: assert all(map(lambda argument: isinstance(argument,dict),(headers,data))), TypeError('Arguments , such as : headers and data - must be dictionaries') resolve = lambda **credentials: instance if (instance:=UserService(**credentials)).id else None #Step 0. if not kwargs['authorization']['access']['valid'] or (owner:=kwargs['authorization']['access']['owner']) is None: disconnect() return None
def accept(self, headers, data, **kwargs): ''' Goal : Generate a grant token, based on the <identification_data> from the verification_token and the <authentication_data> from the data of the request. Arguments:headers:dict, data:dict, kwargs:key-word-argument. headers : meant to contain all headers data , in this particular case - an Authorization field as a sign of authority, must contain a Bearer token , which is the verification_token: {identification_data:<identification_data>,token_type:"verification",activity:int,dnt:float,preaccess:<preaccess_token>}. Note: This argument is used in the authorized decorator - to perform proper authorization process, the result of which is stored in the kwargs. To know more about the authorized decorator - view a separate documentation for the authorized method in the chathouse/utilities/security/validation/headers.py. data : meant to store any data that's passed into the request - json body. The could be 2 cases: signup: <authentication_data>:{ password:str, keyring:{ public_key:str, private_key:{ iv:str, data:str }, g:int, m:int } } or login: <authentication_data>:{ password:str } Note: This argument is used in the verification process of the incoming request data, which is handled by the derived class template - which on itself is a result of create_a_template function, meant to return a proper template instance according to the route. To know more about the create_a_template - view a separate documentation for the create_a_template function in the ./template.py. kwargs: meant to store any data passed into the accept method, from the initial requrest, up to the authorization steps. In particular stores the authorization key , which on it's own stores shall store more nested information related to a token_type. In this instance the token_type is the preaccess one - so kwargs shall store:{ authorization:{ verification:{ valid:bool, status:int, token:{ object:None|Token, location:str } } } } Full verification: 0.Verify the verification_token: 1.Extract the preaccess token from the verification_token and verify it and check existance of the route value. If 0./1. is invalid respond with 401, message:"Invalid verification/preaccess token."; Otherwise resume with the next steps. 2.Having validated the preaccess , extract the route value and create a proper template. 3.Verify that the data is a dictionary an proceed to validate the data against the proper template. 4.Resolve the appropriate user_service object (/w existing or non existing inner instance of the user) , using the resolve function , which returns a UserService|None, based on the route and <identification_data> 4.If the user_service has been resolved: Then according to the route: 5. If route is signup: 5.S.1. If either provided Diffie Hellman parameters or public key is invalid: Respond with a 409, 'Invalid Diffie Hellman data.' Otherwise proceed to the next step: 5.S.2. Establish a payload using the proper_payload function and If user_service.signup(payload) has been successful: Generate a grant_token and the response shall contain it with a 201. Otherwise: Respond with 409, 'Provided data is invalid.' if the prodived public_key doesn't already exists otherwise 'Please submit again.' 5.L.1. Otherwise if the route is login and user_service.login( established payload) has been successful: Generate a grant_token, retrieve a private key from the keyring and DH parameters => inject them into the response, and respond with a 200. At this point the activity_dnt has been upgraded to avoid the usage of previously generated valid tokens. Otherwise: Respond with 401, message:"Invalid authentication data". Otherwise: Respond accordingly with 400, 'Invalid payload data' Lambda functions: map_cases: Goal:create a map object of iterated UserService instances for email and username cases. Iteration itself checks if the isntance has an id -> returns the UserService case if instance has an id else None. Arguments: data:key word argument. Returns: map object. resolve: Goal: resolve email based on the identification data , by verify if the such data is appropriate according to a certain route. If the route is signup: Get respective map_cases, then : If not even one case (not any function) is valid return then return a UserService instance with empty inner instance of the user. Otherwise return None Otherwise: Get respective map_cases, then convert cases into a tuple: If any case isn't None - then return next element of the filtered cases , where every case must not be a None - thus returning an email. Otherwise return None. Returns: str|None , str - represents an email value. valid_dh_parameters: Goal: verify if the provided dh parameters are the same as the configured ones, and the provided public is in bounds 0<public_key<modulus. Helps to omit requests, which contain invalid DH parameters meant for the key establishments. Arguments:initial_keyring:dict - the keyring from the provided data. Returns: True|False Note: When called the g,m values are popped - so, the keyring now has a following structure: keyring:{ public_key:str, private_key:{ iv:str, data:str } } proper_payload: Goal: construct a proper payload based on the incoming_route value and incoming_data. Arguments: incoming_route:str,incoming_data:dict. Actions: If the route is signup ( Note: at this point incoming_data consists of : password:str , keyring:{ public_key:str , private_key: { iv:str,data:str } } ): return { key_data : pop the keyring from the incoming_data user_data : {unpack the <identification_data> from the verification token , and unpack the incoming_data} }. Otherwise the route is login ( Note at this point incoming_data consists of : password:str ): return incoming_data. Returns: a dictionary. token_payload: Goal: construct a proper payload_data for the Token, based on the provided service instance. Arguments: service:UserService instance. Returns: an empty dictionary if the service instance has no inner instance/state, otherwise poceed to create a dictionary of: user_id:int(user's id), token_type:str('grant'), activity:int(activity_state of the UserService), exp:dict(configured expiration value for the grant_token - 30 minutes) Generation: grant_token={user_id: value:int, token_type: "grant":str, activity: timestamp of UserService(id=value of user_id).activity_dnt , dnt:float} Returns: If either verification token or preaccess token(from the verification token) is invalid or the preaccess token has no route value. Return 401, message:"Invalid verification/preaccess token." Otherwise: If route is signup: If either provided Diffie Hellman parameters or public key is invalid: Respond with a 409, 'Invalid Diffie Hellman data.' Otherwise: If the user_service.signup(appropriate payload) has been successful: Return 200, {grant_token:<grant_token>} Otherwise: Return 409, 'Provided data is invalid.' if the prodived public_key doesn't already exists otherwise 'Please submit again.' Otherwise if the route is login and user_service.login(appropriate payload) has been successful: Return 200, {grant_token:<grant_token>,keyring:{raw:{private_key:<encrypted private_key>}, g:<G>, m:<M>}} Otherwise: Return 401, 'Invalid authentication data.' Exceptions: Raises: TypeError - if headers and data arguments are not dictionaries. ValueError - if the current app doesn't contain domain Diffie Hellman parameters. ''' #Code: #Exceptions: assert all( map(lambda argument: isinstance(argument, dict), (headers, data)) ), TypeError( 'Arguments , such as : headers and data - must be dictionaries') assert all( map(lambda parameter: isinstance(parameter, int), current_app.config.get('DH_PARAMETERS', (None, ))) ), ValueError( 'Configuration of the current app must contain domain Diffie Hellman parameters.' ) #Lambda functions: find_case = lambda cases: next(iter(tupled)) if (tupled := tuple( filter(lambda case: case is not None, cases))) else None map_cases = lambda **credentials: map( lambda identification: case if (case := UserService(**{ identification: credentials[identification] })).id else None, credentials) resolve = lambda route,data: (UserService() if not any(map_cases(email=data.get('email',''),username=data.get('username',''))) else None ) if route=='signup'\ else (case if any( (cases:=tuple( map_cases(email=data.get('identification',''),username=data.get('identification','') ) ) ) ) and (case:=find_case(cases)) else None) valid_dh_parameters = lambda initial_keyring: current_app.config[ 'DH_PARAMETERS'] == ( initial_keyring.pop('g', None), initial_keyring.pop('m', None)) and 0 < initial_keyring.get( 'public_key', -1) < current_app.config['DH_PARAMETERS'][1] proper_payload = lambda incoming_data,incoming_route: { \ 'key_data':incoming_data.pop('keyring', {'public_key':None,'private_key':None}),\ 'user_data':{\ **kwargs['authorization']['verification']['token']['object']['identification_data'],\ **incoming_data\ }\ } if incoming_route=='signup' else incoming_data token_payload = lambda service: {'user_id':service.id,\ 'token_type':'grant',\ 'activity':service.activity_state,\ 'exp':current_app.config['GRANT_EXP']}\ if getattr(service,'id',None) is not None else {} #Step 0. #Step 1. if not kwargs['authorization']['verification']['valid'] or not ( preaccess_token := Token(raw_data=kwargs['authorization']['verification']['token'] ['object']['preaccess']) ).is_valid or not preaccess_token['route']: return { 'success': 'False', 'message': 'Unauthorized!', 'reason': 'Invalid verification/preaccess token.' }, 401