summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--app/views/layouts/_navigation.html.haml1
-rw-r--r--help/app/controllers/tickets_controller.rb2
-rw-r--r--help/app/models/ticket_comment.rb2
-rw-r--r--test/unit/email_test.rb18
-rw-r--r--users/app/controllers/controller_extension/authentication.rb47
-rw-r--r--users/app/controllers/controller_extension/token_authentication.rb23
-rw-r--r--users/app/controllers/email_settings_controller.rb41
-rw-r--r--users/app/controllers/v1/sessions_controller.rb2
-rw-r--r--users/app/controllers/v1/users_controller.rb16
-rw-r--r--users/app/models/account_settings.rb36
-rw-r--r--users/app/models/email.rb35
-rw-r--r--users/app/models/identity.rb82
-rw-r--r--users/app/models/local_email.rb68
-rw-r--r--users/app/models/login_format_validation.rb19
-rw-r--r--users/app/models/remote_email.rb14
-rw-r--r--users/app/models/session.rb6
-rw-r--r--users/app/models/signup_service.rb9
-rw-r--r--users/app/models/token.rb4
-rw-r--r--users/app/models/user.rb71
-rw-r--r--users/app/views/email_settings/edit.html.haml38
-rw-r--r--users/app/views/users/_edit.html.haml16
-rw-r--r--users/app/views/users/_warnings.html.haml2
-rw-r--r--users/config/initializers/add_controller_methods.rb1
-rw-r--r--users/config/locales/en.yml3
-rw-r--r--users/leap_web_users.gemspec2
-rw-r--r--users/lib/warden/strategies/secure_remote_password.rb2
-rw-r--r--users/test/factories.rb3
-rw-r--r--users/test/functional/helper_methods_test.rb2
-rw-r--r--users/test/functional/test_helpers_test.rb38
-rw-r--r--users/test/functional/users_controller_test.rb12
-rw-r--r--users/test/functional/v1/sessions_controller_test.rb18
-rw-r--r--users/test/functional/v1/users_controller_test.rb8
-rw-r--r--users/test/integration/api/account_flow_test.rb42
-rwxr-xr-xusers/test/integration/api/python/flow_with_srp.py96
-rw-r--r--users/test/integration/browser/account_test.rb20
-rw-r--r--users/test/support/auth_test_helper.rb9
-rw-r--r--users/test/support/stub_record_helper.rb5
-rw-r--r--users/test/unit/email_aliases_test.rb66
-rw-r--r--users/test/unit/email_test.rb19
-rw-r--r--users/test/unit/identity_test.rb86
-rw-r--r--users/test/unit/local_email_test.rb34
-rw-r--r--users/test/unit/user_test.rb23
43 files changed, 658 insertions, 385 deletions
diff --git a/.gitignore b/.gitignore
index a79d841..84acd8d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,3 +31,5 @@ bin
.*.swp
public/1/*
vendor/bundle/*
+public/img
+config/couchdb.yml.*
diff --git a/app/views/layouts/_navigation.html.haml b/app/views/layouts/_navigation.html.haml
index 3eb2289..fb018a9 100644
--- a/app/views/layouts/_navigation.html.haml
+++ b/app/views/layouts/_navigation.html.haml
@@ -1,7 +1,6 @@
%ul.nav.sidenav
= link_to_navigation t(:overview), user_overview_path(@user), :active => controller?(:overviews)
= link_to_navigation t(:account_settings), edit_user_path(@user), :active => controller?(:users)
- = link_to_navigation t(:email_settings), edit_user_email_settings_path(@user), :active => controller?(:email_settings)
= link_to_navigation t(:support_tickets), auto_tickets_path, :active => controller?(:tickets)
= link_to_navigation t(:billing_settings), show_or_new_customer_link(@user), :active => controller?(:customer, :payments, :subscriptions, :credit_card_info) if engine_enabled('LeapWebBilling')
= link_to_navigation t(:logout), logout_path, :method => :delete
diff --git a/help/app/controllers/tickets_controller.rb b/help/app/controllers/tickets_controller.rb
index 094612c..a03ef22 100644
--- a/help/app/controllers/tickets_controller.rb
+++ b/help/app/controllers/tickets_controller.rb
@@ -18,6 +18,7 @@ class TicketsController < ApplicationController
@ticket = Ticket.new(params[:ticket])
@ticket.comments.last.posted_by = (logged_in? ? current_user.id : nil) #protecting posted_by isn't working, so this should protect it.
+ @ticket.comments.last.private = false unless admin?
@ticket.created_by = current_user.id if logged_in?
@ticket.email = current_user.email_address if logged_in? and current_user.email_address
@@ -58,6 +59,7 @@ class TicketsController < ApplicationController
if @ticket.comments_changed?
@ticket.comments.last.posted_by = (current_user ? current_user.id : nil)
+ @ticket.comments.last.private = false unless admin?
end
if @ticket.changed?
diff --git a/help/app/models/ticket_comment.rb b/help/app/models/ticket_comment.rb
index 1df7eec..13bea2b 100644
--- a/help/app/models/ticket_comment.rb
+++ b/help/app/models/ticket_comment.rb
@@ -7,7 +7,7 @@ class TicketComment
property :posted_at, Time#, :protected => true
#property :posted_verified, TrueClass, :protected => true #should be true if current_user is set when the comment is created
property :body, String
- property :private, TrueClass # private comments are only viewable by admins
+ property :private, TrueClass # private comments are only viewable by admins #this is checked when set, to make sure it was set by an admin
# ? timestamps!
validates :body, :presence => true
diff --git a/test/unit/email_test.rb b/test/unit/email_test.rb
new file mode 100644
index 0000000..e858bd5
--- /dev/null
+++ b/test/unit/email_test.rb
@@ -0,0 +1,18 @@
+require 'test_helper'
+
+class EmailTest < ActiveSupport::TestCase
+
+ test "valid format" do
+ email = Email.new(email_string)
+ assert email.valid?
+ end
+
+ test "validates format" do
+ email = Email.new("email")
+ assert !email.valid?
+ end
+
+ def email_string
+ @email_string ||= Faker::Internet.email
+ end
+end
diff --git a/users/app/controllers/controller_extension/authentication.rb b/users/app/controllers/controller_extension/authentication.rb
index 5fac884..dca3664 100644
--- a/users/app/controllers/controller_extension/authentication.rb
+++ b/users/app/controllers/controller_extension/authentication.rb
@@ -7,28 +7,8 @@ module ControllerExtension::Authentication
helper_method :current_user, :logged_in?, :admin?
end
- def authentication_errors
- return unless attempted_login?
- errors = get_warden_errors
- errors.inject({}) do |translated,err|
- translated[err.first] = I18n.t(err.last)
- translated
- end
- end
-
- def get_warden_errors
- if strategy = warden.winning_strategy
- message = strategy.message
- # in case we get back the default message to fail!
- message.respond_to?(:inject) ? message : { base: message }
- else
- { login: :all_strategies_failed }
- end
- end
-
- def attempted_login?
- request.env['warden.options'] &&
- request.env['warden.options'][:attempted_path]
+ def current_user
+ @current_user ||= token_authenticate || warden.user
end
def logged_in?
@@ -62,4 +42,27 @@ module ControllerExtension::Authentication
access_denied unless admin?
end
+ def authentication_errors
+ return unless attempted_login?
+ errors = get_warden_errors
+ errors.inject({}) do |translated,err|
+ translated[err.first] = I18n.t(err.last)
+ translated
+ end
+ end
+
+ def get_warden_errors
+ if strategy = warden.winning_strategy
+ message = strategy.message
+ # in case we get back the default message to fail!
+ message.respond_to?(:inject) ? message : { base: message }
+ else
+ { login: :all_strategies_failed }
+ end
+ end
+
+ def attempted_login?
+ request.env['warden.options'] &&
+ request.env['warden.options'][:attempted_path]
+ end
end
diff --git a/users/app/controllers/controller_extension/token_authentication.rb b/users/app/controllers/controller_extension/token_authentication.rb
new file mode 100644
index 0000000..3e2816d
--- /dev/null
+++ b/users/app/controllers/controller_extension/token_authentication.rb
@@ -0,0 +1,23 @@
+module ControllerExtension::TokenAuthentication
+ extend ActiveSupport::Concern
+
+ def token_authenticate
+ authenticate_with_http_token do |token_id, options|
+ @token = Token.find(token_id)
+ end
+ @token.user if @token
+ end
+
+ def logout
+ super
+ clear_token
+ end
+
+ def clear_token
+ authenticate_with_http_token do |token_id, options|
+ @token = Token.find(token_id)
+ @token.destroy if @token
+ end
+ end
+end
+
diff --git a/users/app/controllers/email_settings_controller.rb b/users/app/controllers/email_settings_controller.rb
deleted file mode 100644
index f7d85be..0000000
--- a/users/app/controllers/email_settings_controller.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-class EmailSettingsController < UsersBaseController
-
- before_filter :authorize
- before_filter :fetch_user
-
- def edit
- @email_alias = LocalEmail.new
- end
-
- def update
- @user.attributes = cleanup_params(params[:user])
- if @user.changed?
- if @user.save
- flash[:notice] = t(:changes_saved)
- redirect
- else
- if @user.email_aliases.last && !@user.email_aliases.last.valid?
- # display bad alias in text field:
- @email_alias = @user.email_aliases.pop
- end
- render 'email_settings/edit'
- end
- else
- redirect
- end
- end
-
- private
-
- def redirect
- redirect_to edit_user_email_settings_url(@user)
- end
-
- def cleanup_params(user)
- if !user['email_forward'].nil? && user['email_forward'].empty?
- user.delete('email_forward') # don't allow "" as an email forward
- end
- user
- end
-
-end
diff --git a/users/app/controllers/v1/sessions_controller.rb b/users/app/controllers/v1/sessions_controller.rb
index 295c327..1b20a82 100644
--- a/users/app/controllers/v1/sessions_controller.rb
+++ b/users/app/controllers/v1/sessions_controller.rb
@@ -29,7 +29,7 @@ module V1
def destroy
logout
- redirect_to root_path
+ head :no_content
end
protected
diff --git a/users/app/controllers/v1/users_controller.rb b/users/app/controllers/v1/users_controller.rb
index fda56f2..f380c19 100644
--- a/users/app/controllers/v1/users_controller.rb
+++ b/users/app/controllers/v1/users_controller.rb
@@ -18,17 +18,23 @@ module V1
end
def create
- @user = User.create(params[:user])
+ @user = signup_service.register(params[:user])
respond_with @user # return ID instead?
end
def update
- @user.update_attributes params[:user]
- if @user.valid?
- flash[:notice] = t(:user_updated_successfully)
- end
+ account_settings.update params[:user]
respond_with @user
end
+ protected
+
+ def account_settings
+ AccountSettings.new(@user)
+ end
+
+ def signup_service
+ SignupService.new
+ end
end
end
diff --git a/users/app/models/account_settings.rb b/users/app/models/account_settings.rb
new file mode 100644
index 0000000..27fa227
--- /dev/null
+++ b/users/app/models/account_settings.rb
@@ -0,0 +1,36 @@
+class AccountSettings
+
+ def initialize(user)
+ @user = user
+ end
+
+ def update(attrs)
+ if attrs[:password_verifier].present?
+ update_login(attrs[:login])
+ @user.update_attributes attrs.slice(:password_verifier, :password_salt)
+ end
+ # TODO: move into identity controller
+ update_pgp_key(attrs[:public_key]) if attrs.has_key? :public_key
+ @user.save && save_identities
+ end
+
+ protected
+
+ def update_login(login)
+ return unless login.present?
+ @old_identity = Identity.for(@user)
+ @user.login = login
+ @new_identity = Identity.for(@user) # based on the new login
+ @old_identity.destination = @user.email_address # alias old -> new
+ end
+
+ def update_pgp_key(key)
+ @new_identity ||= Identity.for(@user)
+ @new_identity.set_key(:pgp, key)
+ end
+
+ def save_identities
+ @new_identity.try(:save) && @old_identity.try(:save)
+ end
+
+end
diff --git a/users/app/models/email.rb b/users/app/models/email.rb
index 6d82f2a..1bcff1c 100644
--- a/users/app/models/email.rb
+++ b/users/app/models/email.rb
@@ -1,33 +1,22 @@
-module Email
- extend ActiveSupport::Concern
+class Email < String
+ include ActiveModel::Validations
- included do
- validates :email,
- :format => {
- :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/,
- :message => "needs to be a valid email address"
- }
- end
-
- def initialize(attributes = nil, &block)
- attributes = {:email => attributes} if attributes.is_a? String
- super(attributes, &block)
- end
-
- def to_s
- email
- end
-
- def ==(other)
- other.is_a?(Email) ? self.email == other.email : self.email == other
- end
+ validates :email,
+ :format => {
+ :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/,
+ :message => "needs to be a valid email address"
+ }
def to_partial_path
"emails/email"
end
def to_param
- email
+ to_s
+ end
+
+ def email
+ self
end
end
diff --git a/users/app/models/identity.rb b/users/app/models/identity.rb
new file mode 100644
index 0000000..355f67a
--- /dev/null
+++ b/users/app/models/identity.rb
@@ -0,0 +1,82 @@
+class Identity < CouchRest::Model::Base
+
+ use_database :identities
+
+ belongs_to :user
+
+ property :address, LocalEmail
+ property :destination, Email
+ property :keys, HashWithIndifferentAccess
+
+ validate :unique_forward
+ validate :alias_available
+
+ design do
+ view :by_user_id
+ view :by_address_and_destination
+ view :by_address
+ view :pgp_key_by_email,
+ map: <<-EOJS
+ function(doc) {
+ if (doc.type != 'Identity') {
+ return;
+ }
+ emit(doc.address, doc.keys["pgp"]);
+ }
+ EOJS
+
+ end
+
+ def self.for(user, attributes = {})
+ find_for(user, attributes) || build_for(user, attributes)
+ end
+
+ def self.find_for(user, attributes = {})
+ attributes.reverse_merge! attributes_from_user(user)
+ find_by_address_and_destination [attributes[:address], attributes[:destination]]
+ end
+
+ def self.build_for(user, attributes = {})
+ attributes.reverse_merge! attributes_from_user(user)
+ Identity.new(attributes)
+ end
+
+ def self.create_for(user, attributes = {})
+ identity = build_for(user, attributes)
+ identity.save
+ identity
+ end
+
+ def self.attributes_from_user(user)
+ { user_id: user.id,
+ address: user.email_address,
+ destination: user.email_address
+ }
+ end
+
+ def keys
+ read_attribute('keys') || HashWithIndifferentAccess.new
+ end
+
+ def set_key(type, value)
+ return if keys[type] == value
+ write_attribute('keys', keys.merge(type => value))
+ end
+
+ 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"
+ end
+ end
+
+end
diff --git a/users/app/models/local_email.rb b/users/app/models/local_email.rb
index 69cba01..c1f7c11 100644
--- a/users/app/models/local_email.rb
+++ b/users/app/models/local_email.rb
@@ -1,63 +1,39 @@
-class LocalEmail
- include CouchRest::Model::Embeddable
- include Email
+class LocalEmail < Email
- property :username, String
- before_validation :strip_domain_if_needed
-
- validates :username,
- :presence => true,
- :format => { :with => /\A([^@\s]+)(@#{APP_CONFIG[:domain]})?\Z/i, :message => "needs to be a valid login or email address @#{APP_CONFIG[:domain]}"}
-
- validate :unique_on_server
- validate :unique_alias_for_user
- validate :differs_from_login
-
- validates :casted_by, :presence => true
-
- def email
- return '' if username.nil?
- username + '@' + APP_CONFIG[:domain]
+ def self.domain
+ APP_CONFIG[:domain]
end
- def email=(value)
- return if value.blank?
- self.username = value
- strip_domain_if_needed
+ validates :email,
+ :format => {
+ :with => /@#{domain}\Z/i,
+ :message => "needs to end in @#{domain}"
+ }
+
+ def initialize(s)
+ super
+ append_domain_if_needed
end
def to_key
- [username]
+ [handle]
end
- protected
-
- def unique_on_server
- has_email = User.find_by_login_or_alias(username)
- if has_email && has_email != self.casted_by
- errors.add :username, "has already been taken"
- end
+ def handle
+ gsub(/@#{domain}/i, '')
end
- def unique_alias_for_user
- aliases = self.casted_by.email_aliases
- if aliases.select{|a|a.username == self.username}.count > 1
- errors.add :username, "is already your alias"
- end
+ def domain
+ LocalEmail.domain
end
- def differs_from_login
- # If this has not changed but the email let's mark the email invalid instead.
- return if self.persisted?
- user = self.casted_by
- if user.login == self.username
- errors.add :username, "may not be the same as your email address"
- end
- end
+ protected
- def strip_domain_if_needed
- self.username.gsub! /@#{APP_CONFIG[:domain]}/i, ''
+ def append_domain_if_needed
+ unless self.index('@')
+ self << '@' + domain
+ end
end
end
diff --git a/users/app/models/login_format_validation.rb b/users/app/models/login_format_validation.rb
new file mode 100644
index 0000000..1d02bd1
--- /dev/null
+++ b/users/app/models/login_format_validation.rb
@@ -0,0 +1,19 @@
+module LoginFormatValidation
+ extend ActiveSupport::Concern
+
+ included do
+ # Have multiple regular expression validations so we can get specific error messages:
+ validates :login,
+ :format => { :with => /\A.{2,}\z/,
+ :message => "Login must have at least two characters"}
+ validates :login,
+ :format => { :with => /\A[a-z\d_\.-]+\z/,
+ :message => "Only lowercase letters, digits, . - and _ allowed."}
+ validates :login,
+ :format => { :with => /\A[a-z].*\z/,
+ :message => "Login must begin with a lowercase letter"}
+ validates :login,
+ :format => { :with => /\A.*[a-z\d]\z/,
+ :message => "Login must end with a letter or digit"}
+ end
+end
diff --git a/users/app/models/remote_email.rb b/users/app/models/remote_email.rb
deleted file mode 100644
index 4fe7425..0000000
--- a/users/app/models/remote_email.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-class RemoteEmail
- include CouchRest::Model::Embeddable
- include Email
-
- property :email, String
-
- def username
- email.spilt('@').first
- end
-
- def domain
- email.split('@').last
- end
-end
diff --git a/users/app/models/session.rb b/users/app/models/session.rb
index a9fdb1b..0d7e10e 100644
--- a/users/app/models/session.rb
+++ b/users/app/models/session.rb
@@ -1,12 +1,10 @@
class Session < SRP::Session
include ActiveModel::Validations
+ include LoginFormatValidation
attr_accessor :login
- validates :login,
- :presence => true,
- :format => { :with => /\A[A-Za-z\d_]+\z/,
- :message => "Only letters, digits and _ allowed" }
+ validates :login, :presence => true
def initialize(user = nil, aa = nil)
super(user, aa) if user
diff --git a/users/app/models/signup_service.rb b/users/app/models/signup_service.rb
new file mode 100644
index 0000000..f316ca9
--- /dev/null
+++ b/users/app/models/signup_service.rb
@@ -0,0 +1,9 @@
+class SignupService
+
+ def register(attrs)
+ User.create(attrs).tap do |user|
+ Identity.create_for user
+ end
+ end
+
+end
diff --git a/users/app/models/token.rb b/users/app/models/token.rb
index cc62778..514b97f 100644
--- a/users/app/models/token.rb
+++ b/users/app/models/token.rb
@@ -6,6 +6,10 @@ class Token < CouchRest::Model::Base
validates :user_id, presence: true
+ def user
+ User.find(self.user_id)
+ end
+
def initialize(*args)
super
self.id = SecureRandom.urlsafe_base64(32).gsub(/^_*/, '')
diff --git a/users/app/models/user.rb b/users/app/models/user.rb
index 413b4ac..c1988f3 100644
--- a/users/app/models/user.rb
+++ b/users/app/models/user.rb
@@ -1,4 +1,5 @@
class User < CouchRest::Model::Base
+ include LoginFormatValidation
use_database :users
@@ -6,11 +7,6 @@ class User < CouchRest::Model::Base
property :password_verifier, String, :accessible => true
property :password_salt, String, :accessible => true
- property :email_forward, String, :accessible => true
- property :email_aliases, [LocalEmail]
-
- property :public_key, :accessible => true
-
property :enabled, TrueClass, :default => true
validates :login, :password_salt, :password_verifier,
@@ -20,20 +16,6 @@ class User < CouchRest::Model::Base
:uniqueness => true,
:if => :serverside?
- # Have multiple regular expression validations so we can get specific error messages:
- validates :login,
- :format => { :with => /\A.{2,}\z/,
- :message => "Login must have at least two characters"}
- validates :login,
- :format => { :with => /\A[a-z\d_\.-]+\z/,
- :message => "Only lowercase letters, digits, . - and _ allowed."}
- validates :login,
- :format => { :with => /\A[a-z].*\z/,
- :message => "Login must begin with a lowercase letter"}
- validates :login,
- :format => { :with => /\A.*[a-z\d]\z/,
- :message => "Login must end with a letter or digit"}
-
validate :login_is_unique_alias
validates :password_salt, :password_verifier,
@@ -43,10 +25,6 @@ class User < CouchRest::Model::Base
:confirmation => true,
:format => { :with => /.{8}.*/, :message => "needs to be at least 8 characters long" }
- validates :email_forward,
- :allow_blank => true,
- :format => { :with => /\A(([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,}))?\Z/, :message => "needs to be a valid email address"}
-
timestamps!
design do
@@ -54,19 +32,6 @@ class User < CouchRest::Model::Base
load_views(own_path.join('..', 'designs', 'user'))
view :by_login
view :by_created_at
- view :pgp_key_by_handle,
- map: <<-EOJS
- function(doc) {
- if (doc.type != 'User') {
- return;
- }
- emit(doc.login, doc.public_key);
- doc.email_aliases.forEach(function(alias){
- emit(alias.username, doc.public_key);
- });
- }
- EOJS
-
end # end of design
class << self
@@ -105,16 +70,30 @@ class User < CouchRest::Model::Base
APP_CONFIG['admins'].include? self.login
end
- # this currently only adds the first email address submitted.
- # All the ui needs for now.
- def email_aliases_attributes=(attrs)
- email_aliases.build(attrs.values.first) if attrs
- end
-
def most_recent_tickets(count=3)
Ticket.for_user(self).limit(count).all #defaults to having most recent updated first
end
+ # DEPRECATED
+ #
+ # Please set the key on the identity directly
+ # WARNING: This will not be serialized with the user record!
+ # It is only a workaround for the key form.
+ def public_key=(value)
+ identity.set_key(:pgp, value)
+ end
+
+ # DEPRECATED
+ #
+ # Please access identity.keys[:pgp] directly
+ def public_key
+ identity.keys[:pgp]
+ end
+
+ def identity
+ @identity ||= Identity.for(self)
+ end
+
protected
##
@@ -122,12 +101,10 @@ class User < CouchRest::Model::Base
##
def login_is_unique_alias
- has_alias = User.find_by_login_or_alias(username)
- return if has_alias.nil?
- if has_alias != self
+ 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")
- elsif has_alias.login != self.login
- errors.add(:login, "may not be the same as one of your aliases")
end
end
diff --git a/users/app/views/email_settings/edit.html.haml b/users/app/views/email_settings/edit.html.haml
deleted file mode 100644
index 7757a31..0000000
--- a/users/app/views/email_settings/edit.html.haml
+++ /dev/null
@@ -1,38 +0,0 @@
-- form_options = {:url => user_email_settings_path(@user), :html => {:class => 'form-horizontal'}, :validate => true}
-- alias_error_class = @email_alias.username && !@email_alias.valid? ? 'error' : ''
-
-- content_for :head do
- :css
- table.aliases tr:first-child td {
- border-top: none;
- }
-
-= simple_form_for @user, form_options.dup do |f|
- %legend= t(:email_aliases)
- .control-group
- %label.control-label= t(:current_aliases)
- .controls
- %table.table.table-condensed.no-header.slim.aliases
- - if @user.email_aliases.any?
- - @user.email_aliases.each do |email|
- %tr
- %td= email
- %td= link_to(icon(:remove) + t(:remove), user_email_alias_path(@user, email), :method => :delete)
- - else
- %tr
- %td{:colspan=>2}= t(:none)
- .control-group{:class => alias_error_class}
- %label.control-label= t(:add_email_alias)
- .controls
- = f.simple_fields_for :email_aliases, @email_alias do |e|
- .input-append
- = e.input_field :username
- = e.submit t(:add), :class => 'btn'
- = e.error :username
-
-= simple_form_for @user, form_options do |f|
- %legend= t(:advanced_options)
- = f.input :email_forward
- = f.input :public_key, :as => :text, :hint => t(:use_ascii_key), :input_html => {:class => "full-width", :rows => 4}
- .form-actions
- = f.submit t(:save), :class => 'btn btn-primary'
diff --git a/users/app/views/users/_edit.html.haml b/users/app/views/users/_edit.html.haml
index 0402f37..5f74d32 100644
--- a/users/app/views/users/_edit.html.haml
+++ b/users/app/views/users/_edit.html.haml
@@ -23,6 +23,20 @@
= f.submit t(:save), :class => 'btn btn-primary'
-#
+-# CHANGE PGP KEY
+-#
+-# this will be replaced by a identities controller/view at some point
+-#
+
+- form_options = {:html => {:class => user_form_class('form-horizontal'), :id => 'update_pgp_key'}, :validate => true}
+= simple_form_for [:api, @user], form_options do |f|
+ %legend= t(:advanced_options)
+ = f.input :public_key, :as => :text, :hint => t(:use_ascii_key), :input_html => {:class => "full-width", :rows => 4}
+ .control-group
+ .controls
+ = f.submit t(:save), :class => 'btn'
+
+-#
-# DESTROY ACCOUNT
-#
@@ -48,4 +62,4 @@
%p= t(:enable_description)
= link_to enable_user_path(@user), :method => :post, :class => "btn btn-warning" do
%i.icon-ok.icon-white
- = t(:enable) \ No newline at end of file
+ = t(:enable)
diff --git a/users/app/views/users/_warnings.html.haml b/users/app/views/users/_warnings.html.haml
index 7e0b2ce..79ab103 100644
--- a/users/app/views/users/_warnings.html.haml
+++ b/users/app/views/users/_warnings.html.haml
@@ -1,5 +1,5 @@
%noscript
- %div.alert.alert-error=t :js_required
+ %div.alert.alert-error=t :js_required_html
#cookie_warning.alert.alert-error{:style => "display:none"}
=t :cookie_disabled_warning
:javascript
diff --git a/users/config/initializers/add_controller_methods.rb b/users/config/initializers/add_controller_methods.rb
index 2579176..f572ecb 100644
--- a/users/config/initializers/add_controller_methods.rb
+++ b/users/config/initializers/add_controller_methods.rb
@@ -1,3 +1,4 @@
ActiveSupport.on_load(:application_controller) do
include ControllerExtension::Authentication
+ include ControllerExtension::TokenAuthentication
end
diff --git a/users/config/locales/en.yml b/users/config/locales/en.yml
index 1aa7005..55ba3a1 100644
--- a/users/config/locales/en.yml
+++ b/users/config/locales/en.yml
@@ -12,6 +12,7 @@ en:
change_password: "Change Password"
login_message: "Please log in with your account."
invalid_user_pass: "Not a valid username/password combination"
+ invalid_ephemeral: "Invalid random key used. This looked like an attempt to hack the site to us. If it wasn't please contact support so we can look into the issue."
all_strategies_failed: "Could not understand your login attempt. Please first send your login and a SRP ephemeral value A and then send the client_auth in the same session (using cookies)."
update_login_and_password: "Update Login and Password"
destroy_my_account: "Destroy my account"
@@ -31,7 +32,7 @@ en:
not_authorized_login: "Please log in to perform that action."
search: "Search"
cookie_disabled_warning: "You have cookies disabled. You will not be able to login until you enable cookies."
- js_required: "We are sorry, but this doesn't work without javascript enabled. This is for security reasons."
+ js_required_html: "We are sorry, but this doesn't work without javascript enabled. This is because the authentication system used, <a href='http://srp.stanford.edu/'>SRP</a>, requires javascript."
enable_account: "Enable the account %{username}"
enable_description: "This will restore the account to full functionality"
deactivate_account: "Deactivate the account %{username}"
diff --git a/users/leap_web_users.gemspec b/users/leap_web_users.gemspec
index d33328a..7d1f220 100644
--- a/users/leap_web_users.gemspec
+++ b/users/leap_web_users.gemspec
@@ -17,6 +17,6 @@ Gem::Specification.new do |s|
s.add_dependency "leap_web_core", LeapWeb::VERSION
- s.add_dependency "ruby-srp", "~> 0.2.0"
+ s.add_dependency "ruby-srp", "~> 0.2.1"
s.add_dependency "rails_warden"
end
diff --git a/users/lib/warden/strategies/secure_remote_password.rb b/users/lib/warden/strategies/secure_remote_password.rb
index 2c681be..4688fcd 100644
--- a/users/lib/warden/strategies/secure_remote_password.rb
+++ b/users/lib/warden/strategies/secure_remote_password.rb
@@ -49,6 +49,8 @@ module Warden
else
fail! :base => 'invalid_user_pass'
end
+ rescue SRP::InvalidEphemeral
+ fail!(:base => "invalid_ephemeral")
end
def json_response(object)
diff --git a/users/test/factories.rb b/users/test/factories.rb
index 777704b..c87e290 100644
--- a/users/test/factories.rb
+++ b/users/test/factories.rb
@@ -18,4 +18,7 @@ FactoryGirl.define do
end
end
end
+
+ factory :token
+
end
diff --git a/users/test/functional/helper_methods_test.rb b/users/test/functional/helper_methods_test.rb
index 2b2375c..44226ae 100644
--- a/users/test/functional/helper_methods_test.rb
+++ b/users/test/functional/helper_methods_test.rb
@@ -11,7 +11,7 @@ class HelperMethodsTest < ActionController::TestCase
# we test them right in here...
include ApplicationController._helpers
- # they all reference the controller.
+ # the helpers all reference the controller.
def controller
@controller
end
diff --git a/users/test/functional/test_helpers_test.rb b/users/test/functional/test_helpers_test.rb
new file mode 100644
index 0000000..9bd01ad
--- /dev/null
+++ b/users/test/functional/test_helpers_test.rb
@@ -0,0 +1,38 @@
+#
+# There are a few test helpers for dealing with login etc.
+# We test them here and also document their behaviour.
+#
+
+require 'test_helper'
+
+class TestHelpersTest < ActionController::TestCase
+ tests ApplicationController # testing no controller in particular
+
+ def test_login_stubs_warden
+ login
+ assert_equal @current_user, request.env['warden'].user
+ end
+
+ def test_login_token_authenticates
+ login
+ assert_equal @current_user, @controller.send(:token_authenticate)
+ end
+
+ def test_login_stubs_token
+ login
+ assert @token
+ assert_equal @current_user, @token.user
+ end
+
+ def test_login_adds_token_header
+ login
+ token_present = @controller.authenticate_with_http_token do |token, options|
+ assert_equal @token.id, token
+ end
+ # authenticate_with_http_token just returns nil and does not
+ # execute the block if there is no token. So we have to also
+ # ensure it was run:
+ assert token_present
+ end
+end
+
diff --git a/users/test/functional/users_controller_test.rb b/users/test/functional/users_controller_test.rb
index 0ce5cc2..96ae48c 100644
--- a/users/test/functional/users_controller_test.rb
+++ b/users/test/functional/users_controller_test.rb
@@ -59,19 +59,23 @@ class UsersControllerTest < ActionController::TestCase
assert_access_denied
end
- test "show for non-existing user" do
+ test "may not show non-existing user without auth" do
nonid = 'thisisnotanexistinguserid'
- # when unauthenticated:
get :show, :id => nonid
assert_access_denied(true, false)
+ end
- # when authenticated but not admin:
+ test "may not show non-existing user without admin" do
+ nonid = 'thisisnotanexistinguserid'
login
+
get :show, :id => nonid
assert_access_denied
+ end
- # when authenticated as admin:
+ test "redirect admin to user list for non-existing user" do
+ nonid = 'thisisnotanexistinguserid'
login :is_admin? => true
get :show, :id => nonid
assert_response :redirect
diff --git a/users/test/functional/v1/sessions_controller_test.rb b/users/test/functional/v1/sessions_controller_test.rb
index 0c4e325..ff9fca1 100644
--- a/users/test/functional/v1/sessions_controller_test.rb
+++ b/users/test/functional/v1/sessions_controller_test.rb
@@ -7,7 +7,7 @@ class V1::SessionsControllerTest < ActionController::TestCase
setup do
@request.env['HTTP_HOST'] = 'api.lvh.me'
- @user = stub_record :user
+ @user = stub_record :user, {}, true
@client_hex = 'a123'
end
@@ -48,13 +48,22 @@ class V1::SessionsControllerTest < ActionController::TestCase
assert_response :success
assert json_response.keys.include?("id")
assert json_response.keys.include?("token")
+ assert token = Token.find(json_response['token'])
+ assert_equal @user.id, token.user_id
end
- test "logout should reset warden user" do
+ test "logout should reset session" do
expect_warden_logout
delete :destroy
- assert_response :redirect
- assert_redirected_to root_url
+ assert_response 204
+ end
+
+ test "logout should destroy token" do
+ login
+ expect_warden_logout
+ @token.expects(:destroy)
+ delete :destroy
+ assert_response 204
end
def expect_warden_logout
@@ -65,5 +74,4 @@ class V1::SessionsControllerTest < ActionController::TestCase
request.env['warden'].expects(:logout)
end
-
end
diff --git a/users/test/functional/v1/users_controller_test.rb b/users/test/functional/v1/users_controller_test.rb
index 0d44e50..a330bf3 100644
--- a/users/test/functional/v1/users_controller_test.rb
+++ b/users/test/functional/v1/users_controller_test.rb
@@ -5,7 +5,9 @@ class V1::UsersControllerTest < ActionController::TestCase
test "user can change settings" do
user = find_record :user
changed_attribs = record_attributes_for :user_with_settings
- user.expects(:update_attributes).with(changed_attribs)
+ account_settings = stub
+ account_settings.expects(:update).with(changed_attribs)
+ AccountSettings.expects(:new).with(user).returns(account_settings)
login user
put :update, :user => changed_attribs, :id => user.id, :format => :json
@@ -18,7 +20,9 @@ class V1::UsersControllerTest < ActionController::TestCase
test "admin can update user" do
user = find_record :user
changed_attribs = record_attributes_for :user_with_settings
- user.expects(:update_attributes).with(changed_attribs)
+ account_settings = stub
+ account_settings.expects(:update).with(changed_attribs)
+ AccountSettings.expects(:new).with(user).returns(account_settings)
login :is_admin? => true
put :update, :user => changed_attribs, :id => user.id, :format => :json
diff --git a/users/test/integration/api/account_flow_test.rb b/users/test/integration/api/account_flow_test.rb
index 4c94389..e41befa 100644
--- a/users/test/integration/api/account_flow_test.rb
+++ b/users/test/integration/api/account_flow_test.rb
@@ -5,6 +5,7 @@ class AccountFlowTest < RackTest
setup do
@login = "integration_test_user"
+ Identity.find_by_address(@login + '@' + APP_CONFIG[:domain]).tap{|i| i.destroy if i}
User.find_by_login(@login).tap{|u| u.destroy if u}
@password = "srp, verify me!"
@srp = SRP::Client.new @login, :password => @password
@@ -18,7 +19,10 @@ class AccountFlowTest < RackTest
end
teardown do
- @user.destroy if @user
+ if @user.reload
+ @user.identity.destroy
+ @user.destroy
+ end
Warden.test_reset!
end
@@ -74,25 +78,45 @@ class AccountFlowTest < RackTest
assert_nil server_auth
end
+ test "update password via api" do
+ @srp.authenticate(self)
+ @password = "No! Verify me instead."
+ @srp = SRP::Client.new @login, :password => @password
+ @user_params = {
+ # :login => @login,
+ :password_verifier => @srp.verifier.to_s(16),
+ :password_salt => @srp.salt.to_s(16)
+ }
+ put "http://api.lvh.me:3000/1/users/" + @user.id + '.json',
+ :user => @user_params,
+ :format => :json
+ server_auth = @srp.authenticate(self)
+ assert last_response.successful?
+ assert_nil server_auth["errors"]
+ assert server_auth["M2"]
+ end
+
test "update user" do
server_auth = @srp.authenticate(self)
test_public_key = 'asdlfkjslfdkjasd'
original_login = @user.login
new_login = 'zaph'
+ User.find_by_login(new_login).try(:destroy)
+ Identity.by_address.key(new_login + '@' + APP_CONFIG[:domain]).each do |identity|
+ identity.destroy
+ end
put "http://api.lvh.me:3000/1/users/" + @user.id + '.json', :user => {:public_key => test_public_key, :login => new_login}, :format => :json
- @user.reload
- assert_equal test_public_key, @user.public_key
- assert_equal new_login, @user.login
+ assert last_response.successful?
+ assert_equal test_public_key, Identity.for(@user).keys[:pgp]
+ # does not change login if no password_verifier is present
+ assert_equal original_login, @user.login
# eventually probably want to remove most of this into a non-integration functional test
# should not overwrite public key:
put "http://api.lvh.me:3000/1/users/" + @user.id + '.json', :user => {:blee => :blah}, :format => :json
- @user.reload
- assert_equal test_public_key, @user.public_key
+ assert_equal test_public_key, Identity.for(@user).keys[:pgp]
# should overwrite public key:
put "http://api.lvh.me:3000/1/users/" + @user.id + '.json', :user => {:public_key => nil}, :format => :json
- # TODO: not sure why i need this, but when public key is removed, the DB is updated but @user.reload doesn't seem to actually reload.
- @user = User.find(@user.id) # @user.reload
- assert_nil @user.public_key
+ assert_nil Identity.for(@user).keys[:pgp]
end
end
diff --git a/users/test/integration/api/python/flow_with_srp.py b/users/test/integration/api/python/flow_with_srp.py
index 7b741d6..9fc168b 100755
--- a/users/test/integration/api/python/flow_with_srp.py
+++ b/users/test/integration/api/python/flow_with_srp.py
@@ -11,68 +11,86 @@ import binascii
safe_unhexlify = lambda x: binascii.unhexlify(x) if (len(x) % 2 == 0) else binascii.unhexlify('0'+x)
+# using globals for now
+# server = 'https://dev.bitmask.net/1'
+server = 'http://api.lvh.me:3000/1'
+
+def run_tests():
+ login = 'test_' + id_generator()
+ password = id_generator() + id_generator()
+ usr = srp.User( login, password, srp.SHA256, srp.NG_1024 )
+ print_and_parse(signup(login, password))
+
+ auth = print_and_parse(authenticate(usr))
+ verify_or_debug(auth, usr)
+ assert usr.authenticated()
+
+ usr = change_password(auth['id'], login, auth['token'])
+
+ auth = print_and_parse(authenticate(usr))
+ verify_or_debug(auth, usr)
+ # At this point the authentication process is complete.
+ assert usr.authenticated()
+
# let's have some random name
def id_generator(size=6, chars=string.ascii_lowercase + string.digits):
return ''.join(random.choice(chars) for x in range(size))
-# using globals for a start
-server = 'https://api.bitmask.net:4430/1'
-login = id_generator()
-password = id_generator() + id_generator()
-
-# print ' username = "' + login + '"'
-# print ' password = "' + password + '"'
-
# log the server communication
def print_and_parse(response):
- print response.request.method + ': ' + response.url
- print " " + json.dumps(response.request.data)
+ request = response.request
+ print request.method + ': ' + response.url
+ if hasattr(request, 'data'):
+ print " " + json.dumps(response.request.data)
print " -> " + response.text
- return json.loads(response.text)
+ try:
+ return json.loads(response.text)
+ except ValueError:
+ return None
-def signup(session):
+def signup(login, password):
salt, vkey = srp.create_salted_verification_key( login, password, srp.SHA256, srp.NG_1024 )
- # print ' salt = "' + binascii.hexlify(salt) + '"'
- # print ' v = "' + binascii.hexlify(vkey) + '"'
user_params = {
'user[login]': login,
'user[password_verifier]': binascii.hexlify(vkey),
'user[password_salt]': binascii.hexlify(salt)
}
- return session.post(server + '/users.json', data = user_params, verify = False)
+ return requests.post(server + '/users.json', data = user_params, verify = False)
-usr = srp.User( login, password, srp.SHA256, srp.NG_1024 )
+def change_password(user_id, login, token):
+ password = id_generator() + id_generator()
+ salt, vkey = srp.create_salted_verification_key( login, password, srp.SHA256, srp.NG_1024 )
+ user_params = {
+ 'user[password_verifier]': binascii.hexlify(vkey),
+ 'user[password_salt]': binascii.hexlify(salt)
+ }
+ auth_headers = { 'Authorization': 'Token token="' + token + '"'}
+ print user_params
+ print_and_parse(requests.put(server + '/users/' + user_id + '.json', data = user_params, verify = False, headers = auth_headers))
+ return srp.User( login, password, srp.SHA256, srp.NG_1024 )
-def authenticate(session, login):
+
+def authenticate(usr):
+ session = requests.session()
uname, A = usr.start_authentication()
- # print ' aa = "' + binascii.hexlify(A) + '"'
params = {
'login': uname,
'A': binascii.hexlify(A)
}
init = print_and_parse(session.post(server + '/sessions', data = params, verify=False))
- # print ' b = "' + init['b'] + '"'
- # print ' bb = "' + init['B'] + '"'
M = usr.process_challenge( safe_unhexlify(init['salt']), safe_unhexlify(init['B']) )
- # print ' m = "' + binascii.hexlify(M) + '"'
- return session.put(server + '/sessions/' + login, verify = False,
+ return session.put(server + '/sessions/' + uname, verify = False,
data = {'client_auth': binascii.hexlify(M)})
-session = requests.session()
-user = print_and_parse(signup(session))
-
-# SRP signup would happen here and calculate M hex
-auth = print_and_parse(authenticate(session, user['login']))
-if ( 'errors' in auth ):
- print ' u = "%x"' % usr.u
- print ' x = "%x"' % usr.x
- print ' v = "%x"' % usr.v
- print ' S = "%x"' % usr.S
- print ' K = "' + binascii.hexlify(usr.K) + '"'
- print ' M = "%x"' % usr.M
-else:
- usr.verify_session( safe_unhexlify(auth["M2"]) )
-
-# At this point the authentication process is complete.
-assert usr.authenticated()
+def verify_or_debug(auth, usr):
+ if ( 'errors' in auth ):
+ print ' u = "%x"' % usr.u
+ print ' x = "%x"' % usr.x
+ print ' v = "%x"' % usr.v
+ print ' S = "%x"' % usr.S
+ print ' K = "' + binascii.hexlify(usr.K) + '"'
+ print ' M = "' + binascii.hexlify(usr.M) + '"'
+ else:
+ usr.verify_session( safe_unhexlify(auth["M2"]) )
+run_tests()
diff --git a/users/test/integration/browser/account_test.rb b/users/test/integration/browser/account_test.rb
index ce63baf..b412980 100644
--- a/users/test/integration/browser/account_test.rb
+++ b/users/test/integration/browser/account_test.rb
@@ -20,4 +20,24 @@ class AccountTest < BrowserIntegrationTest
assert_equal '/', current_path
end
+ # trying to seed an invalid A for srp login
+ test "detects attempt to circumvent SRP" do
+ user = FactoryGirl.create :user
+ visit '/sessions/new'
+ fill_in 'Username', with: user.login
+ fill_in 'Password', with: "password"
+ inject_malicious_js
+ click_on 'Log In'
+ assert page.has_content?("Invalid random key")
+ assert page.has_no_content?("Welcome")
+ end
+
+ def inject_malicious_js
+ page.execute_script <<-EOJS
+ var calc = new srp.Calculate();
+ calc.A = function(_a) {return "00";};
+ calc.S = calc.A;
+ srp.session = new srp.Session(null, calc);
+ EOJS
+ end
end
diff --git a/users/test/support/auth_test_helper.rb b/users/test/support/auth_test_helper.rb
index 555b5db..47147fc 100644
--- a/users/test/support/auth_test_helper.rb
+++ b/users/test/support/auth_test_helper.rb
@@ -13,8 +13,9 @@ module AuthTestHelper
if user_or_method_hash.respond_to?(:reverse_merge)
user_or_method_hash.reverse_merge! :is_admin? => false
end
- @current_user = stub_record(:user, user_or_method_hash, true)
+ @current_user = stub_record(:user, user_or_method_hash)
request.env['warden'] = stub :user => @current_user
+ request.env['HTTP_AUTHORIZATION'] = header_for_token_auth
return @current_user
end
@@ -37,6 +38,12 @@ module AuthTestHelper
end
end
+ protected
+
+ def header_for_token_auth
+ @token = find_record(:token, :user => @current_user)
+ ActionController::HttpAuthentication::Token.encode_credentials @token.id
+ end
end
class ActionController::TestCase
diff --git a/users/test/support/stub_record_helper.rb b/users/test/support/stub_record_helper.rb
index 8aa1973..5bccb66 100644
--- a/users/test/support/stub_record_helper.rb
+++ b/users/test/support/stub_record_helper.rb
@@ -7,9 +7,8 @@ module StubRecordHelper
# If no record is given but a hash or nil will create a stub based on
# that instead and returns the stub.
#
- def find_record(factory, attribs_hash = {})
- attribs_hash = attribs_hash.reverse_merge(:id => Random.rand(10000).to_s)
- record = stub_record factory, attribs_hash
+ def find_record(factory, record_or_attribs_hash = {})
+ record = stub_record factory, record_or_attribs_hash, true
klass = record.class
finder = klass.respond_to?(:find_by_param) ? :find_by_param : :find
klass.stubs(finder).with(record.to_param.to_s).returns(record)
diff --git a/users/test/unit/email_aliases_test.rb b/users/test/unit/email_aliases_test.rb
deleted file mode 100644
index 86d14aa..0000000
--- a/users/test/unit/email_aliases_test.rb
+++ /dev/null
@@ -1,66 +0,0 @@
-require 'test_helper'
-
-class EmailAliasTest < ActiveSupport::TestCase
-
- setup do
- @user = FactoryGirl.build :user
- @alias = "valid_alias"
- # make sure no existing records are in the way...
- User.find_by_login_or_alias(@alias).try(:destroy)
- end
-
- test "no email aliases set in the beginning" do
- assert_equal [], @user.email_aliases
- end
-
- test "adding email alias through params" do
- @user.attributes = {:email_aliases_attributes => {"0" => {:email => @alias}}}
- assert @user.changed?
- assert @user.save
- assert_equal @alias, @user.email_aliases.first.username
- end
-
- test "adding email alias directly" do
- @user.email_aliases.build :email => @alias
- assert @user.save
- assert_equal @alias, @user.email_aliases.first.username
- end
-
- test "duplicated email aliases are invalid" do
- @user.email_aliases.build :email => @alias
- @user.save
- assert_invalid_alias @alias
- end
-
- test "email alias needs to be different from other peoples login" do
- other_user = FactoryGirl.create :user, :login => @alias
- assert_invalid_alias @alias
- other_user.destroy
- end
-
- test "email needs to be different from other peoples email aliases" do
- other_user = FactoryGirl.create :user, :email_aliases_attributes => {'1' => @alias}
- assert_invalid_alias @alias
- other_user.destroy
- end
-
- test "login is invalid as email alias" do
- @user.login = @alias
- assert_invalid_alias @alias
- end
-
- test "find user by email alias" do
- @user.email_aliases.build :email => @alias
- assert @user.save
- assert_equal @user, User.find_by_login_or_alias(@alias)
- assert_equal @user, User.find_by_alias(@alias)
- assert_nil User.find_by_login(@alias)
- end
-
- def assert_invalid_alias(string)
- email_alias = @user.email_aliases.build :email => string
- assert !email_alias.valid?
- assert !@user.valid?
- end
-
-end
diff --git a/users/test/unit/email_test.rb b/users/test/unit/email_test.rb
new file mode 100644
index 0000000..7cfbc84
--- /dev/null
+++ b/users/test/unit/email_test.rb
@@ -0,0 +1,19 @@
+require 'test_helper'
+
+class EmailTest < ActiveSupport::TestCase
+
+ test "valid format" do
+ email = Email.new(email_string)
+ assert email.valid?
+ end
+
+ test "validates format" do
+ email = Email.new("email")
+ assert !email.valid?
+ assert_equal ["needs to be a valid email address"], email.errors[:email]
+ end
+
+ def email_string
+ @email_string ||= Faker::Internet.email
+ end
+end
diff --git a/users/test/unit/identity_test.rb b/users/test/unit/identity_test.rb
new file mode 100644
index 0000000..bf24f02
--- /dev/null
+++ b/users/test/unit/identity_test.rb
@@ -0,0 +1,86 @@
+require 'test_helper'
+
+class IdentityTest < ActiveSupport::TestCase
+
+ setup do
+ @user = FactoryGirl.create(:user)
+ end
+
+ teardown do
+ @user.destroy
+ end
+
+ test "initial identity for a user" do
+ id = Identity.for(@user)
+ assert_equal @user.email_address, id.address
+ assert_equal @user.email_address, id.destination
+ assert_equal @user, id.user
+ end
+
+ test "add alias" do
+ id = Identity.for @user, address: alias_name
+ assert_equal LocalEmail.new(alias_name), id.address
+ assert_equal @user.email_address, id.destination
+ assert_equal @user, id.user
+ end
+
+ test "add forward" do
+ id = Identity.for @user, destination: forward_address
+ assert_equal @user.email_address, id.address
+ assert_equal Email.new(forward_address), id.destination
+ assert_equal @user, id.user
+ end
+
+ test "forward alias" do
+ id = Identity.for @user, address: alias_name, destination: forward_address
+ assert_equal LocalEmail.new(alias_name), id.address
+ assert_equal Email.new(forward_address), id.destination
+ assert_equal @user, id.user
+ id.save
+ end
+
+ test "prevents duplicates" do
+ id = Identity.create_for @user, address: alias_name, destination: forward_address
+ dup = Identity.build_for @user, address: alias_name, destination: forward_address
+ assert !dup.valid?
+ assert_equal ["This alias already exists"], dup.errors[:base]
+ end
+
+ test "validates availability" do
+ other_user = FactoryGirl.create(:user)
+ id = Identity.create_for @user, address: alias_name, destination: forward_address
+ taken = Identity.build_for other_user, address: alias_name
+ assert !taken.valid?
+ assert_equal ["This email has already been taken"], taken.errors[:base]
+ other_user.destroy
+ end
+
+ test "setting and getting pgp key" do
+ id = Identity.for(@user)
+ id.set_key(:pgp, pgp_key_string)
+ assert_equal pgp_key_string, id.keys[:pgp]
+ end
+
+ test "querying pgp key via couch" do
+ id = Identity.for(@user)
+ id.set_key(:pgp, pgp_key_string)
+ id.save
+ view = Identity.pgp_key_by_email.key(id.address)
+ assert_equal 1, view.rows.count
+ assert result = view.rows.first
+ assert_equal id.address, result["key"]
+ assert_equal id.keys[:pgp], result["value"]
+ end
+
+ def alias_name
+ @alias_name ||= Faker::Internet.user_name
+ end
+
+ def forward_address
+ @forward_address ||= Faker::Internet.email
+ end
+
+ def pgp_key_string
+ @pgp_key ||= "DUMMY PGP KEY ... "+SecureRandom.base64(4096)
+ end
+end
diff --git a/users/test/unit/local_email_test.rb b/users/test/unit/local_email_test.rb
new file mode 100644
index 0000000..b25f46f
--- /dev/null
+++ b/users/test/unit/local_email_test.rb
@@ -0,0 +1,34 @@
+require 'test_helper'
+
+class LocalEmailTest < ActiveSupport::TestCase
+
+ test "appends domain" do
+ local = LocalEmail.new(handle)
+ assert_equal LocalEmail.new(email), local
+ assert local.valid?
+ end
+
+ test "returns handle" do
+ local = LocalEmail.new(email)
+ assert_equal handle, local.handle
+ end
+
+ test "prints full email" do
+ local = LocalEmail.new(handle)
+ assert_equal email, "#{local}"
+ end
+
+ test "validates domain" do
+ local = LocalEmail.new(Faker::Internet.email)
+ assert !local.valid?
+ assert_equal ["needs to end in @#{LocalEmail.domain}"], local.errors[:email]
+ end
+
+ def handle
+ @handle ||= Faker::Internet.user_name
+ end
+
+ def email
+ handle + "@" + APP_CONFIG[:domain]
+ end
+end
diff --git a/users/test/unit/user_test.rb b/users/test/unit/user_test.rb
index c8c837b..89ee749 100644
--- a/users/test/unit/user_test.rb
+++ b/users/test/unit/user_test.rb
@@ -56,23 +56,30 @@ class UserTest < ActiveSupport::TestCase
other_user.destroy
end
- test "login needs to be different from other peoples email aliases" do
+ test "login needs to be unique amongst aliases" do
other_user = FactoryGirl.create :user
- other_user.email_aliases.build :email => @user.login
- other_user.save
+ Identity.create_for other_user, address: @user.login
assert !@user.valid?
other_user.destroy
end
+ test "deprecated public key api still works" do
+ key = SecureRandom.base64(4096)
+ @user.public_key = key
+ assert_equal key, @user.public_key
+ end
+
test "pgp key view" do
- @user.public_key = SecureRandom.base64(4096)
- @user.save
+ key = SecureRandom.base64(4096)
+ identity = Identity.create_for @user
+ identity.set_key('pgp', key)
+ identity.save
- view = User.pgp_key_by_handle.key(@user.login)
+ view = Identity.pgp_key_by_email.key(@user.email_address)
assert_equal 1, view.rows.count
assert result = view.rows.first
- assert_equal @user.login, result["key"]
- assert_equal @user.public_key, result["value"]
+ assert_equal @user.email_address, result["key"]
+ assert_equal key, result["value"]
end
end