Bug 1028398 - FxA will silently provide user's email to privileged apps in 2.0. Part 1: Add moz-firefox-accounts permission. r=jedp, r=fabrice, a=2.0+
authorFernando Jiménez <ferjmoreno@gmail.com>
Fri, 11 Jul 2014 16:13:32 +0200
changeset 207889 5525d9118f72043c073b8e45eb9e0d2145f7eef6
parent 207888 5b786c7afc790b66c4fd2299dc2d50b25acec571
child 207890 76080ed1f8debe59488647f45c76632c09f500ab
push id3741
push userasasaki@mozilla.com
push dateMon, 21 Jul 2014 20:25:18 +0000
treeherdermozilla-beta@4d6f46f5af68 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjedp, fabrice, 2
bugs1028398
milestone32.0a2
Bug 1028398 - FxA will silently provide user's email to privileged apps in 2.0. Part 1: Add moz-firefox-accounts permission. r=jedp, r=fabrice, a=2.0+
dom/apps/src/PermissionsTable.jsm
dom/identity/DOMIdentity.jsm
dom/identity/nsDOMIdentity.js
--- a/dom/apps/src/PermissionsTable.jsm
+++ b/dom/apps/src/PermissionsTable.jsm
@@ -357,16 +357,27 @@ this.PermissionsTable =  { geolocation: 
                            // This permission doesn't actually grant access to
                            // anything. It exists only to check the correctness
                            // of web prompt composed permissions in tests.
                            "test-permission": {
                              app: PROMPT_ACTION,
                              privileged: PROMPT_ACTION,
                              certified: ALLOW_ACTION,
                              access: ["read", "write", "create"]
+                           },
+                           "firefox-accounts": {
+                             app: DENY_ACTION,
+                             privileged: DENY_ACTION,
+                             certified: ALLOW_ACTION
+                           },
+                           "moz-firefox-accounts": {
+                             app: DENY_ACTION,
+                             privileged: PROMPT_ACTION,
+                             certified: ALLOW_ACTION,
+                             substitute: ["firefox-accounts"]
                            }
                          };
 
 /**
  * Append access modes to the permission name as suffixes.
  *   e.g. permission name 'contacts' with ['read', 'write'] =
  *   ['contacts-read', contacts-write']
  * @param string aPermName
--- a/dom/identity/DOMIdentity.jsm
+++ b/dom/identity/DOMIdentity.jsm
@@ -5,16 +5,17 @@
 "use strict";
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 const PREF_FXA_ENABLED = "identity.fxaccounts.enabled";
+const FXA_PERMISSION = "firefox-accounts";
 
 // This is the parent process corresponding to nsDOMIdentity.
 this.EXPORTED_SYMBOLS = ["DOMIdentity"];
 
 XPCOMUtils.defineLazyModuleGetter(this, "objectCopy",
                                   "resource://gre/modules/identity/IdentityUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "IdentityService",
@@ -33,16 +34,20 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this,
                                   "Logger",
                                   "resource://gre/modules/identity/LogUtils.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
                                    "@mozilla.org/parentprocessmessagemanager;1",
                                    "nsIMessageListenerManager");
 
+XPCOMUtils.defineLazyServiceGetter(this, "permissionManager",
+                                   "@mozilla.org/permissionmanager;1",
+                                   "nsIPermissionManager");
+
 function log(...aMessageArgs) {
   Logger.log.apply(Logger, ["DOMIdentity"].concat(aMessageArgs));
 }
 
 function IDDOMMessage(aOptions) {
   objectCopy(aOptions, this);
 }
 
@@ -229,24 +234,50 @@ this.DOMIdentity = {
    * Delete the RPWatchContext object for a given message manager.  Removes the
    * mapping both from _serviceContexts and _mmContexts.
    */
   deleteContextForMM: function(targetMM) {
     this._serviceContexts.delete(this._mmContexts.get(targetMM));
     this._mmContexts.delete(targetMM);
   },
 
