katello 4.12.1 → 4.13.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of katello might be problematic. Click here for more details.

Files changed (245) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/katello/locale/bn/katello.js +3365 -3350
  3. data/app/assets/javascripts/katello/locale/bn_IN/katello.js +3136 -3121
  4. data/app/assets/javascripts/katello/locale/ca/katello.js +3588 -3576
  5. data/app/assets/javascripts/katello/locale/cs/katello.js +3499 -3487
  6. data/app/assets/javascripts/katello/locale/cs_CZ/katello.js +4186 -4186
  7. data/app/assets/javascripts/katello/locale/de/katello.js +5553 -5562
  8. data/app/assets/javascripts/katello/locale/de_AT/katello.js +3008 -2993
  9. data/app/assets/javascripts/katello/locale/de_DE/katello.js +3066 -3051
  10. data/app/assets/javascripts/katello/locale/el/katello.js +3376 -3370
  11. data/app/assets/javascripts/katello/locale/en/katello.js +3008 -2993
  12. data/app/assets/javascripts/katello/locale/en_GB/katello.js +3076 -3073
  13. data/app/assets/javascripts/katello/locale/en_US/katello.js +3008 -2993
  14. data/app/assets/javascripts/katello/locale/es/katello.js +5366 -5372
  15. data/app/assets/javascripts/katello/locale/et_EE/katello.js +3008 -2993
  16. data/app/assets/javascripts/katello/locale/fr/katello.js +5975 -5984
  17. data/app/assets/javascripts/katello/locale/gl/katello.js +3125 -3113
  18. data/app/assets/javascripts/katello/locale/gu/katello.js +3119 -3104
  19. data/app/assets/javascripts/katello/locale/he_IL/katello.js +3020 -3005
  20. data/app/assets/javascripts/katello/locale/hi/katello.js +3137 -3122
  21. data/app/assets/javascripts/katello/locale/id/katello.js +3008 -2993
  22. data/app/assets/javascripts/katello/locale/it/katello.js +4469 -4466
  23. data/app/assets/javascripts/katello/locale/ja/katello.js +5969 -5978
  24. data/app/assets/javascripts/katello/locale/ka/katello.js +5649 -5652
  25. data/app/assets/javascripts/katello/locale/kn/katello.js +3136 -3121
  26. data/app/assets/javascripts/katello/locale/ko/katello.js +4717 -4720
  27. data/app/assets/javascripts/katello/locale/locale/katello.js +1050 -1084
  28. data/app/assets/javascripts/katello/locale/ml_IN/katello.js +3008 -2993
  29. data/app/assets/javascripts/katello/locale/mr/katello.js +3136 -3121
  30. data/app/assets/javascripts/katello/locale/nl_NL/katello.js +3116 -3101
  31. data/app/assets/javascripts/katello/locale/or/katello.js +3137 -3122
  32. data/app/assets/javascripts/katello/locale/pa/katello.js +3136 -3121
  33. data/app/assets/javascripts/katello/locale/pl/katello.js +3210 -3195
  34. data/app/assets/javascripts/katello/locale/pl_PL/katello.js +3008 -2993
  35. data/app/assets/javascripts/katello/locale/pt/katello.js +3009 -2994
  36. data/app/assets/javascripts/katello/locale/pt_BR/katello.js +5362 -5368
  37. data/app/assets/javascripts/katello/locale/ro/katello.js +3008 -2993
  38. data/app/assets/javascripts/katello/locale/ro_RO/katello.js +3008 -2993
  39. data/app/assets/javascripts/katello/locale/ru/katello.js +4638 -4641
  40. data/app/assets/javascripts/katello/locale/sl/katello.js +3051 -3036
  41. data/app/assets/javascripts/katello/locale/sv_SE/katello.js +3156 -3144
  42. data/app/assets/javascripts/katello/locale/ta/katello.js +3365 -3350
  43. data/app/assets/javascripts/katello/locale/ta_IN/katello.js +3121 -3106
  44. data/app/assets/javascripts/katello/locale/te/katello.js +3136 -3121
  45. data/app/assets/javascripts/katello/locale/tr/katello.js +3025 -3010
  46. data/app/assets/javascripts/katello/locale/vi/katello.js +3008 -2993
  47. data/app/assets/javascripts/katello/locale/vi_VN/katello.js +3008 -2993
  48. data/app/assets/javascripts/katello/locale/zh/katello.js +3008 -2993
  49. data/app/assets/javascripts/katello/locale/zh_CN/katello.js +5968 -5977
  50. data/app/assets/javascripts/katello/locale/zh_TW/katello.js +4694 -4697
  51. data/app/controllers/katello/api/registry/registry_proxies_controller.rb +370 -132
  52. data/app/controllers/katello/api/rhsm/candlepin_dynflow_proxy_controller.rb +12 -20
  53. data/app/controllers/katello/api/v2/activation_keys_controller.rb +10 -4
  54. data/app/controllers/katello/api/v2/capsule_content_controller.rb +24 -0
  55. data/app/controllers/katello/api/v2/content_view_versions_controller.rb +9 -2
  56. data/app/controllers/katello/api/v2/debs_controller.rb +1 -1
  57. data/app/controllers/katello/api/v2/errata_controller.rb +1 -1
  58. data/app/controllers/katello/api/v2/host_subscriptions_controller.rb +12 -4
  59. data/app/controllers/katello/api/v2/hosts_bulk_actions_controller.rb +3 -3
  60. data/app/controllers/katello/api/v2/organizations_controller.rb +0 -11
  61. data/app/controllers/katello/api/v2/packages_controller.rb +1 -1
  62. data/app/controllers/katello/api/v2/repositories_controller.rb +19 -13
  63. data/app/controllers/katello/api/v2/repository_sets_controller.rb +2 -1
  64. data/app/controllers/katello/api/v2/simple_content_access_controller.rb +9 -22
  65. data/app/controllers/katello/concerns/api/v2/authorization.rb +1 -1
  66. data/app/helpers/katello/subscription_mailer_helper.rb +1 -1
  67. data/app/jobs/create_manifest_expire_soon_warning_notifications.rb +11 -0
  68. data/app/lib/actions/candlepin/owner/regenerate_upstream_identity_cert.rb +21 -0
  69. data/app/lib/actions/katello/capsule_content/sync.rb +1 -1
  70. data/app/lib/actions/katello/capsule_content/sync_capsule.rb +7 -2
  71. data/app/lib/actions/katello/capsule_content/verify_checksum.rb +75 -0
  72. data/app/lib/actions/katello/content_view/promote.rb +1 -1
  73. data/app/lib/actions/katello/content_view/publish.rb +1 -1
  74. data/app/lib/actions/katello/content_view_version/verify_checksum.rb +29 -0
  75. data/app/lib/actions/katello/host/hypervisors_update.rb +1 -0
  76. data/app/lib/actions/katello/host/update_content_view.rb +2 -2
  77. data/app/lib/actions/katello/organization/manifest_delete.rb +6 -1
  78. data/app/lib/actions/katello/organization/manifest_import.rb +5 -0
  79. data/app/lib/actions/katello/organization/manifest_refresh.rb +3 -0
  80. data/app/lib/actions/katello/repository/create.rb +17 -11
  81. data/app/lib/actions/katello/repository/create_root.rb +4 -2
  82. data/app/lib/actions/katello/repository/metadata_generate.rb +7 -1
  83. data/app/lib/actions/katello/repository/remove_content.rb +1 -0
  84. data/app/lib/actions/katello/repository/sync.rb +2 -1
  85. data/app/lib/actions/katello/repository/upload_files.rb +1 -0
  86. data/app/lib/actions/katello/upstream_subscriptions/bind_entitlement.rb +1 -1
  87. data/app/lib/actions/pulp3/capsule_content/verify_checksum.rb +27 -0
  88. data/app/lib/actions/pulp3/orchestration/content_view_version/export_repository.rb +7 -9
  89. data/app/lib/actions/pulp3/orchestration/content_view_version/syncable_export.rb +5 -4
  90. data/app/lib/actions/pulp3/orchestration/orphan_cleanup/remove_orphans.rb +1 -0
  91. data/app/lib/actions/pulp3/orphan_cleanup/purge_completed_tasks.rb +15 -0
  92. data/app/lib/katello/concerns/base_template_scope_extensions.rb +7 -2
  93. data/app/lib/katello/http_resource.rb +6 -1
  94. data/app/lib/katello/resources/candlepin/consumer.rb +1 -1
  95. data/app/lib/katello/resources/candlepin/upstream_consumer.rb +18 -6
  96. data/app/lib/katello/resources/candlepin/upstream_job.rb +1 -1
  97. data/app/lib/katello/resources/registry.rb +25 -0
  98. data/app/mailers/katello/subscription_mailer.rb +3 -6
  99. data/app/models/katello/concerns/organization_extensions.rb +42 -3
  100. data/app/models/katello/content_view.rb +30 -0
  101. data/app/models/katello/content_view_environment_content_facet.rb +4 -2
  102. data/app/models/katello/glue/provider.rb +19 -12
  103. data/app/models/katello/glue/pulp/repos.rb +11 -3
  104. data/app/models/katello/host/content_facet.rb +1 -1
  105. data/app/models/katello/host/subscription_facet.rb +1 -1
  106. data/app/models/katello/ping.rb +1 -1
  107. data/app/models/katello/repository.rb +32 -1
  108. data/app/models/katello/root_repository.rb +4 -6
  109. data/app/models/katello/trace_status.rb +1 -1
  110. data/app/services/katello/content_unit_indexer.rb +9 -0
  111. data/app/services/katello/pulp3/alternate_content_source.rb +4 -6
  112. data/app/services/katello/pulp3/api/core.rb +21 -0
  113. data/app/services/katello/pulp3/api/docker.rb +4 -0
  114. data/app/services/katello/pulp3/api/yum.rb +11 -0
  115. data/app/services/katello/pulp3/docker_manifest.rb +5 -1
  116. data/app/services/katello/pulp3/repository/generic.rb +1 -1
  117. data/app/services/katello/pulp3/repository/yum.rb +1 -6
  118. data/app/services/katello/pulp3/repository.rb +26 -6
  119. data/app/services/katello/pulp3/repository_mirror.rb +13 -12
  120. data/app/services/katello/pulp3/service_common.rb +2 -10
  121. data/app/services/katello/pulp3/smart_proxy_repository.rb +0 -2
  122. data/app/services/katello/ui_notifications/subscriptions/manifest_expire_soon_warning.rb +75 -0
  123. data/app/views/foreman/job_templates/update_package_-_katello_ansible_default.erb +5 -1
  124. data/app/views/foreman/job_templates/update_packages_by_search_query_-_katello_ansible_default.erb +2 -2
  125. data/app/views/foreman/job_templates/upload_profile.erb +16 -0
  126. data/app/views/foreman/smart_proxies/_content_tab.html.erb +3 -1
  127. data/app/views/katello/api/v2/content_view_filter_rules/show.json.rabl +9 -0
  128. data/app/views/katello/api/v2/docker_manifests/show.json.rabl +1 -0
  129. data/app/views/katello/api/v2/organizations/show.json.rabl +9 -1
  130. data/app/views/overrides/activation_keys/_host_environment_select.html.erb +1 -1
  131. data/app/views/overrides/activation_keys/_host_media_type_select.html.erb +15 -5
  132. data/config/routes/api/registry.rb +4 -8
  133. data/config/routes/api/v2.rb +2 -0
  134. data/db/migrate/20240423112842_add_fields_to_katello_docker_manifest.rb +8 -0
  135. data/db/migrate/20240502192021_change_katello_repository_rpms_id_seq_to_big_int.rb +9 -0
  136. data/db/migrate/20240520142245_add_container_push_props_to_repo.rb +7 -0
  137. data/db/migrate/20240531193030_remove_sha1_repository_checksum_type.rb +10 -0
  138. data/db/seeds.d/109-katello-notification-blueprints.rb +6 -0
  139. data/engines/bastion_katello/app/assets/javascripts/bastion_katello/activation-keys/details/activation-key-repository-sets.controller.js +3 -3
  140. data/engines/bastion_katello/app/assets/javascripts/bastion_katello/content-credentials/new/views/new-content-credential.html +2 -1
  141. data/engines/bastion_katello/app/assets/javascripts/bastion_katello/content-hosts/details/content-host-repository-sets.controller.js +3 -3
  142. data/engines/bastion_katello/app/assets/javascripts/bastion_katello/i18n/bastion_katello.pot +0 -15
  143. data/engines/bastion_katello/app/assets/javascripts/bastion_katello/products/details/repositories/checksum.service.js +6 -1
  144. data/engines/bastion_katello/app/assets/javascripts/bastion_katello/products/details/repositories/details/views/repository-info.html +8 -6
  145. data/engines/bastion_katello/app/assets/javascripts/bastion_katello/products/details/repositories/new/views/new-repository.html +12 -13
  146. data/lib/katello/permission_creator.rb +3 -3
  147. data/lib/katello/permissions/registry_permissions.rb +4 -7
  148. data/lib/katello/plugin.rb +21 -8
  149. data/lib/katello/repository_types/ostree.rb +7 -0
  150. data/lib/katello/scheduled_jobs.rb +7 -1
  151. data/lib/katello/tasks/clean_backend_objects.rake +1 -1
  152. data/lib/katello/tasks/repository.rake +22 -0
  153. data/lib/katello/version.rb +1 -1
  154. data/locale/action_names.rb +4 -3
  155. data/locale/bn/katello.po +166 -151
  156. data/locale/bn_IN/katello.po +166 -151
  157. data/locale/ca/katello.po +166 -151
  158. data/locale/cs/katello.po +166 -151
  159. data/locale/cs_CZ/LC_MESSAGES/katello.mo +0 -0
  160. data/locale/cs_CZ/katello.po +172 -157
  161. data/locale/de/LC_MESSAGES/katello.mo +0 -0
  162. data/locale/de/katello.po +178 -163
  163. data/locale/de_AT/katello.po +166 -151
  164. data/locale/de_DE/katello.po +166 -151
  165. data/locale/el/katello.po +166 -151
  166. data/locale/en/katello.po +166 -151
  167. data/locale/en_GB/katello.po +166 -151
  168. data/locale/en_US/katello.po +166 -151
  169. data/locale/es/LC_MESSAGES/katello.mo +0 -0
  170. data/locale/es/katello.po +178 -163
  171. data/locale/et_EE/katello.po +166 -151
  172. data/locale/fr/LC_MESSAGES/katello.mo +0 -0
  173. data/locale/fr/katello.po +179 -164
  174. data/locale/gl/katello.po +166 -151
  175. data/locale/gu/katello.po +166 -151
  176. data/locale/he_IL/katello.po +166 -151
  177. data/locale/hi/katello.po +166 -151
  178. data/locale/id/katello.po +166 -151
  179. data/locale/it/LC_MESSAGES/katello.mo +0 -0
  180. data/locale/it/katello.po +169 -154
  181. data/locale/ja/LC_MESSAGES/katello.mo +0 -0
  182. data/locale/ja/katello.po +179 -164
  183. data/locale/ka/LC_MESSAGES/katello.mo +0 -0
  184. data/locale/ka/katello.po +177 -162
  185. data/locale/katello.pot +1119 -1062
  186. data/locale/kn/katello.po +166 -151
  187. data/locale/ko/LC_MESSAGES/katello.mo +0 -0
  188. data/locale/ko/katello.po +174 -159
  189. data/locale/ml_IN/katello.po +166 -151
  190. data/locale/mr/katello.po +166 -151
  191. data/locale/nl_NL/katello.po +166 -151
  192. data/locale/or/katello.po +166 -151
  193. data/locale/pa/katello.po +166 -151
  194. data/locale/pl/katello.po +166 -151
  195. data/locale/pl_PL/katello.po +166 -151
  196. data/locale/pt/katello.po +166 -151
  197. data/locale/pt_BR/LC_MESSAGES/katello.mo +0 -0
  198. data/locale/pt_BR/katello.po +178 -163
  199. data/locale/ro/katello.po +166 -151
  200. data/locale/ro_RO/katello.po +166 -151
  201. data/locale/ru/LC_MESSAGES/katello.mo +0 -0
  202. data/locale/ru/katello.po +171 -156
  203. data/locale/sl/katello.po +166 -151
  204. data/locale/sv_SE/katello.po +166 -151
  205. data/locale/ta/katello.po +166 -151
  206. data/locale/ta_IN/katello.po +166 -151
  207. data/locale/te/katello.po +166 -151
  208. data/locale/tr/katello.po +166 -151
  209. data/locale/vi/katello.po +166 -151
  210. data/locale/vi_VN/katello.po +166 -151
  211. data/locale/zh/katello.po +166 -151
  212. data/locale/zh_CN/LC_MESSAGES/katello.mo +0 -0
  213. data/locale/zh_CN/katello.po +179 -164
  214. data/locale/zh_TW/LC_MESSAGES/katello.mo +0 -0
  215. data/locale/zh_TW/katello.po +171 -156
  216. data/package.json +0 -1
  217. data/webpack/ForemanColumnExtensions/index.js +129 -0
  218. data/webpack/components/Content/ContentTable.js +0 -1
  219. data/webpack/components/Content/__tests__/__snapshots__/ContentTable.test.js.snap +0 -1
  220. data/webpack/components/Table/TableWrapper.js +14 -0
  221. data/webpack/components/extensions/HostDetails/ActionsBar/index.js +1 -1
  222. data/webpack/components/extensions/Hosts/ActionsBar/index.js +20 -1
  223. data/webpack/components/extensions/Hosts/BulkActions/BulkChangeHostCVModal/BulkChangeHostCVModal.js +220 -0
  224. data/webpack/components/extensions/Hosts/BulkActions/BulkChangeHostCVModal/actions.js +23 -0
  225. data/webpack/components/extensions/Hosts/BulkActions/BulkChangeHostCVModal/index.js +25 -0
  226. data/webpack/components/extensions/Hosts/BulkActions/__tests__/bulkChangeHostCVModal.test.js +133 -0
  227. data/webpack/global_index.js +9 -0
  228. data/webpack/scenes/Hosts/ChangeContentSource/actions.js +3 -1
  229. data/webpack/scenes/Hosts/ChangeContentSource/components/ContentSourceForm.js +62 -24
  230. data/webpack/scenes/Hosts/ChangeContentSource/index.js +24 -16
  231. data/webpack/scenes/ModuleStreams/ModuleStreamsPage.js +33 -39
  232. data/webpack/scenes/ModuleStreams/__tests__/ModuleStreamPage.test.js +4 -2
  233. data/webpack/scenes/ModuleStreams/__tests__/__snapshots__/ModuleStreamsTable.test.js.snap +0 -1
  234. data/webpack/scenes/RedHatRepositories/__tests__/__snapshots__/RedHatRepositoriesPage.test.js.snap +1 -0
  235. data/webpack/scenes/Subscriptions/Manifest/ManageManifestModal.js +66 -5
  236. data/webpack/scenes/Subscriptions/UpstreamSubscriptions/UpstreamSubscriptionsPage.js +16 -13
  237. data/webpack/scenes/Subscriptions/UpstreamSubscriptions/__tests__/__snapshots__/UpstreamSubscriptionsPage.test.js.snap +14 -8
  238. data/webpack/scenes/Subscriptions/__tests__/__snapshots__/SubscriptionsPage.test.js.snap +1 -0
  239. data/webpack/scenes/Subscriptions/components/SubscriptionsToolbar/SubscriptionsToolbar.js +1 -1
  240. metadata +60 -42
  241. data/app/lib/actions/katello/host/upload_package_profile.rb +0 -45
  242. data/app/lib/actions/katello/host/upload_profiles.rb +0 -47
  243. data/webpack/utils/__tests__/useParamsWithHash.test.js +0 -22
  244. data/webpack/utils/paramsFromHash.js +0 -16
  245. data/webpack/utils/useUrlParams.js +0 -14
