chook 1.1.0 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -80,9 +80,12 @@ logs_to_keep: 10
80
80
  log_max_megs: 10
81
81
 
82
82
  #################
83
- # Any value here will turn on 'HTTP Basic Authentication' and this will be the username
84
- # required to make any connection to the Chook server.
85
- # Leaving this empty will allow any connection without authentication
83
+ # Any value here will turn on 'HTTP Basic Authentication' for webhooks,
84
+ # and this will be the username required for a Jamf Pro server to send
85
+ # webhooks to the Chook server.
86
+ #
87
+ # Leaving this empty will allow receiving of webooks with no authentication
88
+ #
86
89
  # Default: none
87
90
  #
88
91
  webhooks_user:
@@ -102,3 +105,79 @@ webhooks_user:
102
105
  # Default: none
103
106
  #
104
107
  webhooks_user_pw:
108
+
109
+ #################
110
+ # Any value here will require authentication to view the admin web page.
111
+ #
112
+ # Leaving this empty will allow anyone to see the admin web page.
113
+ #
114
+ # If the value here is 'use_jamf' then anyone can log in with their
115
+ # Jamf Pro credentials, and the admin_pw setting is ignored
116
+ # (see the jamf_* settings below)
117
+ #
118
+ # If the value is anything else, it is the username required to
119
+ # log in to the admin web page.
120
+ #
121
+ # Default: none
122
+ #
123
+ admin_user:
124
+
125
+ #################
126
+ # When admin page authentication is enabled by setting admin_user, this
127
+ # tells chook how to learn the password for that user:
128
+ #
129
+ # - if the admin_user is 'use_jamf' then this is ignored, see the jamf_* settings below
130
+ #
131
+ # - If its a path to a file, the file contains the password and nothing else.
132
+ # The file must be owned by the user running the chook server, and must have
133
+ # mode 0600.
134
+ #
135
+ # - If it ends with a pipe character (|), everything execpt the pipe is considered
136
+ # to be a shell command, executable by the user running the chook server.
137
+ # The standard-output of the command will be the password.
138
+ #
139
+ # Default: none
140
+ #
141
+ admin_pw:
142
+
143
+ #################
144
+ # When admin page authentication is enabled by setting admin_user,
145
+ # this is how many seconds before the browser session expires and the
146
+ # admin must log in again.
147
+ #
148
+ # Default: 86400 (24 hours)
149
+ #
150
+ admin_session_expires: 86400
151
+
152
+ #################
153
+ # When the admin_user is 'use_jamf', this is the hostname of the
154
+ # Jamf Pro server to use for authentication
155
+ #
156
+ # Default: none, but /etc/ruby-jss.conf will be honored
157
+ #
158
+ jamf_server:
159
+
160
+ #################
161
+ # When the admin_user is 'use_jamf', this is the port of the
162
+ # Jamf Pro server to use for Authentication
163
+ #
164
+ # Default: none, but /etc/ruby-jss.conf will be honored
165
+ #
166
+ jamf_port:
167
+
168
+ #################
169
+ # When the admin_user is 'use_jamf', should SSL be used when
170
+ # connection to the jamf_server? true or false
171
+ #
172
+ # Default: none, but /etc/ruby-jss.conf will be honored
173
+ #
174
+ jamf_use_ssl:
175
+
176
+ #################
177
+ # When the admin_user is 'use_jamf', and jamf_use_ssl is true,
178
+ # should SSL certificates from the jamf server be
179
+ # validated?
180
+ #
181
+ # Default: none, but /etc/ruby-jss.conf will be honored
182
+ #
183
+ jamf_verify_cert:
@@ -54,7 +54,14 @@ module Chook
54
54
  log_max_megs: :to_i,
55
55
  logs_to_keep: :to_i,
56
56
  webhooks_user: nil,
