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,145 @@
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 Organization members
23
+ # @api REST
24
+ module Members
25
+ # @!group Organizations
26
+
27
+ # @!method get_organizations_organization_members_member
28
+ # @overload GET /organizations/:organization/members/:username
29
+ # @macro rest
30
+ # @param [String] organization Name of the organization.
31
+ # @param [String] username Username of the member.
32
+ # Get the details of the specified user within the organization.
33
+ # @return The requested member.
34
+ # @return [404] The organization or user does not exist, or the user is not a member of
35
+ # the organization.
36
+ # @example Get the 'user1' user from the 'example' organization
37
+ # GET /organizations/example/members/user1 => {"id": 1,
38
+ # "username": "user1",
39
+ # "email":"test@example.com",
40
+ # "permissions":{
41
+ # "admin":true,
42
+ # "write":true,
43
+ # "read":true
44
+ # }}
45
+
46
+ # @!method put_organizations_organization_members_member
47
+ # @overload PUT /organizations/:name/members/:username
48
+ # @macro rest
49
+ # @param [String] organization Name of the organization.
50
+ # @param [String] username Username of the member.
51
+ # Modify the permissions of specified user within the organization.
52
+ # @param [JSON] body User permissions.
53
+ # @option body [Hash] permissions Permissions to apply for the user.
54
+ # @return [200] The member was modified successfully.
55
+ # @return [404] The user does not exist, or is not a member of the organization.
56
+ # @example Give the member 'user1' write & read permissions for the 'example' organization
57
+ # PUT /organizations/example/members/user1 <= {"permissions": {
58
+ # "admin":false,
59
+ # "write":true,
60
+ # "read":true
61
+ # }}
62
+
63
+ # @!endgroup
64
+
65
+ # Sinatra callback
66
+ # @private
67
+ def self.registered(app)
68
+ include Errors::HTTPErrors
69
+
70
+ # Get the details of the specified user within the organization.
71
+ app.get '/:username' do
72
+ authorized_for!(params[:name], Operations::READ)
73
+
74
+ org = Organization.find_by(name: params[:name])
75
+ halt_with_json_response(404, INVALID_ORG, 'organization does not exist') \
76
+ if org.nil?
77
+
78
+ user = org.users.find_by(username: params[:username])
79
+ halt_with_json_response(404, INVALID_USER, 'user does not exist') \
80
+ if user.nil?
81
+
82
+ begin
83
+ perms = user.userpermissions.find_by(organization: org)
84
+
85
+ user_hash = user.serializable_hash
86
+ user_hash.delete_if do |key, _value|
87
+ key == 'password' || key == 'secret'
88
+ end
89
+
90
+ perms_hash = perms.serializable_hash
91
+ perms_hash.delete_if do |key, _value|
92
+ key == 'id' || key == 'user_id' || key == 'organization_id'
93
+ end
94
+
95
+ user_hash['permissions'] = perms_hash
96
+
97
+ return user_hash.to_json
98
+ rescue ActiveRecord::ActiveRecordError, \
99
+ ActiveRecord::UnknownAttributeError => ex
100
+
101
+ Cyclid.logger.debug ex.message
102
+ halt_with_json_response(500, INTERNAL_ERROR, ex.message)
103
+ end
104
+ end
105
+
106
+ # Modify the specified user within the organization.
107
+ app.put '/:username' do
108
+ authorized_for!(params[:name], Operations::WRITE)
109
+
110
+ payload = parse_request_body
111
+ Cyclid.logger.debug payload
112
+
113
+ org = Organization.find_by(name: params[:name])
114
+ halt_with_json_response(404, INVALID_ORG, 'organization does not exist') \
115
+ if org.nil?
116
+
117
+ user = org.users.find_by(username: params[:username])
118
+ halt_with_json_response(404, INVALID_USER, 'user does not exist') \
119
+ if user.nil?
120
+
121
+ begin
122
+ perms = user.userpermissions.find_by(organization: org)
123
+
124
+ payload_perms = payload['permissions'] if payload.key? 'permissions'
125
+ unless payload_perms.nil?
126
+ perms.admin = payload_perms['admin'] if payload_perms.key? 'admin'
127
+ perms.write = payload_perms['write'] if payload_perms.key? 'write'
128
+ perms.read = payload_perms['read'] if payload_perms.key? 'read'
129
+
130
+ Cyclid.logger.debug perms.serializable_hash
131
+
132
+ perms.save!
133
+ end
134
+ rescue ActiveRecord::ActiveRecordError, \
135
+ ActiveRecord::UnknownAttributeError => ex
136
+
137
+ Cyclid.logger.debug ex.message
138
+ halt_with_json_response(500, INTERNAL_ERROR, ex.message)
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,251 @@
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 Organization Stages
23
+ # @api REST
24
+ module Stages
25
+ # @!group Organizations
26
+
27
+ # @!method get_organizations_organization_stages
28
+ # @overload GET /organizations/:organization/stages
29
+ # @macro rest
30
+ # @param [String] organization Name of the organization.
31
+ # Get all of the stages.
32
+ # @return All the stages within the organization.
33
+ # @return [404] The organization does not exist
34
+
35
+ # @!method post_organizations_organization_stages(body)
36
+ # @overload POST /organizations/:organization/stages
37
+ # @macro rest
38
+ # @param [String] organization Name of the organization.
39
+ # Create a new stage.
40
+ # @param [JSON] body New stage
41
+ # @option body [String] name Name of the new stage
42
+ # @option body [String] version (0.0.1) Version of the new stage
43
+ # @option body [Array<Object>] steps List of steps for the stage
44
+ # @return [400] The definition for the stage is invalid.
45
+ # @return [404] The organization does not exist.
46
+ # @return [409] A stage with the same name & version already exists.
47
+ # @example Create a simple stage for 'example' organization
48
+ # POST /organizations/example/stages <= {"name": "example",
49
+ # "steps": [
50
+ # {
51
+ # "action": "command",
52
+ # "cmd": "bundle install"
53
+ # }
54
+ # ]}
55
+
56
+ # @!method get_organizations_organization_stages_stage
57
+ # @overload GET /organizations/:organization/stages/:stage
58
+ # @macro rest
59
+ # @param [String] organization Name of the organization.
60
+ # @param [String] stage Name of the stage.
61
+ # Get the details of the specified stage within the organization. Returns every defined
62
+ # version for the given stage.
63
+ # @return The requested stage.
64
+ # @return [404] The organization or stage does not exist
65
+
66
+ # @!method get_organizations_organization_stages_stage_version
67
+ # @overload GET /organizations/:organization/stages/:stage/:version
68
+ # @macro rest
69
+ # @param [String] organization Name of the organization.
70
+ # @param [String] stage Name of the stage.
71
+ # @param [String] version Version of the stage.
72
+ # Get the details of the specified stage within the organization. Returns the specified
73
+ # version of the given stage.
74
+ # @return The requested stage.
75
+ # @return [404] The organization, stage or version of the stage does not exist
76
+
77
+ # @!endgroup
78
+
79
+ # Sinatra callback
80
+ # @private
81
+ def self.registered(app)
82
+ include Errors::HTTPErrors
83
+
84
+ # Get all of the stages.
85
+ app.get do
86
+ authorized_for!(params[:name], Operations::READ)
87
+
88
+ org = Organization.find_by(name: params[:name])
89
+ halt_with_json_response(404, INVALID_ORG, 'organization does not exist') \
90
+ if org.nil?
91
+
92
+ # Convert each Stage to a hash & sanitize it
93
+ stages = org.stages.all.map do |stage|
94
+ stage_hash = sanitize_stage(stage.serializable_hash)
95
+
96
+ # Santize each step in this stage
97
+ steps = stage.steps.map do |step|
98
+ sanitize_step(step.serializable_hash)
99
+ end
100
+ stage_hash['steps'] = steps
101
+
102
+ stage_hash
103
+ end
104
+
105
+ return stages.to_json
106
+ end
107
+
108
+ # Create a new stage.
109
+ app.post do
110
+ authorized_for!(params[:name], Operations::ADMIN)
111
+
112
+ payload = parse_request_body
113
+ Cyclid.logger.debug payload
114
+
115
+ org = Organization.find_by(name: params[:name])
116
+ halt_with_json_response(404, INVALID_ORG, 'organization does not exist') \
117
+ if org.nil?
118
+
119
+ halt_with_json_response(400, INVALID_JSON, 'stage does not define any steps') \
120
+ unless payload.key? 'steps'
121
+
122
+ # Ensure that a stage with the same name & version doesn't already exist
123
+ version = payload['version'] || '0.0.1'
124
+ halt_with_json_response(409, \
125
+ DUPLICATE, \
126
+ 'A stage with that name and version already exists') \
127
+ if org.stages.exists?(name: payload['name'], version: version)
128
+
129
+ begin
130
+ stage = Stage.new
131
+
132
+ stage.name = payload['name']
133
+ stage.version = payload['version'] if payload.key? 'version'
134
+ stage.organization = org
135
+
136
+ # Create the steps & store their serialized form
137
+ stage.steps << create_steps(payload['steps'])
138
+
139
+ stage.save!
140
+ rescue ActiveRecord::ActiveRecordError, \
141
+ ActiveRecord::UnknownAttributeError => ex
142
+
143
+ Cyclid.logger.debug ex.message
144
+ halt_with_json_response(400, INVALID_JSON, ex.message)
145
+ end
146
+ end
147
+
148
+ # Returns every defined version for the given stage.
149
+ app.get '/:stage' do
150
+ authorized_for!(params[:name], Operations::READ)
151
+
152
+ org = Organization.find_by(name: params[:name])
153
+ halt_with_json_response(404, INVALID_ORG, 'organization does not exist') \
154
+ if org.nil?
155
+
156
+ # There may be multiple versions of the same stage, so we need to
157
+ # find every instance of the given stage, convert each Stage to a
158
+ # hash & sanitize it
159
+ stages = org.stages.where(name: params[:stage]).map do |stage|
160
+ stage_hash = sanitize_stage(stage.serializable_hash)
161
+
162
+ # Santize each step in this stage
163
+ steps = stage.steps.map do |step|
164
+ sanitize_step(step.serializable_hash)
165
+ end
166
+ stage_hash['steps'] = steps
167
+
168
+ stage_hash
169
+ end
170
+
171
+ halt_with_json_response(404, INVALID_STAGE, 'stage does not exist') \
172
+ if stages.empty?
173
+
174
+ return stages.to_json
175
+ end
176
+
177
+ # Get the details of the specified stage within the organization.
178
+ # Returns the specified version of the given stage.
179
+ app.get '/:stage/:version' do
180
+ authorized_for!(params[:name], Operations::READ)
181
+
182
+ org = Organization.find_by(name: params[:name])
183
+ halt_with_json_response(404, INVALID_ORG, 'organization does not exist') \
184
+ if org.nil?
185
+
186
+ stage = org.stages.find_by(name: params[:stage], version: params[:version])
187
+ halt_with_json_response(404, INVALID_STAGE, 'stage does not exist') \
188
+ if stage.nil?
189
+
190
+ # Sanitize the stage
191
+ stage_hash = sanitize_stage(stage.serializable_hash)
192
+
193
+ # Santize each step in this stage
194
+ steps = stage.steps.map do |step|
195
+ sanitize_step(step.serializable_hash)
196
+ end
197
+ stage_hash['steps'] = steps
198
+
199
+ return stage_hash.to_json
200
+ end
201
+
202
+ app.helpers do
203
+ include Helpers
204
+ end
205
+ end
206
+
207
+ # Helpers for Stages
208
+ module Helpers
209
+ include Errors::HTTPErrors
210
+
211
+ # Create the serialized steps
212
+ #
213
+ # For each definition in the payload, inspect it and create the
214
+ # appropriate object for that action; that class is then serialized
215
+ # into JSON and stored in the Step in the database, and can then
216
+ # be unserialized back in to the desired object when it's needed
217
+ # without the database having to be aware of every single
218
+ # permutation of possible actions and arguments to them.
219
+ #
220
+ # @private
221
+ def create_steps(stage_steps)
222
+ sequence = 1
223
+ stage_steps.map do |stage_step|
224
+ step = Step.new
225
+ step.sequence = sequence
226
+
227
+ begin
228
+ action_name = stage_step['action']
229
+ plugin = Cyclid.plugins.find(action_name, Cyclid::API::Plugins::Action)
230
+
231
+ step_action = plugin.new(stage_step)
232
+ raise if step_action.nil?
233
+ rescue StandardError => ex
234
+ # XXX Rescue an internal exception
235
+ halt_with_json_response(404, INVALID_ACTION, ex.message)
236
+ end
237
+
238
+ # Serialize the object into the Step and store it in the database.
239
+ step.action = Oj.dump(step_action)
240
+ step.save!
241
+
242
+ sequence += 1
243
+
244
+ step
245
+ end
246
+ end
247
+ end
248
+ end
249
+ end
250
+ end
251
+ end
@@ -0,0 +1,47 @@
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 'users/*.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 User related API endpoints
23
+ class UserController < ControllerBase
24
+ helpers do
25
+ # Remove sensitive data from the users data
26
+ def sanitize_user(user)
27
+ user.delete_if do |key, _value|
28
+ key == 'password' || key == 'secret'
29
+ end
30
+ end
31
+ end
32
+
33
+ register Sinatra::Namespace
34
+
35
+ namespace '/users' do
36
+ register Users::Collection
37
+
38
+ namespace '/:username' do
39
+ register Users::Document
40
+ end
41
+ end
42
+ end
43
+
44
+ # Register this controller
45
+ Cyclid.controllers << UserController
46
+ end
47
+ end
@@ -0,0 +1,131 @@
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 User related API endpoints
21
+ module Users
22
+ # API endpoints for the User collection
23
+ # @api REST
24
+ module Collection
25
+ # @!group Users
26
+
27
+ # @!method users
28
+ # @overload GET /users
29
+ # @macro rest
30
+ # Get all of the users.
31
+ # @return List of users
32
+ # @example Get a list of users
33
+ # GET /users => [{
34
+ # "id": 1,
35
+ # "username": "user1",
36
+ # "email": "user1@example.com"
37
+ # },
38
+ # {
39
+ # "id": 2,
40
+ # "username": "user2",
41
+ # "email": "user2@example.com"
42
+ # }]
43
+ # @see get_users_user
44
+
45
+ # @!method post_users(body)
46
+ # @overload POST /users
47
+ # @macro rest
48
+ # Create a new user. Note that only *one* of 'password' or 'new_password' should be
49
+ # passed.
50
+ # @param [JSON] body New user
51
+ # @option body [String] username Username of the new user
52
+ # @option body [String] name Users real name
53
+ # @option body [String] email Users email address
54
+ # @option body [String] password Bcrypt2 encrypted password
55
+ # @option body [String] new_password Password in plain text, which will be encrypted
56
+ # before being stored in the databaase.
57
+ # @option body [String] secret HMAC signing secret. This should be a suitably long
58
+ # random string.
59
+ # @return [200] User was created successfully
60
+ # @return [400] The user definition is invalid
61
+ # @return [409] An user with that name already exists
62
+ # @example Create a new user with an encrypted password
63
+ # POST /users <= {"username": "user1",
64
+ # "email": "user1@example.com",
65
+ # "password": "<Bcrypt2 encrypted password>"}
66
+
67
+ # @!endgroup
68
+
69
+ # Sinatra callback
70
+ # @private
71
+ def self.registered(app)
72
+ include Errors::HTTPErrors
73
+
74
+ # @macro [attach] sinatra.get
75
+ # @overload get "$1"
76
+ # @method get_users
77
+ # @return [String] JSON represention of all of all the users.
78
+ # Get all of the users across all organizations.
79
+ app.get do
80
+ authorized_admin!(Operations::READ)
81
+
82
+ # Retrieve the user data in a form we can more easily manipulate so
83
+ # that we can sanitize it
84
+ users = User.all_as_hash
85
+
86
+ # Remove any sensitive data
87
+ users.map! do |user|
88
+ sanitize_user(user)
89
+ end
90
+
91
+ return users.to_json
92
+ end
93
+
94
+ # @macro [attach] sinatra.post
95
+ # @overload post "$1"
96
+ # @method post_users
97
+ # Create a new user.
98
+ app.post do
99
+ authorized_admin!(Operations::ADMIN)
100
+
101
+ payload = parse_request_body
102
+ Cyclid.logger.debug payload
103
+
104
+ begin
105
+ halt_with_json_response(409, \
106
+ DUPLICATE, \
107
+ 'a user with that name already exists') \
108
+ if User.exists?(username: payload['username'])
109
+
110
+ user = User.new
111
+ user.username = payload['username']
112
+ user.email = payload['email']
113
+ user.name = payload['name'] if payload.key? 'name'
114
+ user.password = payload['password'] if payload.key? 'password'
115
+ user.secret = payload['secret'] if payload.key? 'secret'
116
+ user.new_password = payload['new_password'] if payload.key? 'new_password'
117
+ user.save!
118
+ rescue ActiveRecord::ActiveRecordError, \
119
+ ActiveRecord::UnknownAttributeError => ex
120
+
121
+ Cyclid.logger.debug ex.message
122
+ halt_with_json_response(400, INVALID_JSON, ex.message)
123
+ end
124
+
125
+ return json_response(NO_ERROR, "user #{payload['username']} created")
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end