diff --git a/addons/core/translations/actions/en-us.yaml b/addons/core/translations/actions/en-us.yaml
index 5bd43c30d..49f509ee1 100644
--- a/addons/core/translations/actions/en-us.yaml
+++ b/addons/core/translations/actions/en-us.yaml
@@ -70,3 +70,4 @@ hide-errors: Hide Errors
edit-worker-filter: Edit Worker Filter
add-worker-filter: Add Worker Filter
open: Open
+download: Download
diff --git a/addons/core/translations/resources/en-us.yaml b/addons/core/translations/resources/en-us.yaml
index 2b2f70318..147032891 100644
--- a/addons/core/translations/resources/en-us.yaml
+++ b/addons/core/translations/resources/en-us.yaml
@@ -680,6 +680,11 @@ role:
insert-resource-types: insert resource types
insert-id: insert id
insert-resource-ids: insert resource ids
+ export:
+ format: Format
+ options:
+ terraform: Terraform
+ native-hcl: Native HCL
scope:
title: Scope
title_plural: Scopes
diff --git a/ui/admin/app/components/form/role/edit-grants/index.hbs b/ui/admin/app/components/form/role/edit-grants/index.hbs
index d3ee4c92d..c991e8b65 100644
--- a/ui/admin/app/components/form/role/edit-grants/index.hbs
+++ b/ui/admin/app/components/form/role/edit-grants/index.hbs
@@ -25,16 +25,67 @@
+ {{#if this.showExportOptionsFlyout}}
+
+ {{t 'actions.export'}}
+
+
+ {{t 'resources.role.edit-grants.export.format'}}
+
+ {{#each this.exportOptions as |type|}}
+
+ {{/each}}
+
+
+
+
+
+
+
+
+ {{/if}}
+
<:actions>
-
+ {{#if this.grantStringLines}}
+
+ {{/if}}
diff --git a/ui/admin/app/components/form/role/edit-grants/index.js b/ui/admin/app/components/form/role/edit-grants/index.js
new file mode 100644
index 000000000..52f4463b7
--- /dev/null
+++ b/ui/admin/app/components/form/role/edit-grants/index.js
@@ -0,0 +1,92 @@
+/**
+ * Copyright IBM Corp. 2021, 2026
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+import { action } from '@ember/object';
+
+export default class FormRoleEditGrants extends Component {
+ // =attributes
+
+ exportOptionsMap = { terraform: 'terraform', nativeHCL: 'native-hcl' };
+ exportOptions = Object.values(this.exportOptionsMap);
+
+ // TODO: Replace with actual grant lines from code editor once implemented.
+ grantStringLines =
+ 'ids=hc_123;type=host-catalog;actions=read,create,list\nids=ttcp_123;type=target;actions=list\ntype=credential;actions=create';
+
+ @tracked showExportOptionsFlyout = false;
+ @tracked selectedExportOption = this.exportOptions[0];
+
+ /**
+ * Returns the formatted export based on the selected export option.
+ * @type {string}
+ */
+ get formattedExport() {
+ if (this.selectedExportOption === this.exportOptionsMap.terraform) {
+ return this.terraformFormattedExport;
+ }
+ return this.nativeHclFormattedExport;
+ }
+
+ /**
+ * Formats grant strings into a Terraform format.
+ * @type {string}
+ */
+ get terraformFormattedExport() {
+ let formatted = `grant_strings = [ \n`;
+ this.grantStringLines.split('\n').forEach((line) => {
+ formatted += ` "${line}",\n`;
+ });
+ formatted += `]\n`;
+ return formatted;
+ }
+
+ /**
+ * Formats grant strings into a Native HCL format.
+ * @type {string}
+ */
+ get nativeHclFormattedExport() {
+ let formatted = `[ \n`;
+ this.grantStringLines.split('\n').forEach((line) => {
+ formatted += ` "${line}",\n`;
+ });
+ formatted += `]\n`;
+ return formatted;
+ }
+
+ // =actions
+
+ /**
+ * Toggles the export options flyout open and closed.
+ */
+ @action
+ toggleExportOptionsFlyout() {
+ this.showExportOptionsFlyout = !this.showExportOptionsFlyout;
+ }
+
+ /**
+ * Triggers the download of the formatted export as a file.
+ */
+ @action
+ downloadFormattedExport() {
+ const fileName =
+ this.selectedExportOption === this.exportOptionsMap.terraform
+ ? 'grant-strings.tf'
+ : 'grant-strings.hcl';
+ const blob = new Blob([this.formattedExport], { type: 'text/plain' });
+
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+
+ a.href = url;
+ a.download = fileName;
+ const flyoutElement = document.getElementById('export-options-flyout');
+ flyoutElement.appendChild(a);
+ a.click();
+ flyoutElement.removeChild(a);
+ URL.revokeObjectURL(url);
+ }
+}
diff --git a/ui/admin/app/components/grant-string-formats/index.js b/ui/admin/app/components/grant-string-formats/index.js
index 2915008b1..fc6fc5df4 100644
--- a/ui/admin/app/components/grant-string-formats/index.js
+++ b/ui/admin/app/components/grant-string-formats/index.js
@@ -24,9 +24,9 @@ export default class GrantStringFormatsIndex extends Component {
[this.stringFormatTypes.resourceType]:
`type=<${this.intl.t('resources.role.edit-grants.string-formats.insert-resource-types')}>;actions=<${this.intl.t('resources.role.edit-grants.string-formats.insert-actions')}>`,
[this.stringFormatTypes.resource]:
- `id=<${this.intl.t('resources.role.edit-grants.string-formats.insert-resource-ids')}>;actions=<${this.intl.t('resources.role.edit-grants.string-formats.insert-actions')}>`,
+ `ids=<${this.intl.t('resources.role.edit-grants.string-formats.insert-resource-ids')}>;actions=<${this.intl.t('resources.role.edit-grants.string-formats.insert-actions')}>`,
[this.stringFormatTypes.pinnedId]:
- `id=<${this.intl.t('resources.role.edit-grants.string-formats.insert-id')}>;type=<${this.intl.t('resources.role.edit-grants.string-formats.insert-resource-types')}>;actions=<${this.intl.t('resources.role.edit-grants.string-formats.insert-actions')}>`,
+ `ids=<${this.intl.t('resources.role.edit-grants.string-formats.insert-id')}>;type=<${this.intl.t('resources.role.edit-grants.string-formats.insert-resource-types')}>;actions=<${this.intl.t('resources.role.edit-grants.string-formats.insert-actions')}>`,
};
@tracked selectedFormatType = this.stringFormatTypes.resourceType;
diff --git a/ui/admin/tests/acceptance/roles/edit-grants-test.js b/ui/admin/tests/acceptance/roles/edit-grants-test.js
index d625f264a..0365176e2 100644
--- a/ui/admin/tests/acceptance/roles/edit-grants-test.js
+++ b/ui/admin/tests/acceptance/roles/edit-grants-test.js
@@ -4,7 +4,7 @@
*/
import { module, test } from 'qunit';
-import { visit, click, currentURL } from '@ember/test-helpers';
+import { visit, click, currentURL, select } from '@ember/test-helpers';
import { setupApplicationTest } from 'admin/tests/helpers';
import * as selectors from './selectors';
import { setRunOptions } from 'ember-a11y-testing/test-support';
@@ -76,4 +76,50 @@ module('Acceptance | roles/edit grants', function (hooks) {
assert.dom(selectors.MANAGE_DROPDOWN_EDIT_GRANTS).doesNotExist();
assert.strictEqual(currentURL(), urls.role);
});
+
+ test('user can export a terraform formatted grant string', async function (assert) {
+ setRunOptions({
+ rules: {
+ 'color-contrast': {
+ // [ember-a11y-ignore]: axe rule "color-contrast" automatically ignored on 2026-03-17
+ enabled: false,
+ },
+ },
+ });
+
+ await visit(urls.role);
+
+ await click(selectors.MANAGE_DROPDOWN_ROLES);
+ await click(selectors.MANAGE_DROPDOWN_EDIT_GRANTS);
+ await click(selectors.EXPORT_GRANTS_BTN);
+
+ assert.dom(selectors.EXPORT_OPTIONS_FLYOUT).isVisible();
+ assert.dom(selectors.EXPORT_FORMAT_SELECT).hasValue('terraform');
+ // TODO: Update to check for the actual formatted grant string.
+ assert
+ .dom(selectors.FORMATTED_EXPORT_CODE_BLOCK)
+ .includesText('grant_strings');
+ });
+
+ test('user can export a hcl formatted grant string', async function (assert) {
+ setRunOptions({
+ rules: {
+ 'color-contrast': {
+ // [ember-a11y-ignore]: axe rule "color-contrast" automatically ignored on 2026-03-17
+ enabled: false,
+ },
+ },
+ });
+
+ await visit(urls.role);
+
+ await click(selectors.MANAGE_DROPDOWN_ROLES);
+ await click(selectors.MANAGE_DROPDOWN_EDIT_GRANTS);
+ await click(selectors.EXPORT_GRANTS_BTN);
+ await select(selectors.EXPORT_FORMAT_SELECT, 'native-hcl');
+
+ assert.dom(selectors.EXPORT_OPTIONS_FLYOUT).isVisible();
+ assert.dom(selectors.EXPORT_FORMAT_SELECT).hasValue('native-hcl');
+ // TODO: Update to check for the actual formatted grant string.
+ });
});
diff --git a/ui/admin/tests/acceptance/roles/selectors.js b/ui/admin/tests/acceptance/roles/selectors.js
index dc50628e6..dc558ee03 100644
--- a/ui/admin/tests/acceptance/roles/selectors.js
+++ b/ui/admin/tests/acceptance/roles/selectors.js
@@ -60,6 +60,12 @@ export const FIELD_GRANT = 'form:nth-child(2) [name="grant"]';
export const FIELD_GRANT_VALUE = 'ids=123,action=delete';
export const FIELD_GRANT_REMOVE_BTN = 'form:nth-child(2) button[type="button"]';
export const GRANTS_FORM = 'form:nth-child(2)';
+export const EXPORT_OPTIONS_FLYOUT = '[data-test-export-options-flyout]';
+export const EXPORT_FORMAT_SELECT =
+ '[data-test-export-options-flyout] [name="export-format"]';
+export const FORMATTED_EXPORT_CODE_BLOCK =
+ '[data-test-export-options-flyout] .hds-code-block code';
// We need to use a different selector for the grants form because the
// grants form has multiple submit buttons
export const SAVE_BTN = '.rose-form-actions [type=submit]';
+export const EXPORT_GRANTS_BTN = '[data-test-export-grants-btn]';
diff --git a/ui/admin/tests/integration/components/grant-string-formats/index-test.js b/ui/admin/tests/integration/components/grant-string-formats/index-test.js
index 6f44637ef..0c2f31ec1 100644
--- a/ui/admin/tests/integration/components/grant-string-formats/index-test.js
+++ b/ui/admin/tests/integration/components/grant-string-formats/index-test.js
@@ -41,7 +41,7 @@ module(
assert.dom(PINNED_ID_OPTION).isNotChecked();
assert
.dom(STRING_FORMAT_FIELD)
- .hasValue('id=;actions=');
+ .hasValue('ids=;actions=');
});
test('it renders string format when type is changed to pinned id', async function (assert) {
@@ -55,7 +55,7 @@ module(
assert
.dom(STRING_FORMAT_FIELD)
.hasValue(
- 'id=;type=;actions=',
+ 'ids=;type=;actions=',
);
});
},