@@ -3,15 +3,16 @@ module Katello
3
3
  class Api::Registry::RegistryProxiesController < Api::V2::ApiController
4
4
  before_action :disable_strong_params
5
5
  before_action :confirm_settings
6
- before_action :confirm_push_settings, only: [:start_upload_blob, :upload_blob, :finish_upload_blob,
7
- :chunk_upload_blob, :push_manifest]
8
6
  skip_before_action :authorize
9
7
  before_action :optional_authorize, only: [:token, :catalog]
10
8
  before_action :registry_authorize, except: [:token, :v1_search, :catalog]
11
9
  before_action :authorize_repository_read, only: [:pull_manifest, :tags_list]
12
- before_action :authorize_repository_write, only: [:push_manifest]
10
+ # TODO: authorize_repository_write commented out due to container push changes. Additional task needed to fix.
11
+ # before_action :authorize_repository_write, only: [:start_upload_blob, :upload_blob, :finish_upload_blob, :push_manifest]
12
+ before_action :container_push_prop_validation, only: [:start_upload_blob, :upload_blob, :finish_upload_blob, :push_manifest]
13
+ before_action :create_container_repo_if_needed, only: [:start_upload_blob, :upload_blob, :finish_upload_blob, :push_manifest]
13
14
  skip_before_action :check_media_type, only: [:start_upload_blob, :upload_blob, :finish_upload_blob,
14
- :chunk_upload_blob, :push_manifest]
15
+ :push_manifest]
15
16
 
