chook 1.0.1.b2 → 1.1.0

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.
Files changed (38) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGES.md +21 -0
  3. data/README.md +243 -36
  4. data/bin/chook-server +29 -1
  5. data/data/chook.conf.example +104 -0
  6. data/data/sample_handlers/RestAPIOperation.rb +12 -8
  7. data/data/sample_handlers/SmartGroupComputerMembershipChange.rb +3 -6
  8. data/data/sample_jsons/SmartGroupComputerMembershipChange.json +3 -1
  9. data/data/sample_jsons/SmartGroupMobileDeviceMembershipChange.json +3 -1
  10. data/lib/chook/configuration.rb +20 -8
  11. data/lib/chook/event/handled_event.rb +15 -12
  12. data/lib/chook/event/handled_event/handlers.rb +136 -83
  13. data/lib/chook/event/handled_event_logger.rb +86 -0
  14. data/lib/chook/event_handling.rb +1 -0
  15. data/lib/chook/foundation.rb +2 -0
  16. data/lib/chook/procs.rb +17 -1
  17. data/lib/chook/server.rb +71 -74
  18. data/lib/chook/server/log.rb +215 -0
  19. data/lib/chook/server/public/css/chook.css +125 -0
  20. data/lib/chook/server/public/imgs/ChookLogoAlMcWhiggin.png +0 -0
  21. data/lib/chook/server/public/js/chook.js +127 -0
  22. data/lib/chook/server/public/js/logstream.js +101 -0
  23. data/lib/chook/server/routes.rb +45 -0
  24. data/lib/chook/server/routes/handle_webhook_event.rb +22 -3
  25. data/lib/chook/server/routes/handlers.rb +52 -0
  26. data/lib/chook/server/routes/home.rb +34 -1
  27. data/lib/chook/server/routes/log.rb +106 -0
  28. data/lib/chook/server/views/admin.haml +11 -0
  29. data/lib/chook/server/views/bak.haml +48 -0
  30. data/lib/chook/server/views/config.haml +15 -0
  31. data/lib/chook/server/views/handlers.haml +48 -0
  32. data/lib/chook/server/views/layout.haml +39 -0
  33. data/lib/chook/server/views/logstream.haml +32 -0
  34. data/lib/chook/server/views/sketch_admin +44 -0
  35. data/lib/chook/subject.rb +1 -2
  36. data/lib/chook/subject/smart_group.rb +6 -0
  37. data/lib/chook/version.rb +1 -1
  38. metadata +73 -18
@@ -0,0 +1,86 @@
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
+ # a simple object embedded in a Handled Event that
29
+ # allows a standardize way to note event-related log entries
30
+ # with the event object_id.
31
+ #
32
+ # Every Handled Event has one of these instances exposed in it's
33
+ # #logger attribute, and usable from within 'internal' handlers
34
+ #
35
+ # Here's an example.
36
+ #
37
+ # Say you have a ComputerSmartGroupMembershipChanged event
38
+ #
39
+ # calling `event.logger.info "foobar"` will generate the log message:
40
+ #
41
+ # Event 1234567: foobar
42
+ #
43
+ class HandledEventLogger
44
+
45
+ def initialize(event)
46
+ @event = event
47
+ end
48
+
49
+ def event_message(msg)
50
+ "Event #{@event.object_id}: #{msg}"
51
+ end
52
+
53
+ def debug(msg)
54
+ Chook::Server::Log.logger.debug event_message(msg)
55
+ end
56
+
57
+ def info(msg)
58
+ Chook::Server::Log.logger.info event_message(msg)
59
+ end
60
+
61
+ def warn(msg)
62
+ Chook::Server::Log.logger.warn event_message(msg)
63
+ end
64
+
65
+ def error(msg)
66
+ Chook::Server::Log.logger.error event_message(msg)
67
+ end
68
+
69
+ def fatal(msg)
70
+ Chook::Server::Log.logger.fatal event_message(msg)
71
+ end
72
+
73
+ def unknown(msg)
74
+ Chook::Server::Log.logger.unknown event_message(msg)
75
+ end
76
+
77
+ # log an exception - multiple log lines
78
+ # the first being the error message the rest being indented backtrace
79
+ def log_exception(exception)
80
+ error "#{exception.class}: #{exception}"
81
+ exception.backtrace.each { |l| error "..#{l}" }
82
+ end
83
+
84
+ end # class HandledEventLogger
85
+
86
+ end # module
@@ -37,4 +37,5 @@ Chook::HandledSubject.generate_classes
37
37
  # events
