summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorazul <azul@riseup.net>2018-01-28 08:37:45 -0800
committerazul <azul@riseup.net>2018-01-28 08:37:45 -0800
commit19606ec31d7a1606b5e9fc4df2440a6b470e9d97 (patch)
tree3a1c851033c46e1a140de3e3b5a17ad4b7f2647e
parentf77e48b1ffdc2350c454ced2fe1eba6446f7bc76 (diff)
parent54653f75cf44890310a06c3a8a6be59625629d2a (diff)
Merge branch 'feature/different-keytypes' into 'master'
initial feature description for key uploads See merge request leap/webapp!58
-rw-r--r--.gitattributes1
-rw-r--r--app/controllers/api/keys_controller.rb75
-rw-r--r--app/models/identity.rb5
-rw-r--r--app/models/keyring.rb55
-rw-r--r--config/routes.rb1
-rw-r--r--features/2/keys.feature257
-rw-r--r--features/step_definitions/api_steps.rb4
-rw-r--r--features/step_definitions/key_steps.rb26
-rw-r--r--features/support/env.rb2
-rw-r--r--test/unit/identity_test.rb8
-rw-r--r--test/unit/keyring_test.rb100
11 files changed, 531 insertions, 3 deletions
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..f306504
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+*.feature gitlab-language=gherkin
diff --git a/app/controllers/api/keys_controller.rb b/app/controllers/api/keys_controller.rb
new file mode 100644
index 0000000..7eb76ee
--- /dev/null
+++ b/app/controllers/api/keys_controller.rb
@@ -0,0 +1,75 @@
+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::NotFound => e
+ render status: 404, json: {error: e.message}
+ rescue Keyring::Error, ActionController::ParameterMissing => e
+ render status: 422, json: {error: e.message}
+ end
+
+ def destroy
+ keyring.delete type, rev: rev
+ head :no_content
+ rescue Keyring::NotFound => e
+ render status: 404, json: {error: e.message}
+ 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/identity.rb b/app/models/identity.rb
index 92f8f7a..b8c2245 100644
--- a/app/models/identity.rb
+++ b/app/models/identity.rb
@@ -136,6 +136,11 @@ class Identity < CouchRest::Model::Base
write_attribute('keys', keys.merge(type => key.to_s))
end
+ def delete_key(type)
+ raise 'key not found' unless keys[type]
+ write_attribute('keys', keys.except(type))
+ end
+
def cert_fingerprints
read_attribute('cert_fingerprints') || Hash.new
end
diff --git a/app/models/keyring.rb b/app/models/keyring.rb
new file mode 100644
index 0000000..66f7bfd
--- /dev/null
+++ b/app/models/keyring.rb
@@ -0,0 +1,55 @@
+#
+# Keyring
+#
+# A collection of cryptographic keys.
+#
+
+class Keyring
+ class Error < RuntimeError
+ end
+
+ class NotFound < Error
+ def initialize(type)
+ super "no such key: #{type}"
+ end
+ 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:)
+ check_rev type, rev
+ storage.set_key type, {type: type, value: value, rev: new_rev}.to_json
+ storage.save
+ end
+
+ def delete(type, rev:)
+ check_rev type, rev
+ storage.delete_key type
+ storage.save
+ end
+
+ def key_of_type(type)
+ JSON.parse(storage.keys[type]) if storage.keys[type]
+ end
+
+ protected
+ attr_reader :storage
+
+ def check_rev(type, rev)
+ old = key_of_type(type)
+ raise NotFound, type unless old
+ raise Error, "wrong revision: #{rev}" unless old['rev'] == rev
+ end
+
+ def new_rev
+ SecureRandom.urlsafe_base64(8)
+ end
+end
diff --git a/config/routes.rb b/config/routes.rb
index d3d2cec..55d03fa 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, :except=> [:edit, :new]
end
scope "(:locale)", :locale => CommonLanguages.match_available do
diff --git a/features/2/keys.feature b/features/2/keys.feature
new file mode 100644
index 0000000..83e70e7
--- /dev/null
+++ b/features/2/keys.feature
@@ -0,0 +1,257 @@
+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 /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
+ keytypes such as 'openpgp' and 'katzenpost_id' will only allow a
+ single key to be published. Others such as 'katzenpost_link' allow
+ multiple keys to be registered at the same time. We deal with this
+ by allowing arbitrary json data to be specified as the value of the
+ key. So katzenpost_link keys can be combined in a json data structure.
+
+ POST request will register a new key. In order to replace an existing
+ key you need to send a PATCH request to /keys/:type including the last
+ revision (rev) of the key. This way we can detect conflicts between
+ concurrend updates.
+
+ Background:
+ Given I authenticated
+ Given I set headers:
+ | Accept | application/json |
+ | Content-Type | application/json |
+ | Authorization | Token token="MY_AUTH_TOKEN" |
+
+ Scenario: Get initial empty set of keys
+ When I send a GET request to "2/keys"
+ Then the response status should be "200"
+ And the response should be:
+ """
+ {}
+ """
+
+ Scenario: Get all the keys
+ Given I have published a "openpgp" key
+ 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": "DUMMY_KEY",
+ "rev": "DUMMY_REV"
+ },
+ "katzenpost_link": {
+ "type": "katzenpost_link",
+ "value": {
+ "one": "DUMMY_KEY",
+ "two": "DUMMY_KEY"
+ },
+ "rev": "DUMMY_REV"
+ }
+ }
+ """
+
+ Scenario: Get a single key
+ Given I have published a "openpgp" key
+ When I send a GET request to "2/keys/openpgp"
+ Then the response status should be "200"
+ And the response should be:
+ """
+ {
+ "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 "2/keys/katzenpost_link"
+ Then the response status should be "200"
+ And the response should be:
+ """
+ {
+ "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 "2/keys" with the following:
+ """
+ {
+ "type": "openpgp",
+ "value": "ASDF"
+ }
+ """
+ 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 "2/keys" with the following:
+ """
+ {
+ "type": "openpgp",
+ "value": "QWER"
+ }
+ """
+ Then the response status should be "422"
+ And the response should be:
+ """
+ {
+ "error": "key already exists"
+ }
+ """
+
+ Scenario: Publishing an empty key fails
+ When I send a POST request to "2/keys" with the following:
+ """
+ {}
+ """
+ Then the response status should be "422"
+ And the response should be:
+ """
+ {
+ "error": "param is missing or the value is empty: type"
+ }
+ """
+
+ 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 a missing key raises
+ 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 "404"
+ And the response should be:
+ """
+ {
+ "error": "no such key: openpgp"
+ }
+ """
+ And I should not have published a "openpgp" key
+
+ Scenario: Updating an existing key require 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"
+ }
+ """
+ Then the response status should be "422"
+ And the response should be:
+ """
+ {
+ "error": "param is missing or the value is empty: rev"
+ }
+ """
+
+ 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": "WRONG_REV"
+ }
+ """
+ Then the response status should be "422"
+ And the response should be:
+ """
+ {
+ "error": "wrong revision: WRONG_REV"
+ }
+ """
+
+ Scenario: Deleting an existing key
+ Given I have published a "openpgp" key
+ When I send a DELETE request to "2/keys/openpgp" with the following:
+ """
+ {
+ "type": "openpgp",
+ "rev": "DUMMY_REV"
+ }
+ """
+ Then the response status should be "204"
+ And I should not have published a "openpgp" key
+
+ Scenario: Deleting a missing key raises
+ When I send a DELETE request to "2/keys/openpgp" with the following:
+ """
+ {
+ "type": "openpgp",
+ "rev": "DUMMY_REV"
+ }
+ """
+ Then the response status should be "404"
+ And the response should be:
+ """
+ {
+ "error": "no such key: openpgp"
+ }
+ """
+
+ Scenario: Deleting an existing key require revision
+ Given I have published a "openpgp" key
+ When I send a DELETE request to "2/keys/openpgp" with the following:
+ """
+ {
+ "type": "openpgp"
+ }
+ """
+ Then the response status should be "422"
+ And the response should be:
+ """
+ {
+ "error": "param is missing or the value is empty: rev"
+ }
+ """
+ And I should have published a "openpgp" key
+
+ Scenario: Deleting an existing key require right revision
+ Given I have published a "openpgp" key
+ When I send a DELETE request to "2/keys/openpgp" with the following:
+ """
+ {
+ "type": "openpgp",
+ "rev": "WRONG_REV"
+ }
+ """
+ Then the response status should be "422"
+ And the response should be:
+ """
+ {
+ "error": "wrong revision: WRONG_REV"
+ }
+ """
+ And I should have published a "openpgp" key
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..3d5e015
--- /dev/null
+++ b/features/step_definitions/key_steps.rb
@@ -0,0 +1,26 @@
+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
+
+Then /^I should not have published an? "([^"]*)" key$/ do |type|
+ identity = Identity.for(@user)
+ keys = identity.keys
+ refute_includes keys.keys, type
+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/identity_test.rb b/test/unit/identity_test.rb
index 6836487..43f644a 100644
--- a/test/unit/identity_test.rb
+++ b/test/unit/identity_test.rb
@@ -80,6 +80,14 @@ class IdentityTest < ActiveSupport::TestCase
assert_equal pgp_key_string, @id.keys[:pgp]
end
+ test "deleting pgp key" do
+ @id = Identity.for(@user)
+ @id.set_key(:pgp, pgp_key_string)
+ @id.delete_key(:pgp)
+ assert_nil @id.keys[:pgp]
+ assert_equal Hash.new, @id.keys
+ end
+
test "querying pgp key via couch" do
@id = Identity.for(@user)
@id.set_key(:pgp, pgp_key_string)
diff --git a/test/unit/keyring_test.rb b/test/unit/keyring_test.rb
new file mode 100644
index 0000000..c7df63e
--- /dev/null
+++ b/test/unit/keyring_test.rb
@@ -0,0 +1,100 @@
+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 missing key' do
+ assert_raises Keyring::NotFound do
+ keyring.update 'type', rev: nil ,value: 'new value'
+ end
+ assert_nil keyring.key_of_type('type')
+ 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
+
+ test 'delete key' do
+ keyring.create 'type', 'value'
+ initial_rev = keyring.key_of_type('type')['rev']
+ keyring.delete 'type', rev: initial_rev
+ assert_nil keyring.key_of_type('type')
+ end
+
+ test 'raise on deleting missing key' do
+ assert_raises Keyring::NotFound do
+ keyring.delete 'type', rev: nil
+ end
+ end
+
+ test 'raise on deleting without rev' do
+ keyring.create 'type', 'value'
+ assert_raises Keyring::Error do
+ keyring.delete 'type', rev: nil
+ end
+ assert_equal 'value', keyring.key_of_type('type')['value']
+ end
+
+ test 'raise on deleting with wrong rev' do
+ keyring.create 'type', 'value'
+ assert_raises Keyring::Error do
+ keyring.delete 'type', rev: 'wrong rev'
+ 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.delete_key(type)
+ self.delete(type)
+ end
+
+ def dummy.save; end
+ end
+ end
+end