16
17
  wrap_parameters false
17
18
 
@@ -83,6 +84,301 @@ module Katello
83
84
  return false
84
85
  end
85
86
 
87
+ def container_push_prop_validation(props = nil)
88
+ # Handle validation and repo creation for container pushes before talking to pulp
89
+ return false unless confirm_push_settings
90
+ props = parse_blob_push_props if props.nil?
91
+ return false unless check_blob_push_field_syntax(props)
92
+
93
+ # validate input and find the org and product either using downcase label or id
94
+ if props[:schema] == "label"
95
+ return false unless check_blob_push_org_label(props)
96
+ return false unless check_blob_push_product_label(props)
97
+ else
98
+ return false unless check_blob_push_org_id(props)
99
+ return false unless check_blob_push_product_id(props)
100
+ end
101
+
102
+ return false unless check_blob_push_container(props)
103
+ true
104
+ end
105
+
106
+ def parse_blob_push_props(path_string = nil)
107
+ # path string should follow one of these formats:
108
+ # - /v2/{org_label}/{product_label}/{name}/blobs/uploads...
109
+ # - /v2/id/{org_id}/{product_id}/{name}/blobs/uploads...
110
+ # - /v2/{org_label}/{product_label}/{name}/manifests/...
111
+ # - /v2/id/{org_id}/{product_id}/{name}/manifests/...
112
+ # inputs not matching format will return {valid_format: false}
113
+ path_string = @_request.fullpath if path_string.nil?
114
+ segments = path_string.split('/')
115
+
116
+ if segments.length >= 7 && segments[0] == "" && segments[1] == "v2" &&
117
+ segments[2] != "id" && (segments[5] == "blobs" || segments[5] == "manifests")
118
+
119
+ return {
120
+ valid_format: true,
121
+ schema: "label",
122
+ organization: segments[2],
123
+ product: segments[3],
124
+ name: segments[4]
125
+ }
126
+ elsif segments.length >= 8 && segments[0] == "" && segments[1] == "v2" &&
127
+ segments[2] == "id" && (segments[6] == "blobs" || segments[6] == "manifests")
128
+
129
+ return {
130
+ valid_format: true,
131
+ schema: "id",
132
+ organization: segments[3],
133
+ product: segments[4],
134
+ name: segments[5]
135
+ }
136
+ else
137
+ return {valid_format: false}
138
+ end
139
+ end
140
+
141
+ def check_blob_push_field_syntax(props)
142
+ # check basic url field syntax
143
+ unless props[:valid_format]
144
+ return render_podman_error(
145
+ "NAME_INVALID",
146
+ "Invalid format. Container pushes should follow 'organization_label/product_label/name' OR 'id/organization_id/product_id/name' schema.",
147
+ :bad_request
148
+ )
149
+ end
150
+ return true
151
+ end
152
+
153
+ # rubocop:disable Metrics/MethodLength
154
+ def check_blob_push_org_label(props)
155
+ org_label = props[:organization]
156
+ unless org_label.present? && org_label.length > 0
157
+ return render_podman_error(
158
+ "NAME_INVALID",
159
+ "Invalid format. Organization label cannot be blank.",
160
+ :bad_request
161
+ )
162
+ end
163
+ org = Organization.where("LOWER(label) = '#{org_label}'") # convert to lowercase
164
+ # reject ambiguous orgs (possible due to lowercase conversion)
165
+ if org.length > 1
166
+ # Determine if the repo already exists in one of the possible products. If yes,
167
+ # inform the user they need to destroy the existing repo and use the ID format
168
+ unless props[:product].blank? || props[:name].blank?
169
+ org.each do |o|
170
+ products = get_matching_products_from_org(o, props[:product])
171
+ products.each do |prod|
172
+ root_repos = get_root_repo_from_product(prod, props[:name])
173
+ unless root_repos.empty?
174
+ return render_podman_error(
175
+ "NAME_INVALID",
176
+ "Due to a change in your organizations, this container name has become "\
177
+ "ambiguous (org name '#{org_label}'). If you wish to continue using this "\
178
+ "container name, destroy the organization in conflict with '#{o.name} (id "\
179
+ "#{o.id}). If you wish to keep both orgs, destroy '#{o.label}/#{prod.label}/"\
180
+ "#{root_repos.first.label}' and retry your push using the id format.",
181
+ :conflict
182
+ )
183
+ end
184
+ end
185
+ end
186
+ end
187
+
188
+ # Otherwise tell them to try pushing with ID format
189
+ return render_podman_error(
190
+ "NAME_INVALID",
191
+ "Organization label '#{org_label}' is ambiguous. Try using an id-based container name.",
192
+ :conflict
193
+ )
194
+ end
195
+ if org.length == 0
196
+ return render_podman_error(
197
+ "NAME_UNKNOWN",
198
+ "Organization not found: '#{org_label}'",
199
+ :not_found
200
+ )
201
+ end
202
+ @organization = org.first
203
+ true
204
+ end
205
+
206
+ def check_blob_push_org_id(props)
207
+ org_id = props[:organization]
208
+ unless org_id.present? && org_id == org_id.to_i.to_s
209
+ return render_podman_error(
210
+ "NAME_INVALID",
211
+ "Invalid format. Organization id must be an integer without leading zeros.",
212
+ :bad_request
213
+ )
214
+ end
215
+ @organization = Organization.find_by_id(org_id.to_i)
216
+ if @organization.nil?
217
+ return render_podman_error(
218
+ "NAME_UNKNOWN",
219
+ "Organization id not found: '#{org_id}'",
220
+ :not_found
221
+ )
222
+ end
223
+ true
224
+ end
225
+
226
+ def check_blob_push_product_label(props)
227
+ prod_label = props[:product]
228
+ unless prod_label.present? && prod_label.length > 0
229
+ return render_podman_error(
230
+ "NAME_INVALID",
231
+ "Invalid format. Product label cannot be blank.",
232
+ :bad_request
233
+ )
234
+ end
235
+ product = get_matching_products_from_org(@organization, prod_label)
236
+ # reject ambiguous products (possible due to lowercase conversion)
237
+ if product.length > 1
238
+ # Determine if the repo already exists in one of the possible products. If yes,
239
+ # inform the user they need to destroy the existing repo and use the ID format
240
+ unless props[:name].blank?
241
+ product.each do |prod|
242
+ root_repos = get_root_repo_from_product(prod, props[:name])
243
+ unless root_repos.empty?
244
+ return render_podman_error(
245
+ "NAME_INVALID",
246
+ "Due to a change in your products, this container name has become ambiguous "\
247
+ "(product name '#{prod_label}'). If you wish to continue using this container "\
248
+ "name, destroy the product in conflict with '#{prod.name}' (id #{prod.id}). If "\
249
+ "you wish to keep both products, destroy '#{@organization.label}/#{prod.label}/"\
250
+ "#{root_repos.first.label}' and retry your push using the id format.",
251
+ :conflict
252
+ )
253
+ end
254
+ end
255
+ end
256
+
257
+ return render_podman_error(
258
+ "NAME_INVALID",
259
+ "Product label '#{prod_label}' is ambiguous. Try using an id-based container name.",
260
+ :conflict
261
+ )
262
+ end
263
+ if product.length == 0
264
+ return render_podman_error(
265
+ "NAME_UNKNOWN",
266
+ "Product not found: '#{prod_label}'",
267
+ :not_found
268
+ )
269
+ end
270
+ @product = product.first
271
+ true
272
+ end
273
+
274
+ def check_blob_push_product_id(props)
275
+ prod_id = props[:product]
276
+ unless prod_id.present? && prod_id == prod_id.to_i.to_s
277
+ return render_podman_error(
278
+ "NAME_INVALID",
279
+ "Invalid format. Product id must be an integer without leading zeros.",
280
+ :bad_request
281
+ )
282
+ end
283
+ @product = @organization.products.find_by_id(prod_id.to_i)
284
+ if @product.nil?
285
+ return render_podman_error(
286
+ "NAME_UNKNOWN",
287
+ "Product id not found: '#{prod_id}'",
288
+ :not_found
289
+ )
290
+ end
291
+ true
292
+ end
293
+
294
+ def get_matching_products_from_org(organization, product_label)
295
+ return organization.products.where("LOWER(label) = '#{product_label}'") # convert to lowercase
296
+ end
297
+
298
+ def get_root_repo_from_product(product, root_repo_name)
299
+ return product.root_repositories.where(label: root_repo_name)
300
+ end
301
+
302
+ def check_blob_push_container(props)
303
+ unless props[:name].present? && props[:name].length > 0
304
+ return render_podman_error(
305
+ "NAME_INVALID",
306
+ "Invalid format. Container name cannot be blank.",
307
+ :bad_request
308
+ )
309
+ end
310
+
311
+ @container_name = props[:name]
312
+ @container_push_name_format = props[:schema]
313
+ if @container_push_name_format == "label"
314
+ @container_path_input = "#{props[:organization]}/#{props[:product]}/#{props[:name]}"
315
+ else
316
+ @container_path_input = "id/#{props[:organization]}/#{props[:product]}/#{props[:name]}"
317
+ end
318
+
319
+ # If the repo already exists, check if the existing push format matches
320
+ root_repo = get_root_repo_from_product(@product, @container_name).first
321
+ if !root_repo.nil? && @container_push_name_format != root_repo.container_push_name_format
322
+ return render_podman_error(
323
+ "NAME_INVALID",
324
+ "Repository name '#{@container_name}' already exists in this product using a different naming scheme. Please retry your request with the #{root_repo.container_push_name_format} format or destroy and recreate the repository using your preferred schema.",
325
+ :conflict
326
+ )
327
+ end
328
+
329
+ true
330
+ end
331
+
332
+ def create_container_repo_if_needed
333
+ if get_root_repo_from_product(@product, @container_name).empty?
334
+ root = @product.add_repo(
335
+ name: @container_name,
336
+ label: @container_name,
337
+ download_policy: 'immediate',
338
+ content_type: Repository::DOCKER_TYPE,
339
+ unprotected: true,
340
+ is_container_push: true,
341
+ container_push_name: @container_path_input,
342
+ container_push_name_format: @container_push_name_format
343
+ )
344
+ sync_task(::Actions::Katello::Repository::CreateRoot, root, @container_path_input)
345
+ end
346
+ end
347
+
348
+ def blob_push_cleanup
349
+ # after manifest upload, index content and set version href using pulp api
350
+ root_repo = get_root_repo_from_product(@product, @container_name)&.first
351
+ instance_repo = root_repo&.library_instance
352
+
353
+ unless root_repo.present? && instance_repo.present?
354
+ return render_podman_error(
355
+ "BLOB_UPLOAD_UNKNOWN",
356
+ "Could not locate local uploaded repository for content indexing.",
357
+ :not_found
358
+ )
359
+ end
360
+
361
+ api = ::Katello::Pulp3::Repository.api(SmartProxy.pulp_primary, ::Katello::Repository::DOCKER_TYPE).container_push_api
362
+ api_response = api.list(name: @container_path_input)&.results&.first
363
+ latest_version_href = api_response&.latest_version_href
364
+ pulp_href = api_response&.pulp_href
365
+
366
+ if latest_version_href.empty? || pulp_href.empty?
367
+ return render_podman_error(
368
+ "BLOB_UPLOAD_UNKNOWN",
369
+ "Could not locate repository properties for content indexing.",
370
+ :not_found
371
+ )
372
+ end
373
+
374
+ instance_repo.update!(version_href: latest_version_href)
375
+ ::Katello::Pulp3::RepositoryReference.where(root_repository_id: instance_repo.root_id,
376
+ content_view_id: instance_repo.content_view.id, repository_href: pulp_href).create!
377
+ instance_repo.index_content
378
+
379
+ true
380
+ end
381
+
86
382
  def find_writable_repository
