From e94b9471c0bc30cd6a1a5bf5b6b22b746d242e31 Mon Sep 17 00:00:00 2001 From: Azul Date: Fri, 9 May 2014 16:11:20 +0200 Subject: calculate cert fingerprints to store for leap_mx stelfox.net/blog/2014/04/calculating-rsa-key-fingerprints-in-ruby/ --- app/models/client_certificate.rb | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'app/models') 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]) -- cgit v1.2.3 From 5dd6c1529f8f4fc5089c71b0a44e360acaea900d Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 15 May 2014 11:04:56 +0200 Subject: fix Email so User.new.valid? does not crash Email.new(nil) now returns an invalid email rather than crashing. --- app/models/email.rb | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'app/models') 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 -- cgit v1.2.3 From 17b67aeda81dee2273ce1161ac7292a328c3efaa Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 15 May 2014 16:29:49 +0200 Subject: store cert fingerprint with main user identity --- app/models/identity.rb | 1 + 1 file changed, 1 insertion(+) (limited to 'app/models') diff --git a/app/models/identity.rb b/app/models/identity.rb index ad8c01e..2f8d4eb 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, [String] validate :unique_forward validate :alias_available -- cgit v1.2.3 From 3a84578cf33685800c9216cfb4da12ea1fb0032f Mon Sep 17 00:00:00 2001 From: Azul Date: Mon, 19 May 2014 15:07:02 +0200 Subject: store fingerprints with timestamp Only storing the date as that should suffice for normal expiry and is less useful for identifying users by timestamps --- app/models/identity.rb | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) (limited to 'app/models') diff --git a/app/models/identity.rb b/app/models/identity.rb index 2f8d4eb..a4225e7 100644 --- a/app/models/identity.rb +++ b/app/models/identity.rb @@ -8,7 +8,7 @@ class Identity < CouchRest::Model::Base property :address, LocalEmail property :destination, Email property :keys, HashWithIndifferentAccess - property :cert_fingerprints, [String] + property :cert_fingerprints, Hash validate :unique_forward validate :alias_available @@ -108,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 -- cgit v1.2.3 From 5764daae090227bf4c5967900b708392c967be47 Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 1 May 2014 10:45:57 +0200 Subject: hash token with sha512 against timing attacs #3398 --- app/models/token.rb | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) (limited to 'app/models') diff --git a/app/models/token.rb b/app/models/token.rb index e759ee3..ff2ad12 100644 --- a/app/models/token.rb +++ b/app/models/token.rb @@ -1,3 +1,5 @@ +require 'digest/sha2' + class Token < CouchRest::Model::Base use_database :tokens @@ -11,10 +13,16 @@ class Token < CouchRest::Model::Base validates :user_id, presence: true + attr_accessor :token + design do view :by_last_seen_at end + def self.find_by_token(token) + self.find Digest::SHA512.hexdigest(token) + end + def self.expires_after APP_CONFIG[:auth] && APP_CONFIG[:auth][:token_expires_after] end @@ -31,7 +39,7 @@ class Token < CouchRest::Model::Base end def to_s - id + token end def authenticate @@ -65,7 +73,8 @@ class Token < CouchRest::Model::Base def initialize(*args) super if new_record? - self.id = SecureRandom.urlsafe_base64(32).gsub(/^_*/, '') + self.token = SecureRandom.urlsafe_base64(32).gsub(/^_*/, '') + self.id = Digest::SHA512.hexdigest(self.token) self.last_seen_at = Time.now end end -- cgit v1.2.3 From 154d32bbc7cfe21d83141ff2c9a3d805165231b8 Mon Sep 17 00:00:00 2001 From: Azul Date: Wed, 28 May 2014 10:45:14 +0200 Subject: use Identity for testing login availability We create an identity alongside each user. Make sure the identity is valid when creating the user. This also ensures that the login picked is available because otherwise the identities address would not be available anymore. --- app/models/identity.rb | 30 ++++++++++++------------------ app/models/user.rb | 13 ++++++------- 2 files changed, 18 insertions(+), 25 deletions(-) (limited to 'app/models') diff --git a/app/models/identity.rb b/app/models/identity.rb index a4225e7..2be396c 100644 --- a/app/models/identity.rb +++ b/app/models/identity.rb @@ -10,8 +10,9 @@ class Identity < CouchRest::Model::Base property :keys, HashWithIndifferentAccess property :cert_fingerprints, Hash - validate :unique_forward validate :alias_available + validates :destination, presence: true, + uniqueness: {scope: :address} validate :address_local_email validate :destination_email @@ -44,13 +45,12 @@ class Identity < CouchRest::Model::Base end - def self.for(user, attributes = {}) - find_for(user, attributes) || build_for(user, attributes) + def self.for(user) + find_for(user) || build_for(user) end - def self.find_for(user, attributes = {}) - attributes.reverse_merge! attributes_from_user(user) - find_by_address_and_destination [attributes[:address], attributes[:destination]] + def self.find_for(user) + find_by_user_id(user.id) if user && user.persisted? end def self.build_for(user, attributes = {}) @@ -125,23 +125,17 @@ class Identity < CouchRest::Model::Base protected - def unique_forward - same = Identity.find_by_address_and_destination([address, destination]) - if same && same != self - errors.add :base, "This alias already exists" - end - end - def alias_available - same = Identity.find_by_address(address) - if same && same.user != self.user - errors.add :base, "This email has already been taken" + same_address = Identity.by_address.key(address) + if same_address.detect { |other| other.user !=self.user } + errors.add :address, :taken end end def address_local_email - return if address.valid? #this ensures it is LocalEmail - self.errors.add(:address, address.errors.messages[:email].first) #assumes only one error + return if address.valid? + # we only hand on the first error for now. + self.errors.add(:address, address.errors.messages.values.first) end def destination_email diff --git a/app/models/user.rb b/app/models/user.rb index 6678de6..6b4d1a9 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -24,7 +24,7 @@ class User < CouchRest::Model::Base :uniqueness => true, :if => :serverside? - validate :login_is_unique_alias + validate :identity_is_valid validates :password_salt, :password_verifier, :format => { :with => /\A[\dA-Fa-f]+\z/, :message => "Only hex numbers allowed" } @@ -161,12 +161,11 @@ class User < CouchRest::Model::Base # Validation Functions ## - def login_is_unique_alias - alias_identity = Identity.find_by_address(self.email_address) - return if alias_identity.blank? - if alias_identity.user != self - errors.add(:login, "has already been taken") - end + def identity_is_valid + refresh_identity + return if identity.valid? + # hand on the first error only for now + self.errors.add(:login, identity.errors.messages.values.first) end def password -- cgit v1.2.3 From 5c8ab9298cc4705de508a3f3f9d9d6370a01ff5e Mon Sep 17 00:00:00 2001 From: Azul Date: Wed, 28 May 2014 11:43:50 +0200 Subject: minor: beautify handle lookup in etc/passwd some --- app/models/local_email.rb | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) (limited to 'app/models') diff --git a/app/models/local_email.rb b/app/models/local_email.rb index 2b4c65e..ded7baf 100644 --- a/app/models/local_email.rb +++ b/app/models/local_email.rb @@ -58,11 +58,9 @@ class LocalEmail < Email end def handle_in_passwd? - begin - !!Etc.getpwnam(handle) - rescue ArgumentError - # handle was not found - return false - end + Etc.getpwnam(handle).present? + rescue ArgumentError + # handle was not found + return false end end -- cgit v1.2.3 From 682b4060cb86c52ffda638f4f9a837f107540610 Mon Sep 17 00:00:00 2001 From: Azul Date: Wed, 28 May 2014 11:44:12 +0200 Subject: ensure identity is cleared on user.reload - fixes test --- app/models/pgp_key.rb | 3 ++- app/models/user.rb | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) (limited to 'app/models') diff --git a/app/models/pgp_key.rb b/app/models/pgp_key.rb index 66f8660..3384f4c 100644 --- a/app/models/pgp_key.rb +++ b/app/models/pgp_key.rb @@ -25,9 +25,10 @@ class PgpKey # allow comparison with plain keyblock strings. def ==(other) + return false if (self.present? != other.present?) self.equal?(other) or # relax the comparison on line ends. - self.to_s.tr_s("\n\r", '') == other.tr_s("\r\n", '') + self.to_s.tr_s("\n\r", '') == other.tr_s("\n\r", '') end protected diff --git a/app/models/user.rb b/app/models/user.rb index 6b4d1a9..33508b5 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -42,6 +42,11 @@ class User < CouchRest::Model::Base view :by_created_at end # end of design + def reload + super + @identity = nil + end + def to_json(options={}) { :login => login, -- cgit v1.2.3 From 6fea83763f07add7d3bd07e3843b75aaf61e19b4 Mon Sep 17 00:00:00 2001 From: Azul Date: Wed, 28 May 2014 12:20:49 +0200 Subject: bring back the alias functionality in Identities --- app/models/account.rb | 1 + app/models/identity.rb | 23 +++++++++++++---------- 2 files changed, 14 insertions(+), 10 deletions(-) (limited to 'app/models') diff --git a/app/models/account.rb b/app/models/account.rb index cf998e4..bffa288 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -18,6 +18,7 @@ class Account def self.create(attrs) @user = User.create(attrs).tap do |user| Identity.create_for user + user.refresh_identity end end diff --git a/app/models/identity.rb b/app/models/identity.rb index 2be396c..0d25bae 100644 --- a/app/models/identity.rb +++ b/app/models/identity.rb @@ -11,8 +11,7 @@ class Identity < CouchRest::Model::Base property :cert_fingerprints, Hash validate :alias_available - validates :destination, presence: true, - uniqueness: {scope: :address} + validates :destination, uniqueness: {scope: :address} validate :address_local_email validate :destination_email @@ -45,12 +44,14 @@ class Identity < CouchRest::Model::Base end - def self.for(user) - find_for(user) || build_for(user) + def self.for(user, attributes = {}) + find_for(user, attributes) || build_for(user, attributes) end - def self.find_for(user) - find_by_user_id(user.id) if user && user.persisted? + def self.find_for(user, attributes = {}) + attributes.reverse_merge! attributes_from_user(user) + id = find_by_address_and_destination attributes.values_at(:address, :destination) + return id if id && id.user == user end def self.build_for(user, attributes = {}) @@ -67,7 +68,9 @@ class Identity < CouchRest::Model::Base def self.disable_all_for(user) Identity.by_user_id.key(user.id).each do |identity| identity.disable - identity.save + # if the identity is not unique anymore because the destination + # was reset to nil we destroy it. + identity.save || identity.destroy end end @@ -127,15 +130,15 @@ class Identity < CouchRest::Model::Base def alias_available same_address = Identity.by_address.key(address) - if same_address.detect { |other| other.user !=self.user } + if same_address.detect { |other| other.user != self.user } errors.add :address, :taken end end def address_local_email - return if address.valid? + return if address.valid? #this ensures it is a valid local email address # we only hand on the first error for now. - self.errors.add(:address, address.errors.messages.values.first) + self.errors.add(:address, address.errors.messages[:email].first) end def destination_email -- cgit v1.2.3 From 09dfa583eca69a3925c384c67c3d98cd8c69b360 Mon Sep 17 00:00:00 2001 From: Azul Date: Wed, 28 May 2014 12:28:07 +0200 Subject: allow changing the user_id on an identity we set it to nil when we disable it --- app/models/identity.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/models') diff --git a/app/models/identity.rb b/app/models/identity.rb index 0d25bae..a8eaba6 100644 --- a/app/models/identity.rb +++ b/app/models/identity.rb @@ -130,7 +130,7 @@ class Identity < CouchRest::Model::Base def alias_available same_address = Identity.by_address.key(address) - if same_address.detect { |other| other.user != self.user } + if same_address.detect { |other| other != self && other.user != self.user } errors.add :address, :taken end end -- cgit v1.2.3 From 016e61ce9ab44cf58355e843b0c0d0085d373fc7 Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 29 May 2014 09:38:53 +0200 Subject: catch corner cases of account creation Users now always check if their identity is valid. We need to make sure this works if the user is a new record and once it has been persisted. While the user is a new record the identity will have no user_id. Old identities that are left to block the login of a user who canceled their account also have a blank user_id. They still should render the new identity invalid so the user can't be saved with a login that has been reserved. Once the user has been persisted we set the user_id on the identity and save it too when creating an Account. This allows us to create a plain user and save it and it will still have an in memory identity only. But the default is to create the user by means of creating an account so an identity will be created as well. --- app/models/account.rb | 9 ++++++--- app/models/identity.rb | 13 +++++++++---- 2 files changed, 15 insertions(+), 7 deletions(-) (limited to 'app/models') diff --git a/app/models/account.rb b/app/models/account.rb index bffa288..32ed445 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -16,10 +16,13 @@ class Account # Returns the user record so it can be used in views. def self.create(attrs) - @user = User.create(attrs).tap do |user| - Identity.create_for user - user.refresh_identity + @user = User.create(attrs) + if @user.persisted? + identity = @user.identity + identity.user_id = @user.id + identity.save end + return @user end def update(attrs) diff --git a/app/models/identity.rb b/app/models/identity.rb index a8eaba6..25be971 100644 --- a/app/models/identity.rb +++ b/app/models/identity.rb @@ -129,8 +129,12 @@ class Identity < CouchRest::Model::Base protected def alias_available - same_address = Identity.by_address.key(address) - if same_address.detect { |other| other != self && other.user != self.user } + blocking_identities = Identity.by_address.key(address).all + blocking_identities.delete self + if self.user + blocking_identities.reject! { |other| other.user == self.user } + end + if blocking_identities.any? errors.add :address, :taken end end @@ -138,13 +142,14 @@ class Identity < CouchRest::Model::Base def address_local_email return if address.valid? #this ensures it is a valid local email address # we only hand on the first error for now. - self.errors.add(:address, address.errors.messages[:email].first) + self.errors.add(:address, address.errors.messages.values.first) end def destination_email return if destination.nil? # this identity is disabled return if destination.valid? # this ensures it is Email - self.errors.add(:destination, destination.errors.messages[:email].first) #assumes only one error #TODO + # we only hand on the first error for now. + self.errors.add(:destination, destination.errors.messages.values.first) end end -- cgit v1.2.3 From e0d31118d6e4110d2c280afa9415cfe9def29deb Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 29 May 2014 10:04:07 +0200 Subject: hand on errors from Email to Identity to User errors.each iterates through all errors for all attrbibutes nicely. --- app/models/identity.rb | 10 ++++++---- app/models/user.rb | 6 +++--- 2 files changed, 9 insertions(+), 7 deletions(-) (limited to 'app/models') diff --git a/app/models/identity.rb b/app/models/identity.rb index 25be971..f2727c8 100644 --- a/app/models/identity.rb +++ b/app/models/identity.rb @@ -141,15 +141,17 @@ class Identity < CouchRest::Model::Base def address_local_email return if address.valid? #this ensures it is a valid local email address - # we only hand on the first error for now. - self.errors.add(:address, address.errors.messages.values.first) + address.errors.each do |attribute, error| + self.errors.add(:address, error) + end end def destination_email return if destination.nil? # this identity is disabled return if destination.valid? # this ensures it is Email - # we only hand on the first error for now. - self.errors.add(:destination, destination.errors.messages.values.first) + destination.errors.each do |attribute, error| + self.errors.add(:destination, error) + end end end diff --git a/app/models/user.rb b/app/models/user.rb index 33508b5..84a795e 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -167,10 +167,10 @@ class User < CouchRest::Model::Base ## def identity_is_valid - refresh_identity return if identity.valid? - # hand on the first error only for now - self.errors.add(:login, identity.errors.messages.values.first) + identity.errors.each do |attribute, error| + self.errors.add(:login, error) + end end def password -- cgit v1.2.3 From 85e066920568c19b788b8789c4659092224bb517 Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 29 May 2014 10:37:31 +0200 Subject: ensure User#reload returns self --- app/models/user.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/models') diff --git a/app/models/user.rb b/app/models/user.rb index 84a795e..f8b9ddc 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -43,8 +43,8 @@ class User < CouchRest::Model::Base end # end of design def reload - super @identity = nil + super end def to_json(options={}) -- cgit v1.2.3 From bbe7b3b7deb2b44d34f7c39dda2c3db284e2bf10 Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 29 May 2014 11:19:21 +0200 Subject: clearify identity validations Identity.new.valid? should not crash. So validate presence where needed and skip the other validations if the value is absent. --- app/models/identity.rb | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) (limited to 'app/models') diff --git a/app/models/identity.rb b/app/models/identity.rb index f2727c8..2f6241c 100644 --- a/app/models/identity.rb +++ b/app/models/identity.rb @@ -10,7 +10,9 @@ class Identity < CouchRest::Model::Base property :keys, HashWithIndifferentAccess property :cert_fingerprints, Hash - validate :alias_available + validates :address, presence: true + validate :address_available + validates :destination, presence: true, if: :enabled? validates :destination, uniqueness: {scope: :address} validate :address_local_email validate :destination_email @@ -94,7 +96,11 @@ class Identity < CouchRest::Model::Base end def enabled? - self.destination && self.user_id + self.user_id + end + + def disabled? + !enabled? end def disable @@ -123,12 +129,12 @@ class Identity < CouchRest::Model::Base # for LoginFormatValidation def login - self.address.handle + address.handle if address.present? end protected - def alias_available + def address_available blocking_identities = Identity.by_address.key(address).all blocking_identities.delete self if self.user @@ -140,15 +146,18 @@ class Identity < CouchRest::Model::Base end def address_local_email - return if address.valid? #this ensures it is a valid local email address + # caught by presence validation + return if address.blank? + return if address.valid? address.errors.each do |attribute, error| self.errors.add(:address, error) end end def destination_email - return if destination.nil? # this identity is disabled - return if destination.valid? # this ensures it is Email + # caught by presence validation or this identity is disabled + return if destination.blank? + return if destination.valid? destination.errors.each do |attribute, error| self.errors.add(:destination, error) end -- cgit v1.2.3