aaf-gumboot 1.0.0.pre.alpha.2

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 (90) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +15 -0
  5. data/Gemfile +4 -0
  6. data/Guardfile +18 -0
  7. data/LICENSE +202 -0
  8. data/README.md +1069 -0
  9. data/Rakefile +8 -0
  10. data/aaf-gumboot.gemspec +42 -0
  11. data/lib/aaf-gumboot.rb +1 -0
  12. data/lib/gumboot.rb +5 -0
  13. data/lib/gumboot/shared_examples/anonymous_controller.rb +17 -0
  14. data/lib/gumboot/shared_examples/api_constraints.rb +29 -0
  15. data/lib/gumboot/shared_examples/api_controller.rb +206 -0
  16. data/lib/gumboot/shared_examples/api_subjects.rb +44 -0
  17. data/lib/gumboot/shared_examples/application_controller.rb +223 -0
  18. data/lib/gumboot/shared_examples/database_schema.rb +45 -0
  19. data/lib/gumboot/shared_examples/foreign_keys.rb +65 -0
  20. data/lib/gumboot/shared_examples/permissions.rb +45 -0
  21. data/lib/gumboot/shared_examples/roles.rb +15 -0
  22. data/lib/gumboot/shared_examples/subjects.rb +29 -0
  23. data/lib/gumboot/strap.rb +121 -0
  24. data/lib/gumboot/version.rb +3 -0
  25. data/spec/dummy/README.rdoc +28 -0
  26. data/spec/dummy/Rakefile +3 -0
  27. data/spec/dummy/app/assets/images/.keep +0 -0
  28. data/spec/dummy/app/assets/javascripts/application.js +13 -0
  29. data/spec/dummy/app/assets/stylesheets/application.css +15 -0
  30. data/spec/dummy/app/controllers/api/api_controller.rb +78 -0
  31. data/spec/dummy/app/controllers/application_controller.rb +64 -0
  32. data/spec/dummy/app/controllers/concerns/.keep +0 -0
  33. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  34. data/spec/dummy/app/mailers/.keep +0 -0
  35. data/spec/dummy/app/models/.keep +0 -0
  36. data/spec/dummy/app/models/api_subject.rb +23 -0
  37. data/spec/dummy/app/models/api_subject_role.rb +6 -0
  38. data/spec/dummy/app/models/concerns/.keep +0 -0
  39. data/spec/dummy/app/models/permission.rb +7 -0
  40. data/spec/dummy/app/models/role.rb +11 -0
  41. data/spec/dummy/app/models/subject.rb +20 -0
  42. data/spec/dummy/app/models/subject_role.rb +6 -0
  43. data/spec/dummy/app/views/dynamic_errors/forbidden.html.erb +0 -0
  44. data/spec/dummy/app/views/dynamic_errors/unauthorized.html.erb +0 -0
  45. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  46. data/spec/dummy/bin/bundle +3 -0
  47. data/spec/dummy/bin/rails +4 -0
  48. data/spec/dummy/bin/rake +4 -0
  49. data/spec/dummy/config.ru +4 -0
  50. data/spec/dummy/config/application.rb +18 -0
  51. data/spec/dummy/config/boot.rb +5 -0
  52. data/spec/dummy/config/database.yml +5 -0
  53. data/spec/dummy/config/environment.rb +5 -0
  54. data/spec/dummy/config/environments/development.rb +32 -0
  55. data/spec/dummy/config/environments/production.rb +37 -0
  56. data/spec/dummy/config/environments/test.rb +33 -0
  57. data/spec/dummy/config/initializers/assets.rb +4 -0
  58. data/spec/dummy/config/initializers/backtrace_silencers.rb +0 -0
  59. data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
  60. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  61. data/spec/dummy/config/initializers/inflections.rb +15 -0
  62. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  63. data/spec/dummy/config/initializers/session_store.rb +3 -0
  64. data/spec/dummy/config/initializers/wrap_parameters.rb +9 -0
  65. data/spec/dummy/config/locales/en.yml +23 -0
  66. data/spec/dummy/config/routes.rb +2 -0
  67. data/spec/dummy/config/secrets.yml +22 -0
  68. data/spec/dummy/db/schema.rb +51 -0
  69. data/spec/dummy/db/test.sqlite3 +0 -0
  70. data/spec/dummy/lib/api_constraints.rb +16 -0
  71. data/spec/dummy/lib/assets/.keep +0 -0
  72. data/spec/dummy/public/404.html +67 -0
  73. data/spec/dummy/public/422.html +67 -0
  74. data/spec/dummy/public/500.html +66 -0
  75. data/spec/dummy/public/favicon.ico +0 -0
  76. data/spec/factories/api_subjects.rb +20 -0
  77. data/spec/factories/permissions.rb +6 -0
  78. data/spec/factories/roles.rb +5 -0
  79. data/spec/factories/subjects.rb +24 -0
  80. data/spec/gumboot/api_constraints_spec.rb +18 -0
  81. data/spec/gumboot/api_controller_spec.rb +7 -0
  82. data/spec/gumboot/api_subjects_spec.rb +7 -0
  83. data/spec/gumboot/application_controller_spec.rb +7 -0
  84. data/spec/gumboot/foreign_keys_spec.rb +7 -0
  85. data/spec/gumboot/permissions_spec.rb +7 -0
  86. data/spec/gumboot/roles_spec.rb +7 -0
  87. data/spec/gumboot/subjects_spec.rb +7 -0
  88. data/spec/lib/gumboot/strap_spec.rb +330 -0
  89. data/spec/spec_helper.rb +45 -0
  90. metadata +387 -0