87
383
  Repository.docker_type.syncable.find_by_container_repository_name(params[:repository])
88
384
  end
@@ -182,15 +478,8 @@ module Katello
182
478
  end
183
479
 
184
480
  def check_blob
185
- begin
186
- r = Resources::Registry::Proxy.get(@_request.fullpath, 'Accept' => request.headers['Accept'])
187
- response.header['Content-Length'] = "#{r.body.size}"
188
- rescue RestClient::NotFound
189
- digest_file = tmp_file("#{params[:digest][7..-1]}.tar")
190
- raise unless File.exist? digest_file
191
- response.header['Content-Length'] = "#{File.size digest_file}"
192
- end
193
- render json: {}
481
+ pulp_response = Resources::Registry::Proxy.get(@_request.fullpath, 'Accept' => request.headers['Accept'])
482
+ head pulp_response.code
194
483
  end
195
484
 
196
485
  def redirect_client
@@ -210,94 +499,72 @@ module Katello
210
499
  redirect_client { Resources::Registry::Proxy.get(@_request.fullpath, headers, max_redirects: 0) }
211
500
  end
212
501
 
213
- # FIXME: Reimplement for Pulp 3.
214
- def push_manifest
215
- repository = params[:repository]
216
- tag = params[:tag]
217
-
218
- manifest = create_manifest
219
- return if manifest.nil?
220
-
221
- begin
222
- files = get_manifest_files(repository, manifest)
223
- return if files.nil?
224
-
225
- tar_file = create_tar_file(files, repository, tag)
226
- return if tar_file.nil?
227
-
228
- digest = upload_manifest(tar_file)
229
- return if digest.nil?
502
+ def start_upload_blob
503
+ headers = translated_headers_for_proxy
504
+ headers['Content-Type'] = request.headers['Content-Type'] if request.headers['Content-Type']
505
+ headers['Content-Length'] = request.headers['Content-Length'] if request.headers['Content-Length']
506
+ pulp_response = Resources::Registry::Proxy.post(@_request.fullpath, @_request.body, headers)
230
507
 
