Skip to content
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

Add support for API key provider #919

Merged
merged 25 commits into from
Oct 3, 2022
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
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
18 changes: 18 additions & 0 deletions lib/src/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -188,4 +188,22 @@ extension AppInternal on App {
AppHandle get handle => _handle;

static App create(AppHandle handle) => App._(handle);

static AppException createException(String message, String? linkToLogs, int statusCode) => AppException._(message, linkToLogs, statusCode);
}

/// An exception thrown from operations interacting with a Atlas App Services app.
class AppException extends RealmException {
/// A link to the server logs associated with this exception if available.
final String? linkToServerLogs;

/// The HTTP status code returned by the server for this exception.
final int statusCode;

AppException._(super.message, this.linkToServerLogs, this.statusCode);

@override
String toString() {
return "AppException: $message, link to server logs: $linkToServerLogs";
}
}
16 changes: 14 additions & 2 deletions lib/src/cli/atlas_apps/baas_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,8 @@ class BaasClient {
"runResetFunction": true
}''');

await enableProvider(app, 'api-key');

if (publicRSAKey.isNotEmpty) {
String publicRSAKeyEncoded = jsonEncode(publicRSAKey);
final dynamic createSecretResult = await _post('groups/$_groupId/apps/$appId/secrets', '{"name":"rsPublicKey","value":$publicRSAKeyEncoded}');
Expand Down Expand Up @@ -247,7 +249,7 @@ class BaasClient {
"authFunctionName": "authFunc",
"authFunctionId": "$authFuncId"
}''');

const facebookSecret = "876750ac6d06618b323dee591602897f";
final dynamic createFacebookSecretResult = await _post('groups/$_groupId/apps/$appId/secrets', '{"name":"facebookSecret","value":"$facebookSecret"}');
String facebookClientSecretKeyName = createFacebookSecretResult['name'] as String;
Expand Down Expand Up @@ -292,7 +294,7 @@ class BaasClient {
"name": "picture"
}''');
}

print('Creating database db_$name$_appSuffix');

await _createMongoDBService(app, '''{
Expand Down Expand Up @@ -353,6 +355,16 @@ class BaasClient {
}
}

Future<String> createApiKey(BaasApp app, String name, bool enabled) async {
final dynamic result = await _post('groups/$_groupId/apps/${app.appId}/api_keys', '{ "key":"", "name":"$name", "disabled": false }');
if (!enabled) {
// We need to disable the key with another API call - the argument in the create call is ignored.
nirinchev marked this conversation as resolved.
Show resolved Hide resolved
await _put('groups/$_groupId/apps/${app.appId}/api_keys/${result['_id']}/disable', '');
}

return result['key'] as String;
}

Future<void> _authenticate(String provider, String credentials) async {
dynamic response = await _post('auth/providers/$provider/login', credentials);

Expand Down
20 changes: 18 additions & 2 deletions lib/src/credentials.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import 'dart:ffi';

import 'native/realm_core.dart';
import 'app.dart';
import 'user.dart';

/// An enum containing all authentication providers. These have to be enabled manually for the application before they can be used.
/// [Authentication Providers Docs](https://docs.mongodb.com/realm/authentication/providers/)
Expand Down Expand Up @@ -50,8 +51,11 @@ enum AuthProviderType {
/// For authenticating with custom function with payload argument.
function,

_userApiKey,
_serverApiKey
/// For authenticating with user API key generated via [ApiKeyClient.create].
apiKey,

/// For authenticating with an API key generated via the server UI.
serverApiKey
}

/// A class, representing the credentials used for authenticating a [User]
Expand Down Expand Up @@ -106,6 +110,18 @@ class Credentials implements Finalizable {
Credentials.function(String payload)
: _handle = realmCore.createAppCredentialsFunction(payload),
provider = AuthProviderType.function;

/// Returns a [Credentials] object that can be used to authenticate a user with a user API key.
/// To generate an API key, use [ApiKeyClient.create].
Credentials.apiKey(String key)
: _handle = realmCore.createAppCredentialsApiKey(key),
provider = AuthProviderType.apiKey;

/// Returns a [Credentials] object that can be used to authenticate a user with an API key
/// generated via the Web UI.
Credentials.serverApiKey(String key)
: _handle = realmCore.createAppCredentialsServerApiKey(key),
provider = AuthProviderType.serverApiKey;
}

/// @nodoc
Expand Down
169 changes: 159 additions & 10 deletions lib/src/native/realm_core.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1126,6 +1126,20 @@ class _RealmCore {
});
}

RealmAppCredentialsHandle createAppCredentialsApiKey(String key) {
return using((arena) {
final keyPtr = key.toCharPtr(arena);
return RealmAppCredentialsHandle._(_realmLib.realm_app_credentials_new_user_api_key(keyPtr));
});
}

RealmAppCredentialsHandle createAppCredentialsServerApiKey(String key) {
return using((arena) {
final keyPtr = key.toCharPtr(arena);
return RealmAppCredentialsHandle._(_realmLib.realm_app_credentials_new_server_api_key(keyPtr));
});
}