57
- webhooks_user_pw: nil
57
+ webhooks_user_pw: nil,
58
+ admin_user: nil,
59
+ admin_pw: nil,
60
+ admin_session_expires: :to_i,
61
+ jamf_server: nil,
62
+ jamf_port: :to_i,
63
+ jamf_use_ssl: Chook::Procs::STRING_TO_BOOLEAN,
64
+ jamf_verify_cert: Chook::Procs::STRING_TO_BOOLEAN
58
65
  }.freeze
59
66
 
60
67
  # Class Variables
@@ -139,12 +139,12 @@ module Chook
139
139
  end # def handle
140
140
 
141
141
  def pipe_to_executable(handler)
142
- logger.debug "Sending JSON to stdin of '#{handler}'"
142
+ logger.debug "EXTERNAL: Sending JSON to stdin of '#{handler.basename}'"
143
143
  IO.popen([handler.to_s], 'w') { |h| h.puts @raw_json }
144
144
  end
145
145
 
146
146
  def handle_with_proc(handler)
147
- logger.debug "Running Handler defined in #{handler.handler_file}"
147
+ logger.debug "INTERNAL: Running Handler defined in #{handler.handler_file}"
148
148
  handler.handle self
149
149
  end
150
150
 
@@ -25,10 +25,11 @@
25
25
 
26
26
  require 'sinatra/base'
27
27
  require 'sinatra/custom_logger'
28
- # require 'haml'
28
+ require 'haml'
29
29
  require 'openssl'
30
30
  require 'chook/event_handling'
31
31
  require 'chook/server/log'
32
+ require 'chook/server/auth'
32
33
  require 'chook/server/routes'
33
34
 
34
35
  module Chook
@@ -40,9 +41,11 @@ module Chook
40
41
  DEFAULT_PORT = 80
41
42
  DEFAULT_SSL_PORT = 443
42
43
  DEFAULT_CONCURRENCY = true
44
+ DEFAULT_SESSION_EXPIRE = 24 * 60 * 60 # one day
43
45
 
44
46
  # set defaults in config
45
47
  Chook.config.port ||= Chook.config.use_ssl ? DEFAULT_SSL_PORT : DEFAULT_PORT
48
+ Chook.config.admin_session_expires ||= DEFAULT_SESSION_EXPIRE
46
49
 
47
50
  # can't use ||= here cuz nil and false have different meanings
48
51
  Chook.config.concurrency = DEFAULT_CONCURRENCY if Chook.config.concurrency.nil?
@@ -50,21 +53,7 @@ module Chook
50
53
  # Run the server
51
54
  ###################################
52
55
  def self.run!(log_level: nil)
53
- log_level ||= Chook.config.log_level
54
- @log_level = Chook::Procs::STRING_TO_LOG_LEVEL.call log_level
55
-
56
- configure do
57
- set :logger, Log.startup(@log_level)
58
- set :server, :thin
59
- set :bind, '0.0.0.0'
60
- set :port, Chook.config.port
61
- set :show_exceptions, :after_handler if development?
62
- set :root, "#{File.dirname __FILE__}/server"
63
- enable :static
64
- enable :lock unless Chook.config.concurrency
65
- end # configure
66
-
67
- Chook::HandledEvent::Handlers.load_handlers
56
+ prep_to_run
68
57
 
69
58
  if Chook.config.use_ssl
70
59
  super do |server|
@@ -75,43 +64,31 @@ module Chook
75
64
  verify_peer: false
76
65
  }
77
66
  end # super do
78
- else
67
+
68
+ else # no ssl
79
69
  super
80
70
  end # if use ssl
81
71
  end # self.run
82
72
 