231
- tag = upload_tag(digest, tag)
232
- return if tag.nil?
233
- ensure
234
- File.delete(tmp_file('manifest.json')) if File.exist? tmp_file('manifest.json')
508
+ pulp_response.headers.each do |key, value|
509
+ response.header[key.to_s] = value
235
510
  end
236
511
 
237
- render json: {}
512
+ head pulp_response.code
238
513
  end
239
514
 
240
- # FIXME: This is referring to a non-existent Pulp 2 server.
241
- # Pulp 3 container push support is needed instead.
242
- def pulp_content
243
- Katello.pulp_server.resources.content
244
- end
245
-
246
- def start_upload_blob
247
- uuid = SecureRandom.hex(16)
248
- response.header['Location'] = "#{request_url}/v2/#{params[:repository]}/blobs/uploads/#{uuid}"
249
- response.header['Docker-Upload-UUID'] = uuid
250
- response.header['Range'] = '0-0'
251
- head 202
252
- end
253
-
254
- def status_upload_blob
255
- response.header['Location'] = "#{request_url}/v2/#{params[:repository]}/blobs/uploads/#{params[:uuid]}"
256
- response.header['Range'] = "123"
257
- response.header['Docker-Upload-UUID'] = "123"
258
- render plain: '', status: :no_content
259
- end
260
-
261
- def chunk_upload_blob
262
- response.header['Location'] = "#{request_url}/v2/#{params[:repository]}/blobs/uploads/#{params[:uuid]}"
263
- render plain: '', status: :accepted
515
+ def translated_headers_for_proxy
516
+ current_headers = {}
517
+ env = request.env.select do |key, _value|
518
+ key.match("^HTTP_.*")
519
+ end
520
+ env.each do |header|
521
+ current_headers[header[0].split('_')[1..-1].join('-')] = header[1]
522
+ end
523
+ current_headers
264
524
  end
