Skip to content

Commit

Permalink
feat(caring): methods to un/share items with groups
Browse files Browse the repository at this point in the history
AFFECTS PACKAGES:
@esri/arcgis-rest-sharing
@esri/arcgis-rest-auth
  • Loading branch information
jgravois committed Jun 5, 2018
1 parent 9b2a84c commit 8572bb0
Show file tree
Hide file tree
Showing 11 changed files with 893 additions and 45 deletions.
12 changes: 12 additions & 0 deletions packages/arcgis-rest-auth/src/UserSession.ts
Expand Up @@ -10,6 +10,7 @@ import {
} from "@esri/arcgis-rest-request";
import { generateToken } from "./generate-token";
import { fetchToken, IFetchTokenResponse } from "./fetch-token";
import { getUserInfo } from "./user-info";

/**
* Internal utility for resolving a Promise from outside its constructor.
Expand Down Expand Up @@ -154,6 +155,11 @@ export interface IUserSessionOptions {
* Duration (in minutes) that a refresh token will be valid.
*/
refreshTokenTTL?: number;

/**
* The result of an authenticated request to http://www.arcgis.com/sharing/rest/community/users/[username]
*/
userInfo?: any;
}

/**
Expand Down Expand Up @@ -198,6 +204,11 @@ export class UserSession implements IAuthenticationManager {
*/
readonly refreshTokenTTL: number;

// /**
// * The result of an authenticated request to http://www.arcgis.com/sharing/rest/community/users/[username].
// */
userInfo?: any;

private _token: string;
private _tokenExpires: Date;
private _refreshToken: string;
Expand Down Expand Up @@ -264,6 +275,7 @@ export class UserSession implements IAuthenticationManager {
this.refreshTokenTTL = options.refreshTokenTTL || 1440;
this.trustedServers = {};
this._pendingTokenRequests = {};
this.userInfo = null; // placeholder for eventual, optional, metadata request
}

