extensions/permissions/PermissionDelegateHandler.cpp
author Brindusan Cristian <cbrindusan@mozilla.com>
Sun, 24 Jan 2021 01:22:23 +0200
changeset 564409 3837805e531398c01dcc75d0ff93fafe792b8c96
parent 544400 64e0e602e580dcd8e333e86862ed3fe720220799
permissions -rw-r--r--
Backed out changeset 870a4ac0af45 (bug 1583901) for devtools failures at browser_rules_preview-tooltips-sizes.js.

/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "mozilla/PermissionDelegateHandler.h"

#include "nsGlobalWindowInner.h"
#include "nsPIDOMWindow.h"
#include "nsIPrincipal.h"
#include "nsContentPermissionHelper.h"

#include "mozilla/BasePrincipal.h"
#include "mozilla/StaticPrefs_permissions.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/FeaturePolicyUtils.h"
#include "mozilla/dom/WindowContext.h"
#include "mozilla/PermissionManager.h"

using namespace mozilla::dom;

namespace mozilla {

typedef PermissionDelegateHandler::PermissionDelegatePolicy DelegatePolicy;
typedef PermissionDelegateHandler::PermissionDelegateInfo DelegateInfo;

// Particular type of permissions to care about. We decide cases by case and
// give various types of controls over each of these.
static const DelegateInfo sPermissionsMap[] = {
    // Permissions API map
    {"geo", u"geolocation", DelegatePolicy::eDelegateUseFeaturePolicy},
    // The same with geo, but we support both to save some conversions between
    // "geo" and "geolocation"
    {"geolocation", u"geolocation", DelegatePolicy::eDelegateUseFeaturePolicy},
    {"desktop-notification", nullptr,
     DelegatePolicy::ePersistDeniedCrossOrigin},
    {"persistent-storage", nullptr, DelegatePolicy::ePersistDeniedCrossOrigin},
    {"vibration", nullptr, DelegatePolicy::ePersistDeniedCrossOrigin},
    {"midi", nullptr, DelegatePolicy::eDelegateUseIframeOrigin},
    {"storage-access", nullptr, DelegatePolicy::eDelegateUseIframeOrigin},
    {"camera", u"camera", DelegatePolicy::eDelegateUseFeaturePolicy},
    {"microphone", u"microphone", DelegatePolicy::eDelegateUseFeaturePolicy},
    {"screen", u"display-capture", DelegatePolicy::eDelegateUseFeaturePolicy},
    {"xr", u"xr-spatial-tracking", DelegatePolicy::eDelegateUseFeaturePolicy},
};

static_assert(PermissionDelegateHandler::DELEGATED_PERMISSION_COUNT ==
                  (sizeof(sPermissionsMap) / sizeof(DelegateInfo)),
              "The PermissionDelegateHandler::DELEGATED_PERMISSION_COUNT must "
              "match to the "
              "length of sPermissionsMap. Please update it.");

NS_IMPL_CYCLE_COLLECTION(PermissionDelegateHandler)
NS_IMPL_CYCLE_COLLECTING_ADDREF(PermissionDelegateHandler)
NS_IMPL_CYCLE_COLLECTING_RELEASE(PermissionDelegateHandler)

NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PermissionDelegateHandler)
  NS_INTERFACE_MAP_ENTRY(nsIPermissionDelegateHandler)
  NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_END

PermissionDelegateHandler::PermissionDelegateHandler(dom::Document* aDocument)
    : mDocument(aDocument) {
  MOZ_ASSERT(aDocument);
}

/* static */
const DelegateInfo* PermissionDelegateHandler::GetPermissionDelegateInfo(
    const nsAString& aPermissionName) {
  nsAutoString lowerContent(aPermissionName);
  ToLowerCase(lowerContent);

  for (const auto& perm : sPermissionsMap) {
    if (lowerContent.EqualsASCII(perm.mPermissionName)) {
      return &perm;
    }
  }

  return nullptr;
}

