Skip to content

Commit

Permalink
feat: scope aware requests (#2797)
Browse files Browse the repository at this point in the history
Adds helpers to determine if current token has a required scope
Adds helper to calculate the minimum scope to be requested if necessary
Fix test for prepScopes to match new behavior

BREAKING CHANGE: prepScopes now accepts an array of scopes, this is checked against the set of currently consented scopes for the user. If any of the supplied scopes are found, then no additional scopes are requested. If no match is found then the user will be prompted to consent to the first scope in the supplied array of scopes

fix: getGroupImage now correctly requires group.read.all or group.readwrite.all

BREAKING CHANGE: applications using mgt-person with fetch-image and person-detal where the supplied value is a group will now need to consent to either Group.Read.All or Group.ReadWrite.All. This replaces the existing behavior where the group image would silently fail to load and show an http 403 error in the console

fix: updated todo and planner permissions

BREAKING CHANGE: minimal permission for planner calls changed from Group.ReadWrite.All to Tasks.ReadWrite for write operation and from Group.Read.All to Tasks.Read for read operations
  • Loading branch information
gavinbarron authored Jan 4, 2024
1 parent 8900eb4 commit 81d124b
Show file tree
Hide file tree
Showing 35 changed files with 421 additions and 197 deletions.
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"editor.defaultFormatter": "esbenp.prettier-vscode",
"prettier.prettierPath": "./node_modules/prettier",
"editor.codeActionsOnSave": {
"source.fixAll.tslint": true
"source.fixAll.tslint": "explicit"
},
"lit-html.tags": ["mgtHtml"],
"typescript.tsdk": "node_modules/typescript/lib",
Expand Down
62 changes: 58 additions & 4 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,12 @@
scopes="user.read,user.read.all,mail.readBasic,people.read,people.read.all,sites.read.all,user.readbasic.all,contacts.read,presence.read,presence.read.all,tasks.readwrite,tasks.read,calendars.read,group.read.all,files.read,files.read.all,files.readwrite,files.readwrite.all"
></mgt-msal2-provider> -->

<mgt-mock-provider></mgt-mock-provider>
<mgt-msal2-provider
client-id="2dfea037-938a-4ed8-9b35-c05708a1b241"
redirect-uri="http://localhost:3000"
></mgt-msal2-provider>

<!-- <mgt-mock-provider></mgt-mock-provider> -->
<header>
<mgt-theme-toggle></mgt-theme-toggle>
</header>
Expand All @@ -58,7 +63,56 @@ <h2>mgt-login</h2>
</div>
<!-- <mgt-login></mgt-login> -->
<!-- <h2>mgt-person me query two lines card on click with presence</h2> -->
<mgt-person person-query="me" view="twoLines" person-card="hover" show-presence></mgt-person>
<!-- <mgt-person person-query="me" view="twoLines" person-card="hover" show-presence></mgt-person> -->
<mgt-person fetch-image person-details='{
"id": "a2a36b00-b196-4286-adfc-7c1bedbffa39",
"deletedDateTime": null,
"classification": null,
"createdDateTime": "2022-07-07T16:04:37Z",
"creationOptions": [
"Team",
"ExchangeProvisioningFlags:3552"
],
"description": "Welcome to the team that we&apos;ve assembled to create the Mark 8.",
"displayName": "Mark 8 Project Team",
"expirationDateTime": null,
"groupTypes": [
"Unified"
],
"isAssignableToRole": null,
"mail": "[email protected]",
"mailEnabled": true,
"mailNickname": "Mark8ProjectTeam",
"membershipRule": null,
"membershipRuleProcessingState": null,
"onPremisesDomainName": null,
"onPremisesLastSyncDateTime": null,
"onPremisesNetBiosName": null,
"onPremisesSamAccountName": null,
"onPremisesSecurityIdentifier": null,
"onPremisesSyncEnabled": null,
"preferredDataLocation": null,
"preferredLanguage": null,
"proxyAddresses": [
"SPO:SPO_8e2a3ed7-94b3-4a3f-a010-06c6aadcbdbb@SPO_262437ff-351d-4f6c-b543-bac09b5a0c23",
"SMTP:[email protected]"
],
"renewedDateTime": "2022-07-07T16:04:37Z",
"resourceBehaviorOptions": [
"HideGroupInOutlook",
"SubscribeMembersToCalendarEventsDisabled",
"WelcomeEmailDisabled"
],
"resourceProvisioningOptions": [
"Team"
],
"securityEnabled": false,
"securityIdentifier": "S-1-12-1-2728618752-1116123542-461175981-972734445",
"theme": null,
"visibility": "Public",
"onPremisesProvisioningErrors": [],
"serviceProvisioningErrors": []
}'></mgt-person>
<!-- <mgt-person-card person-query="me"></mgt-person-card> -->
<!-- <h2>mgt-people-picker</h2>
<mgt-people-picker selection-mode="single" show-max="6"></mgt-people-picker>
Expand All @@ -69,8 +123,8 @@ <h2>mgt-login</h2>
<mgt-planner></mgt-planner> -->
<!-- <h2>mgt-agenda group-by-day</h2>
<mgt-agenda group-by-day></mgt-agenda> -->
<!-- <h2>mgt-people show-presence</h2>
<mgt-people show-presence></mgt-people> -->
<h2>mgt-people show-presence</h2>
<mgt-people group-id="a2a36b00-b196-4286-adfc-7c1bedbffa39"></mgt-people>
<!-- <h2>mgt-todo</h2>
<mgt-todo></mgt-todo> -->
<!-- <h2>mgt-file-list</h2>
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,9 @@
"setLicense": "gulp setLicense",
"test": "npm run wtr:coverage",
"version:tsc": "tsc -v",
"wtr": "wtr --puppeteer",
"wtr": "wtr --playwright",
"wtr:coverage": "wtr --playwright --coverage",
"wtr:watch": "wtr --puppeteer --watch"
"wtr:watch": "wtr --playwright --watch"
},
"devDependencies": {
"@babel/core": "^7.12.8",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,33 @@
import { GraphPageIterator, IGraph, prepScopes } from '@microsoft/mgt-element';
import * as MicrosoftGraph from '@microsoft/microsoft-graph-types';

/**
*
* @param {IGraph} graph
* @param {string} query the graph resource and query string to be requested
* @param {string[]} additionalScopes an array of scope to be requested before making the request
* Should be calculated by the calling code using `IProvider.needsAdditionalScopes()`
* @returns {Promise<GraphPageIterator<MicrosoftGraph.Event>>} a page iterator to allow
* the calling code to request more data if present and needed
*/
export const getEventsQueryPageIterator = async (
graph: IGraph,
query: string,
scopes = 'calendars.read'
additionalScopes: string[]
): Promise<GraphPageIterator<MicrosoftGraph.Event>> => {
const request = graph.api(query).middlewareOptions(prepScopes(scopes)).orderby('start/dateTime');
const request = graph.api(query).middlewareOptions(prepScopes(additionalScopes)).orderby('start/dateTime');

return GraphPageIterator.create<MicrosoftGraph.Event>(graph, request);
};

/**
* returns Calender events iterator associated with either the logged in user or a specific groupId
*
* @param {IGraph} graph
* @param {Date} startDateTime
* @param {Date} endDateTime
* @param {string} [groupId]
* @param {string} preferredTimezone
* @returns {(Promise<Event[]>)}
* @returns {Promise<GraphPageIterator<MicrosoftGraph.Event>>}
* @memberof Graph
*/
export const getEventsPageIterator = async (
Expand All @@ -37,15 +46,12 @@ export const getEventsPageIterator = async (
const sdt = `startdatetime=${startDateTime.toISOString()}`;
const edt = `enddatetime=${endDateTime.toISOString()}`;

let uri: string;

if (groupId) {
uri = `groups/${groupId}/calendar`;
} else {
uri = 'me';
}

uri += `/calendarview?${sdt}&${edt}`;
const uri: string = groupId
? `groups/${groupId}/calendar/calendarview?${sdt}&${edt}`
: `me/calendarview?${sdt}&${edt}`;
const allValidScopes = groupId
? ['Group.Read.All', 'Group.ReadWrite.All']
: ['Calendars.ReadBasic', 'Calendars.Read', 'Calendars.ReadWrite'];

return getEventsQueryPageIterator(graph, uri);
return getEventsQueryPageIterator(graph, uri, allValidScopes);
};
Original file line number Diff line number Diff line change
Expand Up @@ -607,7 +607,7 @@ export class MgtAgenda extends MgtTemplatedComponent {
} else {
query = this.eventQuery;
}
const iterator = await getEventsQueryPageIterator(graph, query, scope);
const iterator = await getEventsQueryPageIterator(graph, query, scope ? [scope] : []);
if (iterator?.value) {
events = iterator.value;

Expand Down
4 changes: 2 additions & 2 deletions packages/mgt-components/src/components/mgt-get/mgt-get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,7 @@ export class MgtGet extends MgtTemplatedComponent {
let request: GraphRequest = graph.api(uri).version(this.version);

if (this.scopes?.length) {
request = request.middlewareOptions(prepScopes(...this.scopes));
request = request.middlewareOptions(prepScopes(this.scopes));
}

if (this.type === ResponseType.json) {
Expand All @@ -390,7 +390,7 @@ export class MgtGet extends MgtTemplatedComponent {
) {
pageCount++;
const nextResource = (page['@odata.nextLink'] as string).split(this.version)[1];
page = (await graph.client.api(nextResource).version(this.version).get()) as CollectionResponse<Entity>;
page = (await graph.api(nextResource).version(this.version).get()) as CollectionResponse<Entity>;
if (page?.value?.length) {
page.value = response.value.concat(page.value);
response = page;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { getEmailFromGraphEntity } from '../../graph/graph.people';
import { IDynamicPerson } from '../../graph/types';
import { MgtPersonCardState } from './mgt-person-card.types';
import { MgtPersonCardConfig } from './MgtPersonCardConfig';
import { validUserByIdScopes } from '../../graph/graph.user';
import { validInsightScopes } from '../../graph/graph.files';

const userProperties =
'businessPhones,companyName,department,displayName,givenName,jobTitle,mail,mobilePhone,officeLocation,preferredLanguage,surname,userPrincipalName,id,accountEnabled';
Expand Down Expand Up @@ -108,7 +110,7 @@ const buildOrgStructureRequest = (batch: IBatch, userId: string) => {
batch.get(
batchKeys.person,
`users/${userId}?$expand=${expandManagers}&$select=${userProperties}&$count=true`,
['user.read.all'],
validUserByIdScopes,
{
ConsistencyLevel: 'eventual'
}
Expand All @@ -118,11 +120,11 @@ const buildOrgStructureRequest = (batch: IBatch, userId: string) => {
};

const buildWorksWithRequest = (batch: IBatch, userId: string) => {
batch.get(batchKeys.people, `users/${userId}/people?$filter=personType/class eq 'Person'`, ['People.Read.All']);
batch.get(batchKeys.people, `users/${userId}/people?$filter=personType/class eq 'Person'`, validUserByIdScopes);
};

const validMailSearchScopes = ['Mail.ReadBasic', 'Mail.Read', 'Mail.ReadWrite'];
const buildMessagesWithUserRequest = (batch: IBatch, emailAddress: string) => {
batch.get(batchKeys.messages, `me/messages?$search="from:${emailAddress}"`, ['Mail.ReadBasic']);
batch.get(batchKeys.messages, `me/messages?$search="from:${emailAddress}"`, validMailSearchScopes);
};

const buildFilesRequest = (batch: IBatch, emailAddress?: string) => {
Expand All @@ -134,7 +136,7 @@ const buildFilesRequest = (batch: IBatch, emailAddress?: string) => {
request = 'me/insights/used';
}

batch.get(batchKeys.files, request, ['Sites.Read.All']);
batch.get(batchKeys.files, request, validInsightScopes);
};

/**
Expand All @@ -145,7 +147,13 @@ const buildFilesRequest = (batch: IBatch, emailAddress?: string) => {
* @return {*} {Promise<Profile>}
*/
const getProfile = async (graph: IGraph, userId: string): Promise<Profile> =>
(await graph.api(`/users/${userId}/profile`).version('beta').get()) as Profile;
(await graph
.api(`/users/${userId}/profile`)
.version('beta')
.middlewareOptions(prepScopes(validUserByIdScopes))
.get()) as Profile;

const validCreateChatScopes = ['Chat.Create', 'Chat.ReadWrite'];

/**
* Initiate a chat to a user
Expand All @@ -157,7 +165,7 @@ const getProfile = async (graph: IGraph, userId: string): Promise<Profile> =>
*/
export const createChat = async (graph: IGraph, person: string, user: string): Promise<Chat> => {
const chatData = {
chatType: 'oneonOne',
chatType: 'oneOnOne',
members: [
{
'@odata.type': '#microsoft.graph.aadUserConversationMember',
Expand All @@ -174,10 +182,12 @@ export const createChat = async (graph: IGraph, person: string, user: string): P
return (await graph
.api('/chats')
.header('Cache-Control', 'no-store')
.middlewareOptions(prepScopes('Chat.Create', 'Chat.ReadWrite'))
.middlewareOptions(prepScopes(validCreateChatScopes))
.post(chatData)) as Chat;
};

const validSendChatMessageScopes = ['ChatMessage.Send', 'Chat.ReadWrite'];

/**
* Send a chat message to a user
*
Expand All @@ -194,5 +204,5 @@ export const sendMessage = async (
(await graph
.api(`/chats/${chatId}/messages`)
.header('Cache-Control', 'no-store')
.middlewareOptions(prepScopes('Chat.ReadWrite', 'ChatMessage.Send'))
.middlewareOptions(prepScopes(validSendChatMessageScopes))
.post(messageData)) as ChatMessage;
21 changes: 11 additions & 10 deletions packages/mgt-components/src/components/mgt-planner/graph.planner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import { PlannerAssignments, PlannerBucket, PlannerPlan, PlannerTask } from '@mi
import { CollectionResponse } from '@microsoft/mgt-element';
import { ITask } from './task-sources';

const writePlannerDataScopes = ['Tasks.ReadWrite', 'Group.ReadWrite.All'];
const readPlannerDataScopes = ['Tasks.Read', 'Group.Read.All', 'Tasks.ReadWrite', 'Group.ReadWrite.All'];

/**
* async promise, allows developer to create new Planner task
*
Expand All @@ -22,7 +25,7 @@ export const addPlannerTask = async (graph: IGraph, newTask: PlannerTask): Promi
return (await graph
.api('/planner/tasks')
.header('Cache-Control', 'no-store')
.middlewareOptions(prepScopes('Group.ReadWrite.All'))
.middlewareOptions(prepScopes(writePlannerDataScopes))
.post(newTask)) as PlannerTask;
};

Expand Down Expand Up @@ -57,7 +60,7 @@ export const removePlannerTask = async (graph: IGraph, task: ITask): Promise<voi
.api(`/planner/tasks/${task.id}`)
.header('Cache-Control', 'no-store')
.header('If-Match', task.eTag)
.middlewareOptions(prepScopes('Group.ReadWrite.All'))
.middlewareOptions(prepScopes(writePlannerDataScopes))
.delete();
};

Expand Down Expand Up @@ -100,7 +103,7 @@ export const setPlannerTaskDetails = async (graph: IGraph, task: ITask, details:
response = (await graph
.api(`/planner/tasks/${task.id}`)
.header('Cache-Control', 'no-store')
.middlewareOptions(prepScopes('Group.ReadWrite.All'))
.middlewareOptions(prepScopes(writePlannerDataScopes))
.header('Prefer', 'return=representation')
.header('If-Match', task.eTag)
.update(details)) as PlannerTask;
Expand All @@ -119,13 +122,11 @@ export const setPlannerTaskDetails = async (graph: IGraph, task: ITask, details:
* @memberof Graph
*/
export const getPlansForGroup = async (graph: IGraph, groupId: string): Promise<PlannerPlan[]> => {
const scopes = 'Group.Read.All';

const uri = `/groups/${groupId}/planner/plans`;
const plans = (await graph
.api(uri)
.header('Cache-Control', 'no-store')
.middlewareOptions(prepScopes(scopes))
.middlewareOptions(prepScopes(readPlannerDataScopes))
.get()) as CollectionResponse<PlannerPlan>;
return plans?.value;
};
Expand All @@ -142,7 +143,7 @@ export const getSinglePlannerPlan = async (graph: IGraph, planId: string): Promi
(await graph
.api(`/planner/plans/${planId}`)
.header('Cache-Control', 'no-store')
.middlewareOptions(prepScopes('Group.Read.All'))
.middlewareOptions(prepScopes(readPlannerDataScopes))
.get()) as PlannerPlan;

/**
Expand All @@ -157,7 +158,7 @@ export const getBucketsForPlannerPlan = async (graph: IGraph, planId: string): P
const buckets = (await graph
.api(`/planner/plans/${planId}/buckets`)
.header('Cache-Control', 'no-store')
.middlewareOptions(prepScopes('Group.Read.All'))
.middlewareOptions(prepScopes(readPlannerDataScopes))
.get()) as CollectionResponse<PlannerBucket>;

return buckets?.value;
Expand All @@ -174,7 +175,7 @@ export const getAllMyPlannerPlans = async (graph: IGraph): Promise<PlannerPlan[]
const plans = (await graph
.api('/me/planner/plans')
.header('Cache-Control', 'no-store')
.middlewareOptions(prepScopes('Group.Read.All'))
.middlewareOptions(prepScopes(readPlannerDataScopes))
.get()) as CollectionResponse<PlannerPlan>;

return plans?.value;
Expand All @@ -192,7 +193,7 @@ export const getTasksForPlannerBucket = async (graph: IGraph, bucketId: string):
const tasks = (await graph
.api(`/planner/buckets/${bucketId}/tasks`)
.header('Cache-Control', 'no-store')
.middlewareOptions(prepScopes('Group.Read.All'))
.middlewareOptions(prepScopes(readPlannerDataScopes))
.get()) as CollectionResponse<PlannerTask>;

return tasks?.value;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -528,7 +528,7 @@ export class MgtSearchResults extends MgtTemplatedComponent {
let request = graph.api(this.searchEndpoint).version(this.version);

if (this.scopes?.length) {
request = request.middlewareOptions(prepScopes(...this.scopes));
request = request.middlewareOptions(prepScopes(this.scopes));
}

response = (await request.post({ requests: [requestOptions] })) as SearchResponseCollection;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export const getAllMyTeams = async (graph: IGraph, scopes: string[]): Promise<Te
const teams = (await graph
.api('/me/joinedTeams')
.select(['displayName', 'id', 'isArchived'])
.middlewareOptions(prepScopes(...scopes))
.middlewareOptions(prepScopes(scopes))
.get()) as CollectionResponse<Team>;

return teams?.value;
Expand All @@ -42,7 +42,7 @@ type CachePhotos = Record<string, CachePhoto>;
* @param teamIds {string[]}
* @returns {Promise<CachePhotos>}
*/
export const getTeamsPhotosforPhotoIds = async (graph: BetaGraph, teamIds: string[]): Promise<CachePhotos> => {
export const getTeamsPhotosForPhotoIds = async (graph: BetaGraph, teamIds: string[]): Promise<CachePhotos> => {
let cache: CacheStore<CachePhoto>;
let photos: CachePhotos = {};

Expand Down
Loading

0 comments on commit 81d124b

Please sign in to comment.