Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: Creating new Loop pages after SharePoint domain change #19443

Conversation

Philip-Scott
Copy link
Member

@Philip-Scott Philip-Scott commented Feb 1, 2024

Description

After a tenant changes their domain, this lead existing workspaces on the Loop app to stop allowing the creation of new pages. After a redirect, the authorization headers are not sent by the browser. And even if were to just retry with the headers set, SharePoint requires for the auth tokens to be generated for that particular domain.

After these changes, I was able to get new pages to attach again. What I did was catch the redirect on the fetchHelper( function, and allow getFileLink to handle the redirect for us. For this to work, we need to request new tokens for the new domain.

image

The fix is sadly not perfect as we need to go through the following flow in order to detect and retry the redirect error.

  • The first call to getShareURL against the original URL gives us the 308
  • The second one that gets made automatically by the browser to follow the 308 will be made against the new domain, but will fail due to tokens
  • We need to catch that second one, and retry with new tokens. This call will succeed

@Philip-Scott Philip-Scott requested review from a team as code owners February 1, 2024 21:24
@github-actions github-actions bot added area: driver Driver related issues area: odsp-driver base: main PRs targeted against main branch labels Feb 1, 2024
@msfluid-bot
Copy link
Collaborator

msfluid-bot commented Feb 1, 2024

Baseline CI build failed, cannot generate bundle analysis at this time


Baseline commit: 4c94050

Generated by 🚫 dangerJS against bfd5f75

@vladsud vladsud requested a review from jatgarg February 5, 2024 18:10
@github-actions github-actions bot added area: loader Loader related issues public api change Changes to a public API labels Feb 9, 2024
@github-actions github-actions bot removed public api change Changes to a public API area: loader Loader related issues labels Feb 12, 2024
@agarwal-navin
Copy link
Contributor

@vladsud @pragya91 Can you guys please take a look at this PR? @jatgarg is not available right now and you both have the most context here.

@pragya91
Copy link
Contributor

Let's add a test for these new changes in getFileLink.spec.ts. You can mock throwing of the 403 error with redirected property in the response and see if the code works as expected.

options: TokenFetchOptionsEx,
fileItem: FileItemLite,
): Promise<IOdspResponse<Response>> {
for (;;) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the reason for this loop? My understanding from the changes in the code here is that we only want to perform a retry once after we get the 403 error. If this is true, can we structure the code such that we perform retry within the catch block?

let { url, requestInit } = await getRequestInformation...
try {
    response = await fetchHelper();
}catch(e){
    // 403 error occured and we need to retry with correct headers here
    if(error.redirectLocation){
        { url, requestInit } = = await getRequestInformation...
         response = await fetchHelper();
    }
}
return response;

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redirects can actually be chained together! One redirect may point to another redirect and so on.

This was actually a pattern taken from packages/loader/container-loader/src/location-redirection-utilities/resolveWithLocationRedirection.ts

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add comments to explain this logic.

return await fetchHelper(url, requestInit);
} catch (error: any) {
if (
error.errorType !== DriverErrorTypes.fileNotFoundOrAccessDeniedError ||
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checking for fileNotFoundOrAccessDeniedError error type seems unrelated to this change.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is actually the error type we get when throwing a OdspRedirectError that I myself throw on odspErrorUtils.ts.
image

I'm checking for both that error that I throw, as well as that there is a redirect URL available before doing any redirect logic

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider doing the following:

class OdspRedirectError {
	public static errorType = DriverErrorTypes.fileNotFoundOrAccessDeniedError;
	readonly errorType = OdspRedirectError.type;
...
}

if (error?.errorType !== OdspRedirectError .Type || !error?.redirectLocation)  {
    throw error;
}

or even better - way more readable.

class OdspRedirectError {
public static errorType = DriverErrorTypes.fileNotFoundOrAccessDeniedError;
readonly errorType = OdspRedirectError.type;

    static public isRedirectError(error: unknown) {
         return error?.errorType  === OdspRedirectError.errorType && !!error?.redirect;
    }

...
}

if (!OdspRedirectError(error)) {
throw error;
}
...
}

Notice that error does not have to be object (i.e. it can be undefined, null, etc. - we never know)

const newSiteDomain = new URL(error.redirectLocation).origin;
const oldSiteDomain = new URL(odspUrlParts.siteUrl).origin;

odspUrlParts.siteUrl = odspUrlParts.siteUrl.replace(oldSiteDomain, newSiteDomain);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing input parameters of a function is not the best programming practice. (Although I do see it in some parts of the existing code already, but let's avoid adding any new ones.) If possible, always return the new value to the caller, and let the caller assign the value as it may.
(Might be irrelevant if we changed the code here to what I suggested above)

@@ -90,6 +93,86 @@ export async function getFileLink(
return fileLink;
}

async function getRequestInformation(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recommend adding code docs to explain the purpose of this function.

@pragya91
Copy link
Contributor

@Philip-Scott As far as I remember the GetSharingInformation is not a blocking call for attach functionality to go through. However, this api is called when host tries to request the getAbsoluteUrl() on the resolver. Just wanted to highlight that, since the PR talks about the "unable to attach" issue.

// database on server becomes readonly. The driver retries for such cases with exponential backup logic.
if (innerMostErrorCode === OdspServiceReadOnlyErrorCode) {
// After a redirect due to a domain change, auth headers are not sent by the browser and can trigger a 403.
if (response?.redirected) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we assert that response?.redirected => 401 or 403? I.e. that we are not missing cases?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does 404 / @error.redirectLocation case also have redirect property on response?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have not seen a 404 redirect error for me to test that this is valid and works. Do you know how one could trigger a 404 redirection error?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, the case below (for 404) has code that deals with redirects. I do not know much about how redirects work (i.e. what error codes can be returned) - @jatgarg & @marcmasmsft are rigth people to consult here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes 404 could be triggered due to domain change.

const storageToken = await storageTokenFetcher(options, "GetFileLinkCore");
assert(
storageToken !== null,
0x2bb /* "Instrumented token fetcher with throwOnNullToken = true should never return null" */,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every place that calls storageTokenFetcher() asserts that non-null result is returned. Given that's a wrapper produced by driver, can we at least move this assert into toInstrumentedOdspTokenFetcher() itself?
Or better, TokenFetcher type and force callers to change / assert? That's a bit bigger change, but having null on the type does not help.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mmmm that is actually part of the original code before moving it out to its own function. We can probably follow that up on a separate PR to keep this one small, is that alright?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

raised #20409 to fix it

`'${fileItem.sharepointIds.listItemUniqueId}'`,
)}`,
storageToken,
true,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add comments with arg names where it's not very clear what is the arg semantics

true, // forceAccessTokenViaAuthorizationHeader

@christiango
Copy link
Member

Looks like this is now being handled with #21277

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: driver Driver related issues area: odsp-driver base: main PRs targeted against main branch
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants