From 17c2caf1b8b6d0c1865de90c6900c49867def8ee Mon Sep 17 00:00:00 2001 From: Nick Muerdter Date: Wed, 28 Dec 2016 20:13:24 -0700 Subject: [PATCH 01/26] Remove Mozilla Persona as login option since it's shutdown. --- .../files/etc/api-umbrella/api-umbrella.yml | 1 - .../verify/spec/localhost/service_spec.rb | 2 +- config/default.yml | 1 - config/test.yml | 1 - src/api-umbrella/web-app/Gemfile | 1 - src/api-umbrella/web-app/Gemfile.lock | 5 --- .../admins/omniauth_callbacks_controller.rb | 5 --- .../app/views/admin/sessions/new.html.erb | 41 ++++--------------- .../web-app/config/initializers/devise.rb | 3 -- .../web-app/config/locales/de.yml | 1 - .../web-app/config/locales/en.yml | 1 - .../web-app/config/locales/es-419.yml | 1 - .../web-app/config/locales/fi.yml | 1 - .../web-app/config/locales/fr.yml | 1 - .../web-app/config/locales/it.yml | 1 - .../web-app/config/locales/ru.yml | 1 - test/admin_ui/test_login.rb | 5 --- 17 files changed, 9 insertions(+), 63 deletions(-) diff --git a/build/package/files/etc/api-umbrella/api-umbrella.yml b/build/package/files/etc/api-umbrella/api-umbrella.yml index 899895862..320b118e3 100644 --- a/build/package/files/etc/api-umbrella/api-umbrella.yml +++ b/build/package/files/etc/api-umbrella/api-umbrella.yml @@ -11,7 +11,6 @@ # enabled: # - github # - google -# - persona # github: # client_id: # client_secret: diff --git a/build/package/verify/spec/localhost/service_spec.rb b/build/package/verify/spec/localhost/service_spec.rb index 6636b8e96..76bc2bab2 100644 --- a/build/package/verify/spec/localhost/service_spec.rb +++ b/build/package/verify/spec/localhost/service_spec.rb @@ -317,7 +317,7 @@ def install_package(version) it "admin login page loads" do response = RestClient::Request.execute(:method => :get, :url => "https://localhost/admin/login", :verify_ssl => false) - expect(response).to include("Login with Persona") + expect(response).to include("Admin Login") end it "gatekeeper blocks key-less requests" do diff --git a/config/default.yml b/config/default.yml index 594e7fb10..5d2c26239 100644 --- a/config/default.yml +++ b/config/default.yml @@ -85,7 +85,6 @@ web: enabled: - github - google - - persona cas: options: facebook: diff --git a/config/test.yml b/config/test.yml index 697508e97..d7076afdc 100644 --- a/config/test.yml +++ b/config/test.yml @@ -45,7 +45,6 @@ web: - google - ldap - max.gov - - persona facebook: client_id: test_fake client_secret: test_fake diff --git a/src/api-umbrella/web-app/Gemfile b/src/api-umbrella/web-app/Gemfile index fe166b3d3..fdd8bd7b9 100644 --- a/src/api-umbrella/web-app/Gemfile +++ b/src/api-umbrella/web-app/Gemfile @@ -66,7 +66,6 @@ gem "omniauth-facebook", "~> 4.0.0", :require => false gem "omniauth-github", :git => "https://github.com/intridea/omniauth-github.git", :require => false gem "omniauth-google-oauth2", "~> 0.4.1", :require => false gem "omniauth-ldap", "~> 1.0.5", :require => false -gem "omniauth-persona", "~> 0.0.1", :require => false gem "omniauth-twitter", "~> 1.2.1", :require => false # Authorization diff --git a/src/api-umbrella/web-app/Gemfile.lock b/src/api-umbrella/web-app/Gemfile.lock index ce5c6276f..abb5da2a5 100644 --- a/src/api-umbrella/web-app/Gemfile.lock +++ b/src/api-umbrella/web-app/Gemfile.lock @@ -233,10 +233,6 @@ GEM omniauth-oauth2 (1.4.0) oauth2 (~> 1.0) omniauth (~> 1.2) - omniauth-persona (0.0.1) - faraday - multi_json - omniauth (~> 1.0) omniauth-twitter (1.2.1) json (~> 1.3) omniauth-oauth (~> 1.1) @@ -354,7 +350,6 @@ DEPENDENCIES omniauth-github! omniauth-google-oauth2 (~> 0.4.1) omniauth-ldap (~> 1.0.5) - omniauth-persona (~> 0.0.1) omniauth-twitter (~> 1.2.1) premailer-rails (~> 1.9.5) puma (~> 3.6.2) diff --git a/src/api-umbrella/web-app/app/controllers/admin/admins/omniauth_callbacks_controller.rb b/src/api-umbrella/web-app/app/controllers/admin/admins/omniauth_callbacks_controller.rb index 03a7177e7..34d5596a7 100644 --- a/src/api-umbrella/web-app/app/controllers/admin/admins/omniauth_callbacks_controller.rb +++ b/src/api-umbrella/web-app/app/controllers/admin/admins/omniauth_callbacks_controller.rb @@ -48,11 +48,6 @@ def google_oauth2 login end - def persona - @email = request.env["omniauth.auth"]["info"]["email"] - login - end - def ldap uid_field = request.env["omniauth.strategy"].options[:uid] uid = [request.env["omniauth.auth"]["extra"]["raw_info"][uid_field]].flatten.compact.first diff --git a/src/api-umbrella/web-app/app/views/admin/sessions/new.html.erb b/src/api-umbrella/web-app/app/views/admin/sessions/new.html.erb index 278494657..aeaff6e83 100644 --- a/src/api-umbrella/web-app/app/views/admin/sessions/new.html.erb +++ b/src/api-umbrella/web-app/app/views/admin/sessions/new.html.erb @@ -11,40 +11,15 @@
<%- resource_class.omniauth_providers.each do |provider| %> <% end -%> diff --git a/src/api-umbrella/web-app/config/initializers/devise.rb b/src/api-umbrella/web-app/config/initializers/devise.rb index 5103cdae3..349c35d05 100644 --- a/src/api-umbrella/web-app/config/initializers/devise.rb +++ b/src/api-umbrella/web-app/config/initializers/devise.rb @@ -289,9 +289,6 @@ :service_validate_url => "/cas/serviceValidate", :logout_url => "/cas/logout", :ssl => true - when "persona" - require "omniauth-persona" - config.omniauth :persona else raise "Unknown authentication strategy enabled in config: #{strategy.inspect}" end diff --git a/src/api-umbrella/web-app/config/locales/de.yml b/src/api-umbrella/web-app/config/locales/de.yml index 5a44f4397..7bafd2324 100644 --- a/src/api-umbrella/web-app/config/locales/de.yml +++ b/src/api-umbrella/web-app/config/locales/de.yml @@ -7,7 +7,6 @@ de: facebook: Facebook github: GitHub google_oauth2: Google - persona: Persona errors: messages: invalid_host_format: "Erforderte Formatierung : \"example.com\"" diff --git a/src/api-umbrella/web-app/config/locales/en.yml b/src/api-umbrella/web-app/config/locales/en.yml index b6c945fdc..cc4eb0183 100644 --- a/src/api-umbrella/web-app/config/locales/en.yml +++ b/src/api-umbrella/web-app/config/locales/en.yml @@ -6,7 +6,6 @@ en: facebook: Facebook github: GitHub google_oauth2: Google - persona: Persona ldap: LDAP errors: messages: diff --git a/src/api-umbrella/web-app/config/locales/es-419.yml b/src/api-umbrella/web-app/config/locales/es-419.yml index f21ee954a..a50c19f63 100644 --- a/src/api-umbrella/web-app/config/locales/es-419.yml +++ b/src/api-umbrella/web-app/config/locales/es-419.yml @@ -7,7 +7,6 @@ es-419: facebook: Facebook github: GitHub google_oauth2: Google - persona: Persona errors: messages: invalid_host_format: "debe ser en el formato de \"example.com\"" diff --git a/src/api-umbrella/web-app/config/locales/fi.yml b/src/api-umbrella/web-app/config/locales/fi.yml index 8aed97d4b..adc758140 100644 --- a/src/api-umbrella/web-app/config/locales/fi.yml +++ b/src/api-umbrella/web-app/config/locales/fi.yml @@ -7,7 +7,6 @@ fi: facebook: Facebook github: GitHub google_oauth2: Google - persona: Persona errors: messages: invalid_host_format: "täytyy olla muodossa \"example.com\"" diff --git a/src/api-umbrella/web-app/config/locales/fr.yml b/src/api-umbrella/web-app/config/locales/fr.yml index e86702d59..ceba3d67b 100644 --- a/src/api-umbrella/web-app/config/locales/fr.yml +++ b/src/api-umbrella/web-app/config/locales/fr.yml @@ -7,7 +7,6 @@ fr: facebook: Facebook github: GitHub google_oauth2: Google - persona: Persona errors: messages: invalid_host_format: "format à respecter : \"example.com\"" diff --git a/src/api-umbrella/web-app/config/locales/it.yml b/src/api-umbrella/web-app/config/locales/it.yml index e7ef1bd0b..73415c4f7 100644 --- a/src/api-umbrella/web-app/config/locales/it.yml +++ b/src/api-umbrella/web-app/config/locales/it.yml @@ -7,7 +7,6 @@ it: facebook: Facebook github: GitHub google_oauth2: Google - persona: Persona errors: messages: invalid_host_format: "deve essere nel formato \"esempio.com\"" diff --git a/src/api-umbrella/web-app/config/locales/ru.yml b/src/api-umbrella/web-app/config/locales/ru.yml index 339a661c3..a6a3cbab5 100644 --- a/src/api-umbrella/web-app/config/locales/ru.yml +++ b/src/api-umbrella/web-app/config/locales/ru.yml @@ -7,7 +7,6 @@ ru: facebook: Facebook github: GitHub google_oauth2: Google - persona: Persona errors: messages: invalid_host_format: "Должно быть в соответствующем формате: \"example.com\"" diff --git a/test/admin_ui/test_login.rb b/test/admin_ui/test_login.rb index 2585dc689..c8391ced6 100644 --- a/test/admin_ui/test_login.rb +++ b/test/admin_ui/test_login.rb @@ -79,11 +79,6 @@ def test_login_assets :login_button_text => "Login with MAX.gov", :username_path => "uid", }, - { - :provider => :persona, - :login_button_text => "Login with Persona", - :username_path => "info.email", - }, ].each do |options| define_method("test_#{options.fetch(:provider)}_valid_admin") do assert_login_valid_admin(options) From 2b5dbc312b8919ca24e575ee9ebdbba4e786774d Mon Sep 17 00:00:00 2001 From: Nick Muerdter Date: Tue, 3 Jan 2017 21:58:33 -0700 Subject: [PATCH 02/26] A basic implementation of local user accounts using Devise. Still need to implement tests and cleanup various things, but the basic pieces are integrated. --- config/default.yml | 11 +- .../app/components/admins/index-table.js | 24 +- .../app/components/admins/record-form.js | 23 +- .../admin-ui/app/components/error-messages.js | 15 +- .../components/form-fields/password-field.js | 4 + src/api-umbrella/admin-ui/app/mixins/save.js | 24 +- src/api-umbrella/admin-ui/app/models/admin.js | 4 + .../components/admins/record-form.hbs | 26 +- .../app/templates/components/fields-for.hbs | 1 + .../components/form-fields/password-field.hbs | 3 + src/api-umbrella/web-app/Gemfile | 12 +- src/api-umbrella/web-app/Gemfile.lock | 33 +- .../app/assets/stylesheets/admin/login.css | 113 +++-- .../assets/stylesheets/vendor/normalize.css | 461 ------------------ .../admins/omniauth_callbacks_controller.rb | 26 +- .../admin/registrations_controller.rb | 21 + .../controllers/admin/sessions_controller.rb | 26 +- .../controllers/api/v1/admins_controller.rb | 10 +- .../app/controllers/api/v1/base_controller.rb | 1 + .../app/controllers/application_controller.rb | 8 + src/api-umbrella/web-app/app/models/admin.rb | 104 +++- .../app/views/admin/sessions/new.html.erb | 27 - .../web-app/app/views/api/v1/admins/show.rabl | 1 + .../mailer/confirmation_instructions.html.erb | 5 + .../devise/mailer/password_change.html.erb | 3 + .../reset_password_instructions.html.erb | 8 + .../mailer/unlock_instructions.html.erb | 7 + .../app/views/devise/passwords/edit.html.erb | 19 + .../app/views/devise/passwords/new.html.erb | 15 + .../views/devise/registrations/new.html.erb | 21 + .../app/views/devise/sessions/new.html.erb | 60 +++ .../app/views/devise/shared/_links.html.erb | 25 + .../app/views/devise/unlocks/new.html.erb | 16 + .../app/views/layouts/application.html.erb | 17 +- .../web-app/config/application.rb | 2 +- .../web-app/config/initializers/devise.rb | 74 ++- .../config/initializers/simple_form.rb | 165 +++++++ .../initializers/simple_form_bootstrap.rb | 149 ++++++ .../config/locales/devise_invitable.en.yml | 31 ++ .../web-app/config/locales/en.yml | 14 +- .../web-app/config/locales/simple_form.en.yml | 31 ++ src/api-umbrella/web-app/config/routes.rb | 22 +- .../lib/templates/erb/scaffold/_form.html.erb | 13 + 43 files changed, 1017 insertions(+), 658 deletions(-) create mode 100644 src/api-umbrella/admin-ui/app/components/form-fields/password-field.js create mode 100644 src/api-umbrella/admin-ui/app/templates/components/form-fields/password-field.hbs delete mode 100644 src/api-umbrella/web-app/app/assets/stylesheets/vendor/normalize.css create mode 100644 src/api-umbrella/web-app/app/controllers/admin/registrations_controller.rb delete mode 100644 src/api-umbrella/web-app/app/views/admin/sessions/new.html.erb create mode 100644 src/api-umbrella/web-app/app/views/devise/mailer/confirmation_instructions.html.erb create mode 100644 src/api-umbrella/web-app/app/views/devise/mailer/password_change.html.erb create mode 100644 src/api-umbrella/web-app/app/views/devise/mailer/reset_password_instructions.html.erb create mode 100644 src/api-umbrella/web-app/app/views/devise/mailer/unlock_instructions.html.erb create mode 100644 src/api-umbrella/web-app/app/views/devise/passwords/edit.html.erb create mode 100644 src/api-umbrella/web-app/app/views/devise/passwords/new.html.erb create mode 100644 src/api-umbrella/web-app/app/views/devise/registrations/new.html.erb create mode 100644 src/api-umbrella/web-app/app/views/devise/sessions/new.html.erb create mode 100644 src/api-umbrella/web-app/app/views/devise/shared/_links.html.erb create mode 100644 src/api-umbrella/web-app/app/views/devise/unlocks/new.html.erb create mode 100644 src/api-umbrella/web-app/config/initializers/simple_form.rb create mode 100644 src/api-umbrella/web-app/config/initializers/simple_form_bootstrap.rb create mode 100644 src/api-umbrella/web-app/config/locales/devise_invitable.en.yml create mode 100644 src/api-umbrella/web-app/config/locales/simple_form.en.yml create mode 100644 src/api-umbrella/web-app/lib/templates/erb/scaffold/_form.html.erb diff --git a/config/default.yml b/config/default.yml index 5d2c26239..0ba3d651e 100644 --- a/config/default.yml +++ b/config/default.yml @@ -81,12 +81,15 @@ web: max_threads: 24 admin: initial_superusers: [] + password_length_min: 8 + password_length_max: 72 + email_regex: "\\A[^@\\s]+@[^@\\s]+\\z" + password_regex: auth_strategies: enabled: - - github - - google + - local cas: - options: + options: {} facebook: client_id: client_secret: @@ -97,7 +100,7 @@ web: client_id: client_secret: ldap: - options: + options: {} static_site: host: 127.0.0.1 port: 14013 diff --git a/src/api-umbrella/admin-ui/app/components/admins/index-table.js b/src/api-umbrella/admin-ui/app/components/admins/index-table.js index 1b3057d83..b95c77fef 100644 --- a/src/api-umbrella/admin-ui/app/components/admins/index-table.js +++ b/src/api-umbrella/admin-ui/app/components/admins/index-table.js @@ -14,31 +14,17 @@ export default Ember.Component.extend({ { data: 'username', name: 'Username', - title: 'Username', + title: I18n.t('mongoid.attributes.admin.username'), defaultContent: '-', - render: _.bind(function(email, type, data) { - if(type === 'display' && email && email !== '-') { + render: _.bind(function(username, type, data) { + if(type === 'display' && username && username !== '-') { let link = '#/admins/' + data.id + '/edit'; - return '' + _.escape(email) + ''; + return '' + _.escape(username) + ''; } - return email; + return username; }, this), }, - { - data: 'email', - name: 'E-mail', - title: 'E-mail', - defaultContent: '-', - render: DataTablesHelpers.renderEscaped, - }, - { - data: 'name', - name: 'Name', - title: 'Name', - defaultContent: '-', - render: DataTablesHelpers.renderEscaped, - }, { data: 'group_names', name: 'Groups', diff --git a/src/api-umbrella/admin-ui/app/components/admins/record-form.js b/src/api-umbrella/admin-ui/app/components/admins/record-form.js index 1749b43d6..017071653 100644 --- a/src/api-umbrella/admin-ui/app/components/admins/record-form.js +++ b/src/api-umbrella/admin-ui/app/components/admins/record-form.js @@ -16,6 +16,25 @@ export default Ember.Component.extend(Save, { actions: { submit() { this.saveRecord({ + // If updating the current admin user account, then trigger an + // authentication check after the updating the user. This is because + // Devise requires the user to log back in if they've changed their + // password. + afterSave: (callback) => { + if(this.get('model.id') !== this.get('currentAdmin.id')) { + callback(); + } else { + this.get('session').authenticate('authenticator:devise-server-side').then(() => { + callback(); + }, (error) => { + if(error !== 'unexpected_error') { + window.location.href = '/admin/login'; + } else { + callback(); + } + }); + } + }, transitionToRoute: 'admins', message: 'Successfully saved the admin "' + _.escape(this.get('model.username')) + '"', }); @@ -23,9 +42,9 @@ export default Ember.Component.extend(Save, { delete() { this.destroyRecord({ - prompt: 'Are you sure you want to delete the admin "' + _.escape(this.get('model.name')) + '"?', + prompt: 'Are you sure you want to delete the admin "' + _.escape(this.get('model.username')) + '"?', transitionToRoute: 'admins', - message: 'Successfully deleted the admin "' + _.escape(this.get('model.name')) + '"', + message: 'Successfully deleted the admin "' + _.escape(this.get('model.username')) + '"', }); }, }, diff --git a/src/api-umbrella/admin-ui/app/components/error-messages.js b/src/api-umbrella/admin-ui/app/components/error-messages.js index d7c464234..23028ea89 100644 --- a/src/api-umbrella/admin-ui/app/components/error-messages.js +++ b/src/api-umbrella/admin-ui/app/components/error-messages.js @@ -27,7 +27,7 @@ export default Ember.Component.extend({ if(serverErrors) { if(_.isArray(serverErrors)) { _.each(serverErrors, function(serverError) { - let message = serverError.message; + let message = serverError.full_message || serverError.message; if(!message && serverError.title) { message = serverError.title; if(serverError.status) { @@ -51,18 +51,7 @@ export default Ember.Component.extend({ let messages = []; _.each(errors, function(error) { - let message = ''; - if(error.attribute && error.attribute !== 'base') { - message += inflection.titleize(inflection.underscore(error.attribute)) + ': '; - message += error.message || 'Unexpected error'; - } else { - if(error.message) { - message += error.message.charAt(0).toUpperCase() + error.message.slice(1); - } else { - message += 'Unexpected error'; - } - } - + let message = error.message || 'Unexpected error'; messages.push(marked(message)); }); diff --git a/src/api-umbrella/admin-ui/app/components/form-fields/password-field.js b/src/api-umbrella/admin-ui/app/components/form-fields/password-field.js new file mode 100644 index 000000000..16a921a76 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/components/form-fields/password-field.js @@ -0,0 +1,4 @@ +import BaseField from './base-field'; + +export default BaseField.extend({ +}); diff --git a/src/api-umbrella/admin-ui/app/mixins/save.js b/src/api-umbrella/admin-ui/app/mixins/save.js index f0eb9ec75..c509ff677 100644 --- a/src/api-umbrella/admin-ui/app/mixins/save.js +++ b/src/api-umbrella/admin-ui/app/mixins/save.js @@ -8,6 +8,17 @@ export default Ember.Mixin.create({ $.scrollTo('#error_messages', { offset: -60, duration: 200 }); }, + afterSaveComplete(options, button) { + button.button('reset'); + new PNotify({ + type: 'success', + title: 'Saved', + text: (_.isFunction(options.message)) ? options.message(this.get('model')) : options.message, + }); + + this.get('routing').transitionTo(options.transitionToRoute); + }, + saveRecord(options) { let button = $('#save_button'); button.button('loading'); @@ -23,14 +34,11 @@ export default Ember.Mixin.create({ this.scrollToErrors(); } else { this.get('model').save().then(function() { - button.button('reset'); - new PNotify({ - type: 'success', - title: 'Saved', - text: (_.isFunction(options.message)) ? options.message(this.get('model')) : options.message, - }); - - this.get('routing').transitionTo(options.transitionToRoute); + if(options.afterSave) { + options.afterSave(this.afterSaveComplete.bind(this, options, button)); + } else { + this.afterSaveComplete(options, button); + } }.bind(this), function(error) { // Set the errors from the server response on a "serverErrors" property // for the error-messages component display. diff --git a/src/api-umbrella/admin-ui/app/models/admin.js b/src/api-umbrella/admin-ui/app/models/admin.js index 622ce69a5..3ec221ca2 100644 --- a/src/api-umbrella/admin-ui/app/models/admin.js +++ b/src/api-umbrella/admin-ui/app/models/admin.js @@ -7,8 +7,12 @@ const Validations = buildValidations({ export default DS.Model.extend(Validations, { username: DS.attr(), + password: DS.attr(), + passwordConfirmation: DS.attr(), + currentPassword: DS.attr(), email: DS.attr(), name: DS.attr(), + notes: DS.attr(), superuser: DS.attr(), groupIds: DS.attr({ defaultValue() { return [] } }), signInCount: DS.attr(), diff --git a/src/api-umbrella/admin-ui/app/templates/components/admins/record-form.hbs b/src/api-umbrella/admin-ui/app/templates/components/admins/record-form.hbs index 5b9f2de64..999b1f647 100644 --- a/src/api-umbrella/admin-ui/app/templates/components/admins/record-form.hbs +++ b/src/api-umbrella/admin-ui/app/templates/components/admins/record-form.hbs @@ -5,21 +5,33 @@
User Info - {{f.text-field "username" label="Username"}} - {{f.textarea-field "notes" label="Notes"}} + {{f.text-field "username" label=(t "mongoid.attributes.admin.username")}} {{#if model.email}} - {{f.static-field "email" label="E-mail"}} + {{#if (not-eq (unbound model.email) (unbound model.username))}} + {{f.static-field "email" label=(t "mongoid.attributes.admin.email")}} + {{/if}} {{/if}} {{#if model.name}} - {{f.static-field "name" label="Name"}} + {{f.static-field "name" label=(t "mongoid.attributes.admin.name")}} {{/if}} + {{f.textarea-field "notes" label=(t "mongoid.attributes.admin.notes")}}
+ {{#if model.authenticationToken}} +
+ Change Password + + {{f.password-field "currentPassword" label=(t "mongoid.attributes.admin.current_password")}} + {{f.password-field "password" label=(t "mongoid.attributes.admin.password")}} + {{f.password-field "passwordConfirmation" label=(t "mongoid.attributes.admin.password_confirmation")}} +
+ {{/if}} + {{#if model.authenticationToken}}
Admin API Access - {{#f.static-field "authenticationToken" label="Admin API Token"}} + {{#f.static-field "authenticationToken" label=(t "mongoid.attributes.admin.authentication_token")}} {{model.authenticationToken}} {{/f.static-field}}
@@ -28,9 +40,9 @@
Permissions - {{f.checkboxes-field "groupIds" label="Groups" options=groupOptions}} + {{f.checkboxes-field "groupIds" label=(t "mongoid.attributes.admin.groups") options=groupOptions}} {{#if currentAdmin.superuser}} - {{f.checkbox-field "superuser" label="Superuser"}} + {{f.checkbox-field "superuser" label=(t "mongoid.attributes.admin.superuser")}} {{/if}}
diff --git a/src/api-umbrella/admin-ui/app/templates/components/fields-for.hbs b/src/api-umbrella/admin-ui/app/templates/components/fields-for.hbs index 0623a8f59..90952e761 100644 --- a/src/api-umbrella/admin-ui/app/templates/components/fields-for.hbs +++ b/src/api-umbrella/admin-ui/app/templates/components/fields-for.hbs @@ -6,5 +6,6 @@ selectize-field=(component "form-fields/selectize-field" model=model style=style) text-field=(component "form-fields/text-field" model=model style=style) textarea-field=(component "form-fields/textarea-field" model=model style=style) + password-field=(component "form-fields/password-field" model=model style=style) static-field=(component "form-fields/static-field" model=model style=style) )}} diff --git a/src/api-umbrella/admin-ui/app/templates/components/form-fields/password-field.hbs b/src/api-umbrella/admin-ui/app/templates/components/form-fields/password-field.hbs new file mode 100644 index 000000000..25ee9503b --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/form-fields/password-field.hbs @@ -0,0 +1,3 @@ +{{#form-fields/field-wrapper model=model style=style fieldName=fieldName inputId=inputId label=label tooltip=tooltip}} + +{{/form-fields/field-wrapper}} diff --git a/src/api-umbrella/web-app/Gemfile b/src/api-umbrella/web-app/Gemfile index fdd8bd7b9..29b1e10ad 100644 --- a/src/api-umbrella/web-app/Gemfile +++ b/src/api-umbrella/web-app/Gemfile @@ -58,6 +58,7 @@ gem "elasticsearch", "~> 2.0.0" # OmniAuth-based authentication gem "devise", "~> 4.2.0" +gem "devise_invitable", "~> 1.7.0" gem "omniauth", "~> 1.3.1" gem "omniauth-cas", "~> 1.1.0", :git => "https://github.com/GUI/omniauth-cas.git", :branch => "rexml", :require => false gem "omniauth-facebook", "~> 4.0.0", :require => false @@ -66,7 +67,6 @@ gem "omniauth-facebook", "~> 4.0.0", :require => false gem "omniauth-github", :git => "https://github.com/intridea/omniauth-github.git", :require => false gem "omniauth-google-oauth2", "~> 0.4.1", :require => false gem "omniauth-ldap", "~> 1.0.5", :require => false -gem "omniauth-twitter", "~> 1.2.1", :require => false # Authorization gem "pundit", "~> 1.1.0" @@ -114,6 +114,16 @@ gem "i18n-js", ">= 3.0.0.rc15" # Log to stdout instead of file gem "rails_stdout_logging", "~> 0.0.5", :require => false +# Login forms +gem "simple_form", "~> 3.3.1" + +# Login stylesheets +gem "sass-rails", "~> 5.0" +gem "font-awesome-rails", "~> 4.7.0" +source "https://rails-assets.org" do + gem "rails-assets-bootstrap", "~> 3.3.7" +end + # Bundle gems for the local environment. Make sure to # put test-only gems in this group so their generators # and rake tasks are available in development mode: diff --git a/src/api-umbrella/web-app/Gemfile.lock b/src/api-umbrella/web-app/Gemfile.lock index abb5da2a5..caa3e5f35 100644 --- a/src/api-umbrella/web-app/Gemfile.lock +++ b/src/api-umbrella/web-app/Gemfile.lock @@ -48,6 +48,7 @@ GIT GEM remote: https://rubygems.org/ + remote: https://rails-assets.org/ specs: actionmailer (4.2.7.1) actionpack (= 4.2.7.1) @@ -133,6 +134,9 @@ GEM railties (>= 4.1.0, < 5.1) responders warden (~> 1.2.3) + devise_invitable (1.7.0) + actionmailer (>= 4.0.0) + devise (>= 4.0.0) elasticsearch (2.0.0) elasticsearch-api (= 2.0.0) elasticsearch-transport (= 2.0.0) @@ -144,6 +148,8 @@ GEM erubis (2.7.0) faraday (0.9.2) multipart-post (>= 1.2, < 3) + font-awesome-rails (4.7.0.1) + railties (>= 3.2, < 5.1) globalid (0.3.7) activesupport (>= 4.1.0) hashie (3.4.6) @@ -203,7 +209,6 @@ GEM mini_portile2 (~> 2.1.0) non-stupid-digest-assets (1.0.9) sprockets (>= 2.0) - oauth (0.5.1) oauth2 (1.2.0) faraday (>= 0.8, < 0.10) jwt (~> 1.0) @@ -227,15 +232,9 @@ GEM omniauth (~> 1.0) pyu-ruby-sasl (~> 0.0.3.2) rubyntlm (~> 0.3.4) - omniauth-oauth (1.1.0) - oauth - omniauth (~> 1.0) omniauth-oauth2 (1.4.0) oauth2 (~> 1.0) omniauth (~> 1.2) - omniauth-twitter (1.2.1) - json (~> 1.3) - omniauth-oauth (~> 1.1) origin (2.2.2) orm_adapter (0.5.0) parslet (1.7.1) @@ -270,6 +269,9 @@ GEM bundler (>= 1.3.0, < 2.0) railties (= 4.2.7.1) sprockets-rails + rails-assets-bootstrap (3.3.7) + rails-assets-jquery (>= 1.9.1, < 4) + rails-assets-jquery (3.1.1) rails-deprecated_sanitizer (1.0.3) activesupport (>= 4.2.0.alpha) rails-dom-testing (1.0.7) @@ -293,7 +295,17 @@ GEM multi_json rubyntlm (0.3.4) safe_yaml (1.0.4) + sass (3.4.23) + sass-rails (5.0.6) + railties (>= 4.0.0, < 6) + sass (~> 3.1) + sprockets (>= 2.8, < 4.0) + sprockets-rails (>= 2.0, < 4.0) + tilt (>= 1.1, < 3) sequel (4.41.0) + simple_form (3.3.1) + actionpack (> 4, < 5.1) + activemodel (> 4, < 5.1) sixarm_ruby_unaccent (1.1.1) sprockets (3.7.0) concurrent-ruby (~> 1.0) @@ -307,6 +319,7 @@ GEM net-ssh (>= 2.8.0) thor (0.19.4) thread_safe (0.3.5) + tilt (2.0.5) tzinfo (1.2.2) thread_safe (~> 0.1) unicode_utils (1.4.0) @@ -327,7 +340,9 @@ DEPENDENCIES daemons (~> 1.2.4) delayed_job_mongoid (~> 2.2.0) devise (~> 4.2.0) + devise_invitable (~> 1.7.0) elasticsearch (~> 2.0.0) + font-awesome-rails (~> 4.7.0) http_accept_language (~> 2.1.0) i18n-js (>= 3.0.0.rc15) jbuilder (~> 2.6.0) @@ -350,7 +365,6 @@ DEPENDENCIES omniauth-github! omniauth-google-oauth2 (~> 0.4.1) omniauth-ldap (~> 1.0.5) - omniauth-twitter (~> 1.2.1) premailer-rails (~> 1.9.5) puma (~> 3.6.2) pundit (~> 1.1.0) @@ -358,12 +372,15 @@ DEPENDENCIES rack-proxy (~> 0.6.0) rack-timeout (~> 0.4.2) rails (~> 4.2.7.1) + rails-assets-bootstrap (~> 3.3.7)! rails_stdout_logging (~> 0.0.5) request_store (~> 1.3.1) rollbar (~> 2.13.3) safe_yaml (~> 1.0.4) + sass-rails (~> 5.0) seed-fu! sequel (~> 4.41.0) + simple_form (~> 3.3.1) BUNDLED WITH 1.13.6 diff --git a/src/api-umbrella/web-app/app/assets/stylesheets/admin/login.css b/src/api-umbrella/web-app/app/assets/stylesheets/admin/login.css index f6140e3a1..d64f2c1e2 100644 --- a/src/api-umbrella/web-app/app/assets/stylesheets/admin/login.css +++ b/src/api-umbrella/web-app/app/assets/stylesheets/admin/login.css @@ -1,93 +1,98 @@ /* - *= require vendor/normalize.css + *= require bootstrap/bootstrap + *= require font-awesome */ -* { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} - body { padding: 20px; - font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; +} + +.login .btn { font-size: 16px; } -h1 { - margin: 0px; +.login h1 { text-align: center; + margin: 0px 0px 15px 0px; font-weight: 200; } -.btn { +.login label { + font-weight: normal; +} + +.login, .alert { + max-width: 380px; + margin-left: auto; + margin-right: auto; +} + +.login .btn { display: block; width: 100%; - text-decoration: none; - text-align: center; - cursor: pointer; + padding: 10px; background-color: #07c; color: #fff; - border-width: 0px; - padding: 10px; - border-radius: 5px; + border-style: none; } -.btn:hover { +.login .btn:hover { background-color: #005a9a; } -.btn:disabled, -.btn[disabled]:hover, -.btn[disabled]:focus { - cursor: not-allowed; - filter: alpha(opacity=65); +.login .btn:disabled, +.login .btn[disabled]:hover, +.login .btn[disabled]:focus { background-color: #ddd; color: #666; } -.alert { +.login .login-container { + border: 1px solid #ddd; + border-radius: 2px; padding: 15px; - margin-bottom: 20px; - border: 1px solid transparent; - border-radius: 4px; + margin: 15px 0px; } -.alert-success { - color: #3c763d; - background-color: #dff0d8; - border-color: #d6e9c6; + +.external-login { + margin-top: 20px; + line-height: 24px; } -.alert-info { - color: #31708f; - background-color: #d9edf7; - border-color: #bce8f1; + +.external-login:first-child { + margin-top: 0px; } -.alert-warning { - color: #8a6d3b; - background-color: #fcf8e3; - border-color: #faebcc; + +.external-login .alert { + margin-bottom: 2px; + font-size: 14px; } -.alert-danger { - color: #a94442; - background-color: #f2dede; - border-color: #ebccd1; + +.external-login .btn span { + display: inline-block; } -#login, .alert { - max-width: 360px; - margin-left: auto; - margin-right: auto; +.external-login .btn .fa { + margin-right: 8px; + font-size: 24px; } -.admin-login { - margin: 20px 0px; +.external-login .btn span, +.external-login .btn .fa { + vertical-align: middle; + line-height: 24px; } -.admin-login form { - margin: 0px; +.external-login .btn .fa-developer, +.external-login .btn .fa-cas, +.external-login .btn .fa-ldap { + display: none; } -.admin-login .alert { - margin-bottom: 2px; - font-size: 14px; +.fa-google_oauth2:before { + content: "\f1a0"; +} + +.login .checkbox { + margin: 0px; } diff --git a/src/api-umbrella/web-app/app/assets/stylesheets/vendor/normalize.css b/src/api-umbrella/web-app/app/assets/stylesheets/vendor/normalize.css deleted file mode 100644 index 9b77e0eb4..000000000 --- a/src/api-umbrella/web-app/app/assets/stylesheets/vendor/normalize.css +++ /dev/null @@ -1,461 +0,0 @@ -/*! normalize.css v5.0.0 | MIT License | github.com/necolas/normalize.css */ - -/** - * 1. Change the default font family in all browsers (opinionated). - * 2. Correct the line height in all browsers. - * 3. Prevent adjustments of font size after orientation changes in - * IE on Windows Phone and in iOS. - */ - -/* Document - ========================================================================== */ - -html { - font-family: sans-serif; /* 1 */ - line-height: 1.15; /* 2 */ - -ms-text-size-adjust: 100%; /* 3 */ - -webkit-text-size-adjust: 100%; /* 3 */ -} - -/* Sections - ========================================================================== */ - -/** - * Remove the margin in all browsers (opinionated). - */ - -body { - margin: 0; -} - -/** - * Add the correct display in IE 9-. - */ - -article, -aside, -footer, -header, -nav, -section { - display: block; -} - -/** - * Correct the font size and margin on `h1` elements within `section` and - * `article` contexts in Chrome, Firefox, and Safari. - */ - -h1 { - font-size: 2em; - margin: 0.67em 0; -} - -/* Grouping content - ========================================================================== */ - -/** - * Add the correct display in IE 9-. - * 1. Add the correct display in IE. - */ - -figcaption, -figure, -main { /* 1 */ - display: block; -} - -/** - * Add the correct margin in IE 8. - */ - -figure { - margin: 1em 40px; -} - -/** - * 1. Add the correct box sizing in Firefox. - * 2. Show the overflow in Edge and IE. - */ - -hr { - box-sizing: content-box; /* 1 */ - height: 0; /* 1 */ - overflow: visible; /* 2 */ -} - -/** - * 1. Correct the inheritance and scaling of font size in all browsers. - * 2. Correct the odd `em` font sizing in all browsers. - */ - -pre { - font-family: monospace, monospace; /* 1 */ - font-size: 1em; /* 2 */ -} - -/* Text-level semantics - ========================================================================== */ - -/** - * 1. Remove the gray background on active links in IE 10. - * 2. Remove gaps in links underline in iOS 8+ and Safari 8+. - */ - -a { - background-color: transparent; /* 1 */ - -webkit-text-decoration-skip: objects; /* 2 */ -} - -/** - * Remove the outline on focused links when they are also active or hovered - * in all browsers (opinionated). - */ - -a:active, -a:hover { - outline-width: 0; -} - -/** - * 1. Remove the bottom border in Firefox 39-. - * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. - */ - -abbr[title] { - border-bottom: none; /* 1 */ - text-decoration: underline; /* 2 */ - text-decoration: underline dotted; /* 2 */ -} - -/** - * Prevent the duplicate application of `bolder` by the next rule in Safari 6. - */ - -b, -strong { - font-weight: inherit; -} - -/** - * Add the correct font weight in Chrome, Edge, and Safari. - */ - -b, -strong { - font-weight: bolder; -} - -/** - * 1. Correct the inheritance and scaling of font size in all browsers. - * 2. Correct the odd `em` font sizing in all browsers. - */ - -code, -kbd, -samp { - font-family: monospace, monospace; /* 1 */ - font-size: 1em; /* 2 */ -} - -/** - * Add the correct font style in Android 4.3-. - */ - -dfn { - font-style: italic; -} - -/** - * Add the correct background and color in IE 9-. - */ - -mark { - background-color: #ff0; - color: #000; -} - -/** - * Add the correct font size in all browsers. - */ - -small { - font-size: 80%; -} - -/** - * Prevent `sub` and `sup` elements from affecting the line height in - * all browsers. - */ - -sub, -sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; -} - -sub { - bottom: -0.25em; -} - -sup { - top: -0.5em; -} - -/* Embedded content - ========================================================================== */ - -/** - * Add the correct display in IE 9-. - */ - -audio, -video { - display: inline-block; -} - -/** - * Add the correct display in iOS 4-7. - */ - -audio:not([controls]) { - display: none; - height: 0; -} - -/** - * Remove the border on images inside links in IE 10-. - */ - -img { - border-style: none; -} - -/** - * Hide the overflow in IE. - */ - -svg:not(:root) { - overflow: hidden; -} - -/* Forms - ========================================================================== */ - -/** - * 1. Change the font styles in all browsers (opinionated). - * 2. Remove the margin in Firefox and Safari. - */ - -button, -input, -optgroup, -select, -textarea { - font-family: sans-serif; /* 1 */ - font-size: 100%; /* 1 */ - line-height: 1.15; /* 1 */ - margin: 0; /* 2 */ -} - -/** - * Show the overflow in IE. - * 1. Show the overflow in Edge. - */ - -button, -input { /* 1 */ - overflow: visible; -} - -/** - * Remove the inheritance of text transform in Edge, Firefox, and IE. - * 1. Remove the inheritance of text transform in Firefox. - */ - -button, -select { /* 1 */ - text-transform: none; -} - -/** - * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` - * controls in Android 4. - * 2. Correct the inability to style clickable types in iOS and Safari. - */ - -button, -html [type="button"], /* 1 */ -[type="reset"], -[type="submit"] { - -webkit-appearance: button; /* 2 */ -} - -/** - * Remove the inner border and padding in Firefox. - */ - -button::-moz-focus-inner, -[type="button"]::-moz-focus-inner, -[type="reset"]::-moz-focus-inner, -[type="submit"]::-moz-focus-inner { - border-style: none; - padding: 0; -} - -/** - * Restore the focus styles unset by the previous rule. - */ - -button:-moz-focusring, -[type="button"]:-moz-focusring, -[type="reset"]:-moz-focusring, -[type="submit"]:-moz-focusring { - outline: 1px dotted ButtonText; -} - -/** - * Change the border, margin, and padding in all browsers (opinionated). - */ - -fieldset { - border: 1px solid #c0c0c0; - margin: 0 2px; - padding: 0.35em 0.625em 0.75em; -} - -/** - * 1. Correct the text wrapping in Edge and IE. - * 2. Correct the color inheritance from `fieldset` elements in IE. - * 3. Remove the padding so developers are not caught out when they zero out - * `fieldset` elements in all browsers. - */ - -legend { - box-sizing: border-box; /* 1 */ - color: inherit; /* 2 */ - display: table; /* 1 */ - max-width: 100%; /* 1 */ - padding: 0; /* 3 */ - white-space: normal; /* 1 */ -} - -/** - * 1. Add the correct display in IE 9-. - * 2. Add the correct vertical alignment in Chrome, Firefox, and Opera. - */ - -progress { - display: inline-block; /* 1 */ - vertical-align: baseline; /* 2 */ -} - -/** - * Remove the default vertical scrollbar in IE. - */ - -textarea { - overflow: auto; -} - -/** - * 1. Add the correct box sizing in IE 10-. - * 2. Remove the padding in IE 10-. - */ - -[type="checkbox"], -[type="radio"] { - box-sizing: border-box; /* 1 */ - padding: 0; /* 2 */ -} - -/** - * Correct the cursor style of increment and decrement buttons in Chrome. - */ - -[type="number"]::-webkit-inner-spin-button, -[type="number"]::-webkit-outer-spin-button { - height: auto; -} - -/** - * 1. Correct the odd appearance in Chrome and Safari. - * 2. Correct the outline style in Safari. - */ - -[type="search"] { - -webkit-appearance: textfield; /* 1 */ - outline-offset: -2px; /* 2 */ -} - -/** - * Remove the inner padding and cancel buttons in Chrome and Safari on macOS. - */ - -[type="search"]::-webkit-search-cancel-button, -[type="search"]::-webkit-search-decoration { - -webkit-appearance: none; -} - -/** - * 1. Correct the inability to style clickable types in iOS and Safari. - * 2. Change font properties to `inherit` in Safari. - */ - -::-webkit-file-upload-button { - -webkit-appearance: button; /* 1 */ - font: inherit; /* 2 */ -} - -/* Interactive - ========================================================================== */ - -/* - * Add the correct display in IE 9-. - * 1. Add the correct display in Edge, IE, and Firefox. - */ - -details, /* 1 */ -menu { - display: block; -} - -/* - * Add the correct display in all browsers. - */ - -summary { - display: list-item; -} - -/* Scripting - ========================================================================== */ - -/** - * Add the correct display in IE 9-. - */ - -canvas { - display: inline-block; -} - -/** - * Add the correct display in IE. - */ - -template { - display: none; -} - -/* Hidden - ========================================================================== */ - -/** - * Add the correct display in IE 10-. - */ - -[hidden] { - display: none; -} diff --git a/src/api-umbrella/web-app/app/controllers/admin/admins/omniauth_callbacks_controller.rb b/src/api-umbrella/web-app/app/controllers/admin/admins/omniauth_callbacks_controller.rb index 34d5596a7..ff19b162e 100644 --- a/src/api-umbrella/web-app/app/controllers/admin/admins/omniauth_callbacks_controller.rb +++ b/src/api-umbrella/web-app/app/controllers/admin/admins/omniauth_callbacks_controller.rb @@ -12,21 +12,21 @@ def developer raise "The developer OmniAuth strategy should not be used outside of development or test." end - @email = request.env["omniauth.auth"]["uid"] - @admin = Admin.where(:username => @email).first - @admin ||= Admin.create!(:username => @email, :superuser => true) + @username = request.env["omniauth.auth"]["uid"] + @admin = Admin.find_for_database_authentication(:username => @username) + @admin ||= Admin.create!(:username => @username, :superuser => true) login end def cas - @email = request.env["omniauth.auth"]["uid"] + @username = request.env["omniauth.auth"]["uid"] login end def facebook if(request.env["omniauth.auth"]["info"]["verified"]) - @email = request.env["omniauth.auth"]["info"]["email"] + @username = request.env["omniauth.auth"]["info"]["email"] end login @@ -34,7 +34,7 @@ def facebook def github if(request.env["omniauth.auth"]["info"]["email_verified"]) - @email = request.env["omniauth.auth"]["info"]["email"] + @username = request.env["omniauth.auth"]["info"]["email"] end login @@ -42,7 +42,7 @@ def github def google_oauth2 if(request.env["omniauth.auth"]["extra"]["raw_info"]["email_verified"]) - @email = request.env["omniauth.auth"]["info"]["email"] + @username = request.env["omniauth.auth"]["info"]["email"] end login @@ -51,15 +51,15 @@ def google_oauth2 def ldap uid_field = request.env["omniauth.strategy"].options[:uid] uid = [request.env["omniauth.auth"]["extra"]["raw_info"][uid_field]].flatten.compact.first - @email = uid + @username = uid login end private def login - if(!@admin && @email.present?) - @admin = Admin.where(:username => @email.downcase).first + if(!@admin && @username.present?) + @admin = Admin.find_for_database_authentication(:username => @username) end if @admin @@ -80,7 +80,7 @@ def login else flash[:error] = ActionController::Base.helpers.safe_join([ "The account for '", - @email, + @username, "' is not authorized to access the admin. Please ", ActionController::Base.helpers.content_tag(:a, "contact us", :href => ApiUmbrellaConfig[:contact_url]), " for further assistance.", @@ -90,10 +90,6 @@ def login end end - def signed_in_root_path(resource_or_scope) - admin_path - end - def after_omniauth_failure_path_for(scope) new_admin_session_path end diff --git a/src/api-umbrella/web-app/app/controllers/admin/registrations_controller.rb b/src/api-umbrella/web-app/app/controllers/admin/registrations_controller.rb new file mode 100644 index 000000000..3d2f6f571 --- /dev/null +++ b/src/api-umbrella/web-app/app/controllers/admin/registrations_controller.rb @@ -0,0 +1,21 @@ +class Admin::RegistrationsController < Devise::RegistrationsController + before_action :one_time_setup + + protected + + def build_resource(hash=nil) + super + # Make the first admin a superuser on initial setup. + self.resource.superuser = true + self.resource + end + + private + + def one_time_setup + unless(Admin.needs_first_account?) + flash[:notice] = "An initial admin account already exists." + redirect_to admin_path + end + end +end diff --git a/src/api-umbrella/web-app/app/controllers/admin/sessions_controller.rb b/src/api-umbrella/web-app/app/controllers/admin/sessions_controller.rb index bba4ba9e9..49d5039a5 100644 --- a/src/api-umbrella/web-app/app/controllers/admin/sessions_controller.rb +++ b/src/api-umbrella/web-app/app/controllers/admin/sessions_controller.rb @@ -1,13 +1,7 @@ class Admin::SessionsController < Devise::SessionsController + before_action :one_time_setup skip_after_action :verify_authorized - def new - end - - def after_sign_out_path_for(resource_or_scope) - admin_path - end - def auth response = { "authenticated" => !current_admin.nil?, @@ -25,4 +19,22 @@ def auth format.json { render(:json => response) } end end + + private + + def set_flash_message(key, kind, options = {}) + # Don't set the "signed in" flash message, since we redirect to the Ember + # app after signing in, where flashes won't be displayed (so displaying the + # "signed in" message the next time they get back to the Rails login page + # is confusing). + if(kind != :signed_in) + super(key, kind, options) + end + end + + def one_time_setup + if(Admin.needs_first_account?) + redirect_to new_admin_registration_path + end + end end diff --git a/src/api-umbrella/web-app/app/controllers/api/v1/admins_controller.rb b/src/api-umbrella/web-app/app/controllers/api/v1/admins_controller.rb index 092e1d42d..57331b020 100644 --- a/src/api-umbrella/web-app/app/controllers/api/v1/admins_controller.rb +++ b/src/api-umbrella/web-app/app/controllers/api/v1/admins_controller.rb @@ -55,6 +55,10 @@ def update respond_to do |format| if(@admin.save) + if(current_admin && @admin.id == current_admin.id) + bypass_sign_in(current_admin, :scope => :admin) + end + format.json { render("show", :status => :ok, :location => api_v1_admin_url(@admin)) } else format.json { render(:json => errors_response(@admin), :status => :unprocessable_entity) } @@ -73,14 +77,16 @@ def destroy def save! authorize(@admin) unless(@admin.new_record?) - @admin.assign_attributes(admin_params) + @admin.assign_with_password(admin_params) authorize(@admin) - @admin.save end def admin_params params.require(:admin).permit([ :username, + :password, + :password_confirmation, + :current_password, :email, :name, :notes, diff --git a/src/api-umbrella/web-app/app/controllers/api/v1/base_controller.rb b/src/api-umbrella/web-app/app/controllers/api/v1/base_controller.rb index 68cd6e9d4..b027e3e9c 100644 --- a/src/api-umbrella/web-app/app/controllers/api/v1/base_controller.rb +++ b/src/api-umbrella/web-app/app/controllers/api/v1/base_controller.rb @@ -63,6 +63,7 @@ def errors_response(record) :code => "INVALID_INPUT", :message => message, :field => field, + :full_message => record.errors.full_message(field, message), } end diff --git a/src/api-umbrella/web-app/app/controllers/application_controller.rb b/src/api-umbrella/web-app/app/controllers/application_controller.rb index ce205770e..532ea8ba6 100644 --- a/src/api-umbrella/web-app/app/controllers/application_controller.rb +++ b/src/api-umbrella/web-app/app/controllers/application_controller.rb @@ -105,6 +105,14 @@ def set_time_zone Time.zone = old_time_zone end + def signed_in_root_path(resource_or_scope) + admin_path + end + + def after_sign_out_path_for(resource_or_scope) + admin_path + end + private def set_userstamp diff --git a/src/api-umbrella/web-app/app/models/admin.rb b/src/api-umbrella/web-app/app/models/admin.rb index 36179432e..fdf0f7762 100644 --- a/src/api-umbrella/web-app/app/models/admin.rb +++ b/src/api-umbrella/web-app/app/models/admin.rb @@ -6,24 +6,53 @@ class Admin include Mongoid::Delorean::Trackable # Devise-based authentication using OmniAuth - devise :omniauthable, :trackable + devise :database_authenticatable, + :omniauthable, + :recoverable, + :registerable, + :rememberable, + :trackable, + :lockable, + :invitable # Fields field :_id, :type => String, :overwrite => true, :default => lambda { SecureRandom.uuid } field :username, :type => String - field :email, :type => String field :name, :type => String field :notes, :type => String field :superuser, :type => Boolean field :authentication_token, :type => String field :last_sign_in_provider, :type => String + ## Database authenticatable + field :email, :type => String + field :encrypted_password, :type => String + + ## Recoverable + field :reset_password_token, :type => String + field :reset_password_sent_at, :type => Time + + ## Rememberable + field :remember_created_at, :type => Time + ## Trackable - field :sign_in_count, :type => Integer, :default => 0 + field :sign_in_count, :type => Integer, :default => 0 field :current_sign_in_at, :type => Time - field :last_sign_in_at, :type => Time + field :last_sign_in_at, :type => Time field :current_sign_in_ip, :type => String - field :last_sign_in_ip, :type => String + field :last_sign_in_ip, :type => String + + ## Lockable + field :failed_attempts, :type => Integer, :default => 0 # Only if lock strategy is :failed_attempts + field :unlock_token, :type => String # Only if unlock strategy is :email or :both + field :locked_at, type: Time + + ## Invitable + field :invitation_token, :type => String + field :invitation_created_at, :type => Time + field :invitation_sent_at, :type => Time + field :invitation_accepted_at, :type => Time + field :invitation_limit, :type => Integer # Relations has_and_belongs_to_many :groups, :class_name => "AdminGroup", :inverse_of => nil @@ -37,15 +66,40 @@ class Admin validates :username, :presence => true, :uniqueness => true + validates :username, + :format => Devise.email_regexp, + :if => :username_is_email? + validates :email, + :presence => true, + :format => Devise.email_regexp, + :if => :email_required? + validates :password, + :presence => true, + :confirmation => true, + :length => { :in => Devise.password_length }, + :if => :password_required? + if(ApiUmbrellaConfig[:web][:admin][:password_regex]) + validates :password, + :format => { :with => Regexp.new(ApiUmbrellaConfig[:web][:admin][:password_regex]), :message => :password_format }, + :if => :password_required? + end + validates :password_confirmation, + :presence => true, + :if => :password_required? validate :validate_superuser_or_groups # Callbacks + before_validation :sync_username_and_email before_validation :generate_authentication_token, :on => :create def self.sorted order_by(:username.asc) end + def self.needs_first_account? + self.unscoped.count == 0 + end + def group_names unless @group_names @group_names = self.groups.sorted.map { |group| group.name } @@ -156,8 +210,48 @@ def serializable_hash(options = nil) hash end + def username_is_email? + true + end + + def email_required? + !username_is_email? + end + + def password_required? + password.present? || password_confirmation.present? + end + + def assign_without_password(params, *options) + params.delete(:password) + params.delete(:password_confirmation) + self.assign_attributes(params, *options) + end + + def assign_with_password(params, *options) + current_password = params.delete(:current_password) + + # Don't try to set the password unless it was explicitly set. + if(params[:password].blank?) + params.delete(:password) + params.delete(:password_confirmation) if(params[:password_confirmation].blank?) + end + + self.assign_attributes(params, *options) + if(!valid_password?(current_password)) + self.valid? + self.errors.add(:current_password, current_password.blank? ? :blank : :invalid) + end + end + private + def sync_username_and_email + if(self.username_is_email?) + self.email = self.username + end + end + def generate_authentication_token unless self.authentication_token # Generate a key containing A-Z, a-z, and 0-9 that's 40 chars in diff --git a/src/api-umbrella/web-app/app/views/admin/sessions/new.html.erb b/src/api-umbrella/web-app/app/views/admin/sessions/new.html.erb deleted file mode 100644 index aeaff6e83..000000000 --- a/src/api-umbrella/web-app/app/views/admin/sessions/new.html.erb +++ /dev/null @@ -1,27 +0,0 @@ -
-

Admin Login

- - <% if(!resource_class.omniauth_providers.include?(:developer) && Admin.count == 0) %> -
- No admins currently exist.
- Initial admin accounts may be defined in /etc/api-umbrella/api-umbrella.yml (see the web.admin.initial_superusers section) -
- <% end %> - -
- <%- resource_class.omniauth_providers.each do |provider| %> - - <% end -%> -
-
diff --git a/src/api-umbrella/web-app/app/views/api/v1/admins/show.rabl b/src/api-umbrella/web-app/app/views/api/v1/admins/show.rabl index b24de82ce..534584c65 100644 --- a/src/api-umbrella/web-app/app/views/api/v1/admins/show.rabl +++ b/src/api-umbrella/web-app/app/views/api/v1/admins/show.rabl @@ -3,6 +3,7 @@ attributes :id, :username, :email, :name, + :notes, :superuser, :group_ids, :sign_in_count, diff --git a/src/api-umbrella/web-app/app/views/devise/mailer/confirmation_instructions.html.erb b/src/api-umbrella/web-app/app/views/devise/mailer/confirmation_instructions.html.erb new file mode 100644 index 000000000..dc55f64f6 --- /dev/null +++ b/src/api-umbrella/web-app/app/views/devise/mailer/confirmation_instructions.html.erb @@ -0,0 +1,5 @@ +

Welcome <%= @email %>!

+ +

You can confirm your account email through the link below:

+ +

<%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %>

diff --git a/src/api-umbrella/web-app/app/views/devise/mailer/password_change.html.erb b/src/api-umbrella/web-app/app/views/devise/mailer/password_change.html.erb new file mode 100644 index 000000000..b41daf476 --- /dev/null +++ b/src/api-umbrella/web-app/app/views/devise/mailer/password_change.html.erb @@ -0,0 +1,3 @@ +

Hello <%= @resource.email %>!

+ +

We're contacting you to notify you that your password has been changed.

diff --git a/src/api-umbrella/web-app/app/views/devise/mailer/reset_password_instructions.html.erb b/src/api-umbrella/web-app/app/views/devise/mailer/reset_password_instructions.html.erb new file mode 100644 index 000000000..f667dc12f --- /dev/null +++ b/src/api-umbrella/web-app/app/views/devise/mailer/reset_password_instructions.html.erb @@ -0,0 +1,8 @@ +

Hello <%= @resource.email %>!

+ +

Someone has requested a link to change your password. You can do this through the link below.

+ +

<%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) %>

+ +

If you didn't request this, please ignore this email.

+

Your password won't change until you access the link above and create a new one.

diff --git a/src/api-umbrella/web-app/app/views/devise/mailer/unlock_instructions.html.erb b/src/api-umbrella/web-app/app/views/devise/mailer/unlock_instructions.html.erb new file mode 100644 index 000000000..41e148bf2 --- /dev/null +++ b/src/api-umbrella/web-app/app/views/devise/mailer/unlock_instructions.html.erb @@ -0,0 +1,7 @@ +

Hello <%= @resource.email %>!

+ +

Your account has been locked due to an excessive number of unsuccessful sign in attempts.

+ +

Click the link below to unlock your account:

+ +

<%= link_to 'Unlock my account', unlock_url(@resource, unlock_token: @token) %>

diff --git a/src/api-umbrella/web-app/app/views/devise/passwords/edit.html.erb b/src/api-umbrella/web-app/app/views/devise/passwords/edit.html.erb new file mode 100644 index 000000000..6371e1884 --- /dev/null +++ b/src/api-umbrella/web-app/app/views/devise/passwords/edit.html.erb @@ -0,0 +1,19 @@ +

Change your password

+ + diff --git a/src/api-umbrella/web-app/app/views/devise/passwords/new.html.erb b/src/api-umbrella/web-app/app/views/devise/passwords/new.html.erb new file mode 100644 index 000000000..089023ce3 --- /dev/null +++ b/src/api-umbrella/web-app/app/views/devise/passwords/new.html.erb @@ -0,0 +1,15 @@ +

Forgot your password?

+ + diff --git a/src/api-umbrella/web-app/app/views/devise/registrations/new.html.erb b/src/api-umbrella/web-app/app/views/devise/registrations/new.html.erb new file mode 100644 index 000000000..505d4e0d8 --- /dev/null +++ b/src/api-umbrella/web-app/app/views/devise/registrations/new.html.erb @@ -0,0 +1,21 @@ +

Welcome!

+ +
+ It looks like you're setting up API Umbrella for the first time. Create your first admin account to get started. +
+ + diff --git a/src/api-umbrella/web-app/app/views/devise/sessions/new.html.erb b/src/api-umbrella/web-app/app/views/devise/sessions/new.html.erb new file mode 100644 index 000000000..dbd45fcfe --- /dev/null +++ b/src/api-umbrella/web-app/app/views/devise/sessions/new.html.erb @@ -0,0 +1,60 @@ +

Admin Sign In

+ + diff --git a/src/api-umbrella/web-app/app/views/devise/shared/_links.html.erb b/src/api-umbrella/web-app/app/views/devise/shared/_links.html.erb new file mode 100644 index 000000000..e6a3e4196 --- /dev/null +++ b/src/api-umbrella/web-app/app/views/devise/shared/_links.html.erb @@ -0,0 +1,25 @@ +<%- if controller_name != 'sessions' %> + <%= link_to "Log in", new_session_path(resource_name) %>
+<% end -%> + +<%- if devise_mapping.registerable? && controller_name != 'registrations' %> + <%= link_to "Sign up", new_registration_path(resource_name) %>
+<% end -%> + +<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %> + <%= link_to "Forgot your password?", new_password_path(resource_name) %>
+<% end -%> + +<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %> + <%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %>
+<% end -%> + +<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %> + <%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %>
+<% end -%> + +<%- if devise_mapping.omniauthable? %> + <%- resource_class.omniauth_providers.each do |provider| %> + <%= link_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider) %>
+ <% end -%> +<% end -%> diff --git a/src/api-umbrella/web-app/app/views/devise/unlocks/new.html.erb b/src/api-umbrella/web-app/app/views/devise/unlocks/new.html.erb new file mode 100644 index 000000000..2278259e3 --- /dev/null +++ b/src/api-umbrella/web-app/app/views/devise/unlocks/new.html.erb @@ -0,0 +1,16 @@ +

Resend unlock instructions

+ + diff --git a/src/api-umbrella/web-app/app/views/layouts/application.html.erb b/src/api-umbrella/web-app/app/views/layouts/application.html.erb index ba604a63c..69a8379f2 100644 --- a/src/api-umbrella/web-app/app/views/layouts/application.html.erb +++ b/src/api-umbrella/web-app/app/views/layouts/application.html.erb @@ -8,16 +8,15 @@ <%= csrf_meta_tags %> -
-
- <% flash.each do |flash_type, message| %> -
- <%= message.html_safe %> -
- <% end %> + + <%= yield %>
diff --git a/src/api-umbrella/web-app/config/application.rb b/src/api-umbrella/web-app/config/application.rb index 22cf8ac24..9755ca890 100644 --- a/src/api-umbrella/web-app/config/application.rb +++ b/src/api-umbrella/web-app/config/application.rb @@ -132,7 +132,7 @@ class Application < Rails::Application end end - if(ENV["RAILS_PUBLIC_PATH"].present?) + if(ENV["RAILS_PUBLIC_PATH"].present? && !%w(test development).include?(Rails.env)) config.paths["public"] = ENV["RAILS_PUBLIC_PATH"] end diff --git a/src/api-umbrella/web-app/config/initializers/devise.rb b/src/api-umbrella/web-app/config/initializers/devise.rb index 349c35d05..855e606f5 100644 --- a/src/api-umbrella/web-app/config/initializers/devise.rb +++ b/src/api-umbrella/web-app/config/initializers/devise.rb @@ -12,7 +12,7 @@ # Configure the e-mail address which will be shown in Devise::Mailer, # note that it will be overwritten if you use your own mailer class # with default "from" parameter. - config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com' + config.mailer_sender = "noreply@#{ApiUmbrellaConfig[:web][:default_host]}" # Configure the class responsible to send e-mails. # config.mailer = 'Devise::Mailer' @@ -34,7 +34,7 @@ # session. If you need permissions, you should implement that in a before filter. # You can also supply a hash where the value is a boolean determining whether # or not authentication should be aborted when the value is not present. - # config.authentication_keys = [:email] + config.authentication_keys = [:username] # Configure parameters from the request object used for authentication. Each entry # given should be a request method and it will automatically be passed to the @@ -75,7 +75,7 @@ # It will change confirmation, password recovery and other workflows # to behave the same regardless if the e-mail provided was right or wrong. # Does not affect registerable. - # config.paranoid = true + config.paranoid = true # By default Devise will store the user in session. You can skip storage for # particular strategies by setting this option. @@ -111,7 +111,55 @@ # config.pepper = '4cec288f0a01acf8450902e2fa70c6bec7ae2e6053bbf54c502b70a700020863b46e32aceab69d52df637db8473e55a85c3aca79b6ecc42f8f26091e6024fd92' # Send a notification email when the user's password is changed - # config.send_password_change_notification = false + config.send_password_change_notification = true + + # ==> Configuration for :invitable + # The period the generated invitation token is valid, after + # this period, the invited resource won't be able to accept the invitation. + # When invite_for is 0 (the default), the invitation won't expire. + # config.invite_for = 2.weeks + + # Number of invitations users can send. + # - If invitation_limit is nil, there is no limit for invitations, users can + # send unlimited invitations, invitation_limit column is not used. + # - If invitation_limit is 0, users can't send invitations by default. + # - If invitation_limit n > 0, users can send n invitations. + # You can change invitation_limit column for some users so they can send more + # or less invitations, even with global invitation_limit = 0 + # Default: nil + # config.invitation_limit = 5 + + # The key to be used to check existing users when sending an invitation + # and the regexp used to test it when validate_on_invite is not set. + # config.invite_key = {:email => /\A[^@]+@[^@]+\z/} + # config.invite_key = {:email => /\A[^@]+@[^@]+\z/, :username => nil} + + # Flag that force a record to be valid before being actually invited + # Default: false + # config.validate_on_invite = true + + # Resend invitation if user with invited status is invited again + # Default: true + # config.resend_invitation = false + + # The class name of the inviting model. If this is nil, + # the #invited_by association is declared to be polymorphic. + # Default: nil + # config.invited_by_class_name = 'User' + + # The foreign key to the inviting model (if invited_by_class_name is set) + # Default: :invited_by_id + # config.invited_by_foreign_key = :invited_by_id + + # The column name used for counter_cache column. If this is nil, + # the #invited_by association is declared without counter_cache. + # Default: nil + # config.invited_by_counter_cache = :invitations_count + + # Auto-login after the user accepts the invite. If this is false, + # the user will need to manually log in after accepting the invite. + # Default: true + # config.allow_insecure_sign_in_after_accept = false # ==> Configuration for :confirmable # A period that the user is allowed to access the website even without @@ -127,7 +175,7 @@ # their account can't be confirmed with the token any more. # Default is nil, meaning there is no restriction on how long a user can take # before confirming their account. - # config.confirm_within = 3.days + config.confirm_within = 3.days # If true, requires any email changes to be confirmed (exactly the same way as # initial account confirmation) to be applied. Requires additional unconfirmed_email @@ -154,12 +202,12 @@ # ==> Configuration for :validatable # Range for password length. - config.password_length = 6..128 + config.password_length = ApiUmbrellaConfig[:web][:admin][:password_length_min]..ApiUmbrellaConfig[:web][:admin][:password_length_max] # Email regex used to validate email formats. It simply asserts that # one (and only one) @ exists in the given string. This is mainly # to give user feedback and not to assert the e-mail validity. - config.email_regexp = /\A[^@\s]+@[^@\s]+\z/ + config.email_regexp = Regexp.new(ApiUmbrellaConfig[:web][:admin][:email_regex]) # ==> Configuration for :timeoutable # The time you want to timeout the user session without activity. After this @@ -170,7 +218,7 @@ # Defines which strategy will be used to lock an account. # :failed_attempts = Locks an account after a number of failed attempts to sign in. # :none = No lock strategy. You should handle locking by yourself. - # config.lock_strategy = :failed_attempts + config.lock_strategy = :failed_attempts # Defines which key will be used when locking and unlocking an account # config.unlock_keys = [:email] @@ -180,14 +228,14 @@ # :time = Re-enables login after a certain amount of time (see :unlock_in below) # :both = Enables both strategies # :none = No unlock strategy. You should handle unlocking by yourself. - # config.unlock_strategy = :both + config.unlock_strategy = :both # Number of authentication tries before locking an account if lock_strategy # is failed attempts. - # config.maximum_attempts = 20 + config.maximum_attempts = 10 # Time interval to unlock the account if :time is enabled as unlock_strategy. - # config.unlock_in = 1.hour + config.unlock_in = 2.hours # Warn on the last attempt before the account is locked. # config.last_attempt_warning = true @@ -250,7 +298,7 @@ # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo' if(%w(development test).include?(Rails.env)) config.omniauth :developer, - :fields => [:email] + :fields => [:username] end ApiUmbrellaConfig[:web][:admin][:auth_strategies][:enabled].each do |strategy| @@ -289,6 +337,8 @@ :service_validate_url => "/cas/serviceValidate", :logout_url => "/cas/logout", :ssl => true + when "local" + # Ignore else raise "Unknown authentication strategy enabled in config: #{strategy.inspect}" end diff --git a/src/api-umbrella/web-app/config/initializers/simple_form.rb b/src/api-umbrella/web-app/config/initializers/simple_form.rb new file mode 100644 index 000000000..5a69fcfa0 --- /dev/null +++ b/src/api-umbrella/web-app/config/initializers/simple_form.rb @@ -0,0 +1,165 @@ +# Use this setup block to configure all options available in SimpleForm. +SimpleForm.setup do |config| + # Wrappers are used by the form builder to generate a + # complete input. You can remove any component from the + # wrapper, change the order or even add your own to the + # stack. The options given below are used to wrap the + # whole input. + config.wrappers :default, class: :input, + hint_class: :field_with_hint, error_class: :field_with_errors do |b| + ## Extensions enabled by default + # Any of these extensions can be disabled for a + # given input by passing: `f.input EXTENSION_NAME => false`. + # You can make any of these extensions optional by + # renaming `b.use` to `b.optional`. + + # Determines whether to use HTML5 (:email, :url, ...) + # and required attributes + b.use :html5 + + # Calculates placeholders automatically from I18n + # You can also pass a string as f.input placeholder: "Placeholder" + b.use :placeholder + + ## Optional extensions + # They are disabled unless you pass `f.input EXTENSION_NAME => true` + # to the input. If so, they will retrieve the values from the model + # if any exists. If you want to enable any of those + # extensions by default, you can change `b.optional` to `b.use`. + + # Calculates maxlength from length validations for string inputs + b.optional :maxlength + + # Calculates pattern from format validations for string inputs + b.optional :pattern + + # Calculates min and max from length validations for numeric inputs + b.optional :min_max + + # Calculates readonly automatically from readonly attributes + b.optional :readonly + + ## Inputs + b.use :label_input + b.use :hint, wrap_with: { tag: :span, class: :hint } + b.use :error, wrap_with: { tag: :span, class: :error } + + ## full_messages_for + # If you want to display the full error message for the attribute, you can + # use the component :full_error, like: + # + # b.use :full_error, wrap_with: { tag: :span, class: :error } + end + + # The default wrapper to be used by the FormBuilder. + config.default_wrapper = :default + + # Define the way to render check boxes / radio buttons with labels. + # Defaults to :nested for bootstrap config. + # inline: input + label + # nested: label > input + config.boolean_style = :nested + + # Default class for buttons + config.button_class = 'btn' + + # Method used to tidy up errors. Specify any Rails Array method. + # :first lists the first message for each field. + # Use :to_sentence to list all errors for each field. + # config.error_method = :first + + # Default tag used for error notification helper. + config.error_notification_tag = :div + + # CSS class to add for error notification helper. + config.error_notification_class = 'error_notification' + + # ID to add for error notification helper. + # config.error_notification_id = nil + + # Series of attempts to detect a default label method for collection. + # config.collection_label_methods = [ :to_label, :name, :title, :to_s ] + + # Series of attempts to detect a default value method for collection. + # config.collection_value_methods = [ :id, :to_s ] + + # You can wrap a collection of radio/check boxes in a pre-defined tag, defaulting to none. + # config.collection_wrapper_tag = nil + + # You can define the class to use on all collection wrappers. Defaulting to none. + # config.collection_wrapper_class = nil + + # You can wrap each item in a collection of radio/check boxes with a tag, + # defaulting to :span. + # config.item_wrapper_tag = :span + + # You can define a class to use in all item wrappers. Defaulting to none. + # config.item_wrapper_class = nil + + # How the label text should be generated altogether with the required text. + config.label_text = lambda { |label, required, explicit_label| "#{label}" } + + # You can define the class to use on all labels. Default is nil. + # config.label_class = nil + + # You can define the default class to be used on forms. Can be overriden + # with `html: { :class }`. Defaulting to none. + # config.default_form_class = nil + + # You can define which elements should obtain additional classes + # config.generate_additional_classes_for = [:wrapper, :label, :input] + + # Whether attributes are required by default (or not). Default is true. + # config.required_by_default = true + + # Tell browsers whether to use the native HTML5 validations (novalidate form option). + # These validations are enabled in SimpleForm's internal config but disabled by default + # in this configuration, which is recommended due to some quirks from different browsers. + # To stop SimpleForm from generating the novalidate option, enabling the HTML5 validations, + # change this configuration to true. + config.browser_validations = false + + # Collection of methods to detect if a file type was given. + # config.file_methods = [ :mounted_as, :file?, :public_filename ] + + # Custom mappings for input types. This should be a hash containing a regexp + # to match as key, and the input type that will be used when the field name + # matches the regexp as value. + # config.input_mappings = { /count/ => :integer } + + # Custom wrappers for input types. This should be a hash containing an input + # type as key and the wrapper that will be used for all inputs with specified type. + # config.wrapper_mappings = { string: :prepend } + + # Namespaces where SimpleForm should look for custom input classes that + # override default inputs. + # config.custom_inputs_namespaces << "CustomInputs" + + # Default priority for time_zone inputs. + # config.time_zone_priority = nil + + # Default priority for country inputs. + # config.country_priority = nil + + # When false, do not use translations for labels. + # config.translate_labels = true + + # Automatically discover new inputs in Rails' autoload path. + # config.inputs_discovery = true + + # Cache SimpleForm inputs discovery + # config.cache_discovery = !Rails.env.development? + + # Default class for inputs + # config.input_class = nil + + # Define the default class of the input wrapper of the boolean input. + config.boolean_label_class = 'checkbox' + + # Defines if the default input wrapper class should be included in radio + # collection wrappers. + # config.include_default_input_wrapper_class = true + + # Defines which i18n scope will be used in Simple Form. + # config.i18n_scope = 'simple_form' +end diff --git a/src/api-umbrella/web-app/config/initializers/simple_form_bootstrap.rb b/src/api-umbrella/web-app/config/initializers/simple_form_bootstrap.rb new file mode 100644 index 000000000..109d29a37 --- /dev/null +++ b/src/api-umbrella/web-app/config/initializers/simple_form_bootstrap.rb @@ -0,0 +1,149 @@ +# Use this setup block to configure all options available in SimpleForm. +SimpleForm.setup do |config| + config.error_notification_class = 'alert alert-danger' + config.button_class = 'btn btn-default' + config.boolean_label_class = nil + + config.wrappers :vertical_form, tag: 'div', class: 'form-group', error_class: 'has-error' do |b| + b.use :html5 + b.use :placeholder + b.optional :maxlength + b.optional :pattern + b.optional :min_max + b.optional :readonly + b.use :label, class: 'control-label' + + b.use :input, class: 'form-control' + b.use :error, wrap_with: { tag: 'span', class: 'help-block' } + b.use :hint, wrap_with: { tag: 'p', class: 'help-block' } + end + + config.wrappers :vertical_file_input, tag: 'div', class: 'form-group', error_class: 'has-error' do |b| + b.use :html5 + b.use :placeholder + b.optional :maxlength + b.optional :readonly + b.use :label, class: 'control-label' + + b.use :input + b.use :error, wrap_with: { tag: 'span', class: 'help-block' } + b.use :hint, wrap_with: { tag: 'p', class: 'help-block' } + end + + config.wrappers :vertical_boolean, tag: 'div', class: 'form-group', error_class: 'has-error' do |b| + b.use :html5 + b.optional :readonly + + b.wrapper tag: 'div', class: 'checkbox' do |ba| + ba.use :label_input + end + + b.use :error, wrap_with: { tag: 'span', class: 'help-block' } + b.use :hint, wrap_with: { tag: 'p', class: 'help-block' } + end + + config.wrappers :vertical_radio_and_checkboxes, tag: 'div', class: 'form-group', error_class: 'has-error' do |b| + b.use :html5 + b.optional :readonly + b.use :label, class: 'control-label' + b.use :input + b.use :error, wrap_with: { tag: 'span', class: 'help-block' } + b.use :hint, wrap_with: { tag: 'p', class: 'help-block' } + end + + config.wrappers :horizontal_form, tag: 'div', class: 'form-group', error_class: 'has-error' do |b| + b.use :html5 + b.use :placeholder + b.optional :maxlength + b.optional :pattern + b.optional :min_max + b.optional :readonly + b.use :label, class: 'col-sm-3 control-label' + + b.wrapper tag: 'div', class: 'col-sm-9' do |ba| + ba.use :input, class: 'form-control' + ba.use :error, wrap_with: { tag: 'span', class: 'help-block' } + ba.use :hint, wrap_with: { tag: 'p', class: 'help-block' } + end + end + + config.wrappers :horizontal_file_input, tag: 'div', class: 'form-group', error_class: 'has-error' do |b| + b.use :html5 + b.use :placeholder + b.optional :maxlength + b.optional :readonly + b.use :label, class: 'col-sm-3 control-label' + + b.wrapper tag: 'div', class: 'col-sm-9' do |ba| + ba.use :input + ba.use :error, wrap_with: { tag: 'span', class: 'help-block' } + ba.use :hint, wrap_with: { tag: 'p', class: 'help-block' } + end + end + + config.wrappers :horizontal_boolean, tag: 'div', class: 'form-group', error_class: 'has-error' do |b| + b.use :html5 + b.optional :readonly + + b.wrapper tag: 'div', class: 'col-sm-offset-3 col-sm-9' do |wr| + wr.wrapper tag: 'div', class: 'checkbox' do |ba| + ba.use :label_input + end + + wr.use :error, wrap_with: { tag: 'span', class: 'help-block' } + wr.use :hint, wrap_with: { tag: 'p', class: 'help-block' } + end + end + + config.wrappers :horizontal_radio_and_checkboxes, tag: 'div', class: 'form-group', error_class: 'has-error' do |b| + b.use :html5 + b.optional :readonly + + b.use :label, class: 'col-sm-3 control-label' + + b.wrapper tag: 'div', class: 'col-sm-9' do |ba| + ba.use :input + ba.use :error, wrap_with: { tag: 'span', class: 'help-block' } + ba.use :hint, wrap_with: { tag: 'p', class: 'help-block' } + end + end + + config.wrappers :inline_form, tag: 'div', class: 'form-group', error_class: 'has-error' do |b| + b.use :html5 + b.use :placeholder + b.optional :maxlength + b.optional :pattern + b.optional :min_max + b.optional :readonly + b.use :label, class: 'sr-only' + + b.use :input, class: 'form-control' + b.use :error, wrap_with: { tag: 'span', class: 'help-block' } + b.use :hint, wrap_with: { tag: 'p', class: 'help-block' } + end + + config.wrappers :multi_select, tag: 'div', class: 'form-group', error_class: 'has-error' do |b| + b.use :html5 + b.optional :readonly + b.use :label, class: 'control-label' + b.wrapper tag: 'div', class: 'form-inline' do |ba| + ba.use :input, class: 'form-control' + ba.use :error, wrap_with: { tag: 'span', class: 'help-block' } + ba.use :hint, wrap_with: { tag: 'p', class: 'help-block' } + end + end + # Wrappers for forms and inputs using the Bootstrap toolkit. + # Check the Bootstrap docs (http://getbootstrap.com) + # to learn about the different styles for forms and inputs, + # buttons and other elements. + config.default_wrapper = :vertical_form + config.wrapper_mappings = { + check_boxes: :vertical_radio_and_checkboxes, + radio_buttons: :vertical_radio_and_checkboxes, + file: :vertical_file_input, + boolean: :vertical_boolean, + datetime: :multi_select, + date: :multi_select, + time: :multi_select + } +end diff --git a/src/api-umbrella/web-app/config/locales/devise_invitable.en.yml b/src/api-umbrella/web-app/config/locales/devise_invitable.en.yml new file mode 100644 index 000000000..2d6750dfe --- /dev/null +++ b/src/api-umbrella/web-app/config/locales/devise_invitable.en.yml @@ -0,0 +1,31 @@ +en: + devise: + failure: + invited: "You have a pending invitation, accept it to finish creating your account." + invitations: + send_instructions: "An invitation email has been sent to %{email}." + invitation_token_invalid: "The invitation token provided is not valid!" + updated: "Your password was set successfully. You are now signed in." + updated_not_active: "Your password was set successfully." + no_invitations_remaining: "No invitations remaining" + invitation_removed: "Your invitation was removed." + new: + header: "Send invitation" + submit_button: "Send an invitation" + edit: + header: "Set your password" + submit_button: "Set my password" + mailer: + invitation_instructions: + subject: "Invitation instructions" + hello: "Hello %{email}" + someone_invited_you: "Someone has invited you to %{url}, you can accept it through the link below." + accept: "Accept invitation" + accept_until: "This invitation will be due in %{due_date}." + ignore: "If you don't want to accept the invitation, please ignore this email.
\nYour account won't be created until you access the link above and set your password." + time: + formats: + devise: + mailer: + invitation_instructions: + accept_until_format: "%B %d, %Y %I:%M %p" diff --git a/src/api-umbrella/web-app/config/locales/en.yml b/src/api-umbrella/web-app/config/locales/en.yml index cc4eb0183..8349ed235 100644 --- a/src/api-umbrella/web-app/config/locales/en.yml +++ b/src/api-umbrella/web-app/config/locales/en.yml @@ -11,6 +11,7 @@ en: messages: invalid_host_format: must be in the format of "example.com" invalid_url_prefix_format: must start with "/" + password_format: "must contain at least one lowercase letter, one uppercase letter, and one number" admin_not_authorized: |- You are not authorized to perform this action. You are only authorized to perform actions for APIs in the following areas: @@ -19,6 +20,17 @@ en: Contact your API Umbrella administrator if you need access to new APIs. mongoid: attributes: + admin: + username: Email + email: Email + password: Password + password_confirmation: Password Confirmation + current_password: Current Password + name: Name + notes: Notes + authentication_token: Admin API Token + groups: Groups + superuser: Superuser api: backend_host: Backend Host backend_protocol: Backend Protocol @@ -69,7 +81,7 @@ en: dashboard: Dashboard filter_logs: Filter Logs import_export: Import/Export - logout: Logout + logout: Sign out my_account: My Account permissions_management: Permissions Management publish_changes: Publish Changes diff --git a/src/api-umbrella/web-app/config/locales/simple_form.en.yml b/src/api-umbrella/web-app/config/locales/simple_form.en.yml new file mode 100644 index 000000000..237438334 --- /dev/null +++ b/src/api-umbrella/web-app/config/locales/simple_form.en.yml @@ -0,0 +1,31 @@ +en: + simple_form: + "yes": 'Yes' + "no": 'No' + required: + text: 'required' + mark: '*' + # You can uncomment the line below if you need to overwrite the whole required html. + # When using html, text and mark won't be used. + # html: '*' + error_notification: + default_message: "Please review the problems below:" + # Examples + # labels: + # defaults: + # password: 'Password' + # user: + # new: + # email: 'E-mail to sign in.' + # edit: + # email: 'E-mail.' + # hints: + # defaults: + # username: 'User name to sign in.' + # password: 'No special characters, please.' + # include_blanks: + # defaults: + # age: 'Rather not say' + # prompts: + # defaults: + # age: 'Select your age' diff --git a/src/api-umbrella/web-app/config/routes.rb b/src/api-umbrella/web-app/config/routes.rb index aa47190e9..06ae51b50 100644 --- a/src/api-umbrella/web-app/config/routes.rb +++ b/src/api-umbrella/web-app/config/routes.rb @@ -58,12 +58,30 @@ end end - devise_for :admins, :controllers => { :omniauth_callbacks => "admin/admins/omniauth_callbacks" } - + devise_for :admins, + :skip => [ + :sessions, + :registrations, + ], + :path_names => { + :sign_in => "login", + :sign_out => "logout", + }, + :controllers => { + :omniauth_callbacks => "admin/admins/omniauth_callbacks", + } devise_scope :admin do get "/admin/login" => "admin/sessions#new", :as => :new_admin_session + post "/admin/login" => "admin/sessions#create", :as => :admin_session delete "/admin/logout" => "admin/sessions#destroy", :as => :destroy_admin_session get "/admin/auth" => "admin/sessions#auth" + + resource :registration, + :only => [:new, :create], + :path => "admins", + :path_names => { :new => "signup" }, + :controller => "admin/registrations", + :as => :admin_registration end namespace :admin do diff --git a/src/api-umbrella/web-app/lib/templates/erb/scaffold/_form.html.erb b/src/api-umbrella/web-app/lib/templates/erb/scaffold/_form.html.erb new file mode 100644 index 000000000..201a069e2 --- /dev/null +++ b/src/api-umbrella/web-app/lib/templates/erb/scaffold/_form.html.erb @@ -0,0 +1,13 @@ +<%%= simple_form_for(@<%= singular_table_name %>) do |f| %> + <%%= f.error_notification %> + +
+ <%- attributes.each do |attribute| -%> + <%%= f.<%= attribute.reference? ? :association : :input %> :<%= attribute.name %> %> + <%- end -%> +
+ +
+ <%%= f.button :submit %> +
+<%% end %> From 70b7663c3d57005b87a7dc4ddf53fe2b3789ed9d Mon Sep 17 00:00:00 2001 From: Nick Muerdter Date: Wed, 4 Jan 2017 21:51:46 -0700 Subject: [PATCH 03/26] Fix so the admin doesn't get logged out when they change their password. --- .../app/components/admins/record-form.js | 19 ------------------- .../controllers/api/v1/admins_controller.rb | 5 ++++- src/api-umbrella/web-app/config/routes.rb | 6 ++++++ 3 files changed, 10 insertions(+), 20 deletions(-) diff --git a/src/api-umbrella/admin-ui/app/components/admins/record-form.js b/src/api-umbrella/admin-ui/app/components/admins/record-form.js index 017071653..f801350fd 100644 --- a/src/api-umbrella/admin-ui/app/components/admins/record-form.js +++ b/src/api-umbrella/admin-ui/app/components/admins/record-form.js @@ -16,25 +16,6 @@ export default Ember.Component.extend(Save, { actions: { submit() { this.saveRecord({ - // If updating the current admin user account, then trigger an - // authentication check after the updating the user. This is because - // Devise requires the user to log back in if they've changed their - // password. - afterSave: (callback) => { - if(this.get('model.id') !== this.get('currentAdmin.id')) { - callback(); - } else { - this.get('session').authenticate('authenticator:devise-server-side').then(() => { - callback(); - }, (error) => { - if(error !== 'unexpected_error') { - window.location.href = '/admin/login'; - } else { - callback(); - } - }); - } - }, transitionToRoute: 'admins', message: 'Successfully saved the admin "' + _.escape(this.get('model.username')) + '"', }); diff --git a/src/api-umbrella/web-app/app/controllers/api/v1/admins_controller.rb b/src/api-umbrella/web-app/app/controllers/api/v1/admins_controller.rb index 57331b020..e422693d9 100644 --- a/src/api-umbrella/web-app/app/controllers/api/v1/admins_controller.rb +++ b/src/api-umbrella/web-app/app/controllers/api/v1/admins_controller.rb @@ -55,8 +55,11 @@ def update respond_to do |format| if(@admin.save) + # If a user is updating themselves, make sure they remain signed in. + # This eliminates the current user getting logged out if they change + # their password. if(current_admin && @admin.id == current_admin.id) - bypass_sign_in(current_admin, :scope => :admin) + bypass_sign_in(@admin, :scope => :admin) end format.json { render("show", :status => :ok, :location => api_v1_admin_url(@admin)) } diff --git a/src/api-umbrella/web-app/config/routes.rb b/src/api-umbrella/web-app/config/routes.rb index 06ae51b50..0b5ceb72f 100644 --- a/src/api-umbrella/web-app/config/routes.rb +++ b/src/api-umbrella/web-app/config/routes.rb @@ -130,4 +130,10 @@ ], ] } + + # Add a dummy /admin/ route. This URL actually gets routed to the Ember.js + # app, not the Rails app, but we create this dummy route so we have the Rails + # "admin_path" and "admin_url" URL helpers available (for redirecting to the + # root of the admin). + get "/admin/", :to => proc { [200, {}, ["OK"]] } end From 059fe5b7a063b3357bee7ca4b56cb6d5aac23fc5 Mon Sep 17 00:00:00 2001 From: Nick Muerdter Date: Sun, 8 Jan 2017 20:57:55 -0700 Subject: [PATCH 04/26] Try to fix memory leak with delayed job running in development mode. --- .../web-app/config/environments/development.rb | 9 ++++++++- templates/etc/perp/web-delayed-job/rc.env.mustache | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/api-umbrella/web-app/config/environments/development.rb b/src/api-umbrella/web-app/config/environments/development.rb index 0feba2ef1..d914d84c1 100644 --- a/src/api-umbrella/web-app/config/environments/development.rb +++ b/src/api-umbrella/web-app/config/environments/development.rb @@ -4,7 +4,14 @@ # In the development environment your application's code is reloaded on # every request. This slows down response time but is perfect for development # since you don't have to restart the web server when you make code changes. - config.cache_classes = false + if(ENV["DELAYED_JOB"] == "true") + # Cache classes for delayed job in development, since otherwise its memory + # seems to get out of control: + # https://github.com/collectiveidea/delayed_job/issues/823 + config.cache_classes = true + else + config.cache_classes = false + end # Do not eager load code on boot. config.eager_load = false diff --git a/templates/etc/perp/web-delayed-job/rc.env.mustache b/templates/etc/perp/web-delayed-job/rc.env.mustache index b48a237d5..286406fc1 100644 --- a/templates/etc/perp/web-delayed-job/rc.env.mustache +++ b/templates/etc/perp/web-delayed-job/rc.env.mustache @@ -1,4 +1,5 @@ RAILS_ENV={{app_env}} +DELAYED_JOB=true API_UMBRELLA_RUNTIME_CONFIG={{_api_umbrella_config_runtime_file}} RAILS_LOG_TO_STDOUT=true RAILS_TMP_PATH={{tmp_dir}}/web-app From 58e43c2f15d965a676b61db07ca0171b335cb1ab Mon Sep 17 00:00:00 2001 From: Nick Muerdter Date: Sun, 8 Jan 2017 21:35:24 -0700 Subject: [PATCH 05/26] Begin to adjust tests for local login process. --- Gemfile | 3 + Gemfile.lock | 2 + config/test.yml | 15 +-- .../admins/omniauth_callbacks_controller.rb | 14 ++- .../omniauth_custom_forms_controller.rb | 7 ++ .../admin/registrations_controller.rb | 6 +- .../controllers/admin/sessions_controller.rb | 4 +- .../app/controllers/application_controller.rb | 2 +- src/api-umbrella/web-app/app/models/admin.rb | 4 +- .../omniauth_custom_forms/developer.html.erb | 21 ++++ .../app/views/devise/sessions/new.html.erb | 1 - .../web-app/config/environments/test.rb | 2 +- .../web-app/config/initializers/devise.rb | 8 +- .../config/initializers/simple_form.rb | 10 +- .../initializers/simple_form_bootstrap.rb | 112 +++++++++--------- .../initializers => lib/mongoid}/userstamp.rb | 0 .../perp/test-env-mailhog/rc.main.mustache | 6 +- .../test_external_providers.rb} | 100 +++++++++------- test/admin_ui/login/test_first_time_setup.rb | 88 ++++++++++++++ test/admin_ui/login/test_forgot_password.rb | 77 ++++++++++++ .../test_local_and_external_providers.rb | 57 +++++++++ test/admin_ui/login/test_local_provider.rb | 75 ++++++++++++ test/admin_ui/test_api_users_welcome_email.rb | 2 +- test/apis/v1/admins/test_admin_permissions.rb | 2 +- test/apis/v1/admins/test_create.rb | 2 +- test/apis/v1/admins/test_index.rb | 2 +- .../apis/v1/users/test_create_notify_email.rb | 2 +- .../v1/users/test_create_welcome_email.rb | 2 +- test/apis/v1/users/test_permissions.rb | 2 +- test/apis/v1/users/test_role_permissions.rb | 2 +- test/factories/admins.rb | 3 +- .../api_umbrella_test_helpers/admin_auth.rb | 80 +++++++++++-- .../api_umbrella_test_helpers/delayed_job.rb | 2 +- .../api_umbrella_test_helpers/process.rb | 2 +- test/support/models/admin.rb | 20 +++- test/support/test_namespaces.rb | 4 +- 36 files changed, 579 insertions(+), 162 deletions(-) create mode 100644 src/api-umbrella/web-app/app/controllers/admin/admins/omniauth_custom_forms_controller.rb create mode 100644 src/api-umbrella/web-app/app/views/admin/admins/omniauth_custom_forms/developer.html.erb rename src/api-umbrella/web-app/{config/initializers => lib/mongoid}/userstamp.rb (100%) rename test/admin_ui/{test_login.rb => login/test_external_providers.rb} (73%) create mode 100644 test/admin_ui/login/test_first_time_setup.rb create mode 100644 test/admin_ui/login/test_forgot_password.rb create mode 100644 test/admin_ui/login/test_local_and_external_providers.rb create mode 100644 test/admin_ui/login/test_local_provider.rb diff --git a/Gemfile b/Gemfile index 7b0837ec4..df69587ec 100644 --- a/Gemfile +++ b/Gemfile @@ -77,6 +77,9 @@ gem "concurrent-ruby", "~> 1.0.2" # Time zone randomization for tests. gem "zonebie", "~> 0.6.1" +# Encrypting admin passwords. +gem "bcrypt", "~> 3.1.11" + # Color output gem "rainbow", "~> 2.1.0" diff --git a/Gemfile.lock b/Gemfile.lock index 8e62afaee..0f6eb548b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -26,6 +26,7 @@ GEM descendants_tracker (~> 0.0.4) ice_nine (~> 0.11.0) thread_safe (~> 0.3, >= 0.3.1) + bcrypt (3.1.11) bson (4.2.0) builder (3.2.2) capybara (2.11.0) @@ -151,6 +152,7 @@ DEPENDENCIES activesupport (~> 5.0.0) addressable (~> 2.5.0) awesome_print (~> 1.7.0) + bcrypt (~> 3.1.11) capybara (~> 2.11.0) capybara-screenshot (~> 1.0.14) childprocess (~> 0.5.9) diff --git a/config/test.yml b/config/test.yml index d7076afdc..a56dfea58 100644 --- a/config/test.yml +++ b/config/test.yml @@ -36,15 +36,7 @@ api_server: web: port: 13012 admin: - initial_superusers: - - initial.admin@example.com auth_strategies: - enabled: - - facebook - - github - - google - - ldap - - max.gov facebook: client_id: test_fake client_secret: test_fake @@ -105,8 +97,9 @@ unbound: port: 13100 control_port: 13101 mailhog: - smtp_bind_addr: "127.0.0.1:13102" - api_bind_addr: "127.0.0.1:13103" - ui_bind_addr: "127.0.0.1:13103" + bind_addr: "127.0.0.1" + smtp_port: 13102 + api_port: 13103 + ui_port: 13103 apiSettings: require_https: optional diff --git a/src/api-umbrella/web-app/app/controllers/admin/admins/omniauth_callbacks_controller.rb b/src/api-umbrella/web-app/app/controllers/admin/admins/omniauth_callbacks_controller.rb index ff19b162e..cd704c15c 100644 --- a/src/api-umbrella/web-app/app/controllers/admin/admins/omniauth_callbacks_controller.rb +++ b/src/api-umbrella/web-app/app/controllers/admin/admins/omniauth_callbacks_controller.rb @@ -1,22 +1,24 @@ class Admin::Admins::OmniauthCallbacksController < Devise::OmniauthCallbacksController skip_after_action :verify_authorized - # The developer strategy doesn't include the CSRF token in the form: - # https://github.com/omniauth/omniauth/pull/674 - skip_before_action :verify_authenticity_token, :only => :developer - # For the developer strategy, simply find or create a new admin account with # whatever login details they give. This is not for use on production. def developer - unless(%w(development test).include?(Rails.env)) + unless(Rails.env == "development") raise "The developer OmniAuth strategy should not be used outside of development or test." end @username = request.env["omniauth.auth"]["uid"] @admin = Admin.find_for_database_authentication(:username => @username) - @admin ||= Admin.create!(:username => @username, :superuser => true) + unless(@admin) + @admin = Admin.new(:username => @username, :superuser => true) + @admin.save! + end login + rescue Mongoid::Errors::Validations + flash[:error] = @admin.errors.full_messages.join(", ") + redirect_to admin_developer_omniauth_authorize_path end def cas diff --git a/src/api-umbrella/web-app/app/controllers/admin/admins/omniauth_custom_forms_controller.rb b/src/api-umbrella/web-app/app/controllers/admin/admins/omniauth_custom_forms_controller.rb new file mode 100644 index 000000000..c7f565ca6 --- /dev/null +++ b/src/api-umbrella/web-app/app/controllers/admin/admins/omniauth_custom_forms_controller.rb @@ -0,0 +1,7 @@ +class Admin::Admins::OmniauthCustomFormsController < ApplicationController + def developer + end + + def ldap + end +end diff --git a/src/api-umbrella/web-app/app/controllers/admin/registrations_controller.rb b/src/api-umbrella/web-app/app/controllers/admin/registrations_controller.rb index 3d2f6f571..4339ebd28 100644 --- a/src/api-umbrella/web-app/app/controllers/admin/registrations_controller.rb +++ b/src/api-umbrella/web-app/app/controllers/admin/registrations_controller.rb @@ -1,9 +1,9 @@ class Admin::RegistrationsController < Devise::RegistrationsController - before_action :one_time_setup + before_action :first_time_setup protected - def build_resource(hash=nil) + def build_resource(hash = nil) super # Make the first admin a superuser on initial setup. self.resource.superuser = true @@ -12,7 +12,7 @@ def build_resource(hash=nil) private - def one_time_setup + def first_time_setup unless(Admin.needs_first_account?) flash[:notice] = "An initial admin account already exists." redirect_to admin_path diff --git a/src/api-umbrella/web-app/app/controllers/admin/sessions_controller.rb b/src/api-umbrella/web-app/app/controllers/admin/sessions_controller.rb index 49d5039a5..5b0bf3de6 100644 --- a/src/api-umbrella/web-app/app/controllers/admin/sessions_controller.rb +++ b/src/api-umbrella/web-app/app/controllers/admin/sessions_controller.rb @@ -1,5 +1,5 @@ class Admin::SessionsController < Devise::SessionsController - before_action :one_time_setup + before_action :first_time_setup skip_after_action :verify_authorized def auth @@ -32,7 +32,7 @@ def set_flash_message(key, kind, options = {}) end end - def one_time_setup + def first_time_setup if(Admin.needs_first_account?) redirect_to new_admin_registration_path end diff --git a/src/api-umbrella/web-app/app/controllers/application_controller.rb b/src/api-umbrella/web-app/app/controllers/application_controller.rb index 532ea8ba6..55d66ec6b 100644 --- a/src/api-umbrella/web-app/app/controllers/application_controller.rb +++ b/src/api-umbrella/web-app/app/controllers/application_controller.rb @@ -2,7 +2,7 @@ class ApplicationController < ActionController::Base include Pundit include DatatablesHelper prepend_around_filter :use_locale - protect_from_forgery :with => :null_session + protect_from_forgery :with => :exception around_action :set_userstamp diff --git a/src/api-umbrella/web-app/app/models/admin.rb b/src/api-umbrella/web-app/app/models/admin.rb index fdf0f7762..7ae46bd44 100644 --- a/src/api-umbrella/web-app/app/models/admin.rb +++ b/src/api-umbrella/web-app/app/models/admin.rb @@ -45,7 +45,7 @@ class Admin ## Lockable field :failed_attempts, :type => Integer, :default => 0 # Only if lock strategy is :failed_attempts field :unlock_token, :type => String # Only if unlock strategy is :email or :both - field :locked_at, type: Time + field :locked_at, :type => Time ## Invitable field :invitation_token, :type => String @@ -97,7 +97,7 @@ def self.sorted end def self.needs_first_account? - self.unscoped.count == 0 + ApiUmbrellaConfig[:web][:admin][:auth_strategies][:enabled].include?("local") && self.unscoped.count == 0 end def group_names diff --git a/src/api-umbrella/web-app/app/views/admin/admins/omniauth_custom_forms/developer.html.erb b/src/api-umbrella/web-app/app/views/admin/admins/omniauth_custom_forms/developer.html.erb new file mode 100644 index 000000000..0c066a327 --- /dev/null +++ b/src/api-umbrella/web-app/app/views/admin/admins/omniauth_custom_forms/developer.html.erb @@ -0,0 +1,21 @@ +

Dummy Sign In

+ +
+ Enter any email address you'd like.
+ This login option is for development only. Enter any email address and you'll be logged in under that account (an account will be created if one doesn't already exist). +
+ + diff --git a/src/api-umbrella/web-app/app/views/devise/sessions/new.html.erb b/src/api-umbrella/web-app/app/views/devise/sessions/new.html.erb index dbd45fcfe..077881160 100644 --- a/src/api-umbrella/web-app/app/views/devise/sessions/new.html.erb +++ b/src/api-umbrella/web-app/app/views/devise/sessions/new.html.erb @@ -21,7 +21,6 @@
<%= f.button :submit, "Sign in" %>
- <% end %> <% if(resource_class.omniauth_providers.any?) %> diff --git a/src/api-umbrella/web-app/config/environments/test.rb b/src/api-umbrella/web-app/config/environments/test.rb index 51d091fa1..0f024a63e 100644 --- a/src/api-umbrella/web-app/config/environments/test.rb +++ b/src/api-umbrella/web-app/config/environments/test.rb @@ -8,7 +8,7 @@ # Deliver real e-mail if running integration tests with local MailHog as our # test SMTP server. - if(!config.action_mailer.smtp_settings || config.action_mailer.smtp_settings[:address] != "127.0.0.1" || config.action_mailer.smtp_settings[:port] != 13102) + if(!config.action_mailer.smtp_settings || config.action_mailer.smtp_settings[:address] != "127.0.0.1" || config.action_mailer.smtp_settings[:port] != ApiUmbrellaConfig[:mailhog][:smtp_port]) config.action_mailer.delivery_method = :test end diff --git a/src/api-umbrella/web-app/config/initializers/devise.rb b/src/api-umbrella/web-app/config/initializers/devise.rb index 855e606f5..f6a53c3b9 100644 --- a/src/api-umbrella/web-app/config/initializers/devise.rb +++ b/src/api-umbrella/web-app/config/initializers/devise.rb @@ -296,9 +296,11 @@ # Add a new OmniAuth provider. Check the wiki for more information on setting # up on your models and hooks. # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo' - if(%w(development test).include?(Rails.env)) + if(Rails.env == "development") config.omniauth :developer, - :fields => [:username] + :fields => [:username], + :uid_field => :username, + :form => Admin::Admins::OmniauthCustomFormsController.action(:developer) end ApiUmbrellaConfig[:web][:admin][:auth_strategies][:enabled].each do |strategy| @@ -337,7 +339,7 @@ :service_validate_url => "/cas/serviceValidate", :logout_url => "/cas/logout", :ssl => true - when "local" + when "local" # rubocop:disable Lint/EmptyWhen # Ignore else raise "Unknown authentication strategy enabled in config: #{strategy.inspect}" diff --git a/src/api-umbrella/web-app/config/initializers/simple_form.rb b/src/api-umbrella/web-app/config/initializers/simple_form.rb index 5a69fcfa0..e2dff59d9 100644 --- a/src/api-umbrella/web-app/config/initializers/simple_form.rb +++ b/src/api-umbrella/web-app/config/initializers/simple_form.rb @@ -5,8 +5,8 @@ # wrapper, change the order or even add your own to the # stack. The options given below are used to wrap the # whole input. - config.wrappers :default, class: :input, - hint_class: :field_with_hint, error_class: :field_with_errors do |b| + config.wrappers :default, :class => :input, + :hint_class => :field_with_hint, :error_class => :field_with_errors do |b| ## Extensions enabled by default # Any of these extensions can be disabled for a # given input by passing: `f.input EXTENSION_NAME => false`. @@ -41,8 +41,8 @@ ## Inputs b.use :label_input - b.use :hint, wrap_with: { tag: :span, class: :hint } - b.use :error, wrap_with: { tag: :span, class: :error } + b.use :hint, :wrap_with => { :tag => :span, :class => :hint } + b.use :error, :wrap_with => { :tag => :span, :class => :error } ## full_messages_for # If you want to display the full error message for the attribute, you can @@ -97,7 +97,7 @@ # config.item_wrapper_class = nil # How the label text should be generated altogether with the required text. - config.label_text = lambda { |label, required, explicit_label| "#{label}" } + config.label_text = lambda { |label, required, explicit_label| label.to_s } # You can define the class to use on all labels. Default is nil. # config.label_class = nil diff --git a/src/api-umbrella/web-app/config/initializers/simple_form_bootstrap.rb b/src/api-umbrella/web-app/config/initializers/simple_form_bootstrap.rb index 109d29a37..2475da1d5 100644 --- a/src/api-umbrella/web-app/config/initializers/simple_form_bootstrap.rb +++ b/src/api-umbrella/web-app/config/initializers/simple_form_bootstrap.rb @@ -4,132 +4,132 @@ config.button_class = 'btn btn-default' config.boolean_label_class = nil - config.wrappers :vertical_form, tag: 'div', class: 'form-group', error_class: 'has-error' do |b| + config.wrappers :vertical_form, :tag => 'div', :class => 'form-group', :error_class => 'has-error' do |b| b.use :html5 b.use :placeholder b.optional :maxlength b.optional :pattern b.optional :min_max b.optional :readonly - b.use :label, class: 'control-label' + b.use :label, :class => 'control-label' - b.use :input, class: 'form-control' - b.use :error, wrap_with: { tag: 'span', class: 'help-block' } - b.use :hint, wrap_with: { tag: 'p', class: 'help-block' } + b.use :input, :class => 'form-control' + b.use :error, :wrap_with => { :tag => 'span', :class => 'help-block' } + b.use :hint, :wrap_with => { :tag => 'p', :class => 'help-block' } end - config.wrappers :vertical_file_input, tag: 'div', class: 'form-group', error_class: 'has-error' do |b| + config.wrappers :vertical_file_input, :tag => 'div', :class => 'form-group', :error_class => 'has-error' do |b| b.use :html5 b.use :placeholder b.optional :maxlength b.optional :readonly - b.use :label, class: 'control-label' + b.use :label, :class => 'control-label' b.use :input - b.use :error, wrap_with: { tag: 'span', class: 'help-block' } - b.use :hint, wrap_with: { tag: 'p', class: 'help-block' } + b.use :error, :wrap_with => { :tag => 'span', :class => 'help-block' } + b.use :hint, :wrap_with => { :tag => 'p', :class => 'help-block' } end - config.wrappers :vertical_boolean, tag: 'div', class: 'form-group', error_class: 'has-error' do |b| + config.wrappers :vertical_boolean, :tag => 'div', :class => 'form-group', :error_class => 'has-error' do |b| b.use :html5 b.optional :readonly - b.wrapper tag: 'div', class: 'checkbox' do |ba| + b.wrapper :tag => 'div', :class => 'checkbox' do |ba| ba.use :label_input end - b.use :error, wrap_with: { tag: 'span', class: 'help-block' } - b.use :hint, wrap_with: { tag: 'p', class: 'help-block' } + b.use :error, :wrap_with => { :tag => 'span', :class => 'help-block' } + b.use :hint, :wrap_with => { :tag => 'p', :class => 'help-block' } end - config.wrappers :vertical_radio_and_checkboxes, tag: 'div', class: 'form-group', error_class: 'has-error' do |b| + config.wrappers :vertical_radio_and_checkboxes, :tag => 'div', :class => 'form-group', :error_class => 'has-error' do |b| b.use :html5 b.optional :readonly - b.use :label, class: 'control-label' + b.use :label, :class => 'control-label' b.use :input - b.use :error, wrap_with: { tag: 'span', class: 'help-block' } - b.use :hint, wrap_with: { tag: 'p', class: 'help-block' } + b.use :error, :wrap_with => { :tag => 'span', :class => 'help-block' } + b.use :hint, :wrap_with => { :tag => 'p', :class => 'help-block' } end - config.wrappers :horizontal_form, tag: 'div', class: 'form-group', error_class: 'has-error' do |b| + config.wrappers :horizontal_form, :tag => 'div', :class => 'form-group', :error_class => 'has-error' do |b| b.use :html5 b.use :placeholder b.optional :maxlength b.optional :pattern b.optional :min_max b.optional :readonly - b.use :label, class: 'col-sm-3 control-label' + b.use :label, :class => 'col-sm-3 control-label' - b.wrapper tag: 'div', class: 'col-sm-9' do |ba| - ba.use :input, class: 'form-control' - ba.use :error, wrap_with: { tag: 'span', class: 'help-block' } - ba.use :hint, wrap_with: { tag: 'p', class: 'help-block' } + b.wrapper :tag => 'div', :class => 'col-sm-9' do |ba| + ba.use :input, :class => 'form-control' + ba.use :error, :wrap_with => { :tag => 'span', :class => 'help-block' } + ba.use :hint, :wrap_with => { :tag => 'p', :class => 'help-block' } end end - config.wrappers :horizontal_file_input, tag: 'div', class: 'form-group', error_class: 'has-error' do |b| + config.wrappers :horizontal_file_input, :tag => 'div', :class => 'form-group', :error_class => 'has-error' do |b| b.use :html5 b.use :placeholder b.optional :maxlength b.optional :readonly - b.use :label, class: 'col-sm-3 control-label' + b.use :label, :class => 'col-sm-3 control-label' - b.wrapper tag: 'div', class: 'col-sm-9' do |ba| + b.wrapper :tag => 'div', :class => 'col-sm-9' do |ba| ba.use :input - ba.use :error, wrap_with: { tag: 'span', class: 'help-block' } - ba.use :hint, wrap_with: { tag: 'p', class: 'help-block' } + ba.use :error, :wrap_with => { :tag => 'span', :class => 'help-block' } + ba.use :hint, :wrap_with => { :tag => 'p', :class => 'help-block' } end end - config.wrappers :horizontal_boolean, tag: 'div', class: 'form-group', error_class: 'has-error' do |b| + config.wrappers :horizontal_boolean, :tag => 'div', :class => 'form-group', :error_class => 'has-error' do |b| b.use :html5 b.optional :readonly - b.wrapper tag: 'div', class: 'col-sm-offset-3 col-sm-9' do |wr| - wr.wrapper tag: 'div', class: 'checkbox' do |ba| + b.wrapper :tag => 'div', :class => 'col-sm-offset-3 col-sm-9' do |wr| + wr.wrapper :tag => 'div', :class => 'checkbox' do |ba| ba.use :label_input end - wr.use :error, wrap_with: { tag: 'span', class: 'help-block' } - wr.use :hint, wrap_with: { tag: 'p', class: 'help-block' } + wr.use :error, :wrap_with => { :tag => 'span', :class => 'help-block' } + wr.use :hint, :wrap_with => { :tag => 'p', :class => 'help-block' } end end - config.wrappers :horizontal_radio_and_checkboxes, tag: 'div', class: 'form-group', error_class: 'has-error' do |b| + config.wrappers :horizontal_radio_and_checkboxes, :tag => 'div', :class => 'form-group', :error_class => 'has-error' do |b| b.use :html5 b.optional :readonly - b.use :label, class: 'col-sm-3 control-label' + b.use :label, :class => 'col-sm-3 control-label' - b.wrapper tag: 'div', class: 'col-sm-9' do |ba| + b.wrapper :tag => 'div', :class => 'col-sm-9' do |ba| ba.use :input - ba.use :error, wrap_with: { tag: 'span', class: 'help-block' } - ba.use :hint, wrap_with: { tag: 'p', class: 'help-block' } + ba.use :error, :wrap_with => { :tag => 'span', :class => 'help-block' } + ba.use :hint, :wrap_with => { :tag => 'p', :class => 'help-block' } end end - config.wrappers :inline_form, tag: 'div', class: 'form-group', error_class: 'has-error' do |b| + config.wrappers :inline_form, :tag => 'div', :class => 'form-group', :error_class => 'has-error' do |b| b.use :html5 b.use :placeholder b.optional :maxlength b.optional :pattern b.optional :min_max b.optional :readonly - b.use :label, class: 'sr-only' + b.use :label, :class => 'sr-only' - b.use :input, class: 'form-control' - b.use :error, wrap_with: { tag: 'span', class: 'help-block' } - b.use :hint, wrap_with: { tag: 'p', class: 'help-block' } + b.use :input, :class => 'form-control' + b.use :error, :wrap_with => { :tag => 'span', :class => 'help-block' } + b.use :hint, :wrap_with => { :tag => 'p', :class => 'help-block' } end - config.wrappers :multi_select, tag: 'div', class: 'form-group', error_class: 'has-error' do |b| + config.wrappers :multi_select, :tag => 'div', :class => 'form-group', :error_class => 'has-error' do |b| b.use :html5 b.optional :readonly - b.use :label, class: 'control-label' - b.wrapper tag: 'div', class: 'form-inline' do |ba| - ba.use :input, class: 'form-control' - ba.use :error, wrap_with: { tag: 'span', class: 'help-block' } - ba.use :hint, wrap_with: { tag: 'p', class: 'help-block' } + b.use :label, :class => 'control-label' + b.wrapper :tag => 'div', :class => 'form-inline' do |ba| + ba.use :input, :class => 'form-control' + ba.use :error, :wrap_with => { :tag => 'span', :class => 'help-block' } + ba.use :hint, :wrap_with => { :tag => 'p', :class => 'help-block' } end end # Wrappers for forms and inputs using the Bootstrap toolkit. @@ -138,12 +138,12 @@ # buttons and other elements. config.default_wrapper = :vertical_form config.wrapper_mappings = { - check_boxes: :vertical_radio_and_checkboxes, - radio_buttons: :vertical_radio_and_checkboxes, - file: :vertical_file_input, - boolean: :vertical_boolean, - datetime: :multi_select, - date: :multi_select, - time: :multi_select + :check_boxes => :vertical_radio_and_checkboxes, + :radio_buttons => :vertical_radio_and_checkboxes, + :file => :vertical_file_input, + :boolean => :vertical_boolean, + :datetime => :multi_select, + :date => :multi_select, + :time => :multi_select, } end diff --git a/src/api-umbrella/web-app/config/initializers/userstamp.rb b/src/api-umbrella/web-app/lib/mongoid/userstamp.rb similarity index 100% rename from src/api-umbrella/web-app/config/initializers/userstamp.rb rename to src/api-umbrella/web-app/lib/mongoid/userstamp.rb diff --git a/templates/etc/perp/test-env-mailhog/rc.main.mustache b/templates/etc/perp/test-env-mailhog/rc.main.mustache index b1f427118..a1f9310a5 100755 --- a/templates/etc/perp/test-env-mailhog/rc.main.mustache +++ b/templates/etc/perp/test-env-mailhog/rc.main.mustache @@ -14,9 +14,9 @@ if [ "${1}" = "start" ]; then fi exec runtool "${run_args[@]}" mailhog \ - -smtp-bind-addr "{{mailhog.smtp_bind_addr}}" \ - -api-bind-addr "{{mailhog.api_bind_addr}}" \ - -ui-bind-addr "{{mailhog.ui_bind_addr}}" + -smtp-bind-addr "{{mailhog.bind_addr}}:{{mailhog.smtp_port}}" \ + -api-bind-addr "{{mailhog.bind_addr}}:{{mailhog.api_port}}" \ + -ui-bind-addr "{{mailhog.bind_addr}}:{{mailhog.ui_port}}" fi exit 0 diff --git a/test/admin_ui/test_login.rb b/test/admin_ui/login/test_external_providers.rb similarity index 73% rename from test/admin_ui/test_login.rb rename to test/admin_ui/login/test_external_providers.rb index c8391ced6..c197a452f 100644 --- a/test/admin_ui/test_login.rb +++ b/test/admin_ui/login/test_external_providers.rb @@ -1,82 +1,96 @@ -require_relative "../test_helper" +require_relative "../../test_helper" -class Test::AdminUi::TestLogin < Minitest::Capybara::Test +class Test::AdminUi::Login::TestExternalProviders < Minitest::Capybara::Test include Capybara::Screenshot::MiniTestPlugin - include ApiUmbrellaTestHelpers::DelayServerResponses include ApiUmbrellaTestHelpers::Setup + include ApiUmbrellaTestHelpers::AdminAuth + include Minitest::Hooks def setup setup_server - Admin.where(:registration_source.ne => "seed").delete_all + Admin.delete_all + once_per_class_setup do + override_config_set({ + "web" => { + "admin" => { + "auth_strategies" => { + "enabled" => [ + "facebook", + "max.gov", + "github", + "google", + "ldap", + ], + }, + }, + }, + }, ["--router", "--web"]) + end end - def test_login_redirects - # Slow down the server side responses to validate the "Loading..." spinner - # shows up (without slowing things down, it periodically goes away too - # quickly for the tests to catch). - delay_server_responses(0.5) do - visit "/admin/" - - # Ensure we get the loading spinner until authentication takes place. - assert_content("Loading...") - - # Navigation should not be visible while loading. - refute_selector("nav") - refute_content("Analytics") + def after_all + super + override_config_reset(["--router", "--web"]) + end - # Ensure that we eventually get redirected to the login page. - assert_content("Admin Login") - assert_content("Login with") - end + def test_forbids_first_time_admin_creation + assert_equal(0, Admin.count) + assert_first_time_admin_creation_forbidden end - # Since we do some custom things related to the Rails asset path, make sure - # everything is hooked up and the production cache-bused assets are served - # up. - def test_login_assets + def test_shows_external_login_links_in_order_no_local_fields visit "/admin/login" - assert_content("Admin Login") - - # Find the stylesheet on the Rails login page, which should have a - # cache-busted URL (note that the href on the page appears to be relative, - # but capybara seems to read it as absolute. That's fine, but noting it in - # case Capybara's future behavior changes). - stylesheet = find("link[rel=stylesheet]", :visible => :hidden) - assert_match(%r{\Ahttps://127\.0\.0\.1:9081/web-assets/admin/login-\w{64}\.css\z}, stylesheet[:href]) - - # Verify that the asset URL can be fetched and returns data. - response = Typhoeus.get(stylesheet[:href], keyless_http_options) - assert_response_code(200, response) - assert_equal("text/css", response.headers["content-type"]) + + assert_content("Admin Sign In") + + # No local login fields + refute_field("Email") + refute_field("Password") + refute_field("Remember me") + refute_link("Forgot your password?") + refute_button("Sign in") + + # External login links + assert_content("Sign in with") + + # Order matches enabled array order. + buttons = page.all(".external-login .btn").map { |btn| btn.text } + assert_equal([ + "Sign in with Facebook", + "Sign in with MAX.gov", + "Sign in with GitHub", + "Sign in with Google", + "Sign in with LDAP", + ], buttons) end [ { :provider => :facebook, - :login_button_text => "Login with Facebook", + :login_button_text => "Sign in with Facebook", :username_path => "info.email", :verified_path => "info.verified", }, { :provider => :github, - :login_button_text => "Login with GitHub", + :login_button_text => "Sign in with GitHub", :username_path => "info.email", :verified_path => "info.email_verified", }, { :provider => :google_oauth2, - :login_button_text => "Login with Google", + :login_button_text => "Sign in with Google", :username_path => "info.email", :verified_path => "extra.raw_info.email_verified", }, { :provider => :ldap, - :login_button_text => "Login with LDAP", + :login_button_text => "Sign in with LDAP", :username_path => "extra.raw_info.sAMAccountName", }, { :provider => :cas, - :login_button_text => "Login with MAX.gov", + :login_button_text => "Sign in with MAX.gov", :username_path => "uid", }, ].each do |options| diff --git a/test/admin_ui/login/test_first_time_setup.rb b/test/admin_ui/login/test_first_time_setup.rb new file mode 100644 index 000000000..e3010329b --- /dev/null +++ b/test/admin_ui/login/test_first_time_setup.rb @@ -0,0 +1,88 @@ +require_relative "../../test_helper" + +class Test::AdminUi::Login::TestFirstTimeSetup < Minitest::Capybara::Test + include Capybara::Screenshot::MiniTestPlugin + include ApiUmbrellaTestHelpers::Setup + include ApiUmbrellaTestHelpers::AdminAuth + + def setup + setup_server + Admin.delete_all + end + + def test_redirects_to_signup_on_first_login + assert_equal(0, Admin.count) + visit "/admin/" + + assert_content("Welcome!") + assert_content("It looks like you're setting up API Umbrella for the first time. Create your first admin account to get started.") + assert_equal("/admins/signup", page.current_path) + + fill_in "Email", :with => "new@example.com" + fill_in "Password", :with => "password123456" + fill_in "Password Confirmation", :with => "password123456" + click_button "Create Account" + + # Ensure the user gets logged in. + assert_logged_in + assert_equal(1, Admin.count) + + # First user should be superuser. + admin = Admin.first + assert_equal(true, admin.superuser) + end + + def test_redirects_to_login_if_admin_exists + FactoryGirl.create(:admin) + assert_equal(1, Admin.count) + visit "/admin/" + + assert_content("Admin Sign In") + refute_content("An initial admin account already exists.") + assert_equal("/admin/login", page.current_path) + end + + def test_redirects_away_from_signup_if_admin_exists + FactoryGirl.create(:admin) + assert_equal(1, Admin.count) + visit "/admins/signup" + + assert_content("Admin Sign In") + assert_content("An initial admin account already exists.") + assert_equal("/admin/login", page.current_path) + end + + def test_redirects_away_from_submit_if_admin_exists + assert_equal(0, Admin.count) + visit "/admins/signup" + + assert_content("Welcome!") + assert_content("It looks like you're setting up API Umbrella for the first time. Create your first admin account to get started.") + + fill_in "Email", :with => "new@example.com" + fill_in "Password", :with => "password123456" + fill_in "Password Confirmation", :with => "password123456" + + # Insert an admin before hitting submit to ensure the submit endpoint can't + # be hit directly. + FactoryGirl.create(:admin) + assert_equal(1, Admin.count) + + click_button "Create Account" + + assert_content("Admin Sign In") + assert_content("An initial admin account already exists.") + assert_equal("/admin/login", page.current_path) + + assert_equal(1, Admin.count) + end + + def test_allows_admin_creation_if_no_admin_exists + assert_first_time_admin_creation_allowed + end + + def test_forbids_admin_creation_if_admin_exists + FactoryGirl.create(:admin) + assert_first_time_admin_creation_forbidden + end +end diff --git a/test/admin_ui/login/test_forgot_password.rb b/test/admin_ui/login/test_forgot_password.rb new file mode 100644 index 000000000..4af530d09 --- /dev/null +++ b/test/admin_ui/login/test_forgot_password.rb @@ -0,0 +1,77 @@ +require_relative "../../test_helper" + +class Test::AdminUi::Login::TestForgotPassword < Minitest::Capybara::Test + include Capybara::Screenshot::MiniTestPlugin + include ApiUmbrellaTestHelpers::Setup + include ApiUmbrellaTestHelpers::AdminAuth + include ApiUmbrellaTestHelpers::DelayedJob + + def setup + setup_server + Admin.delete_all + response = Typhoeus.delete("http://127.0.0.1:#{$config["mailhog"]["api_port"]}/api/v1/messages") + assert_response_code(200, response) + end + + def test_non_existent_email + visit "/admins/password/new" + + fill_in "Email", :with => "foobar@example.com" + click_button "Send me reset password instructions" + assert_content("If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes.") + + assert_equal(0, delayed_job_sent_messages.length) + end + + def test_reset_process + admin = FactoryGirl.create(:admin, :username => "admin@example.com") + assert_nil(admin.reset_password_token) + assert_nil(admin.reset_password_sent_at) + assert_nil(admin.encrypted_password) + + visit "/admins/password/new" + + # Reset password + fill_in "Email", :with => "admin@example.com" + click_button "Send me reset password instructions" + assert_content("If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes.") + + # Check for reset token on database record. + admin.reload + assert(admin.reset_password_token) + assert(admin.reset_password_sent_at) + assert_nil(admin.encrypted_password) + + # Find sent email + messages = delayed_job_sent_messages + assert_equal(1, messages.length) + message = messages.first + + # To + assert_equal(["admin@example.com"], message["Content"]["Headers"]["To"]) + + # Subject + assert_equal(["Reset password instructions"], message["Content"]["Headers"]["Subject"]) + + # Use description in body + assert_match(%r{http://localhost/admins/password/edit\?reset_password_token=[^"]+}, message["_mime_parts"]["text/html; charset=UTF-8"]["Body"]) + assert_match(%r{http://localhost/admins/password/edit\?reset_password_token=[^"]+}, message["_mime_parts"]["text/plain; charset=UTF-8"]["Body"]) + + # Follow link to reset URL + reset_url = message["_mime_parts"]["text/html; charset=UTF-8"]["Body"].match(%r{/admins/password/edit\?reset_password_token=[^"]+})[0] + visit reset_url + + fill_in "New password", :with => "password" + fill_in "Confirm your new password", :with => "password" + click_button "Change my password" + + # Ensure the user gets logged in. + assert_logged_in(admin) + + # Check for database record updates. + admin.reload + assert_nil(admin.reset_password_token) + assert_nil(admin.reset_password_sent_at) + assert(admin.encrypted_password) + end +end diff --git a/test/admin_ui/login/test_local_and_external_providers.rb b/test/admin_ui/login/test_local_and_external_providers.rb new file mode 100644 index 000000000..857f82217 --- /dev/null +++ b/test/admin_ui/login/test_local_and_external_providers.rb @@ -0,0 +1,57 @@ +require_relative "../../test_helper" + +class Test::AdminUi::Login::TestLocalAndExternalProviders < Minitest::Capybara::Test + include Capybara::Screenshot::MiniTestPlugin + include ApiUmbrellaTestHelpers::Setup + include ApiUmbrellaTestHelpers::AdminAuth + include Minitest::Hooks + + def setup + setup_server + Admin.delete_all + once_per_class_setup do + override_config_set({ + "web" => { + "admin" => { + "auth_strategies" => { + "enabled" => [ + "local", + "google", + ], + }, + }, + }, + }, ["--router", "--web"]) + end + end + + def after_all + super + override_config_reset(["--router", "--web"]) + end + + def test_allows_first_time_admin_creation + assert_equal(0, Admin.count) + assert_first_time_admin_creation_allowed + end + + def test_shows_local_login_fields_and_external_login_links + FactoryGirl.create(:admin) + visit "/admin/login" + + assert_content("Admin Sign In") + + # Local login fields + assert_field("Email") + assert_field("Password") + assert_field("Remember me") + assert_link("Forgot your password?") + assert_button("Sign in") + + # External login links + assert_content("Sign in with") + + buttons = page.all(".external-login .btn").map { |btn| btn.text } + assert_equal(["Sign in with Google"], buttons) + end +end diff --git a/test/admin_ui/login/test_local_provider.rb b/test/admin_ui/login/test_local_provider.rb new file mode 100644 index 000000000..f4c52d132 --- /dev/null +++ b/test/admin_ui/login/test_local_provider.rb @@ -0,0 +1,75 @@ +require_relative "../../test_helper" + +class Test::AdminUi::Login::TestLocalProvider < Minitest::Capybara::Test + include Capybara::Screenshot::MiniTestPlugin + include ApiUmbrellaTestHelpers::AdminAuth + include ApiUmbrellaTestHelpers::DelayServerResponses + include ApiUmbrellaTestHelpers::Setup + + def setup + setup_server + Admin.delete_all + @admin = FactoryGirl.create(:admin) + end + + def test_allows_first_time_admin_creation + Admin.delete_all + assert_equal(0, Admin.count) + assert_first_time_admin_creation_allowed + end + + def test_shows_local_login_fields_no_external_login_links + visit "/admin/login" + + assert_content("Admin Sign In") + + # Local login fields + assert_field("Email") + assert_field("Password") + assert_field("Remember me") + assert_link("Forgot your password?") + assert_button("Sign in") + + # No external login links + refute_content("Sign in with") + end + + def test_login_redirects + # Slow down the server side responses to validate the "Loading..." spinner + # shows up (without slowing things down, it periodically goes away too + # quickly for the tests to catch). + delay_server_responses(0.5) do + visit "/admin/" + + # Ensure we get the loading spinner until authentication takes place. + assert_content("Loading...") + + # Navigation should not be visible while loading. + refute_selector("nav") + refute_content("Analytics") + + # Ensure that we eventually get redirected to the login page. + assert_content("Admin Sign In") + end + end + + # Since we do some custom things related to the Rails asset path, make sure + # everything is hooked up and the production cache-bused assets are served + # up. + def test_login_assets + visit "/admin/login" + assert_content("Admin Sign In") + + # Find the stylesheet on the Rails login page, which should have a + # cache-busted URL (note that the href on the page appears to be relative, + # but capybara seems to read it as absolute. That's fine, but noting it in + # case Capybara's future behavior changes). + stylesheet = find("link[rel=stylesheet]", :visible => :hidden) + assert_match(%r{\Ahttps://127\.0\.0\.1:9081/web-assets/admin/login-\w{64}\.css\z}, stylesheet[:href]) + + # Verify that the asset URL can be fetched and returns data. + response = Typhoeus.get(stylesheet[:href], keyless_http_options) + assert_response_code(200, response) + assert_equal("text/css", response.headers["content-type"]) + end +end diff --git a/test/admin_ui/test_api_users_welcome_email.rb b/test/admin_ui/test_api_users_welcome_email.rb index 3b1c13cd9..4c456c0b1 100644 --- a/test/admin_ui/test_api_users_welcome_email.rb +++ b/test/admin_ui/test_api_users_welcome_email.rb @@ -9,7 +9,7 @@ class Test::AdminUi::TestApiUsersWelcomeEmail < Minitest::Capybara::Test def setup setup_server - response = Typhoeus.delete("http://127.0.0.1:13103/api/v1/messages") + response = Typhoeus.delete("http://127.0.0.1:#{$config["mailhog"]["api_port"]}/api/v1/messages") assert_response_code(200, response) end diff --git a/test/apis/v1/admins/test_admin_permissions.rb b/test/apis/v1/admins/test_admin_permissions.rb index 4183850f6..509f41557 100644 --- a/test/apis/v1/admins/test_admin_permissions.rb +++ b/test/apis/v1/admins/test_admin_permissions.rb @@ -7,7 +7,7 @@ class Test::Apis::V1::Admins::TestAdminPermissions < Minitest::Test def setup setup_server - Admin.where(:registration_source.ne => "seed").delete_all + Admin.delete_all AdminGroup.delete_all ApiScope.delete_all end diff --git a/test/apis/v1/admins/test_create.rb b/test/apis/v1/admins/test_create.rb index 40e7ffec0..7e73165e8 100644 --- a/test/apis/v1/admins/test_create.rb +++ b/test/apis/v1/admins/test_create.rb @@ -6,7 +6,7 @@ class Test::Apis::V1::Admins::TestCreate < Minitest::Test def setup setup_server - Admin.where(:registration_source.ne => "seed").delete_all + Admin.delete_all end def test_downcases_username diff --git a/test/apis/v1/admins/test_index.rb b/test/apis/v1/admins/test_index.rb index f9f14caa0..eae2fc3cd 100644 --- a/test/apis/v1/admins/test_index.rb +++ b/test/apis/v1/admins/test_index.rb @@ -6,7 +6,7 @@ class Test::Apis::V1::Admins::TestIndex < Minitest::Test def setup setup_server - Admin.where(:registration_source.ne => "seed").delete_all + Admin.delete_all end def test_paginate_results diff --git a/test/apis/v1/users/test_create_notify_email.rb b/test/apis/v1/users/test_create_notify_email.rb index b90f28be5..d85e60028 100644 --- a/test/apis/v1/users/test_create_notify_email.rb +++ b/test/apis/v1/users/test_create_notify_email.rb @@ -9,7 +9,7 @@ def setup setup_server ApiUser.where(:registration_source.ne => "seed").delete_all - response = Typhoeus.delete("http://127.0.0.1:13103/api/v1/messages") + response = Typhoeus.delete("http://127.0.0.1:#{$config["mailhog"]["api_port"]}/api/v1/messages") assert_response_code(200, response) end diff --git a/test/apis/v1/users/test_create_welcome_email.rb b/test/apis/v1/users/test_create_welcome_email.rb index 8af183fb6..eb2e1e137 100644 --- a/test/apis/v1/users/test_create_welcome_email.rb +++ b/test/apis/v1/users/test_create_welcome_email.rb @@ -9,7 +9,7 @@ def setup setup_server ApiUser.where(:registration_source.ne => "seed").delete_all - response = Typhoeus.delete("http://127.0.0.1:13103/api/v1/messages") + response = Typhoeus.delete("http://127.0.0.1:#{$config["mailhog"]["api_port"]}/api/v1/messages") assert_response_code(200, response) end diff --git a/test/apis/v1/users/test_permissions.rb b/test/apis/v1/users/test_permissions.rb index 973381908..d3a6d6847 100644 --- a/test/apis/v1/users/test_permissions.rb +++ b/test/apis/v1/users/test_permissions.rb @@ -8,7 +8,7 @@ class Test::Apis::V1::Users::TestPermissions < Minitest::Test def setup setup_server ApiUser.where(:registration_source.ne => "seed").delete_all - Admin.where(:registration_source.ne => "seed").delete_all + Admin.delete_all AdminGroup.delete_all Api.delete_all ApiScope.delete_all diff --git a/test/apis/v1/users/test_role_permissions.rb b/test/apis/v1/users/test_role_permissions.rb index 8fec34737..2edea7e9d 100644 --- a/test/apis/v1/users/test_role_permissions.rb +++ b/test/apis/v1/users/test_role_permissions.rb @@ -8,7 +8,7 @@ class Test::Apis::V1::Users::TestRolePermissions < Minitest::Test def setup setup_server ApiUser.where(:registration_source.ne => "seed").delete_all - Admin.where(:registration_source.ne => "seed").delete_all + Admin.delete_all AdminGroup.delete_all Api.delete_all ApiScope.delete_all diff --git a/test/factories/admins.rb b/test/factories/admins.rb index f1cff81be..cbedad576 100644 --- a/test/factories/admins.rb +++ b/test/factories/admins.rb @@ -1,6 +1,7 @@ FactoryGirl.define do factory :admin do - sequence(:username) { |n| "aburnside#{n}" } + sequence(:username) { |n| "aburnside#{n}@example.com" } + sequence(:email) { username } superuser true factory :limited_admin do diff --git a/test/support/api_umbrella_test_helpers/admin_auth.rb b/test/support/api_umbrella_test_helpers/admin_auth.rb index 8d6e2ceae..9843f33e1 100644 --- a/test/support/api_umbrella_test_helpers/admin_auth.rb +++ b/test/support/api_umbrella_test_helpers/admin_auth.rb @@ -3,17 +3,25 @@ module ApiUmbrellaTestHelpers module AdminAuth def admin_login(admin = nil) - admin ||= FactoryGirl.create(:admin) + admin ||= FactoryGirl.create(:admin, :encrypted_password => BCrypt::Password.create("password")) - visit "/admins/auth/developer" - fill_in "Email:", :with => admin.username - click_button "Sign In" + visit "/admin/login" + fill_in "Email", :with => admin.username + fill_in "Password", :with => "password" + click_button "Sign in" + assert_logged_in(admin) + end - # Wait for the page to fully load, including the /admin/auth ajax request - # which will fill out the "My Account" link. If we don't wait, then - # navigating to another page immediately may cancel the previous - # /admin/auth ajax request if it hadn't finished throwing some errors. - assert_link("my_account_nav_link", :href => /#{admin.id}/, :visible => :all) + def csrf_session + csrf_token = SecureRandom.base64(32) + + cookies_utils = RailsCompatibleCookiesUtils.new("aeec385fb48a0594b6bb0b18f62473190f1d01b0b6113766af525be2ae1a317a03ab0ee1b3ee6aca3fb1572dc87684e033dcec21acd90d0ca0f111ca1785d0e9") + session = cookies_utils.encrypt({ + "session_id" => SecureRandom.hex(16), + "_csrf_token" => csrf_token, + }) + + { :headers => { "Cookie" => "_api_umbrella_session=#{session}", "X-CSRF-Token" => csrf_token } } end def admin_session(admin = nil) @@ -31,5 +39,59 @@ def admin_token(admin = nil) admin ||= FactoryGirl.create(:admin) { :headers => { "X-Admin-Auth-Token" => admin.authentication_token } } end + + def assert_logged_in(admin = nil) + # Wait for the page to fully load, including the /admin/auth ajax request + # which will fill out the "My Account" link. If we don't wait, then + # navigating to another page immediately may cancel the previous + # /admin/auth ajax request if it hadn't finished throwing some errors. + if(admin) + assert_link("my_account_nav_link", :href => /#{admin.id}/, :visible => :all) + else + assert_link("my_account_nav_link", :visible => :all) + end + end + + def assert_first_time_admin_creation_allowed + assert_equal(0, Admin.count) + + get_response, create_response = make_first_time_admin_creation_requests + assert_response_code(200, get_response) + assert_response_code(302, create_response) + + assert_equal("https://127.0.0.1:9081/admin/#/login", create_response.headers["Location"]) + + assert_equal(1, Admin.count) + end + + def assert_first_time_admin_creation_forbidden + initial_count = Admin.count + + get_response, create_response = make_first_time_admin_creation_requests + assert_response_code(302, get_response) + assert_response_code(302, create_response) + + assert_equal("https://127.0.0.1:9081/admin", get_response.headers["Location"]) + assert_equal("https://127.0.0.1:9081/admin", create_response.headers["Location"]) + + assert_equal(initial_count, Admin.count) + end + + def make_first_time_admin_creation_requests + get_response = Typhoeus.get("https://127.0.0.1:9081/admins/signup", keyless_http_options) + + create_response = Typhoeus.post("https://127.0.0.1:9081/admins", keyless_http_options.deep_merge(csrf_session).deep_merge({ + :headers => { "Content-Type" => "application/x-www-form-urlencoded" }, + :body => { + :admin => { + :username => "new@example.com", + :password => "password", + :password_confirmation => "password", + }, + }, + })) + + [get_response, create_response] + end end end diff --git a/test/support/api_umbrella_test_helpers/delayed_job.rb b/test/support/api_umbrella_test_helpers/delayed_job.rb index cb84bff18..cb73b4559 100644 --- a/test/support/api_umbrella_test_helpers/delayed_job.rb +++ b/test/support/api_umbrella_test_helpers/delayed_job.rb @@ -20,7 +20,7 @@ def wait_for_delayed_jobs def delayed_job_sent_messages wait_for_delayed_jobs - response = Typhoeus.get("http://127.0.0.1:13103/api/v1/messages") + response = Typhoeus.get("http://127.0.0.1:#{$config["mailhog"]["api_port"]}/api/v1/messages") assert_response_code(200, response) messages = MultiJson.load(response.body) diff --git a/test/support/api_umbrella_test_helpers/process.rb b/test/support/api_umbrella_test_helpers/process.rb index eee766667..915b6c248 100644 --- a/test/support/api_umbrella_test_helpers/process.rb +++ b/test/support/api_umbrella_test_helpers/process.rb @@ -137,7 +137,7 @@ def self.stop end def self.reload(flag) - reload = ChildProcess.build(*[File.join(API_UMBRELLA_SRC_ROOT, "bin/api-umbrella"), "reload", flag].compact) + reload = ChildProcess.build(*[File.join(API_UMBRELLA_SRC_ROOT, "bin/api-umbrella"), "reload", flag].flatten.compact) reload.io.inherit! reload.environment["API_UMBRELLA_EMBEDDED_ROOT"] = EMBEDDED_ROOT reload.environment["API_UMBRELLA_CONFIG"] = CONFIG diff --git a/test/support/models/admin.rb b/test/support/models/admin.rb index 729634a85..0a4bae767 100644 --- a/test/support/models/admin.rb +++ b/test/support/models/admin.rb @@ -3,16 +3,28 @@ class Admin include Mongoid::Timestamps field :_id, :type => String, :overwrite => true, :default => lambda { SecureRandom.uuid } field :username, :type => String - field :email, :type => String field :name, :type => String field :notes, :type => String field :superuser, :type => Boolean field :authentication_token, :type => String, :default => lambda { SecureRandom.hex(20) } field :last_sign_in_provider, :type => String - field :sign_in_count, :type => Integer, :default => 0 + field :email, :type => String + field :encrypted_password, :type => String + field :reset_password_token, :type => String + field :reset_password_sent_at, :type => Time + field :remember_created_at, :type => Time + field :sign_in_count, :type => Integer, :default => 0 field :current_sign_in_at, :type => Time - field :last_sign_in_at, :type => Time + field :last_sign_in_at, :type => Time field :current_sign_in_ip, :type => String - field :last_sign_in_ip, :type => String + field :last_sign_in_ip, :type => String + field :failed_attempts, :type => Integer, :default => 0 + field :unlock_token, :type => String + field :locked_at, :type => Time + field :invitation_token, :type => String + field :invitation_created_at, :type => Time + field :invitation_sent_at, :type => Time + field :invitation_accepted_at, :type => Time + field :invitation_limit, :type => Integer has_and_belongs_to_many :groups, :class_name => "AdminGroup", :inverse_of => nil end diff --git a/test/support/test_namespaces.rb b/test/support/test_namespaces.rb index 0c517a2cd..6d29602a8 100644 --- a/test/support/test_namespaces.rb +++ b/test/support/test_namespaces.rb @@ -3,7 +3,9 @@ # We pre-define all these here so that we can use the shorter, more succinct # single-line/non-nested syntax in all the test files. module Test - module AdminUi; end + module AdminUi + module Login; end + end module Apis module Admin From ed6542705e3998ec66441bdc527ab77aa6081acc Mon Sep 17 00:00:00 2001 From: Nick Muerdter Date: Mon, 9 Jan 2017 19:38:04 -0700 Subject: [PATCH 06/26] Fix tests not using precompiled assets for login page. Other fixes for build process to ensure assets always get precompiled during build and in production mode. --- build/cmake/core-web-app.cmake | 3 ++- build/cmake/core.cmake | 1 + src/api-umbrella/web-app/Gemfile | 7 ++++--- src/api-umbrella/web-app/Gemfile.lock | 15 ++++++++++----- .../stylesheets/admin/{login.css => login.scss} | 7 +++---- src/api-umbrella/web-app/config/application.rb | 10 +++++++++- 6 files changed, 29 insertions(+), 14 deletions(-) rename src/api-umbrella/web-app/app/assets/stylesheets/admin/{login.css => login.scss} (94%) diff --git a/build/cmake/core-web-app.cmake b/build/cmake/core-web-app.cmake index 1b22564e9..b79ac2dd4 100644 --- a/build/cmake/core-web-app.cmake +++ b/build/cmake/core-web-app.cmake @@ -15,6 +15,7 @@ add_custom_command( file(GLOB_RECURSE web_asset_files ${CMAKE_SOURCE_DIR}/src/api-umbrella/web-app/app/assets/*.css + ${CMAKE_SOURCE_DIR}/src/api-umbrella/web-app/app/assets/*.scss ${CMAKE_SOURCE_DIR}/src/api-umbrella/web-app/app/assets/*.erb ${CMAKE_SOURCE_DIR}/src/api-umbrella/web-app/app/assets/*.js ) @@ -24,7 +25,7 @@ add_custom_command( ${STAMP_DIR}/core-web-app-bundle ${web_asset_files} ${CMAKE_SOURCE_DIR}/src/api-umbrella/web-app/config/initializers/assets.rb - COMMAND env PATH=${STAGE_EMBEDDED_DIR}/bin:$ENV{PATH} BUNDLE_GEMFILE=${CMAKE_SOURCE_DIR}/src/api-umbrella/web-app/Gemfile BUNDLE_APP_CONFIG=${WORK_DIR}/src/web-app/.bundle RAILS_TMP_PATH=/tmp/web-app-build RAILS_PUBLIC_PATH=${CORE_BUILD_DIR}/tmp/web-app-build bundle exec rake -f ${CMAKE_SOURCE_DIR}/src/api-umbrella/web-app/Rakefile assets:clobber assets:precompile + COMMAND env PATH=${STAGE_EMBEDDED_DIR}/bin:$ENV{PATH} BUNDLE_GEMFILE=${CMAKE_SOURCE_DIR}/src/api-umbrella/web-app/Gemfile BUNDLE_APP_CONFIG=${WORK_DIR}/src/web-app/.bundle RAILS_TMP_PATH=/tmp/web-app-build RAILS_PUBLIC_PATH=${CORE_BUILD_DIR}/tmp/web-app-build RAILS_ENV=production bundle exec rake -f ${CMAKE_SOURCE_DIR}/src/api-umbrella/web-app/Rakefile assets:clobber assets:precompile COMMAND touch ${STAMP_DIR}/core-web-app-precompile ) diff --git a/build/cmake/core.cmake b/build/cmake/core.cmake index ebf6fd4dc..f732ddd0f 100644 --- a/build/cmake/core.cmake +++ b/build/cmake/core.cmake @@ -20,6 +20,7 @@ file(GLOB_RECURSE core_files ${CMAKE_SOURCE_DIR}/bin/* ${CMAKE_SOURCE_DIR}/config/* ${CMAKE_SOURCE_DIR}/templates/* + ${CMAKE_SOURCE_DIR}/src/api-umbrella/web-app/Gemfile* ) add_custom_command( OUTPUT ${STAMP_DIR}/core-build-release-dir diff --git a/src/api-umbrella/web-app/Gemfile b/src/api-umbrella/web-app/Gemfile index 29b1e10ad..09f3ea643 100644 --- a/src/api-umbrella/web-app/Gemfile +++ b/src/api-umbrella/web-app/Gemfile @@ -119,10 +119,11 @@ gem "simple_form", "~> 3.3.1" # Login stylesheets gem "sass-rails", "~> 5.0" +gem "bootstrap-sass", "~> 3.3.7" gem "font-awesome-rails", "~> 4.7.0" -source "https://rails-assets.org" do - gem "rails-assets-bootstrap", "~> 3.3.7" -end + +# Login javascript minification +gem "uglifier", "~> 3.0.4" # Bundle gems for the local environment. Make sure to # put test-only gems in this group so their generators diff --git a/src/api-umbrella/web-app/Gemfile.lock b/src/api-umbrella/web-app/Gemfile.lock index caa3e5f35..4dc6c3b3e 100644 --- a/src/api-umbrella/web-app/Gemfile.lock +++ b/src/api-umbrella/web-app/Gemfile.lock @@ -48,7 +48,6 @@ GIT GEM remote: https://rubygems.org/ - remote: https://rails-assets.org/ specs: actionmailer (4.2.7.1) actionpack (= 4.2.7.1) @@ -90,9 +89,14 @@ GEM airbrussh (1.1.1) sshkit (>= 1.6.1, != 1.7.0) arel (6.0.3) + autoprefixer-rails (6.6.1) + execjs awesome_print (1.7.0) bcrypt (3.1.11) blankslate (3.1.3) + bootstrap-sass (3.3.7) + autoprefixer-rails (>= 5.2.1) + sass (>= 3.3.4) brakeman (3.4.1) bson (4.2.0) builder (3.2.2) @@ -146,6 +150,7 @@ GEM faraday multi_json erubis (2.7.0) + execjs (2.7.0) faraday (0.9.2) multipart-post (>= 1.2, < 3) font-awesome-rails (4.7.0.1) @@ -269,9 +274,6 @@ GEM bundler (>= 1.3.0, < 2.0) railties (= 4.2.7.1) sprockets-rails - rails-assets-bootstrap (3.3.7) - rails-assets-jquery (>= 1.9.1, < 4) - rails-assets-jquery (3.1.1) rails-deprecated_sanitizer (1.0.3) activesupport (>= 4.2.0.alpha) rails-dom-testing (1.0.7) @@ -322,6 +324,8 @@ GEM tilt (2.0.5) tzinfo (1.2.2) thread_safe (~> 0.1) + uglifier (3.0.4) + execjs (>= 0.3.0, < 3) unicode_utils (1.4.0) warden (1.2.6) rack (>= 1.0) @@ -331,6 +335,7 @@ PLATFORMS DEPENDENCIES awesome_print (~> 1.7.0) + bootstrap-sass (~> 3.3.7) brakeman bundler-audit capistrano (~> 3.6.1) @@ -372,7 +377,6 @@ DEPENDENCIES rack-proxy (~> 0.6.0) rack-timeout (~> 0.4.2) rails (~> 4.2.7.1) - rails-assets-bootstrap (~> 3.3.7)! rails_stdout_logging (~> 0.0.5) request_store (~> 1.3.1) rollbar (~> 2.13.3) @@ -381,6 +385,7 @@ DEPENDENCIES seed-fu! sequel (~> 4.41.0) simple_form (~> 3.3.1) + uglifier (~> 3.0.4) BUNDLED WITH 1.13.6 diff --git a/src/api-umbrella/web-app/app/assets/stylesheets/admin/login.css b/src/api-umbrella/web-app/app/assets/stylesheets/admin/login.scss similarity index 94% rename from src/api-umbrella/web-app/app/assets/stylesheets/admin/login.css rename to src/api-umbrella/web-app/app/assets/stylesheets/admin/login.scss index d64f2c1e2..78d83fa4f 100644 --- a/src/api-umbrella/web-app/app/assets/stylesheets/admin/login.css +++ b/src/api-umbrella/web-app/app/assets/stylesheets/admin/login.scss @@ -1,7 +1,6 @@ -/* - *= require bootstrap/bootstrap - *= require font-awesome - */ +@import "bootstrap-sprockets"; +@import "bootstrap"; +@import "font-awesome"; body { padding: 20px; diff --git a/src/api-umbrella/web-app/config/application.rb b/src/api-umbrella/web-app/config/application.rb index 9755ca890..6db74d273 100644 --- a/src/api-umbrella/web-app/config/application.rb +++ b/src/api-umbrella/web-app/config/application.rb @@ -121,6 +121,8 @@ class Application < Rails::Application # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] # config.i18n.default_locale = :de + # Allow the Rails tmp path to be configured to be outside of the source + # directory. if(ENV["RAILS_TMP_PATH"].present?) config.paths["tmp"] = ENV["RAILS_TMP_PATH"] config.assets.configure do |env| @@ -132,7 +134,13 @@ class Application < Rails::Application end end - if(ENV["RAILS_PUBLIC_PATH"].present? && !%w(test development).include?(Rails.env)) + # Allow the Rails public path to be configured to be outside of the source + # directory. This allows API Umbrella builds (resulting in precompiled + # assets) to happen in a build-specific directory. + # + # However, in development, ignore this, since we don't want precompiled + # assets from a build to be picked up and used. + if(ENV["RAILS_PUBLIC_PATH"].present? && Rails.env != "development") config.paths["public"] = ENV["RAILS_PUBLIC_PATH"] end From 6b6df7671098ffcc11baabafcd8e7aa6e5143a6b Mon Sep 17 00:00:00 2001 From: Nick Muerdter Date: Wed, 11 Jan 2017 21:35:34 -0700 Subject: [PATCH 07/26] Add translations to new devise login/signup pages. Fix i18n tests. - Use devise-i18n to provide translations for the various default Devise views. - Update the Accept-Language header detection to take into account region (so "es-419" is properly handled now that devise-i18n also has "es" data). - Update the existing i18n tests to account for the new server-side content on the default login page. We'll no longer try to read the i18n files directly, and instead put the expected i18n values directly in the test script (since otherwise, it's tricky to try and fetch the devise-i18n default values from within the test suite). --- Gemfile | 3 - Gemfile.lock | 1 - src/api-umbrella/web-app/Gemfile | 3 +- src/api-umbrella/web-app/Gemfile.lock | 2 + .../app/controllers/application_controller.rb | 2 +- .../mailer/confirmation_instructions.html.erb | 8 +- .../devise/mailer/password_change.html.erb | 5 +- .../reset_password_instructions.html.erb | 11 +- .../mailer/unlock_instructions.html.erb | 9 +- .../app/views/devise/passwords/edit.html.erb | 12 +- .../app/views/devise/passwords/new.html.erb | 4 +- .../views/devise/registrations/new.html.erb | 2 +- .../app/views/devise/sessions/new.html.erb | 6 +- .../app/views/devise/shared/_links.html.erb | 16 +-- .../app/views/devise/unlocks/new.html.erb | 8 +- .../web-app/config/initializers/i18n.rb | 17 +++ .../web-app/config/locales/devise.en.yml | 62 --------- .../web-app/config/locales/en.yml | 4 + src/api-umbrella/web-app/config/routes.rb | 2 +- test/admin_ui/test_locales.rb | 125 +++++++++++++----- .../api_umbrella_test_helpers/admin_auth.rb | 6 +- 21 files changed, 161 insertions(+), 147 deletions(-) create mode 100644 src/api-umbrella/web-app/config/initializers/i18n.rb delete mode 100644 src/api-umbrella/web-app/config/locales/devise.en.yml diff --git a/Gemfile b/Gemfile index df69587ec..1b155caef 100644 --- a/Gemfile +++ b/Gemfile @@ -41,9 +41,6 @@ gem "database_cleaner", "~> 1.5.3" # Programmatically generate Rails session cookies. gem "rails_compatible_cookies_utils", "~> 0.1.0" -# Localization tests -gem "i18n", "~> 0.7.0" - # URL parsing/generation gem "addressable", "~> 2.5.0" diff --git a/Gemfile.lock b/Gemfile.lock index 0f6eb548b..760a0a553 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -162,7 +162,6 @@ DEPENDENCIES elasticsearch-persistence (~> 0.1.9) factory_girl (~> 4.7.0) faker (~> 1.6.6) - i18n (~> 0.7.0) lazyhash (~> 0.1.1) minitest (~> 5.10.1) minitest-capybara (~> 0.8.2)! diff --git a/src/api-umbrella/web-app/Gemfile b/src/api-umbrella/web-app/Gemfile index 09f3ea643..dfdd48a2b 100644 --- a/src/api-umbrella/web-app/Gemfile +++ b/src/api-umbrella/web-app/Gemfile @@ -58,6 +58,7 @@ gem "elasticsearch", "~> 2.0.0" # OmniAuth-based authentication gem "devise", "~> 4.2.0" +gem "devise-i18n", "~> 1.1.1" gem "devise_invitable", "~> 1.7.0" gem "omniauth", "~> 1.3.1" gem "omniauth-cas", "~> 1.1.0", :git => "https://github.com/GUI/omniauth-cas.git", :branch => "rexml", :require => false @@ -123,7 +124,7 @@ gem "bootstrap-sass", "~> 3.3.7" gem "font-awesome-rails", "~> 4.7.0" # Login javascript minification -gem "uglifier", "~> 3.0.4" +gem "uglifier", "~> 3.0.4", :require => false # Bundle gems for the local environment. Make sure to # put test-only gems in this group so their generators diff --git a/src/api-umbrella/web-app/Gemfile.lock b/src/api-umbrella/web-app/Gemfile.lock index 4dc6c3b3e..97365d09a 100644 --- a/src/api-umbrella/web-app/Gemfile.lock +++ b/src/api-umbrella/web-app/Gemfile.lock @@ -138,6 +138,7 @@ GEM railties (>= 4.1.0, < 5.1) responders warden (~> 1.2.3) + devise-i18n (1.1.1) devise_invitable (1.7.0) actionmailer (>= 4.0.0) devise (>= 4.0.0) @@ -345,6 +346,7 @@ DEPENDENCIES daemons (~> 1.2.4) delayed_job_mongoid (~> 2.2.0) devise (~> 4.2.0) + devise-i18n (~> 1.1.1) devise_invitable (~> 1.7.0) elasticsearch (~> 2.0.0) font-awesome-rails (~> 4.7.0) diff --git a/src/api-umbrella/web-app/app/controllers/application_controller.rb b/src/api-umbrella/web-app/app/controllers/application_controller.rb index 55d66ec6b..9f1c5d895 100644 --- a/src/api-umbrella/web-app/app/controllers/application_controller.rb +++ b/src/api-umbrella/web-app/app/controllers/application_controller.rb @@ -83,7 +83,7 @@ def parse_post_for_pseudo_ie_cors end def use_locale - locale = http_accept_language.compatible_language_from(I18n.available_locales) || I18n.default_locale + locale = http_accept_language.language_region_compatible_from(I18n.available_locales) || I18n.default_locale I18n.with_locale(locale) do yield end diff --git a/src/api-umbrella/web-app/app/views/devise/mailer/confirmation_instructions.html.erb b/src/api-umbrella/web-app/app/views/devise/mailer/confirmation_instructions.html.erb index dc55f64f6..7cd20be9a 100644 --- a/src/api-umbrella/web-app/app/views/devise/mailer/confirmation_instructions.html.erb +++ b/src/api-umbrella/web-app/app/views/devise/mailer/confirmation_instructions.html.erb @@ -1,5 +1,5 @@ -

Welcome <%= @email %>!

+<% require 'devise/version' %> +

<%= t('.greeting', recipient: @resource.email) %>

-

You can confirm your account email through the link below:

- -

<%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %>

+

<%= t('.instruction') %>

+

<%= link_to t('.action'), confirmation_url(@resource, confirmation_token: (Devise::VERSION.start_with?('3.') ? @token : @resource.confirmation_token)) %>

\ No newline at end of file diff --git a/src/api-umbrella/web-app/app/views/devise/mailer/password_change.html.erb b/src/api-umbrella/web-app/app/views/devise/mailer/password_change.html.erb index b41daf476..7800a1219 100644 --- a/src/api-umbrella/web-app/app/views/devise/mailer/password_change.html.erb +++ b/src/api-umbrella/web-app/app/views/devise/mailer/password_change.html.erb @@ -1,3 +1,4 @@ -

Hello <%= @resource.email %>!

+<% require 'devise/version' %> +

<%= t('.greeting', recipient: @resource.email) %>

-

We're contacting you to notify you that your password has been changed.

+

<%= t('.message') %>

diff --git a/src/api-umbrella/web-app/app/views/devise/mailer/reset_password_instructions.html.erb b/src/api-umbrella/web-app/app/views/devise/mailer/reset_password_instructions.html.erb index f667dc12f..f25f09dc8 100644 --- a/src/api-umbrella/web-app/app/views/devise/mailer/reset_password_instructions.html.erb +++ b/src/api-umbrella/web-app/app/views/devise/mailer/reset_password_instructions.html.erb @@ -1,8 +1,9 @@ -

Hello <%= @resource.email %>!

+<% require 'devise/version' %> +

<%= t('.greeting', recipient: @resource.email) %>

-

Someone has requested a link to change your password. You can do this through the link below.

+

<%= t('.instruction') %>

-

<%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) %>

+

<%= link_to t('.action'), edit_password_url(@resource, reset_password_token: (Devise::VERSION.start_with?('3.', '4.') ? @token : @resource.reset_password_token)) %>

-

If you didn't request this, please ignore this email.

-

Your password won't change until you access the link above and create a new one.

+

<%= t('.instruction_2') %>

+

<%= t('.instruction_3') %>

diff --git a/src/api-umbrella/web-app/app/views/devise/mailer/unlock_instructions.html.erb b/src/api-umbrella/web-app/app/views/devise/mailer/unlock_instructions.html.erb index 41e148bf2..efe1edc6d 100644 --- a/src/api-umbrella/web-app/app/views/devise/mailer/unlock_instructions.html.erb +++ b/src/api-umbrella/web-app/app/views/devise/mailer/unlock_instructions.html.erb @@ -1,7 +1,8 @@ -

Hello <%= @resource.email %>!

+<% require 'devise/version' %> +

<%= t('.greeting', recipient: @resource.email) %>

-

Your account has been locked due to an excessive number of unsuccessful sign in attempts.

+

<%= t('.message') %>

-

Click the link below to unlock your account:

+

<%= t('.instruction') %>

-

<%= link_to 'Unlock my account', unlock_url(@resource, unlock_token: @token) %>

+

<%= link_to t('.action'), unlock_url(@resource, unlock_token: (Devise::VERSION.start_with?('3.') ? @token :@resource.unlock_token)) %>

diff --git a/src/api-umbrella/web-app/app/views/devise/passwords/edit.html.erb b/src/api-umbrella/web-app/app/views/devise/passwords/edit.html.erb index 6371e1884..1c97866a2 100644 --- a/src/api-umbrella/web-app/app/views/devise/passwords/edit.html.erb +++ b/src/api-umbrella/web-app/app/views/devise/passwords/edit.html.erb @@ -1,19 +1,19 @@ -

Change your password

+

<%= t(".change_your_password") %>

diff --git a/src/api-umbrella/web-app/app/views/devise/passwords/new.html.erb b/src/api-umbrella/web-app/app/views/devise/passwords/new.html.erb index 089023ce3..b4f02df07 100644 --- a/src/api-umbrella/web-app/app/views/devise/passwords/new.html.erb +++ b/src/api-umbrella/web-app/app/views/devise/passwords/new.html.erb @@ -1,4 +1,4 @@ -

Forgot your password?

+

<%= t(".forgot_your_password") %>

- <%= f.button :submit, "Send me reset password instructions" %> + <%= f.button :submit, t(".send_me_reset_password_instructions") %>
<% end %>
diff --git a/src/api-umbrella/web-app/app/views/devise/registrations/new.html.erb b/src/api-umbrella/web-app/app/views/devise/registrations/new.html.erb index 505d4e0d8..e6654e6d5 100644 --- a/src/api-umbrella/web-app/app/views/devise/registrations/new.html.erb +++ b/src/api-umbrella/web-app/app/views/devise/registrations/new.html.erb @@ -15,7 +15,7 @@
- <%= f.button :submit, "Create Account" %> + <%= f.button :submit, t(".sign_up") %>
<% end %> diff --git a/src/api-umbrella/web-app/app/views/devise/sessions/new.html.erb b/src/api-umbrella/web-app/app/views/devise/sessions/new.html.erb index 077881160..5ed583ca2 100644 --- a/src/api-umbrella/web-app/app/views/devise/sessions/new.html.erb +++ b/src/api-umbrella/web-app/app/views/devise/sessions/new.html.erb @@ -1,4 +1,4 @@ -

Admin Sign In

+

<%= t(".admin_sign_in") %>

<% if(ApiUmbrellaConfig[:web][:admin][:auth_strategies][:enabled].include?("local")) %> @@ -12,14 +12,14 @@
<%- if devise_mapping.recoverable? %>
- <%= link_to "Forgot your password?", new_password_path(resource_name) %>
+ <%= link_to t("devise.shared.links.forgot_your_password"), new_password_path(resource_name) %>
<% end -%>
- <%= f.button :submit, "Sign in" %> + <%= f.button :submit, t(".sign_in"), :id => "sign_in" %>
<% end %> diff --git a/src/api-umbrella/web-app/app/views/devise/shared/_links.html.erb b/src/api-umbrella/web-app/app/views/devise/shared/_links.html.erb index e6a3e4196..a7070c48c 100644 --- a/src/api-umbrella/web-app/app/views/devise/shared/_links.html.erb +++ b/src/api-umbrella/web-app/app/views/devise/shared/_links.html.erb @@ -1,25 +1,25 @@ <%- if controller_name != 'sessions' %> - <%= link_to "Log in", new_session_path(resource_name) %>
+ <%= link_to t(".sign_in"), new_session_path(resource_name) %>
<% end -%> <%- if devise_mapping.registerable? && controller_name != 'registrations' %> - <%= link_to "Sign up", new_registration_path(resource_name) %>
+ <%= link_to t(".sign_up"), new_registration_path(resource_name) %>
<% end -%> -<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %> - <%= link_to "Forgot your password?", new_password_path(resource_name) %>
+<%- if devise_mapping.recoverable? && controller_name != 'passwords' %> + <%= link_to t(".forgot_your_password"), new_password_path(resource_name) %>
<% end -%> <%- if devise_mapping.confirmable? && controller_name != 'confirmations' %> - <%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %>
+ <%= link_to t('.didn_t_receive_confirmation_instructions'), new_confirmation_path(resource_name) %>
<% end -%> <%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %> - <%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %>
+ <%= link_to t('.didn_t_receive_unlock_instructions'), new_unlock_path(resource_name) %>
<% end -%> <%- if devise_mapping.omniauthable? %> <%- resource_class.omniauth_providers.each do |provider| %> - <%= link_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider) %>
+ <%= link_to t('.sign_in_with_provider', provider: provider.to_s.titleize), omniauth_authorize_path(resource_name, provider) %>
<% end -%> -<% end -%> +<% end -%> \ No newline at end of file diff --git a/src/api-umbrella/web-app/app/views/devise/unlocks/new.html.erb b/src/api-umbrella/web-app/app/views/devise/unlocks/new.html.erb index 2278259e3..f86f98f2f 100644 --- a/src/api-umbrella/web-app/app/views/devise/unlocks/new.html.erb +++ b/src/api-umbrella/web-app/app/views/devise/unlocks/new.html.erb @@ -1,16 +1,16 @@ -

Resend unlock instructions

+

<%= t(".resend_unlock_instructions") %>

- <%= simple_form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f| %> + <%= simple_form_for(resource, :as => resource_name, :url => unlock_path(resource_name), :html => { :method => :post }) do |f| %> <%= f.error_notification %> <%= f.full_error :unlock_token %>
- <%= f.input :email, required: true, autofocus: true %> + <%= f.input :email, :required => true, :autofocus => true %>
- <%= f.button :submit, "Resend unlock instructions" %> + <%= f.button :submit, t(".resend_unlock_instructions") %>
<% end %>
diff --git a/src/api-umbrella/web-app/config/initializers/i18n.rb b/src/api-umbrella/web-app/config/initializers/i18n.rb new file mode 100644 index 000000000..8737e7387 --- /dev/null +++ b/src/api-umbrella/web-app/config/initializers/i18n.rb @@ -0,0 +1,17 @@ +Rails.application.config.after_initialize do + # Copy the locale data from devise-i18n's built in data for activerecord user + # attributes to the mongoid attributes for the admin user. This is so the + # default fields like "Password" can use the data built into devise-i18n when + # using the Mongoid Admin model. + I18n.available_locales.each do |locale| + admin_data = {} + if(I18n.backend.exists?(locale, "activerecord.attributes.user")) + admin_data.deep_merge!(I18n.backend.translate(locale, "activerecord.attributes.user")) + end + if(I18n.backend.exists?(locale, "mongoid.attributes.admin")) + admin_data.deep_merge!(I18n.backend.translate(locale, "mongoid.attributes.admin")) + end + + I18n.backend.store_translations(locale, { :mongoid => { :attributes => { :admin => admin_data } } }) + end +end diff --git a/src/api-umbrella/web-app/config/locales/devise.en.yml b/src/api-umbrella/web-app/config/locales/devise.en.yml deleted file mode 100644 index bd4c3ebc6..000000000 --- a/src/api-umbrella/web-app/config/locales/devise.en.yml +++ /dev/null @@ -1,62 +0,0 @@ -# Additional translations at https://github.com/plataformatec/devise/wiki/I18n - -en: - devise: - confirmations: - confirmed: "Your email address has been successfully confirmed." - send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes." - send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes." - failure: - already_authenticated: "You are already signed in." - inactive: "Your account is not activated yet." - invalid: "Invalid %{authentication_keys} or password." - locked: "Your account is locked." - last_attempt: "You have one more attempt before your account is locked." - not_found_in_database: "Invalid %{authentication_keys} or password." - timeout: "Your session expired. Please sign in again to continue." - unauthenticated: "You need to sign in or sign up before continuing." - unconfirmed: "You have to confirm your email address before continuing." - mailer: - confirmation_instructions: - subject: "Confirmation instructions" - reset_password_instructions: - subject: "Reset password instructions" - unlock_instructions: - subject: "Unlock instructions" - password_change: - subject: "Password Changed" - omniauth_callbacks: - failure: "Could not authenticate you from %{kind} because \"%{reason}\"." - success: "Successfully authenticated from %{kind} account." - passwords: - no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided." - send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes." - send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes." - updated: "Your password has been changed successfully. You are now signed in." - updated_not_active: "Your password has been changed successfully." - registrations: - destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon." - signed_up: "Welcome! You have signed up successfully." - signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated." - signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked." - signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account." - update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirm link to confirm your new email address." - updated: "Your account has been updated successfully." - sessions: - signed_in: "Signed in successfully." - signed_out: "Signed out successfully." - already_signed_out: "Signed out successfully." - unlocks: - send_instructions: "You will receive an email with instructions for how to unlock your account in a few minutes." - send_paranoid_instructions: "If your account exists, you will receive an email with instructions for how to unlock it in a few minutes." - unlocked: "Your account has been unlocked successfully. Please sign in to continue." - errors: - messages: - already_confirmed: "was already confirmed, please try signing in" - confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one" - expired: "has expired, please request a new one" - not_found: "not found" - not_locked: "was not locked" - not_saved: - one: "1 error prohibited this %{resource} from being saved:" - other: "%{count} errors prohibited this %{resource} from being saved:" diff --git a/src/api-umbrella/web-app/config/locales/en.yml b/src/api-umbrella/web-app/config/locales/en.yml index 8349ed235..5b0ebeef3 100644 --- a/src/api-umbrella/web-app/config/locales/en.yml +++ b/src/api-umbrella/web-app/config/locales/en.yml @@ -432,3 +432,7 @@ en: description_markdown: |- The content type of the response. *Example:* `application/json; charset=utf-8` + devise: + sessions: + new: + admin_sign_in: Admin Sign In diff --git a/src/api-umbrella/web-app/config/routes.rb b/src/api-umbrella/web-app/config/routes.rb index 0b5ceb72f..b88f20bcd 100644 --- a/src/api-umbrella/web-app/config/routes.rb +++ b/src/api-umbrella/web-app/config/routes.rb @@ -114,7 +114,7 @@ # "navigator.languages" can't really seem to be changed in # Capybara+poltergeist). get "/admin/i18n_detection.js", :to => proc { |env| - locale = env["http_accept_language.parser"].compatible_language_from(I18n.available_locales) || I18n.default_locale + locale = env["http_accept_language.parser"].language_region_compatible_from(I18n.available_locales) || I18n.default_locale [ 200, diff --git a/test/admin_ui/test_locales.rb b/test/admin_ui/test_locales.rb index 11ed6bd58..4acf3f246 100644 --- a/test/admin_ui/test_locales.rb +++ b/test/admin_ui/test_locales.rb @@ -1,16 +1,63 @@ require_relative "../test_helper" -locales_root_dir = File.join(API_UMBRELLA_SRC_ROOT, "src/api-umbrella/web-app/config/locales") -I18n.load_path = Dir[File.join(locales_root_dir, "*.yml")] -I18n.backend.load_translations - class Test::AdminUi::TestLocales < Minitest::Capybara::Test include Capybara::Screenshot::MiniTestPlugin include ApiUmbrellaTestHelpers::AdminAuth include ApiUmbrellaTestHelpers::Setup + LOCALES_ROOT_DIR = File.join(API_UMBRELLA_SRC_ROOT, "src/api-umbrella/web-app/config/locales") + EXPECTED_I18N = { + :de => { + :allowed_ips => "IP-Adresse Beschränkungen", + :analytics => "Analytics", + :forgot_password => "Passwort vergessen?", + :password => "Passwort", + }, + :en => { + :allowed_ips => "Restrict Access to IPs", + :analytics => "Analytics", + :forgot_password => "Forgot your password?", + :password => "Password", + }, + :"es-419" => { + :allowed_ips => "Restringir acceso a IPs", + :analytics => "Analítica", + :forgot_password => "¿Ha olvidado su contraseña?", + :password => "Contraseña", + }, + :fi => { + :allowed_ips => "Rajoita pääsyä IP:siin", + :analytics => "Analytiikka", + :forgot_password => "Unohditko salasanasi?", + :password => "Salasana", + }, + :fr => { + :allowed_ips => "Liste noire IP", + :analytics => "Statistiques", + :forgot_password => "Mot de passe oublié ?", + :password => "Mot de passe", + }, + :it => { + :allowed_ips => "Limita Accesso ad IP", + :analytics => "Analitiche", + :forgot_password => "Password dimenticata?", + :password => "Password", + }, + :ru => { + :allowed_ips => "Ограничить доступ к IP", + :analytics => "Аналитика", + :forgot_password => "Забыли пароль?", + :password => "Пароль", + }, + }.freeze + def setup setup_server + once_per_class_setup do + # Ensure at least one admin exists so the login page can be hit directly + # without redirecting to the first-time admin create page. + FactoryGirl.create(:admin) + end end # Test all the available locales except the special test "zy" (which we use @@ -22,12 +69,12 @@ def setup define_method("test_server_side_translations_in_#{locale_method_name}_locale") do page.driver.add_headers("Accept-Language" => locale.to_s) visit "/admin/login" - refute_empty(I18n.t("omniauth_providers.developer", :locale => locale)) - assert_text(I18n.t("omniauth_providers.developer", :locale => locale)) - if(locale != :en) - refute_empty(I18n.t("omniauth_providers.developer", :locale => :en)) - refute_text(I18n.t("omniauth_providers.developer", :locale => :en)) - end + + # From devise-i18n based on attribute names + assert_i18n_text(locale, :password, find("label[for=admin_password]")) + + # From devise-i18n manually assigned in view + assert_i18n_text(locale, :forgot_password, find("a[href='/admins/password/new']")) end define_method("test_client_side_translations_in_#{locale_method_name}_locale") do @@ -36,52 +83,58 @@ def setup visit "/admin/#/api_users/new" # Form - refute_empty(I18n.t("mongoid.attributes.api/settings.allowed_ips", :locale => locale)) - assert_text(I18n.t("mongoid.attributes.api/settings.allowed_ips", :locale => locale)) - if(locale != :en) - refute_empty(I18n.t("mongoid.attributes.api/settings.allowed_ips", :locale => :en)) - refute_text(I18n.t("mongoid.attributes.api/settings.allowed_ips", :locale => :en)) - end + assert_i18n_text(locale, :allowed_ips, find("label[for$='allowedIpsString']")) # Navigation - refute_empty(I18n.t("admin.nav.analytics", :locale => locale)) - assert_text(I18n.t("admin.nav.analytics", :locale => locale)) + assert_i18n_text(locale, :analytics, find("li.nav-analytics > a")) end end def test_server_side_fall_back_to_english_for_unknown_locale - page.driver.add_headers("Accept-Language" => "zz") + locale = "zz" + page.driver.add_headers("Accept-Language" => locale) visit "/admin/login" - assert_raises I18n::InvalidLocale do - I18n.t("omniauth_providers.developer", :locale => :zz) - end - refute_empty(I18n.t("omniauth_providers.developer", :locale => :en)) - assert_text(I18n.t("omniauth_providers.developer", :locale => :en)) + + refute(File.exist?(File.join(LOCALES_ROOT_DIR, "#{locale}.yml"))) + assert_i18n_text(:en, :password, find("label[for=admin_password]")) end def test_client_side_fall_back_to_english_for_unknown_locale - page.driver.add_headers("Accept-Language" => "zz") + locale = "zz" + page.driver.add_headers("Accept-Language" => locale) admin_login visit "/admin/#/api_users/new" - assert_raises I18n::InvalidLocale do - I18n.t("mongoid.attributes.api/settings.allowed_ips", :locale => :zz) - end - refute_empty(I18n.t("mongoid.attributes.api/settings.allowed_ips", :locale => :en)) - assert_text(I18n.t("mongoid.attributes.api/settings.allowed_ips", :locale => :en)) + + refute(File.exist?(File.join(LOCALES_ROOT_DIR, "#{locale}.yml"))) + assert_i18n_text(:en, :allowed_ips, find("label[for$='allowedIpsString']")) end def test_server_side_fall_back_to_english_for_missing_data_in_known_locale - page.driver.add_headers("Accept-Language" => "zy") + locale = "zy" + page.driver.add_headers("Accept-Language" => locale) visit "/admin/login" - assert_equal("translation missing: zy.omniauth_providers.developer", I18n.t("omniauth_providers.developer", :locale => :zy)) - assert_text(I18n.t("omniauth_providers.developer", :locale => :en)) + + assert(File.exist?(File.join(LOCALES_ROOT_DIR, "#{locale}.yml"))) + assert_i18n_text(:en, :password, find("label[for=admin_password]")) end def test_client_side_fall_back_to_english_for_missing_data_in_known_locale - page.driver.add_headers("Accept-Language" => "zy") + locale = "zy" + page.driver.add_headers("Accept-Language" => locale) admin_login visit "/admin/#/api_users/new" - assert_equal("translation missing: zy.mongoid.attributes.api/settings.allowed_ips", I18n.t("mongoid.attributes.api/settings.allowed_ips", :locale => :zy)) - assert_text(I18n.t("mongoid.attributes.api/settings.allowed_ips", :locale => :en)) + + assert(File.exist?(File.join(LOCALES_ROOT_DIR, "#{locale}.yml"))) + assert_i18n_text(:en, :allowed_ips, find("label[for$='allowedIpsString']")) + end + + private + + def assert_i18n_text(expected_locale, expected_key, element) + assert(element) + + expected_text = EXPECTED_I18N.fetch(expected_locale).fetch(expected_key) + refute_empty(expected_text) + assert_equal(expected_text, element.text) end end diff --git a/test/support/api_umbrella_test_helpers/admin_auth.rb b/test/support/api_umbrella_test_helpers/admin_auth.rb index 9843f33e1..3e0cb9e2a 100644 --- a/test/support/api_umbrella_test_helpers/admin_auth.rb +++ b/test/support/api_umbrella_test_helpers/admin_auth.rb @@ -6,9 +6,9 @@ def admin_login(admin = nil) admin ||= FactoryGirl.create(:admin, :encrypted_password => BCrypt::Password.create("password")) visit "/admin/login" - fill_in "Email", :with => admin.username - fill_in "Password", :with => "password" - click_button "Sign in" + fill_in "admin_username", :with => admin.username + fill_in "admin_password", :with => "password" + click_button "sign_in" assert_logged_in(admin) end From a28efea4e8b05fc48494ce3401e3007f96ae9f7a Mon Sep 17 00:00:00 2001 From: Nick Muerdter Date: Wed, 11 Jan 2017 21:45:57 -0700 Subject: [PATCH 08/26] Disable CSRF checking for API requests where there won't be tokens. Since adding the new login process with local accounts, we had flipped the ApplicationController into "exception" mode for CSRF protection. However, this broke our POST/PUT API endpoints, since those requests won't have the CSRF token (we're relying on API keys and admin tokens instead). So this restores the previous behavior of just nullifying the session for API-specific endpoints instead of throwing exceptions when the CSRF token is missing. --- .../web-app/app/controllers/api/api_users_controller.rb | 3 +++ .../web-app/app/controllers/api/health_checks_controller.rb | 3 +++ .../web-app/app/controllers/api/v0/analytics_controller.rb | 3 +++ .../web-app/app/controllers/api/v1/base_controller.rb | 3 +++ 4 files changed, 12 insertions(+) diff --git a/src/api-umbrella/web-app/app/controllers/api/api_users_controller.rb b/src/api-umbrella/web-app/app/controllers/api/api_users_controller.rb index 03abed69d..9d5414e9f 100644 --- a/src/api-umbrella/web-app/app/controllers/api/api_users_controller.rb +++ b/src/api-umbrella/web-app/app/controllers/api/api_users_controller.rb @@ -1,4 +1,7 @@ class Api::ApiUsersController < ApplicationController + # API requests won't pass CSRF tokens, so don't reject requests without them. + protect_from_forgery :with => :null_session + def validate @user = ApiUser.where(:api_key => params[:id]).first diff --git a/src/api-umbrella/web-app/app/controllers/api/health_checks_controller.rb b/src/api-umbrella/web-app/app/controllers/api/health_checks_controller.rb index 36eb47c55..024e80bbc 100644 --- a/src/api-umbrella/web-app/app/controllers/api/health_checks_controller.rb +++ b/src/api-umbrella/web-app/app/controllers/api/health_checks_controller.rb @@ -1,4 +1,7 @@ class Api::HealthChecksController < ApplicationController + # API requests won't pass CSRF tokens, so don't reject requests without them. + protect_from_forgery :with => :null_session + def ip render(:json => { :ip => request.ip }) end diff --git a/src/api-umbrella/web-app/app/controllers/api/v0/analytics_controller.rb b/src/api-umbrella/web-app/app/controllers/api/v0/analytics_controller.rb index 466737787..aef6d784a 100644 --- a/src/api-umbrella/web-app/app/controllers/api/v0/analytics_controller.rb +++ b/src/api-umbrella/web-app/app/controllers/api/v0/analytics_controller.rb @@ -1,4 +1,7 @@ class Api::V0::AnalyticsController < Api::V1::BaseController + # API requests won't pass CSRF tokens, so don't reject requests without them. + protect_from_forgery :with => :null_session + before_action :set_analytics_adapter skip_before_action :authenticate_admin!, :only => [:summary] skip_after_action :verify_authorized, :only => [:summary] diff --git a/src/api-umbrella/web-app/app/controllers/api/v1/base_controller.rb b/src/api-umbrella/web-app/app/controllers/api/v1/base_controller.rb index b027e3e9c..b3bba6d41 100644 --- a/src/api-umbrella/web-app/app/controllers/api/v1/base_controller.rb +++ b/src/api-umbrella/web-app/app/controllers/api/v1/base_controller.rb @@ -1,4 +1,7 @@ class Api::V1::BaseController < ApplicationController + # API requests won't pass CSRF tokens, so don't reject requests without them. + protect_from_forgery :with => :null_session + # Try authenticating from an admin token (for direct API access). before_action :authenticate_admin_from_token! From 2c3f806025863acebc02a6c18a71591abe1e5b25 Mon Sep 17 00:00:00 2001 From: Nick Muerdter Date: Wed, 11 Jan 2017 22:19:09 -0700 Subject: [PATCH 09/26] Add missing Rails secret token for asset precompiling phase. This got changed around during the Rails 4.2 upgrade. Also remove no longer used DEVISE_SECRET_KEY environment variable reference. --- build/cmake/core-web-app.cmake | 2 +- deploy/config/deploy.rb | 1 - src/api-umbrella/web-app/config/secrets.yml | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/build/cmake/core-web-app.cmake b/build/cmake/core-web-app.cmake index b79ac2dd4..3613bd62b 100644 --- a/build/cmake/core-web-app.cmake +++ b/build/cmake/core-web-app.cmake @@ -25,7 +25,7 @@ add_custom_command( ${STAMP_DIR}/core-web-app-bundle ${web_asset_files} ${CMAKE_SOURCE_DIR}/src/api-umbrella/web-app/config/initializers/assets.rb - COMMAND env PATH=${STAGE_EMBEDDED_DIR}/bin:$ENV{PATH} BUNDLE_GEMFILE=${CMAKE_SOURCE_DIR}/src/api-umbrella/web-app/Gemfile BUNDLE_APP_CONFIG=${WORK_DIR}/src/web-app/.bundle RAILS_TMP_PATH=/tmp/web-app-build RAILS_PUBLIC_PATH=${CORE_BUILD_DIR}/tmp/web-app-build RAILS_ENV=production bundle exec rake -f ${CMAKE_SOURCE_DIR}/src/api-umbrella/web-app/Rakefile assets:clobber assets:precompile + COMMAND env PATH=${STAGE_EMBEDDED_DIR}/bin:$ENV{PATH} BUNDLE_GEMFILE=${CMAKE_SOURCE_DIR}/src/api-umbrella/web-app/Gemfile BUNDLE_APP_CONFIG=${WORK_DIR}/src/web-app/.bundle RAILS_TMP_PATH=/tmp/web-app-build RAILS_PUBLIC_PATH=${CORE_BUILD_DIR}/tmp/web-app-build RAILS_ENV=production RAILS_SECRET_TOKEN=temp bundle exec rake -f ${CMAKE_SOURCE_DIR}/src/api-umbrella/web-app/Rakefile assets:clobber assets:precompile COMMAND touch ${STAMP_DIR}/core-web-app-precompile ) diff --git a/deploy/config/deploy.rb b/deploy/config/deploy.rb index 1fb943347..fd5868aa1 100644 --- a/deploy/config/deploy.rb +++ b/deploy/config/deploy.rb @@ -51,7 +51,6 @@ # web app is started. But for rake task purposes (like asset precompilation # where these don't matter), just set some dummy values during deploy. "RAILS_SECRET_TOKEN" => "TEMP", - "DEVISE_SECRET_KEY" => "TEMP", }) # Default value for keep_releases is 5 diff --git a/src/api-umbrella/web-app/config/secrets.yml b/src/api-umbrella/web-app/config/secrets.yml index 0ffae1256..b5693fd3c 100644 --- a/src/api-umbrella/web-app/config/secrets.yml +++ b/src/api-umbrella/web-app/config/secrets.yml @@ -19,4 +19,4 @@ test: # Do not keep production secrets in the repository, # instead read values from the environment. production: - secret_key_base: <%= ENV["SECRET_KEY_BASE"] || ApiUmbrellaConfig[:web][:rails_secret_token] %> + secret_key_base: <%= ENV["RAILS_SECRET_TOKEN"] || ApiUmbrellaConfig[:web][:rails_secret_token] %> From 2b40456f97074c963844bd120b03e5382ed7c89f Mon Sep 17 00:00:00 2001 From: Nick Muerdter Date: Wed, 11 Jan 2017 22:21:36 -0700 Subject: [PATCH 10/26] Adjust tests due to new sign in page title. --- build/package/verify/spec/localhost/service_spec.rb | 2 +- test/admin_ui/test_page_title.rb | 2 +- test/admin_ui/test_version_display.rb | 2 +- test/proxy/routing/test_admin.rb | 2 +- test/support/api_umbrella_shared_tests/routing.rb | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/build/package/verify/spec/localhost/service_spec.rb b/build/package/verify/spec/localhost/service_spec.rb index 76bc2bab2..bacc07501 100644 --- a/build/package/verify/spec/localhost/service_spec.rb +++ b/build/package/verify/spec/localhost/service_spec.rb @@ -317,7 +317,7 @@ def install_package(version) it "admin login page loads" do response = RestClient::Request.execute(:method => :get, :url => "https://localhost/admin/login", :verify_ssl => false) - expect(response).to include("Admin Login") + expect(response).to include("Admin Sign In") end it "gatekeeper blocks key-less requests" do diff --git a/test/admin_ui/test_page_title.rb b/test/admin_ui/test_page_title.rb index 6280ca895..c96eff693 100644 --- a/test/admin_ui/test_page_title.rb +++ b/test/admin_ui/test_page_title.rb @@ -11,7 +11,7 @@ def setup def test_rails_login_page_title visit "/admin/" - assert_content("Admin Login") + assert_content("Admin Sign In") assert_equal("API Umbrella Admin", page.title) end diff --git a/test/admin_ui/test_version_display.rb b/test/admin_ui/test_version_display.rb index 6418a930a..8a029a4a6 100644 --- a/test/admin_ui/test_version_display.rb +++ b/test/admin_ui/test_version_display.rb @@ -12,7 +12,7 @@ def setup def test_rails_login_page_no_version visit "/admin/" - assert_content("Admin Login") + assert_content("Admin Sign In") refute_content("API Umbrella Version") refute_content(@expected_version) end diff --git a/test/proxy/routing/test_admin.rb b/test/proxy/routing/test_admin.rb index fe9e30dc5..caed72dda 100644 --- a/test/proxy/routing/test_admin.rb +++ b/test/proxy/routing/test_admin.rb @@ -63,7 +63,7 @@ def test_gives_precedence_to_admin_over_api_prefixes ]) do response = Typhoeus.get("https://127.0.0.1:9081/admin/login", keyless_http_options) assert_response_code(200, response) - assert_match("Admin Login", response.body) + assert_match("Admin Sign In", response.body) end end end diff --git a/test/support/api_umbrella_shared_tests/routing.rb b/test/support/api_umbrella_shared_tests/routing.rb index 4e4977924..cd7721d3a 100644 --- a/test/support/api_umbrella_shared_tests/routing.rb +++ b/test/support/api_umbrella_shared_tests/routing.rb @@ -248,7 +248,7 @@ def test_admin_ui_wildcard_host def test_admin_web_app response = Typhoeus.get("https://127.0.0.1:9081/admin/login", keyless_http_options) assert_response_code(200, response) - assert_match("Admin Login", response.body) + assert_match("Admin Sign In", response.body) end def test_admin_web_app_wildcard_host @@ -271,7 +271,7 @@ def test_admin_web_app_wildcard_host end else assert_response_code(200, response) - assert_match("Admin Login", response.body) + assert_match("Admin Sign In", response.body) end end end From 78b6ec55a45a28bace1fc9f9e7636f1462d533bc Mon Sep 17 00:00:00 2001 From: Nick Muerdter Date: Sat, 14 Jan 2017 16:39:04 -0700 Subject: [PATCH 11/26] Fixes and get tests running again after local admin account additions. - Consolidate where "rails_secret_token" is set for the test environment, so it behaves more like production and can be read more easily for tests. - Fix validation error message display to restore more consistent behavior between client side and server side, since we switched server side to use i18n full_messages. - Fix client-side validation errors not displaying inline on the form after hitting submit. - Get rid of unused "devise_secret_key" setting after Rails 4 upgrade. - Upgrade to released version of mongoid-embedded-errors gem that has Mongoid 4+ compatibility. - In the admin auth endpoint, just return the needed metadata about the current admin user. This prevents us from returning a bunch of other things, like the encrypted_password attribute to the client. - Fix server side validation response using full_messages leading to an error because mongoid-embedded-errors and full_messages was trying to set new keys in the errors hash while we looped over it. - Add more tests for the validation display handling. - Fix various tests due to the local admin changes. --- config/default.yml | 3 +- config/test.yml | 1 + .../admin-ui/app/components/error-messages.js | 24 +++- .../components/form-fields/field-wrapper.js | 6 + src/api-umbrella/cli/read_config.lua | 1 - src/api-umbrella/web-app/Gemfile | 5 +- src/api-umbrella/web-app/Gemfile.lock | 23 ++-- .../controllers/admin/sessions_controller.rb | 7 +- .../app/controllers/admin/stats_controller.rb | 3 + .../app/controllers/api/v1/base_controller.rb | 19 ++- .../web-app/config/locales/en.yml | 1 + src/api-umbrella/web-app/config/secrets.yml | 2 +- .../admin_ui/login/test_external_providers.rb | 2 +- test/admin_ui/login/test_first_time_setup.rb | 4 +- test/admin_ui/login/test_forgot_password.rb | 8 +- .../login/test_initial_superuser_seeding.rb | 40 ++++++ test/admin_ui/test_admins.rb | 4 +- test/admin_ui/test_locales.rb | 2 +- test/admin_ui/test_validations.rb | 130 ++++++++++++++++++ test/apis/admin/test_auth.rb | 16 --- test/factories/admins.rb | 3 +- test/proxy/test_config.rb | 38 ++++- test/proxy/test_database_seeding.rb | 10 -- .../api_umbrella_test_helpers/admin_auth.rb | 23 +++- 24 files changed, 302 insertions(+), 73 deletions(-) create mode 100644 test/admin_ui/login/test_initial_superuser_seeding.rb create mode 100644 test/admin_ui/test_validations.rb diff --git a/config/default.yml b/config/default.yml index 0ba3d651e..b0d1e34eb 100644 --- a/config/default.yml +++ b/config/default.yml @@ -74,7 +74,6 @@ web: host: 127.0.0.1 port: 14012 rails_secret_token: - devise_secret_key: puma: workers: 2 min_threads: 2 @@ -101,6 +100,8 @@ web: client_secret: ldap: options: {} + mailer: + smtp_settings: static_site: host: 127.0.0.1 port: 14013 diff --git a/config/test.yml b/config/test.yml index a56dfea58..1307c1c88 100644 --- a/config/test.yml +++ b/config/test.yml @@ -35,6 +35,7 @@ api_server: port: 13010 web: port: 13012 + rails_secret_token: aeec385fb48a0594b6bb0b18f62473190f1d01b0b6113766af525be2ae1a317a03ab0ee1b3ee6aca3fb1572dc87684e033dcec21acd90d0ca0f111ca1785d0e9 admin: auth_strategies: facebook: diff --git a/src/api-umbrella/admin-ui/app/components/error-messages.js b/src/api-umbrella/admin-ui/app/components/error-messages.js index 23028ea89..a500ecc7b 100644 --- a/src/api-umbrella/admin-ui/app/components/error-messages.js +++ b/src/api-umbrella/admin-ui/app/components/error-messages.js @@ -3,6 +3,7 @@ import Ember from 'ember'; export default Ember.Component.extend({ messages: Ember.computed('model.clientErrors', 'model.serverErrors', function() { let errors = []; + let modelI18nRoot = 'mongoid.attributes.' + this.get('model.constructor.modelName').replace('-', '_'); let clientErrors = this.get('model.clientErrors'); if(clientErrors) { @@ -27,7 +28,7 @@ export default Ember.Component.extend({ if(serverErrors) { if(_.isArray(serverErrors)) { _.each(serverErrors, function(serverError) { - let message = serverError.full_message || serverError.message; + let message = serverError.message; if(!message && serverError.title) { message = serverError.title; if(serverError.status) { @@ -39,6 +40,7 @@ export default Ember.Component.extend({ errors.push({ attribute: serverError.field, message: message, + fullMessage: serverError.full_message, }); } else { errors.push({ message: 'Unexpected error' }); @@ -51,7 +53,25 @@ export default Ember.Component.extend({ let messages = []; _.each(errors, function(error) { - let message = error.message || 'Unexpected error'; + let message = ''; + if(error.fullMessage) { + message += error.fullMessage; + } else if(error.attribute && error.attribute !== 'base') { + let attributeTitle = I18n.t(modelI18nRoot + '.' + inflection.underscore(error.attribute), { defaultValue: false }); + if(attributeTitle === false) { + attributeTitle = inflection.titleize(inflection.underscore(error.attribute)); + } + + message += attributeTitle + ': '; + message += error.message || 'Unexpected error'; + } else { + if(error.message) { + message += error.message.charAt(0).toUpperCase() + error.message.slice(1); + } else { + message += 'Unexpected error'; + } + } + messages.push(marked(message)); }); diff --git a/src/api-umbrella/admin-ui/app/components/form-fields/field-wrapper.js b/src/api-umbrella/admin-ui/app/components/form-fields/field-wrapper.js index 9970e1441..e1ce1f2a3 100644 --- a/src/api-umbrella/admin-ui/app/components/form-fields/field-wrapper.js +++ b/src/api-umbrella/admin-ui/app/components/form-fields/field-wrapper.js @@ -42,6 +42,12 @@ export default Ember.Component.extend({ this.set('canShowErrors', true); }, + // If the page is submitted, show any errors on the page (even if the fields + // haven't been focused and then unfocused yet). + showErrorsOnSubmit: Ember.observer('model.clientErrors', function() { + this.set('canShowErrors', true); + }), + // Anytime the model changes, reset the error display so errors aren't // displayed until the field is unfocused again. // diff --git a/src/api-umbrella/cli/read_config.lua b/src/api-umbrella/cli/read_config.lua index 1d5a2d672..72c611d69 100644 --- a/src/api-umbrella/cli/read_config.lua +++ b/src/api-umbrella/cli/read_config.lua @@ -126,7 +126,6 @@ local function set_cached_random_tokens() local cached = { web = { rails_secret_token = random_token(128), - devise_secret_key = random_token(128), }, static_site = { api_key = random_token(40), diff --git a/src/api-umbrella/web-app/Gemfile b/src/api-umbrella/web-app/Gemfile index dfdd48a2b..781397156 100644 --- a/src/api-umbrella/web-app/Gemfile +++ b/src/api-umbrella/web-app/Gemfile @@ -34,10 +34,7 @@ gem "mongoid-paranoia", "~> 2.0.0" gem "mongoid_delorean", "~> 1.3.0" # Display deeply nested validation errors on embedded documents. -# -# Fork to fix Mongoid 4+ compatibility: -# https://github.com/glooko/mongoid-embedded-errors/pull/6 -gem "mongoid-embedded-errors", "~> 2.0.1", :git => "https://github.com/calfzhou/mongoid-embedded-errors.git" +gem "mongoid-embedded-errors", "~> 2.1.1" # Data migrations gem "mongoid_rails_migrations", "~> 1.1.0" diff --git a/src/api-umbrella/web-app/Gemfile.lock b/src/api-umbrella/web-app/Gemfile.lock index 97365d09a..2e15f075f 100644 --- a/src/api-umbrella/web-app/Gemfile.lock +++ b/src/api-umbrella/web-app/Gemfile.lock @@ -23,13 +23,6 @@ GIT activesupport (>= 3) mongoid (>= 3) -GIT - remote: https://github.com/calfzhou/mongoid-embedded-errors.git - revision: 4add4cd70154c34f79b5db8781ef0b13da23cec9 - specs: - mongoid-embedded-errors (2.0.1) - mongoid (>= 3.0.0) - GIT remote: https://github.com/intridea/omniauth-github.git revision: 45f2fc73d6d06f30863adac0e6aa112bcaaadf67 @@ -98,8 +91,8 @@ GEM autoprefixer-rails (>= 5.2.1) sass (>= 3.3.4) brakeman (3.4.1) - bson (4.2.0) - builder (3.2.2) + bson (4.2.1) + builder (3.2.3) bundler-audit (0.5.0) bundler (~> 1.2) thor (~> 0.18) @@ -168,7 +161,7 @@ GEM jbuilder (2.6.1) activesupport (>= 3.0.0, < 5.1) multi_json (~> 1.2) - json (1.8.3) + json (1.8.6) jwt (1.5.6) kramdown (1.13.1) loofah (2.0.3) @@ -183,8 +176,8 @@ GEM money (6.7.1) i18n (>= 0.6.4, <= 0.7.0) sixarm_ruby_unaccent (>= 1.1.1, < 2) - mongo (2.4.0) - bson (~> 4.2.0) + mongo (2.4.1) + bson (>= 4.2.1, < 5.0.0) mongoid (5.1.6) activemodel (~> 4.0) mongo (~> 2.1) @@ -193,6 +186,8 @@ GEM mongoid-compatibility (0.4.0) activesupport mongoid (>= 2.0) + mongoid-embedded-errors (2.1.1) + mongoid (>= 3.0) mongoid-paranoia (2.0.0) activesupport (~> 4.0) mongoid (>= 4.0.0, <= 6.0.0) @@ -241,7 +236,7 @@ GEM omniauth-oauth2 (1.4.0) oauth2 (~> 1.0) omniauth (~> 1.2) - origin (2.2.2) + origin (2.3.0) orm_adapter (0.5.0) parslet (1.7.1) blankslate (>= 2.0, <= 4.0) @@ -356,7 +351,7 @@ DEPENDENCIES kramdown (~> 1.13.1) lucene_query_parser! mongoid (~> 5.1.6) - mongoid-embedded-errors (~> 2.0.1)! + mongoid-embedded-errors (~> 2.1.1) mongoid-paranoia (~> 2.0.0) mongoid-store! mongoid_delorean (~> 1.3.0) diff --git a/src/api-umbrella/web-app/app/controllers/admin/sessions_controller.rb b/src/api-umbrella/web-app/app/controllers/admin/sessions_controller.rb index 5b0bf3de6..9dd6dbbad 100644 --- a/src/api-umbrella/web-app/app/controllers/admin/sessions_controller.rb +++ b/src/api-umbrella/web-app/app/controllers/admin/sessions_controller.rb @@ -10,7 +10,12 @@ def auth if current_admin response["api_umbrella_version"] = API_UMBRELLA_VERSION - response["admin"] = current_admin.as_json + response["admin"] = current_admin.as_json.slice( + "email", + "id", + "superuser", + "username", + ) response["api_key"] = ApiUser.where(:email => "web.admin.ajax@internal.apiumbrella").order_by(:created_at.asc).first.api_key response["csrf_token"] = form_authenticity_token if(protect_against_forgery?) end diff --git a/src/api-umbrella/web-app/app/controllers/admin/stats_controller.rb b/src/api-umbrella/web-app/app/controllers/admin/stats_controller.rb index f149949a5..0a66257e3 100644 --- a/src/api-umbrella/web-app/app/controllers/admin/stats_controller.rb +++ b/src/api-umbrella/web-app/app/controllers/admin/stats_controller.rb @@ -1,6 +1,9 @@ require "csv_streamer" class Admin::StatsController < Admin::BaseController + # API requests won't pass CSRF tokens, so don't reject requests without them. + protect_from_forgery :with => :null_session + before_action :set_analytics_adapter around_action :set_time_zone skip_after_action :verify_authorized diff --git a/src/api-umbrella/web-app/app/controllers/api/v1/base_controller.rb b/src/api-umbrella/web-app/app/controllers/api/v1/base_controller.rb index b3bba6d41..e640bd5da 100644 --- a/src/api-umbrella/web-app/app/controllers/api/v1/base_controller.rb +++ b/src/api-umbrella/web-app/app/controllers/api/v1/base_controller.rb @@ -61,13 +61,18 @@ def user_not_authorized(exception) def errors_response(record) response = { :errors => [] } - record.errors.each do |field, message| - response[:errors] << { - :code => "INVALID_INPUT", - :message => message, - :field => field, - :full_message => record.errors.full_message(field, message), - } + record.errors.to_hash.each do |field, field_messages| + field_messages.each do |message| + full_message = record.errors.full_message(field, message) + full_message[0] = full_message[0].upcase + + response[:errors] << { + :code => "INVALID_INPUT", + :message => message, + :field => field, + :full_message => full_message, + } + end end response diff --git a/src/api-umbrella/web-app/config/locales/en.yml b/src/api-umbrella/web-app/config/locales/en.yml index 5b0ebeef3..e784dc33d 100644 --- a/src/api-umbrella/web-app/config/locales/en.yml +++ b/src/api-umbrella/web-app/config/locales/en.yml @@ -8,6 +8,7 @@ en: google_oauth2: Google ldap: LDAP errors: + format: "%{attribute}: %{message}" messages: invalid_host_format: must be in the format of "example.com" invalid_url_prefix_format: must start with "/" diff --git a/src/api-umbrella/web-app/config/secrets.yml b/src/api-umbrella/web-app/config/secrets.yml index b5693fd3c..b92340b6b 100644 --- a/src/api-umbrella/web-app/config/secrets.yml +++ b/src/api-umbrella/web-app/config/secrets.yml @@ -14,7 +14,7 @@ development: secret_key_base: 9362d7f88f100d3424229b6c54ff776734927bf3ce1bd67134174d3474b647c8b6e9cd046478bdaa3d640bfad1cd280535fc1198233a6808ce42a0e42ea4b2ea test: - secret_key_base: aeec385fb48a0594b6bb0b18f62473190f1d01b0b6113766af525be2ae1a317a03ab0ee1b3ee6aca3fb1572dc87684e033dcec21acd90d0ca0f111ca1785d0e9 + secret_key_base: <%= ENV["RAILS_SECRET_TOKEN"] || ApiUmbrellaConfig[:web][:rails_secret_token] %> # Do not keep production secrets in the repository, # instead read values from the environment. diff --git a/test/admin_ui/login/test_external_providers.rb b/test/admin_ui/login/test_external_providers.rb index c197a452f..ea90a2a3a 100644 --- a/test/admin_ui/login/test_external_providers.rb +++ b/test/admin_ui/login/test_external_providers.rb @@ -38,7 +38,7 @@ def test_forbids_first_time_admin_creation assert_first_time_admin_creation_forbidden end - def test_shows_external_login_links_in_order_no_local_fields + def test_shows_external_login_links_in_order_and_no_local_fields visit "/admin/login" assert_content("Admin Sign In") diff --git a/test/admin_ui/login/test_first_time_setup.rb b/test/admin_ui/login/test_first_time_setup.rb index e3010329b..34bca88be 100644 --- a/test/admin_ui/login/test_first_time_setup.rb +++ b/test/admin_ui/login/test_first_time_setup.rb @@ -21,7 +21,7 @@ def test_redirects_to_signup_on_first_login fill_in "Email", :with => "new@example.com" fill_in "Password", :with => "password123456" fill_in "Password Confirmation", :with => "password123456" - click_button "Create Account" + click_button "Sign up" # Ensure the user gets logged in. assert_logged_in @@ -68,7 +68,7 @@ def test_redirects_away_from_submit_if_admin_exists FactoryGirl.create(:admin) assert_equal(1, Admin.count) - click_button "Create Account" + click_button "Sign up" assert_content("Admin Sign In") assert_content("An initial admin account already exists.") diff --git a/test/admin_ui/login/test_forgot_password.rb b/test/admin_ui/login/test_forgot_password.rb index 4af530d09..1809fb3a1 100644 --- a/test/admin_ui/login/test_forgot_password.rb +++ b/test/admin_ui/login/test_forgot_password.rb @@ -27,7 +27,8 @@ def test_reset_process admin = FactoryGirl.create(:admin, :username => "admin@example.com") assert_nil(admin.reset_password_token) assert_nil(admin.reset_password_sent_at) - assert_nil(admin.encrypted_password) + original_encrypted_password = admin.encrypted_password + assert(original_encrypted_password) visit "/admins/password/new" @@ -40,7 +41,7 @@ def test_reset_process admin.reload assert(admin.reset_password_token) assert(admin.reset_password_sent_at) - assert_nil(admin.encrypted_password) + assert_equal(original_encrypted_password, admin.encrypted_password) # Find sent email messages = delayed_job_sent_messages @@ -62,7 +63,7 @@ def test_reset_process visit reset_url fill_in "New password", :with => "password" - fill_in "Confirm your new password", :with => "password" + fill_in "Confirm new password", :with => "password" click_button "Change my password" # Ensure the user gets logged in. @@ -73,5 +74,6 @@ def test_reset_process assert_nil(admin.reset_password_token) assert_nil(admin.reset_password_sent_at) assert(admin.encrypted_password) + refute_equal(original_encrypted_password, admin.encrypted_password) end end diff --git a/test/admin_ui/login/test_initial_superuser_seeding.rb b/test/admin_ui/login/test_initial_superuser_seeding.rb new file mode 100644 index 000000000..cd3d203e1 --- /dev/null +++ b/test/admin_ui/login/test_initial_superuser_seeding.rb @@ -0,0 +1,40 @@ +require_relative "../../test_helper" + +class Test::AdminUi::Login::TestInitialSuperuserSeeding < Minitest::Test + include ApiUmbrellaTestHelpers::Setup + include Minitest::Hooks + + def setup + setup_server + once_per_class_setup do + Admin.delete_all + assert_equal(0, Admin.count) + + override_config_set({ + "web" => { + "admin" => { + "initial_superusers" => [ + "initial.admin@example.com", + ], + }, + }, + }, ["--router"]) + end + end + + def after_all + super + override_config_reset(["--router"]) + end + + def test_initial_superusers + admins = Admin.where(:username => "initial.admin@example.com").all + assert_equal(1, admins.length) + + admin = admins.first.attributes + assert(admin["superuser"]) + assert_match(/\A[0-9a-f\-]{36}\z/, admin["_id"]) + assert_match(/\A[a-zA-Z0-9]{40}\z/, admin["authentication_token"]) + assert_nil(admin["encrypted_password"]) + end +end diff --git a/test/admin_ui/test_admins.rb b/test/admin_ui/test_admins.rb index 5f8c35ccc..2ac68054d 100644 --- a/test/admin_ui/test_admins.rb +++ b/test/admin_ui/test_admins.rb @@ -13,7 +13,7 @@ def test_superuser_checkbox_as_superuser_admin admin_login visit "/admin/#/admins/new" - assert_content("Username") + assert_content("Email") assert_content("Superuser") end @@ -21,7 +21,7 @@ def test_superuser_checkbox_as_limited_admin admin_login(FactoryGirl.create(:limited_admin)) visit "/admin/#/admins/new" - assert_content("Username") + assert_content("Email") refute_content("Superuser") end diff --git a/test/admin_ui/test_locales.rb b/test/admin_ui/test_locales.rb index 4acf3f246..d94c45275 100644 --- a/test/admin_ui/test_locales.rb +++ b/test/admin_ui/test_locales.rb @@ -62,7 +62,7 @@ def setup # Test all the available locales except the special test "zy" (which we use # to test for incomplete data). - valid_locales = I18n.available_locales - [:zy] + valid_locales = EXPECTED_I18N.keys valid_locales.each do |locale| locale_method_name = locale.to_s.downcase.gsub(/[^\w]/, "_") diff --git a/test/admin_ui/test_validations.rb b/test/admin_ui/test_validations.rb new file mode 100644 index 000000000..0455749ca --- /dev/null +++ b/test/admin_ui/test_validations.rb @@ -0,0 +1,130 @@ +require_relative "../test_helper" + +class Test::AdminUi::TestValidations < Minitest::Capybara::Test + include Capybara::Screenshot::MiniTestPlugin + include ApiUmbrellaTestHelpers::AdminAuth + include ApiUmbrellaTestHelpers::Setup + + def setup + setup_server + + Api.delete_all + end + + def test_client_side_validations + admin_login + visit "/admin/#/api_users/new" + + # Messages + refute_text("Oops") + + # Top messages container + refute_selector(".alert-danger") + refute_selector(".error-messages") + + # Inline messages + refute_selector(".has-error") + refute_selector(".with-errors") + + # Trigger validations with save of empty form. + click_button("Save") + + # Messages + assert_text("Oops") + assert_text("can't be blank") + messages = page.all(".error-messages li").map { |msg| msg.text } + assert_equal([ + "First Name: This field can't be blank", + "Last Name: This field can't be blank", + "Email: This field can't be blank", + ], messages) + + # Top messages container + assert_selector(".alert-danger") + assert_selector(".error-messages") + + # Inline messages + assert_selector(".has-error", :count => 3) + assert_selector(".with-errors", :count => 3) + end + + def test_inline_client_side_validations_on_blur + admin_login + visit "/admin/#/api_users/new" + + refute_selector(".has-error") + refute_selector(".with-errors") + + find_field("E-mail").trigger("blur") + + assert_selector(".has-error", :count => 1) + assert_selector(".with-errors", :count => 1) + end + + def test_server_side_validations + admin_login + visit "/admin/#/api_users/new" + + # Messages + refute_text("Oops") + refute_text("can't be blank") + + # Top messages container + refute_selector(".alert-danger") + refute_selector(".error-messages") + + # Inline messages + refute_selector(".has-error") + refute_selector(".with-errors") + + # Trigger validations with save of filled out (but invalid) form. + fill_in "E-mail", :with => "invalid" + fill_in "First Name", :with => "John" + fill_in "Last Name", :with => "Doe" + click_button("Save") + + # Messages + assert_text("Oops") + messages = page.all(".error-messages li").map { |msg| msg.text } + assert_equal([ + "Email: Provide a valid email address.", + "Terms and conditions: Check the box to agree to the terms and conditions.", + ], messages) + + # Top messages container + assert_selector(".alert-danger") + assert_selector(".error-messages") + + # Inline messages + refute_selector(".has-error") + refute_selector(".with-errors") + end + + def test_i18n_client_side + admin_login + visit "/admin/#/admins/new" + + click_button("Save") + + assert_text("Oops") + messages = page.all(".error-messages li").map { |msg| msg.text } + assert_equal([ + "Email: This field can't be blank", + ], messages) + end + + def test_i18n_server_side + admin_login + visit "/admin/#/admins/new" + + fill_in "Email", :with => "invalid" + click_button("Save") + + assert_text("Oops") + messages = page.all(".error-messages li").map { |msg| msg.text } + assert_equal([ + "Email: is invalid", + "Groups: must belong to at least one group or be a superuser", + ], messages) + end +end diff --git a/test/apis/admin/test_auth.rb b/test/apis/admin/test_auth.rb index 6490a34a3..b4113153a 100644 --- a/test/apis/admin/test_auth.rb +++ b/test/apis/admin/test_auth.rb @@ -48,26 +48,10 @@ def test_authenticated assert_includes([TrueClass, FalseClass], data["enable_beta_analytics"].class) assert_equal([ - "created_at", - "created_by", - "current_sign_in_at", - "current_sign_in_ip", - "deleted_at", "email", - "group_ids", - "group_names", "id", - "last_sign_in_at", - "last_sign_in_ip", - "last_sign_in_provider", - "name", - "notes", - "sign_in_count", "superuser", - "updated_at", - "updated_by", "username", - "version", ].sort, data["admin"].keys.sort) assert_equal(File.read(File.join(API_UMBRELLA_SRC_ROOT, "src/api-umbrella/version.txt")).strip, data["api_umbrella_version"]) assert_equal(true, data["authenticated"]) diff --git a/test/factories/admins.rb b/test/factories/admins.rb index cbedad576..e94a1c3d4 100644 --- a/test/factories/admins.rb +++ b/test/factories/admins.rb @@ -1,7 +1,8 @@ FactoryGirl.define do factory :admin do sequence(:username) { |n| "aburnside#{n}@example.com" } - sequence(:email) { username } + email { username } + encrypted_password { BCrypt::Password.create("password") } superuser true factory :limited_admin do diff --git a/test/proxy/test_config.rb b/test/proxy/test_config.rb index 489470ff4..e3b6f6896 100644 --- a/test/proxy/test_config.rb +++ b/test/proxy/test_config.rb @@ -8,9 +8,43 @@ def setup setup_server end - def test_overrides_default_null_value + # Since lyaml reads in null values as a special object type, ensure that when + # deep merging occurs, this null value gets overwritten by other object + # types. + def test_overrides_default_null_value_with_hash default_config = YAML.load_file(File.join(API_UMBRELLA_SRC_ROOT, "config/default.yml")) - assert_nil(default_config["web"]["admin"]["auth_strategies"]["ldap"]["options"]) + assert(default_config["web"]["mailer"].key?("smtp_settings")) + assert_nil(default_config["web"]["mailer"]["smtp_settings"]) + + expected_test_value = { + "address" => "127.0.0.1", + "port" => 13102, + } + + test_config = YAML.load_file(File.join(API_UMBRELLA_SRC_ROOT, "config/test.yml")) + assert_equal(expected_test_value, test_config["web"]["mailer"]["smtp_settings"]) + + runtime_config = YAML.load_file(File.join($config["root_dir"], "var/run/runtime_config.yml")) + assert_equal(expected_test_value, runtime_config["web"]["mailer"]["smtp_settings"]) + end + + def test_overrides_default_null_value_with_string + default_config = YAML.load_file(File.join(API_UMBRELLA_SRC_ROOT, "config/default.yml")) + assert(default_config["web"]["admin"]["auth_strategies"]["google"].key?("client_secret")) + assert_nil(default_config["web"]["admin"]["auth_strategies"]["google"]["client_secret"]) + + expected_test_value = "test_fake" + + test_config = YAML.load_file(File.join(API_UMBRELLA_SRC_ROOT, "config/test.yml")) + assert_equal(expected_test_value, test_config["web"]["admin"]["auth_strategies"]["google"]["client_secret"]) + + runtime_config = YAML.load_file(File.join($config["root_dir"], "var/run/runtime_config.yml")) + assert_equal(expected_test_value, runtime_config["web"]["admin"]["auth_strategies"]["google"]["client_secret"]) + end + + def test_overrides_default_empty_hash_value + default_config = YAML.load_file(File.join(API_UMBRELLA_SRC_ROOT, "config/default.yml")) + assert_equal({}, default_config["web"]["admin"]["auth_strategies"]["ldap"]["options"]) expected_test_value = { "host" => "127.0.0.1", diff --git a/test/proxy/test_database_seeding.rb b/test/proxy/test_database_seeding.rb index ac944eed9..ea8976c57 100644 --- a/test/proxy/test_database_seeding.rb +++ b/test/proxy/test_database_seeding.rb @@ -77,16 +77,6 @@ def test_api_key_for_admin assert_match(/\A[0-9a-f\-]{36}\z/, user["settings"]["_id"]) end - def test_initial_superusers - admins = Admin.where(:username => "initial.admin@example.com").all - assert_equal(1, admins.length) - - admin = admins.first.attributes - assert(admin["superuser"]) - assert_match(/\A[0-9a-f\-]{36}\z/, admin["_id"]) - assert_match(/\A[a-zA-Z0-9]{40}\z/, admin["authentication_token"]) - end - def test_admin_permission_records permissions = AdminPermission.all assert_equal(6, permissions.length) diff --git a/test/support/api_umbrella_test_helpers/admin_auth.rb b/test/support/api_umbrella_test_helpers/admin_auth.rb index 3e0cb9e2a..6d963f6a2 100644 --- a/test/support/api_umbrella_test_helpers/admin_auth.rb +++ b/test/support/api_umbrella_test_helpers/admin_auth.rb @@ -3,7 +3,7 @@ module ApiUmbrellaTestHelpers module AdminAuth def admin_login(admin = nil) - admin ||= FactoryGirl.create(:admin, :encrypted_password => BCrypt::Password.create("password")) + admin ||= FactoryGirl.create(:admin) visit "/admin/login" fill_in "admin_username", :with => admin.username @@ -15,7 +15,7 @@ def admin_login(admin = nil) def csrf_session csrf_token = SecureRandom.base64(32) - cookies_utils = RailsCompatibleCookiesUtils.new("aeec385fb48a0594b6bb0b18f62473190f1d01b0b6113766af525be2ae1a317a03ab0ee1b3ee6aca3fb1572dc87684e033dcec21acd90d0ca0f111ca1785d0e9") + cookies_utils = RailsCompatibleCookiesUtils.new(test_rails_secret_token) session = cookies_utils.encrypt({ "session_id" => SecureRandom.hex(16), "_csrf_token" => csrf_token, @@ -26,10 +26,12 @@ def csrf_session def admin_session(admin = nil) admin ||= FactoryGirl.create(:admin) - cookies_utils = RailsCompatibleCookiesUtils.new("aeec385fb48a0594b6bb0b18f62473190f1d01b0b6113766af525be2ae1a317a03ab0ee1b3ee6aca3fb1572dc87684e033dcec21acd90d0ca0f111ca1785d0e9") + cookies_utils = RailsCompatibleCookiesUtils.new(test_rails_secret_token) + + authenticatable_salt = admin.encrypted_password[0, 29] if(admin.encrypted_password) session = cookies_utils.encrypt({ "session_id" => SecureRandom.hex(16), - "warden.user.admin.key" => [[admin.id], nil], + "warden.user.admin.key" => [[admin.id], authenticatable_salt], }) { :headers => { "Cookie" => "_api_umbrella_session=#{session}" } } @@ -93,5 +95,18 @@ def make_first_time_admin_creation_requests [get_response, create_response] end + + private + + @@test_rails_secret_token = nil + def test_rails_secret_token + unless @@test_rails_secret_token + test_config = YAML.load_file(File.join(API_UMBRELLA_SRC_ROOT, "config/test.yml")) + @@test_rails_secret_token = test_config["web"]["rails_secret_token"] + assert(@@test_rails_secret_token) + end + + @@test_rails_secret_token + end end end From f2338cc5413ddb74268d401fba8fa28de973e5f6 Mon Sep 17 00:00:00 2001 From: Nick Muerdter Date: Sat, 28 Jan 2017 18:17:36 -0700 Subject: [PATCH 12/26] Don't run "yarn clean" since it may remove needed files. This isn't quite the same as "npm prune" that we were replacing. "clean" will remove files from inside the npm directories that it thinks are inessential. But this doesn't always work. --- build/cmake/core-admin-ui.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/cmake/core-admin-ui.cmake b/build/cmake/core-admin-ui.cmake index 8dd042590..f70ba9be6 100644 --- a/build/cmake/core-admin-ui.cmake +++ b/build/cmake/core-admin-ui.cmake @@ -28,7 +28,7 @@ add_custom_command( DEPENDS yarn ${STAMP_DIR}/core-admin-ui-build-dir - COMMAND cd ${CORE_BUILD_DIR}/tmp/admin-ui-build && env PATH=${DEV_INSTALL_PREFIX}/bin:$ENV{PATH} yarn install && env PATH=${DEV_INSTALL_PREFIX}/bin:$ENV{PATH} yarn clean + COMMAND cd ${CORE_BUILD_DIR}/tmp/admin-ui-build && env PATH=${DEV_INSTALL_PREFIX}/bin:$ENV{PATH} yarn install COMMAND touch ${STAMP_DIR}/core-admin-ui-yarn-install ) From 64ece947e030a22ecf3dbafdb9be5fc5d78e2610 Mon Sep 17 00:00:00 2001 From: Nick Muerdter Date: Sat, 28 Jan 2017 18:52:18 -0700 Subject: [PATCH 13/26] More local admin account implementation, fixes, and tests. - More thoroughly implement the concept of the "username_is_email" option that hides the separate email input when true. - As a result, change a few things about how we share i18n data being server-side and client-side so we can dynamically override the i18n "username" text with the "email" text if this setting is true. - Various fixes for when local authentication is disabled, like disabling the login HTTP POST endpoint. - Bump default minimum password length to 14 chars. --- Gemfile | 3 + Gemfile.lock | 4 + config/default.yml | 3 +- src/api-umbrella/admin-ui/.eslintrc.js | 1 - .../app/components/admins/index-table.js | 1 + .../app/components/api-users/record-form.js | 1 + .../components/apis/settings/common-fields.js | 1 + .../admin-ui/app/components/error-messages.js | 1 + .../app/components/stats/query-form.js | 1 + src/api-umbrella/admin-ui/app/helpers/t.js | 1 + src/api-umbrella/admin-ui/app/index.html | 3 +- .../admin-ui/app/models/api-scope.js | 1 + src/api-umbrella/admin-ui/app/models/api.js | 1 + .../admin-ui/app/models/api/server.js | 1 + .../admin-ui/app/models/api/url-match.js | 1 + .../admin-ui/app/models/website-backend.js | 1 + .../components/admins/record-form.hbs | 24 +- src/api-umbrella/admin-ui/package.json | 2 + src/api-umbrella/admin-ui/yarn.lock | 806 +++++++++++++++--- src/api-umbrella/cli/read_config.lua | 5 + src/api-umbrella/web-app/Gemfile | 7 - src/api-umbrella/web-app/Gemfile.lock | 8 - .../admin/_common_validations.js.erb | 5 - .../javascripts/admin/server_side_loader.js | 3 - .../admin/registrations_controller.rb | 4 +- .../controllers/admin/sessions_controller.rb | 13 +- src/api-umbrella/web-app/app/models/admin.rb | 11 +- .../app/views/devise/sessions/new.html.erb | 6 +- .../web-app/config/application.rb | 1 + .../web-app/config/initializers/assets.rb | 12 - .../web-app/config/initializers/i18n.rb | 14 +- .../web-app/config/locales/en.yml | 8 +- src/api-umbrella/web-app/config/routes.rb | 48 +- templates/etc/nginx/router.conf.mustache | 2 +- .../admin_ui/login/test_external_providers.rb | 18 + test/admin_ui/login/test_forgot_password.rb | 23 +- .../test_local_and_external_providers.rb | 13 + test/admin_ui/login/test_local_provider.rb | 110 +++ test/admin_ui/login/test_username_is_email.rb | 72 ++ .../admin_ui/login/test_username_not_email.rb | 91 ++ test/factories/admins.rb | 2 +- .../api_umbrella_test_helpers/admin_auth.rb | 108 ++- 42 files changed, 1231 insertions(+), 210 deletions(-) delete mode 100644 src/api-umbrella/web-app/app/assets/javascripts/admin/_common_validations.js.erb delete mode 100644 src/api-umbrella/web-app/app/assets/javascripts/admin/server_side_loader.js create mode 100644 test/admin_ui/login/test_username_is_email.rb create mode 100644 test/admin_ui/login/test_username_not_email.rb diff --git a/Gemfile b/Gemfile index e00d07f3a..aee57ac1d 100644 --- a/Gemfile +++ b/Gemfile @@ -5,6 +5,9 @@ gem "rake", "~> 12.0.0" # Tests gem "minitest", "~> 5.10.1" +# CLI helper for running tests +gem "minitest-sprint", "~> 1.2.0" + # More test outputs gem "minitest-reporters", "~> 1.1.14" diff --git a/Gemfile.lock b/Gemfile.lock index b408012b3..4e940230c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -96,6 +96,8 @@ GEM builder minitest (>= 5.0) ruby-progressbar + minitest-sprint (1.2.0) + path_expander (~> 1.0) mongo (2.4.1) bson (>= 4.2.1, < 5.0.0) mongoid (6.0.3) @@ -108,6 +110,7 @@ GEM oj (2.18.1) parser (2.3.3.1) ast (~> 2.2) + path_expander (1.0.1) poltergeist (1.13.0) capybara (~> 2.1) cliver (~> 0.3.1) @@ -168,6 +171,7 @@ DEPENDENCIES minitest-ci (~> 3.1.0) minitest-hooks (~> 1.4.0) minitest-reporters (~> 1.1.14) + minitest-sprint (~> 1.2.0) mongoid (~> 6.0.3) multi_json (~> 1.12.1) nokogiri (~> 1.7.0) diff --git a/config/default.yml b/config/default.yml index b0d1e34eb..a18bfa23c 100644 --- a/config/default.yml +++ b/config/default.yml @@ -80,7 +80,8 @@ web: max_threads: 24 admin: initial_superusers: [] - password_length_min: 8 + username_is_email: true + password_length_min: 14 password_length_max: 72 email_regex: "\\A[^@\\s]+@[^@\\s]+\\z" password_regex: diff --git a/src/api-umbrella/admin-ui/.eslintrc.js b/src/api-umbrella/admin-ui/.eslintrc.js index 3454bc141..43fc8d8d1 100644 --- a/src/api-umbrella/admin-ui/.eslintrc.js +++ b/src/api-umbrella/admin-ui/.eslintrc.js @@ -26,7 +26,6 @@ module.exports = { globals: { '$': true, 'CommonValidations': true, - 'I18n': true, 'JsDiff': true, 'PNotify': true, '_': true, diff --git a/src/api-umbrella/admin-ui/app/components/admins/index-table.js b/src/api-umbrella/admin-ui/app/components/admins/index-table.js index b95c77fef..4468dd91b 100644 --- a/src/api-umbrella/admin-ui/app/components/admins/index-table.js +++ b/src/api-umbrella/admin-ui/app/components/admins/index-table.js @@ -1,4 +1,5 @@ import Ember from 'ember'; +import I18n from 'npm:i18n-js'; import DataTablesHelpers from 'api-umbrella-admin-ui/utils/data-tables-helpers'; export default Ember.Component.extend({ diff --git a/src/api-umbrella/admin-ui/app/components/api-users/record-form.js b/src/api-umbrella/admin-ui/app/components/api-users/record-form.js index 0b2d8235a..9bc1bfe44 100644 --- a/src/api-umbrella/admin-ui/app/components/api-users/record-form.js +++ b/src/api-umbrella/admin-ui/app/components/api-users/record-form.js @@ -1,4 +1,5 @@ import Ember from 'ember'; +import I18n from 'npm:i18n-js'; import Save from 'api-umbrella-admin-ui/mixins/save'; export default Ember.Component.extend(Save, { diff --git a/src/api-umbrella/admin-ui/app/components/apis/settings/common-fields.js b/src/api-umbrella/admin-ui/app/components/apis/settings/common-fields.js index 4550fbf90..3e540165e 100644 --- a/src/api-umbrella/admin-ui/app/components/apis/settings/common-fields.js +++ b/src/api-umbrella/admin-ui/app/components/apis/settings/common-fields.js @@ -1,4 +1,5 @@ import Ember from 'ember'; +import I18n from 'npm:i18n-js'; export default Ember.Component.extend({ store: Ember.inject.service(), diff --git a/src/api-umbrella/admin-ui/app/components/error-messages.js b/src/api-umbrella/admin-ui/app/components/error-messages.js index a500ecc7b..974b11160 100644 --- a/src/api-umbrella/admin-ui/app/components/error-messages.js +++ b/src/api-umbrella/admin-ui/app/components/error-messages.js @@ -1,4 +1,5 @@ import Ember from 'ember'; +import I18n from 'npm:i18n-js'; export default Ember.Component.extend({ messages: Ember.computed('model.clientErrors', 'model.serverErrors', function() { diff --git a/src/api-umbrella/admin-ui/app/components/stats/query-form.js b/src/api-umbrella/admin-ui/app/components/stats/query-form.js index 8a42d1145..81d8a59d7 100644 --- a/src/api-umbrella/admin-ui/app/components/stats/query-form.js +++ b/src/api-umbrella/admin-ui/app/components/stats/query-form.js @@ -1,4 +1,5 @@ import Ember from 'ember'; +import I18n from 'npm:i18n-js'; export default Ember.Component.extend({ session: Ember.inject.service('session'), diff --git a/src/api-umbrella/admin-ui/app/helpers/t.js b/src/api-umbrella/admin-ui/app/helpers/t.js index ef8c72035..80747105d 100644 --- a/src/api-umbrella/admin-ui/app/helpers/t.js +++ b/src/api-umbrella/admin-ui/app/helpers/t.js @@ -1,4 +1,5 @@ import Ember from 'ember'; +import I18n from 'npm:i18n-js'; export function t(params, options) { let key = params[0]; diff --git a/src/api-umbrella/admin-ui/app/index.html b/src/api-umbrella/admin-ui/app/index.html index 373f98904..25f041587 100644 --- a/src/api-umbrella/admin-ui/app/index.html +++ b/src/api-umbrella/admin-ui/app/index.html @@ -43,9 +43,8 @@ } + - - {{content-for "body-footer"}} diff --git a/src/api-umbrella/admin-ui/app/models/api-scope.js b/src/api-umbrella/admin-ui/app/models/api-scope.js index a6326aae7..e052d458c 100644 --- a/src/api-umbrella/admin-ui/app/models/api-scope.js +++ b/src/api-umbrella/admin-ui/app/models/api-scope.js @@ -1,5 +1,6 @@ import Ember from 'ember'; import DS from 'ember-data'; +import I18n from 'npm:i18n-js'; import { validator, buildValidations } from 'ember-cp-validations'; const Validations = buildValidations({ diff --git a/src/api-umbrella/admin-ui/app/models/api.js b/src/api-umbrella/admin-ui/app/models/api.js index abfef585f..5e992d5bc 100644 --- a/src/api-umbrella/admin-ui/app/models/api.js +++ b/src/api-umbrella/admin-ui/app/models/api.js @@ -1,5 +1,6 @@ import Ember from 'ember'; import DS from 'ember-data'; +import I18n from 'npm:i18n-js'; import { validator, buildValidations } from 'ember-cp-validations'; const Validations = buildValidations({ diff --git a/src/api-umbrella/admin-ui/app/models/api/server.js b/src/api-umbrella/admin-ui/app/models/api/server.js index 2eebd79a3..7e6438140 100644 --- a/src/api-umbrella/admin-ui/app/models/api/server.js +++ b/src/api-umbrella/admin-ui/app/models/api/server.js @@ -1,5 +1,6 @@ import Ember from 'ember'; import DS from 'ember-data'; +import I18n from 'npm:i18n-js'; import { validator, buildValidations } from 'ember-cp-validations'; const Validations = buildValidations({ diff --git a/src/api-umbrella/admin-ui/app/models/api/url-match.js b/src/api-umbrella/admin-ui/app/models/api/url-match.js index 564734bd2..ec563d260 100644 --- a/src/api-umbrella/admin-ui/app/models/api/url-match.js +++ b/src/api-umbrella/admin-ui/app/models/api/url-match.js @@ -1,5 +1,6 @@ import Ember from 'ember'; import DS from 'ember-data'; +import I18n from 'npm:i18n-js'; import { validator, buildValidations } from 'ember-cp-validations'; const Validations = buildValidations({ diff --git a/src/api-umbrella/admin-ui/app/models/website-backend.js b/src/api-umbrella/admin-ui/app/models/website-backend.js index c3058446b..d42ba8fcb 100644 --- a/src/api-umbrella/admin-ui/app/models/website-backend.js +++ b/src/api-umbrella/admin-ui/app/models/website-backend.js @@ -1,4 +1,5 @@ import DS from 'ember-data'; +import I18n from 'npm:i18n-js'; import { validator, buildValidations } from 'ember-cp-validations'; const Validations = buildValidations({ diff --git a/src/api-umbrella/admin-ui/app/templates/components/admins/record-form.hbs b/src/api-umbrella/admin-ui/app/templates/components/admins/record-form.hbs index 999b1f647..976bd7afb 100644 --- a/src/api-umbrella/admin-ui/app/templates/components/admins/record-form.hbs +++ b/src/api-umbrella/admin-ui/app/templates/components/admins/record-form.hbs @@ -6,11 +6,9 @@ User Info {{f.text-field "username" label=(t "mongoid.attributes.admin.username")}} - {{#if model.email}} - {{#if (not-eq (unbound model.email) (unbound model.username))}} - {{f.static-field "email" label=(t "mongoid.attributes.admin.email")}} - {{/if}} - {{/if}} + {{#unless session.data.authenticated.username_is_email}} + {{f.text-field "email" label=(t "mongoid.attributes.admin.email")}} + {{/unless}} {{#if model.name}} {{f.static-field "name" label=(t "mongoid.attributes.admin.name")}} {{/if}} @@ -18,16 +16,16 @@ {{#if model.authenticationToken}} -
- Change Password + {{#if session.data.authenticated.local_auth_enabled}} +
+ {{t "devise.passwords.edit.change_your_password"}} - {{f.password-field "currentPassword" label=(t "mongoid.attributes.admin.current_password")}} - {{f.password-field "password" label=(t "mongoid.attributes.admin.password")}} - {{f.password-field "passwordConfirmation" label=(t "mongoid.attributes.admin.password_confirmation")}} -
- {{/if}} + {{f.password-field "currentPassword" label=(t "mongoid.attributes.admin.current_password")}} + {{f.password-field "password" label=(t "devise.passwords.edit.new_password")}} + {{f.password-field "passwordConfirmation" label=(t "devise.passwords.edit.confirm_new_password")}} +
+ {{/if}} - {{#if model.authenticationToken}}
Admin API Access diff --git a/src/api-umbrella/admin-ui/package.json b/src/api-umbrella/admin-ui/package.json index ed026b80f..89531ea5b 100644 --- a/src/api-umbrella/admin-ui/package.json +++ b/src/api-umbrella/admin-ui/package.json @@ -22,6 +22,7 @@ "bower": "~1.8.0", "broccoli-asset-rev": "^2.4.2", "ember-bootstrap": "~0.11.2", + "ember-browserify": "~1.1.13", "ember-buffered-proxy": "~0.6.0", "ember-busy-blocker": "~0.1.0", "ember-cli": "~2.8.0", @@ -47,6 +48,7 @@ "ember-simple-auth": "~1.1.0", "ember-truth-helpers": "~1.2.0", "emberx-select": "~2.2.2", + "i18n-js": "http://github.com/fnando/i18n-js/archive/v3.0.0.rc15.tar.gz", "loader.js": "^4.0.1" }, "ember-addon": { diff --git a/src/api-umbrella/admin-ui/yarn.lock b/src/api-umbrella/admin-ui/yarn.lock index d2b5cddc1..419ef4453 100644 --- a/src/api-umbrella/admin-ui/yarn.lock +++ b/src/api-umbrella/admin-ui/yarn.lock @@ -2,6 +2,13 @@ # yarn lockfile v1 +JSONStream@^1.0.3: + version "1.3.0" + resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.0.tgz#680ab9ac6572a8a1a207e0b38721db1c77b215e5" + dependencies: + jsonparse "^1.2.0" + through ">=2.2.7 <3" + abbrev@1, abbrev@~1.0.7: version "1.0.9" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135" @@ -19,11 +26,19 @@ acorn-jsx@^3.0.0: dependencies: acorn "^3.0.4" +acorn@^1.0.3: + version "1.2.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-1.2.2.tgz#c8ce27de0acc76d896d2b1fad3df588d9e82f014" + +acorn@^2.6.4, acorn@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-2.7.0.tgz#ab6e7d9d886aaca8b085bc3312b79a198433f0e7" + acorn@^3.0.4, acorn@^3.1.0: version "3.3.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" -acorn@^4.0.1: +acorn@^4.0.1, acorn@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.3.tgz#1a3e850b428e73ba6b09d1cc527f5aaad4d03ef1" @@ -70,14 +85,14 @@ ansi-escapes@^1.1.0: version "1.4.0" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e" -ansi-regex@*, ansi-regex@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.0.0.tgz#c5061b6e0ef8a81775e50f5d66151bf6bf371107" - ansi-regex@^0.2.0, ansi-regex@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-0.2.1.tgz#0d8e946967a3d8143f93e24e298525fc1b2235f9" +ansi-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.0.0.tgz#c5061b6e0ef8a81775e50f5d66151bf6bf371107" + ansi-styles@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.1.0.tgz#eaecbf66cd706882760b2f4691582b8f55d7a7de" @@ -131,6 +146,10 @@ array-equal@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93" +array-filter@~0.0.0: + version "0.0.1" + resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-0.0.1.tgz#7da8cf2e26628ed732803581fd21f67cacd2eeec" + array-find-index@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" @@ -146,6 +165,14 @@ array-index@^1.0.0: debug "^2.2.0" es6-symbol "^3.0.2" +array-map@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/array-map/-/array-map-0.0.0.tgz#88a2bab73d1cf7bcd5c1b118a003f66f665fa662" + +array-reduce@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/array-reduce/-/array-reduce-0.0.0.tgz#173899d3ffd1c7d9383e4479525dbe278cab5f2b" + array-to-error@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/array-to-error/-/array-to-error-1.1.1.tgz#d68812926d14097a205579a667eeaf1856a44c07" @@ -178,6 +205,14 @@ asap@^2.0.0: version "2.0.5" resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.5.tgz#522765b50c3510490e52d7dcfe085ef9ba96958f" +asn1.js@^4.0.0: + version "4.9.1" + resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.9.1.tgz#48ba240b45a9280e94748990ba597d216617fd40" + dependencies: + bn.js "^4.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + asn1@~0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" @@ -190,6 +225,12 @@ assert-plus@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" +assert@^1.4.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91" + dependencies: + util "0.10.3" + ast-traverse@~0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/ast-traverse/-/ast-traverse-0.1.1.tgz#69cf2b8386f19dcda1bb1e05d68fe359d8897de6" @@ -198,14 +239,16 @@ ast-types@0.8.12: version "0.8.12" resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.8.12.tgz#a0d90e4351bb887716c83fd637ebf818af4adfcc" -ast-types@0.8.15: - version "0.8.15" - resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.8.15.tgz#8eef0827f04dff0ec8857ba925abe3fea6194e52" - ast-types@0.9.2: version "0.9.2" resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.9.2.tgz#2cc19979d15c655108bf565323b8e7ee38751f6b" +astw@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/astw/-/astw-2.0.0.tgz#08121ac8288d35611c0ceec663f6cd545604897d" + dependencies: + acorn "^1.0.3" + async-disk-cache@^1.0.0: version "1.0.9" resolved "https://registry.yarnpkg.com/async-disk-cache/-/async-disk-cache-1.0.9.tgz#23bafb823184f463407e474e8d5f87899f72ca63" @@ -413,6 +456,10 @@ base64-arraybuffer@0.1.5: version "0.1.5" resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8" +base64-js@^1.0.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.0.tgz#a39992d723584811982be5e290bb6a53d86700f1" + base64id@0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/base64id/-/base64id-0.1.0.tgz#02ce0fdeee0cef4f40080e1e73e834f0b1bfce3f" @@ -469,6 +516,10 @@ bluebird@^3.1.1, bluebird@^3.4.6: version "3.4.6" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.6.tgz#01da8d821d87813d158967e743d5fe6c62cf8c0f" +bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: + version "4.11.6" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.6.tgz#53344adb14617a13f6e8dd2ce28905d1c0ba3215" + body-parser@~1.14.0: version "1.14.2" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.14.2.tgz#1015cb1fe2c443858259581db53332f8d0cf50f9" @@ -821,6 +872,129 @@ broccoli-writer@~0.1.1: quick-temp "^0.1.0" rsvp "^3.0.6" +brorand@^1.0.1: + version "1.0.6" + resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.0.6.tgz#4028706b915f91f7b349a2e0bf3c376039d216e5" + +browser-pack@^6.0.1: + version "6.0.2" + resolved "https://registry.yarnpkg.com/browser-pack/-/browser-pack-6.0.2.tgz#f86cd6cef4f5300c8e63e07a4d512f65fbff4531" + dependencies: + JSONStream "^1.0.3" + combine-source-map "~0.7.1" + defined "^1.0.0" + through2 "^2.0.0" + umd "^3.0.0" + +browser-resolve@^1.11.0, browser-resolve@^1.7.0: + version "1.11.2" + resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-1.11.2.tgz#8ff09b0a2c421718a1051c260b32e48f442938ce" + dependencies: + resolve "1.1.7" + +browserify-aes@^1.0.0, browserify-aes@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.0.6.tgz#5e7725dbdef1fd5930d4ebab48567ce451c48a0a" + dependencies: + buffer-xor "^1.0.2" + cipher-base "^1.0.0" + create-hash "^1.1.0" + evp_bytestokey "^1.0.0" + inherits "^2.0.1" + +browserify-cipher@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.0.tgz#9988244874bf5ed4e28da95666dcd66ac8fc363a" + dependencies: + browserify-aes "^1.0.4" + browserify-des "^1.0.0" + evp_bytestokey "^1.0.0" + +browserify-des@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.0.tgz#daa277717470922ed2fe18594118a175439721dd" + dependencies: + cipher-base "^1.0.1" + des.js "^1.0.0" + inherits "^2.0.1" + +browserify-rsa@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524" + dependencies: + bn.js "^4.1.0" + randombytes "^2.0.1" + +browserify-sign@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.0.0.tgz#10773910c3c206d5420a46aad8694f820b85968f" + dependencies: + bn.js "^4.1.1" + browserify-rsa "^4.0.0" + create-hash "^1.1.0" + create-hmac "^1.1.2" + elliptic "^6.0.0" + inherits "^2.0.1" + parse-asn1 "^5.0.0" + +browserify-zlib@~0.1.2: + version "0.1.4" + resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.1.4.tgz#bb35f8a519f600e0fa6b8485241c979d0141fb2d" + dependencies: + pako "~0.2.0" + +browserify@^13.0.0: + version "13.3.0" + resolved "https://registry.yarnpkg.com/browserify/-/browserify-13.3.0.tgz#b5a9c9020243f0c70e4675bec8223bc627e415ce" + dependencies: + JSONStream "^1.0.3" + assert "^1.4.0" + browser-pack "^6.0.1" + browser-resolve "^1.11.0" + browserify-zlib "~0.1.2" + buffer "^4.1.0" + cached-path-relative "^1.0.0" + concat-stream "~1.5.1" + console-browserify "^1.1.0" + constants-browserify "~1.0.0" + crypto-browserify "^3.0.0" + defined "^1.0.0" + deps-sort "^2.0.0" + domain-browser "~1.1.0" + duplexer2 "~0.1.2" + events "~1.1.0" + glob "^7.1.0" + has "^1.0.0" + htmlescape "^1.1.0" + https-browserify "~0.0.0" + inherits "~2.0.1" + insert-module-globals "^7.0.0" + labeled-stream-splicer "^2.0.0" + module-deps "^4.0.8" + os-browserify "~0.1.1" + parents "^1.0.1" + path-browserify "~0.0.0" + process "~0.11.0" + punycode "^1.3.2" + querystring-es3 "~0.2.0" + read-only-stream "^2.0.0" + readable-stream "^2.0.2" + resolve "^1.1.4" + shasum "^1.0.0" + shell-quote "^1.6.1" + stream-browserify "^2.0.0" + stream-http "^2.0.0" + string_decoder "~0.10.0" + subarg "^1.0.0" + syntax-error "^1.1.1" + through2 "^2.0.0" + timers-browserify "^1.0.1" + tty-browserify "~0.0.0" + url "~0.11.0" + util "~0.10.1" + vm-browserify "~0.0.1" + xtend "^4.0.0" + bser@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/bser/-/bser-1.0.2.tgz#381116970b2a6deea5646dd15dd7278444b56169" @@ -831,10 +1005,26 @@ buffer-shims@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/buffer-shims/-/buffer-shims-1.0.0.tgz#9978ce317388c649ad8793028c3477ef044a8b51" +buffer-xor@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" + +buffer@^4.1.0: + version "4.9.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298" + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + isarray "^1.0.0" + builtin-modules@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" +builtin-status-codes@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" + builtins@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/builtins/-/builtins-0.0.7.tgz#355219cd6cf18dbe7c01cc7fd2dce765cfdc549a" @@ -851,6 +1041,10 @@ bytes@2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.4.0.tgz#7d97196f9d5baf7f6935e25985549edd2a6c2339" +cached-path-relative@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/cached-path-relative/-/cached-path-relative-1.0.0.tgz#d1094c577fbd9a8b8bd43c96af6188aa205d05f4" + caller-path@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f" @@ -961,6 +1155,12 @@ chownr@^1.0.1, chownr@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.0.1.tgz#e2a75042a9551908bebd25b8523d5f9769d79181" +cipher-base@^1.0.0, cipher-base@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.3.tgz#eeabf194419ce900da3018c207d212f2a6df0a07" + dependencies: + inherits "^2.0.1" + circular-json@^0.3.0: version "0.3.1" resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.1.tgz#be8b36aefccde8b3ca7aa2d6afc07a37242c0d2d" @@ -1089,6 +1289,15 @@ columnify@~1.5.4: strip-ansi "^3.0.0" wcwidth "^1.0.0" +combine-source-map@~0.7.1: + version "0.7.2" + resolved "https://registry.yarnpkg.com/combine-source-map/-/combine-source-map-0.7.2.tgz#0870312856b307a87cc4ac486f3a9a62aeccc09e" + dependencies: + convert-source-map "~1.1.0" + inline-source-map "~0.6.0" + lodash.memoize "~3.0.3" + source-map "~0.5.3" + combined-stream@^1.0.5, combined-stream@~1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" @@ -1162,7 +1371,7 @@ concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" -concat-stream@^1.4.6, concat-stream@^1.4.7: +concat-stream@^1.4.6, concat-stream@^1.4.7, concat-stream@~1.5.0, concat-stream@~1.5.1: version "1.5.2" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.5.2.tgz#708978624d856af41a5a741defdd261da752c266" dependencies: @@ -1200,7 +1409,7 @@ connect@^3.3.3: parseurl "~1.3.1" utils-merge "1.0.0" -console-browserify@1.1.x: +console-browserify@1.1.x, console-browserify@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10" dependencies: @@ -1216,6 +1425,10 @@ consolidate@^0.14.0: dependencies: bluebird "^3.1.1" +constants-browserify@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" + content-disposition@0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.1.tgz#87476c6a67c8daa87e32e87616df883ba7fb071b" @@ -1228,6 +1441,10 @@ convert-source-map@^1.1.0: version "1.3.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.3.0.tgz#e9f3e9c6e2728efc2676696a70eb382f73106a67" +convert-source-map@~1.1.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.1.3.tgz#4829c877e9fe49b3161f3bf3673888e204699860" + cookie-signature@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" @@ -1258,6 +1475,29 @@ core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" +create-ecdh@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.0.tgz#888c723596cdf7612f6498233eebd7a35301737d" + dependencies: + bn.js "^4.1.0" + elliptic "^6.0.0" + +create-hash@^1.1.0, create-hash@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.1.2.tgz#51210062d7bb7479f6c65bb41a92208b1d61abad" + dependencies: + cipher-base "^1.0.1" + inherits "^2.0.1" + ripemd160 "^1.0.0" + sha.js "^2.3.6" + +create-hmac@^1.1.0, create-hmac@^1.1.2: + version "1.1.4" + resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.4.tgz#d3fb4ba253eb8b3f56e39ea2fbcb8af747bd3170" + dependencies: + create-hash "^1.1.0" + inherits "^2.0.1" + cross-spawn@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-3.0.1.tgz#1256037ecb9f0c5f79e3d6ef135e30770184b982" @@ -1278,6 +1518,21 @@ cryptiles@2.x.x: dependencies: boom "2.x.x" +crypto-browserify@^3.0.0: + version "3.11.0" + resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.11.0.tgz#3652a0906ab9b2a7e0c3ce66a408e957a2485522" + dependencies: + browserify-cipher "^1.0.0" + browserify-sign "^4.0.0" + create-ecdh "^4.0.0" + create-hash "^1.1.0" + create-hmac "^1.1.0" + diffie-hellman "^5.0.0" + inherits "^2.0.1" + pbkdf2 "^3.0.3" + public-encrypt "^4.0.0" + randombytes "^2.0.0" + currently-unhandled@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" @@ -1377,6 +1632,32 @@ depd@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.0.tgz#e1bd82c6aab6ced965b97b88b17ed3e528ca18c3" +deps-sort@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/deps-sort/-/deps-sort-2.0.0.tgz#091724902e84658260eb910748cccd1af6e21fb5" + dependencies: + JSONStream "^1.0.3" + shasum "^1.0.0" + subarg "^1.0.0" + through2 "^2.0.0" + +derequire@^2.0.3: + version "2.0.6" + resolved "https://registry.yarnpkg.com/derequire/-/derequire-2.0.6.tgz#31a414bb7ca176239fa78b116636ef77d517e768" + dependencies: + acorn "^4.0.3" + concat-stream "^1.4.6" + escope "^3.6.0" + through2 "^2.0.0" + yargs "^6.5.0" + +des.js@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc" + dependencies: + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + destroy@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" @@ -1389,7 +1670,7 @@ detect-indent@^3.0.0: minimist "^1.1.0" repeating "^1.1.0" -detective@^4.3.1: +detective@^4.0.0, detective@^4.3.1: version "4.3.2" resolved "https://registry.yarnpkg.com/detective/-/detective-4.3.2.tgz#77697e2e7947ac3fe7c8e26a6d6f115235afa91c" dependencies: @@ -1411,6 +1692,14 @@ diff@^1.3.1: version "1.4.0" resolved "https://registry.yarnpkg.com/diff/-/diff-1.4.0.tgz#7f28d2eb9ee7b15a97efd89ce63dcfdaa3ccbabf" +diffie-hellman@^5.0.0: + version "5.0.2" + resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.2.tgz#b5835739270cfe26acf632099fded2a07f209e5e" + dependencies: + bn.js "^4.1.0" + miller-rabin "^4.0.0" + randombytes "^2.0.0" + doctrine@^1.2.2: version "1.5.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" @@ -1425,6 +1714,10 @@ dom-serializer@0: domelementtype "~1.1.1" entities "~1.1.1" +domain-browser@~1.1.0: + version "1.1.7" + resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.1.7.tgz#867aa4b093faa05f1de08c06f4d7b21fdf8698bc" + domelementtype@1: version "1.3.0" resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.0.tgz#b17aed82e8ab59e52dd9c19b1756e0fc187204c2" @@ -1452,6 +1745,12 @@ dot-prop@^3.0.0: dependencies: is-obj "^1.0.0" +duplexer2@^0.1.2, duplexer2@~0.1.0, duplexer2@~0.1.2: + version "0.1.4" + resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" + dependencies: + readable-stream "^2.0.2" + ecc-jsbn@~0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" @@ -1470,6 +1769,15 @@ ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" +elliptic@^6.0.0: + version "6.3.2" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.3.2.tgz#e4c81e0829cf0a65ab70e998b8232723b5c1bc48" + dependencies: + bn.js "^4.4.0" + brorand "^1.0.1" + hash.js "^1.0.0" + inherits "^2.0.1" + ember-bootstrap@~0.11.2: version "0.11.3" resolved "https://registry.yarnpkg.com/ember-bootstrap/-/ember-bootstrap-0.11.3.tgz#810d9e7202cfb5439d731360589a62144087e96c" @@ -1481,6 +1789,33 @@ ember-bootstrap@~0.11.2: ember-runtime-enumerable-includes-polyfill "^1.0.1" ember-wormhole "^0.4.1" +ember-browserify@~1.1.13: + version "1.1.13" + resolved "https://registry.yarnpkg.com/ember-browserify/-/ember-browserify-1.1.13.tgz#df74eea4adf4694e8c364222f9c5fd605000930b" + dependencies: + acorn "^2.6.4" + broccoli-caching-writer "^3.0.3" + broccoli-kitchen-sink-helpers "^0.3.1" + broccoli-merge-trees "^1.1.2" + broccoli-plugin "^1.2.1" + browserify "^13.0.0" + core-object "^1.1.0" + debug "^2.2.0" + derequire "^2.0.3" + ember-cli-version-checker "^1.1.4" + fs-tree "^1.0.0" + fs-tree-diff "^0.5.0" + lodash "^4.5.1" + md5-hex "^1.3.0" + mkdirp "^0.5.0" + promise-map-series "^0.2.0" + quick-temp "^0.1.2" + rimraf "^2.2.8" + rsvp "^3.0.14" + symlink-or-copy "^1.0.0" + through2 "^2.0.0" + walk-sync "^0.2.7" + ember-buffered-proxy@~0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/ember-buffered-proxy/-/ember-buffered-proxy-0.6.0.tgz#4575bf8a16e4ac28711e3f577789844f65a14409" @@ -2254,6 +2589,16 @@ events-to-array@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/events-to-array/-/events-to-array-1.0.2.tgz#b3484465534fe4ff66fbdd1a83b777713ba404aa" +events@~1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" + +evp_bytestokey@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.0.tgz#497b66ad9fef65cd7c08a6180824ba1476b66e53" + dependencies: + create-hash "^1.1.1" + exec-sh@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.2.0.tgz#14f75de3f20d286ef933099b2ce50a90359cef10" @@ -2505,6 +2850,13 @@ fs-tree-diff@^0.5.0, fs-tree-diff@^0.5.2, fs-tree-diff@^0.5.3, fs-tree-diff@^0.5 path-posix "^1.0.0" symlink-or-copy "^1.1.8" +fs-tree@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-tree/-/fs-tree-1.0.0.tgz#ef64da3e6dd32cc0df27c3b3e0c299ffa575c026" + dependencies: + mkdirp "~0.5.0" + rimraf "~2.2.8" + fs-vacuum@~1.2.9: version "1.2.9" resolved "https://registry.yarnpkg.com/fs-vacuum/-/fs-vacuum-1.2.9.tgz#4f90193ab8ea02890995bcd4e804659a5d366b2d" @@ -2550,6 +2902,10 @@ fstream@^1.0.0, fstream@^1.0.2, fstream@~1.0.8: mkdirp ">=0.5 0" rimraf "2" +function-bind@^1.0.2: + version "1.1.0" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.0.tgz#16176714c801798e4e8f2cf7f7529467bb4a5771" + gauge@~1.2.5: version "1.2.7" resolved "https://registry.yarnpkg.com/gauge/-/gauge-1.2.7.tgz#e9cec5483d3d4ee0ef44b60a7d99e4935e136d93" @@ -2560,20 +2916,6 @@ gauge@~1.2.5: lodash.padend "^4.1.0" lodash.padstart "^4.1.0" -gauge@~2.6.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.6.0.tgz#d35301ad18e96902b4751dcbbe40f4218b942a46" - dependencies: - aproba "^1.0.3" - console-control-strings "^1.0.0" - has-color "^0.1.7" - has-unicode "^2.0.0" - object-assign "^4.1.0" - signal-exit "^3.0.0" - string-width "^1.0.1" - strip-ansi "^3.0.1" - wide-align "^1.1.0" - gauge@~2.7.1: version "2.7.2" resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.2.tgz#15cecc31b02d05345a5d6b0e171cdb3ad2307774" @@ -2670,7 +3012,7 @@ glob@^6.0.0: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.1.1, glob@~7.1.1: +glob@^7.1.0, glob@^7.1.1, glob@~7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" dependencies: @@ -2763,7 +3105,7 @@ has-binary@0.1.7: dependencies: isarray "0.0.1" -has-color@^0.1.7, has-color@~0.1.0: +has-color@~0.1.0: version "0.1.7" resolved "https://registry.yarnpkg.com/has-color/-/has-color-0.1.7.tgz#67144a5260c34fc3cca677d041daf52fe7b78b2f" @@ -2775,6 +3117,12 @@ has-unicode@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" +has@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.1.tgz#8461733f538b0837c9361e39a9ab9e9704dc2f28" + dependencies: + function-bind "^1.0.2" + hash-for-dep@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/hash-for-dep/-/hash-for-dep-1.0.3.tgz#b57f18a0ace56380951638a3b36a6b73d8619b8b" @@ -2782,6 +3130,12 @@ hash-for-dep@^1.0.2: broccoli-kitchen-sink-helpers "^0.3.1" resolve "^1.1.6" +hash.js@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.0.3.tgz#1332ff00156c0a0ffdd8236013d07b77a0451573" + dependencies: + inherits "^2.0.1" + hawk@~3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" @@ -2819,6 +3173,10 @@ hosted-git-info@^2.1.4, hosted-git-info@^2.1.5, hosted-git-info@~2.1.4: version "2.1.5" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.1.5.tgz#0ba81d90da2e25ab34a332e6ec77936e1598118b" +htmlescape@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/htmlescape/-/htmlescape-1.1.1.tgz#3a03edc2214bca3b66424a3e7959349509cb0351" + htmlparser2@3.8.x: version "3.8.3" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.8.3.tgz#996c28b191516a8be86501a7d79757e5c70c1068" @@ -2859,10 +3217,22 @@ http-signature@~1.1.0: jsprim "^1.2.2" sshpk "^1.7.0" +https-browserify@~0.0.0: + version "0.0.1" + resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-0.0.1.tgz#3f91365cabe60b77ed0ebba24b454e3e09d95a82" + +"i18n-js@http://github.com/fnando/i18n-js/archive/v3.0.0.rc15.tar.gz": + version "0.0.0" + resolved "http://github.com/fnando/i18n-js/archive/v3.0.0.rc15.tar.gz#006992545a89fcb12808afec0740f9ec4b393631" + iconv-lite@0.4.13, iconv-lite@^0.4.5, iconv-lite@~0.4.13: version "0.4.13" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.13.tgz#1f88aba4ab0b1508e8312acc39345f36e992e2f2" +ieee754@^1.1.4: + version "1.1.8" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4" + iferr@^0.1.5: version "0.1.5" resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501" @@ -2871,7 +3241,7 @@ ignore@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.2.0.tgz#8d88f03c3002a0ac52114db25d2c673b0bf1e435" -imurmurhash@*, imurmurhash@^0.1.4: +imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" @@ -2908,6 +3278,10 @@ inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@~2.0.0, inherits@~2.0.1: version "2.0.3" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" +inherits@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" + ini@^1.3.4, ini@~1.3.4: version "1.3.4" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e" @@ -2935,6 +3309,12 @@ inline-source-map-comment@^1.0.5: sum-up "^1.0.1" xtend "^4.0.0" +inline-source-map@~0.6.0: + version "0.6.2" + resolved "https://registry.yarnpkg.com/inline-source-map/-/inline-source-map-0.6.2.tgz#f9393471c18a79d1724f863fa38b586370ade2a5" + dependencies: + source-map "~0.5.3" + inquirer@^0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-0.12.0.tgz#1ef2bfd63504df0bc75785fff8c2c41df12f077e" @@ -2972,6 +3352,19 @@ inquirer@^1.0.2: strip-ansi "^3.0.0" through "^2.3.6" +insert-module-globals@^7.0.0: + version "7.0.1" + resolved "https://registry.yarnpkg.com/insert-module-globals/-/insert-module-globals-7.0.1.tgz#c03bf4e01cb086d5b5e5ace8ad0afe7889d638c3" + dependencies: + JSONStream "^1.0.3" + combine-source-map "~0.7.1" + concat-stream "~1.5.1" + is-buffer "^1.1.0" + lexical-scope "^1.2.0" + process "~0.11.0" + through2 "^2.0.0" + xtend "^4.0.0" + interpret@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.1.tgz#d579fb7f693b858004947af39fa0db49f795602c" @@ -2988,7 +3381,7 @@ is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" -is-buffer@^1.0.2: +is-buffer@^1.0.2, is-buffer@^1.1.0: version "1.1.4" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.4.tgz#cfc86ccd5dc5a52fa80489111c6920c457e2d98b" @@ -3085,7 +3478,7 @@ is-utf8@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" -isarray@0.0.1: +isarray@0.0.1, isarray@~0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" @@ -3179,6 +3572,12 @@ json-stable-stringify@^1.0.0, json-stable-stringify@^1.0.1: dependencies: jsonify "~0.0.0" +json-stable-stringify@~0.0.0: + version "0.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-0.0.1.tgz#611c23e814db375527df851193db59dd2af27f45" + dependencies: + jsonify "~0.0.0" + json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" @@ -3205,6 +3604,10 @@ jsonify@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" +jsonparse@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.0.tgz#85fc245b1d9259acc6941960b905adf64e7de0e8" + jsonpointer@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.0.tgz#6661e161d2fc445f19f98430231343722e1fcbd5" @@ -3233,6 +3636,14 @@ klaw@^1.0.0: optionalDependencies: graceful-fs "^4.1.9" +labeled-stream-splicer@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/labeled-stream-splicer/-/labeled-stream-splicer-2.0.0.tgz#a52e1d138024c00b86b1c0c91f677918b8ae0a59" + dependencies: + inherits "^2.0.1" + isarray "~0.0.1" + stream-splicer "^2.0.0" + lazy-cache@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" @@ -3263,6 +3674,12 @@ levn@^0.3.0, levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" +lexical-scope@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/lexical-scope/-/lexical-scope-1.2.0.tgz#fcea5edc704a4b3a8796cdca419c3a0afaf22df4" + dependencies: + astw "^2.0.0" + linkify-it@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-2.0.2.tgz#994629a4adfa5a7d34e08c075611575ab9b6fcfc" @@ -3441,6 +3858,10 @@ lodash.keysin@^3.0.0: lodash.isarguments "^3.0.0" lodash.isarray "^3.0.0" +lodash.memoize@~3.0.3: + version "3.0.4" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-3.0.4.tgz#2dcbd2c287cbc0a55cc42328bd0c736150d53e3f" + lodash.merge@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-3.3.2.tgz#0d90d93ed637b1878437bb3e21601260d7afe994" @@ -3603,7 +4024,7 @@ matcher-collection@^1.0.0, matcher-collection@^1.0.1: dependencies: minimatch "^3.0.2" -md5-hex@^1.0.2, md5-hex@^1.2.1: +md5-hex@^1.0.2, md5-hex@^1.2.1, md5-hex@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/md5-hex/-/md5-hex-1.3.0.tgz#d2c4afe983c4370662179b8cad145219135046c4" dependencies: @@ -3654,6 +4075,13 @@ methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" +miller-rabin@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.0.tgz#4a62fb1d42933c05583982f4c716f6fb9e6c6d3d" + dependencies: + bn.js "^4.0.0" + brorand "^1.0.1" + "mime-db@>= 1.24.0 < 2", mime-db@~1.25.0: version "1.25.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.25.0.tgz#c18dbd7c73a5dbf6f44a024dc0d165a1e7b1c392" @@ -3668,6 +4096,10 @@ mime@1.3.4, mime@^1.2.11: version "1.3.4" resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53" +minimalistic-assert@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz#702be2dda6b37f4836bcb3f5db56641b64a1d3d3" + minimatch@1: version "1.0.0" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-1.0.0.tgz#e0dd2120b49e1b724ce8d714c520822a9438576d" @@ -3715,6 +4147,26 @@ mktemp@~0.3.4: version "0.3.5" resolved "https://registry.yarnpkg.com/mktemp/-/mktemp-0.3.5.tgz#a1504c706d0d2b198c6a0eb645f7fdaf8181f7de" +module-deps@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/module-deps/-/module-deps-4.0.8.tgz#55fd70623399706c3288bef7a609ff1e8c0ed2bb" + dependencies: + JSONStream "^1.0.3" + browser-resolve "^1.7.0" + cached-path-relative "^1.0.0" + concat-stream "~1.5.0" + defined "^1.0.0" + detective "^4.0.0" + duplexer2 "^0.1.2" + inherits "^2.0.1" + parents "^1.0.0" + readable-stream "^2.0.2" + resolve "^1.1.3" + stream-combiner2 "^1.1.1" + subarg "^1.0.0" + through2 "^2.0.0" + xtend "^4.0.0" + morgan@^1.5.2: version "1.7.0" resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.7.0.tgz#eb10ca8e50d1abe0f8d3dad5c0201d052d981c62" @@ -3774,26 +4226,7 @@ node-fetch@^1.3.3: encoding "^0.1.11" is-stream "^1.0.1" -node-gyp@^3.3.1: - version "3.4.0" - resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.4.0.tgz#dda558393b3ecbbe24c9e6b8703c71194c63fa36" - dependencies: - fstream "^1.0.0" - glob "^7.0.3" - graceful-fs "^4.1.2" - minimatch "^3.0.2" - mkdirp "^0.5.0" - nopt "2 || 3" - npmlog "0 || 1 || 2 || 3" - osenv "0" - path-array "^1.0.0" - request "2" - rimraf "2" - semver "2.x || 3.x || 4 || 5" - tar "^2.0.0" - which "1" - -node-gyp@~3.3.1: +node-gyp@^3.3.1, node-gyp@~3.3.1: version "3.3.1" resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.3.1.tgz#80f7b6d7c2f9c0495ba42c518a670c99bdf6e4a0" dependencies: @@ -4002,7 +4435,7 @@ npm@2.15.5: wrappy "~1.0.1" write-file-atomic "~1.1.4" -"npmlog@0 || 1 || 2", "npmlog@0.1 || 1 || 2", npmlog@~2.0.3: +"npmlog@0 || 1 || 2", "npmlog@0.1 || 1 || 2", "npmlog@~2.0.0 || ~3.1.0", npmlog@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-2.0.4.tgz#98b52530f2514ca90d09ec5b22c8846722375692" dependencies: @@ -4010,15 +4443,6 @@ npm@2.15.5: are-we-there-yet "~1.1.2" gauge "~1.2.5" -"npmlog@0 || 1 || 2 || 3", "npmlog@~2.0.0 || ~3.1.0": - version "3.1.2" - resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-3.1.2.tgz#2d46fa874337af9498a2f12bb43d8d0be4a36873" - dependencies: - are-we-there-yet "~1.1.2" - console-control-strings "~1.1.0" - gauge "~2.6.0" - set-blocking "~2.0.0" - npmlog@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.0.1.tgz#d14f503b4cd79710375553004ba96e6662fbc0b8" @@ -4113,6 +4537,10 @@ ora@^0.2.0: cli-spinners "^0.1.2" object-assign "^4.0.1" +os-browserify@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.1.2.tgz#49ca0293e0b19590a5f5de10c7f265a617d8fe54" + os-homedir@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" @@ -4146,6 +4574,26 @@ output-file-sync@^1.1.0: mkdirp "^0.5.1" object-assign "^4.1.0" +pako@~0.2.0: + version "0.2.9" + resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75" + +parents@^1.0.0, parents@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parents/-/parents-1.0.1.tgz#fedd4d2bf193a77745fe71e371d73c3307d9c751" + dependencies: + path-platform "~0.11.15" + +parse-asn1@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.0.0.tgz#35060f6d5015d37628c770f4e091a0b5a278bc23" + dependencies: + asn1.js "^4.0.0" + browserify-aes "^1.0.0" + create-hash "^1.1.0" + evp_bytestokey "^1.0.0" + pbkdf2 "^3.0.3" + parse-json@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" @@ -4180,6 +4628,10 @@ path-array@^1.0.0: dependencies: array-index "^1.0.0" +path-browserify@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a" + path-exists@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-1.0.0.tgz#d5a8998eb71ef37a74c34eb0d9eba6e878eea081" @@ -4198,6 +4650,10 @@ path-is-inside@^1.0.1, path-is-inside@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" +path-platform@~0.11.15: + version "0.11.15" + resolved "https://registry.yarnpkg.com/path-platform/-/path-platform-0.11.15.tgz#e864217f74c36850f0852b78dc7bf7d4a5721bf2" + path-posix@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/path-posix/-/path-posix-1.0.0.tgz#06b26113f56beab042545a23bfa88003ccac260f" @@ -4214,6 +4670,12 @@ path-type@^1.0.0: pify "^2.0.0" pinkie-promise "^2.0.0" +pbkdf2@^3.0.3: + version "3.0.9" + resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.9.tgz#f2c4b25a600058b3c3773c086c37dbbee1ffe693" + dependencies: + create-hmac "^1.1.2" + pify@^2.0.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" @@ -4262,11 +4724,15 @@ process-relative-require@^1.0.0: dependencies: node-modules-path "^1.0.0" +process@~0.11.0: + version "0.11.9" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.9.tgz#7bd5ad21aa6253e7da8682264f1e11d11c0318c1" + progress@^1.1.8: version "1.1.8" resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" -promise-map-series@^0.2.1: +promise-map-series@^0.2.0, promise-map-series@^0.2.1: version "0.2.3" resolved "https://registry.yarnpkg.com/promise-map-series/-/promise-map-series-0.2.3.tgz#c2d377afc93253f6bd03dbb77755eb88ab20a847" dependencies: @@ -4293,7 +4759,21 @@ pseudomap@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" -punycode@^1.4.1: +public-encrypt@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.0.tgz#39f699f3a46560dd5ebacbca693caf7c65c18cc6" + dependencies: + bn.js "^4.1.0" + browserify-rsa "^4.0.0" + create-hash "^1.1.0" + parse-asn1 "^5.0.0" + randombytes "^2.0.1" + +punycode@1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" + +punycode@^1.3.2, punycode@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" @@ -4321,6 +4801,14 @@ qs@~6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.0.tgz#f403b264f23bc01228c74131b407f18d5ea5d442" +querystring-es3@~0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" + +querystring@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" + quick-temp@0.1.5, quick-temp@^0.1.0, quick-temp@^0.1.2, quick-temp@^0.1.3, quick-temp@^0.1.5: version "0.1.5" resolved "https://registry.yarnpkg.com/quick-temp/-/quick-temp-0.1.5.tgz#0d0d67f0fb6a589a0e142f90985f76cdbaf403f7" @@ -4333,6 +4821,10 @@ qunitjs@^1.20.0: version "1.23.1" resolved "https://registry.yarnpkg.com/qunitjs/-/qunitjs-1.23.1.tgz#1971cf97ac9be01a64d2315508d2e48e6fd4e719" +randombytes@^2.0.0, randombytes@^2.0.1: + version "2.0.3" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.3.tgz#674c99760901c3c4112771a31e521dc349cc09ec" + range-parser@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" @@ -4358,6 +4850,12 @@ read-installed@~4.0.3: optionalDependencies: graceful-fs "^4.1.2" +read-only-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-only-stream/-/read-only-stream-2.0.0.tgz#2724fd6a8113d73764ac288d4386270c1dbf17f0" + dependencies: + readable-stream "^2.0.2" + "read-package-json@1 || 2", read-package-json@^2.0.0, read-package-json@~2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/read-package-json/-/read-package-json-2.0.4.tgz#61ed1b2256ea438d8008895090be84b8e799c853" @@ -4389,9 +4887,9 @@ read@1, read@~1.0.1, read@~1.0.7: dependencies: mute-stream "~0.0.4" -"readable-stream@1 || 2", readable-stream@^2, "readable-stream@^2.0.0 || ^1.1.13", readable-stream@^2.0.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.2.tgz#a9e6fec3c7dda85f8bb1b3ba7028604556fc825e" +"readable-stream@1 || 2", readable-stream@^2, "readable-stream@^2.0.0 || ^1.1.13", readable-stream@^2.0.2, readable-stream@^2.1.0, readable-stream@^2.1.5, readable-stream@~2.1.2: + version "2.1.5" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.1.5.tgz#66fa8b720e1438b364681f2ad1a63c618448c9d0" dependencies: buffer-shims "^1.0.0" core-util-is "~1.0.0" @@ -4430,18 +4928,6 @@ readable-stream@~2.0.0, readable-stream@~2.0.5: string_decoder "~0.10.x" util-deprecate "~1.0.1" -readable-stream@~2.1.2: - version "2.1.5" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.1.5.tgz#66fa8b720e1438b364681f2ad1a63c618448c9d0" - dependencies: - buffer-shims "^1.0.0" - core-util-is "~1.0.0" - inherits "~2.0.1" - isarray "~1.0.0" - process-nextick-args "~1.0.6" - string_decoder "~0.10.x" - util-deprecate "~1.0.1" - readdir-scoped-modules@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.0.2.tgz#9fafa37d286be5d92cbaebdee030dc9b5f406747" @@ -4466,7 +4952,7 @@ realize-package-specifier@~3.0.3: dezalgo "^1.0.1" npm-package-arg "^4.1.1" -recast@0.10.33: +recast@0.10.33, recast@^0.10.10: version "0.10.33" resolved "https://registry.yarnpkg.com/recast/-/recast-0.10.33.tgz#942808f7aa016f1fa7142c461d7e5704aaa8d697" dependencies: @@ -4475,15 +4961,6 @@ recast@0.10.33: private "~0.1.5" source-map "~0.5.0" -recast@^0.10.10: - version "0.10.43" - resolved "https://registry.yarnpkg.com/recast/-/recast-0.10.43.tgz#b95d50f6d60761a5f6252e15d80678168491ce7f" - dependencies: - ast-types "0.8.15" - esprima-fb "~15001.1001.0-dev-harmony-fb" - private "~0.1.5" - source-map "~0.5.0" - recast@^0.11.17, recast@^0.11.3: version "0.11.18" resolved "https://registry.yarnpkg.com/recast/-/recast-0.11.18.tgz#07af6257ca769868815209401d4d60eef1b5b947" @@ -4643,7 +5120,7 @@ resolve-from@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226" -resolve@^1.1.2, resolve@^1.1.6, resolve@^1.1.7: +resolve@1.1.7, resolve@^1.1.2, resolve@^1.1.3, resolve@^1.1.4, resolve@^1.1.6, resolve@^1.1.7: version "1.1.7" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" @@ -4674,10 +5151,14 @@ rimraf@2, rimraf@^2.2.8, rimraf@^2.3.2, rimraf@^2.3.4, rimraf@^2.4.3, rimraf@^2. dependencies: glob "^7.0.5" -rimraf@~2.2.6: +rimraf@~2.2.6, rimraf@~2.2.8: version "2.2.8" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.2.8.tgz#e439be2aaee327321952730f99a8929e4fc50582" +ripemd160@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-1.0.1.tgz#93a4bbd4942bc574b69a8fa57c71de10ecca7d6e" + rsvp@^3.0.14, rsvp@^3.0.16, rsvp@^3.0.17, rsvp@^3.0.18, rsvp@^3.0.21, rsvp@^3.0.6, rsvp@^3.1.0, rsvp@^3.2.1: version "3.3.3" resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.3.3.tgz#34633caaf8bc66ceff4be3c2e1dffd032538a813" @@ -4776,6 +5257,12 @@ setprototypeof@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.2.tgz#81a552141ec104b88e89ce383103ad5c66564d08" +sha.js@^2.3.6, sha.js@~2.4.4: + version "2.4.8" + resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.8.tgz#37068c2c476b6baf402d14a49c67f597921f634f" + dependencies: + inherits "^2.0.1" + sha@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/sha/-/sha-2.0.1.tgz#6030822fbd2c9823949f8f72ed6411ee5cf25aae" @@ -4783,10 +5270,26 @@ sha@~2.0.1: graceful-fs "^4.1.2" readable-stream "^2.0.2" +shasum@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/shasum/-/shasum-1.0.2.tgz#e7012310d8f417f4deb5712150e5678b87ae565f" + dependencies: + json-stable-stringify "~0.0.0" + sha.js "~2.4.4" + shebang-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" +shell-quote@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.6.1.tgz#f4781949cce402697127430ea3b3c5476f481767" + dependencies: + array-filter "~0.0.0" + array-map "~0.0.0" + array-reduce "~0.0.0" + jsonify "~0.0.0" + shelljs@0.3.x: version "0.3.0" resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.3.0.tgz#3596e6307a781544f591f37da618360f31db57b1" @@ -4923,7 +5426,7 @@ source-map@0.4.x, source-map@^0.4.2, source-map@^0.4.4: dependencies: amdefine ">=0.0.4" -source-map@^0.5.0, source-map@~0.5.0, source-map@~0.5.1: +source-map@^0.5.0, source-map@~0.5.0, source-map@~0.5.1, source-map@~0.5.3: version "0.5.6" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" @@ -4983,7 +5486,38 @@ statuses@1, "statuses@>= 1.3.1 < 2", statuses@~1.3.0: version "1.3.1" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" -string-width@^1.0.1: +stream-browserify@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db" + dependencies: + inherits "~2.0.1" + readable-stream "^2.0.2" + +stream-combiner2@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/stream-combiner2/-/stream-combiner2-1.1.1.tgz#fb4d8a1420ea362764e21ad4780397bebcb41cbe" + dependencies: + duplexer2 "~0.1.0" + readable-stream "^2.0.2" + +stream-http@^2.0.0: + version "2.6.3" + resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.6.3.tgz#4c3ddbf9635968ea2cfd4e48d43de5def2625ac3" + dependencies: + builtin-status-codes "^3.0.0" + inherits "^2.0.1" + readable-stream "^2.1.0" + to-arraybuffer "^1.0.0" + xtend "^4.0.0" + +stream-splicer@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/stream-splicer/-/stream-splicer-2.0.0.tgz#1b63be438a133e4b671cc1935197600175910d83" + dependencies: + inherits "^2.0.1" + readable-stream "^2.0.2" + +string-width@^1.0.1, string-width@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" dependencies: @@ -5002,7 +5536,7 @@ string.prototype.codepointat@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/string.prototype.codepointat/-/string.prototype.codepointat-0.2.0.tgz#6b26e9bd3afcaa7be3b4269b526de1b82000ac78" -string_decoder@~0.10.x: +string_decoder@~0.10.0, string_decoder@~0.10.x: version "0.10.31" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" @@ -5058,6 +5592,12 @@ styled_string@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/styled_string/-/styled_string-0.0.1.tgz#d22782bd81295459bc4f1df18c4bad8e94dd124a" +subarg@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/subarg/-/subarg-1.0.0.tgz#f62cf17581e996b48fc965699f54c06ae268b8d2" + dependencies: + minimist "^1.1.0" + sum-up@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/sum-up/-/sum-up-1.0.3.tgz#1c661f667057f63bcb7875aa1438bc162525156e" @@ -5080,6 +5620,12 @@ sync-exec@^0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/sync-exec/-/sync-exec-0.6.2.tgz#717d22cc53f0ce1def5594362f3a89a2ebb91105" +syntax-error@^1.1.1: + version "1.1.6" + resolved "https://registry.yarnpkg.com/syntax-error/-/syntax-error-1.1.6.tgz#b4549706d386cc1c1dc7c2423f18579b6cade710" + dependencies: + acorn "^2.7.0" + table@^3.7.8: version "3.8.3" resolved "https://registry.yarnpkg.com/table/-/table-3.8.3.tgz#2bbc542f0fda9861a755d3947fefd8b3f513855f" @@ -5155,10 +5701,23 @@ text-table@~0.2.0: version "2.0.1" resolved "https://registry.yarnpkg.com/textextensions/-/textextensions-2.0.1.tgz#be8cf22d65379c151319f88f0335ad8f667abdca" -through@^2.3.6, through@~2.3.8: +through2@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be" + dependencies: + readable-stream "^2.1.5" + xtend "~4.0.1" + +"through@>=2.2.7 <3", through@^2.3.6, through@~2.3.8: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" +timers-browserify@^1.0.1: + version "1.4.2" + resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-1.4.2.tgz#c9c58b575be8407375cb5e2462dacee74359f41d" + dependencies: + process "~0.11.0" + tiny-lr@0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/tiny-lr/-/tiny-lr-0.2.1.tgz#b3fdba802e5d56a33c2f6f10794b32e477ac729d" @@ -5190,6 +5749,10 @@ to-array@0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/to-array/-/to-array-0.1.4.tgz#17e6c11f73dd4f3d74cda7a4ff3238e9ad9bf890" +to-arraybuffer@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" + to-fast-properties@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.2.tgz#f3f5c0c3ba7299a7ef99427e44633257ade43320" @@ -5234,6 +5797,10 @@ tryor@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/tryor/-/tryor-0.1.2.tgz#8145e4ca7caff40acde3ccf946e8b8bb75b4172b" +tty-browserify@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" + tunnel-agent@~0.4.1: version "0.4.3" resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.4.3.tgz#6373db76909fe570e08d73583365ed828a74eeeb" @@ -5288,6 +5855,10 @@ umask@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/umask/-/umask-1.1.0.tgz#f29cebf01df517912bb58ff9c4e50fde8e33320d" +umd@^3.0.0: + version "3.0.1" + resolved "http://registry.npmjs.org/umd/-/umd-3.0.1.tgz#8ae556e11011f63c2596708a8837259f01b3d60e" + underscore.string@~2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/underscore.string/-/underscore.string-2.3.3.tgz#71c08bf6b428b1133f37e78fa3a21c82f7329b0d" @@ -5306,6 +5877,13 @@ untildify@^2.1.0: dependencies: os-homedir "^1.0.0" +url@~0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" + dependencies: + punycode "1.3.2" + querystring "0.2.0" + user-home@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/user-home/-/user-home-1.1.1.tgz#2b5be23a32b63a7c9deb8d0f28d485724a3df190" @@ -5324,6 +5902,12 @@ util-extend@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/util-extend/-/util-extend-1.0.3.tgz#a7c216d267545169637b3b6edc6ca9119e2ff93f" +util@0.10.3, util@~0.10.1: + version "0.10.3" + resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" + dependencies: + inherits "2.0.1" + utils-merge@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.0.tgz#0294fb922bb9375153541c4f7096231f287c8af8" @@ -5359,6 +5943,12 @@ verror@1.3.6: dependencies: extsprintf "1.0.2" +vm-browserify@~0.0.1: + version "0.0.4" + resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-0.0.4.tgz#5d7ea45bbef9e4a6ff65f95438e0a87c357d5a73" + dependencies: + indexof "0.0.1" + walk-sync@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/walk-sync/-/walk-sync-0.1.3.tgz#8a07261a00bda6cfb1be25e9f100fad57546f583" @@ -5501,7 +6091,7 @@ xmlhttprequest-ssl@1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.1.tgz#3b7741fea4a86675976e908d296d4445961faa67" -xtend@^4.0.0: +xtend@^4.0.0, xtend@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" @@ -5528,6 +6118,12 @@ yargs-parser@^2.4.1: camelcase "^3.0.0" lodash.assign "^4.0.6" +yargs-parser@^4.2.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-4.2.1.tgz#29cceac0dc4f03c6c87b4a9f217dd18c9f74871c" + dependencies: + camelcase "^3.0.0" + yargs@^4.7.1: version "4.8.1" resolved "https://registry.yarnpkg.com/yargs/-/yargs-4.8.1.tgz#c0c42924ca4aaa6b0e6da1739dfb216439f9ddc0" @@ -5547,6 +6143,24 @@ yargs@^4.7.1: y18n "^3.2.1" yargs-parser "^2.4.1" +yargs@^6.5.0: + version "6.6.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-6.6.0.tgz#782ec21ef403345f830a808ca3d513af56065208" + dependencies: + camelcase "^3.0.0" + cliui "^3.2.0" + decamelize "^1.1.1" + get-caller-file "^1.0.1" + os-locale "^1.4.0" + read-pkg-up "^1.0.1" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^1.0.2" + which-module "^1.0.0" + y18n "^3.2.1" + yargs-parser "^4.2.0" + yargs@~3.10.0: version "3.10.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1" diff --git a/src/api-umbrella/cli/read_config.lua b/src/api-umbrella/cli/read_config.lua index 72c611d69..1ac9595ce 100644 --- a/src/api-umbrella/cli/read_config.lua +++ b/src/api-umbrella/cli/read_config.lua @@ -367,6 +367,11 @@ local function set_computed_config() dir = src_root_dir, }, web = { + admin = { + auth_strategies = { + ["_local_enabled?"] = array_includes(config["web"]["admin"]["auth_strategies"]["enabled"], "local"), + }, + }, dir = path.join(src_root_dir, "src/api-umbrella/web-app"), puma = { bind = "unix://" .. config["run_dir"] .. "/puma.sock", diff --git a/src/api-umbrella/web-app/Gemfile b/src/api-umbrella/web-app/Gemfile index 728a68559..4d7e9cbfe 100644 --- a/src/api-umbrella/web-app/Gemfile +++ b/src/api-umbrella/web-app/Gemfile @@ -69,10 +69,6 @@ gem "omniauth-ldap", "~> 1.0.5", :require => false # Authorization gem "pundit", "~> 1.1.0" -# Generate non-digest assets for i18n content that the admin-ui component can -# link to (without knowing the cache busted URLs). -gem "non-stupid-digest-assets", "~> 1.0.9" - # Views/templates for APIs gem "rabl", "~> 0.13.1" gem "jbuilder", "~> 2.6.0" @@ -120,9 +116,6 @@ gem "sass-rails", "~> 5.0" gem "bootstrap-sass", "~> 3.3.7" gem "font-awesome-rails", "~> 4.7.0" -# Login javascript minification -gem "uglifier", "~> 3.0.4", :require => false - # Bundle gems for the local environment. Make sure to # put test-only gems in this group so their generators # and rake tasks are available in development mode: diff --git a/src/api-umbrella/web-app/Gemfile.lock b/src/api-umbrella/web-app/Gemfile.lock index 06edee42c..2aa43563c 100644 --- a/src/api-umbrella/web-app/Gemfile.lock +++ b/src/api-umbrella/web-app/Gemfile.lock @@ -209,10 +209,6 @@ GEM net-ssh (4.0.1) nokogiri (1.7.0.1) mini_portile2 (~> 2.1.0) - non-stupid-digest-assets (1.0.9) - sprockets (>= 2.0) - oauth2 (1.2.0) - faraday (>= 0.8, < 0.10) oauth2 (1.3.0) faraday (>= 0.8, < 0.11) jwt (~> 1.0) @@ -323,8 +319,6 @@ GEM tilt (2.0.5) tzinfo (1.2.2) thread_safe (~> 0.1) - uglifier (3.0.4) - execjs (>= 0.3.0, < 3) unicode_utils (1.4.0) warden (1.2.7) rack (>= 1.0) @@ -361,7 +355,6 @@ DEPENDENCIES mongoid_rails_migrations (~> 1.1.0) multi_json (~> 1.12.1) nokogiri (~> 1.7.0) - non-stupid-digest-assets (~> 1.0.9) oj (~> 2.18.1) oj_mimic_json (~> 1.0.1) omniauth (~> 1.3.2) @@ -385,7 +378,6 @@ DEPENDENCIES seed-fu! sequel (~> 4.42.1) simple_form (~> 3.3.1) - uglifier (~> 3.0.4) BUNDLED WITH 1.14.3 diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/_common_validations.js.erb b/src/api-umbrella/web-app/app/assets/javascripts/admin/_common_validations.js.erb deleted file mode 100644 index 51c2c9283..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/_common_validations.js.erb +++ /dev/null @@ -1,5 +0,0 @@ -var CommonValidations = { - host_format: new RegExp(<%= CommonValidations.to_js(CommonValidations::HOST_FORMAT).to_json %>), - host_format_with_wildcard: new RegExp(<%= CommonValidations.to_js(CommonValidations::HOST_FORMAT_WITH_WILDCARD).to_json %>), - url_prefix_format: new RegExp(<%= CommonValidations.to_js(CommonValidations::URL_PREFIX_FORMAT).to_json %>) -}; diff --git a/src/api-umbrella/web-app/app/assets/javascripts/admin/server_side_loader.js b/src/api-umbrella/web-app/app/assets/javascripts/admin/server_side_loader.js deleted file mode 100644 index 86fb57d0a..000000000 --- a/src/api-umbrella/web-app/app/assets/javascripts/admin/server_side_loader.js +++ /dev/null @@ -1,3 +0,0 @@ -//= require i18n.js -//= require i18n/translations.js -//= require admin/_common_validations.js diff --git a/src/api-umbrella/web-app/app/controllers/admin/registrations_controller.rb b/src/api-umbrella/web-app/app/controllers/admin/registrations_controller.rb index 4339ebd28..ace0d642e 100644 --- a/src/api-umbrella/web-app/app/controllers/admin/registrations_controller.rb +++ b/src/api-umbrella/web-app/app/controllers/admin/registrations_controller.rb @@ -1,5 +1,5 @@ class Admin::RegistrationsController < Devise::RegistrationsController - before_action :first_time_setup + before_action :first_time_setup_check protected @@ -12,7 +12,7 @@ def build_resource(hash = nil) private - def first_time_setup + def first_time_setup_check unless(Admin.needs_first_account?) flash[:notice] = "An initial admin account already exists." redirect_to admin_path diff --git a/src/api-umbrella/web-app/app/controllers/admin/sessions_controller.rb b/src/api-umbrella/web-app/app/controllers/admin/sessions_controller.rb index 9dd6dbbad..d588bed7a 100644 --- a/src/api-umbrella/web-app/app/controllers/admin/sessions_controller.rb +++ b/src/api-umbrella/web-app/app/controllers/admin/sessions_controller.rb @@ -1,11 +1,14 @@ class Admin::SessionsController < Devise::SessionsController - before_action :first_time_setup + before_action :first_time_setup_check + before_action :only_for_local_auth, :only => [:create] skip_after_action :verify_authorized def auth response = { "authenticated" => !current_admin.nil?, "enable_beta_analytics" => (ApiUmbrellaConfig[:analytics][:adapter] == "kylin" || (ApiUmbrellaConfig[:analytics][:outputs] && ApiUmbrellaConfig[:analytics][:outputs].include?("kylin"))), + "username_is_email" => ApiUmbrellaConfig[:web][:admin][:username_is_email], + "local_auth_enabled" => ApiUmbrellaConfig[:web][:admin][:auth_strategies][:_local_enabled?], } if current_admin @@ -37,9 +40,15 @@ def set_flash_message(key, kind, options = {}) end end - def first_time_setup + def first_time_setup_check if(Admin.needs_first_account?) redirect_to new_admin_registration_path end end + + def only_for_local_auth + unless(ApiUmbrellaConfig[:web][:admin][:auth_strategies][:_local_enabled?]) + raise ActionController::RoutingError.new("Not Found") + end + end end diff --git a/src/api-umbrella/web-app/app/models/admin.rb b/src/api-umbrella/web-app/app/models/admin.rb index 7ae46bd44..b7c9efbba 100644 --- a/src/api-umbrella/web-app/app/models/admin.rb +++ b/src/api-umbrella/web-app/app/models/admin.rb @@ -71,8 +71,7 @@ class Admin :if => :username_is_email? validates :email, :presence => true, - :format => Devise.email_regexp, - :if => :email_required? + :format => Devise.email_regexp validates :password, :presence => true, :confirmation => true, @@ -97,7 +96,7 @@ def self.sorted end def self.needs_first_account? - ApiUmbrellaConfig[:web][:admin][:auth_strategies][:enabled].include?("local") && self.unscoped.count == 0 + ApiUmbrellaConfig[:web][:admin][:auth_strategies][:_local_enabled?] && self.unscoped.count == 0 end def group_names @@ -211,11 +210,7 @@ def serializable_hash(options = nil) end def username_is_email? - true - end - - def email_required? - !username_is_email? + ApiUmbrellaConfig[:web][:admin][:username_is_email] end def password_required? diff --git a/src/api-umbrella/web-app/app/views/devise/sessions/new.html.erb b/src/api-umbrella/web-app/app/views/devise/sessions/new.html.erb index 5ed583ca2..d56098fd6 100644 --- a/src/api-umbrella/web-app/app/views/devise/sessions/new.html.erb +++ b/src/api-umbrella/web-app/app/views/devise/sessions/new.html.erb @@ -1,7 +1,7 @@

<%= t(".admin_sign_in") %>

{{/if}} diff --git a/src/api-umbrella/admin-ui/app/templates/components/form-fields/ace-field.hbs b/src/api-umbrella/admin-ui/app/templates/components/form-fields/ace-field.hbs index 694f2f2c4..11ed73bab 100644 --- a/src/api-umbrella/admin-ui/app/templates/components/form-fields/ace-field.hbs +++ b/src/api-umbrella/admin-ui/app/templates/components/form-fields/ace-field.hbs @@ -1,3 +1,3 @@ -{{#form-fields/field-wrapper model=model style=style fieldName=fieldName inputId=inputId labelForId=aceTextInputId label=label tooltip=tooltip}} +{{#form-fields/field-wrapper model=model style=style fieldName=fieldName inputId=inputId labelForId=aceTextInputId label=label tooltip=tooltip hint=hint}} {{one-way-textarea value=(get model fieldName) update=(action (mut (get model fieldName))) class="form-control" data-ace-mode=mode}} {{/form-fields/field-wrapper}} diff --git a/src/api-umbrella/admin-ui/app/templates/components/form-fields/checkboxes-field.hbs b/src/api-umbrella/admin-ui/app/templates/components/form-fields/checkboxes-field.hbs index 2c82d545c..6e59b061c 100644 --- a/src/api-umbrella/admin-ui/app/templates/components/form-fields/checkboxes-field.hbs +++ b/src/api-umbrella/admin-ui/app/templates/components/form-fields/checkboxes-field.hbs @@ -1,4 +1,4 @@ -{{#form-fields/field-wrapper model=model style=style fieldName=fieldName inputId=inputId label=label tooltip=tooltip}} +{{#form-fields/field-wrapper model=model style=style fieldName=fieldName inputId=inputId label=label tooltip=tooltip hint=hint}} {{#multiselect-checkboxes options=options selection=(get model fieldName) tagName="div" valueProperty="id" as |option isSelected|}}
diff --git a/src/api-umbrella/admin-ui/app/templates/components/form-fields/field-wrapper.hbs b/src/api-umbrella/admin-ui/app/templates/components/form-fields/field-wrapper.hbs index 23b3cf2cf..29b70393b 100644 --- a/src/api-umbrella/admin-ui/app/templates/components/form-fields/field-wrapper.hbs +++ b/src/api-umbrella/admin-ui/app/templates/components/form-fields/field-wrapper.hbs @@ -7,6 +7,7 @@
{{yield}} + {{form-fields/hint hint=hint}} {{form-fields/error-messages hasErrors=fieldErrorMessages errorMessages=fieldErrorMessages}}
@@ -20,6 +21,7 @@
{{yield}} + {{form-fields/hint hint=hint}} {{form-fields/error-messages hasErrors=fieldErrorMessages errorMessages=fieldErrorMessages}}
@@ -29,6 +31,7 @@ {{help-tooltip tooltip=tooltip}} {{yield}} + {{form-fields/hint hint=hint}} {{form-fields/error-messages hasErrors=fieldErrorMessages errorMessages=fieldErrorMessages}} {{/if}} diff --git a/src/api-umbrella/admin-ui/app/templates/components/form-fields/hint.hbs b/src/api-umbrella/admin-ui/app/templates/components/form-fields/hint.hbs new file mode 100644 index 000000000..e8a886953 --- /dev/null +++ b/src/api-umbrella/admin-ui/app/templates/components/form-fields/hint.hbs @@ -0,0 +1,3 @@ +{{#if hint}} +

{{hint}}

+{{/if}} diff --git a/src/api-umbrella/admin-ui/app/templates/components/form-fields/password-field.hbs b/src/api-umbrella/admin-ui/app/templates/components/form-fields/password-field.hbs index 25ee9503b..ced04717f 100644 --- a/src/api-umbrella/admin-ui/app/templates/components/form-fields/password-field.hbs +++ b/src/api-umbrella/admin-ui/app/templates/components/form-fields/password-field.hbs @@ -1,3 +1,3 @@ -{{#form-fields/field-wrapper model=model style=style fieldName=fieldName inputId=inputId label=label tooltip=tooltip}} +{{#form-fields/field-wrapper model=model style=style fieldName=fieldName inputId=inputId label=label tooltip=tooltip hint=hint}} {{/form-fields/field-wrapper}} diff --git a/src/api-umbrella/admin-ui/app/templates/components/form-fields/select-field.hbs b/src/api-umbrella/admin-ui/app/templates/components/form-fields/select-field.hbs index ebe2b6f2d..a81845eba 100644 --- a/src/api-umbrella/admin-ui/app/templates/components/form-fields/select-field.hbs +++ b/src/api-umbrella/admin-ui/app/templates/components/form-fields/select-field.hbs @@ -1,3 +1,3 @@ -{{#form-fields/field-wrapper model=model style=style fieldName=fieldName inputId=inputId label=label tooltip=tooltip}} +{{#form-fields/field-wrapper model=model style=style fieldName=fieldName inputId=inputId label=label tooltip=tooltip hint=hint}} {{select-menu value=(get model fieldName) action=(action (mut (get model fieldName))) options=options inputId=inputId inputClass="form-control"}} {{/form-fields/field-wrapper}} diff --git a/src/api-umbrella/admin-ui/app/templates/components/form-fields/selectize-field.hbs b/src/api-umbrella/admin-ui/app/templates/components/form-fields/selectize-field.hbs index df4c2305e..b2d020fcc 100644 --- a/src/api-umbrella/admin-ui/app/templates/components/form-fields/selectize-field.hbs +++ b/src/api-umbrella/admin-ui/app/templates/components/form-fields/selectize-field.hbs @@ -1,3 +1,3 @@ -{{#form-fields/field-wrapper model=model style=style fieldName=fieldName inputId=inputId labelForId=selectizeTextInputId label=label tooltip=tooltip}} +{{#form-fields/field-wrapper model=model style=style fieldName=fieldName inputId=inputId labelForId=selectizeTextInputId label=label tooltip=tooltip hint=hint}} {{/form-fields/field-wrapper}} diff --git a/src/api-umbrella/admin-ui/app/templates/components/form-fields/static-field.hbs b/src/api-umbrella/admin-ui/app/templates/components/form-fields/static-field.hbs index 3901d2c9b..17c5d01b7 100644 --- a/src/api-umbrella/admin-ui/app/templates/components/form-fields/static-field.hbs +++ b/src/api-umbrella/admin-ui/app/templates/components/form-fields/static-field.hbs @@ -1,4 +1,4 @@ -{{#form-fields/field-wrapper model=model style=style fieldName=fieldName inputId=inputId label=label tooltip=tooltip}} +{{#form-fields/field-wrapper model=model style=style fieldName=fieldName inputId=inputId label=label tooltip=tooltip hint=hint}}
{{#if hasBlock}} {{yield}} diff --git a/src/api-umbrella/admin-ui/app/templates/components/form-fields/text-field.hbs b/src/api-umbrella/admin-ui/app/templates/components/form-fields/text-field.hbs index f1f9cdf6b..f067c910c 100644 --- a/src/api-umbrella/admin-ui/app/templates/components/form-fields/text-field.hbs +++ b/src/api-umbrella/admin-ui/app/templates/components/form-fields/text-field.hbs @@ -1,3 +1,3 @@ -{{#form-fields/field-wrapper model=model style=style fieldName=fieldName inputId=inputId label=label tooltip=tooltip}} +{{#form-fields/field-wrapper model=model style=style fieldName=fieldName inputId=inputId label=label tooltip=tooltip hint=hint}} {{/form-fields/field-wrapper}} diff --git a/src/api-umbrella/admin-ui/app/templates/components/form-fields/textarea-field.hbs b/src/api-umbrella/admin-ui/app/templates/components/form-fields/textarea-field.hbs index 97e59ed5e..2969dc339 100644 --- a/src/api-umbrella/admin-ui/app/templates/components/form-fields/textarea-field.hbs +++ b/src/api-umbrella/admin-ui/app/templates/components/form-fields/textarea-field.hbs @@ -1,3 +1,3 @@ -{{#form-fields/field-wrapper model=model style=style fieldName=fieldName inputId=inputId label=label tooltip=tooltip}} +{{#form-fields/field-wrapper model=model style=style fieldName=fieldName inputId=inputId label=label tooltip=tooltip hint=hint}} {{/form-fields/field-wrapper}} diff --git a/src/api-umbrella/web-app/app/controllers/admin/sessions_controller.rb b/src/api-umbrella/web-app/app/controllers/admin/sessions_controller.rb index d588bed7a..a40159ce2 100644 --- a/src/api-umbrella/web-app/app/controllers/admin/sessions_controller.rb +++ b/src/api-umbrella/web-app/app/controllers/admin/sessions_controller.rb @@ -9,6 +9,7 @@ def auth "enable_beta_analytics" => (ApiUmbrellaConfig[:analytics][:adapter] == "kylin" || (ApiUmbrellaConfig[:analytics][:outputs] && ApiUmbrellaConfig[:analytics][:outputs].include?("kylin"))), "username_is_email" => ApiUmbrellaConfig[:web][:admin][:username_is_email], "local_auth_enabled" => ApiUmbrellaConfig[:web][:admin][:auth_strategies][:_local_enabled?], + "password_length_min" => ApiUmbrellaConfig[:web][:admin][:password_length_min], } if current_admin diff --git a/src/api-umbrella/web-app/app/views/devise/passwords/edit.html.erb b/src/api-umbrella/web-app/app/views/devise/passwords/edit.html.erb index 1c97866a2..5a5b18515 100644 --- a/src/api-umbrella/web-app/app/views/devise/passwords/edit.html.erb +++ b/src/api-umbrella/web-app/app/views/devise/passwords/edit.html.erb @@ -8,7 +8,7 @@ <%= f.full_error :reset_password_token %>
- <%= f.input :password, :label => t(".new_password"), :required => true, :autofocus => true %> + <%= f.input :password, :label => t(".new_password"), :required => true, :autofocus => true, :hint => t("devise.passwords.password_length_hint", :min => ApiUmbrellaConfig[:web][:admin][:password_length_min]) %> <%= f.input :password_confirmation, :label => t(".confirm_new_password"), :required => true %>
diff --git a/src/api-umbrella/web-app/app/views/devise/registrations/new.html.erb b/src/api-umbrella/web-app/app/views/devise/registrations/new.html.erb index e6654e6d5..c50efc25c 100644 --- a/src/api-umbrella/web-app/app/views/devise/registrations/new.html.erb +++ b/src/api-umbrella/web-app/app/views/devise/registrations/new.html.erb @@ -10,7 +10,7 @@
<%= f.input :username, :required => true, :autofocus => true %> - <%= f.input :password, :required => true, :hint => ("#{@minimum_password_length} characters minimum" if @minimum_password_length) %> + <%= f.input :password, :required => true, :hint => t("devise.passwords.password_length_hint", :min => ApiUmbrellaConfig[:web][:admin][:password_length_min]) %> <%= f.input :password_confirmation, :required => true %>
diff --git a/src/api-umbrella/web-app/config/locales/en.yml b/src/api-umbrella/web-app/config/locales/en.yml index 74c9f45e2..22d42d7dd 100644 --- a/src/api-umbrella/web-app/config/locales/en.yml +++ b/src/api-umbrella/web-app/config/locales/en.yml @@ -438,6 +438,7 @@ en: new: admin_sign_in: Admin Sign In passwords: + password_length_hint: "%{min} characters minimum" edit: change_my_password: Change My Password change_your_password: Change Your Password diff --git a/test/admin_ui/login/test_first_time_setup.rb b/test/admin_ui/login/test_first_time_setup.rb index 34bca88be..037c8c29e 100644 --- a/test/admin_ui/login/test_first_time_setup.rb +++ b/test/admin_ui/login/test_first_time_setup.rb @@ -16,6 +16,7 @@ def test_redirects_to_signup_on_first_login assert_content("Welcome!") assert_content("It looks like you're setting up API Umbrella for the first time. Create your first admin account to get started.") + assert_content("14 characters minimum") assert_equal("/admins/signup", page.current_path) fill_in "Email", :with => "new@example.com" diff --git a/test/admin_ui/login/test_forgot_password.rb b/test/admin_ui/login/test_forgot_password.rb index 7ad80634e..d854def8c 100644 --- a/test/admin_ui/login/test_forgot_password.rb +++ b/test/admin_ui/login/test_forgot_password.rb @@ -62,6 +62,9 @@ def test_reset_process reset_url = message["_mime_parts"]["text/html; charset=UTF-8"]["Body"].match(%r{/admins/password/edit\?reset_password_token=[^"]+})[0] visit reset_url + assert_text("Change Your Password") + assert_content("14 characters minimum") + # Too short password fill_in "New Password", :with => "short" fill_in "Confirm New Password", :with => "short" diff --git a/test/support/api_umbrella_test_helpers/admin_auth.rb b/test/support/api_umbrella_test_helpers/admin_auth.rb index c8430d3f8..b96c77265 100644 --- a/test/support/api_umbrella_test_helpers/admin_auth.rb +++ b/test/support/api_umbrella_test_helpers/admin_auth.rb @@ -109,6 +109,7 @@ def assert_password_fields_on_my_account_admin_form_only assert_field("Current Password") assert_field("New Password") assert_field("Confirm New Password") + assert_content("14 characters minimum") # Admins cannot set new admin passwords visit "/admin/#/admins/new" From 6fabd3744c70c66cd3bb8578121e1add3f4709d1 Mon Sep 17 00:00:00 2001 From: Nick Muerdter Date: Sat, 28 Jan 2017 22:39:54 -0700 Subject: [PATCH 15/26] Fix password assignment. --- .../controllers/api/v1/admins_controller.rb | 8 +- src/api-umbrella/web-app/app/models/admin.rb | 44 +++- test/admin_ui/login/test_local_provider.rb | 34 ++- test/apis/v1/admins/test_create.rb | 16 +- test/apis/v1/admins/test_passwords.rb | 226 ++++++++++++++++++ 5 files changed, 312 insertions(+), 16 deletions(-) create mode 100644 test/apis/v1/admins/test_passwords.rb diff --git a/src/api-umbrella/web-app/app/controllers/api/v1/admins_controller.rb b/src/api-umbrella/web-app/app/controllers/api/v1/admins_controller.rb index e422693d9..c69d2a554 100644 --- a/src/api-umbrella/web-app/app/controllers/api/v1/admins_controller.rb +++ b/src/api-umbrella/web-app/app/controllers/api/v1/admins_controller.rb @@ -58,7 +58,7 @@ def update # If a user is updating themselves, make sure they remain signed in. # This eliminates the current user getting logged out if they change # their password. - if(current_admin && @admin.id == current_admin.id) + if(@admin.id == current_admin.id) bypass_sign_in(@admin, :scope => :admin) end @@ -80,7 +80,11 @@ def destroy def save! authorize(@admin) unless(@admin.new_record?) - @admin.assign_with_password(admin_params) + if(@admin.id && @admin.id == current_admin.id) + @admin.assign_with_password(admin_params) + else + @admin.assign_without_password(admin_params) + end authorize(@admin) end diff --git a/src/api-umbrella/web-app/app/models/admin.rb b/src/api-umbrella/web-app/app/models/admin.rb index b7c9efbba..3ee67456a 100644 --- a/src/api-umbrella/web-app/app/models/admin.rb +++ b/src/api-umbrella/web-app/app/models/admin.rb @@ -54,6 +54,9 @@ class Admin field :invitation_accepted_at, :type => Time field :invitation_limit, :type => Integer + # Virtual fields + attr_accessor :current_password_invalid_reason + # Relations has_and_belongs_to_many :groups, :class_name => "AdminGroup", :inverse_of => nil @@ -68,24 +71,31 @@ class Admin :uniqueness => true validates :username, :format => Devise.email_regexp, + :allow_blank => true, :if => :username_is_email? validates :email, :presence => true, - :format => Devise.email_regexp + :if => :email_required? + validates :email, + :format => Devise.email_regexp, + :allow_blank => true validates :password, :presence => true, :confirmation => true, - :length => { :in => Devise.password_length }, :if => :password_required? + validates :password, + :length => { :in => Devise.password_length }, + :allow_blank => true if(ApiUmbrellaConfig[:web][:admin][:password_regex]) validates :password, :format => { :with => Regexp.new(ApiUmbrellaConfig[:web][:admin][:password_regex]), :message => :password_format }, - :if => :password_required? + :allow_blank => true end validates :password_confirmation, :presence => true, :if => :password_required? validate :validate_superuser_or_groups + validate :validate_current_password # Callbacks before_validation :sync_username_and_email @@ -213,10 +223,20 @@ def username_is_email? ApiUmbrellaConfig[:web][:admin][:username_is_email] end + # Only require the password fields for validation if they've been entered (if + # they're left blank, we don't want to require these fields). def password_required? password.present? || password_confirmation.present? end + # Only require the email field for validation if it won't be synced with the + # username field. This just prevents duplicate validation errors from showing + # up when the fields are synced (since the user doesn't see the separate + # email field, even though we populate it). + def email_required? + !ApiUmbrellaConfig[:web][:admin][:username_is_email] + end + def assign_without_password(params, *options) params.delete(:password) params.delete(:password_confirmation) @@ -227,16 +247,16 @@ def assign_with_password(params, *options) current_password = params.delete(:current_password) # Don't try to set the password unless it was explicitly set. - if(params[:password].blank?) + if(params[:password].present? || params[:password_confirmation].present?) + unless(valid_password?(current_password)) + self.current_password_invalid_reason = if(current_password.blank?) then :blank else :invalid end + end + else params.delete(:password) - params.delete(:password_confirmation) if(params[:password_confirmation].blank?) + params.delete(:password_confirmation) end self.assign_attributes(params, *options) - if(!valid_password?(current_password)) - self.valid? - self.errors.add(:current_password, current_password.blank? ? :blank : :invalid) - end end private @@ -265,4 +285,10 @@ def validate_superuser_or_groups self.errors.add(:groups, "must belong to at least one group or be a superuser") end end + + def validate_current_password + if(self.current_password_invalid_reason) + self.errors.add(:current_password, self.current_password_invalid_reason) + end + end end diff --git a/test/admin_ui/login/test_local_provider.rb b/test/admin_ui/login/test_local_provider.rb index 306505afe..ba3065dc6 100644 --- a/test/admin_ui/login/test_local_provider.rb +++ b/test/admin_ui/login/test_local_provider.rb @@ -158,7 +158,7 @@ def test_update_my_account_with_password fill_in "New Password", :with => "short" fill_in "Confirm New Password", :with => "short" click_button "Save" - assert_content("is too short (minimum is 14 characters)") + assert_content("Password: is too short (minimum is 14 characters)") @admin.reload assert_equal(original_encrypted_password, @admin.encrypted_password) assert_nil(@admin.notes) @@ -167,19 +167,45 @@ def test_update_my_account_with_password fill_in "New Password", :with => "mismatch123456" fill_in "Confirm New Password", :with => "mismatcH123456" click_button "Save" - assert_content("doesn't match Password") + assert_content("Password Confirmation: doesn't match Password") + @admin.reload + assert_equal(original_encrypted_password, @admin.encrypted_password) + assert_nil(@admin.notes) + + # No current password + fill_in "Current Password", :with => "" + fill_in "New Password", :with => "password234567" + fill_in "Confirm New Password", :with => "password234567" + click_button "Save" + assert_content("Current Password: can't be blank") + @admin.reload + assert_equal(original_encrypted_password, @admin.encrypted_password) + assert_nil(@admin.notes) + + # Invalid current password + fill_in "Current Password", :with => "password345678" + fill_in "New Password", :with => "password234567" + fill_in "Confirm New Password", :with => "password234567" + click_button "Save" + assert_content("Current Password: is invalid") @admin.reload assert_equal(original_encrypted_password, @admin.encrypted_password) assert_nil(@admin.notes) # Valid password - fill_in "New Password", :with => "password123456" - fill_in "Confirm New Password", :with => "password123456" + fill_in "Current Password", :with => "password123456" + fill_in "New Password", :with => "password234567" + fill_in "Confirm New Password", :with => "password234567" click_button "Save" assert_content("Successfully saved the admin") @admin.reload assert(@admin.encrypted_password) refute_equal(original_encrypted_password, @admin.encrypted_password) assert_equal("Foo", @admin.notes) + + # Stays signed in after changing password + admin = FactoryGirl.create(:admin, :notes => "After password change") + visit "/admin/#/admins/#{admin.id}/edit" + assert_field("Notes", :with => "After password change") end end diff --git a/test/apis/v1/admins/test_create.rb b/test/apis/v1/admins/test_create.rb index 7e73165e8..87000a1ba 100644 --- a/test/apis/v1/admins/test_create.rb +++ b/test/apis/v1/admins/test_create.rb @@ -3,10 +3,10 @@ class Test::Apis::V1::Admins::TestCreate < Minitest::Test include ApiUmbrellaTestHelpers::AdminAuth include ApiUmbrellaTestHelpers::Setup + parallelize_me! def setup setup_server - Admin.delete_all end def test_downcases_username @@ -24,4 +24,18 @@ def test_downcases_username admin = Admin.find(data["admin"]["id"]) assert_equal("hello@example.com", admin.username) end + + def test_required_validations + response = Typhoeus.post("https://127.0.0.1:9081/api-umbrella/v1/admins.json", http_options.deep_merge(admin_token).deep_merge({ + :headers => { "Content-Type" => "application/x-www-form-urlencoded" }, + :body => { :admin => {} }, + })) + assert_response_code(422, response) + + data = MultiJson.load(response.body) + assert_equal([ + "Email: can't be blank", + "Groups: must belong to at least one group or be a superuser", + ].sort, data["errors"].map { |e| e["full_message"] }.sort) + end end diff --git a/test/apis/v1/admins/test_passwords.rb b/test/apis/v1/admins/test_passwords.rb new file mode 100644 index 000000000..380062df6 --- /dev/null +++ b/test/apis/v1/admins/test_passwords.rb @@ -0,0 +1,226 @@ +require_relative "../../../test_helper" + +class Test::Apis::V1::Admins::TestPasswords < Minitest::Test + include ApiUmbrellaTestHelpers::AdminAuth + include ApiUmbrellaTestHelpers::Setup + parallelize_me! + + def setup + setup_server + end + + def test_ignores_passwords_on_create + attributes = FactoryGirl.build(:admin).serializable_hash.deep_merge({ + "encrypted_password" => BCrypt::Password.create("password234567"), + "password" => "password234567", + "password_confirmation" => "password234567", + }) + response = Typhoeus.post("https://127.0.0.1:9081/api-umbrella/v1/admins.json", http_options.deep_merge(admin_token).deep_merge({ + :headers => { "Content-Type" => "application/x-www-form-urlencoded" }, + :body => { :admin => attributes }, + })) + assert_response_code(201, response) + + data = MultiJson.load(response.body) + admin = Admin.find(data["admin"]["id"]) + assert_nil(admin.encrypted_password) + end + + def test_ignores_passwords_when_updating_another_admin + other_admin = FactoryGirl.create(:admin) + original_encrypted_password = other_admin.encrypted_password + + attributes = FactoryGirl.build(:admin).serializable_hash.deep_merge({ + "encrypted_password" => BCrypt::Password.create("password234567"), + "password" => "password234567", + "password_confirmation" => "password234567", + }) + response = Typhoeus.put("https://127.0.0.1:9081/api-umbrella/v1/admins/#{other_admin.id}.json", http_options.deep_merge(admin_token).deep_merge({ + :headers => { "Content-Type" => "application/x-www-form-urlencoded" }, + :body => { :admin => attributes }, + })) + assert_response_code(200, response) + + other_admin.reload + assert_equal(original_encrypted_password, other_admin.encrypted_password) + end + + def test_accepts_password_change_when_updating_own_admin + admin = FactoryGirl.create(:admin) + original_encrypted_password = admin.encrypted_password + + attributes = admin.serializable_hash.deep_merge({ + "encrypted_password" => BCrypt::Password.create("ignored"), + "current_password" => "password123456", + "password" => "password234567", + "password_confirmation" => "password234567", + }) + response = Typhoeus.put("https://127.0.0.1:9081/api-umbrella/v1/admins/#{admin.id}.json", http_options.deep_merge(admin_token(admin)).deep_merge({ + :headers => { "Content-Type" => "application/x-www-form-urlencoded" }, + :body => { :admin => attributes }, + })) + assert_response_code(200, response) + + admin.reload + refute_equal(original_encrypted_password, admin.encrypted_password) + end + + def test_rejects_password_change_when_current_password_missing + admin = FactoryGirl.create(:admin) + original_encrypted_password = admin.encrypted_password + + attributes = admin.serializable_hash.deep_merge({ + "password" => "password234567", + "password_confirmation" => "password234567", + }) + response = Typhoeus.put("https://127.0.0.1:9081/api-umbrella/v1/admins/#{admin.id}.json", http_options.deep_merge(admin_token(admin)).deep_merge({ + :headers => { "Content-Type" => "application/x-www-form-urlencoded" }, + :body => { :admin => attributes }, + })) + assert_response_code(422, response) + data = MultiJson.load(response.body) + assert_equal([ + "Current Password: can't be blank", + ].sort, data["errors"].map { |e| e["full_message"] }.sort) + + admin.reload + assert_equal(original_encrypted_password, admin.encrypted_password) + end + + def test_rejects_password_change_when_current_password_empty + admin = FactoryGirl.create(:admin) + original_encrypted_password = admin.encrypted_password + + attributes = admin.serializable_hash.deep_merge({ + "current_password" => "", + "password" => "password234567", + "password_confirmation" => "password234567", + }) + response = Typhoeus.put("https://127.0.0.1:9081/api-umbrella/v1/admins/#{admin.id}.json", http_options.deep_merge(admin_token(admin)).deep_merge({ + :headers => { "Content-Type" => "application/x-www-form-urlencoded" }, + :body => { :admin => attributes }, + })) + assert_response_code(422, response) + data = MultiJson.load(response.body) + assert_equal([ + "Current Password: can't be blank", + ].sort, data["errors"].map { |e| e["full_message"] }.sort) + + admin.reload + assert_equal(original_encrypted_password, admin.encrypted_password) + end + + def test_rejects_password_change_when_current_password_invalid + admin = FactoryGirl.create(:admin) + original_encrypted_password = admin.encrypted_password + + attributes = admin.serializable_hash.deep_merge({ + "current_password" => "password234567", + "password" => "password234567", + "password_confirmation" => "password234567", + }) + response = Typhoeus.put("https://127.0.0.1:9081/api-umbrella/v1/admins/#{admin.id}.json", http_options.deep_merge(admin_token(admin)).deep_merge({ + :headers => { "Content-Type" => "application/x-www-form-urlencoded" }, + :body => { :admin => attributes }, + })) + assert_response_code(422, response) + data = MultiJson.load(response.body) + assert_equal([ + "Current Password: is invalid", + ].sort, data["errors"].map { |e| e["full_message"] }.sort) + + admin.reload + assert_equal(original_encrypted_password, admin.encrypted_password) + end + + def test_requires_confirmation_if_password_present + admin = FactoryGirl.create(:admin) + original_encrypted_password = admin.encrypted_password + + attributes = admin.serializable_hash.deep_merge({ + "current_password" => "password123456", + "password" => "password234567", + }) + response = Typhoeus.put("https://127.0.0.1:9081/api-umbrella/v1/admins/#{admin.id}.json", http_options.deep_merge(admin_token(admin)).deep_merge({ + :headers => { "Content-Type" => "application/x-www-form-urlencoded" }, + :body => { :admin => attributes }, + })) + assert_response_code(422, response) + data = MultiJson.load(response.body) + assert_equal([ + "Password Confirmation: can't be blank", + ].sort, data["errors"].map { |e| e["full_message"] }.sort) + + admin.reload + assert_equal(original_encrypted_password, admin.encrypted_password) + end + + def test_requires_password_if_confirmation_present + admin = FactoryGirl.create(:admin) + original_encrypted_password = admin.encrypted_password + + attributes = admin.serializable_hash.deep_merge({ + "current_password" => "password123456", + "password_confirmation" => "password234567", + }) + response = Typhoeus.put("https://127.0.0.1:9081/api-umbrella/v1/admins/#{admin.id}.json", http_options.deep_merge(admin_token(admin)).deep_merge({ + :headers => { "Content-Type" => "application/x-www-form-urlencoded" }, + :body => { :admin => attributes }, + })) + assert_response_code(422, response) + data = MultiJson.load(response.body) + assert_equal([ + "Password: can't be blank", + "Password Confirmation: doesn't match Password", + ].sort, data["errors"].map { |e| e["full_message"] }.sort) + + admin.reload + assert_equal(original_encrypted_password, admin.encrypted_password) + end + + def test_validates_password_length + admin = FactoryGirl.create(:admin) + original_encrypted_password = admin.encrypted_password + + attributes = admin.serializable_hash.deep_merge({ + "current_password" => "password123456", + "password" => "short", + "password_confirmation" => "short", + }) + response = Typhoeus.put("https://127.0.0.1:9081/api-umbrella/v1/admins/#{admin.id}.json", http_options.deep_merge(admin_token(admin)).deep_merge({ + :headers => { "Content-Type" => "application/x-www-form-urlencoded" }, + :body => { :admin => attributes }, + })) + assert_response_code(422, response) + data = MultiJson.load(response.body) + assert_equal([ + "Password: is too short (minimum is 14 characters)", + ].sort, data["errors"].map { |e| e["full_message"] }.sort) + + admin.reload + assert_equal(original_encrypted_password, admin.encrypted_password) + end + + def test_validates_password_confirmation_matches + admin = FactoryGirl.create(:admin) + original_encrypted_password = admin.encrypted_password + + attributes = admin.serializable_hash.deep_merge({ + "current_password" => "password123456", + "password" => "mismatch123456", + "password_confirmation" => "mismatcH123456", + }) + response = Typhoeus.put("https://127.0.0.1:9081/api-umbrella/v1/admins/#{admin.id}.json", http_options.deep_merge(admin_token(admin)).deep_merge({ + :headers => { "Content-Type" => "application/x-www-form-urlencoded" }, + :body => { :admin => attributes }, + })) + assert_response_code(422, response) + data = MultiJson.load(response.body) + assert_equal([ + "Password Confirmation: doesn't match Password", + ].sort, data["errors"].map { |e| e["full_message"] }.sort) + + admin.reload + assert_equal(original_encrypted_password, admin.encrypted_password) + end +end From e42234fc8c411ab5df885025e4bdbf0a86c29e94 Mon Sep 17 00:00:00 2001 From: Nick Muerdter Date: Sun, 29 Jan 2017 11:01:38 -0700 Subject: [PATCH 16/26] Reorganize how the password fields are checked in test helpers. --- .../api_umbrella_test_helpers/admin_auth.rb | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/test/support/api_umbrella_test_helpers/admin_auth.rb b/test/support/api_umbrella_test_helpers/admin_auth.rb index b96c77265..47ae936e3 100644 --- a/test/support/api_umbrella_test_helpers/admin_auth.rb +++ b/test/support/api_umbrella_test_helpers/admin_auth.rb @@ -84,16 +84,18 @@ def assert_no_password_fields_on_admin_forms # Admin cannot edit their own password visit "/admin/#/admins/#{admin1.id}/edit" assert_content("Edit Admin") - refute_content("Password") - - # Admins cannot set new admin passwords - visit "/admin/#/admins/new" - assert_content("Add Admin") + assert_field("Email", :with => admin1.username) refute_content("Password") # Admins cannot edit other admin passwords visit "/admin/#/admins/#{admin2.id}/edit" assert_content("Edit Admin") + assert_field("Email", :with => admin2.username) + refute_content("Password") + + # Admins cannot set new admin passwords + visit "/admin/#/admins/new" + assert_content("Add Admin") refute_content("Password") end @@ -105,20 +107,22 @@ def assert_password_fields_on_my_account_admin_form_only # Admin can edit their own password visit "/admin/#/admins/#{admin1.id}/edit" assert_content("Edit Admin") + assert_field("Email", :with => admin1.username) assert_content("Change Your Password") assert_field("Current Password") assert_field("New Password") assert_field("Confirm New Password") assert_content("14 characters minimum") - # Admins cannot set new admin passwords - visit "/admin/#/admins/new" - assert_content("Add Admin") - refute_content("Password") - # Admins cannot edit other admin passwords visit "/admin/#/admins/#{admin2.id}/edit" assert_content("Edit Admin") + assert_field("Email", :with => admin2.username) + refute_content("Password") + + # Admins cannot set new admin passwords + visit "/admin/#/admins/new" + assert_content("Add Admin") refute_content("Password") end From 0549f9602d0ea6d95979cbaa8d2166232803a4c0 Mon Sep 17 00:00:00 2001 From: Nick Muerdter Date: Sun, 29 Jan 2017 11:03:23 -0700 Subject: [PATCH 17/26] More changes to align with setup "super" and "assert_text" changes. Change additions on this local admin branch to align with these changes in master: b163093890af7da9600c6e3bebf1f77f0c8df7c8 07e3dc25f5f5de2be60a5904935b484b13cd8c6f --- .../admin_ui/login/test_external_providers.rb | 4 +-- test/admin_ui/login/test_first_time_setup.rb | 23 +++++++------- test/admin_ui/login/test_forgot_password.rb | 11 ++++--- .../login/test_initial_superuser_seeding.rb | 1 + .../test_local_and_external_providers.rb | 5 +-- test/admin_ui/login/test_local_provider.rb | 31 ++++++++++--------- test/admin_ui/login/test_username_is_email.rb | 23 +++++++------- .../admin_ui/login/test_username_not_email.rb | 23 +++++++------- test/admin_ui/test_validations.rb | 1 + test/apis/v1/admins/test_passwords.rb | 1 + .../api_umbrella_test_helpers/admin_auth.rb | 26 ++++++++-------- 11 files changed, 79 insertions(+), 70 deletions(-) diff --git a/test/admin_ui/login/test_external_providers.rb b/test/admin_ui/login/test_external_providers.rb index 1a3a9ea21..18f8a9071 100644 --- a/test/admin_ui/login/test_external_providers.rb +++ b/test/admin_ui/login/test_external_providers.rb @@ -42,7 +42,7 @@ def test_forbids_first_time_admin_creation def test_shows_external_login_links_in_order_and_no_local_fields visit "/admin/login" - assert_content("Admin Sign In") + assert_text("Admin Sign In") # No local login fields refute_field("Email") @@ -52,7 +52,7 @@ def test_shows_external_login_links_in_order_and_no_local_fields refute_button("Sign in") # External login links - assert_content("Sign in with") + assert_text("Sign in with") # Order matches enabled array order. buttons = page.all(".external-login .btn").map { |btn| btn.text } diff --git a/test/admin_ui/login/test_first_time_setup.rb b/test/admin_ui/login/test_first_time_setup.rb index 037c8c29e..5a4936cd9 100644 --- a/test/admin_ui/login/test_first_time_setup.rb +++ b/test/admin_ui/login/test_first_time_setup.rb @@ -6,6 +6,7 @@ class Test::AdminUi::Login::TestFirstTimeSetup < Minitest::Capybara::Test include ApiUmbrellaTestHelpers::AdminAuth def setup + super setup_server Admin.delete_all end @@ -14,9 +15,9 @@ def test_redirects_to_signup_on_first_login assert_equal(0, Admin.count) visit "/admin/" - assert_content("Welcome!") - assert_content("It looks like you're setting up API Umbrella for the first time. Create your first admin account to get started.") - assert_content("14 characters minimum") + assert_text("Welcome!") + assert_text("It looks like you're setting up API Umbrella for the first time. Create your first admin account to get started.") + assert_text("14 characters minimum") assert_equal("/admins/signup", page.current_path) fill_in "Email", :with => "new@example.com" @@ -38,8 +39,8 @@ def test_redirects_to_login_if_admin_exists assert_equal(1, Admin.count) visit "/admin/" - assert_content("Admin Sign In") - refute_content("An initial admin account already exists.") + assert_text("Admin Sign In") + refute_text("An initial admin account already exists.") assert_equal("/admin/login", page.current_path) end @@ -48,8 +49,8 @@ def test_redirects_away_from_signup_if_admin_exists assert_equal(1, Admin.count) visit "/admins/signup" - assert_content("Admin Sign In") - assert_content("An initial admin account already exists.") + assert_text("Admin Sign In") + assert_text("An initial admin account already exists.") assert_equal("/admin/login", page.current_path) end @@ -57,8 +58,8 @@ def test_redirects_away_from_submit_if_admin_exists assert_equal(0, Admin.count) visit "/admins/signup" - assert_content("Welcome!") - assert_content("It looks like you're setting up API Umbrella for the first time. Create your first admin account to get started.") + assert_text("Welcome!") + assert_text("It looks like you're setting up API Umbrella for the first time. Create your first admin account to get started.") fill_in "Email", :with => "new@example.com" fill_in "Password", :with => "password123456" @@ -71,8 +72,8 @@ def test_redirects_away_from_submit_if_admin_exists click_button "Sign up" - assert_content("Admin Sign In") - assert_content("An initial admin account already exists.") + assert_text("Admin Sign In") + assert_text("An initial admin account already exists.") assert_equal("/admin/login", page.current_path) assert_equal(1, Admin.count) diff --git a/test/admin_ui/login/test_forgot_password.rb b/test/admin_ui/login/test_forgot_password.rb index d854def8c..b49463a23 100644 --- a/test/admin_ui/login/test_forgot_password.rb +++ b/test/admin_ui/login/test_forgot_password.rb @@ -7,6 +7,7 @@ class Test::AdminUi::Login::TestForgotPassword < Minitest::Capybara::Test include ApiUmbrellaTestHelpers::DelayedJob def setup + super setup_server Admin.delete_all response = Typhoeus.delete("http://127.0.0.1:#{$config["mailhog"]["api_port"]}/api/v1/messages") @@ -18,7 +19,7 @@ def test_non_existent_email fill_in "Email", :with => "foobar@example.com" click_button "Send me reset password instructions" - assert_content("If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes.") + assert_text("If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes.") assert_equal(0, delayed_job_sent_messages.length) end @@ -35,7 +36,7 @@ def test_reset_process # Reset password fill_in "Email", :with => "admin@example.com" click_button "Send me reset password instructions" - assert_content("If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes.") + assert_text("If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes.") # Check for reset token on database record. admin.reload @@ -63,13 +64,13 @@ def test_reset_process visit reset_url assert_text("Change Your Password") - assert_content("14 characters minimum") + assert_text("14 characters minimum") # Too short password fill_in "New Password", :with => "short" fill_in "Confirm New Password", :with => "short" click_button "Change My Password" - assert_content("is too short (minimum is 14 characters)") + assert_text("is too short (minimum is 14 characters)") admin.reload assert_equal(original_encrypted_password, admin.encrypted_password) @@ -77,7 +78,7 @@ def test_reset_process fill_in "New Password", :with => "mismatch123456" fill_in "Confirm New Password", :with => "mismatcH123456" click_button "Change My Password" - assert_content("doesn't match Password") + assert_text("doesn't match Password") admin.reload assert_equal(original_encrypted_password, admin.encrypted_password) diff --git a/test/admin_ui/login/test_initial_superuser_seeding.rb b/test/admin_ui/login/test_initial_superuser_seeding.rb index cd3d203e1..40ce17e5a 100644 --- a/test/admin_ui/login/test_initial_superuser_seeding.rb +++ b/test/admin_ui/login/test_initial_superuser_seeding.rb @@ -5,6 +5,7 @@ class Test::AdminUi::Login::TestInitialSuperuserSeeding < Minitest::Test include Minitest::Hooks def setup + super setup_server once_per_class_setup do Admin.delete_all diff --git a/test/admin_ui/login/test_local_and_external_providers.rb b/test/admin_ui/login/test_local_and_external_providers.rb index 60bb48521..4d3363a98 100644 --- a/test/admin_ui/login/test_local_and_external_providers.rb +++ b/test/admin_ui/login/test_local_and_external_providers.rb @@ -7,6 +7,7 @@ class Test::AdminUi::Login::TestLocalAndExternalProviders < Minitest::Capybara:: include Minitest::Hooks def setup + super setup_server Admin.delete_all once_per_class_setup do @@ -39,7 +40,7 @@ def test_shows_local_login_fields_and_external_login_links FactoryGirl.create(:admin) visit "/admin/login" - assert_content("Admin Sign In") + assert_text("Admin Sign In") # Local login fields assert_field("Email") @@ -49,7 +50,7 @@ def test_shows_local_login_fields_and_external_login_links assert_button("Sign in") # External login links - assert_content("Sign in with") + assert_text("Sign in with") buttons = page.all(".external-login .btn").map { |btn| btn.text } assert_equal(["Sign in with Google"], buttons) diff --git a/test/admin_ui/login/test_local_provider.rb b/test/admin_ui/login/test_local_provider.rb index ba3065dc6..0858991cf 100644 --- a/test/admin_ui/login/test_local_provider.rb +++ b/test/admin_ui/login/test_local_provider.rb @@ -7,6 +7,7 @@ class Test::AdminUi::Login::TestLocalProvider < Minitest::Capybara::Test include ApiUmbrellaTestHelpers::Setup def setup + super setup_server Admin.delete_all @admin = FactoryGirl.create(:admin) @@ -21,7 +22,7 @@ def test_allows_first_time_admin_creation def test_shows_local_login_fields_no_external_login_links visit "/admin/login" - assert_content("Admin Sign In") + assert_text("Admin Sign In") # Local login fields assert_field("Email") @@ -31,7 +32,7 @@ def test_shows_local_login_fields_no_external_login_links assert_button("Sign in") # No external login links - refute_content("Sign in with") + refute_text("Sign in with") end def test_password_fields_only_for_my_account @@ -51,7 +52,7 @@ def test_login_invalid_password fill_in "admin_username", :with => @admin.username fill_in "admin_password", :with => "password1234567" click_button "sign_in" - assert_content("Invalid Email or password") + assert_text("Invalid Email or password") end def test_login_empty_password @@ -59,7 +60,7 @@ def test_login_empty_password fill_in "admin_username", :with => @admin.username fill_in "admin_password", :with => "" click_button "sign_in" - assert_content("Invalid Email or password") + assert_text("Invalid Email or password") end def test_login_empty_password_for_admin_without_password @@ -70,7 +71,7 @@ def test_login_empty_password_for_admin_without_password fill_in "admin_username", :with => admin.username fill_in "admin_password", :with => "" click_button "sign_in" - assert_content("Invalid Email or password") + assert_text("Invalid Email or password") end def test_login_requires_csrf @@ -102,14 +103,14 @@ def test_login_redirects visit "/admin/" # Ensure we get the loading spinner until authentication takes place. - assert_content("Loading...") + assert_text("Loading...") # Navigation should not be visible while loading. refute_selector("nav") - refute_content("Analytics") + refute_text("Analytics") # Ensure that we eventually get redirected to the login page. - assert_content("Admin Sign In") + assert_text("Admin Sign In") end end @@ -118,7 +119,7 @@ def test_login_redirects # up. def test_login_assets visit "/admin/login" - assert_content("Admin Sign In") + assert_text("Admin Sign In") # Find the stylesheet on the Rails login page, which should have a # cache-busted URL (note that the href on the page appears to be relative, @@ -140,7 +141,7 @@ def test_update_my_account_without_changing_password fill_in "Notes", :with => "Foo" click_button "Save" - assert_content("Successfully saved the admin") + assert_text("Successfully saved the admin") @admin.reload assert_equal("Foo", @admin.notes) @@ -158,7 +159,7 @@ def test_update_my_account_with_password fill_in "New Password", :with => "short" fill_in "Confirm New Password", :with => "short" click_button "Save" - assert_content("Password: is too short (minimum is 14 characters)") + assert_text("Password: is too short (minimum is 14 characters)") @admin.reload assert_equal(original_encrypted_password, @admin.encrypted_password) assert_nil(@admin.notes) @@ -167,7 +168,7 @@ def test_update_my_account_with_password fill_in "New Password", :with => "mismatch123456" fill_in "Confirm New Password", :with => "mismatcH123456" click_button "Save" - assert_content("Password Confirmation: doesn't match Password") + assert_text("Password Confirmation: doesn't match Password") @admin.reload assert_equal(original_encrypted_password, @admin.encrypted_password) assert_nil(@admin.notes) @@ -177,7 +178,7 @@ def test_update_my_account_with_password fill_in "New Password", :with => "password234567" fill_in "Confirm New Password", :with => "password234567" click_button "Save" - assert_content("Current Password: can't be blank") + assert_text("Current Password: can't be blank") @admin.reload assert_equal(original_encrypted_password, @admin.encrypted_password) assert_nil(@admin.notes) @@ -187,7 +188,7 @@ def test_update_my_account_with_password fill_in "New Password", :with => "password234567" fill_in "Confirm New Password", :with => "password234567" click_button "Save" - assert_content("Current Password: is invalid") + assert_text("Current Password: is invalid") @admin.reload assert_equal(original_encrypted_password, @admin.encrypted_password) assert_nil(@admin.notes) @@ -197,7 +198,7 @@ def test_update_my_account_with_password fill_in "New Password", :with => "password234567" fill_in "Confirm New Password", :with => "password234567" click_button "Save" - assert_content("Successfully saved the admin") + assert_text("Successfully saved the admin") @admin.reload assert(@admin.encrypted_password) refute_equal(original_encrypted_password, @admin.encrypted_password) diff --git a/test/admin_ui/login/test_username_is_email.rb b/test/admin_ui/login/test_username_is_email.rb index 3915bd1c6..5bc928164 100644 --- a/test/admin_ui/login/test_username_is_email.rb +++ b/test/admin_ui/login/test_username_is_email.rb @@ -6,6 +6,7 @@ class Test::AdminUi::Login::TestUsernameIsEmail < Minitest::Capybara::Test include ApiUmbrellaTestHelpers::AdminAuth def setup + super setup_server Admin.delete_all @admin = FactoryGirl.create(:admin) @@ -13,25 +14,25 @@ def setup def test_email_label_on_login visit "/admin/login" - assert_content("Admin Sign In") + assert_text("Admin Sign In") - assert_content("Email") - refute_content("Username") + assert_text("Email") + refute_text("Username") end def test_email_label_on_listing admin_login visit "/admin/#/admins" - assert_content("Admins") + assert_text("Admins") - assert_content("Email") - refute_content("Username") + assert_text("Email") + refute_text("Username") end def test_email_label_on_form admin_login visit "/admin/#/admins/new" - assert_content("Add Admin") + assert_text("Add Admin") assert_field("Email", :count => 1) refute_field("Username") @@ -42,11 +43,11 @@ def test_keeps_username_and_email_in_sync # Create admin visit "/admin/#/admins/new" - assert_content("Add Admin") + assert_text("Add Admin") fill_in "Email", :with => "#{unique_test_id.upcase}@example.com" check "Superuser" click_button "Save" - assert_content("Successfully saved the admin") + assert_text("Successfully saved the admin") page.execute_script("PNotify.removeAll()") # Find admin record, and ensure username and email are different. @@ -57,11 +58,11 @@ def test_keeps_username_and_email_in_sync # Edit admin visit "/admin/#/admins/#{admin.id}/edit" - assert_content("Edit Admin") + assert_text("Edit Admin") assert_field("Email", :with => "#{unique_test_id.downcase}@example.com") fill_in "Email", :with => "#{unique_test_id.upcase}-update@example.com" click_button "Save" - assert_content("Successfully saved the admin") + assert_text("Successfully saved the admin") page.execute_script("PNotify.removeAll()") # Ensure edits still keep things different. diff --git a/test/admin_ui/login/test_username_not_email.rb b/test/admin_ui/login/test_username_not_email.rb index 83ee863b7..06717463f 100644 --- a/test/admin_ui/login/test_username_not_email.rb +++ b/test/admin_ui/login/test_username_not_email.rb @@ -7,6 +7,7 @@ class Test::AdminUi::Login::TestUsernameNotEmail < Minitest::Capybara::Test include Minitest::Hooks def setup + super setup_server once_per_class_setup do Admin.delete_all @@ -29,25 +30,25 @@ def after_all def test_username_label_on_login visit "/admin/login" - assert_content("Admin Sign In") + assert_text("Admin Sign In") - assert_content("Username") - refute_content("Email") + assert_text("Username") + refute_text("Email") end def test_username_label_on_listing admin_login visit "/admin/#/admins" - assert_content("Admins") + assert_text("Admins") - assert_content("Username") - refute_content("Email") + assert_text("Username") + refute_text("Email") end def test_username_and_email_label_on_form admin_login visit "/admin/#/admins/new" - assert_content("Add Admin") + assert_text("Add Admin") assert_field("Username", :count => 1) assert_field("Email", :count => 1) @@ -58,12 +59,12 @@ def test_allows_different_username_and_email # Create admin visit "/admin/#/admins/new" - assert_content("Add Admin") + assert_text("Add Admin") fill_in "Username", :with => unique_test_id.upcase fill_in "Email", :with => "#{unique_test_id.upcase}@example.com" check "Superuser" click_button "Save" - assert_content("Successfully saved the admin") + assert_text("Successfully saved the admin") page.execute_script("PNotify.removeAll()") # Find admin record, and ensure username and email are different. @@ -74,13 +75,13 @@ def test_allows_different_username_and_email # Edit admin visit "/admin/#/admins/#{admin.id}/edit" - assert_content("Edit Admin") + assert_text("Edit Admin") assert_field("Username", :with => unique_test_id.downcase) assert_field("Email", :with => "#{unique_test_id.downcase}@example.com") fill_in "Username", :with => "#{unique_test_id.upcase}-update" fill_in "Email", :with => "#{unique_test_id.upcase}-different@example.com" click_button "Save" - assert_content("Successfully saved the admin") + assert_text("Successfully saved the admin") page.execute_script("PNotify.removeAll()") # Ensure edits still keep things different. diff --git a/test/admin_ui/test_validations.rb b/test/admin_ui/test_validations.rb index 0455749ca..10f1d6f37 100644 --- a/test/admin_ui/test_validations.rb +++ b/test/admin_ui/test_validations.rb @@ -6,6 +6,7 @@ class Test::AdminUi::TestValidations < Minitest::Capybara::Test include ApiUmbrellaTestHelpers::Setup def setup + super setup_server Api.delete_all diff --git a/test/apis/v1/admins/test_passwords.rb b/test/apis/v1/admins/test_passwords.rb index 380062df6..2f7daffe9 100644 --- a/test/apis/v1/admins/test_passwords.rb +++ b/test/apis/v1/admins/test_passwords.rb @@ -6,6 +6,7 @@ class Test::Apis::V1::Admins::TestPasswords < Minitest::Test parallelize_me! def setup + super setup_server end diff --git a/test/support/api_umbrella_test_helpers/admin_auth.rb b/test/support/api_umbrella_test_helpers/admin_auth.rb index 47ae936e3..640893ae1 100644 --- a/test/support/api_umbrella_test_helpers/admin_auth.rb +++ b/test/support/api_umbrella_test_helpers/admin_auth.rb @@ -83,20 +83,20 @@ def assert_no_password_fields_on_admin_forms # Admin cannot edit their own password visit "/admin/#/admins/#{admin1.id}/edit" - assert_content("Edit Admin") + assert_text("Edit Admin") assert_field("Email", :with => admin1.username) - refute_content("Password") + refute_text("Password") # Admins cannot edit other admin passwords visit "/admin/#/admins/#{admin2.id}/edit" - assert_content("Edit Admin") + assert_text("Edit Admin") assert_field("Email", :with => admin2.username) - refute_content("Password") + refute_text("Password") # Admins cannot set new admin passwords visit "/admin/#/admins/new" - assert_content("Add Admin") - refute_content("Password") + assert_text("Add Admin") + refute_text("Password") end def assert_password_fields_on_my_account_admin_form_only @@ -106,24 +106,24 @@ def assert_password_fields_on_my_account_admin_form_only # Admin can edit their own password visit "/admin/#/admins/#{admin1.id}/edit" - assert_content("Edit Admin") + assert_text("Edit Admin") assert_field("Email", :with => admin1.username) - assert_content("Change Your Password") + assert_text("Change Your Password") assert_field("Current Password") assert_field("New Password") assert_field("Confirm New Password") - assert_content("14 characters minimum") + assert_text("14 characters minimum") # Admins cannot edit other admin passwords visit "/admin/#/admins/#{admin2.id}/edit" - assert_content("Edit Admin") + assert_text("Edit Admin") assert_field("Email", :with => admin2.username) - refute_content("Password") + refute_text("Password") # Admins cannot set new admin passwords visit "/admin/#/admins/new" - assert_content("Add Admin") - refute_content("Password") + assert_text("Add Admin") + refute_text("Password") end def make_first_time_admin_creation_requests From 91f363b4075c4d605071b8214805b09eb1031955 Mon Sep 17 00:00:00 2001 From: Nick Muerdter Date: Sun, 29 Jan 2017 14:50:49 -0700 Subject: [PATCH 18/26] Update auth API tests, and move more outputs to authenticated-only. --- .../controllers/admin/sessions_controller.rb | 31 +++++++++++-------- test/apis/admin/test_auth.rb | 7 ++--- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/api-umbrella/web-app/app/controllers/admin/sessions_controller.rb b/src/api-umbrella/web-app/app/controllers/admin/sessions_controller.rb index a40159ce2..768096513 100644 --- a/src/api-umbrella/web-app/app/controllers/admin/sessions_controller.rb +++ b/src/api-umbrella/web-app/app/controllers/admin/sessions_controller.rb @@ -6,22 +6,27 @@ class Admin::SessionsController < Devise::SessionsController def auth response = { "authenticated" => !current_admin.nil?, - "enable_beta_analytics" => (ApiUmbrellaConfig[:analytics][:adapter] == "kylin" || (ApiUmbrellaConfig[:analytics][:outputs] && ApiUmbrellaConfig[:analytics][:outputs].include?("kylin"))), - "username_is_email" => ApiUmbrellaConfig[:web][:admin][:username_is_email], - "local_auth_enabled" => ApiUmbrellaConfig[:web][:admin][:auth_strategies][:_local_enabled?], - "password_length_min" => ApiUmbrellaConfig[:web][:admin][:password_length_min], } if current_admin - response["api_umbrella_version"] = API_UMBRELLA_VERSION - response["admin"] = current_admin.as_json.slice( - "email", - "id", - "superuser", - "username", - ) - response["api_key"] = ApiUser.where(:email => "web.admin.ajax@internal.apiumbrella").order_by(:created_at.asc).first.api_key - response["csrf_token"] = form_authenticity_token if(protect_against_forgery?) + response.merge!({ + "enable_beta_analytics" => (ApiUmbrellaConfig[:analytics][:adapter] == "kylin" || (ApiUmbrellaConfig[:analytics][:outputs] && ApiUmbrellaConfig[:analytics][:outputs].include?("kylin"))), + "username_is_email" => ApiUmbrellaConfig[:web][:admin][:username_is_email], + "local_auth_enabled" => ApiUmbrellaConfig[:web][:admin][:auth_strategies][:_local_enabled?], + "password_length_min" => ApiUmbrellaConfig[:web][:admin][:password_length_min], + "api_umbrella_version" => API_UMBRELLA_VERSION, + "admin" => current_admin.as_json.slice( + "email", + "id", + "superuser", + "username", + ), + "api_key" => ApiUser.where(:email => "web.admin.ajax@internal.apiumbrella").order_by(:created_at.asc).first.api_key, + }) + + if(protect_against_forgery?) + response["csrf_token"] = form_authenticity_token + end end respond_to do|format| diff --git a/test/apis/admin/test_auth.rb b/test/apis/admin/test_auth.rb index d698b40d8..ec61fbd5a 100644 --- a/test/apis/admin/test_auth.rb +++ b/test/apis/admin/test_auth.rb @@ -17,12 +17,8 @@ def test_unauthenticated assert_equal([ "authenticated", - "enable_beta_analytics", ].sort, data.keys.sort) - assert_includes([TrueClass, FalseClass], data["authenticated"].class) - assert_includes([TrueClass, FalseClass], data["enable_beta_analytics"].class) - assert_equal(false, data["authenticated"]) end @@ -39,6 +35,9 @@ def test_authenticated "authenticated", "csrf_token", "enable_beta_analytics", + "local_auth_enabled", + "password_length_min", + "username_is_email", ].sort, data.keys.sort) assert_kind_of(Hash, data["admin"]) From 9848d9aef809ea5db962e545de7f39615ec531ff Mon Sep 17 00:00:00 2001 From: Nick Muerdter Date: Sun, 29 Jan 2017 16:45:27 -0700 Subject: [PATCH 19/26] Fix client-side i18n fallbacks. --- src/api-umbrella/web-app/config/routes.rb | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/api-umbrella/web-app/config/routes.rb b/src/api-umbrella/web-app/config/routes.rb index 6775915cd..1b09b3b4e 100644 --- a/src/api-umbrella/web-app/config/routes.rb +++ b/src/api-umbrella/web-app/config/routes.rb @@ -109,7 +109,7 @@ # detection, and some shared validations. get "/admin/server_side_loader.js", :to => proc { |env| # Detect the user's language based on their Accept-Language HTTP header. - locale = env["http_accept_language.parser"].language_region_compatible_from(I18n.available_locales) || I18n.default_locale + locale = (env["http_accept_language.parser"].language_region_compatible_from(I18n.available_locales) || I18n.default_locale).to_s # Cache the generated javascript on a per-locale basis (since the response # will differ depending on the user's locale). @@ -121,11 +121,17 @@ end unless(script) + # Fetch the locale data just for the user's language, as well as the + # default language (if it's different) for fallback support. + locale_data = {} + locale_data[locale] = I18n::JS.translations[locale.to_sym] + locale_data[I18n.default_locale.to_s] ||= I18n::JS.translations[I18n.default_locale.to_sym] + script = <<~eos I18n = window.I18n || {}; I18n.defaultLocale = #{I18n.default_locale.to_json}; I18n.locale = #{locale.to_json}; - I18n.translations = #{{ locale => I18n::JS.translations[locale.to_sym] }.to_json}; + I18n.translations = #{locale_data.to_json}; I18n.fallbacks = true; var CommonValidations = { host_format: new RegExp(#{CommonValidations.to_js(CommonValidations::HOST_FORMAT).to_json}), From d5951d3b0dab65aeb227f9b0962fae98177f03ae Mon Sep 17 00:00:00 2001 From: Nick Muerdter Date: Sun, 29 Jan 2017 17:32:38 -0700 Subject: [PATCH 20/26] Fix duplicate email validation errors. --- src/api-umbrella/web-app/app/models/admin.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/api-umbrella/web-app/app/models/admin.rb b/src/api-umbrella/web-app/app/models/admin.rb index 3ee67456a..338afae73 100644 --- a/src/api-umbrella/web-app/app/models/admin.rb +++ b/src/api-umbrella/web-app/app/models/admin.rb @@ -78,7 +78,8 @@ class Admin :if => :email_required? validates :email, :format => Devise.email_regexp, - :allow_blank => true + :allow_blank => true, + :if => :email_required? validates :password, :presence => true, :confirmation => true, From 824d62af12c67b602e0247b57241fb21e17c4230 Mon Sep 17 00:00:00 2001 From: Nick Muerdter Date: Sun, 29 Jan 2017 20:52:15 -0700 Subject: [PATCH 21/26] Implement email invites for admins. Cleanup "trackable" admin login. - Allow email invitations to be sent for new admin accounts created. - The email invite process allows new admins to set their own password when using the local authentication mechanism. - Remove devise_invitable, since our concept of invitations isn't quite the same as that gem's (I was thinking the gem could be used for this when I added it, but our needs are actually simpler). - Cleanup some things around Devise's "trackable" plugin for admins. We were previously referring to the "last_*" attributes (eg, last_sign_in_at), but it turns out this isn't really correct for what we want. The last known login session is actually under the "current_*" attributes instead (last_* is for the previous). So shift most of our displays for "last login" to use the "current_*" attributes. - Better align our last provider tracking with these updates. We weren't previously keeping track of both the current and last, so now we keep track of both. Also implement it in such a way that we can track the provider for the "local" admin accounts. --- .../app/components/admins/index-table.js | 2 +- src/api-umbrella/admin-ui/app/models/admin.js | 4 + .../admin-ui/app/routes/admins/new.js | 2 +- .../components/admin-groups/record-form.hbs | 2 +- .../components/admins/record-form.hbs | 9 +- src/api-umbrella/web-app/Gemfile | 1 - src/api-umbrella/web-app/Gemfile.lock | 4 - .../admins/omniauth_callbacks_controller.rb | 8 +- .../controllers/api/v1/admins_controller.rb | 20 +++ .../web-app/app/mailers/admin_mailer.rb | 15 +++ src/api-umbrella/web-app/app/models/admin.rb | 50 +++++-- .../app/views/admin_mailer/invite.html.erb | 11 ++ .../web-app/app/views/api/v1/admins/show.rabl | 3 + .../web-app/config/initializers/devise.rb | 48 ------- .../config/locales/devise_invitable.en.yml | 31 ----- test/admin_ui/login/test_forgot_password.rb | 2 +- test/admin_ui/login/test_invite.rb | 123 ++++++++++++++++++ test/admin_ui/login/test_tracking.rb | 67 ++++++++++ test/admin_ui/login/test_username_is_email.rb | 4 +- test/support/models/admin.rb | 1 + 20 files changed, 300 insertions(+), 107 deletions(-) create mode 100644 src/api-umbrella/web-app/app/mailers/admin_mailer.rb create mode 100644 src/api-umbrella/web-app/app/views/admin_mailer/invite.html.erb delete mode 100644 src/api-umbrella/web-app/config/locales/devise_invitable.en.yml create mode 100644 test/admin_ui/login/test_invite.rb create mode 100644 test/admin_ui/login/test_tracking.rb diff --git a/src/api-umbrella/admin-ui/app/components/admins/index-table.js b/src/api-umbrella/admin-ui/app/components/admins/index-table.js index 4468dd91b..83793ac98 100644 --- a/src/api-umbrella/admin-ui/app/components/admins/index-table.js +++ b/src/api-umbrella/admin-ui/app/components/admins/index-table.js @@ -34,7 +34,7 @@ export default Ember.Component.extend({ render: DataTablesHelpers.renderListEscaped, }, { - data: 'last_sign_in_at', + data: 'current_sign_in_at', type: 'date', name: 'Last Signed In', title: 'Last Signed In', diff --git a/src/api-umbrella/admin-ui/app/models/admin.js b/src/api-umbrella/admin-ui/app/models/admin.js index 3ec221ca2..718d1c308 100644 --- a/src/api-umbrella/admin-ui/app/models/admin.js +++ b/src/api-umbrella/admin-ui/app/models/admin.js @@ -11,13 +11,17 @@ export default DS.Model.extend(Validations, { passwordConfirmation: DS.attr(), currentPassword: DS.attr(), email: DS.attr(), + sendInviteEmail: DS.attr('boolean'), name: DS.attr(), notes: DS.attr(), superuser: DS.attr(), groupIds: DS.attr({ defaultValue() { return [] } }), signInCount: DS.attr(), + currentSignInAt: DS.attr(), lastSignInAt: DS.attr(), + currentSignInIp: DS.attr(), lastSignInIp: DS.attr(), + currentSignInProvider: DS.attr(), lastSignInProvider: DS.attr(), authenticationToken: DS.attr(), createdAt: DS.attr(), diff --git a/src/api-umbrella/admin-ui/app/routes/admins/new.js b/src/api-umbrella/admin-ui/app/routes/admins/new.js index 0473f0b93..7436ced94 100644 --- a/src/api-umbrella/admin-ui/app/routes/admins/new.js +++ b/src/api-umbrella/admin-ui/app/routes/admins/new.js @@ -3,6 +3,6 @@ import Form from './form'; export default Form.extend({ model() { this.clearStoreCache(); - return this.fetchModels(this.get('store').createRecord('admin')); + return this.fetchModels(this.get('store').createRecord('admin', { sendInviteEmail: true })); }, }); diff --git a/src/api-umbrella/admin-ui/app/templates/components/admin-groups/record-form.hbs b/src/api-umbrella/admin-ui/app/templates/components/admin-groups/record-form.hbs index 84107b1e4..f49739575 100644 --- a/src/api-umbrella/admin-ui/app/templates/components/admin-groups/record-form.hbs +++ b/src/api-umbrella/admin-ui/app/templates/components/admin-groups/record-form.hbs @@ -12,7 +12,7 @@ {{#if model.admins}}
    {{#each model.admins as |admin|}} -
  • {{#link-to "admins.edit" admin.id}}{{admin.username}}{{/link-to}} (Last Login: {{#if admin.last_sign_in_at}}{{format-date admin.last_sign_in_at}}{{else}}Never{{/if}})
  • +
  • {{#link-to "admins.edit" admin.id}}{{admin.username}}{{/link-to}} (Last Login: {{#if admin.current_sign_in_at}}{{format-date admin.current_sign_in_at}}{{else}}Never{{/if}})
  • {{/each}}
{{else}} diff --git a/src/api-umbrella/admin-ui/app/templates/components/admins/record-form.hbs b/src/api-umbrella/admin-ui/app/templates/components/admins/record-form.hbs index a73ff313f..c2e7da678 100644 --- a/src/api-umbrella/admin-ui/app/templates/components/admins/record-form.hbs +++ b/src/api-umbrella/admin-ui/app/templates/components/admins/record-form.hbs @@ -13,6 +13,13 @@ {{f.static-field "name" label=(t "mongoid.attributes.admin.name")}} {{/if}} {{f.textarea-field "notes" label=(t "mongoid.attributes.admin.notes")}} + {{#if model.id}} + {{#unless model.currentSignInAt}} + {{f.checkbox-field "sendInviteEmail" label="Resend invite email"}} + {{/unless}} + {{else}} + {{f.checkbox-field "sendInviteEmail" label="Send invite email"}} + {{/if}} {{#if model.authenticationToken}} @@ -52,7 +59,7 @@ {{#if model.id}} Created: {{format-date model.createdAt}} by {{model.creator.username}}
Last Updated: {{format-date model.updatedAt}} by {{model.updater.username}}
- Last Login: {{format-date model.lastSignInAt}} from {{model.lastSignInIp}} via {{model.lastSignInProvider}}
+ {{#if model.currentSignInAt}}Last Login: {{format-date model.currentSignInAt}} from {{model.currentSignInIp}} via {{model.currentSignInProvider}}
{{/if}} Logged in: {{model.signInCount}} times
{{/if}}
diff --git a/src/api-umbrella/web-app/Gemfile b/src/api-umbrella/web-app/Gemfile index 4d7e9cbfe..910a6d22b 100644 --- a/src/api-umbrella/web-app/Gemfile +++ b/src/api-umbrella/web-app/Gemfile @@ -56,7 +56,6 @@ gem "elasticsearch", "~> 2.0.1" # OmniAuth-based authentication gem "devise", "~> 4.2.0" gem "devise-i18n", "~> 1.1.1" -gem "devise_invitable", "~> 1.7.0" gem "omniauth", "~> 1.3.2" gem "omniauth-cas", "~> 1.1.0", :git => "https://github.com/GUI/omniauth-cas.git", :branch => "rexml", :require => false gem "omniauth-facebook", "~> 4.0.0", :require => false diff --git a/src/api-umbrella/web-app/Gemfile.lock b/src/api-umbrella/web-app/Gemfile.lock index 2aa43563c..818be9892 100644 --- a/src/api-umbrella/web-app/Gemfile.lock +++ b/src/api-umbrella/web-app/Gemfile.lock @@ -133,9 +133,6 @@ GEM responders warden (~> 1.2.3) devise-i18n (1.1.1) - devise_invitable (1.7.0) - actionmailer (>= 4.0.0) - devise (>= 4.0.0) elasticsearch (2.0.1) elasticsearch-api (= 2.0.1) elasticsearch-transport (= 2.0.1) @@ -339,7 +336,6 @@ DEPENDENCIES delayed_job_mongoid (~> 2.2.0) devise (~> 4.2.0) devise-i18n (~> 1.1.1) - devise_invitable (~> 1.7.0) elasticsearch (~> 2.0.1) font-awesome-rails (~> 4.7.0) http_accept_language (~> 2.1.0) diff --git a/src/api-umbrella/web-app/app/controllers/admin/admins/omniauth_callbacks_controller.rb b/src/api-umbrella/web-app/app/controllers/admin/admins/omniauth_callbacks_controller.rb index cd704c15c..807bad181 100644 --- a/src/api-umbrella/web-app/app/controllers/admin/admins/omniauth_callbacks_controller.rb +++ b/src/api-umbrella/web-app/app/controllers/admin/admins/omniauth_callbacks_controller.rb @@ -65,19 +65,13 @@ def login end if @admin - @admin.last_sign_in_provider = request.env["omniauth.auth"]["provider"] if request.env["omniauth.auth"]["info"].present? - if request.env["omniauth.auth"]["info"]["email"].present? - @admin.email = request.env["omniauth.auth"]["info"]["email"] - end - if request.env["omniauth.auth"]["info"]["name"].present? @admin.name = request.env["omniauth.auth"]["info"]["name"] + @admin.save! end end - @admin.save! - sign_in_and_redirect(:admin, @admin) else flash[:error] = ActionController::Base.helpers.safe_join([ diff --git a/src/api-umbrella/web-app/app/controllers/api/v1/admins_controller.rb b/src/api-umbrella/web-app/app/controllers/api/v1/admins_controller.rb index c69d2a554..06ab3e087 100644 --- a/src/api-umbrella/web-app/app/controllers/api/v1/admins_controller.rb +++ b/src/api-umbrella/web-app/app/controllers/api/v1/admins_controller.rb @@ -42,6 +42,10 @@ def create respond_to do |format| if(@admin.save) + if(send_invite_email) + @admin.send_invite_instructions + end + format.json { render("show", :status => :created, :location => api_v1_admin_url(@admin)) } else format.json { render(:json => errors_response(@admin), :status => :unprocessable_entity) } @@ -55,6 +59,10 @@ def update respond_to do |format| if(@admin.save) + if(!@admin.current_sign_in_at && send_invite_email) + @admin.send_invite_instructions + end + # If a user is updating themselves, make sure they remain signed in. # This eliminates the current user getting logged out if they change # their password. @@ -104,4 +112,16 @@ def admin_params logger.error("Parameters error: #{e}") ActionController::Parameters.new({}).permit! end + + def send_invite_email + send_invite_email = (params[:options] && params[:options][:send_invite_email].to_s == "true") + + # For the admin tool, it's easier to have this attribute on the user + # model, rather than options. + if(!send_invite_email && params[:admin] && params[:admin][:send_invite_email].to_s == "true") + send_invite_email = true + end + + send_invite_email + end end diff --git a/src/api-umbrella/web-app/app/mailers/admin_mailer.rb b/src/api-umbrella/web-app/app/mailers/admin_mailer.rb new file mode 100644 index 000000000..c6c603fc9 --- /dev/null +++ b/src/api-umbrella/web-app/app/mailers/admin_mailer.rb @@ -0,0 +1,15 @@ +class AdminMailer < ActionMailer::Base + default :from => "noreply@#{ApiUmbrellaConfig[:web][:default_host]}" + + def invite(admin_id, token) + @admin = Admin.find(admin_id) + @token = token + + @site_name = ApiUmbrellaConfig[:site_name] + from = "noreply@#{ApiUmbrellaConfig[:web][:default_host]}" + + mail :subject => "#{@site_name} Admin Access", + :from => from, + :to => @admin.email + end +end diff --git a/src/api-umbrella/web-app/app/models/admin.rb b/src/api-umbrella/web-app/app/models/admin.rb index 338afae73..ad2619838 100644 --- a/src/api-umbrella/web-app/app/models/admin.rb +++ b/src/api-umbrella/web-app/app/models/admin.rb @@ -12,8 +12,7 @@ class Admin :registerable, :rememberable, :trackable, - :lockable, - :invitable + :lockable # Fields field :_id, :type => String, :overwrite => true, :default => lambda { SecureRandom.uuid } @@ -22,6 +21,7 @@ class Admin field :notes, :type => String field :superuser, :type => Boolean field :authentication_token, :type => String + field :current_sign_in_provider, :type => String field :last_sign_in_provider, :type => String ## Database authenticatable @@ -47,13 +47,6 @@ class Admin field :unlock_token, :type => String # Only if unlock strategy is :email or :both field :locked_at, :type => Time - ## Invitable - field :invitation_token, :type => String - field :invitation_created_at, :type => Time - field :invitation_sent_at, :type => Time - field :invitation_accepted_at, :type => Time - field :invitation_limit, :type => Integer - # Virtual fields attr_accessor :current_password_invalid_reason @@ -260,6 +253,31 @@ def assign_with_password(params, *options) self.assign_attributes(params, *options) end + def send_invite_instructions + token = nil + if(ApiUmbrellaConfig[:web][:admin][:auth_strategies][:_local_enabled?]) + token = set_invite_reset_password_token + end + + AdminMailer.invite(self.id, token).deliver_later + end + + def update_tracked_fields(request) + old_current = self.current_sign_in_provider + new_current = "local" + if(request.env["omniauth.auth"] && request.env["omniauth.auth"]["provider"]) + new_current = request.env["omniauth.auth"]["provider"] + end + self.last_sign_in_provider = old_current || new_current + self.current_sign_in_provider = new_current + + super + end + + def last_sign_in_provider + self.read_attribute(:last_sign_in_provider) || self.current_sign_in_provider + end + private def sync_username_and_email @@ -292,4 +310,18 @@ def validate_current_password self.errors.add(:current_password, self.current_password_invalid_reason) end end + + # Like Devise Recoverable's reset_password_sent_at, but set the + # reset_password_sent_at date 2 weeks into the future. This allows for the + # normal reset password valid period to be shorter (6 hours), but we can + # leverage the same reset password process for the initial invite where we + # want the period to be longer. + def set_invite_reset_password_token + raw, enc = Devise.token_generator.generate(self.class, :reset_password_token) + + self.reset_password_token = enc + self.reset_password_sent_at = Time.now.utc + 2.weeks + save(:validate => false) + raw + end end diff --git a/src/api-umbrella/web-app/app/views/admin_mailer/invite.html.erb b/src/api-umbrella/web-app/app/views/admin_mailer/invite.html.erb new file mode 100644 index 000000000..0bfb65722 --- /dev/null +++ b/src/api-umbrella/web-app/app/views/admin_mailer/invite.html.erb @@ -0,0 +1,11 @@ +

Hi <%= @admin.email %>,

+ +<% if(@token) %> +

Welcome to the <%= @site_name %> admin. To get started, set your password with the link below.

+

<%= link_to "Set my password", edit_password_url(@admin, :reset_password_token => @token, :invite => true) %>

+<% else %> +

Welcome to the <%= @site_name %> admin. To get started, sign in with an account associated with your <%= @admin.email %> email address at the link below.

+

<%= link_to "Admin sign in", new_admin_session_url %>

+<% end %> + +

If you didn't request an account, please ignore this email.

diff --git a/src/api-umbrella/web-app/app/views/api/v1/admins/show.rabl b/src/api-umbrella/web-app/app/views/api/v1/admins/show.rabl index 534584c65..a858644d2 100644 --- a/src/api-umbrella/web-app/app/views/api/v1/admins/show.rabl +++ b/src/api-umbrella/web-app/app/views/api/v1/admins/show.rabl @@ -7,8 +7,11 @@ attributes :id, :superuser, :group_ids, :sign_in_count, + :current_sign_in_at, :last_sign_in_at, + :current_sign_in_ip, :last_sign_in_ip, + :current_sign_in_provider, :last_sign_in_provider, :created_at, :updated_at diff --git a/src/api-umbrella/web-app/config/initializers/devise.rb b/src/api-umbrella/web-app/config/initializers/devise.rb index f8cbaed35..4912ca8f5 100644 --- a/src/api-umbrella/web-app/config/initializers/devise.rb +++ b/src/api-umbrella/web-app/config/initializers/devise.rb @@ -115,54 +115,6 @@ # Send a notification email when the user's password is changed config.send_password_change_notification = true - # ==> Configuration for :invitable - # The period the generated invitation token is valid, after - # this period, the invited resource won't be able to accept the invitation. - # When invite_for is 0 (the default), the invitation won't expire. - # config.invite_for = 2.weeks - - # Number of invitations users can send. - # - If invitation_limit is nil, there is no limit for invitations, users can - # send unlimited invitations, invitation_limit column is not used. - # - If invitation_limit is 0, users can't send invitations by default. - # - If invitation_limit n > 0, users can send n invitations. - # You can change invitation_limit column for some users so they can send more - # or less invitations, even with global invitation_limit = 0 - # Default: nil - # config.invitation_limit = 5 - - # The key to be used to check existing users when sending an invitation - # and the regexp used to test it when validate_on_invite is not set. - # config.invite_key = {:email => /\A[^@]+@[^@]+\z/} - # config.invite_key = {:email => /\A[^@]+@[^@]+\z/, :username => nil} - - # Flag that force a record to be valid before being actually invited - # Default: false - # config.validate_on_invite = true - - # Resend invitation if user with invited status is invited again - # Default: true - # config.resend_invitation = false - - # The class name of the inviting model. If this is nil, - # the #invited_by association is declared to be polymorphic. - # Default: nil - # config.invited_by_class_name = 'User' - - # The foreign key to the inviting model (if invited_by_class_name is set) - # Default: :invited_by_id - # config.invited_by_foreign_key = :invited_by_id - - # The column name used for counter_cache column. If this is nil, - # the #invited_by association is declared without counter_cache. - # Default: nil - # config.invited_by_counter_cache = :invitations_count - - # Auto-login after the user accepts the invite. If this is false, - # the user will need to manually log in after accepting the invite. - # Default: true - # config.allow_insecure_sign_in_after_accept = false - # ==> Configuration for :confirmable # A period that the user is allowed to access the website even without # confirming their account. For instance, if set to 2.days, the user will be diff --git a/src/api-umbrella/web-app/config/locales/devise_invitable.en.yml b/src/api-umbrella/web-app/config/locales/devise_invitable.en.yml deleted file mode 100644 index 2d6750dfe..000000000 --- a/src/api-umbrella/web-app/config/locales/devise_invitable.en.yml +++ /dev/null @@ -1,31 +0,0 @@ -en: - devise: - failure: - invited: "You have a pending invitation, accept it to finish creating your account." - invitations: - send_instructions: "An invitation email has been sent to %{email}." - invitation_token_invalid: "The invitation token provided is not valid!" - updated: "Your password was set successfully. You are now signed in." - updated_not_active: "Your password was set successfully." - no_invitations_remaining: "No invitations remaining" - invitation_removed: "Your invitation was removed." - new: - header: "Send invitation" - submit_button: "Send an invitation" - edit: - header: "Set your password" - submit_button: "Set my password" - mailer: - invitation_instructions: - subject: "Invitation instructions" - hello: "Hello %{email}" - someone_invited_you: "Someone has invited you to %{url}, you can accept it through the link below." - accept: "Accept invitation" - accept_until: "This invitation will be due in %{due_date}." - ignore: "If you don't want to accept the invitation, please ignore this email.
\nYour account won't be created until you access the link above and set your password." - time: - formats: - devise: - mailer: - invitation_instructions: - accept_until_format: "%B %d, %Y %I:%M %p" diff --git a/test/admin_ui/login/test_forgot_password.rb b/test/admin_ui/login/test_forgot_password.rb index b49463a23..d8b3cb54b 100644 --- a/test/admin_ui/login/test_forgot_password.rb +++ b/test/admin_ui/login/test_forgot_password.rb @@ -55,7 +55,7 @@ def test_reset_process # Subject assert_equal(["Reset password instructions"], message["Content"]["Headers"]["Subject"]) - # Use description in body + # Password reset URL in body assert_match(%r{http://localhost/admins/password/edit\?reset_password_token=[^"]+}, message["_mime_parts"]["text/html; charset=UTF-8"]["Body"]) assert_match(%r{http://localhost/admins/password/edit\?reset_password_token=[^"]+}, message["_mime_parts"]["text/plain; charset=UTF-8"]["Body"]) diff --git a/test/admin_ui/login/test_invite.rb b/test/admin_ui/login/test_invite.rb new file mode 100644 index 000000000..61aa43599 --- /dev/null +++ b/test/admin_ui/login/test_invite.rb @@ -0,0 +1,123 @@ +require_relative "../../test_helper" + +class Test::AdminUi::Login::TestInvite < Minitest::Capybara::Test + include Capybara::Screenshot::MiniTestPlugin + include ApiUmbrellaTestHelpers::AdminAuth + include ApiUmbrellaTestHelpers::Setup + include ApiUmbrellaTestHelpers::DelayedJob + + def setup + super + setup_server + Admin.delete_all + response = Typhoeus.delete("http://127.0.0.1:#{$config["mailhog"]["api_port"]}/api/v1/messages") + assert_response_code(200, response) + end + + def test_defaults_to_sending_invite_for_new_accounts + admin_login + + # Create admin + visit "/admin/#/admins/new" + assert_text("Add Admin") + fill_in "Email", :with => "#{unique_test_id}@example.com" + assert_checked_field("Send invite email") + check "Superuser" + click_button "Save" + assert_text("Successfully saved the admin") + + # Logout + ::Capybara.reset_session! + page.driver.clear_memory_cache + + # Find admin record + admin = Admin.where(:username => "#{unique_test_id.downcase}@example.com").first + assert(admin) + + # Find sent email + messages = delayed_job_sent_messages + assert_equal(1, messages.length) + message = messages.first + + # To + assert_equal(["#{unique_test_id.downcase}@example.com"], message["Content"]["Headers"]["To"]) + + # Subject + assert_equal(["API Umbrella Admin Access"], message["Content"]["Headers"]["Subject"]) + + # Password reset URL in body + assert_match(%r{http://localhost/admins/password/edit\?invite=true&reset_password_token=[^" ]+}, message["_mime_parts"]["text/html; charset=UTF-8"]["Body"]) + assert_match(%r{http://localhost/admins/password/edit\?invite=true&reset_password_token=[^" ]+}, message["_mime_parts"]["text/plain; charset=UTF-8"]["Body"]) + + # Follow link to reset URL + reset_url = message["_mime_parts"]["text/plain; charset=UTF-8"]["Body"].match(%r{/admins/password/edit\?invite=true&reset_password_token=[^" ]+})[0] + visit reset_url + fill_in "New Password", :with => "password123456" + fill_in "Confirm New Password", :with => "password123456" + click_button "Change My Password" + + # Ensure the user gets logged in. + assert_logged_in(admin) + end + + def test_invites_can_be_skipped_for_new_users + admin_login + + # Create admin + visit "/admin/#/admins/new" + assert_text("Add Admin") + fill_in "Email", :with => "#{unique_test_id}@example.com" + uncheck "Send invite email" + check "Superuser" + click_button "Save" + assert_text("Successfully saved the admin") + + # Find admin record + admin = Admin.where(:username => "#{unique_test_id.downcase}@example.com").first + assert(admin) + + # No email + assert_equal(0, delayed_job_sent_messages.length) + end + + def test_invites_can_be_resent + admin_login + + admin = FactoryGirl.create(:admin) + assert_nil(admin.notes) + + # Ensure edits don't resend invites by default. + visit "/admin/#/admins/#{admin.id}/edit" + assert_text("Edit Admin") + fill_in "Notes", :with => "Foo" + assert_equal(false, find_field("Resend invite email").checked?) + click_button "Save" + assert_text("Successfully saved the admin") + page.execute_script("PNotify.removeAll()") + + admin.reload + assert_equal("Foo", admin.notes) + assert_equal(0, delayed_job_sent_messages.length) + + # Force the invite to be resent. + visit "/admin/#/admins/#{admin.id}/edit" + assert_text("Edit Admin") + fill_in "Notes", :with => "Bar" + assert_equal(false, find_field("Resend invite email").checked?) + check "Resend invite email" + click_button "Save" + assert_text("Successfully saved the admin") + page.execute_script("PNotify.removeAll()") + + admin.reload + assert_equal("Bar", admin.notes) + messages = delayed_job_sent_messages + assert_equal(1, messages.length) + message = messages.first + assert_equal(["API Umbrella Admin Access"], message["Content"]["Headers"]["Subject"]) + + admin.update_attributes(:current_sign_in_at => Time.now.utc) + visit "/admin/#/admins/#{admin.id}/edit" + refute_field("Resend invite email") + end +end diff --git a/test/admin_ui/login/test_tracking.rb b/test/admin_ui/login/test_tracking.rb new file mode 100644 index 000000000..66947c88b --- /dev/null +++ b/test/admin_ui/login/test_tracking.rb @@ -0,0 +1,67 @@ +require_relative "../../test_helper" + +class Test::AdminUi::Login::TestTracking < Minitest::Capybara::Test + include Capybara::Screenshot::MiniTestPlugin + include ApiUmbrellaTestHelpers::AdminAuth + include ApiUmbrellaTestHelpers::Setup + + def setup + super + setup_server + Admin.delete_all + end + + def test_populates_tracking_fields_on_first_login + admin = FactoryGirl.create(:admin) + assert_nil(admin.current_sign_in_at) + assert_nil(admin.last_sign_in_at) + assert_nil(admin.current_sign_in_ip) + assert_nil(admin.last_sign_in_ip) + assert_nil(admin.current_sign_in_provider) + assert_nil(admin.last_sign_in_provider) + assert_equal(0, admin.sign_in_count) + + visit "/admin/login" + fill_in "admin_username", :with => admin.username + fill_in "admin_password", :with => "password123456" + click_button "sign_in" + assert_logged_in(admin) + + admin.reload + assert_kind_of(Time, admin.current_sign_in_at) + assert_kind_of(Time, admin.last_sign_in_at) + assert_kind_of(String, admin.current_sign_in_ip) + assert_kind_of(String, admin.last_sign_in_ip) + assert_equal("local", admin.current_sign_in_provider) + assert_equal("local", admin.last_sign_in_provider) + assert_equal(1, admin.sign_in_count) + end + + def test_shifts_current_values_to_last_values_on_subsequent_logins + admin = FactoryGirl.create(:admin, { + :current_sign_in_at => Time.iso8601("2017-01-01T01:27:00Z"), + :last_sign_in_at => Time.iso8601("2017-01-02T01:27:00Z"), + :current_sign_in_ip => "127.0.0.100", + :last_sign_in_ip => "127.0.0.200", + :current_sign_in_provider => "google_oauth2", + :last_sign_in_provider => "github", + :sign_in_count => 8, + }) + + visit "/admin/login" + fill_in "admin_username", :with => admin.username + fill_in "admin_password", :with => "password123456" + click_button "sign_in" + assert_logged_in(admin) + + admin.reload + assert_kind_of(Time, admin.current_sign_in_at) + refute_equal(Time.iso8601("2017-01-01T01:27:00Z"), admin.current_sign_in_at) + assert_equal(Time.iso8601("2017-01-01T01:27:00Z"), admin.last_sign_in_at) + refute_equal("127.0.0.100", admin.current_sign_in_ip) + assert_equal("127.0.0.100", admin.last_sign_in_ip) + assert_equal("local", admin.current_sign_in_provider) + assert_equal("google_oauth2", admin.last_sign_in_provider) + assert_equal(9, admin.sign_in_count) + end +end diff --git a/test/admin_ui/login/test_username_is_email.rb b/test/admin_ui/login/test_username_is_email.rb index 5bc928164..dd79a795a 100644 --- a/test/admin_ui/login/test_username_is_email.rb +++ b/test/admin_ui/login/test_username_is_email.rb @@ -50,7 +50,7 @@ def test_keeps_username_and_email_in_sync assert_text("Successfully saved the admin") page.execute_script("PNotify.removeAll()") - # Find admin record, and ensure username and email are different. + # Find admin record, and ensure username and email are the same. admin = Admin.where(:username => "#{unique_test_id.downcase}@example.com").first assert(admin) assert_equal("#{unique_test_id.downcase}@example.com", admin.username) @@ -65,7 +65,7 @@ def test_keeps_username_and_email_in_sync assert_text("Successfully saved the admin") page.execute_script("PNotify.removeAll()") - # Ensure edits still keep things different. + # Ensure edits still keep things the same. admin.reload assert_equal("#{unique_test_id.downcase}-update@example.com", admin.username) assert_equal(admin.username, admin.email) diff --git a/test/support/models/admin.rb b/test/support/models/admin.rb index 0a4bae767..db8ade983 100644 --- a/test/support/models/admin.rb +++ b/test/support/models/admin.rb @@ -7,6 +7,7 @@ class Admin field :notes, :type => String field :superuser, :type => Boolean field :authentication_token, :type => String, :default => lambda { SecureRandom.hex(20) } + field :current_sign_in_provider, :type => String field :last_sign_in_provider, :type => String field :email, :type => String field :encrypted_password, :type => String From 69a8c78ebb039dc07ee78d2a064095be71ea69cf Mon Sep 17 00:00:00 2001 From: Nick Muerdter Date: Sat, 4 Feb 2017 14:28:54 -0700 Subject: [PATCH 22/26] Improve LDAP authentication integration. - Use a custom LDAP login form so CSRF token is passed along. - If LDAP is the only login option, put the login on the initial page. - Use the LDAP "title" on the various pages and forms, so things can read "Sign in with Company Name" (instead of the generic "LDAP"). - Make email optional so when the username is not the email address, we can still login and create accounts from LDAP when all we know is the username (since LDAP may not provide email). - Add integration testing for LDAP authentication with simple openldap server. --- CMakeLists.txt | 1 + build/cmake/test/openldap.cmake | 8 ++ build/cmake/versions.cmake | 2 + config/test.yml | 2 + scripts/rake/outdated_packages.rb | 6 + src/api-umbrella/cli/read_config.lua | 1 + .../web-app/app/helpers/application_helper.rb | 20 ++++ src/api-umbrella/web-app/app/models/admin.rb | 14 +-- .../omniauth_custom_forms/developer.html.erb | 4 +- .../omniauth_custom_forms/ldap.html.erb | 5 + .../devise/sessions/_ldap_login_form.html.erb | 16 +++ .../sessions/_local_login_form.html.erb | 20 ++++ .../_omniauth_provider_buttons.html.erb | 14 +++ .../app/views/devise/sessions/new.html.erb | 60 +++------- .../web-app/config/initializers/devise.rb | 4 +- .../web-app/config/initializers/i18n.rb | 16 ++- templates/etc/perp/test-env-openldap/rc.log | 2 + .../perp/test-env-openldap/rc.main.mustache | 33 ++++++ .../etc/test-env/openldap/seed.ldif.mustache | 28 +++++ .../etc/test-env/openldap/slapd.ldif.mustache | 32 ++++++ .../admin_ui/login/test_external_providers.rb | 6 + test/admin_ui/login/test_ldap_provider.rb | 103 ++++++++++++++++++ 22 files changed, 336 insertions(+), 61 deletions(-) create mode 100644 build/cmake/test/openldap.cmake create mode 100644 src/api-umbrella/web-app/app/views/admin/admins/omniauth_custom_forms/ldap.html.erb create mode 100644 src/api-umbrella/web-app/app/views/devise/sessions/_ldap_login_form.html.erb create mode 100644 src/api-umbrella/web-app/app/views/devise/sessions/_local_login_form.html.erb create mode 100644 src/api-umbrella/web-app/app/views/devise/sessions/_omniauth_provider_buttons.html.erb create mode 100755 templates/etc/perp/test-env-openldap/rc.log create mode 100755 templates/etc/perp/test-env-openldap/rc.main.mustache create mode 100644 templates/etc/test-env/openldap/seed.ldif.mustache create mode 100644 templates/etc/test-env/openldap/slapd.ldif.mustache create mode 100644 test/admin_ui/login/test_ldap_provider.rb diff --git a/CMakeLists.txt b/CMakeLists.txt index f94bb707c..85dfa7d94 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -92,6 +92,7 @@ if(ENABLE_TEST_DEPENDENCIES) include(${CMAKE_SOURCE_DIR}/build/cmake/test/lua-deps.cmake) include(${CMAKE_SOURCE_DIR}/build/cmake/test/mailhog.cmake) include(${CMAKE_SOURCE_DIR}/build/cmake/test/mongo-orchestration.cmake) + include(${CMAKE_SOURCE_DIR}/build/cmake/test/openldap.cmake) include(${CMAKE_SOURCE_DIR}/build/cmake/test/phantomjs.cmake) include(${CMAKE_SOURCE_DIR}/build/cmake/test/shellcheck.cmake) include(${CMAKE_SOURCE_DIR}/build/cmake/test/unbound.cmake) diff --git a/build/cmake/test/openldap.cmake b/build/cmake/test/openldap.cmake new file mode 100644 index 000000000..347d906fc --- /dev/null +++ b/build/cmake/test/openldap.cmake @@ -0,0 +1,8 @@ +# OpenLDAP: For testing LDAP admin auth. +ExternalProject_Add( + openldap + URL ftp://ftp.openldap.org/pub/OpenLDAP/openldap-release/openldap-${OPENLDAP_VERSION}.tgz + URL_HASH SHA1=${OPENLDAP_HASH} + CONFIGURE_COMMAND /configure --prefix=${TEST_INSTALL_PREFIX} --disable-backends --enable-mdb + BUILD_COMMAND make depend && make +) diff --git a/build/cmake/versions.cmake b/build/cmake/versions.cmake index c99cd1ae6..768e320cc 100644 --- a/build/cmake/versions.cmake +++ b/build/cmake/versions.cmake @@ -80,6 +80,8 @@ set(NGX_TXID_VERSION f1c197cb9c42e364a87fbb28d5508e486592ca42) set(NGX_TXID_HASH 408ee86eb6e42e27a51514f711c41d6b) set(NODEJS_VERSION 6.9.4) set(NODEJS_HASH d28c331e1af88468e8220477e9b4d48d4ce041855b9c939ea2320de2929e7ce1) +set(OPENLDAP_VERSION 2.4.44) +set(OPENLDAP_HASH 016a738d050a68d388602a74b5e991035cdba149) set(OPENRESTY_VERSION 1.11.2.2) set(OPENRESTY_HASH f4b9aa960e57ca692c4d3da731b7e38b) set(OPENSSL_VERSION 1.0.2k) diff --git a/config/test.yml b/config/test.yml index 1307c1c88..89d29d82e 100644 --- a/config/test.yml +++ b/config/test.yml @@ -102,5 +102,7 @@ mailhog: smtp_port: 13102 api_port: 13103 ui_port: 13103 +openldap: + port: 13104 apiSettings: require_https: optional diff --git a/scripts/rake/outdated_packages.rb b/scripts/rake/outdated_packages.rb index 233077647..c90c9bfc7 100644 --- a/scripts/rake/outdated_packages.rb +++ b/scripts/rake/outdated_packages.rb @@ -138,6 +138,9 @@ class OutdatedPackages :git => "https://github.com/nodejs/node.git", :constraint => "~> 6.9.1", }, + "openldap" => { + :git => "https://github.com/openldap/openldap.git", + }, "openresty" => { :git => "https://github.com/openresty/openresty.git", }, @@ -218,6 +221,9 @@ def tag_to_semver(name, tag) tag.gsub!(/^go/, "") when "json_c" tag.gsub!(/-\d{8}$/, "") + when "openldap" + tag.gsub!(/^rel_eng_/, "") + tag.gsub!(/_/, ".") when "openssl", "ruby" tag.gsub!(/_/, ".") end diff --git a/src/api-umbrella/cli/read_config.lua b/src/api-umbrella/cli/read_config.lua index 1ac9595ce..723215511 100644 --- a/src/api-umbrella/cli/read_config.lua +++ b/src/api-umbrella/cli/read_config.lua @@ -370,6 +370,7 @@ local function set_computed_config() admin = { auth_strategies = { ["_local_enabled?"] = array_includes(config["web"]["admin"]["auth_strategies"]["enabled"], "local"), + ["_only_ldap_enabled?"] = (#config["web"]["admin"]["auth_strategies"]["enabled"] == 1 and config["web"]["admin"]["auth_strategies"]["enabled"][1] == "ldap"), }, }, dir = path.join(src_root_dir, "src/api-umbrella/web-app"), diff --git a/src/api-umbrella/web-app/app/helpers/application_helper.rb b/src/api-umbrella/web-app/app/helpers/application_helper.rb index 19f895b79..272504aed 100644 --- a/src/api-umbrella/web-app/app/helpers/application_helper.rb +++ b/src/api-umbrella/web-app/app/helpers/application_helper.rb @@ -30,4 +30,24 @@ def web_admin_ajax_api_user user end + + def omniauth_external_providers + unless @omniauth_external_providers + @omniauth_external_providers = Admin.omniauth_providers + if(ApiUmbrellaConfig[:web][:admin][:auth_strategies][:_only_ldap_enabled?]) + @omniauth_external_providers.delete(:ldap) + end + end + + @omniauth_external_providers + end + + def display_login_form? + ApiUmbrellaConfig[:web][:admin][:auth_strategies][:_local_enabled?] || ApiUmbrellaConfig[:web][:admin][:auth_strategies][:_only_ldap_enabled?] + end + + def ldap_title + strategy = Devise.omniauth_configs[:ldap].strategy + strategy[:title].presence || t(:ldap, :scope => [:omniauth_providers]) + end end diff --git a/src/api-umbrella/web-app/app/models/admin.rb b/src/api-umbrella/web-app/app/models/admin.rb index ad2619838..60680c931 100644 --- a/src/api-umbrella/web-app/app/models/admin.rb +++ b/src/api-umbrella/web-app/app/models/admin.rb @@ -66,13 +66,9 @@ class Admin :format => Devise.email_regexp, :allow_blank => true, :if => :username_is_email? - validates :email, - :presence => true, - :if => :email_required? validates :email, :format => Devise.email_regexp, - :allow_blank => true, - :if => :email_required? + :allow_nil => true validates :password, :presence => true, :confirmation => true, @@ -223,14 +219,6 @@ def password_required? password.present? || password_confirmation.present? end - # Only require the email field for validation if it won't be synced with the - # username field. This just prevents duplicate validation errors from showing - # up when the fields are synced (since the user doesn't see the separate - # email field, even though we populate it). - def email_required? - !ApiUmbrellaConfig[:web][:admin][:username_is_email] - end - def assign_without_password(params, *options) params.delete(:password) params.delete(:password_confirmation) diff --git a/src/api-umbrella/web-app/app/views/admin/admins/omniauth_custom_forms/developer.html.erb b/src/api-umbrella/web-app/app/views/admin/admins/omniauth_custom_forms/developer.html.erb index 0c066a327..47a5ec0f9 100644 --- a/src/api-umbrella/web-app/app/views/admin/admins/omniauth_custom_forms/developer.html.erb +++ b/src/api-umbrella/web-app/app/views/admin/admins/omniauth_custom_forms/developer.html.erb @@ -9,8 +9,8 @@ <%= form_tag(admin_developer_omniauth_callback_path) do |f| %>
- <%= label_tag :username, t("mongoid.attributes.admin.username"), :class => "control-label" %> - <%= text_field_tag :username, nil, :class => "form-control" %> + <%= label_tag :username, t("mongoid.attributes.admin.username"), :class => "control-label string optional" %> + <%= text_field_tag :username, nil, :class => "form-control string optional" %>
diff --git a/src/api-umbrella/web-app/app/views/admin/admins/omniauth_custom_forms/ldap.html.erb b/src/api-umbrella/web-app/app/views/admin/admins/omniauth_custom_forms/ldap.html.erb new file mode 100644 index 000000000..d0bd43045 --- /dev/null +++ b/src/api-umbrella/web-app/app/views/admin/admins/omniauth_custom_forms/ldap.html.erb @@ -0,0 +1,5 @@ +

Sign in with <%= t(:ldap, :scope => [:omniauth_providers]) %>

+ + diff --git a/src/api-umbrella/web-app/app/views/devise/sessions/_ldap_login_form.html.erb b/src/api-umbrella/web-app/app/views/devise/sessions/_ldap_login_form.html.erb new file mode 100644 index 000000000..abd210448 --- /dev/null +++ b/src/api-umbrella/web-app/app/views/devise/sessions/_ldap_login_form.html.erb @@ -0,0 +1,16 @@ +<%= form_tag(admin_ldap_omniauth_callback_path) do |f| %> +
+
+ <%= label_tag :username, "#{t(:ldap, :scope => [:omniauth_providers])} #{t("mongoid.attributes.admin.username")}", :class => "control-label" %> + <%= text_field_tag :username, nil, :class => "form-control string optional" %> +
+
+ <%= label_tag :password, "#{t(:ldap, :scope => [:omniauth_providers])} #{t("mongoid.attributes.admin.password")}", :class => "control-label" %> + <%= password_field_tag :password, nil, :class => "form-control password optional" %> +
+
+ +
+ <%= submit_tag "Sign in", :class => "btn btn-default" %> +
+<% end %> diff --git a/src/api-umbrella/web-app/app/views/devise/sessions/_local_login_form.html.erb b/src/api-umbrella/web-app/app/views/devise/sessions/_local_login_form.html.erb new file mode 100644 index 000000000..647aaa563 --- /dev/null +++ b/src/api-umbrella/web-app/app/views/devise/sessions/_local_login_form.html.erb @@ -0,0 +1,20 @@ +<%= simple_form_for(resource, :as => resource_name, :url => session_path(resource_name)) do |f| %> +
+ <%= f.input :username, :required => false, :autofocus => true %> + <%= f.input :password, :required => false %> +
+
+ <%= f.input :remember_me, :as => :boolean if devise_mapping.rememberable? %> +
+ <%- if devise_mapping.recoverable? %> +
+ <%= link_to t("devise.shared.links.forgot_your_password"), new_password_path(resource_name) %>
+
+ <% end -%> +
+
+ +
+ <%= f.button :submit, t("devise.sessions.new.sign_in"), :id => "sign_in" %> +
+<% end %> diff --git a/src/api-umbrella/web-app/app/views/devise/sessions/_omniauth_provider_buttons.html.erb b/src/api-umbrella/web-app/app/views/devise/sessions/_omniauth_provider_buttons.html.erb new file mode 100644 index 000000000..62f99ea1e --- /dev/null +++ b/src/api-umbrella/web-app/app/views/devise/sessions/_omniauth_provider_buttons.html.erb @@ -0,0 +1,14 @@ +<%- omniauth_external_providers.each do |provider| %> + +<% end -%> diff --git a/src/api-umbrella/web-app/app/views/devise/sessions/new.html.erb b/src/api-umbrella/web-app/app/views/devise/sessions/new.html.erb index d56098fd6..ed3c32d34 100644 --- a/src/api-umbrella/web-app/app/views/devise/sessions/new.html.erb +++ b/src/api-umbrella/web-app/app/views/devise/sessions/new.html.erb @@ -1,56 +1,28 @@

<%= t(".admin_sign_in") %>