1 ////////////////////////////////////////////////////////////////////////////
3 // Copyright 2017 Realm Inc.
5 // Licensed under the Apache License, Version 2.0 (the "License");
6 // you may not use this file except in compliance with the License.
7 // You may obtain a copy of the License at
9 // http://www.apache.org/licenses/LICENSE-2.0
11 // Unless required by applicable law or agreed to in writing, software
12 // distributed under the License is distributed on an "AS IS" BASIS,
13 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 // See the License for the specific language governing permissions and
15 // limitations under the License.
17 ////////////////////////////////////////////////////////////////////////////
19 #include "sync/sync_permission.hpp"
21 #include "impl/notification_wrapper.hpp"
22 #include "impl/object_accessor_impl.hpp"
23 #include "object_schema.hpp"
24 #include "property.hpp"
26 #include "sync/sync_config.hpp"
27 #include "sync/sync_manager.hpp"
28 #include "sync/sync_session.hpp"
29 #include "sync/sync_user.hpp"
30 #include "util/event_loop_signal.hpp"
31 #include "util/uuid.hpp"
33 #include <realm/query_expression.hpp>
35 using namespace realm;
36 using namespace std::chrono;
42 // Make a handler that extracts either an exception pointer, or the string value
43 // of the property with the specified name.
44 Permissions::AsyncOperationHandler make_handler_extracting_property(std::string property,
45 Permissions::PermissionOfferCallback callback)
47 return [property=std::move(property),
48 callback=std::move(callback)](Object* object, std::exception_ptr exception) {
50 callback(none, exception);
53 auto token = any_cast<std::string>(object->get_property_value<util::Any>(context, property));
54 callback(util::make_optional<std::string>(std::move(token)), nullptr);
59 AccessLevel extract_access_level(Object& permission, CppContext& context)
61 auto may_manage = permission.get_property_value<util::Any>(context, "mayManage");
62 if (may_manage.has_value() && any_cast<bool>(may_manage))
63 return AccessLevel::Admin;
65 auto may_write = permission.get_property_value<util::Any>(context, "mayWrite");
66 if (may_write.has_value() && any_cast<bool>(may_write))
67 return AccessLevel::Write;
69 auto may_read = permission.get_property_value<util::Any>(context, "mayRead");
70 if (may_read.has_value() && any_cast<bool>(may_read))
71 return AccessLevel::Read;
73 return AccessLevel::None;
76 /// Turn a system time point value into the 64-bit integer representing ns since the Unix epoch.
77 int64_t ns_since_unix_epoch(const system_clock::time_point& point)
80 unix_epoch.tm_year = 70;
81 time_t epoch_time = mktime(&unix_epoch);
82 auto epoch_point = system_clock::from_time_t(epoch_time);
83 return duration_cast<nanoseconds>(point - epoch_point).count();
86 } // anonymous namespace
90 Permission::Permission(Object& permission)
93 path = any_cast<std::string>(permission.get_property_value<util::Any>(context, "path"));
94 access = extract_access_level(permission, context);
95 condition = Condition(any_cast<std::string>(permission.get_property_value<util::Any>(context, "userId")));
96 updated_at = any_cast<Timestamp>(permission.get_property_value<util::Any>(context, "updatedAt"));
99 Permission::Permission(std::string path, AccessLevel access, Condition condition, Timestamp updated_at)
100 : path(std::move(path))
102 , condition(std::move(condition))
103 , updated_at(std::move(updated_at))
106 std::string Permission::description_for_access_level(AccessLevel level)
109 case AccessLevel::None: return "none";
110 case AccessLevel::Read: return "read";
111 case AccessLevel::Write: return "write";
112 case AccessLevel::Admin: return "admin";
117 bool Permission::paths_are_equivalent(std::string path_1, std::string path_2,
118 const std::string& user_id_1, const std::string& user_id_2)
120 REALM_ASSERT_DEBUG(path_1.length() > 0);
121 REALM_ASSERT_DEBUG(path_2.length() > 0);
122 if (path_1 == path_2) {
123 // If both paths are identical and contain `/~/`, the user IDs must match.
124 return (path_1.find("/~/") == std::string::npos) || (user_id_1 == user_id_2);
126 // Make substitutions for the first `/~/` in the string.
127 size_t index = path_1.find("/~/");
128 if (index != std::string::npos)
129 path_1.replace(index + 1, 1, user_id_1);
131 index = path_2.find("/~/");
132 if (index != std::string::npos)
133 path_2.replace(index + 1, 1, user_id_2);
135 return path_1 == path_2;
138 // MARK: - Permissions
140 void Permissions::get_permissions(std::shared_ptr<SyncUser> user,
141 PermissionResultsCallback callback,
142 const ConfigMaker& make_config)
144 auto realm = Permissions::permission_realm(user, make_config);
145 auto table = ObjectStore::table_for_object_type(realm->read_group(), "Permission");
146 auto results = std::make_shared<_impl::NotificationWrapper<Results>>(std::move(realm), *table);
148 // `get_permissions` works by temporarily adding an async notifier to the permission Realm.
149 // This notifier will run the `async` callback until the Realm contains permissions or
150 // an error happens. When either of these two things happen, the notifier will be
151 // unregistered by nulling out the `results_wrapper` container.
152 auto async = [results, callback=std::move(callback)](CollectionChangeSet, std::exception_ptr ex) mutable {
154 callback(Results(), ex);
158 if (results->size() > 0) {
159 // We monitor the raw results. The presence of a `__management` Realm indicates
160 // that the permissions have been downloaded (hence, we wait until size > 0).
161 TableRef table = ObjectStore::table_for_object_type(results->get_realm()->read_group(), "Permission");
162 size_t col_idx = table->get_descriptor()->get_column_index("path");
163 auto query = !(table->column<StringData>(col_idx).ends_with("/__permission")
164 || table->column<StringData>(col_idx).ends_with("/__perm")
165 || table->column<StringData>(col_idx).ends_with("/__management"));
166 // Call the callback with our new permissions object. This object will exclude the
168 callback(results->filter(std::move(query)), nullptr);
172 results->add_notification_callback(std::move(async));
175 void Permissions::set_permission(std::shared_ptr<SyncUser> user,
176 Permission permission,
177 PermissionChangeCallback callback,
178 const ConfigMaker& make_config)
180 auto props = AnyDict{
181 {"userId", permission.condition.user_id},
182 {"realmUrl", user->server_url() + permission.path},
183 {"mayRead", permission.access != AccessLevel::None},
184 {"mayWrite", permission.access == AccessLevel::Write || permission.access == AccessLevel::Admin},
185 {"mayManage", permission.access == AccessLevel::Admin},
187 if (permission.condition.type == Permission::Condition::Type::KeyValue) {
188 props.insert({"metadataKey", permission.condition.key_value.first});
189 props.insert({"metadataValue", permission.condition.key_value.second});
191 auto cb = [callback=std::move(callback)](Object*, std::exception_ptr exception) {
194 perform_async_operation("PermissionChange", std::move(user), std::move(cb), std::move(props), make_config);
197 void Permissions::delete_permission(std::shared_ptr<SyncUser> user,
198 Permission permission,
199 PermissionChangeCallback callback,
200 const ConfigMaker& make_config)
202 permission.access = AccessLevel::None;
203 set_permission(std::move(user), std::move(permission), std::move(callback), make_config);
206 void Permissions::make_offer(std::shared_ptr<SyncUser> user,
207 PermissionOffer offer,
208 PermissionOfferCallback callback,
209 const ConfigMaker& make_config)
211 auto props = AnyDict{
212 {"expiresAt", std::move(offer.expiration)},
213 {"userId", user->identity()},
214 {"realmUrl", user->server_url() + offer.path},
215 {"mayRead", offer.access != AccessLevel::None},
216 {"mayWrite", offer.access == AccessLevel::Write || offer.access == AccessLevel::Admin},
217 {"mayManage", offer.access == AccessLevel::Admin},
219 perform_async_operation("PermissionOffer",
221 make_handler_extracting_property("token", std::move(callback)),
226 void Permissions::accept_offer(std::shared_ptr<SyncUser> user,
227 const std::string& token,
228 PermissionOfferCallback callback,
229 const ConfigMaker& make_config)
231 perform_async_operation("PermissionOfferResponse",
233 make_handler_extracting_property("realmUrl", std::move(callback)),
234 AnyDict{ {"token", token} },
238 void Permissions::perform_async_operation(const std::string& object_type,
239 std::shared_ptr<SyncUser> user,
240 AsyncOperationHandler handler,
241 AnyDict additional_props,
242 const ConfigMaker& make_config)
244 auto realm = Permissions::management_realm(std::move(user), make_config);
247 // Get the current time.
248 int64_t ns_since_epoch = ns_since_unix_epoch(system_clock::now());
249 int64_t s_arg = ns_since_epoch / (int64_t)Timestamp::nanoseconds_per_second;
250 int32_t ns_arg = ns_since_epoch % Timestamp::nanoseconds_per_second;
252 auto props = AnyDict{
253 {"id", util::uuid_string()},
254 {"createdAt", Timestamp(s_arg, ns_arg)},
255 {"updatedAt", Timestamp(s_arg, ns_arg)},
257 props.insert(additional_props.begin(), additional_props.end());
259 // Write the permission object.
260 realm->begin_transaction();
261 auto raw = Object::create<util::Any>(context, realm, *realm->schema().find(object_type), std::move(props), false);
262 auto object = std::make_shared<_impl::NotificationWrapper<Object>>(std::move(raw));
263 realm->commit_transaction();
265 // Observe the permission object until the permission change has been processed or failed.
266 // The notifier is automatically unregistered upon the completion of the permission
267 // change, one way or another.
268 auto block = [object, handler=std::move(handler)](CollectionChangeSet, std::exception_ptr ex) mutable {
270 handler(nullptr, ex);
276 auto status_code = object->get_property_value<util::Any>(context, "statusCode");
277 if (!status_code.has_value()) {
278 // Continue waiting for the sync server to complete the operation.
282 // Determine whether an error happened or not.
283 if (auto code = any_cast<long long>(status_code)) {
284 // The permission change failed because an error was returned from the server.
285 auto status = object->get_property_value<util::Any>(context, "statusMessage");
286 std::string error_str = (status.has_value()
287 ? any_cast<std::string>(status)
288 : util::format("Error code: %1", code));
289 handler(nullptr, std::make_exception_ptr(PermissionActionException(error_str, code)));
292 handler(object.get(), nullptr);
296 object->add_notification_callback(std::move(block));
299 SharedRealm Permissions::management_realm(std::shared_ptr<SyncUser> user, const ConfigMaker& make_config)
301 // FIXME: maybe we should cache the management Realm on the user, so we don't need to open it every time.
302 const auto realm_url = util::format("realm%1/~/__management", user->server_url().substr(4));
303 Realm::Config config = make_config(user, std::move(realm_url));
304 config.sync_config->stop_policy = SyncSessionStopPolicy::Immediately;
305 config.schema = Schema{
306 {"PermissionChange", {
307 Property{"id", PropertyType::String, Property::IsPrimary{true}},
308 Property{"createdAt", PropertyType::Date},
309 Property{"updatedAt", PropertyType::Date},
310 Property{"statusCode", PropertyType::Int|PropertyType::Nullable},
311 Property{"statusMessage", PropertyType::String|PropertyType::Nullable},
312 Property{"userId", PropertyType::String},
313 Property{"metadataKey", PropertyType::String|PropertyType::Nullable},
314 Property{"metadataValue", PropertyType::String|PropertyType::Nullable},
315 Property{"metadataNameSpace", PropertyType::String|PropertyType::Nullable},
316 Property{"realmUrl", PropertyType::String},
317 Property{"mayRead", PropertyType::Bool|PropertyType::Nullable},
318 Property{"mayWrite", PropertyType::Bool|PropertyType::Nullable},
319 Property{"mayManage", PropertyType::Bool|PropertyType::Nullable},
321 {"PermissionOffer", {
322 Property{"id", PropertyType::String, Property::IsPrimary{true}},
323 Property{"createdAt", PropertyType::Date},
324 Property{"updatedAt", PropertyType::Date},
325 Property{"expiresAt", PropertyType::Date|PropertyType::Nullable},
326 Property{"statusCode", PropertyType::Int|PropertyType::Nullable},
327 Property{"statusMessage", PropertyType::String|PropertyType::Nullable},
328 Property{"token", PropertyType::String|PropertyType::Nullable},
329 Property{"realmUrl", PropertyType::String},
330 Property{"mayRead", PropertyType::Bool},
331 Property{"mayWrite", PropertyType::Bool},
332 Property{"mayManage", PropertyType::Bool},
334 {"PermissionOfferResponse", {
335 Property{"id", PropertyType::String, Property::IsPrimary{true}},
336 Property{"createdAt", PropertyType::Date},
337 Property{"updatedAt", PropertyType::Date},
338 Property{"statusCode", PropertyType::Int|PropertyType::Nullable},
339 Property{"statusMessage", PropertyType::String|PropertyType::Nullable},
340 Property{"token", PropertyType::String},
341 Property{"realmUrl", PropertyType::String|PropertyType::Nullable},
344 config.schema_version = 0;
345 auto shared_realm = Realm::get_shared_realm(std::move(config));
346 user->register_management_session(shared_realm->config().path);
350 SharedRealm Permissions::permission_realm(std::shared_ptr<SyncUser> user, const ConfigMaker& make_config)
352 // FIXME: maybe we should cache the permission Realm on the user, so we don't need to open it every time.
353 const auto realm_url = util::format("realm%1/~/__permission", user->server_url().substr(4));
354 Realm::Config config = make_config(user, std::move(realm_url));
355 config.sync_config->stop_policy = SyncSessionStopPolicy::Immediately;
356 config.schema = Schema{
358 {"updatedAt", PropertyType::Date},
359 {"userId", PropertyType::String},
360 {"path", PropertyType::String},
361 {"mayRead", PropertyType::Bool},
362 {"mayWrite", PropertyType::Bool},
363 {"mayManage", PropertyType::Bool},
366 config.schema_version = 0;
367 auto shared_realm = Realm::get_shared_realm(std::move(config));
368 user->register_permission_session(shared_realm->config().path);