RealmHttpTransportHandle _createHttpTransport(HttpClient httpClient) {
final requestCallback = Pointer.fromFunction<Void Function(Handle, realm_http_request, Pointer<Void>)>(_request_callback);
final requestCallbackUserdata = _realmLib.realm_dart_userdata_async_new(httpClient, requestCallback.cast(), scheduler.handle._pointer);
Expand All @@ -1142,7 +1156,7 @@ class _RealmCore {
// we explicitly call realm_http_transport_complete_request to
// mark request as completed later.
//
// Therefor we need to copy everything out of request before returning.
// Therefore we need to copy everything out of request before returning.
// We cannot clone request on the native side with realm_clone,
// since realm_http_request does not inherit from WrapC.

Expand All @@ -1152,7 +1166,7 @@ class _RealmCore {

final url = Uri.parse(request.url.cast<Utf8>().toRealmDartString()!);

final body = request.body.cast<Utf8>().toRealmDartString(length: request.body_size)!;
final body = request.body.cast<Utf8>().toRealmDartString(length: request.body_size);

final headers = <String, String>{};
for (int i = 0; i < request.num_headers; ++i) {
Expand All @@ -1170,7 +1184,7 @@ class _RealmCore {
HttpClient client,
int requestMethod,
Uri url,
String body,
String? body,
Map<String, String> headers,
Pointer<Void> request_context,
) async {
Expand Down Expand Up @@ -1206,7 +1220,9 @@ class _RealmCore {
request.headers.add(header.key, header.value);
}

request.add(utf8.encode(body));
if (body != null) {
request.add(utf8.encode(body));
}

// Do the call..
final response = await request.close();
Expand Down Expand Up @@ -1300,8 +1316,7 @@ class _RealmCore {
}

if (error != nullptr) {
final message = error.ref.message.cast<Utf8>().toRealmDartString()!;
completer.completeError(RealmException(message));
completer.completeWithAppError(error);
return;
}

Expand Down Expand Up @@ -1335,8 +1350,7 @@ class _RealmCore {
}

if (error != nullptr) {
final message = error.ref.message.cast<Utf8>().toRealmDartString()!;
completer.completeError(RealmException(message));
completer.completeWithAppError(error);
return;
}

Expand Down Expand Up @@ -1462,8 +1476,7 @@ class _RealmCore {
}

if (error != nullptr) {
final message = error.ref.message.cast<Utf8>().toRealmDartString()!;
completer.completeError(RealmException(message));
completer.completeWithAppError(error);
return;
}

Expand Down Expand Up @@ -1653,6 +1666,16 @@ class _RealmCore {
return UserProfile(profileData as Map<String, dynamic>);
}

String userGetRefreshToken(User user) {
final token = _realmLib.invokeGetPointer(() => _realmLib.realm_user_get_refresh_token(user.handle._pointer));
return token.cast<Utf8>().toRealmDartString(freeRealmMemory: true)!;
}

String userGetAccessToken(User user) {
final token = _realmLib.invokeGetPointer(() => _realmLib.realm_user_get_access_token(user.handle._pointer));
return token.cast<Utf8>().toRealmDartString(freeRealmMemory: true)!;
}

SessionHandle realmGetSession(Realm realm) {
return SessionHandle._(_realmLib.invokeGetPointer(() => _realmLib.realm_sync_session_get(realm.handle._pointer)));
}
Expand Down Expand Up @@ -1863,6 +1886,124 @@ class _RealmCore {
return null;
});
}

static void _app_api_key_completion_callback(Pointer<Void> userdata, Pointer<realm_app_user_apikey> apiKey, Pointer<realm_app_error> error) {
final Completer<ApiKey>? completer = userdata.toObject(isPersistent: true);
if (completer == null) {
return;
}

if (error != nullptr) {
completer.completeWithAppError(error);
return;
}

final id = apiKey.ref.id.toDart();
final name = apiKey.ref.name.cast<Utf8>().toDartString();
final value = apiKey.ref.key.cast<Utf8>().toRealmDartString(treatEmptyAsNull: true);
final isEnabled = !apiKey.ref.disabled;

completer.complete(UserInternal.createApiKey(id, name, value, isEnabled));
}

static void _app_api_key_array_completion_callback(Pointer<Void> userdata, Pointer<realm_app_user_apikey> apiKey, int size, Pointer<realm_app_error> error) {
final Completer<List<ApiKey>>? completer = userdata.toObject(isPersistent: true);
if (completer == null) {
return;
}

if (error != nullptr) {
completer.completeWithAppError(error);
return;
}

final result = <ApiKey>[];

for (var i = 0; i < size; i++) {
final id = apiKey[i].id.toDart();
final name = apiKey[i].name.cast<Utf8>().toDartString();
final value = apiKey[i].key.cast<Utf8>().toRealmDartString(treatEmptyAsNull: true);
final isEnabled = !apiKey[i].disabled;

result.add(UserInternal.createApiKey(id, name, value, isEnabled));
}

completer.complete(result);
}

Future<ApiKey> createApiKey(User user, String name) {
return using((Arena arena) {
final namePtr = name.toCharPtr(arena);
final completer = Completer<ApiKey>();
_realmLib.invokeGetBool(() => _realmLib.realm_app_user_apikey_provider_client_create_apikey(user.app.handle._pointer, user.handle._pointer, namePtr,
Pointer.fromFunction(_app_api_key_completion_callback), completer.toPersistentHandle(), _realmLib.addresses.realm_dart_delete_persistent_handle));

return completer.future;
});
}

Future<ApiKey> fetchApiKey(User user, ObjectId id) {
return using((Arena arena) {
final completer = Completer<ApiKey>();
final native_id = id.toNative(arena);
_realmLib.invokeGetBool(() => _realmLib.realm_app_user_apikey_provider_client_fetch_apikey(user.app.handle._pointer, user.handle._pointer, native_id.ref,
Pointer.fromFunction(_app_api_key_completion_callback), completer.toPersistentHandle(), _realmLib.addresses.realm_dart_delete_persistent_handle));

return completer.future;
});
}

Future<List<ApiKey>> fetchAllApiKeys(User user) {
return using((Arena arena) {
final completer = Completer<List<ApiKey>>();
_realmLib.invokeGetBool(() => _realmLib.realm_app_user_apikey_provider_client_fetch_apikeys(
user.app.handle._pointer,
user.handle._pointer,
Pointer.fromFunction(_app_api_key_array_completion_callback),
completer.toPersistentHandle(),
_realmLib.addresses.realm_dart_delete_persistent_handle));

return completer.future;
});
}

Future<void> deleteApiKey(User user, ObjectId id) {
return using((Arena arena) {
final completer = Completer<void>();
final native_id = id.toNative(arena);
_realmLib.invokeGetBool(() => _realmLib.realm_app_user_apikey_provider_client_delete_apikey(user.app.handle._pointer, user.handle._pointer, native_id.ref,
Pointer.fromFunction(void_completion_callback), completer.toPersistentHandle(), _realmLib.addresses.realm_dart_delete_persistent_handle));

return completer.future;
});
}

Future<void> disableApiKey(User user, ObjectId objectId) {
return using((Arena arena) {
final completer = Completer<void>();
final native_id = objectId.toNative(arena);
_realmLib.invokeGetBool(() => _realmLib.realm_app_user_apikey_provider_client_disable_apikey(
user.app.handle._pointer,
user.handle._pointer,
native_id.ref,
Pointer.fromFunction(void_completion_callback),
completer.toPersistentHandle(),
_realmLib.addresses.realm_dart_delete_persistent_handle));

return completer.future;
});
}

Future<void> enableApiKey(User user, ObjectId objectId) {
return using((Arena arena) {
final completer = Completer<void>();
final native_id = objectId.toNative(arena);
_realmLib.invokeGetBool(() => _realmLib.realm_app_user_apikey_provider_client_enable_apikey(user.app.handle._pointer, user.handle._pointer, native_id.ref,
Pointer.fromFunction(void_completion_callback), completer.toPersistentHandle(), _realmLib.addresses.realm_dart_delete_persistent_handle));

return completer.future;
});
}
}

class LastError {
Expand Down Expand Up @@ -2309,6 +2450,14 @@ extension on realm_property_info {
}
}

extension on Completer<Object?> {
void completeWithAppError(Pointer<realm_app_error> error) {
final message = error.ref.message.cast<Utf8>().toRealmDartString()!;
final linkToLogs = error.ref.link_to_server_logs.cast<Utf8>().toRealmDartString();
completeError(AppInternal.createException(message, linkToLogs, error.ref.http_status_code));
}
}

enum _CustomErrorCode {
noError(0),
unknownHttp(998),
Expand Down
4 changes: 2 additions & 2 deletions lib/src/realm_class.dart
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export 'package:realm_common/realm_common.dart'
Uuid;

// always expose with `show` to explicitly control the public API surface
export 'app.dart' show AppConfiguration, MetadataPersistenceMode, App;
export 'app.dart' show AppConfiguration, MetadataPersistenceMode, App, AppException;
export "configuration.dart"
show
Configuration,
Expand All @@ -80,7 +80,7 @@ export 'realm_object.dart' show RealmEntity, RealmException, UserCallbackExcepti
export 'realm_property.dart';
export 'results.dart' show RealmResults, RealmResultsChanges;
export 'subscription.dart' show Subscription, SubscriptionSet, SubscriptionSetState, MutableSubscriptionSet;
export 'user.dart' show User, UserState, UserIdentity;
export 'user.dart' show User, UserState, UserIdentity, ApiKeyClient, ApiKey;
export 'session.dart' show Session, SessionState, ConnectionState, ProgressDirection, ProgressMode, SyncProgress, ConnectionStateChange;

/// A [Realm] instance represents a `Realm` database.
Expand Down
Loading