proxes 0.9.8 → 0.9.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/lib/ditty/components/proxes.rb +89 -0
  3. data/lib/proxes/controllers/permissions.rb +41 -0
  4. data/lib/proxes/controllers/search.rb +55 -0
  5. data/lib/proxes/controllers/status.rb +115 -0
  6. data/lib/proxes/forwarder.rb +49 -0
  7. data/lib/proxes/helpers/indices.rb +33 -0
  8. data/lib/proxes/loggers/elasticsearch.rb +10 -0
  9. data/lib/proxes/middleware/error_handling.rb +64 -0
  10. data/lib/proxes/middleware/metrics.rb +25 -0
  11. data/lib/proxes/middleware/security.rb +59 -0
  12. data/lib/proxes/models/permission.rb +55 -0
  13. data/lib/proxes/policies/permission_policy.rb +37 -0
  14. data/lib/proxes/policies/request/bulk_policy.rb +24 -0
  15. data/lib/proxes/policies/request/cat_policy.rb +12 -0
  16. data/lib/proxes/policies/request/create_policy.rb +15 -0
  17. data/lib/proxes/policies/request/index_policy.rb +19 -0
  18. data/lib/proxes/policies/request/root_policy.rb +13 -0
  19. data/lib/proxes/policies/request/search_policy.rb +14 -0
  20. data/lib/proxes/policies/request/snapshot_policy.rb +15 -0
  21. data/lib/proxes/policies/request/stats_policy.rb +12 -0
  22. data/lib/proxes/policies/request_policy.rb +62 -0
  23. data/lib/proxes/policies/search_policy.rb +29 -0
  24. data/lib/proxes/policies/status_policy.rb +21 -0
  25. data/lib/proxes/request.rb +84 -0
  26. data/lib/proxes/request/bulk.rb +40 -0
  27. data/lib/proxes/request/cat.rb +32 -0
  28. data/lib/proxes/request/create.rb +33 -0
  29. data/lib/proxes/request/index.rb +33 -0
  30. data/lib/proxes/request/root.rb +11 -0
  31. data/lib/proxes/request/search.rb +37 -0
  32. data/lib/proxes/request/snapshot.rb +17 -0
  33. data/lib/proxes/request/stats.rb +35 -0
  34. data/lib/proxes/services/es.rb +34 -0
  35. data/lib/proxes/services/listener.rb +29 -0
  36. data/lib/proxes/services/search.rb +45 -0
  37. data/lib/proxes/version.rb +5 -0
  38. data/migrate/20170209_permissions.rb +13 -0
  39. data/migrate/20170416_user_specific_permissions.rb +9 -0
  40. data/public/browserconfig.xml +9 -0
  41. data/public/manifest.json +25 -0
  42. data/views/index.haml +1 -0
  43. data/views/layout.haml +60 -0
  44. metadata +44 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 19cf2985a0f5c1cc1e76a9e0acd7319ed6e600a8f43b0fe34ddb99ddad7a47f7
4
- data.tar.gz: ae63df70c4d596af1fd8ec87656ffbc05e35b433c0719ac1ae7497978f30bc17
3
+ metadata.gz: 9f06c68171f233ccce5d7cb06ab76f6503254ae8159c8906a53170af8e6e4b78
4
+ data.tar.gz: c4022844adcbf0d5740fe3b78f841a2be8c0db6b6d182a9d383f79ceb6d28299
5
5
  SHA512:
6
- metadata.gz: 57f5d54d14b06f89f2067a0f6ca18e0dc08307a18d156d7748569a1b9ef1b1feef54e4650218957555b7c028491899fdc1be2874d8317b2850c39824a3313634
7
- data.tar.gz: 7089ca97c7dd821183d82248c6820bb59d5c76b9d61f547855a1930548bb6ba13407690b74f799f13311ea7d53c1088b92b26832f2b7b33ddcf92feb78514912
6
+ metadata.gz: 9b7d4ad313c188b7dd2e9db490870828bf6b6c2590c15349ccc58d167bc29c6f1e455e3706a6202cb78d072a1b105308c0c9004cb239d06cefb4fe0ae951e7cf
7
+ data.tar.gz: 3ed39b26565420d629984d4e932bce2b239369554b84d0daf2744cca469a65ec634ee17f514790b11d8a5a4d7833d23b15e55519051fdec046371d9b0e49524d
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ditty'
4
+
5
+ module Ditty
6
+ class ProxES
7
+ def self.load
8
+ controllers = File.expand_path('../../proxes/controllers', __dir__)
9
+ Dir.glob("#{controllers}/*.rb").each { |f| require f }
10
+ require 'proxes/models/permission'
11
+ require 'proxes/services/listener'
12
+ end
13
+
14
+ def self.migrations
15
+ File.expand_path('../../../migrate', __dir__)
16
+ end
17
+
18
+ def self.view_folder
19
+ File.expand_path('../../../views', __dir__)
20
+ end
21
+
22
+ def self.public_folder
23
+ File.expand_path('../../../public', __dir__)
24
+ end
25
+
26
+ def self.routes
27
+ load
28
+ {
29
+ '/search' => ::ProxES::Search,
30
+ '/status' => ::ProxES::Status,
31
+ '/permissions' => ::ProxES::Permissions
32
+ }
33
+ end
34
+
35
+ def self.navigation
36
+ load
37
+ [
38
+ { order: 0, link: '/status/check', text: 'Status Check', target: ::ProxES::Status, icon: 'dashboard' },
39
+ { order: 1, link: '/search', text: 'Search', target: ::ProxES::Status, icon: 'search' },
40
+ { order: 15, link: '/permissions', text: 'Permissions', target: ::ProxES::Permission, icon: 'check-square' }
41
+ ]
42
+ end
43
+
44
+ def self.seeder
45
+ proc do
46
+ require 'ditty/models/user'
47
+ require 'ditty/models/role'
48
+ require 'proxes/models/permission'
49
+
50
+ sa = ::Ditty::Role.find_or_create(name: 'super_admin')
51
+ %w[GET POST PUT DELETE HEAD OPTIONS INDEX].each do |verb|
52
+ ::ProxES::Permission.find_or_create(role: sa, verb: verb, pattern: '.*')
53
+ end
54
+
55
+ # Admin Role
56
+ ::Ditty::Role.find_or_create(name: 'admin')
57
+
58
+ # User Role
59
+ user_role = ::Ditty::Role.find_or_create(name: 'user')
60
+ ::ProxES::Permission.find_or_create(role: user_role, verb: 'GET', pattern: '/_cluster/stats')
61
+ ::ProxES::Permission.find_or_create(role: user_role, verb: 'GET', pattern: '/_nodes')
62
+ ::ProxES::Permission.find_or_create(role: user_role, verb: 'GET', pattern: '/_nodes/stats')
63
+ ::ProxES::Permission.find_or_create(role: user_role, verb: 'GET', pattern: '/_stats')
64
+ ::ProxES::Permission.find_or_create(role: user_role, verb: 'INDEX', pattern: 'user-{user.id}')
65
+
66
+ # Kibana Specific
67
+ anon = ::Ditty::User.find_or_create(email: 'anonymous@proxes.io')
68
+ anon.remove_role user_role
69
+ anon_role = ::Ditty::Role.find_or_create(name: 'anonymous')
70
+ anon.add_role anon_role unless anon.role?('anonymous')
71
+ ::ProxES::Permission.find_or_create(role: anon_role, verb: 'GET', pattern: '/.kibana/config/*')
72
+ ::ProxES::Permission.find_or_create(role: anon_role, verb: 'INDEX', pattern: '.kibana')
73
+
74
+ kibana = ::Ditty::Role.find_or_create(name: 'kibana')
75
+ ::ProxES::Permission.find_or_create(role: kibana, verb: 'INDEX', pattern: '.kibana')
76
+ ::ProxES::Permission.find_or_create(role: kibana, verb: 'HEAD', pattern: '/')
77
+ ::ProxES::Permission.find_or_create(role: kibana, verb: 'GET', pattern: '/_nodes*')
78
+ ::ProxES::Permission.find_or_create(role: kibana, verb: 'GET', pattern: '/_cluster/health*')
79
+ ::ProxES::Permission.find_or_create(role: kibana, verb: 'GET', pattern: '/_cluster/settings*')
80
+ ::ProxES::Permission.find_or_create(role: kibana, verb: 'POST', pattern: '/_mget')
81
+ ::ProxES::Permission.find_or_create(role: kibana, verb: 'POST', pattern: '/_search')
82
+ ::ProxES::Permission.find_or_create(role: kibana, verb: 'POST', pattern: '/_msearch')
83
+ ::ProxES::Permission.find_or_create(role: kibana, verb: 'POST', pattern: '/_refresh')
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ Ditty::Components.register_component(:proxes, Ditty::ProxES)
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ditty/controllers/component'
4
+ require 'proxes/models/permission'
5
+ require 'ditty/policies/user_policy'
6
+ require 'ditty/policies/role_policy'
7
+ require 'proxes/policies/permission_policy'
8
+
9
+ module ProxES
10
+ class Permissions < Ditty::Component
11
+ set model_class: Permission
12
+
13
+ FILTERS = [
14
+ { name: :user, field: 'user.email' },
15
+ { name: :role, field: 'role.name' },
16
+ { name: :verb }
17
+ ].freeze
18
+
19
+ SEARCHABLE = %i[pattern].freeze
20
+
21
+ helpers do
22
+ def user_options
23
+ policy_scope(::Ditty::User).as_hash(:email, :email)
24
+ end
25
+
26
+ def role_options
27
+ policy_scope(::Ditty::Role).as_hash(:name, :name)
28
+ end
29
+
30
+ def verb_options
31
+ ProxES::Permission.verbs
32
+ end
33
+ end
34
+
35
+ def find_template(views, name, engine, &block)
36
+ super(views, name, engine, &block) # Root
37
+ super(::Ditty::ProxES.view_folder, name, engine, &block) # This Component
38
+ super(::Ditty::App.view_folder, name, engine, &block) # Ditty
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/object/blank'
4
+ require 'ditty/controllers/application'
5
+ require 'proxes/services/search'
6
+ require 'proxes/policies/search_policy'
7
+
8
+ module ProxES
9
+ class Search < Ditty::Application
10
+ set base_path: "#{settings.map_path}/search"
11
+
12
+ get '/' do
13
+ authorize self, :search
14
+
15
+ param :page, Integer, min: 0, default: 1
16
+ param :count, Integer, min: 0, default: 25
17
+ from = ((params[:page] - 1) * params[:count])
18
+ params[:q] = '*' if params[:q].blank?
19
+ result = ProxES::Services::Search.search(params[:q], index: params[:indices], from: from, size: params[:count])
20
+ haml :"#{view_location}/index",
21
+ locals: {
22
+ title: 'Search',
23
+ indices: ProxES::Services::Search.indices,
24
+ fields: ProxES::Services::Search.fields(index: params[:indices], names_only: true),
25
+ result: result
26
+ }
27
+ end
28
+
29
+ get '/fields/?:indices?/?' do
30
+ authorize self, :fields
31
+
32
+ param :names_only, Boolean, default: false
33
+ json ProxES::Services::Search.fields index: params[:indices], names_only: params[:names_only]
34
+ end
35
+
36
+ get '/indices/?' do
37
+ authorize self, :indices
38
+
39
+ json ProxES::Services::Search.indices
40
+ end
41
+
42
+ get '/values/:field/?:indices?/?' do |field|
43
+ authorize self, :values
44
+
45
+ param :size, Integer, min: 0, default: 25
46
+ json ProxES::Services::Search.values(field, size: params[:size], index: params[:indices])
47
+ end
48
+
49
+ def find_template(views, name, engine, &block)
50
+ super(views, name, engine, &block) # Root
51
+ super(::Ditty::ProxES.view_folder, name, engine, &block) # This Component
52
+ super(::Ditty::App.view_folder, name, engine, &block) # Ditty
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ditty/controllers/application'
4
+ require 'proxes/policies/status_policy'
5
+ require 'proxes/services/es'
6
+
7
+ module ProxES
8
+ class Status < Ditty::Application
9
+ helpers ProxES::Services::ES
10
+
11
+ def find_template(views, name, engine, &block)
12
+ super(views, name, engine, &block) # Root
13
+ super(::Ditty::ProxES.view_folder, name, engine, &block) # This Component
14
+ super(::Ditty::App.view_folder, name, engine, &block) # Ditty
15
+ end
16
+
17
+ # This provides a URL that can be polled by a monitoring system. It will return
18
+ # 200 OK if all the checks pass, or 500 if any of the checks fail.
19
+ get '/check' do
20
+ checks = []
21
+ begin
22
+ health = client.cluster.health level: 'cluster'
23
+ checks << { text: 'Cluster Reachable', passed: true, value: health['cluster_name'] }
24
+ checks << { text: 'Cluster Health', passed: health['status'] == 'green', value: health['status'] }
25
+
26
+ node_stats = client.nodes.stats
27
+
28
+ master_nodes = []
29
+ data_nodes = []
30
+ ingestion_nodes = []
31
+ node_stats['nodes'].each_value do |node|
32
+ if node['roles']
33
+ master_nodes << node['name'] if node['roles'].include? 'master'
34
+ data_nodes << node['name'] if node['roles'].include? 'data'
35
+ ingestion_nodes << node['name'] if node['roles'].include? 'ingest'
36
+ elsif node['attributes']
37
+ master_nodes << node['name'] unless node['attributes']['master'] == 'false'
38
+ data_nodes << node['name'] unless node['attributes']['data'] == 'false'
39
+ ingestion_nodes << node['name'] unless node['attributes']['ingest'] == 'false'
40
+ elsif node['settings']
41
+ master_nodes << node['name'] unless node['settings']['node']['master'] == 'false'
42
+ data_nodes << node['name'] unless node['settings']['node']['data'] == 'false'
43
+ ingestion_nodes << node['name'] unless node['settings']['node']['ingest'] == 'false'
44
+ end
45
+ end
46
+ checks << {
47
+ text: 'Master Nodes',
48
+ passed: master_nodes.count > 0,
49
+ value: master_nodes.count > 0 ? master_nodes.sort : 'None'
50
+ }
51
+ checks << {
52
+ text: 'Data Nodes',
53
+ passed: data_nodes.count > 0,
54
+ value: data_nodes.count > 0 ? data_nodes.sort : 'None'
55
+ }
56
+ checks << {
57
+ text: 'Ingestion Nodes',
58
+ passed: true,
59
+ value: ingestion_nodes.count > 0 ? ingestion_nodes.sort : 'None'
60
+ }
61
+
62
+ jvm_values = []
63
+ jvm_passed = true
64
+ node_stats['nodes'].each_value do |node|
65
+ jvm_values << "#{node['name']}: #{node['jvm']['mem']['heap_used_percent']}%"
66
+ jvm_passed = false if node['jvm']['mem']['heap_used_percent'] > 85
67
+ end
68
+ checks << { text: 'Node JVM Heap', passed: jvm_passed, value: jvm_values.sort }
69
+
70
+ fs_values = []
71
+ fs_passed = true
72
+ node_stats['nodes'].each_value do |node|
73
+ next if node['attributes'] && node['attributes']['data'] == 'false'
74
+ next if node['roles'] && node['roles'].include?('data') == false
75
+ stats = node['fs']['total']
76
+ left = stats['available_in_bytes'] / stats['total_in_bytes'].to_f * 100
77
+ fs_values << "#{node['name']}: #{format('%.02f', left)}% Free"
78
+ fs_passed = false if left < 10
79
+ end
80
+ checks << { text: 'Node File Systems', passed: fs_passed, value: fs_values.sort }
81
+
82
+ cpu_values = []
83
+ cpu_passed = true
84
+ node_stats['nodes'].each_value do |node|
85
+ value = (node['os']['cpu_percent'] || node['os']['cpu']['percent'])
86
+ cpu_values << "#{node['name']}: #{value}"
87
+ cpu_passed = false if value.to_i > 70
88
+ end
89
+ checks << { text: 'Node CPU Usage', passed: cpu_passed, value: cpu_values.sort }
90
+
91
+ memory_values = []
92
+ memory_sum = 0
93
+ node_stats['nodes'].each_value do |node|
94
+ memory_sum += node['os']['mem']['used_percent']
95
+ memory_values << "#{node['name']}: #{node['os']['mem']['used_percent']}"
96
+ end
97
+ memory_passed = (memory_sum / memory_values.size).to_i < 100
98
+ checks << { text: 'Node Memory Usage', passed: memory_passed, value: memory_values.sort }
99
+ rescue Faraday::Error => e
100
+ checks << { text: 'Cluster Reachable', passed: false, value: e.message }
101
+ end
102
+
103
+ status checks.find { |c| c[:passed] == false } ? 500 : 200
104
+
105
+ respond_to do |format|
106
+ format.html do
107
+ haml :'status/check', locals: { title: 'Status Check', checks: checks }
108
+ end
109
+ format.json do
110
+ json checks
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,49 @@
1
+ require 'proxes/services/es'
2
+ require 'net/http/persistent'
3
+ require 'singleton'
4
+ require 'rack'
5
+
6
+ module ProxES
7
+ # A lot of code in this comes from Rack::Proxy
8
+ class Forwarder
9
+ include Singleton
10
+ include ProxES::Services::ES
11
+
12
+ def call(env)
13
+ source = Rack::Request.new(env)
14
+ response = conn.send(source.request_method.downcase) do |req|
15
+ source_body = body_from(source)
16
+ req.body = source_body if source_body
17
+ req.url source.fullpath == '' ? URI.parse(env['REQUEST_URI']).request_uri : source.fullpath
18
+ end
19
+ mangle response
20
+ end
21
+
22
+ def mangle(response)
23
+ headers = (response.respond_to?(:headers) && response.headers) || self.class.normalize_headers(response.to_hash)
24
+ body = response.body || ['']
25
+ body = [body] unless body.respond_to?(:each)
26
+
27
+ # Not sure where this is coming from, but it causes timeouts on the client
28
+ headers.delete('transfer-encoding')
29
+ # Ensure that the content length rack middleware kicks in
30
+ headers.delete('content-length')
31
+
32
+ [response.status, headers, body]
33
+ end
34
+
35
+ def body_from(request)
36
+ return nil if request.body.nil? || (Kernel.const_defined?('::Puma::NullIO') && request.body.is_a?(Puma::NullIO))
37
+ request.body.read.tap { |_r| request.body.rewind }
38
+ end
39
+
40
+ class << self
41
+ def normalize_headers(headers)
42
+ mapped = headers.map do |k, v|
43
+ [k, v.is_a?(Array) ? v.join("\n") : v]
44
+ end
45
+ Rack::Utils::HeaderHash.new Hash[mapped]
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support'
4
+ require 'active_support/core_ext/object/blank'
5
+
6
+ module ProxES
7
+ module Helpers
8
+ module Indices
9
+ def filter(asked, against)
10
+ return against.map { |a| a.gsub(/\.\*/, '*') } if asked == ['*'] || asked.blank?
11
+
12
+ answer = []
13
+ against.each do |pattern|
14
+ answer.concat(asked.select { |idx| idx =~ /#{pattern}/ })
15
+ end
16
+ answer
17
+ end
18
+
19
+ def patterns
20
+ return [] if user.nil?
21
+ patterns_for('INDEX').map do |permission|
22
+ return nil if permission.pattern.blank?
23
+ permission.pattern.gsub(/\{user.(.*)\}/) { |_match| user.send(Regexp.last_match[1].to_sym) }
24
+ end.compact
25
+ end
26
+
27
+ def patterns_for(action)
28
+ return [] if user.nil?
29
+ Permission.for_user(user, action)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ module ProxES
6
+ module Loggers
7
+ class Elasticsearch < ::Logger
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'wisper'
4
+ require 'proxes/request'
5
+ require 'ditty/services/logger'
6
+
7
+ module ProxES
8
+ module Middleware
9
+ class ErrorHandling
10
+ attr_reader :logger
11
+
12
+ include Wisper::Publisher
13
+
14
+ def initialize(app, logger = nil)
15
+ @app = app
16
+ @logger = logger || ::Ditty::Services::Logger.instance
17
+ end
18
+
19
+ def call(env)
20
+ request = ProxES::Request.from_env(env)
21
+ response = @app.call env
22
+ broadcast(:es_request_failed, request, response) unless (200..299).cover?(response[0])
23
+ response
24
+ rescue Errno::EHOSTUNREACH
25
+ error 'Could not reach Elasticsearch at ' + ENV['ELASTICSEARCH_URL']
26
+ rescue Errno::ECONNREFUSED, Faraday::ConnectionFailed, SocketError
27
+ error 'Elasticsearch not listening at ' + ENV['ELASTICSEARCH_URL']
28
+ rescue Pundit::NotAuthorizedError, Ditty::Helpers::NotAuthenticated => e
29
+ broadcast(:es_request_denied, request, e)
30
+ log_not_authorized request
31
+ raise e if env['APP_ENV'] == 'development'
32
+ return [401, {}, []] if request.head?
33
+ request.html? && request.user.nil? ? login_and_redirect(request) : error('Not Authorized', 401)
34
+ rescue StandardError => e
35
+ broadcast(:es_request_denied, request, e)
36
+ log_not_authorized request
37
+ raise e if env['APP_ENV'] == 'development'
38
+ return [403, {}. []] if request.head?
39
+ error 'Forbidden', 403
40
+ end
41
+
42
+ def log_not_authorized(request)
43
+ user = request.user ? request.user.email : 'unauthenticated request'
44
+ logger.error "Access denied for #{user} by security layer: #{request.detail}"
45
+ end
46
+
47
+ # Response Helpers
48
+ def error(message, code = 500)
49
+ headers = { 'Content-Type' => 'application/json' }
50
+ headers['WWW-Authenticate'] = 'Basic realm="security"' if code == 401
51
+ [code, headers, ['{"error":"' + message + '"}']]
52
+ end
53
+
54
+ def login_and_redirect(request)
55
+ request.session['omniauth.origin'] = request.url unless request.url == '/_proxes/auth/login'
56
+ redirect '/_proxes/auth/login'
57
+ end
58
+
59
+ def redirect(destination, code = 302)
60
+ [code, { 'Location' => destination }, []]
61
+ end
62
+ end
63
+ end
64
+ end