insights-api-common 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +202 -0
  3. data/README.md +102 -0
  4. data/Rakefile +18 -0
  5. data/app/models/authentication.rb +19 -0
  6. data/app/models/concerns/encryption_concern.rb +52 -0
  7. data/app/models/encryption.rb +13 -0
  8. data/lib/generators/shared_utilities/migration_generator.rb +79 -0
  9. data/lib/generators/shared_utilities/orm_helper.rb +25 -0
  10. data/lib/generators/shared_utilities/templates/migration.rb +27 -0
  11. data/lib/generators/shared_utilities/templates/migration_existing.rb +28 -0
  12. data/lib/insights.rb +1 -0
  13. data/lib/insights/api/common.rb +12 -0
  14. data/lib/insights/api/common/application_controller_mixins/api_doc.rb +39 -0
  15. data/lib/insights/api/common/application_controller_mixins/common.rb +27 -0
  16. data/lib/insights/api/common/application_controller_mixins/exception_handling.rb +41 -0
  17. data/lib/insights/api/common/application_controller_mixins/openapi_enabled.rb +13 -0
  18. data/lib/insights/api/common/application_controller_mixins/parameters.rb +134 -0
  19. data/lib/insights/api/common/application_controller_mixins/request_body_validation.rb +48 -0
  20. data/lib/insights/api/common/application_controller_mixins/request_parameter_validation.rb +29 -0
  21. data/lib/insights/api/common/application_controller_mixins/request_path.rb +70 -0
  22. data/lib/insights/api/common/engine.rb +20 -0
  23. data/lib/insights/api/common/entitlement.rb +37 -0
  24. data/lib/insights/api/common/error_document.rb +29 -0
  25. data/lib/insights/api/common/filter.rb +175 -0
  26. data/lib/insights/api/common/graphql.rb +127 -0
  27. data/lib/insights/api/common/graphql/associated_records.rb +44 -0
  28. data/lib/insights/api/common/graphql/association_loader.rb +35 -0
  29. data/lib/insights/api/common/graphql/generator.rb +148 -0
  30. data/lib/insights/api/common/graphql/templates/model_type.erb +35 -0
  31. data/lib/insights/api/common/graphql/templates/query_type.erb +49 -0
  32. data/lib/insights/api/common/graphql/templates/schema.erb +6 -0
  33. data/lib/insights/api/common/graphql/types/big_int.rb +23 -0
  34. data/lib/insights/api/common/graphql/types/date_time.rb +16 -0
  35. data/lib/insights/api/common/graphql/types/query_filter.rb +16 -0
  36. data/lib/insights/api/common/graphql/types/query_sort_by.rb +16 -0
  37. data/lib/insights/api/common/inflections.rb +28 -0
  38. data/lib/insights/api/common/logging.rb +17 -0
  39. data/lib/insights/api/common/metrics.rb +39 -0
  40. data/lib/insights/api/common/middleware/web_server_metrics.rb +62 -0
  41. data/lib/insights/api/common/open_api.rb +2 -0
  42. data/lib/insights/api/common/open_api/docs.rb +54 -0
  43. data/lib/insights/api/common/open_api/docs/component_collection.rb +67 -0
  44. data/lib/insights/api/common/open_api/docs/doc_v3.rb +102 -0
  45. data/lib/insights/api/common/open_api/docs/object_definition.rb +39 -0
  46. data/lib/insights/api/common/open_api/generator.rb +520 -0
  47. data/lib/insights/api/common/open_api/serializer.rb +31 -0
  48. data/lib/insights/api/common/option_redirect_enhancements.rb +23 -0
  49. data/lib/insights/api/common/paginated_response.rb +108 -0
  50. data/lib/insights/api/common/rbac/access.rb +66 -0
  51. data/lib/insights/api/common/rbac/acl.rb +74 -0
  52. data/lib/insights/api/common/rbac/policies.rb +33 -0
  53. data/lib/insights/api/common/rbac/query_shared_resource.rb +45 -0
  54. data/lib/insights/api/common/rbac/roles.rb +77 -0
  55. data/lib/insights/api/common/rbac/seed.rb +140 -0
  56. data/lib/insights/api/common/rbac/service.rb +67 -0
  57. data/lib/insights/api/common/rbac/share_resource.rb +60 -0
  58. data/lib/insights/api/common/rbac/unshare_resource.rb +32 -0
  59. data/lib/insights/api/common/rbac/utilities.rb +30 -0
  60. data/lib/insights/api/common/request.rb +111 -0
  61. data/lib/insights/api/common/routing.rb +26 -0
  62. data/lib/insights/api/common/user.rb +48 -0
  63. data/lib/insights/api/common/version.rb +7 -0
  64. data/lib/tasks/insights/api/common_tasks.rake +4 -0
  65. data/spec/support/default_as_json.rb +17 -0
  66. data/spec/support/rbac_shared_contexts.rb +44 -0
  67. data/spec/support/requests_spec_helper.rb +7 -0
  68. data/spec/support/service_spec_helper.rb +26 -0
  69. data/spec/support/user_header_spec_helper.rb +68 -0
  70. metadata +403 -0
