Skip to end of metadata
Go to start of metadata

Learning Objective

After completing this lesson, you will be able to:

  • use modules in KRL
  • wrap an external API call and make it available from a module
  • use the keys pragma for API keys

Prerequisites

You should have done the following before starting this lesson:

Contents

Modules

Modules make KRL programming easier. KRL has a powerful parameterized module facility. 

In this lesson, we're going to explore how modules can be used to wrap an external API and make it available inside a pico. Along the way we'll explore using HTTP actions, creating our own actions, and managing API keys. 

For this lesson, we're going to be using the Twilio API to send SMS messages. Twilio provides a RESTful API for both voice and SMS interactions. Before you can complete this lesson, you'll need to register for a Twilio account. 

An SMS Action

Our first attempt will ignore some aspects of creating a useful module in order to just get something that works. Later we'll come back and clean those up. 

We'll start by defining an action called send_sms() that takes all of the information we need to use the Twilio API to send a message, including the Twilio API keys. 

A user-defined action is similar to a KRL function in that its body consists of a set of declarations, but instead of the last line being an expression representing the return value, the last line is an action. We used the defaction() keyword to define the function. In our case, the action will be the http:post() action that POSTs to the Twilio API. Here's our action definition:

send_sms = defaction(to, from, message, account_sid, auth_token){
  base_url = <<https://#{account_sid}:#{auth_token}@api.twilio.com/2010-04-01/Accounts/#{account_sid}/>>
  http:post(base_url + "Messages.json", form = 
                {"From":from,
                 "To":to,
                 "Body":message
                })
}

The action takes the following arguments:

  • to - the number to send the SMS to
  • from - the number the SMS is coming from (this must be a number from your Twilio account)
  • message - the message to send
  • account_sid - the Twilio account SID
  • auth_token - the Twilio authentication token

The action creates a base URL using the instructions in the Twilio API docs and then POSTs to that URL. 

We can test this action, by creating a rule that uses it as follows. 

rule test_send_sms {
  select when test new_message
  send_sms(event:attr("to"),
           event:attr("from"),
           event:attr("message"),
           event:attr("account_sid"),
           event:attr("auth_token"))
}

This simple rule selects on the test:new_message event and uses the action we just defined. All the needed arguments must be included as event attributes. 

After we install a ruleset containing this action definition and rule in our pico, we can use the Sky Event Console to publish an event to the pico. If you're using valid values for the account SID, authentication token, and from phone number, the to phone number should receive an SMS with the message you give. 

The user-defined action hides some of the details of the Twilio API and presents a simple action that any rule can use to send an SMS. 

Actions and Functions

Unlike many programming languages, KRL rules make a syntactic distinction between operations that merely read state and those that change it. The KRL prelude is expected to not have an effect on state, either internal or external to the ruleset. This is more than stylistic; there are security and programming advantages to this segregation. Specifically, the action (and postlude) are conditional while the prelude declarations are not. Consequently, state changes made in the prelude that affect state are difficult to guard.

KRL itself takes pains to not permit state changing language features in the prelude, but a module developer could create functions that make state changes in the external API. In general, you should avoid this so that your module is easy for developers to use correctly and easily. In KRL rules, actions should generally be used for API requests done with a POST, PUT, or DEL since those requests change state in the API while GET requests should be used in functions. This ensures that operations in your API that affect external state can be guarded by the rule's conditional and any calls made in the prelude or event expression will not change state. Of course, how your module does this will depend a lot on how the API is built. 

A Module for Twilio

The previously defined action is not as useful as it could be because it requires that the account SID and authorization token be passed in each time. This is where KRL's parameterized modules come in handy. 

Modules are parameterized using a configure pragma in the meta block. The variables declared in the configure pragma take on the values that are set when the module is used. The configuration allows for default values. 

The following is a complete module for the send_sms() action using configuration. 

ruleset io.picolabs.twilio_v2 {
  meta {
    configure using account_sid = ""
                    auth_token = ""
    provides 
        send_sms
  }

  global {
    send_sms = defaction(to, from, message) {
       base_url = <<https://#{account_sid}:#{auth_token}@api.twilio.com/2010-04-01/Accounts/#{account_sid}/>>
       http:post(base_url + "Messages.json", form = {
                "From":from,
                "To":to,
                "Body":message
            })
    }
  }
}

The configure pragma's defaults are the empty string since there's no workable default for the Twilio keys. You can see that those variables are referenced in the send_sms() action to create the based URL for the Twilio call. 

We use a parameterized module by declaring its use in the meta block of the ruleset that wants to use the module. The use pragma allows parameters to be supplied for configuration variables. The following shows a ruleset that uses the Twilio module. 

ruleset io.picolabs.use_twilio_v2 {
  meta {
    key twilio {
          "account_sid": "<your SID goes here>",  
          "auth_token" : "<your auth token goes here>" 
    }
    use module io.picolabs.twilio_v2 alias twilio
        with account_sid = keys:twilio("account_sid")
             auth_token =  keys:twilio("auth_token")
  }

  rule test_send_sms {
    select when test new_message
    twilio:send_sms(event:attr("to"),
                    event:attr("from"),
                    event:attr("message")
                   )
  }
}

In this example, we've used the key pragma to declare the Twilio keys. Those keys are included in the use pragma that uses the Twilio module. By putting the keys in a key pragma, we tell the KRL parser and interpreter that they are keys so that it can take greater care. For example, they could be automatically redacted when shared. 

Security Warning

Don't put keys in rulesets that are publicly hosted, on GitHub for example. Any ruleset containing keys must be carefully stored so that only authorized parties can see them. See the Security Considerations section of the KRL documentation on keys for more information.

But, putting the keys in the ruleset means that it can't be publicly hosted and we have to take special care to ensure the keys aren't exposed. Consequently, best practice is to store keys in a module specifically designed for keys. That way, you only have to be careful with modules you are aware have keys. 

We can do this by defining a keys module that only contains keys. The provides keys pragma limits which keys are shared with which module for security. 

ruleset io.picolabs.lesson_keys {
  meta {
    key twilio {
          "account_sid": "<your account SID here>",  
          "auth_token" : "<your auth token here>" 
    }
    provides keys twilio to io.picolabs.use_twilio_v2
  }
}

We modify the preceding ruleset using the keys to load this ruleset as a module like so:

ruleset io.picolabs.use_twilio_v2 {
  meta {
  	use module io.picolabs.lesson_keys 
    use module io.picolabs.twilio_v2 alias twilio
        with account_sid = keys:twilio("account_sid")
             auth_token =  keys:twilio("auth_token")
  }

  rule test_send_sms {
    select when test new_message
    twilio:send_sms(event:attr("to"),
                    event:attr("from"),
                    event:attr("message")
                   )
  }
}

For this to work, the module containing the keys must be registered as io.picolabs.twilio_keys because that's the name of the module we load. The ruleset using the keys must be registered as io.piolabs.use_twilio_v2 since that's the name of the ruleset the keys are provided to in the provide keys pragma. You can store the ruleset containing the keys on AWS S3 behind a private URL, on a Web server with Basic Auth, or using some other scheme that will protect it from viewing by people who would steal the keys. The ruleset using the keys can be stored at a public URL.

Conclusion

We now have a Twilio module that wraps the Twilio API in functions and actions that are more convenient for KRL rulesets to use. We've also taken steps to protect sensitive keys while making their use convenient. 



  • No labels