Skip to content

Commit

Permalink
Add support for unmanaged sessions, async determineOwner (#678)
Browse files Browse the repository at this point in the history
* Add support for unmanaged sessions, async determineOwner

* Update packages/arcgis-rest-portal/src/items/helpers.ts

Co-Authored-By: Tom Wayson <tom@tomwayson.com>

* Bump

* Bump

Co-authored-by: Tom Wayson <tom@tomwayson.com>
  • Loading branch information
patrickarlt and tomwayson authored Apr 2, 2020
1 parent 869f466 commit b8d099a
Show file tree
Hide file tree
Showing 14 changed files with 507 additions and 282 deletions.
42 changes: 37 additions & 5 deletions packages/arcgis-rest-auth/src/UserSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -616,6 +616,7 @@ export class UserSession implements IAuthenticationManager {
private _tokenExpires: Date;
private _refreshToken: string;
private _refreshTokenExpires: Date;
private _pendingUserRequest: Promise<IUser>;

/**
* Internal object to keep track of pending token requests. Used to prevent
Expand Down Expand Up @@ -728,23 +729,49 @@ export class UserSession implements IAuthenticationManager {
* @returns A Promise that will resolve with the data from the response.
*/
public getUser(requestOptions?: IRequestOptions): Promise<IUser> {
if (this._user && this._user.username === this.username) {
if (this._pendingUserRequest) {
return this._pendingUserRequest;
} else if (this._user) {
return Promise.resolve(this._user);
} else {
const url = `${this.portal}/community/users/${encodeURIComponent(
this.username
)}`;
const url = `${this.portal}/community/self`;

const options = {
httpMethod: "GET",
authentication: this,
...requestOptions,
rawResponse: false
} as IRequestOptions;
return request(url, options).then(response => {

this._pendingUserRequest = request(url, options).then(response => {
this._user = response;
this._pendingUserRequest = null;
return response;
});

return this._pendingUserRequest;
}
}

/**
* Returns the username for the currently logged in [user](https://developers.arcgis.com/rest/users-groups-and-items/user.htm). Subsequent calls will *not* result in additional web traffic. This is also used internally when a username is required for some requests but is not present in the options.
*
* * ```js
* session.getUsername()
* .then(response => {
* console.log(response); // "casey_jones"
* })
* ```
*/
public getUsername() {
if (this.username) {
return Promise.resolve(this.username);
} else if (this._user) {
return Promise.resolve(this._user.username);
} else {
return this.getUser().then(user => {
return user.username;
});
}
}

Expand Down Expand Up @@ -794,6 +821,7 @@ export class UserSession implements IAuthenticationManager {
): Promise<UserSession> {
// make sure subsequent calls to getUser() don't returned cached metadata
this._user = null;

if (this.username && this.password) {
return this.refreshWithUsernameAndPassword(requestOptions);
}
Expand Down Expand Up @@ -931,6 +959,10 @@ export class UserSession implements IAuthenticationManager {
* Returns an unexpired token for the current `portal`.
*/
private getFreshToken(requestOptions?: ITokenRequestOptions) {
if (this.token && !this.tokenExpires) {
return Promise.resolve(this.token);
}

if (
this.token &&
this.tokenExpires &&
Expand Down
109 changes: 106 additions & 3 deletions packages/arcgis-rest-auth/test/UserSession.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,22 @@ describe("UserSession", () => {
});
});

it("should pass through a token when no token expiration is present", done => {
const session = new UserSession({
token: "token"
});

session
.getToken("https://www.arcgis.com/sharing/rest/portals/self")
.then(token1 => {
expect(token1).toBe("token");
done();
})
.catch(e => {
fail(e);
});
});

it("should generate a token for an untrusted, federated server", done => {
const session = new UserSession({
clientId: "id",
Expand Down Expand Up @@ -1114,7 +1130,7 @@ describe("UserSession", () => {
it("should cache metadata about the user", done => {
// we intentionally only mock one response
fetchMock.once(
"https://www.arcgis.com/sharing/rest/community/users/jsmith?f=json&token=token",
"https://www.arcgis.com/sharing/rest/community/self?f=json&token=token",
{
username: "jsmith",
fullName: "John Smith",
Expand Down Expand Up @@ -1152,6 +1168,93 @@ describe("UserSession", () => {
fail(e);
});
});

it("should never make more then 1 request", done => {
// we intentionally only mock one response
fetchMock.once(
"https://www.arcgis.com/sharing/rest/community/self?f=json&token=token",
{
username: "jsmith",
fullName: "John Smith",
role: "org_publisher"
}
);

const session = new UserSession({
clientId: "clientId",
redirectUri: "https://example-app.com/redirect-uri",
token: "token",
tokenExpires: TOMORROW,
refreshToken: "refreshToken",
refreshTokenExpires: TOMORROW,
refreshTokenTTL: 1440,
username: "jsmith",
password: "123456"
});

Promise.all([session.getUser(), session.getUser()])
.then(() => {
done();
})
.catch(e => {
fail(e);
});
});
});

describe(".getUsername()", () => {
afterEach(fetchMock.restore);

it("should fetch the username via getUser()", done => {
// we intentionally only mock one response
fetchMock.once(
"https://www.arcgis.com/sharing/rest/community/self?f=json&token=token",
{
username: "jsmith"
}
);

const session = new UserSession({
token: "token"
});

session
.getUsername()
.then(response => {
expect(response).toEqual("jsmith");

// also test getting it from the cache.
session
.getUsername()
.then(username => {
done();

expect(username).toEqual("jsmith");
})
.catch(e => {
fail(e);
});
})
.catch(e => {
fail(e);
});
});

it("should use a username if passed in the session", done => {
const session = new UserSession({
username: "jsmith"
});

session
.getUsername()
.then(response => {
expect(response).toEqual("jsmith");
done();
})
.catch(e => {
fail(e);
});
});
});

describe("to/fromCredential()", () => {
Expand Down Expand Up @@ -1251,7 +1354,7 @@ describe("UserSession", () => {
});

describe("non-federated server", () => {
it("shouldnt fetch a fresh token if the current one isnt expired.", done => {
it("shouldnt fetch a fresh token if the current one isn't expired.", done => {
const MOCK_USER_SESSION = new UserSession({
username: "c@sey",
password: "123456",
Expand Down Expand Up @@ -1377,7 +1480,7 @@ describe("UserSession", () => {
})
.catch(err => {
expect(err.code).toBe("NOT_FEDERATED");
expect(err.originalMessage).toBe(
expect(err.originalMessage).toEqual(
"https://fakeserver2.com/arcgis/rest/services/Fake/MapServer/ is not federated with any portal and is not explicitly trusted."
);
done();
Expand Down
69 changes: 35 additions & 34 deletions packages/arcgis-rest-portal/src/items/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ export interface IAddItemDataOptions extends IUserItemOptions {
export function addItemData(
requestOptions: IAddItemDataOptions
): Promise<IUpdateItemResponse> {
const owner = determineOwner(requestOptions);
const options: any = {
item: {
id: requestOptions.id,
Expand Down Expand Up @@ -77,18 +76,18 @@ export function addItemData(
export function addItemRelationship(
requestOptions: IManageItemRelationshipOptions
): Promise<{ success: boolean }> {
const owner = determineOwner(requestOptions);
const url = `${getPortalUrl(
requestOptions
)}/content/users/${owner}/addRelationship`;
return determineOwner(requestOptions).then(owner => {
const url = `${getPortalUrl(
requestOptions
)}/content/users/${owner}/addRelationship`;

const options = appendCustomParams<IManageItemRelationshipOptions>(
requestOptions,
["originItemId", "destinationItemId", "relationshipType"],
{ params: { ...requestOptions.params } }
);

return request(url, options);
const options = appendCustomParams<IManageItemRelationshipOptions>(
requestOptions,
["originItemId", "destinationItemId", "relationshipType"],
{ params: { ...requestOptions.params } }
);
return request(url, options);
});
}

/**
Expand Down Expand Up @@ -121,20 +120,21 @@ export function addItemRelationship(
export function addItemResource(
requestOptions: IItemResourceOptions
): Promise<IItemResourceResponse> {
const owner = determineOwner(requestOptions);
const url = `${getPortalUrl(requestOptions)}/content/users/${owner}/items/${
requestOptions.id
}/addResources`;
return determineOwner(requestOptions).then(owner => {
const url = `${getPortalUrl(requestOptions)}/content/users/${owner}/items/${
requestOptions.id
}/addResources`;

requestOptions.params = {
file: requestOptions.resource,
fileName: requestOptions.name,
text: requestOptions.content,
access: requestOptions.private ? "private" : "inherit",
...requestOptions.params
};
requestOptions.params = {
file: requestOptions.resource,
fileName: requestOptions.name,
text: requestOptions.content,
access: requestOptions.private ? "private" : "inherit",
...requestOptions.params
};

return request(url, requestOptions);
return request(url, requestOptions);
});
}

/**
Expand All @@ -158,16 +158,17 @@ export function addItemResource(
export function addItemPart(
requestOptions?: IItemPartOptions
): Promise<IUpdateItemResponse> {
const owner = determineOwner(requestOptions);
const url = `${getPortalUrl(requestOptions)}/content/users/${owner}/items/${
requestOptions.id
}/addPart`;
return determineOwner(requestOptions).then(owner => {
const url = `${getPortalUrl(requestOptions)}/content/users/${owner}/items/${
requestOptions.id
}/addPart`;

const options = appendCustomParams<IItemPartOptions>(
requestOptions,
["file", "partNum"],
{ params: { ...requestOptions.params } }
);
const options = appendCustomParams<IItemPartOptions>(
requestOptions,
["file", "partNum"],
{ params: { ...requestOptions.params } }
);

return request(url, options);
return request(url, options);
});
}
Loading

0 comments on commit b8d099a

Please sign in to comment.