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