Skip to content

Commit 4d10c7f

Browse files
authored
Securely signed releases PRD (#1249)
* Add custom_keys * Fix error messages * Add spec * Add audit logging * Adjust custom key audit
1 parent 85cbe96 commit 4d10c7f

File tree

17 files changed

+355
-2
lines changed

17 files changed

+355
-2
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
module Travis::API::V3
2+
class Models::CustomKey < Model
3+
belongs_to :owner, polymorphic: true
4+
5+
serialize :private_key, Travis::Model::EncryptedColumn.new
6+
7+
validates_each :private_key do |record, attr, private_key|
8+
record.errors.add(attr, :missing_attr, message: 'missing_attr') if private_key.blank?
9+
record.errors.add(attr, :invalid_pem, message: 'invalid_pem') unless record.valid_pem?
10+
end
11+
12+
def save_key!(owner_type, owner_id, name, description, private_key, added_by)
13+
self.owner_type = owner_type
14+
self.owner_id = owner_id
15+
self.private_key = private_key
16+
self.name = name
17+
self.description = description
18+
self.added_by = added_by
19+
20+
if self.valid?
21+
self.fingerprint = calculate_fingerprint(private_key)
22+
self.public_key = OpenSSL::PKey::RSA.new(private_key).public_key.to_s
23+
24+
self.save!
25+
end
26+
27+
self
28+
end
29+
30+
def valid_pem?
31+
private_key && OpenSSL::PKey::RSA.new(private_key)
32+
true
33+
rescue OpenSSL::PKey::RSAError
34+
false
35+
end
36+
37+
private
38+
39+
def calculate_fingerprint(source)
40+
rsa_key = OpenSSL::PKey::RSA.new(source)
41+
public_ssh_rsa = "\x00\x00\x00\x07ssh-rsa" + rsa_key.e.to_s(0) + rsa_key.n.to_s(0)
42+
OpenSSL::Digest::MD5.new(public_ssh_rsa).hexdigest.scan(/../).join(':')
43+
end
44+
end
45+
end

lib/travis/api/v3/models/organization.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ def build_priorities_enabled?
2929
Travis::Features.owner_active?(:build_priorities_org, self)
3030
end
3131

32+
def custom_keys
33+
return @custom_keys if defined? @custom_keys
34+
@custom_keys = Models::CustomKey.where(owner_type: 'Organization', owner_id: id)
35+
end
36+
3237
alias members users
3338
end
3439
end

lib/travis/api/v3/models/user.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,5 +82,10 @@ def installation
8282
def github?
8383
vcs_type == 'GithubUser'
8484
end
85+
86+
def custom_keys
87+
return @custom_keys if defined? @custom_keys
88+
@custom_keys = Models::CustomKey.where(owner_type: 'User', owner_id: id)
89+
end
8590
end
8691
end
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
module Travis::API::V3
2+
class Queries::CustomKey < Query
3+
def create(params, current_user)
4+
raise UnprocessableEntity, 'Key with this identifier already exists.' unless Travis::API::V3::Models::CustomKey.where(name: params['name'], owner_id: params['owner_id'], owner_type: params['owner_type']).count.zero?
5+
6+
if params['owner_type'] == 'User'
7+
org_ids = User.find(params['owner_id']).organizations.map(&:id)
8+
9+
raise UnprocessableEntity, 'Key with this identifier already exists in one of your organizations.' unless Travis::API::V3::Models::CustomKey.where(name: params['name'], owner_id: org_ids, owner_type: 'Organization').count.zero?
10+
elsif params['owner_type'] == 'Organization'
11+
user_ids = Membership.where(organization_id: params['owner_id']).map(&:user_id)
12+
13+
raise UnprocessableEntity, 'Key with this identifier already exists for your user.' unless Travis::API::V3::Models::CustomKey.where(name: params['name'], owner_id: user_ids, owner_type: 'User').count.zero?
14+
end
15+
16+
key = Travis::API::V3::Models::CustomKey.new.save_key!(
17+
params['owner_type'],
18+
params['owner_id'],
19+
params['name'],
20+
params['description'],
21+
params['private_key'],
22+
params['added_by']
23+
)
24+
handle_errors(key) unless key.valid?
25+
26+
Travis::API::V3::Models::Audit.create!(
27+
owner: current_user,
28+
change_source: 'travis-api',
29+
source: key,
30+
source_changes: {
31+
action: 'create',
32+
fingerprint: key.fingerprint
33+
}
34+
)
35+
36+
key
37+
end
38+
39+
def delete(params, current_user)
40+
key = Travis::API::V3::Models::CustomKey.find(params['id'])
41+
Travis::API::V3::Models::Audit.create!(
42+
owner: current_user,
43+
change_source: 'travis-api',
44+
source: key,
45+
source_changes: {
46+
action: 'delete',
47+
name: key.name,
48+
owner_type: key.owner_type,
49+
owner_id: key.owner_id,
50+
fingerprint: key.fingerprint
51+
}
52+
)
53+
54+
key.destroy
55+
end
56+
57+
private
58+
59+
def handle_errors(key)
60+
private_key = key.errors[:private_key]
61+
raise UnprocessableEntity, 'This key is not a private key.' if private_key.include?('invalid_pem')
62+
raise WrongParams if private_key.include?('missing_attr')
63+
end
64+
end
65+
end
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module Travis::API::V3
2+
class Renderer::CustomKey < ModelRenderer
3+
representation :standard, :id, :name, :description, :public_key, :fingerprint, :added_by_login, :created_at
4+
representation :minimal, *representations[:standard]
5+
6+
def added_by_login
7+
model.added_by.nil? ? '' : User.find(model.added_by).login
8+
end
9+
end
10+
end

lib/travis/api/v3/renderer/owner.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ class Renderer::Owner < ModelRenderer
66

77
representation(:minimal, :id, :login, :name, :vcs_type, :ro_mode)
88
representation(:standard, :id, :login, :name, :github_id, :vcs_id, :vcs_type, :avatar_url, :education,
9-
:allow_migration, :allowance, :ro_mode)
9+
:allow_migration, :allowance, :ro_mode, :custom_keys)
1010
representation(:additional, :repositories, :installation)
1111

