yodel_production_environment 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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