265
525
 
266
526
  def upload_blob
267
- File.open(tmp_file("#{params[:uuid]}.tar"), 'ab', 0600) do |file|
268
- file.write request.body.read
527
+ headers = translated_headers_for_proxy
528
+ headers['Content-Type'] = request.headers['Content-Type'] if request.headers['Content-Type']
529
+ headers['Content-Range'] = request.headers['Content-Range'] if request.headers['Content-Range']
530
+ headers['Content-Length'] = request.headers['Content-Length'] if request.headers['Content-Length']
531
+ body = @_request.body.read
532
+ pulp_response = Resources::Registry::Proxy.patch(@_request.fullpath, body, headers)
533
+
534
+ pulp_response.headers.each do |key, value|
535
+ response.header[key.to_s] = value
269
536
  end
270
537
 
271
- # ???? true chunked data?
272
- if request.headers['Content-Range']
273
- render_error 'unprocessable_entity', :status => :unprocessable_entity
274
- end
275
-
276
- response.header['Location'] = "#{request_url}/v2/#{params[:repository]}/blobs/uploads/#{params[:uuid]}"
277
- response.header['Range'] = "1-#{request.body.size}"
278
- response.header['Docker-Upload-UUID'] = params[:uuid]
279
- head 204
538
+ head pulp_response.code
280
539
  end
