Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions redisinsight/api/src/constants/error-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export default {
'Key with this name does not exist or does not have an associated timeout.',
SERVER_NOT_AVAILABLE: 'Server is not available. Please try again later.',
REDIS_CLOUD_FORBIDDEN: 'Error fetching account details.',
NO_INFO_COMMAND_PERMISSION: 'has no permissions to run the \'info\' command',

DATABASE_IS_INACTIVE: 'The database is inactive.',
DATABASE_ALREADY_EXISTS: 'The database already exists.',
Expand Down
45 changes: 45 additions & 0 deletions redisinsight/api/src/modules/database/dto/redis-info.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,48 @@ export class RedisDatabaseModuleDto {
})
ver?: number;
}

export class RedisDatabaseHelloResponse {
@ApiProperty({
description: 'Redis database id',
type: Number,
})
id: number;

@ApiProperty({
description: 'Redis database server name',
type: String,
})
server: string;

@ApiProperty({
description: 'Redis database version',
type: String,
})
version: string;

@ApiProperty({
description: 'Redis database proto',
type: Number,
})
proto: number;

@ApiProperty({
description: 'Redis database mode',
type: String,
})
mode: "standalone" | "sentinel" | "cluster";

@ApiProperty({
description: 'Redis database role',
type: String,
})
role: 'master' | 'slave';

@ApiProperty({
description: 'Redis database modules',
type: RedisDatabaseModuleDto,
isArray: true,
})
modules: RedisDatabaseModuleDto[]
}
Original file line number Diff line number Diff line change
Expand Up @@ -379,13 +379,40 @@ describe('DatabaseInfoProvider', () => {
nodes: [mockRedisGeneralInfo, mockRedisGeneralInfo],
});
});
it('should throw an error if no permission to run \'info\' command', async () => {
it('should get info from hello command when info command is not available', async () => {
when(standaloneClient.sendCommand)
.calledWith(['info'], { replyEncoding: 'utf8' })
.mockRejectedValue({
message: 'NOPERM this user has no permissions to run the \'info\' command',
});

when(standaloneClient.sendCommand)
.calledWith(['hello'], { replyEncoding: 'utf8' })
.mockResolvedValue([
'version', mockRedisGeneralInfo.version,
'server', mockRedisServerInfoDto,
]);

const result = await service.getRedisGeneralInfo(standaloneClient);

expect(result).toEqual({
version: mockRedisGeneralInfo.version,
server: mockRedisServerInfoDto,
});
});
it('should throw an error if no permission to run \'info\' and \'hello\' commands', async () => {
when(standaloneClient.sendCommand)
.calledWith(['info'], { replyEncoding: 'utf8' })
.mockRejectedValue({
message: 'NOPERM this user has no permissions to run the \'info\' command',
});

when(standaloneClient.sendCommand)
.calledWith(['hello'], { replyEncoding: 'utf8' })
.mockRejectedValue({
message: 'NOPERM this user has no permissions to run the \'hello\' command',
});

try {
await service.getRedisGeneralInfo(standaloneClient);
fail('Should throw an error');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,21 @@ import { Injectable } from '@nestjs/common';
import {
calculateRedisHitRatio,
catchAclError,
convertArrayOfKeyValuePairsToObject,
convertIntToSemanticVersion,
convertRedisInfoReplyToObject,
} from 'src/utils';
import { AdditionalRedisModule } from 'src/modules/database/models/additional.redis.module';
import { REDIS_MODULES_COMMANDS, SUPPORTED_REDIS_MODULES } from 'src/constants';
import { get, isNil } from 'lodash';
import { RedisDatabaseInfoResponse } from 'src/modules/database/dto/redis-info.dto';
import { RedisDatabaseHelloResponse, RedisDatabaseInfoResponse } from 'src/modules/database/dto/redis-info.dto';
import { FeatureService } from 'src/modules/feature/feature.service';
import { KnownFeatures } from 'src/modules/feature/constants';
import { convertArrayReplyToObject, convertMultilineReplyToObject } from 'src/modules/redis/utils';
import { RedisClient, RedisClientConnectionType } from 'src/modules/redis/client';
import ERROR_MESSAGES from 'src/constants/error-messages';
import { SessionMetadata } from 'src/common/models';
import { plainToClass } from 'class-transformer';

@Injectable()
export class DatabaseInfoProvider {
Expand Down Expand Up @@ -161,6 +164,16 @@ export class DatabaseInfoProvider {
server: serverInfo,
};
} catch (error) {
if (error.message.includes(ERROR_MESSAGES.NO_INFO_COMMAND_PERMISSION)) {
// Fallback to hello command
const { version, server } = await this.getRedisHelloInfo(client);

return {
version,
server,
};
}

throw catchAclError(error);
}
}
Expand Down Expand Up @@ -261,4 +274,17 @@ export class DatabaseInfoProvider {
throw catchAclError(e);
}
}

private async getRedisHelloInfo(client: RedisClient): Promise<RedisDatabaseHelloResponse> {
try {
const helloResponse = convertArrayOfKeyValuePairsToObject(await client.sendCommand(
['hello'],
{ replyEncoding: 'utf8' },
) as any[]);

return plainToClass(RedisDatabaseHelloResponse, helloResponse)
} catch (e) {
throw catchAclError(e);
}
}
}
18 changes: 18 additions & 0 deletions redisinsight/api/src/utils/converter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,21 @@ export const convertStringToNumber = (value: any, defaultValue?: number): number

return num;
};

export const convertArrayOfKeyValuePairsToObject = (
array: any[],
): Record<string, any> =>
array.reduce(
(memo, current, index, array) =>
index % 2 === 1
? memo
: {
...memo,
[current]: Array.isArray(array[index + 1]?.[0])
? array[index + 1].map((element) =>
convertArrayOfKeyValuePairsToObject(element),
)
: array[index + 1],
},
{},
);
Loading