diff options
| -rw-r--r-- | app/controllers/api/keys_controller.rb | 63 | ||||
| -rw-r--r-- | app/models/keyring.rb | 38 | ||||
| -rw-r--r-- | config/routes.rb | 1 | ||||
| -rw-r--r-- | features/2/keys.feature (renamed from features/1/keys.feature) | 78 | ||||
| -rw-r--r-- | features/step_definitions/api_steps.rb | 4 | ||||
| -rw-r--r-- | features/step_definitions/key_steps.rb | 20 | ||||
| -rw-r--r-- | features/support/env.rb | 2 | ||||
| -rw-r--r-- | test/unit/keyring_test.rb | 60 | 
8 files changed, 238 insertions, 28 deletions
| diff --git a/app/controllers/api/keys_controller.rb b/app/controllers/api/keys_controller.rb new file mode 100644 index 0000000..d4cb759 --- /dev/null +++ b/app/controllers/api/keys_controller.rb @@ -0,0 +1,63 @@ +class Api::KeysController < ApiController + +  before_filter :require_login +  before_filter :require_enabled + +  # get /keys +  def index +    keys = identity.keys.map do |k,v| +      [k, JSON.parse(v)] +    end +    render json: keys.to_h +  end + +  def show +    render json: JSON.parse(identity.keys[params[:id]]) +  end + +  def create +    keyring.create type, value +    head :no_content +  rescue Keyring::Error, ActionController::ParameterMissing => e +    render status: 422, json: {error: e.message} +  end + +  def update +    keyring.update type, rev: rev, value: value +    head :no_content +  rescue Keyring::Error, ActionController::ParameterMissing => e +    render status: 422, json: {error: e.message} +  end + +  protected + +  def require_enabled +    if !current_user.enabled? +      access_denied +    end +  end + +  def service_level +    current_user.effective_service_level +  end + +  def type +    params.require :type +  end + +  def value +    params.require :value +  end + +  def rev +    params.require :rev +  end + +  def keyring +    @keyring ||= Keyring.new identity +  end + +  def identity +    @identity ||= Identity.for(current_user) +  end +end diff --git a/app/models/keyring.rb b/app/models/keyring.rb new file mode 100644 index 0000000..6779d5d --- /dev/null +++ b/app/models/keyring.rb @@ -0,0 +1,38 @@ +# +# Keyring +# +# A collection of cryptographic keys. +# + +class Keyring +  class Error < RuntimeError +  end + +  def initialize(storage) +    @storage = storage +  end + +  def create(type, value) +    raise Error, "key already exists" if storage.keys[type].present? +    storage.set_key type, {type: type, value: value, rev: new_rev}.to_json +    storage.save +  end + +  def update(type, rev:, value:) +    old_rev = key_of_type(type)['rev'] +    raise Error, "wrong revision: #{rev}" unless old_rev == rev +    storage.set_key type, {type: type, value: value, rev: new_rev}.to_json +    storage.save +  end + +  def key_of_type(type) +    JSON.parse(storage.keys[type]) +  end + +  protected +  attr_reader :storage + +  def new_rev +    SecureRandom.urlsafe_base64(8) +  end +end diff --git a/config/routes.rb b/config/routes.rb index d3d2cec..ba8f168 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -41,6 +41,7 @@ LeapWeb::Application.routes.draw do      resource :service, :only => [:show]      resources :configs, :only => [:index, :show]      resources :identities, :only => [:show] +    resources :keys, :only=> [:index, :show, :create, :update]    end    scope "(:locale)", :locale => CommonLanguages.match_available do diff --git a/features/1/keys.feature b/features/2/keys.feature index 23af72f..cc87da0 100644 --- a/features/1/keys.feature +++ b/features/2/keys.feature @@ -3,7 +3,7 @@ Feature: Handle current users collection of keys    LEAP currently uses OpenPGP and is working on implementing katzenpost.    Both systems require public keys of a user to be available for retrival. -  The /1/keys endpoint allows the client to manage the public keys +  The /2/keys endpoint allows the client to manage the public keys    registered for their users email address.    You need to specify the type of the key when publishing it. Some @@ -26,7 +26,7 @@ Feature: Handle current users collection of keys        | Authorization | Token token="MY_AUTH_TOKEN" |    Scenario: Get initial empty set of keys -    When I send a GET request to "1/keys" +    When I send a GET request to "2/keys"      Then the response status should be "200"      And the response should be:      """ @@ -35,51 +35,59 @@ Feature: Handle current users collection of keys    Scenario: Get all the keys      Given I have published a "openpgp" key -    And I have published "katzenpost_kink" keys -    When I send a GET request to "1/keys" +    And I have published "katzenpost_link" keys +    When I send a GET request to "2/keys"      Then the response status should be "200"      And the response should be:      """      {      "openpgp": {        "type": "openpgp", -      "value": "ASDF", -      "rev": "1234567890" +      "value": "DUMMY_KEY", +      "rev": "DUMMY_REV"        },      "katzenpost_link": {        "type": "katzenpost_link",        "value": { -        "one": "ASDF", -        "two": "QWER" +        "one": "DUMMY_KEY", +        "two": "DUMMY_KEY"        }, -      "rev": "1234567890" +      "rev": "DUMMY_REV"        }      }      """    Scenario: Get a single key      Given I have published a "openpgp" key -    When I send a GET request to "1/keys/openpgp" +    When I send a GET request to "2/keys/openpgp"      Then the response status should be "200"      And the response should be:      """ -    "ASDF" +    { +      "type": "openpgp", +      "value": "DUMMY_KEY", +      "rev": "DUMMY_REV" +    }      """    Scenario: Get a set of keys for one type      Given I have published "katzenpost_link" keys -    When I send a GET request to "1/keys/katzenpost_link" +    When I send a GET request to "2/keys/katzenpost_link"      Then the response status should be "200"      And the response should be:      """        { -        "one": "ASDF", -        "two": "QWER" +        "type": "katzenpost_link", +        "value": { +          "one": "DUMMY_KEY", +          "two": "DUMMY_KEY" +          }, +        "rev": "DUMMY_REV"        }      """    Scenario: Publish an initial OpenPGP key -    When I send a POST request to "1/keys" with the following: +    When I send a POST request to "2/keys" with the following:      """        {        "type": "openpgp", @@ -87,10 +95,11 @@ Feature: Handle current users collection of keys        }      """      Then the response status should be "204" +    And I should have published a "openpgp" key    Scenario: Do not overwrite an existing key      Given I have published a "openpgp" key -    When I send a POST request to "1/keys" with the following: +    When I send a POST request to "2/keys" with the following:      """        {        "type": "openpgp", @@ -105,9 +114,22 @@ Feature: Handle current users collection of keys        }      """ +  Scenario: Updating an existing key +    Given I have published a "openpgp" key +    When I send a PATCH request to "2/keys/openpgp" with the following: +    """ +      { +      "type": "openpgp", +      "value": "QWER", +      "rev": "DUMMY_REV" +      } +    """ +    Then the response status should be "204" +    And I should have published a "openpgp" key with value "QWER" +    Scenario: Updating an existing key require revision      Given I have published a "openpgp" key -    When I send a PATCH request to "1/keys/openpgp" with the following: +    When I send a PATCH request to "2/keys/openpgp" with the following:      """        {        "type": "openpgp", @@ -118,24 +140,30 @@ Feature: Handle current users collection of keys      And the response should be:      """        { -      "error": "no revision specified" +      "error": "param is missing or the value is empty: rev"        }      """ -  Scenario: Updating an existing key -    Given I have published a "openpgp" key with revision "1234567890" -    When I send a PATCH request to "1/keys/openpgp" with the following: +  Scenario: Updating an existing key require right revision +    Given I have published a "openpgp" key +    When I send a PATCH request to "2/keys/openpgp" with the following:      """        {        "type": "openpgp",        "value": "QWER", -      "rev": "1234567890" +      "rev": "WRONG_REV" +      } +    """ +    Then the response status should be "422" +    And the response should be: +    """ +      { +      "error": "wrong revision: WRONG_REV"        }      """ -    Then the response status should be "204"    Scenario: Publishing an empty key fails -    When I send a POST request to "1/keys" with the following: +    When I send a POST request to "2/keys" with the following:      """        {}      """ @@ -143,6 +171,6 @@ Feature: Handle current users collection of keys      And the response should be:      """        { -      "error": "key type missing" +      "error": "param is missing or the value is empty: type"        }      """ diff --git a/features/step_definitions/api_steps.rb b/features/step_definitions/api_steps.rb index 7188694..7b73272 100644 --- a/features/step_definitions/api_steps.rb +++ b/features/step_definitions/api_steps.rb @@ -37,7 +37,7 @@ When /^I digest\-authenticate as the user "(.*?)" with the password "(.*?)"$/ do    digest_authorize user, pass  end -When /^I (?:have sent|send) a (GET|POST|PUT|DELETE) request (?:for|to) "([^"]*)"(?: with the following:)?$/ do |*args| +When /^I (?:have sent|send) a (GET|POST|PUT|DELETE|PATCH) request (?:for|to) "([^"]*)"(?: with the following:)?$/ do |*args|    request_type = args.shift    path = args.shift    input = args.shift @@ -45,7 +45,7 @@ When /^I (?:have sent|send) a (GET|POST|PUT|DELETE) request (?:for|to) "([^"]*)"    request_opts = {method: request_type.downcase.to_sym}    unless input.nil? -    if input.class == Cucumber::Ast::Table +    if input.class == Cucumber::MultilineArgument::DataTable        request_opts[:params] = input.rows_hash      else        request_opts[:input] = input diff --git a/features/step_definitions/key_steps.rb b/features/step_definitions/key_steps.rb new file mode 100644 index 0000000..70a13bd --- /dev/null +++ b/features/step_definitions/key_steps.rb @@ -0,0 +1,20 @@ +Given /^I have published a "([^"]*)" key$/ do |type| +  identity = Identity.for(@user) +  keyring = Keyring.new(identity) +  SecureRandom.stubs(urlsafe_base64: 'DUMMY_REV') +  keyring.create type, 'DUMMY_KEY' +end + +Given /^I have published "([^"]*)" keys$/ do |type| +  identity = Identity.for(@user) +  keyring = Keyring.new(identity) +  SecureRandom.stubs(urlsafe_base64: 'DUMMY_REV') +  keyring.create type, one: 'DUMMY_KEY', two: 'DUMMY_KEY' +end + +Then /^I should have published an? "([^"]*)" key(?: with value "([^"]*)")?$/ do |type, value| +  identity = Identity.for(@user) +  keys = identity.keys +  assert_includes keys.keys, type +  assert_equal value, JSON.parse(keys[type])['value'] if value +end diff --git a/features/support/env.rb b/features/support/env.rb index d3067db..d722b8e 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -55,4 +55,4 @@ end  # The :transaction strategy is faster, but might give you threading problems.  # See https://github.com/cucumber/cucumber-rails/blob/master/features/choose_javascript_database_strategy.feature  Cucumber::Rails::Database.javascript_strategy = :truncation - +require 'mocha/setup' diff --git a/test/unit/keyring_test.rb b/test/unit/keyring_test.rb new file mode 100644 index 0000000..059b8dd --- /dev/null +++ b/test/unit/keyring_test.rb @@ -0,0 +1,60 @@ +require 'test_helper' + +class KeyringTest < ActiveSupport::TestCase + +  test 'create initial key' do +    keyring.create 'type', 'value' +    assert_equal 'value', keyring.key_of_type('type')['value'] +  end + +  test 'raise on creating twice' do +    keyring.create 'type', 'value' +    assert_raises Keyring::Error do +      keyring.create 'type', 'value' +    end +  end + +  test 'update with new key' do +    keyring.create 'type', 'value' +    initial_rev = keyring.key_of_type('type')['rev'] +    keyring.update 'type', rev: initial_rev, value: 'new value' +    assert_equal 'new value', keyring.key_of_type('type')['value'] +  end + +  test 'raise on updating without rev' do +    keyring.create 'type', 'value' +    assert_raises Keyring::Error do +      keyring.update 'type', rev: nil ,value: 'new value' +    end +    assert_equal 'value', keyring.key_of_type('type')['value'] +  end + +  test 'raise on updating with wrong rev' do +    keyring.create 'type', 'value' +    assert_raises Keyring::Error do +      keyring.update 'type', rev: 'wrong rev', value: 'new value' +    end +    assert_equal 'value', keyring.key_of_type('type')['value'] +  end + + +  protected + +  def keyring +    @keyring ||= Keyring.new(teststorage) +  end + +  def teststorage +    @teststorage ||= Hash.new.tap do |dummy| +      def dummy.set_key(type, value) +        self[type] = value +      end + +      def dummy.keys +        self +      end + +      def dummy.save; end +    end +  end +end | 