+  hasPermission: function(aMessage) {
+    // We only check that the firefox accounts permission is present in the
+    // manifest.
+    if (aMessage.json && aMessage.json.wantIssuer == "firefox-accounts") {
+      if (!aMessage.principal) {
+        return false;
+      }
+      let secMan = Cc["@mozilla.org/scriptsecuritymanager;1"]
+                     .getService(Ci.nsIScriptSecurityManager);
+      let uri = Services.io.newURI(aMessage.principal.origin, null, null);
+      let principal = secMan.getAppCodebasePrincipal(uri,
+        aMessage.principal.appId, aMessage.principal.isInBrowserElement);
+
+      let permission =
+        permissionManager.testPermissionFromPrincipal(principal,
+                                                      FXA_PERMISSION);
+      return permission != Ci.nsIPermissionManager.UNKNOWN_ACTION &&
+             permission != Ci.nsIPermissionManager.DENY_ACTION;
+    }
+    return true;
+  },
+
   // nsIMessageListener
   receiveMessage: function DOMIdentity_receiveMessage(aMessage) {
     let msg = aMessage.json;
 
     // Target is the frame message manager that called us and is
     // used to send replies back to the proper window.
     let targetMM = aMessage.target;
 
+    if (!this.hasPermission(aMessage)) {
+      throw new Error("PERMISSION_DENIED");
+    }
+
     switch (aMessage.name) {
       // RP
       case "Identity:RP:Watch":
         this._watch(msg, targetMM);
         break;
       case "Identity:RP:Unwatch":
         this._unwatch(msg, targetMM);
         break;
--- a/dom/identity/nsDOMIdentity.js
+++ b/dom/identity/nsDOMIdentity.js
@@ -35,18 +35,16 @@ XPCOMUtils.defineLazyServiceGetter(this,
 
 // This is the child process corresponding to nsIDOMIdentity
 XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
                                    "@mozilla.org/childprocessmessagemanager;1",
                                    "nsIMessageSender");
 
 
 const ERRORS = {
-  "ERROR_NOT_AUTHORIZED_FOR_FIREFOX_ACCOUNTS":
-    "Only privileged and certified apps may use Firefox Accounts",
   "ERROR_INVALID_ASSERTION_AUDIENCE":
     "Assertion audience may not differ from origin",
   "ERROR_REQUEST_WHILE_NOT_HANDLING_USER_INPUT":
     "The request() method may only be invoked when handling user input",
 };
 
 function nsDOMIdentity(aIdentityInternal) {
   this._identityInternal = aIdentityInternal;
@@ -145,17 +143,22 @@ nsDOMIdentity.prototype = {
     this._rpWatcher.audience = message.audience;
 
     if (message.errors.length) {
       this.reportErrors(message);
       // We don't delete the rpWatcher object, because we don't want the
       // broken client to be able to call watch() any more.  It's broken.
       return;
     }
-    this._identityInternal._mm.sendAsyncMessage("Identity:RP:Watch", message);
+    this._identityInternal._mm.sendAsyncMessage(
+      "Identity:RP:Watch",
+      message,
+      null,
+      this._window.document.nodePrincipal
+    );
   },
 
   request: function nsDOMIdentity_request(aOptions = {}) {
     aOptions = Cu.waiveXrays(aOptions);
     this._log("request: " + JSON.stringify(aOptions));
 
     // Has the caller called watch() before this?
     if (!this._rpWatcher) {
@@ -216,17 +219,22 @@ nsDOMIdentity.prototype = {
         throw new Error("oncancel is not a function");
       } else {
         // Store optional cancel callback for later.
         this._onCancelRequestCallback = aOptions.oncancel;
       }
     }
 
     this._rpCalls++;
-    this._identityInternal._mm.sendAsyncMessage("Identity:RP:Request", message);
+    this._identityInternal._mm.sendAsyncMessage(
+      "Identity:RP:Request",
+      message,
+      null,
+      this._window.document.nodePrincipal
+    );
   },
 
   logout: function nsDOMIdentity_logout() {
     if (!this._rpWatcher) {
       throw new Error("navigator.id.logout called before navigator.id.watch");
     }
     if (this._rpCalls > MAX_RP_CALLS) {
       throw new Error("navigator.id.logout called too many times");
@@ -236,17 +244,22 @@ nsDOMIdentity.prototype = {
     let message = this.DOMIdentityMessage();
 
     // Report and fail hard on any errors.
     if (message.errors.length) {
       this.reportErrors(message);
       return;
     }
 
-    this._identityInternal._mm.sendAsyncMessage("Identity:RP:Logout", message);
+    this._identityInternal._mm.sendAsyncMessage(
+      "Identity:RP:Logout",
+      message,
+      null,
+      this._window.document.nodePrincipal
+    );
   },
 
   /*
    * Get an assertion.  This function is deprecated.  RPs are
    * encouraged to use the observer API instead (watch + request).
    */
   get: function nsDOMIdentity_get(aCallback, aOptions) {
     var opts = {};
@@ -319,65 +332,83 @@ nsDOMIdentity.prototype = {
     if (this._beginProvisioningCallback) {
       throw new Error("navigator.id.beginProvisioning already called.");
     }
     if (!aCallback || typeof(aCallback) !== "function") {
       throw new Error("beginProvisioning callback is required.");
     }
 
     this._beginProvisioningCallback = aCallback;
-    this._identityInternal._mm.sendAsyncMessage("Identity:IDP:BeginProvisioning",
-                                                this.DOMIdentityMessage());
+    this._identityInternal._mm.sendAsyncMessage(
+      "Identity:IDP:BeginProvisioning",
+      this.DOMIdentityMessage(),
+      null,
+      this._window.document.nodePrincipal
+    );
   },
 
   genKeyPair: function nsDOMIdentity_genKeyPair(aCallback) {
     this._log("genKeyPair");
     if (!this._beginProvisioningCallback) {
       throw new Error("navigator.id.genKeyPair called outside of provisioning");
     }
     if (this._genKeyPairCallback) {
       throw new Error("navigator.id.genKeyPair already called.");
     }
     if (!aCallback || typeof(aCallback) !== "function") {
       throw new Error("genKeyPair callback is required.");
     }
 
     this._genKeyPairCallback = aCallback;
-    this._identityInternal._mm.sendAsyncMessage("Identity:IDP:GenKeyPair",
-                                                this.DOMIdentityMessage());
+    this._identityInternal._mm.sendAsyncMessage(
+      "Identity:IDP:GenKeyPair",
+      this.DOMIdentityMessage(),
+      null,
+      this._window.document.nodePrincipal
+    );
   },
 
   registerCertificate: function nsDOMIdentity_registerCertificate(aCertificate) {
     this._log("registerCertificate");
     if (!this._genKeyPairCallback) {
       throw new Error("navigator.id.registerCertificate called outside of provisioning");
     }
     if (this._provisioningEnded) {
       throw new Error("Provisioning already ended");
     }
     this._provisioningEnded = true;
 
     let message = this.DOMIdentityMessage();
     message.cert = aCertificate;
-    this._identityInternal._mm.sendAsyncMessage("Identity:IDP:RegisterCertificate", message);
+    this._identityInternal._mm.sendAsyncMessage(
+      "Identity:IDP:RegisterCertificate",
+      message,
+      null,
+      this._window.document.nodePrincipal
+    );
   },
 
   raiseProvisioningFailure: function nsDOMIdentity_raiseProvisioningFailure(aReason) {
     this._log("raiseProvisioningFailure '" + aReason + "'");
     if (this._provisioningEnded) {
       throw new Error("Provisioning already ended");
     }
     if (!aReason || typeof(aReason) != "string") {
       throw new Error("raiseProvisioningFailure reason is required");
     }
     this._provisioningEnded = true;
 
     let message = this.DOMIdentityMessage();
     message.reason = aReason;
-    this._identityInternal._mm.sendAsyncMessage("Identity:IDP:ProvisioningFailure", message);
+    this._identityInternal._mm.sendAsyncMessage(
+      "Identity:IDP:ProvisioningFailure",
+      message,
+      null,
+      this._window.document.nodePrincipal
+    );
   },
 
   /**
    *  Identity Provider (IDP) Authentication APIs
    */
 
   beginAuthentication: function nsDOMIdentity_beginAuthentication(aCallback) {
     this._log("beginAuthentication");
@@ -387,44 +418,57 @@ nsDOMIdentity.prototype = {
     if (typeof(aCallback) !== "function") {
       throw new Error("beginAuthentication callback is required.");
     }
     if (!aCallback || typeof(aCallback) !== "function") {
       throw new Error("beginAuthentication callback is required.");
     }
 
     this._beginAuthenticationCallback = aCallback;
-    this._identityInternal._mm.sendAsyncMessage("Identity:IDP:BeginAuthentication",
-                                                this.DOMIdentityMessage());
+    this._identityInternal._mm.sendAsyncMessage(
+      "Identity:IDP:BeginAuthentication",
+      this.DOMIdentityMessage(),
+      null,
+      this._window.document.nodePrincipal
+    );
   },
 
   completeAuthentication: function nsDOMIdentity_completeAuthentication() {
     if (this._authenticationEnded) {
       throw new Error("Authentication already ended");
     }
     if (!this._beginAuthenticationCallback) {
       throw new Error("navigator.id.completeAuthentication called outside of authentication");
     }
     this._authenticationEnded = true;
 
-    this._identityInternal._mm.sendAsyncMessage("Identity:IDP:CompleteAuthentication",
-                                                this.DOMIdentityMessage());
+    this._identityInternal._mm.sendAsyncMessage(
+      "Identity:IDP:CompleteAuthentication",
+      this.DOMIdentityMessage(),
+      null,
+      this._window.document.nodePrincipal
+    );
   },
 
   raiseAuthenticationFailure: function nsDOMIdentity_raiseAuthenticationFailure(aReason) {
     if (this._authenticationEnded) {
       throw new Error("Authentication already ended");
     }
     if (!aReason || typeof(aReason) != "string") {
       throw new Error("raiseProvisioningFailure reason is required");
     }
 
     let message = this.DOMIdentityMessage();
     message.reason = aReason;
-    this._identityInternal._mm.sendAsyncMessage("Identity:IDP:AuthenticationFailure", message);
+    this._identityInternal._mm.sendAsyncMessage(
+      "Identity:IDP:AuthenticationFailure",
+      message,
+      null,
+      this._window.document.nodePrincipal
+    );
   },
 
   // Private.
   _init: function nsDOMIdentity__init(aWindow) {
 
     this._initializeState();
 
     // Store window and origin URI.
@@ -505,27 +549,29 @@ nsDOMIdentity.prototype = {
 
         if (this._rpWatcher.onready) {
           this._rpWatcher.onready();
         }
         break;
       case "Identity:RP:Watch:OnCancel":
         // Do we have a watcher?
         if (!this._rpWatcher) {
-          this._log("WARNING: Received OnCancel message, but there is no RP watcher");
+          this._log("WARNING: Received OnCancel message, but there is no RP " +
+                    "watcher");
           return;
         }
 
         if (this._onCancelRequestCallback) {
           this._onCancelRequestCallback();
         }
         break;
       case "Identity:RP:Watch:OnError":
         if (!this._rpWatcher) {
-          this._log("WARNING: Received OnError message, but there is no RP watcher");
+          this._log("WARNING: Received OnError message, but there is no RP " +
+                    "watcher");
           return;
         }
 
         if (this._rpWatcher.onerror) {
           this._rpWatcher.onerror(JSON.stringify({name: msg.message.error}));
         }
         break;
       case "Identity:IDP:CallBeginProvisioningCallback":
@@ -588,52 +634,38 @@ nsDOMIdentity.prototype = {
    * overwrite id, origin, audience, and appStatus.  The caller
    * does not get to set those.
    */
   DOMIdentityMessage: function DOMIdentityMessage(aOptions) {
     aOptions = aOptions || {};
     let message = {
       errors: []
     };
-    let principal = Ci.nsIPrincipal;
 
     objectCopy(aOptions, message);
 
     // outer window id
     message.id = this._id;
 
     // window origin
     message.origin = this._origin;
 
-    // On b2g, an app's status can be NOT_INSTALLED, INSTALLED, PRIVILEGED, or
-    // CERTIFIED.  Compare the appStatus value to the constants enumerated in
-    // Ci.nsIPrincipal.APP_STATUS_*.
-    message.appStatus = this._appStatus;
-
-    // Currently, we only permit certified and privileged apps to use
-    // Firefox Accounts.
-    if (aOptions.wantIssuer == "firefox-accounts" &&
-        this._appStatus !== principal.APP_STATUS_PRIVILEGED &&
-        this._appStatus !== principal.APP_STATUS_CERTIFIED) {
-      message.errors.push("ERROR_NOT_AUTHORIZED_FOR_FIREFOX_ACCOUNTS");
-    }
-
     // Normally the window origin will be the audience in assertions.  On b2g,
     // certified apps have the power to override this and declare any audience
     // the want.  Privileged apps can also declare a different audience, as
     // long as it is the same as the origin specified in their manifest files.
     // All other apps are stuck with b2g origins of the form app://{guid}.
     // Since such an origin is meaningless for the purposes of verification,
     // they will have to jump through some hoops to sign in: Specifically, they
     // will have to host their sign-in flows and DOM API requests in an iframe,
     // have the iframe xhr post assertions up to their server for verification,
     // and then post-message the results down to their app.
     let _audience = message.origin;
     if (message.audience && message.audience != message.origin) {
-      if (this._appStatus === principal.APP_STATUS_CERTIFIED) {
+      if (this._appStatus === Ci.nsIPrincipal.APP_STATUS_CERTIFIED) {
         _audience = message.audience;
         this._log("Certified app setting assertion audience: " + _audience);
       } else {
         message.errors.push("ERROR_INVALID_ASSERTION_AUDIENCE");
       }
     }
 
     // Replace any audience supplied by the RP with one that has been sanitised
@@ -643,17 +675,19 @@ nsDOMIdentity.prototype = {
 
     return message;
   },
 
   uninit: function DOMIdentity_uninit() {
     this._log("nsDOMIdentity uninit() " + this._id);
     this._identityInternal._mm.sendAsyncMessage(
       "Identity:RP:Unwatch",
-      { id: this._id }
+      { id: this._id },
+      null,
+      this._window.document.nodePrincipal
     );
   }
 
 };
 
 /**
  * Internal functions that shouldn't be exposed to content.
  */