83
- # Learn the client password, if we're using basic auth
84
- ###################################
85
- def self.webhooks_user_pw
86
- return @webhooks_user_pw if @webhooks_user_pw
87
- return nil unless Chook.config.webhooks_user_pw
88
-
89
- setting = Chook.config.webhooks_user_pw
90
-
91
- @webhooks_user_pw =
92
- if setting.end_with? '|'
93
- # if the path ends with a pipe, its a command that will
94
- # return the desired password, so remove the pipe,
95
- # execute it, and return stdout from it.
96
- cmd = setting.chomp '|'
97
- output = `#{cmd} 2>&1`.chomp
98
- raise "Can't get webhooks user password: #{output}" unless $CHILD_STATUS.exitstatus.zero?
99
- output
100
-
101
- else
102
- # otherwise its a file path, and read the pw from the contents
103
- file = Pathname.new setting
104
- return nil unless file.file?
105
- stat = file.stat
106
- mode = format('%o', stat.mode)
107
- raise 'Password file for webhooks user has insecure mode, must be 0600.' unless mode.end_with?('0600')
108
- raise "Password file for webhooks user has insecure owner, must be owned by UID #{Process.euid}." unless stat.owned?
73
+ def self.prep_to_run
74
+ log_level ||= Chook.config.log_level
75
+ @log_level = Chook::Procs::STRING_TO_LOG_LEVEL.call log_level
109
76
 
110
- # chomping an empty string removes all trailing \n's and \r\n's
111
- file.read.chomp('')
77
+ configure do
78
+ set :logger, Log.startup(@log_level)
79
+ set :server, :thin
80
+ set :bind, '0.0.0.0'
81
+ set :port, Chook.config.port
82
+ set :show_exceptions, :after_handler if development?
83
+ set :root, "#{File.dirname __FILE__}/server"
84
+ enable :static
85
+ enable :sessions
86
+ set :sessions, expire_after: Chook.config.admin_session_expires if Chook.config.admin_user
87
+ enable :lock unless Chook.config.concurrency
88
+ end # configure
112
89
 
113
- end # if else
114
- end # self.webhooks_user_pw
90
+ Chook::HandledEvent::Handlers.load_handlers
91
+ end # prep to run
115
92
 
116
93
  end # class server
117
94
 
