chook 1.1.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGES.md +7 -4
- data/README.md +289 -264
- data/data/chook.conf.example +82 -3
- data/lib/chook/configuration.rb +8 -1
- data/lib/chook/event/handled_event.rb +2 -2
- data/lib/chook/server.rb +24 -47
- data/lib/chook/server/auth.rb +164 -0
- data/lib/chook/server/public/css/chook.css +4 -0
- data/lib/chook/server/public/js/chook.js +20 -0
- data/lib/chook/server/routes.rb +7 -25
- data/lib/chook/server/routes/handle_webhook_event.rb +1 -1
- data/lib/chook/server/routes/handlers.rb +0 -2
- data/lib/chook/server/routes/home.rb +0 -1
- data/lib/chook/server/routes/log.rb +2 -3
- data/lib/chook/server/routes/login_logout.rb +48 -0
- data/lib/chook/server/views/layout.haml +21 -1
- data/lib/chook/version.rb +1 -1
- metadata +4 -2
data/data/chook.conf.example
CHANGED
@@ -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'
|
84
|
-
#
|
85
|
-
#
|
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:
|
data/lib/chook/configuration.rb
CHANGED
@@ -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
|
|
data/lib/chook/server.rb
CHANGED
@@ -25,10 +25,11 @@
|
|
25
25
|
|
26
26
|
require 'sinatra/base'
|
27
27
|
require 'sinatra/custom_logger'
|
28
|
-
|
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
|
-
|
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
|
-
|
67
|
+
|
68
|
+
else # no ssl
|
79
69
|
super
|
80
70
|
end # if use ssl
|
81
71
|
end # self.run
|
82
72
|
|
83
|
-
|
84
|
-
|
85
|
-
|
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
|
-
|
111
|
-
|
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
|
-
|
114
|
-
end #
|
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'
|
@@ -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() {
|
data/lib/chook/server/routes.rb
CHANGED
@@ -28,32 +28,13 @@ module Chook
|
|
28
28
|
# the server
|
29
29
|
class Server < Sinatra::Base
|
30
30
|
|
31
|
-
|
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
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
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'
|