yodel_production_environment 0.0.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.
Files changed (36) hide show
  1. data/.gitignore +6 -0
  2. data/Gemfile +4 -0
  3. data/Rakefile +1 -0
  4. data/lib/layouts/common.html +23 -0
  5. data/lib/layouts/common/site.html +79 -0
  6. data/lib/layouts/common/sites.html +13 -0
  7. data/lib/layouts/common/user.html +10 -0
  8. data/lib/layouts/common/users.html +30 -0
  9. data/lib/layouts/home.html +49 -0
  10. data/lib/layouts/production_login_page.html +131 -0
  11. data/lib/migrations/site/01_users_model.rb +23 -0
  12. data/lib/migrations/site/02_page_structure.rb +58 -0
  13. data/lib/migrations/site/03_security_and_home_page.rb +103 -0
  14. data/lib/migrations/yodel/01_record_model.rb +29 -0
  15. data/lib/migrations/yodel/02_page_model.rb +45 -0
  16. data/lib/migrations/yodel/03_layout_model.rb +38 -0
  17. data/lib/migrations/yodel/04_group_model.rb +61 -0
  18. data/lib/migrations/yodel/05_user_model.rb +23 -0
  19. data/lib/migrations/yodel/06_snippet_model.rb +13 -0
  20. data/lib/migrations/yodel/07_search_page_model.rb +32 -0
  21. data/lib/migrations/yodel/08_default_site_options.rb +21 -0
  22. data/lib/migrations/yodel/09_security_page_models.rb +36 -0
  23. data/lib/migrations/yodel/10_record_proxy_page_model.rb +17 -0
  24. data/lib/migrations/yodel/11_email_model.rb +28 -0
  25. data/lib/migrations/yodel/12_api_call_model.rb +23 -0
  26. data/lib/migrations/yodel/13_redirect_page_model.rb +13 -0
  27. data/lib/migrations/yodel/14_menu_model.rb +20 -0
  28. data/lib/models/git_http.rb +326 -0
  29. data/lib/models/git_page.rb +38 -0
  30. data/lib/models/production_login_page.rb +13 -0
  31. data/lib/models/production_sites_page.rb +127 -0
  32. data/lib/models/production_user.rb +17 -0
  33. data/lib/public/css/screen.css +64 -0
  34. data/lib/yodel_production_environment.rb +3 -0
  35. data/yodel_production_environment.gemspec +22 -0
  36. metadata +80 -0