@@ -0,0 +1,140 @@
1
+ module Insights
2
+ module API
3
+ module Common
4
+ module RBAC
5
+ require 'rbac-api-client'
6
+
7
+ class Seed
8
+ def initialize(seed_file, user_file = nil)
9
+ @acl_data = YAML.load_file(seed_file)
10
+ @request = Insights::API::Common::Request.current || create_request(user_file)
11
+ end
12
+
13
+ def process
14
+ Insights::API::Common::Request.with_request(@request) do
15
+ create_groups
16
+ create_roles
17
+ create_policies
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def create_groups
24
+ current = current_groups
25
+ names = current.collect(&:name)
26
+ group = RBACApiClient::Group.new
27
+ begin
28
+ Service.call(RBACApiClient::GroupApi) do |api_instance|
29
+ @acl_data['groups'].each do |grp|
30
+ next if names.include?(grp['name'])
31
+
32
+ Rails.logger.info("Creating #{grp['name']}")
33
+ group.name = grp['name']
34
+ group.description = grp['description']
35
+ api_instance.create_group(group)
36
+ end
37
+ end
38
+ rescue RBACApiClient::ApiError => e
39
+ Rails.logger.error("Exception when calling GroupApi->create_group: #{e}")
40
+ raise
41
+ end
42
+ end
43
+
44
+ def current_groups
45
+ Service.call(RBACApiClient::GroupApi) do |api|
46
+ Service.paginate(api, :list_groups, {}).to_a
47
+ end
48
+ end
49
+
50
+ def create_roles
51
+ current = current_roles
52
+ names = current.collect(&:name)
53
+ role_in = RBACApiClient::RoleIn.new
54
+ begin
55
+ Service.call(RBACApiClient::RoleApi) do |api_instance|
56
+ @acl_data['roles'].each do |role|
57
+ next if names.include?(role['name'])
58
+
59
+ role_in.name = role['name']
60
+ role_in.access = []
61
+ role['access'].each do |obj|
62
+ access = RBACApiClient::Access.new
63
+ access.permission = obj['permission']
64
+ access.resource_definitions = create_rds(obj)
65
+ role_in.access << access
66
+ end
67
+ api_instance.create_roles(role_in)
68
+ end
69
+ end
70
+ rescue RBACApiClient::ApiError => e
71
+ Rails.logger.error("Exception when calling RoleApi->create_roles: #{e}")
72
+ raise
73
+ end
74
+ end
75
+
76
+ def create_rds(obj)
77
+ obj.fetch('resource_definitions', []).collect do |item|
78
+ RBACApiClient::ResourceDefinition.new.tap do |rd|
79
+ rd.attribute_filter = RBACApiClient::ResourceDefinitionFilter.new.tap do |rdf|
80
+ rdf.key = item['attribute_filter']['key']
81
+ rdf.value = item['attribute_filter']['value']
82
+ rdf.operation = item['attribute_filter']['operation']
83
+ end
84
+ end
85
+ end
86
+ end
87
+
88
+ def current_roles
89
+ Service.call(RBACApiClient::RoleApi) do |api|
90
+ Service.paginate(api, :list_roles, {}).to_a
91
+ end
92
+ end
93
+
94
+ def create_policies
95
+ names = current_policies.collect(&:name)
96
+ groups = current_groups
97
+ roles = current_roles
98
+ policy_in = RBACApiClient::PolicyIn.new
99
+ begin
100
+ Service.call(RBACApiClient::PolicyApi) do |api_instance|
101
+ @acl_data['policies'].each do |policy|
102
+ next if names.include?(policy['name'])
103
+
104
+ policy_in.name = policy['name']
105
+ policy_in.description = policy['description']
106
+ policy_in.group = find_uuid('Group', groups, policy['group']['name'])
107
+ policy_in.roles = [find_uuid('Role', roles, policy['role']['name'])]
108
+ api_instance.create_policies(policy_in)
109
+ end
110
+ end
111
+ rescue RBACApiClient::ApiError => e
112
+ Rails.logger.error("Exception when calling PolicyApi->create_policies: #{e}")
113
+ raise
114
+ end
115
+ end
116
+
117
+ def current_policies
118
+ Service.call(RBACApiClient::PolicyApi) do |api|
119
+ Service.paginate(api, :list_policies, {}).to_a
120
+ end
121
+ end
122
+
123
+ def find_uuid(type, data, name)
124
+ result = data.detect { |item| item.name == name }
125
+ raise "#{type} #{name} not found in RBAC service" unless result
126
+
127
+ result.uuid
128
+ end
129
+
130
+ def create_request(user_file)
131
+ raise "File #{user_file} not found" unless File.exist?(user_file)
132
+
133
+ user = YAML.load_file(user_file)
134
+ {:headers => {'x-rh-identity' => Base64.strict_encode64(user.to_json)}, :original_url => '/'}
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,67 @@
1
+ module Insights
2
+ module API
3
+ module Common
4
+ module RBAC
5
+ require 'rbac-api-client'
6
+
7
+ class Service
8
+ def self.call(klass)
9
+ setup
10
+ yield init(klass)
11
+ rescue RBACApiClient::ApiError => err
12
+ Rails.logger.error("RBACApiClient::ApiError #{err.message} ")
13
+ raise
14
+ end
15
+
16
+ def self.paginate(obj, method, pagination_options, *method_args)
17
+ Enumerator.new do |enum|
18
+ opts = { :limit => 10, :offset => 0 }.merge(pagination_options)
19
+ count = nil
20
+ fetched = 0
21
+ begin
22
+ loop do
23
+ args = [method_args, opts].flatten.compact
24
+ result = obj.send(method, *args)
25
+ count ||= result.meta.count
26
+ opts[:offset] = opts[:offset] + result.data.count
27
+ result.data.each do |element|
28
+ enum.yield element
29
+ end
30
+ fetched += result.data.count
31
+ break if count == fetched || result.data.empty?
32
+ end
33
+ rescue StandardError => e
34
+ Rails.logger.error("Exception when calling pagination on #{method} #{e}")
35
+ raise
36
+ end
37
+ end
38
+ end
39
+
40
+ private_class_method def self.setup
41
+ RBACApiClient.configure do |config|
42
+ config.host = ENV['RBAC_URL'] || 'localhost'
43
+ config.scheme = URI.parse(ENV['RBAC_URL']).try(:scheme) || 'http'
44
+ dev_credentials(config)
45
+ end
46
+ end
47
+
48
+ private_class_method def self.init(klass)
49
+ headers = Insights::API::Common::Request.current_forwardable
50
+ Rails.logger.info("Sending Headers to RBAC #{headers}")
51
+ klass.new.tap do |api|
52
+ api.api_client.default_headers = api.api_client.default_headers.merge(headers)
53
+ end
54
+ end
55
+
56
+ private_class_method def self.dev_credentials(config)
57
+ # Set up user/pass for basic auth if we're in dev and they exist.
58
+ if Rails.env.development?
59
+ config.username = ENV.fetch('DEV_USERNAME')
60
+ config.password = ENV.fetch('DEV_PASSWORD')
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,60 @@
1
+ module Insights
2
+ module API
3
+ module Common
4
+ module RBAC
5
+ class ShareResource
6
+ require 'rbac-api-client'
7
+ include Utilities
8
+
9
+ def initialize(options)
10
+ @app_name = options[:app_name]
11
+ @resource_name = options[:resource_name]
12
+ @permissions = options[:permissions]
13
+ @resource_ids = options[:resource_ids]
14
+ @group_uuids = SortedSet.new(options[:group_uuids])
15
+ @acls = RBAC::ACL.new
16
+ end
17
+
18
+ def process
19
+ validate_groups
20
+ @roles = RBAC::Roles.new("#{@app_name}-#{@resource_name}-", 'account')
21
+ @group_uuids.each { |uuid| manage_roles_for_group(uuid) }
22
+ self
23
+ end
24
+
25
+ private
26
+
27
+ def manage_roles_for_group(group_uuid)
28
+ @resource_ids.each do |resource_id|
29
+ name = unique_name(resource_id, group_uuid)
30
+ role = @roles.find(name)
31
+ role ? update_existing_role(role, resource_id) : add_new_role(name, group_uuid, resource_id)
32
+ end
33
+ end
34
+
35
+ def update_existing_role(role, resource_id)
36
+ role.access = @acls.add(role.access, resource_id, @permissions)
37
+ @roles.update(role) if role.access.present?
38
+ end
39
+
40
+ def add_new_role(name, group_uuid, resource_id)
41
+ acls = @acls.create(resource_id, @permissions)
42
+ role = @roles.add(name, acls)
43
+ add_policy(name, group_uuid, role.uuid)
44
+ end
45
+
46
+ def add_policy(name, group_uuid, role_uuid)
47
+ Service.call(RBACApiClient::PolicyApi) do |api_instance|
48
+ policy_in = RBACApiClient::PolicyIn.new
49
+ policy_in.name = name
50
+ policy_in.description = 'Shared Policy'
51
+ policy_in.group = group_uuid
52
+ policy_in.roles = [role_uuid]
53
+ api_instance.create_policies(policy_in)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,32 @@
1
+ module Insights
2
+ module API
3
+ module Common
4
+ module RBAC
5
+ require 'rbac-api-client'
6
+
7
+ class UnshareResource < ShareResource
8
+ attr_accessor :count
9
+
10
+ def initialize(options)
11
+ @count = 0
12
+ super
13
+ end
14
+
15
+ private
16
+
17
+ def manage_roles_for_group(group_uuid)
18
+ @resource_ids.each do |resource_id|
19
+ name = unique_name(resource_id, group_uuid)
20
+ role = @roles.find(name)
21
+ next unless role
22
+
23
+ role.access = @acls.remove(role.access, resource_id, @permissions)
24
+ role.access.present? ? @roles.update(role) : @roles.delete(role)
25
+ @count += 1
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,30 @@
1
+ module Insights
2
+ module API
3
+ module Common
4
+ module RBAC
5
+ module Utilities
6
+ def validate_groups
7
+ Service.call(RBACApiClient::GroupApi) do |api|
8
+ uuids = SortedSet.new
9
+ Service.paginate(api, :list_groups, {}).each { |group| uuids << group.uuid }
10
+ missing = @group_uuids - uuids
11
+ raise Insights::API::Common::InvalidParameter, "The following group uuids are missing #{missing.to_a.join(",")}" unless missing.empty?
12
+ end
13
+ end
14
+
15
+ def unique_name(resource_id, group_id)
16
+ "#{@app_name}-#{@resource_name}-#{resource_id}-group-#{group_id}"
17
+ end
18
+
19
+ def parse_ids_from_name(name)
20
+ @regexp ||= Regexp.new("#{@app_name}-#{@resource_name}-(?<resource_id>.*)-group-(?<group_uuid>.*)")
21
+ result = @regexp.match(name)
22
+ if result
23
+ [result[:resource_id], result[:group_uuid]]
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,111 @@
1
+ module Insights
2
+ module API
3
+ module Common
4
+ class RequestNotSet < ArgumentError
5
+ def initialize
6
+ super("Current request has not been set")
7
+ end
8
+ end
9
+
10
+ class InvalidParameter < StandardError
11
+ end
12
+
13
+ class Request
14
+ REQUEST_ID_KEY = "x-rh-insights-request-id".freeze
15
+ IDENTITY_KEY = 'x-rh-identity'.freeze
16
+ PERSONA_KEY = 'x-rh-persona'.freeze
17
+ FORWARDABLE_HEADER_KEYS = [REQUEST_ID_KEY, IDENTITY_KEY, PERSONA_KEY].freeze
18
+ OPTIONAL_AUTH_PATHS = [
19
+ %r{\A/api/v[0-9]+(\.[0-9]+)?/openapi.json\z},
20
+ %r{\A/api/[^/]+/v[0-9]+(\.[0-9]+)?/openapi.json\z}
21
+ ].freeze
22
+
23
+ def self.current
24
+ Thread.current[:current_request]
25
+ end
26
+
27
+ def self.current!
28
+ current || raise(RequestNotSet)
29
+ end
30
+
31
+ def self.current=(request)
32
+ Thread.current[:current_request] =
33
+ case request
34
+ when ActionDispatch::Request
35
+ new(:headers => request.headers, :original_url => request.original_url)
36
+ when Hash
37
+ new(request)
38
+ when Request, nil
39
+ request
40
+ else
41
+ raise ArgumentError, 'Not an Insights::API::Common::Request or ActionDispatch::Request Class, Hash, or nil'
42
+ end
43
+ end
44
+
45
+ def self.with_request(request)
46
+ saved = current
47
+ self.current = request
48
+ yield current
49
+ ensure
50
+ self.current = saved
51
+ end
52
+
53
+ def self.current_forwardable
54
+ current!.forwardable
55
+ end
56
+
57
+ attr_reader :headers, :original_url
58
+
59
+ def initialize(headers:, original_url:, **_kwargs)
60
+ headers = from_hash(headers) if headers.kind_of?(Hash)
61
+ @headers, @original_url = headers, original_url
62
+ end
63
+
64
+ def request_id
65
+ headers.fetch(REQUEST_ID_KEY, nil)
66
+ end
67
+
68
+ def identity
69
+ @identity ||= JSON.parse(Base64.decode64(headers.fetch(IDENTITY_KEY)))
70
+ rescue KeyError
71
+ raise IdentityError, "x-rh-identity not found"
72
+ end
73
+
74
+ def user
75
+ @user ||= User.new(identity)
76
+ end
77
+
78
+ def entitlement
79
+ @entitlement ||= Entitlement.new(identity)
80
+ end
81
+
82
+ def to_h
83
+ {:headers => forwardable, :original_url => original_url}
84
+ end
85
+
86
+ def forwardable
87
+ FORWARDABLE_HEADER_KEYS.each_with_object({}) do |key, hash|
88
+ hash[key] = headers[key] if headers.key?(key)
89
+ end
90
+ end
91
+
92
+ def required_auth?
93
+ !optional_auth?
94
+ end
95
+
96
+ def optional_auth?
97
+ uri_path = URI.parse(original_url).path
98
+ OPTIONAL_AUTH_PATHS.any? { |optional_auth_path_regex| optional_auth_path_regex.match(uri_path) }
99
+ end
100
+
101
+ private
102
+
103
+ def from_hash(hash)
104
+ ActionDispatch::Http::Headers.from_hash({}).tap do |headers|
105
+ hash.each { |k, v| headers.add(k, v) }
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,26 @@
1
+ module Insights
2
+ module API
3
+ module Common
4
+ class Routing
5
+ attr_reader :route_mapper
6
+
7
+ def initialize(route_mapper)
8
+ @route_mapper = route_mapper
9
+ end
10
+
11
+ def redirect_major_version(version, prefix, via: [:delete, :get, :options, :patch, :post])
12
+ route_mapper.match(
13
+ "/#{version.split('.').first}/*path(.:format)",
14
+ :format => false,
15
+ :via => via,
16
+ :to => route_mapper.redirect(
17
+ :path => "/#{prefix}/#{version}/%{path}",
18
+ :only_path => true,
19
+ :status => 302
20
+ )
21
+ )
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end