Skip to content

Commit 1c6fcfb

Browse files
committed
revert changes to api key service
1 parent a0b383b commit 1c6fcfb

File tree

2 files changed

+27
-171
lines changed

2 files changed

+27
-171
lines changed

api/src/unraid-api/auth/api-key.service.ts

Lines changed: 26 additions & 166 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,6 @@ export class ApiKeyService implements OnModuleInit {
2727
private readonly logger = new Logger(ApiKeyService.name);
2828
protected readonly basePath: string;
2929
protected memoryApiKeys: Array<ApiKeyWithSecret> = [];
30-
private persistentKeys = new Map<string, ApiKeyWithSecret>();
31-
private ephemeralKeys = new Map<string, ApiKeyWithSecret>();
3230
private static readonly validRoles: Set<Role> = new Set(Object.values(Role));
3331

3432
constructor() {
@@ -37,20 +35,10 @@ export class ApiKeyService implements OnModuleInit {
3735
}
3836

3937
async onModuleInit() {
40-
// Load persistent keys once at startup
41-
const diskKeys = await this.loadAllFromDisk();
42-
for (const key of diskKeys) {
43-
this.persistentKeys.set(key.id, key);
38+
this.memoryApiKeys = await this.loadAllFromDisk();
39+
if (environment.IS_MAIN_PROCESS) {
40+
this.setupWatch();
4441
}
45-
46-
// Clean up legacy internal keys
47-
await this.cleanupLegacyInternalKeys();
48-
49-
// Update memoryApiKeys for backwards compatibility
50-
this.updateMemoryApiKeys();
51-
52-
// NO file watching - manage in memory
53-
this.logger.log(`Loaded ${this.persistentKeys.size} persistent API keys`);
5442
}
5543

5644
public convertApiKeyWithSecretToApiKey(key: ApiKeyWithSecret): ApiKey {
@@ -59,43 +47,20 @@ export class ApiKeyService implements OnModuleInit {
5947
}
6048

6149
public async findAll(): Promise<ApiKey[]> {
62-
// Only return persistent keys, not ephemeral ones
63-
return Array.from(this.persistentKeys.values()).map((key) =>
64-
this.convertApiKeyWithSecretToApiKey(key)
50+
return Promise.all(
51+
this.memoryApiKeys.map(async (key) => {
52+
const keyWithoutSecret = this.convertApiKeyWithSecretToApiKey(key);
53+
return keyWithoutSecret;
54+
})
6555
);
6656
}
6757

68-
private updateMemoryApiKeys() {
69-
// Combine persistent and ephemeral keys for backwards compatibility
70-
this.memoryApiKeys = [
71-
...Array.from(this.persistentKeys.values()),
72-
...Array.from(this.ephemeralKeys.values()),
73-
];
74-
}
75-
76-
private async cleanupLegacyInternalKeys() {
77-
const legacyNames = ['CliInternal', 'ConnectInternal', 'CLI', 'Connect', 'CliAdmin', 'Internal'];
78-
const keysToDelete: string[] = [];
79-
80-
for (const [id, key] of this.persistentKeys) {
81-
if (legacyNames.includes(key.name)) {
82-
keysToDelete.push(id);
83-
this.logger.log(`Removing legacy internal key: ${key.name}`);
84-
}
85-
}
86-
87-
if (keysToDelete.length > 0) {
88-
// Remove from disk
89-
for (const id of keysToDelete) {
90-
try {
91-
await unlink(join(this.basePath, `${id}.json`));
92-
} catch (error) {
93-
// File might not exist, that's ok
94-
}
95-
this.persistentKeys.delete(id);
96-
}
97-
this.logger.log(`Cleaned up ${keysToDelete.length} legacy internal keys`);
98-
}
58+
private setupWatch() {
59+
watch(this.basePath, { ignoreInitial: false }).on('all', async (path) => {
60+
this.logger.debug(`API key changed: ${path}`);
61+
this.memoryApiKeys = [];
62+
this.memoryApiKeys = await this.loadAllFromDisk();
63+
});
9964
}
10065

10166
private sanitizeName(name: string): string {
@@ -189,10 +154,6 @@ export class ApiKeyService implements OnModuleInit {
189154

190155
await this.saveApiKey(apiKey as ApiKeyWithSecret);
191156

192-
// Update persistent keys in memory
193-
this.persistentKeys.set(apiKey.id as string, apiKey as ApiKeyWithSecret);
194-
this.updateMemoryApiKeys();
195-
196157
return apiKey as ApiKeyWithSecret;
197158
}
198159

@@ -277,97 +238,17 @@ export class ApiKeyService implements OnModuleInit {
277238
public findByField(field: keyof ApiKeyWithSecret, value: string): ApiKeyWithSecret | null {
278239
if (!value) return null;
279240

280-
// Check ephemeral keys first
281-
for (const keyData of this.ephemeralKeys.values()) {
282-
if (keyData[field] === value) {
283-
return keyData;
284-
}
285-
}
286-
287-
// Then check persistent keys
288-
for (const keyData of this.persistentKeys.values()) {
289-
if (keyData[field] === value) {
290-
return keyData;
291-
}
292-
}
293-
294-
return null;
241+
return this.memoryApiKeys.find((k) => k[field] === value) ?? null;
295242
}
296243

297244
findByKey(key: string): ApiKeyWithSecret | null {
298-
if (!key) return null;
299-
300-
// Check ephemeral keys first (faster, in-memory)
301-
for (const keyData of this.ephemeralKeys.values()) {
302-
if (keyData.key === key) {
303-
return keyData;
304-
}
305-
}
306-
307-
// Then check persistent keys
308-
for (const keyData of this.persistentKeys.values()) {
309-
if (keyData.key === key) {
310-
return keyData;
311-
}
312-
}
313-
314-
return null;
245+
return this.findByField('key', key);
315246
}
316247

317248
private generateApiKey(): string {
318249
return crypto.randomBytes(32).toString('hex');
319250
}
320251

321-
/**
322-
* Register an in-process module with an ephemeral token.
323-
* Used by Connect and other modules running in the same process.
324-
*/
325-
public async registerModule(moduleId: string, roles: Role[]): Promise<string> {
326-
// Generate 256-bit cryptographically secure token
327-
const token = crypto.randomBytes(32).toString('base64url');
328-
329-
const keyData: ApiKeyWithSecret = {
330-
id: `module-${moduleId}`,
331-
key: token,
332-
name: `Module-${moduleId}`,
333-
description: `Ephemeral token for ${moduleId} module`,
334-
roles,
335-
permissions: [],
336-
createdAt: new Date().toISOString(),
337-
};
338-
339-
this.ephemeralKeys.set(keyData.id, keyData);
340-
this.updateMemoryApiKeys();
341-
this.logger.debug(`Registered module ${moduleId} with ephemeral token`);
342-
343-
return token;
344-
}
345-
346-
/**
347-
* Register an ephemeral key for external processes like CLI.
348-
* The key is provided by the caller and registered in memory.
349-
*/
350-
public async registerEphemeralKey(config: {
351-
key: string;
352-
name: string;
353-
roles: Role[];
354-
type: string;
355-
}): Promise<void> {
356-
const keyData: ApiKeyWithSecret = {
357-
id: `ephemeral-${config.type}`,
358-
key: config.key,
359-
name: config.name,
360-
description: `Ephemeral key for ${config.type}`,
361-
roles: config.roles,
362-
permissions: [],
363-
createdAt: new Date().toISOString(),
364-
};
365-
366-
this.ephemeralKeys.set(keyData.id, keyData);
367-
this.updateMemoryApiKeys();
368-
this.logger.debug(`Registered ephemeral key for ${config.type}`);
369-
}
370-
371252
private logApiKeyValidationError(file: string, error: ValidationError): void {
372253
this.logger.error(`Invalid API key structure in file ${file}.
373254
Errors: ${JSON.stringify(error.constraints, null, 2)}`);
@@ -428,30 +309,17 @@ export class ApiKeyService implements OnModuleInit {
428309
throw new Error(`API keys not found: ${missingKeys.join(', ')}`);
429310
}
430311

431-
// Separate persistent and ephemeral keys
432-
const persistentIds = ids.filter((id) => this.persistentKeys.has(id));
433-
const ephemeralIds = ids.filter((id) => this.ephemeralKeys.has(id));
434-
435-
// Delete persistent keys from disk
436-
if (persistentIds.length > 0) {
437-
const { errors } = await batchProcess(persistentIds, async (id) => {
438-
await unlink(join(this.basePath, `${id}.json`));
439-
this.persistentKeys.delete(id);
440-
return id;
441-
});
442-
443-
if (errors.length > 0) {
444-
throw errors;
445-
}
446-
}
312+
// Delete all files in parallel
313+
const { errors, data: deletedIds } = await batchProcess(ids, async (id) => {
314+
await unlink(join(this.basePath, `${id}.json`));
315+
return id;
316+
});
447317

448-
// Remove ephemeral keys from memory
449-
for (const id of ephemeralIds) {
450-
this.ephemeralKeys.delete(id);
318+
const deletedSet = new Set(deletedIds);
319+
this.memoryApiKeys = this.memoryApiKeys.filter((key) => !deletedSet.has(key.id));
320+
if (errors.length > 0) {
321+
throw errors;
451322
}
452-
453-
// Update memory cache
454-
this.updateMemoryApiKeys();
455323
}
456324

457325
async update({
@@ -486,15 +354,7 @@ export class ApiKeyService implements OnModuleInit {
486354
if (permissions) {
487355
apiKey.permissions = permissions;
488356
}
489-
490-
// Only save to disk if it's a persistent key
491-
if (this.persistentKeys.has(id)) {
492-
await this.saveApiKey(apiKey);
493-
this.persistentKeys.set(id, apiKey);
494-
}
495-
496-
// Update memory cache
497-
this.updateMemoryApiKeys();
357+
await this.saveApiKey(apiKey);
498358
return apiKey;
499359
}
500360

api/src/unraid-api/cli/cli.module.spec.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
import { ConfigModule } from '@nestjs/config';
22
import { Test, TestingModule } from '@nestjs/testing';
33

4-
import {
5-
CANONICAL_INTERNAL_CLIENT_TOKEN,
6-
CANONICAL_INTERNAL_CLIENT_TOKEN,
7-
INTERNAL_CLIENT_SERVICE_TOKEN,
8-
} from '@unraid/shared';
4+
import { CANONICAL_INTERNAL_CLIENT_TOKEN, INTERNAL_CLIENT_SERVICE_TOKEN } from '@unraid/shared';
95
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
106

117
import { CliServicesModule } from '@app/unraid-api/cli/cli-services.module.js';

0 commit comments

Comments
 (0)