38
38
  require 'chook/event'
39
39
  require 'chook/event/handled_event'
40
+ require 'chook/event/handled_event_logger'
40
41
  Chook::HandledEvent.generate_classes
@@ -27,6 +27,8 @@
27
27
  require 'json'
28
28
  require 'open-uri'
29
29
  require 'pathname'
30
+ require 'logger'
31
+ require 'English'
30
32
 
31
33
  require 'chook/version'
32
34
  require 'chook/procs' # must load before configuration
@@ -30,12 +30,28 @@ module Chook
30
30
  module Procs
31
31
 
32
32
  TRUE_RE = /^\s*(true|yes)\s*$/i
33
+
33
34
  JSS_EPOCH_TO_TIME = proc { |val| Time.strptime val.to_s[0..-4], '%s' }
35
+
34
36
  STRING_TO_BOOLEAN = proc { |val| val =~ TRUE_RE ? true : false }
37
+
35
38
  STRING_TO_PATHNAME = proc { |val| Pathname.new val }
39
+
40
+ STRING_TO_LOG_LEVEL = proc do |level|
41
+ if (0..5).cover? level
42
+ level
43
+ else
44
+ lvl = Chook::Server::Log::LOG_LEVELS[level.to_sym]
45
+ lvl ? lvl : Logger::UNKNOWN
46
+ end # if..else
47
+ end
48
+
36
49
  MOBILE_USERID = proc { |_device| '-1' }
50
+
37
51
  PRODUCT = proc { |_device| nil }
38
- ALWAYS_TRUE = proc { |_boolean| True }
52
+
53
+ ALWAYS_TRUE = proc { |_boolean| true }
54
+
39
55
  COMPUTER_USERID = proc do |comp|
40
56
  id = '-1' unless comp.groups_accounts[:local_accounts].find { |acct| acct[:name] == comp.username }
41
57
  id.is_a?(Hash) ? id[:uid] : '-1'
@@ -22,9 +22,14 @@
22
22
  ### language governing permissions and limitations under the Apache License.
23
23
  ###
24
24
  ###
25
- require 'chook/event_handling'
25
+
26
26
  require 'sinatra/base'
27
+ require 'sinatra/custom_logger'
28
+ # require 'haml'
27
29
  require 'openssl'
30
+ require 'chook/event_handling'
31
+ require 'chook/server/log'
32
+ require 'chook/server/routes'
28
33
 
29
34
  module Chook
30
35
 
@@ -32,90 +37,82 @@ module Chook
32
37
  # the engine of your choice.
33
38
  class Server < Sinatra::Base
34
39
 
35
- DEFAULT_SERVER_ENGINE = :webrick
36
- DEFAULT_PORT = 8000
40
+ DEFAULT_PORT = 80
41
+ DEFAULT_SSL_PORT = 443
42
+ DEFAULT_CONCURRENCY = true
37
43
 
38
- @server_engine = Chook::CONFIG.server_engine || DEFAULT_SERVER_ENGINE
39
- require @server_engine.to_s
40
- @server_port = Chook::CONFIG.server_port || DEFAULT_PORT
44
+ # set defaults in config
45
+ Chook.config.port ||= Chook.config.use_ssl ? DEFAULT_SSL_PORT : DEFAULT_PORT
41
46
 
