Skip to content

Commit

Permalink
dlp: Provide file_access for indexed db copy
Browse files Browse the repository at this point in the history
The file_access is already granted by blink. The part still missing for
indexed db is using it when a file is saved in the idb. Saving a file is
done by copying it to a special folder. For this copy operation file
access has to be requested in the dlp case.

BUG=b:262372416

Change-Id: Ie24aa71eb878c669843fcb1d7c435030828df2dd
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4757152
Commit-Queue: Daniel Brinkers <brinky@google.com>
Reviewed-by: Evan Stade <estade@chromium.org>
Reviewed-by: Aya Elsayed <ayaelattar@chromium.org>
Reviewed-by: Luca Accorsi <accorsi@google.com>
Reviewed-by: Austin Sullivan <asully@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1184614}
  • Loading branch information
Daniel Brinkers authored and Chromium LUCI CQ committed Aug 17, 2023
1 parent 68e1353 commit c9de335
Show file tree
Hide file tree
Showing 3 changed files with 210 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
#include "base/path_service.h"
#include "chrome/browser/apps/platform_apps/app_browsertest_util.h"
#include "chrome/browser/extensions/extension_browsertest.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_navigator.h"
#include "chrome/test/base/chrome_test_utils.h"
#include "chromeos/dbus/dlp/dlp_client.h"
#include "chromeos/dbus/dlp/fake_dlp_client.h"
Expand Down Expand Up @@ -63,21 +65,27 @@ class DlpScopedFileAccessDelegateBrowserTest : public InProcessBrowserTest {
delegate_ = std::make_unique<DlpScopedFileAccessDelegate>(
chromeos::DlpClient::Get());
EXPECT_TRUE(tmp_.CreateUniqueTempDir());

content::WebContents* const web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
GURL test_url =
embedded_test_server()->GetURL("localhost", "/dlp_files_test.html");
EXPECT_TRUE(content::NavigateToURL(web_contents, test_url));
content::WebContentsConsoleObserver con_observer(web_contents);
con_observer.SetPattern("db opened");
EXPECT_TRUE(con_observer.Wait());

fake_dlp_client_ =
static_cast<chromeos::FakeDlpClient*>(chromeos::DlpClient::Get());
}

void TearDownOnMainThread() override { fake_dlp_client_ = nullptr; }

// Loads the test website and executes `action` as JavaScript in the context
// of this website. The actios is expected to trigger printing
// `expectedConsole` on the console.
// Executes `action` as JavaScript in the context of the opened website. The
// actions is expected to trigger printing `expectedConsole` on the console.
void TestJSAction(std::string action, std::string expectedConsole) {
content::WebContents* const web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
GURL test_url =
embedded_test_server()->GetURL("localhost", "/dlp_files_test.html");
EXPECT_TRUE(content::NavigateToURL(web_contents, test_url));

content::WebContentsConsoleObserver console_observer(web_contents);
console_observer.SetPattern(expectedConsole);
Expand Down Expand Up @@ -170,4 +178,75 @@ IN_PROC_BROWSER_TEST_F(DlpScopedFileAccessDelegateBrowserTest,
TestJSAction("document.getElementById('file_service').click()",
kErrorMessage);
}

IN_PROC_BROWSER_TEST_F(DlpScopedFileAccessDelegateBrowserTest,
UploadFrameIDBProtectedAllow) {
fake_dlp_client_->SetFileAccessAllowed(true);
auto delegate = PrepareChooser();
TestJSAction("document.getElementById('idb_clear').click()", "cleared");
TestJSAction("document.getElementById('idb_save').click()", "saved");
TestJSAction("document.getElementById('idb_open').click()", kTestContent);
}

IN_PROC_BROWSER_TEST_F(DlpScopedFileAccessDelegateBrowserTest,
UploadFrameIDBProtectedDeny) {
fake_dlp_client_->SetFileAccessAllowed(false);
auto delegate = PrepareChooser();
TestJSAction("document.getElementById('idb_clear').click()", "cleared");
TestJSAction("document.getElementById('idb_save').click()", "saved");
TestJSAction("document.getElementById('idb_open').click()", kErrorMessage);
}

IN_PROC_BROWSER_TEST_F(DlpScopedFileAccessDelegateBrowserTest,
UploadFrameRestoreProtectedAllow) {
fake_dlp_client_->SetFileAccessAllowed(true);
auto delegate = PrepareChooser();
TestJSAction("document.getElementById('idb_save').click()", "saved");

NavigateParams params(browser(), GURL("about:blank"),
ui::PAGE_TRANSITION_AUTO_TOPLEVEL);
params.disposition = WindowOpenDisposition::NEW_BACKGROUND_TAB;
Navigate(&params);

TestJSAction("document.getElementById('idb_cached').click()", kTestContent);

chrome::CloseTab(browser());

chrome::RestoreTab(browser());

content::WebContentsConsoleObserver console_observer(
browser()->tab_strip_model()->GetActiveWebContents());
console_observer.SetPattern("db opened");
EXPECT_TRUE(console_observer.Wait());

TestJSAction("document.getElementById('idb_cached').click()", kTestContent);
}

IN_PROC_BROWSER_TEST_F(DlpScopedFileAccessDelegateBrowserTest,
UploadFrameRestoreProtectedDenyRestore) {
fake_dlp_client_->SetFileAccessAllowed(true);
auto delegate = PrepareChooser();
TestJSAction("document.getElementById('idb_save').click()", "saved");

NavigateParams params(browser(), GURL("about:blank"),
ui::PAGE_TRANSITION_AUTO_TOPLEVEL);
params.disposition = WindowOpenDisposition::NEW_BACKGROUND_TAB;
Navigate(&params);

TestJSAction("document.getElementById('idb_cached').click()", kTestContent);

chrome::CloseTab(browser());

fake_dlp_client_->SetFileAccessAllowed(false);

chrome::RestoreTab(browser());

content::WebContentsConsoleObserver console_observer(
browser()->tab_strip_model()->GetActiveWebContents());
console_observer.SetPattern("db opened");
EXPECT_TRUE(console_observer.Wait());

TestJSAction("document.getElementById('idb_cached').click()", kErrorMessage);
}

} // namespace policy
99 changes: 89 additions & 10 deletions chrome/test/data/dlp/dlp_files_test.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,27 @@
<head>
<script defer type="module" type="text/javascript">

