diff --git a/packages/api-v4/CHANGELOG.md b/packages/api-v4/CHANGELOG.md index ba68e404cca..31527acef3d 100644 --- a/packages/api-v4/CHANGELOG.md +++ b/packages/api-v4/CHANGELOG.md @@ -1,3 +1,10 @@ +## [2025-12-16] - v0.154.1 + + +### Added: + +- `Akamai Cloud Pulse Logs LKE-E Audit` to the `AccountCapability` type ([#13171](https://github.com/linode/manager/pull/13171)) + ## [2025-12-09] - v0.154.0 diff --git a/packages/api-v4/package.json b/packages/api-v4/package.json index bc12f54b67b..4b3a45e9a92 100644 --- a/packages/api-v4/package.json +++ b/packages/api-v4/package.json @@ -1,6 +1,6 @@ { "name": "@linode/api-v4", - "version": "0.154.0", + "version": "0.154.1", "homepage": "https://github.com/linode/manager/tree/develop/packages/api-v4", "bugs": { "url": "https://github.com/linode/manager/issues" diff --git a/packages/api-v4/src/account/types.ts b/packages/api-v4/src/account/types.ts index 2f88bc7b053..9c952bd2dd7 100644 --- a/packages/api-v4/src/account/types.ts +++ b/packages/api-v4/src/account/types.ts @@ -63,6 +63,7 @@ export const accountCapabilities = [ 'Akamai Cloud Load Balancer', 'Akamai Cloud Pulse', 'Akamai Cloud Pulse Logs', + 'Akamai Cloud Pulse Logs LKE-E Audit', 'Block Storage', 'Block Storage Encryption', 'Cloud Firewall', diff --git a/packages/manager/CHANGELOG.md b/packages/manager/CHANGELOG.md index efe48704bd1..ce60b989007 100644 --- a/packages/manager/CHANGELOG.md +++ b/packages/manager/CHANGELOG.md @@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [2025-12-16] - v1.156.1 + + +### Changed: + +- Logs: in Stream Form limit access to "lke_audit_logs" type based on Akamai Cloud Pulse Logs LKE-E Audit capability ([#13171](https://github.com/linode/manager/pull/13171)) + +### Fixed: + +- IAM: Inability for restricted users to update own username in their profile ([#13198](https://github.com/linode/manager/pull/13198)) +- IAM: Remove Role filter (already assigned roles) in ChangeRoleForEntityDrawer ([#13201](https://github.com/linode/manager/pull/13201)) + ## [2025-12-09] - v1.156.0 diff --git a/packages/manager/cypress/e2e/core/delivery/create-stream.spec.ts b/packages/manager/cypress/e2e/core/delivery/create-stream.spec.ts index c5327ed7854..2bb0b56b2d4 100644 --- a/packages/manager/cypress/e2e/core/delivery/create-stream.spec.ts +++ b/packages/manager/cypress/e2e/core/delivery/create-stream.spec.ts @@ -1,5 +1,6 @@ import { streamType } from '@linode/api-v4'; import { mockDestination } from 'support/constants/delivery'; +import { mockGetAccount } from 'support/intercepts/account'; import { mockCreateDestination, mockCreateStream, @@ -12,9 +13,11 @@ import { ui } from 'support/ui'; import { logsStreamForm } from 'support/ui/pages/logs-stream-form'; import { randomLabel } from 'support/util/random'; -import { kubernetesClusterFactory } from 'src/factories'; +import { accountFactory, kubernetesClusterFactory } from 'src/factories'; describe('Create Stream', () => { + const account = accountFactory.build(); + beforeEach(() => { mockAppendFeatureFlags({ aclpLogs: { @@ -23,6 +26,14 @@ describe('Create Stream', () => { bypassAccountCapabilities: true, }, }); + + mockGetAccount({ + ...account, + capabilities: [ + ...account.capabilities, + 'Akamai Cloud Pulse Logs LKE-E Audit', + ], + }); }); describe('given Audit Logs Stream Type', () => { diff --git a/packages/manager/package.json b/packages/manager/package.json index 90cce025425..57a7a6f5844 100644 --- a/packages/manager/package.json +++ b/packages/manager/package.json @@ -2,7 +2,7 @@ "name": "linode-manager", "author": "Linode", "description": "The Linode Manager website", - "version": "1.156.0", + "version": "1.156.1", "private": true, "type": "module", "bugs": { diff --git a/packages/manager/src/features/Delivery/Streams/StreamForm/StreamFormGeneralInfo.test.tsx b/packages/manager/src/features/Delivery/Streams/StreamForm/StreamFormGeneralInfo.test.tsx index 4ca55775ecf..6b28b56f2e9 100644 --- a/packages/manager/src/features/Delivery/Streams/StreamForm/StreamFormGeneralInfo.test.tsx +++ b/packages/manager/src/features/Delivery/Streams/StreamForm/StreamFormGeneralInfo.test.tsx @@ -4,10 +4,23 @@ import userEvent from '@testing-library/user-event'; import React from 'react'; import { describe, expect } from 'vitest'; +import { accountFactory } from 'src/factories'; import { renderWithThemeAndHookFormContext } from 'src/utilities/testHelpers'; import { StreamFormGeneralInfo } from './StreamFormGeneralInfo'; +const queryMocks = vi.hoisted(() => ({ + useAccount: vi.fn().mockReturnValue({}), +})); + +vi.mock('@linode/queries', async () => { + const actual = await vi.importActual('@linode/queries'); + return { + ...actual, + useAccount: queryMocks.useAccount, + }; +}); + describe('StreamFormGeneralInfo', () => { describe('when in create mode', () => { it('should render Name input and allow to type text', async () => { @@ -24,35 +37,77 @@ describe('StreamFormGeneralInfo', () => { }); }); - it('should render Stream type input and allow to select different options', async () => { - renderWithThemeAndHookFormContext({ - component: , - useFormOptions: { - defaultValues: { - stream: { - type: streamType.AuditLogs, + describe('when user has Akamai Cloud Pulse Logs LKE-E Audit capability', () => { + it('should render Stream type input and allow to select different options', async () => { + const account = accountFactory.build({ + capabilities: ['Akamai Cloud Pulse Logs LKE-E Audit'], + }); + + queryMocks.useAccount.mockReturnValue({ + data: account, + isLoading: false, + error: null, + }); + + renderWithThemeAndHookFormContext({ + component: , + useFormOptions: { + defaultValues: { + stream: { + type: streamType.AuditLogs, + }, }, }, - }, - }); + }); - const streamTypesAutocomplete = screen.getByRole('combobox'); + const streamTypesAutocomplete = screen.getByRole('combobox'); - expect(streamTypesAutocomplete).toHaveValue('Audit Logs'); - - // Open the dropdown - await userEvent.click(streamTypesAutocomplete); + expect(streamTypesAutocomplete).toHaveValue('Audit Logs'); - // Select the "Kubernetes API Audit Logs" option - const kubernetesApiAuditLogs = await screen.findByText( - 'Kubernetes API Audit Logs' - ); - await userEvent.click(kubernetesApiAuditLogs); + // Open the dropdown + await userEvent.click(streamTypesAutocomplete); - await waitFor(() => { - expect(streamTypesAutocomplete).toHaveValue( + // Select the "Kubernetes API Audit Logs" option + const kubernetesApiAuditLogs = await screen.findByText( 'Kubernetes API Audit Logs' ); + await userEvent.click(kubernetesApiAuditLogs); + + await waitFor(() => { + expect(streamTypesAutocomplete).toHaveValue( + 'Kubernetes API Audit Logs' + ); + }); + }); + }); + + describe('when user does not have Akamai Cloud Pulse Logs LKE-E Audit capability', () => { + it('should render disabled Stream type input with Audit Logs selected', async () => { + const account = accountFactory.build({ + capabilities: [], + }); + + queryMocks.useAccount.mockReturnValue({ + data: account, + isLoading: false, + error: null, + }); + + renderWithThemeAndHookFormContext({ + component: , + useFormOptions: { + defaultValues: { + stream: { + type: streamType.AuditLogs, + }, + }, + }, + }); + + const streamTypesAutocomplete = screen.getByRole('combobox'); + + expect(streamTypesAutocomplete).toBeDisabled(); + expect(streamTypesAutocomplete).toHaveValue('Audit Logs'); }); }); }); diff --git a/packages/manager/src/features/Delivery/Streams/StreamForm/StreamFormGeneralInfo.tsx b/packages/manager/src/features/Delivery/Streams/StreamForm/StreamFormGeneralInfo.tsx index bf2def9dc75..42244a7e24c 100644 --- a/packages/manager/src/features/Delivery/Streams/StreamForm/StreamFormGeneralInfo.tsx +++ b/packages/manager/src/features/Delivery/Streams/StreamForm/StreamFormGeneralInfo.tsx @@ -1,4 +1,5 @@ import { streamType } from '@linode/api-v4'; +import { useAccount } from '@linode/queries'; import { Autocomplete, Box, @@ -35,6 +36,10 @@ export const StreamFormGeneralInfo = (props: StreamFormGeneralInfoProps) => { const theme = useTheme(); const { control, setValue } = useFormContext(); + const { data: account } = useAccount(); + const isLkeEAuditLogsTypeSelectionDisabled = !account?.capabilities?.includes( + 'Akamai Cloud Pulse Logs LKE-E Audit' + ); const capitalizedMode = capitalize(mode); const description = { @@ -47,8 +52,13 @@ export const StreamFormGeneralInfo = (props: StreamFormGeneralInfoProps) => { audit_logs: `Logs Delivery Streams ${capitalizedMode}-Audit Logs`, lke_audit_logs: `Logs Delivery Streams ${capitalizedMode}-Kubernetes Audit Logs`, }; + + const filteredStreamTypeOptions = isLkeEAuditLogsTypeSelectionDisabled + ? streamTypeOptions.filter(({ value }) => value !== streamType.LKEAuditLogs) + : streamTypeOptions; + const streamTypeOptionsWithPendo: AutocompleteOption[] = - streamTypeOptions.map((option) => ({ + filteredStreamTypeOptions.map((option) => ({ ...option, pendoId: pendoIds[option.value as StreamType], })); @@ -98,7 +108,9 @@ export const StreamFormGeneralInfo = (props: StreamFormGeneralInfoProps) => { render={({ field, fieldState }) => ( - entity.roles.includes(el.value) - ) && matchesRoleContext - ); + return matchesRoleContext; } return true; }); - }, [accountRoles, role, assignedRoles]); + }, [accountRoles, role]); const { control, diff --git a/packages/manager/src/features/IAM/Users/UserDetails/UserEmailPanel.test.tsx b/packages/manager/src/features/IAM/Users/UserDetails/UserEmailPanel.test.tsx index f7ee71ade27..aa9377a1490 100644 --- a/packages/manager/src/features/IAM/Users/UserDetails/UserEmailPanel.test.tsx +++ b/packages/manager/src/features/IAM/Users/UserDetails/UserEmailPanel.test.tsx @@ -108,7 +108,7 @@ describe('UserEmailPanel', () => { expect(errorText).toBeInTheDocument(); }); - it('disables the save button when the user does not have update_user permission', async () => { + it('disables the save button when the user does not have is_account_admin permission', async () => { const user = accountUserFactory.build({ email: 'my-linode-email', }); diff --git a/packages/manager/src/features/IAM/Users/UserDetails/UserProfile.tsx b/packages/manager/src/features/IAM/Users/UserDetails/UserProfile.tsx index f7359289a03..9f0fe3392a5 100644 --- a/packages/manager/src/features/IAM/Users/UserDetails/UserProfile.tsx +++ b/packages/manager/src/features/IAM/Users/UserDetails/UserProfile.tsx @@ -19,11 +19,7 @@ import { UsernamePanel } from './UsernamePanel'; export const UserProfile = () => { const { username } = useParams({ from: '/iam/users/$username' }); - const { data: permissions } = usePermissions('account', [ - 'is_account_admin', - 'update_user', - 'delete_user', - ]); + const { data: permissions } = usePermissions('account', ['is_account_admin']); const isAccountAdmin = permissions?.is_account_admin; @@ -34,10 +30,6 @@ export const UserProfile = () => { } = useAccountUser(username ?? '', isAccountAdmin); const { data: assignedRoles } = useUserRoles(username ?? '', isAccountAdmin); - // Only admin users get update_user and delete_user permissions, but doing a bit of defensive programming here to be safe. - const canUpdateUser = isAccountAdmin || permissions?.update_user; - const canDeleteUser = isAccountAdmin || permissions?.delete_user; - if (isLoading) { return ; } @@ -66,9 +58,9 @@ export const UserProfile = () => { sx={(theme) => ({ marginTop: theme.tokens.spacing.S16 })} > - - - + + + ); diff --git a/packages/manager/src/features/IAM/Users/UserDetails/UsernamePanel.test.tsx b/packages/manager/src/features/IAM/Users/UserDetails/UsernamePanel.test.tsx index eea6ec395b3..1ec994012d6 100644 --- a/packages/manager/src/features/IAM/Users/UserDetails/UsernamePanel.test.tsx +++ b/packages/manager/src/features/IAM/Users/UserDetails/UsernamePanel.test.tsx @@ -9,7 +9,7 @@ import { UsernamePanel } from './UsernamePanel'; const queryMocks = vi.hoisted(() => ({ userPermissions: vi.fn(() => ({ data: { - update_user: false, + is_account_admin: false, }, })), })); @@ -31,7 +31,7 @@ describe('UsernamePanel', () => { expect(usernameTextField).toHaveDisplayValue(user.username); }); - it('disables the input if the user doesn not have update_user permission', async () => { + it('disables the input if the user doesn not have is_account_admin permission', async () => { const user = accountUserFactory.build(); const { getByLabelText } = renderWithTheme( @@ -50,7 +50,7 @@ describe('UsernamePanel', () => { it("does not allow the user to update a proxy user's username", async () => { queryMocks.userPermissions.mockReturnValue({ data: { - update_user: true, + is_account_admin: true, }, }); @@ -76,14 +76,14 @@ describe('UsernamePanel', () => { expect(getByText('Save').closest('button')).toBeDisabled(); }); - it('enables the save button when the user makes a change to the username and has update_user permission', async () => { + it('enables the save button when the user makes a change to the username and has is_account_admin permission', async () => { const user = accountUserFactory.build({ username: 'my-linode-username', }); queryMocks.userPermissions.mockReturnValue({ data: { - update_user: true, + is_account_admin: true, }, }); @@ -102,14 +102,14 @@ describe('UsernamePanel', () => { expect(saveButton).toBeEnabled(); }); - it('disables the save button when the user does not have update_user permission', async () => { + it('disables the save button when the user does not have is_account_admin permission', async () => { const user = accountUserFactory.build({ username: 'my-linode-username', }); queryMocks.userPermissions.mockReturnValue({ data: { - update_user: false, + is_account_admin: false, }, }); diff --git a/packages/manager/src/features/Profile/DisplaySettings/UsernameForm.test.tsx b/packages/manager/src/features/Profile/DisplaySettings/UsernameForm.test.tsx index 8c3e0d1fe90..4b9fa26bb54 100644 --- a/packages/manager/src/features/Profile/DisplaySettings/UsernameForm.test.tsx +++ b/packages/manager/src/features/Profile/DisplaySettings/UsernameForm.test.tsx @@ -11,7 +11,7 @@ import { UsernameForm } from './UsernameForm'; const queryMocks = vi.hoisted(() => ({ userPermissions: vi.fn(() => ({ data: { - update_user: false, + is_account_admin: false, }, })), })); @@ -38,7 +38,7 @@ describe('UsernameForm', () => { await findByDisplayValue(profile.username); }); - it('disables the input if the user doesn not have update_user permission', async () => { + it('disables the input if the user doesn not have is_account_admin permission', async () => { const { getByLabelText } = renderWithTheme(); expect(getByLabelText('Username')).toBeDisabled(); @@ -53,7 +53,7 @@ describe('UsernameForm', () => { it('disables the input if the user is a proxy user', async () => { queryMocks.userPermissions.mockReturnValue({ data: { - update_user: true, + is_account_admin: true, }, }); @@ -73,14 +73,14 @@ describe('UsernameForm', () => { expect(getByLabelText('This field can’t be modified.')).toBeVisible(); }); - it('enables the save button when the user makes a change to the username and has update_user permission', async () => { + it('enables the save button when the user makes a change to the username and has is_account_admin permission', async () => { const profile = profileFactory.build({ username: 'my-linode-username' }); server.use(http.get('*/v4/profile', () => HttpResponse.json(profile))); queryMocks.userPermissions.mockReturnValue({ data: { - update_user: true, + is_account_admin: true, }, }); diff --git a/packages/manager/src/features/Profile/DisplaySettings/UsernameForm.tsx b/packages/manager/src/features/Profile/DisplaySettings/UsernameForm.tsx index 0ca2f5a06ba..3231dcdedd9 100644 --- a/packages/manager/src/features/Profile/DisplaySettings/UsernameForm.tsx +++ b/packages/manager/src/features/Profile/DisplaySettings/UsernameForm.tsx @@ -24,7 +24,7 @@ export const UsernameForm = () => { const values = { username: profile?.username ?? '' }; - const { data: permissions } = usePermissions('account', ['update_user']); + const { data: permissions } = usePermissions('account', ['is_account_admin']); const { control, @@ -37,7 +37,7 @@ export const UsernameForm = () => { values, }); - const tooltipForDisabledUsernameField = !permissions.update_user + const tooltipForDisabledUsernameField = !permissions.is_account_admin ? 'Restricted users cannot update their username. Please contact an account administrator.' : profile?.user_type === 'proxy' ? RESTRICTED_FIELD_TOOLTIP