summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAzul <azul@riseup.net>2018-01-15 18:21:44 +0100
committerAzul <azul@riseup.net>2018-01-18 16:43:23 +0100
commitb8ba4f27a82868e0b3338b4af761f7c44226e729 (patch)
tree45b495e18bab72508342b86cd42ab3d56ed1eacc
parentfd2fc85c2daf60605641cc582d75134a10e7b4a4 (diff)
(WIP) first steps towards implementing keys API
-rw-r--r--app/controllers/api/keys_controller.rb63
-rw-r--r--app/models/keyring.rb38
-rw-r--r--config/routes.rb1
-rw-r--r--features/2/keys.feature (renamed from features/1/keys.feature)78
-rw-r--r--features/step_definitions/api_steps.rb4
-rw-r--r--features/step_definitions/key_steps.rb20
-rw-r--r--features/support/env.rb2
-rw-r--r--test/unit/keyring_test.rb60
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