1212
def initialize(*)

lib/travis/api/v3/renderer/user.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
module Travis::API::V3
44
class Renderer::User < Renderer::Owner
5-
representation(:standard, :email, :is_syncing, :synced_at, :recently_signed_up, :secure_user_hash, :ro_mode, :confirmed_at)
5+
representation(:standard, :email, :is_syncing, :synced_at, :recently_signed_up, :secure_user_hash, :ro_mode, :confirmed_at, :custom_keys)
66
representation(:additional, :emails)
77

88
def email

lib/travis/api/v3/routes.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,16 @@ module Routes
311311
end
312312
end
313313

314+
hidden_resource :custom_keys do
315+
route '/custom_keys'
316+
post :create
317+
end
318+
319+
hidden_resource :custom_key do
320+
route '/custom_key/{id}'
321+
delete :delete
322+
end
323+
314324
hidden_resource :beta_migration_requests do
315325
route '/beta_migration_requests'
316326

lib/travis/api/v3/services.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ module Services
2323
CreditsCalculator = Module.new { extend Services }
2424
Cron = Module.new { extend Services }
2525
Crons = Module.new { extend Services }
26+
CustomKey = Module.new { extend Services }
27+
CustomKeys = Module.new { extend Services }
2628
EmailSubscription = Module.new { extend Services }
2729
EnvVar = Module.new { extend Services }
2830
EnvVars = Module.new { extend Services }
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module Travis::API::V3
2+
class Services::CustomKey::Delete < Service
3+
def run!
4+
raise LoginRequired unless access_control.full_access_or_logged_in?
5+
6+
query(:custom_key).delete(params, access_control.user)
7+
deleted
8+
end
9+
end
10+
end

0 commit comments

Comments
 (0)