Skip to content

Commit 47fd25f

Browse files
authored
feat(quickstart): add permission fetching for quickstart roles (#1373)
* feat(quickstart): add permission fetching for quickstart roles Signed-off-by: Yi Cai <yicai@redhat.com> * added changeset Signed-off-by: Yi Cai <yicai@redhat.com> * updated logic for rbac not enabled Signed-off-by: Yi Cai <yicai@redhat.com> * updated readme Signed-off-by: Yi Cai <yicai@redhat.com> * updated logic Signed-off-by: Yi Cai <yicai@redhat.com> * updated package version Signed-off-by: Yi Cai <yicai@redhat.com> * updated test to reduce duplicates Signed-off-by: Yi Cai <yicai@redhat.com> * hide quickstart when no items setup Signed-off-by: Yi Cai <yicai@redhat.com> --------- Signed-off-by: Yi Cai <yicai@redhat.com>
1 parent 9807798 commit 47fd25f

16 files changed

Lines changed: 393 additions & 367 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@red-hat-developer-hub/backstage-plugin-quickstart': minor
3+
---
4+
5+
Enabled Quickstart items for developer role.

workspaces/quickstart/plugins/quickstart/README.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ The Quickstart plugin provides a guided onboarding experience for new users of R
99
- **Configurable Content**: Define custom quickstart items through app configuration
1010
- **Visual Progress Indicator**: Shows overall completion progress with a progress bar
1111
- **Call-to-Action Support**: Each step can include clickable action buttons
12+
- **Role-Based Access Control**: Show different quickstart items based on user roles (admin/developer)
1213

1314
## Installation
1415

@@ -44,18 +45,28 @@ app:
4445
- title: 'Welcome to Developer Hub'
4546
description: 'Learn the basics of navigating the Developer Hub interface'
4647
icon: 'home'
48+
roles: ['admin', 'developer'] # Show to both roles
4749
cta:
4850
text: 'Get Started'
4951
link: '/catalog'
52+
- title: 'Configure RBAC Policies'
53+
description: 'Set up role-based access control for your organization'
54+
icon: 'security'
55+
roles: ['admin'] # Admin-only quickstart item
56+
cta:
57+
text: 'Configure RBAC'
58+
link: '/rbac'
5059
- title: 'Create Your First Component'
5160
description: 'Follow our guide to register your first software component'
5261
icon: 'code'
62+
# No roles specified - defaults to 'admin'
5363
cta:
5464
text: 'Create Component'
5565
link: '/catalog-import'
5666
- title: 'Explore Templates'
5767
description: 'Discover available software templates to bootstrap new projects'
5868
icon: 'template'
69+
roles: ['developer'] # Developer-only quickstart item
5970
cta:
6071
text: 'Browse Templates'
6172
link: '/create'
@@ -68,10 +79,56 @@ Each quickstart item supports the following properties:
6879
- `title` (required): The display title for the quickstart step
6980
- `description` (required): A brief description of what the step covers
7081
- `icon` (optional): Icon identifier (supports Material UI icons)
82+
- `roles` (optional): Array of user roles that should see this quickstart item. Supported values: `['admin', 'developer']`. If not specified, defaults to `['admin']`
7183
- `cta` (optional): Call-to-action object with:
7284
- `text`: Button text
7385
- `link`: Target URL or route
7486

87+
## Role-Based Access Control (RBAC)
88+
89+
The quickstart plugin integrates with Backstage's RBAC system to show different quickstart items based on user roles.
90+
91+
### User Role Determination
92+
93+
The plugin determines user roles using the following logic:
94+
95+
- **When RBAC is disabled** (`permission.enabled: false` or not configured): Users are assumed to be platform engineers setting up RHDH and are assigned the `admin` role
96+
- **When RBAC is enabled** (`permission.enabled: true`): User roles are determined based on permissions:
97+
- Users with `policy.entity.create` permission are assigned the `admin` role
98+
- Users without this permission are assigned the `developer` role
99+
100+
### Supported Roles
101+
102+
- **`admin`**: Platform engineers, administrators, and users with elevated permissions
103+
- **`developer`**: Regular developers and users with standard permissions
104+
105+
### Role Assignment Behavior
106+
107+
- Quickstart items without a `roles` property default to `['admin']`
108+
- Items can specify multiple roles: `roles: ['admin', 'developer']`
109+
- Users only see quickstart items that match their assigned role
110+
111+
### Configuration Example
112+
113+
Enable RBAC in your `app-config.yaml`:
114+
115+
```yaml
116+
permission:
117+
enabled: true
118+
119+
app:
120+
quickstart:
121+
- title: 'Platform Configuration'
122+
roles: ['admin']
123+
# Only admins see this
124+
- title: 'Getting Started as Developer'
125+
roles: ['developer']
126+
# Only developers see this
127+
- title: 'Universal Welcome Guide'
128+
roles: ['admin', 'developer']
129+
# Both roles see this
130+
```
131+
75132
## Usage
76133

77134
### Using the Context Hook

workspaces/quickstart/plugins/quickstart/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,15 @@
3232
"postpack": "backstage-cli package postpack"
3333
},
3434
"dependencies": {
35+
"@backstage-community/plugin-rbac-common": "^1.19.0",
3536
"@backstage/core-components": "^0.17.4",
3637
"@backstage/core-plugin-api": "^1.10.9",
38+
"@backstage/plugin-permission-react": "^0.4.36",
3739
"@backstage/theme": "^0.6.7",
3840
"@mui/icons-material": "5.18.0",
3941
"@mui/material": "5.18.0",
4042
"@red-hat-developer-hub/backstage-plugin-theme": "^0.9.0",
41-
"react-use": "^17.2.4"
43+
"react-use": "^17.6.0"
4244
},
4345
"peerDependencies": {
4446
"react": "^16.13.1 || ^17.0.0 || ^18.0.0"
@@ -47,6 +49,7 @@
4749
"@backstage/cli": "^0.33.1",
4850
"@backstage/core-app-api": "^1.18.0",
4951
"@backstage/dev-utils": "^1.1.12",
52+
"@backstage/plugin-permission-common": "^0.9.1",
5053
"@backstage/test-utils": "^1.7.10",
5154
"@testing-library/jest-dom": "^6.0.0",
5255
"@testing-library/react": "^14.0.0",

workspaces/quickstart/plugins/quickstart/src/components/Quickstart.test.tsx

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ describe('Quickstart', () => {
3838
<Quickstart
3939
quickstartItems={items}
4040
handleDrawerClose={mockHandleDrawerClose}
41+
isLoading={false}
4142
/>,
4243
);
4344
};
@@ -92,16 +93,6 @@ describe('Quickstart', () => {
9293

9394
expectHideButton();
9495
});
95-
96-
it('shows empty state when no items match user role', async () => {
97-
await renderWithRole('manager');
98-
99-
// Should show empty state
100-
expect(
101-
screen.getByText('Quickstart content not available for your role.'),
102-
).toBeInTheDocument();
103-
expectHideButton();
104-
});
10596
});
10697