42
- def self.run!
43
- # trap HUPs to reload handlers
44
- Signal.trap('HUP') do
45
- Chook::HandledEvent::Handlers.load_handlers reload: true
46
- end
47
- Chook::HandledEvent::Handlers.load_handlers
48
- chook_configure
49
- case @server_engine.to_sym
50
- when :webrick
51
- super
52
- when :thin
53
- if Chook::CONFIG.use_ssl
54
- super do |server|
55
- server.ssl = true
56
- server.ssl_options = {
57
- cert_chain_file: Chook::CONFIG.ssl_cert_path.to_s,
58
- private_key_file: Chook::CONFIG.ssl_private_key_path.to_s,
59
- verify_peer: false
60
- }
61
- end # super do
62
- else
63
- super
64
- end # if use ssl
65
- end # case
66
- end # self.run
47
+ # can't use ||= here cuz nil and false have different meanings
48
+ Chook.config.concurrency = DEFAULT_CONCURRENCY if Chook.config.concurrency.nil?
49
+
50
+ # Run the server
51
+ ###################################
52
+ 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
67
55
 
68
- # Sinatra Settings
69
- def self.chook_configure
70
56
  configure do
71
- set :environment, :production
72
- enable :logging, :lock
57
+ set :logger, Log.startup(@log_level)
58
+ set :server, :thin
73
59
  set :bind, '0.0.0.0'
74
- set :server, @server_engine
75
- set :port, @server_port
76
-
77
- if Chook::CONFIG.use_ssl
78
- case @server_engine.to_sym
79
- when :webrick
80
- require 'webrick/https'
81
- key = Chook::CONFIG.ssl_private_key_path.read
82
- cert = Chook::CONFIG.ssl_cert_path.read
83
- cert_name = Chook::CONFIG.ssl_cert_name
84
- set :SSLEnable, true
85
- set :SSLVerifyClient, OpenSSL::SSL::VERIFY_NONE
86
- set :SSLPrivateKey, OpenSSL::PKey::RSA.new(key, ssl_key_password)
87
- set :SSLCertificate, OpenSSL::X509::Certificate.new(cert)
88
- set :SSLCertName, [['CN', cert_name]]
89
- when :thin
90
- true
91
- end # case
92
- end # if ssl
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
93
65
  end # configure
94
- end # chook_configure
95
66
 
96
- def self.ssl_key_password
97
- path = Chook::CONFIG.ssl_private_key_pw_path
98
- raise 'No config setting for "ssl_private_key_pw_path"' unless path
99
- file = Pathname.new path
67
+ Chook::HandledEvent::Handlers.load_handlers
100
68
 
101
- # if the path ends with a pipe, its a command that will
102
- # return the desired password, so remove the pipe,
103
- # execute it, and return stdout from it.
104
- if path.end_with? '|'
105
- raise 'ssl_private_key_pw_path: #{path} is not an executable file.' unless file.executable?
106
- return `#{path.chomp '|'}`.chomp
107
- end
69
+ if Chook.config.use_ssl
70
+ super do |server|
71
+ server.ssl = true
72
+ server.ssl_options = {
73
+ cert_chain_file: Chook.config.ssl_cert_path.to_s,
74
+ private_key_file: Chook.config.ssl_private_key_path.to_s,
75
+ verify_peer: false
76
+ }
77
+ end # super do
78
+ else
79
+ super
80
+ end # if use ssl
81
+ end # self.run
82
+
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
108
88
 
109
- raise 'ssl_private_key_pw_path: #{path} is not a readable file.' unless file.readable?
110
- stat = file.stat
111
- raise "Password file for '#{pw}' has insecure permissions, must be 0600." unless ('%o' % stat.mode).end_with? '0600'
89
+ setting = Chook.config.webhooks_user_pw
112
90
 
113
- # chomping an empty string removes all trailing \n's and \r\n's
114
- file.read.chomp('')
115
- end # ssl_key_password
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?
109
+
110
+ # chomping an empty string removes all trailing \n's and \r\n's
111
+ file.read.chomp('')
112
+
113
+ end # if else
114
+ end # self.webhooks_user_pw
116
115
 
117
116
  end # class server
118
117
 
119
118
  end # module
