Implement App-Scoped Security scoped bookmarks (#11711)

* implementation of security scoped bookmarks

* option is now only available on mas builds
This commit is contained in:
acheronfail 2018-02-13 05:25:06 +11:00 committed by shelley vohr
parent 9f78ef0179
commit d1d50a4c92
11 changed files with 226 additions and 42 deletions

View file

@ -1262,6 +1262,10 @@ void App::BuildPrototype(
.SetMethod("moveToApplicationsFolder", &App::MoveToApplicationsFolder) .SetMethod("moveToApplicationsFolder", &App::MoveToApplicationsFolder)
.SetMethod("isInApplicationsFolder", &App::IsInApplicationsFolder) .SetMethod("isInApplicationsFolder", &App::IsInApplicationsFolder)
#endif #endif
#if defined(MAS_BUILD)
.SetMethod("startAccessingSecurityScopedResource",
&App::StartAccessingSecurityScopedResource)
#endif
.SetMethod("getAppMemoryInfo", &App::GetAppMetrics); .SetMethod("getAppMemoryInfo", &App::GetAppMetrics);
} }

View file

@ -209,6 +209,10 @@ class App : public AtomBrowserClient::Delegate,
bool MoveToApplicationsFolder(mate::Arguments* args); bool MoveToApplicationsFolder(mate::Arguments* args);
bool IsInApplicationsFolder(); bool IsInApplicationsFolder();
#endif #endif
#if defined(MAS_BUILD)
base::Callback<void()> StartAccessingSecurityScopedResource(
mate::Arguments* args);
#endif
#if defined(OS_WIN) #if defined(OS_WIN)
// Get the current Jump List settings. // Get the current Jump List settings.

View file

@ -0,0 +1,59 @@
// Copyright (c) 2013 GitHub, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#include "atom/browser/api/atom_api_app.h"
#import <Cocoa/Cocoa.h>
#include "base/strings/sys_string_conversions.h"
namespace atom {
namespace api {
// Callback passed to js which will stop accessing the given bookmark.
void OnStopAccessingSecurityScopedResource(NSURL* bookmarkUrl) {
[bookmarkUrl stopAccessingSecurityScopedResource];
[bookmarkUrl release];
}
// Get base64 encoded NSData, create a bookmark for it and start accessing it.
base::Callback<void ()> App::StartAccessingSecurityScopedResource(mate::Arguments* args) {
std::string data;
args->GetNext(&data);
NSString *base64str = base::SysUTF8ToNSString(data);
NSData *bookmarkData = [[NSData alloc] initWithBase64EncodedString: base64str options: 0];
// Create bookmarkUrl from NSData.
BOOL isStale = false;
NSError *error = nil;
NSURL *bookmarkUrl = [NSURL URLByResolvingBookmarkData: bookmarkData
options: NSURLBookmarkResolutionWithSecurityScope
relativeToURL: nil
bookmarkDataIsStale: &isStale
error: &error];
if (error != nil) {
NSString *err = [NSString stringWithFormat: @"NSError: %@ %@", error, [error userInfo]];
args->ThrowError(base::SysNSStringToUTF8(err));
}
if (isStale) {
args->ThrowError("bookmarkDataIsStale - try recreating the bookmark");
}
if (error == nil && isStale == false) {
[bookmarkUrl startAccessingSecurityScopedResource];
}
// Stop the NSURL from being GC'd.
[bookmarkUrl retain];
// Return a js callback which will close the bookmark.
return base::Bind(&OnStopAccessingSecurityScopedResource, bookmarkUrl);
}
} // namespace atom
} // namespace api

View file

@ -54,6 +54,9 @@ struct Converter<file_dialog::DialogSettings> {
dict.Get("filters", &(out->filters)); dict.Get("filters", &(out->filters));
dict.Get("properties", &(out->properties)); dict.Get("properties", &(out->properties));
dict.Get("showsTagField", &(out->shows_tag_field)); dict.Get("showsTagField", &(out->shows_tag_field));
#if defined(MAS_BUILD)
dict.Get("securityScopedBookmarks", &(out->security_scoped_bookmarks));
#endif
return true; return true;
} }
}; };

View file

