1616
1717from c8y_api ._auth import HTTPBearerAuth
1818from c8y_api ._jwt import JWT
19+ from c8y_api ._util import validate_base_url
1920
2021
2122class ProcessingMode :
@@ -40,6 +41,12 @@ def __init__(self, method: str, url: str = None, message: str = "Unauthorized.")
4041 super ().__init__ (method , url , 401 , message )
4142
4243
44+ class MissingTfaError (UnauthorizedError ):
45+ """Error raised for unauthorized access."""
46+ def __init__ (self , method : str , url : str = None , message : str = "Missing TFA Token." ):
47+ super ().__init__ (method , url , message )
48+
49+
4350class AccessDeniedError (HttpError ):
4451 """Error raised for denied access."""
4552 def __init__ (self , method : str , url : str = None , message : str = "Access denied." ):
@@ -69,30 +76,28 @@ class CumulocityRestApi:
6976 CONTENT_MANAGED_OBJECT = 'application/vnd.com.nsn.cumulocity.managedobject+json'
7077 CONTENT_MEASUREMENT_COLLECTION = 'application/vnd.com.nsn.cumulocity.measurementcollection+json'
7178
72- def __init__ (self , base_url : str , tenant_id : str , username : str = None , password : str = None , tfa_token : str = None ,
79+ def __init__ (self , base_url : str , tenant_id : str , username : str = None , password : str = None ,
7380 auth : AuthBase = None , application_key : str = None , processing_mode : str = None ):
7481 """Build a CumulocityRestApi instance.
7582
76- One of `auth` or `username/password` must be provided. The TFA token
77- parameter is only sensible for basic authentication.
83+ One of `auth` or `username/password` must be provided.
7884
7985 Args:
8086 base_url (str): Cumulocity base URL, e.g. https://cumulocity.com
8187 tenant_id (str): The ID of the tenant to connect to
8288 username (str): Username
8389 password (str): User password
84- tfa_token (str): Currently valid two-factor authorization token
8590 auth (AuthBase): Authentication details
8691 application_key (str): Application ID to include in requests
8792 (for billing/metering purposes).
8893 processing_mode (str); Connection processing mode (see
8994 also https://cumulocity.com/api/core/#processing-mode)
9095 """
91- self .base_url = base_url .rstrip ('/' )
96+ self .base_url = validate_base_url (base_url )
97+ self .is_tls = self .base_url .startswith ('https' )
9298 self .tenant_id = tenant_id
9399 self .application_key = application_key
94100 self .processing_mode = processing_mode
95- self .is_tls = self .base_url .startswith ('https' )
96101
97102 if auth :
98103 self .auth = auth
@@ -103,14 +108,49 @@ def __init__(self, base_url: str, tenant_id: str, username: str = None, password
103108 else :
104109 raise ValueError ("One of 'auth' or 'username/password' must be defined." )
105110
106- self .__default_headers = {}
107- if tfa_token :
108- self .__default_headers ['tfatoken' ] = tfa_token
109- if self .application_key :
110- self .__default_headers [self .HEADER_APPLICATION_KEY ] = self .application_key
111- if self .processing_mode :
112- self .__default_headers [self .HEADER_PROCESSING_MODE ] = self .processing_mode
113- self .session = self ._create_session ()
111+ self ._session = None
112+
113+ @property
114+ def session (self ) -> requests .Session :
115+ """Provide session."""
116+ if not self ._session :
117+ self ._session = self ._create_session ()
118+ return self ._session
119+
120+ @classmethod
121+ def authenticate (
122+ cls ,
123+ base_url : str ,
124+ tenant_id : str ,
125+ username : str ,
126+ password : str ,
127+ tfa_token : str = None ,
128+ ) -> (str , str ):
129+ """Authenticate a user using OAI Secure login method.
130+
131+ Args:
132+ base_url (str): Cumulocity base URL, e.g. https://cumulocity.com
133+ tenant_id (str): The ID of the tenant to connect to
134+ username (str): Username
135+ password (str): User password
136+ tfa_token (str): Currently valid two-factor authorization token
137+
138+ Returns:
139+ A string tuple of JWT auth token and corresponding XRSF token.
140+ """
141+ url = f'{ base_url .rstrip ("/" )} /tenant/oauth?tenant_id={ tenant_id } '
142+ form_data = {'grant_type' : 'PASSWORD' , 'username' : username , 'password' : password , 'tfa_token' : tfa_token }
143+ response = requests .post (url = url , data = form_data , timeout = 60.0 )
144+ if response .status_code == 401 :
145+ message = response .json ()['message' ] if response .json () and 'message' in response .json () else None
146+ # 1st request might fail due to missing TFA code
147+ if any (x in response .json ()['message' ] for x in ['TOTP' , 'TFA' ]):
148+ raise MissingTfaError (cls .METHOD_POST , response .url , message )
149+ raise UnauthorizedError (cls .METHOD_POST , response .url , message )
150+ if response .status_code != 200 :
151+ message = response .json ()['message' ] if 'message' in response .json () else "Invalid request!"
152+ raise HttpError (cls .METHOD_POST , response .url , response .status_code , message )
153+ return response .cookies ['authorization' ], response .cookies ['XSRF-TOKEN' ]
114154
115155 def _create_session (self ) -> requests .Session :
116156 s = requests .Session ()
@@ -136,10 +176,8 @@ def prepare_request(self, method: str, resource: str,
136176 Returns:
137177 A PreparedRequest instance
138178 """
139- hs = self .__default_headers
140- if additional_headers :
141- hs .update (additional_headers )
142- rq = requests .Request (method = method , url = self .base_url + resource , headers = hs , auth = self .auth )
179+ headers = {** (self .session .headers or {}), ** (additional_headers or {})}
180+ rq = requests .Request (method = method , url = self .base_url + resource , headers = headers , auth = self .auth )
143181 if json :
144182 rq .json = json
145183 return rq .prepare ()
0 commit comments