281
540
 
282
541
  def finish_upload_blob
283
- # error by client if no params[:digest]
542
+ headers = translated_headers_for_proxy
543
+ headers['Content-Type'] = request.headers['Content-Type'] if request.headers['Content-Type']
544
+ headers['Content-Range'] = request.headers['Content-Range'] if request.headers['Content-Range']
545
+ headers['Content-Length'] = request.headers['Content-Length'] if request.headers['Content-Length']
546
+ pulp_response = Resources::Registry::Proxy.put(@_request.fullpath, @_request.body, headers)
547
+
548
+ pulp_response.headers.each do |key, value|
549
+ response.header[key.to_s] = value
550
+ end
284
551
 
285
- uuid_file = tmp_file("#{params[:uuid]}.tar")
286
- digest_file = tmp_file("#{params[:digest][7..-1]}.tar")
552
+ head pulp_response.code
553
+ end
287
554
 
288
- File.delete(digest_file) if File.exist? digest_file
289
- File.rename(uuid_file, digest_file)
555
+ def push_manifest
556
+ headers = translated_headers_for_proxy
557
+ headers['Content-Type'] = request.headers['Content-Type'] if request.headers['Content-Type']
558
+ body = @_request.body.read
559
+ pulp_response = Resources::Registry::Proxy.put(@_request.fullpath, body, headers)
560
+ pulp_response.headers.each do |key, value|
561
+ response.header[key.to_s] = value
562
+ end
290
563
 
291
- response.header['Location'] = "#{request_url}/v2/#{params[:repository]}/blobs/#{params[:digest]}"
292
- response.header['Docker-Content-Digest'] = params[:digest]
293
- response.header['Content-Range'] = "1-#{File.size(digest_file)}"
294
- response.header['Content-Length'] = "0"
295
- response.header['Docker-Upload-UUID'] = params[:uuid]
296
- head 201
297
- end
564
+ cleanup_result = blob_push_cleanup if pulp_response.code.between?(200, 299)
565
+ return false unless cleanup_result
298
566
 
299
- def cancel_upload_blob
300
- render plain: '', status: :ok
567
+ head pulp_response.code
301
568
  end
302
569
 
303
570
  def ping
@@ -310,10 +577,10 @@ module Katello
310
577
  end
311
578
 
312
579
  def v1_search
313
- # Checks for podman client and issues a 404 in that case. Podman
580
+ # Checks for v2 client and issues a 404 in that case. Podman
314
581
  # examines the response from a /v1_search request. If the result
315
582
  # is a 4XX, it will then proceed with a request to /_catalog
316
- if request.headers['HTTP_USER_AGENT'].downcase.include?('libpod')
583
+ if request.headers['HTTP_DOCKER_DISTRIBUTION_API_VERSION'] == 'registry/2.0'
317
584
  render json: {}, status: :not_found
318
585
  return
319
586
  end
@@ -411,47 +678,6 @@ module Katello
411
678
  tar_file
412
679
  end
413
680
 
414
- # FIXME: Reimplement for Pulp 3.
415
- def upload_manifest(tar_file)
416
- upload_id = pulp_content.create_upload_request['upload_id']
417
- filename = tmp_file(tar_file)
418
- uploads = []
419
-
420
- File.open(filename, 'rb') do |file|
421
- content = file.read
422
- pulp_content.upload_bits(upload_id, 0, content)
423
-
424
- uploads << {
425
- id: upload_id,
426
- name: filename,
427
- size: file.size,
428
- checksum: Digest::SHA256.hexdigest(content)
429
- }
430
- end
431
-
432
- File.delete(filename)
433
- task = sync_task(::Actions::Katello::Repository::ImportUpload,
434
- @repository, uploads, generate_metadata: true, sync_capsule: true)
435
- task.output['upload_results'][0]['digest']
436
- ensure
437
- pulp_content.delete_upload_request(upload_id) if upload_id
438
- end
439
-
440
- # FIXME: Reimplement for Pulp 3.
441
- def upload_tag(digest, tag)
442
- upload_id = pulp_content.create_upload_request['upload_id']
443
- uploads = [{
444
- id: upload_id,
445
- name: tag,
446
- digest: digest
447
- }]
448
- sync_task(::Actions::Katello::Repository::ImportUpload, @repository, uploads,
449
- :generate_metadata => true, :sync_capsule => true)
450
- tag
451
- ensure
452
- pulp_content.delete_upload_request(upload_id) if upload_id
453
- end
454
-
455
681
  def tmp_dir
456
682
  "#{Rails.root}/tmp"
457
683
  end
@@ -496,8 +722,11 @@ module Katello
496
722
 
497
723
  def confirm_push_settings
498
724
  return true if SETTINGS.dig(:katello, :container_image_registry, :allow_push)
499
- render_error('custom_error', :status => :not_found,
500
- :locals => { :message => "Registry push not supported" })
725
+ render_podman_error(
726
+ "UNSUPPORTED",
727
+ "Registry push is not enabled. To enable, add ':katello:'->':container_image_registry:'->':allow_push: true' in the katello settings file.",
728
+ :unprocessable_entity
729
+ )
501
730
  end
502
731
 
503
732
  def request_url
@@ -519,10 +748,19 @@ module Katello
519
748
  Rails.logger.debug "With body: #{filter_sensitive_data(response.body)}\n" unless route_name == 'pull_blob'
520
749
  end
521
750
 
751
+ def render_podman_error(code, message, status = :bad_request)
752
+ # Renders a podman-compatible error and returns false.
753
+ # code: uppercase string code from opencontainer error code spec:
754
+ # https://specs.opencontainers.org/distribution-spec/?v=v1.0.0#DISTRIBUTION-SPEC-140
755
+ # message: a custom error string
756
+ # status: a symbol in the 400 block of the rails response code table:
757
+ # https://guides.rubyonrails.org/layouts_and_rendering.html#the-status-option
758
+ render json: {errors: [{code: code, message: message}]}, status: status
759
+ false
760
+ end
761
+
522
762
  def item_not_found(item)