/**
Expand Down
1 change: 1 addition & 0 deletions packages/arcgis-rest-auth/src/index.ts
Expand Up @@ -3,3 +3,4 @@ export * from "./UserSession";
export * from "./fetch-token";
export * from "./generate-token";
export * from "./authenticated-request-options";
export * from "./user-info";
28 changes: 28 additions & 0 deletions packages/arcgis-rest-auth/src/user-info.ts
@@ -0,0 +1,28 @@
import { UserSession } from "./UserSession";

import { IUserRequestOptions } from "./authenticated-request-options";
import { getPortalUrl, request } from "@esri/arcgis-rest-request";

export interface IUserInfo {
role?: string;
}

/**
* Used internally by packages for requests that require user authentication.
*/
export function getUserInfo(session: UserSession): Promise<IUserInfo> {
if (session.userInfo) {
return new Promise(resolve => resolve(session.userInfo));
} else {
const url = `${session.portal}/community/users/${encodeURIComponent(
session.username
)}`;
return request(url, {
authentication: session,
httpMethod: "GET"
}).then(response => {
session.userInfo = response;
return response;
});
}
}
2 changes: 1 addition & 1 deletion packages/arcgis-rest-items/test/mocks/search.ts
Expand Up @@ -8,7 +8,7 @@ export const SearchResponse: ISearchResult = {
nextStart: 2,
results: [
{
id: "a5b15fe368684a66b8c85a6cadaef9e5",
id: "a5b",
owner: "dcadminqa",
created: 1496748288000,
modified: 1508856526000,
Expand Down
@@ -1,5 +1,6 @@
/* Copyright (c) 2018 Environmental Systems Research Institute, Inc.
* Apache-2.0 */

import {
request,
IRequestOptions,
Expand All @@ -8,26 +9,21 @@ import {

import { UserSession } from "@esri/arcgis-rest-auth";

export interface ISetAccessRequestOptions extends IRequestOptions {
/**
* Item identifier
*/
id: string;
/**
* Item owner, if different from the authenticated user.
*/
owner?: string;
import {
ISharingRequestOptions,
ISharingResponse,
isItemOwner,
getSharingUrl,
isOrgAdmin
} from "./helpers";

export interface ISetAccessRequestOptions extends ISharingRequestOptions {
/**
* "private" indicates that the item can only be accessed by the user. "public" means accessible to anyone. An item shared to the organization has an access level of "org".
*/
access: "private" | "org" | "public";
authentication?: UserSession;
}

export interface ISharingResponse {
notSharedWith: string[];
itemId: string;
}
/**
* Set access level of an item to 'public', 'org', or 'private'.
*
Expand All @@ -47,32 +43,25 @@ export interface ISharingResponse {
export function setItemAccess(
requestOptions: ISetAccessRequestOptions
): Promise<ISharingResponse> {
const username = requestOptions.authentication.username;
const owner = requestOptions.owner || username;
const sharingUrl = `${getPortalUrl(
requestOptions
)}/content/users/${encodeURIComponent(owner)}/items/${
requestOptions.id
}/share`;
const usernameUrl = `${getPortalUrl(
requestOptions
)}/community/users/${encodeURIComponent(username)}`;
const url = getSharingUrl(requestOptions);

if (owner !== username) {
// more manual than calling out to "@esri/arcgis-rest-users, but one less dependency
return request(usernameUrl, {
authentication: requestOptions.authentication
}).then(response => {
if (!response.role || response.role !== "org_admin") {
if (isItemOwner(requestOptions)) {
// if the user owns the item, proceed
return updateItemAccess(url, requestOptions);
} else {
// otherwise we need to check to see if they are an organization admin
return isOrgAdmin(requestOptions).then(admin => {
if (admin) {
return updateItemAccess(url, requestOptions);
} else {
// if neither, updating the sharing isnt possible
throw Error(
`This item can not be shared by ${username}. They are neither the item owner nor an organization admin.`
`This item can not be shared by ${
requestOptions.authentication.username
}. They are neither the item owner nor an organization admin.`
);
} else {
return updateItemAccess(sharingUrl, requestOptions);
}
});
} else {
return updateItemAccess(sharingUrl, requestOptions);
}
}

Expand All @@ -86,12 +75,14 @@ function updateItemAccess(
...requestOptions.params
};

// if the user wants to make the item private, it needs to be unshared from any/all groups as well
if (requestOptions.access === "private") {
requestOptions.params.groups = " ";
}
if (requestOptions.access === "org") {
requestOptions.params.org = true;
}
// if sharing with everyone, share with the entire organization as well.
if (requestOptions.access === "public") {
requestOptions.params.org = true;
requestOptions.params.everyone = true;
Expand Down
206 changes: 206 additions & 0 deletions packages/arcgis-rest-sharing/src/group-sharing.ts
@@ -0,0 +1,206 @@
/* Copyright (c) 2018 Environmental Systems Research Institute, Inc.
* Apache-2.0 */

import {
request,
IRequestOptions,
getPortalUrl
} from "@esri/arcgis-rest-request";

import {
ISharingRequestOptions,
ISharingResponse,
isOrgAdmin,
getUserMembership
} from "./helpers";

export interface IGroupSharingRequestOptions extends ISharingRequestOptions {
/**
* Group identifier
*/
groupId: string;
confirmItemControl?: boolean;
}

interface IGroupSharingUnsharingRequestOptions
extends IGroupSharingRequestOptions {
action: "share" | "unshare";
}

/**
* Share an item with a group.
*
* ```js
* import { shareItemWithGroup } from '@esri/arcgis-rest-sharing';
*
* shareItemWithGroup({
* id: "abc123",
* groupId: "xyz987",
* authentication: session
* })
* ```
*
* @param requestOptions - Options for the request.
* @returns A Promise that will resolve with the data from the response.
*/
export function shareItemWithGroup(
requestOptions: IGroupSharingRequestOptions
): Promise<ISharingResponse> {
return changeGroupSharing({ action: "share", ...requestOptions });
}

/**
* Stop sharing an item with a group.
*
* ```js
* import { unshareItemWithGroup } from '@esri/arcgis-rest-sharing';
*
* unshareItemWithGroup({
* id: "abc123",
* groupId: "xyz987",
* authentication: session
* })
* ```
*
* @param requestOptions - Options for the request.
* @returns A Promise that will resolve with the data from the response.
*/
export function unshareItemWithGroup(
requestOptions: IGroupSharingRequestOptions
): Promise<ISharingResponse> {
return changeGroupSharing({ action: "unshare", ...requestOptions });
}

/**
* @param requestOptions - Options for the request.
* @returns A Promise that will resolve with the data from the response.
*/
function changeGroupSharing(
requestOptions: IGroupSharingUnsharingRequestOptions
): Promise<ISharingResponse> {
const username = requestOptions.authentication.username;
const owner = requestOptions.owner || username;

return isOrgAdmin(requestOptions).then(admin => {
const resultProp =
requestOptions.action === "share" ? "notSharedWith" : "notUnsharedFrom";
// check if the item has already been shared with the group...
return isItemSharedWithGroup(requestOptions).then(result => {
// console.log(admin);
// if we are sharing and result is true OR we are unsharing and result is false... short circuit
if (
(requestOptions.action === "share" && result === true) ||
(requestOptions.action === "unshare" && result === false)
) {
// and send back the same response structure ArcGIS Online would
const response = { itemId: requestOptions.id, shortcut: true } as any;
response[resultProp] = [];
return response;
} else {
// next check to ensure the user is a member of the group
return getUserMembership(requestOptions)
.then(membership => {
if (membership === "nonmember") {
// abort and reject promise
throw Error(
`This item can not be ${
requestOptions.action
}d by ${username} as they are not a member of the specified group ${
requestOptions.groupId
}.`
);
} else {
// if orgAdmin or owner (and member of group) share using the owner url
if (owner === username || admin) {
return `${getPortalUrl(
requestOptions
)}/content/users/${owner}/items/${requestOptions.id}/${
requestOptions.action
}`;
} else {
// if they are a group admin/owner, use the bare item url
if (membership === "admin") {
return `${getPortalUrl(requestOptions)}/content/items/${
requestOptions.id
}/${requestOptions.action}`;
} else {
// otherwise abort
throw Error(
`This item can not be ${
requestOptions.action
}d by ${username} as they are neither the owner, a groupAdmin of ${
requestOptions.groupId
}, nor an org_admin.`
);
}
}
}
})
.then(url => {
// now its finally time to do the sharing
requestOptions.params = {
groups: requestOptions.groupId,
confirmItemControl: requestOptions.confirmItemControl
};
// dont mixin to ensure that old query parameters from the search request arent included
return request(url, requestOptions);
})
.then(sharingResponse => {
if (sharingResponse[resultProp].length) {
throw Error(
`Item ${requestOptions.id} could not be ${
requestOptions.action
}d to group ${requestOptions.groupId}.`
);
} else {
// all is well
return sharingResponse;
}
});
} // else
}); // then
});
}

/**
* Find out whether or not an item is already shared with a group.
*
* @param requestOptions - Options for the request.
* @returns A Promise that will resolve with the data from the response.
*/
function isItemSharedWithGroup(
requestOptions: IGroupSharingRequestOptions
): Promise<boolean> {
const query = {
q: `id: ${requestOptions.id} AND group: ${requestOptions.groupId}`,
start: 1,
num: 10,
sortField: "title"
};

// instead of calling out to "@esri/arcgis-rest-items, make the request manually to forgoe another dependency
requestOptions.params = {
...query,
...requestOptions.params
};

const url = `${getPortalUrl(requestOptions)}/search`;

return request(url, requestOptions).then(searchResult => {
// if there are no search results at all, we know the item hasnt already been shared with the group
if (searchResult.total === 0) {
return false;
} else {
// otherwise loop through and search for the id
const results = searchResult.results;
const itm = results.find((shadowedItm: { id: string }) => {
return shadowedItm.id === requestOptions.id;
});
if (itm) {
return true;
} else {
return false;
}
}
});
}

0 comments on commit 8572bb0

Please sign in to comment.