let directory
let file
let directory;
let file;
let worker = new Worker("dlp_files_test_worker.js");
let shared = new SharedWorker("dlp_files_test_worker.js");

let db;

const request = indexedDB.open("db", 1);
request.onerror = (ev) => {
console.log(ev);
};
request.onupgradeneeded = (ev) => {
console.log('db schema create');
const db = ev.target.result;
const objectStore = db.createObjectStore("files", { keyPath: "name" });
};
request.onsuccess = function (e) {
db = e.target.result;
console.log('db opened');
};

shared.port.onmessage = (e) => {
console.log(e.data);
};
Expand Down Expand Up @@ -35,12 +52,20 @@
file = await window.showSaveFilePicker({});
})
document.getElementById('read').addEventListener('click', async () => {
if (!file) {
[file] = await window.showOpenFilePicker({});
if (!directory) {
directory = await window.showDirectoryPicker();
}
if ((await directory.queryPermission()) === 'granted') {
try {
const dirFile = await directory.getFileHandle('input.txt');
const r = await dirFile.getFile();
const content = await r.text();
console.log(content);
} catch (err) {
console.log("Could not read file.");
console.log(err.name + ": " + err.message);
}
}
let r = await file.getFile();
let content = await r.text();
console.log(content);
})
document.getElementById('reset').addEventListener('click', async () => {
file = null;
Expand All @@ -61,10 +86,12 @@
worker.postMessage({ 'action': 1, 'file': file });
})
document.getElementById('ded_read').addEventListener('click', async () => {
if (!file) {
[file] = await window.showOpenFilePicker({});
if (!directory) {
directory = await window.showDirectoryPicker();
}
if ((await directory.queryPermission()) === 'granted') {
worker.postMessage({ 'action': 4, 'directory': directory });
}
worker.postMessage({ 'action': 2, 'file': file });
})
document.getElementById('file').addEventListener('change', async (e) => {
try {
Expand Down Expand Up @@ -96,6 +123,51 @@
url.pathname = "/public";
window.history.pushState({}, "", url);
})
document.getElementById('idb_save').addEventListener('change', (e) => {
const save = db.transaction(["files"], 'readwrite')
.objectStore("files")
.put({ 'name': 'input.txt', 'file': e.target.files[0] });
save.onsuccess = (ev) => {
// Force history update for restore test.
window.history.pushState({}, "", new URL(window.location));
console.log("saved");
};
save.onerror = (err) => {
console.log("Could not read file.");
console.log(err.name + ": " + err.message);
};
});
document.getElementById('idb_open').addEventListener('click', async (e) => {
db.transaction("files")
.objectStore("files")
.get("input.txt").onsuccess = async (ev) => {
try {
const file = ev.target.result.file;
const content = await file.text();
console.log(content);
} catch (err) {
console.log("Could not read file.");
console.log(err.name + ": " + err.message);
}
};
});
document.getElementById('idb_clear').addEventListener('click', async (e) => {
db.transaction(["files"], 'readwrite')
.objectStore("files")
.delete("input.txt").onsuccess = async (ev) => {
console.log("cleared");
}
});
document.getElementById('idb_cached').addEventListener('click', async (e) => {
try {
const file = document.getElementById("idb_save").files[0];
const content = await file.text();
console.log(content);
} catch (err) {
console.log("Could not read file.");
console.log(err.name + ": " + err.message);
}
});
const registerServiceWorker = async () => {
const registration = await navigator.serviceWorker.register("dlp_files_test_worker.js", { scope: "/" });
if (registration.installing) {
Expand Down Expand Up @@ -155,6 +227,13 @@ <h3>Change url / history</h3>
<h3>Download Link</h3>
<a href="index.html" download> download </a>
</dev>
<dev>
<h3>Indexed db</h3>
<input type="file" id="idb_save" value="idb_save" />
<input type="button" id="idb_open" value="open" />
<input type="button" id="idb_clear" value="clear" />
<input type="button" id="idb_cached" value="cached" />
</dev>
</form>
</body>

Expand Down
45 changes: 36 additions & 9 deletions storage/browser/blob/write_blob_to_file.cc
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@

#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/functional/callback_forward.h"
#include "base/numerics/checked_math.h"
#include "base/numerics/safe_conversions.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "components/file_access/scoped_file_access.h"
#include "storage/browser/blob/blob_data_handle.h"
#include "storage/browser/blob/blob_data_item.h"
#include "storage/browser/blob/blob_data_snapshot.h"
Expand All @@ -34,6 +36,8 @@ using DelegateNoProgressWriteCallback = base::OnceCallback<void(
base::File::Error result,
int64_t bytes,
FileWriterDelegate::WriteProgressStatus write_status)>;
using CopyCallback = base::OnceCallback<mojom::WriteBlobToFileResult(
file_access::ScopedFileAccess)>;

struct WriteState {
std::unique_ptr<FileWriterDelegate> delegate;
Expand Down Expand Up @@ -132,15 +136,18 @@ bool CopyFileContentsWithOffsetAndSize(base::File* infile,
// modified time of the |copy_from| file. Afterwards, the |last_modified| date
// is optionally saved as the last modified & last accessed time of |copy_to|.
// If |flush_on_close| is true, then Flush is called on the |copy_to| file
// before it is closed.
// before it is closed. The `file_access::ScopedFileAccess` parameter is
// expected to allow access to the source file. It has to be kept in scope of
// this function because it must be alive while the copy operation is happening.
mojom::WriteBlobToFileResult CopyFileAndMaybeWriteTimeModified(
const base::FilePath& copy_from,
base::Time expected_last_modified_copy_from,
const base::FilePath& copy_to,
int64_t offset,
absl::optional<int64_t> size,
absl::optional<base::Time> last_modified,
bool flush_on_close) {
bool flush_on_close,
file_access::ScopedFileAccess) {
// Do a full file copy if the sizes match and there is no offset.
if (offset == 0) {
base::File::Info info;
Expand Down Expand Up @@ -268,6 +275,20 @@ void HandleModifiedTimeOnBlobFileWriteComplete(
std::move(callback).Run(mojom::WriteBlobToFileResult::kSuccess);
}

void PostCopyTaskToFileThreadIfAllowed(
CopyCallback copy_cb,
mojom::BlobStorageContext::WriteBlobToFileCallback callback,
file_access::ScopedFileAccess scoped_file_access) {
if (!scoped_file_access.is_allowed()) {
std::move(callback).Run(mojom::WriteBlobToFileResult::kIOError);
return;
}
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE},
base::BindOnce(std::move(copy_cb), std::move(scoped_file_access)),
std::move(callback));
}

void WriteConstructedBlobToFile(
std::unique_ptr<BlobDataHandle> blob_handle,
const base::FilePath& file_path,
Expand Down Expand Up @@ -304,13 +325,19 @@ void WriteConstructedBlobToFile(
return;
}

base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE},
base::BindOnce(CopyFileAndMaybeWriteTimeModified, item.path(),
item.expected_modification_time(), file_path,
item.offset(), optional_size, last_modified,
flush_on_write),
std::move(callback));
base::OnceCallback<void(file_access::ScopedFileAccess)> post_copy_task =
base::BindOnce(
PostCopyTaskToFileThreadIfAllowed,
base::BindOnce(CopyFileAndMaybeWriteTimeModified, item.path(),
item.expected_modification_time(), file_path,
item.offset(), std::move(optional_size),
std::move(last_modified), flush_on_write),
std::move(callback));
if (item.file_access()) {
item.file_access().Run({item.path()}, std::move(post_copy_task));
} else {
std::move(post_copy_task).Run(file_access::ScopedFileAccess::Allowed());
}
return;
}
}
Expand Down

0 comments on commit c9de335

Please sign in to comment.