NS_IMETHODIMP
PermissionDelegateHandler::MaybeUnsafePermissionDelegate(
    const nsTArray<nsCString>& aTypes, bool* aMaybeUnsafe) {
  *aMaybeUnsafe = false;
  if (!StaticPrefs::permissions_delegation_enabled()) {
    return NS_OK;
  }

  for (auto& type : aTypes) {
    const DelegateInfo* info =
        GetPermissionDelegateInfo(NS_ConvertUTF8toUTF16(type));
    if (!info) {
      continue;
    }

    nsAutoString featureName(info->mFeatureName);
    if (FeaturePolicyUtils::IsFeatureUnsafeAllowedAll(mDocument, featureName)) {
      *aMaybeUnsafe = true;
      return NS_OK;
    }
  }

  return NS_OK;
}

NS_IMETHODIMP
PermissionDelegateHandler::GetPermissionDelegateFPEnabled(bool* aEnabled) {
  MOZ_ASSERT(NS_IsMainThread());
  *aEnabled = StaticPrefs::permissions_delegation_enabled();
  return NS_OK;
}

/* static */
nsresult PermissionDelegateHandler::GetDelegatePrincipal(
    const nsACString& aType, nsIContentPermissionRequest* aRequest,
    nsIPrincipal** aResult) {
  MOZ_ASSERT(aRequest);

  if (!StaticPrefs::permissions_delegation_enabled()) {
    return aRequest->GetPrincipal(aResult);
  }

  const DelegateInfo* info =
      GetPermissionDelegateInfo(NS_ConvertUTF8toUTF16(aType));
  if (!info) {
    *aResult = nullptr;
    return NS_OK;
  }

  if (info->mPolicy == DelegatePolicy::eDelegateUseTopOrigin ||
      info->mPolicy == DelegatePolicy::eDelegateUseFeaturePolicy) {
    return aRequest->GetTopLevelPrincipal(aResult);
  }

  return aRequest->GetPrincipal(aResult);
}

bool PermissionDelegateHandler::Initialize() {
  MOZ_ASSERT(mDocument);

  mPermissionManager = PermissionManager::GetInstance();
  if (!mPermissionManager) {
    return false;
  }

  mPrincipal = mDocument->NodePrincipal();
  return true;
}

static bool IsCrossOriginContentToTop(Document* aDocument) {
  MOZ_ASSERT(aDocument);

  RefPtr<BrowsingContext> bc = aDocument->GetBrowsingContext();
  if (!bc) {
    return true;
  }
  RefPtr<BrowsingContext> topBC = bc->Top();

  // In Fission, we can know if it is cross-origin by checking whether both
  // contexts in the same process. So, If they are not in the same process, we
  // can say that it's cross-origin.
  if (!topBC->IsInProcess()) {
    return true;
  }

  RefPtr<Document> topDoc = topBC->GetDocument();
  if (!topDoc) {
    return true;
  }

  nsCOMPtr<nsIPrincipal> topLevelPrincipal = topDoc->NodePrincipal();

  return !aDocument->NodePrincipal()->Subsumes(topLevelPrincipal);
}

bool PermissionDelegateHandler::HasFeaturePolicyAllowed(
    const DelegateInfo* info) const {
  if (info->mPolicy != DelegatePolicy::eDelegateUseFeaturePolicy ||
      !info->mFeatureName) {
    return true;
  }

  nsAutoString featureName(info->mFeatureName);
  return FeaturePolicyUtils::IsFeatureAllowed(mDocument, featureName);
}