@@ -0,0 +1,164 @@
1
+ ### Copyright 2017 Pixar
2
+
3
+ ###
4
+ ### Licensed under the Apache License, Version 2.0 (the "Apache License")
5
+ ### with the following modification; you may not use this file except in
6
+ ### compliance with the Apache License and the following modification to it:
7
+ ### Section 6. Trademarks. is deleted and replaced with:
8
+ ###
9
+ ### 6. Trademarks. This License does not grant permission to use the trade
10
+ ### names, trademarks, service marks, or product names of the Licensor
11
+ ### and its affiliates, except as required to comply with Section 4(c) of
12
+ ### the License and to reproduce the content of the NOTICE file.
13
+ ###
14
+ ### You may obtain a copy of the Apache License at
15
+ ###
16
+ ### http://www.apache.org/licenses/LICENSE-2.0
17
+ ###
18
+ ### Unless required by applicable law or agreed to in writing, software
19
+ ### distributed under the Apache License with the above modification is
20
+ ### distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
21
+ ### KIND, either express or implied. See the Apache License for the specific
22
+ ### language governing permissions and limitations under the Apache License.
23
+ ###
24
+ ###
25
+
26
+ module Chook
27
+
28
+ # the server
29
+ class Server < Sinatra::Base
30
+
31
+ # helper module for authentication
32
+ module Auth
33
+
34
+ USE_JAMF_ADMIN_USER = 'use_jamf'.freeze
35
+
36
+ def protect_via_basic_auth!
37
+ # don't protect if user isn't defined
38
+ return unless Chook.config.webhooks_user
39
+ return if webhook_user_authorized?
40
+ headers['WWW-Authenticate'] = 'Basic realm="Restricted Area"'
41
+ halt 401, "Not authorized\n"
42
+ end
43
+
44
+ def webhook_user_authorized?
45
+ @auth ||= Rack::Auth::Basic::Request.new(request.env)
46
+
47
+ # gotta have basic auth presented to us
48
+ unless @auth.provided? && @auth.basic? && @auth.credentials
49
+ Chook.logger.debug "No basic auth provided on protected route: #{request.path_info} from: #{request.ip}"
50
+ return false
51
+ end
52
+
53
+ authenticate_webhooks_user @auth.credentials
54
+ end # authorized?
55
+
56
+ # webhook user auth always comes from config
57
+ def authenticate_webhooks_user(creds)
58
+ if creds.first == Chook.config.webhooks_user && creds.last == Chook::Server.webhooks_user_pw
59
+ Chook.logger.debug "Got HTTP Basic auth for webhooks user: #{Chook.config.webhooks_user}@#{request.ip}"
60
+ true
61
+ else
62
+ Chook.logger.error "FAILED auth for webhooks user: #{Chook.config.webhooks_user}@#{request.ip}"
63
+ false
64
+ end
65
+ end # authenticate_webhooks_user
66
+
67
+ # admin user auth might come from config, might come from Jamf Pro
68
+ def authenticate_admin(user, pw)
69
+ return authenticate_jamf_admin(user, pw) if Chook.config.admin_user == USE_JAMF_ADMIN_USER
70
+ authenticate_admin_user(user, pw)
71
+ end
72
+
73
+ # admin auth from config
74
+ def authenticate_admin_user(user, pw)
75
+ if user == Chook.config.admin_user && pw == Chook::Server.admin_user_pw
76
+ Chook.logger.debug "Got auth for admin user: #{user}@#{request.ip}"
77
+ session[:authed_admin] = user
78
+ true
79
+ else
80
+ Chook.logger.warn "FAILED auth for admin user: #{user}@#{request.ip}"
81
+ session[:authed_admin] = nil
82
+ false
83
+ end
84
+ end
85
+
86
+ # admin auth from jamf pro
87
+ def authenticate_jamf_admin(user, pw)
88
+ require 'ruby-jss'
89
+ JSS::APIConnection.new(
90
+ user: user,
91
+ pw: pw,
92
+ server: Chook.config.jamf_server,
93
+ port: Chook.config.jamf_port,
94
+ use_ssl: Chook.config.jamf_use_ssl,
95
+ verify_cert: Chook.config.jamf_verify_cert
96
+ )
97
+ Chook.logger.debug "Jamf Admin login for: #{user}@#{request.ip}"
98
+
99
+ session[:authed_admin] = user
100
+ true
101
+ rescue JSS::AuthenticationError
102
+ Chook.logger.warn "Jamf Admin login FAILED for: #{user}@#{request.ip}"
103
+ session[:authed_admin] = nil
104
+ false
105
+ end # authenticate_jamf_admin
106
+
107
+ end # module auth
108
+
109
+ helpers Chook::Server::Auth
110
+
111
+ # Learn the webhook or admin passwords from config.
112
+ # so we can authenticate them from the browser and the JSS
113
+ #
114
+ # This is at the Server level, since we only need read it
115
+ # once per server startup, so we store it in a server
116
+ # instance var.
117
+ ###################################
118
+ def self.webhooks_user_pw
119
+ @webhooks_user_pw ||= pw_from_conf Chook.config.webhooks_user_pw
120
+ end # self.webhooks_user_pw
121
+
122
+ def self.admin_user_pw
123
+ @admin_user_pw ||= pw_from_conf Chook.config.admin_pw
124
+ end
125
+
126
+ def self.pw_from_conf(setting)
127
+ return '' unless setting
128
+
129
+ # if the path ends with a pipe, its a command that will
130
+ # return the desired password, so remove the pipe,
131
+ # execute it, and return stdout from it.
132
+ return pw_from_command(setting) if setting.end_with? '|'
133
+
134
+ # otherwise its a file path, and read the pw from the contents
135
+ pw_from_file(setting)
136
+ end # def pw_from_conf(setting)
137
+
138
+ def self.pw_from_command(cmd)
139
+ cmd = cmd.chomp '|'
140
+ output = `#{cmd} 2>&1`.chomp
141
+ raise "Can't get password from #{setting}: #{output}" unless $CHILD_STATUS.exitstatus.zero?
142
+ output
143
+ end
144
+
145
+ def self.pw_from_file(file)
146
+ file = Pathname.new file
147
+ return nil unless file.file?
148
+ stat = file.stat
149
+ mode = format('%o', stat.mode)
150
+ raise "Password file #{setting} has insecure mode, must be 0600." unless mode.end_with?('0600')
151
+ raise "Password file #{setting} has insecure owner, must be owned by UID #{Process.euid}." unless stat.owned?
152
+ # chomping an empty string removes all trailing \n's and \r\n's
153
+ file.read.chomp('')
154
+ end
155
+
156
+
157
+ end # server
158
+
159
+ end # Chook
160
+
161
+ require 'chook/server/routes/home'
162
+ require 'chook/server/routes/handle_webhook_event'
163
+ require 'chook/server/routes/handlers'
164
+ require 'chook/server/routes/log'
@@ -39,6 +39,10 @@
39
39
  font-size: 1.2em;
