A mailer, member database, and so much more, for digital activism.
Identity can be setup to pull data from ControlShift Labs (CSL) platform.
The first and most fundamental step in setting up a CSL <-> Identity integration is getting data about who has signed petitions, subscribed to emails, etc into Identity. There are multiple ways to do this, and two most commonly used methods are listed below. However, both involve some common setup steps in Identity.
csl = APIUser.create_with_permission!('csl-webhook', name: "controlshift", active: true)
csl.token
To import CSL Signatures (into the Identity MemberAction
table), set the PROCESS_CBY_ACTIONS
Identity ENV VAR to true. Note, that when actions are recorded, Member
records for the user who signed the petition are also created automatically.
Optional: To import CSL Members (into the Identity Member
table), set the PROCESS_CBY_MEMBERS
Identity ENV VAR to true
. Note, that this is not required, since users signing petitions will have Member
records created anyway - and having this option enabled has caused issues for some orgs, such as empty names for members. Therefore a number of organisations currently leave this option disabled.
PROCESS_CBY_ACTIONS
means actions (CSL signatures) from the incremental CSL webhook are processed by ID. If you also want to process nightly full exports of all historic actions (CSL signatures) you also need to set the Identity ENV VAR PROCESS_CBY_FULL_ACTIONS
to true
, as well as ticking the ‘nightly csv exports’ box in the CSL Admin UI (this is covered later as well). If you have more than 400k signatures in CSL and you’re running on heroku, it is recommended to leave the PROCESS_CBY_FULL_ACTIONS
ENV VAR turned off, because processing this many events every night will take a long time.
The LOG_ALL_CONTROLSHIFT_WEBHOOKS
Identity ENV VAR can be set to true
for extra logging of the webhooks Identity processes.
If you want to, set CONTROLSHIFT_API_RATE_LIMIT (defaults to 1000 per interval).
This is the simplest and most commonly used method to get CSL data into Identity.
Identity listens for CSL webbooks notifying it of CSL data that has been exported to CSV files. It then queues background jobs to download these CSV files and import them. The detailed workflow is as follows:
The Identity api/ctrlshift_webhook
endpoint receives all CSL webhook payloads.
Payloads which have a type of data.incremental_table_exported
or data.full_table_exported
are processed. Most others are ignored (a small handful of other event-driven webhooks are processed).
The exported table name and URL are extracted from the payload, and saved in the Identity database with CtrlshiftWebhook
record.
Depending on the payload type and settings, a ControlshiftWebhookWorker
is queued with the name of the Identity model that corresponds to the CSL table, and the URL the data was exported to.
When the ControlshiftWebhookWorker
is run, it downloads the CSV data, and queues a ControlshiftWebhookRowWorker
job for each row, with the name of the model and the row data.
When the ControlshiftWebhookRowWorker
runs, it calls the load_from_csv
method on the model with the row data, which saves the data.
More information on the schema of the exported tables can be found in the CSL developer docs.
https://<instance-url>/api/ctrlshift_webhook?api_token=<api-token>
, replacing <instance-url>
with the URL of your Identity instance and <api-token>
with the APIUser token you generated in ID earlier.Tick the ‘incremental csv exports’ and ‘nightly csv exports’ boxes according to your organizations needs. Incremental exports are frequent CSV exports, recommended if you need any realtime analytics of CSL signatures. Nightly is a once-per-day bulk export of all data. Usually organisations enable both of these.
TODO: Document how to setup HMAC signing of CSL payloads
This option may be useful if you are experiencing heavy load on your Identity instance, or if you are worried about downtime on your Identity instance leading to loss of events from CSL.
However, this method is significantly more complex, and involves utilising multiple services in Amazon Web Services.
With this method, CSL webhook events are pushed onto an Amazon Web Services queue first, and the events from the AWS queue are then pulled off by a process running in Identity.
CSL is configured to push events to an AWS endpoint, which then stores the event in a SQS queue. A process in Identity then reads events from this queue and processes them in the same way as the direct CSL -> Identity method.
CSL is configured to push webhook events to an Amazon Web Services API Gateway
The API Gateway passes on the event data (including URL parameters and HTTP headers) to an Amazon Web Services Lambda for processing
The AWS Lambda pushes this data onto an Amazon Web Services Simple Queue Service (SQS)
Identity must be configured with the AWS SQS queue name, and configured to run the appropriate rake task (in the sqs.rake
file), which will continuously pull from the SQS queue
Event payloads from the SQS queue are then processed in the same manner as payloads which come in through the api/ctrlshift_webhook
endpoint - refer to the documentation above regarding how these are processed.
csl = APIUser.create_with_permission!('csl-webhook', name: "controlshift", active: true)
csl.token
Create an AWS SQS queue to queue CSL webhook events for processing by Identity. You should refer to AWS documentation on exactly how to create this, since the AWS UI may change over time.
Create a REST-based AWS API Gateway. You should refer to AWS documentation on exactly how to create this, since the AWS UI may change over time. But you want a REST API endpoint which CSL can POST to, and which pushes data to an AWS Lambda.
Ensure Query parameters, HTTP headers, etc are passed through to the Lambda function. The AWS documentation on this is not great. This article explains how to pass HTTP Header through, but it doesn’t deal with URL Query parameters. So the following explains how to set this up, correct at the time of writing: When configuring the Integration Request for the AWS API Gateway, select an Integration Type of ‘Lambda Function’ and expand the ‘Mapping Templates’ section. For Request Body Passthrough, select ‘When there are no templates defined (recommended)’. Add a mapping template with Content-Type of ‘application/json’, and then in the Generate Template drop-down box, select ‘Method Request Passthrough’. This will generate code in the text box which will take HTTP headers, URL query parameters, and other context of the HTTP request, and pass this through to the Lambda function. There are also comments in the sqs.rake
file which detail the message format required in case eg. the code generated by AWS changes.
Setup your Lambda function to push to your SQS queue. The sample code below does this, and works with a Node.js 12.x runtime:
var AWS = require('aws-sdk');
var sqs = new AWS.SQS({region : '<YOUR_AWS_SQS_REGION>'});
var http = require('https');
var postToSQS = function(data, callback) {
sqs.sendMessage({
MessageBody: JSON.stringify(data),
QueueUrl: '<YOUR_SQS_QUEUE_URL>'
}, function(err, data) {
callback(err, data);
});
};
exports.handler = function(event, context) {
postToSQS(event, function(err, data) {
if (err) {
console.log('error:', "Error posting to SQS: " + err);
context.done('error', "Error sending action data");
} else {
console.log('Success', 'Action posted');
context.done(null, '');
}
});
};
You may wish to limit access to the AWS API Gateway to only the specific CSL IP Addresses. This AWS documentation explains how to do so. Note, restrictions are applied to your entire API Gateway, not to just a single endpoint. If you use the same API Gateway for other endpoints (eg. Speakout -> AWS -> Identity data) then you need to ensure the source IPs which use those endpoints are also allowed!
Login to your CSL instance as an Admin, and navigate to Settings, then Integrations
Click on the NEW WEBHOOK ENDPOINT button, and enter your AWS API Gateway URL, adding an api_token URL parameter, eg: https://<aws-api-gateway-url>?api_token=<api-token>
, replacing <aws-api-gateway-url>
with the URL of your AWS API Gateway, and <api-token>
with the APIUser token you generated in ID earlier.
Click Save, and you should see that the webhook endpoint has been saved and Enabled. If ControlShift can’t access your AWS API Gateway, you’ll see an error explaining why.
Tick the ‘incremental csv exports’ and ‘nightly csv exports’ boxes according to your organizations needs. Incremental exports are frequent CSV exports, recommended if you need any realtime analytics of CSL signatures. Nightly is a once-per-day bulk export of all data. Usually organisations enable both of these.
TODO: Document how to setup HMAC signing of CSL payloads
In your Identity environment, set the AWS_REGION
, AWS_ACCESS_KEY_ID
& AWS_SECRET_ACCESS_KEY
ENV VARs if not set already. The AWS IAM user for Identity must also have access to read from the SQS queue you setup earlier.
In your Identity environment, set the SQS_WEBHOOKS_QUEUE
ENV VAR to the name of your SQS queue - just the name, not the full URL!
Finally, ensure that your Identity installation has a process running the sqs:process_webhooks
rake task (see the sqs.rake
file). The Identity Procfile
includes a line to run this, so if you deploy to Heroku then it is likely this is already running, but you should check (eg. in Heroku, ensure the worker_ctrlshift_sqs
process has 1 dyno running)
If you want a nice (and rigorous) connection between CSL opt-ins / consents and Identity, you can follow these steps to configure your consent texts and subscriptions. Check out the separate docs on user consents for more background.
Settings.gdpr.disable_auto_subscribe
by adding it your org’s settings .yml
file. This guarantees that the default behaviour (subscribing members based on CSL’s join_organisation
column without creating consents) will not be used.https://<YOUR.CSL.URL>/org/email_opt_in_types/advanced
. Currently reached via Settings > Email Opt-In Types > Manage Email Opt-In Types. Go ahead and set an External ID
(this is just a name, pick anything you like, for example email_opt_in_v1
). Grab the ID of your opt in to use later, you can see this in the URL when you click through like org/email_opt_in_types/advanced/123
would be opt-in ID 123
. consent_text = ConsentText.create!(
public_id: 'EMAIL_OPT_IN_V1',
consent_short_text: "I'D LIKE TO BE EMAILED ABOUT THIS CAMPAIGN! I HOPE YOUR CONSENT TEXT IS LESS SHOUTY THAN THIS!",
full_legal_text_link: 'https://YOUR.ORGANISATIONS/PRIVACY_POLICY'
)
controlshift_consent = ControlshiftConsent.create(controlshift_consent_type:"email_opt_in_type", controlshift_version_id:<PUT_YOUR_CSL_OPTIN_ID_HERE>, controlshift_consent_external_id:"EMAIL_OPT_IN_V1")
ControlshiftConsentMapping.create(controlshift_consent_id:controlshift_consent.id,consent_text_id:consent_text.id,method:"CSL-CHECKBOX",opt_in_level:"explicit_opt_in",opt_in_option:"CONSENT TEXT AGAIN HERE",opt_out_level:"none_given",opt_out_option:"OPPOSITE OF CONSENT TEXT")
public_id
and controlshift_consent_external_id
do not need to be the same, but your life will be less confusing and lead to greater enlightenment if they are.method
is just a string, try to put something descriptive in thereopt_in_level
and opt_out_level
must be one of no_change
, none_given
, implicit
, explicit_not_opt_out
or explicit_opt_in
. For more discussion of their meaning check docs on user consents.opt_in_option
and opt_out_option
are designed for a consent involving radio buttons, in which case they should correspond to the labels for the Yes and No options. If you’re using a checkbox, it’s best to put a copy of your consent short text in opt_in_option
and the opposite in opt_out_option
.https://<YOUR.CSL.URL>/org/consent_content_versions
to set an external ID and find the CSL ID (hover over the Add/Edit button in the External ID column). Then follow the same process to make the corresponding records in Identity, but use a controlshift_consent_type
of consent_content_version
instead of email_opt_in_type
.PROCESS_CONTROLSHIFT_CONSENTS
to true in Identity.MemberActionConsent
exists. PostConsentMethod.create(consent_text_id:YOUR_CONSENT_TEXT_ID,min_consent_level:4,max_consent_level:4,method:'email_subscribe')
PostConsentMethod.create(consent_text_id:YOUR_CONSENT_TEXT_ID,min_consent_level:1,max_consent_level:1,method:'email_unsubscribe')
YOUR_CONSENT_TEXT_ID
consent_level
here relates to the opt_in_level
and opt_out_level
we set above. Remember the levels are no_change
, none_given
, implicit
, explicit_not_opt_out
or explicit_opt_in
. In this case, after a consent is stored with a certain level (based on CSL saying checkbox ticked/unticked or which radio button was selected), we can run a certain method
.email_subscribe
and email_unsubscribe
are probably all you need, but if you want to do something more advanced feel free to take a look and add methods to member_action_consent.rb:post_consent_methods
.subscribe_by_id
and unsubscribe_by_id
. They work similarly to the code above, but just add a subscription_id
to express which subscription you want to use:
PostConsentMethod.create(consent_text_id:YOUR_CONSENT_TEXT_ID,min_consent_level:4,max_consent_level:4,method:'subscribe_by_id', subscription_id: YOUR_SUBSCRIPTION_ID)
PostConsentMethod.create(consent_text_id:YOUR_CONSENT_TEXT_ID,min_consent_level:1,max_consent_level:1,method:'unsubscribe_by_id', subscription_id: YOUR_SUBSCRIPTION_ID)
options.unsub_csl_when_unsub_email
, either by modifying your organisation’s settings file, or setting the env var UNSUB_CSL_WHEN_UNSUB_EMAIL
to trueIn some circumstances, whether GDPR does not apply to your organisation or you have your own specific legal advice about not needing separate consent records, you may not wish to configure consents between CSL and Identity, but you still want signatures in CSL to create members subscribed to email in Identity.
Disclaimer: This section is intended as advice about how identity should behave. As it is a core functionality for most organisations, with legal requirements in some jurisdictions, it is recommended that you understand and verify this setup for yourself. The best way to do this is to test it yourself - sign a petition and check if you’re subscribed in identity. The second best way is to have a techie in your organisation review Identity’s code.
Settings.gdpr.disable_auto_subscribe
. It’s actually false/disabled by default but to avoid being impacting by the default changing in the future, it’s safest to add it to your org’s settings .yml
file.join_organisation
column set to ‘true’ or ‘t’ and they previously had no subscription, or their previous unsubscribe time was earlier than the action being recorded.NOTE: This is a very aggressive configuration. Unless you are astonishingly confident that every new member record (not just the one’s coming from CSL) should be subscribed to every channel (email, SMS, push notifications, phone, facebook), then you should consider a different configuration.
Settings.options.allow_subscribe_via_upsert_member
- you need to add it to your org’s settings .yml
file, it is false
by default. This setting allows subscriptions to be modified in two ways - using defaults (with the setting below) or by passing a subscriptions
hash when uploading a member CSV.Settings.options.default_member_opt_in_subscriptions
- you can add this to your org’s settings yml
file, or simply add a DEFAULT_MEMBER_OPT_IN_SUBSCRIPTIONS
environment variable. This setting has no effect unless the previous one is enabled. When both are enabled, all new members will be subscribed to all default subscriptions (email, SMS, push notifications, phone, facebook)If you’d like to pre-fill member details (name and email address) on CSL petition pages, you should contact CSL support and ask them to add the “id” integration to your account. They will ask for your identity host (just the domain where your identity is hosted, no https://
) and API token. You can re-use the same API token from earlier in this guide, but you’ll need to grant extra permissions like this:
csl.role.permissions << Permission.find_by_slug('members-api')
After the integration is added, if you’re using consents, you can also configure CSL to avoid presenting the consent question to members who have already consented. Just follow the instructions in CSL and add the correct public ID(s).
In order for this feature to work, a member needs to click a link in an identity email, which briefly redirects them via identity and sets a cookie with their identity GUID, then CSL uses this GUID cookie to make a request to identity for the member’s details. It’s worth double checking that identity is rewriting your CSL links, you need to either have CSL running on a subdomain of Settings.app.base_domain
or you can enable Settings.options.add_tracking_to_all_links
to add tracking to all links. (This is true by default but that may change in future!)
If you would like the Anonymize Member Data
button to also delete user data in CSL, you’ll need to configure Identity as an OAuth client for CSL.
Identity
would be fine)urn:ietf:wg:oauth:2.0:oob
CONTROLSHIFT_API_CLIENT_ID
to the Application Id, CONTROLSHIFT_API_CLIENT_SECRET
to the Secret and CONTROLSHIFT_API_URL
to your base CSL URL like https://my.csl.com
.Available in identity version 20.04 and later
If you use petition categories in CSL and want to use that data in identity, you can import by enabling the setting features.pull_issue_categories_from_controlshift
in your org’s settings yaml file. If you’re enabling it for the first time and want to pull old category data, it’s worth running ControlshiftReviseIssueCategoriesWorker.perform_async(false)
once in a rails console to pull all the categories for your current petitions. It’s also synced nightly so if you change a petition’s categories, that will be reflected within 24 hours.
Once you’ve got your category data pulling into identity, you can access it via the “Issue Categories include” (has-taken-issue-category
) search filter. This allows you to quickly and easily gather all the people who have signed at least one petition marked with that category.
If you’re interested in summary information about your members’ category interests, you could take a look at these SQL queries as a starting point.
If you use speakout as well as CSL, you can replicate your category setup over there. You’ll need a speakout version of v0.18.0 or later. Configure these settings in Speakout:
Settings.tweaks.set_categories
- set to true so you can set categories on your campaignsSettings.identity_api.base_url
- the root address of your identity install, for example https://id.example.com/IDENTITY_API_TOKEN
(Env setting)- the api token speakout uses when making requests to identity. For more information on token configuration, check SECURITY.mdTo get you setup with an initial list of categories matching what you have in identity (so maybe pull from CSL first ;-)), you can run bundle exec rake categories:pull_categories_from_id
in Speakout. Now you’re all set and ready to configure categories in speakout campaigns. To make sure that info makes its way back to identity, make sure you enable features.pull_issue_categories_from_speakout
in your org’s identity settings yaml file.
I have a lot of signatures to import.