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,34 @@
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_rel 'auth/*.rb'
17
+
18
+ # Top level module for all of the core Cyclid code.
19
+ module Cyclid
20
+ # Module for the Cyclid API
21
+ module API
22
+ # Controller for all Auth related API endpoints
23
+ class AuthController < ControllerBase
24
+ register Sinatra::Namespace
25
+
26
+ namespace '/token' do
27
+ register Auth::Token
28
+ end
29
+ end
30
+
31
+ # Register this controller
32
+ Cyclid.controllers << AuthController
33
+ end
34
+ end
@@ -0,0 +1,78 @@
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 'jwt'
17
+
18
+ # Top level module for all of the core Cyclid code.
19
+ module Cyclid
20
+ # Module for the Cyclid API
21
+ module API
22
+ # Module for all Auth related API endpoints
23
+ module Auth
24
+ # API endpoints for managing API tokens
25
+ # @api REST
26
+ module Token
27
+ # @!group Tokens
28
+
29
+ # @!method post_token_username
30
+ # @overload POST /token/:username
31
+ # @param [String] username Username of the user to generate a token for.
32
+ # @macro rest
33
+ # Generate a JSON Web Token for use with the Token authentication scheme.
34
+ # The user must authenticate using one of the other available methods
35
+ # (HTTP Basic or HMAC) to obtain a token.
36
+ # @return A JWT token.
37
+ # @return [404] The user does not exist
38
+
39
+ # @!endgroup
40
+
41
+ # Sinatra callback
42
+ # @private
43
+ def self.registered(app)
44
+ include Errors::HTTPErrors
45
+
46
+ app.post '/:username' do
47
+ authorized_as!(params[:username], Operations::READ)
48
+
49
+ payload = parse_request_body
50
+ Cyclid.logger.debug payload
51
+
52
+ user = User.find_by(username: params[:username])
53
+ halt_with_json_response(404, INVALID_USER, 'user does not exist') \
54
+ if user.nil?
55
+
56
+ # Create a JSON Web Token. Use the provided payload as the intial
57
+ # set of claims but remove some of the standard claims we don't
58
+ # want users to be able to set.
59
+ #
60
+ # Requests MAY set 'exp' and 'nbf' if they wish.
61
+ payload.delete_if{ |k, _v| %w(iss aud jti iat sub).include? k }
62
+
63
+ # If 'exp' was not set, set it now. Default is +6 hours.
64
+ payload['exp'] = Time.now.to_i + 21_600_000 unless payload.key? 'exp'
65
+ # Subject is this user
66
+ payload['sub'] = params[:username]
67
+
68
+ # Create the token; use the users HMAC key as the signing key
69
+ token = JWT.encode payload, user.secret, 'HS256'
70
+
71
+ token_hash = { token: token }
72
+ return token_hash.to_json
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,96 @@
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 'sinatra-health-check'
17
+
18
+ # Top level module for all of the core Cyclid code.
19
+ module Cyclid
20
+ # Module for the Cyclid API
21
+ module API
22
+ # Controller for all Health related API endpoints
23
+ # @api REST
24
+ class HealthController < ControllerBase
25
+ include Errors::HTTPErrors
26
+
27
+ def initialize(_app)
28
+ super
29
+ @checker = SinatraHealthCheck::Checker.new(logger: Cyclid.logger,
30
+ timeout: 0)
31
+
32
+ # Add internal health checks
33
+ @checker.systems[:database] = Cyclid::API::Health::Database
34
+
35
+ # Add each plugin, which can choose to provide a healthcheck by
36
+ # implementing #status
37
+ Cyclid.plugins.all.each do |plugin|
38
+ name = "#{plugin.human_name}_#{plugin.name}".to_sym
39
+ @checker.systems[name] = plugin
40
+ end
41
+ end
42
+
43
+ # @!group Health
44
+
45
+ # @!method get_health_status
46
+ # @overload GET /health/status
47
+ # @macro rest
48
+ # Return either 200 (healthy) or 503 (unhealthy) based on the status of
49
+ # the healthchecks. This is intended to be used by things like load
50
+ # balancers and active monitors.
51
+ # @return [200] Application is healthy.
52
+ # @return [503] Application is unhealthy.
53
+ get '/health/status' do
54
+ @checker.healthy? ? 200 : 503
55
+ end
56
+
57
+ # @!method get_health_info
58
+ # @overload GET /health/info
59
+ # @macro rest
60
+ # Return verbose information on the status of the individual checks;
61
+ # note that this method always returns 200 with a message body, so it is
62
+ # not suitable for general health checks unless the caller intends to
63
+ # parse the message body for the health status.
64
+ # @return JSON description of the individual health check statuses.
65
+ get '/health/info' do
66
+ @checker.status.to_json
67
+ end
68
+
69
+ # @!endgroup
70
+ end
71
+
72
+ # Register this controller
73
+ Cyclid.controllers << HealthController
74
+
75
+ # Healthchecks
76
+ module Health
77
+ # Internal database connection health check
78
+ module Database
79
+ # Check that ActiveRecord can connect to the database
80
+ def self.status
81
+ connected = begin
82
+ ActiveRecord::Base.connection_pool.with_connection(&:active?)
83
+ rescue
84
+ false
85
+ end
86
+
87
+ if connected
88
+ SinatraHealthCheck::Status.new(:ok, 'database connection is okay')
89
+ else
90
+ SinatraHealthCheck::Status.new(:error, 'database is not connected')
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,104 @@
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_rel 'organizations/*.rb'
17
+
18
+ # Top level module for all of the core Cyclid code.
19
+ module Cyclid
20
+ # Module for the Cyclid API
21
+ module API
22
+ # Controller for all Organization related API endpoints
23
+ class OrganizationController < ControllerBase
24
+ helpers do
25
+ # Clean up stage data
26
+ def sanitize_stage(stage)
27
+ stage.delete_if do |key, _value|
28
+ key == 'organization_id' || key == 'id'
29
+ end
30
+ end
31
+
32
+ # Clean up step data
33
+ def sanitize_step(step)
34
+ Cyclid.logger.debug step.inspect
35
+
36
+ # Re-factor the step definition back into the original. The internal
37
+ # representation carries a lot of extra metadata that users don't
38
+ # need to worry about; the original definition is actually the
39
+ # 'action' object inside of the step. The original 'action' key can
40
+ # be reversed from the Action object name, without having to
41
+ # unserialize the object
42
+ #
43
+ # Note that we use JSON and not Oj; if we try to use Oj it will
44
+ # attempt to unserialise the object, which is not what we want.
45
+ action = JSON.parse(step['action'])
46
+
47
+ # Reverse the action name from the object name and remove the object
48
+ # name
49
+ action['action'] = action['^o'].split('::').last.downcase
50
+ action.delete('^o')
51
+
52
+ return action
53
+ end
54
+
55
+ # Remove sensitive data from the organization data
56
+ def sanitize_organization(org)
57
+ org.delete_if do |key, _value|
58
+ key == 'rsa_private_key' || key == 'rsa_public_key' || key == 'salt'
59
+ end
60
+ end
61
+ end
62
+
63
+ register Sinatra::Namespace
64
+
65
+ namespace '/organizations' do
66
+ register Organizations::Collection
67
+
68
+ namespace '/:name' do
69
+ register Organizations::Document
70
+
71
+ namespace '/members' do
72
+ register Organizations::Members
73
+ end
74
+
75
+ namespace '/stages' do
76
+ register Organizations::Stages
77
+ end
78
+
79
+ namespace '/jobs' do
80
+ register Organizations::Jobs
81
+ end
82
+
83
+ namespace '/configs/:type' do
84
+ register Organizations::Configs
85
+ end
86
+
87
+ namespace '/plugins' do
88
+ Cyclid.plugins.all(Cyclid::API::Plugins::Api).each do |plugin|
89
+ Cyclid.logger.debug "Registering API extension plugin #{plugin.name}"
90
+
91
+ # Create a namespace for this plugin and register it
92
+ namespace "/#{plugin.name}" do
93
+ ctrl = plugin.controller
94
+ register ctrl
95
+ helpers ctrl.plugin_methods
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ Cyclid.controllers << OrganizationController
103
+ end
104
+ end
@@ -0,0 +1,134 @@
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 all of the core Cyclid code.
17
+ module Cyclid
18
+ # Module for the Cyclid API
19
+ module API
20
+ # Module for all Organization related API endpoints
21
+ module Organizations
22
+ # API endpoints for the Organization collection
23
+ # @api REST
24
+ module Collection
25
+ # @!group Organizations
26
+
27
+ # @!method get_organizations
28
+ # @overload GET /organizations
29
+ # @macro rest
30
+ # Get all of the organizations.
31
+ # @return List of organizations
32
+ # @example Get a list of organizations
33
+ # GET /organizations => [{"id": 1, "name": "example", "owner_email": "admin@example.com"}]
34
+ # @see get_organizations_organization
35
+
36
+ # @!method post_organizations(body)
37
+ # @overload POST /organizations
38
+ # @macro rest
39
+ # Create a new organization.
40
+ # @param [JSON] body New organization
41
+ # @option body [String] name Name of the new organization
42
+ # @option body [String] owner_email Email address of the organization owner
43
+ # @option body [Array<String>] users ([]) List of users to add to the organization
44
+ # @return [200] Organization was created successfully
45
+ # @return [404] A user in the list of members does not exist
46
+ # @return [409] An organization with that name already exists
47
+ # @example Create a new organization with user1 & user2 as members
48
+ # POST /organizations <= {"name": "example",
49
+ # "owner_email": "admin@example.com",
50
+ # "users": ["user1", "user2"]}
51
+ # ***
52
+ # @example Create a new organization with no users as members
53
+ # POST /organizations <= {"name": "example",
54
+ # "owner_email": "admin@example.com"}
55
+ # ***
56
+
57
+ # @!endgroup
58
+
59
+ # Sinatra callback
60
+ # @private
61
+ def self.registered(app)
62
+ include Errors::HTTPErrors
63
+ include Constants
64
+
65
+ # Get all of the organizations.
66
+ app.get do
67
+ authorized_admin!(Operations::READ)
68
+
69
+ # Retrieve the organization data in a form we can more easily
70
+ # manipulate so that we can sanitize it
71
+ orgs = Organization.all_as_hash
72
+
73
+ # Remove any sensitive data
74
+ orgs.map! do |org|
75
+ sanitize_organization(org)
76
+ end
77
+
78
+ return orgs.to_json
79
+ end
80
+
81
+ # Create a new organization.
82
+ app.post do
83
+ authorized_admin!(Operations::ADMIN)
84
+
85
+ payload = parse_request_body
86
+ Cyclid.logger.debug payload
87
+
88
+ begin
89
+ halt_with_json_response(409, \
90
+ DUPLICATE, \
91
+ 'An organization with that name already exists') \
92
+ if Organization.exists?(name: payload['name'])
93
+
94
+ org = Organization.new
95
+ org['name'] = payload['name']
96
+ org['owner_email'] = payload['owner_email']
97
+
98
+ # Generate an RSA key-pair and a Salt
99
+ key = OpenSSL::PKey::RSA.new(RSA_KEY_LENGTH)
100
+
101
+ org['rsa_private_key'] = key.to_der
102
+ org['rsa_public_key'] = key.public_key.to_der
103
+
104
+ org['salt'] = SecureRandom.hex(32)
105
+
106
+ # Add each provided user to the Organization
107
+ users = payload['users'] || []
108
+
109
+ org.users = users.map do |username|
110
+ user = User.find_by(username: username)
111
+
112
+ halt_with_json_response(404, \
113
+ INVALID_USER, \
114
+ "user #{user} does not exist") \
115
+ if user.nil?
116
+
117
+ user
118
+ end
119
+
120
+ org.save!
121
+ rescue ActiveRecord::ActiveRecordError, \
122
+ ActiveRecord::UnknownAttributeError => ex
123
+
124
+ Cyclid.logger.debug ex.message
125
+ halt_with_json_response(400, INVALID_JSON, ex.message)
126
+ end
127
+
128
+ return json_response(NO_ERROR, "organization #{payload['name']} created")
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end