10798
describe('Progress calculation with role-based items', () => {
@@ -180,19 +171,5 @@ describe('Quickstart', () => {
180171

181172
expect(mockHandleDrawerClose).toHaveBeenCalledTimes(1);
182173
});
183-
184-
it('handles empty items array gracefully', async () => {
185-
await renderInTestApp(
186-
<Quickstart
187-
quickstartItems={[]}
188-
handleDrawerClose={mockHandleDrawerClose}
189-
/>,
190-
);
191-
192-
expect(
193-
screen.getByText('Quickstart content not available for your role.'),
194-
).toBeInTheDocument();
195-
expectHideButton();
196-
});
197174
});
198175
});

workspaces/quickstart/plugins/quickstart/src/components/Quickstart.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,13 @@ import { QuickstartItemData } from '../types';
2525
type QuickstartProps = {
2626
quickstartItems: QuickstartItemData[];
2727
handleDrawerClose: () => void;
28+
isLoading: boolean;
2829
};
2930

3031
export const Quickstart = ({
3132
quickstartItems,
3233
handleDrawerClose,
34+
isLoading,
3335
}: QuickstartProps) => {
3436
const itemCount = quickstartItems.length;
3537
const [progress, setProgress] = useState<number>(0);
@@ -75,6 +77,7 @@ export const Quickstart = ({
7577
const newProgress = calculateProgress();
7678
setProgress(newProgress);
7779
}}
80+
isLoading={isLoading}
7881
/>
7982
</Box>
8083
<QuickstartFooter

workspaces/quickstart/plugins/quickstart/src/components/QuickstartButton/QuickstartButton.test.tsx

Lines changed: 89 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,59 +14,115 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { render, screen, fireEvent } from '@testing-library/react';
17+
import { screen, fireEvent } from '@testing-library/react';
18+
import {
19+
mockApis,
20+
renderInTestApp,
21+
TestApiProvider,
22+
} from '@backstage/test-utils';
23+
import { configApiRef } from '@backstage/core-plugin-api';
1824
import { QuickstartButton } from './QuickstartButton';
19-
import { useQuickstartPermission } from '../../hooks/useQuickstartPermission';
2025
import { useQuickstartDrawerContext } from '../../hooks/useQuickstartDrawerContext';
26+
import { useQuickstartRole } from '../../hooks/useQuickstartRole';
2127

2228
// Mock the hooks
23-
jest.mock('../../hooks/useQuickstartPermission', () => ({
24-
useQuickstartPermission: jest.fn(),
25-
}));
26-
2729
jest.mock('../../hooks/useQuickstartDrawerContext', () => ({
2830
useQuickstartDrawerContext: jest.fn(),
2931
}));
3032

33+
jest.mock('../../hooks/useQuickstartRole', () => ({
34+
useQuickstartRole: jest.fn(),
35+
}));
36+
3137
describe('QuickstartButton', () => {
3238
const mockToggleDrawer = jest.fn();
3339
const mockOnClick = jest.fn();
3440

41+
const mockConfigApi = mockApis.config({
42+
data: {
43+
app: {
44+
quickstart: [
45+
{
46+
title: 'Test Quickstart',
47+
roles: ['admin'],
48+
steps: [],
49+
},
50+
],
51+
},
52+
},
53+
});
54+
3555
beforeEach(() => {
3656
jest.clearAllMocks();
37-
(useQuickstartPermission as jest.Mock).mockReturnValue(true);
3857
(useQuickstartDrawerContext as jest.Mock).mockReturnValue({
3958
toggleDrawer: mockToggleDrawer,
4059
});
60+
(useQuickstartRole as jest.Mock).mockReturnValue({
61+
isLoading: false,
62+
userRole: 'admin',
63+
});
4164
});
4265

43-
it('renders the button when permission is allowed', () => {
44-
render(<QuickstartButton />);
66+
const renderWithApi = (configApi = mockConfigApi) => {
67+
return renderInTestApp(
68+
<TestApiProvider apis={[[configApiRef, configApi]]}>
69+
<QuickstartButton />
70+
</TestApiProvider>,
71+
);
72+
};
73+
74+
it('renders the button when user has quickstart items', async () => {
75+
await renderWithApi();
4576

4677
const button = screen.getByTestId('quickstart-button');
4778
expect(button).toBeInTheDocument();
4879
expect(screen.getByText('Quick start')).toBeInTheDocument();
4980
});
5081

51-
it('does not render when permission is denied', () => {
52-
(useQuickstartPermission as jest.Mock).mockReturnValue(false);
82+
it('does not render when user has no quickstart items', async () => {
83+
const emptyConfigApi = mockApis.config({
84+
data: {
85+
app: {
86+
quickstart: [],
87+
},
88+
},
89+
});
5390

54-
render(<QuickstartButton />);
91+
await renderWithApi(emptyConfigApi);
5592

56-
expect(screen.queryByTestId('quickstart-button')).not.toBeInTheDocument();
93+
const button = screen.queryByTestId('quickstart-button');
94+
expect(button).not.toBeInTheDocument();
5795
});
5896

59-
it('calls toggleDrawer when clicked', () => {
60-
render(<QuickstartButton />);
97+
it('does not render when user role does not match any items', async () => {
98+
(useQuickstartRole as jest.Mock).mockReturnValue({
99+
isLoading: false,
100+
userRole: 'developer', // No items for developer in our mock config
101+
});
102+
103+
await renderWithApi();
104+
105+
const button = screen.queryByTestId('quickstart-button');
106+
expect(button).not.toBeInTheDocument();
107+
});
108+
109+
it('calls toggleDrawer when clicked', async () => {
110+
await renderWithApi();
61111

62112
const button = screen.getByTestId('quickstart-button');
63113
fireEvent.click(button);
64114

65115
expect(mockToggleDrawer).toHaveBeenCalledTimes(1);
66116
});
67117

68-
it('calls custom onClick when provided', () => {
69-
render(<QuickstartButton onClick={mockOnClick} />);
118+
it('calls custom onClick when provided', async () => {
119+
const { rerender } = await renderWithApi();
120+
121+
rerender(
122+
<TestApiProvider apis={[[configApiRef, mockConfigApi]]}>
123+
<QuickstartButton onClick={mockOnClick} />
124+
</TestApiProvider>,
125+
);
70126

71127
const button = screen.getByTestId('quickstart-button');
72128
fireEvent.click(button);
@@ -75,15 +131,27 @@ describe('QuickstartButton', () => {
75131
expect(mockOnClick).toHaveBeenCalledTimes(1);
76132
});
77133

78-
it('renders with custom title', () => {
79-
render(<QuickstartButton title="Custom Quickstart" />);
134+
it('renders with custom title', async () => {
135+
const { rerender } = await renderWithApi();
136+
137+
rerender(
138+
<TestApiProvider apis={[[configApiRef, mockConfigApi]]}>
139+
<QuickstartButton title="Custom Quickstart" />
140+
</TestApiProvider>,
141+
);
80142

81143
expect(screen.getByText('Custom Quickstart')).toBeInTheDocument();
82144
});
83145

84-
it('applies custom styles', () => {
146+
it('applies custom styles', async () => {
85147
const customStyle = { backgroundColor: 'red' };
86-
render(<QuickstartButton style={customStyle} />);
148+
const { rerender } = await renderWithApi();
149+
150+
rerender(
151+
<TestApiProvider apis={[[configApiRef, mockConfigApi]]}>
152+
<QuickstartButton style={customStyle} />
153+
</TestApiProvider>,
154+
);
87155

88156
const button = screen.getByTestId('quickstart-button');
89157
expect(button).toHaveStyle('background-color: red');

0 commit comments

Comments
 (0)