cyclid 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +174 -0
  3. data/README.md +54 -0
  4. data/app/cyclid.rb +61 -0
  5. data/app/cyclid/config.rb +38 -0
  6. data/app/cyclid/controllers.rb +123 -0
  7. data/app/cyclid/controllers/auth.rb +34 -0
  8. data/app/cyclid/controllers/auth/token.rb +78 -0
  9. data/app/cyclid/controllers/health.rb +96 -0
  10. data/app/cyclid/controllers/organizations.rb +104 -0
  11. data/app/cyclid/controllers/organizations/collection.rb +134 -0
  12. data/app/cyclid/controllers/organizations/config.rb +128 -0
  13. data/app/cyclid/controllers/organizations/document.rb +135 -0
  14. data/app/cyclid/controllers/organizations/job.rb +266 -0
  15. data/app/cyclid/controllers/organizations/members.rb +145 -0
  16. data/app/cyclid/controllers/organizations/stages.rb +251 -0
  17. data/app/cyclid/controllers/users.rb +47 -0
  18. data/app/cyclid/controllers/users/collection.rb +131 -0
  19. data/app/cyclid/controllers/users/document.rb +133 -0
  20. data/app/cyclid/health_helpers.rb +40 -0
  21. data/app/cyclid/job.rb +3 -0
  22. data/app/cyclid/job/helpers.rb +67 -0
  23. data/app/cyclid/job/job.rb +164 -0
  24. data/app/cyclid/job/runner.rb +275 -0
  25. data/app/cyclid/job/stage.rb +67 -0
  26. data/app/cyclid/log_buffer.rb +104 -0
  27. data/app/cyclid/models.rb +3 -0
  28. data/app/cyclid/models/job_record.rb +25 -0
  29. data/app/cyclid/models/organization.rb +64 -0
  30. data/app/cyclid/models/plugin_config.rb +25 -0
  31. data/app/cyclid/models/stage.rb +42 -0
  32. data/app/cyclid/models/step.rb +29 -0
  33. data/app/cyclid/models/user.rb +60 -0
  34. data/app/cyclid/models/user_permissions.rb +28 -0
  35. data/app/cyclid/monkey_patches.rb +37 -0
  36. data/app/cyclid/plugin_registry.rb +75 -0
  37. data/app/cyclid/plugins.rb +125 -0
  38. data/app/cyclid/plugins/action.rb +48 -0
  39. data/app/cyclid/plugins/action/command.rb +89 -0
  40. data/app/cyclid/plugins/action/email.rb +207 -0
  41. data/app/cyclid/plugins/action/email/html.erb +58 -0
  42. data/app/cyclid/plugins/action/email/text.erb +13 -0
  43. data/app/cyclid/plugins/action/script.rb +90 -0
  44. data/app/cyclid/plugins/action/slack.rb +129 -0
  45. data/app/cyclid/plugins/action/slack/note.erb +5 -0
  46. data/app/cyclid/plugins/api.rb +195 -0
  47. data/app/cyclid/plugins/api/github.rb +111 -0
  48. data/app/cyclid/plugins/api/github/callback.rb +66 -0
  49. data/app/cyclid/plugins/api/github/methods.rb +201 -0
  50. data/app/cyclid/plugins/api/github/status.rb +67 -0
  51. data/app/cyclid/plugins/builder.rb +80 -0
  52. data/app/cyclid/plugins/builder/mist.rb +107 -0
  53. data/app/cyclid/plugins/dispatcher.rb +89 -0
  54. data/app/cyclid/plugins/dispatcher/local.rb +167 -0
  55. data/app/cyclid/plugins/provisioner.rb +40 -0
  56. data/app/cyclid/plugins/provisioner/debian.rb +90 -0
  57. data/app/cyclid/plugins/provisioner/ubuntu.rb +98 -0
  58. data/app/cyclid/plugins/source.rb +39 -0
  59. data/app/cyclid/plugins/source/git.rb +64 -0
  60. data/app/cyclid/plugins/transport.rb +63 -0
  61. data/app/cyclid/plugins/transport/ssh.rb +155 -0
  62. data/app/cyclid/sinatra/api_helpers.rb +66 -0
  63. data/app/cyclid/sinatra/auth_helpers.rb +127 -0
  64. data/app/cyclid/sinatra/warden/strategies/api_token.rb +62 -0
  65. data/app/cyclid/sinatra/warden/strategies/basic.rb +58 -0
  66. data/app/cyclid/sinatra/warden/strategies/hmac.rb +76 -0
  67. data/app/db.rb +51 -0
  68. data/bin/cyclid-db-init +107 -0
  69. data/db/schema.rb +92 -0
  70. data/lib/cyclid/app.rb +4 -0
  71. metadata +407 -0
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+ # Copyright 2016 Liqwyd Ltd.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ # Top level module for the core Cyclid code.
17
+ module Cyclid
18
+ # Module for the Cyclid API
19
+ module API
20
+ # Some constants to identify types of API operation
21
+ module Operations
22
+ # Read operations
23
+ READ = 1
24
+ # Write (Create, Update, Delete) operations
25
+ WRITE = 2
26
+ # Administrator operations
27
+ ADMIN = 3
28
+ end
29
+
30
+ # Sinatra Warden AuthN/AuthZ helpers
31
+ module AuthHelpers
32
+ # Return an HTTP error with a RESTful JSON response
33
+ # XXX Should probably be in ApiHelpers?
34
+ def halt_with_json_response(error, id, description)
35
+ halt error, json_response(id, description)
36
+ end
37
+
38
+ # Call the Warden authenticate! method
39
+ def authenticate!
40
+ env['warden'].authenticate!
41
+ end
42
+
43
+ # Authenticate the user, then ensure that the user is authorized for
44
+ # the given organization and operation
45
+ def authorized_for!(org_name, operation)
46
+ authenticate!
47
+
48
+ user = current_user
49
+
50
+ # Promote the organization to 'admins' if the user is a SuperAdmin
51
+ org_name = 'admins' if super_admin?(user)
52
+
53
+ begin
54
+ organization = user.organizations.find_by(name: org_name)
55
+ halt_with_json_response(401, Errors::HTTPErrors::AUTH_FAILURE, 'unauthorized') \
56
+ if organization.nil?
57
+ Cyclid.logger.debug "authorized_for! organization: #{organization.name}"
58
+
59
+ # Check what Permissions are applied to the user for this Org & match
60
+ # against operation
61
+ permissions = user.userpermissions.find_by(organization: organization)
62
+ Cyclid.logger.debug "authorized_for! #{permissions.inspect}"
63
+
64
+ # Admins have full authority, regardless of the operation
65
+ return true if permissions.admin
66
+ return true if operation == Operations::WRITE && permissions.write
67
+ return true if operation == Operations::READ && (permissions.write || permissions.read)
68
+
69
+ Cyclid.logger.info "user #{user.username} is not authorized for operation #{operation}"
70
+
71
+ halt_with_json_response(401, Errors::HTTPErrors::AUTH_FAILURE, 'unauthorized')
72
+ rescue StandardError => ex # XXX: Use a more specific rescue
73
+ Cyclid.logger.info "authorization failed: #{ex}"
74
+ halt_with_json_response(401, Errors::HTTPErrors::AUTH_FAILURE, 'unauthorized')
75
+ end
76
+ end
77
+
78
+ # Authenticate the user, then ensure that the user is an admin and
79
+ # authorized for the resource for the given username & operation
80
+ def authorized_admin!(operation)
81
+ authorized_for!('admins', operation)
82
+ end
83
+
84
+ # Authenticate the user, then ensure that the user is authorized for the
85
+ # resource for the given username & operation
86
+ def authorized_as!(username, operation)
87
+ authenticate!
88
+
89
+ user = current_user
90
+
91
+ # Users are always authorized for any operation on their own data
92
+ return true if user.username == username
93
+
94
+ # Super Admins may be authorized, depending on the operation
95
+ if super_admin?(user)
96
+ begin
97
+ organization = user.organizations.find_by(name: 'admins')
98
+ permissions = user.userpermissions.find_by(organization: organization)
99
+ Cyclid.logger.debug permissions
100
+
101
+ # Admins have full authority, regardless of the operation
102
+ return true if permissions.admin
103
+ return true if operation == Operations::WRITE && permissions.write
104
+ return true if operation == Operations::READ && (permissions.write || permissions.read)
105
+ rescue StandardError => ex # XXX: Use a more specific rescue
106
+ Cyclid.logger.info "authorization failed: #{ex}"
107
+ halt_with_json_response(401, Errors::HTTPErrors::AUTH_FAILURE, 'unauthorized')
108
+ end
109
+
110
+ end
111
+
112
+ halt_with_json_response(401, Errors::HTTPErrors::AUTH_FAILURE, 'unauthorized')
113
+ end
114
+
115
+ # Check if the given user is a Super Admin; any user that belongs to the
116
+ # 'admins' organization is a super admin
117
+ def super_admin?(user)
118
+ user.organizations.exists?(name: 'admins')
119
+ end
120
+
121
+ # Current User object from the session
122
+ def current_user
123
+ env['warden'].user
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+ # Copyright 2016 Liqwyd Ltd.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ require 'warden'
17
+ require 'jwt'
18
+
19
+ # Top level module for the core Cyclid code.
20
+ module Cyclid
21
+ # Module for the Cyclid API
22
+ module API
23
+ # Warden Strategies
24
+ module Strategies
25
+ # API Token based strategy
26
+ module APIToken
27
+ # Authenticate via. an API token
28
+ Warden::Strategies.add(:api_token) do
29
+ def valid?
30
+ request.env['HTTP_AUTHORIZATION'].is_a? String and \
31
+ request.env['HTTP_AUTHORIZATION'] =~ /^Token .*$/
32
+ end
33
+
34
+ def authenticate!
35
+ begin
36
+ authorization = request.env['HTTP_AUTHORIZATION']
37
+ username, token = authorization.match(/^Token (.*):(.*)$/).captures
38
+ rescue
39
+ fail! 'invalid API token'
40
+ end
41
+
42
+ user = User.find_by(username: username)
43
+ fail! 'invalid user' if user.nil?
44
+
45
+ begin
46
+ # Decode the token
47
+ token_data = JWT.decode token, user.secret, true, algorithm: 'HS256'
48
+ claims = token_data.first
49
+ if claims['sub'] == user.username
50
+ success! user
51
+ else
52
+ fail! 'invalid user'
53
+ end
54
+ rescue
55
+ fail! 'invalid API token'
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+ # Copyright 2016 Liqwyd Ltd.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ require 'warden'
17
+ require 'bcrypt'
18
+
19
+ # Top level module for the core Cyclid code.
20
+ module Cyclid
21
+ # Module for the Cyclid API
22
+ module API
23
+ # Warden Strategies
24
+ module Strategies
25
+ # HTTTP Basic authentication based strategy
26
+ module Basic
27
+ # Authenticate via. HTTP Basic auth.
28
+ Warden::Strategies.add(:basic) do
29
+ def valid?
30
+ request.env['HTTP_AUTHORIZATION'].is_a? String and \
31
+ request.env['HTTP_AUTHORIZATION'] =~ /^Basic .*$/
32
+ end
33
+
34
+ def authenticate!
35
+ begin
36
+ authorization = request.env['HTTP_AUTHORIZATION']
37
+ digest = authorization.match(/^Basic (.*)$/).captures.first
38
+
39
+ user_pass = Base64.decode64(digest)
40
+ username, password = user_pass.split(':')
41
+ rescue
42
+ fail! 'invalid digest'
43
+ end
44
+
45
+ user = User.find_by(username: username)
46
+ if user.nil?
47
+ fail! 'invalid user'
48
+ elsif BCrypt::Password.new(user.password).is_password? password
49
+ success! user
50
+ else
51
+ fail! 'invalid user'
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+ # Copyright 2016 Liqwyd Ltd.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ require 'warden'
17
+
18
+ # Top level module for the core Cyclid code.
19
+ module Cyclid
20
+ # Module for the Cyclid API
21
+ module API
22
+ # Warden Strategies
23
+ module Strategies
24
+ # HMAC based strategy
25
+ module HMAC
26
+ # Authenticate via. HMAC
27
+ Warden::Strategies.add(:hmac) do
28
+ def valid?
29
+ request.env['HTTP_AUTHORIZATION'].is_a? String and \
30
+ request.env['HTTP_AUTHORIZATION'] =~ /^HMAC .*$/
31
+ end
32
+
33
+ def authenticate!
34
+ begin
35
+ authorization = request.env['HTTP_AUTHORIZATION']
36
+ username, hmac = authorization.match(/^HMAC (.*):(.*)$/).captures
37
+
38
+ # The nonce may be empty; that isn't an error and the signature
39
+ # will validate with a Nil nonce
40
+ nonce = request.env['HTTP_X_HMAC_NONCE']
41
+ rescue
42
+ fail! 'invalid HMAC'
43
+ end
44
+
45
+ user = User.find_by(username: username)
46
+ fail! 'invalid user' if user.nil?
47
+
48
+ begin
49
+ method = request.env['REQUEST_METHOD']
50
+ path = request.env['PATH_INFO']
51
+ date = request.env['HTTP_DATE']
52
+
53
+ Cyclid.logger.debug "user=#{user.username} method=#{method} path=#{path} \
54
+ date=#{date} HMAC=#{hmac} nonce=#{nonce}"
55
+
56
+ signer = Cyclid::HMAC::Signer.new
57
+ if signer.validate_signature(hmac,
58
+ secret: user.secret,
59
+ method: method,
60
+ path: path,
61
+ date: date,
62
+ nonce: nonce)
63
+ success! user
64
+ else
65
+ fail! 'invalid user'
66
+ end
67
+ rescue StandardError => ex
68
+ Cyclid.logger.debug "failure during HMAC authentication: #{ex}"
69
+ fail! 'invalid headers'
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+ # Copyright 2016 Liqwyd Ltd.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ require 'active_record'
17
+ require 'logger'
18
+
19
+ begin
20
+ case ENV['RACK_ENV']
21
+ when 'development'
22
+ database = if defined? Cyclid
23
+ Cyclid.config.database
24
+ else
25
+ 'sqlite3:development.db'
26
+ end
27
+
28
+ ActiveRecord::Base.establish_connection(
29
+ database
30
+ )
31
+
32
+ ActiveRecord::Base.logger = if defined? Cyclid
33
+ Cyclid.logger
34
+ else
35
+ Logger.new(STDERR)
36
+ end
37
+ when 'production'
38
+ ActiveRecord::Base.establish_connection(
39
+ Cyclid.config.database
40
+ )
41
+
42
+ ActiveRecord::Base.logger = Cyclid.logger
43
+
44
+ Cyclid.logger.level = Logger::INFO
45
+ when 'test'
46
+ Cyclid.logger.info 'In test mode; not creating database connection'
47
+ end
48
+
49
+ rescue StandardError => ex
50
+ abort "Failed to initialize ActiveRecord: #{ex}"
51
+ end
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+ # Copyright 2016 Liqwyd Ltd.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ $LOAD_PATH.push File.expand_path('../../app', __FILE__)
18
+
19
+ require 'require_all'
20
+ require 'logger'
21
+ require 'active_record'
22
+ require 'securerandom'
23
+
24
+ ENV['RACK_ENV'] = ENV['RACK_ENV'] || 'development'
25
+
26
+ require 'cyclid/config'
27
+
28
+ # Top level module for the core Cyclid code; just stub out to provide the
29
+ # bare minimum required to inject data via. the models.
30
+ module Cyclid
31
+ class << self
32
+ attr_accessor :logger, :config
33
+
34
+ begin
35
+ Cyclid.logger = Logger.new(STDERR)
36
+
37
+ config_path = ENV['CYCLID_CONFIG'] || File.join(%w(/ etc cyclid config))
38
+ Cyclid.config = API::Config.new(config_path)
39
+ rescue StandardError => ex
40
+ abort "Failed to initialize: #{ex}"
41
+ end
42
+ end
43
+ end
44
+
45
+ require 'db'
46
+ require 'cyclid/models'
47
+
48
+ require_relative '../db/schema.rb'
49
+
50
+ include Cyclid::API
51
+
52
+ ADMINS_ORG = 'admins'
53
+ RSA_KEY_LENGTH = 2048
54
+
55
+ def generate_password
56
+ (('a'..'z').to_a.concat \
57
+ ('A'..'Z').to_a.concat \
58
+ ('0'..'9').to_a.concat \
59
+ %w($ % ^ & * _)).sample(8).join
60
+ end
61
+
62
+ def create_admin_user
63
+ secret = SecureRandom.hex(32)
64
+ password = generate_password
65
+ user = User.new
66
+ user.username = 'admin'
67
+ user.email = 'admin@example.com'
68
+ user.secret = secret
69
+ user.new_password = password
70
+ user.save!
71
+
72
+ [secret, password]
73
+ end
74
+
75
+ def create_admin_organization
76
+ key = OpenSSL::PKey::RSA.new(RSA_KEY_LENGTH)
77
+
78
+ org = Organization.new
79
+ org.name = ADMINS_ORG
80
+ org.owner_email = 'admins@example.com'
81
+ org.rsa_private_key = key.to_der
82
+ org.rsa_public_key = key.public_key.to_der
83
+ org.salt = SecureRandom.hex(32)
84
+ org.users << User.find_by(username: 'admin')
85
+ end
86
+
87
+ def update_user_perms
88
+ # 'admin' user is a Super Admin
89
+ user = User.find_by(username: 'admin')
90
+ organization = user.organizations.find_by(name: ADMINS_ORG)
91
+ permissions = user.userpermissions.find_by(organization: organization)
92
+ Cyclid.logger.debug permissions
93
+
94
+ permissions.admin = true
95
+ permissions.write = true
96
+ permissions.read = true
97
+ permissions.save!
98
+ end
99
+
100
+ secret, password = create_admin_user
101
+ create_admin_organization
102
+
103
+ update_user_perms
104
+
105
+ STDERR.puts '*' * 80
106
+ STDERR.puts "Admin secret: #{secret}"
107
+ STDERR.puts "Admin password: #{password}"