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
Пример #2
0
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
Пример #4
0
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!
Пример #5
0
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
Пример #6
0
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)