523
- msg = "#{item} was not found!"
524
- # returning errors based on registry specifications in https://docs.docker.com/registry/spec/api/#errors
525
- render json: {errors: [code: :invalid_request, message: msg, details: msg]}, status: :not_found
763
+ render_podman_error("NAME_UNKNOWN", "#{item} was not found!", :not_found)
526
764
  end
527
765
  end
528
766
  end
@@ -15,16 +15,12 @@ module Katello
15
15
  #param :id, String, :desc => N_("UUID of the consumer"), :required => true
16
16
  def upload_package_profile
17
17
  User.as_anonymous_admin do
18
- if Setting['upload_profiles_without_dynflow']
19
- uploader = ::Katello::Host::PackageProfileUploader.new(
20
- host: @host,
21
- profile_string: request.raw_post
22
- )
23
- uploader.upload
24
- uploader.trigger_applicability_generation
25
- else
26
- async_task(::Actions::Katello::Host::UploadPackageProfile, @host, request.raw_post)
27
- end
18
+ uploader = ::Katello::Host::PackageProfileUploader.new(
19
+ host: @host,
20
+ profile_string: request.raw_post
21
+ )
22
+ uploader.upload
23
+ uploader.trigger_applicability_generation
28
24
  end
29
25
  render :json => Resources::Candlepin::Consumer.get(@host.subscription_facet.uuid)
30
26
  end
@@ -33,16 +29,12 @@ module Katello
33
29
  param :id, String, :desc => N_("UUID of the consumer"), :required => true
34
30
  def upload_profiles
35
31
  User.as_anonymous_admin do
36
- if Setting['upload_profiles_without_dynflow']
37
- uploader = ::Katello::Host::ProfilesUploader.new(
38
- host: @host,
39
- profile_string: request.raw_post
40
- )
41
- uploader.upload
42
- uploader.trigger_applicability_generation
43
- else
44
- async_task(::Actions::Katello::Host::UploadProfiles, @host, request.raw_post)
45
- end
32
+ uploader = ::Katello::Host::ProfilesUploader.new(
33
+ host: @host,
34
+ profile_string: request.raw_post
35
+ )
36
+ uploader.upload
37
+ uploader.trigger_applicability_generation
46
38
  end
47
39
  render :json => Resources::Candlepin::Consumer.get(@host.subscription_facet.uuid)
48
40
  end
@@ -40,7 +40,7 @@ module Katello
40
40
  param :unlimited_hosts, :bool, :desc => N_("can the activation key have unlimited hosts")
41
41
  param :release_version, String, :desc => N_("content release version")
42
42
  param :service_level, String, :desc => N_("service level")
43
- param :auto_attach, :bool, :desc => N_("auto attach subscriptions upon registration")
43
+ param :auto_attach, :bool, :desc => N_("auto attach subscriptions upon registration"), deprecated: true
44
44
  param :purpose_usage, String, :desc => N_("Sets the system purpose usage")
45
45
  param :purpose_role, String, :desc => N_("Sets the system purpose usage")
46
46
  param :purpose_addons, Array, :desc => N_("Sets the system add-ons")
@@ -163,7 +163,11 @@ module Katello
163
163
  respond_for_show(:resource => @activation_key)
164
164
  end
165
165
 
166
- api :PUT, "/activation_keys/:id/add_subscriptions", N_("Attach a subscription")
166
+ def deprecate_entitlement_mode_endpoint
167
+ ::Foreman::Deprecation.api_deprecation_warning(N_("This endpoint is deprecated and will be removed in an upcoming release. Simple Content Access is the only supported content access mode."))
168
+ end
169
+
170
+ api :PUT, "/activation_keys/:id/add_subscriptions", N_("Attach a subscription"), deprecated: true
167
171
  param :id, :number, :desc => N_("ID of the activation key"), :required => true
168
172
  param :subscription_id, :number, :desc => N_("Subscription identifier"), :required => false
169
173
  param :quantity, :number, :desc => N_("Quantity of this subscription to add"), :required => false
@@ -172,6 +176,7 @@ module Katello
172
176
  param :quantity, :number, :desc => N_("Quantity of this subscriptions to add"), :required => false
173
177
  end
174
178
  def add_subscriptions
179
+ deprecate_entitlement_mode_endpoint
175
180
  if params[:subscriptions]
176
181
  params[:subscriptions].each { |subscription| @activation_key.subscribe(subscription[:id], subscription[:quantity]) }
177
182
  elsif params[:subscription_id]
@@ -181,13 +186,14 @@ module Katello
181
186
  respond_for_index(:collection => subscription_index, :template => 'subscriptions')
182
187
  end
183
188
 
184
- api :PUT, "/activation_keys/:id/remove_subscriptions", N_("Unattach a subscription")
189
+ api :PUT, "/activation_keys/:id/remove_subscriptions", N_("Unattach a subscription"), deprecated: true
185
190
  param :id, :number, :desc => N_("ID of the activation key"), :required => true
186
191
  param :subscription_id, String, :desc => N_("Subscription ID"), :required => false
187
192
  param :subscriptions, Array, :desc => N_("Array of subscriptions to add"), :required => false do
188
193
  param :id, String, :desc => N_("Subscription Pool uuid"), :required => false
189
194
  end
190
195
  def remove_subscriptions
196
+ deprecate_entitlement_mode_endpoint
191
197
  if params[:subscriptions]
192
198
  params[:subscriptions].each { |subscription| @activation_key.unsubscribe(subscription[:id]) }
193
199
  elsif params[:subscription_id]
@@ -229,7 +235,7 @@ module Katello
229
235
 
230
236
  api :GET, "/activation_keys/:id/product_content", N_("Show content available for an activation key")
231
237
  param :id, String, :desc => N_("ID of the activation key"), :required => true
232
- param :content_access_mode_all, :bool, :desc => N_("Get all content available, not just that provided by subscriptions")
238
+ param :content_access_mode_all, :bool, :desc => N_("Get all content available, not just that provided by subscriptions"), deprecated: true, default: true
233
239
  param :content_access_mode_env, :bool, :desc => N_("Limit content to just that available in the activation key's content view version")
234
240
  param_group :search, Api::V2::ApiController
235
241
  def product_content