diff --git a/Rakefile b/Rakefile index 6a8c49c..d449c5b 100644 --- a/Rakefile +++ b/Rakefile @@ -1,6 +1,7 @@ require 'rubygems' require 'bundler/gem_tasks' require 'rspec/core/rake_task' +require 'proxes/rake_tasks' RSpec::Core::RakeTask.new(:spec) diff --git a/Vagrantfile b/Vagrantfile index a37c343..fd3cb55 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -2,7 +2,7 @@ # vi: set ft=ruby : Vagrant.configure(2) do |config| - config.vm.box = "ubuntu/trusty64" + config.vm.box = "ubuntu/xenial64" config.vm.network :private_network, ip: '172.16.248.110' @@ -16,7 +16,7 @@ Vagrant.configure(2) do |config| sudo apt-get install -y screen curl git build-essential libssl-dev # Ruby - sudo apt-get install ruby2.0 + sudo apt-get install -y ruby2.3 ruby2.3-dev # if [ ! -f /home/vagrant/.rvm/scripts/rvm ] # then # gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 @@ -27,7 +27,7 @@ Vagrant.configure(2) do |config| # Ruby and it's Gems cd /vagrant # rvm use $(cat .ruby-version) --install - gem install bundler --no-rdoc --no-ri + sudo gem install bundler --no-rdoc --no-ri bundle install # Node diff --git a/config.ru b/config.ru index 38cee2f..29c48e7 100644 --- a/config.ru +++ b/config.ru @@ -2,48 +2,61 @@ libdir = File.expand_path(File.dirname(__FILE__) + '/lib') $LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir) +raise 'Unconfigured' unless ENV['ELASTICSEARCH_URL'] + require 'proxes' require 'proxes/db' -raise 'Unconfigured' unless ENV['ELASTICSEARCH_URL'] - use Rack::Static, urls: ['/assets'], root: 'public' - -use Rack::Session::Pool -# use Rack::Session::Cookie, -# :key => '_ProxES_session', -# #:secure=>!TEST_MODE, # Uncomment if only allowing https:// access -# :secret=>File.read('.session_secret') +use Rack::MethodOverride +use Rack::Session::Cookie, + :key => '_ProxES_session', + #:secure=>!TEST_MODE, # Uncomment if only allowing https:// access + :secret=>File.read('.session_secret') require 'omniauth' require 'omniauth-identity' +require 'proxes/controllers/auth_identity' # OmniAuth.config.test_mode = true - use OmniAuth::Builder do # The identity provider is used by the App. provider :identity, fields: [:username], + callback_path: '/_proxes/auth/identity/callback', model: ProxES::Identity, - on_login: ProxES::Security, - on_registration: ProxES::Security, + on_login: ProxES::AuthIdentity, + on_registration: ProxES::AuthIdentity, locate_conditions: lambda{|req| {username: req['username']} } end - -OmniAuth.config.on_failure = Proc.new { |env| - OmniAuth::FailureEndpoint.new(env).redirect_to_failure -} +OmniAuth.config.on_failure = ProxES::AuthIdentity require 'warden' require 'proxes/strategies/jwt_token' use Warden::Manager do |manager| manager.default_strategies :jwt_token manager.scope_defaults :default, action: '_proxes/unauthenticated' - manager.failure_app = ProxES::Security + manager.failure_app = ProxES::App end - Warden::Manager.serialize_into_session { |user| user.id } Warden::Manager.serialize_from_session { |id| ProxES::User[id] } +# Management App +require 'proxes/controllers' + +map '/_proxes' do + { + '/users' => ProxES::Users, + '/user-roles' => ProxES::UserRoles, + }.each do |route, app| + map route do + run app + end + end + + run ProxES::App +end + + # Proxy all Elasticsearch requests map '/' do # Security @@ -52,8 +65,3 @@ map '/' do # Forward requests to ES run Rack::Proxy.new(backend: ENV['ELASTICSEARCH_URL']) end - -# Management App -map '/_proxes' do - run ProxES::App -end diff --git a/lib/proxes.rb b/lib/proxes.rb index d2cd10e..53b9ab7 100644 --- a/lib/proxes.rb +++ b/lib/proxes.rb @@ -1,5 +1,3 @@ require 'proxes/version' -require 'proxes/base' require 'proxes/app' require 'proxes/security' -require 'proxes/es_request' diff --git a/lib/proxes/app.rb b/lib/proxes/app.rb index 575cc6a..a460cac 100644 --- a/lib/proxes/app.rb +++ b/lib/proxes/app.rb @@ -1,28 +1,49 @@ -require 'proxes/base' -require 'proxes/routes' +require 'proxes/controllers/application' module ProxES # Manage your Elasticsearch cluster, user and user sessions - class App < ProxES::Base - plugin :multi_route + class App < Application + get '/' do + authenticate! + haml :index + end - def logger - require 'logger' - @logger ||= Logger.new($stdout) + ['/unauthenticated', '/_proxes/unauthenticated'].each do |path| + get path do + redirect '/auth/identity' + end end - def root_url - @root_url = opts[:root_url] || '/_proxes' + post '/auth/identity/new' do + identity = Identity.new(params['identity']) + if identity.valid? && identity.save + flash[:info] = 'Successfully Registered. Please log in' + redirect '/auth/identity' + else + flash.now[:warning] = 'Could not complete the registration. Please try again.' + view 'security/register', locals: { identity: identity } + end end - route do |r| - r.multi_route + post '/auth/identity/callback' do + user = User.find_or_create(email: env['omniauth.auth']['info']['email']) + user.add_user_role role: 'user' unless user.has_role? 'user' + user.add_user_role(role: 'super_admin') if (user.id == 1 && user.has_role?('super_admin') == false) - r.get do - authenticate! + identity = Identity.find(username: user.email) + user.add_identity identity unless identity.user == user - view 'index' - end + set_user user + flash[:success] = 'Logged In' + redirect '/_proxes' + end + + delete '/auth/identity' do + logout + + flash[:info] = 'Logged Out' + + redirect '/_proxes' end end end diff --git a/lib/proxes/base.rb b/lib/proxes/base.rb deleted file mode 100644 index 2ea3cc2..0000000 --- a/lib/proxes/base.rb +++ /dev/null @@ -1,57 +0,0 @@ -require 'tilt/haml' -require 'roda' -require 'rack' - -module ProxES - # Base Roda App - class Base < Roda - opts[:root] ||= File.expand_path(File.dirname(__FILE__) + '/../../') - - plugin :middleware - - use Rack::MethodOverride - plugin :all_verbs - plugin :empty_root - - plugin :default_headers, - 'Content-Type'=>'text/html', - # 'Content-Security-Policy'=>"default-src 'self' https://oss.maxcdn.com/ https://maxcdn.bootstrapcdn.com https://ajax.googleapis.com", - #'Strict-Transport-Security'=>'max-age=16070400;', # Uncomment if only allowing https:// access - 'X-Frame-Options'=>'deny', - 'X-Content-Type-Options'=>'nosniff', - 'X-XSS-Protection'=>'1; mode=block' - - plugin :render, engine: 'haml', views: opts[:views] - plugin :partials - plugin :csrf, raise: true, skip_if: lambda { |r| !(r.path =~ %r{/_proxes/.*}) } - plugin :indifferent_params - plugin :flash - plugin :halt - - plugin(:not_found) { view 'http_404' } - plugin(:error_handler) do |e| - case true - when e.is_a?(Roda::RodaPlugins::Authentication::NotAuthenticated) || e.is_a?(OmniAuth::Error) - request.redirect '/auth/identity' - when e.is_a?(Pundit::NotAuthorizedError) - request.halt 404 - else - logger.error e - raise e unless ENV['RACK_ENV'] == 'production' - view 'error', locals: { error: e } - end - end - - plugin :authentication - plugin :pundit - - def logger - require 'logger' - @logger ||= Logger.new($stdout) - end - - def root_url - @root_url = opts[:root_url] || '/_proxes' - end - end -end diff --git a/lib/proxes/controllers.rb b/lib/proxes/controllers.rb new file mode 100644 index 0000000..a3802a4 --- /dev/null +++ b/lib/proxes/controllers.rb @@ -0,0 +1,2 @@ +require 'proxes/controllers/users' +require 'proxes/controllers/user_roles' diff --git a/lib/proxes/controllers/application.rb b/lib/proxes/controllers/application.rb new file mode 100644 index 0000000..25c80ed --- /dev/null +++ b/lib/proxes/controllers/application.rb @@ -0,0 +1,39 @@ +require 'sinatra/base' +require 'sinatra/flash' +require 'proxes/helpers/views' +require 'proxes/helpers/pundit' +require 'proxes/helpers/authentication' + +module ProxES + class Application < Sinatra::Base + set :root, ::File.expand_path(::File.dirname(__FILE__) + '/../../../') + register Sinatra::Flash + helpers ProxES::Helpers::Pundit, ProxES::Helpers::Views, ProxES::Helpers::Authentication + + configure :production do + disable :show_exceptions + end + + configure :development do + set :show_exceptions, :after_handler + end + + configure :production, :development do + enable :logging + end + + not_found do + haml :'404', locals: { title: '4 oh 4' } + end + + error do + error = env['sinatra.error'] + haml :error, locals: { title: 'Something went wrong', message: error } + end + + error ::Pundit::NotAuthorizedError do + flash[:warning] = 'Please log in first.' + redirect '/auth/identity' + end + end +end diff --git a/lib/proxes/controllers/auth_identity.rb b/lib/proxes/controllers/auth_identity.rb new file mode 100644 index 0000000..d3cab56 --- /dev/null +++ b/lib/proxes/controllers/auth_identity.rb @@ -0,0 +1,20 @@ +require 'proxes/controllers/application' + +module ProxES + class AuthIdentity < Application + get '/auth/identity' do + haml :'identity/login', locals: { title: 'Log In' } + end + + # Failed Login + post '/_proxes/auth/identity/callback' do + flash[:warning] = 'Invalid credentials. Please try again.' + redirect '/auth/identity' + end + + get '/auth/identity/register' do + identity = Identity.new + haml :'identity/register', locals: { title: 'Register', identity: identity } + end + end +end diff --git a/lib/proxes/controllers/component.rb b/lib/proxes/controllers/component.rb new file mode 100644 index 0000000..58a6288 --- /dev/null +++ b/lib/proxes/controllers/component.rb @@ -0,0 +1,88 @@ +require 'proxes/controllers/application' +require 'proxes/helpers/component' + +module ProxES + class Component < Application + helpers ProxES::Helpers::Component + set base_path: nil + set view_location: nil + + # List + get '/' do + authorize settings.model_class, :list + + actions = {} + actions["#{base_path}/new"] = "New #{heading}" if policy(settings.model_class).create? + + haml :"#{view_location}/index", locals: { list: list, title: heading(:list), actions: actions } + end + + # Create Form + get '/new' do + authorize settings.model_class, :create + + entity = settings.model_class.new(permitted_attributes(settings.model_class, :create)) + haml :"#{view_location}/new", locals: { entity: entity, title: heading(:new) } + end + + # Create + post '/' do + authorize settings.model_class, :create + + entity = settings.model_class.new(permitted_attributes(settings.model_class, :create)) + if entity.valid? && entity.save + flash[:success] = "#{heading} Created" + redirect "#{base_path}/#{entity.id}" + else + haml :"#{view_location}/new", locals: { entity: entity, title: heading(:new) } + end + end + + # Read + get '/:id' do |id| + entity = dataset[id.to_i] + halt 404 unless entity + authorize entity, :read + + actions = {} + actions["#{base_path}/#{entity.id}/edit"] = "Edit #{heading}" if policy(entity).update? + + haml :"#{view_location}/display", locals: { entity: entity, title: heading, actions: actions } + end + + # Update Form + get '/:id/edit' do |id| + entity = dataset.find(id: id.to_i) + halt 404 unless entity + authorize entity, :update + + haml :"#{view_location}/edit", locals: { entity: entity, title: heading(:edit) } + end + + # Update + put '/:id' do |id| + entity = dataset.find(id: id.to_i) + halt 404 unless entity + authorize entity, :update + + entity.set(permitted_attributes(settings.model_class, :create)) + if entity.valid? && entity.save + flash[:success] = "#{heading} Updated" + redirect "#{base_path}/#{entity.id}" + else + haml :"#{view_location}/edit", locals: { entity: entity, title: heading(:edit) } + end + end + + delete '/:id' do |id| + entity = dataset.find(id: id.to_i) + halt 404 unless entity + authorize entity, :delete + + entity.destroy + + flash[:success] = "#{heading} Deleted" + redirect "#{base_path}" + end + end +end diff --git a/lib/proxes/controllers/user_roles.rb b/lib/proxes/controllers/user_roles.rb new file mode 100644 index 0000000..21e5803 --- /dev/null +++ b/lib/proxes/controllers/user_roles.rb @@ -0,0 +1,10 @@ +require 'proxes/controllers/component' +require 'proxes/models/user_role' + +module ProxES + class UserRoles < Component + set model_class: ProxES::UserRole + set view_location: 'user_roles' + set base_path: '/user-roles' + end +end diff --git a/lib/proxes/controllers/users.rb b/lib/proxes/controllers/users.rb new file mode 100644 index 0000000..195ad89 --- /dev/null +++ b/lib/proxes/controllers/users.rb @@ -0,0 +1,90 @@ +require 'proxes/controllers/component' +require 'proxes/models/user' + +module ProxES + class Users < Component + set model_class: ProxES::User + + # New + get '/new' do + authorize settings.model_class, :create + + locals = { + title: heading(:new), + entity: User.new, + identity: Identity.new + } + haml :"#{view_location}/new", locals: locals, layout_opts: { locals: locals } + end + + # Create + post '/' do + authorize settings.model_class, :create + + locals = { title: heading(:new) } + + user_params = permitted_attributes(User, :create) + identity_params = permitted_attributes(Identity, :create) + user_params['email'] = identity_params['username'] + roles = user_params.delete('user_roles') + user = locals[:user] = User.new(user_params) + identity = locals[:identity] = Identity.new(identity_params) + if identity.valid? && user.valid? + DB.transaction(:isolation => :serializable) do + identity.save + user.save + user.add_identity identity + + if roles + user.remove_all_user_roles + roles.each { |role| user.add_user_role(role: role) } + end + end + + flash[:success] = 'User created' + redirect "/_proxes/users/#{user.id}" + else + flash.now[:danger] = 'Could not create the user' + locals[:entity] = user + locals[:identity] = identity + haml :"#{view_location}/new", locals: locals + end + end + + # Update + put '/:id' do |id| + entity = dataset.find(id: id.to_i) + halt 404 unless entity + authorize entity, :update + + values = permitted_attributes(settings.model_class, :update) + roles = values.delete('user_roles') + entity.set values + if entity.valid? && entity.save + if roles + entity.remove_all_user_roles + roles.each { |role| entity.add_user_role(role: role) } + end + + flash[:success] = "#{heading} Updated" + redirect "/_proxes/users/#{entity.id}" + else + haml :"#{view_location}/edit", locals: { entity: entity, title: heading(:edit) } + end + end + + # Delete + delete '/:id' do |id| + entity = dataset.find(id: id.to_i) + halt 404 unless entity + authorize entity, :delete + + entity.remove_all_identity + entity.remove_all_user_roles + entity.destroy + + flash[:success] = "#{heading} Deleted" + redirect "/_proxes/users" + end + end +end diff --git a/lib/proxes/db.rb b/lib/proxes/db.rb index 12dc0af..a70f5d7 100644 --- a/lib/proxes/db.rb +++ b/lib/proxes/db.rb @@ -7,6 +7,8 @@ DB.loggers << Logger.new($stdout) +DB.extension(:pagination) + Sequel::Model.plugin :auto_validations Sequel::Model.plugin :prepared_statements Sequel::Model.plugin :prepared_statements_associations diff --git a/lib/proxes/helpers/authentication.rb b/lib/proxes/helpers/authentication.rb new file mode 100644 index 0000000..7c0c3a4 --- /dev/null +++ b/lib/proxes/helpers/authentication.rb @@ -0,0 +1,31 @@ +module ProxES::Helpers + module Authentication + def current_user + env['warden'] ? env['warden'].user : nil + end + + def authenticate + env['warden'] && env['warden'].authenticate + end + + def authenticated? + env['warden'] && env['warden'].authenticated? + end + + def authenticate! + raise NotAuthenticated unless env['warden'] + env['warden'].authenticate! + end + + def logout + env['warden'] && env['warden'].logout + end + + def set_user(user) + env['warden'].set_user(user) + end + end + + class NotAuthenticated < StandardError + end +end diff --git a/lib/proxes/helpers/component.rb b/lib/proxes/helpers/component.rb new file mode 100644 index 0000000..009d1bb --- /dev/null +++ b/lib/proxes/helpers/component.rb @@ -0,0 +1,39 @@ +require 'active_support' +require 'active_support/inflector' + +module ProxES::Helpers + module Component + def dataset + policy_scope(settings.model_class) + end + + def list + params['count'] = params['count'] ? params['count'].to_i : 10 + params['page'] = params['page'] ? params['page'].to_i : 1 + + dataset.select.paginate(params['page'], params['count']) + end + + def heading(action = nil) + heading = ActiveSupport::Inflector.demodulize settings.model_class + case action + when :list + ActiveSupport::Inflector.pluralize heading + when :new + "New #{heading}" + when :edit + "Edit #{heading}" + else + heading + end + end + + def base_path + settings.base_path || "/#{heading(:list).downcase}" + end + + def view_location + settings.view_location || "#{heading(:list).downcase}" + end + end +end diff --git a/lib/proxes/helpers/pundit.rb b/lib/proxes/helpers/pundit.rb new file mode 100644 index 0000000..923bc08 --- /dev/null +++ b/lib/proxes/helpers/pundit.rb @@ -0,0 +1,42 @@ +require 'pundit' +require 'proxes/es_request' + +module ProxES + module Helpers + module Pundit + include ::Pundit + + def authorize(record, query = nil) + if record.is_a?(::ProxES::ESRequest) + if record.action.nil? && record.index + query = '_index?' + else + query = record.action ? record.action.to_s + '?' : '_root?' + end + else + raise ArgumentError, 'Pundit cannot determine the query' if query.nil? + end + query = :"#{query}?" unless query[-1] == '?' + super + end + + def permitted_attributes(record, action) + param_key = PolicyFinder.new(record).param_key + policy = policy(record) + method_name = if policy.respond_to?("permitted_attributes_for_#{action}") + "permitted_attributes_for_#{action}" + else + 'permitted_attributes' + end + + request.params.fetch(param_key, {}).select do |key, value| + policy.public_send(method_name).include? key.to_sym + end + end + + def pundit_user + current_user + end + end + end +end diff --git a/lib/proxes/helpers/views.rb b/lib/proxes/helpers/views.rb new file mode 100644 index 0000000..280c66f --- /dev/null +++ b/lib/proxes/helpers/views.rb @@ -0,0 +1,37 @@ +module ProxES::Helpers + module Views + def form_control(name, model, opts = {}) + id = opts.delete(:id) || name + type = opts.delete(:type) || 'text' + label = opts.delete(:label) || name.to_s.titlecase + klass = opts.delete(:class) || 'form-control' unless type == 'file' + group = opts.delete(:group) || model.class.name.split('::').last.downcase + + attributes = opts.merge(id: id, name: "#{group}[#{name}]", type: type, class: klass) + locals = { model: model, label: label, attributes: attributes, name: name, group: group } + haml :'partials/form_control', locals: locals + end + + def flash_messages(key = :flash) + return "" if flash(key).empty? + id = (key == :flash ? "flash" : "flash_#{key}") + messages = flash(key).collect {|message| " \n"} + "
\n" + messages.join + "
" + end + + def delete_form(entity, label = 'Delete') + locals = { delete_label: label, entity: entity } + haml :'partials/delete_form', locals: locals + end + + def pagination(list, base_path, count: params[:count]) + locals = { + next_link: list.last_page? ? '#' : "#{base_path}?page=#{list.next_page}&count=#{list.page_size}", + prev_link: list.first_page? ? '#' : "#{base_path}?page=#{list.prev_page}&count=#{list.page_size}", + base_path: base_path, + list: list, + } + haml :'partials/pager', locals: locals + end + end +end diff --git a/lib/proxes/models/user.rb b/lib/proxes/models/user.rb index ddb848a..56f9435 100644 --- a/lib/proxes/models/user.rb +++ b/lib/proxes/models/user.rb @@ -8,9 +8,18 @@ module ProxES class User < Sequel::Model one_to_many :identity + one_to_many :user_roles def has_role?(check) - check.to_sym == role.to_sym + user_roles.map(&:role).map(&:to_sym).include? check.to_sym + end + + def admin? + (user_roles.map(&:role) & ['admin', 'super_admin']).any? + end + + def admin? + has_role?(:admin) || has_role?(:super_admin) end def method_missing(method_sym, *arguments, &block) @@ -30,9 +39,6 @@ def validate validates_presence :email validates_unique :email unless email.blank? validates_format /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i, :email unless email.blank? - - validates_presence :role - validates_includes ['super_admin', 'admin', 'owner', 'user'], :role unless role.blank? end def index_prefix diff --git a/lib/proxes/models/user_role.rb b/lib/proxes/models/user_role.rb new file mode 100644 index 0000000..564669d --- /dev/null +++ b/lib/proxes/models/user_role.rb @@ -0,0 +1,19 @@ +require 'sequel' + +module ProxES + class UserRole < Sequel::Model + many_to_one :user + + subset(:admins, role: ['admin', 'super_admin']) + subset(:authorisers, role: 'authoriser') + + def validate + validates_presence :user_id + validates_includes self.class.role_names, :role + end + + def self.role_names + ['super_admin', 'admin', 'user'] + end + end +end diff --git a/lib/proxes/policies/application_policy.rb b/lib/proxes/policies/application_policy.rb index 6da7673..06a281c 100644 --- a/lib/proxes/policies/application_policy.rb +++ b/lib/proxes/policies/application_policy.rb @@ -1,17 +1,19 @@ -class ApplicationPolicy - attr_reader :user, :record +module ProxES + class ApplicationPolicy + attr_reader :user, :record - def initialize(user, record) - @user = user - @record = record - end + def initialize(user, record) + @user = user + @record = record + end - class Scope - attr_reader :user, :scope + class Scope + attr_reader :user, :scope - def initialize(user, scope) - @user = user - @scope = scope + def initialize(user, scope) + @user = user + @scope = scope + end end end end diff --git a/lib/proxes/policies/token_policy.rb b/lib/proxes/policies/token_policy.rb new file mode 100644 index 0000000..58d1da0 --- /dev/null +++ b/lib/proxes/policies/token_policy.rb @@ -0,0 +1,45 @@ +require_relative 'application_policy' + +module ProxES + class TokenPolicy < ApplicationPolicy + def create? + user.admin? + end + + def list? + create? + end + + def read? + record.id == user.id || user.admin? + end + + def update? + read? + end + + def delete? + create? + end + + def register? + true + end + + def permitted_attributes + attribs = [:email, :name, :surname] + attribs << :role if user.admin? + attribs + end + + class Scope < ApplicationPolicy::Scope + def resolve + if user.admin? + scope.all + else + [] + end + end + end + end +end diff --git a/lib/proxes/policies/user_policy.rb b/lib/proxes/policies/user_policy.rb index 39e1f8f..f03fe6e 100644 --- a/lib/proxes/policies/user_policy.rb +++ b/lib/proxes/policies/user_policy.rb @@ -1,21 +1,22 @@ -require_relative 'application_policy' +require 'proxes/policies/application_policy' + module ProxES class UserPolicy < ApplicationPolicy def create? - user.admin? + user && user.admin? end def list? create? end - def get? - record.id == user.id || user.admin? + def read? + user && (record.id == user.id || user.admin?) end def update? - get? + read? end def delete? @@ -28,16 +29,16 @@ def register? def permitted_attributes attribs = [:email, :name, :surname] - attribs << :role if user.admin? + attribs << :user_roles if user.admin? attribs end class Scope < ApplicationPolicy::Scope def resolve - if user.admin? - scope.all + if user && user.admin? + scope else - [] + scope.where(id: -1) end end end diff --git a/lib/proxes/security.rb b/lib/proxes/security.rb index 44fbde7..3924663 100644 --- a/lib/proxes/security.rb +++ b/lib/proxes/security.rb @@ -1,108 +1,52 @@ -require 'proxes/base' +require 'rack-proxy' require 'proxes/es_request' require 'proxes/policies/es_policy' +require 'proxes/helpers/pundit' +require 'proxes/helpers/authentication' module ProxES - # Provide OmniAuth::Identity roots and Pundit checks - class Security < ProxES::Base - route do |r| - # Warden - r.on '_proxes' do - r.get 'unauthenticated' do - r.redirect '/auth/identity' - end - end - - # Omniauth Identity paths - r.on 'auth' do - r.get 'failure' do - message = case params['message'] - when 'invalid_credentials' - 'Invalid credentials. Please try again.' - else - params['message'] - end - - flash[:warning] = message - r.redirect '/auth/identity' - end + class Security + attr_reader :env - r.on 'identity' do - identity = Identity.new + include ProxES::Helpers::Authentication + include ProxES::Helpers::Pundit - r.delete do - session.delete('user_id') - flash[:info] = 'Logged Out' - redirect '/' - end - - r.get 'register' do - view 'security/register', locals: { identity: identity } - end - - r.post 'callback' do - user = User.find_or_create(email: env['omniauth.auth']['info']['email']){|u| u.role = 'user' } - identity = Identity.find(username: user.email) - user.add_identity identity unless identity.user + def initialize(app) + @app = app + end - flash[:success] = 'Logged In' - env['warden'].set_user(user) - r.redirect root_url - end + def call(env) + @env = env - r.post 'new' do - authorize Identity, :register - identity = Identity.new(permitted_attributes(Identity, :register)) - if identity.valid? && identity.save - flash[:info] = 'Successfully Registered. Please log in' - r.redirect '/auth/identity' - else - flash.now[:warning] = 'Could not complete the registration. Please try again.' - view 'security/register', locals: { identity: identity } - end - end + request = ProxES::ESRequest.new(env) - r.get do - view 'security/login' - end - end + unless ENV['RACK_ENV'] == 'production' + puts '================================================================================' + puts '= ' + "Request: #{request.fullpath}".ljust(76) + '=' + puts '= ' + "Endpoint: #{request.endpoint}".ljust(76) + '=' + puts '= ' + "Index: #{request.index}".ljust(76) + '=' + puts '= ' + "Type: #{request.type}".ljust(76) + '=' + puts '= ' + "Action: #{request.action}".ljust(76) + '=' + puts '================================================================================' end - # Everything Else - r.on proc{true} do - authenticate! - - request = ProxES::ESRequest.new(env) - - unless ENV['RACK_ENV'] == 'production' - puts '================================================================================' - puts '= ' + "Request: #{request.fullpath}".ljust(76) + '=' - puts '= ' + "Endpoint: #{request.endpoint}".ljust(76) + '=' - puts '= ' + "Index: #{request.index}".ljust(76) + '=' - puts '= ' + "Type: #{request.type}".ljust(76) + '=' - puts '= ' + "Action: #{request.action}".ljust(76) + '=' - puts '================================================================================' - end - - if request.has_indices? - policy_scope request - else - authorize request - end - - unless ENV['RACK_ENV'] == 'production' - puts '================================================================================' - puts '= ' + "Request: #{request.fullpath}".ljust(76) + '=' - puts '= ' + "Endpoint: #{request.endpoint}".ljust(76) + '=' - puts '= ' + "Index: #{request.index}".ljust(76) + '=' - puts '= ' + "Type: #{request.type}".ljust(76) + '=' - puts '= ' + "Action: #{request.action}".ljust(76) + '=' - puts '================================================================================' - end + if request.has_indices? + policy_scope request + else + authorize request + end - # Throw so that we move on to the next middleware - throw :next, true + unless ENV['RACK_ENV'] == 'production' + puts '================================================================================' + puts '= ' + "Request: #{request.fullpath}".ljust(76) + '=' + puts '= ' + "Endpoint: #{request.endpoint}".ljust(76) + '=' + puts '= ' + "Index: #{request.index}".ljust(76) + '=' + puts '= ' + "Type: #{request.type}".ljust(76) + '=' + puts '= ' + "Action: #{request.action}".ljust(76) + '=' + puts '================================================================================' end + + @app.call env end end end diff --git a/lib/roda/plugins/authentication.rb b/lib/roda/plugins/authentication.rb deleted file mode 100644 index ffc42af..0000000 --- a/lib/roda/plugins/authentication.rb +++ /dev/null @@ -1,35 +0,0 @@ -class Roda - module RodaPlugins - # Provide helper methods to access Warden - module Authentication - module InstanceMethods - - def current_user - env['warden'] ? env['warden'].user : nil - end - - def authenticate - env['warden'] && env['warden'].authenticate - end - - def authenticated? - env['warden'] && env['warden'].authenticated? - end - - def authenticate! - raise NotAuthenticated unless env['warden'] - env['warden'].authenticate! - end - - def logout - env['warden'] && env['warden'].logout - end - end - - class NotAuthenticated < StandardError - end - end - - register_plugin(:authentication, Authentication) - end -end diff --git a/lib/roda/plugins/pundit.rb b/lib/roda/plugins/pundit.rb deleted file mode 100644 index b4f7175..0000000 --- a/lib/roda/plugins/pundit.rb +++ /dev/null @@ -1,51 +0,0 @@ -require 'pundit' -require 'proxes/es_request' - -class Roda - module RodaPlugins - module Pundit - def self.configure(app, opts = {}) - policies_path = File.expand_path(opts[:policies]||'policies', app.opts[:root]) - Dir[File.join(policies_path, '/**/*.rb')].each { |file| require file } - end - - module InstanceMethods - include ::Pundit - - def authorize(record, query = nil) - if record.is_a?(::ProxES::ESRequest) - if record.action.nil? && record.index - query = '_index?' - else - query = record.action ? record.action.to_s + '?' : '_root?' - end - else - raise ArgumentError, 'Pundit cannot determine the query' if query.nil? - end - query = query.to_s + '?' unless query[-1] == '?' - super(record, query) - end - - def pundit_user - current_user - end - - def permitted_attributes(record, action) - param_key = PolicyFinder.new(record).param_key - policy = policy(record) - method_name = if policy.respond_to?("permitted_attributes_for_#{action}") - "permitted_attributes_for_#{action}" - else - 'permitted_attributes' - end - - request.params.fetch(param_key, {}).select do |key, value| - policy.public_send(method_name).include? key.to_sym - end - end - end - end - - register_plugin(:pundit, Pundit) - end -end diff --git a/migrate/001_tables.rb b/migrate/001_tables.rb index d8c1f04..a8ffa12 100644 --- a/migrate/001_tables.rb +++ b/migrate/001_tables.rb @@ -5,7 +5,6 @@ String :name String :surname String :email - String :role DateTime :created_at DateTime :updated_at end @@ -18,5 +17,12 @@ DateTime :created_at DateTime :updated_at end + + create_table :user_roles do + primary_key :id + foreign_key :user_id, :users + String :role + unique [:user_id, :role] + end end end diff --git a/proxes.gemspec b/proxes.gemspec index 1fd742b..4c6b1ce 100644 --- a/proxes.gemspec +++ b/proxes.gemspec @@ -22,14 +22,20 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'bundler', '~> 1.12' spec.add_development_dependency 'rake', '~> 10.0' spec.add_development_dependency 'rspec', '~> 3.0' + spec.add_development_dependency 'racksh' + spec.add_development_dependency 'rack-test' + spec.add_development_dependency 'pry-byebug' + spec.add_development_dependency 'rubocop' spec.add_development_dependency 'guard' spec.add_development_dependency 'guard-rake' spec.add_development_dependency 'guard-rspec' spec.add_development_dependency 'guard-shotgun' + spec.add_development_dependency 'database_cleaner' + spec.add_development_dependency 'factory_girl' - spec.add_dependency 'rack_csrf' spec.add_dependency 'rack-proxy' - spec.add_dependency 'roda' + spec.add_dependency 'sinatra' + spec.add_dependency 'sinatra-flash' spec.add_dependency 'elasticsearch' spec.add_dependency 'logger' spec.add_dependency 'pundit' diff --git a/spec/crud_spec.rb b/spec/crud_spec.rb new file mode 100644 index 0000000..a0d40ec --- /dev/null +++ b/spec/crud_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' +require 'proxes/controllers' +require 'support/crud_shared_examples' + +{ + '/users' => ProxES::Users, +}.each do |route, controller| + describe controller do + def app + described_class + end + + context 'as super_admin_user' do + let(:user) { create(:super_admin_user) } + let(:model) { create(app.model_class.name.to_sym) } + + before(:each) do + # Log in + warden = double(Warden::Proxy) + allow(warden).to receive(:user).and_return(user) + env 'warden', warden + end + + it_behaves_like 'a CRUD Controller', route + end + + context 'as user' do + let(:user) { create(:user) } + let(:model) { create(app.model_class.name.to_sym) } + + before(:each) do + # Log in + warden = double(Warden::Proxy) + allow(warden).to receive(:user).and_return(user) + env 'warden', warden + end + + it_behaves_like 'a CRUD Controller', route + end + end +end diff --git a/spec/factories.rb b/spec/factories.rb new file mode 100644 index 0000000..53ff269 --- /dev/null +++ b/spec/factories.rb @@ -0,0 +1,34 @@ +require 'proxes/models/user' +require 'proxes/models/user_role' + +FactoryGirl.define do + to_create { |i| i.save } + + sequence(:email) { |n| "person-#{n}@example.com" } + sequence(:name) { |n| "Name-#{n}" } + + factory :user, class: ProxES::User, aliases: [:'ProxES::User'] do + email + + after(:create) do |user, evaluator| + user.add_user_role(role: 'user') + end + + factory :admin_user do + after(:create) do |user, evaluator| + user.add_user_role(role: 'admin') + end + end + + factory :super_admin_user do + after(:create) do |user, evaluator| + user.add_user_role(role: 'super_admin') + end + end + end + + factory :user_role, class: ProxES::UserRole, aliases: [:'ProxES::UserRole'] do + role + user + end +end diff --git a/spec/proxes/security_spec.rb b/spec/proxes/security_spec.rb new file mode 100644 index 0000000..96bc6e3 --- /dev/null +++ b/spec/proxes/security_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' +require 'proxes/security' +require 'rack' +require 'pundit/rspec' + +describe ProxES::Security do + def app + ProxES::Security.new(proc{[200,{},['Hello, world.']]}) + end + + context '#call' do + fit 'rejects anonymous requests' do + expect { get('/') }.to raise_error(ProxES::Helpers::NotAuthenticated) + end + + context 'logged in' do + let(:user) { create(:user) } + + before(:each) do + # Log in + warden = double(Warden::Proxy) + allow(warden).to receive(:user).and_return(user) + allow(warden).to receive(:authenticate!) + env 'warden', warden + end + + it 'authorizes calls that return data' do + expect(get("/#{user.email}/_search")).to be_ok + expect{ get('/notmyindex/_search') }.to raise_error(Pundit::NotAuthorizedError) + end + + it 'authorizes calls that do actions' do + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 459cfab..a1ac977 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,2 +1,35 @@ -$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) +ENV['RACK_ENV'] = 'test' + require 'proxes' +require 'proxes/db' +require 'rspec' +require 'rack/test' +require 'warden' +require 'factory_girl' +require 'database_cleaner' + +RSpec.configure do |config| + config.include Rack::Test::Methods + config.include Warden::Test::Helpers + config.include FactoryGirl::Syntax::Methods + + config.alias_example_to :fit, focus: true + config.filter_run focus: true + config.run_all_when_everything_filtered = true + + config.before(:suite) do + DatabaseCleaner.strategy = :transaction + + FactoryGirl.find_definitions + end + + config.around(:each) do |example| + DatabaseCleaner.cleaning do + example.run + end + end + + config.after(:each) do + Warden.test_reset! + end +end diff --git a/spec/support/crud_shared_examples.rb b/spec/support/crud_shared_examples.rb new file mode 100644 index 0000000..40ee8c7 --- /dev/null +++ b/spec/support/crud_shared_examples.rb @@ -0,0 +1,44 @@ +shared_examples 'a CRUD Controller' do |route| + context 'GET' do + it "#{route}" do + model # Ensure that there's at least one item in the list + get '/' + + if Pundit.policy(user, app.model_class).list? + expect(last_response).to be_ok + else + expect(last_response).to_not be_ok + end + end + + it "#{route}/new" do + get '/' + + if Pundit.policy(user, app.model_class).list? + expect(last_response).to be_ok + else + expect(last_response).to_not be_ok + end + end + + it "#{route}/id" do + get "/#{model.id}" + + if Pundit.policy(user, app.model_class).list? + expect(last_response).to be_ok + else + expect(last_response).to_not be_ok + end + end + + it "#{route}/id/edit" do + get "/#{model.id}" + + if Pundit.policy(user, app.model_class).list? + expect(last_response).to be_ok + else + expect(last_response).to_not be_ok + end + end + end +end diff --git a/views/http_404.haml b/views/404.haml similarity index 100% rename from views/http_404.haml rename to views/404.haml diff --git a/views/_getting_started.haml b/views/getting_started.haml similarity index 85% rename from views/_getting_started.haml rename to views/getting_started.haml index bcc361f..121cfca 100644 --- a/views/_getting_started.haml +++ b/views/getting_started.haml @@ -14,8 +14,7 @@ %pre curl -X GET 'http://proxes.co.za/#{ current_user.index_prefix}/_search' -H 'Authorization: YourTokenHere' - %form.text-center.lead{ method: 'post', action: root_url + '/tokens' } + %form.text-center.lead{ method: 'post', action: '/tokens' } No Token yet? - = csrf_tag %button.btn.btn-primary{ type: 'submit' } Get One diff --git a/views/security/login.haml b/views/identity/login.haml similarity index 86% rename from views/security/login.haml rename to views/identity/login.haml index 6c45fa0..58856ea 100644 --- a/views/security/login.haml +++ b/views/identity/login.haml @@ -5,8 +5,7 @@ .panel-heading ProxES Login .panel-body - %form{ method: 'post', action: '/auth/identity/callback' } - = csrf_tag + %form{ method: 'post', action: '/_proxes/auth/identity/callback' } .form-group %label.control-label Username %input.form-control.border-input{ name: 'username' } diff --git a/views/identity/register.haml b/views/identity/register.haml new file mode 100644 index 0000000..249a606 --- /dev/null +++ b/views/identity/register.haml @@ -0,0 +1,17 @@ +.row + .col-sm-12 + %h1 ProxES Registration +.row + .col-sm-2 + .col-sm-8 + .panel.panel-default + .panel-heading + ProxES Registration + .panel-body + %form.form-horizontal{ method: 'post', action: '/_proxes/auth/identity/new' } + = form_control(:username, identity, label: 'Email', placeholder: 'Your email address') + = form_control(:password, identity, label: 'Password', type: :password) + = form_control(:password_confirmation, identity, label: 'Confirm Password', type: :password) + + %button.btn.btn-primary{ type: 'submit' } Register + .col-sm-2 diff --git a/views/index.haml b/views/index.haml index 8a0c3d9..6f3b1b6 100644 --- a/views/index.haml +++ b/views/index.haml @@ -1,6 +1,6 @@ -= partial('getting_started') += haml :getting_started #indexlist -%script{ src: root_url + '/assets/js/vendors.js', type: 'text/javascript' } -%script{ src: root_url + '/assets/js/bundle.js', type: 'text/javascript' } +%script{ src: '/assets/js/vendors.js', type: 'text/javascript' } +%script{ src: '/assets/js/bundle.js', type: 'text/javascript' } diff --git a/views/layout.haml b/views/layout.haml index b131a90..343ee61 100644 --- a/views/layout.haml +++ b/views/layout.haml @@ -4,7 +4,7 @@ %meta{ charset: 'utf-8' } %meta{ 'http-equiv': "X-UA-Compatible", content: "IE=edge,chrome=1" } %meta{ name: "viewport", content: "width=device-width, initial-scale=1" } - %link{ rel: "apple-touch-icon", sizes: "76x76", href: root_url + '/assets/img/apple-icon.png' } + %link{ rel: "apple-touch-icon", sizes: "76x76", href: '/assets/img/apple-icon.png' } %title ProxES @@ -24,11 +24,11 @@ %body #wrapper - = partial('navbar', locals: { title: (defined?(title) ? title : 'ProxES') }) + = haml :'partials/navbar', locals: { title: (defined?(title) ? title : 'ProxES') } #page-wrapper .row .col-md-12 - = partial('notifications') + = haml :'partials/notifications' = yield %footer.footer.text-muted.text-center %hr diff --git a/views/partials/delete_form.haml b/views/partials/delete_form.haml new file mode 100644 index 0000000..6e1ec8e --- /dev/null +++ b/views/partials/delete_form.haml @@ -0,0 +1,4 @@ +
+ + +
diff --git a/views/partials/form_control.haml b/views/partials/form_control.haml new file mode 100644 index 0000000..aa763e0 --- /dev/null +++ b/views/partials/form_control.haml @@ -0,0 +1,17 @@ +%div{ class: "form-group#{model.errors[name] ? ' has-error' : ''}" } + %label.col-sm-3.control-label{ for: attributes[:id] }= label + .col-sm-9 + - type = attributes.delete(:type) + - if type == 'select' + - options = attributes.delete(:options) + %select{attributes} + %option{ value: ""} -- Select One -- + - options.each do |k,v| k ||= v; v ||= k; + %option{ value: k, selected: (k.to_s == model[name].to_s)} = v + - elsif type == 'textarea' + %textarea{attributes} + = model[name] + - else + %input{attributes, type: type, value: model[name]} + - if model.errors[name] + %p.help-block.text-danger= model.errors[name].join(', ') diff --git a/views/_navbar.haml b/views/partials/navbar.haml similarity index 86% rename from views/_navbar.haml rename to views/partials/navbar.haml index b016b87..604c48a 100644 --- a/views/_navbar.haml +++ b/views/partials/navbar.haml @@ -10,16 +10,15 @@ %span.icon-bar.bar2 %span.icon-bar.bar3 -if current_user - %form.nav.navbar-top-links.navbar-form.navbar-right{ action: '/auth/identity', method: 'post' } - = csrf_tag + %form.nav.navbar-top-links.navbar-form.navbar-right{ action: '/_proxes/auth/identity', method: 'post' } %input{ name: '_method', value: 'DELETE', type: 'hidden' } %button.btn.btn-default{ type: 'submit' } / %i.ti-panel Logout - .navbar-default.sidebar{ role: 'navigation' } - = partial('sidebar') - else %ul.nav.navbar-top-links.navbar-right %li %a.btn.btn-link{ href: '/auth/identity' } Log In + .navbar-default.sidebar{ role: 'navigation' } + = haml :'partials/sidebar' diff --git a/views/_notifications.haml b/views/partials/notifications.haml similarity index 100% rename from views/_notifications.haml rename to views/partials/notifications.haml diff --git a/views/partials/pager.haml b/views/partials/pager.haml new file mode 100644 index 0000000..bc4ccd0 --- /dev/null +++ b/views/partials/pager.haml @@ -0,0 +1,19 @@ + diff --git a/views/_sidebar.haml b/views/partials/sidebar.haml similarity index 67% rename from views/_sidebar.haml rename to views/partials/sidebar.haml index 6cda9f7..75ac95b 100644 --- a/views/_sidebar.haml +++ b/views/partials/sidebar.haml @@ -1,20 +1,20 @@ %ul.nav.nav-pills.nav-stacked - if defined?(current_user) && current_user %li - %a{ href: root_url + '/' } + %a{ href: '/_proxes' } %i.fa.fa-dashboard.fa-fw Dashboard - if current_user.admin? %li - %a{ href: root_url + '/users' } - %i.fa.fa-user + %a{ href: '/_proxes/users' } + %i.fa.fa-user.fa-fw Users - else %li.active %a{ href: '/auth/identity' } - %i.fa.fa-user + %i.fa.fa-user.fa-fw Log In %li %a{ href: '/auth/identity/register' } - %i.fa.fa-pencil-square-o + %i.fa.fa-pencil-square-o.fa-fw Register diff --git a/views/security/register.haml b/views/security/register.haml deleted file mode 100644 index d301d26..0000000 --- a/views/security/register.haml +++ /dev/null @@ -1,32 +0,0 @@ -.row - .col-sm-2 - .col-sm-8 - .panel.panel-default - .panel-heading - ProxES Registration - .panel-body - %form{ method: 'post', action: '/auth/identity/new' } - = csrf_tag - .form-group{ class: ('has-error' if identity.errors[:username]) } - %label.control-label Email - %input.form-control.border-input{ name: 'identity[username]', id: :'identity_username', type: 'text', autofocus: true, value: identity.username, placeholder: 'Your email address' } - -if identity.errors[:username] - %p.help-block.text-danger - = identity.errors[:username].join(', ') - - .form-group{ class: ('has-error' if identity.errors[:password]) } - %label.control-label Password - %input.form-control.border-input{ name: 'identity[password]', id: 'identity_password', type: 'password', placeholder: 'Your password' } - -if identity.errors[:password] - %p.help-block.text-danger - = identity.errors[:password].join(', ') - - .form-group{ class: ('has-error' if identity.errors[:password_confirmation]) } - %label.control-label Confirm Password - %input.form-control.border-input{ name: 'identity[password_confirmation]', id: 'identity_password_confirmation', type: 'password', placeholder: 'Confirm your password' } - -if identity.errors[:password_confirmation] - %p.help-block.text-danger - = identity.errors[:password_confirmation].join(', ') - - %button.btn.btn-primary{ type: 'submit' } Register - .col-sm-2 diff --git a/views/sidebar.haml b/views/sidebar.haml new file mode 100644 index 0000000..51f19d0 --- /dev/null +++ b/views/sidebar.haml @@ -0,0 +1,20 @@ +%ul.nav.nav-pills.nav-stacked + - if current_user + %li + %a{ href: '/_proxes' } + %i.fa.fa-dashboard.fa-fw + Dashboard + - if current_user.admin? + %li + %a{ href: '/_proxes/users' } + %i.fa.fa-user.fa-fw + Users + - else + %li.active + %a{ href: '/auth/identity' } + %i.fa.fa-user.fa-fw + Log In + %li + %a{ href: '/auth/identity/register' } + %i.fa.fa-pencil-square-o.fa-fw + Register diff --git a/views/users/_identity.haml b/views/users/_identity.haml deleted file mode 100644 index 150ad2d..0000000 --- a/views/users/_identity.haml +++ /dev/null @@ -1,23 +0,0 @@ -.form-group{ class: ('has-error' if identity.errors[:username]) } - %label.control-label Email - - if identity.id - %input.form-control.border-input{ disabled: true, id: :'identity_username', type: 'text', value: identity.username } - - else - %input.form-control.border-input{ name: 'identity[username]', id: :'identity_username', type: 'text', autofocus: true, value: identity.username, placeholder: 'Your email address' } - -if identity.errors[:username] - %p.help-block.text-danger - = identity.errors[:username].join(', ') - -.form-group{ class: ('has-error' if identity.errors[:password]) } - %label.control-label Password - %input.form-control.border-input{ name: 'identity[password]', id: 'identity_password', type: 'password', placeholder: 'Your password' } - -if identity.errors[:password] - %p.help-block.text-danger - = identity.errors[:password].join(', ') - -.form-group{ class: ('has-error' if identity.errors[:password_confirmation]) } - %label.control-label Confirm Password - %input.form-control.border-input{ name: 'identity[password_confirmation]', id: 'identity_password_confirmation', type: 'password', placeholder: 'Confirm your password' } - -if identity.errors[:password_confirmation] - %p.help-block.text-danger - = identity.errors[:password_confirmation].join(', ') diff --git a/views/users/_user.haml b/views/users/_user.haml deleted file mode 100644 index b892e26..0000000 --- a/views/users/_user.haml +++ /dev/null @@ -1,23 +0,0 @@ -.form-group{ class: ('has-error' if user.errors[:name]) } - %label.control-label Name - %input.form-control.border-input{ name: 'user[name]', id: :'user_name', type: 'text', autofocus: true, value: user.name, placeholder: 'Your name' } - -if user.errors[:name] - %p.help-block.text-danger - = user.errors[:name].join(', ') - -.form-group{ class: ('has-error' if user.errors[:surname]) } - %label.control-label Surname - %input.form-control.border-input{ name: 'user[surname]', id: :'user_surname', type: 'text', autofocus: true, value: user.surname, placeholder: 'Your surname' } - -if user.errors[:surname] - %p.help-block.text-danger - = user.errors[:surname].join(', ') - -.form-group{ class: ('has-error' if user.errors[:role]) } - %label.control-label Role - %select.form-control.border-input{ name: 'user[role]', id: 'user_role' } - %option{ value: 'user', selected: (user.role == 'user') } User - %option{ value: 'owner', selected: (user.role == 'owner') } Owner - %option{ value: 'admin', selected: (user.role == 'admin') } Admin - -if user.errors[:role] - %p.help-block.text-danger - = user.errors[:role].join(', ') diff --git a/views/users/display.haml b/views/users/display.haml new file mode 100644 index 0000000..f54f590 --- /dev/null +++ b/views/users/display.haml @@ -0,0 +1,32 @@ +.row + .col-md-2 + .col-md-8 + .panel.panel-default + .panel-body + .author + %img.pull-right.thumbnail{ src: entity.gravatar } + %h4.title= entity.email + + %hr + %p.description + %label Name: + = entity.name + %p.description + %label Surname: + = entity.surname + %p.description + %label Roles: + = entity.user_roles.map(&:role).map(&:titlecase).join(', ') + %p.description + %label Signed up: + = entity.created_at.strftime('%Y-%m-%d %H:%M:%S') + + .row + .col-md-6 + %a.btn.btn-default{ href: "/_proxes/users/#{entity.id}/edit" } Edit + .col-md-6.text-right + - if policy(entity).delete? + %form{ method: 'post', action: "/_proxes/users/#{entity.id}" } + %input{ name: '_method', value: 'DELETE', type: 'hidden' } + %button.btn.btn-warning{ type: 'submit' } Delete + .col-md-2 diff --git a/views/users/edit.haml b/views/users/edit.haml index f38d3d2..db10eca 100644 --- a/views/users/edit.haml +++ b/views/users/edit.haml @@ -3,10 +3,9 @@ .col-md-8 .panel.panel-default .panel-body - %form{ method: 'post', action: root_url + "/users/#{user.id}" } + %form.form-horizontal{ method: 'post', action: "/_proxes/users/#{entity.id}" } %input{ name: '_method', value: 'PUT', type: 'hidden' } - = csrf_tag - = partial('users/user', locals: { user: user }) + = haml :'users/user', locals: { user: entity } %button.btn.btn-primary{ type: 'submit' } Update User .col-md-2 diff --git a/views/users/identity.haml b/views/users/identity.haml new file mode 100644 index 0000000..aa887a9 --- /dev/null +++ b/views/users/identity.haml @@ -0,0 +1,3 @@ += form_control(:username, identity, label: 'Email', placeholder: 'Your email address') += form_control(:password, identity, type: 'password', placeholder: 'Your password') += form_control(:password_confirmation, identity, type: 'password', label: 'Confirm Password', placeholder: 'Confirm your password') diff --git a/views/users/index.haml b/views/users/index.haml new file mode 100644 index 0000000..7d1ba17 --- /dev/null +++ b/views/users/index.haml @@ -0,0 +1,22 @@ +.row + .col-md-12 + .panel.panel-default + .panel-heading + = title + %table.table.table-striped + %thead + %tr + %th Email + %th Name + %th Surname + %th Roles + %tbody + -list.each do |entity| + %tr + %td + %a{ href: "/_proxes/users/#{entity.id}" }= entity.email + %td= entity.name + %td= entity.surname + %td= entity.user_roles.map(&:role).map(&:titlecase).join(', ') + .panel-body.text-right + %a.btn.btn-primary{ href: '/_proxes/users/new' } New User \ No newline at end of file diff --git a/views/users/list.haml b/views/users/list.haml deleted file mode 100644 index 479a772..0000000 --- a/views/users/list.haml +++ /dev/null @@ -1,22 +0,0 @@ -.row - .col-md-12 - .panel.panel-default - .panel-heading - = title - %table.table.table-striped - %thead - %tr - %th Email - %th Name - %th Surname - %th Roles - %tbody - -users.each do |user| - %tr - %td - %a{ href: root_url + "/users/#{user.id}" }= user.email - %td= user.name - %td= user.surname - %td= user.role - .panel-body.text-right - %a.btn.btn-primary{ href: root_url + '/users/new' } New User \ No newline at end of file diff --git a/views/users/new.haml b/views/users/new.haml index 197ce08..baa9aa9 100644 --- a/views/users/new.haml +++ b/views/users/new.haml @@ -3,10 +3,9 @@ .col-md-8 .panel.panel-default .panel-body - %form{ method: 'post', action: root_url + '/users' } - = csrf_tag - = partial('users/identity', locals: { identity: identity }) - = partial('users/user', locals: { user: user }) + %form.form-horizontal{ method: 'post', action: '/_proxes/users' } + = haml :'users/identity', locals: { identity: identity } + = haml :'users/user', locals: { user: entity } %button.btn.btn-primary.btn{ type: 'submit' } Create User .col-md-2 diff --git a/views/users/show.haml b/views/users/show.haml deleted file mode 100644 index 708de93..0000000 --- a/views/users/show.haml +++ /dev/null @@ -1,23 +0,0 @@ -.row - .col-md-2 - .col-md-8 - .panel.panel-default - .panel-body - .author - %img.pull-right.thumbnail{ src: user.gravatar } - %h4.title= user.email - - %hr - %p.description - %label Name: - = user.name - %p.description - %label Surname: - = user.surname - %p.description - %label Signed up: - = user.created_at.strftime('%Y-%m-%d %H:%M:%S') - - %p.text-right - %a.btn.btn-default{ href: root_url + "/users/#{user.id}/edit" } Edit - .col-md-2 diff --git a/views/users/user.haml b/views/users/user.haml new file mode 100644 index 0000000..1290f31 --- /dev/null +++ b/views/users/user.haml @@ -0,0 +1,11 @@ += form_control(:name, user) += form_control(:surname, user) +.form-group{ class: ('has-error' if user.errors[:user_roles]) } + %label.control-label.col-sm-3 Role + .col-sm-9 + %select.form-control.border-input{ name: 'user[user_roles][]', id: 'user_user_roles', multiple: true } + - ProxES::UserRole.role_names.each do |name| + %option{ value: name, selected: user.has_role?(name) }= name.titlecase + -if user.errors[:role] + %p.help-block.text-danger + = user.errors[:role].join(', ')