| // Copyright 2021 The ChromiumOS Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include "dlp/dlp_adaptor.h" | |
| #include <cstdint> | |
| #include <set> | |
| #include <string> | |
| #include <sys/types.h> | |
| #include <type_traits> | |
| #include <utility> | |
| #include <vector> | |
| #include <base/check.h> | |
| #include <base/containers/contains.h> | |
| #include <base/files/file_enumerator.h> | |
| #include <base/files/file_path.h> | |
| #include <base/files/file_util.h> | |
| #include <base/files/scoped_file.h> | |
| #include <base/functional/bind.h> | |
| #include <base/functional/callback_helpers.h> | |
| #include <base/location.h> | |
| #include <base/logging.h> | |
| #include <base/process/process_handle.h> | |
| #include <base/strings/string_number_conversions.h> | |
| #include <base/task/single_thread_task_runner.h> | |
| #include <base/time/time.h> | |
| #include <brillo/dbus/dbus_object.h> | |
| #include <brillo/errors/error.h> | |
| #include <dbus/dlp/dbus-constants.h> | |
| #include <featured/feature_library.h> | |
| #include <google/protobuf/message_lite.h> | |
| #include <session_manager/dbus-proxies.h> | |
| #include <sqlite3.h> | |
| #include "dlp/proto_bindings/dlp_service.pb.h" | |
| namespace dlp { | |
| // The maximum delay in between file creation and adding file to database. | |
| constexpr base::TimeDelta kAddFileMaxDelay = base::Minutes(1); | |
| namespace { | |
| // Serializes |proto| to a vector of bytes. CHECKs for success (should | |
| // never fail if there are no required proto fields). | |
| std::vector<uint8_t> SerializeProto( | |
| const google::protobuf::MessageLite& proto) { | |
| std::vector<uint8_t> proto_blob(proto.ByteSizeLong()); | |
| CHECK(proto.SerializeToArray(proto_blob.data(), proto_blob.size())); | |
| return proto_blob; | |
| } | |
| // Parses a proto from an array of bytes |proto_blob|. Returns | |
| // error message or empty string if no error. | |
| std::string ParseProto(const base::Location& from_here, | |
| google::protobuf::MessageLite* proto, | |
| const std::vector<uint8_t>& proto_blob) { | |
| if (!proto->ParseFromArray(proto_blob.data(), proto_blob.size())) { | |
| const std::string error_message = "Failed to parse proto message."; | |
| LOG(ERROR) << from_here.ToString() << " " << error_message; | |
| return error_message; | |
| } | |
| return ""; | |
| } | |
| FileEntry ConvertToFileEntry(FileId id, AddFileRequest request) { | |
| FileEntry result; | |
| result.id = id; | |
| if (request.has_source_url()) | |
| result.source_url = request.source_url(); | |
| if (request.has_referrer_url()) | |
| result.referrer_url = request.referrer_url(); | |
| return result; | |
| } | |
| std::set<std::pair<base::FilePath, FileId>> EnumerateFiles( | |
| scoped_refptr<base::SingleThreadTaskRunner> task_runner, | |
| const base::FilePath& root_path) { | |
| CHECK(task_runner->RunsTasksInCurrentSequence()); | |
| std::set<std::pair<base::FilePath, FileId>> files; | |
| base::FileEnumerator enumerator(root_path, /*recursive=*/true, | |
| base::FileEnumerator::FILES); | |
| for (base::FilePath entry = enumerator.Next(); !entry.empty(); | |
| entry = enumerator.Next()) { | |
| FileId file_id = GetFileId(entry.value()); | |
| if (file_id.first > 0) { | |
| files.insert(std::make_pair(entry, file_id)); | |
| } | |
| } | |
| return files; | |
| } | |
| // Checks whether the list of the rules is exactly the same. | |
| bool SameRules(const std::vector<DlpFilesRule> rules1, | |
| const std::vector<DlpFilesRule> rules2) { | |
| if (rules1.size() != rules2.size()) { | |
| return false; | |
| } | |
| for (int i = 0; i < rules1.size(); ++i) { | |
| if (SerializeProto(rules1[i]) != SerializeProto(rules2[i])) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| // Whether the cached level should be re-checked. | |
| bool RequiresCheck(RestrictionLevel level) { | |
| return level == LEVEL_UNSPECIFIED || level == LEVEL_WARN_CANCEL; | |
| } | |
| // If the action is permanently blocked by the current policy. | |
| bool IsAlwaysBlocked(RestrictionLevel level) { | |
| return level == LEVEL_BLOCK; | |
| } | |
| } // namespace | |
| const struct VariationsFeature kCrOSLateBootDlpDatabaseCleanupFeature = { | |
| .name = "CrOSLateBootDlpDatabaseCleanupFeature", | |
| .default_state = FEATURE_DISABLED_BY_DEFAULT, | |
| }; | |
| DlpAdaptor::DlpAdaptor( | |
| std::unique_ptr<brillo::dbus_utils::DBusObject> dbus_object, | |
| feature::PlatformFeaturesInterface* feature_lib, | |
| int fanotify_perm_fd, | |
| int fanotify_notif_fd, | |
| const base::FilePath& home_path) | |
| : org::chromium::DlpAdaptor(this), | |
| dbus_object_(std::move(dbus_object)), | |
| feature_lib_(feature_lib), | |
| home_path_(home_path), | |
| file_enumeration_thread_("file_enumeration_thread") { | |
| dlp_metrics_ = std::make_unique<DlpMetrics>(); | |
| fanotify_watcher_ = std::make_unique<FanotifyWatcher>(this, fanotify_perm_fd, | |
| fanotify_notif_fd); | |
| dlp_files_policy_service_ = | |
| std::make_unique<org::chromium::DlpFilesPolicyServiceProxy>( | |
| dbus_object_->GetBus().get(), kDlpFilesPolicyServiceName); | |
| CHECK(file_enumeration_thread_.Start()) | |
| << "Failed to start file enumeration thread."; | |
| file_enumeration_task_runner_ = file_enumeration_thread_.task_runner(); | |
| CHECK(!file_enumeration_task_runner_->RunsTasksInCurrentSequence()); | |
| } | |
| DlpAdaptor::~DlpAdaptor() { | |
| if (!pending_files_to_add_.empty()) { | |
| DCHECK(!db_); | |
| dlp_metrics_->SendAdaptorError( | |
| AdaptorError::kAddFileNotCompleteBeforeDestruction); | |
| } | |
| file_enumeration_thread_.Stop(); | |
| } | |
| void DlpAdaptor::RegisterAsync( | |
| brillo::dbus_utils::AsyncEventSequencer::CompletionAction | |
| completion_callback) { | |
| RegisterWithDBusObject(dbus_object_.get()); | |
| dbus_object_->RegisterAsync(std::move(completion_callback)); | |
| } | |
| std::vector<uint8_t> DlpAdaptor::SetDlpFilesPolicy( | |
| const std::vector<uint8_t>& request_blob) { | |
| LOG(INFO) << "Received DLP files policy."; | |
| SetDlpFilesPolicyRequest request; | |
| std::string error_message = ParseProto(FROM_HERE, &request, request_blob); | |
| SetDlpFilesPolicyResponse response; | |
| if (!error_message.empty()) { | |
| response.set_error_message(error_message); | |
| dlp_metrics_->SendAdaptorError(AdaptorError::kInvalidProtoError); | |
| return SerializeProto(response); | |
| } | |
| auto new_rules = | |
| std::vector<DlpFilesRule>(request.rules().begin(), request.rules().end()); | |
| // No update is needed. | |
| if (SameRules(policy_rules_, new_rules)) { | |
| return SerializeProto(response); | |
| } | |
| requests_cache_.ResetCache(); | |
| policy_rules_.swap(new_rules); | |
| if (!policy_rules_.empty()) { | |
| EnsureFanotifyWatcherStarted(); | |
| } else { | |
| fanotify_watcher_->SetActive(false); | |
| } | |
| return SerializeProto(response); | |
| } | |
| void DlpAdaptor::AddFiles( | |
| std::unique_ptr< | |
| brillo::dbus_utils::DBusMethodResponse<std::vector<uint8_t>>> response, | |
| const std::vector<uint8_t>& request_blob) { | |
| AddFilesRequest request; | |
| const std::string parse_error = ParseProto(FROM_HERE, &request, request_blob); | |
| if (!parse_error.empty()) { | |
| ReplyOnAddFiles(std::move(response), | |
| "Failed to parse AddFiles request: " + parse_error); | |
| dlp_metrics_->SendAdaptorError(AdaptorError::kInvalidProtoError); | |
| return; | |
| } | |
| if (request.add_file_requests().empty()) { | |
| ReplyOnAddFiles(std::move(response), std::string()); | |
| return; | |
| } | |
| LOG(INFO) << "Adding " << request.add_file_requests().size() | |
| << " files to the database."; | |
| std::vector<FileEntry> files_to_add; | |
| std::vector<base::FilePath> files_paths; | |
| std::vector<FileId> files_ids; | |
| for (const AddFileRequest& add_file_request : request.add_file_requests()) { | |
| LOG(INFO) << "Adding file to the database: " | |
| << add_file_request.file_path(); | |
| base::FilePath file_path(add_file_request.file_path()); | |
| if (base::IsLink(file_path)) { | |
| auto resolved_path = base::ReadSymbolicLinkAbsolute(file_path); | |
| if (!resolved_path) { | |
| ReplyOnAddFiles(std::move(response), "Failed to follow symlink"); | |
| dlp_metrics_->SendAdaptorError(AdaptorError::kFailedToFollowSymlink); | |
| return; | |
| } | |
| file_path = resolved_path.value(); | |
| LOG(INFO) << "Resolved link to: " << file_path; | |
| } | |
| const FileId id = GetFileId(file_path.value()); | |
| if (!id.first) { | |
| ReplyOnAddFiles(std::move(response), "Failed to get inode"); | |
| dlp_metrics_->SendAdaptorError(AdaptorError::kInodeRetrievalError); | |
| return; | |
| } | |
| // If file is created too long time ago - do not allow addition to the | |
| // database. DLP is only for new files. | |
| const base::Time crtime = base::Time::FromTimeT(id.second); | |
| if (base::Time::Now() - crtime >= kAddFileMaxDelay) { | |
| ReplyOnAddFiles(std::move(response), "File is too old"); | |
| dlp_metrics_->SendAdaptorError(AdaptorError::kAddedFileIsTooOld); | |
| return; | |
| } | |
| if (!home_path_.IsParent(file_path)) { | |
| ReplyOnAddFiles(std::move(response), "File is not on user's home"); | |
| dlp_metrics_->SendAdaptorError(AdaptorError::kAddedFileIsNotOnUserHome); | |
| return; | |
| } | |
| FileEntry file_entry = ConvertToFileEntry(id, add_file_request); | |
| files_to_add.push_back(file_entry); | |
| files_paths.push_back(file_path); | |
| files_ids.emplace_back(id); | |
| } | |
| if (!db_) { | |
| LOG(WARNING) << "Database is not ready, pending addition of the file"; | |
| pending_files_to_add_.insert(pending_files_to_add_.end(), | |
| files_to_add.begin(), files_to_add.end()); | |
| ReplyOnAddFiles(std::move(response), std::string()); | |
| dlp_metrics_->SendAdaptorError(AdaptorError::kDatabaseNotReadyError); | |
| return; | |
| } | |
| db_->UpsertFileEntries( | |
| files_to_add, | |
| base::BindOnce(&DlpAdaptor::OnFilesUpserted, weak_factory_.GetWeakPtr(), | |
| std::move(response), std::move(files_paths), | |
| std::move(files_ids))); | |
| } | |
| void DlpAdaptor::RequestFileAccess( | |
| std::unique_ptr<brillo::dbus_utils::DBusMethodResponse<std::vector<uint8_t>, | |
| base::ScopedFD>> | |
| response, | |
| const std::vector<uint8_t>& request_blob) { | |
| base::ScopedFD local_fd, remote_fd; | |
| if (!base::CreatePipe(&local_fd, &remote_fd, /*non_blocking=*/true)) { | |
| PLOG(ERROR) << "Failed to create lifeline pipe"; | |
| dlp_metrics_->SendAdaptorError(AdaptorError::kCreatePipeError); | |
| std::move(response)->ReplyWithError( | |
| FROM_HERE, brillo::errors::dbus::kDomain, dlp::kErrorFailedToCreatePipe, | |
| "Failed to create lifeline pipe"); | |
| return; | |
| } | |
| RequestFileAccessRequest request; | |
| const std::string parse_error = ParseProto(FROM_HERE, &request, request_blob); | |
| if (!parse_error.empty()) { | |
| LOG(ERROR) << "Failed to parse RequestFileAccess request: " << parse_error; | |
| dlp_metrics_->SendAdaptorError(AdaptorError::kInvalidProtoError); | |
| ReplyOnRequestFileAccess(std::move(response), std::move(remote_fd), | |
| /*allowed=*/false, parse_error); | |
| return; | |
| } | |
| if (!db_) { | |
| ReplyOnRequestFileAccess(std::move(response), std::move(remote_fd), | |
| /*allowed=*/true, | |
| /*error_message=*/std::string()); | |
| return; | |
| } | |
| std::vector<FileId> ids; | |
| for (const auto& file_path : request.files_paths()) { | |
| const FileId id = GetFileId(file_path); | |
| if (id.first > 0) { | |
| ids.push_back(id); | |
| } | |
| } | |
| // If no valid ids provided, return immediately. | |
| if (ids.empty()) { | |
| ReplyOnRequestFileAccess(std::move(response), std::move(remote_fd), | |
| /*allowed=*/true, | |
| /*error_message=*/std::string()); | |
| return; | |
| } | |
| db_->GetFileEntriesByIds( | |
| ids, base::BindOnce(&DlpAdaptor::ProcessRequestFileAccessWithData, | |
| weak_factory_.GetWeakPtr(), std::move(response), | |
| std::move(request), std::move(local_fd), | |
| std::move(remote_fd))); | |
| } | |
| void DlpAdaptor::ProcessRequestFileAccessWithData( | |
| std::unique_ptr<brillo::dbus_utils::DBusMethodResponse<std::vector<uint8_t>, | |
| base::ScopedFD>> | |
| response, | |
| RequestFileAccessRequest request, | |
| base::ScopedFD local_fd, | |
| base::ScopedFD remote_fd, | |
| std::map<FileId, FileEntry> file_entries) { | |
| IsFilesTransferRestrictedRequest matching_request; | |
| std::vector<FileId> request_ids; | |
| std::vector<FileId> granted_ids; | |
| bool allow_all_files = | |
| request.has_destination_component() && | |
| request.destination_component() == DlpComponent::SYSTEM; | |
| for (const auto& file_path : request.files_paths()) { | |
| const FileId id = GetFileId(file_path); | |
| auto it = file_entries.find(id); | |
| if (it == std::end(file_entries)) { | |
| // Skip file if it's not DLP-protected as access to it is always allowed. | |
| continue; | |
| } | |
| if (allow_all_files) { | |
| granted_ids.emplace_back(id); | |
| continue; | |
| } | |
| RestrictionLevel cached_level = requests_cache_.Get( | |
| id, file_path, | |
| request.has_destination_url() ? request.destination_url() : "", | |
| request.has_destination_component() ? request.destination_component() | |
| : DlpComponent::UNKNOWN_COMPONENT); | |
| // Reply immediately is a file is always blocked. | |
| if (IsAlwaysBlocked(cached_level)) { | |
| ReplyOnRequestFileAccess(std::move(response), std::move(remote_fd), | |
| /*allowed=*/false, | |
| /*error_message=*/std::string()); | |
| return; | |
| } | |
| // Was previously allowed, no need to check again. | |
| if (!RequiresCheck(cached_level)) { | |
| granted_ids.emplace_back(id); | |
| continue; | |
| } | |
| request_ids.push_back(id); | |
| FileMetadata* file_metadata = matching_request.add_transferred_files(); | |
| file_metadata->set_inode(id.first); | |
| file_metadata->set_crtime(id.second); | |
| file_metadata->set_source_url(it->second.source_url); | |
| file_metadata->set_referrer_url(it->second.referrer_url); | |
| file_metadata->set_path(file_path); | |
| } | |
| // If access to all requested files was allowed, return immediately. | |
| if (request_ids.empty()) { | |
| if (!granted_ids.empty()) { | |
| int lifeline_fd = AddLifelineFd(local_fd.get()); | |
| approved_requests_.insert_or_assign( | |
| lifeline_fd, | |
| std::make_pair(std::move(granted_ids), request.process_id())); | |
| } | |
| ReplyOnRequestFileAccess(std::move(response), std::move(remote_fd), | |
| /*allowed=*/true, | |
| /*error_message=*/std::string()); | |
| return; | |
| } | |
| std::pair<RequestFileAccessCallback, RequestFileAccessCallback> callbacks = | |
| base::SplitOnceCallback(base::BindOnce( | |
| &DlpAdaptor::ReplyOnRequestFileAccess, weak_factory_.GetWeakPtr(), | |
| std::move(response), std::move(remote_fd))); | |
| if (request.has_destination_url()) | |
| matching_request.set_destination_url(request.destination_url()); | |
| if (request.has_destination_component()) | |
| matching_request.set_destination_component(request.destination_component()); | |
| matching_request.set_file_action(FileAction::TRANSFER); | |
| auto cache_callback = | |
| base::BindOnce(&DlpRequestsCache::CacheResult, | |
| base::Unretained(&requests_cache_), matching_request); | |
| dlp_files_policy_service_->IsFilesTransferRestrictedAsync( | |
| SerializeProto(matching_request), | |
| base::BindOnce(&DlpAdaptor::OnRequestFileAccess, | |
| weak_factory_.GetWeakPtr(), std::move(request_ids), | |
| std::move(granted_ids), request.process_id(), | |
| std::move(local_fd), std::move(callbacks.first), | |
| std::move(cache_callback)), | |
| base::BindOnce(&DlpAdaptor::OnRequestFileAccessError, | |
| weak_factory_.GetWeakPtr(), std::move(callbacks.second)), | |
| /*timeout_ms=*/base::Minutes(5).InMilliseconds()); | |
| } | |
| void DlpAdaptor::GetFilesSources( | |
| std::unique_ptr< | |
| brillo::dbus_utils::DBusMethodResponse<std::vector<uint8_t>>> response, | |
| const std::vector<uint8_t>& request_blob) { | |
| GetFilesSourcesRequest request; | |
| GetFilesSourcesResponse response_proto; | |
| const std::string parse_error = ParseProto(FROM_HERE, &request, request_blob); | |
| if (!parse_error.empty()) { | |
| LOG(ERROR) << "Failed to parse GetFilesSources request: " << parse_error; | |
| dlp_metrics_->SendAdaptorError(AdaptorError::kInvalidProtoError); | |
| response_proto.set_error_message(parse_error); | |
| response->Return(SerializeProto(response_proto)); | |
| return; | |
| } | |
| if (!db_) { | |
| dlp_metrics_->SendAdaptorError(AdaptorError::kDatabaseNotReadyError); | |
| response_proto.set_error_message("Database not ready"); | |
| response->Return(SerializeProto(response_proto)); | |
| return; | |
| } | |
| std::vector<FileId> ids; | |
| std::vector<std::pair<FileId, std::string>> requested_files; | |
| for (const auto& path : request.files_paths()) { | |
| const FileId id = GetFileId(path); | |
| if (id.first > 0) { | |
| ids.push_back(id); | |
| requested_files.emplace_back(id, path); | |
| } | |
| } | |
| db_->GetFileEntriesByIds( | |
| ids, base::BindOnce(&DlpAdaptor::ProcessGetFilesSourcesWithData, | |
| weak_factory_.GetWeakPtr(), std::move(response), | |
| requested_files)); | |
| } | |
| void DlpAdaptor::CheckFilesTransfer( | |
| std::unique_ptr< | |
| brillo::dbus_utils::DBusMethodResponse<std::vector<uint8_t>>> response, | |
| const std::vector<uint8_t>& request_blob) { | |
| CheckFilesTransferRequest request; | |
| CheckFilesTransferResponse response_proto; | |
| const std::string parse_error = ParseProto(FROM_HERE, &request, request_blob); | |
| if (!parse_error.empty()) { | |
| LOG(ERROR) << "Failed to parse CheckFilesTransfer request: " << parse_error; | |
| dlp_metrics_->SendAdaptorError(AdaptorError::kInvalidProtoError); | |
| response_proto.set_error_message(parse_error); | |
| response->Return(SerializeProto(response_proto)); | |
| return; | |
| } | |
| if (!db_) { | |
| dlp_metrics_->SendAdaptorError(AdaptorError::kDatabaseNotReadyError); | |
| response_proto.set_error_message("Database is not ready"); | |
| response->Return(SerializeProto(response_proto)); | |
| return; | |
| } | |
| std::vector<FileId> ids; | |
| for (const auto& file_path : request.files_paths()) { | |
| const FileId file_id = GetFileId(file_path); | |
| if (file_id.first > 0) { | |
| ids.push_back(file_id); | |
| } | |
| } | |
| db_->GetFileEntriesByIds( | |
| ids, base::BindOnce(&DlpAdaptor::ProcessCheckFilesTransferWithData, | |
| weak_factory_.GetWeakPtr(), std::move(response), | |
| std::move(request))); | |
| } | |
| void DlpAdaptor::GetDatabaseEntries( | |
| std::unique_ptr< | |
| brillo::dbus_utils::DBusMethodResponse<std::vector<uint8_t>>> | |
| response) { | |
| if (!db_) { | |
| GetDatabaseEntriesResponse response_proto; | |
| dlp_metrics_->SendAdaptorError(AdaptorError::kDatabaseNotReadyError); | |
| response_proto.set_error_message("Database is not ready"); | |
| response->Return(SerializeProto(response_proto)); | |
| return; | |
| } | |
| db_->GetDatabaseEntries( | |
| base::BindOnce(&DlpAdaptor::ProcessGetDatabaseEntriesWithData, | |
| weak_factory_.GetWeakPtr(), std::move(response))); | |
| } | |
| void DlpAdaptor::SetFanotifyWatcherStartedForTesting(bool is_started) { | |
| is_fanotify_watcher_started_for_testing_ = is_started; | |
| } | |
| void DlpAdaptor::CloseDatabaseForTesting() { | |
| db_.reset(); | |
| } | |
| void DlpAdaptor::SetMetricsLibraryForTesting( | |
| std::unique_ptr<MetricsLibraryInterface> metrics_lib) { | |
| dlp_metrics_->SetMetricsLibraryForTesting(std::move(metrics_lib)); | |
| } | |
| void DlpAdaptor::InitDatabase(const base::FilePath& database_path, | |
| base::OnceClosure init_callback) { | |
| LOG(INFO) << "Opening database in: " << database_path.value(); | |
| const base::FilePath database_file = database_path.Append("database"); | |
| if (!base::PathExists(database_file)) { | |
| LOG(INFO) << "Creating database file"; | |
| base::WriteFile(database_path, "\0", 1); | |
| } | |
| std::unique_ptr<DlpDatabase> db = | |
| std::make_unique<DlpDatabase>(database_file, this); | |
| DlpDatabase* db_ptr = db.get(); | |
| db_ptr->Init(base::BindOnce( | |
| &DlpAdaptor::OnDatabaseInitialized, weak_factory_.GetWeakPtr(), | |
| std::move(init_callback), std::move(db), database_path)); | |
| } | |
| void DlpAdaptor::OnDatabaseInitialized(base::OnceClosure init_callback, | |
| std::unique_ptr<DlpDatabase> db, | |
| const base::FilePath& database_path, | |
| std::pair<int, bool> db_status) { | |
| if (db_status.first != SQLITE_OK) { | |
| LOG(ERROR) << "Cannot connect to database " << database_path; | |
| dlp_metrics_->SendAdaptorError(AdaptorError::kDatabaseConnectionError); | |
| std::move(init_callback).Run(); | |
| return; | |
| } | |
| dlp_metrics_->SendBooleanHistogram(kDlpDatabaseMigrationNeededHistogram, | |
| db_status.second); | |
| // Migration is needed. | |
| if (db_status.second) { | |
| LOG(INFO) << "Migrating from legacy table"; | |
| file_enumeration_task_runner_->PostTaskAndReplyWithResult( | |
| FROM_HERE, | |
| base::BindOnce(&EnumerateFiles, file_enumeration_task_runner_, | |
| home_path_), | |
| base::BindOnce(&DlpAdaptor::MigrateDatabase, weak_factory_.GetWeakPtr(), | |
| std::move(db), std::move(init_callback), database_path)); | |
| return; | |
| } | |
| if (!pending_files_to_add_.empty()) { | |
| LOG(INFO) << "Upserting pending entries"; | |
| DlpDatabase* db_ptr = db.get(); | |
| std::vector<FileEntry> files_being_added; | |
| files_being_added.swap(pending_files_to_add_); | |
| db_ptr->UpsertFileEntries( | |
| files_being_added, | |
| base::BindOnce(&DlpAdaptor::OnPendingFilesUpserted, | |
| weak_factory_.GetWeakPtr(), std::move(init_callback), | |
| std::move(db), database_path, db_status)); | |
| return; | |
| } | |
| // Check whether cleanup of deleted files should happen when database is | |
| // initialized. | |
| // This could be disabled because for now the daemon can't traverse some | |
| // of the home directory folders due to inconsistent access permissions. | |
| if (feature_lib_ && | |
| feature_lib_->IsEnabledBlocking(kCrOSLateBootDlpDatabaseCleanupFeature)) { | |
| LOG(INFO) << "Starting database cleanup"; | |
| file_enumeration_task_runner_->PostTaskAndReplyWithResult( | |
| FROM_HERE, | |
| base::BindOnce(&EnumerateFiles, file_enumeration_task_runner_, | |
| home_path_), | |
| base::BindOnce(&DlpAdaptor::CleanupAndSetDatabase, | |
| weak_factory_.GetWeakPtr(), std::move(db), | |
| std::move(init_callback))); | |
| } else { | |
| OnDatabaseCleaned(std::move(db), std::move(init_callback), | |
| /*success=*/true); | |
| } | |
| } | |
| void DlpAdaptor::OnPendingFilesUpserted(base::OnceClosure init_callback, | |
| std::unique_ptr<DlpDatabase> db, | |
| const base::FilePath& database_path, | |
| std::pair<int, bool> db_status, | |
| bool success) { | |
| if (!success) { | |
| LOG(ERROR) << "Error while adding pending files."; | |
| dlp_metrics_->SendAdaptorError(AdaptorError::kAddFileError); | |
| } | |
| OnDatabaseInitialized(std::move(init_callback), std::move(db), database_path, | |
| db_status); | |
| } | |
| void DlpAdaptor::AddPerFileWatch( | |
| const std::set<std::pair<base::FilePath, FileId>>& files) { | |
| if (!fanotify_watcher_->IsActive()) | |
| return; | |
| // No files to add -> exit early. | |
| if (files.empty()) | |
| return; | |
| // Not expected, but if the DB is not there -> delay adding watches. | |
| if (!db_) { | |
| pending_per_file_watches_ = true; | |
| return; | |
| } | |
| std::vector<FileId> ids; | |
| for (const auto& entry : files) { | |
| ids.push_back(entry.second); | |
| } | |
| db_->GetFileEntriesByIds( | |
| ids, base::BindOnce(&DlpAdaptor::ProcessAddPerFileWatchWithData, | |
| weak_factory_.GetWeakPtr(), files)); | |
| } | |
| void DlpAdaptor::ProcessAddPerFileWatchWithData( | |
| const std::set<std::pair<base::FilePath, FileId>>& files, | |
| std::map<FileId, FileEntry> file_entries) { | |
| for (const auto& entry : files) { | |
| if (file_entries.find(entry.second) != std::end(file_entries)) { | |
| fanotify_watcher_->AddFileDeleteWatch(entry.first); | |
| } | |
| } | |
| } | |
| void DlpAdaptor::EnsureFanotifyWatcherStarted() { | |
| if (fanotify_watcher_->IsActive()) | |
| return; | |
| if (is_fanotify_watcher_started_for_testing_) | |
| return; | |
| LOG(INFO) << "Activating fanotify watcher"; | |
| fanotify_watcher_->SetActive(true); | |
| // If the database is not initialized yet, we delay adding per file watch | |
| // till it'll be created. | |
| if (db_) { | |
| file_enumeration_task_runner_->PostTaskAndReplyWithResult( | |
| FROM_HERE, | |
| base::BindOnce(&EnumerateFiles, file_enumeration_task_runner_, | |
| home_path_), | |
| base::BindOnce(&DlpAdaptor::AddPerFileWatch, | |
| weak_factory_.GetWeakPtr())); | |
| } else { | |
| pending_per_file_watches_ = true; | |
| } | |
| } | |
| void DlpAdaptor::ProcessFileOpenRequest( | |
| FileId id, int pid, base::OnceCallback<void(bool)> callback) { | |
| if (pid == base::GetCurrentProcId()) { | |
| // Allowing itself all file accesses (to database files). | |
| std::move(callback).Run(/*allowed=*/true); | |
| return; | |
| } | |
| if (!db_) { | |
| LOG(WARNING) << "DLP database is not ready yet. Allowing the file request"; | |
| dlp_metrics_->SendAdaptorError(AdaptorError::kDatabaseNotReadyError); | |
| std::move(callback).Run(/*allowed=*/true); | |
| return; | |
| } | |
| db_->GetFileEntriesByIds( | |
| {id}, | |
| base::BindOnce(&DlpAdaptor::ProcessFileOpenRequestWithData, | |
| weak_factory_.GetWeakPtr(), pid, std::move(callback))); | |
| } | |
| void DlpAdaptor::ProcessFileOpenRequestWithData( | |
| int pid, | |
| base::OnceCallback<void(bool)> callback, | |
| std::map<FileId, FileEntry> file_entries) { | |
| if (file_entries.size() != 1) { | |
| std::move(callback).Run(/*allowed=*/true); | |
| return; | |
| } | |
| const FileEntry& file_entry = file_entries.cbegin()->second; | |
| int lifeline_fd = -1; | |
| for (const auto& [key, value] : approved_requests_) { | |
| if (base::Contains(value.first, file_entry.id) && value.second == pid) { | |
| lifeline_fd = key; | |
| break; | |
| } | |
| } | |
| if (lifeline_fd != -1) { | |
| std::move(callback).Run(/*allowed=*/true); | |
| return; | |
| } | |
| // If the file can be restricted by any DLP rule, do not allow access there. | |
| IsDlpPolicyMatchedRequest request; | |
| request.mutable_file_metadata()->set_inode(file_entry.id.first); | |
| request.mutable_file_metadata()->set_crtime(file_entry.id.second); | |
| request.mutable_file_metadata()->set_source_url(file_entry.source_url); | |
| request.mutable_file_metadata()->set_referrer_url(file_entry.referrer_url); | |
| // TODO(crbug.com/1357967) | |
| // request.mutable_file_metadata()->set_path(); | |
| std::pair<base::OnceCallback<void(bool)>, base::OnceCallback<void(bool)>> | |
| callbacks = base::SplitOnceCallback(std::move(callback)); | |
| dlp_files_policy_service_->IsDlpPolicyMatchedAsync( | |
| SerializeProto(request), | |
| base::BindOnce(&DlpAdaptor::OnDlpPolicyMatched, | |
| weak_factory_.GetWeakPtr(), std::move(callbacks.first)), | |
| base::BindOnce(&DlpAdaptor::OnDlpPolicyMatchedError, | |
| weak_factory_.GetWeakPtr(), std::move(callbacks.second))); | |
| } | |
| void DlpAdaptor::OnFileDeleted(ino64_t inode) { | |
| if (!db_) { | |
| LOG(WARNING) << "DLP database is not ready yet."; | |
| dlp_metrics_->SendAdaptorError(AdaptorError::kDatabaseNotReadyError); | |
| return; | |
| } | |
| db_->DeleteFileEntryByInode(inode, | |
| /*callback=*/base::DoNothing()); | |
| } | |
| void DlpAdaptor::OnFanotifyError(FanotifyError error) { | |
| dlp_metrics_->SendFanotifyError(error); | |
| } | |
| void DlpAdaptor::OnDatabaseError(DatabaseError error) { | |
| dlp_metrics_->SendDatabaseError(error); | |
| } | |
| void DlpAdaptor::OnDlpPolicyMatched(base::OnceCallback<void(bool)> callback, | |
| const std::vector<uint8_t>& response_blob) { | |
| IsDlpPolicyMatchedResponse response; | |
| std::string parse_error = ParseProto(FROM_HERE, &response, response_blob); | |
| if (!parse_error.empty()) { | |
| LOG(ERROR) << "Failed to parse IsDlpPolicyMatched response: " | |
| << parse_error; | |
| dlp_metrics_->SendAdaptorError(AdaptorError::kInvalidProtoError); | |
| std::move(callback).Run(/*allowed=*/false); | |
| return; | |
| } | |
| std::move(callback).Run(!response.restricted()); | |
| } | |
| void DlpAdaptor::OnDlpPolicyMatchedError( | |
| base::OnceCallback<void(bool)> callback, brillo::Error* error) { | |
| LOG(ERROR) << "Failed to check whether file could be restricted"; | |
| dlp_metrics_->SendAdaptorError(AdaptorError::kRestrictionDetectionError); | |
| std::move(callback).Run(/*allowed=*/false); | |
| } | |
| void DlpAdaptor::OnRequestFileAccess( | |
| std::vector<FileId> request_ids, | |
| std::vector<FileId> granted_ids, | |
| int pid, | |
| base::ScopedFD local_fd, | |
| RequestFileAccessCallback callback, | |
| base::OnceCallback<void(IsFilesTransferRestrictedResponse)> cache_callback, | |
| const std::vector<uint8_t>& response_blob) { | |
| IsFilesTransferRestrictedResponse response; | |
| std::string parse_error = ParseProto(FROM_HERE, &response, response_blob); | |
| if (!parse_error.empty()) { | |
| LOG(ERROR) << "Failed to parse IsFilesTransferRestrictedResponse response: " | |
| << parse_error; | |
| dlp_metrics_->SendAdaptorError(AdaptorError::kInvalidProtoError); | |
| std::move(callback).Run( | |
| /*allowed=*/false, parse_error); | |
| return; | |
| } | |
| // Cache the response. | |
| std::move(cache_callback).Run(response); | |
| bool allowed = true; | |
| for (const auto& file : response.files_restrictions()) { | |
| if (file.restriction_level() == ::dlp::RestrictionLevel::LEVEL_BLOCK || | |
| file.restriction_level() == | |
| ::dlp::RestrictionLevel::LEVEL_WARN_CANCEL) { | |
| allowed = false; | |
| break; | |
| } | |
| } | |
| if (allowed) { | |
| request_ids.insert(request_ids.end(), granted_ids.begin(), | |
| granted_ids.end()); | |
| int lifeline_fd = AddLifelineFd(local_fd.get()); | |
| approved_requests_.insert_or_assign( | |
| lifeline_fd, std::make_pair(std::move(request_ids), pid)); | |
| } | |
| std::move(callback).Run(allowed, /*error_message=*/std::string()); | |
| } | |
| void DlpAdaptor::OnRequestFileAccessError(RequestFileAccessCallback callback, | |
| brillo::Error* error) { | |
| LOG(ERROR) << "Failed to check whether file could be restricted"; | |
| dlp_metrics_->SendAdaptorError(AdaptorError::kRestrictionDetectionError); | |
| std::move(callback).Run(/*allowed=*/false, error->GetMessage()); | |
| } | |
| void DlpAdaptor::ReplyOnRequestFileAccess( | |
| std::unique_ptr<brillo::dbus_utils::DBusMethodResponse<std::vector<uint8_t>, | |
| base::ScopedFD>> | |
| response, | |
| base::ScopedFD remote_fd, | |
| bool allowed, | |
| const std::string& error_message) { | |
| RequestFileAccessResponse response_proto; | |
| response_proto.set_allowed(allowed); | |
| if (!error_message.empty()) | |
| response_proto.set_error_message(error_message); | |
| response->Return(SerializeProto(response_proto), std::move(remote_fd)); | |
| } | |
| void DlpAdaptor::OnFilesUpserted( | |
| std::unique_ptr< | |
| brillo::dbus_utils::DBusMethodResponse<std::vector<uint8_t>>> response, | |
| std::vector<base::FilePath> files_paths, | |
| std::vector<FileId> files_ids, | |
| bool success) { | |
| CHECK(files_paths.size() == files_ids.size()); | |
| if (success) { | |
| for (size_t i = 0; i < files_paths.size(); ++i) { | |
| AddPerFileWatch({std::make_pair(files_paths[i], files_ids[i])}); | |
| } | |
| ReplyOnAddFiles(std::move(response), std::string()); | |
| } else { | |
| ReplyOnAddFiles(std::move(response), "Failed to add entries to database"); | |
| } | |
| } | |
| void DlpAdaptor::ReplyOnAddFiles( | |
| std::unique_ptr< | |
| brillo::dbus_utils::DBusMethodResponse<std::vector<uint8_t>>> response, | |
| std::string error_message) { | |
| AddFilesResponse response_proto; | |
| if (!error_message.empty()) { | |
| LOG(ERROR) << "Error while adding files: " << error_message; | |
| dlp_metrics_->SendAdaptorError(AdaptorError::kAddFileError); | |
| response_proto.set_error_message(error_message); | |
| } | |
| response->Return(SerializeProto(response_proto)); | |
| } | |
| void DlpAdaptor::ProcessCheckFilesTransferWithData( | |
| std::unique_ptr< | |
| brillo::dbus_utils::DBusMethodResponse<std::vector<uint8_t>>> response, | |
| CheckFilesTransferRequest request, | |
| std::map<FileId, FileEntry> file_entries) { | |
| CheckFilesTransferResponse response_proto; | |
| IsFilesTransferRestrictedRequest matching_request; | |
| base::flat_set<std::string> files_to_check; | |
| std::vector<std::string> already_restricted; | |
| for (const auto& file_path : request.files_paths()) { | |
| const FileId file_id = GetFileId(file_path); | |
| auto it = file_entries.find(file_id); | |
| if (it == std::end(file_entries)) { | |
| // Skip file if it's not DLP-protected as access to it is always allowed. | |
| continue; | |
| } | |
| RestrictionLevel cached_level = requests_cache_.Get( | |
| file_id, file_path, | |
| request.has_destination_url() ? request.destination_url() : "", | |
| request.has_destination_component() ? request.destination_component() | |
| : DlpComponent::UNKNOWN_COMPONENT); | |
| // The file is always blocked. | |
| if (IsAlwaysBlocked(cached_level)) { | |
| already_restricted.push_back(file_path); | |
| } | |
| // Was previously allowed/blocked, no need to check again. | |
| if (!RequiresCheck(cached_level)) { | |
| continue; | |
| } | |
| files_to_check.insert(file_path); | |
| FileMetadata* file_metadata = matching_request.add_transferred_files(); | |
| file_metadata->set_inode(file_id.first); | |
| file_metadata->set_crtime(file_id.second); | |
| file_metadata->set_source_url(it->second.source_url); | |
| file_metadata->set_referrer_url(it->second.referrer_url); | |
| file_metadata->set_path(file_path); | |
| } | |
| if (files_to_check.empty()) { | |
| ReplyOnCheckFilesTransfer(std::move(response), | |
| std::move(already_restricted), | |
| /*error_message=*/std::string()); | |
| return; | |
| } | |
| if (request.has_destination_url()) | |
| matching_request.set_destination_url(request.destination_url()); | |
| if (request.has_destination_component()) | |
| matching_request.set_destination_component(request.destination_component()); | |
| if (request.has_file_action()) | |
| matching_request.set_file_action(request.file_action()); | |
| if (request.has_io_task_id()) | |
| matching_request.set_io_task_id(request.io_task_id()); | |
| auto callbacks = base::SplitOnceCallback( | |
| base::BindOnce(&DlpAdaptor::ReplyOnCheckFilesTransfer, | |
| weak_factory_.GetWeakPtr(), std::move(response))); | |
| auto cache_callback = | |
| base::BindOnce(&DlpRequestsCache::CacheResult, | |
| base::Unretained(&requests_cache_), matching_request); | |
| dlp_files_policy_service_->IsFilesTransferRestrictedAsync( | |
| SerializeProto(matching_request), | |
| base::BindOnce(&DlpAdaptor::OnIsFilesTransferRestricted, | |
| weak_factory_.GetWeakPtr(), std::move(files_to_check), | |
| std::move(already_restricted), std::move(callbacks.first), | |
| std::move(cache_callback)), | |
| base::BindOnce(&DlpAdaptor::OnIsFilesTransferRestrictedError, | |
| weak_factory_.GetWeakPtr(), std::move(callbacks.second)), | |
| /*timeout_ms=*/base::Minutes(5).InMilliseconds()); | |
| } | |
| void DlpAdaptor::OnIsFilesTransferRestricted( | |
| base::flat_set<std::string> checked_files, | |
| std::vector<std::string> restricted_files, | |
| CheckFilesTransferCallback callback, | |
| base::OnceCallback<void(IsFilesTransferRestrictedResponse)> cache_callback, | |
| const std::vector<uint8_t>& response_blob) { | |
| IsFilesTransferRestrictedResponse response; | |
| std::string parse_error = ParseProto(FROM_HERE, &response, response_blob); | |
| if (!parse_error.empty()) { | |
| LOG(ERROR) << "Failed to parse IsFilesTransferRestricted response: " | |
| << parse_error; | |
| dlp_metrics_->SendAdaptorError(AdaptorError::kInvalidProtoError); | |
| std::move(callback).Run(std::vector<std::string>(), parse_error); | |
| return; | |
| } | |
| // Cache the response. | |
| std::move(cache_callback).Run(response); | |
| for (const auto& file : response.files_restrictions()) { | |
| DCHECK(base::Contains(checked_files, file.file_metadata().path())); | |
| if (file.restriction_level() == ::dlp::RestrictionLevel::LEVEL_BLOCK || | |
| file.restriction_level() == | |
| ::dlp::RestrictionLevel::LEVEL_WARN_CANCEL) { | |
| restricted_files.push_back(file.file_metadata().path()); | |
| } | |
| } | |
| std::move(callback).Run(std::move(restricted_files), | |
| /*error_message=*/std::string()); | |
| } | |
| void DlpAdaptor::OnIsFilesTransferRestrictedError( | |
| CheckFilesTransferCallback callback, brillo::Error* error) { | |
| LOG(ERROR) << "Failed to check which file should be restricted"; | |
| dlp_metrics_->SendAdaptorError(AdaptorError::kRestrictionDetectionError); | |
| std::move(callback).Run(std::vector<std::string>(), error->GetMessage()); | |
| } | |
| void DlpAdaptor::ReplyOnCheckFilesTransfer( | |
| std::unique_ptr< | |
| brillo::dbus_utils::DBusMethodResponse<std::vector<uint8_t>>> response, | |
| std::vector<std::string> restricted_files_paths, | |
| const std::string& error) { | |
| CheckFilesTransferResponse response_proto; | |
| *response_proto.mutable_files_paths() = {restricted_files_paths.begin(), | |
| restricted_files_paths.end()}; | |
| if (!error.empty()) | |
| response_proto.set_error_message(error); | |
| response->Return(SerializeProto(response_proto)); | |
| } | |
| void DlpAdaptor::ProcessGetFilesSourcesWithData( | |
| std::unique_ptr< | |
| brillo::dbus_utils::DBusMethodResponse<std::vector<uint8_t>>> response, | |
| const std::vector<std::pair<FileId, std::string>>& requested_files, | |
| std::map<FileId, FileEntry> file_entries) { | |
| GetFilesSourcesResponse response_proto; | |
| for (const auto& [id, path] : requested_files) { | |
| // GetFilesSources request might've contained only inode info, so we need to | |
| // compare elements only by inode part of |id|. | |
| for (const auto& [file_id, file_entry] : file_entries) { | |
| if (file_id.first == id.first) { | |
| FileMetadata* file_metadata = response_proto.add_files_metadata(); | |
| file_metadata->set_inode(file_id.first); | |
| file_metadata->set_crtime(file_id.second); | |
| if (!path.empty()) { | |
| file_metadata->set_path(path); | |
| } | |
| file_metadata->set_source_url(file_entry.source_url); | |
| file_metadata->set_referrer_url(file_entry.referrer_url); | |
| break; | |
| } | |
| } | |
| } | |
| response->Return(SerializeProto(response_proto)); | |
| } | |
| int DlpAdaptor::AddLifelineFd(int dbus_fd) { | |
| int fd = dup(dbus_fd); | |
| if (fd < 0) { | |
| PLOG(ERROR) << "dup failed"; | |
| dlp_metrics_->SendAdaptorError(AdaptorError::kFileDescriptorDupError); | |
| return -1; | |
| } | |
| lifeline_fd_controllers_[fd] = base::FileDescriptorWatcher::WatchReadable( | |
| fd, base::BindRepeating(&DlpAdaptor::OnLifelineFdClosed, | |
| weak_factory_.GetWeakPtr(), fd)); | |
| return fd; | |
| } | |
| bool DlpAdaptor::DeleteLifelineFd(int fd) { | |
| auto iter = lifeline_fd_controllers_.find(fd); | |
| if (iter == lifeline_fd_controllers_.end()) { | |
| return false; | |
| } | |
| iter->second.reset(); // Destruct the controller, which removes the callback. | |
| lifeline_fd_controllers_.erase(iter); | |
| // AddLifelineFd() calls dup(), so this function should close the fd. | |
| // We still return true since at this point the FileDescriptorWatcher object | |
| // has been destructed. | |
| if (IGNORE_EINTR(close(fd)) < 0) { | |
| PLOG(ERROR) << "close failed"; | |
| dlp_metrics_->SendAdaptorError(AdaptorError::kFileDescriptorCloseError); | |
| } | |
| return true; | |
| } | |
| void DlpAdaptor::OnLifelineFdClosed(int client_fd) { | |
| // The process that requested this access has died/exited. | |
| DeleteLifelineFd(client_fd); | |
| // Remove the approvals tied to the lifeline fd. | |
| approved_requests_.erase(client_fd); | |
| } | |
| void DlpAdaptor::CleanupAndSetDatabase( | |
| std::unique_ptr<DlpDatabase> db, | |
| base::OnceClosure callback, | |
| const std::set<std::pair<base::FilePath, FileId>>& files) { | |
| DCHECK(db); | |
| DlpDatabase* db_ptr = db.get(); | |
| std::set<FileId> ids; | |
| for (const auto& entry : files) { | |
| ids.insert(entry.second); | |
| } | |
| db_ptr->DeleteFileEntriesWithIdsNotInSet( | |
| ids, | |
| base::BindOnce(&DlpAdaptor::OnDatabaseCleaned, weak_factory_.GetWeakPtr(), | |
| std::move(db), std::move(callback))); | |
| } | |
| void DlpAdaptor::OnDatabaseCleaned(std::unique_ptr<DlpDatabase> db, | |
| base::OnceClosure callback, | |
| bool success) { | |
| if (success) { | |
| db_.swap(db); | |
| LOG(INFO) << "Database is initialized"; | |
| // If fanotify watcher is already started, we need to add watches for all | |
| // files from the database. | |
| if (pending_per_file_watches_) { | |
| file_enumeration_task_runner_->PostTaskAndReplyWithResult( | |
| FROM_HERE, | |
| base::BindOnce(&EnumerateFiles, file_enumeration_task_runner_, | |
| home_path_), | |
| base::BindOnce(&DlpAdaptor::AddPerFileWatch, | |
| weak_factory_.GetWeakPtr())); | |
| pending_per_file_watches_ = false; | |
| } | |
| std::move(callback).Run(); | |
| } | |
| } | |
| void DlpAdaptor::MigrateDatabase( | |
| std::unique_ptr<DlpDatabase> db, | |
| base::OnceClosure callback, | |
| const base::FilePath& database_path, | |
| const std::set<std::pair<base::FilePath, FileId>>& files) { | |
| DCHECK(db); | |
| DlpDatabase* db_ptr = db.get(); | |
| std::vector<FileId> ids; | |
| for (const auto& entry : files) { | |
| ids.push_back(entry.second); | |
| } | |
| db_ptr->MigrateDatabase( | |
| ids, base::BindOnce(&DlpAdaptor::OnDatabaseMigrated, | |
| weak_factory_.GetWeakPtr(), std::move(db), | |
| std::move(callback), database_path)); | |
| } | |
| void DlpAdaptor::OnDatabaseMigrated(std::unique_ptr<DlpDatabase> db, | |
| base::OnceClosure init_callback, | |
| const base::FilePath& database_path, | |
| bool success) { | |
| if (!success) { | |
| LOG(ERROR) << "Error while migrating database."; | |
| dlp_metrics_->SendAdaptorError(AdaptorError::kDatabaseMigrationError); | |
| } else { | |
| LOG(INFO) << "Database was succesfully migrated."; | |
| } | |
| OnDatabaseInitialized(std::move(init_callback), std::move(db), database_path, | |
| {SQLITE_OK, false}); | |
| } | |
| void DlpAdaptor::ProcessGetDatabaseEntriesWithData( | |
| std::unique_ptr< | |
| brillo::dbus_utils::DBusMethodResponse<std::vector<uint8_t>>> response, | |
| std::map<FileId, FileEntry> files_entries) { | |
| GetDatabaseEntriesResponse response_proto; | |
| for (const auto& [file_id, file_entry] : files_entries) { | |
| FileMetadata* file_metadata = response_proto.add_files_entries(); | |
| file_metadata->set_inode(file_id.first); | |
| file_metadata->set_crtime(file_id.second); | |
| file_metadata->set_source_url(file_entry.source_url); | |
| file_metadata->set_referrer_url(file_entry.referrer_url); | |
| } | |
| response->Return(SerializeProto(response_proto)); | |
| } | |
| } // namespace dlp |