@@ -0,0 +1,45 @@
1
+ RSpec.shared_examples 'Database Schema' do
2
+ context 'AAF shared implementation' do
3
+ RSpec::Matchers.define :have_collation do |expected, name|
4
+ match { |actual| actual[:Collation] == expected }
5
+
6
+ failure_message do |actual|
7
+ "expected #{name} to use collation #{expected}, but was " \
8
+ "#{actual[:Collation]}"
9
+ end
10
+ end
11
+
12
+ before { expect(connection).to be_a(Mysql2::Client) }
13
+
14
+ def query(sql)
15
+ connection.query(sql, as: :hash, symbolize_keys: true)
16
+ end
17
+
18
+ it 'has the correct encoding set for the connection' do
19
+ expect(connection.query_options).to include(encoding: 'utf8')
20
+ end
21
+
22
+ it 'has the correct collation set for the connection' do
23
+ expect(connection.query_options).to include(collation: 'utf8_bin')
24
+ end
25
+
26
+ it 'has the correct collation' do
27
+ db_collation = query('SHOW VARIABLES LIKE "collation_database"')
28
+ .first[:Value]
29
+ expect(db_collation).to eq('utf8_bin')
30
+
31
+ query('SHOW TABLE STATUS').each do |table|
32
+ table_name = table[:Name]
33
+ next if table_name == 'schema_migrations'
34
+ expect(table).to have_collation('utf8_bin', "`#{table_name}`")
35
+
36
+ query("SHOW FULL COLUMNS FROM #{table[:Name]}").each do |column|
37
+ next unless column[:Collation]
38
+ expect(column)
39
+ .to have_collation('utf8_bin',
40
+ " `#{table_name}`.`#{column[:Field]}`")
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,65 @@
1
+ RSpec.shared_examples 'Gumboot Foreign Keys' do
2
+ RSpec.shared_examples 'gumboot fk' do
3
+ let(:conn) do
4
+ ActiveRecord::Base.connection
5
+ end
6
+
7
+ let(:foreign_key) do
8
+ conn.foreign_keys(from_table).find do |key|
9
+ key.options[:column] == column
10
+ end
11
+ end
12
+
13
+ it 'has valid foreign key' do
14
+ if conn.supports_foreign_keys?
15
+ expect(foreign_key).to(be_truthy)
16
+ expect(foreign_key.to_table).to eql to_table
17
+ end
18
+ end
19
+ end
20
+
21
+ context 'Permission' do
22
+ context 'Roles' do
23
+ include_examples 'gumboot fk' do
24
+ let(:from_table) { 'permissions' }
25
+ let(:to_table) { 'roles' }
26
+ let(:column) { 'role_id' }
27
+ end
28
+ end
29
+ end
30
+
31
+ context 'API Subject' do
32
+ context 'Roles' do
33
+ include_examples 'gumboot fk' do
34
+ let(:from_table) { 'api_subject_roles' }
35
+ let(:to_table) { 'api_subjects' }
36
+ let(:column) { 'api_subject_id' }
37
+ end
38
+ end
39
+ end
40
+
41
+ context 'Subject' do
42
+ context 'Roles' do
43
+ include_examples 'gumboot fk' do
44
+ let(:from_table) { 'subject_roles' }
45
+ let(:to_table) { 'subjects' }
46
+ let(:column) { 'subject_id' }
47
+ end
48
+ end
49
+ end
50
+
51
+ context 'Roles' do
52
+ let(:to_table) { 'roles' }
53
+ let(:column) { 'role_id' }
54
+ context 'Subjects' do
55
+ include_examples 'gumboot fk' do
56
+ let(:from_table) { 'subject_roles' }
57
+ end
58
+ end
59
+ context 'API Subjects' do
60
+ include_examples 'gumboot fk' do
61
+ let(:from_table) { 'api_subject_roles' }
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,45 @@
1
+ RSpec.shared_examples 'Permissions' do
2
+ context 'AAF shared implementation' do
3
+ subject { build :permission }
4
+
5
+ it { is_expected.to be_valid }
6
+
7
+ it 'is invalid without a role' do
8
+ subject.role = nil
9
+ expect(subject).not_to be_valid
10
+ end
11
+
12
+ it 'is invalid without a value' do
13
+ subject.value = nil
14
+ expect(subject).not_to be_valid
15
+ end
16
+
17
+ it 'allows wildcard values' do
18
+ subject.value = '*'
19
+ expect(subject).to be_valid
20
+ end
21
+
22
+ it 'allows permission string values' do
23
+ subject.value = 'a:b:c:d'
24
+ expect(subject).to be_valid
25
+ end
26
+
27
+ it 'disallows invalid characters' do
28
+ subject.value = 'a:b:%'
29
+ expect(subject).not_to be_valid
30
+ end
31
+
32
+ it 'must have a unique value per role' do
33
+ other = create(:permission, role: subject.role, value: 'other')
34
+
35
+ expect { subject.value = other.value }
36
+ .to change { subject.valid? }.to(be_falsey)
37
+ end
38
+
39
+ it 'can have a value used in a different role' do
40
+ other = create(:permission, value: 'other')
41
+ subject.value = other.value
42
+ expect(subject).to be_valid
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,15 @@
1
+ RSpec.shared_examples 'Roles' do
2
+ context 'AAF shared implementation' do
3
+ subject { build :role }
4
+
5
+ it { is_expected.to be_valid }
6
+ it { is_expected.to respond_to(:api_subjects) }
7
+ it { is_expected.to respond_to(:subjects) }
8
+ it { is_expected.to respond_to(:permissions) }
9
+
10
+ it 'is invalid without a name' do
11
+ subject.name = nil
12
+ expect(subject).not_to be_valid
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,29 @@
1
+ RSpec.shared_examples 'Subjects' do
2
+ context 'AAF shared implementation' do
3
+ subject { build :subject }
4
+
5
+ it { is_expected.to be_valid }
6
+ it { is_expected.to be_an(Accession::Principal) }
7
+ it { is_expected.to respond_to(:roles) }
8
+ it { is_expected.to respond_to(:permissions) }
9
+ it { is_expected.to respond_to(:permits?) }
10
+ it { is_expected.to respond_to(:functioning?) }
11
+
12
+ it 'is invalid without a name' do
13
+ subject.name = nil
14
+ expect(subject).not_to be_valid
15
+ end
16
+ it 'is invalid without mail' do
17
+ subject.mail = nil
18
+ expect(subject).not_to be_valid
19
+ end
20
+ it 'is invalid without an enabled state' do
21
+ subject.enabled = nil
22
+ expect(subject).not_to be_valid
23
+ end
24
+ it 'is invalid without a complete state' do
25
+ subject.complete = nil
26
+ expect(subject).not_to be_valid
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,121 @@
1
+ require 'yaml'
2
+ require 'active_support/core_ext/hash/deep_merge'
3
+
4
+ module Gumboot
5
+ module Strap
6
+ def client
7
+ file = File.expand_path('~/.my.cnf')
8
+ @client ||= Mysql2::Client.new(default_file: file,
9
+ default_group: 'client',
10
+ host: '127.0.0.1')
11
+ end
12
+
13
+ def ensure_activerecord_databases(environments)
14
+ environments.each do |env|
15
+ message "Preparing #{env} database"
16
+
17
+ db = ActiveRecord::Base.configurations[env]
18
+
19
+ ensure_database(db)
20
+ ensure_database_user(db)
21
+ end
22
+ end
23
+
24
+ def ensure_database(db)
25
+ adapter, database = db.values_at('adapter', 'database')
26
+ raise('Only supports mysql2 adapter') unless adapter == 'mysql2'
27
+
28
+ puts "Ensuring database `#{database}` exists"
29
+ client.query("CREATE DATABASE IF NOT EXISTS `#{database}` " \
30
+ 'CHARACTER SET utf8 COLLATE utf8_bin')
31
+ end
32
+
33
+ def ensure_database_user(db)
34
+ adapter, database, username, password =
35
+ db.values_at('adapter', 'database', 'username', 'password')
36
+
37
+ raise('Only supports mysql2 adapter') unless adapter == 'mysql2'
38
+
39
+ puts "Ensuring access to `#{database}` for #{username} user is granted"
40
+ client.query("GRANT ALL PRIVILEGES ON `#{database}`.* " \
41
+ "TO '#{client.escape(username)}'@'localhost' " \
42
+ "IDENTIFIED BY '#{client.escape(password)}'")
43
+ end
44
+
45
+ def maintain_activerecord_schema
46
+ message 'Loading database schema'
47
+
48
+ if ActiveRecord::Base.connection.execute('SHOW TABLES').count.zero?
49
+ puts 'No tables exist yet, loading schema'
50
+ system 'rake db:schema:load'
51
+ end
52
+
53
+ puts 'Running migrations'
54
+ system 'rake db:migrate'
55
+ end
56
+
57
+ def load_seeds
58
+ message 'Loading seeds'
59
+
60
+ system 'rake db:seed'
61
+ end
62
+
63
+ def clean_logs
64
+ message 'Removing old tempfiles'
65
+ system 'rm -f log/*'
66
+ end
67
+
68
+ def clean_tempfiles
69
+ message 'Removing old tempfiles'
70
+ system 'rm -rf tmp/cache'
71
+ end
72
+
73
+ def link_global_configuration(files)
74
+ files.each do |file|
75
+ src = File.expand_path("~/.aaf/#{file}")
76
+ raise("Missing global config file: #{src}") unless File.exist?(src)
77
+
78
+ dest = "config/#{file}"
79
+ next if File.exist?(dest)
80
+ FileUtils.ln_s(src, dest)
81
+ end
82
+ end
83
+
84
+ def update_local_configuration(files)
85
+ files.each do |file|
86
+ src = "config/#{file}.dist"
87
+ raise("Not a .yml file: #{file}") unless file.end_with?('.yml')
88
+ raise("Missing dist config file: #{src}") unless File.exist?(src)
89
+
90
+ merge_config(src, "config/#{file}")
91
+ end
92
+ end
93
+
94
+ def install_dist_template(files)
95
+ files.each do |file|
96
+ src = "config/#{file}.dist"
97
+ dest = "config/#{file}"
98
+
99
+ raise("Missing dist config file: #{src}") unless File.exist?(src)
100
+
101
+ next if File.exist?(dest)
102
+ FileUtils.copy(src, dest)
103
+ end
104
+ end
105
+
106
+ private
107
+
108
+ def message(msg)
109
+ puts "\n== #{msg} =="
110
+ end
111
+
112
+ def merge_config(src, dest)
113
+ new_config = YAML.load(File.read(src))
114
+ old_config = File.exist?(dest) ? YAML.load(File.read(dest)) : {}
115
+
116
+ File.open(dest, 'w') do |f|
117
+ f.write(YAML.dump(new_config.deep_merge(old_config)))
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,3 @@
1
+ module Gumboot
2
+ VERSION = '1.0.0-alpha.2'.freeze
3
+ end
@@ -0,0 +1,28 @@
1
+ == README
2
+
3
+ This README would normally document whatever steps are necessary to get the
4
+ application up and running.
5
+
6
+ Things you may want to cover:
7
+
8
+ * Ruby version
9
+
10
+ * System dependencies
11
+
12
+ * Configuration
13
+
14
+ * Database creation
15
+
16
+ * Database initialization
17
+
18
+ * How to run the test suite
19
+
20
+ * Services (job queues, cache servers, search engines, etc.)
21
+
22
+ * Deployment instructions
23
+
24
+ * ...
25
+
26
+
27
+ Please feel free to use a different markup language if you do not plan to run
28
+ <tt>rake doc:app</tt>.
@@ -0,0 +1,3 @@
1
+ require File.expand_path('../config/application', __FILE__)
2
+
3
+ Rails.application.load_tasks
File without changes
@@ -0,0 +1,13 @@
1
+ // This is a manifest file that'll be compiled into application.js, which will include all the files
2
+ // listed below.
3
+ //
4
+ // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5
+ // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
6
+ //
7
+ // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8
+ // compiled file.
9
+ //
10
+ // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details
11
+ // about supported directives.
12
+ //
13
+ //= require_tree .
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any styles
10
+ * defined in the other CSS/SCSS files in this directory. It is generally better to create a new
11
+ * file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
@@ -0,0 +1,78 @@
1
+ require 'openssl'
2
+
3
+ module API
4
+ class APIController < ActionController::Base
5
+ Forbidden = Class.new(StandardError)
6
+ private_constant :Forbidden
7
+ rescue_from Forbidden, with: :forbidden
8
+
9
+ Unauthorized = Class.new(StandardError)
10
+ private_constant :Unauthorized
11
+ rescue_from Unauthorized, with: :unauthorized
12
+
13
+ protect_from_forgery with: :null_session
14
+ before_action :ensure_authenticated
15
+ after_action :ensure_access_checked
16
+
17
+ attr_reader :subject
18
+
19
+ protected
20
+
21
+ def ensure_authenticated
22
+ # Ensure API subject exists and is functioning
23
+ @subject = APISubject.find_by(x509_cn: x509_cn)
24
+ raise(Unauthorized, 'Subject invalid') unless @subject
25
+ raise(Unauthorized, 'Subject not functional') unless @subject.functioning?
26
+ end
27
+
28
+ def ensure_access_checked
29
+ return if @access_checked
30
+
31
+ method = "#{self.class.name}##{params[:action]}"
32
+ raise("No access control performed by #{method}")
33
+ end
34
+
35
+ def x509_cn
36
+ # Verified DN pushed by nginx following successful client SSL verification
37
+ # nginx is always going to do a better job of terminating SSL then we can
38
+ raise(Unauthorized, 'Subject DN') if x509_dn.nil?
39
+
40
+ x509_dn_parsed = OpenSSL::X509::Name.parse(x509_dn)
41
+ x509_dn_hash = Hash[
42
+ x509_dn_parsed.to_a.map do |components|
43
+ components[0..1]
44
+ end
45
+ ]
46
+
47
+ x509_dn_hash['CN'] || raise(Unauthorized, 'Subject CN invalid')
48
+
49
+ rescue OpenSSL::X509::NameError
50
+ raise(Unauthorized, 'Subject DN invalid')
51
+ end
52
+
53
+ def x509_dn
54
+ x509_dn = request.headers['HTTP_X509_DN'].try(:force_encoding, 'UTF-8')
55
+ x509_dn == '(null)' ? nil : x509_dn
56
+ end
57
+
58
+ def check_access!(action)
59
+ raise(Forbidden) unless @subject.permits?(action)
60
+ @access_checked = true
61
+ end
62
+
63
+ def public_action
64
+ @access_checked = true
65
+ end
66
+
67
+ def unauthorized(exception)
68
+ message = 'SSL client failure.'
69
+ error = exception.message
70
+ render json: { message: message, error: error }, status: :unauthorized
71
+ end
72
+
73
+ def forbidden(_exception)
74
+ message = 'The request was understood but explicitly denied.'
75
+ render json: { message: message }, status: :forbidden
76
+ end
77
+ end
78
+ end