40
40
  }
41
41
 
42
+ #login_incorrect {
43
+ color: red;
44
+ }
45
+
42
46
  /* ******* Section Label Areas ******* */
43
47
 
44
48
  .section_label {
@@ -1,3 +1,23 @@
1
+ // log out of basic auth by sending an incorrect name /pw
2
+ // may not work on all browsers
3
+ // see second comment at
4
+ // https://stackoverflow.com/questions/233507/how-to-log-out-user-from-web-site-using-basic-authentication#492926
5
+
6
+ function logout() {
7
+ //var logged_out_page = window.location.protocol + '//xxxx:xxxx@' + window.location.host + '/login';
8
+
9
+ var xhttp = new XMLHttpRequest();
10
+
11
+ xhttp.onreadystatechange = function() {
12
+ if (this.readyState == 4) {
13
+ window.location = "/login";
14
+ }
15
+ };
16
+
17
+ xhttp.open("GET", "/LOG_OUT:no_such_pw@logout", true);
18
+ xhttp.setRequestHeader("Authorization", "Basic " + btoa("LOG_OUT:no_such_pw"));
19
+ xhttp.send();
20
+ }
1
21
 
2
22
  // show the logbox
3
23
  function view_log() {
@@ -28,32 +28,13 @@ module Chook
28
28
  # the server
29
29
  class Server < Sinatra::Base
30
30
 
31
- # These two helpers let us decude which routes need
32
- # http basic auth and which don't
33
- #
34
- # To protect a route, put `protected!` as the
35
- # first line of code in the route.
36
- #
37
- # See http://sinatrarb.com/faq.html#auth
38
- #
39
- helpers do
40
- def protected!
41
- # don't protect if user isn't defined
42
- return unless Chook.config.webhooks_user
31
+ HANDLE_EVENT_ROUTE = '/handle_webhook_event'.freeze
43
32
 
44
- return if authorized?
45
- headers['WWW-Authenticate'] = 'Basic realm="Restricted Area"'
46
- halt 401, "Not authorized\n"
47
- end
48
-
49
- def authorized?
50
- @auth ||= Rack::Auth::Basic::Request.new(request.env)
51
- @auth.provided? && \
52
- @auth.basic? && \
53
- @auth.credentials && \
54
- @auth.credentials == [Chook.config.webhooks_user, Chook::Server.webhooks_user_pw]
55
- end
56
- end
33
+ # before do
34
+ # break if request.path_info == Chook::Server::HANDLE_EVENT_ROUTE
35
+ # # break if request.path_info == '/' && session[:authed_admin]
36
+ # # redirect '/' unless session[:authed_admin]
37
+ # end
57
38
 
58
39
  # log errors in production (in dev, they go to stdout and the browser)
59
40
  error do
@@ -69,4 +50,5 @@ end # Chook
69
50
  require 'chook/server/routes/home'
70
51
  require 'chook/server/routes/handle_webhook_event'
71
52
  require 'chook/server/routes/handlers'
53
+ require 'chook/server/routes/login_logout'
72
54
  require 'chook/server/routes/log'