summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorazul <azul@leap.se>2014-05-26 10:08:07 +0200
committerazul <azul@leap.se>2014-05-26 10:08:07 +0200
commitdf298887221cffc8cacc8965d73a0d7850118849 (patch)
treee13fc7c05956b10ca051377b89487d97e659528d
parent0f686b1256b4190522bcb101ba06cd2c7406eb36 (diff)
parentf221e5313fe54a2efa127b547916c7c812110449 (diff)
Merge pull request #165 from azul/feature/cert-fingerprints
Feature/cert fingerprints
-rw-r--r--app/controllers/v1/certs_controller.rb8
-rw-r--r--app/controllers/v1/smtp_certs_controller.rb37
-rw-r--r--app/models/client_certificate.rb8
-rw-r--r--app/models/email.rb5
-rw-r--r--app/models/identity.rb11
-rw-r--r--config/routes.rb3
-rw-r--r--test/functional/v1/certs_controller_test.rb20
-rw-r--r--test/functional/v1/smtp_certs_controller_test.rb36
-rw-r--r--test/integration/api/cert_test.rb30
-rw-r--r--test/integration/api/smtp_cert_test.rb52
-rw-r--r--test/support/api_integration_test.rb26
-rw-r--r--test/support/assert_responses.rb30
-rw-r--r--test/support/auth_test_helper.rb4
-rw-r--r--test/support/browser_integration_test.rb1
-rw-r--r--test/unit/user_test.rb7
15 files changed, 266 insertions, 12 deletions
diff --git a/app/controllers/v1/certs_controller.rb b/app/controllers/v1/certs_controller.rb
index 73409ef..b6d1d0b 100644
--- a/app/controllers/v1/certs_controller.rb
+++ b/app/controllers/v1/certs_controller.rb
@@ -3,7 +3,15 @@ class V1::CertsController < ApplicationController
before_filter :require_login, :unless => :anonymous_certs_allowed?
# GET /cert
+ # deprecated - we actually create a new cert and that can
+ # be reflected in the action. GET /cert will eventually go
+ # away and be replaced by POST /cert
def show
+ create
+ end
+
+ # POST /cert
+ def create
@cert = ClientCertificate.new(:prefix => service_level.cert_prefix)
render text: @cert.to_s, content_type: 'text/plain'
end
diff --git a/app/controllers/v1/smtp_certs_controller.rb b/app/controllers/v1/smtp_certs_controller.rb
new file mode 100644
index 0000000..377a49c
--- /dev/null
+++ b/app/controllers/v1/smtp_certs_controller.rb
@@ -0,0 +1,37 @@
+class V1::SmtpCertsController < ApplicationController
+
+ before_filter :require_login
+ before_filter :require_email_account
+ before_filter :fetch_identity
+
+ # POST /1/smtp_cert
+ def create
+ @cert = ClientCertificate.new prefix: current_user.email_address
+ @identity.register_cert(@cert)
+ @identity.save
+ render text: @cert.to_s, content_type: 'text/plain'
+ end
+
+ protected
+
+ #
+ # Filters
+ #
+
+ def require_email_account
+ access_denied unless service_level.provides? 'email'
+ end
+
+ def fetch_identity
+ @identity = current_user.identity
+ end
+
+ #
+ # Helper methods
+ #
+
+ def service_level
+ current_user.effective_service_level
+ end
+
+end
diff --git a/app/models/client_certificate.rb b/app/models/client_certificate.rb
index 76b07a2..63de9e1 100644
--- a/app/models/client_certificate.rb
+++ b/app/models/client_certificate.rb
@@ -43,8 +43,16 @@ class ClientCertificate
self.key.to_pem + self.cert.to_pem
end
+ def fingerprint
+ OpenSSL::Digest::SHA1.hexdigest(openssl_cert.to_der).scan(/../).join(':')
+ end
+
private
+ def openssl_cert
+ cert.openssl_body
+ end
+
def self.root_ca
@root_ca ||= begin
crt = File.read(APP_CONFIG[:client_ca_cert])
diff --git a/app/models/email.rb b/app/models/email.rb
index a9a503f..4090275 100644
--- a/app/models/email.rb
+++ b/app/models/email.rb
@@ -7,6 +7,11 @@ class Email < String
:message => "needs to be a valid email address"
}
+ # Make sure we can call Email.new(nil) and get an invalid email address
+ def initialize(s)
+ super(s.to_s)
+ end
+
def to_partial_path
"emails/email"
end
diff --git a/app/models/identity.rb b/app/models/identity.rb
index ad8c01e..a4225e7 100644
--- a/app/models/identity.rb
+++ b/app/models/identity.rb
@@ -8,6 +8,7 @@ class Identity < CouchRest::Model::Base
property :address, LocalEmail
property :destination, Email
property :keys, HashWithIndifferentAccess
+ property :cert_fingerprints, Hash
validate :unique_forward
validate :alias_available
@@ -107,6 +108,16 @@ class Identity < CouchRest::Model::Base
write_attribute('keys', keys.merge(type => key.to_s))
end
+ def cert_fingerprints
+ read_attribute('cert_fingerprints') || Hash.new
+ end
+
+ def register_cert(cert)
+ today = DateTime.now.to_date.to_s
+ write_attribute 'cert_fingerprints',
+ cert_fingerprints.merge(cert.fingerprint => today)
+ end
+
# for LoginFormatValidation
def login
self.address.handle
diff --git a/config/routes.rb b/config/routes.rb
index 9e0b72d..4ccfe62 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -32,7 +32,8 @@ LeapWeb::Application.routes.draw do
delete "logout" => "sessions#destroy", :as => "logout"
resources :users, :only => [:create, :update, :destroy, :index]
resources :messages, :only => [:index, :update]
- resource :cert, :only => [:show]
+ resource :cert, :only => [:show, :create]
+ resource :smtp_cert, :only => [:create]
resource :service, :only => [:show]
end
diff --git a/test/functional/v1/certs_controller_test.rb b/test/functional/v1/certs_controller_test.rb
index fb8e9c4..ec34b01 100644
--- a/test/functional/v1/certs_controller_test.rb
+++ b/test/functional/v1/certs_controller_test.rb
@@ -2,26 +2,34 @@ require 'test_helper'
class V1::CertsControllerTest < ActionController::TestCase
- test "send unlimited cert without login" do
+ test "create unlimited cert without login" do
with_config allow_anonymous_certs: true do
cert = expect_cert('UNLIMITED')
- get :show
+ post :create
assert_response :success
assert_equal cert.to_s, @response.body
end
end
- test "send limited cert" do
+ test "create limited cert" do
with_config allow_limited_certs: true do
login
cert = expect_cert('LIMITED')
- get :show
+ post :create
assert_response :success
assert_equal cert.to_s, @response.body
end
end
- test "send unlimited cert" do
+ test "create unlimited cert" do
+ login effective_service_level: ServiceLevel.new(id: 2)
+ cert = expect_cert('UNLIMITED')
+ post :create
+ assert_response :success
+ assert_equal cert.to_s, @response.body
+ end
+
+ test "GET still works as an alias" do
login effective_service_level: ServiceLevel.new(id: 2)
cert = expect_cert('UNLIMITED')
get :show
@@ -30,7 +38,7 @@ class V1::CertsControllerTest < ActionController::TestCase
end
test "redirect if no eip service offered" do
- get :show
+ post :create
assert_response :redirect
end
diff --git a/test/functional/v1/smtp_certs_controller_test.rb b/test/functional/v1/smtp_certs_controller_test.rb
new file mode 100644
index 0000000..9281ae6
--- /dev/null
+++ b/test/functional/v1/smtp_certs_controller_test.rb
@@ -0,0 +1,36 @@
+require 'test_helper'
+
+class V1::SmtpCertsControllerTest < ActionController::TestCase
+
+ test "no smtp cert without login" do
+ with_config allow_anonymous_certs: true do
+ post :create
+ assert_login_required
+ end
+ end
+
+ test "require service level with email" do
+ login
+ post :create
+ assert_access_denied
+ end
+
+ test "send cert with username" do
+ login effective_service_level: ServiceLevel.new(id: 2)
+ cert = expect_cert(@current_user.email_address)
+ cert.expects(:fingerprint).returns('fingerprint')
+ post :create
+ assert_response :success
+ assert_equal cert.to_s, @response.body
+ end
+
+ protected
+
+ def expect_cert(prefix)
+ cert = stub :to_s => "#{prefix.downcase} cert"
+ ClientCertificate.expects(:new).
+ with(:prefix => prefix).
+ returns(cert)
+ return cert
+ end
+end
diff --git a/test/integration/api/cert_test.rb b/test/integration/api/cert_test.rb
new file mode 100644
index 0000000..74d439a
--- /dev/null
+++ b/test/integration/api/cert_test.rb
@@ -0,0 +1,30 @@
+require 'test_helper'
+
+class CertTest < ApiIntegrationTest
+
+ test "retrieve eip cert" do
+ login
+ get '/1/cert', {}, RACK_ENV
+ assert_text_response
+ assert_response_includes "BEGIN RSA PRIVATE KEY"
+ assert_response_includes "END RSA PRIVATE KEY"
+ assert_response_includes "BEGIN CERTIFICATE"
+ assert_response_includes "END CERTIFICATE"
+ end
+
+ test "fetching certs requires login by default" do
+ get '/1/cert', {}, RACK_ENV
+ assert_json_response error: I18n.t(:not_authorized)
+ end
+
+ test "retrieve anonymous eip cert" do
+ with_config allow_anonymous_certs: true do
+ get '/1/cert', {}, RACK_ENV
+ assert_text_response
+ assert_response_includes "BEGIN RSA PRIVATE KEY"
+ assert_response_includes "END RSA PRIVATE KEY"
+ assert_response_includes "BEGIN CERTIFICATE"
+ assert_response_includes "END CERTIFICATE"
+ end
+ end
+end
diff --git a/test/integration/api/smtp_cert_test.rb b/test/integration/api/smtp_cert_test.rb
new file mode 100644
index 0000000..04e6f31
--- /dev/null
+++ b/test/integration/api/smtp_cert_test.rb
@@ -0,0 +1,52 @@
+require 'test_helper'
+require 'openssl'
+
+class SmtpCertTest < ApiIntegrationTest
+
+ test "retrieve smtp cert" do
+ @user = FactoryGirl.create :user, effective_service_level_code: 2
+ login
+ post '/1/smtp_cert', {}, RACK_ENV
+ assert_text_response
+ assert_response_includes "BEGIN RSA PRIVATE KEY"
+ assert_response_includes "END RSA PRIVATE KEY"
+ assert_response_includes "BEGIN CERTIFICATE"
+ assert_response_includes "END CERTIFICATE"
+ end
+
+ test "cert and key" do
+ @user = FactoryGirl.create :user, effective_service_level_code: 2
+ login
+ post '/1/smtp_cert', {}, RACK_ENV
+ assert_text_response
+ cert = OpenSSL::X509::Certificate.new(get_response.body)
+ key = OpenSSL::PKey::RSA.new(get_response.body)
+ assert cert.check_private_key(key)
+ prefix = "/CN=#{@user.email_address}"
+ assert_equal prefix, cert.subject.to_s.slice(0,prefix.size)
+ end
+
+ test "fingerprint is stored with identity" do
+ @user = FactoryGirl.create :user, effective_service_level_code: 2
+ login
+ post '/1/smtp_cert', {}, RACK_ENV
+ assert_text_response
+ cert = OpenSSL::X509::Certificate.new(get_response.body)
+ fingerprint = OpenSSL::Digest::SHA1.hexdigest(cert.to_der).scan(/../).join(':')
+ today = DateTime.now.to_date.to_s
+ assert_equal({fingerprint => today}, @user.identity.cert_fingerprints)
+ end
+
+ test "fetching smtp certs requires email account" do
+ login
+ post '/1/smtp_cert', {}, RACK_ENV
+ assert_json_response error: I18n.t(:not_authorized)
+ end
+
+ test "no anonymous smtp certs" do
+ with_config allow_anonymous_certs: true do
+ post '/1/smtp_cert', {}, RACK_ENV
+ assert_json_response error: I18n.t(:not_authorized)
+ end
+ end
+end
diff --git a/test/support/api_integration_test.rb b/test/support/api_integration_test.rb
new file mode 100644
index 0000000..bd10f11
--- /dev/null
+++ b/test/support/api_integration_test.rb
@@ -0,0 +1,26 @@
+class ApiIntegrationTest < ActionDispatch::IntegrationTest
+
+ DUMMY_TOKEN = Token.new
+ RACK_ENV = {'HTTP_AUTHORIZATION' => %Q(Token token="#{DUMMY_TOKEN.to_s}")}
+
+ def login(user = nil)
+ @user ||= user ||= FactoryGirl.create(:user)
+ # DUMMY_TOKEN will be frozen. So let's use a dup
+ @token ||= DUMMY_TOKEN.dup
+ # make sure @token is up to date if it already exists
+ @token.reload if @token.persisted?
+ @token.user_id = @user.id
+ @token.last_seen_at = Time.now
+ @token.save
+ end
+
+ teardown do
+ if @user && @user.persisted?
+ Identity.destroy_all_for @user
+ @user.reload.destroy
+ end
+ if @token && @token.persisted?
+ @token.reload.destroy
+ end
+ end
+end
diff --git a/test/support/assert_responses.rb b/test/support/assert_responses.rb
index b01166f..19c2768 100644
--- a/test/support/assert_responses.rb
+++ b/test/support/assert_responses.rb
@@ -8,21 +8,27 @@ module AssertResponses
@response || last_response
end
- def assert_attachement_filename(name)
- assert_equal %Q(attachment; filename="#{name}"),
- get_response.headers["Content-Disposition"]
+ def content_type
+ get_response.content_type.to_s.split(';').first
end
def json_response
+ return nil unless content_type == 'application/json'
response = JSON.parse(get_response.body)
response.respond_to?(:with_indifferent_access) ?
response.with_indifferent_access :
response
end
+ def assert_text_response(body = nil)
+ assert_equal 'text/plain', content_type
+ unless body.nil?
+ assert_equal body, get_response.body
+ end
+ end
+
def assert_json_response(object)
- assert_equal 'application/json',
- get_response.content_type.to_s.split(';').first
+ assert_equal 'application/json', content_type
if object.is_a? Hash
object.stringify_keys! if object.respond_to? :stringify_keys!
assert_equal object, json_response
@@ -35,6 +41,20 @@ module AssertResponses
object.stringify_keys! if object.respond_to? :stringify_keys!
assert_json_response :errors => object
end
+
+ # checks for the presence of a key in a json response
+ # or a string in a text response
+ def assert_response_includes(string_or_key)
+ response = json_response || get_response.body
+ assert response.include?(string_or_key),
+ "response should have included #{string_or_key}"
+ end
+
+ def assert_attachement_filename(name)
+ assert_equal %Q(attachment; filename="#{name}"),
+ get_response.headers["Content-Disposition"]
+ end
+
end
class ::ActionController::TestCase
diff --git a/test/support/auth_test_helper.rb b/test/support/auth_test_helper.rb
index 57f9f9b..e1961aa 100644
--- a/test/support/auth_test_helper.rb
+++ b/test/support/auth_test_helper.rb
@@ -19,6 +19,10 @@ module AuthTestHelper
return @current_user
end
+ def assert_login_required
+ assert_access_denied(true, false)
+ end
+
def assert_access_denied(denied = true, logged_in = true)
if denied
if @response.content_type == 'application/json'
diff --git a/test/support/browser_integration_test.rb b/test/support/browser_integration_test.rb
index 1c872ff..4fec59f 100644
--- a/test/support/browser_integration_test.rb
+++ b/test/support/browser_integration_test.rb
@@ -54,6 +54,7 @@ class BrowserIntegrationTest < ActionDispatch::IntegrationTest
end
# currently this only works for tests with poltergeist.
+ # ApiIntegrationTest has a working implementation for RackTest
def login(user = nil)
@user ||= user ||= FactoryGirl.create(:user)
token = Token.create user_id: user.id
diff --git a/test/unit/user_test.rb b/test/unit/user_test.rb
index ffbb7d8..b3c831b 100644
--- a/test/unit/user_test.rb
+++ b/test/unit/user_test.rb
@@ -65,4 +65,11 @@ class UserTest < ActiveSupport::TestCase
assert_equal key, @user.public_key
end
+ #
+ ## Regression tests
+ #
+ test "make sure valid does not crash" do
+ assert !User.new.valid?
+ end
+
end