insights-api-common 3.0.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.
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