cyclid 0.2.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 (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}"