front_end_builds 0.0.26 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 454309571c8187f9172c22223e66b72ba3f4a1dd
4
- data.tar.gz: 0b379fc6a91e2ea5df974b19497880005621109a
3
+ metadata.gz: b76de17ea5a85d7b6e1c9ca8d26e7a0b55d16399
4
+ data.tar.gz: 1e0730650a1db2545efd83710b9d6471dfab7786
5
5
  SHA512:
6
- metadata.gz: b689b859ed5d190458ffd33bd46de053187288404c510297479e7cc06d5a7d4922f321b9b76e4c7022a9cbb3e3e73eb6b4930c1c7f6c18a506c3cd40918abe13
7
- data.tar.gz: 8374c3856062d4eac9f6452e361aa13dd549e93b9d72e1e937da9414c0de82704feb0e02308b93733e2a0f8969ba9478ccc9757b777526ce844d88ea0dd5959c
6
+ metadata.gz: 9068e3c19df8ade4559bec36fd41bdc0d0543c567773142ed9ead4561dfab6c08bd6379110294c84e6294cace0f45921a0b7176ba2779253cd6388966691576f
7
+ data.tar.gz: e51b645290dd483bd68c2d65291056fa093c7e9188e451b96aa868773d8493330327f31e9a643e28d32679c0868c32a4cc5f1a76067e84f12af7ca69ad702700
@@ -17,5 +17,9 @@ module FrontEndBuilds
17
17
  end
18
18
  end
19
19
 
20
+ def error!(errors, status = :unprocessable_entity)
21
+ respond_with_json({ errors: errors }, status: status)
22
+ end
23
+
20
24
  end
21
25
  end
@@ -25,7 +25,7 @@ module FrontEndBuilds
25
25
  def create
26
26
  @app = FrontEndBuilds::App.new( use_params(:app_create_params) )
27
27
 