@@ -0,0 +1,23 @@
1
+ class APICallModelMigration < Migration
2
+ def self.up(site)
3
+ site.records.create_model :api_calls do |api_calls|
4
+ add_field :name, :string
5
+ add_field :http_method, :string
6
+ add_field :domain, :string
7
+ add_field :port, :integer, default: 80
8
+ add_field :path, :string
9
+ add_field :username, :string
10
+ add_field :password, :string
11
+ add_field :authentication, :enum, options: %w{basic digest}
12
+ add_field :mime_type, :string, default: 'json'
13
+ add_field :body, :text
14
+ add_field :body_layout, :string
15
+ add_field :function, :string
16
+ api_calls.record_class_name = 'APICall'
17
+ end
18
+ end
19
+
20
+ def self.down(site)
21
+ site.api_calls.destroy
22
+ end
23
+ end
@@ -0,0 +1,13 @@
1
+ class RedirectPageModelMigration < Migration
2
+ def self.up(site)
3
+ site.pages.create_model :redirect_page do |redirect_pages|
4
+ add_field :url, :string, searchable: false
5
+ add_one :page, show_blank: true, blank_text: 'None'
6
+ redirect_pages.record_class_name = 'RedirectPage'
7
+ end
8
+ end
9
+
10
+ def self.down(site)
11
+ site.redirect_pages.destroy
12
+ end
13
+ end
@@ -0,0 +1,20 @@
1
+ class MenuModelMigration < Migration
2
+ def self.up(site)
3
+ site.records.create_model :menu do |menus|
4
+ add_one :root, model: :page, validations: {required: {}}
5
+ add_field :include_root, :boolean, default: false
6
+ add_field :include_all_children, :boolean, default: true
7
+ add_field :depth, :integer, default: 0, validations: {required: {}}
8
+ add_embed_many :exceptions do
9
+ add_one :page
10
+ add_field :show, :boolean, default: false
11
+ add_field :depth, :integer, default: 0, validations: {required: {}}
12
+ end
13
+ menus.record_class_name = 'Menu'
14
+ end
15
+ end
16
+
17
+ def self.down(site)
18
+ site.menus.destroy
19
+ end
20
+ end
@@ -0,0 +1,326 @@
1
+ # (The MIT License)
2
+ #
3
+ # Copyright (c) 2009 Scott Chacon <schacon@gmail.com>
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining
6
+ # a copy of this software and associated documentation files (the
7
+ # 'Software'), to deal in the Software without restriction, including
8
+ # without limitation the rights to use, copy, modify, merge, publish,
9
+ # distribute, sublicense, and/or sell copies of the Software, and to
10
+ # permit persons to whom the Software is furnished to do so, subject to
11
+ # the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be
14
+ # included in all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
17
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20
+ # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21
+ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22
+ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ require 'zlib'
25
+ require 'rack/request'
26
+ require 'rack/response'
27
+ require 'rack/utils'
28
+ require 'time'
29
+
30
+ class GitHttp
31
+ class App
32
+
33
+ SERVICES = [
34
+ ["POST", 'service_rpc', "(.*?)/git-upload-pack$", 'upload-pack'],
35
+ ["POST", 'service_rpc', "(.*?)/git-receive-pack$", 'receive-pack'],
36
+
37
+ ["GET", 'get_info_refs', "(.*?)/info/refs$"],
38
+ ["GET", 'get_text_file', "(.*?)/HEAD$"],
39
+ ["GET", 'get_text_file', "(.*?)/objects/info/alternates$"],
40
+ ["GET", 'get_text_file', "(.*?)/objects/info/http-alternates$"],
41
+ ["GET", 'get_info_packs', "(.*?)/objects/info/packs$"],
42
+ ["GET", 'get_text_file', "(.*?)/objects/info/[^/]*$"],
43
+ ["GET", 'get_loose_object', "(.*?)/objects/[0-9a-f]{2}/[0-9a-f]{38}$"],
44
+ ["GET", 'get_pack_file', "(.*?)/objects/pack/pack-[0-9a-f]{40}\\.pack$"],
45
+ ["GET", 'get_idx_file', "(.*?)/objects/pack/pack-[0-9a-f]{40}\\.idx$"],
46
+ ]
47
+
48
+ def initialize(config = false)
49
+ set_config(config)
50
+ end
51
+
52
+ def set_config(config)
53
+ @config = config || {}
54
+ end
55
+
56
+ def set_config_setting(key, value)
57
+ @config[key] = value
58
+ end
59
+
60
+ def call(env)
61
+ @env = env
62
+ @req = Rack::Request.new(env)
63
+ cmd, path, @reqfile, @rpc = match_routing
64
+
65
+ return render_method_not_allowed if cmd == 'not_allowed'
66
+ return render_not_found if !cmd
67
+
68
+ @dir = get_git_dir(path)
69
+ return render_not_found if !@dir
70
+
71
+ Dir.chdir(@dir) do
72
+ self.method(cmd).call()
73
+ end
74
+ end
75
+
76
+ # ---------------------------------
77
+ # actual command handling functions
78
+ # ---------------------------------
79
+
80
+ def service_rpc
81
+ return render_no_access if !has_access(@rpc, true)
82
+ input = read_body
83
+
84
+ @res = Rack::Response.new
85
+ @res.status = 200
86
+ @res["Content-Type"] = "application/x-git-%s-result" % @rpc
87
+ @res.finish do
88
+ command = git_command("#{@rpc} --stateless-rpc #{@dir}")
89
+ IO.popen(command, File::RDWR) do |pipe|
90
+ pipe.write(input)
91
+ while !pipe.eof?
92
+ block = pipe.read(8192) # 8M at a time
93
+ @res.write block # steam it to the client
94
+ end
95
+ end
96
+ end
97
+ end
98
+
99
+ def get_info_refs
100
+ service_name = get_service_type
101
+
102
+ if has_access(service_name)
103
+ cmd = git_command("#{service_name} --stateless-rpc --advertise-refs .")
104
+ refs = `#{cmd}`
105
+
106
+ @res = Rack::Response.new
107
+ @res.status = 200
108
+ @res["Content-Type"] = "application/x-git-%s-advertisement" % service_name
109
+ hdr_nocache
110
+ @res.write(pkt_write("# service=git-#{service_name}\n"))
111
+ @res.write(pkt_flush)
112
+ @res.write(refs)
113
+ @res.finish
114
+ else
115
+ dumb_info_refs
116
+ end
117
+ end
118
+
119
+ def dumb_info_refs
120
+ update_server_info
121
+ send_file(@reqfile, "text/plain; charset=utf-8") do
122
+ hdr_nocache
123
+ end
124
+ end
125
+
126
+ def get_info_packs
127
+ # objects/info/packs
128
+ send_file(@reqfile, "text/plain; charset=utf-8") do
129
+ hdr_nocache
130
+ end
131
+ end
132
+
133
+ def get_loose_object
134
+ send_file(@reqfile, "application/x-git-loose-object") do
135
+ hdr_cache_forever
136
+ end
137
+ end
138
+
139
+ def get_pack_file
140
+ send_file(@reqfile, "application/x-git-packed-objects") do
141
+ hdr_cache_forever
142
+ end
143
+ end
144
+
145
+ def get_idx_file
146
+ send_file(@reqfile, "application/x-git-packed-objects-toc") do
147
+ hdr_cache_forever
148
+ end
149
+ end
150
+
151
+ def get_text_file
152
+ send_file(@reqfile, "text/plain") do
153
+ hdr_nocache
154
+ end
155
+ end
156
+
157
+ # ------------------------
158
+ # logic helping functions
159
+ # ------------------------
160
+
161
+ F = ::File
162
+
163
+ # some of this borrowed from the Rack::File implementation
164
+ def send_file(reqfile, content_type)
165
+ reqfile = File.join(@dir, reqfile)
166
+ return render_not_found if !F.exists?(reqfile)
167
+
168
+ @res = Rack::Response.new
169
+ @res.status = 200
170
+ @res["Content-Type"] = content_type
171
+ @res["Last-Modified"] = F.mtime(reqfile).httpdate
172
+
173
+ yield
174
+
175
+ if size = F.size?(reqfile)
176
+ @res["Content-Length"] = size.to_s
177
+ @res.finish do
178
+ F.open(reqfile, "rb") do |file|
179
+ while part = file.read(8192)
180
+ @res.write part
181
+ end
182
+ end
183
+ end
184
+ else
185
+ body = [F.read(reqfile)]
186
+ size = Rack::Utils.bytesize(body.first)
187
+ @res["Content-Length"] = size
188
+ @res.write body
189
+ @res.finish
190
+ end
191
+ end
192
+
193
+ def get_git_dir(path)
194
+ root = @config[:project_root] || `pwd`
195
+ path = File.join(root, path)
196
+ if File.exists?(path) # TODO: check is a valid git directory
197
+ return path
198
+ end
199
+ false
200
+ end
201
+
202
+ def get_service_type
203
+ service_type = @req.params['service']
204
+ return false if !service_type
205
+ return false if service_type[0, 4] != 'git-'
206
+ service_type.gsub('git-', '')
207
+ end
208
+
209
+ def match_routing
210
+ cmd = nil
211
+ path = nil
212
+ SERVICES.each do |method, handler, match, rpc|
213
+ if m = Regexp.new(match).match(@req.path_info)
214
+ return ['not_allowed'] if method != @req.request_method
215
+ cmd = handler
216
+ path = m[1]
217
+ file = @req.path_info.sub(path + '/', '')
218
+ return [cmd, path, file, rpc]
219
+ end
220
+ end
221
+ return nil
222
+ end
223
+
224
+ def has_access(rpc, check_content_type = false)
225
+ if check_content_type
226
+ return false if @req.content_type != "application/x-git-%s-request" % rpc
227
+ end
228
+ return false if !['upload-pack', 'receive-pack'].include? rpc
229
+ if rpc == 'receive-pack'
230
+ return @config[:receive_pack] if @config.include? :receive_pack
231
+ end
232
+ if rpc == 'upload-pack'
233
+ return @config[:upload_pack] if @config.include? :upload_pack
234
+ end
235
+ return get_config_setting(rpc)
236
+ end
237
+
238
+ def get_config_setting(service_name)
239
+ service_name = service_name.gsub('-', '')
240
+ setting = get_git_config("http.#{service_name}")
241
+ if service_name == 'uploadpack'
242
+ return setting != 'false'
243
+ else
244
+ return setting == 'true'
245
+ end
246
+ end
247
+
248
+ def get_git_config(config_name)
249
+ cmd = git_command("config #{config_name}")
250
+ `#{cmd}`.chomp
251
+ end
252
+
253
+ def read_body
254
+ if @env["HTTP_CONTENT_ENCODING"] =~ /gzip/
255
+ input = Zlib::GzipReader.new(@req.body).read
256
+ else
257
+ input = @req.body.read
258
+ end
259
+ end
260
+
261
+ def update_server_info
262
+ cmd = git_command("update-server-info")
263
+ `#{cmd}`
264
+ end
265
+
266
+ def git_command(command)
267
+ git_bin = @config[:git_path] || 'git'
268
+ command = "#{git_bin} #{command}"
269
+ command
270
+ end
271
+
272
+ # --------------------------------------
273
+ # HTTP error response handling functions
274
+ # --------------------------------------
275
+
276
+ PLAIN_TYPE = {"Content-Type" => "text/plain"}
277
+
278
+ def render_method_not_allowed
279
+ if @env['SERVER_PROTOCOL'] == "HTTP/1.1"
280
+ [405, PLAIN_TYPE, ["Method Not Allowed"]]
281
+ else
282
+ [400, PLAIN_TYPE, ["Bad Request"]]
283
+ end
284
+ end
285
+
286
+ def render_not_found
287
+ [404, PLAIN_TYPE, ["Not Found"]]
288
+ end
289
+
290
+ def render_no_access
291
+ [403, PLAIN_TYPE, ["Forbidden"]]
292
+ end
293
+
294
+
295
+ # ------------------------------
296
+ # packet-line handling functions
297
+ # ------------------------------
298
+
299
+ def pkt_flush
300
+ '0000'
301
+ end
302
+
303
+ def pkt_write(str)
304
+ (str.size + 4).to_s(base=16).rjust(4, '0') + str
305
+ end
306
+
307
+
308
+ # ------------------------
309
+ # header writing functions
310
+ # ------------------------
311
+
312
+ def hdr_nocache
313
+ @res["Expires"] = "Fri, 01 Jan 1980 00:00:00 GMT"
314
+ @res["Pragma"] = "no-cache"
315
+ @res["Cache-Control"] = "no-cache, max-age=0, must-revalidate"
316
+ end
317
+
318
+ def hdr_cache_forever
319
+ now = Time.now().to_i
320
+ @res["Date"] = now.to_s
321
+ @res["Expires"] = (now + 31536000).to_s;
322
+ @res["Cache-Control"] = "public, max-age=31536000";
323
+ end
324
+
325
+ end
326
+ end
@@ -0,0 +1,38 @@
1
+ class GitPage < Page
2
+ respond_to :get do
3
+ with :html do
4
+ process
5
+ end
6
+ end
7
+
8
+ respond_to :post do
9
+ with :html do
10
+ process
11
+ end
12
+ end
13
+
14
+ private
15
+ def process
16
+ prompt_login(:basic) and return unless logged_in?(:basic)
17
+
18
+ # path is: /git/SITE_ID/GIT_PATH
19
+ components = params['glob'].split('/')[1..-1]
20
+ site_id = components.shift
21
+ status(404) and return if site_id.blank?
22
+
23
+ # try and load the site and ensure the user is authorised to access it
24
+ git_site = Site.find(BSON::ObjectId.from_string(site_id))
25
+ status(404) and return if git_site.nil?
26
+ status(403) unless current_user.sites.include?(git_site)
27
+
28
+ # reconstruct the path to match the on disk repository structure
29
+ env['PATH_INFO'] = "/#{site_id}/.git/#{components.join('/')}"
30
+ @_response = GitHttp::App.new({
31
+ project_root: Yodel.config.sites_root,
32
+ git_path: Yodel.config.git_path,
33
+ upload_pack: true,
34
+ receive_pack: true
35
+ }).call(env)
36
+ @finished = true
37
+ end
38
+ end
@@ -0,0 +1,13 @@
1
+ class ProductionLoginPage < LoginPage
2
+ respond_to :post do
3
+ with :html do
4
+ # production user overrides passwords_match? to compare passwords without hashing the
5
+ # password being tested. Yodel development clients store the password as a hash so
6
+ # there's no need to hash the password again server side. For html clients logging in,
7
+ # passwords will be in plain text; the password is hashed here, then control passed up
8
+ # for login to proceed as normal.
9
+ params[password_field] = Password.hashed_password(nil, params[password_field])
10
+ super()
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,127 @@
1
+ class ProductionSitesPage < RecordProxyPage
2
+ # record proxy pages deal with site models (site.model_name). Override the methods it uses
3
+ # to interact with these models we can edit Sites (a non site model)
4
+ def find_record(id)
5
+ Site.find(id)
6
+ end
7
+
8
+ def all_records
9
+ Site.all.reject {|site| site.name == 'yodel'}
10
+ end
11
+
12
+ def construct_record
13
+ Site.new
14
+ end
15
+
16
+
17
+ # get list of sites
18
+ respond_to :get do
19
+ with :json do
20
+ prompt_login and return {success: false, reason: 'Login required'} unless logged_in?
21
+
22
+ if params['id']
23
+ # authorisation
24
+ return {success: false, reason: 'Site not found'} if record.nil?
25
+ return {success: false, reason: 'Unauthorised'} unless current_user.sites.include?(record)
26
+
27
+ # users are represented by name and email address; id is used to distinguish between users
28
+ # client side when a user's name or email is changed (the id remains constant)
29
+ site_users = users.where(sites: requested_site.id).all.collect do |user|
30
+ {id: user.id, name: user.name, email: user.email}
31
+ end
32
+
33
+ {success: true, domains: requested_site.domains, users: site_users, latest_revision: requested_site.latest_revision}
34
+
35
+ else
36
+ # if no site is requested, send back a list of sites available to the user
37
+ {success: true, sites: current_user.sites.collect {|site| {id: site.id, name: site.name}}}
38
+ end
39
+ end
40
+ end
41
+
42
+
43
+ # create a site
44
+ respond_to :post do
45
+ with :json do
46
+ prompt_login and return {success: false, reason: 'Login required'} unless logged_in?
47
+
48
+ # create the site record
49
+ new_site = Site.new
50
+ new_site.root_directory = File.join(Yodel.config.sites_root, new_site.id.to_s)
51
+ return {success: false, reason: 'Unable to create site'} unless new_site.save
52
+
53
+ # initialise the root directory as a git repository
54
+ unless `#{Yodel.config.git_path} init #{new_site.root_directory}` =~ /^Initialized empty Git repository/
55
+ new_site.destroy
56
+ return {success: false, reason: 'Unable to create git repository'}
57
+ end
58
+
59
+ # install the post receive hook to deploy the site on pushes, and
60
+ # allow pushes to a non bare repository
61
+ Dir.chdir(new_site.root_directory) do
62
+ unless `#{Yodel.config.git_path} config 'receive.denyCurrentBranch' 'ignore'`.strip.empty?
63
+ new_site.destroy
64
+ return {success: false, reason: 'Unable to configure git repository'}
65
+ end
66
+
67
+ # FIXME: need error checking here
68
+ post_receive_script = File.join(new_site.root_directory, '.git', 'hooks', 'post-receive')
69
+ File.open(post_receive_script, 'w') do |file|
70
+ file.write "#!/bin/bash
71
+ cd ..
72
+ env -i git reset --hard
73
+ #{Yodel.config.deploy_command} deploy `basename $PWD`
74
+ "
75
+ end
76
+ FileUtils.chmod(0755, post_receive_script)
77
+ end
78
+
79
+ # allow the current user to administer the new site
80
+ current_user.sites << new_site
81
+ unless current_user.save
82
+ new_site.destroy
83
+ return {success: false, reason: 'Unable to update user'}
84
+ end
85
+
86
+ {success: true, id: new_site.id}
87
+ end
88
+ end
89
+
90
+
91
+ # update a site; currently only supports adding/removing users to a site
92
+ respond_to :put do
93
+ with :json do
94
+ prompt_login and return {success: false, reason: 'Login required'} unless logged_in?
95
+
96
+ # authorisation
97
+ return {success: false, reason: 'Site not found'} if record.nil?
98
+ return {success: false, reason: 'Unauthorised'} unless current_user.sites.include?(record)
99
+
100
+ # find the user by id or email
101
+ if params['user_id']
102
+ user = site.users.find(BSON::ObjectId.from_string(params['user_id']))
103
+ elsif params['user_email']
104
+ user = site.users.where(email: params['user_email']).first
105
+ else
106
+ return {success: false, reason: 'No user id or email address provided'}
107
+ end
108
+ return {success: false, reason: 'Unknown user'} if user.nil?
109
+
110
+ # add or remove the user from the site
111
+ case params['action']
112
+ when 'add'
113
+ user.sites << record
114
+ when 'remove'
115
+ user.sites.delete(record)
116
+ else
117
+ return {success: false, reason: 'Unknown action'}
118
+ end
119
+
120
+ if user.save
121
+ {success: true}
122
+ else
123
+ {success: false, reason: 'Save failed'}
124
+ end
125
+ end
126
+ end
127
+ end