require 'cgi' require 'omniauth' require 'mustache/sinatra' require 'sinatra/base' module Omnigollum module Views; class Layout < Mustache; end; end module Models class OmniauthUserInitError < StandardError; end class User attr_reader :uid, :name, :email, :nickname, :provider end class OmniauthUser < User def initialize (hash, options) # Validity checks, don't trust providers @uid = hash['uid'].to_s.strip raise OmniauthUserInitError, "Insufficient data from authentication provider, uid not provided or empty" if @uid.empty? @name = hash['info']['name'].to_s.strip if hash['info'].has_key?('name') @name = options[:default_name] if !@name || @name.empty? raise OmniauthUserInitError, "Insufficient data from authentication provider, name not provided or empty" if !@name || @name.empty? @email = hash['info']['email'].to_s.strip if hash['info'].has_key?('email') @email = options[:default_email] if !@email || @email.empty? raise OmniauthUserInitError, "Insufficient data from authentication provider, email not provided or empty" if !@email || @email.empty? @nickname = hash['info']['nickname'].to_s.strip if hash['info'].has_key?('nickname') @provider = hash['provider'] self end end end module Helpers def user_authed? session.has_key? :omniauth_user end def user_auth @title = 'Authentication is required' @subtext = 'Please choose a login service' show_login end def kick_back redirect !request.referrer.nil? && request.referrer !~ /#{Regexp.escape(settings.send(:omnigollum)[:route_prefix])}\/.*/ ? request.referrer: '/' halt end def get_user session[:omniauth_user] end def user_deauth session.delete :omniauth_user end def auth_config options = settings.send(:omnigollum) @auth = { :route_prefix => options[:route_prefix], :providers => options[:provider_names], :path_images => options[:path_images], :logo_suffix => options[:logo_suffix], :logo_missing => options[:logo_missing] } end def show_login options = settings.send(:omnigollum) # Don't bother showing the login screen, just redirect if options[:provider_names].count == 1 if !request.params['origin'].nil? origin = request.params['origin'] elsif !request.path.nil? origin = request.path else origin = '/' end redirect (request.script_name || '') + options[:route_prefix] + '/auth/' + options[:provider_names].first.to_s + "?origin=" + CGI.escape(origin) else auth_config require options[:path_views] + '/login' halt mustache Omnigollum::Views::Login end end def show_error options = settings.send(:omnigollum) auth_config require options[:path_views] + '/error' halt mustache Omnigollum::Views::Error end def commit_message if user_authed? user = get_user return { :message => params[:message], :name => user.name, :email => user.email} else return { :message => params[:message]} end end end # Config class provides default values for omnigollum configuration, and an array # of all providers which have been enabled if a omniauth config block is passed to # eval_omniauth_config. class Config attr_accessor :default_options class << self; attr_accessor :default_options; end @default_options = { # Gollum 4 uses /create, /create/*, etc, while Gollum 5 uses # /gollum/create, /gollum/create/*, etc. Protect both by # default so that omnigollum works with either out of the box. :protected_routes => [ 'create', 'delete', 'edit', 'rename', 'revert', 'upload', ].map { |x| ["/#{x}", "/#{x}/*"].map { |y| [y, "/gollum#{y}"] } }.flatten, :route_prefix => '/__omnigollum__', :dummy_auth => true, :providers => Proc.new { provider :github, '', '' }, :path_base => dir = File.expand_path(File.dirname(__FILE__) + '/..'), :logo_suffix => "_logo.png", :logo_missing => "omniauth", # Set to false to disable missing logos :path_images => "#{dir}/public/images", :path_views => "#{dir}/views", :path_templates => "#{dir}/templates", :default_name => nil, :default_email => nil, :provider_names => [], :authorized_users => [], :author_format => Proc.new { |user| user.nickname ? user.name + ' (' + user.nickname + ')' : user.name }, :author_email => Proc.new { |user| user.email } } def initialize @default_options = self.class.default_options end # Register provider name # # name - Provider symbol # args - Arbitrary arguments def provider(name, *args) @default_options[:provider_names].push name end # Evaluate procedure calls in an omniauth config block/proc in the context # of this class. # # This allows us to learn about omniauth config items that would otherwise be inaccessible. # # block - Omniauth proc or block def eval_omniauth_config(&block) self.instance_eval(&block) end # Catches missing methods we haven't implemented, but which omniauth accepts # in its config block. # # args - Arbitrary list of arguments def method_missing(*args); end end module Sinatra def self.registered(app) # As options determine which routes are created, they must be set before registering omniauth config = Omnigollum::Config.new options = app.settings.respond_to?(:omnigollum) ? config.default_options.merge(app.settings.send(:omnigollum)) : config.default_options # Set omniauth path prefix based on options OmniAuth.config.path_prefix = options[:route_prefix] + OmniAuth.config.path_prefix # Setup test_mode options if options[:dummy_auth] OmniAuth.config.test_mode = true OmniAuth.config.mock_auth[:default] = { 'uid' => '12345', "info" => { "email" => "user@example.com", "name" => "example user" }, 'provider' => 'local' } end # Register helpers app.helpers Helpers # Enable sinatra session support app.set :sessions, true # Setup omniauth providers if !options[:providers].nil? app.use OmniAuth::Builder, &options[:providers] # You told omniauth, now tell us! config.eval_omniauth_config &options[:providers] if options[:provider_names].count == 0 end # Populates instance variables used to display currently logged in user app.before '/*' do @omnigollum_enabled = true @user_authed = user_authed? @user = get_user end # Stop browsers from screwing up our referrer information # FIXME: This is hacky... app.before '/favicon.ico' do halt 403 unless user_authed? end # Explicit login (user followed login link) clears previous redirect info app.before options[:route_prefix] + '/login' do kick_back if user_authed? @auth_params = "?origin=#{CGI.escape(request.referrer)}" unless request.referrer.nil? user_auth end app.before options[:route_prefix] + '/logout' do user_deauth kick_back end app.before options[:route_prefix] + '/auth/failure' do user_deauth @title = 'Authentication failed' @subtext = "[#{params[:message]}] either you entered incorrect username/password, or there is some problem on the server (the former is far more likely)" @auth_params = "?origin=#{CGI.escape(request.env['omniauth.origin'])}" unless request.env['omniauth.origin'].nil? show_error end app.before options[:route_prefix] + '/auth/:name/callback' do begin if !request.env['omniauth.auth'].nil? user = Omnigollum::Models::OmniauthUser.new(request.env['omniauth.auth'], options) case (authorized_users = options[:authorized_users]) when Regexp user_authorized = (user.email =~ authorized_users) when Array user_authorized = authorized_users.include?(user.email) || authorized_users.include?(user.nickname) else user_authorized = true end # Check authorized users if !user_authorized @title = 'Authorization failed' @subtext = 'User was not found in the authorized users list' @auth_params = "?origin=#{CGI.escape(request.env['omniauth.origin'])}" unless request.env['omniauth.origin'].nil? show_error end session[:omniauth_user] = user # Update gollum's author hash, so commits are recorded correctly session['gollum.author'] = { :name => options[:author_format].call(user), :email => options[:author_email].call(user) } redirect request.env['omniauth.origin'] elsif !user_authed? @title = 'Authentication failed' @subtext = 'Omniauth experienced an error processing your request' @auth_params = "?origin=#{CGI.escape(request.env['omniauth.origin'])}" unless request.env['omniauth.origin'].nil? show_error end rescue StandardError => fail_reason @title = 'Authentication failed' @subtext = fail_reason @auth_params = "?origin=#{CGI.escape(request.env['omniauth.origin'])}" unless request.env['omniauth.origin'].nil? show_error end end app.before options[:route_prefix] + '/images/:image.png' do content_type :png send_file options[:path_images] + '/' + params[:image] + '.png' end # Stop sinatra processing and hand off to omniauth app.before options[:route_prefix] + '/auth/:provider' do halt 404 end # Pre-empt protected routes options[:protected_routes].each {|route| app.before(route) {user_auth unless user_authed?}} # Write the actual config back to the app instance app.set(:omnigollum, options) end end end