bool PermissionDelegateHandler::HasPermissionDelegated(
    const nsACString& aType) {
  MOZ_ASSERT(mDocument);

  // System principal should have right to make permission request
  if (mPrincipal->IsSystemPrincipal()) {
    return true;
  }

  const DelegateInfo* info =
      GetPermissionDelegateInfo(NS_ConvertUTF8toUTF16(aType));
  if (!info || !HasFeaturePolicyAllowed(info)) {
    return false;
  }

  if (!StaticPrefs::permissions_delegation_enabled()) {
    return true;
  }

  if (info->mPolicy == DelegatePolicy::ePersistDeniedCrossOrigin &&
      !mDocument->IsTopLevelContentDocument() &&
      IsCrossOriginContentToTop(mDocument)) {
    return false;
  }

  return true;
}

nsresult PermissionDelegateHandler::GetPermission(const nsACString& aType,
                                                  uint32_t* aPermission,
                                                  bool aExactHostMatch) {
  MOZ_ASSERT(mDocument);
  MOZ_ASSERT(mPrincipal);

  if (mPrincipal->IsSystemPrincipal()) {
    *aPermission = nsIPermissionManager::ALLOW_ACTION;
    return NS_OK;
  }

  const DelegateInfo* info =
      GetPermissionDelegateInfo(NS_ConvertUTF8toUTF16(aType));
  if (!info || !HasFeaturePolicyAllowed(info)) {
    *aPermission = nsIPermissionManager::DENY_ACTION;
    return NS_OK;
  }

  nsresult (NS_STDCALL nsIPermissionManager::*testPermission)(
      nsIPrincipal*, const nsACString&, uint32_t*) =
      aExactHostMatch ? &nsIPermissionManager::TestExactPermissionFromPrincipal
                      : &nsIPermissionManager::TestPermissionFromPrincipal;

  if (!StaticPrefs::permissions_delegation_enabled()) {
    return (mPermissionManager->*testPermission)(mPrincipal, aType,
                                                 aPermission);
  }

  if (info->mPolicy == DelegatePolicy::ePersistDeniedCrossOrigin &&
      !mDocument->IsTopLevelContentDocument() &&
      IsCrossOriginContentToTop(mDocument)) {
    *aPermission = nsIPermissionManager::DENY_ACTION;
    return NS_OK;
  }

  nsIPrincipal* principal = mPrincipal;
  // If we cannot get the browsing context from the document, we fallback to use
  // the prinicpal of the document to test the permission.
  RefPtr<BrowsingContext> bc = mDocument->GetBrowsingContext();

  if ((info->mPolicy == DelegatePolicy::eDelegateUseTopOrigin ||
       info->mPolicy == DelegatePolicy::eDelegateUseFeaturePolicy) &&
      bc) {
    RefPtr<WindowContext> topWC = bc->GetTopWindowContext();

    if (topWC && topWC->IsInProcess()) {
      // If the top-level window context is in the same process, we directly get
      // the node principal from the top-level document to test the permission.
      // We cannot check the lists in the window context in this case since the
      // 'perm-changed' could be notified in the iframe before the top-level in
      // certain cases, for example, request permissions in first-party iframes.
      // In this case, the list in window context hasn't gotten updated, so it
      // would has an out-dated value until the top-level window get the
      // observer. So, we have to test permission manager directly if we can.
      RefPtr<Document> topDoc = topWC->GetBrowsingContext()->GetDocument();

      if (topDoc) {
        principal = topDoc->NodePrincipal();
      }
    } else if (topWC) {
      // Get the delegated permissions from the top-level window context.
      DelegatedPermissionList list =
          aExactHostMatch ? topWC->GetDelegatedExactHostMatchPermissions()
                          : topWC->GetDelegatedPermissions();
      size_t idx = std::distance(sPermissionsMap, info);
      *aPermission = list.mPermissions[idx];
      return NS_OK;
    }
  }

  return (mPermissionManager->*testPermission)(principal, aType, aPermission);
}

nsresult PermissionDelegateHandler::GetPermissionForPermissionsAPI(
    const nsACString& aType, uint32_t* aPermission) {
  return GetPermission(aType, aPermission, false);
}

