front_end_builds 0.0.26 → 0.1.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.
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