28
- if @app.save!
28
+ if @app.save
29
29
  respond_with_json(
30
30
  { app: @app.serialize },
31
31
  location: nil
@@ -2,7 +2,7 @@ require_dependency "front_end_builds/application_controller"
2
2
 
3
3
  module FrontEndBuilds
4
4
  class BuildsController < ApplicationController
5
- before_filter :set_app!, only: :create
5
+ before_filter :set_app!, only: [:create]
6
6
 
7
7
  def index
8
8
  builds = FrontEndBuilds::Build.where(app_id: params[:app_id])
@@ -14,12 +14,13 @@ module FrontEndBuilds
14
14
  def create
15
15
  build = @app.builds.new(use_params(:build_create_params))
16
16
 
17
- if build.save
18
- build.fetch!
19
- build.activate! if build.automatic_activation? and build.master?
17
+ if build.verify && build.save
18
+ build.setup!
20
19
  head :ok
21
20
 
22
21
  else
22
+ build.errors[:base] << 'No access - invalid SSH key' if !build.verify
23
+
23
24
  render(
24
25
  text: 'Could not create the build: ' + build.errors.full_messages.to_s,
25
26
  status: :unprocessable_entity
@@ -37,38 +38,37 @@ module FrontEndBuilds
37
38
  private
38
39
 
39
40
  def set_app!
40
- @app = find_app
41
+ @app = FrontEndBuilds::App
42
+ .where(name: params[:app_name])
43
+ .limit(1)
44
+ .first
45
+
41
46
  if @app.nil?
42
47
  render(
43
- text: 'That app name/API combination was not found.',
48
+ text: "No app named #{params[:app_name]}.",
44
49
  status: :unprocessable_entity
45
50
  )
51
+
52
+ return false
46
53
  end
47
54
  end
48
55
 
49
- def build_create_params_rails_3
50
- params.slice(
56
+ def _create_params
57
+ [
51
58
  :branch,
52
59
  :sha,
53
60
  :job,
54
- :endpoint
55
- )
61
+ :endpoint,
62
+ :signature
63
+ ]
56
64
  end
57
65
 
58
- def build_create_params_rails_4
59
- params.permit(
60
- :branch,
61
- :sha,
62
- :job,
63
- :endpoint
64
- )
66
+ def build_create_params_rails_3
67
+ params.slice(*_create_params)
65
68
  end
66
69
 
67
- def find_app
68
- FrontEndBuilds::App.where(
69
- name: params[:app_name],
70
- api_key: params[:api_key]
71
- ).limit(1).first
70
+ def build_create_params_rails_4
71
+ params.permit(*_create_params)
72
72
  end
73
73
  end
74
74
  end
@@ -0,0 +1,50 @@
1
+ require_dependency "front_end_builds/application_controller"
2
+
3
+ module FrontEndBuilds
4
+ class PubkeysController < ApplicationController
5
+ def index
6
+ keys = FrontEndBuilds::Pubkey.order(:name)
7
+ respond_with_json(pubkeys: keys.map(&:serialize))
8
+ end
9
+
10
+ def create
11
+ pubkey = FrontEndBuilds::Pubkey
12
+ .new( use_params(:pubkey_create_params) )
13
+
14
+ if pubkey.save
15
+ respond_with_json(
16
+ { pubkey: pubkey.serialize },
17
+ location: nil
18
+ )
19
+ else
20
+ error!(pubkey.errors)
21
+ end
22
+ end
23
+
24
+ def destroy
25
+ pubkey = FrontEndBuilds::Pubkey.find(params[:id])
26
+
27
+ if pubkey.destroy
28
+ respond_with_json(
29
+ { pubkey: { id: pubkey.id } },
30
+ location: nil
31
+ )
32
+ else
33
+ error!(pubkey.errors)
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def pubkey_create_params_rails_3
40
+ params[:pubkey].slice(:name, :pubkey)
41
+ end
42
+
43
+ def pubkey_create_params_rails_4
44
+ params.require(:pubkey).permit(
45
+ :name,
46
+ :pubkey
47
+ )
48
+ end
49
+ end
50
+ end
@@ -22,9 +22,6 @@ module FrontEndBuilds
22
22
  end
23
23
 
24
24
  validates :name, presence: true
25
- validates :api_key, presence: true
26
-
27
- before_validation :ensure_api_key!
28
25
 
29
26
  def self.register_url(name, url)
30
27
  @_url ||= {}
@@ -40,15 +37,10 @@ module FrontEndBuilds
40
37
  self.class.get_url(name)
41
38
  end
42
39
 
43
- def ensure_api_key!
44
- self.api_key = SecureRandom.uuid if api_key.blank?
45
- end
46
-
47
40
  def serialize
48
41
  {
49
42
  id: id,
50
43
  name: name,
51
- api_key: api_key,
52
44
  build_ids: recent_builds.map(&:id),
53
45
  live_build_id: (live_build ? live_build.id : nil),
54
46
  location: get_url,
@@ -5,15 +5,18 @@ module FrontEndBuilds
5
5
  if defined?(ProtectedAttributes) || ::ActiveRecord::VERSION::MAJOR < 4
6
6
  attr_accessible :branch,
7
7
  :sha,
8
- :endpoint
8
+ :endpoint,
9
+ :signature
9
10
  end
10
11
 
11
12
  belongs_to :app, class_name: "FrontEndBuilds::App"
13
+ belongs_to :pubkey, class_name: "FrontEndBuilds::Pubkey"
12
14
 
13
15
  validates :app, presence: true
14
16
  validates :sha, presence: true
15
17
  validates :branch, presence: true
16
18
  validates :endpoint, presence: true
19
+ validates :signature, presence: true
17
20
 
18
21
  scope :recent, -> { limit(10).order('created_at desc') }
19
22
 
@@ -62,6 +65,29 @@ module FrontEndBuilds
62
65
  .first
63
66
  end
64
67
 
68
+ # Public: Is the signature is valid for the build.
69
+ #
70
+ # Returns boolean.
71
+ def verify
72
+ !!matching_pubkey
73
+ end
74
+
75
+ # Public: Find the pubkey that can verify the builds
76
+ # signature.
77
+ def matching_pubkey
78
+ Pubkey.all
79
+ .detect { |key| key.verify(self) }
80
+ .tap { |key| self.pubkey = key }
81
+ end
82
+
83
+ def setup!
84
+ fetch!
85
+
86
+ if automatic_activation? && master?
87
+ activate!
88
+ end
89
+ end
90
+
65
91
  def live?
66
92
  self == app.live_build
67
93
  end
@@ -0,0 +1,74 @@
1
+ require 'front_end_builds/utils/ssh_pubkey_convert'
2
+ require 'base64'
3
+ require 'openssl'
4
+
5
+ module FrontEndBuilds
6
+ class Pubkey < ActiveRecord::Base
7
+ if defined?(ProtectedAttributes) || ::ActiveRecord::VERSION::MAJOR < 4
8
+ attr_accessible :name,
9
+ :pubkey
10
+ end
11
+
12
+ validates :name, presence: true
13
+ validates :pubkey, presence: true
14
+
15
+ has_many :builds, class_name: "FrontEndBuilds::Build"
16
+
17
+ def fingerprint
18
+ content = pubkey.split(/\s/)[1]
19
+
20
+ if content
21
+ Digest::MD5.hexdigest(Base64.decode64(content))
22
+ .scan(/.{1,2}/)
23
+ .join(":")
24
+ else
25
+ 'Unknown'
26
+ end
27
+ end
28
+
29
+ def ssh_pubkey?
30
+ (type, b64, _) = pubkey.split(/\s/)
31
+ %w{ssh-rsa ssh-dss}.include?(type) && b64.present?
32
+ end
33
+
34
+ # Public: In order to verify a signature we need the key to be an OpenSSL
35
+ # RSA PKey and not a string that you would find in an ssh pubkey key. Most
36
+ # people are going to be adding ssh public keys to their build system, this
37
+ # method will covert them to OpenSSL RSA if needed.
38
+ def to_rsa_pkey
39
+ FrontEndBuilds::Utils::SSHPubKeyConvert
40
+ .convert(pubkey)
41
+ end
42
+
43
+ # Public: Will verify that the sigurate has access to deploy the build
44
+ # object. The signature includes the endpoint and app name.
45
+ #
46
+ # Returns boolean
47
+ def verify(build)
48
+ # TODO might as well cache this and store in the db so we dont have to
49
+ # convert every time
50
+ pkey = to_rsa_pkey
51
+ signature = Base64.decode64(build.signature)
52
+ digest = OpenSSL::Digest::SHA256.new
53
+ expected = "#{build.app.name}-#{build.endpoint}"
54
+
55
+ pkey.verify(digest, signature, expected)
56
+ end
57
+
58
+ def last_build
59
+ builds
60
+ .order('created_at desc')
61
+ .limit(1)
62
+ .first
63
+ end
64
+
65
+ def serialize
66
+ {
67
+ id: id,
68
+ name: name,
69
+ fingerprint: fingerprint,
70
+ lastUsedAt: last_build.try(:created_at)
71
+ }
72
+ end
73
+ end
74
+ end
@@ -8,7 +8,7 @@
8
8
  <meta name="viewport" content="width=device-width, initial-scale=1">
9
9
 
10
10
  <base href="/BASEURL/" />
11
- <meta name="admin/config/environment" content="%7B%22modulePrefix%22%3A%22admin%22%2C%22environment%22%3A%22production%22%2C%22baseURL%22%3A%22BASEURL%22%2C%22locationType%22%3A%22auto%22%2C%22usePodsByDefault%22%3Atrue%2C%22podModulePrefix%22%3A%22admin/pods%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%7D%2C%22APP%22%3A%7B%7D%2C%22usePretender%22%3Afalse%2C%22contentSecurityPolicyHeader%22%3A%22Content-Security-Policy-Report-Only%22%2C%22contentSecurityPolicy%22%3A%7B%22default-src%22%3A%22%27none%27%22%2C%22script-src%22%3A%22%27self%27%22%2C%22font-src%22%3A%22%27self%27%22%2C%22connect-src%22%3A%22%27self%27%22%2C%22img-src%22%3A%22%27self%27%22%2C%22style-src%22%3A%22%27self%27%22%2C%22media-src%22%3A%22%27self%27%22%7D%2C%22exportApplicationGlobal%22%3Afalse%7D" />
11
+ <meta name="admin/config/environment" content="%7B%22modulePrefix%22%3A%22admin%22%2C%22environment%22%3A%22production%22%2C%22baseURL%22%3A%22BASEURL%22%2C%22locationType%22%3A%22auto%22%2C%22usePodsByDefault%22%3Atrue%2C%22podModulePrefix%22%3A%22admin/pods%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%7D%2C%22APP%22%3A%7B%7D%2C%22contentSecurityPolicyHeader%22%3A%22Content-Security-Policy-Report-Only%22%2C%22contentSecurityPolicy%22%3A%7B%22default-src%22%3A%22%27none%27%22%2C%22script-src%22%3A%22%27self%27%22%2C%22font-src%22%3A%22%27self%27%22%2C%22connect-src%22%3A%22%27self%27%22%2C%22img-src%22%3A%22%27self%27%22%2C%22style-src%22%3A%22%27self%27%22%2C%22media-src%22%3A%22%27self%27%22%7D%2C%22ember-pretenderify%22%3A%7B%22usingProxy%22%3Afalse%7D%2C%22exportApplicationGlobal%22%3Afalse%7D" />
12
12
 
13
13
  <script>
14
14
  window.RAILS_ENV = {
@@ -16,15 +16,15 @@
16
16
  }
17
17
  </script>
18
18
  <link rel="stylesheet" href="/front_end_builds/assets/vendor-a8105f0efcfa2b3ff559e3a06333c43e.css">
19
- <link rel="stylesheet" href="/front_end_builds/assets/admin-bd8ae9b4c42b63d1827d52d21829066d.css">
19
+ <link rel="stylesheet" href="/front_end_builds/assets/admin-e75b37fa41fedcd7e63427d0d395331a.css">
20
20
 
21
21
 
22
22
  </head>
23
23
  <body>
24
24
 
25
25
 
26
- <script src="/front_end_builds/assets/vendor-7a09b85604a427672dac82dc0bcac289.js"></script>
27
- <script src="/front_end_builds/assets/admin-c8d319818b689f7119754abfe0c8742d.js"></script>
26
+ <script src="/front_end_builds/assets/vendor-04e7c54f17406375028018e3cb9433af.js"></script>
27
+ <script src="/front_end_builds/assets/admin-8ff31b44f8d2b836f2f6a419fb1d4766.js"></script>
28
28
 
29
29
 
30
30
  </body>
data/config/routes.rb CHANGED
@@ -3,6 +3,7 @@ FrontEndBuilds::Engine.routes.draw do
3
3
  scope :api, path: '/api' do
4
4
  resources :apps, only: [:index, :show, :create, :update, :destroy]
5
5
  resources :builds, only: [:index, :show]
6
+ resources :pubkeys, only: [:index, :create, :destroy]
6
7
  resources :host_apps, only: [:show]
7
8
  end
8
9
 
@@ -0,0 +1,10 @@
1
+ class CreateFrontEndBuildsPubkeys < ActiveRecord::Migration
2
+ def change
3
+ create_table :front_end_builds_pubkeys do |t|
4
+ t.string :name, null: false
5
+ t.text :pubkey, null: false
6
+
7
+ t.timestamps null: false
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,7 @@
1
+ class AddPubkeyToBuild < ActiveRecord::Migration
2
+ def change
3
+ # Track what public deployed each build
4
+ add_column :front_end_builds_builds, :pubkey_id, :integer
5
+ add_column :front_end_builds_builds, :signature, :text
6
+ end
7
+ end
@@ -0,0 +1,5 @@
1
+ class RemoveApiKeyFromApps < ActiveRecord::Migration
2
+ def change
3
+ remove_column :front_end_builds_apps, :api_key
4
+ end
5
+ end
@@ -0,0 +1,92 @@
1
+ # This is needed to convert an SSH pubkey into a RSA OpenSSL pubkey that we can
2
+ # use to verify the signature. I got this from:
3
+ #
4
+ # https://github.com/mytestbed/omf/blob/master/omf_common/lib/omf_common/auth/ssh_pub_key_convert.rb
5
+ #
6
+
7
+ module FrontEndBuilds::Utils
8
+ # Copyright (c) 2012 National ICT Australia Limited (NICTA).
9
+ # This software may be used and distributed solely under the terms of the MIT license (License).
10
+ # You should find a copy of the License in LICENSE.TXT or at http://opensource.org/licenses/MIT.
11
+ # By downloading or using this software you accept the terms and the liability disclaimer in the License.
12
+
13
+ require 'base64'
14
+ require 'openssl'
15
+
16
+ # This file provides a converter that accepts an SSH public key string
17
+ # and converts it to an OpenSSL::PKey::RSA object for use in verifying
18
+ # received messages. (DSA support pending).
19
+ #
20
+ class SSHPubKeyConvert
21
+ # Unpack a 4-byte unsigned integer from the +bytes+ array.
22
+ #
23
+ # Returns a pair (+u32+, +bytes+), where +u32+ is the extracted
24
+ # unsigned integer, and +bytes+ is the remainder of the original
25
+ # +bytes+ array that follows +u32+.
26
+ #
27
+ def self.unpack_u32(bytes)
28
+ return bytes.unpack("N")[0], bytes[4..-1]
29
+ end
30
+
31
+ # Unpack a string from the +bytes+ array. Exactly +len+ bytes will
32
+ # be extracted.
33
+ #
34
+ # Returns a pair (+string+, +bytes+), where +string+ is the
35
+ # extracted string (of length +len+), and +bytes+ is the remainder
36
+ # of the original +bytes+ array that follows +string+.
37
+ #
38
+ def self.unpack_string(bytes, len)
39
+ return bytes.unpack("A#{len}")[0], bytes[len..-1]
40
+ end
41
+
42
+ # Convert a string in SSH public key format to a key object
43
+ # suitable for use with OpenSSL. If the key is an RSA key then an
44
+ # OpenSSL::PKey::RSA object is returned. If the key is a DSA key
45
+ # then an OpenSSL::PKey::DSA object is returned. In either case,
46
+ # the object returned is suitable for encrypting data or verifying
47
+ # signatures, but cannot be used for decrypting or signing.
48
+ #
49
+ # The +keystring+ should be a single line, as per an SSH public key
50
+ # file as generated by +ssh-keygen+, or a line from an SSH
51
+ # +authorized_keys+ file.
52
+ #
53
+ def self.convert(keystring)
54
+ (_, b64, _) = keystring.split(' ')
55
+ raise ArgumentError, "Invalid SSH public key '#{keystring}'" if b64.nil?
56
+
57
+ decoded_key = Base64.decode64(b64)
58
+ (n, bytes) = unpack_u32(decoded_key)
59
+ (keytype, bytes) = unpack_string(bytes, n)
60
+
61
+ if keytype == "ssh-rsa"
62
+ (n, bytes) = unpack_u32(bytes)
63
+ (estr, bytes) = unpack_string(bytes, n)
64
+ (n, bytes) = unpack_u32(bytes)
65
+ (nstr, bytes) = unpack_string(bytes, n)
66
+
67
+ key = OpenSSL::PKey::RSA.new
68
+ key.n = OpenSSL::BN.new(nstr, 2)
69
+ key.e = OpenSSL::BN.new(estr, 2)
70
+ key
71
+ elsif keytype == 'ssh-dss'
72
+ (n, bytes) = unpack_u32(bytes)
73
+ (pstr, bytes) = unpack_string(bytes, n)
74
+ (n, bytes) = unpack_u32(bytes)
75
+ (qstr, bytes) = unpack_string(bytes, n)
76
+ (n, bytes) = unpack_u32(bytes)
77
+ (gstr, bytes) = unpack_string(bytes, n)
78
+ (n, bytes) = unpack_u32(bytes)
79
+ (pkstr, bytes) = unpack_string(bytes, n)
80
+
81
+ key = OpenSSL::PKey::DSA.new
82
+ key.p = OpenSSL::BN.new(pstr, 2)
83
+ key.q = OpenSSL::BN.new(qstr, 2)
84
+ key.g = OpenSSL::BN.new(gstr, 2)
85
+ key.pub_key = OpenSSL::BN.new(pkstr, 2)
86
+ key
87
+ else
88
+ nil
89
+ end
90
+ end
91
+ end
92
+ end
@@ -1,3 +1,3 @@
1
1
  module FrontEndBuilds
2
- VERSION = "0.0.26"
2
+ VERSION = "0.1.0"
3
3
  end