Skip to content

Commit d1a7802

Browse files
srelonclaude
andcommitted
fix: forbid backendOnly on primary key AdminForth/1934/list-rows-lose-their-primary-k
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 26e2493 commit d1a7802

4 files changed

Lines changed: 63 additions & 0 deletions

File tree

‎adminforth/documentation/docs/tutorial/03-Customization/12-security.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,8 @@ So to completely hide the email field from all users apart superadmins, you shou
296296
297297
So if you will configure the email column in user resource like this, only superadmin will be able to see emails, and only in the list view.
298298
299+
> ☝️ `backendOnly` cannot be set on a primary key column (neither `true` nor a function): frontend needs primary key values to build record links, so AdminForth refuses such config on startup.
300+
299301
## Blocking login attempts before credentials check
300302
301303
`auth.beforeLoginAttempt` hooks are called on every request to the login endpoint, before AdminForth looks the user up in the database.

‎adminforth/modules/configValidator.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,12 @@ export default class ConfigValidator implements IConfigValidator {
527527
errors.push(`Resource "${res.resourceId}" has duplicate column name "${col.name}"`);
528528
}
529529

530+
// frontend builds record links from primary key values, so they must always reach it.
531+
// a backendOnly function is rejected too: it can hide the key only for some users
532+
if (col.primaryKey && col.backendOnly) {
533+
errors.push(`Resource "${res.resourceId}" column "${col.name}" is a primary key and cannot be backendOnly`);
534+
}
535+
530536
col.label = col.label || guessLabelFromName(col.name);
531537
//define default sortable
532538
if (!Object.keys(col).includes('sortable')) { col.sortable = !col.virtual; }

‎adminforth/types/Common.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1037,6 +1037,7 @@ export interface AdminForthResourceColumnInputCommon {
10371037

10381038
/**
10391039
* if true field will !not be passed to UI under no circumstances, but will be presented in hooks
1040+
* Not allowed on primary key columns.
10401041
*/
10411042
backendOnly?: boolean,
10421043

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { createRequire } from 'node:module';
2+
import path from 'node:path';
3+
import { fileURLToPath, pathToFileURL } from 'node:url';
4+
5+
const currentDir = path.dirname(fileURLToPath(import.meta.url));
6+
const applicationDir = path.resolve(currentDir, '../application');
7+
8+
// resolves to the BUILT package (adminforth/dist) the test application runs on;
9+
// rebuild it (`npx tsc` in adminforth/) after editing configValidator or the assertions below test stale code
10+
const require = createRequire(import.meta.url);
11+
const adminforthEntry = require.resolve('adminforth', { paths: [applicationDir] });
12+
const { default: AdminForth } = await import(pathToFileURL(adminforthEntry).href);
13+
14+
function appWithPkColumn(pkColumn: Record<string, unknown>) {
15+
// AdminForth allows one instance per process; each test needs a fresh one
16+
delete (globalThis as any).adminforth;
17+
return new AdminForth({
18+
baseUrl: '',
19+
auth: {
20+
usersResourceId: 'adminuser',
21+
usernameField: 'email',
22+
passwordHashField: 'password_hash',
23+
},
24+
dataSources: [{ id: 'sqlite', url: `sqlite://${path.join(applicationDir, '.db.sqlite')}` }],
25+
resources: [{
26+
resourceId: 'adminuser',
27+
table: 'adminuser',
28+
dataSource: 'sqlite',
29+
columns: [
30+
{ name: 'id', primaryKey: true, ...pkColumn },
31+
{ name: 'email', required: true },
32+
{ name: 'password_hash', backendOnly: true },
33+
],
34+
}],
35+
menu: [{ label: 'Users', resourceId: 'adminuser' }],
36+
});
37+
}
38+
39+
const pkBackendOnlyError = /column "id" is a primary key and cannot be backendOnly/;
40+
41+
describe('primary key column cannot be backendOnly', () => {
42+
it('rejects backendOnly: true on the primary key', () => {
43+
expect(() => appWithPkColumn({ backendOnly: true })).toThrow(pkBackendOnlyError);
44+
});
45+
46+
it('rejects backendOnly function on the primary key', () => {
47+
expect(() => appWithPkColumn({ backendOnly: () => false })).toThrow(pkBackendOnlyError);
48+
});
49+
50+
it('accepts backendOnly: false on the primary key and backendOnly on other columns', () => {
51+
expect(() => appWithPkColumn({ backendOnly: false })).not.toThrow();
52+
expect(() => appWithPkColumn({})).not.toThrow();
53+
});
54+
});

0 commit comments

Comments
 (0)