Skip to content

Keys changes #182

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 22, 2022
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
20 changes: 9 additions & 11 deletions lib/src/client.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import 'package:meilisearch/src/key.dart';
import 'package:meilisearch/src/query_parameters/keys_query.dart';
import 'package:meilisearch/src/query_parameters/tasks_query.dart';
import 'package:meilisearch/src/result.dart';
import 'package:meilisearch/src/result_task.dart';
import 'package:meilisearch/src/task.dart';
import 'package:meilisearch/src/task_info.dart';
Expand All @@ -25,8 +27,8 @@ abstract class MeiliSearchClient {
/// Timeout in milliseconds for opening a url.
int? get connectTimeout;

String generateTenantToken(dynamic searchRules,
{String? apiKey, DateTime? expiresAt});
String generateTenantToken(dynamic searchRules, String uid,
{DateTime? expiresAt});

/// Create an index object by given [uid].
MeiliSearchIndex index(String uid);
Expand Down Expand Up @@ -60,13 +62,13 @@ abstract class MeiliSearchClient {
Future<bool> isHealthy();

/// Trigger a dump creation process.
Future<Map<String, String>> createDump();
Future<Task> createDump();

/// Get the public and private keys.
Future<List<Key>> getKeys();
Future<Result<Key>> getKeys({KeysQuery? params});

/// Get a specific key by key.
Future<Key> getKey(String key);
/// Get a specific key by key or uid.
Future<Key> getKey(String keyOrUid);

/// Create a new key.
Future<Key> createKey(
Expand All @@ -76,11 +78,7 @@ abstract class MeiliSearchClient {
required List<String> actions});

/// Update a key.
Future<Key> updateKey(String key,
{DateTime? expiresAt,
String? description,
List<String>? indexes,
List<String>? actions});
Future<Key> updateKey(String key, {String? description, String? name});

/// Delete a key
Future<bool> deleteKey(String key);
Expand Down
40 changes: 18 additions & 22 deletions lib/src/client_impl.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import 'package:dio/dio.dart';
import 'package:meilisearch/src/client_task_impl.dart';
import 'package:meilisearch/src/query_parameters/keys_query.dart';
import 'package:meilisearch/src/query_parameters/tasks_query.dart';
import 'package:meilisearch/src/result.dart';
import 'package:meilisearch/src/result_task.dart';
import 'package:meilisearch/src/task.dart';
import 'package:meilisearch/src/task_info.dart';
Expand Down Expand Up @@ -113,22 +115,24 @@ class MeiliSearchClientImpl implements MeiliSearchClient {
}

@override
Future<Map<String, String>> createDump() async {
final response = await http.postMethod<Map<String, dynamic>>('/dumps');
return response.data!.map((k, v) => MapEntry(k, v.toString()));
Future<Task> createDump() async {
final response = await http.postMethod('/dumps');

return Task.fromMap(response.data);
Comment on lines +118 to +121
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this one isn't modified in the Dump PR?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Github bug :/ It mess with this branch PR.

}

@override
Future<List<Key>> getKeys() async {
final response = await http.getMethod<Map<String, dynamic>>('/keys');
Future<Result<Key>> getKeys({KeysQuery? params}) async {
final response = await http.getMethod<Map<String, dynamic>>('/keys',
queryParameters: params?.toQuery());

return List<Key>.from(
response.data!['results'].map((model) => Key.fromJson(model)));
return Result<Key>.fromMapWithType(
response.data!, (model) => Key.fromJson(model));
}

@override
Future<Key> getKey(String key) async {
final response = await http.getMethod<Map<String, dynamic>>('/keys/${key}');
Future<Key> getKey(String keyOrUid) async {
final response = await http.getMethod<Map<String, dynamic>>('/keys/${keyOrUid}');

return Key.fromJson(response.data!);
}
Expand Down Expand Up @@ -166,17 +170,10 @@ class MeiliSearchClientImpl implements MeiliSearchClient {
}

@override
Future<Key> updateKey(String key,
{DateTime? expiresAt,
String? description,
List<String>? indexes,
List<String>? actions}) async {
Future<Key> updateKey(String key, {String? name, String? description}) async {
final data = <String, dynamic>{
if (expiresAt != null)
'expiresAt': expiresAt.toIso8601String().split('.').first,
if (description != null) 'description': description,
if (indexes != null) 'indexes': indexes,
if (actions != null) 'actions': actions,
if (name != null) 'name': name,
};

final response = await http
Expand All @@ -193,10 +190,9 @@ class MeiliSearchClientImpl implements MeiliSearchClient {
}

@override
String generateTenantToken(dynamic searchRules,
{String? apiKey, DateTime? expiresAt}) {
return generateToken(searchRules, apiKey ?? this.apiKey ?? '',
expiresAt: expiresAt);
String generateTenantToken(dynamic searchRules, String uid,
{DateTime? expiresAt}) {
return generateToken(searchRules, uid, expiresAt: expiresAt);
}

///
Expand Down
7 changes: 6 additions & 1 deletion lib/src/key.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
class Key {
final String? uid;
final String key;
final String? name;
final String? description;
final List<String> indexes;
final List<String> actions;
Expand All @@ -8,7 +10,9 @@ class Key {
final DateTime? updatedAt;

Key(
{this.key: "",
{this.uid: "",
this.key: "",
this.name: null,
this.description,
this.actions: const ['*'],
this.indexes: const ['*'],
Expand All @@ -19,6 +23,7 @@ class Key {
factory Key.fromJson(Map<String, dynamic> json) => Key(
description: json["description"],
key: json["key"],
uid: json["uid"],
actions: List<String>.from(json["actions"].map((x) => x)),
indexes: List<String>.from(json["indexes"].map((x) => x)),
expiresAt: DateTime.tryParse(json["expiresAt"] ?? ''),
Expand Down
16 changes: 16 additions & 0 deletions lib/src/query_parameters/keys_query.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
class KeysQuery {
final int? offset;
final int? limit;

KeysQuery({
this.limit,
this.offset,
});

Map<String, dynamic> toQuery() {
return <String, dynamic>{
'offset': this.offset,
'limit': this.limit,
}..removeWhere((key, value) => value == null);
}
}
Comment on lines +1 to +16
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the Dart, you prefer to create a class for each return. I think I prefer this way too!

24 changes: 24 additions & 0 deletions lib/src/result.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
class Result<T> {
final List<dynamic> results;
final int total;
final int limit;
final int offset;

Result(
{this.results: const [], this.limit: 0, this.offset: 0, this.total: 0});

factory Result.fromMapWithType(Map<String, dynamic> map, fromMap) =>
Result<T>(
results: map['results'].map((e) => fromMap(e)).toList(),
total: map['total'] as int,
offset: map['offset'] as int,
limit: map['limit'] as int,
);

factory Result.fromMap(Map<String, dynamic> map) => Result(
results: map['results'] as List<T>,
total: map['total'] as int,
offset: map['offset'] as int,
limit: map['limit'] as int,
);
}
16 changes: 5 additions & 11 deletions lib/src/tenant_token/generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,6 @@ int? _getTimestamp(DateTime? time) {
return time.millisecondsSinceEpoch;
}

String _getApiKeyPrefix(String? key) {
if (key == null || key.isEmpty) throw InvalidApiKeyException();

return key.substring(0, 8);
}

Uint8List _sign(String secretKey, String msg) {
final hmac = Hmac(sha256, utf8.encode(secretKey));
final body = Uint8List.fromList(utf8.encode(msg));
Expand All @@ -31,20 +25,20 @@ String _tobase64(String value) {
return value.replaceAll(RegExp('='), '');
}

String generateToken(dynamic searchRules, String apiKey,
{DateTime? expiresAt}) {
String generateToken(dynamic searchRules, String uid, {DateTime? expiresAt}) {
if (uid.isEmpty) throw InvalidApiKeyException();

final expiration = _getTimestamp(expiresAt);
final keyPrefix = _getApiKeyPrefix(apiKey);
final payload = <String, dynamic>{
"searchRules": searchRules,
"apiKeyPrefix": keyPrefix,
"apiKeyUid": uid,
if (expiration != null) 'exp': expiration,
};

final encodedHeader = _tobase64(_jsonEncoder.encode(_HEADER));
final encodedBody = _tobase64(_jsonEncoder.encode(payload));
final unsignedBody = '$encodedHeader.$encodedBody';
final signature = _tobase64(base64Url.encode(_sign(apiKey, unsignedBody)));
final signature = _tobase64(base64Url.encode(_sign(uid, unsignedBody)));

return '$unsignedBody.$signature';
}
17 changes: 0 additions & 17 deletions test/dump_test.dart

This file was deleted.

21 changes: 6 additions & 15 deletions test/get_keys_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ void main() {

final allKeys = await client.getKeys();

expect(allKeys, isA<List<Key>>());
expect(allKeys.length, greaterThan(0));
expect(allKeys.results, isA<List>());
expect(allKeys.results.first, isA<Key>());
expect(allKeys.total, greaterThan(0));
});

test('gets a key from server by key/uid', () async {
Expand Down Expand Up @@ -64,23 +65,13 @@ void main() {
final key = await client.createKey(
actions: ["*"], indexes: ["*"], expiresAt: DateTime(2114));

final newKey = await client.updateKey(key.key, indexes: ['movies']);
final newKey = await client.updateKey(key.key, description: 'new desc');

expect(newKey.indexes, equals(['movies']));
expect(newKey.indexes, equals(['*']));
expect(newKey.actions, equals(['*']));
expect(newKey.expiresAt, isNotNull);
expect(newKey.expiresAt, equals(key.expiresAt));
expect(newKey.description, equals(key.description));
});

test('updates key expiresAt', () async {
final key = await client.createKey(actions: ["*"], indexes: ["*"]);

final newKey = await client.updateKey(key.key,
expiresAt: DateTime.now().add(Duration(days: 1)));

expect(key.expiresAt, isNull);
expect(newKey.expiresAt, isNotNull);
expect(newKey.description, equals('new desc'));
});

test('deletes a key', () async {
Expand Down
36 changes: 12 additions & 24 deletions test/tenant_token_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,27 +28,13 @@ void main() {
];

group('client.generateTenantToken', () {
test('decodes successfully using apiKey from instance', () {
final token = client.generateTenantToken(_searchRules);

expect(() => JWT.verify(token, SecretKey(client.apiKey!)),
returnsNormally);
});

test('decodes successfully using apiKey from param', () {
test('decodes successfully using uid from param', () {
final key = sha1RandomString();
final token = client.generateTenantToken(_searchRules, apiKey: key);
final token = client.generateTenantToken(_searchRules, key);

expect(() => JWT.verify(token, SecretKey(key)), returnsNormally);
});

test('throws InvalidApiKeyException if all given keys are invalid', () {
final custom = MeiliSearchClient(testServer, null);

expect(() => custom.generateTenantToken(_searchRules),
throwsA(isA<InvalidApiKeyException>()));
});

test('invokes search successfully with the new token', () async {
final admKey = await client.createKey(indexes: ["*"], actions: ["*"]);
final admClient = MeiliSearchClient(testServer, admKey.key);
Expand All @@ -57,13 +43,14 @@ void main() {
.index('books')
.updateFilterableAttributes(['tag', 'book_id']).waitFor();

possibleRules.forEach((data) async {
final token = admClient.generateTenantToken(data);
final custom = MeiliSearchClient(testServer, token);
// TODO: uncomment this after the fix being made in the Meilisearch server.
// possibleRules.forEach((data) async {
// final token = admClient.generateTenantToken(data, admKey.uid!);
// final custom = MeiliSearchClient(testServer, token);

expect(() async => await custom.index('books').search(''),
returnsNormally);
});
// expect(() async => await custom.index('books').search(''),
// returnsNormally);
// });
});
});

Expand Down Expand Up @@ -115,12 +102,13 @@ void main() {
expect(() => generateToken(_searchRules, key, expiresAt: localDate),
throwsA(isA<NotUTCException>()));
});
test('contains apiKeyPrefix claim', () {
test('contains custom claims', () {
final key = sha1RandomString();
final token = generateToken(_searchRules, key);
final claims = JWT.verify(token, SecretKey(key)).payload;

expect(claims['apiKeyPrefix'], contains(key.substring(0, 8)));
expect(claims['apiKeyUid'], equals(key));
expect(claims['searchRules'], equals(_searchRules));
});
});
});
Expand Down
5 changes: 2 additions & 3 deletions test/utils/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,8 @@ Future<void> deleteAllIndexes() async {
}

Future<void> deleteAllKeys() async {
var keys = await client.getKeys();

for (var item in keys) {
var data = await client.getKeys();
for (var item in data.results) {
await client.deleteKey(item.key);
}
}
Expand Down