diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 8501ed37a..f9a6e8475 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -2,692 +2,14 @@ require 'exceptions' class ApplicationController < ActionController::Base - include ErrorHandling - - helper_method :current_user, - :authentication_check, - :config_frontend, - :http_log_config, - :model_create_render, - :model_update_render, - :model_restory_render, - :mode_show_rendeder, - :model_index_render - - skip_before_action :verify_authenticity_token - before_action :verify_csrf_token, :transaction_begin, :set_user, :session_update, :user_device_check, :cors_preflight_check - after_action :transaction_end, :http_log, :set_access_control_headers, :set_csrf_token_headers - - # For all responses in this controller, return the CORS access control headers. - def set_access_control_headers - return if @_auth_type != 'token_auth' && @_auth_type != 'basic_auth' - set_access_control_headers_execute - end - - def set_access_control_headers_execute - headers['Access-Control-Allow-Origin'] = '*' - headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, DELETE, PATCH, OPTIONS' - headers['Access-Control-Max-Age'] = '1728000' - headers['Access-Control-Allow-Headers'] = 'Content-Type, Depth, User-Agent, X-File-Size, X-Requested-With, If-Modified-Since, X-File-Name, Cache-Control, Accept-Language' - end - - # If this is a preflight OPTIONS request, then short-circuit the - # request, return only the necessary headers and return an empty - # text/plain. - def cors_preflight_check - return true if @_auth_type != 'token_auth' && @_auth_type != 'basic_auth' - cors_preflight_check_execute - end - - def cors_preflight_check_execute - return true if request.method != 'OPTIONS' - headers['Access-Control-Allow-Origin'] = '*' - headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, DELETE, PATCH, OPTIONS' - headers['Access-Control-Allow-Headers'] = 'Content-Type, Depth, User-Agent, X-File-Size, X-Requested-With, If-Modified-Since, X-File-Name, Cache-Control, Accept-Language' - headers['Access-Control-Max-Age'] = '1728000' - render text: '', content_type: 'text/plain' - false - end - - def set_csrf_token_headers - return true if @_auth_type.present? && @_auth_type != 'session' - headers['CSRF-TOKEN'] = form_authenticity_token - end - - def verify_csrf_token - return true if request.method != 'POST' && request.method != 'PUT' && request.method != 'DELETE' && request.method != 'PATCH' - return true if @_auth_type == 'token_auth' || @_auth_type == 'basic_auth' - return true if valid_authenticity_token?(session, params[:authenticity_token] || request.headers['X-CSRF-Token']) - logger.info 'CSRF token verification failed' - raise Exceptions::NotAuthorized, 'CSRF token verification failed!' - end - - def http_log_config(config) - @http_log_support = config - end - - private - - def transaction_begin - ApplicationHandleInfo.current = 'application_server' - PushMessages.init - end - - def transaction_end - Observer::Transaction.commit - PushMessages.finish - ActiveSupport::Dependencies::Reference.clear! - end - - # Finds the User with the ID stored in the session with the key - # :current_user_id This is a common way to handle user login in - # a Rails application; logging in sets the session value and - # logging out removes it. - def current_user - return @_current_user if @_current_user - return if !session[:user_id] - @_current_user = User.lookup(id: session[:user_id]) - end - - def current_user_set(user, auth_type = 'session') - session[:user_id] = user.id - @_auth_type = auth_type - @_current_user = user - set_user - end - - # Sets the current user into a named Thread location so that it can be accessed - # by models and observers - def set_user - if !current_user - UserInfo.current_user_id = 1 - return - end - UserInfo.current_user_id = current_user.id - end - - # update session updated_at - def session_update - #sleep 0.6 - - session[:ping] = Time.zone.now.iso8601 - - # check if remote ip need to be updated - if session[:user_id] - if !session[:remote_ip] || session[:remote_ip] != request.remote_ip - session[:remote_ip] = request.remote_ip - session[:geo] = Service::GeoIp.location(request.remote_ip) - end - end - - # fill user agent - return if session[:user_agent] - session[:user_agent] = request.env['HTTP_USER_AGENT'] - end - - # log http access - def http_log - return if !@http_log_support - - # request - request_data = { - content: '', - content_type: request.headers['Content-Type'], - content_encoding: request.headers['Content-Encoding'], - source: request.headers['User-Agent'] || request.headers['Server'], - } - request.headers.each { |key, value| - next if key[0, 5] != 'HTTP_' - request_data[:content] += if key == 'HTTP_COOKIE' - "#{key}: xxxxx\n" - else - "#{key}: #{value}\n" - end - } - body = request.body.read - if body - request_data[:content] += "\n" + body - end - request_data[:content] = request_data[:content].slice(0, 8000) - - # response - response_data = { - code: response.status = response.code, - content: '', - content_type: nil, - content_encoding: nil, - source: nil, - } - response.headers.each { |key, value| - response_data[:content] += "#{key}: #{value}\n" - } - body = response.body - if body - response_data[:content] += "\n" + body - end - response_data[:content] = response_data[:content].slice(0, 8000) - record = { - direction: 'in', - facility: @http_log_support[:facility], - url: url_for(only_path: false, overwrite_params: {}), - status: response.status, - ip: request.remote_ip, - request: request_data, - response: response_data, - method: request.method, - } - HttpLog.create(record) - end - - def user_device_check - return false if !user_device_log(current_user, 'session') - true - end - - def user_device_log(user, type) - switched_from_user_id = ENV['SWITCHED_FROM_USER_ID'] || session[:switched_from_user_id] - return true if params[:controller] == 'init' # do no device logging on static inital page - return true if switched_from_user_id - return true if !user - return true if !user.permissions?('user_preferences.device') - - time_to_check = true - user_device_updated_at = session[:user_device_updated_at] - if ENV['USER_DEVICE_UPDATED_AT'] - user_device_updated_at = Time.zone.parse(ENV['USER_DEVICE_UPDATED_AT']) - end - - if user_device_updated_at - # check if entry exists / only if write action - diff = Time.zone.now - 10.minutes - method = request.method - if method == 'GET' || method == 'OPTIONS' || method == 'HEAD' - diff = Time.zone.now - 30.minutes - end - - # only update if needed - if user_device_updated_at > diff - time_to_check = false - end - end - - # if ip has not changed and ttl in still valid - remote_ip = ENV['TEST_REMOTE_IP'] || request.remote_ip - return true if time_to_check == false && session[:user_device_remote_ip] == remote_ip - session[:user_device_remote_ip] = remote_ip - - # for sessions we need the fingperprint - if type == 'session' - if !session[:user_device_updated_at] && !params[:fingerprint] && !session[:user_device_fingerprint] - raise Exceptions::UnprocessableEntity, 'Need fingerprint param!' - end - if params[:fingerprint] - session[:user_device_fingerprint] = params[:fingerprint] - end - end - - session[:user_device_updated_at] = Time.zone.now - - # add device if needed - http_user_agent = ENV['HTTP_USER_AGENT'] || request.env['HTTP_USER_AGENT'] - Delayed::Job.enqueue( - Observer::UserDeviceLogJob.new( - http_user_agent, - remote_ip, - user.id, - session[:user_device_fingerprint], - type, - ) - ) - end - - def authentication_check_only(auth_param = {}) - #logger.debug 'authentication_check' - #logger.debug params.inspect - #logger.debug session.inspect - #logger.debug cookies.inspect - - # already logged in, early exit - if session.id && session[:user_id] - logger.debug 'session based auth check' - user = User.lookup(id: session[:user_id]) - return authentication_check_prerequesits(user, 'session', auth_param) if user - end - - # check sso based authentication - sso_user = User.sso(params) - if sso_user - if authentication_check_prerequesits(sso_user, 'session', auth_param) - session[:persistent] = true - return sso_user - end - end - - # check http basic based authentication - authenticate_with_http_basic do |username, password| - request.session_options[:skip] = true # do not send a session cookie - logger.debug "http basic auth check '#{username}'" - if Setting.get('api_password_access') == false - raise Exceptions::NotAuthorized, 'API password access disabled!' - end - user = User.authenticate(username, password) - return authentication_check_prerequesits(user, 'basic_auth', auth_param) if user - end - - # check http token based authentication - authenticate_with_http_token do |token_string, _options| - logger.debug "http token auth check '#{token_string}'" - request.session_options[:skip] = true # do not send a session cookie - if Setting.get('api_token_access') == false - raise Exceptions::NotAuthorized, 'API token access disabled!' - end - user = Token.check( - action: 'api', - name: token_string, - inactive_user: true, - ) - if user && auth_param[:permission] - user = Token.check( - action: 'api', - name: token_string, - permission: auth_param[:permission], - inactive_user: true, - ) - raise Exceptions::NotAuthorized, 'Not authorized (token)!' if !user - end - - if user - token = Token.find_by(name: token_string) - - token.last_used_at = Time.zone.now - token.save! - - if token.expires_at && - Time.zone.today >= token.expires_at - raise Exceptions::NotAuthorized, 'Not authorized (token expired)!' - end - end - - @_token_auth = token_string # remember for permission_check - return authentication_check_prerequesits(user, 'token_auth', auth_param) if user - end - - # check oauth2 token based authentication - token = Doorkeeper::OAuth::Token.from_bearer_authorization(request) - if token - request.session_options[:skip] = true # do not send a session cookie - logger.debug "oauth2 token auth check '#{token}'" - access_token = Doorkeeper::AccessToken.by_token(token) - - if !access_token - raise Exceptions::NotAuthorized, 'Invalid token!' - end - - # check expire - if access_token.expires_in && (access_token.created_at + access_token.expires_in) < Time.zone.now - raise Exceptions::NotAuthorized, 'OAuth2 token is expired!' - end - - # if access_token.scopes.empty? - # raise Exceptions::NotAuthorized, 'OAuth2 scope missing for token!' - # end - - user = User.find(access_token.resource_owner_id) - return authentication_check_prerequesits(user, 'token_auth', auth_param) if user - end - - false - end - - def authentication_check_prerequesits(user, auth_type, auth_param) - if check_maintenance_only(user) - raise Exceptions::NotAuthorized, 'Maintenance mode enabled!' - end - - if user.active == false - raise Exceptions::NotAuthorized, 'User is inactive!' - end - - # check scopes / permission check - if auth_param[:permission] && !user.permissions?(auth_param[:permission]) - raise Exceptions::NotAuthorized, 'Not authorized (user)!' - end - - current_user_set(user, auth_type) - user_device_log(user, auth_type) - logger.debug "#{auth_type} for '#{user.login}'" - true - end - - def authentication_check(auth_param = {}) - user = authentication_check_only(auth_param) - - # check if basic_auth fallback is possible - if auth_param[:basic_auth_promt] && !user - return request_http_basic_authentication - end - - # return auth not ok - if !user - raise Exceptions::NotAuthorized, 'authentication failed' - end - - # return auth ok - true - end - - def ticket_permission(ticket) - return true if ticket.permission(current_user: current_user) - raise Exceptions::NotAuthorized - end - - def article_permission(article) - ticket = Ticket.lookup(id: article.ticket_id) - return true if ticket.permission(current_user: current_user) - raise Exceptions::NotAuthorized - end - - def article_create(ticket, params) - - # create article if given - form_id = params[:form_id] - params.delete(:form_id) - - # check min. params - raise Exceptions::UnprocessableEntity, 'Need at least article: { body: "some text" }' if !params[:body] - - # fill default values - if params[:type_id].empty? && params[:type].empty? - params[:type_id] = Ticket::Article::Type.lookup(name: 'note').id - end - if params[:sender_id].empty? && params[:sender].empty? - sender = 'Customer' - if current_user.permissions?('ticket.agent') - sender = 'Agent' - end - params[:sender_id] = Ticket::Article::Sender.lookup(name: sender).id - end - - # remember time accounting - time_unit = params[:time_unit] - - clean_params = Ticket::Article.association_name_to_id_convert(params) - clean_params = Ticket::Article.param_cleanup(clean_params, true) - - # overwrite params - if !current_user.permissions?('ticket.agent') - clean_params[:sender_id] = Ticket::Article::Sender.lookup(name: 'Customer').id - clean_params.delete(:sender) - type = Ticket::Article::Type.lookup(id: clean_params[:type_id]) - if type.name !~ /^(note|web)$/ - clean_params[:type_id] = Ticket::Article::Type.lookup(name: 'note').id - end - clean_params.delete(:type) - clean_params[:internal] = false - end - - article = Ticket::Article.new(clean_params) - article.ticket_id = ticket.id - - # store dataurl images to store - if form_id && article.body && article.content_type =~ %r{text/html}i - article.body.gsub!( %r{(}i ) { |_item| - file_attributes = StaticAssets.data_url_attributes($2) - cid = "#{ticket.id}.#{form_id}.#{rand(999_999)}@#{Setting.get('fqdn')}" - headers_store = { - 'Content-Type' => file_attributes[:mime_type], - 'Mime-Type' => file_attributes[:mime_type], - 'Content-ID' => cid, - 'Content-Disposition' => 'inline', - } - store = Store.add( - object: 'UploadCache', - o_id: form_id, - data: file_attributes[:content], - filename: cid, - preferences: headers_store - ) - "#{$1}cid:#{cid}\">" - } - end - - # find attachments in upload cache - if form_id - article.attachments = Store.list( - object: 'UploadCache', - o_id: form_id, - ) - end - article.save! - - # account time - if time_unit.present? - Ticket::TimeAccounting.create!( - ticket_id: article.ticket_id, - ticket_article_id: article.id, - time_unit: time_unit - ) - end - - # remove attachments from upload cache - return article if !form_id - - Store.remove( - object: 'UploadCache', - o_id: form_id, - ) - - article - end - - def permission_check(key) - if @_token_auth - user = Token.check( - action: 'api', - name: @_token_auth, - permission: key, - ) - return false if user - raise Exceptions::NotAuthorized, 'Not authorized (token)!' - end - - return false if current_user && current_user.permissions?(key) - raise Exceptions::NotAuthorized, 'Not authorized (user)!' - end - - def valid_session_with_user - return true if current_user - raise Exceptions::UnprocessableEntity, 'No session user!' - end - - def response_access_deny - raise Exceptions::NotAuthorized - end - - def config_frontend - - # config - config = {} - Setting.select('name, preferences').where(frontend: true).each { |setting| - next if setting.preferences[:authentication] == true && !current_user - value = Setting.get(setting.name) - next if !current_user && (value == false || value.nil?) - config[setting.name] = value - } - - # remember if we can to swich back to user - if session[:switched_from_user_id] - config['switch_back_to_possible'] = true - end - - # remember session_id for websocket logon - if current_user - config['session_id'] = session.id - end - - config - end - - # model helper - def model_create_render(object, params) - - clean_params = object.association_name_to_id_convert(params) - clean_params = object.param_cleanup(clean_params, true) - - # create object - generic_object = object.new(clean_params) - - # save object - generic_object.save! - - # set relations - generic_object.associations_from_param(params) - - if params[:expand] - render json: generic_object.attributes_with_association_names, status: :created - return - end - - model_create_render_item(generic_object) - end - - def model_create_render_item(generic_object) - render json: generic_object.attributes_with_association_ids, status: :created - end - - def model_update_render(object, params) - - # find object - generic_object = object.find(params[:id]) - - clean_params = object.association_name_to_id_convert(params) - clean_params = object.param_cleanup(clean_params, true) - - generic_object.with_lock do - - # set attributes - generic_object.update_attributes!(clean_params) - - # set relations - generic_object.associations_from_param(params) - end - - if params[:expand] - render json: generic_object.attributes_with_association_names, status: :ok - return - end - - model_update_render_item(generic_object) - end - - def model_update_render_item(generic_object) - render json: generic_object.attributes_with_association_ids, status: :ok - end - - def model_destroy_render(object, params) - generic_object = object.find(params[:id]) - generic_object.destroy! - model_destroy_render_item() - end - - def model_destroy_render_item () - render json: {}, status: :ok - end - - def model_show_render(object, params) - - if params[:expand] - generic_object = object.find(params[:id]) - render json: generic_object.attributes_with_association_names, status: :ok - return - end - - if params[:full] - generic_object_full = object.full(params[:id]) - render json: generic_object_full, status: :ok - return - end - - generic_object = object.find(params[:id]) - model_show_render_item(generic_object) - end - - def model_show_render_item(generic_object) - render json: generic_object.attributes_with_association_ids, status: :ok - end - - def model_index_render(object, params) - offset = 0 - per_page = 500 - if params[:page] && params[:per_page] - offset = (params[:page].to_i - 1) * params[:per_page].to_i - limit = params[:per_page].to_i - end - - if per_page > 500 - per_page = 500 - end - - generic_objects = if offset.positive? - object.limit(params[:per_page]).order(id: 'ASC').offset(offset).limit(limit) - else - object.all.order(id: 'ASC').offset(offset).limit(limit) - end - - if params[:expand] - list = [] - generic_objects.each { |generic_object| - list.push generic_object.attributes_with_association_names - } - render json: list, status: :ok - return - end - - if params[:full] - assets = {} - item_ids = [] - generic_objects.each { |item| - item_ids.push item.id - assets = item.assets(assets) - } - render json: { - record_ids: item_ids, - assets: assets, - }, status: :ok - return - end - - generic_objects_with_associations = [] - generic_objects.each { |item| - generic_objects_with_associations.push item.attributes_with_association_ids - } - model_index_render_result(generic_objects_with_associations) - end - - def model_index_render_result(generic_objects) - render json: generic_objects, status: :ok - end - - def model_references_check(object, params) - generic_object = object.find(params[:id]) - result = Models.references(object, generic_object.id) - return false if result.empty? - raise Exceptions::UnprocessableEntity, 'Can\'t delete, object has references.' - rescue => e - raise Exceptions::UnprocessableEntity, e - end - - # check maintenance mode - def check_maintenance_only(user) - return false if Setting.get('maintenance_mode') != true - return false if user.permissions?('admin.maintenance') - Rails.logger.info "Maintenance mode enabled, denied login for user #{user.login}, it's no admin user." - true - end - - def check_maintenance(user) - return false if !check_maintenance_only(user) - raise Exceptions::NotAuthorized, 'Maintenance mode enabled!' - end - + include ApplicationController::HandlesErrors + include ApplicationController::HandlesDevices + include ApplicationController::HandlesTransitions + include ApplicationController::Authenticates + include ApplicationController::SetsHeaders + include ApplicationController::ChecksMaintainance + include ApplicationController::RendersModels + include ApplicationController::HasUser + include ApplicationController::PreventsCsrf + include ApplicationController::LogsHttpAccess end diff --git a/app/controllers/application_controller/authenticates.rb b/app/controllers/application_controller/authenticates.rb new file mode 100644 index 000000000..b77b96c97 --- /dev/null +++ b/app/controllers/application_controller/authenticates.rb @@ -0,0 +1,163 @@ +module ApplicationController::Authenticates + extend ActiveSupport::Concern + + included do + skip_before_action :verify_authenticity_token + end + + private + + def response_access_deny + raise Exceptions::NotAuthorized + end + + def permission_check(key) + if @_token_auth + user = Token.check( + action: 'api', + name: @_token_auth, + permission: key, + ) + return false if user + raise Exceptions::NotAuthorized, 'Not authorized (token)!' + end + + return false if current_user && current_user.permissions?(key) + raise Exceptions::NotAuthorized, 'Not authorized (user)!' + end + + def authentication_check(auth_param = {}) + user = authentication_check_only(auth_param) + + # check if basic_auth fallback is possible + if auth_param[:basic_auth_promt] && !user + return request_http_basic_authentication + end + + # return auth not ok + if !user + raise Exceptions::NotAuthorized, 'authentication failed' + end + + # return auth ok + true + end + + def authentication_check_only(auth_param = {}) + #logger.debug 'authentication_check' + #logger.debug params.inspect + #logger.debug session.inspect + #logger.debug cookies.inspect + + # already logged in, early exit + if session.id && session[:user_id] + logger.debug 'session based auth check' + user = User.lookup(id: session[:user_id]) + return authentication_check_prerequesits(user, 'session', auth_param) if user + end + + # check sso based authentication + sso_user = User.sso(params) + if sso_user + if authentication_check_prerequesits(sso_user, 'session', auth_param) + session[:persistent] = true + return sso_user + end + end + + # check http basic based authentication + authenticate_with_http_basic do |username, password| + request.session_options[:skip] = true # do not send a session cookie + logger.debug "http basic auth check '#{username}'" + if Setting.get('api_password_access') == false + raise Exceptions::NotAuthorized, 'API password access disabled!' + end + user = User.authenticate(username, password) + return authentication_check_prerequesits(user, 'basic_auth', auth_param) if user + end + + # check http token based authentication + authenticate_with_http_token do |token_string, _options| + logger.debug "http token auth check '#{token_string}'" + request.session_options[:skip] = true # do not send a session cookie + if Setting.get('api_token_access') == false + raise Exceptions::NotAuthorized, 'API token access disabled!' + end + user = Token.check( + action: 'api', + name: token_string, + inactive_user: true, + ) + if user && auth_param[:permission] + user = Token.check( + action: 'api', + name: token_string, + permission: auth_param[:permission], + inactive_user: true, + ) + raise Exceptions::NotAuthorized, 'Not authorized (token)!' if !user + end + + if user + token = Token.find_by(name: token_string) + + token.last_used_at = Time.zone.now + token.save! + + if token.expires_at && + Time.zone.today >= token.expires_at + raise Exceptions::NotAuthorized, 'Not authorized (token expired)!' + end + end + + @_token_auth = token_string # remember for permission_check + return authentication_check_prerequesits(user, 'token_auth', auth_param) if user + end + + # check oauth2 token based authentication + token = Doorkeeper::OAuth::Token.from_bearer_authorization(request) + if token + request.session_options[:skip] = true # do not send a session cookie + logger.debug "oauth2 token auth check '#{token}'" + access_token = Doorkeeper::AccessToken.by_token(token) + + if !access_token + raise Exceptions::NotAuthorized, 'Invalid token!' + end + + # check expire + if access_token.expires_in && (access_token.created_at + access_token.expires_in) < Time.zone.now + raise Exceptions::NotAuthorized, 'OAuth2 token is expired!' + end + + # if access_token.scopes.empty? + # raise Exceptions::NotAuthorized, 'OAuth2 scope missing for token!' + # end + + user = User.find(access_token.resource_owner_id) + return authentication_check_prerequesits(user, 'token_auth', auth_param) if user + end + + false + end + + def authentication_check_prerequesits(user, auth_type, auth_param) + if check_maintenance_only(user) + raise Exceptions::NotAuthorized, 'Maintenance mode enabled!' + end + + if user.active == false + raise Exceptions::NotAuthorized, 'User is inactive!' + end + + # check scopes / permission check + if auth_param[:permission] && !user.permissions?(auth_param[:permission]) + raise Exceptions::NotAuthorized, 'Not authorized (user)!' + end + + current_user_set(user, auth_type) + user_device_log(user, auth_type) + logger.debug "#{auth_type} for '#{user.login}'" + true + end +end diff --git a/app/controllers/application_controller/checks_maintainance.rb b/app/controllers/application_controller/checks_maintainance.rb new file mode 100644 index 000000000..591a1f30e --- /dev/null +++ b/app/controllers/application_controller/checks_maintainance.rb @@ -0,0 +1,18 @@ +module ApplicationController::ChecksMaintainance + extend ActiveSupport::Concern + + private + + def check_maintenance(user) + return false if !check_maintenance_only(user) + raise Exceptions::NotAuthorized, 'Maintenance mode enabled!' + end + + # check maintenance mode + def check_maintenance_only(user) + return false if Setting.get('maintenance_mode') != true + return false if user.permissions?('admin.maintenance') + Rails.logger.info "Maintenance mode enabled, denied login for user #{user.login}, it's no admin user." + true + end +end diff --git a/app/controllers/application_controller/handles_devices.rb b/app/controllers/application_controller/handles_devices.rb new file mode 100644 index 000000000..9bead8f45 --- /dev/null +++ b/app/controllers/application_controller/handles_devices.rb @@ -0,0 +1,69 @@ +module ApplicationController::HandlesDevices + extend ActiveSupport::Concern + + included do + before_action :user_device_check + end + + def user_device_check + return false if !user_device_log(current_user, 'session') + true + end + + def user_device_log(user, type) + switched_from_user_id = ENV['SWITCHED_FROM_USER_ID'] || session[:switched_from_user_id] + return true if params[:controller] == 'init' # do no device logging on static inital page + return true if switched_from_user_id + return true if !user + return true if !user.permissions?('user_preferences.device') + + time_to_check = true + user_device_updated_at = session[:user_device_updated_at] + if ENV['USER_DEVICE_UPDATED_AT'] + user_device_updated_at = Time.zone.parse(ENV['USER_DEVICE_UPDATED_AT']) + end + + if user_device_updated_at + # check if entry exists / only if write action + diff = Time.zone.now - 10.minutes + method = request.method + if method == 'GET' || method == 'OPTIONS' || method == 'HEAD' + diff = Time.zone.now - 30.minutes + end + + # only update if needed + if user_device_updated_at > diff + time_to_check = false + end + end + + # if ip has not changed and ttl in still valid + remote_ip = ENV['TEST_REMOTE_IP'] || request.remote_ip + return true if time_to_check == false && session[:user_device_remote_ip] == remote_ip + session[:user_device_remote_ip] = remote_ip + + # for sessions we need the fingperprint + if type == 'session' + if !session[:user_device_updated_at] && !params[:fingerprint] && !session[:user_device_fingerprint] + raise Exceptions::UnprocessableEntity, 'Need fingerprint param!' + end + if params[:fingerprint] + session[:user_device_fingerprint] = params[:fingerprint] + end + end + + session[:user_device_updated_at] = Time.zone.now + + # add device if needed + http_user_agent = ENV['HTTP_USER_AGENT'] || request.env['HTTP_USER_AGENT'] + Delayed::Job.enqueue( + Observer::UserDeviceLogJob.new( + http_user_agent, + remote_ip, + user.id, + session[:user_device_fingerprint], + type, + ) + ) + end +end diff --git a/app/controllers/concerns/error_handling.rb b/app/controllers/application_controller/handles_errors.rb similarity index 98% rename from app/controllers/concerns/error_handling.rb rename to app/controllers/application_controller/handles_errors.rb index 020f0593c..14fc510ae 100644 --- a/app/controllers/concerns/error_handling.rb +++ b/app/controllers/application_controller/handles_errors.rb @@ -1,4 +1,4 @@ -module ErrorHandling +module ApplicationController::HandlesErrors extend ActiveSupport::Concern included do diff --git a/app/controllers/application_controller/handles_transitions.rb b/app/controllers/application_controller/handles_transitions.rb new file mode 100644 index 000000000..d708af9ba --- /dev/null +++ b/app/controllers/application_controller/handles_transitions.rb @@ -0,0 +1,21 @@ +module ApplicationController::HandlesTransitions + extend ActiveSupport::Concern + + included do + before_action :transaction_begin + after_action :transaction_end + end + + private + + def transaction_begin + ApplicationHandleInfo.current = 'application_server' + PushMessages.init + end + + def transaction_end + Observer::Transaction.commit + PushMessages.finish + ActiveSupport::Dependencies::Reference.clear! + end +end diff --git a/app/controllers/application_controller/has_user.rb b/app/controllers/application_controller/has_user.rb new file mode 100644 index 000000000..8658e5ec5 --- /dev/null +++ b/app/controllers/application_controller/has_user.rb @@ -0,0 +1,60 @@ +module ApplicationController::HasUser + extend ActiveSupport::Concern + + included do + before_action :set_user, :session_update + end + + private + + # Finds the User with the ID stored in the session with the key + # :current_user_id This is a common way to handle user login in + # a Rails application; logging in sets the session value and + # logging out removes it. + def current_user + return @_current_user if @_current_user + return if !session[:user_id] + @_current_user = User.lookup(id: session[:user_id]) + end + + def current_user_set(user, auth_type = 'session') + session[:user_id] = user.id + @_auth_type = auth_type + @_current_user = user + set_user + end + + # Sets the current user into a named Thread location so that it can be accessed + # by models and observers + def set_user + if !current_user + UserInfo.current_user_id = 1 + return + end + UserInfo.current_user_id = current_user.id + end + + # update session updated_at + def session_update + #sleep 0.6 + + session[:ping] = Time.zone.now.iso8601 + + # check if remote ip need to be updated + if session[:user_id] + if !session[:remote_ip] || session[:remote_ip] != request.remote_ip + session[:remote_ip] = request.remote_ip + session[:geo] = Service::GeoIp.location(request.remote_ip) + end + end + + # fill user agent + return if session[:user_agent] + session[:user_agent] = request.env['HTTP_USER_AGENT'] + end + + def valid_session_with_user + return true if current_user + raise Exceptions::UnprocessableEntity, 'No session user!' + end +end diff --git a/app/controllers/application_controller/logs_http_access.rb b/app/controllers/application_controller/logs_http_access.rb new file mode 100644 index 000000000..ef49c2f3c --- /dev/null +++ b/app/controllers/application_controller/logs_http_access.rb @@ -0,0 +1,67 @@ +module ApplicationController::LogsHttpAccess + extend ActiveSupport::Concern + + included do + after_action :http_log + end + + private + + def http_log_config(config) + @http_log_support = config + end + + # log http access + def http_log + return if !@http_log_support + + # request + request_data = { + content: '', + content_type: request.headers['Content-Type'], + content_encoding: request.headers['Content-Encoding'], + source: request.headers['User-Agent'] || request.headers['Server'], + } + request.headers.each { |key, value| + next if key[0, 5] != 'HTTP_' + request_data[:content] += if key == 'HTTP_COOKIE' + "#{key}: xxxxx\n" + else + "#{key}: #{value}\n" + end + } + body = request.body.read + if body + request_data[:content] += "\n" + body + end + request_data[:content] = request_data[:content].slice(0, 8000) + + # response + response_data = { + code: response.status = response.code, + content: '', + content_type: nil, + content_encoding: nil, + source: nil, + } + response.headers.each { |key, value| + response_data[:content] += "#{key}: #{value}\n" + } + body = response.body + if body + response_data[:content] += "\n" + body + end + response_data[:content] = response_data[:content].slice(0, 8000) + record = { + direction: 'in', + facility: @http_log_support[:facility], + url: url_for(only_path: false, overwrite_params: {}), + status: response.status, + ip: request.remote_ip, + request: request_data, + response: response_data, + method: request.method, + } + HttpLog.create(record) + end +end diff --git a/app/controllers/application_controller/prevents_csrf.rb b/app/controllers/application_controller/prevents_csrf.rb new file mode 100644 index 000000000..ba7450fac --- /dev/null +++ b/app/controllers/application_controller/prevents_csrf.rb @@ -0,0 +1,23 @@ +module ApplicationController::PreventsCsrf + extend ActiveSupport::Concern + + included do + before_action :verify_csrf_token + after_action :set_csrf_token_headers + end + + private + + def set_csrf_token_headers + return true if @_auth_type.present? && @_auth_type != 'session' + headers['CSRF-TOKEN'] = form_authenticity_token + end + + def verify_csrf_token + return true if request.method != 'POST' && request.method != 'PUT' && request.method != 'DELETE' && request.method != 'PATCH' + return true if @_auth_type == 'token_auth' || @_auth_type == 'basic_auth' + return true if valid_authenticity_token?(session, params[:authenticity_token] || request.headers['X-CSRF-Token']) + logger.info 'CSRF token verification failed' + raise Exceptions::NotAuthorized, 'CSRF token verification failed!' + end +end diff --git a/app/controllers/application_controller/renders_models.rb b/app/controllers/application_controller/renders_models.rb new file mode 100644 index 000000000..d58932667 --- /dev/null +++ b/app/controllers/application_controller/renders_models.rb @@ -0,0 +1,154 @@ +module ApplicationController::RendersModels + extend ActiveSupport::Concern + + private + + # model helper + def model_create_render(object, params) + + clean_params = object.association_name_to_id_convert(params) + clean_params = object.param_cleanup(clean_params, true) + + # create object + generic_object = object.new(clean_params) + + # save object + generic_object.save! + + # set relations + generic_object.associations_from_param(params) + + if params[:expand] + render json: generic_object.attributes_with_association_names, status: :created + return + end + + model_create_render_item(generic_object) + end + + def model_create_render_item(generic_object) + render json: generic_object.attributes_with_association_ids, status: :created + end + + def model_update_render(object, params) + + # find object + generic_object = object.find(params[:id]) + + clean_params = object.association_name_to_id_convert(params) + clean_params = object.param_cleanup(clean_params, true) + + generic_object.with_lock do + + # set attributes + generic_object.update_attributes!(clean_params) + + # set relations + generic_object.associations_from_param(params) + end + + if params[:expand] + render json: generic_object.attributes_with_association_names, status: :ok + return + end + + model_update_render_item(generic_object) + end + + def model_update_render_item(generic_object) + render json: generic_object.attributes_with_association_ids, status: :ok + end + + def model_destroy_render(object, params) + generic_object = object.find(params[:id]) + generic_object.destroy! + model_destroy_render_item() + end + + def model_destroy_render_item () + render json: {}, status: :ok + end + + def model_show_render(object, params) + + if params[:expand] + generic_object = object.find(params[:id]) + render json: generic_object.attributes_with_association_names, status: :ok + return + end + + if params[:full] + generic_object_full = object.full(params[:id]) + render json: generic_object_full, status: :ok + return + end + + generic_object = object.find(params[:id]) + model_show_render_item(generic_object) + end + + def model_show_render_item(generic_object) + render json: generic_object.attributes_with_association_ids, status: :ok + end + + def model_index_render(object, params) + offset = 0 + per_page = 500 + if params[:page] && params[:per_page] + offset = (params[:page].to_i - 1) * params[:per_page].to_i + limit = params[:per_page].to_i + end + + if per_page > 500 + per_page = 500 + end + + generic_objects = if offset.positive? + object.limit(params[:per_page]).order(id: 'ASC').offset(offset).limit(limit) + else + object.all.order(id: 'ASC').offset(offset).limit(limit) + end + + if params[:expand] + list = [] + generic_objects.each { |generic_object| + list.push generic_object.attributes_with_association_names + } + render json: list, status: :ok + return + end + + if params[:full] + assets = {} + item_ids = [] + generic_objects.each { |item| + item_ids.push item.id + assets = item.assets(assets) + } + render json: { + record_ids: item_ids, + assets: assets, + }, status: :ok + return + end + + generic_objects_with_associations = [] + generic_objects.each { |item| + generic_objects_with_associations.push item.attributes_with_association_ids + } + model_index_render_result(generic_objects_with_associations) + end + + def model_index_render_result(generic_objects) + render json: generic_objects, status: :ok + end + + def model_references_check(object, params) + generic_object = object.find(params[:id]) + result = Models.references(object, generic_object.id) + return false if result.empty? + raise Exceptions::UnprocessableEntity, 'Can\'t delete, object has references.' + rescue => e + raise Exceptions::UnprocessableEntity, e + end +end diff --git a/app/controllers/application_controller/sets_headers.rb b/app/controllers/application_controller/sets_headers.rb new file mode 100644 index 000000000..d99c9edc9 --- /dev/null +++ b/app/controllers/application_controller/sets_headers.rb @@ -0,0 +1,41 @@ +module ApplicationController::SetsHeaders + extend ActiveSupport::Concern + + included do + before_action :cors_preflight_check + after_action :set_access_control_headers + end + + private + + # For all responses in this controller, return the CORS access control headers. + def set_access_control_headers + return if @_auth_type != 'token_auth' && @_auth_type != 'basic_auth' + set_access_control_headers_execute + end + + def set_access_control_headers_execute + headers['Access-Control-Allow-Origin'] = '*' + headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, DELETE, PATCH, OPTIONS' + headers['Access-Control-Max-Age'] = '1728000' + headers['Access-Control-Allow-Headers'] = 'Content-Type, Depth, User-Agent, X-File-Size, X-Requested-With, If-Modified-Since, X-File-Name, Cache-Control, Accept-Language' + end + + # If this is a preflight OPTIONS request, then short-circuit the + # request, return only the necessary headers and return an empty + # text/plain. + def cors_preflight_check + return true if @_auth_type != 'token_auth' && @_auth_type != 'basic_auth' + cors_preflight_check_execute + end + + def cors_preflight_check_execute + return true if request.method != 'OPTIONS' + headers['Access-Control-Allow-Origin'] = '*' + headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, DELETE, PATCH, OPTIONS' + headers['Access-Control-Allow-Headers'] = 'Content-Type, Depth, User-Agent, X-File-Size, X-Requested-With, If-Modified-Since, X-File-Name, Cache-Control, Accept-Language' + headers['Access-Control-Max-Age'] = '1728000' + render text: '', content_type: 'text/plain' + false + end +end diff --git a/app/controllers/concerns/accesses_tickets.rb b/app/controllers/concerns/accesses_tickets.rb new file mode 100644 index 000000000..d8cd734ad --- /dev/null +++ b/app/controllers/concerns/accesses_tickets.rb @@ -0,0 +1,10 @@ +module AccessesTickets + extend ActiveSupport::Concern + + private + + def ticket_permission(ticket) + return true if ticket.permission(current_user: current_user) + raise Exceptions::NotAuthorized + end +end diff --git a/app/controllers/concerns/creates_ticket_articles.rb b/app/controllers/concerns/creates_ticket_articles.rb new file mode 100644 index 000000000..77e95166b --- /dev/null +++ b/app/controllers/concerns/creates_ticket_articles.rb @@ -0,0 +1,98 @@ +module CreatesTicketArticles + extend ActiveSupport::Concern + + private + + def article_create(ticket, params) + + # create article if given + form_id = params[:form_id] + params.delete(:form_id) + + # check min. params + raise Exceptions::UnprocessableEntity, 'Need at least article: { body: "some text" }' if !params[:body] + + # fill default values + if params[:type_id].empty? && params[:type].empty? + params[:type_id] = Ticket::Article::Type.lookup(name: 'note').id + end + if params[:sender_id].empty? && params[:sender].empty? + sender = 'Customer' + if current_user.permissions?('ticket.agent') + sender = 'Agent' + end + params[:sender_id] = Ticket::Article::Sender.lookup(name: sender).id + end + + # remember time accounting + time_unit = params[:time_unit] + + clean_params = Ticket::Article.association_name_to_id_convert(params) + clean_params = Ticket::Article.param_cleanup(clean_params, true) + + # overwrite params + if !current_user.permissions?('ticket.agent') + clean_params[:sender_id] = Ticket::Article::Sender.lookup(name: 'Customer').id + clean_params.delete(:sender) + type = Ticket::Article::Type.lookup(id: clean_params[:type_id]) + if type.name !~ /^(note|web)$/ + clean_params[:type_id] = Ticket::Article::Type.lookup(name: 'note').id + end + clean_params.delete(:type) + clean_params[:internal] = false + end + + article = Ticket::Article.new(clean_params) + article.ticket_id = ticket.id + + # store dataurl images to store + if form_id && article.body && article.content_type =~ %r{text/html}i + article.body.gsub!( %r{(}i ) { |_item| + file_attributes = StaticAssets.data_url_attributes($2) + cid = "#{ticket.id}.#{form_id}.#{rand(999_999)}@#{Setting.get('fqdn')}" + headers_store = { + 'Content-Type' => file_attributes[:mime_type], + 'Mime-Type' => file_attributes[:mime_type], + 'Content-ID' => cid, + 'Content-Disposition' => 'inline', + } + store = Store.add( + object: 'UploadCache', + o_id: form_id, + data: file_attributes[:content], + filename: cid, + preferences: headers_store + ) + "#{$1}cid:#{cid}\">" + } + end + + # find attachments in upload cache + if form_id + article.attachments = Store.list( + object: 'UploadCache', + o_id: form_id, + ) + end + article.save! + + # account time + if time_unit.present? + Ticket::TimeAccounting.create!( + ticket_id: article.ticket_id, + ticket_article_id: article.id, + time_unit: time_unit + ) + end + + # remove attachments from upload cache + return article if !form_id + + Store.remove( + object: 'UploadCache', + o_id: form_id, + ) + + article + end +end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 5c4c88765..a7c918d5a 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -301,4 +301,29 @@ class SessionsController < ApplicationController render json: {} end + private + + def config_frontend + + # config + config = {} + Setting.select('name, preferences').where(frontend: true).each { |setting| + next if setting.preferences[:authentication] == true && !current_user + value = Setting.get(setting.name) + next if !current_user && (value == false || value.nil?) + config[setting.name] = value + } + + # remember if we can to swich back to user + if session[:switched_from_user_id] + config['switch_back_to_possible'] = true + end + + # remember session_id for websocket logon + if current_user + config['session_id'] = session.id + end + + config + end end diff --git a/app/controllers/ticket_articles_controller.rb b/app/controllers/ticket_articles_controller.rb index 37e967a62..490b0054f 100644 --- a/app/controllers/ticket_articles_controller.rb +++ b/app/controllers/ticket_articles_controller.rb @@ -1,6 +1,9 @@ # Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ class TicketArticlesController < ApplicationController + include AccessesTickets + include CreatesTicketArticles + prepend_before_action :authentication_check # GET /articles @@ -272,6 +275,12 @@ class TicketArticlesController < ApplicationController private + def article_permission(article) + ticket = Ticket.lookup(id: article.ticket_id) + return true if ticket.permission(current_user: current_user) + raise Exceptions::NotAuthorized + end + def sanitized_disposition disposition = params.fetch(:disposition, 'inline') valid_disposition = %w(inline attachment) diff --git a/app/controllers/tickets_controller.rb b/app/controllers/tickets_controller.rb index 1fb239e73..740136ea2 100644 --- a/app/controllers/tickets_controller.rb +++ b/app/controllers/tickets_controller.rb @@ -1,6 +1,9 @@ # Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ class TicketsController < ApplicationController + include AccessesTickets + include CreatesTicketArticles + prepend_before_action :authentication_check # GET /api/v1/tickets