TUTORIAL – RUBY API

Introduction

The Conjur API for Ruby provides a robust programmatic interface to Conjur. You can use the Ruby API to authenticate with Conjur, load policies, fetch secrets, perform permission checks, and more.

Prerequisites

Setup

To demonstrate the usage the Conjur API, we need some sample data loaded into the server.

Save this file as “conjur.yml”:

- !policy
  id: db
  body:
  - &variables
    - !variable password

  - !group secrets-users

  - !permit
    resource: *variables
    privileges: [ read, execute ]
    roles: !group secrets-users

- !policy
  id: backend
  body:
  - !webservice

  - !group clients

  - !permit
    resource: !webservice
    privilege: execute
    roles: !group clients

- !policy
  id: myapp
  body:
  - !layer
    annotations:
      description: My application layer

- !host myapp-01

- !grant
  role: !layer myapp
  member: !host myapp-01

- !grant
  role: !group db/secrets-users
  member: !host myapp-01

- !grant
  role: !group backend/clients
  member: !host myapp-01

It defines:

  • variable:db/password Contains the database password.
  • webservice:backend
  • layer:myapp A layer (group of hosts) with access to the password and the webservice.

In addition, it ensures that the host myapp-01 is able to access the db/password secret and is a member of the backend/clients group that has permission to execute the backend webservice.

Load the policy using the following command:

$ conjur policy load --replace root conjur.yml
Loaded policy 'root'
{
  "created_roles": {
    "myorg:host:myapp-01": {
      "id": "myorg:host:myapp-01",
      "api_key": "1wgv7h2pw1vta2a7dnzk370ger03nnakkq33sex2a1jmbbnz3h8cye9"
    }
  },
  "version": 1
}

Now, use OpenSSL to generate a random secret, and load it into the database password variable:

$ password=$(openssl rand -hex 12)
$ echo $password
ac8932bccf835a5a13586100
$ conjur variable values add db/password $password
Value added
$ conjur variable value db/password
ac8932bccf835a5a13586100

Configuration

The Ruby API is configured using the Conjur.configuration object. The most important options are:

  • appliance_url The URL to the Conjur server.
  • account The Conjur organization account name.

Create a new Ruby program, require the conjur-api library, and set these two parameters in the following manner:

irb(main)> require 'conjur-api'
irb(main)> Conjur.configuration.appliance_url = "https://eval.conjur.org"
irb(main)> Conjur.configuration.account = "myorg" # <- REPLACE ME!
Note Configuration can also be provided via environment variables. The environment variable pattern is CONJUR_<setting>. For example, CONJUR_APPLIANCE_URL=https://eval.conjur.org

Authentication

Once the server connection is configured, the next step is to authenticate to obtain an access token. When you create a Conjur Host, the server issues an API key which you can use to authenticate as that host. Here’s how you use it in Ruby (note: token formatted and abridged in the interests of readability):

irb(main)> host_id = "host/myapp-01"
irb(main)> api_key = "1vgw4jzvyzmay95mrx2s5ad1d28gt3gh2gesb1411kqcah3nrv01r"
irb(main)> conjur = Conjur::API.new_from_key host_id, api_key
irb(main)> puts conjur.token
{
  "protected": "eyJhbGciOiJjb25qdXIub3JnL3Nsb3NpbG8vdjIiLCJra
WQiOiIzZTY4N2E3N2Q0ZjkzOTkxYzZmMzBkMzkzYTNmZGM1MyJ9",
  "payload": "eyJzdWIiOiJhbGljZSIsImlhdCI6MTUwNTgzMjg1NX0=",
  "signature": "jzwY1MmbYQUEl[...]6l2OyHzDx"
}
Note Authentication credentials can also be provided via environment variables. Use CONJUR_AUTHN_LOGIN for the login name, and
CONJUR_AUTHN_API_KEY for the API key.

Secrets Fetching

Once authenticated, the API client can be used to fetch the database password:

irb(main)> variable = conjur.resource("#{Conjur.configuration.account}:variable:db/password")
irb(main)> puts variable.value
ef0a4822539369659fbfb267

Permission Checking

To check a permission, load the Conjur resource (typically a Webservice) on which the permission is defined.

Then use the permitted? method to test whether the Conjur user has a specified privilege on the resource.

In this example, we determine that host:myapp-01 is permitted to execute but not update the resource webservice:backend:

irb(main)> webservice = conjur.resource("#{Conjur.configuration.account}:webservice:backend")
irb(main)> puts webservice.permitted? 'execute'
true
irb(main)> puts webservice.permitted? 'update'
false

Webservice Authorization

Conjur can provide a declarative system for authenticating and authorizing access to web services. As we have seen above, the first step is to create a !webservice object in a policy. Then, privileges on the web service can be managed using !permit and !grant.

In the runtime environment, a bit of code will intercept the inbound request and check for authentication and authorization. Here’s how to simulate that in an interactive Ruby session.

First, the web service client will authenticate with Conjur to obtain an access token. Then this token is formed into an HTTP Authorization header:

irb(main)> token = conjur.token
irb(main)> require 'base64'
irb(main)> token_header = %Q(Token token="#{Base64.strict_encode64 token.to_json}")
=> "Authorization: Token token=\"eyJkYXRh...k4YjQifQ==\""

Of course, the client does not have to be Ruby, it can be any language or even a tool like cURL.

On the server side, the HTTP request is intercepted and the access token is parsed out of the header.

irb(main)> token_header[/^Token token="(.*)"/]
irb(main)> token = JSON.parse(Base64.decode64($1))

Once the token is obtained, it can be used to construct a Conjur::API object. Then the webservice resource is obtained from the Conjur API, and the permission check is performed:

irb(main)> conjur = Conjur::API.new_from_token token
irb(main)> webservice = conjur.resource("#{Conjur.configuration.account}:webservice:backend")
irb(main)> puts webservice.permitted? 'execute'
true

If the token is expired or invalid, then the Conjur API will raise an authentication error. If the token is valid, but the client does not have the requested privilege, then permitted? will return false. In either case, the authorization interceptor should deny access to the web service function.

Note that different web service functions may have different required privileges. For example, a GET method may require read, and a DELETE method may require update. The semantic mapping between the web service methods and the Conjur privileges is up to you.

Sinatra Example

Now let’s run through an example of implementing the above using Sinatra.

First, let’s create a new folder for our demo project MyApp:

$ mkdir my_app
$ cd my_app

Next, let’s create Gemfile to manage our dependencies:

# Gemfile

source "https://rubygems.org"

gem 'conjur-api', git: 'https://github.com/conjurinc/api-ruby.git', branch: 'possum'
gem 'sinatra', '~> 2.0'

Note that we’re building the Conjur API gem from source rather than downloading it from Ruby Gems. The Conjur CE release marks a major overhaul of the API. This version will be published to Ruby Gems prior to our public release.

Now we can install all our required gems with:

$ bundle install

Now let’s setup our Rackup file:

# config.ru

require 'rubygems'
require 'bundler'

Bundler.require

# Setup our Conjur configuration
Conjur.configuration.account = ENV['CONJUR_ACCOUNT']
Conjur.configuration.appliance_url = ENV['CONJUR_URL']

require './my_app'

run Sinatra::Application

The above will let us pass our Conjur Account name and URL when we start the app.

Now let’s write a simple page that displays the secret stored in Conjur using the Host API token above:

# my_app.rb

class MyApp < Sinatra::Base
  get "/" do
    <<-CONTENT
      <html>
        <head></head>
        <body>
          <h1>Welcome to the Demo App</h1>
          <p><strong>DB Password:</strong> #{retrieve_secret('db/password')}</p>
        </body>
      </html>
    CONTENT
  end

  protected

  # Helper method for managing a connection to the Conjur API
  def conjur_api
    @conjur_api ||= begin
      Conjur::API.new_from_key(ENV['CONJUR_USER'], ENV['CONJUR_API_KEY'])
    end
  end

  # Helper method to access the variable by its resource id (`account:resource_type:name`)
  # The `resource` method will fail with:
  #   RestClient::Unauthorized if the entity does not have execute permission.
  #   RestClient::ResourceNotFound if the resource does not exist.
  def retrieve_secret(variable)
    conjur_api.resource("#{Conjur.configuration.account}:variable:#{variable}").value
  end
end

The above application can be run with:

$ CONJUR_USER=host/myapp-01 \
  CONJUR_API_KEY=host/myapp-01 API as used prior # <- REPLACE ME! \
  CONJUR_ACCOUNT=myorg # <- REPLACE ME! \
  CONJUR_URL=https://eval.conjur.org \
  rackup

and displays the value of the secret stored in Conjur.

Next Steps