120
-
121
- require 'chook/server/routes'
@@ -0,0 +1,215 @@
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
+ # the main module
27
+ module Chook
28
+
29
+ # the server app
30
+ class Server < Sinatra::Base
31
+
32
+ # the CustomLogger helper allows us to
33
+ # use our own Logger instance as the
34
+ # Sinatra `logger` available in routes vis `set :logger...`
35
+ helpers Sinatra::CustomLogger
36
+
37
+ # This module defines our custom Logger instance from the config settings
38
+ # and makes it available in the .logger module method,
39
+ # which is used anywhere outside of a route
40
+ # (inside of a route, the #logger method is locally available)
41
+ #
42
+ # General access to the logger:
43
+ # from anywhere in Chook, as long as a server is running, the Logger
44
+ # instance is available at Chook.logger or Chook::Server::Log.logger
45
+ #
46
+ #
47
+ # Logging from inside a route:
48
+ # inside a Sinatra route, the local `logger` method returnes the
49
+ # Logger instance.
50
+ #
51
+ # Logging from an internal handler:
52
+ # In an internal WebHook handler, starting with `Chook.event_handler do |event|`
53
+ # the logger should be accesses via the event's own wrapper: event.logger
54
+ # Each message will be prepended with the event'd ID
55
+ #
56
+ # Logging from an external handler:
57
+ # from an external handler you can POST a JSON formatted log entry to
58
+ # http(s)://chookserver/log. The body must be a JSON object with 2 keys:
59
+ # 'level' and 'message', with non-empty strings.
60
+ # The level must be one of: fatal, error, warn, info, or debug. Any other
61
+ # value as the level will always be logged regardless of current logging
62
+ # level. NOTE: if your server requires authentication, you must provide it
63
+ # when using this route.
64
+ #
65
+ # Here's an example with curl, split to multi-line for clarity:
66
+ #
67
+ # curl -H "Content-Type: application/json" \
68
+ # -X POST \
69
+ # --data '{"level":"debug", "message":"It Worked"}' \
70
+ # https://user:passwd@chookserver.myorg.org:443/log
71
+ #
72
+ module Log
73
+
74
+ # Using an instance of this as the Logger target sends logfile writes
75
+ # to all registered streams as well as the file
76
+ class LogFileWithStream < File
77
+
78
+ # ServerSent Events data lines always start with this
79
+ LOGSTREAM_DATA_PFX = 'data:'.freeze
80
+
81
+ def write(str)
82
+ super # writes out to the file
83
+ flush
84
+ # send to any active streams
85
+ Chook::Server::Log.log_streams.keys.each do |active_stream|
86
+ # ignore streams closed at the client end,
87
+ # they get removed when a new stream starts
88
+ # see the route: get '/subscribe_to_log_stream'
89
+ next if active_stream.closed?
90
+
91
+ # send new data to the stream
92
+ active_stream << "#{LOGSTREAM_DATA_PFX}#{str}\n\n"
93
+ end
94
+ end
95
+ end # class
96
+
97
+ # mapping of integer levels to symbols
98
+ LOG_LEVELS = {
99
+ fatal: Logger::FATAL,
100
+ error: Logger::ERROR,
101
+ warn: Logger::WARN,
102
+ info: Logger::INFO,
103
+ debug: Logger::DEBUG
104
+ }.freeze
105
+
106
+ # log Streaming
107
+ # ServerSent Events data lines always start with this
108
+ LOGSTREAM_DATA_PFX = 'data:'.freeze
109
+
110
+ # Send this to the clients at least every LOGSTREAM_KEEPALIVE_MAX secs
111
+ # even if there's no data for the stream
112
+ LOGSTREAM_KEEPALIVE_MSG = "#{LOGSTREAM_DATA_PFX} I'm Here!\n\n".freeze
113
+ LOGSTREAM_KEEPALIVE_MAX = 10
114
+
115
+ # the clients will recognize M3_LOG_STREAM_CLOSED and stop trying
116
+ # to connect via ssh.
117
+ LOGSTREAM_CLOSED_PFX = "#{LOGSTREAM_DATA_PFX} M3_LOG_STREAM_CLOSED:".freeze
118
+
119
+ DEFAULT_FILE = Pathname.new '/var/log/chook-server.log'
120
+ DEFAULT_MAX_MEGS = 10
121
+ DEFAULT_TO_KEEP = 10
122
+ DEFAULT_LEVEL = Logger::INFO
123
+
124
+ # set defaults in config
125
+ Chook.config.log_file ||= DEFAULT_FILE
126
+ Chook.config.logs_to_keep ||= DEFAULT_TO_KEEP
127
+ Chook.config.log_max_megs ||= DEFAULT_MAX_MEGS
128
+ Chook.config.log_level ||= DEFAULT_LEVEL
129
+
130
+ # Create the logger,
131
+ # make the first log entry for this run,
132
+ # and return it so it can be used by the server
133
+ # when it does `set :logger, Log.startup(@log_level)`
134
+ def self.startup(level = Chook.config.log_level)
135
+ # create the logger using a LogFileWithStream instance
136
+ @logger =
137
+ if Chook.config.logs_to_keep && Chook.config.logs_to_keep > 0
138
+ Logger.new(
139
+ LogFileWithStream.new(Chook.config.log_file, 'a'),
140
+ Chook.config.logs_to_keep,
141
+ (Chook.config.log_max_megs * 1024 * 1024)
142
+ )
143
+ else
144
+ Logger.new(LogFileWithStream.new(Chook.config.log_file, 'a'))
145
+ end
146
+
147
+ # date and line format
148
+ @logger.datetime_format = '%Y-%m-%d %H:%M:%S'
149
+
150
+ @logger.formatter = proc do |severity, datetime, _progname, msg|
151
+ "#{datetime}: [#{severity}] #{msg}\n"
152
+ end
153
+
154
+ # level
155
+ level &&= Chook::Procs::STRING_TO_LOG_LEVEL.call level
156
+ level ||= Chook.config.log_level
157
+ level ||= DEFAULT_LEVEL
158
+ @logger.level = level
159
+
160
+ # first startup entry
161
+ @logger.unknown "Chook Server v#{Chook::VERSION} starting up. PID: #{$PROCESS_ID}, Port: #{Chook.config.port}, SSL: #{Chook.config.use_ssl}"
162
+
163
+ # if debug, log our config
164
+ if level == Logger::DEBUG
165
+ @logger.debug 'Config: '
166
+ Chook::Configuration::CONF_KEYS.keys.each do |key|
167
+ @logger.debug " Chook.config.#{key} = #{Chook.config.send key}"
168
+ end
169
+ end
170
+
171
+ # return the logger, the server uses it as a helper
172
+ @logger
173
+ end # log
174
+
175
+ # general access to the logger as Chook::Server::Log.logger
176
+ def self.logger
177
+ @logger ||= startup
178
+ end
179
+
180
+ # a Hash of registered log streams
181
+ # streams are keys, valus are their IP addrs
182
+ # see the `get '/subscribe_to_log_stream'` route
183
+ #
184
+ def self.log_streams
185
+ @log_streams ||= {}
186
+ end
187
+
188
+ def self.clean_log_streams
189
+ log_streams.delete_if do |stream, ip|
190
+ if stream.closed?
191
+ logger.debug "Removing closed log stream for #{ip}"
192
+ true
193
+ else
194
+ false
195
+ end # if
196
+ end # delete if
197
+ end # clean_log_streams
198
+
199
+ end # module
200
+
201
+ end # server
202
+
203
+ # access from everywhere as Chook.logger
204
+ def self.logger
205
+ Server::Log.logger
206
+ end
207
+
208
+ # log an exception - multiple log lines
209
+ # the first being the error message the rest being indented backtrace
210
+ def self.log_exception(exception)
211
+ logger.error exception.to_s
212
+ exception.backtrace.each { |l| logger.error "..#{l}" }
213
+ end
214
+
215
+ end # Chook