Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/api-v4/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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


Expand Down
2 changes: 1 addition & 1 deletion packages/api-v4/package.json
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
1 change: 1 addition & 0 deletions packages/api-v4/src/account/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
12 changes: 12 additions & 0 deletions packages/manager/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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: {
Expand All @@ -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', () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/manager/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,23 @@
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 () => {
Expand All @@ -24,35 +37,77 @@
});
});

it('should render Stream type input and allow to select different options', async () => {
renderWithThemeAndHookFormContext({
component: <StreamFormGeneralInfo mode="create" />,
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: <StreamFormGeneralInfo mode="create" />,
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');

Check warning on line 65 in packages/manager/src/features/Delivery/Streams/StreamForm/StreamFormGeneralInfo.test.tsx

View workflow job for this annotation

GitHub Actions / eslint

[eslint] packages/manager/src/features/Delivery/Streams/StreamForm/StreamFormGeneralInfo.test.tsx#L65 <sonarjs/no-duplicate-string>(https://sonarsource.github.io/rspec/#/rspec/S1192/javascript)

Define a constant instead of duplicating this literal 3 times.
Raw output
{"ruleId":"sonarjs/no-duplicate-string","severity":1,"message":"Define a constant instead of duplicating this literal 3 times.","line":65,"column":53,"nodeType":"Literal","endLine":65,"endColumn":65}

// 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(() => {

Check warning on line 76 in packages/manager/src/features/Delivery/Streams/StreamForm/StreamFormGeneralInfo.test.tsx

View workflow job for this annotation

GitHub Actions / eslint

[eslint] packages/manager/src/features/Delivery/Streams/StreamForm/StreamFormGeneralInfo.test.tsx#L76 <sonarjs/no-nested-functions>(https://sonarsource.github.io/rspec/#/rspec/S2004/javascript)

Refactor this code to not nest functions more than 4 levels deep.
Raw output
{"ruleId":"sonarjs/no-nested-functions","severity":1,"message":"Refactor this code to not nest functions more than 4 levels deep.","line":76,"column":26,"nodeType":null,"endLine":76,"endColumn":28}
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: <StreamFormGeneralInfo mode="create" />,
useFormOptions: {
defaultValues: {
stream: {
type: streamType.AuditLogs,
},
},
},
});

const streamTypesAutocomplete = screen.getByRole('combobox');

expect(streamTypesAutocomplete).toBeDisabled();
expect(streamTypesAutocomplete).toHaveValue('Audit Logs');
});
});
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { streamType } from '@linode/api-v4';
import { useAccount } from '@linode/queries';
import {
Autocomplete,
Box,
Expand Down Expand Up @@ -35,6 +36,10 @@ export const StreamFormGeneralInfo = (props: StreamFormGeneralInfoProps) => {

const theme = useTheme();
const { control, setValue } = useFormContext<StreamAndDestinationFormType>();
const { data: account } = useAccount();
const isLkeEAuditLogsTypeSelectionDisabled = !account?.capabilities?.includes(
'Akamai Cloud Pulse Logs LKE-E Audit'
);

const capitalizedMode = capitalize(mode);
const description = {
Expand All @@ -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],
}));
Expand Down Expand Up @@ -98,7 +108,9 @@ export const StreamFormGeneralInfo = (props: StreamFormGeneralInfoProps) => {
render={({ field, fieldState }) => (
<Autocomplete
disableClearable
disabled={isFormInEditMode(mode)}
disabled={
isFormInEditMode(mode) || isLkeEAuditLogsTypeSelectionDisabled
}
errorText={fieldState.error?.message}
label="Stream Type"
onBlur={field.onBlur}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,24 +90,16 @@ export const ChangeRoleForEntityDrawer = ({
el.access === role?.access &&
el.value !== role?.role_name;

// Exclude account roles already assigned to the user
if (isAccountRole(el)) {
return (
!assignedRoles?.account_access.includes(el.value) &&
matchesRoleContext
);
return matchesRoleContext;
}
// Exclude entity roles already assigned to the user

if (isEntityRole(el)) {
return (
!assignedRoles?.entity_access.some((entity) =>
entity.roles.includes(el.value)
) && matchesRoleContext
);
return matchesRoleContext;
}
return true;
});
}, [accountRoles, role, assignedRoles]);
}, [accountRoles, role]);

const {
control,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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 <CircleProgress />;
}
Expand Down Expand Up @@ -66,9 +58,9 @@ export const UserProfile = () => {
sx={(theme) => ({ marginTop: theme.tokens.spacing.S16 })}
>
<UserDetailsPanel activeUser={user} assignedRoles={assignedRoles} />
<UsernamePanel activeUser={user} canUpdateUser={canUpdateUser} />
<UserEmailPanel activeUser={user} canUpdateUser={canUpdateUser} />
<DeleteUserPanel activeUser={user} canDeleteUser={canDeleteUser} />
<UsernamePanel activeUser={user} canUpdateUser={isAccountAdmin} />
<UserEmailPanel activeUser={user} canUpdateUser={isAccountAdmin} />
<DeleteUserPanel activeUser={user} canDeleteUser={isAccountAdmin} />
</Stack>
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { UsernamePanel } from './UsernamePanel';
const queryMocks = vi.hoisted(() => ({
userPermissions: vi.fn(() => ({
data: {
update_user: false,
is_account_admin: false,
},
})),
}));
Expand All @@ -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(
Expand All @@ -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,
},
});

Expand All @@ -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,
},
});

Expand All @@ -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,
},
});

Expand Down
Loading
Loading