fix(auth): add node-admin token scope for panel updates (#6666)

* feat(auth): add node-admin API token scope

Keep ordinary node-sync credentials unable to self-update a remote panel while allowing explicitly elevated node administrators to inherit synchronization access and invoke updatePanel.

* style(docs): format API scope table

* fix(auth): preserve mTLS node-sync semantics

Allow only an explicit node-admin bearer to elevate a verified mTLS request. Existing admin, monitor, and node-sync bearer values continue to use the historical node-sync identity on mTLS connections.

* chore(ci): rerun flaky checks

---------

Co-authored-by: libmur-dev <333915961+libmur-dev@users.noreply.github.com>
This commit is contained in:
libmur-dev
2026-10-18 00:45:26 +03:00
committed by GitHub
parent a1f2589eb5
commit a8d0700bf2
42 changed files with 506 additions and 123 deletions
+46 -1
View File
@@ -1,4 +1,4 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import type { AllSetting } from '@/models/setting';
@@ -16,12 +16,16 @@ describe('API token creation date', () => {
name: 'seconds-token',
enabled: true,
createdAt: 1782485394,
scope: 'node-sync',
expiresAt: 0,
},
{
id: 3,
name: 'legacy-milliseconds-token',
enabled: true,
createdAt: 1782485394270,
scope: 'node-admin',
expiresAt: 0,
},
],
});
@@ -34,5 +38,46 @@ describe('API token creation date', () => {
expect(await screen.findByText('seconds-token')).toBeTruthy();
expect(screen.getByText('legacy-milliseconds-token')).toBeTruthy();
expect(screen.getAllByText(/2026/)).toHaveLength(2);
expect(screen.getByText('Node synchronization')).toBeTruthy();
expect(screen.getByText('Node administrator')).toBeTruthy();
});
it('creates a token with the selected scope', async () => {
vi.spyOn(HttpUtil, 'get').mockResolvedValue({ success: true, msg: '', obj: [] });
const post = vi.spyOn(HttpUtil, 'post').mockResolvedValue({
success: true,
msg: '',
obj: { token: 'created-token' },
});
render(
<SecurityTab allSetting={{} as AllSetting} updateSetting={vi.fn()} saveSetting={vi.fn()} />,
);
fireEvent.click(screen.getByRole('tab', { name: /API Token/ }));
fireEvent.click(await screen.findByRole('button', { name: /New token/ }));
fireEvent.change(screen.getByPlaceholderText('e.g. central-panel-a'), {
target: { value: 'central-node' },
});
const scope = screen.getByRole('combobox', { name: 'Scope' });
fireEvent.mouseDown(scope.closest('.ant-select') ?? scope);
const option = Array.from(document.querySelectorAll('.ant-select-item-option')).find(
(item) => item.textContent === 'Node administrator',
);
if (!option) throw new Error('Node administrator option not found');
fireEvent.click(option);
expect(
screen.getByText(
'Includes node synchronization permissions and can also start panel software updates.',
),
).toBeTruthy();
fireEvent.click(within(screen.getByRole('dialog')).getByRole('button', { name: 'Confirm' }));
await waitFor(() =>
expect(post).toHaveBeenCalledWith('/panel/api/setting/apiTokens/create', {
name: 'central-node',
scope: 'node-admin',
}),
);
});
});