Bug 1888259 - Show a notification when a Content Analysis request is denied for lack of an agent connection r=dlp-reviewers,fluent-reviewers,handyman

Note that this also covers the case when the connection initially succeeds but then the agent goes away - we hope to handle this better in bug 1888293. This also shows a message if the signature verification fails.

Differential Revision: https://phabricator.services.mozilla.com/D206024
This commit is contained in:
Greg Stoll 2024-04-02 11:53:40 +00:00
parent e5eba44e91
commit 2bd7572f35
5 changed files with 126 additions and 16 deletions

View file

@ -243,7 +243,7 @@ export const ContentAnalysis = {
}, },
// nsIObserver // nsIObserver
async observe(aSubj, aTopic) { async observe(aSubj, aTopic, _aData) {
switch (aTopic) { switch (aTopic) {
case "quit-application-requested": { case "quit-application-requested": {
let pendingRequests = let pendingRequests =
@ -345,7 +345,7 @@ export const ContentAnalysis = {
}); });
} }
break; break;
case "dlp-response": case "dlp-response": {
const request = aSubj.QueryInterface(Ci.nsIContentAnalysisResponse); const request = aSubj.QueryInterface(Ci.nsIContentAnalysisResponse);
// Cancels timer or slow message UI, // Cancels timer or slow message UI,
// if present, and possibly presents the CA verdict. // if present, and possibly presents the CA verdict.
@ -379,12 +379,14 @@ export const ContentAnalysis = {
windowAndResourceNameOrOperationType.resourceNameOrOperationType, windowAndResourceNameOrOperationType.resourceNameOrOperationType,
windowAndResourceNameOrOperationType.browsingContext, windowAndResourceNameOrOperationType.browsingContext,
request.requestToken, request.requestToken,
responseResult responseResult,
request.cancelError
); );
this._showAnotherPendingDialog( this._showAnotherPendingDialog(
windowAndResourceNameOrOperationType.browsingContext windowAndResourceNameOrOperationType.browsingContext
); );
break; break;
}
} }
}, },
@ -662,7 +664,8 @@ export const ContentAnalysis = {
aResourceNameOrOperationType, aResourceNameOrOperationType,
aBrowsingContext, aBrowsingContext,
aRequestToken, aRequestToken,
aCAResult aCAResult,
aRequestCancelError
) { ) {
let message = null; let message = null;
let timeoutMs = 0; let timeoutMs = 0;
@ -683,7 +686,7 @@ export const ContentAnalysis = {
); );
timeoutMs = this._RESULT_NOTIFICATION_FAST_TIMEOUT_MS; timeoutMs = this._RESULT_NOTIFICATION_FAST_TIMEOUT_MS;
break; break;
case Ci.nsIContentAnalysisResponse.eWarn: case Ci.nsIContentAnalysisResponse.eWarn: {
const result = await Services.prompt.asyncConfirmEx( const result = await Services.prompt.asyncConfirmEx(
aBrowsingContext, aBrowsingContext,
Ci.nsIPromptService.MODAL_TYPE_TAB, Ci.nsIPromptService.MODAL_TYPE_TAB,
@ -711,6 +714,7 @@ export const ContentAnalysis = {
const allow = result.get("buttonNumClicked") === 0; const allow = result.get("buttonNumClicked") === 0;
lazy.gContentAnalysis.respondToWarnDialog(aRequestToken, allow); lazy.gContentAnalysis.respondToWarnDialog(aRequestToken, allow);
return null; return null;
}
case Ci.nsIContentAnalysisResponse.eBlock: case Ci.nsIContentAnalysisResponse.eBlock:
if (!lazy.showBlockedResult) { if (!lazy.showBlockedResult) {
// Don't show anything // Don't show anything
@ -724,13 +728,51 @@ export const ContentAnalysis = {
timeoutMs = this._RESULT_NOTIFICATION_TIMEOUT_MS; timeoutMs = this._RESULT_NOTIFICATION_TIMEOUT_MS;
break; break;
case Ci.nsIContentAnalysisResponse.eUnspecified: case Ci.nsIContentAnalysisResponse.eUnspecified:
message = await this.l10n.formatValue("contentanalysis-error-message", { message = await this.l10n.formatValue(
content: this._getResourceNameFromNameOrOperationType( "contentanalysis-unspecified-error-message",
aResourceNameOrOperationType {
), agent: lazy.agentName,
}); content: this._getResourceNameFromNameOrOperationType(
aResourceNameOrOperationType
),
}
);
timeoutMs = this._RESULT_NOTIFICATION_TIMEOUT_MS; timeoutMs = this._RESULT_NOTIFICATION_TIMEOUT_MS;
break; break;
case Ci.nsIContentAnalysisResponse.eCanceled:
{
let messageId;
switch (aRequestCancelError) {
case Ci.nsIContentAnalysisResponse.eUserInitiated:
console.error(
"Got unexpected cancel response with eUserInitiated"
);
return null;
case Ci.nsIContentAnalysisResponse.eNoAgent:
messageId = "contentanalysis-no-agent-connected-message";
break;
case Ci.nsIContentAnalysisResponse.eInvalidAgentSignature:
messageId = "contentanalysis-invalid-agent-signature-message";
break;
case Ci.nsIContentAnalysisResponse.eErrorOther:
messageId = "contentanalysis-unspecified-error-message";
break;
default:
console.error(
"Unexpected CA cancelError value: " + aRequestCancelError
);
messageId = "contentanalysis-unspecified-error-message";
break;
}
message = await this.l10n.formatValue(messageId, {
agent: lazy.agentName,
content: this._getResourceNameFromNameOrOperationType(
aResourceNameOrOperationType
),
});
timeoutMs = this._RESULT_NOTIFICATION_TIMEOUT_MS;
}
break;
default: default:
throw new Error("Unexpected CA result value: " + aCAResult); throw new Error("Unexpected CA result value: " + aCAResult);
} }

View file

@ -612,6 +612,12 @@ ContentAnalysisResponse::GetAction(Action* aAction) {
return NS_OK; return NS_OK;
} }
NS_IMETHODIMP
ContentAnalysisResponse::GetCancelError(CancelError* aCancelError) {
*aCancelError = mCancelError;
return NS_OK;
}
static void LogAcknowledgement( static void LogAcknowledgement(
content_analysis::sdk::ContentAnalysisAcknowledgement* aPbAck) { content_analysis::sdk::ContentAnalysisAcknowledgement* aPbAck) {
if (!static_cast<LogModule*>(gContentAnalysisLog) if (!static_cast<LogModule*>(gContentAnalysisLog)
@ -643,6 +649,10 @@ void ContentAnalysisResponse::SetOwner(RefPtr<ContentAnalysis> aOwner) {
mOwner = std::move(aOwner); mOwner = std::move(aOwner);
} }
void ContentAnalysisResponse::SetCancelError(CancelError aCancelError) {
mCancelError = aCancelError;
}
void ContentAnalysisResponse::ResolveWarnAction(bool aAllowContent) { void ContentAnalysisResponse::ResolveWarnAction(bool aAllowContent) {
MOZ_ASSERT(mAction == Action::eWarn); MOZ_ASSERT(mAction == Action::eWarn);
mAction = aAllowContent ? Action::eAllow : Action::eBlock; mAction = aAllowContent ? Action::eAllow : Action::eBlock;
@ -955,6 +965,20 @@ nsresult ContentAnalysis::CancelWithError(nsCString aRequestToken,
: nsIContentAnalysisResponse::Action::eCanceled, : nsIContentAnalysisResponse::Action::eCanceled,
aRequestToken); aRequestToken);
response->SetOwner(owner); response->SetOwner(owner);
nsIContentAnalysisResponse::CancelError cancelError;
switch (aResult) {
case NS_ERROR_NOT_AVAILABLE:
cancelError = nsIContentAnalysisResponse::CancelError::eNoAgent;
break;
case NS_ERROR_INVALID_SIGNATURE:
cancelError =
nsIContentAnalysisResponse::CancelError::eInvalidAgentSignature;
break;
default:
cancelError = nsIContentAnalysisResponse::CancelError::eErrorOther;
break;
}
response->SetCancelError(cancelError);
obsServ->NotifyObservers(response, "dlp-response", nullptr); obsServ->NotifyObservers(response, "dlp-response", nullptr);
nsMainThreadPtrHandle<nsIContentAnalysisCallback> callbackHolder; nsMainThreadPtrHandle<nsIContentAnalysisCallback> callbackHolder;
{ {
@ -1236,8 +1260,28 @@ NS_IMETHODIMP
ContentAnalysis::AnalyzeContentRequestCallback( ContentAnalysis::AnalyzeContentRequestCallback(
nsIContentAnalysisRequest* aRequest, bool aAutoAcknowledge, nsIContentAnalysisRequest* aRequest, bool aAutoAcknowledge,
nsIContentAnalysisCallback* aCallback) { nsIContentAnalysisCallback* aCallback) {
MOZ_ASSERT(NS_IsMainThread());
NS_ENSURE_ARG(aRequest); NS_ENSURE_ARG(aRequest);
NS_ENSURE_ARG(aCallback); NS_ENSURE_ARG(aCallback);
nsresult rv = AnalyzeContentRequestCallbackPrivate(aRequest, aAutoAcknowledge,
aCallback);
if (NS_FAILED(rv)) {
nsCString requestToken;
nsresult requestTokenRv = aRequest->GetRequestToken(requestToken);
NS_ENSURE_SUCCESS(requestTokenRv, requestTokenRv);
CancelWithError(requestToken, rv);
}
return rv;
}
nsresult ContentAnalysis::AnalyzeContentRequestCallbackPrivate(
nsIContentAnalysisRequest* aRequest, bool aAutoAcknowledge,
nsIContentAnalysisCallback* aCallback) {
// Make sure we send the notification first, so if we later return
// an error the JS will handle it correctly.
nsCOMPtr<nsIObserverService> obsServ =
mozilla::services::GetObserverService();
obsServ->NotifyObservers(aRequest, "dlp-request-made", nullptr);
bool isActive; bool isActive;
nsresult rv = GetIsActive(&isActive); nsresult rv = GetIsActive(&isActive);
@ -1246,10 +1290,6 @@ ContentAnalysis::AnalyzeContentRequestCallback(
return NS_ERROR_NOT_AVAILABLE; return NS_ERROR_NOT_AVAILABLE;
} }
nsCOMPtr<nsIObserverService> obsServ =
mozilla::services::GetObserverService();
obsServ->NotifyObservers(aRequest, "dlp-request-made", nullptr);
MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(NS_IsMainThread());
// since we're on the main thread, don't need to synchronize this // since we're on the main thread, don't need to synchronize this
int64_t requestCount = ++mRequestCount; int64_t requestCount = ++mRequestCount;

View file

@ -135,6 +135,10 @@ class ContentAnalysis final : public nsIContentAnalysis {
nsresult CreateContentAnalysisClient(nsCString&& aPipePathName, nsresult CreateContentAnalysisClient(nsCString&& aPipePathName,
nsString&& aClientSignatureSetting, nsString&& aClientSignatureSetting,
bool aIsPerUser); bool aIsPerUser);
nsresult AnalyzeContentRequestCallbackPrivate(
nsIContentAnalysisRequest* aRequest, bool aAutoAcknowledge,
nsIContentAnalysisCallback* aCallback);
nsresult RunAnalyzeRequestTask( nsresult RunAnalyzeRequestTask(
const RefPtr<nsIContentAnalysisRequest>& aRequest, bool aAutoAcknowledge, const RefPtr<nsIContentAnalysisRequest>& aRequest, bool aAutoAcknowledge,
int64_t aRequestCount, int64_t aRequestCount,
@ -219,6 +223,7 @@ class ContentAnalysisResponse final : public nsIContentAnalysisResponse {
void SetOwner(RefPtr<ContentAnalysis> aOwner); void SetOwner(RefPtr<ContentAnalysis> aOwner);
void DoNotAcknowledge() { mDoNotAcknowledge = true; } void DoNotAcknowledge() { mDoNotAcknowledge = true; }
void SetCancelError(CancelError aCancelError);
private: private:
~ContentAnalysisResponse() = default; ~ContentAnalysisResponse() = default;
@ -239,7 +244,11 @@ class ContentAnalysisResponse final : public nsIContentAnalysisResponse {
// Identifier for the corresponding nsIContentAnalysisRequest // Identifier for the corresponding nsIContentAnalysisRequest
nsCString mRequestToken; nsCString mRequestToken;
// ContentAnalysis (or, more precisely, it's Client object) must outlive // If mAction is eCanceled, this is the error explaining why the request was
// canceled, or eUserInitiated if the user canceled it.
CancelError mCancelError = CancelError::eUserInitiated;
// ContentAnalysis (or, more precisely, its Client object) must outlive
// the transaction. // the transaction.
RefPtr<ContentAnalysis> mOwner; RefPtr<ContentAnalysis> mOwner;

View file

@ -52,8 +52,18 @@ interface nsIContentAnalysisResponse : nsISupports
eCanceled = 1001, eCanceled = 1001,
}; };
cenum CancelError : 32 {
eUserInitiated = 0,
eNoAgent = 1,
eInvalidAgentSignature = 2,
eErrorOther = 3,
};
[infallible] readonly attribute nsIContentAnalysisResponse_Action action; [infallible] readonly attribute nsIContentAnalysisResponse_Action action;
[infallible] readonly attribute boolean shouldAllowContent; [infallible] readonly attribute boolean shouldAllowContent;
// If action is eCanceled, this is the error explaining why the request was canceled,
// or eUserInitiated if the user canceled it.
[infallible] readonly attribute nsIContentAnalysisResponse_CancelError cancelError;
// Identifier for the corresponding nsIContentAnalysisRequest // Identifier for the corresponding nsIContentAnalysisRequest
readonly attribute ACString requestToken; readonly attribute ACString requestToken;

View file

@ -42,8 +42,17 @@ contentanalysis-genericresponse-message = Content Analysis responded with { $res
# $content - Description of the content being blocked, such as "clipboard" or "aFile.txt" # $content - Description of the content being blocked, such as "clipboard" or "aFile.txt"
contentanalysis-block-message = Your organization uses data-loss prevention software that has blocked this content: { $content }. contentanalysis-block-message = Your organization uses data-loss prevention software that has blocked this content: { $content }.
# Variables: # Variables:
# $agent - The name of the DLP agent doing the analysis
# $content - Description of the content being blocked, such as "clipboard" or "aFile.txt" # $content - Description of the content being blocked, such as "clipboard" or "aFile.txt"
contentanalysis-error-message = An error occurred in communicating with the data-loss prevention software. Transfer denied for resource: { $content }. contentanalysis-unspecified-error-message = An error occurred in communicating with { $agent }. Transfer denied for resource: { $content }.
# Variables:
# $agent - The name of the DLP agent doing the analysis
# $content - Description of the content being blocked, such as "clipboard" or "aFile.txt"
contentanalysis-no-agent-connected-message = Unable to connect to { $agent }. Transfer denied for resource: { $content }.
# Variables:
# $agent - The name of the DLP agent doing the analysis
# $content - Description of the content being blocked, such as "clipboard" or "aFile.txt"
contentanalysis-invalid-agent-signature-message = Failed signature verification for { $agent }. Transfer denied for resource: { $content }.
contentanalysis-inprogress-quit-title = Quit { -brand-shorter-name }? contentanalysis-inprogress-quit-title = Quit { -brand-shorter-name }?
contentanalysis-inprogress-quit-message = Several actions are in progress. If you quit { -brand-shorter-name }, these actions will not be completed. contentanalysis-inprogress-quit-message = Several actions are in progress. If you quit { -brand-shorter-name }, these actions will not be completed.