├── .gitattributes
├── .gitignore
├── LICENSE
├── Nuget.Config
├── README.md
├── SECURITY.md
├── WebJobs.Extensions.O365.sln
├── appveyor.yml
├── blog.md
├── samples
└── GraphExtensionSamples
│ ├── ExcelExamples.cs
│ ├── GraphExtensionSamples.csproj
│ ├── OneDriveExamples.cs
│ ├── OutlookExamples.cs
│ ├── TokenExamples.cs
│ ├── WebhookSubscriptionExamples.cs
│ └── WebhookTriggerExamples.cs
├── sign.snk
├── src
├── MicrosoftGraphBinding
│ ├── Bindings
│ │ ├── ExcelAsyncCollector.cs
│ │ ├── ExcelAttribute.cs
│ │ ├── GraphTokenBaseAttribute.cs
│ │ ├── GraphWebhookChangeType.cs
│ │ ├── GraphWebhookSubscriptionAction.cs
│ │ ├── GraphWebhookSubscriptionAsyncCollector.cs
│ │ ├── GraphWebhookSubscriptionAttribute.cs
│ │ ├── GraphWebhookSubscriptionHandler.cs
│ │ ├── GraphWebhookTriggerAttribute.cs
│ │ ├── O365Constants.cs
│ │ ├── O365Models.cs
│ │ ├── OneDriveAsyncCollector.cs
│ │ ├── OneDriveAttribute.cs
│ │ ├── OneDriveWriteStream.cs
│ │ ├── OutlookAsyncCollector.cs
│ │ ├── OutlookAttribute.cs
│ │ ├── WebhookTriggerBindingProvider.cs
│ │ ├── WebhookTriggerData.cs
│ │ └── WebhookTriggerListener.cs
│ ├── Config
│ │ ├── Converters
│ │ │ ├── ExcelConverters.cs
│ │ │ ├── GraphWebhookSubscriptionConverters.cs
│ │ │ ├── OneDriveConverter.cs
│ │ │ └── OutlookConverter.cs
│ │ ├── GraphOptions.cs
│ │ ├── GraphServiceClientManager.cs
│ │ ├── GraphServiceClientProvider.cs
│ │ ├── IGraphServiceClientProvider.cs
│ │ ├── IGraphSubscriptionStore.cs
│ │ ├── MicrosoftGraphExtensionConfigProvider.cs
│ │ ├── MicrosoftGraphWebJobsBuilderExtensions.cs
│ │ ├── MicrosoftGraphWebJobsStartup.cs
│ │ ├── SubscriptionEntry.cs
│ │ └── WebhookSubscriptionStore.cs
│ ├── MicrosoftGraphBinding.csproj
│ ├── Services
│ │ ├── ExcelClient.cs
│ │ ├── ExcelService.cs
│ │ ├── OneDriveClient.cs
│ │ ├── OneDriveService.cs
│ │ ├── OutlookClient.cs
│ │ └── OutlookService.cs
│ └── stylecop.json
└── TokenBinding
│ ├── AadClient.cs
│ ├── AuthTokenExtensionConfigProvider.cs
│ ├── AuthTokenWebJobsBuilderExtensions.cs
│ ├── AuthTokenWebJobsStartup.cs
│ ├── Constants.cs
│ ├── EasyAuthTokenClient.cs
│ ├── EasyAuthTokenManager.cs
│ ├── EasyAuthTokenStoreEntry.cs
│ ├── IAadClient.cs
│ ├── IEasyAuthClient.cs
│ ├── TokenAttribute.cs
│ ├── TokenBaseAttribute.cs
│ ├── TokenBinding.csproj
│ ├── TokenConverter.cs
│ ├── TokenIdentityMode.cs
│ └── TokenOptions.cs
├── test
├── MicrosoftGraph.Tests
│ ├── ExcelMockUtilities.cs
│ ├── ExcelTests.cs
│ ├── MemorySubscriptionStore.cs
│ ├── MicrosoftGraph.Tests.csproj
│ ├── MockGraphServiceClientProvider.cs
│ ├── MockTokenConverter.cs
│ ├── OneDriveMockUtilities.cs
│ ├── OneDriveTests.cs
│ ├── OutlookMockUtilities.cs
│ ├── OutlookTests.cs
│ └── common
│ │ └── CommonUtilities.cs
└── TokenExtension.Tests
│ ├── TokenExtension.Tests.csproj
│ ├── TokenTests.cs
│ └── common
│ ├── FakeLocator.cs
│ ├── OutputContainer.cs
│ └── TestHelpers.cs
└── update
├── README.md
└── extensions.csproj
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | bin
2 | debug
3 | obj
4 | *.suo
5 | packages
6 |
7 | publishprofiles
8 | .vs
9 | *.user
10 |
11 | *.config
12 | appsettings*
13 | buildoutput
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) .NET Foundation. All rights reserved.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE
22 |
--------------------------------------------------------------------------------
/Nuget.Config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Azure Functions bindings to O365
2 |
3 | **NOTE: This project is no longer maintained and is not recommended for new development. To integrate with the Microsoft Graph from Azure Functions, please instead see [this tutorial](https://docs.microsoft.com/en-us/graph/tutorials/azure-functions).**
4 |
5 | Prototype for some WebJobs extension bindings.
6 |
7 | This provides a sample Azure Function extensions for Office.
8 |
9 | ## This provides a few bindings:
10 |
11 | - [Outlook] - sends emails from an O365 account
12 | - [OneDrive] - reads/writes a Onedrive file
13 | - [Excel] - reads/writes an Excel table or worksheet.
14 | - [Token] - this has been extended to allow binding to the MS Graph SDK's Graph Service Client
15 | - [GraphWebhookSubscription] - creates, deletes, or refreshes a Graph Webhook. See https://github.com/microsoftgraph/aspnet-webhooks-rest-sample for graph webhooks sample.
16 | - [GraphWebhookTrigger] - the trigger that activates a function when a Graph Webhook is called for its datatype
17 |
18 | The bindings found in the Microsoft Graph extension use the same authentication process as those in the Token Extension. You can see how to use these bindings in the samples directory.
19 |
20 | ## For Authentication
21 |
22 | Authentication is built using Easy Auth's token store. (see https://cgillum.tech/2016/03/07/app-service-token-store/ )
23 | The app has an AAD app registered that has been configured with access to the Graph API and given appropriate scopes. The bindings can access the client secret (via appsettings) and use that to perform token exchanges.
24 |
25 | The bindings can authenticate in 4 different ways:
26 | - UserFromId - the token is grabbed on behalf of a provided user id
27 | - UserFromToken - the token is grabbed on behalf of a provided user id token
28 | - UserFromRequest - the token is grabbed on behalf of the user id token found in the HTTP header `X-MS-TOKEN-AAD-ID-TOKEN`
29 | - ClientCredentials - uses the app's credentials to access AAD
30 |
31 | ## Source layout
32 | The samples directory contains examples of how to use the bindings. The code for both the Token and Microsoft Graph extensions can be found in the src directory, and in-memory tests can be found in the tests directory.
33 |
34 | ## Local Development
35 | First create a Functions app using the Functions CLI found https://docs.microsoft.com/en-us/azure/azure-functions/functions-run-local. Be sure to use the Version 2.x runtime.
36 |
37 | To install the Token extension, run the command `func extensions install --package Microsoft.Azure.WebJobs.Extensions.AuthTokens -v `.
38 |
39 | To install the Microsoft Graph extension, run the command `func extensions install --package Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph -v `.
40 |
41 | The easiest way to utilize most of the features of these features is to configure an Azure Functions app in the Portal, enable Authentication/Authorization, add the extension, and go through the configuration to enable the proper Microsoft Graph permissions.
42 |
43 | If you are making code changes to the extensions themselves and wish to test these locally, you can manually copy the .dll files that you build into your bin directory in your local function app's directory.
44 |
45 | App Settings to Modify in `local.settings.json`:
46 | - `WEBSITE_AUTH_CLIENT_ID` - Copy from your App Settings in Kudu from your configured app
47 | - `WEBSITE_AUTH_CLIENT_SECRET` - Copy from your App Settings in Kudu from your configured app
48 | - `WEBSITE_AUTH_OPENID_ISSUER` - (Optional, Required for a `ClientCredentials` `TokenIdentityMode`) Copy from your App Settings in Kudu from your configured app (or set to `https://sts.windows.net/` for Azure Active Directory)
49 | - `BYOB_TokenMap` - A valid local directory that you have read/write access to
50 |
51 | ## Current Version
52 | Latest Version: 1.0.0-beta6
53 | Portal Version: 1.0.0-beta5
54 |
55 | If you want to get the latest features and bugfixes, you can manually update to the latest version by following the instructions in the `update` folder. NOTE: If you update to a different version than the version in the portal, you cannot use the Token and Microsoft Graph templates.
56 |
57 | ## License
58 |
59 | This project is under the benevolent umbrella of the [.NET Foundation](http://www.dotnetfoundation.org/) and is licensed under [the MIT License](https://github.com/Azure/azure-webjobs-sdk/blob/master/LICENSE.txt)
60 |
61 | ## Contributing
62 |
63 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
64 |
65 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ## Security
4 |
5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/).
6 |
7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below.
8 |
9 | ## Reporting Security Issues
10 |
11 | **Please do not report security vulnerabilities through public GitHub issues.**
12 |
13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report).
14 |
15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey).
16 |
17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc).
18 |
19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
20 |
21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
22 | * Full paths of source file(s) related to the manifestation of the issue
23 | * The location of the affected source code (tag/branch/commit or direct URL)
24 | * Any special configuration required to reproduce the issue
25 | * Step-by-step instructions to reproduce the issue
26 | * Proof-of-concept or exploit code (if possible)
27 | * Impact of the issue, including how an attacker might exploit the issue
28 |
29 | This information will help us triage your report more quickly.
30 |
31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs.
32 |
33 | ## Preferred Languages
34 |
35 | We prefer all communications to be in English.
36 |
37 | ## Policy
38 |
39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd).
40 |
41 |
42 |
--------------------------------------------------------------------------------
/WebJobs.Extensions.O365.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 15
4 | VisualStudioVersion = 15.0.27004.2005
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MicrosoftGraphBinding", "src\MicrosoftGraphBinding\MicrosoftGraphBinding.csproj", "{B503849F-9A01-48E8-9401-76769F4CC745}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TokenBinding", "src\TokenBinding\TokenBinding.csproj", "{397A114E-0075-48F8-AA01-11F3FDFDEC25}"
9 | EndProject
10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{06065EFC-CB7A-49E4-B57F-B89B8CC01AA8}"
11 | EndProject
12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TokenExtension.Tests", "test\TokenExtension.Tests\TokenExtension.Tests.csproj", "{39053A38-67E7-46BB-AFD6-EFEF705ECE6C}"
13 | EndProject
14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MicrosoftGraph.Tests", "test\MicrosoftGraph.Tests\MicrosoftGraph.Tests.csproj", "{680406E6-3C6B-4006-81A3-B6AD88B78ABA}"
15 | EndProject
16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{3F0C104A-CC6F-4A80-8196-94C476495CF2}"
17 | EndProject
18 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{E0C76B7E-D030-4B52-A288-3790ABC4A72C}"
19 | EndProject
20 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphExtensionSamples", "samples\GraphExtensionSamples\GraphExtensionSamples.csproj", "{B0A7EE27-89D7-47BF-B132-2FC1FB9C5F14}"
21 | EndProject
22 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{D9B31118-B878-4314-9B7C-0E8D887648CC}"
23 | ProjectSection(SolutionItems) = preProject
24 | README.md = README.md
25 | EndProjectSection
26 | EndProject
27 | Global
28 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
29 | Debug|Any CPU = Debug|Any CPU
30 | Release|Any CPU = Release|Any CPU
31 | EndGlobalSection
32 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
33 | {B503849F-9A01-48E8-9401-76769F4CC745}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
34 | {B503849F-9A01-48E8-9401-76769F4CC745}.Debug|Any CPU.Build.0 = Debug|Any CPU
35 | {B503849F-9A01-48E8-9401-76769F4CC745}.Release|Any CPU.ActiveCfg = Release|Any CPU
36 | {B503849F-9A01-48E8-9401-76769F4CC745}.Release|Any CPU.Build.0 = Release|Any CPU
37 | {397A114E-0075-48F8-AA01-11F3FDFDEC25}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
38 | {397A114E-0075-48F8-AA01-11F3FDFDEC25}.Debug|Any CPU.Build.0 = Debug|Any CPU
39 | {397A114E-0075-48F8-AA01-11F3FDFDEC25}.Release|Any CPU.ActiveCfg = Release|Any CPU
40 | {397A114E-0075-48F8-AA01-11F3FDFDEC25}.Release|Any CPU.Build.0 = Release|Any CPU
41 | {39053A38-67E7-46BB-AFD6-EFEF705ECE6C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
42 | {39053A38-67E7-46BB-AFD6-EFEF705ECE6C}.Debug|Any CPU.Build.0 = Debug|Any CPU
43 | {39053A38-67E7-46BB-AFD6-EFEF705ECE6C}.Release|Any CPU.ActiveCfg = Release|Any CPU
44 | {39053A38-67E7-46BB-AFD6-EFEF705ECE6C}.Release|Any CPU.Build.0 = Release|Any CPU
45 | {680406E6-3C6B-4006-81A3-B6AD88B78ABA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
46 | {680406E6-3C6B-4006-81A3-B6AD88B78ABA}.Debug|Any CPU.Build.0 = Debug|Any CPU
47 | {680406E6-3C6B-4006-81A3-B6AD88B78ABA}.Release|Any CPU.ActiveCfg = Release|Any CPU
48 | {680406E6-3C6B-4006-81A3-B6AD88B78ABA}.Release|Any CPU.Build.0 = Release|Any CPU
49 | {B0A7EE27-89D7-47BF-B132-2FC1FB9C5F14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
50 | {B0A7EE27-89D7-47BF-B132-2FC1FB9C5F14}.Debug|Any CPU.Build.0 = Debug|Any CPU
51 | {B0A7EE27-89D7-47BF-B132-2FC1FB9C5F14}.Release|Any CPU.ActiveCfg = Release|Any CPU
52 | {B0A7EE27-89D7-47BF-B132-2FC1FB9C5F14}.Release|Any CPU.Build.0 = Release|Any CPU
53 | EndGlobalSection
54 | GlobalSection(SolutionProperties) = preSolution
55 | HideSolutionNode = FALSE
56 | EndGlobalSection
57 | GlobalSection(NestedProjects) = preSolution
58 | {B503849F-9A01-48E8-9401-76769F4CC745} = {3F0C104A-CC6F-4A80-8196-94C476495CF2}
59 | {397A114E-0075-48F8-AA01-11F3FDFDEC25} = {3F0C104A-CC6F-4A80-8196-94C476495CF2}
60 | {39053A38-67E7-46BB-AFD6-EFEF705ECE6C} = {06065EFC-CB7A-49E4-B57F-B89B8CC01AA8}
61 | {680406E6-3C6B-4006-81A3-B6AD88B78ABA} = {06065EFC-CB7A-49E4-B57F-B89B8CC01AA8}
62 | {B0A7EE27-89D7-47BF-B132-2FC1FB9C5F14} = {E0C76B7E-D030-4B52-A288-3790ABC4A72C}
63 | EndGlobalSection
64 | GlobalSection(ExtensibilityGlobals) = postSolution
65 | SolutionGuid = {443E037E-BBF5-466D-831A-91A375A7FFDC}
66 | EndGlobalSection
67 | EndGlobal
68 |
--------------------------------------------------------------------------------
/appveyor.yml:
--------------------------------------------------------------------------------
1 | version: 1.0.{build}
2 | pull_requests:
3 | do_not_increment_build_number: true
4 | branches:
5 | only:
6 | - master
7 | image: Visual Studio 2017
8 | install:
9 | - ps: >-
10 | $env:CommitHash = "$env:APPVEYOR_REPO_COMMIT"
11 |
12 | cache: '%USERPROFILE%\.nuget\packages'
13 | build_script:
14 | - ps: >
15 | dotnet --version
16 |
17 | $PackArguments = '-o', '..\buildoutput', '--no-build'
18 |
19 | If ($env:APPVEYOR_REPO_BRANCH -ne "master")
20 | {
21 | $PackArguments = $PackArguments + '--version-suffix', '"-$env:APPVEYOR_BUILD_NUMBER"'
22 | }
23 |
24 | dotnet build -v q
25 |
26 | dotnet pack .\src\TokenBinding\TokenBinding.csproj $PackArguments
27 |
28 | dotnet pack .\src\MicrosoftGraphBinding\MicrosoftGraphBinding.csproj $PackArguments
29 |
30 | test_script:
31 | - ps: >-
32 | dotnet test .\test\TokenExtension.Tests\ -v q --no-build
33 |
34 | dotnet test .\test\MicrosoftGraph.Tests\ -v q --no-build
35 |
36 | artifacts:
37 | - path: '**\*.nupkg'
38 | name: Binaries
39 | hosts:
40 | api.nuget.org: 93.184.221.200
--------------------------------------------------------------------------------
/samples/GraphExtensionSamples/ExcelExamples.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 | namespace GraphExtensionSamples
4 | {
5 | using Microsoft.Azure.WebJobs;
6 | using Microsoft.AspNetCore.Http;
7 | using System.Collections.Generic;
8 | using Microsoft.Graph;
9 |
10 | public static class ExcelExamples
11 | {
12 | //Appending row(s) to an excel worksheet or table
13 |
14 | public static void AppendRowsToExcelSpreadsheetWithJaggedArray([Excel(
15 | Path = "TestSheet.xlsx",
16 | WorksheetName = "Sheet1",
17 | UserId = "%UserId%",
18 | Identity = TokenIdentityMode.UserFromId)] out object[][] output)
19 | {
20 | output = new object[2][];
21 | output[0] = new object[]
22 | {
23 | "samplepartname", 42, 3.75,
24 | };
25 | output[1] = new object[]
26 | {
27 | "part2", 73, 43.20,
28 | };
29 | }
30 |
31 | [NoAutomaticTrigger]
32 | public static void AppendRowToExcelTableWithSinglePoco([Excel(
33 | Path = "TestSheet.xlsx",
34 | WorksheetName = "Sheet1",
35 | TableName = "Parts",
36 | UserId = "%UserId%",
37 | Identity = TokenIdentityMode.UserFromId)] out PartsTableRow output)
38 | {
39 | output = new PartsTableRow
40 | {
41 | Part = "samplepartname",
42 | Id = 42,
43 | Price = 3.75,
44 | };
45 | }
46 |
47 | //Updating an excel table
48 |
49 | [NoAutomaticTrigger]
50 | public static void UpdateEntireExcelTableWithPocos(
51 | [Excel(
52 | Path = "TestSheet.xlsx",
53 | WorksheetName = "Sheet1",
54 | TableName = "Parts",
55 | UserId = "%UserId%",
56 | UpdateType = "Update",
57 | Identity = TokenIdentityMode.UserFromId)] out PartsTableRow[] output)
58 | {
59 | output = new PartsTableRow[2];
60 | output[0] = (new PartsTableRow
61 | {
62 | Part = "part1",
63 | Id = 35,
64 | Price = 0.75
65 | });
66 | output[1] = (new PartsTableRow
67 | {
68 | Part = "part2",
69 | Id = 73,
70 | Price = 42.37,
71 | });
72 | }
73 |
74 | //Retrieving values from an excel table or worksheet
75 | public static void GetEntireExcelWorksheetAsJaggedStringArray([Excel(
76 | Path = "TestSheet.xlsx",
77 | WorksheetName = "Sheet1",
78 | UserId = "%UserId%",
79 | Identity = TokenIdentityMode.UserFromId)] string[][] rows)
80 | {
81 | //Perform any operations on the string[][], where each string[] is
82 | //a row in the worksheet.
83 | }
84 |
85 | public static void GetExcelTableAsWorkbookTable([Excel(
86 | Path = "TestSheet.xlsx",
87 | WorksheetName = "Sheet1",
88 | TableName = "sampletable",
89 | UserId = "%UserId%",
90 | Identity = TokenIdentityMode.UserFromId)] WorkbookTable table)
91 | {
92 | //See https://github.com/microsoftgraph/msgraph-sdk-dotnet/blob/81c50e72166152f9f84dc38b2516379b7a536300/src/Microsoft.Graph/Models/Generated/WorkbookTable.cs
93 | //for usage
94 | }
95 |
96 | public static void GetExcelTableAsPocoList([Excel(
97 | Path = "TestSheet.xlsx",
98 | WorksheetName = "Sheet1",
99 | TableName = "sampletable",
100 | UserId = "%UserId%",
101 | Identity = TokenIdentityMode.UserFromId)] PartsTableRow table)
102 | {
103 | //Note that each POCO object represents one row, and the values correspond to
104 | //the column titles that match the POCO's property names.
105 | }
106 |
107 | public class PartsTableRow
108 | {
109 | public string Part { get; set; }
110 |
111 | public int Id { get; set; }
112 |
113 | public double Price { get; set; }
114 | }
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/samples/GraphExtensionSamples/GraphExtensionSamples.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp2.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/samples/GraphExtensionSamples/OneDriveExamples.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | namespace GraphExtensionSamples
5 | {
6 | using System;
7 | using System.IO;
8 | using Microsoft.Azure.WebJobs;
9 | using Microsoft.Graph;
10 |
11 | public static class OneDriveExamples
12 | {
13 | //NOTE: In the current release, if these bindings are being used in Azure Functions, you
14 | //should explicitly set a property Access to Read or Write, depending on which operation
15 | //you are trying to perform. In the future, this will be autopopulated based on whether it
16 | //is an input or an output binding.
17 |
18 | public static void ReadOneDriveFileAsByteArray([OneDrive(
19 | Identity = TokenIdentityMode.UserFromId,
20 | UserId = "sampleuserid",
21 | Path = "samplepath.txt")] byte[] array)
22 | {
23 | Console.Write(System.Text.Encoding.UTF8.GetString(array, 0, array.Length));
24 | }
25 |
26 |
27 | //NOTE: These strings read the file assuming UTF-8 encoding.
28 | public static void ReadOneDriveFileAsString([OneDrive(
29 | Identity = TokenIdentityMode.UserFromId,
30 | UserId = "sampleuserid",
31 | Path = "samplepath.txt")] string fileText)
32 | {
33 | Console.Write(fileText);
34 | }
35 |
36 | //The binding also supports paths in the form of share links
37 | public static void ReadOneDriveFileAsByteArrayFromShareLink([OneDrive(
38 | Identity = TokenIdentityMode.UserFromId,
39 | UserId = "sampleuserid",
40 | Path = "https://microsoft-my.sharepoint.com/:t:/p/comcmaho/randomstringhere")] byte[] array)
41 | {
42 | Console.Write(System.Text.Encoding.UTF8.GetString(array, 0, array.Length));
43 | }
44 |
45 | public static void GetDriveItem([OneDrive(
46 | Identity = TokenIdentityMode.UserFromId,
47 | UserId = "sampleuserid",
48 | Path = "samplepath.txt")] DriveItem array)
49 | {
50 | //See https://github.com/microsoftgraph/msgraph-sdk-dotnet/blob/81c50e72166152f9f84dc38b2516379b7a536300/src/Microsoft.Graph/Models/Generated/DriveItem.cs
51 | //for usage
52 | }
53 |
54 | public static void GetOneDriveStream([OneDrive(
55 | Identity = TokenIdentityMode.UserFromId,
56 | UserId = "sampleuserid",
57 | Path = "samplepath.txt")] Stream stream)
58 | {
59 | byte[] buffer = new byte[256];
60 | stream.Read(buffer, 0, 256);
61 | }
62 |
63 | public static void GetOneDriveStreamWithWriteAccess([OneDrive(FileAccess.Write,
64 | Identity = TokenIdentityMode.UserFromId,
65 | UserId = "sampleuserid",
66 | Path = "samplepath.txt")] Stream stream)
67 | {
68 | string sampleText = "sampleText";
69 | byte[] encodedText = System.Text.Encoding.UTF8.GetBytes(sampleText);
70 | stream.Write(encodedText, 0, encodedText.Length);
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/samples/GraphExtensionSamples/OutlookExamples.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 | namespace GraphExtensionSamples
4 | {
5 | using System.Collections.Generic;
6 | using Microsoft.Azure.WebJobs;
7 | using Microsoft.Graph;
8 | using Newtonsoft.Json.Linq;
9 |
10 | public static class OutlookExamples
11 | {
12 | //Sending messages
13 |
14 | public static void SendMailFromMessageObject([Outlook(
15 | Identity = TokenIdentityMode.UserFromId,
16 | UserId = "sampleuserid")] out Message message)
17 | {
18 | message = new Message();
19 | //See https://github.com/microsoftgraph/msgraph-sdk-dotnet/blob/dev/src/Microsoft.Graph/Models/Generated/Message.cs
20 | //for usage
21 | }
22 |
23 | public static void SendMailFromJObject([Outlook(
24 | Identity = TokenIdentityMode.UserFromId,
25 | UserId = "sampleuserid")] out JObject message)
26 | {
27 | message = new JObject();
28 | //See https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/resources/message for a json
29 | //representation. Note that a "recipient" or "recipients" field takes the place of the ToRecipient field in
30 | //the schema, with "recipient" being a single object and "recipients" being multiple
31 | }
32 |
33 | public static void SendMailFromPoco([Outlook(
34 | Identity = TokenIdentityMode.UserFromId,
35 | UserId = "sampleuserid")] out MessagePoco message)
36 | {
37 | message = new MessagePoco();
38 | //See https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/resources/message for a json
39 | //representation. Note that a "recipient" or "recipients" field takes the place of the ToRecipient field in
40 | //the schema, with "recipient" being a single object and "recipients" being multiple
41 | }
42 |
43 | public class MessagePoco
44 | {
45 | public string Subject { get; set; }
46 | public string Body { get; set; }
47 | public List Recipients { get; set; }
48 | }
49 |
50 | public class RecipientPoco
51 | {
52 | public string Address { get; set; }
53 | public string Name { get; set; }
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/samples/GraphExtensionSamples/TokenExamples.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 | namespace GraphExtensionSamples
4 | {
5 | using System;
6 | using Microsoft.Azure.WebJobs;
7 |
8 | public static class TokenExamples
9 | {
10 | [NoAutomaticTrigger]
11 | public static void GraphTokenFromId([Token(
12 | UserId = "%UserId%",
13 | IdentityProvider = "AAD",
14 | Identity = TokenIdentityMode.UserFromId,
15 | Resource = "https://graph.microsoft.com")] string token)
16 | {
17 | Console.Write("The Microsoft graph token for the user is: " + token);
18 | }
19 |
20 | [NoAutomaticTrigger]
21 | public static void GraphTokenFromUserToken([Token(
22 | Identity = TokenIdentityMode.UserFromToken,
23 | UserToken = "%UserToken%",
24 | IdentityProvider = "AAD",
25 | Resource = "https://graph.microsoft.com")] string token)
26 | {
27 | Console.Write("The microsoft graph token for the user is: " + token);
28 | }
29 |
30 | // NOTE: This would only work in a Function with an HTTP trigger with
31 | // requests having the header X-MS-TOKEN-AAD-ID-TOKEN
32 | [NoAutomaticTrigger]
33 | public static void GraphTokenFromHttpRequest([Token(
34 | Identity = TokenIdentityMode.UserFromRequest,
35 | IdentityProvider = "AAD",
36 | Resource = "https://graph.microsoft.com")] string token)
37 | {
38 | Console.Write("The microsoft graph token for the user is: " + token);
39 | }
40 |
41 | // This template uses application permissions and requires consent from an Azure Active Directory admin.
42 | // See https://go.microsoft.com/fwlink/?linkid=858780
43 | [NoAutomaticTrigger]
44 | public static void GraphTokenFromApplicationIdentity([Token(
45 | Identity = TokenIdentityMode.ClientCredentials,
46 | IdentityProvider = "AAD",
47 | Resource = "https://graph.microsoft.com")] string token)
48 | {
49 | Console.Write("The microsoft graph token for the application is: " + token);
50 | }
51 |
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/samples/GraphExtensionSamples/WebhookSubscriptionExamples.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 | namespace GraphExtensionSamples
4 | {
5 | using System;
6 | using Microsoft.Azure.WebJobs;
7 |
8 | public static class WebhookSubscriptionExamples
9 | {
10 | //Note that this function will only pass if the ClientCredentials identity has permission to refresh all of the available webhooks
11 | public static void RefreshAllSubscriptions(
12 | [GraphWebhookSubscription] string[] subIds,
13 | [GraphWebhookSubscription(
14 | Action = GraphWebhookSubscriptionAction.Refresh,
15 | Identity = TokenIdentityMode.ClientCredentials)] ICollector refreshedSubscriptions)
16 | {
17 | foreach (var subId in subIds)
18 | {
19 | refreshedSubscriptions.Add(subId);
20 | }
21 | }
22 |
23 | //This more complicated function without setting up an application identity with lots of permissions
24 | public static async void RefreshAllSubscriptionsWithoutClientCredentials(
25 | [GraphWebhookSubscription] SubscriptionPoco[] existingSubscriptions,
26 | IBinder binder)
27 | {
28 | foreach (var subscription in existingSubscriptions)
29 | {
30 | // binding in code to allow dynamic identity
31 | var subscriptionsToRefresh = await binder.BindAsync>(
32 | new GraphWebhookSubscriptionAttribute()
33 | {
34 | Action = GraphWebhookSubscriptionAction.Refresh,
35 | Identity = TokenIdentityMode.UserFromId,
36 | UserId = subscription.UserId
37 | }
38 | );
39 | {
40 | await subscriptionsToRefresh.AddAsync(subscription.Id);
41 | }
42 | }
43 | }
44 |
45 | public static void RefreshSubscriptionsSelectively(
46 | [GraphWebhookSubscription] SubscriptionPoco[] existingSubscriptions,
47 | [GraphWebhookSubscription(
48 | Action = GraphWebhookSubscriptionAction.Refresh,
49 | Identity = TokenIdentityMode.ClientCredentials)] ICollector refreshedSubscriptions)
50 | {
51 | foreach (var subscription in existingSubscriptions)
52 | {
53 | if(subscription.ChangeType.Equals("updated") && subscription.ODataType.Equals("#Microsoft.Graph.Message"))
54 | {
55 | refreshedSubscriptions.Add(subscription.Id);
56 | }
57 | }
58 | }
59 |
60 | public static void DeleteAllSubscriptions(
61 | [GraphWebhookSubscription(Identity = TokenIdentityMode.ClientCredentials)] string[] subIds,
62 | [GraphWebhookSubscription(
63 | Action = GraphWebhookSubscriptionAction.Delete,
64 | Identity = TokenIdentityMode.ClientCredentials)] ICollector deletedSubscriptions)
65 | {
66 | foreach (var subId in subIds)
67 | {
68 | deletedSubscriptions.Add(subId);
69 | }
70 | }
71 |
72 | public static void SubscribeToInbox([GraphWebhookSubscription(
73 | UserId = "%UserId%",
74 | Identity = TokenIdentityMode.UserFromId,
75 | SubscriptionResource = "me/mailFolders('Inbox')/messages",
76 | ChangeTypes = new GraphWebhookChangeType[] {GraphWebhookChangeType.Created, GraphWebhookChangeType.Updated },
77 | Action = GraphWebhookSubscriptionAction.Create)] out string clientState)
78 | {
79 | clientState = Guid.NewGuid().ToString();
80 | }
81 |
82 | public class SubscriptionPoco
83 | {
84 | public string UserId { get; set; }
85 | //All of the below are properties of the Subscription object that can be used in your custom POCO
86 | //See https://github.com/microsoftgraph/msgraph-sdk-dotnet/blob/81c50e72166152f9f84dc38b2516379b7a536300/src/Microsoft.Graph/Models/Generated/Subscription.cs
87 | //for usage
88 | public string Id { get; set; }
89 | public string ODataType { get; set; }
90 | public string Resource { get; set; }
91 | public string ChangeType { get; set; }
92 | public string ClientState { get; set; }
93 | public string NotificationUrl { get; set; }
94 | public DateTimeOffset? ExpirationDateTime { get; set; }
95 | }
96 |
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/samples/GraphExtensionSamples/WebhookTriggerExamples.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | namespace GraphExtensionSamples
5 | {
6 | using Microsoft.Azure.WebJobs;
7 | using Microsoft.Graph;
8 |
9 | public static class WebhookTriggerExamples
10 | {
11 | //NOTE: There can only be one trigger per resource type. That means all graph webhook subscriptions for the
12 | //given resource type will go through the same function.
13 |
14 | public static void OnMessage([GraphWebhookTrigger(ResourceType = "#Microsoft.Graph.Message")] Message message)
15 | {
16 | //See https://github.com/microsoftgraph/msgraph-sdk-dotnet/blob/dev/src/Microsoft.Graph/Models/Generated/Message.cs
17 | //for usage
18 | }
19 |
20 | public static void OnOneDriveChange([GraphWebhookTrigger(ResourceType = "#Microsoft.Graph.DriveItem")] DriveItem driveItem)
21 | {
22 | //See https://github.com/microsoftgraph/msgraph-sdk-dotnet/blob/dev/src/Microsoft.Graph/Models/Generated/DriveItem.cs
23 | //for usage
24 | }
25 |
26 | public static void OnContactChange([GraphWebhookTrigger(ResourceType = "#Microsoft.Graph.Contact")] Contact contact)
27 | {
28 | //See https://github.com/microsoftgraph/msgraph-sdk-dotnet/blob/dev/src/Microsoft.Graph/Models/Generated/Contact.cs
29 | //for usage
30 | }
31 |
32 | public static void OnEventChange([GraphWebhookTrigger(ResourceType = "#Microsoft.Graph.Event")] Event eventNotice)
33 | {
34 | //See https://github.com/microsoftgraph/msgraph-sdk-dotnet/blob/dev/src/Microsoft.Graph/Models/Generated/Event.cs
35 | //for usage
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/sign.snk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure/azure-functions-microsoftgraph-extension/0edb2e497e3f003943223a35f5c6f74dab78708e/sign.snk
--------------------------------------------------------------------------------
/src/MicrosoftGraphBinding/Bindings/ExcelAsyncCollector.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph
5 | {
6 | using System;
7 | using System.Collections.Generic;
8 | using System.Collections.ObjectModel;
9 | using System.Linq;
10 | using System.Threading;
11 | using System.Threading.Tasks;
12 | using Microsoft.Azure.WebJobs;
13 | using Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Services;
14 | using Newtonsoft.Json.Linq;
15 |
16 | ///
17 | /// Collector class used to accumulate and then dispatch requests to MS Graph related to Excel
18 | ///
19 | internal class ExcelAsyncCollector : IAsyncCollector
20 | {
21 | private readonly ExcelService _service;
22 | private readonly ExcelAttribute _attribute;
23 | private readonly List _rows;
24 |
25 | ///
26 | /// Initializes a new instance of the class.
27 | ///
28 | /// GraphServiceClient used to make calls to MS Graph
29 | /// ExcelAttribute containing necessary info about workbook, etc.
30 | public ExcelAsyncCollector(ExcelService manager, ExcelAttribute attribute)
31 | {
32 | _service = manager;
33 | _attribute = attribute;
34 | _rows = new List();
35 | }
36 |
37 | ///
38 | /// Add a string representation of a JObject to the list of items that need to be processed
39 | ///
40 | /// JSON string to be added (contains table, worksheet, etc. data)
41 | /// Used to propagate notifications
42 | /// Task representing the addition of the item
43 | public Task AddAsync(string item, CancellationToken cancellationToken = default(CancellationToken))
44 | {
45 | if (item == null)
46 | {
47 | throw new ArgumentNullException(nameof(item));
48 | }
49 | JToken parsedToken = JToken.Parse(item);
50 | if (parsedToken is JObject)
51 | {
52 | _rows.Add(parsedToken as JObject);
53 | }
54 | else
55 | {
56 | //JavaScript Array, so add metadata manually
57 | var array = parsedToken as JArray;
58 | bool arrayIsNested = array.All(element => element is JArray);
59 | if (arrayIsNested)
60 | {
61 | var consolidatedRow = new JObject();
62 | consolidatedRow[O365Constants.ValuesKey] = array;
63 | consolidatedRow[O365Constants.RowsKey] = array.Count;
64 | // No exception -- array is rectangular by default
65 | consolidatedRow[O365Constants.ColsKey] = array[0].Children().Count();
66 | _rows.Add(consolidatedRow);
67 | }
68 | else
69 | {
70 | throw new InvalidOperationException("Only nested arrays are supported for Excel output bindings.");
71 | }
72 |
73 | }
74 |
75 | return Task.CompletedTask;
76 | }
77 |
78 | ///
79 | /// Send all of the items in the collector to Microsoft Graph API
80 | ///
81 | /// Used to propagate notifications
82 | /// Task representing the flushing of the collector
83 | public async Task FlushAsync(CancellationToken cancellationToken = default(CancellationToken))
84 | {
85 | if (_rows.Count == 0)
86 | {
87 | return;
88 | }
89 | // Distinguish between appending and updating
90 | if (this._attribute.UpdateType != null && this._attribute.UpdateType == "Update")
91 | {
92 | if (_rows.FirstOrDefault(row => row["column"] != null && row["value"] != null) != null)
93 | {
94 | foreach (var row in this._rows)
95 | {
96 | row[O365Constants.RowsKey] = 1;
97 | row[O365Constants.ColsKey] = row.Children().Count();
98 | if (row["column"] != null && row["value"] != null)
99 | {
100 | await _service.UpdateColumnAsync(this._attribute, row, cancellationToken);
101 | }
102 | else
103 | {
104 | // Update whole worksheet
105 | await _service.UpdateWorksheetAsync(this._attribute, row, cancellationToken);
106 | }
107 | }
108 | }
109 | else if (_rows.Count > 0)
110 | {
111 | // Update whole worksheet at once
112 | JObject consolidatedRows = GetConsolidatedRows(_rows);
113 | await _service.UpdateWorksheetAsync(_attribute, consolidatedRows, cancellationToken);
114 | }
115 | }
116 | else
117 | {
118 | // DEFAULT: Append (rows to specific table)
119 | foreach (var row in this._rows)
120 | {
121 | await _service.AddRowAsync(this._attribute, row, cancellationToken);
122 | }
123 | }
124 |
125 | this._rows.Clear();
126 | }
127 |
128 | public JObject GetConsolidatedRows(List rows)
129 | {
130 | JObject consolidatedRows = new JObject();
131 | if (rows.Count > 0 && rows[0][O365Constants.ValuesKey] == null)
132 | {
133 | //Each row is a POCO, so make it one value, and consolidate into one row
134 | consolidatedRows[O365Constants.ValuesKey] = JArray.FromObject(rows);
135 | consolidatedRows[O365Constants.RowsKey] = rows.Count;
136 | // No exception -- array is rectangular by default
137 | consolidatedRows[O365Constants.ColsKey] = rows[0].Children().Count();
138 | // Set POCO key to indicate that the values need to be ordered to match the header of the existing table
139 | consolidatedRows[O365Constants.POCOKey] = rows[0][O365Constants.POCOKey];
140 | }
141 | else if (rows.Count == 1 && rows[0][O365Constants.ValuesKey] != null)
142 | {
143 | return rows[0];
144 | }
145 | else
146 | {
147 | var rowsAsString = $"[ {string.Join(", ", rows.Select(jobj => jobj.ToString()))} ]";
148 | throw new InvalidOperationException($"Could not consolidate the following rows: {rowsAsString}");
149 | }
150 | return consolidatedRows;
151 | }
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/src/MicrosoftGraphBinding/Bindings/ExcelAttribute.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | namespace Microsoft.Azure.WebJobs
5 | {
6 | using Microsoft.Azure.WebJobs.Description;
7 |
8 | ///
9 | /// Attribute used to describe Excel files
10 | /// Files must be on OneDrive
11 | ///
12 | [Binding]
13 | public class ExcelAttribute : GraphTokenBaseAttribute
14 | {
15 | ///
16 | /// Gets or sets path FROM ONEDRIVE ROOT to Excel file
17 | /// e.g. "Documents/TestSheet.xlsx"
18 | ///
19 | [AutoResolve]
20 | public string Path { get; set; }
21 |
22 | ///
23 | /// Gets or sets name of Excel table
24 | ///
25 | [AutoResolve]
26 | public string TableName { get; set; }
27 |
28 | ///
29 | /// Gets or sets worksheet name
30 | ///
31 | [AutoResolve]
32 | public string WorksheetName { get; set; }
33 |
34 | ///
35 | /// Gets or sets used when reading/writing from/to file
36 | /// Append (row) or Update (whole worksheet, specific column)
37 | ///
38 | [AutoResolve]
39 | public string UpdateType { get; set; }
40 | }
41 | }
--------------------------------------------------------------------------------
/src/MicrosoftGraphBinding/Bindings/GraphTokenBaseAttribute.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | namespace Microsoft.Azure.WebJobs
5 | {
6 | using Microsoft.Azure.WebJobs.Description;
7 | using Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph;
8 |
9 | public abstract class GraphTokenBaseAttribute : TokenBaseAttribute
10 | {
11 | ///
12 | /// Initializes a new instance of the class.
13 | ///
14 | public GraphTokenBaseAttribute()
15 | {
16 | this.Resource = O365Constants.GraphBaseUrl;
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/MicrosoftGraphBinding/Bindings/GraphWebhookChangeType.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | namespace Microsoft.Azure.WebJobs
5 | {
6 | ///
7 | /// Enum of changes that can be subscribed to
8 | ///
9 | public enum GraphWebhookChangeType
10 | {
11 | ///
12 | /// Webhook activated when a new item of the subscribed resource is CREATED
13 | ///
14 | Created,
15 |
16 | ///
17 | /// Webhook activated when a new item of the subscribed resource is UPDATED
18 | ///
19 | Updated,
20 |
21 | ///
22 | /// Webhook activated when a new item of the subscribed resource is DELETED
23 | ///
24 | Deleted,
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/MicrosoftGraphBinding/Bindings/GraphWebhookSubscriptionAction.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | namespace Microsoft.Azure.WebJobs
5 | {
6 | ///
7 | /// Determines what action the output binding for GraphWebhookActionAttribute
8 | /// will perform. Note that different modes have different semantics for the strings
9 | /// received by the IAsyncCollector.
10 | ///
11 | public enum GraphWebhookSubscriptionAction
12 | {
13 | ///
14 | /// Creates a new webhook (string = clientState)
15 | ///
16 | Create,
17 | ///
18 | /// Deletes a webhook (string = subscriptionId)
19 | ///
20 | Delete,
21 | ///
22 | /// Refreshes a webhook (string = subscriptionId)
23 | ///
24 | Refresh,
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/MicrosoftGraphBinding/Bindings/GraphWebhookSubscriptionAsyncCollector.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph
5 | {
6 | using System;
7 | using System.Collections.Generic;
8 | using System.Linq;
9 | using System.Threading;
10 | using System.Threading.Tasks;
11 | using Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Config;
12 | using Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Services;
13 | using Microsoft.Extensions.Logging;
14 | using Microsoft.Graph;
15 |
16 | ///
17 | /// Collector class used to accumulate client states and create new subscriptions and cache them
18 | ///
19 | internal class GraphWebhookSubscriptionAsyncCollector : IAsyncCollector
20 | {
21 | private readonly GraphServiceClientManager _clientManager; // already has token
22 | private readonly ILogger _log;
23 | private readonly IGraphSubscriptionStore _subscriptionStore;
24 | private readonly Uri _notificationUrl;
25 | private readonly GraphOptions _options;
26 |
27 |
28 | // User attribute that we're bound against.
29 | // Has key properties (e.g. what resource we're listening to)
30 | private readonly GraphWebhookSubscriptionAttribute _attribute;
31 |
32 | private List _values;
33 |
34 | public GraphWebhookSubscriptionAsyncCollector(GraphServiceClientManager clientManager, GraphOptions options, ILoggerFactory logFactory, IGraphSubscriptionStore subscriptionStore, Uri notificationUrl, GraphWebhookSubscriptionAttribute attribute)
35 | {
36 | _clientManager = clientManager;
37 | _log = logFactory?.CreateLogger(MicrosoftGraphExtensionConfigProvider.CreateBindingCategory("GraphWebhook"));
38 | _subscriptionStore = subscriptionStore;
39 | _notificationUrl = notificationUrl;
40 | _attribute = attribute;
41 | _options = options;
42 | _values = new List();
43 |
44 | _attribute.Validate();
45 | }
46 |
47 | public Task AddAsync(string value, CancellationToken cancellationToken)
48 | {
49 | _values.Add(value);
50 | return Task.CompletedTask;
51 | }
52 |
53 | public async Task FlushAsync(CancellationToken cancellationToken)
54 | {
55 | switch (_attribute.Action)
56 | {
57 | case GraphWebhookSubscriptionAction.Create:
58 | await CreateSubscriptionsFromClientStatesAsync(cancellationToken);
59 | break;
60 | case GraphWebhookSubscriptionAction.Delete:
61 | await DeleteSubscriptionsFromSubscriptionIds(cancellationToken);
62 | break;
63 | case GraphWebhookSubscriptionAction.Refresh:
64 | await RefreshSubscriptionsFromSubscriptionIds(cancellationToken);
65 | break;
66 | }
67 |
68 | _values.Clear();
69 | }
70 |
71 | private async Task CreateSubscriptionsFromClientStatesAsync(CancellationToken cancellationToken)
72 | {
73 | var client = await _clientManager.GetMSGraphClientFromTokenAttributeAsync(_attribute, cancellationToken);
74 | var userInfo = await client.Me.Request().Select("Id").GetAsync();
75 |
76 | var subscriptions = _values.Select(GetSubscription);
77 | foreach (var subscription in subscriptions)
78 | {
79 | _log.LogTrace($"Sending a request to {_notificationUrl} expecting a 200 response for a subscription to {subscription.Resource}");
80 | var newSubscription = await client.Subscriptions.Request().AddAsync(subscription);
81 | await _subscriptionStore.SaveSubscriptionEntryAsync(newSubscription, userInfo.Id);
82 | }
83 | }
84 |
85 | private Subscription GetSubscription(string clientState)
86 | {
87 | clientState = clientState ?? Guid.NewGuid().ToString();
88 | return new Subscription
89 | {
90 | Resource = _attribute.SubscriptionResource,
91 | ChangeType = ChangeTypeExtension.ConvertArrayToString(_attribute.ChangeTypes),
92 | NotificationUrl = _notificationUrl.AbsoluteUri,
93 | ExpirationDateTime = DateTime.UtcNow + _options.WebhookExpirationTimeSpan,
94 | ClientState = clientState,
95 | };
96 | }
97 |
98 | private async Task DeleteSubscriptionsFromSubscriptionIds(CancellationToken token)
99 | {
100 | var client = await _clientManager.GetMSGraphClientFromTokenAttributeAsync(_attribute, token);
101 | var subscriptionIds = _values;
102 | await Task.WhenAll(subscriptionIds.Select(id => DeleteSubscription(client, id)));
103 | }
104 |
105 | private async Task DeleteSubscription(IGraphServiceClient client, string id)
106 | {
107 | try
108 | {
109 | await client.Subscriptions[id].Request().DeleteAsync();
110 | _log.LogInformation($"Successfully deleted MS Graph subscription {id}.");
111 | }
112 | catch
113 | {
114 | _log.LogWarning($"Failed to delete MS Graph subscription {id}.\n Either it never existed or it has already expired.");
115 | }
116 | finally
117 | {
118 | // Regardless of whether or not deleting the Graph subscription succeeded, delete the file
119 | await _subscriptionStore.DeleteAsync(id);
120 | }
121 | }
122 |
123 | private async Task RefreshSubscriptionsFromSubscriptionIds(CancellationToken token)
124 | {
125 | var client = await _clientManager.GetMSGraphClientFromTokenAttributeAsync(_attribute, token);
126 | var subscriptionIds = _values;
127 | await Task.WhenAll(subscriptionIds.Select(id => RefreshSubscription(client, id)));
128 | }
129 |
130 | private async Task RefreshSubscription(IGraphServiceClient client, string id)
131 | {
132 | try
133 | {
134 | var subscription = new Subscription
135 | {
136 | ExpirationDateTime = DateTime.UtcNow + _options.WebhookExpirationTimeSpan,
137 | };
138 |
139 | var result = await client.Subscriptions[id].Request().UpdateAsync(subscription);
140 |
141 | _log.LogInformation($"Successfully renewed MS Graph subscription {id}. \n Active until {subscription.ExpirationDateTime}");
142 | }
143 | catch (Exception ex)
144 | {
145 | // If the subscription is expired, it can no longer be renewed, so delete the file
146 | var subscriptionEntry = await _subscriptionStore.GetSubscriptionEntryAsync(id);
147 | if (subscriptionEntry != null)
148 | {
149 | if(subscriptionEntry.Subscription.ExpirationDateTime < DateTime.UtcNow)
150 | {
151 | await _subscriptionStore.DeleteAsync(id);
152 | } else
153 | {
154 | _log.LogError(ex, "A non-expired subscription failed to renew");
155 | }
156 | } else
157 | {
158 | _log.LogWarning("The subscription with id " + id + " was not present in the local subscription store.");
159 | }
160 | }
161 | }
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/src/MicrosoftGraphBinding/Bindings/GraphWebhookSubscriptionAttribute.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | namespace Microsoft.Azure.WebJobs
5 | {
6 | using System;
7 | using Microsoft.Azure.WebJobs.Description;
8 |
9 | // This is used to retrieve the locally stored subscriptions
10 | [Binding]
11 | public class GraphWebhookSubscriptionAttribute : GraphTokenBaseAttribute
12 | {
13 | private string _filter;
14 |
15 | ///
16 | /// Gets or sets the UserId to filter subscriptions on; Optional
17 | ///
18 | public string Filter
19 | {
20 | get
21 | {
22 | return _filter;
23 | }
24 | set
25 | {
26 | if (TokenIdentityMode.UserFromRequest.ToString().Equals(value, StringComparison.OrdinalIgnoreCase))
27 | {
28 | Identity = TokenIdentityMode.UserFromRequest;
29 | }
30 | _filter = value;
31 | }
32 | }
33 |
34 | ///
35 | /// Resource for which we're subscribing to changes.
36 | /// ie: me/mailFolders('Inbox')/messages
37 | ///
38 | [AutoResolve]
39 | public string SubscriptionResource { get; set; }
40 |
41 | ///
42 | /// Gets or sets types of changes that we're subscribing to.
43 | /// This is specific to the resource
44 | ///
45 | public GraphWebhookChangeType[] ChangeTypes { get; set; }
46 |
47 | public GraphWebhookSubscriptionAction Action { get; set; }
48 |
49 | internal void Validate()
50 | {
51 | switch (Action)
52 | {
53 | case GraphWebhookSubscriptionAction.Create:
54 | if (string.IsNullOrEmpty(SubscriptionResource))
55 | {
56 | throw new ArgumentException($"A value for listen must be provided in ${Action} mode.");
57 | }
58 |
59 | if (ChangeTypes == null || ChangeTypes.Length == 0)
60 | {
61 | ChangeTypes = new GraphWebhookChangeType[] { GraphWebhookChangeType.Created, GraphWebhookChangeType.Deleted, GraphWebhookChangeType.Updated };
62 | }
63 |
64 | break;
65 | case GraphWebhookSubscriptionAction.Delete:
66 | case GraphWebhookSubscriptionAction.Refresh:
67 | if (!string.IsNullOrEmpty(SubscriptionResource))
68 | {
69 | throw new ArgumentException($"No value should be provided for listen in {Action} mode.");
70 | }
71 |
72 | if (ChangeTypes != null && ChangeTypes.Length > 0)
73 | {
74 | throw new ArgumentException($"No values should be provided for changeTypes in ${Action} mode.");
75 | }
76 |
77 | break;
78 | }
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/MicrosoftGraphBinding/Bindings/GraphWebhookSubscriptionHandler.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph
5 | {
6 | using System;
7 | using System.Collections.Generic;
8 | using System.Linq;
9 | using System.Net;
10 | using System.Net.Http;
11 | using System.Text;
12 | using System.Threading;
13 | using System.Threading.Tasks;
14 | using System.Web;
15 | using Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Config;
16 | using Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Services;
17 | using Microsoft.Extensions.Logging;
18 | using Newtonsoft.Json;
19 | using Newtonsoft.Json.Linq;
20 |
21 | // Handles the subscription validation and notification payloads
22 | internal class GraphWebhookSubscriptionHandler
23 | {
24 | private readonly IGraphSubscriptionStore _subscriptionStore; // already has token
25 | private readonly Uri _notificationUri;
26 | private readonly ILogger _log;
27 | private readonly GraphServiceClientManager _clientManager;
28 | private readonly WebhookTriggerBindingProvider _bindingProvider;
29 |
30 | public GraphWebhookSubscriptionHandler(GraphServiceClientManager clientManager, IGraphSubscriptionStore subscriptionStore, ILoggerFactory loggerFactory, Uri notificationUri, WebhookTriggerBindingProvider bindingProvider)
31 | {
32 | _clientManager = clientManager;
33 | _subscriptionStore = subscriptionStore;
34 | _log = loggerFactory?.CreateLogger(MicrosoftGraphExtensionConfigProvider.CreateBindingCategory("GraphWebhookSubscription"));
35 | _notificationUri = notificationUri;
36 | _bindingProvider = bindingProvider;
37 | }
38 |
39 | private async Task HandleNotificationsAsync(NotificationPayload notifications, CancellationToken cancellationToken)
40 | {
41 | // A single webhook might get notifications from different users.
42 | List resources = new List();
43 |
44 | foreach (Notification notification in notifications.Value)
45 | {
46 | var subId = notification.SubscriptionId;
47 | var entry = await _subscriptionStore.GetSubscriptionEntryAsync(subId);
48 | if (entry == null)
49 | {
50 | _log.LogError($"No subscription exists in our store for subscription id: {subId}");
51 | // mapping of subscription ID to principal ID does not exist in file system
52 | continue;
53 | }
54 |
55 | if (entry.Subscription.ClientState != notification.ClientState)
56 | {
57 | _log.LogTrace($"The subscription store's client state: {entry.Subscription.ClientState} did not match the notifications's client state: {notification.ClientState}");
58 | // Stored client state does not match client state we just received
59 | continue;
60 | }
61 |
62 | // call onto Graph to fetch the resource
63 | var userId = entry.UserId;
64 | var graphClient = await _clientManager.GetMSGraphClientFromUserIdAsync(userId, cancellationToken);
65 |
66 | _log.LogTrace($"A graph client was obtained for subscription id: {subId}");
67 |
68 | // Prepend with / if necessary
69 | if (notification.Resource[0] != '/')
70 | {
71 | notification.Resource = '/' + notification.Resource;
72 | }
73 |
74 | var url = graphClient.BaseUrl + notification.Resource;
75 |
76 | HttpRequestMessage request = new HttpRequestMessage
77 | {
78 | Method = HttpMethod.Get,
79 | RequestUri = new Uri(url),
80 | };
81 |
82 | _log.LogTrace($"Making a GET request to {url} on behalf of subId: {subId}");
83 |
84 | await graphClient.AuthenticationProvider.AuthenticateRequestAsync(request); // Add authentication header
85 | var response = await graphClient.HttpProvider.SendAsync(request);
86 | string responseContent = await response.Content.ReadAsStringAsync();
87 |
88 | _log.LogTrace($"Recieved {responseContent} from request to {url}");
89 |
90 | var actualPayload = JObject.Parse(responseContent);
91 |
92 | // Superimpose some common properties onto the JObject for easy access.
93 | actualPayload["ClientState"] = entry.Subscription.ClientState;
94 |
95 | // Drive items only payload without resource data
96 | string odataType = null;
97 | if (notification.ResourceData != null)
98 | {
99 | odataType = notification.ResourceData.ODataType;
100 | }
101 | else if (notification.Resource.StartsWith("/me/drive"))
102 | {
103 | odataType = "#Microsoft.Graph.DriveItem";
104 | }
105 |
106 | actualPayload["@odata.type"] = odataType;
107 |
108 | var data = new WebhookTriggerData
109 | {
110 | client = graphClient,
111 | Payload = actualPayload,
112 | odataType = odataType,
113 | };
114 |
115 | resources.Add(data);
116 | }
117 |
118 | _log.LogTrace($"Triggering {resources.Count} GraphWebhookTriggers");
119 | Task[] webhookReceipts = resources.Select(item => _bindingProvider.PushDataAsync(item)).ToArray();
120 |
121 | Task.WaitAll(webhookReceipts);
122 | _log.LogTrace($"Finished responding to notifications.");
123 | }
124 |
125 | // See here for subscribing and payload information.
126 | // https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/api/subscription_post_subscriptions
127 | public async Task ProcessAsync(
128 | HttpRequestMessage request, CancellationToken cancellationToken)
129 | {
130 | var nvc = HttpUtility.ParseQueryString(request.RequestUri.Query);
131 |
132 | string validationToken = nvc["validationToken"];
133 | if (validationToken != null)
134 | {
135 |
136 | return HandleInitialValidation(validationToken);
137 | }
138 |
139 | return await HandleNotificationPayloadAsync(request, cancellationToken);
140 | }
141 |
142 | private HttpResponseMessage HandleInitialValidation(string validationToken)
143 | {
144 | _log.LogTrace($"Returning a 200 OK Response to a request to {_notificationUri} with a validation token of {validationToken}");
145 | return new HttpResponseMessage(HttpStatusCode.OK)
146 | {
147 | Content = new StringContent(validationToken, Encoding.UTF8, "plain/text"),
148 | };
149 | }
150 |
151 | private async Task HandleNotificationPayloadAsync(HttpRequestMessage request, CancellationToken cancellationToken)
152 | {
153 | string json = await request.Content.ReadAsStringAsync();
154 | var notifications = JsonConvert.DeserializeObject(json);
155 |
156 | _log.LogTrace($"Received a notification payload of {json}");
157 | // We have 30sec to reply to the payload.
158 | // So offload everything else (especially fetches back to the graph and executing the user function)
159 | Task.Run(() => HandleNotificationsAsync(notifications, cancellationToken));
160 |
161 | // Still return a 200 so the service doesn't resend the notification.
162 | return new HttpResponseMessage(HttpStatusCode.OK);
163 | }
164 |
165 |
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/src/MicrosoftGraphBinding/Bindings/GraphWebhookTriggerAttribute.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | namespace Microsoft.Azure.WebJobs
5 | {
6 | using System;
7 | using Microsoft.Azure.WebJobs.Description;
8 |
9 | [Binding]
10 | public class GraphWebhookTriggerAttribute : Attribute
11 | {
12 | ///
13 | /// Gets or sets oData type of payload
14 | /// "#Microsoft.Graph.Message", "#Microsoft.Graph.DriveItem", "#Microsoft.Graph.Contact", "#Microsoft.Graph.Event"
15 | ///
16 | public string ResourceType { get; set; }
17 | }
18 | }
--------------------------------------------------------------------------------
/src/MicrosoftGraphBinding/Bindings/O365Constants.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph
5 | {
6 | using System;
7 |
8 | ///
9 | /// Constants needed to communicate with Graph, find app settings, etc.
10 | ///
11 | internal static class O365Constants
12 | {
13 | ///
14 | /// Base URL used to make any rest API calls
15 | ///
16 | public const string GraphBaseUrl = "https://graph.microsoft.com";
17 |
18 | ///
19 | /// JObject key that holds range values (I/O)
20 | ///
21 | public const string ValuesKey = "Microsoft.O365Bindings.values";
22 |
23 | ///
24 | /// JObject key that holds number of rows in ValuesKey
25 | ///
26 | public const string RowsKey = "Microsoft.O365Bindings.rows";
27 |
28 | ///
29 | /// JObject key that holds number of columns in ValuesKey (rectangular data)
30 | ///
31 | public const string ColsKey = "Microsoft.O365Bindings.columns";
32 |
33 | ///
34 | /// JObject key to indicate whether or not data came from POCO objects (list, array, or otherwise)
35 | ///
36 | public const string POCOKey = "Microsoft.O365Bindings.POCO";
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/MicrosoftGraphBinding/Bindings/O365Models.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph
5 | {
6 | using System;
7 | using System.Collections.Generic;
8 | using Microsoft.Graph;
9 | using Newtonsoft.Json;
10 |
11 | ///
12 | /// GraphServiceClient + time the token used to authenticate calls expires
13 | ///
14 | internal class CachedClient {
15 | internal IGraphServiceClient client;
16 | internal int expirationDate;
17 | }
18 |
19 | ///
20 | /// Class containing an array of notifications received in a single blast from MS Graph
21 | ///
22 | internal class NotificationPayload
23 | {
24 | ///
25 | /// Gets or sets the array of notifications received by the function app
26 | ///
27 | public Notification[] Value { get; set; }
28 | }
29 |
30 | ///
31 | /// Single notification from MS Graph indicating a resource that the user subscribed to has been created/updated/deleted
32 | /// Several of these might come in at once
33 | ///
34 | internal class Notification
35 | {
36 | ///
37 | /// Gets or sets the type of change.
38 | ///
39 | [JsonProperty(PropertyName = "changeType")]
40 | public string ChangeType { get; set; }
41 |
42 | ///
43 | /// Gets or sets the client state used to verify that the notification is from Microsoft Graph.
44 | /// Compare the value received with the notification to the value you sent with the subscription request.
45 | ///
46 | [JsonProperty(PropertyName = "clientState")]
47 | public string ClientState { get; set; }
48 |
49 | ///
50 | /// Gets or sets the endpoint of the resource that changed.
51 | /// For example, a message uses the format ../Users/{user-id}/Messages/{message-id}
52 | ///
53 | [JsonProperty(PropertyName = "resource")]
54 | public string Resource { get; set; }
55 |
56 | ///
57 | /// Gets or sets the UTC date and time when the webhooks subscription expires.
58 | ///
59 | [JsonProperty(PropertyName = "subscriptionExpirationDateTime")]
60 | public DateTimeOffset SubscriptionExpirationDateTime { get; set; }
61 |
62 | ///
63 | /// Gets or sets the unique identifier for the webhooks subscription.
64 | ///
65 | [JsonProperty(PropertyName = "subscriptionId")]
66 | public string SubscriptionId { get; set; }
67 |
68 | ///
69 | /// Gets or sets the properties of the changed resource.
70 | ///
71 | [JsonProperty(PropertyName = "resourceData")]
72 | public ResourceData ResourceData { get; set; }
73 | }
74 |
75 | ///
76 | /// From within a given Notification
77 | /// Message, Contact, and Calendar all contain this ResourceData. OneDrive does not.
78 | ///
79 | internal class ResourceData
80 | {
81 | ///
82 | /// Gets or sets the ID of the resource.
83 | ///
84 | [JsonProperty(PropertyName = "id")]
85 | public string Id { get; set; }
86 |
87 | ///
88 | /// Gets or sets the OData etag property.
89 | ///
90 | [JsonProperty(PropertyName = "@odata.etag")]
91 | public string ODataEtag { get; set; }
92 |
93 | ///
94 | /// Gets or sets the OData ID of the resource. This is the same value as the resource property.
95 | ///
96 | [JsonProperty(PropertyName = "@odata.id")]
97 | public string ODataId { get; set; }
98 |
99 | ///
100 | /// Gets or sets the OData type of the resource
101 | /// "#Microsoft.Graph.Message", "#Microsoft.Graph.Event", or "#Microsoft.Graph.Contact".
102 | ///
103 | [JsonProperty(PropertyName = "@odata.type")]
104 | public string ODataType { get; set; }
105 | }
106 |
107 | ///
108 | /// Helper class for change types
109 | ///
110 | internal class ChangeTypeExtension
111 | {
112 | ///
113 | /// Convert an array of ChangeTypes to a Microsoft Graph-friendly list
114 | ///
115 | /// Array of change types
116 | /// lowercase array of strings representing the change types
117 | public static string ConvertArrayToString(GraphWebhookChangeType[] array)
118 | {
119 | List result = new List();
120 | foreach (GraphWebhookChangeType ct in array)
121 | {
122 | result.Add(ct.ToString().ToLower());
123 | }
124 |
125 | return string.Join(", ", result);
126 | }
127 | }
128 | }
--------------------------------------------------------------------------------
/src/MicrosoftGraphBinding/Bindings/OneDriveAsyncCollector.cs:
--------------------------------------------------------------------------------
1 | /// Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph
5 | {
6 | using System;
7 | using System.Collections.ObjectModel;
8 | using System.IO;
9 | using System.Threading;
10 | using System.Threading.Tasks;
11 | using Microsoft.Azure.WebJobs;
12 | using Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Services;
13 | using Microsoft.Graph;
14 |
15 | ///
16 | /// Collector class used to accumulate and then dispatch requests to MS Graph related to OneDrive
17 | ///
18 | internal class OneDriveAsyncCollector : IAsyncCollector
19 | {
20 | private readonly OneDriveService _service;
21 | private readonly OneDriveAttribute _attribute;
22 | private readonly Collection _fileStreams;
23 |
24 | ///
25 | /// Initializes a new instance of the class.
26 | ///
27 | /// GraphServiceClient used to make calls to MS Graph
28 | /// OneDriveAttribute containing necessary info about file
29 | public OneDriveAsyncCollector(OneDriveService service, OneDriveAttribute attribute)
30 | {
31 | _service = service;
32 | _attribute = attribute;
33 | _fileStreams = new Collection();
34 | }
35 |
36 | ///
37 | /// Add a stream representing a file to the list of objects needing to be processed
38 | ///
39 | /// Stream representing OneDrive file
40 | /// Used to propagate notifications
41 | /// Task whose resolution results in Stream being added to the collector
42 | public Task AddAsync(byte[] item, CancellationToken cancellationToken = default(CancellationToken))
43 | {
44 | if (item == null)
45 | {
46 | throw new ArgumentNullException("No stream found");
47 | }
48 |
49 | _fileStreams.Add(item);
50 | return Task.CompletedTask;
51 | }
52 |
53 | ///
54 | /// Execute methods associated with file streams
55 | ///
56 | /// Used to propagate notifications
57 | /// Task representing the file streams being uploaded
58 | public async Task FlushAsync(CancellationToken cancellationToken = default(CancellationToken))
59 | {
60 | // Upload all files
61 | foreach (var stream in _fileStreams)
62 | {
63 | await _service.UploadOneDriveContentsAsync(_attribute, new MemoryStream(stream), cancellationToken);
64 | }
65 |
66 | this._fileStreams.Clear();
67 | }
68 | }
69 | }
--------------------------------------------------------------------------------
/src/MicrosoftGraphBinding/Bindings/OneDriveAttribute.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | namespace Microsoft.Azure.WebJobs
5 | {
6 | using System.IO;
7 | using Microsoft.Azure.WebJobs.Description;
8 |
9 | ///
10 | /// Binding to O365 OneDrive.
11 | ///
12 | [Binding]
13 | public class OneDriveAttribute : GraphTokenBaseAttribute
14 | {
15 | ///
16 | /// Gets or sets path FROM ONEDRIVE ROOT to file
17 | /// e.g. "Documents/testing.docx"
18 | ///
19 | [AutoResolve]
20 | public string Path { get; set; }
21 |
22 | public FileAccess? Access { get; set; }
23 |
24 | public OneDriveAttribute()
25 | {
26 |
27 | }
28 |
29 | public OneDriveAttribute(FileAccess access)
30 | {
31 | Access = access;
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/src/MicrosoftGraphBinding/Bindings/OneDriveWriteStream.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph
5 | {
6 | using System;
7 | using System.IO;
8 | using System.Threading;
9 | using System.Threading.Tasks;
10 | using Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Services;
11 | using Microsoft.Graph;
12 |
13 | internal class OneDriveWriteStream : Stream
14 | {
15 | private readonly IGraphServiceClient _client;
16 | private readonly Stream _stream;
17 | private readonly string _path;
18 |
19 | public OneDriveWriteStream(IGraphServiceClient client, string path)
20 | {
21 | _client = client;
22 | _path = path;
23 | _stream = new MemoryStream();
24 | }
25 |
26 | public override bool CanRead => false;
27 |
28 | public override bool CanSeek => false;
29 |
30 | public override bool CanWrite => true;
31 |
32 | public override long Length => _stream.Length;
33 |
34 | public override long Position { get => _stream.Position; set => throw new NotSupportedException("This stream cannot seek"); }
35 |
36 | public override void Flush()
37 | {
38 | //NO-OP since flush must be idempotent, and the upload operation is not
39 | }
40 |
41 | public override int Read(byte[] buffer, int offset, int count)
42 | {
43 | throw new NotSupportedException("This stream does not support read operations.");
44 | }
45 |
46 | public override long Seek(long offset, SeekOrigin origin)
47 | {
48 | throw new NotSupportedException("This stream does not support seek operations");
49 | }
50 |
51 | public override void SetLength(long value)
52 | {
53 | throw new NotSupportedException("This stream does not support seek operations");
54 | }
55 |
56 | public override void Close()
57 | {
58 | Task.Run(() => _client.UploadOneDriveItemAsync(_path, _stream, CancellationToken.None)).GetAwaiter().GetResult();
59 | }
60 |
61 | public override void Write(byte[] buffer, int offset, int count)
62 | {
63 | _stream.Write(buffer, offset, count);
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/MicrosoftGraphBinding/Bindings/OutlookAsyncCollector.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph
5 | {
6 | using System;
7 | using System.Collections.ObjectModel;
8 | using System.Threading;
9 | using System.Threading.Tasks;
10 | using Microsoft.Azure.WebJobs;
11 | using Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Services;
12 | using Microsoft.Graph;
13 |
14 | internal class OutlookAsyncCollector : IAsyncCollector
15 | {
16 | private readonly OutlookService _client;
17 | private readonly OutlookAttribute _attribute;
18 | private readonly Collection _messages;
19 |
20 | public OutlookAsyncCollector(OutlookService client, OutlookAttribute attribute)
21 | {
22 | _client = client;
23 | _attribute = attribute;
24 | _messages = new Collection();
25 | }
26 |
27 | public Task AddAsync(Message item, CancellationToken cancellationToken = default(CancellationToken))
28 | {
29 | if (item == null)
30 | {
31 | throw new ArgumentNullException("No message item");
32 | }
33 |
34 | _messages.Add(item);
35 | return Task.CompletedTask;
36 | }
37 |
38 | public async Task FlushAsync(CancellationToken cancellationToken = default(CancellationToken))
39 | {
40 | foreach (var msg in _messages)
41 | {
42 | await _client.SendMessageAsync(_attribute, msg, cancellationToken);
43 | }
44 |
45 | _messages.Clear();
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/MicrosoftGraphBinding/Bindings/OutlookAttribute.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | namespace Microsoft.Azure.WebJobs
5 | {
6 | using Microsoft.Azure.WebJobs.Description;
7 |
8 | ///
9 | /// Outlook Attribute inherits from TokenAttribute
10 | /// No additional info necessary, but remains separate class in order to maintain clarity
11 | ///
12 | [Binding]
13 | public class OutlookAttribute : GraphTokenBaseAttribute
14 | {
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/MicrosoftGraphBinding/Bindings/WebhookTriggerData.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph
5 | {
6 | using Microsoft.Graph;
7 | using Newtonsoft.Json.Linq;
8 |
9 | ///
10 | /// internal trigger payload for when a graph webhook fires.
11 | ///
12 | internal class WebhookTriggerData
13 | {
14 | ///
15 | /// Authenticated client that can call & access the resource;
16 | /// should be the same client that subscribed to the notification.
17 | /// Used to allow other O365 bindings to get on-behalf credentials
18 | ///
19 | public IGraphServiceClient client;
20 |
21 | ///
22 | /// Results of GET request made using the resource in the notification
23 | /// Ultimately what ends up being passed to the user's fx
24 | ///
25 | public JObject Payload;
26 |
27 | ///
28 | /// Type used for filtering notifications
29 | /// (Should also be in Payload)
30 | ///
31 | public string odataType;
32 | }
33 | }
--------------------------------------------------------------------------------
/src/MicrosoftGraphBinding/Bindings/WebhookTriggerListener.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph
5 | {
6 | using System.Threading;
7 | using System.Threading.Tasks;
8 | using Microsoft.Azure.WebJobs.Host.Executors;
9 | using Microsoft.Azure.WebJobs.Host.Listeners;
10 |
11 | // Per-function.
12 | // The listener is "passive". It just tracks the context invoker used for calling functions.
13 | // This receives context that can invoke the function
14 | internal class WebhookTriggerListener : IListener
15 | {
16 | // The context contains an invoker that can call the user function
17 | private readonly ListenerFactoryContext _context;
18 | private readonly WebhookTriggerBindingProvider _parent;
19 | private readonly GraphWebhookTriggerAttribute _attribute;
20 |
21 | public WebhookTriggerListener(ListenerFactoryContext context, WebhookTriggerBindingProvider parent, GraphWebhookTriggerAttribute attribute)
22 | {
23 | this._context = context;
24 | this._parent = parent;
25 | this._attribute = attribute;
26 | }
27 |
28 | public void Cancel()
29 | {
30 | }
31 |
32 | public void Dispose()
33 | {
34 | }
35 |
36 | public Task StartAsync(CancellationToken cancellationToken)
37 | {
38 | this._parent.AddListener(this._attribute, this);
39 | return Task.CompletedTask;
40 | }
41 |
42 | public Task StopAsync(CancellationToken cancellationToken)
43 | {
44 | this._parent.StopListeners();
45 |
46 | return Task.CompletedTask;
47 | }
48 |
49 | public ITriggeredFunctionExecutor Executor => this._context.Executor;
50 | }
51 | }
--------------------------------------------------------------------------------
/src/MicrosoftGraphBinding/Config/Converters/ExcelConverters.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Config.Converters
5 | {
6 | using System.Linq;
7 | using System.Collections.Generic;
8 | using System.Threading;
9 | using System.Threading.Tasks;
10 | using Newtonsoft.Json.Linq;
11 | using Microsoft.Graph;
12 | using Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Services;
13 |
14 | internal class ExcelConverters
15 | {
16 | internal class ExcelConverter :
17 | IAsyncConverter,
18 | IAsyncConverter,
19 | IAsyncConverter>,
20 | IConverter
21 | {
22 | private readonly ExcelService _service;
23 |
24 | public ExcelConverter(ExcelService service)
25 | {
26 | _service = service;
27 | }
28 |
29 | public string Convert(JObject input)
30 | {
31 | return input.ToString();
32 | }
33 |
34 | async Task> IAsyncConverter>.ConvertAsync(ExcelAttribute attr, CancellationToken token)
35 | {
36 | return new ExcelAsyncCollector(_service, attr);
37 | }
38 |
39 | async Task IAsyncConverter.ConvertAsync(ExcelAttribute attr, CancellationToken cancellationToken)
40 | {
41 | return await _service.GetExcelRangeAsync(attr, cancellationToken);
42 | }
43 |
44 | async Task IAsyncConverter.ConvertAsync(ExcelAttribute input, CancellationToken cancellationToken)
45 | {
46 | return await _service.GetExcelTableAsync(input, cancellationToken);
47 | }
48 | }
49 |
50 | internal class ExcelGenericsConverter : IAsyncConverter,
51 | IConverter
52 | {
53 | private readonly ExcelService _service;
54 |
55 | ///
56 | /// Initializes a new instance of the class.
57 | ///
58 | /// O365Extension to which the result of the request for data will be returned
59 | public ExcelGenericsConverter(ExcelService service)
60 | {
61 | _service = service;
62 | }
63 |
64 | async Task IAsyncConverter.ConvertAsync(ExcelAttribute input, CancellationToken cancellationToken)
65 | {
66 | return await _service.GetExcelRangePOCOAsync(input, cancellationToken);
67 | }
68 |
69 | ///
70 | /// Convert from POCO -> string (either row or rows)
71 | ///
72 | /// POCO input from fx
73 | /// String representation of JSON
74 | public string Convert(T input)
75 | {
76 | // handle T[]
77 | if (typeof(T).IsArray)
78 | {
79 | var array = input as object[];
80 | return ConvertEnumerable(array).ToString();
81 | }
82 | else
83 | {
84 | // handle T
85 | JObject data = JObject.FromObject(input);
86 | data[O365Constants.POCOKey] = true; // Set Microsoft.O365Bindings.POCO flag to indicate that data is from POCO (vs. object[][])
87 |
88 | return data.ToString();
89 | }
90 | }
91 |
92 | ///
93 | /// Convert from List -> JObject
94 | ///
95 | /// POCO input from fx
96 | /// JObject with proper keys set
97 | public JObject Convert(List input)
98 | {
99 | return ConvertEnumerable(input);
100 | }
101 |
102 | private JObject ConvertEnumerable(IEnumerable input)
103 | {
104 | JObject jsonContent = new JObject();
105 |
106 | JArray rowData = JArray.FromObject(input);
107 |
108 | // List -> JArray
109 | jsonContent[O365Constants.ValuesKey] = rowData;
110 |
111 | // Set rows, columns needed if updating entire worksheet
112 | jsonContent[O365Constants.RowsKey] = rowData.Count();
113 |
114 | // No exception -- array is rectangular by default
115 | jsonContent[O365Constants.ColsKey] = rowData.First.Count();
116 |
117 | // Set POCO key to indicate that the values need to be ordered to match the header of the existing table
118 | jsonContent[O365Constants.POCOKey] = true;
119 |
120 | return jsonContent;
121 | }
122 | }
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/src/MicrosoftGraphBinding/Config/Converters/GraphWebhookSubscriptionConverters.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Config.Converters
5 | {
6 | using System;
7 | using System.Collections.Generic;
8 | using System.Linq;
9 | using System.Reflection;
10 | using System.Threading;
11 | using System.Threading.Tasks;
12 | using Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Services;
13 | using Microsoft.Graph;
14 | using Newtonsoft.Json.Linq;
15 |
16 | internal class GraphWebhookSubscriptionConverters
17 | {
18 | internal class GraphWebhookSubscriptionConverter :
19 | IAsyncConverter,
20 | IAsyncConverter,
21 | IAsyncConverter
22 | {
23 | private readonly GraphServiceClientManager _clientManager;
24 | private readonly IGraphSubscriptionStore _subscriptionStore;
25 | private readonly GraphOptions _options;
26 |
27 | public GraphWebhookSubscriptionConverter(GraphServiceClientManager clientManager, GraphOptions options, IGraphSubscriptionStore subscriptionStore)
28 | {
29 | _options = options;
30 | _clientManager = clientManager;
31 | _subscriptionStore = subscriptionStore;
32 | }
33 |
34 | async Task IAsyncConverter.ConvertAsync(GraphWebhookSubscriptionAttribute input, CancellationToken cancellationToken)
35 | {
36 | return (await GetSubscriptionsFromAttributeAsync(input, cancellationToken))
37 | .Select(entry => entry.Subscription)
38 | .ToArray();
39 | }
40 |
41 | async Task IAsyncConverter.ConvertAsync(GraphWebhookSubscriptionAttribute input, CancellationToken cancellationToken)
42 | {
43 | return (await GetSubscriptionsFromAttributeAsync(input, cancellationToken))
44 | .Select(entry => entry.Subscription.Id)
45 | .ToArray();
46 | }
47 |
48 | async Task IAsyncConverter.ConvertAsync(GraphWebhookSubscriptionAttribute input, CancellationToken cancellationToken)
49 | {
50 | SubscriptionEntry[] subscriptions = await GetSubscriptionsFromAttributeAsync(input, cancellationToken);
51 | var serializedSubscriptions = new JArray();
52 | foreach (var subscription in subscriptions)
53 | {
54 | serializedSubscriptions.Add(JObject.FromObject(subscription));
55 | }
56 | return serializedSubscriptions;
57 | }
58 |
59 | protected async Task GetSubscriptionsFromAttributeAsync(GraphWebhookSubscriptionAttribute attribute, CancellationToken cancellationToken)
60 | {
61 | IEnumerable subscriptionEntries = await _subscriptionStore.GetAllSubscriptionsAsync();
62 | if (TokenIdentityMode.UserFromRequest.ToString().Equals(attribute.Filter, StringComparison.OrdinalIgnoreCase))
63 | {
64 | var dummyTokenAttribute = new TokenAttribute()
65 | {
66 | Resource = _options.GraphBaseUrl,
67 | Identity = TokenIdentityMode.UserFromToken,
68 | UserToken = attribute.UserToken,
69 | IdentityProvider = "AAD",
70 | };
71 | var graph = await _clientManager.GetMSGraphClientFromTokenAttributeAsync(dummyTokenAttribute, cancellationToken);
72 | var user = await graph.Me.Request().GetAsync();
73 | subscriptionEntries = subscriptionEntries.Where(entry => entry.UserId.Equals(user.Id));
74 | }
75 | else if (attribute.Filter != null)
76 | {
77 | throw new InvalidOperationException($"There is no filter for {attribute.Filter}");
78 | }
79 | return subscriptionEntries.ToArray();
80 | }
81 | }
82 |
83 | internal class GenericGraphWebhookSubscriptionConverter : GraphWebhookSubscriptionConverter,
84 | IAsyncConverter
85 | {
86 | public GenericGraphWebhookSubscriptionConverter(GraphServiceClientManager clientManager, GraphOptions options, IGraphSubscriptionStore subscriptionStore) : base(clientManager, options, subscriptionStore)
87 | {
88 | }
89 |
90 | public async Task ConvertAsync(GraphWebhookSubscriptionAttribute input, CancellationToken cancellationToken)
91 | {
92 | return ConvertSubscriptionEntries(await this.GetSubscriptionsFromAttributeAsync(input, cancellationToken));
93 | }
94 |
95 | //Converts a Subscription Entry into a "flattened" POCO representation where the properties
96 | //of the POCO can be UserId or any of the properties of Subscription
97 | public T[] ConvertSubscriptionEntries(SubscriptionEntry[] entries)
98 | {
99 | var pocoType = typeof(T);
100 | var subEntryType = typeof(Subscription);
101 | var subscriptionProperties = subEntryType.GetProperties();
102 | var pocoProperties = pocoType.GetProperties();
103 |
104 | T[] pocos = new T[entries.Length];
105 | for (int i = 0; i < pocos.Length; i++)
106 | {
107 | pocos[i] = (T)Activator.CreateInstance(typeof(T), new object[] { });
108 | }
109 |
110 | foreach (PropertyInfo pocoProperty in pocoProperties)
111 | {
112 | var subscriptionProperty = subEntryType.GetProperty(pocoProperty.Name, pocoProperty.PropertyType);
113 | if (subscriptionProperty != null)
114 | {
115 | for (int i = 0; i < pocos.Length; i++)
116 | {
117 | pocoProperty.SetValue(pocos[i], subscriptionProperty.GetValue(entries[i].Subscription));
118 | }
119 | }
120 | }
121 |
122 | var pocoUserIdProperty = pocoType.GetProperty("UserId");
123 | if (pocoUserIdProperty != null)
124 | {
125 | var subEntryUserIdProperty = typeof(SubscriptionEntry).GetProperty("UserId");
126 | for (int i = 0; i < pocos.Length; i++)
127 | {
128 | pocoUserIdProperty.SetValue(pocos[i], subEntryUserIdProperty.GetValue(entries[i]));
129 | }
130 | }
131 |
132 | return pocos;
133 | }
134 | }
135 | }
136 | }
--------------------------------------------------------------------------------
/src/MicrosoftGraphBinding/Config/Converters/OneDriveConverter.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Config.Converters
5 | {
6 | using System.IO;
7 | using System.Text;
8 | using System.Threading;
9 | using System.Threading.Tasks;
10 | using Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Services;
11 | using Microsoft.Graph;
12 |
13 | internal class OneDriveConverter :
14 | IAsyncConverter,
15 | IAsyncConverter,
16 | IAsyncConverter,
17 | IAsyncConverter,
18 | IAsyncConverter>
19 | {
20 | private readonly OneDriveService _service;
21 |
22 | public OneDriveConverter(OneDriveService service)
23 | {
24 | _service = service;
25 | }
26 |
27 | async Task IAsyncConverter.ConvertAsync(OneDriveAttribute input, CancellationToken cancellationToken)
28 | {
29 | return await _service.GetOneDriveContentsAsByteArrayAsync(input, cancellationToken);
30 | }
31 |
32 | async Task IAsyncConverter.ConvertAsync(OneDriveAttribute input, CancellationToken cancellationToken)
33 | {
34 | var byteArray = await _service.GetOneDriveContentsAsByteArrayAsync(input, cancellationToken);
35 | return Encoding.UTF8.GetString(byteArray);
36 | }
37 |
38 | async Task IAsyncConverter.ConvertAsync(OneDriveAttribute input, CancellationToken cancellationToken)
39 | {
40 | return await _service.GetOneDriveContentsAsStreamAsync(input, cancellationToken);
41 | }
42 |
43 | async Task IAsyncConverter.ConvertAsync(OneDriveAttribute input, CancellationToken cancellationToken)
44 | {
45 | return await _service.GetOneDriveItemAsync(input, cancellationToken);
46 | }
47 |
48 | public async Task> ConvertAsync(OneDriveAttribute input, CancellationToken cancellationToken)
49 | {
50 | return new OneDriveAsyncCollector(_service, input);
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/MicrosoftGraphBinding/Config/Converters/OutlookConverter.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Config.Converters
5 | {
6 | using System;
7 | using System.Collections.Generic;
8 | using System.Threading;
9 | using System.Threading.Tasks;
10 | using Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Services;
11 | using Microsoft.Graph;
12 | using Newtonsoft.Json.Linq;
13 |
14 | internal class OutlookConverter : IConverter, IConverter, IAsyncConverter>
15 | {
16 | private OutlookService _outlookService;
17 |
18 | public OutlookConverter(OutlookService outlookService)
19 | {
20 | _outlookService = outlookService;
21 | }
22 |
23 | public Message Convert(JObject input)
24 | {
25 | // Set up recipient(s)
26 | List recipientList = new List();
27 |
28 | JToken recipientToken = GetPropertyValueIgnoreCase(input, "recipient", throwException: false)
29 | ?? GetPropertyValueIgnoreCase(input, "recipients", throwException: false);
30 |
31 | if (recipientToken == null)
32 | {
33 | throw new InvalidOperationException("The object needs to have a 'recipient' or 'recipients' field.");
34 | }
35 |
36 | List recipients;
37 |
38 | // MS Graph Message expects a list of recipients
39 | if (recipientToken is JArray)
40 | {
41 | // JArray -> List
42 | recipients = recipientToken.ToObject>();
43 | }
44 | else
45 | {
46 | // List with one JObject
47 | recipients = new List();
48 | recipients.Add(recipientToken.ToObject());
49 | }
50 |
51 | if (recipients.Count == 0)
52 | {
53 | throw new InvalidOperationException("At least one recipient must be provided.");
54 | }
55 |
56 | foreach (JObject recip in recipients)
57 | {
58 | Recipient recipient = new Recipient
59 | {
60 | EmailAddress = new EmailAddress
61 | {
62 | Address = GetPropertyValueIgnoreCase(recip, "address"),
63 | Name = GetPropertyValueIgnoreCase(recip, "name", throwException: false),
64 | },
65 | };
66 | recipientList.Add(recipient);
67 | }
68 |
69 | // Actually create message
70 | var msg = new Message
71 | {
72 | Body = new ItemBody
73 | {
74 | Content = GetPropertyValueIgnoreCase(input, "body"),
75 | ContentType = BodyType.Text,
76 | },
77 | Subject = GetPropertyValueIgnoreCase(input, "subject"),
78 | ToRecipients = recipientList,
79 | };
80 |
81 | return msg;
82 | }
83 |
84 | public Message Convert(string input)
85 | {
86 | return Convert(JObject.Parse(input));
87 | }
88 |
89 | private static T GetPropertyValueIgnoreCase(JObject input, string key, bool throwException = true)
90 | {
91 | JToken value;
92 | if (!input.TryGetValue(key, StringComparison.OrdinalIgnoreCase, out value))
93 | {
94 | if (throwException)
95 | {
96 | throw new InvalidOperationException($"The object needs to have a {key} field.");
97 | }
98 | return default(T);
99 | }
100 | return value.ToObject();
101 | }
102 |
103 | public async Task> ConvertAsync(OutlookAttribute input, CancellationToken cancellationToken)
104 | {
105 | return new OutlookAsyncCollector(_outlookService, input);
106 | }
107 | }
108 |
109 | // This converter goes directly to Message instead of T -> JObject and composing
110 | // with JObject -> Message as composition conversions with OpenTypes are broken
111 | internal class OutlookGenericsConverter : IConverter
112 | {
113 | private readonly OutlookConverter _converter;
114 |
115 | public OutlookGenericsConverter(OutlookService service)
116 | {
117 | _converter = new OutlookConverter(service);
118 | }
119 |
120 | public Message Convert(T input)
121 | {
122 | return _converter.Convert(JObject.FromObject(input));
123 | }
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/src/MicrosoftGraphBinding/Config/GraphOptions.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | using System;
5 |
6 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Config
7 | {
8 | public class GraphOptions
9 | {
10 | private const string defaultTokenMapLocation = "D:/home/data/byob_graphmap";
11 | public string SubscriptionStoreLocationAppSettingName { get; set; } = "BYOB_TokenMap";
12 |
13 | public string GraphBaseUrl { get; set; } = O365Constants.GraphBaseUrl;
14 |
15 | public string TokenMapLocation { get; set; } = defaultTokenMapLocation;
16 |
17 | public TimeSpan WebhookExpirationTimeSpan { get; set; } = new TimeSpan(0, 0, 4230, 0);
18 |
19 | public void SetAppSettings(INameResolver appSettings)
20 | {
21 | var settingsTokenMapLocation = appSettings.Resolve(SubscriptionStoreLocationAppSettingName);
22 | if (!string.IsNullOrEmpty(settingsTokenMapLocation) && TokenMapLocation == defaultTokenMapLocation)
23 | TokenMapLocation = settingsTokenMapLocation;
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/MicrosoftGraphBinding/Config/GraphServiceClientManager.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Services
5 | {
6 | using System;
7 | using System.Collections.Concurrent;
8 | using System.IdentityModel.Tokens.Jwt;
9 | using System.Linq;
10 | using System.Threading;
11 | using System.Threading.Tasks;
12 | using Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Config;
13 | using Microsoft.Graph;
14 |
15 | internal class GraphServiceClientManager
16 | {
17 | private readonly IAsyncConverter _tokenProvider;
18 | private readonly IGraphServiceClientProvider _clientProvider;
19 | private readonly GraphOptions _options;
20 |
21 | ///
22 | /// Map principal Id + scopes -> GraphServiceClient + token expiration date
23 | ///
24 | private ConcurrentDictionary _clients = new ConcurrentDictionary();
25 |
26 | public GraphServiceClientManager(GraphOptions options, IAsyncConverter tokenProvider, IGraphServiceClientProvider clientProvider)
27 | {
28 | _tokenProvider = tokenProvider;
29 | _clientProvider = clientProvider;
30 | _options = options;
31 | }
32 |
33 | ///
34 | /// Retrieve audience from raw JWT
35 | ///
36 | /// JWT
37 | /// Token audience
38 | private static string GetTokenOID(string rawToken)
39 | {
40 | var jwt = new JwtSecurityToken(rawToken);
41 | var oidClaim = jwt.Claims.FirstOrDefault(claim => claim.Type == "oid");
42 | if (oidClaim == null)
43 | {
44 | throw new InvalidOperationException("The graph token is missing an oid. Check your Microsoft Graph binding configuration.");
45 | }
46 | return oidClaim.Value;
47 | }
48 |
49 | ///
50 | /// Given a JWT, return the list of scopes in alphabetical order
51 | ///
52 | /// raw JWT
53 | /// string of scopes in alphabetical order, separated by a space
54 | private static string GetTokenOrderedScopes(string rawToken)
55 | {
56 | var jwt = new JwtSecurityToken(rawToken);
57 | var stringScopes = jwt.Claims.FirstOrDefault(claim => claim.Type == "scp")?.Value;
58 | if (stringScopes == null)
59 | {
60 | throw new InvalidOperationException("The graph token has no scopes. Ensure your application is properly configured to access the Microsoft Graph.");
61 | }
62 | var scopes = stringScopes.Split(' ');
63 | Array.Sort(scopes);
64 | return string.Join(" ", scopes);
65 | }
66 |
67 | ///
68 | /// Retrieve integer token expiration date
69 | ///
70 | /// raw JWT
71 | /// parsed expiration date
72 | private static int GetTokenExpirationDate(string rawToken)
73 | {
74 | var jwt = new JwtSecurityToken(rawToken);
75 | var stringTime = jwt.Claims.FirstOrDefault(claim => claim.Type == "exp").Value;
76 | int result;
77 | if (int.TryParse(stringTime, out result))
78 | {
79 | return result;
80 | }
81 | else
82 | {
83 | return -1;
84 | }
85 | }
86 |
87 | ///
88 | /// Hydrate GraphServiceClient from a moniker (serialized TokenAttribute)
89 | ///
90 | /// string representing serialized TokenAttribute
91 | /// Authenticated GraphServiceClient
92 | public async Task GetMSGraphClientFromUserIdAsync(string userId, CancellationToken token)
93 | {
94 | var attr = new TokenAttribute
95 | {
96 | UserId = userId,
97 | Resource = _options.GraphBaseUrl,
98 | Identity = TokenIdentityMode.UserFromId,
99 | };
100 |
101 | return await this.GetMSGraphClientFromTokenAttributeAsync(attr, token);
102 | }
103 |
104 | ///
105 | /// Either retrieve existing GSC or create a new one
106 | /// GSCs are cached using a combination of the user's principal ID and the scopes of the token used to authenticate
107 | ///
108 | /// Token attribute with either principal ID or ID token
109 | /// Authenticated GSC
110 | public virtual async Task GetMSGraphClientFromTokenAttributeAsync(TokenBaseAttribute attribute, CancellationToken cancellationToken)
111 | {
112 | string token = await this._tokenProvider.ConvertAsync(attribute, cancellationToken);
113 | string principalId = GetTokenOID(token);
114 |
115 | var key = string.Concat(principalId, " ", GetTokenOrderedScopes(token));
116 |
117 | CachedClient cachedClient = null;
118 |
119 | // Check to see if there already exists a GSC associated with this principal ID and the token scopes.
120 | if (_clients.TryGetValue(key, out cachedClient))
121 | {
122 | // Check if token is expired
123 | if (cachedClient.expirationDate < DateTimeOffset.Now.ToUnixTimeSeconds())
124 | {
125 | // Need to update the client's token & expiration date
126 | // $$ todo -- just reset token instead of whole new authentication provider?
127 | _clientProvider.UpdateGraphServiceClientAuthToken(cachedClient.client, token);
128 | cachedClient.expirationDate = GetTokenExpirationDate(token);
129 | }
130 |
131 | return cachedClient.client;
132 | }
133 | else
134 | {
135 | cachedClient = new CachedClient
136 | {
137 | client = _clientProvider.CreateNewGraphServiceClient(token),
138 | expirationDate = GetTokenExpirationDate(token),
139 | };
140 | _clients.TryAdd(key, cachedClient);
141 | return cachedClient.client;
142 | }
143 | }
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/src/MicrosoftGraphBinding/Config/GraphServiceClientProvider.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Config
5 | {
6 | using System;
7 | using System.Net.Http.Headers;
8 | using System.Threading.Tasks;
9 | using Microsoft.Graph;
10 |
11 | internal class GraphServiceClientProvider : IGraphServiceClientProvider
12 | {
13 | public IGraphServiceClient CreateNewGraphServiceClient(string token)
14 | {
15 | return new GraphServiceClient(
16 | new DelegateAuthenticationProvider(
17 | (requestMessage) =>
18 | {
19 | requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", token);
20 | return Task.CompletedTask;
21 | }));
22 | }
23 |
24 | public void UpdateGraphServiceClientAuthToken(IGraphServiceClient client, string token)
25 | {
26 | GraphServiceClient typedClient = client as GraphServiceClient;
27 | if (typedClient == null)
28 | {
29 | throw new InvalidOperationException($"Only {nameof(IGraphServiceClient)} of type {nameof(GraphServiceClient)} should be created with this client provider");
30 | }
31 |
32 | typedClient.AuthenticationProvider = new DelegateAuthenticationProvider(
33 | (requestMessage) =>
34 | {
35 | requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", token);
36 |
37 | return Task.CompletedTask;
38 | });
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/MicrosoftGraphBinding/Config/IGraphServiceClientProvider.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Config
5 | {
6 | using Microsoft.Graph;
7 |
8 | internal interface IGraphServiceClientProvider
9 | {
10 | IGraphServiceClient CreateNewGraphServiceClient(string token);
11 |
12 | void UpdateGraphServiceClientAuthToken(IGraphServiceClient client, string token);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/MicrosoftGraphBinding/Config/IGraphSubscriptionStore.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 | using System.Runtime.CompilerServices;
4 |
5 | [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100cd1dabd5a893b40e75dc901fe7293db4a3caf9cd4d3e3ed6178d49cd476969abe74a9e0b7f4a0bb15edca48758155d35a4f05e6e852fff1b319d103b39ba04acbadd278c2753627c95e1f6f6582425374b92f51cca3deb0d2aab9de3ecda7753900a31f70a236f163006beefffe282888f85e3c76d1205ec7dfef7fa472a17b1")]
6 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Config
7 | {
8 | using System.Threading.Tasks;
9 | using Microsoft.Graph;
10 |
11 | internal interface IGraphSubscriptionStore
12 | {
13 | ///
14 | /// Saves the subscription in the token store, along with the user id for the subscription
15 | ///
16 | /// The subscription being saved
17 | /// The user id of the user who the subscription belongs to
18 | /// A task that represents the asynchronous operation.
19 | Task SaveSubscriptionEntryAsync(Subscription subscription, string userId);
20 |
21 | ///
22 | /// Retrieves all subscription entries in the store.
23 | ///
24 | /// A task that respresents the asynchronous operation. The task's result contains the
25 | /// subscription entries.
26 | Task GetAllSubscriptionsAsync();
27 |
28 | ///
29 | /// Retrieves the subscription entry with the given subscription id.
30 | /// Returns null if the subscriptionId does not match any subscriptions in the store
31 | ///
32 | /// The subscription id of the entry to retrieve
33 | /// A task that respresents the asynchronous operation. The task's result contains the
34 | /// subscription entry with the subscription Id provided
35 | Task GetSubscriptionEntryAsync(string subscriptionId);
36 |
37 | ///
38 | /// Deletes the subscription entry in the store with the given subscription Id.
39 | /// NoOps if the subscriptionId does not match any subscriptions in the store
40 | ///
41 | /// The subscription id of the entry to delete
42 | /// A task that represents the asynchronous operation
43 | Task DeleteAsync(string subscriptionId);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/MicrosoftGraphBinding/Config/MicrosoftGraphWebJobsBuilderExtensions.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Config
5 | {
6 | using System;
7 | using Microsoft.Azure.WebJobs.Extensions.AuthTokens;
8 | using Microsoft.Extensions.DependencyInjection;
9 |
10 | internal static class MicrosoftGraphWebJobsBuilderExtensions
11 | {
12 | public static IWebJobsBuilder AddMicrosoftGraph(this IWebJobsBuilder builder)
13 | {
14 | if (builder == null)
15 | {
16 | throw new ArgumentNullException(nameof(builder));
17 | }
18 |
19 | builder.AddExtension()
20 | .BindOptions()
21 | .BindOptions()
22 | .Services
23 | .AddAuthTokenServices()
24 | .AddSingleton, TokenConverter>()
25 | .AddSingleton()
26 | .AddSingleton();
27 | return builder;
28 | }
29 |
30 | public static IWebJobsBuilder AddMicrosoftGraphForTests(this IWebJobsBuilder builder)
31 | {
32 | if (builder == null)
33 | {
34 | throw new ArgumentNullException(nameof(builder));
35 | }
36 |
37 | builder.AddExtension();
38 | return builder;
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/MicrosoftGraphBinding/Config/MicrosoftGraphWebJobsStartup.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | using Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph;
5 | using Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Config;
6 | using Microsoft.Azure.WebJobs.Hosting;
7 |
8 | [assembly: WebJobsStartup(typeof(MicrosoftGraphWebJobsStartup))]
9 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph
10 | {
11 | public class MicrosoftGraphWebJobsStartup : IWebJobsStartup
12 | {
13 | public void Configure(IWebJobsBuilder builder)
14 | {
15 | builder.AddMicrosoftGraph();
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/MicrosoftGraphBinding/Config/SubscriptionEntry.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Config
5 | {
6 | using Microsoft.Graph;
7 |
8 | internal class SubscriptionEntry
9 | {
10 | ///
11 | /// Gets or sets subscription ID returned by MS Graph after creation
12 | ///
13 | public Subscription Subscription { get; set; }
14 |
15 | ///
16 | /// Gets or sets the user id for the subscription
17 | ///
18 | public string UserId { get; set; }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/MicrosoftGraphBinding/Config/WebhookSubscriptionStore.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph
5 | {
6 | using System;
7 | using System.Collections.Concurrent;
8 | using System.Collections.Generic;
9 | using System.IO;
10 | using System.Linq;
11 | using System.Threading.Tasks;
12 | using Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Config;
13 | using Microsoft.Extensions.Options;
14 | using Microsoft.Graph;
15 | using Newtonsoft.Json;
16 | using File = System.IO.File;
17 |
18 | ///
19 | /// Store mapping from webhook subscription IDs to a token.
20 | ///
21 | internal class WebhookSubscriptionStore : IGraphSubscriptionStore
22 | {
23 | private readonly GraphOptions _options;
24 |
25 | private FileLock _fileLock;
26 |
27 | ///
28 | /// Initializes a new instance of the class.
29 | /// Find webhook token cache path
30 | /// If it doesn't exist, create directory
31 | ///
32 | /// Value of app setting used to det. webhook token cache path
33 | public WebhookSubscriptionStore(IOptions options)
34 | {
35 | _options = options.Value;
36 | _fileLock = new FileLock();
37 | _fileLock.PerformWriteIO(_options.TokenMapLocation, () => CreateRootDirectory());
38 | }
39 |
40 | private void CreateRootDirectory()
41 | {
42 | if (!Directory.Exists(_options.TokenMapLocation))
43 | {
44 | Directory.CreateDirectory(_options.TokenMapLocation);
45 | }
46 | }
47 |
48 | private string GetSubscriptionPath(string subscriptionId)
49 | {
50 | return Path.Combine(_options.TokenMapLocation, subscriptionId);
51 | }
52 |
53 | private string GetSubscriptionPath(Subscription subscription)
54 | {
55 | return GetSubscriptionPath(subscription.Id);
56 | }
57 |
58 | public async Task SaveSubscriptionEntryAsync(Subscription subscription, string userId)
59 | {
60 | var entry = new SubscriptionEntry
61 | {
62 | Subscription = subscription,
63 | UserId = userId,
64 | };
65 | var subPath = this.GetSubscriptionPath(subscription);
66 | var jsonString = JsonConvert.SerializeObject(entry);
67 | await _fileLock.PerformWriteIOAsync(subPath, () => File.WriteAllText(subPath, jsonString));
68 | }
69 |
70 | public async Task GetAllSubscriptionsAsync()
71 | {
72 | var subscriptionPaths = await _fileLock.PerformReadIOAsync>(_options.TokenMapLocation, Directory.EnumerateFiles);
73 | var entryTasks = subscriptionPaths.Select(path => this.GetAsyncFromPath(path));
74 | return await Task.WhenAll(entryTasks);
75 | }
76 |
77 | private async Task GetAsyncFromPath(string path)
78 | {
79 | var contents = await _fileLock.PerformReadIOAsync(path, File.ReadAllText);
80 | var entry = JsonConvert.DeserializeObject(contents);
81 | return entry;
82 | }
83 |
84 | public async Task GetSubscriptionEntryAsync(string subId)
85 | {
86 | string path = this.GetSubscriptionPath(subId);
87 | return await this.GetAsyncFromPath(path);
88 | }
89 |
90 | ///
91 | /// Delete a single subscription entry
92 | ///
93 | /// Subscription entry to be deleted
94 | public async Task DeleteAsync(string subscriptionId)
95 | {
96 | var path = this.GetSubscriptionPath(subscriptionId);
97 | await _fileLock.PerformWriteIOAsync(path, () => DeleteFileIfExists(path));
98 | }
99 |
100 | private void DeleteFileIfExists(string path)
101 | {
102 | if (File.Exists(path))
103 | {
104 | File.Delete(path);
105 | }
106 | }
107 |
108 | private class FileLock
109 | {
110 | private readonly ConcurrentDictionary _locks = new ConcurrentDictionary();
111 |
112 | public void PerformWriteIO(string path, Action ioAction)
113 | {
114 | lock (_locks.GetOrAdd(path, new object()))
115 | {
116 | try
117 | {
118 | ioAction();
119 | }
120 | finally
121 | {
122 | object removedLock;
123 | _locks.TryRemove(path, out removedLock);
124 | }
125 | }
126 | }
127 |
128 | public async Task PerformWriteIOAsync(string path, Action ioAction)
129 | {
130 | PerformWriteIO(path, ioAction);
131 | }
132 |
133 | public async Task PerformReadIOAsync(string path, Func ioAction)
134 | {
135 | lock (_locks.GetOrAdd(path, new object()))
136 | {
137 | T result = default(T);
138 | try
139 | {
140 | result = ioAction(path);
141 | }
142 | finally
143 | {
144 | object removedLock;
145 | _locks.TryRemove(path, out removedLock);
146 | }
147 |
148 | return result;
149 | }
150 | }
151 | }
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/src/MicrosoftGraphBinding/MicrosoftGraphBinding.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 | Microsoft.Azure.WebJobs.Extensions.O365
6 | Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph
7 |
8 | Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph
9 | This extension adds bindings for Microsoft Graph
10 | 1
11 | 0
12 | 0
13 | beta6
14 | $(MajorVersion).0.0.0
15 | $(MajorVersion).$(MinorVersion).$(PatchVersion)-$(BetaVersion)$(VersionSuffix)
16 | N/A
17 | $(Version) Commit hash: $(CommitHash)
18 |
19 | Microsoft
20 | Microsoft
21 | © .NET Foundation. All rights reserved.
22 | https://go.microsoft.com/fwlink/?linkid=2028464
23 |
24 | git
25 | https://github.com/Azure/azure-functions-microsoftgraph-extension
26 | true
27 | https://github.com/Azure/azure-functions-microsoftgraph-extension
28 | true
29 | ../../sign.snk
30 |
31 |
32 |
33 | Full
34 | true
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | All
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/src/MicrosoftGraphBinding/Services/ExcelClient.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Services
5 | {
6 | using System.Threading;
7 | using System.Threading.Tasks;
8 | using Microsoft.Graph;
9 | using Newtonsoft.Json.Linq;
10 |
11 | ///
12 | /// Helper for calling onto Excel (MS) Graph
13 | ///
14 | internal static class ExcelClient
15 | {
16 | public static async Task GetTableWorkbookAsync(this IGraphServiceClient client, string path, string tableName, CancellationToken token)
17 | {
18 | return await client
19 | .GetWorkbookTableRequest(path, tableName)
20 | .Request()
21 | .GetAsync(token);
22 | }
23 |
24 | public static async Task GetTableWorkbookRangeAsync(this IGraphServiceClient client, string path, string tableName, CancellationToken token)
25 | {
26 | return await client
27 | .GetWorkbookTableRequest(path, tableName)
28 | .Range()
29 | .Request()
30 | .GetAsync(token);
31 | }
32 |
33 | public static async Task GetWorksheetWorkbookAsync(this IGraphServiceClient client, string path, string worksheetName, CancellationToken token)
34 | {
35 | return await client
36 | .GetWorkbookWorksheetRequest(path, worksheetName)
37 | .UsedRange()
38 | .Request()
39 | .GetAsync(token);
40 | }
41 |
42 | public static async Task GetWorkSheetWorkbookInRangeAsync(this IGraphServiceClient client, string path, string worksheetName, string range, CancellationToken token)
43 | {
44 | return await client
45 | .GetWorkbookWorksheetRequest(path, worksheetName)
46 | .Range(range)
47 | .Request()
48 | .GetAsync(token);
49 | }
50 |
51 | public static async Task GetTableHeaderRowAsync(this IGraphServiceClient client, string path, string tableName, CancellationToken token)
52 | {
53 | var headerRowRange = await client
54 | .GetWorkbookTableRequest(path, tableName)
55 | .HeaderRowRange()
56 | .Request()
57 | .GetAsync(token);
58 | return headerRowRange.Values.ToObject()[0]; //header row array is embedded as the only element in its own array
59 | }
60 |
61 | public static async Task PostTableRowAsync(this IGraphServiceClient client, string path, string tableName, JToken row, CancellationToken token)
62 | {
63 | return await client
64 | .GetWorkbookTableRequest(path, tableName)
65 | .Rows
66 | .Add(null, row)
67 | .Request()
68 | .PostAsync(token);
69 | }
70 |
71 | public static async Task PatchWorksheetAsync(this IGraphServiceClient client, string path, string worksheetName, string range, WorkbookRange newWorkbook, CancellationToken token)
72 | {
73 | return await client
74 | .GetWorkbookWorksheetRequest(path, worksheetName)
75 | .Range(range)
76 | .Request()
77 | .PatchAsync(newWorkbook, token);
78 | }
79 |
80 |
81 | private static IWorkbookTableRequestBuilder GetWorkbookTableRequest(this IGraphServiceClient client, string path, string tableName)
82 | {
83 | return client
84 | .Me
85 | .Drive
86 | .Root
87 | .ItemWithPath(path)
88 | .Workbook
89 | .Tables[tableName];
90 | }
91 |
92 | private static IWorkbookWorksheetRequestBuilder GetWorkbookWorksheetRequest(this IGraphServiceClient client, string path, string worksheetName)
93 | {
94 | return client
95 | .Me
96 | .Drive
97 | .Root
98 | .ItemWithPath(path)
99 | .Workbook
100 | .Worksheets[worksheetName];
101 | }
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/src/MicrosoftGraphBinding/Services/OneDriveClient.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Services
5 | {
6 | using System.IO;
7 | using System.Threading;
8 | using System.Threading.Tasks;
9 | using Microsoft.Graph;
10 | using Newtonsoft.Json;
11 |
12 | ///
13 | /// Helper class for calling onto (MS) OneDrive Graph
14 | ///
15 | internal static class OneDriveClient
16 | {
17 | ///
18 | /// Retrieve contents of OneDrive file
19 | ///
20 | /// Authenticated Graph Service Client used to retrieve file
21 | /// Attribute with necessary data (e.g. path)
22 | /// Stream of file content
23 | public static async Task GetOneDriveContentStreamAsync(this IGraphServiceClient client, string path, CancellationToken token)
24 | {
25 | // Retrieve stream of OneDrive item
26 | return await client
27 | .Me
28 | .Drive
29 | .Root
30 | .ItemWithPath(path)
31 | .Content
32 | .Request()
33 | .GetAsync(token);
34 | }
35 |
36 | public static async Task GetOneDriveItemAsync(this IGraphServiceClient client, string path, CancellationToken token)
37 | {
38 | // Retrieve OneDrive item
39 | return await client
40 | .Me
41 | .Drive
42 | .Root
43 | .ItemWithPath(path)
44 | .Request()
45 | .GetAsync(token);
46 | }
47 |
48 | public static async Task GetOneDriveContentStreamFromShareAsync(this IGraphServiceClient client, string shareToken, CancellationToken token)
49 | {
50 | return await client
51 | .Shares[shareToken]
52 | .Root
53 | .Content
54 | .Request()
55 | .GetAsync(token);
56 | }
57 |
58 | ///
59 | /// Uploads new OneDrive Item OR updates existing OneDrive Item
60 | ///
61 | ///
62 | ///
63 | /// Stream of input to be uploaded
64 | /// Drive item representing newly added/updated item
65 | public static async Task UploadOneDriveItemAsync(this IGraphServiceClient client, string path, Stream fileStream, CancellationToken token)
66 | {
67 | fileStream.Position = 0;
68 | DriveItem result = await client
69 | .Me
70 | .Drive
71 | .Root
72 | .ItemWithPath(path)
73 | .Content
74 | .Request()
75 | .PutAsync(fileStream, token);
76 | return result;
77 | }
78 |
79 | class GetRootModel
80 | {
81 | [JsonProperty("@microsoft.graph.downloadUrl")]
82 | public string DownloadUrl { get; set; }
83 |
84 | public string name { get; set; }
85 |
86 | public int size { get; set; }
87 | }
88 | }
89 | }
--------------------------------------------------------------------------------
/src/MicrosoftGraphBinding/Services/OneDriveService.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | using System.IO;
5 | using System.Threading;
6 | using System.Threading.Tasks;
7 | using Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Config;
8 | using Microsoft.Graph;
9 |
10 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Services
11 | {
12 | internal class OneDriveService
13 | {
14 | private GraphServiceClientManager _clientProvider;
15 |
16 | public OneDriveService(GraphServiceClientManager clientProvider)
17 | {
18 | _clientProvider = clientProvider;
19 | }
20 |
21 | ///
22 | /// Retrieve contents of OneDrive file
23 | ///
24 | ///
25 | ///
26 | ///
27 | ///
28 | public async Task GetOneDriveContentsAsByteArrayAsync(OneDriveAttribute attr, CancellationToken token)
29 | {
30 | var response = await GetOneDriveContentsAsStreamAsync(attr, token);
31 |
32 | using (MemoryStream ms = new MemoryStream())
33 | {
34 | await response.CopyToAsync(ms);
35 | return ms.ToArray();
36 | }
37 | }
38 |
39 | public Stream ConvertStream(Stream stream, OneDriveAttribute attribute, IGraphServiceClient client)
40 | {
41 | if (attribute.Access != FileAccess.Write)
42 | {
43 | return stream;
44 | }
45 | return new OneDriveWriteStream(client, attribute.Path);
46 | }
47 |
48 | public async Task GetOneDriveContentsAsStreamAsync(OneDriveAttribute attr, CancellationToken token)
49 | {
50 | IGraphServiceClient client = await _clientProvider.GetMSGraphClientFromTokenAttributeAsync(attr, token);
51 | Stream response;
52 | // How to download from OneDrive: https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/api/item_downloadcontent
53 | // GET https://graph.microsoft.com/v1.0/me/drive/root:/test1/hi.txt:/content HTTP/1.1
54 | bool isShare = attr.Path.StartsWith("https://");
55 | if (isShare)
56 | {
57 | // TODO: Move this to GraphServiceClient
58 |
59 | // Download via a Share URL
60 | var shareToken = UrlToSharingToken(attr.Path);
61 | response = await client.GetOneDriveContentStreamFromShareAsync(shareToken, token);
62 | }
63 | else
64 | {
65 | try
66 | {
67 | // Retrieve stream of OneDrive item
68 | response = await client.GetOneDriveContentStreamAsync(attr.Path, token);
69 | } catch
70 | {
71 | //File does not exist, so create new memory stream
72 | response = new MemoryStream();
73 | }
74 |
75 |
76 | }
77 |
78 | return ConvertStream(response, attr, client);
79 | }
80 |
81 | public async Task GetOneDriveItemAsync(OneDriveAttribute attr, CancellationToken token)
82 | {
83 | IGraphServiceClient client = await _clientProvider.GetMSGraphClientFromTokenAttributeAsync(attr, token);
84 | return await client.GetOneDriveItemAsync(attr.Path, token);
85 | }
86 |
87 | public async Task UploadOneDriveContentsAsync(OneDriveAttribute attr, Stream fileStream, CancellationToken token)
88 | {
89 | IGraphServiceClient client = await _clientProvider.GetMSGraphClientFromTokenAttributeAsync(attr, token);
90 | return await client.UploadOneDriveItemAsync(attr.Path, fileStream, token);
91 | }
92 |
93 | // https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/api/shares_get#transform-a-sharing-url
94 | private static string UrlToSharingToken(string inputUrl)
95 | {
96 | var base64Value = System.Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(inputUrl));
97 | return "u!" + base64Value.TrimEnd('=').Replace('/', '_').Replace('+', '-');
98 | }
99 |
100 |
101 | public static Stream CreateStream(byte[] byteArray)
102 | {
103 | return new MemoryStream(byteArray);
104 | }
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/MicrosoftGraphBinding/Services/OutlookClient.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Services
5 | {
6 | using System.Threading;
7 | using System.Threading.Tasks;
8 | using Microsoft.Graph;
9 |
10 | internal static class OutlookClient
11 | {
12 | ///
13 | /// Send an email with a dynamically set body
14 | ///
15 | /// GraphServiceClient used to send request
16 | /// Async task for posted message
17 | public static async Task SendMessageAsync(this IGraphServiceClient client, Message msg, CancellationToken token)
18 | {
19 | // Send message & save to sent items folder
20 | await client.Me.SendMail(msg, true).Request().PostAsync(token);
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/MicrosoftGraphBinding/Services/OutlookService.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | using System.Threading;
5 | using System.Threading.Tasks;
6 | using Microsoft.Graph;
7 |
8 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Services
9 | {
10 | internal class OutlookService
11 | {
12 | private GraphServiceClientManager _clientManager;
13 |
14 | public OutlookService(GraphServiceClientManager clientManager)
15 | {
16 | _clientManager = clientManager;
17 | }
18 |
19 | public async Task SendMessageAsync(OutlookAttribute attr, Message msg, CancellationToken token)
20 | {
21 | IGraphServiceClient client = await _clientManager.GetMSGraphClientFromTokenAttributeAsync(attr, token);
22 | await client.SendMessageAsync(msg, token);
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/MicrosoftGraphBinding/stylecop.json:
--------------------------------------------------------------------------------
1 | {
2 | // ACTION REQUIRED: This file was automatically added to your project, but it
3 | // will not take effect until additional steps are taken to enable it. See the
4 | // following page for additional information:
5 | //
6 | // https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/EnableConfiguration.md
7 |
8 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json",
9 | "settings": {
10 | "documentationRules": {
11 | "companyName": "Microsoft"
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/TokenBinding/AadClient.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | namespace Microsoft.Azure.WebJobs.Extensions.AuthTokens
5 | {
6 | using System;
7 | using System.Threading.Tasks;
8 | using Microsoft.Azure.Services.AppAuthentication;
9 | using Microsoft.Extensions.Options;
10 | using Microsoft.IdentityModel.Clients.ActiveDirectory;
11 |
12 | ///
13 | /// Helper class for calling onto ADAL
14 | ///
15 | internal class AadClient : IAadClient
16 | {
17 | private AuthenticationContext _authContext;
18 | private ClientCredential _clientCredentials;
19 | private TokenOptions _options;
20 |
21 | private AuthenticationContext AuthContext {
22 | get
23 | {
24 | if (_authContext == null)
25 | {
26 | // NOTE: We had to turn off authority validation here, otherwise we would
27 | // get the error "AADSTS50049: Unknown or invalid instance" for some tenants.
28 | _authContext = new AuthenticationContext(_options.TenantUrl, false);
29 | }
30 | return _authContext;
31 | }
32 | }
33 |
34 | private ClientCredential ClientCredentials
35 | {
36 | get
37 | {
38 | if (_clientCredentials == null)
39 | {
40 | if (_options.ClientId == null || _options.ClientSecret == null)
41 | {
42 | throw new InvalidOperationException($"The Application Settings {Constants.ClientIdName} and {Constants.ClientSecretName} must be set to perform this operation.");
43 | }
44 | _clientCredentials = new ClientCredential(_options.ClientId, _options.ClientSecret);
45 | }
46 |
47 | return _clientCredentials;
48 | }
49 | }
50 |
51 | public AadClient(IOptions options)
52 | {
53 | _options = options.Value;
54 | }
55 |
56 | ///
57 | /// Use client credentials to retrieve auth token
58 | /// Typically used to retrieve a token for a different audience
59 | ///
60 | /// User's token for a given resource
61 | /// Resource the token is for (e.g. https://graph.microsoft.com)
62 | /// Access token for correct audience
63 | public async Task GetTokenOnBehalfOfUserAsync(
64 | string userToken,
65 | string resource)
66 | {
67 | if (string.IsNullOrEmpty(userToken))
68 | {
69 | throw new ArgumentException("A usertoken is required to retrieve a token for a user.");
70 | }
71 |
72 | if (string.IsNullOrEmpty(resource))
73 | {
74 | throw new ArgumentException("A resource is required to retrieve a token for a user.");
75 | }
76 |
77 | UserAssertion userAssertion = new UserAssertion(userToken);
78 | AuthenticationResult ar = await AuthContext.AcquireTokenAsync(resource, ClientCredentials, userAssertion);
79 | return ar.AccessToken;
80 | }
81 |
82 | public async Task GetTokenFromClientCredentials(string resource)
83 | {
84 | if (string.IsNullOrEmpty(resource))
85 | {
86 | throw new ArgumentException("A resource is required to retrieve a token from client credentials.");
87 | }
88 |
89 | AuthenticationResult authResult = await AuthContext.AcquireTokenAsync(resource, ClientCredentials);
90 | return authResult.AccessToken;
91 | }
92 |
93 | public async Task GetTokenFromAppIdentity(string resource, string connectionString)
94 | {
95 | if (string.IsNullOrEmpty(resource))
96 | {
97 | throw new ArgumentException("A resource is required to retrieve a token via the application's managed identity.");
98 | }
99 |
100 | var azureServiceTokenProvider = new AzureServiceTokenProvider(connectionString);
101 |
102 | var token = await azureServiceTokenProvider.GetAccessTokenAsync(resource);
103 |
104 | return token;
105 | }
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/src/TokenBinding/AuthTokenExtensionConfigProvider.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 | using System.Runtime.CompilerServices;
4 |
5 | [assembly: InternalsVisibleTo("Microsoft.Azure.WebJobs.Extensions.Token.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100cd1dabd5a893b40e75dc901fe7293db4a3caf9cd4d3e3ed6178d49cd476969abe74a9e0b7f4a0bb15edca48758155d35a4f05e6e852fff1b319d103b39ba04acbadd278c2753627c95e1f6f6582425374b92f51cca3deb0d2aab9de3ecda7753900a31f70a236f163006beefffe282888f85e3c76d1205ec7dfef7fa472a17b1")]
6 | [assembly: InternalsVisibleTo("Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100cd1dabd5a893b40e75dc901fe7293db4a3caf9cd4d3e3ed6178d49cd476969abe74a9e0b7f4a0bb15edca48758155d35a4f05e6e852fff1b319d103b39ba04acbadd278c2753627c95e1f6f6582425374b92f51cca3deb0d2aab9de3ecda7753900a31f70a236f163006beefffe282888f85e3c76d1205ec7dfef7fa472a17b1")]
7 | [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100cd1dabd5a893b40e75dc901fe7293db4a3caf9cd4d3e3ed6178d49cd476969abe74a9e0b7f4a0bb15edca48758155d35a4f05e6e852fff1b319d103b39ba04acbadd278c2753627c95e1f6f6582425374b92f51cca3deb0d2aab9de3ecda7753900a31f70a236f163006beefffe282888f85e3c76d1205ec7dfef7fa472a17b1")]
8 | namespace Microsoft.Azure.WebJobs.Extensions.AuthTokens
9 | {
10 | using Microsoft.Azure.WebJobs;
11 | using Microsoft.Azure.WebJobs.Host.Config;
12 | using Microsoft.Extensions.Options;
13 |
14 | ///
15 | /// WebJobs SDK Extension for Token binding.
16 | ///
17 | internal class AuthTokenExtensionConfigProvider : IExtensionConfigProvider
18 | {
19 | private TokenConverter _converter;
20 | private TokenOptions _options;
21 |
22 | public AuthTokenExtensionConfigProvider(IOptions options, IAadClient aadClient, IEasyAuthClient easyAuthClient, INameResolver appSettings)
23 | {
24 | _options = options.Value;
25 | _options.SetAppSettings(appSettings);
26 | _converter = new TokenConverter(options, easyAuthClient, aadClient);
27 | }
28 |
29 | //TODO: https://github.com/Azure/azure-functions-microsoftgraph-extension/issues/48
30 | internal static string CreateBindingCategory(string bindingName)
31 | {
32 | return $"Host.Bindings.{bindingName}";
33 | }
34 |
35 | ///
36 | /// Initialize the binding extension
37 | ///
38 | /// Context for extension
39 | public void Initialize(ExtensionConfigContext context)
40 | {
41 | var tokenRule = context.AddBindingRule();
42 | tokenRule.BindToInput(_converter);
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/TokenBinding/AuthTokenWebJobsBuilderExtensions.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | namespace Microsoft.Azure.WebJobs.Extensions.AuthTokens
5 | {
6 |
7 | using System;
8 | using Microsoft.Extensions.DependencyInjection;
9 |
10 | public static class AuthTokenWebJobsBuilderExtensions
11 | {
12 | public static IWebJobsBuilder AddAuthToken(this IWebJobsBuilder builder)
13 | {
14 | if (builder == null)
15 | {
16 | throw new ArgumentNullException(nameof(builder));
17 | }
18 |
19 | builder.AddExtension()
20 | .BindOptions()
21 | .Services
22 | .AddSingleton()
23 | .AddSingleton();
24 | return builder;
25 | }
26 |
27 | public static IWebJobsBuilder AddAuthTokenForTests(this IWebJobsBuilder builder)
28 | {
29 | if (builder == null)
30 | {
31 | throw new ArgumentNullException(nameof(builder));
32 | }
33 |
34 | builder.AddExtension();
35 | return builder;
36 | }
37 |
38 | public static IServiceCollection AddAuthTokenServices(this IServiceCollection services)
39 | {
40 | services.AddSingleton()
41 | .AddSingleton();
42 | return services;
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/TokenBinding/AuthTokenWebJobsStartup.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | using Microsoft.Azure.WebJobs.Extensions.AuthTokens;
5 | using Microsoft.Azure.WebJobs.Hosting;
6 |
7 | [assembly: WebJobsStartup(typeof(AuthTokenWebJobsStartup))]
8 | namespace Microsoft.Azure.WebJobs.Extensions.AuthTokens
9 | {
10 | public class AuthTokenWebJobsStartup : IWebJobsStartup
11 | {
12 | public void Configure(IWebJobsBuilder builder)
13 | {
14 | builder.AddAuthToken();
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/TokenBinding/Constants.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | namespace Microsoft.Azure.WebJobs.Extensions.AuthTokens
5 | {
6 | internal static class Constants
7 | {
8 | public const string ClientIdName = "WEBSITE_AUTH_CLIENT_ID";
9 |
10 | public const string ClientSecretName = "WEBSITE_AUTH_CLIENT_SECRET";
11 |
12 | public const string WebsiteHostname = "WEBSITE_HOSTNAME";
13 |
14 | public const string WebsiteAuthSigningKey = "WEBSITE_AUTH_SIGNING_KEY";
15 |
16 | public const string WebsiteAuthOpenIdIssuer = "WEBSITE_AUTH_OPENID_ISSUER";
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/TokenBinding/EasyAuthTokenClient.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | namespace Microsoft.Azure.WebJobs.Extensions.AuthTokens
5 | {
6 | using System;
7 | using System.IdentityModel.Tokens.Jwt;
8 | using System.Net;
9 | using System.Net.Http;
10 | using System.Threading.Tasks;
11 | using Microsoft.Extensions.Logging;
12 | using Microsoft.Extensions.Options;
13 | using Newtonsoft.Json;
14 |
15 | ///
16 | /// The client responsible for handling all EasyAuth token-related tasks.
17 | ///
18 | internal class EasyAuthTokenClient : IEasyAuthClient
19 | {
20 | private static readonly HttpClient _httpClient = new HttpClient();
21 | private readonly ILogger _log;
22 | private readonly TokenOptions _options;
23 |
24 | ///
25 | /// Initializes a new instance of the class.
26 | ///
27 | /// The hostname of the webapp
28 | /// The website authorization signing key
29 | public EasyAuthTokenClient(IOptions options, ILoggerFactory loggerFactory)
30 | {
31 | _log = loggerFactory.CreateLogger(AuthTokenExtensionConfigProvider.CreateBindingCategory("AuthToken"));
32 | _options = options.Value;
33 | }
34 |
35 | public async Task GetTokenStoreEntry(JwtSecurityToken jwt, TokenBaseAttribute attribute)
36 | {
37 | // Send the token to the local /.auth/me endpoint and return the JSON
38 | string meUrl = $"https://{_options.HostName}/.auth/me?provider={attribute.IdentityProvider}";
39 |
40 | using (var request = new HttpRequestMessage(HttpMethod.Get, meUrl))
41 | {
42 | request.Headers.Add("x-zumo-auth", jwt.RawData);
43 | _log.LogTrace($"Fetching user token data from {meUrl}");
44 | using (HttpResponseMessage response = await _httpClient.SendAsync(request))
45 | {
46 | _log.LogTrace($"Response from '${meUrl}: {response.StatusCode}");
47 | if (!response.IsSuccessStatusCode)
48 | {
49 | string errorResponse = await response.Content.ReadAsStringAsync();
50 | throw new InvalidOperationException($"Request to {meUrl} failed. Status Code: {response.StatusCode}; Body: {errorResponse}");
51 | }
52 | var responseString = await response.Content.ReadAsStringAsync();
53 | return JsonConvert.DeserializeObject(responseString);
54 | }
55 | }
56 | }
57 |
58 | public async Task RefreshToken(JwtSecurityToken jwt, TokenBaseAttribute attribute)
59 | {
60 | if (string.IsNullOrEmpty(attribute.Resource))
61 | {
62 | throw new ArgumentException("A resource is required to renew an access token.");
63 | }
64 |
65 | if (string.IsNullOrEmpty(attribute.UserId))
66 | {
67 | throw new ArgumentException("A userId is required to renew an access token.");
68 | }
69 |
70 | if (string.IsNullOrEmpty(attribute.IdentityProvider))
71 | {
72 | throw new ArgumentException("A provider is necessary to renew an access token.");
73 | }
74 |
75 | string refreshUrl = $"https://{_options.HostName}/.auth/refresh?resource=" + WebUtility.UrlEncode(attribute.Resource);
76 |
77 | using (var refreshRequest = new HttpRequestMessage(HttpMethod.Get, refreshUrl))
78 | {
79 | refreshRequest.Headers.Add("x-zumo-auth", jwt.RawData);
80 | _log.LogTrace($"Refreshing ${attribute.IdentityProvider} access token for user ${attribute.UserId} at ${refreshUrl}");
81 | using (HttpResponseMessage response = await _httpClient.SendAsync(refreshRequest))
82 | {
83 | _log.LogTrace($"Response from ${refreshUrl}: {response.StatusCode}");
84 | if (!response.IsSuccessStatusCode)
85 | {
86 | string errorResponse = await response.Content.ReadAsStringAsync();
87 | throw new InvalidOperationException($"Failed to refresh {attribute.UserId} {attribute.IdentityProvider} error={response.StatusCode} {errorResponse}");
88 | }
89 | }
90 | }
91 | }
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/TokenBinding/EasyAuthTokenManager.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | namespace Microsoft.Azure.WebJobs.Extensions.AuthTokens
5 | {
6 | using System;
7 | using System.IdentityModel.Tokens.Jwt;
8 | using System.Security.Claims;
9 | using System.Threading.Tasks;
10 | using Microsoft.Extensions.Options;
11 | using Microsoft.IdentityModel.Tokens;
12 |
13 | ///
14 | /// Class representing an application's [EasyAuth] Token Store
15 | /// see https://cgillum.tech/2016/03/07/app-service-token-store/
16 | ///
17 | internal class EasyAuthTokenManager
18 | {
19 | internal readonly JwtSecurityTokenHandler JwtHandler = new JwtSecurityTokenHandler();
20 | private const int GraphTokenBufferInMinutes = 5;
21 | private const int JwtTokenDurationInMinutes = 15;
22 |
23 | private readonly TokenOptions _options;
24 | private readonly IEasyAuthClient _client;
25 |
26 | ///
27 | /// Initializes a new instance of the class.
28 | ///
29 | /// The hostname of the keystore.
30 | /// The website authorization signing key
31 | public EasyAuthTokenManager(IEasyAuthClient client, IOptions options)
32 | {
33 | _client = client;
34 | _options = options.Value;
35 | }
36 |
37 | ///
38 | /// Retrieve Easy Auth token based on provider & principal ID
39 | ///
40 | /// The metadata for the token to grab
41 | /// Task with Token Store entry of the token
42 | public async Task GetEasyAuthAccessTokenAsync(TokenBaseAttribute attribute)
43 | {
44 | var jwt = CreateTokenForEasyAuthAccess(attribute);
45 | EasyAuthTokenStoreEntry tokenStoreEntry = await _client.GetTokenStoreEntry(jwt, attribute);
46 |
47 | bool isTokenValid = IsTokenValid(tokenStoreEntry.AccessToken);
48 | bool isTokenExpired = tokenStoreEntry.ExpiresOn <= DateTime.UtcNow.AddMinutes(GraphTokenBufferInMinutes);
49 | bool isRefreshable = IsRefreshableProvider(attribute.IdentityProvider);
50 |
51 | if (isRefreshable && (isTokenExpired || !isTokenValid))
52 | {
53 | await _client.RefreshToken(jwt, attribute);
54 |
55 | // Now that the refresh has occured, grab the new token
56 | tokenStoreEntry = await _client.GetTokenStoreEntry(jwt, attribute);
57 | }
58 |
59 | return tokenStoreEntry.AccessToken;
60 | }
61 |
62 | private bool IsTokenValid(string token)
63 | {
64 | return JwtHandler.CanReadToken(token);
65 | }
66 |
67 | private static bool IsRefreshableProvider(string provider)
68 | {
69 | //TODO: For now, since we are focusing on AAD, only include it in the refresh path.
70 | return provider.Equals("AAD", StringComparison.OrdinalIgnoreCase);
71 | }
72 |
73 | private JwtSecurityToken CreateTokenForEasyAuthAccess(TokenBaseAttribute attribute)
74 | {
75 | if (string.IsNullOrEmpty(attribute.UserId))
76 | {
77 | throw new ArgumentException("A userId is required to obtain an access token.");
78 | }
79 |
80 | if (string.IsNullOrEmpty(attribute.IdentityProvider))
81 | {
82 | throw new ArgumentException("A provider is necessary to obtain an access token.");
83 | }
84 |
85 | var identityClaims = new ClaimsIdentity(attribute.UserId);
86 | identityClaims.AddClaim(new Claim(ClaimTypes.NameIdentifier, attribute.UserId));
87 | identityClaims.AddClaim(new Claim("idp", attribute.IdentityProvider));
88 |
89 | var baseUrl = $"https://{_options.HostName}/";
90 | var descr = new SecurityTokenDescriptor
91 | {
92 | Audience = baseUrl,
93 | Issuer = baseUrl,
94 | Subject = identityClaims,
95 | Expires = DateTime.UtcNow.AddMinutes(JwtTokenDurationInMinutes),
96 | SigningCredentials = new HmacSigningCredentials(_options.SigningKey),
97 | };
98 |
99 | return (JwtSecurityToken)JwtHandler.CreateToken(descr);
100 | }
101 |
102 |
103 | public class HmacSigningCredentials : SigningCredentials
104 | {
105 | public HmacSigningCredentials(string base64EncodedKey)
106 | : this(ParseKeyString(base64EncodedKey))
107 | { }
108 |
109 | public HmacSigningCredentials(byte[] key)
110 | : base(new SymmetricSecurityKey(key), CreateSignatureAlgorithm(key))
111 | {
112 | }
113 |
114 | ///
115 | /// Converts a base64 OR hex-encoded string into a byte array.
116 | ///
117 | protected static byte[] ParseKeyString(string keyString)
118 | {
119 | if (string.IsNullOrEmpty(keyString))
120 | {
121 | return new byte[0];
122 | }
123 | else if (IsHexString(keyString))
124 | {
125 | return HexStringToByteArray(keyString);
126 | }
127 | else
128 | {
129 | return Convert.FromBase64String(keyString);
130 | }
131 | }
132 |
133 | protected static bool IsHexString(string input)
134 | {
135 | if (string.IsNullOrEmpty(input))
136 | {
137 | throw new ArgumentNullException(nameof(input));
138 | }
139 |
140 | for (int i = 0; i < input.Length; i++)
141 | {
142 | char c = input[i];
143 | bool isHexChar = (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f');
144 | if (!isHexChar)
145 | {
146 | return false;
147 | }
148 | }
149 |
150 | return true;
151 | }
152 |
153 | protected static byte[] HexStringToByteArray(string hexString)
154 | {
155 | byte[] bytes = new byte[hexString.Length / 2];
156 | for (int i = 0; i < hexString.Length; i += 2)
157 | {
158 | bytes[i / 2] = Convert.ToByte(hexString.Substring(i, 2), 16);
159 | }
160 |
161 | return bytes;
162 | }
163 |
164 | protected static string CreateSignatureAlgorithm(byte[] key)
165 | {
166 | if (key.Length <= 32)
167 | {
168 | return Algorithms.HmacSha256Signature;
169 | }
170 | else if (key.Length <= 48)
171 | {
172 | return Algorithms.HmacSha384Signature;
173 | }
174 | else
175 | {
176 | return Algorithms.HmacSha512Signature;
177 | }
178 | }
179 |
180 | ///
181 | /// Key value pairs (algorithm name, w3.org link)
182 | ///
183 | protected static class Algorithms
184 | {
185 | public const string HmacSha256Signature = "http://www.w3.org/2001/04/xmldsig-more#hmac-sha256";
186 | public const string HmacSha384Signature = "http://www.w3.org/2001/04/xmldsig-more#hmac-sha384";
187 | public const string HmacSha512Signature = "http://www.w3.org/2001/04/xmldsig-more#hmac-sha512";
188 | }
189 | }
190 | }
191 | }
192 |
--------------------------------------------------------------------------------
/src/TokenBinding/EasyAuthTokenStoreEntry.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 | namespace Microsoft.Azure.WebJobs.Extensions.AuthTokens
4 | {
5 | using System;
6 | using System.Runtime.Serialization;
7 |
8 | ///
9 | /// Class representing a single entry in an application's [EasyAuth] Token Store
10 | /// Names of the fields match the names of the fields returned by the /.auth/me endpoint
11 | ///
12 | [DataContract]
13 | public class EasyAuthTokenStoreEntry
14 | {
15 | [DataMember(Name = "access_token", EmitDefaultValue = false)]
16 | public string AccessToken { get; set; }
17 |
18 | [DataMember(Name = "id_token", EmitDefaultValue = false)]
19 | public string IdToken { get; set; }
20 |
21 | [DataMember(Name = "refresh_token", EmitDefaultValue = false)]
22 | public string RefreshToken { get; set; }
23 |
24 | [DataMember(Name = "provider_name")]
25 | public string ProviderName { get; set; }
26 |
27 | [DataMember(Name = "user_id")]
28 | public string UserId { get; set; }
29 |
30 | [DataMember(Name = "expires_on", EmitDefaultValue = false)]
31 | public DateTime ExpiresOn { get; set; }
32 |
33 | [DataContract]
34 | public class Claim
35 | {
36 | [DataMember(Name = "typ")]
37 | public string Type { get; set; }
38 |
39 | [DataMember(Name = "val")]
40 | public string Value { get; set; }
41 | }
42 |
43 | [DataMember(Name = "user_claims", EmitDefaultValue = false)]
44 | public Claim[] UserClaims { get; set; }
45 | }
46 | }
--------------------------------------------------------------------------------
/src/TokenBinding/IAadClient.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | namespace Microsoft.Azure.WebJobs.Extensions.AuthTokens
5 | {
6 | using System.Threading.Tasks;
7 |
8 | public interface IAadClient
9 | {
10 | Task GetTokenOnBehalfOfUserAsync(string userToken, string resource);
11 |
12 | Task GetTokenFromClientCredentials(string resource);
13 |
14 | Task GetTokenFromAppIdentity(string resource, string connectionString);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/TokenBinding/IEasyAuthClient.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | namespace Microsoft.Azure.WebJobs.Extensions.AuthTokens
5 | {
6 | using System.IdentityModel.Tokens.Jwt;
7 | using System.Threading.Tasks;
8 |
9 | public interface IEasyAuthClient
10 | {
11 | Task GetTokenStoreEntry(JwtSecurityToken jwt, TokenBaseAttribute attribute);
12 |
13 | Task RefreshToken(JwtSecurityToken jwt, TokenBaseAttribute attribute);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/TokenBinding/TokenAttribute.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | namespace Microsoft.Azure.WebJobs
5 | {
6 | using Microsoft.Azure.WebJobs.Description;
7 |
8 | [Binding]
9 | public sealed class TokenAttribute : TokenBaseAttribute
10 | {
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/TokenBinding/TokenBaseAttribute.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | namespace Microsoft.Azure.WebJobs
5 | {
6 | using System;
7 | using Microsoft.Azure.WebJobs.Description;
8 |
9 | public abstract class TokenBaseAttribute : Attribute
10 | {
11 | private TokenIdentityMode _identity;
12 |
13 | ///
14 | /// Gets or sets a resource for a token exchange. Optional
15 | ///
16 | public string Resource { get; set; }
17 |
18 | ///
19 | /// Gets or sets an identity provider for the token exchange. Optional
20 | ///
21 | public string IdentityProvider { get; set; }
22 |
23 | ///
24 | /// Gets or sets token to grab on-behalf-of. Required if Identity="userFromToken".
25 | ///
26 | [AutoResolve]
27 | public string UserToken { get; set; }
28 |
29 | ///
30 | /// Gets or sets user id to grab token for. Required if Identity="userFromId".
31 | ///
32 | [AutoResolve]
33 | public string UserId { get; set; }
34 |
35 | ///
36 | /// Gets or sets connection string to use for an application's managed identity. Optional
37 | ///
38 | [AutoResolve]
39 | public string IdentityConnectionString { get; set; }
40 |
41 | ///
42 | /// Gets or sets how to determine identity. Required.
43 | ///
44 | public TokenIdentityMode Identity
45 | {
46 | get
47 | {
48 | return _identity;
49 | }
50 |
51 | set
52 | {
53 | if (value == TokenIdentityMode.UserFromRequest)
54 | {
55 | _identity = TokenIdentityMode.UserFromToken;
56 | this.UserToken = "{headers.X-MS-TOKEN-AAD-ID-TOKEN}";
57 | }
58 | else
59 | {
60 | _identity = value;
61 | }
62 | }
63 | }
64 |
65 | public void CheckValidity()
66 | {
67 | switch (this.Identity)
68 | {
69 | case TokenIdentityMode.ClientCredentials:
70 | break;
71 | case TokenIdentityMode.UserFromId:
72 | if (string.IsNullOrWhiteSpace(this.UserId))
73 | {
74 | throw new FormatException("A token attribute with identity=userFromId requires a userId");
75 | }
76 |
77 | break;
78 | case TokenIdentityMode.UserFromToken:
79 | if (string.IsNullOrWhiteSpace(this.UserToken))
80 | {
81 | throw new FormatException("A token attribute with identity=userFromToken requires a userToken");
82 | }
83 |
84 | break;
85 | }
86 | }
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/TokenBinding/TokenBinding.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 | Microsoft.Azure.WebJobs.Extensions.Tokens
6 | Microsoft.Azure.WebJobs.Extensions.AuthTokens
7 |
8 |
9 | Microsoft.Azure.WebJobs.Extensions.AuthTokens
10 | This extension adds bindings for AuthTokens
11 | 1
12 | 0
13 | 0
14 | beta6
15 | $(MajorVersion).0.0.0
16 | $(MajorVersion).$(MinorVersion).$(PatchVersion)-$(BetaVersion)$(VersionSuffix)
17 | N/A
18 | $(Version) Commit hash: $(CommitHash)
19 |
20 | Microsoft
21 | Microsoft
22 | © .NET Foundation. All rights reserved.
23 | https://go.microsoft.com/fwlink/?linkid=2028464
24 |
25 | git
26 | https://github.com/Azure/azure-functions-microsoftgraph-extension
27 | https://github.com/Azure/azure-functions-microsoftgraph-extension
28 | true
29 | true
30 | ../../sign.snk
31 |
32 |
33 |
34 | Full
35 | true
36 |
37 |
38 |
39 |
40 |
41 |
42 | All
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/src/TokenBinding/TokenConverter.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | namespace Microsoft.Azure.WebJobs.Extensions.AuthTokens
5 | {
6 | using System;
7 | using System.IdentityModel.Tokens.Jwt;
8 | using System.Linq;
9 | using System.Threading;
10 | using System.Threading.Tasks;
11 | using Microsoft.Extensions.Options;
12 |
13 | public class TokenConverter : IAsyncConverter, IAsyncConverter
14 | {
15 | private IOptions _options;
16 | private IAadClient _aadManager;
17 | private IEasyAuthClient _easyAuthClient;
18 |
19 | ///
20 | /// Initializes a new instance of the class.
21 | ///
22 | /// TokenExtensionConfig containing necessary context & methods
23 | public TokenConverter(IOptions options, IEasyAuthClient easyAuthClient, IAadClient aadClient)
24 | {
25 | _options = options;
26 | _easyAuthClient = easyAuthClient;
27 | _aadManager = aadClient;
28 | }
29 |
30 | public async Task ConvertAsync(TokenBaseAttribute attribute, CancellationToken cancellationToken)
31 | {
32 | attribute.CheckValidity();
33 | switch (attribute.Identity)
34 | {
35 | case TokenIdentityMode.UserFromId:
36 | // If the attribute has no identity provider, assume AAD
37 | attribute.IdentityProvider = attribute.IdentityProvider ?? "AAD";
38 | var easyAuthTokenManager = new EasyAuthTokenManager(_easyAuthClient, _options);
39 | return await easyAuthTokenManager.GetEasyAuthAccessTokenAsync(attribute);
40 | case TokenIdentityMode.UserFromToken:
41 | return await GetAuthTokenFromUserToken(attribute.UserToken, attribute.Resource);
42 | case TokenIdentityMode.ClientCredentials:
43 | return await _aadManager.GetTokenFromClientCredentials(attribute.Resource);
44 | case TokenIdentityMode.AppIdentity:
45 | return await _aadManager.GetTokenFromAppIdentity(attribute.Resource, attribute.IdentityConnectionString);
46 | }
47 |
48 | throw new InvalidOperationException("Unable to authorize without Principal ID or ID Token.");
49 | }
50 |
51 | public async Task ConvertAsync(TokenAttribute attribute, CancellationToken cancellationToken)
52 | {
53 | return await ConvertAsync(attribute as TokenBaseAttribute, cancellationToken);
54 | }
55 |
56 | private async Task GetAuthTokenFromUserToken(string userToken, string resource)
57 | {
58 | if (string.IsNullOrWhiteSpace(resource))
59 | {
60 | throw new ArgumentException("A resource is required to get an auth token on behalf of a user.");
61 | }
62 |
63 | // If the incoming token already has the correct audience (resource), then skip the exchange (it will fail with AADSTS50013!)
64 | var currentAudience = GetAudience(userToken);
65 | if (currentAudience != resource)
66 | {
67 | string token = await _aadManager.GetTokenOnBehalfOfUserAsync(
68 | userToken,
69 | resource);
70 | return token;
71 | }
72 |
73 | // No exchange requested, return token directly.
74 | return userToken;
75 | }
76 |
77 | private string GetAudience(string rawToken)
78 | {
79 | var jwt = new JwtSecurityToken(rawToken);
80 | var audience = jwt.Audiences.FirstOrDefault();
81 | return audience;
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/TokenBinding/TokenIdentityMode.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | namespace Microsoft.Azure.WebJobs
5 | {
6 | ///
7 | /// How the binding should grab the access token
8 | ///
9 | public enum TokenIdentityMode
10 | {
11 | /// Obtains the access token on behalf of the user whose token is in the userToken field of metadata.
12 | UserFromToken,
13 | /// Same as with the user token taken from the X-MS-TOKEN-AAD-ID-TOKEN header. Only works for HttpTrigger
14 | UserFromRequest, //This cannot be the default value, as it requires binding expressions that could cause errors.
15 | /// Obtains the access token for the user with the id found in the userId field of metadata.
16 | UserFromId,
17 | /// Obtains the access token for the client credentials found in the application settings.
18 | ClientCredentials,
19 | /// Obtains an access token for the application using the application's identity.
20 | AppIdentity
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/TokenBinding/TokenOptions.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | namespace Microsoft.Azure.WebJobs.Extensions.AuthTokens
5 | {
6 | public class TokenOptions
7 | {
8 | ///
9 | /// The website hostname
10 | ///
11 | public string HostName { get; set; }
12 |
13 | ///
14 | /// Application setting key for the client ID
15 | ///
16 | public string ClientId { get; set; }
17 |
18 | ///
19 | /// The client secret used with AAD
20 | ///
21 | public string ClientSecret { get; set; }
22 |
23 | ///
24 | /// Base tenant URL for AAD
25 | ///
26 | public string TenantUrl { get; set; }
27 |
28 | ///
29 | /// The signing key used for EasyAuth tokens
30 | ///
31 | public string SigningKey { get; set; }
32 |
33 | ///
34 | /// The default base url to gr.
35 | ///
36 | public string DefaultEnvironmentBaseUrl { get; set; } = "https://login.windows.net/";
37 |
38 | ///
39 | /// The default tenant to grab the token for
40 | ///
41 | public string DefaultTenantId { get; set; } = "common";
42 |
43 | public virtual void SetAppSettings(INameResolver appSettings)
44 | {
45 | if (HostName == null)
46 | {
47 | HostName = appSettings.Resolve(Constants.WebsiteHostname);
48 | }
49 | if (ClientId == null)
50 | {
51 | ClientId = appSettings.Resolve(Constants.ClientIdName);
52 | }
53 | if (ClientSecret == null)
54 | {
55 | ClientSecret = appSettings.Resolve(Constants.ClientSecretName);
56 | }
57 | if (TenantUrl == null)
58 | {
59 | TenantUrl = appSettings.Resolve(Constants.WebsiteAuthOpenIdIssuer);
60 | }
61 | if (SigningKey == null)
62 | {
63 | SigningKey = appSettings.Resolve(Constants.WebsiteAuthSigningKey);
64 | }
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/test/MicrosoftGraph.Tests/ExcelMockUtilities.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Tests
5 | {
6 | using System;
7 | using System.Linq.Expressions;
8 | using System.Threading;
9 | using System.Threading.Tasks;
10 | using Microsoft.Graph;
11 | using Moq;
12 | using Newtonsoft.Json.Linq;
13 |
14 | public static class ExcelMockUtilities
15 | {
16 | public static void MockGetTableWorkbookAsync(this Mock mock, WorkbookTable returnValue)
17 | {
18 | mock.Setup(client => client
19 | .Me
20 | .Drive
21 | .Root
22 | .ItemWithPath(It.IsAny())
23 | .Workbook
24 | .Tables[It.IsAny()]
25 | .Request()
26 | .GetAsync(It.IsAny())).Returns(Task.FromResult(returnValue));
27 | }
28 |
29 | public static void MockGetTableWorkbookRangeAsync(this Mock mock, WorkbookRange returnValue)
30 | {
31 | mock.Setup(client => client
32 | .Me
33 | .Drive
34 | .Root
35 | .ItemWithPath(It.IsAny())
36 | .Workbook
37 | .Tables[It.IsAny()]
38 | .Range()
39 | .Request(null)
40 | .GetAsync(It.IsAny())).Returns(Task.FromResult(returnValue));
41 | }
42 |
43 | public static void MockGetWorksheetWorkbookAsync(this Mock mock, WorkbookRange returnValue)
44 | {
45 | mock.Setup(client => client
46 | .Me
47 | .Drive
48 | .Root
49 | .ItemWithPath(It.IsAny())
50 | .Workbook
51 | .Worksheets[It.IsAny()]
52 | .UsedRange()
53 | .Request(null)
54 | .GetAsync(It.IsAny())).Returns(Task.FromResult(returnValue));
55 | }
56 |
57 | public static void MockGetWorkSheetWorkbookInRangeAsync(this Mock mock, WorkbookRange returnValue)
58 | {
59 | mock.Setup(client => client
60 | .Me
61 | .Drive
62 | .Root
63 | .ItemWithPath(It.IsAny())
64 | .Workbook
65 | .Worksheets[It.IsAny()]
66 | .Range(It.IsAny())
67 | .Request(null)
68 | .GetAsync(It.IsAny())).Returns(Task.FromResult(returnValue));
69 | }
70 |
71 | public static void MockGetTableHeaderRowAsync(this Mock mock, WorkbookRange returnValue)
72 | {
73 | mock.Setup(client => client
74 | .Me
75 | .Drive
76 | .Root
77 | .ItemWithPath(It.IsAny())
78 | .Workbook
79 | .Tables[It.IsAny()]
80 | .HeaderRowRange()
81 | .Request(null)
82 | .GetAsync(It.IsAny())).Returns(Task.FromResult(returnValue));
83 | }
84 |
85 | public static void MockPostTableRowAsyc(this Mock mock, WorkbookTableRow returnValue)
86 | {
87 | mock.Setup(client => client
88 | .Me
89 | .Drive
90 | .Root
91 | .ItemWithPath(It.IsAny())
92 | .Workbook
93 | .Tables[It.IsAny()]
94 | .Rows
95 | .Add(null, It.IsAny())
96 | .Request(null)
97 | .PostAsync(It.IsAny())).Returns(Task.FromResult(returnValue));
98 | }
99 |
100 | public static void VerifyPostTableRowAsync(this Mock mock, string path, string tableName, Expression> rowCondition)
101 | {
102 | //first verify PostAsync() called
103 | mock.Verify(client => client
104 | .Me
105 | .Drive
106 | .Root
107 | .ItemWithPath(path)
108 | .Workbook
109 | .Tables[tableName]
110 | .Rows
111 | .Add(null, It.IsAny())
112 | .Request(null)
113 | .PostAsync(It.IsAny()));
114 |
115 | //Now verify row condition is true
116 | mock.Verify(client => client
117 | .Me
118 | .Drive
119 | .Root
120 | .ItemWithPath(path)
121 | .Workbook
122 | .Tables[tableName]
123 | .Rows
124 | .Add(null, It.Is(rowCondition)));
125 | }
126 |
127 | public static void MockPatchWorksheetAsync(this Mock mock, WorkbookRange returnValue)
128 | {
129 | mock.Setup(client => client
130 | .Me
131 | .Drive
132 | .Root
133 | .ItemWithPath(It.IsAny())
134 | .Workbook
135 | .Worksheets[It.IsAny()]
136 | .Range(It.IsAny())
137 | .Request(null)
138 | .PatchAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(returnValue));
139 | }
140 |
141 | public static void VerifyPatchWorksheetAsync(this Mock mock, string path, string worksheetName, string range, Expression> newWorkbookCondition)
142 | {
143 | mock.Verify(client => client
144 | .Me
145 | .Drive
146 | .Root
147 | .ItemWithPath(path)
148 | .Workbook
149 | .Worksheets[worksheetName]
150 | .Range(range)
151 | .Request(null)
152 | .PatchAsync(It.Is(newWorkbookCondition), It.IsAny()));
153 | }
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/test/MicrosoftGraph.Tests/MemorySubscriptionStore.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Tests
5 | {
6 | using System.Collections.Generic;
7 | using System.Linq;
8 | using System.Threading.Tasks;
9 | using Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Config;
10 | using Microsoft.Graph;
11 |
12 | class MemorySubscriptionStore : IGraphSubscriptionStore
13 | {
14 | //The key is the subscription id of the subscription entry
15 | private IDictionary _map = new Dictionary();
16 |
17 | public async Task DeleteAsync(string subscriptionId)
18 | {
19 | if (_map.ContainsKey(subscriptionId))
20 | {
21 | _map.Remove(subscriptionId);
22 | }
23 | }
24 |
25 | public async Task GetAllSubscriptionsAsync()
26 | {
27 | return _map.Values.ToArray();
28 | }
29 |
30 | public async Task GetSubscriptionEntryAsync(string subscriptionId)
31 | {
32 | SubscriptionEntry value = null;
33 | _map.TryGetValue(subscriptionId, out value);
34 | return value;
35 | }
36 |
37 | public async Task SaveSubscriptionEntryAsync(Subscription subscription, string userId)
38 | {
39 | _map[subscription.Id] = new SubscriptionEntry()
40 | {
41 | Subscription = subscription,
42 | UserId = userId,
43 | };
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/test/MicrosoftGraph.Tests/MicrosoftGraph.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp2.0
5 | Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Tests
6 | Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Tests
7 | true
8 | ../../sign.snk
9 |
10 |
11 |
12 | full
13 | true
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/test/MicrosoftGraph.Tests/MockGraphServiceClientProvider.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Tests
5 | {
6 | using Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Config;
7 | using Microsoft.Graph;
8 |
9 | class MockGraphServiceClientProvider : IGraphServiceClientProvider
10 | {
11 | private IGraphServiceClient _client;
12 |
13 | public MockGraphServiceClientProvider(IGraphServiceClient client)
14 | {
15 | _client = client;
16 | }
17 |
18 | public IGraphServiceClient CreateNewGraphServiceClient(string token)
19 | {
20 | return _client;
21 | }
22 |
23 |
24 | public void UpdateGraphServiceClientAuthToken(IGraphServiceClient client, string token)
25 | {
26 | //NO-OP
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/test/MicrosoftGraph.Tests/MockTokenConverter.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Tests
5 | {
6 | using System;
7 | using System.IdentityModel.Tokens.Jwt;
8 | using System.Security.Claims;
9 | using System.Text;
10 | using System.Threading;
11 | using System.Threading.Tasks;
12 | using Microsoft.IdentityModel.Tokens;
13 |
14 | internal class MockTokenConverter : IAsyncConverter
15 | {
16 | private readonly string _secretKey = "dummy_secret_key";
17 | private readonly JwtSecurityTokenHandler _tokenHandler = new JwtSecurityTokenHandler();
18 |
19 | public Task ConvertAsync(TokenBaseAttribute input, CancellationToken cancellationToken)
20 | {
21 | ClaimsIdentity identity = new ClaimsIdentity();
22 | identity.AddClaim(new Claim(ClaimTypes.Name, "Sample"));
23 | identity.AddClaim(new Claim("idp", "aad"));
24 | identity.AddClaim(new Claim("oid", Guid.NewGuid().ToString()));
25 | identity.AddClaim(new Claim("scp", "read"));
26 |
27 | var descr = new SecurityTokenDescriptor
28 | {
29 | Audience = "https://sample.com",
30 | Issuer = "https://microsoft.graph.com",
31 | Subject = identity,
32 | SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_secretKey)), SecurityAlgorithms.HmacSha256),
33 | Expires = DateTime.UtcNow.AddHours(1)
34 | };
35 | string accessToken = _tokenHandler.CreateJwtSecurityToken(descr).RawData;
36 | return Task.FromResult(accessToken);
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/test/MicrosoftGraph.Tests/OneDriveMockUtilities.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Tests
5 | {
6 | using System;
7 | using System.IO;
8 | using System.Linq.Expressions;
9 | using System.Threading;
10 | using System.Threading.Tasks;
11 | using Microsoft.Graph;
12 | using Moq;
13 |
14 | internal static class OneDriveMockUtilities
15 | {
16 | public static void MockGetOneDriveContentStreamAsync(this Mock mock, Stream returnValue)
17 | {
18 | mock.Setup(client => client.Me
19 | .Drive
20 | .Root
21 | .ItemWithPath(It.IsAny())
22 | .Content
23 | .Request(null)
24 | .GetAsync(It.IsAny(), System.Net.Http.HttpCompletionOption.ResponseContentRead))
25 | .Returns(Task.FromResult(returnValue));
26 | }
27 |
28 | public static void MockGetOneDriveContentStreamFromShareAsync(this Mock mock, Stream returnValue)
29 | {
30 | mock.Setup(client => client
31 | .Shares[It.IsAny()]
32 | .Root
33 | .Content
34 | .Request(null)
35 | .GetAsync(It.IsAny(), System.Net.Http.HttpCompletionOption.ResponseContentRead))
36 | .Returns(Task.FromResult(returnValue));
37 | }
38 |
39 | public static void VerifyGetOneDriveContentStreamFromShareAsync(this Mock mock, string shareToken)
40 | {
41 | //First verify GetAsync() called
42 | mock.Verify(client => client
43 | .Shares[shareToken]
44 | .Root
45 | .Content
46 | .Request(null)
47 | .GetAsync(It.IsAny(), System.Net.Http.HttpCompletionOption.ResponseContentRead));
48 |
49 | //Then verify sharetoken correct
50 | mock.Verify(client => client
51 | .Shares[shareToken]);
52 | }
53 |
54 | public static void MockGetOneDriveItemAsync(this Mock mock, DriveItem returnValue)
55 | {
56 | mock.Setup(client => client
57 | .Me
58 | .Drive
59 | .Root
60 | .ItemWithPath(It.IsAny())
61 | .Request()
62 | .GetAsync(It.IsAny()))
63 | .Returns(Task.FromResult(returnValue));
64 | }
65 |
66 | public static void MockUploadOneDriveItemAsync(this Mock mock, DriveItem returnValue)
67 | {
68 | mock.Setup(client => client
69 | .Me
70 | .Drive
71 | .Root
72 | .ItemWithPath(It.IsAny())
73 | .Content
74 | .Request(null)
75 | .PutAsync(It.IsAny(), It.IsAny(), System.Net.Http.HttpCompletionOption.ResponseContentRead))
76 | .Returns(Task.FromResult(returnValue));
77 | }
78 |
79 | public static void VerifyUploadOneDriveItemAsync(this Mock mock, string path, Expression> streamCondition)
80 | {
81 | mock.Verify(client => client
82 | .Me
83 | .Drive
84 | .Root
85 | .ItemWithPath(path)
86 | .Content
87 | .Request(null)
88 | .PutAsync(It.Is(streamCondition), It.IsAny(), System.Net.Http.HttpCompletionOption.ResponseContentRead));
89 | }
90 |
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/test/MicrosoftGraph.Tests/OutlookMockUtilities.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Tests
5 | {
6 | using System;
7 | using System.Linq.Expressions;
8 | using System.Threading;
9 | using System.Threading.Tasks;
10 | using Microsoft.Graph;
11 | using Moq;
12 |
13 | internal static class OutlookMockUtilities
14 | {
15 | public static void MockSendMessage(this Mock mock)
16 | {
17 | mock.Setup(client => client
18 | .Me
19 | .SendMail(It.IsAny(), true)
20 | .Request(null)
21 | .PostAsync(It.IsAny())).Returns(Task.CompletedTask);
22 | }
23 |
24 | public static void VerifySendMessage(this Mock mock, Expression> messageCondition)
25 | {
26 | // First verify PostAsync() called
27 | mock.Verify(client => client
28 | .Me
29 | .SendMail(It.IsAny(), true)
30 | .Request(null)
31 | .PostAsync(It.IsAny()));
32 |
33 | // Now verify that message condition holds for sent message
34 | mock.Verify(client => client
35 | .Me
36 | .SendMail(It.Is(messageCondition), true));
37 | }
38 |
39 | public static void VerifyDidNotSendMessage(this Mock mock)
40 | {
41 | mock.Verify(client => client
42 | .Me
43 | .SendMail(It.IsAny(), true)
44 | .Request(null)
45 | .PostAsync(It.IsAny()), Times.Never());
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/test/MicrosoftGraph.Tests/OutlookTests.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) .NET Foundation. All rights reserved.
2 | // Licensed under the MIT License. See License.txt in the project root for license information.
3 |
4 | namespace Microsoft.Azure.WebJobs.Extensions.MicrosoftGraph.Tests
5 | {
6 | using System;
7 | using System.Collections.Generic;
8 | using System.Linq;
9 | using System.Threading.Tasks;
10 | using Microsoft.Graph;
11 | using Moq;
12 | using Newtonsoft.Json.Linq;
13 | using Xunit;
14 |
15 | public class OutlookTests
16 | {
17 | private static string name = "Sample User";
18 | private static string address = "sampleuser@microsoft.com";
19 | private static string subject = "hello";
20 | private static string body = "world";
21 | private static BodyType contentType = BodyType.Text;
22 |
23 | [Fact]
24 | public static async Task JObjectOutput_SendsProperMessage()
25 | {
26 | var clientMock = SendMessageMock();
27 |
28 | await CommonUtilities.ExecuteFunction("OutlookFunctions.SendJObject", clientMock);
29 |
30 | clientMock.VerifySendMessage(msg => MessageEquals(msg, GetMessage()));
31 | }
32 |
33 | [Fact]
34 | public static async Task MessageOutput_SendsProperMessage()
35 | {
36 | var clientMock = SendMessageMock();
37 |
38 | await CommonUtilities.ExecuteFunction("OutlookFunctions.SendMessage", clientMock);
39 |
40 | clientMock.VerifySendMessage(msg => MessageEquals(msg, GetMessage()));
41 | }
42 |
43 | [Fact]
44 | public static async Task PocoOutput_SendsProperMessage()
45 | {
46 | var clientMock = SendMessageMock();
47 |
48 | await CommonUtilities.ExecuteFunction("OutlookFunctions.SendPoco", clientMock);
49 |
50 | clientMock.VerifySendMessage(msg => MessageEquals(msg, GetMessage()));
51 | }
52 |
53 | [Fact]
54 | public static async Task PocoOutputWithNoRecipients_ThrowsException()
55 | {
56 | var clientMock = SendMessageMock();
57 |
58 | await Assert.ThrowsAnyAsync(async () => await CommonUtilities.ExecuteFunction("OutlookFunctions.NoRecipients", clientMock));
59 | clientMock.VerifyDidNotSendMessage();
60 | }
61 |
62 | private static Mock