From 00a32c79d14f22ff28843f647d59d5ff1450123a Mon Sep 17 00:00:00 2001 From: Mark Hammond <mhammond@skippinet.com.au> Date: Fri, 12 Sep 2014 13:24:10 +1000 Subject: [PATCH] Bug 506446 - nsPermissionManager now reads a file with default permissions. r=bsmedberg --- browser/app/default_permissions | 9 + browser/app/jar.mn | 4 + browser/app/moz.build | 2 + extensions/cookie/nsPermissionManager.cpp | 177 ++++++++++++++++-- extensions/cookie/nsPermissionManager.h | 12 +- .../test/unit/test_permmanager_defaults.js | 177 ++++++++++++++++++ extensions/cookie/test/unit/xpcshell.ini | 1 + 7 files changed, 366 insertions(+), 16 deletions(-) create mode 100644 browser/app/default_permissions create mode 100644 browser/app/jar.mn create mode 100644 extensions/cookie/test/unit/test_permmanager_defaults.js diff --git a/browser/app/default_permissions b/browser/app/default_permissions new file mode 100644 index 0000000000000..dc7ea54726cf4 --- /dev/null +++ b/browser/app/default_permissions @@ -0,0 +1,9 @@ +# This file has default permissions for the permission manager. +# The file-format is strict: +# * matchtype \t type \t permission \t host +# * Only "host" is supported for matchtype +# * type is a string that identifies the type of permission (e.g. "cookie") +# * permission is an integer between 1 and 15 +# See nsPermissionManager.cpp for more... + +# (This file is intentionally blank for the moment...) diff --git a/browser/app/jar.mn b/browser/app/jar.mn new file mode 100644 index 0000000000000..e02941263b390 --- /dev/null +++ b/browser/app/jar.mn @@ -0,0 +1,4 @@ +browser.jar: + +# The file that holds the default permissions (which is loaded by nsPermissionManager) for the browser. + default_permissions (default_permissions) diff --git a/browser/app/moz.build b/browser/app/moz.build index 3d22eab10b044..e1c51cb5a8a8d 100644 --- a/browser/app/moz.build +++ b/browser/app/moz.build @@ -72,3 +72,5 @@ if CONFIG['MOZ_LINKER']: if CONFIG['HAVE_CLOCK_MONOTONIC']: OS_LIBS += CONFIG['REALTIME_LIBS'] + +JAR_MANIFESTS += ['jar.mn'] diff --git a/extensions/cookie/nsPermissionManager.cpp b/extensions/cookie/nsPermissionManager.cpp index 6192c4575e53e..2382b9a56b26f 100644 --- a/extensions/cookie/nsPermissionManager.cpp +++ b/extensions/cookie/nsPermissionManager.cpp @@ -20,6 +20,7 @@ #include "nsILineInputStream.h" #include "nsIIDNService.h" #include "nsAppDirectoryServiceDefs.h" +#include "nsDirectoryServiceDefs.h" #include "prprf.h" #include "mozilla/storage.h" #include "mozilla/Attributes.h" @@ -33,6 +34,8 @@ #include "nsPIDOMWindow.h" #include "nsIDocument.h" #include "mozilla/net/NeckoMessageUtils.h" +#include "mozilla/Preferences.h" +#include "nsReadLine.h" static nsPermissionManager *gPermissionManager = nullptr; @@ -358,6 +361,12 @@ static const char kPermissionsFileName[] = "permissions.sqlite"; static const char kHostpermFileName[] = "hostperm.1"; +// Default permissions are read from a URL - this is the preference we read +// to find that URL. +static const char kDefaultsUrlPrefName[] = "permissions.manager.defaultsUrl"; +// If the pref above doesn't exist, the URL we use by default. +static const char kDefaultsUrl[] = "resource://app/chrome/browser/default_permissions"; + static const char kPermissionChangeNotification[] = PERM_CHANGE_NOTIFICATION; NS_IMPL_ISUPPORTS(nsPermissionManager, nsIPermissionManager, nsIObserver, nsISupportsWeakReference) @@ -589,6 +598,8 @@ nsPermissionManager::InitDB(bool aRemoveFile) getter_AddRefs(mStmtUpdate)); NS_ENSURE_SUCCESS(rv, rv); + // Always import default permissions. + ImportDefaults(); // check whether to import or just read in the db if (tableExists) return Read(); @@ -743,6 +754,12 @@ nsPermissionManager::AddInternal(nsIPrincipal* aPrincipal, (aExpireType == nsIPermissionManager::EXPIRE_NEVER || aExpireTime == oldPermissionEntry.mExpireTime)) op = eOperationNone; + else if (oldPermissionEntry.mID == cIDPermissionIsDefault) + // The existing permission is one added as a default and the new permission + // doesn't exactly match so we are replacing the default. This is true + // even if the new permission is UNKNOWN_ACTION (which means a "logical + // remove" of the default) + op = eOperationReplacingDefault; else if (aPermission == nsIPermissionManager::UNKNOWN_ACTION) op = eOperationRemoving; else @@ -869,6 +886,62 @@ nsPermissionManager::AddInternal(nsIPrincipal* aPrincipal, break; } + case eOperationReplacingDefault: + { + // this is handling the case when we have an existing permission + // entry that was created as a "default" (and thus isn't in the DB) with + // an explicit permission (that may include UNKNOWN_ACTION.) + // Note we will *not* get here if we are replacing an already replaced + // default value - that is handled as eOperationChanging. + + // So this is a hybrid of eOperationAdding (as we are writing a new entry + // to the DB) and eOperationChanging (as we are replacing the in-memory + // repr and sending a "changed" notification). + + // We want a new ID even if not writing to the DB, so the modified entry + // in memory doesn't have the magic cIDPermissionIsDefault value. + id = ++mLargestID; + + // The default permission being replaced can't have session expiry. + NS_ENSURE_TRUE(entry->GetPermissions()[index].mExpireType != nsIPermissionManager::EXPIRE_SESSION, + NS_ERROR_UNEXPECTED); + // We don't support the new entry having any expiry - supporting that would + // make things far more complex and none of the permissions we set as a + // default support that. + NS_ENSURE_TRUE(aExpireType == EXPIRE_NEVER, NS_ERROR_UNEXPECTED); + + // update the existing entry in memory. + entry->GetPermissions()[index].mID = id; + entry->GetPermissions()[index].mPermission = aPermission; + entry->GetPermissions()[index].mExpireType = aExpireType; + entry->GetPermissions()[index].mExpireTime = aExpireTime; + + // If requested, create the entry in the DB. + if (aDBOperation == eWriteToDB) { + uint32_t appId; + rv = aPrincipal->GetAppId(&appId); + NS_ENSURE_SUCCESS(rv, rv); + + bool isInBrowserElement; + rv = aPrincipal->GetIsInBrowserElement(&isInBrowserElement); + NS_ENSURE_SUCCESS(rv, rv); + + UpdateDB(eOperationAdding, mStmtInsert, id, host, aType, aPermission, aExpireType, aExpireTime, appId, isInBrowserElement); + } + + if (aNotifyOperation == eNotify) { + NotifyObserversWithPermission(host, + entry->GetKey()->mAppId, + entry->GetKey()->mIsInBrowserElement, + mTypeArray[typeIndex], + aPermission, + aExpireType, + aExpireTime, + MOZ_UTF16("changed")); + } + + } + break; } return NS_OK; @@ -944,6 +1017,10 @@ nsPermissionManager::RemoveAllInternal(bool aNotifyObservers) // database is authoritative, we do not need confirmation from the // on-disk database to notify observers. RemoveAllFromMemory(); + + // Re-import the defaults + ImportDefaults(); + if (aNotifyObservers) { NotifyObservers(nullptr, MOZ_UTF16("cleared")); } @@ -1262,6 +1339,13 @@ AddPermissionsToList(nsPermissionManager::PermissionHashKey* entry, void *arg) for (uint32_t i = 0; i < entry->GetPermissions().Length(); ++i) { nsPermissionManager::PermissionEntry& permEntry = entry->GetPermissions()[i]; + // given how "default" permissions work and the possibility of them being + // overridden with UNKNOWN_ACTION, we might see this value here - but we + // do *not* want to return them via the enumerator. + if (permEntry.mPermission == nsIPermissionManager::UNKNOWN_ACTION) { + continue; + } + nsPermission *perm = new nsPermission(entry->GetKey()->mHost, entry->GetKey()->mAppId, entry->GetKey()->mIsInBrowserElement, @@ -1633,11 +1717,12 @@ nsPermissionManager::Read() static const char kMatchTypeHost[] = "host"; +// Import() will read a file from the profile directory and add them to the +// database before deleting the file - ie, this is a one-shot operation that +// will not succeed on subsequent runs as the file imported from is removed. nsresult nsPermissionManager::Import() { - ENSURE_NOT_CHILD_PROCESS; - nsresult rv; nsCOMPtr<nsIFile> permissionsFile; @@ -1650,14 +1735,73 @@ nsPermissionManager::Import() nsCOMPtr<nsIInputStream> fileInputStream; rv = NS_NewLocalFileInputStream(getter_AddRefs(fileInputStream), permissionsFile); - if (NS_FAILED(rv)) return rv; + NS_ENSURE_SUCCESS(rv, rv); + + rv = _DoImport(fileInputStream, mDBConn); + NS_ENSURE_SUCCESS(rv, rv); + + // we successfully imported and wrote to the DB - delete the old file. + permissionsFile->Remove(false); + return NS_OK; +} + +// ImportDefaults will read a URL with default permissions and add them to the +// in-memory copy of permissions. The database is *not* written to. +nsresult +nsPermissionManager::ImportDefaults() +{ + // We allow prefs to override the default permissions URI, mainly as a hook + // for testing. + nsCString defaultsURL; + if (mozilla::Preferences::HasUserValue(kDefaultsUrlPrefName)) { + defaultsURL = mozilla::Preferences::GetCString(kDefaultsUrlPrefName); + } else { + defaultsURL = NS_LITERAL_CSTRING(kDefaultsUrl); + } + + nsresult rv; + nsCOMPtr<nsIIOService> ioservice = + do_GetService("@mozilla.org/network/io-service;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIURI> defaultsURI; + rv = NS_NewURI(getter_AddRefs(defaultsURI), defaultsURL, + nullptr, nullptr, ioservice); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIChannel> channel; + rv = ioservice->NewChannelFromURI(defaultsURI, getter_AddRefs(channel)); + NS_ENSURE_SUCCESS(rv, rv); - nsCOMPtr<nsILineInputStream> lineInputStream = do_QueryInterface(fileInputStream, &rv); + nsCOMPtr<nsIInputStream> inputStream; + rv = channel->Open(getter_AddRefs(inputStream)); NS_ENSURE_SUCCESS(rv, rv); + rv = _DoImport(inputStream, nullptr); + inputStream->Close(); + return rv; +} + +// _DoImport reads the specified stream and adds the parsed elements. If +// |conn| is passed, the imported data will be written to the database, but if +// |conn| is null the data will be added only to the in-memory copy of the +// database. +nsresult +nsPermissionManager::_DoImport(nsIInputStream *inputStream, mozIStorageConnection *conn) +{ + ENSURE_NOT_CHILD_PROCESS; + + nsresult rv; // start a transaction on the storage db, to optimize insertions. // transaction will automically commit on completion - mozStorageTransaction transaction(mDBConn, true); + // (note the transaction is a no-op if a null connection is passed) + mozStorageTransaction transaction(conn, true); + + // The DB operation - we only try and write if a connection was passed. + DBOperationType operation = conn ? eWriteToDB : eNoDBOperation; + // and if no DB connection was passed we assume this is a "default" permission, + // so use the special ID which indicates this. + int64_t id = conn ? 0 : cIDPermissionIsDefault; /* format is: * matchtype \t type \t permission \t host @@ -1666,17 +1810,24 @@ nsPermissionManager::Import() * permission is an integer between 1 and 15 */ - nsAutoCString buffer; + // Ideally we'd do this with nsILineInputString, but this is called with an + // nsIInputStream that comes from a resource:// URI, which doesn't support + // that interface. So NS_ReadLine to the rescue... + nsLineBuffer<char> lineBuffer; + nsCString line; bool isMore = true; - while (isMore && NS_SUCCEEDED(lineInputStream->ReadLine(buffer, &isMore))) { - if (buffer.IsEmpty() || buffer.First() == '#') { + do { + rv = NS_ReadLine(inputStream, &lineBuffer, line, &isMore); + NS_ENSURE_SUCCESS(rv, rv); + + if (line.IsEmpty() || line.First() == '#') { continue; } nsTArray<nsCString> lineArray; // Split the line at tabs - ParseString(buffer, '\t', lineArray); + ParseString(line, '\t', lineArray); if (lineArray[0].EqualsLiteral(kMatchTypeHost) && lineArray.Length() == 4) { @@ -1697,14 +1848,12 @@ nsPermissionManager::Import() nsresult rv = GetPrincipal(lineArray[3], getter_AddRefs(principal)); NS_ENSURE_SUCCESS(rv, rv); - rv = AddInternal(principal, lineArray[1], permission, 0, - nsIPermissionManager::EXPIRE_NEVER, 0, eDontNotify, eWriteToDB); + rv = AddInternal(principal, lineArray[1], permission, id, + nsIPermissionManager::EXPIRE_NEVER, 0, eDontNotify, operation); NS_ENSURE_SUCCESS(rv, rv); } - } - // we're done importing - delete the old file - permissionsFile->Remove(false); + } while (isMore); return NS_OK; } diff --git a/extensions/cookie/nsPermissionManager.h b/extensions/cookie/nsPermissionManager.h index ec8bac866118f..1755d891bc236 100644 --- a/extensions/cookie/nsPermissionManager.h +++ b/extensions/cookie/nsPermissionManager.h @@ -11,7 +11,7 @@ #include "nsIObserverService.h" #include "nsWeakReference.h" #include "nsCOMPtr.h" -#include "nsIFile.h" +#include "nsIInputStream.h" #include "nsTHashtable.h" #include "nsTArray.h" #include "nsString.h" @@ -175,7 +175,8 @@ public: eOperationNone, eOperationAdding, eOperationRemoving, - eOperationChanging + eOperationChanging, + eOperationReplacingDefault }; enum DBOperationType { @@ -188,6 +189,11 @@ public: eNotify }; + // A special value for a permission ID that indicates the ID was loaded as + // a default value. These will never be written to the database, but may + // be overridden with an explicit permission (including UNKNOWN_ACTION) + static const int64_t cIDPermissionIsDefault = -1; + nsresult AddInternal(nsIPrincipal* aPrincipal, const nsAFlatCString &aType, uint32_t aPermission, @@ -226,6 +232,8 @@ private: nsresult InitDB(bool aRemoveFile); nsresult CreateTable(); nsresult Import(); + nsresult ImportDefaults(); + nsresult _DoImport(nsIInputStream *inputStream, mozIStorageConnection *aConn); nsresult Read(); void NotifyObserversWithPermission(const nsACString &aHost, uint32_t aAppId, diff --git a/extensions/cookie/test/unit/test_permmanager_defaults.js b/extensions/cookie/test/unit/test_permmanager_defaults.js new file mode 100644 index 0000000000000..ff0c454262ae0 --- /dev/null +++ b/extensions/cookie/test/unit/test_permmanager_defaults.js @@ -0,0 +1,177 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// The origin we use in most of the tests. +const TEST_ORIGIN = "example.org"; +const TEST_PERMISSION = "test-permission"; + +function run_test() { + run_next_test(); +} + +add_task(function* do_test() { + // setup a profile. + do_get_profile(); + + // create a file in the temp directory with the defaults. + let file = do_get_tempdir(); + file.append("test_default_permissions"); + + // write our test data to it. + let ostream = Cc["@mozilla.org/network/file-output-stream;1"]. + createInstance(Ci.nsIFileOutputStream); + ostream.init(file, -1, 0666, 0); + let conv = Cc["@mozilla.org/intl/converter-output-stream;1"]. + createInstance(Ci.nsIConverterOutputStream); + conv.init(ostream, "UTF-8", 0, 0); + + conv.writeString("# this is a comment\n"); + conv.writeString("\n"); // a blank line! + conv.writeString("host\t" + TEST_PERMISSION + "\t1\t" + TEST_ORIGIN + "\n"); + ostream.close(); + + // Set the preference used by the permission manager so the file is read. + Services.prefs.setCharPref("permissions.manager.defaultsUrl", "file://" + file.path); + + // initialize the permission manager service - it will read that default. + let pm = Cc["@mozilla.org/permissionmanager;1"]. + getService(Ci.nsIPermissionManager); + + // test the default permission was applied. + let permURI = NetUtil.newURI("http://" + TEST_ORIGIN); + let principal = Services.scriptSecurityManager.getNoAppCodebasePrincipal(permURI); + + do_check_eq(Ci.nsIPermissionManager.ALLOW_ACTION, + pm.testPermissionFromPrincipal(principal, TEST_PERMISSION)); + + // the permission should exist in the enumerator. + do_check_eq(Ci.nsIPermissionManager.ALLOW_ACTION, findCapabilityViaEnum()); + // but should not have been written to the DB + yield checkCapabilityViaDB(null); + + // remove all should not throw and the default should remain + pm.removeAll(); + + do_check_eq(Ci.nsIPermissionManager.ALLOW_ACTION, + pm.testPermissionFromPrincipal(principal, TEST_PERMISSION)); + + // Asking for this permission to be removed should result in that permission + // having UNKNOWN_ACTION + pm.removeFromPrincipal(principal, TEST_PERMISSION); + do_check_eq(Ci.nsIPermissionManager.UNKNOWN_ACTION, + pm.testPermissionFromPrincipal(principal, TEST_PERMISSION)); + // and we should have this UNKNOWN_ACTION reflected in the DB + yield checkCapabilityViaDB(Ci.nsIPermissionManager.UNKNOWN_ACTION); + // but the permission should *not* appear in the enumerator. + do_check_eq(null, findCapabilityViaEnum()); + + // and a subsequent RemoveAll should restore the default + pm.removeAll(); + + do_check_eq(Ci.nsIPermissionManager.ALLOW_ACTION, + pm.testPermissionFromPrincipal(principal, TEST_PERMISSION)); + // and allow it to again be seen in the enumerator. + do_check_eq(Ci.nsIPermissionManager.ALLOW_ACTION, findCapabilityViaEnum()); + + // now explicitly add a permission - this too should override the default. + pm.addFromPrincipal(principal, TEST_PERMISSION, Ci.nsIPermissionManager.DENY_ACTION); + + // it should be reflected in a permission check, in the enumerator and the DB + do_check_eq(Ci.nsIPermissionManager.DENY_ACTION, + pm.testPermissionFromPrincipal(principal, TEST_PERMISSION)); + do_check_eq(Ci.nsIPermissionManager.DENY_ACTION, findCapabilityViaEnum()); + yield checkCapabilityViaDB(Ci.nsIPermissionManager.DENY_ACTION); + + // explicitly add a different permission - in this case we are no longer + // replacing the default, but instead replacing the replacement! + pm.addFromPrincipal(principal, TEST_PERMISSION, Ci.nsIPermissionManager.PROMPT_ACTION); + + // it should be reflected in a permission check, in the enumerator and the DB + do_check_eq(Ci.nsIPermissionManager.PROMPT_ACTION, + pm.testPermissionFromPrincipal(principal, TEST_PERMISSION)); + do_check_eq(Ci.nsIPermissionManager.PROMPT_ACTION, findCapabilityViaEnum()); + yield checkCapabilityViaDB(Ci.nsIPermissionManager.PROMPT_ACTION); + + // remove the temp file we created. + file.remove(false); +}); + +// use an enumerator to find the requested permission. Returns the permission +// value (ie, the "capability" in nsIPermission parlance) or null if it can't +// be found. +function findCapabilityViaEnum(host = TEST_ORIGIN, type = TEST_PERMISSION) { + let result = undefined; + let e = Services.perms.enumerator; + while (e.hasMoreElements()) { + let perm = e.getNext().QueryInterface(Ci.nsIPermission); + if (perm.host == host && + perm.type == type) { + if (result !== undefined) { + // we've already found one previously - that's bad! + do_throw("enumerator found multiple entries"); + } + result = perm.capability; + } + } + return result || null; +} + +// A function to check the DB has the specified capability. As the permission +// manager uses async DB operations without a completion callback, the +// distinct possibility exists that our checking of the DB will happen before +// the permission manager update has completed - so we just retry a few times. +// Returns a promise. +function checkCapabilityViaDB(expected, host = TEST_ORIGIN, type = TEST_PERMISSION) { + let deferred = Promise.defer(); + let count = 0; + let max = 20; + let do_check = () => { + let got = findCapabilityViaDB(host, type); + if (got == expected) { + // the do_check_eq() below will succeed - which is what we want. + do_check_eq(got, expected, "The database has the expected value"); + deferred.resolve(); + return; + } + // value isn't correct - see if we've retried enough + if (count++ == max) { + // the do_check_eq() below will fail - which is what we want. + do_check_eq(got, expected, "The database wasn't updated with the expected value"); + deferred.resolve(); + return; + } + // we can retry... + do_timeout(100, do_check); + } + do_check(); + return deferred.promise; +} + +// use the DB to find the requested permission. Returns the permission +// value (ie, the "capability" in nsIPermission parlance) or null if it can't +// be found. +function findCapabilityViaDB(host = TEST_ORIGIN, type = TEST_PERMISSION) { + let file = Services.dirsvc.get("ProfD", Ci.nsIFile); + file.append("permissions.sqlite"); + + let storage = Cc["@mozilla.org/storage/service;1"] + .getService(Ci.mozIStorageService); + + let connection = storage.openDatabase(file); + + let query = connection.createStatement( + "SELECT permission FROM moz_hosts WHERE host = :host AND type = :type"); + query.bindByName("host", host); + query.bindByName("type", type); + + if (!query.executeStep()) { + // no row + return null; + } + let result = query.getInt32(0); + if (query.executeStep()) { + // this is bad - we never expect more than 1 row here. + do_throw("More than 1 row found!") + } + return result; +} diff --git a/extensions/cookie/test/unit/xpcshell.ini b/extensions/cookie/test/unit/xpcshell.ini index dcf3104909468..070183a2554e7 100644 --- a/extensions/cookie/test/unit/xpcshell.ini +++ b/extensions/cookie/test/unit/xpcshell.ini @@ -18,6 +18,7 @@ support-files = [test_cookies_thirdparty_session.js] [test_domain_eviction.js] [test_eviction.js] +[test_permmanager_defaults.js] [test_permmanager_expiration.js] [test_permmanager_getPermissionObject.js] [test_permmanager_notifications.js] -- GitLab