@ -33,11 +33,25 @@ enum FileDialogProperty {
FILE_DIALOG_TREAT_PACKAGE_APP_AS_DIRECTORY = 1 << 7, FILE_DIALOG_TREAT_PACKAGE_APP_AS_DIRECTORY = 1 << 7,
}; };
typedef base::Callback<void( #if defined(MAS_BUILD)
bool result, const std::vector<base::FilePath>& paths)> OpenDialogCallback; typedef base::Callback<void(
bool result,
const std::vector<base::FilePath>& paths,
const std::vector<std::string>& bookmarkData)> OpenDialogCallback;
typedef base::Callback<void( typedef base::Callback<void(
bool result, const base::FilePath& path)> SaveDialogCallback; bool result,
const base::FilePath& path,
const std::string& bookmarkData)> SaveDialogCallback;
#else
typedef base::Callback<void(
bool result,
const std::vector<base::FilePath>& paths)> OpenDialogCallback;
typedef base::Callback<void(
bool result,
const base::FilePath& path)> SaveDialogCallback;
#endif
struct DialogSettings { struct DialogSettings {
atom::NativeWindow* parent_window = nullptr; atom::NativeWindow* parent_window = nullptr;
@ -50,6 +64,7 @@ struct DialogSettings {
int properties = 0; int properties = 0;
bool shows_tag_field = true; bool shows_tag_field = true;
bool force_detached = false; bool force_detached = false;
bool security_scoped_bookmarks = false;
}; };
bool ShowOpenDialog(const DialogSettings& settings, bool ShowOpenDialog(const DialogSettings& settings,

View file

@ -185,11 +185,44 @@ int RunModalDialog(NSSavePanel* dialog, const DialogSettings& settings) {
return chosen; return chosen;
} }
void ReadDialogPaths(NSOpenPanel* dialog, std::vector<base::FilePath>* paths) { // Create bookmark data and serialise it into a base64 string.
std::string GetBookmarkDataFromNSURL(NSURL* url) {
// Create the file if it doesn't exist (necessary for NSSavePanel options).
NSFileManager *defaultManager = [NSFileManager defaultManager];
if (![defaultManager fileExistsAtPath: [url path]]) {
[defaultManager createFileAtPath: [url path] contents: nil attributes: nil];
}
NSError *error = nil;
NSData *bookmarkData = [url bookmarkDataWithOptions: NSURLBookmarkCreationWithSecurityScope
includingResourceValuesForKeys: nil
relativeToURL: nil
error: &error];
if (error != nil) {
// Send back an empty string if there was an error.
return "";
} else {
// Encode NSData in base64 then convert to NSString.
NSString *base64data = [[NSString alloc] initWithData: [bookmarkData base64EncodedDataWithOptions: 0]
encoding: NSUTF8StringEncoding];
return base::SysNSStringToUTF8(base64data);
}
}
void ReadDialogPathsWithBookmarks(NSOpenPanel* dialog,
std::vector<base::FilePath>* paths,
std::vector<std::string>* bookmarks) {
NSArray* urls = [dialog URLs]; NSArray* urls = [dialog URLs];
for (NSURL* url in urls) for (NSURL* url in urls)
if ([url isFileURL]) if ([url isFileURL]) {
paths->push_back(base::FilePath(base::SysNSStringToUTF8([url path]))); paths->push_back(base::FilePath(base::SysNSStringToUTF8([url path])));
bookmarks->push_back(GetBookmarkDataFromNSURL(url));
}
}
void ReadDialogPaths(NSOpenPanel* dialog, std::vector<base::FilePath>* paths) {
std::vector<std::string> ignored_bookmarks;
ReadDialogPathsWithBookmarks(dialog, paths, &ignored_bookmarks);
} }
} // namespace } // namespace
@ -210,6 +243,33 @@ bool ShowOpenDialog(const DialogSettings& settings,
return true; return true;
} }
void OpenDialogCompletion(int chosen, NSOpenPanel* dialog,
const DialogSettings& settings,
const OpenDialogCallback& callback) {
if (chosen == NSFileHandlingPanelCancelButton) {
#if defined(MAS_BUILD)
callback.Run(false, std::vector<base::FilePath>(),
std::vector<std::string>());
#else
callback.Run(false, std::vector<base::FilePath>());
#endif
} else {
std::vector<base::FilePath> paths;
#if defined(MAS_BUILD)
std::vector<std::string> bookmarks;
if (settings.security_scoped_bookmarks) {
ReadDialogPathsWithBookmarks(dialog, &paths, &bookmarks);
} else {
ReadDialogPaths(dialog, &paths);
}
callback.Run(true, paths, bookmarks);
#else
ReadDialogPaths(dialog, &paths);
callback.Run(true, paths);
#endif
}
}
void ShowOpenDialog(const DialogSettings& settings, void ShowOpenDialog(const DialogSettings& settings,
const OpenDialogCallback& c) { const OpenDialogCallback& c) {
NSOpenPanel* dialog = [NSOpenPanel openPanel]; NSOpenPanel* dialog = [NSOpenPanel openPanel];
@ -224,24 +284,12 @@ void ShowOpenDialog(const DialogSettings& settings,
if (!settings.parent_window || !settings.parent_window->GetNativeWindow() || if (!settings.parent_window || !settings.parent_window->GetNativeWindow() ||
settings.force_detached) { settings.force_detached) {
int chosen = [dialog runModal]; int chosen = [dialog runModal];
if (chosen == NSFileHandlingPanelCancelButton) { OpenDialogCompletion(chosen, dialog, settings, callback);
callback.Run(false, std::vector<base::FilePath>());
} else {
std::vector<base::FilePath> paths;
ReadDialogPaths(dialog, &paths);
callback.Run(true, paths);
}
} else { } else {
NSWindow* window = settings.parent_window->GetNativeWindow(); NSWindow* window = settings.parent_window->GetNativeWindow();
[dialog beginSheetModalForWindow:window [dialog beginSheetModalForWindow:window
completionHandler:^(NSInteger chosen) { completionHandler:^(NSInteger chosen) {
if (chosen == NSFileHandlingPanelCancelButton) { OpenDialogCompletion(chosen, dialog, settings, callback);
callback.Run(false, std::vector<base::FilePath>());
} else {
std::vector<base::FilePath> paths;
ReadDialogPaths(dialog, &paths);
callback.Run(true, paths);
}
}]; }];
} }
} }
@ -261,6 +309,29 @@ bool ShowSaveDialog(const DialogSettings& settings,
return true; return true;
} }
void SaveDialogCompletion(int chosen, NSSavePanel* dialog,
const DialogSettings& settings,
const SaveDialogCallback& callback) {
if (chosen == NSFileHandlingPanelCancelButton) {
#if defined(MAS_BUILD)
callback.Run(false, base::FilePath(), "");
#else
callback.Run(false, base::FilePath());
#endif
} else {
std::string path = base::SysNSStringToUTF8([[dialog URL] path]);
#if defined(MAS_BUILD)
std::string bookmark;
if (settings.security_scoped_bookmarks) {
bookmark = GetBookmarkDataFromNSURL([dialog URL]);
}
callback.Run(true, base::FilePath(path), bookmark);
#else
callback.Run(true, base::FilePath(path));
#endif
}
}
void ShowSaveDialog(const DialogSettings& settings, void ShowSaveDialog(const DialogSettings& settings,
const SaveDialogCallback& c) { const SaveDialogCallback& c) {
NSSavePanel* dialog = [NSSavePanel savePanel]; NSSavePanel* dialog = [NSSavePanel savePanel];
@ -273,22 +344,12 @@ void ShowSaveDialog(const DialogSettings& settings,
if (!settings.parent_window || !settings.parent_window->GetNativeWindow() || if (!settings.parent_window || !settings.parent_window->GetNativeWindow() ||
settings.force_detached) { settings.force_detached) {
int chosen = [dialog runModal]; int chosen = [dialog runModal];
if (chosen == NSFileHandlingPanelCancelButton) { SaveDialogCompletion(chosen, dialog, settings, callback);
callback.Run(false, base::FilePath());
} else {
std::string path = base::SysNSStringToUTF8([[dialog URL] path]);
callback.Run(true, base::FilePath(path));
}
} else { } else {
NSWindow* window = settings.parent_window->GetNativeWindow(); NSWindow* window = settings.parent_window->GetNativeWindow();
[dialog beginSheetModalForWindow:window [dialog beginSheetModalForWindow:window
completionHandler:^(NSInteger chosen) { completionHandler:^(NSInteger chosen) {
if (chosen == NSFileHandlingPanelCancelButton) { SaveDialogCompletion(chosen, dialog, settings, callback);
callback.Run(false, base::FilePath());
} else {
std::string path = base::SysNSStringToUTF8([[dialog URL] path]);
callback.Run(true, base::FilePath(path));
}
}]; }];
} }
} }

View file

@ -53,7 +53,13 @@ class FileSelectHelper : public base::RefCounted<FileSelectHelper>,
~FileSelectHelper() override {} ~FileSelectHelper() override {}
void OnOpenDialogDone(bool result, const std::vector<base::FilePath>& paths) { #if defined(MAS_BUILD)
void OnOpenDialogDone(bool result, const std::vector<base::FilePath>& paths,
const std::vector<std::string>& bookmarks)
#else
void OnOpenDialogDone(bool result, const std::vector<base::FilePath>& paths)
#endif
{
std::vector<content::FileChooserFileInfo> file_info; std::vector<content::FileChooserFileInfo> file_info;
if (result) { if (result) {
for (auto& path : paths) { for (auto& path : paths) {
@ -73,7 +79,13 @@ class FileSelectHelper : public base::RefCounted<FileSelectHelper>,
OnFilesSelected(file_info); OnFilesSelected(file_info);
} }
void OnSaveDialogDone(bool result, const base::FilePath& path) { #if defined(MAS_BUILD)
void OnSaveDialogDone(bool result, const base::FilePath& path,
const std::string& bookmark)
#else
void OnSaveDialogDone(bool result, const base::FilePath& path)
#endif
{
std::vector<content::FileChooserFileInfo> file_info; std::vector<content::FileChooserFileInfo> file_info;
if (result) { if (result) {
content::FileChooserFileInfo info; content::FileChooserFileInfo info;

View file

@ -970,6 +970,23 @@ details. Disabled by default.
Set the about panel options. This will override the values defined in the app's Set the about panel options. This will override the values defined in the app's
`.plist` file. See the [Apple docs][about-panel-options] for more details. `.plist` file. See the [Apple docs][about-panel-options] for more details.
### `app.startAccessingSecurityScopedResource(bookmarkData)` _macOS (mas)_
* `bookmarkData` String - The base64 encoded security scoped bookmark data returned by the `dialog.showOpenDialog` or `dialog.showSaveDialog` methods.
Returns `Function` - This function **must** be called once you have finished accessing the security scoped file. If you do not remember to stop accessing the bookmark, [kernel resources will be leaked](https://developer.apple.com/reference/foundation/nsurl/1417051-startaccessingsecurityscopedreso?language=objc) and your app will lose its ability to reach outside the sandbox completely, until your app is restarted.
```js
// Start accessing the file.
const stopAccessingSecurityScopedResource = app.startAccessingSecurityScopedResource(data)
// You can now access the file outside of the sandbox 🎉
// Remember to stop accessing the file once you've finished with it.
stopAccessingSecurityScopedResource()
```
Start accessing a security scoped resource. With this method electron applications that are packaged for the Mac App Store may reach outside their sandbox to access files chosen by the user. See [Apple's documentation](https://developer.apple.com/library/content/documentation/Security/Conceptual/AppSandboxDesignGuide/AppSandboxInDepth/AppSandboxInDepth.html#//apple_ref/doc/uid/TP40011183-CH3-SW16) for a description of how this system works.
### `app.commandLine.appendSwitch(switch[, value])` ### `app.commandLine.appendSwitch(switch[, value])`
* `switch` String - A command-line switch * `switch` String - A command-line switch

View file

@ -50,8 +50,10 @@ The `dialog` module has the following methods:
as a directory instead of a file. as a directory instead of a file.
* `message` String (optional) _macOS_ - Message to display above input * `message` String (optional) _macOS_ - Message to display above input
boxes. boxes.
* `securityScopedBookmarks` _masOS_ _mas_ - Create [security scoped bookmarks](https://developer.apple.com/library/content/documentation/Security/Conceptual/AppSandboxDesignGuide/AppSandboxInDepth/AppSandboxInDepth.html#//apple_ref/doc/uid/TP40011183-CH3-SW16) when packaged for the Mac App Store.
* `callback` Function (optional) * `callback` Function (optional)
* `filePaths` String[] - An array of file paths chosen by the user * `filePaths` String[] - An array of file paths chosen by the user
* `bookmarks` String[] _macOS_ _mas_ - An array matching the `filePaths` array of base64 encoded strings which contains security scoped bookmark data. `securityScopedBookmarks` must be enabled for this to be populated.
Returns `String[]`, an array of file paths chosen by the user, Returns `String[]`, an array of file paths chosen by the user,
if the callback is provided it returns `undefined`. if the callback is provided it returns `undefined`.
@ -99,8 +101,10 @@ shown.
displayed in front of the filename text field. displayed in front of the filename text field.
* `showsTagField` Boolean (optional) _macOS_ - Show the tags input box, * `showsTagField` Boolean (optional) _macOS_ - Show the tags input box,
defaults to `true`. defaults to `true`.
* `securityScopedBookmarks` Boolean (optional) _masOS_ _mas_ - Create a [security scoped bookmark](https://developer.apple.com/library/content/documentation/Security/Conceptual/AppSandboxDesignGuide/AppSandboxInDepth/AppSandboxInDepth.html#//apple_ref/doc/uid/TP40011183-CH3-SW16) when packaged for the Mac App Store. If this option is enabled and the file doesn't already exist a blank file will be created at the chosen path.
* `callback` Function (optional) * `callback` Function (optional)
* `filename` String * `filename` String
* `bookmark` String _macOS_ _mas_ - Base64 encoded string which contains the security scoped bookmark data for the saved file. `securityScopedBookmarks` must be enabled for this to be present.
Returns `String`, the path of the file chosen by the user, Returns `String`, the path of the file chosen by the user,
if a callback is provided it returns `undefined`. if a callback is provided it returns `undefined`.

View file

@ -740,6 +740,11 @@
'atom/app/node_main.h', 'atom/app/node_main.h',
], ],
}], # enable_run_as_node }], # enable_run_as_node
['mas_build==1', {
'lib_sources': [
'atom/browser/api/atom_api_app_mas.mm',
],
}], # mas_build==1
], ],
}, },
} }

View file

@ -83,7 +83,7 @@ module.exports = {
} }
} }
let {buttonLabel, defaultPath, filters, properties, title, message} = options let {buttonLabel, defaultPath, filters, properties, title, message, securityScopedBookmarks = false} = options
if (properties == null) { if (properties == null) {
properties = ['openFile'] properties = ['openFile']
@ -126,10 +126,10 @@ module.exports = {
throw new TypeError('Message must be a string') throw new TypeError('Message must be a string')
} }
const wrappedCallback = typeof callback === 'function' ? function (success, result) { const wrappedCallback = typeof callback === 'function' ? function (success, result, bookmarkData) {
return callback(success ? result : void 0) return success ? callback(result, bookmarkData) : callback()
} : null } : null
const settings = {title, buttonLabel, defaultPath, filters, message, window} const settings = {title, buttonLabel, defaultPath, filters, message, securityScopedBookmarks, window}
settings.properties = dialogProperties settings.properties = dialogProperties
return binding.showOpenDialog(settings, wrappedCallback) return binding.showOpenDialog(settings, wrappedCallback)
}, },
@ -145,7 +145,7 @@ module.exports = {
} }
} }
let {buttonLabel, defaultPath, filters, title, message, nameFieldLabel, showsTagField} = options let {buttonLabel, defaultPath, filters, title, message, securityScopedBookmarks = false, nameFieldLabel, showsTagField} = options
if (title == null) { if (title == null) {
title = '' title = ''
@ -185,10 +185,10 @@ module.exports = {
showsTagField = true showsTagField = true
} }
const wrappedCallback = typeof callback === 'function' ? function (success, result) { const wrappedCallback = typeof callback === 'function' ? function (success, result, bookmarkData) {
return callback(success ? result : void 0) return success ? callback(result, bookmarkData) : callback()
} : null } : null
const settings = {title, buttonLabel, defaultPath, filters, message, nameFieldLabel, showsTagField, window} const settings = {title, buttonLabel, defaultPath, filters, message, securityScopedBookmarks, nameFieldLabel, showsTagField, window}
return binding.showSaveDialog(settings, wrappedCallback) return binding.showSaveDialog(settings, wrappedCallback)
}, },