def reauthenticate_check(r, redirect): """If need be, provide redirect for reauthentication Parameters: r -- a general method response. Field err_code will be examined If it is PLEASE_REAUTHENTICATE then the session will be setup to redirect after auth and the auth redirect url will be returned redirect -- where the browser should be redirected after a successful re-suthentication. Returns: False or the redirect url """ if service_integration: # No need for re-authentication for Service apps return False # See if we're testing the re-authentication oauth_force_re_auth = False if "oauth_force_re_auth" in session: oauth_force_re_auth = session["oauth_force_re_auth"] if oauth_force_re_auth: # Check that we can do a re-authentication auth = session["auth"] oauth_force_re_auth = (auth["type"] == "oauth_code" and auth["client_id"] and auth["secret_key"] and auth["redirect_uri"]) session["oauth_force_re_auth"] = False # reset if oauth_force_re_auth: ds_recipe_lib.log("reauthenticate_check: forcing re-authentication...") if oauth_force_re_auth or ('err_code' in r and r['err_code'] == "PLEASE_REAUTHENTICATE"): session["auth_redirect"] = redirect ds_recipe_lib.log("reauthenticate_check: final redirect will be to " + redirect) auth = session["auth"] oauth_state = hashlib.sha256(os.urandom(1024)).hexdigest() auth["oauth_state"] = oauth_state session["auth"] = auth # Store updated info redirect = (oauth_start + "?response_type=code" + "&scope=" + oauth_scope + "&client_id=" + auth["client_id"] + "&state=" + oauth_state + "&redirect_uri=" + auth["redirect_uri"]) return redirect else: return False
def token_refresh(auth): """Call OAuth: token_refresh Returns true if the user needs to re-authenticate. False indicates that the refresh worked and there is still time left on the new token """ client_secret = auth["client_id"] + ':' + auth["secret_key"] client_secret_b64 = base64.b64encode(client_secret) try: r = requests.post(oauth_token, headers={'Authorization': "Basic " + client_secret_b64}, data={'grant_type': 'refresh_token', 'refresh_token': auth["refresh_token"]}) except requests.exceptions.RequestException as e: err = "Error calling DocuSign for OAuth token refresh: " + str(e) ds_recipe_lib.log(err) return True status = r.status_code if (status != 200): err = "Error calling DocuSign for OAuth token refresh: " + r.content + " (" + str(status) + "). " ds_recipe_lib.log(err) return True # we should now have the following in r.json() -- # access_token The token you will use in the Authorization header of calls to the DocuSign API. # token_type This is the kind of token. It is usually Bearer # refresh_token A token you can use to get a new access_token without requiring user interaction. # expires_in The number of seconds before the access_token expires. # # Example content: {"access_token":"[587 characters]","token_type":"Bearer","refresh_token":"[587 characters]","expires_in":28800,"user_api":null} token_info = r.json() # Save in the session. auth['access_token'] = token_info["access_token"] auth['refresh_token'] = token_info["refresh_token"] auth['token_type'] = token_info["token_type"] auth["auth_header_key"] = "Authorization" auth["auth_header_value"] = auth['token_type'] + " " + auth['access_token'] auth["expires"] = int(time.time()) + token_info["expires_in"] expires_soon = (auth["expires"] - int(time.time())) < oauth_opportunistic_re_auth if expires_soon: ds_recipe_lib.log("Token refresh: we have a good response, but the new token also expires soon!") return True ds_recipe_lib.log("Token refresh: Success! Expires in " + str(datetime.timedelta(seconds= auth["expires"] - int(time.time())))) # Save info session["auth"] = auth return False
def reauthenticate_check(r, redirect): """If need be, provide redirect for reauthentication Parameters: r -- a general method response. Field err_code will be examined If it is PLEASE_REAUTHENTICATE then the session will be setup to redirect after auth and the auth redirect url will be returned redirect -- where the browser should be redirected after a successful re-suthentication. Returns: False or the redirect url """ # See if we're testing the re-authentication oauth_force_re_auth = False if "oauth_force_re_auth" in session: oauth_force_re_auth = session["oauth_force_re_auth"] if oauth_force_re_auth: # Check that we can do a re-authentication auth = session["auth"] oauth_force_re_auth = (auth["type"] == "oauth_code" and auth["client_id"] and auth["secret_key"] and auth["redirect_uri"]) session["oauth_force_re_auth"] = False # reset if oauth_force_re_auth: ds_recipe_lib.log("Forcing re-authentication. Trying refresh...") refresh_didnt_work = token_refresh(auth) if refresh_didnt_work: ds_recipe_lib.log("Forcing re-authentication. Token refresh failed. Full re-auth...") else: return if oauth_force_re_auth or ('err_code' in r and r['err_code'] == "PLEASE_REAUTHENTICATE"): session["auth_redirect"] = redirect ds_recipe_lib.log("reauthenticate_check: final redirect will be to " + redirect) auth = session["auth"] oauth_state = hashlib.sha256(os.urandom(1024)).hexdigest() auth["oauth_state"] = oauth_state session["auth"] = auth # Store updated info redirect = (oauth_start + "?response_type=code" + "&scope=" + oauth_scope + "&client_id=" + auth["client_id"] + "&state=" + oauth_state + "&redirect_uri=" + auth["redirect_uri"]) return redirect else: return False
def auth_redirect(): """Process the incoming data from OAuth Authorization Code Grant redirect""" # An unsuccessful authentication redirect (User chose to not grant access) # error=access_denied&error_message=The%20user%20did%20not%20consent%20to%20connecting%20the%20application.&state=c611e8268536e1218f24f6991f1abd1a3c23cbbe6787887afc3138aede2f1840 # Success: # code=eyJ0eXAiOi_blah_blah_P6BwFtw&state=70a5333877c3f57bcdc407a7776c747290f95ce8ec1ff5203d6af35e9d5b36e3 # First check that the state is correct. This is important to prevent CSRF attacks. See http://goo.gl/av06hU # In this example, the user is notified of the behavior. # You might also want to alert an administrator or two. Or log all of the request's information ds_recipe_lib.log("Received incoming data from OAuth Authorization Code Grant redirect") if 'auth' in session and 'oauth_state' in session['auth']: auth = session['auth'] else: e = "No authentication information in the session! Possible CSRF attack!" ds_recipe_lib.log("OAuth problem: " + e) delete_auth() return e oauth_state = auth['oauth_state'] incoming_state = request.args.get('state') if incoming_state != oauth_state: e = "Authentication state information mismatch! Possible CSRF attack!" ds_recipe_lib.log("OAuth problem: " + e) ds_recipe_lib.log("Incoming state: " + incoming_state + " Stored state: " + oauth_state) delete_auth() return e incoming_error_message = request.args.get('error_message') if incoming_error_message: ds_recipe_lib.log("OAuth error message: " + incoming_error_message) delete_auth() return incoming_error_message incoming_code = request.args.get('code') if not incoming_code: e = "Code was not supplied by server! Please contact your administrator." ds_recipe_lib.log("OAuth problem: " + e) delete_auth() return e # Exchange the code for tokens e = oauth_token_for_code(auth, incoming_code) if e: ds_recipe_lib.log("OAuth problem converting code to token: " + e) delete_auth() return e else: ds_recipe_lib.log("OAuth: converted code to token!") # Get the User Info e = get_oauth_user_info() if e: ds_recipe_lib.log("OAuth problem getting user info: " + e) else: ds_recipe_lib.log("OAuth: got user info!") return e # If no errors, then continue. The user is now authenticated!
def provision_community_member(cache, email, first_name, last_name, sfdc_account_url): '''Create a contact and external user in the sfdc account Returns err_msg => None or the problem info ''' # Interpreter command to test this method # from app.lib_master_python import ds_salesforce; from app.lib_master_python import ds_cache; cache = ds_cache.get() # ds_salesforce.provision_community_member(cache, '*****@*****.**', 'Sally', 'Ride', 'https://name-dev-ed.my.salesforce.com/0016100000cgXXX') ds_recipe_lib.log('################## Starting SFDC provisioning') # Check that the url is for an SFDC account # See http://www.salesforceben.com/salesforce-url-consist/ url_parts = urlparse(sfdc_account_url) path = url_parts.path # includes leading / obj_prefix = path[1:4] account_obj_prefix = '001' account_id = path[1:] # ParseResult(scheme='http', netloc='www.cwi.nl:80', path='/%7Eguido/Python.html', params='', query='', fragment='') if obj_prefix != account_obj_prefix: return 'The SFDC url is not an Account reference' sf = Salesforce(instance=url_parts.netloc, session_id='') sf = Salesforce(username=cache['sfdc_username'], password=cache['sfdc_pw'], security_token=cache['sfdc_security_token']) # Create the contact try: r = sf.Contact.create({ 'AccountID': account_id, 'LastName': last_name, 'FirstName': first_name, 'Email': email }) except (SalesforceMoreThanOneRecord, SalesforceMalformedRequest, SalesforceExpiredSession, SalesforceRefusedRequest, SalesforceResourceNotFound, SalesforceGeneralError) as e: ds_recipe_lib.log("##################") ds_recipe_lib.log("##################") ds_recipe_lib.log( "################## ERROR creating contact for account {}".format( account_id)) ds_recipe_lib.log(" url: " + e.url) ds_recipe_lib.log( " content: " + str(e.content) ) # This is actually a JSON string. It could be decoded and used. # Example: [{u'errorCode': u'LICENSE_LIMIT_EXCEEDED', u'fields': [], u'message': u'License Limit Exceeded'}, {u'errorCode': u'LICENSE_LIMIT_EXCEEDED', u'fields': [], u'message': u'license limit exceeded'}] ds_recipe_lib.log(" status: " + str(e.status)) ds_recipe_lib.log("resource_name: " + e.resource_name) return 'Problem creating the contact: ' + str(e.content) if r['success']: ds_recipe_lib.log('Created contact for account {}!'.format(account_id)) contact_id = r['id'] else: ds_recipe_lib.log('ERROR creating contact for account {} -- {}'.format( account_id, str(r['errors']))) return 'Problem creating the contact: ' + str(r['errors']) # Create the user. See https://developer.salesforce.com/docs/atlas.en-us.api.meta/api/sforce_api_objects_user.htm username = email.split('@')[0] + '@' + cache['sfdc_user_name_domain'] ### In production, you'd want to automatically handle a username clash with an existing user, ### and then try alternate usernames for the new user. ## To be done: if there's an error while creating the contact's user record then ## we should delete the contact? Or maybe leave it for manual processing? try: r = sf.User.create({ 'ContactId': contact_id, 'Email': email, 'LastName': last_name, 'FirstName': first_name, 'IsActive': True, 'ProfileId': cache['sfdc_profile_id'], 'Username': username, 'UserPreferencesLightningExperiencePreferred': True, 'Alias': first_name[0:0] + last_name[0:6], # Alias is required, max length 8. # In production you'd want to watch for alias clashes and then append a digit if needed 'TimeZoneSidKey': cache['sfdc_time_zone_sid_key'], 'LocaleSidKey': cache['sfdc_locale_sid_key'], 'EmailEncodingKey': cache['sfdc_email_encoding_key'], 'LanguageLocaleKey': cache['sfdc_language_locale_key'], 'ProfileId': cache['sfdc_profile_id'] }) except (SalesforceMoreThanOneRecord, SalesforceMalformedRequest, SalesforceExpiredSession, SalesforceRefusedRequest, SalesforceResourceNotFound, SalesforceGeneralError) as e: ds_recipe_lib.log("##################") ds_recipe_lib.log("##################") ds_recipe_lib.log("################## ERROR creating the user") ds_recipe_lib.log(" url: " + e.url) ds_recipe_lib.log( " content: " + str(e.content) ) # This is actually a JSON string. It could be decoded and used. # Example: [{u'errorCode': u'LICENSE_LIMIT_EXCEEDED', u'fields': [], u'message': u'License Limit Exceeded'}, {u'errorCode': u'LICENSE_LIMIT_EXCEEDED', u'fields': [], u'message': u'license limit exceeded'}] ds_recipe_lib.log(" status: " + str(e.status)) ds_recipe_lib.log("resource_name: " + e.resource_name) return 'Problem creating the contact: ' + str(e.content) if r['success']: user_id = r['id'] ds_recipe_lib.log('Created user record! ID = ' + user_id) else: ds_recipe_lib.log('ERROR creating user -- {}'.format(str(r['errors']))) return 'Problem creating the user: '******'errors']) send_welcome_email(cache, email, first_name, last_name, username) ds_recipe_lib.log('################## Completed SFDC provisioning') return None
def webhook_listener(): # Process the incoming webhook data. See the DocuSign Connect guide # for more information # # Strategy: examine the data to pull out the envelope_id and time_generated fields. # Then store the entire xml on our local file system using those fields. # # This function could also enter the data into a dbms, add it to a queue, etc. # Note that the total processing time of this function must be less than # 100 seconds to ensure that DocuSign's request to your app doesn't time out. # Tip: aim for no more than a couple of seconds! Use a separate queuing service # if need be. data = request.data # This is the entire incoming POST content. # This is dependent on your web server. In this case, Flask cache = ds_cache.get() # Settings # Note, there are many options for parsing XML in Python # For this recipe, we're using Beautiful Soup, http://www.crummy.com/software/BeautifulSoup/ xml = BeautifulSoup(data, "xml") envelope_id = xml.EnvelopeStatus.EnvelopeID.string time_generated = xml.EnvelopeStatus.TimeGenerated.string # "CustomFields appears in multiple places in the XML. # So we look for one with the right parent good_custom_fields = False for custom_fields in xml.EnvelopeStatus.find_all("CustomFields"): if custom_fields.parent.name == 'EnvelopeStatus': good_custom_fields = custom_fields if not good_custom_fields: ds_recipe_lib.log("Account number not found in XML, skipping!") return account_id = False for custom_field in good_custom_fields.children: # <Name>AccountId</Name> # <Value>1703272</Value> name = custom_field.find('Name') if name != -1 and name.string == "AccountId": account_id = custom_field.find('Value').string if not account_id: ds_recipe_lib.log("Account number not found in XML, skipping!") return # Check that the right template generated this notification template_name = xml.find("TemplateName") != None and xml.find( "TemplateName").string if template_name != cache['template_name']: ds_recipe_lib.log( "Missing or incorrect template name in XML, skipping!") return # Pull out the recipient 1 (PowerForm user) name and email # Check that the person actually signed! sender_name = None sender_email = None sender_signer = None for recipient in xml.RecipientStatuses.find_all("RecipientStatus"): r_type = recipient.find('Type') order = recipient.find('RoutingOrder') if (r_type != -1 and r_type.string == "Signer" and order != -1 and order.string == '1'): sender_name = recipient.find('UserName').string sender_email = recipient.find('Email').string sender_signed = recipient.find('Status').string == "Completed" if sender_name: ds_recipe_lib.log("PowerForm sender (new partner): {} <{}>".format( sender_name, sender_email)) if not sender_signed: ds_recipe_lib.log("The PowerForm sender didn't sign the contract!") return else: ds_recipe_lib.log( "The PowerForm sender was not found in the XML, skipping!") return # Get the pname (Partner Name) field value # Get the pfname (Partner First Name) field value # Get the plname (Partner Last Name) field value # Get the wpartner (Partner url) field value partner_name = None partner_first_name = None partner_last_name = None sfdc_partner_url = None for tab in xml.find_all("TabStatus"): label = tab.find('TabLabel') value = tab.find('TabValue') if label != -1 and value != -1: if label.string == 'pname': partner_name = value.string if label.string == 'pfname': partner_first_name = value.string if label.string == 'plname': partner_last_name = value.string if label.string == 'wpartner': sfdc_partner_url = value.string if not partner_name or not sfdc_partner_url or not partner_first_name or not partner_last_name: ds_recipe_lib.log( "The XML is missing the partner name and/or the SFDC URL, skipping!" ) return ds_recipe_lib.log("Partner name: {}, SFDC partner url: {}".format( partner_name, sfdc_partner_url)) # Store the file. # Some systems might still not like files or directories to start with numbers. # So we prefix the envelope ids with E and the timestamps with T setup_output_dir(account_id) account_dir = get_account_dir(account_id) filename = "T" + time_generated.replace( ':', '_') + ".xml" # substitute _ for : for windows-land filepath = os.path.join(account_dir, filename) with open(filepath, "w") as xml_file: xml_file.write(data) ds_salesforce.provision_community_member(cache, sender_email, partner_first_name, partner_last_name, sfdc_partner_url)