void PermissionDelegateHandler::PopulateAllDelegatedPermissions() {
  MOZ_ASSERT(mDocument);
  MOZ_ASSERT(mPermissionManager);

  // We only populate the delegated permissions for the top-level content.
  if (!mDocument->IsTopLevelContentDocument()) {
    return;
  }

  RefPtr<WindowContext> wc = mDocument->GetWindowContext();
  NS_ENSURE_TRUE_VOID(wc && !wc->IsDiscarded());

  DelegatedPermissionList list;
  DelegatedPermissionList exactHostMatchList;

  for (const auto& perm : sPermissionsMap) {
    size_t idx = std::distance(sPermissionsMap, &perm);

    nsDependentCString type(perm.mPermissionName);
    // Populate the permission.
    uint32_t permission = nsIPermissionManager::UNKNOWN_ACTION;
    Unused << mPermissionManager->TestPermissionFromPrincipal(mPrincipal, type,
                                                              &permission);
    list.mPermissions[idx] = permission;

    // Populate the exact-host-match permission.
    permission = nsIPermissionManager::UNKNOWN_ACTION;
    Unused << mPermissionManager->TestExactPermissionFromPrincipal(
        mPrincipal, type, &permission);
    exactHostMatchList.mPermissions[idx] = permission;
  }

  WindowContext::Transaction txn;
  txn.SetDelegatedPermissions(list);
  txn.SetDelegatedExactHostMatchPermissions(exactHostMatchList);
  MOZ_ALWAYS_SUCCEEDS(txn.Commit(wc));
}

void PermissionDelegateHandler::UpdateDelegatedPermission(
    const nsACString& aType) {
  MOZ_ASSERT(mDocument);
  MOZ_ASSERT(mPermissionManager);

  // We only update the delegated permission for the top-level content.
  if (!mDocument->IsTopLevelContentDocument()) {
    return;
  }

  RefPtr<WindowContext> wc = mDocument->GetWindowContext();
  NS_ENSURE_TRUE_VOID(wc);

  const DelegateInfo* info =
      GetPermissionDelegateInfo(NS_ConvertUTF8toUTF16(aType));
  NS_ENSURE_TRUE_VOID(info);
  size_t idx = std::distance(sPermissionsMap, info);

  WindowContext::Transaction txn;
  bool changed = false;
  DelegatedPermissionList list = wc->GetDelegatedPermissions();

  if (UpdateDelegatePermissionInternal(
          list, aType, idx,
          &nsIPermissionManager::TestPermissionFromPrincipal)) {
    txn.SetDelegatedPermissions(list);
    changed = true;
  }

  DelegatedPermissionList exactHostMatchList =
      wc->GetDelegatedExactHostMatchPermissions();

  if (UpdateDelegatePermissionInternal(
          exactHostMatchList, aType, idx,
          &nsIPermissionManager::TestExactPermissionFromPrincipal)) {
    txn.SetDelegatedExactHostMatchPermissions(exactHostMatchList);
    changed = true;
  }

  // We only commit if there is any change of permissions.
  if (changed) {
    MOZ_ALWAYS_SUCCEEDS(txn.Commit(wc));
  }
}

bool PermissionDelegateHandler::UpdateDelegatePermissionInternal(
    PermissionDelegateHandler::DelegatedPermissionList& aList,
    const nsACString& aType, size_t aIdx,
    nsresult (NS_STDCALL nsIPermissionManager::*aTestFunc)(nsIPrincipal*,
                                                           const nsACString&,
                                                           uint32_t*)) {
  MOZ_ASSERT(aTestFunc);
  MOZ_ASSERT(mPermissionManager);
  MOZ_ASSERT(mPrincipal);

  uint32_t permission = nsIPermissionManager::UNKNOWN_ACTION;
  Unused << (mPermissionManager->*aTestFunc)(mPrincipal, aType, &permission);

  if (aList.mPermissions[aIdx] != permission) {
    aList.mPermissions[aIdx] = permission;
    return true;
  }

  return false;
}

}  // namespace mozilla