insights-api-common 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +202 -0
- data/README.md +102 -0
- data/Rakefile +18 -0
- data/app/models/authentication.rb +19 -0
- data/app/models/concerns/encryption_concern.rb +52 -0
- data/app/models/encryption.rb +13 -0
- data/lib/generators/shared_utilities/migration_generator.rb +79 -0
- data/lib/generators/shared_utilities/orm_helper.rb +25 -0
- data/lib/generators/shared_utilities/templates/migration.rb +27 -0
- data/lib/generators/shared_utilities/templates/migration_existing.rb +28 -0
- data/lib/insights.rb +1 -0
- data/lib/insights/api/common.rb +12 -0
- data/lib/insights/api/common/application_controller_mixins/api_doc.rb +39 -0
- data/lib/insights/api/common/application_controller_mixins/common.rb +27 -0
- data/lib/insights/api/common/application_controller_mixins/exception_handling.rb +41 -0
- data/lib/insights/api/common/application_controller_mixins/openapi_enabled.rb +13 -0
- data/lib/insights/api/common/application_controller_mixins/parameters.rb +134 -0
- data/lib/insights/api/common/application_controller_mixins/request_body_validation.rb +48 -0
- data/lib/insights/api/common/application_controller_mixins/request_parameter_validation.rb +29 -0
- data/lib/insights/api/common/application_controller_mixins/request_path.rb +70 -0
- data/lib/insights/api/common/engine.rb +20 -0
- data/lib/insights/api/common/entitlement.rb +37 -0
- data/lib/insights/api/common/error_document.rb +29 -0
- data/lib/insights/api/common/filter.rb +175 -0
- data/lib/insights/api/common/graphql.rb +127 -0
- data/lib/insights/api/common/graphql/associated_records.rb +44 -0
- data/lib/insights/api/common/graphql/association_loader.rb +35 -0
- data/lib/insights/api/common/graphql/generator.rb +148 -0
- data/lib/insights/api/common/graphql/templates/model_type.erb +35 -0
- data/lib/insights/api/common/graphql/templates/query_type.erb +49 -0
- data/lib/insights/api/common/graphql/templates/schema.erb +6 -0
- data/lib/insights/api/common/graphql/types/big_int.rb +23 -0
- data/lib/insights/api/common/graphql/types/date_time.rb +16 -0
- data/lib/insights/api/common/graphql/types/query_filter.rb +16 -0
- data/lib/insights/api/common/graphql/types/query_sort_by.rb +16 -0
- data/lib/insights/api/common/inflections.rb +28 -0
- data/lib/insights/api/common/logging.rb +17 -0
- data/lib/insights/api/common/metrics.rb +39 -0
- data/lib/insights/api/common/middleware/web_server_metrics.rb +62 -0
- data/lib/insights/api/common/open_api.rb +2 -0
- data/lib/insights/api/common/open_api/docs.rb +54 -0
- data/lib/insights/api/common/open_api/docs/component_collection.rb +67 -0
- data/lib/insights/api/common/open_api/docs/doc_v3.rb +102 -0
- data/lib/insights/api/common/open_api/docs/object_definition.rb +39 -0
- data/lib/insights/api/common/open_api/generator.rb +520 -0
- data/lib/insights/api/common/open_api/serializer.rb +31 -0
- data/lib/insights/api/common/option_redirect_enhancements.rb +23 -0
- data/lib/insights/api/common/paginated_response.rb +108 -0
- data/lib/insights/api/common/rbac/access.rb +66 -0
- data/lib/insights/api/common/rbac/acl.rb +74 -0
- data/lib/insights/api/common/rbac/policies.rb +33 -0
- data/lib/insights/api/common/rbac/query_shared_resource.rb +45 -0
- data/lib/insights/api/common/rbac/roles.rb +77 -0
- data/lib/insights/api/common/rbac/seed.rb +140 -0
- data/lib/insights/api/common/rbac/service.rb +67 -0
- data/lib/insights/api/common/rbac/share_resource.rb +60 -0
- data/lib/insights/api/common/rbac/unshare_resource.rb +32 -0
- data/lib/insights/api/common/rbac/utilities.rb +30 -0
- data/lib/insights/api/common/request.rb +111 -0
- data/lib/insights/api/common/routing.rb +26 -0
- data/lib/insights/api/common/user.rb +48 -0
- data/lib/insights/api/common/version.rb +7 -0
- data/lib/tasks/insights/api/common_tasks.rake +4 -0
- data/spec/support/default_as_json.rb +17 -0
- data/spec/support/rbac_shared_contexts.rb +44 -0
- data/spec/support/requests_spec_helper.rb +7 -0
- data/spec/support/service_spec_helper.rb +26 -0
- data/spec/support/user_header_spec_helper.rb +68 -0
- 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
|