This commit is contained in:
Colby Williams 2018-02-28 08:57:40 -05:00
Родитель 8713edd548
Коммит d88e277240
27 изменённых файлов: 1290 добавлений и 10 удалений

9
.github/contributing.md поставляемый Normal file
Просмотреть файл

@ -0,0 +1,9 @@
# Contributing
This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit https://cla.microsoft.com.
As this is an active (pre 1.0) project, please [submit an issue](https://github.com/Azure/Azure.Mobile/issues/new) for new features so that they can be discussed before submitting pull requests. Pull requests for bug fixes are welcome!
When you submit a pull request, a CLA-bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repositories using our CLA.
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.

18
.github/issue_template.md поставляемый Normal file
Просмотреть файл

@ -0,0 +1,18 @@
*Only submit issues here for the deployment template and functions -- Issues in the iOS & Android SDKs should be submit in aka.ms/azureios & aka.ms/azureandroid.*
If you are creating an issue for a **BUG** please fill out the applicable information below. If you are asking a question or requesting a feature you can delete the sections below.
**Failure to fill out this information will result in this issue being closed.** If you post a full stack trace in a bug it will be closed, please post it to http://gist.github.com and link to it here.
## Bug Information
### Steps to Reproduce
### Expected Behavior
### Actual Behavior
### Code snippet
### Screenshots

8
.github/pull_request_template.md поставляемый Normal file
Просмотреть файл

@ -0,0 +1,8 @@
Thanks for taking the time to contribute. Please take a moment to fill out the information below.
Fixes # .
Changes Proposed in this pull request:
-
-
-

4
.gitignore поставляемый
Просмотреть файл

@ -286,3 +286,7 @@ __pycache__/
*.btm.cs
*.odx.cs
*.xsd.cs
*.DS_Store
*.sln.cache

116
README.md
Просмотреть файл

@ -1,14 +1,110 @@
_Looking for the client SDKs? You can find them in the [Azure.iOS][azure-ios] and [Azure.Android][azure-android] repos._
# Azure.Mobile
**[Azure.Mobile](https://aka.ms/mobile) is a framework for rapidly creating iOS and android apps with modern, highly-scalable backends on Azure.**
Azure.Mobile has two simple objectives:
1. Enable developers to create, configure, deploy all necessary backend services fast — ideally under 10 minutes with only a few clicks
2. Provide native iOS and android SDKs with delightful APIs to interact with the services
## What's included?
It includes one-click deployment templates and native client SDKs for the following:
- [Database (document)][cosmos]
- [Blob/File/Queue Storage][storage]
- [Authentication][app-service]
- [Push Notifications][notification-hub]
- [Serverless Functions][functions]
- [Client/Server Analytics][app-insights]
- [Secure Key Storage][key-vault]
![architecture-diagram](assets/AzureMobile1400_1000.png?raw=true "architecture diagram")
# Getting Started
## 1. Azure Account
To use Azure.Mobile, you'll need an Azure account. If you already have one, make sure youre [logged in](https://portal.azure.com) and move to the next step.
If you don't have an Azure account, [sign up for a Azure free account][azure-free] before moving to the next step.
## 2. Deploy Azure Services
Deploying the Azure resources is as simple as clicking the link below then filling out the form per the instructions in the next step:
[![Deploy to Azure][azure-deploy-button]][azure-deploy]
## 3. Fill in Template Form
There's a few fields to fill out in order to create and deploy the Azure resources defined in the template.
Below is a brief explanation/guidance for filling in each field, please [file an issue](issues/new?labels=docs) if you have questions or require additional help.
- **`Subscription:`** Choose which Azure subscription you want to use to deploy the backend. If you only have one choice, or you don't see this option at all, don't sweat it.
- **`Resource group:`** Unless you have an existing Resource group that you know you want to use, select __Create new__ and provide a name for the new group. _(a resource group is essentially a parent folder to deploy the new database, app service, etc. to)_
- **`Location:`** Select the region to deploy the new resources. You want to choose a region that best describes your location (or your users location).
- **`Web Site Name:`** Provide a name for your app. This can be the same name as your Resource group, and will be used as the subdomain for your service endpoint. For example, if you used `superawesome`, your serverless app would live at `superawesome.azurewebsites.net`.
- **`Function Language:`** The template will deploy a serverless app with a few boilerplate functions. This is the programming language those functions will be written in. Choose the language you're most comfortable with.
- **Agree & Purchase:** Read and agree to the _TERMS AND CONDITIONS_, then click _Purchase_.
## 4. Configure iOS/Android app
Once you deploy the Azure services, all that's left to do is your app. You'll find detailed instructions for setting up and using the iOS & Android SDKs in their respective repos:
- [iOS SDK][azure-ios]
- [Android SDK][azure-android]
# How is this different than Azure Mobile Apps?
Azure Mobile Apps _(formally Azure App Services)_ is...
# What is the price/cost?
Most of these services have a generous free tier. [Sign up for a Azure free account][azure-free] to get $200 credit.
# Contributing
This project welcomes contributions and suggestions. Most contributions require you to agree to a
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
the rights to use your contribution. For details, visit https://cla.microsoft.com.
When you submit a pull request, a CLA-bot will automatically determine whether you need to provide
a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions
provided by the bot. You will only need to do this once across all repos using our CLA.
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.
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.
[azure-ios]:https://aka.ms/azureios
[azure-android]:https://aka.ms/azureandroid
[cosmos]:https://azure.microsoft.com/en-us/services/cosmos-db
[key-vault]:https://azure.microsoft.com/en-us/services/key-vault
[app-service]:https://azure.microsoft.com/en-us/services/app-service
[functions]:https://azure.microsoft.com/en-us/services/functions
[storage]:https://azure.microsoft.com/en-us/services/storage
[notification-hub]:https://azure.microsoft.com/en-us/services/notification-hubs
[app-insights]:https://azure.microsoft.com/en-us/services/application-insights
[azure-deploy]:https://aka.ms/mobile-deploy
[azure-deploy-button]:https://azuredeploy.net/deploybutton.svg
[azure-visualize]:http://armviz.io/#/?load=https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2FAzure.Mobile%2Fmaster%2Fazuredeploy.json
[azure-visualize-button]:http://armviz.io/visualizebutton.png
[azure-free]:https://azure.microsoft.com/en-us/free/

Просмотреть файл

@ -0,0 +1,8 @@
## Reporting Security Issues
Security issues and bugs should be reported privately, via email, to the Microsoft Security
Response Center (MSRC) at [secure@microsoft.com](mailto:secure@microsoft.com). 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. Further information, including the
[MSRC PGP](https://technet.microsoft.com/en-us/security/dn606155) key, can be found in
the [Security TechCenter](https://technet.microsoft.com/en-us/security/default).

Двоичные данные
assets/AzureMobile.pdf Normal file

Двоичный файл не отображается.

Двоичные данные
assets/AzureMobile1400_1000.jpg Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 120 KiB

Двоичные данные
assets/AzureMobile1400_1000.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 134 KiB

283
azuredeploy.json Normal file
Просмотреть файл

@ -0,0 +1,283 @@
{
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
"contentVersion": "1.0.5.0",
"parameters": {
"functionAppName": {
"type": "string",
"metadata": {
"description": "The name of the function app that you wish to create, this will also be used as the subdomain of your service endpoint (i.e. myapp.azurewebsites.net)."
}
},
"functionAppLanguage": {
"type": "string",
"allowedValues": [
"Javascript",
"C#",
"C# Script (.csx)"
],
"metadata": {
"description": "The language to write your serverless functions in."
}
}
},
"variables": {
"functionAppNameLower": "[toLower(parameters('functionAppName'))]",
"documentDbName": "[concat('database', uniqueString(resourceGroup().id))]",
"storageAccountName": "[concat('storage', uniqueString(resourceGroup().id))]",
"notificationHubNamespace": "[concat('namespace', uniqueString(resourceGroup().id))]",
"notificationHubName": "[concat('hub', uniqueString(resourceGroup().id))]",
"keyVaultName": "[concat('keyvault', uniqueString(resourceGroup().id))]",
"functionAppUrl": "[concat(variables('functionAppNameLower'),'.azurewebsites.net')]",
"identityResourceId": "[concat(resourceId('Microsoft.Web/sites', variables('functionAppNameLower')),'/providers/Microsoft.ManagedIdentity/Identities/default')]",
"branch": "master",
"repoURL": "https://github.com/colbylwilliams/Azure.Mobile.git"
},
"resources": [
{
"type": "Microsoft.Web/serverfarms",
"kind": "functionapp",
"name": "[variables('functionAppNameLower')]",
"apiVersion": "2016-09-01",
"location": "[resourceGroup().location]",
"properties": {
"name": "[variables('functionAppNameLower')]",
"hostingEnvironment": ""
},
"sku": {
"name": "Y1",
"tier": "Dynamic"
}
},
{
"type": "Microsoft.Web/sites",
"kind": "functionapp",
"name": "[variables('functionAppNameLower')]",
"apiVersion": "2016-08-01",
"location": "[resourceGroup().location]",
"identity": {
"type": "SystemAssigned"
},
"properties": {
"name": "[variables('functionAppNameLower')]",
"serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('functionAppNameLower'))]",
"siteConfig": {
"phpVersion": "off",
"clientAffinityEnabled": false,
"hostingEnvironment": "",
"push": {
"isPushEnabled": true
}
}
},
"dependsOn": [
"[resourceId('Microsoft.Web/serverfarms', variables('functionAppNameLower'))]",
"[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]",
"[resourceId('Microsoft.DocumentDb/databaseAccounts', variables('documentDbName'))]",
"[resourceId('Microsoft.NotificationHubs/namespaces', variables('notificationHubNamespace'))]",
"[resourceId('Microsoft.NotificationHubs/namespaces/notificationHubs', variables('notificationHubNamespace'), variables('notificationHubName'))]"
],
"resources": [
{
"type": "config",
"name": "connectionstrings",
"apiVersion": "2016-08-01",
"properties": {
"MS_NotificationHubConnectionString": {
"value": "[listKeys(resourceId('Microsoft.NotificationHubs/namespaces/notificationHubs/authorizationRules', variables('notificationHubNamespace'), variables('notificationHubName'), 'DefaultFullSharedAccessSignature'), '2014-09-01').primaryConnectionString]",
"type": "NotificationHub"
},
"MS_AzureStorageAccountConnectionString": {
"value": "[concat('DefaultEndpointsProtocol=https;AccountName=',variables('storageAccountName'),';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2017-06-01').keys[0].value)]",
"type": "Custom"
}
},
"dependsOn": [
"[resourceId('Microsoft.Web/sites', variables('functionAppNameLower'))]"
]
},
{
"type": "config",
"name": "appsettings",
"apiVersion": "2016-08-01",
"properties": {
"FUNCTION_APP_EDIT_MODE": "[if(equals(parameters('functionAppLanguage'),'C#'), 'readonly', 'readwrite')]",
"FUNCTIONS_EXTENSION_VERSION": "[if(equals(parameters('functionAppLanguage'),'C#'), 'beta', '~1')]",
"WEBSITE_AUTH_HIDE_DEPRECATED_SID": "true",
"WEBSITE_CONTENTAZUREFILECONNECTIONSTRING": "[concat('DefaultEndpointsProtocol=https;AccountName=',variables('storageAccountName'),';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2017-06-01').keys[0].value)]",
"WEBSITE_CONTENTSHARE": "[variables('functionAppNameLower')]",
"WEBSITE_HTTPLOGGING_RETENTION_DAYS": "2",
"WEBSITE_NODE_DEFAULT_VERSION": "6.5.0",
"PROJECT": "[if(equals(parameters('functionAppLanguage'),'C#'), 'csharp/csharp', if(equals(parameters('functionAppLanguage'),'Javascript'), 'javascript', 'csharpscript'))]",
"AzureWebJobsStorage": "[concat('DefaultEndpointsProtocol=https;AccountName=',variables('storageAccountName'),';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2017-06-01').keys[0].value)]",
"AzureWebJobsDashboard": "[concat('DefaultEndpointsProtocol=https;AccountName=',variables('storageAccountName'),';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2017-06-01').keys[0].value)]",
"AzureWebJobsDocumentDBConnectionString": "[concat('AccountEndpoint=', reference(concat('Microsoft.DocumentDb/databaseAccounts/', variables('documentDbName'))).documentEndpoint, ';AccountKey=', listKeys(resourceId('Microsoft.DocumentDb/databaseAccounts', variables('documentDbName')), '2015-04-08').primaryMasterKey)]",
"AzureWebJobsNotificationHubName": "[variables('notificationHubName')]",
"AzureWebJobsNotificationHubsConnectionString": "[listKeys(resourceId('Microsoft.NotificationHubs/namespaces/notificationHubs/authorizationRules', variables('notificationHubNamespace'), variables('notificationHubName'), 'DefaultFullSharedAccessSignature'), '2014-09-01').primaryConnectionString]",
"AzureWebJobsSecretStorageType": "Blob",
"RemoteDocumentDbKey": "[listKeys(resourceId('Microsoft.DocumentDb/databaseAccounts', variables('documentDbName')), '2015-04-08').primaryMasterKey]",
"RemoteDocumentDbUrl": "[reference(concat('Microsoft.DocumentDb/databaseAccounts/', variables('documentDbName'))).documentEndpoint]",
"APPINSIGHTS_INSTRUMENTATIONKEY": "[reference(resourceId('Microsoft.Insights/components', variables('functionAppNameLower')), '2015-05-01').InstrumentationKey]",
"MS_NotificationHubId": "[resourceId('Microsoft.NotificationHubs/namespaces/notificationHubs', variables('notificationHubNamespace'), variables('notificationHubName'))]",
"MS_NotificationHubName": "[variables('notificationHubName')]",
"AzureKeyVaultName": "[variables('keyVaultName')]",
"AzureKeyVaultUrl": "[concat('https://', variables('keyVaultName'), '.vault.azure.net')]"
},
"dependsOn": [
"[resourceId('Microsoft.Web/sites', variables('functionAppNameLower'))]"
]
},
{
"type": "sourcecontrols",
"name": "web",
"apiVersion": "2016-08-01",
"comments": "This section configures continuous deployment of the Function App from the GitHub repo.",
"properties": {
"repoUrl": "[variables('repoURL')]",
"branch": "[variables('branch')]",
"isManualIntegration": true
},
"dependsOn": [
"[resourceId('Microsoft.Web/serverfarms', variables('functionAppNameLower'))]",
"[resourceId('Microsoft.Web/sites', variables('functionAppNameLower'))]",
"[resourceId('Microsoft.Web/sites/config', variables('functionAppNameLower'), 'appsettings')]",
"[resourceId('Microsoft.Web/sites/config', variables('functionAppNameLower'), 'connectionstrings')]",
"[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]",
"[resourceId('Microsoft.DocumentDb/databaseAccounts', variables('documentDbName'))]",
"[resourceId('Microsoft.NotificationHubs/namespaces', variables('notificationHubNamespace'))]",
"[resourceId('Microsoft.NotificationHubs/namespaces/notificationHubs', variables('notificationHubNamespace'), variables('notificationHubName'))]",
"[resourceId('Microsoft.KeyVault/vaults', variables('keyVaultName'))]",
"[resourceId('Microsoft.Insights/components', variables('functionAppNameLower'))]"
]
}
]
},
{
"type": "Microsoft.KeyVault/vaults",
"name": "[variables('keyVaultName')]",
"apiVersion": "2015-06-01",
"location": "[resourceGroup().location]",
"tags": {},
"properties": {
"sku": {
"family": "A",
"name": "Standard"
},
"tenantId": "[reference(variables('identityResourceId'), '2015-08-31-PREVIEW').tenantId]",
"accessPolicies": [
{
"tenantId": "[reference(variables('identityResourceId'), '2015-08-31-PREVIEW').tenantId]",
"objectId": "[reference(variables('identityResourceId'), '2015-08-31-PREVIEW').principalId]",
"permissions": {
"secrets": [
"Get",
"List",
"Set",
"Delete"
]
}
}
],
"enabledForDeployment": false
},
"dependsOn": [
"[resourceId('Microsoft.Web/sites', variables('functionAppNameLower'))]",
"[resourceId('Microsoft.Web/serverfarms', variables('functionAppNameLower'))]"
]
},
{
"type": "Microsoft.Insights/components",
"kind": "web",
"name": "[variables('functionAppNameLower')]",
"apiVersion": "2014-04-01",
"location": "[resourceGroup().location]",
"properties": {
"ApplicationId": "[variables('functionAppNameLower')]"
},
"tags": {
"[concat('hidden-link:', resourceId('Microsoft.Web/sites', variables('functionAppNameLower')))]": "Resource"
},
"dependsOn": [
"[resourceId('Microsoft.Web/sites', variables('functionAppNameLower'))]",
"[resourceId('Microsoft.Web/serverfarms', variables('functionAppNameLower'))]"
]
},
{
"type": "Microsoft.DocumentDB/databaseAccounts",
"name": "[variables('documentDbName')]",
"apiVersion": "2015-04-08",
"location": "[resourceGroup().location]",
"tags": {
"defaultExperience": "DocumentDB"
},
"properties": {
"name": "[variables('documentDbName')]",
"databaseAccountOfferType": "Standard",
"locations": [
{
"locationName": "[resourceGroup().location]",
"failoverPriority": 0
}
]
}
},
{
"type": "Microsoft.Storage/storageAccounts",
"name": "[variables('storageAccountName')]",
"apiVersion": "2017-06-01",
"kind": "Storage",
"location": "[resourceGroup().location]",
"sku": {
"name": "Standard_LRS",
"tier": "Standard"
},
"properties": {
"name": "[variables('storageAccountName')]"
}
},
{
"type": "Microsoft.NotificationHubs/namespaces",
"name": "[variables('notificationHubNamespace')]",
"apiVersion": "2014-09-01",
"location": "[resourceGroup().location]",
"sku": {
"name": "Free"
},
"properties": {
"name": "[variables('notificationHubNamespace')]",
"namespaceType": "NotificationHub"
},
"resources": [
{
"type": "Microsoft.NotificationHubs/namespaces/notificationHubs",
"name": "[concat(variables('notificationHubNamespace'), '/', variables('notificationHubName'))]",
"apiVersion": "2014-09-01",
"location": "[resourceGroup().location]",
"properties": {
"name": "[variables('notificationHubName')]"
},
"dependsOn": [
"[resourceId('Microsoft.NotificationHubs/namespaces', variables('notificationHubNamespace'))]"
]
}
]
}
],
"outputs": {
"Configure_iOS": {
"type": "string",
"value": "[concat('AzureData.configure(forAccountNamed:\"', variables('documentDbName'), '\", withKey: \"', listKeys(resourceId('Microsoft.DocumentDb/databaseAccounts', variables('documentDbName')), '2015-04-08').primaryMasterKey, '\", ofType: .master)')]"
},
"Configure_Android": {
"type": "string",
"value": "[concat('AzureData.configure(applicationContext, \"', variables('documentDbName'), '\", \"', listKeys(resourceId('Microsoft.DocumentDb/databaseAccounts', variables('documentDbName')), '2015-04-08').primaryMasterKey, '\", TokenType.MASTER)')]"
},
"FunctionsUrl": {
"type": "string",
"value": "[concat('https://',reference(resourceId('Microsoft.Web/sites', variables('functionAppNameLower'))).hostNames[0])]"
},
"DocumentDbUrl": {
"type": "string",
"value": "[reference(concat('Microsoft.DocumentDb/databaseAccounts/', variables('documentDbName'))).documentEndpoint]"
}
}
}

Просмотреть файл

@ -0,0 +1,12 @@
{
"$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"webSiteName": {
"value": "GEN-UNIQUE"
},
"functionLanguage": {
"value": "C#"
}
}
}

24
csharp/csharp.sln Normal file
Просмотреть файл

@ -0,0 +1,24 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.27130.2024
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "csharp", "csharp\csharp.csproj", "{58AE23F0-CCA8-41DB-A6A9-A9658063A484}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{58AE23F0-CCA8-41DB-A6A9-A9658063A484}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{58AE23F0-CCA8-41DB-A6A9-A9658063A484}.Debug|Any CPU.Build.0 = Debug|Any CPU
{58AE23F0-CCA8-41DB-A6A9-A9658063A484}.Release|Any CPU.ActiveCfg = Release|Any CPU
{58AE23F0-CCA8-41DB-A6A9-A9658063A484}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {4EDAC02C-4F32-4CD2-A2AE-6D4CF5BB3FB4}
EndGlobalSection
EndGlobal

Просмотреть файл

@ -0,0 +1,410 @@
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Azure.Documents;
using Microsoft.Azure.Documents.Client;
using Microsoft.Azure.WebJobs.Host;
using HttpStatusCode = System.Net.HttpStatusCode;
namespace csharp
{
public static class DocumentClientExtensions
{
static RequestOptions PermissionRequestOptions(int durationInSeconds) => new RequestOptions { ResourceTokenExpirySeconds = durationInSeconds };
static string GetUserPermissionId(string databaseId, string userId, PermissionMode permissionMode) => $"{databaseId}-{userId}-{permissionMode.ToString().ToUpper()}";
public static async Task<Permission> GetOrCreatePermission(this DocumentClient client, (string DatabaseId, string CollectionId) collection, string userId, PermissionMode permissionMode, int durationInSeconds, TraceWriter log)
{
var permissionId = string.Empty;
try
{
await client.EnsureCollection(collection, log);
log?.Info($" ... getting collection ({collection.CollectionId}) in database ({collection.DatabaseId})");
var collectionResponse = await client.ReadDocumentCollectionAsync(UriFactory.CreateDocumentCollectionUri(collection.DatabaseId, collection.CollectionId));
var documentCollection = collectionResponse?.Resource ?? throw new Exception($"Could not find Document Collection in Database {collection.DatabaseId} with CollectionId: {collection.CollectionId}");
var userTup = await client.GetOrCreateUser(collection.DatabaseId, userId, log);
var user = userTup.user;
Permission permission;
permissionId = GetUserPermissionId(collection.DatabaseId, user.Id, permissionMode);
// if the user was newly created, go ahead and create the permission
if (userTup.created && !string.IsNullOrEmpty(user?.Id))
{
permission = await client.CreateNewPermission(collection.DatabaseId, documentCollection, user, permissionId, permissionMode, durationInSeconds, log);
}
else // else look for an existing permission with the id
{
var permissionUri = UriFactory.CreatePermissionUri(collection.DatabaseId, user.Id, permissionId);
try
{
log?.Info($" ... getting permission ({permissionId}) at uri: {permissionUri}");
var permissionResponse = await client.ReadPermissionAsync(permissionUri, PermissionRequestOptions(durationInSeconds));
permission = permissionResponse?.Resource;
if (permission != null)
{
log?.Info($" ... found existing permission ({permission.Id})");
}
}
catch (DocumentClientException dcx)
{
dcx.Print(log);
switch (dcx.StatusCode)
{
case HttpStatusCode.NotFound:
log?.Info($" ... could not find permission ({permissionId}) at uri: {permissionUri} - creating...");
permission = await client.CreateNewPermission(collection.DatabaseId, documentCollection, user, permissionId, permissionMode, durationInSeconds, log);
break;
default: throw;
}
}
}
return permission;
}
catch (Exception ex)
{
log?.Error($"Error creating new new {permissionMode.ToString().ToUpper()} Permission [Database: {collection.DatabaseId} Collection: {collection.CollectionId} User: {userId} Permission: {permissionId}", ex);
throw;
}
}
static async Task<Permission> CreateNewPermission(this DocumentClient client, string databaseId, DocumentCollection collection, User user, string permissionId, PermissionMode permissionMode, int durationInSeconds, TraceWriter log)
{
log?.Info($" ... creating new permission ({permissionId}) for collection ({collection?.Id})");
var newPermission = new Permission { Id = permissionId, ResourceLink = collection.SelfLink, PermissionMode = permissionMode };
try
{
var permissionResponse = await client.CreatePermissionAsync(user.SelfLink, newPermission, PermissionRequestOptions(durationInSeconds));
var permission = permissionResponse?.Resource;
if (permission != null)
{
log?.Info($" ... created new permission ({permission.Id})");
}
return permission;
}
catch (DocumentClientException dcx)
{
dcx.Print(log);
switch (dcx.StatusCode)
{
case HttpStatusCode.Conflict:
var oldPermissionId = permissionId.Replace(permissionMode.ToString().ToUpper(), permissionMode == PermissionMode.All ? PermissionMode.Read.ToString().ToUpper() : PermissionMode.All.ToString().ToUpper());
log?.Info($" ... deleting old permission ({oldPermissionId})");
await client.DeletePermissionAsync(UriFactory.CreatePermissionUri(databaseId, user.Id, oldPermissionId));
log?.Info($" ... creating new permission ({permissionId}) for collection ({collection?.Id})");
var permissionResponse = await client.CreatePermissionAsync(user.SelfLink, newPermission, PermissionRequestOptions(durationInSeconds));
var permission = permissionResponse?.Resource;
if (permission != null)
{
log?.Info($" ... created new permission ({permission.Id})");
}
return permission;
default: throw;
}
}
catch (Exception ex)
{
log?.Error($"Error creating new Permission with Id: {permissionId} for Collection: {collection?.Id}", ex);
throw;
}
}
static async Task<(User user, bool created)> GetOrCreateUser(this DocumentClient client, string databaseId, string userId, TraceWriter log)
{
User user = null;
try
{
log?.Info($" ... getting user ({userId}) in database ({databaseId})");
var response = await client.ReadUserAsync(UriFactory.CreateUserUri(databaseId, userId));
user = response?.Resource;
if (user != null)
{
log?.Info($" ... found existing user ({userId}) in database ({databaseId})");
}
return (user, false);
}
catch (DocumentClientException dcx)
{
dcx.Print(log);
switch (dcx.StatusCode)
{
case HttpStatusCode.NotFound:
log?.Info($" ... did not find user ({userId}) - creating...");
var response = await client.CreateUserAsync(UriFactory.CreateDatabaseUri(databaseId), new User { Id = userId });
user = response?.Resource;
if (user != null)
{
log?.Info($" ... created new user ({userId}) in database ({databaseId})");
}
return (user, user != null);
default: throw;
}
}
catch (Exception ex)
{
log?.Error($"Error getting User with Id: {userId}\n", ex);
throw;
}
}
public static void Print(this DocumentClientException dex, TraceWriter log)
{
if ((int)dex.StatusCode == 429)
{
log?.Info("TooManyRequests - This means you have exceeded the number of request units per second. Consult the DocumentClientException.RetryAfter value to see how long you should wait before retrying this operation.");
}
else
{
switch (dex.StatusCode)
{
case HttpStatusCode.BadRequest:
log?.Info("BadRequest - This means something was wrong with the document supplied. It is likely that disableAutomaticIdGeneration was true and an id was not supplied");
break;
case HttpStatusCode.Forbidden:
log?.Info("Forbidden - This likely means the collection in to which you were trying to create the document is full.");
break;
case HttpStatusCode.Conflict:
log?.Info("Conflict - This means a Document with an id matching the id field of document already existed");
break;
case HttpStatusCode.RequestEntityTooLarge:
log?.Info("RequestEntityTooLarge - This means the Document exceeds the current max entity size. Consult documentation for limits and quotas.");
break;
default:
break;
}
}
}
#region Initialization (database & collections)
static readonly Dictionary<string, ClientStatus> _databaseStatuses = new Dictionary<string, ClientStatus>();
static readonly Dictionary<string, Task<ResourceResponse<Database>>> _databaseCreationTasks = new Dictionary<string, Task<ResourceResponse<Database>>>();
static readonly Dictionary<(string DatabaseId, string CollectionId), ClientStatus> _collectionStatuses = new Dictionary<(string DatabaseId, string CollectionId), ClientStatus>();
static readonly Dictionary<(string DatabaseId, string CollectionId), Task<ResourceResponse<DocumentCollection>>> _collectionCreationTasks = new Dictionary<(string DatabaseId, string CollectionId), Task<ResourceResponse<DocumentCollection>>>();
static bool IsInitialized((string DatabaseId, string CollectionId) collection) => _collectionStatuses.TryGetValue(collection, out ClientStatus status) && status == ClientStatus.Initialized;
static async Task EnsureCollection(this DocumentClient client, (string DatabaseId, string CollectionId) collection, TraceWriter log)
{
if (!(IsInitialized(collection) || await client.InitializeCollection(collection, log)))
{
throw new Exception($"Could not find Document Collection in Database {collection.DatabaseId} with CollectionId: {collection.CollectionId}");
}
}
static async Task<bool> InitializeCollection(this DocumentClient client, (string DatabaseId, string CollectionId) collection, TraceWriter log)
{
if (!(_databaseStatuses.TryGetValue(collection.DatabaseId, out ClientStatus databaseStatus) && databaseStatus == ClientStatus.Initialized))
{
await client.CreateDatabaseIfNotExistsAsync(collection.DatabaseId, log);
}
if (!IsInitialized(collection))
{
await client.CreateCollectionIfNotExistsAsync(collection, log);
}
return IsInitialized(collection);
}
static async Task CreateDatabaseIfNotExistsAsync(this DocumentClient client, string databaseId, TraceWriter log)
{
if (_databaseCreationTasks.TryGetValue(databaseId, out Task<ResourceResponse<Database>> task) && !task.IsNullFinishCanceledOrFaulted())
{
log?.Info($" ... database ({databaseId}) is already being created, returning existing task");
await task;
}
else
{
try
{
log?.Info($" ... checking for database ({databaseId})");
_databaseCreationTasks[databaseId] = client.ReadDatabaseAsync(UriFactory.CreateDatabaseUri(databaseId));
var database = await _databaseCreationTasks[databaseId];
if (database?.Resource != null)
{
_databaseStatuses[databaseId] = ClientStatus.Initialized;
log?.Info($" ... found existing database ({databaseId})");
}
}
catch (DocumentClientException dex)
{
switch (dex.StatusCode)
{
case HttpStatusCode.NotFound:
_databaseCreationTasks[databaseId] = client.CreateDatabaseAsync(new Database { Id = databaseId });
var database = await _databaseCreationTasks[databaseId];
if (database?.Resource != null)
{
_databaseStatuses[databaseId] = ClientStatus.Initialized;
log?.Info($" ... created new database ({databaseId})");
}
break;
default: throw;
}
}
catch (Exception ex)
{
_databaseStatuses[databaseId] = ClientStatus.NotInitialized;
log?.Error(ex.Message, ex);
throw;
}
}
}
static async Task CreateCollectionIfNotExistsAsync<T>(this DocumentClient client, string databaseId, TraceWriter log)
{
await client.CreateCollectionIfNotExistsAsync((databaseId, typeof(T).Name), log);
}
static async Task CreateCollectionIfNotExistsAsync(this DocumentClient client, (string DatabaseId, string CollectionId) collection, TraceWriter log)
{
if (_collectionCreationTasks.TryGetValue(collection, out Task<ResourceResponse<DocumentCollection>> task) && !task.IsNullFinishCanceledOrFaulted())
{
log?.Info($" ... collection ({collection.CollectionId}) in database ({collection.DatabaseId}) is already being created, returning existing task");
await task;
}
else
{
try
{
log?.Info($" ... checking for collection ({collection.CollectionId}) in database ({collection.DatabaseId})");
_collectionCreationTasks[collection] = client.ReadDocumentCollectionAsync(UriFactory.CreateDocumentCollectionUri(collection.DatabaseId, collection.CollectionId));
var collectionResponse = await _collectionCreationTasks[collection];
if (collectionResponse?.Resource != null)
{
_collectionStatuses[collection] = ClientStatus.Initialized;
log?.Info($" ... found existing collection ({collection.CollectionId}) in database ({collection.DatabaseId})");
}
}
catch (DocumentClientException dex)
{
switch (dex.StatusCode)
{
case HttpStatusCode.NotFound:
_collectionCreationTasks[collection] = client.CreateDocumentCollectionAsync(UriFactory.CreateDatabaseUri(collection.DatabaseId), new DocumentCollection { Id = collection.CollectionId }, new RequestOptions { OfferThroughput = 1000 });
var collectionResponse = await _collectionCreationTasks[collection];
if (collectionResponse?.Resource != null)
{
_collectionStatuses[collection] = ClientStatus.Initialized;
log?.Info($" ... created new collection ({collection.CollectionId}) in database ({collection.DatabaseId})");
}
break;
default: throw;
}
}
catch (Exception ex)
{
_collectionStatuses[collection] = ClientStatus.NotInitialized;
log?.Error(ex.Message, ex);
throw;
}
}
}
#endregion
}
public enum ClientStatus
{
NotInitialized,
Initializing,
Initialized
}
}

Просмотреть файл

@ -0,0 +1,44 @@
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
using System;
namespace csharp
{
public static class EnvironmentVariables
{
static Uri _documentDbUri;
public static Uri DocumentDbUri => _documentDbUri ?? (_documentDbUri = new Uri(DocumentDbUrl));
public static readonly string DocumentDbUrl = Environment.GetEnvironmentVariable(RemoteDocumentDbUrl);
public static readonly string DocumentDbKey = Environment.GetEnvironmentVariable(RemoteDocumentDbKey);
public static readonly string StorageAccountConnection = Environment.GetEnvironmentVariable(AzureWebJobsStorage);
public static readonly string NotificationHubName = Environment.GetEnvironmentVariable(AzureWebJobsNotificationHubName);
public static readonly string NotificationHubConnectionString = Environment.GetEnvironmentVariable(AzureWebJobsNotificationHubsConnectionString);
public static readonly string KeyVaultName = Environment.GetEnvironmentVariable(AzureKeyVaultName);
public static readonly string KeyVaultUrl = Environment.GetEnvironmentVariable(AzureKeyVaultUrl);
public const string AzureWebJobsStorage = nameof(AzureWebJobsStorage);
public const string RemoteDocumentDbUrl = nameof(RemoteDocumentDbUrl);
public const string RemoteDocumentDbKey = nameof(RemoteDocumentDbKey);
public const string AzureWebJobsNotificationHubsConnectionString = nameof(AzureWebJobsNotificationHubsConnectionString);
public const string AzureWebJobsNotificationHubName = nameof(AzureWebJobsNotificationHubName);
public const string AzureKeyVaultName = nameof(AzureKeyVaultName);
public const string AzureKeyVaultUrl = nameof(AzureKeyVaultUrl);
}
}

Просмотреть файл

@ -0,0 +1,98 @@
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
using System;
using System.Linq;
using System.Net.Http;
using System.Security.Claims;
using System.Security.Principal;
namespace csharp
{
public static class SecurityExtensions
{
const string zumoAuthHeaderKey = "x-zumo-auth";
const string JwtRegisteredClaimNamesIss = "iss";
// https://github.com/Azure/azure-mobile-apps-net-server/wiki/Understanding-User-Ids
public static string UniqueIdentifier(this IPrincipal user)
{
if (user is ClaimsPrincipal principal)
{
if (principal.Identity is ClaimsIdentity identity)
{
return identity.UniqueIdentifier();
}
}
return null;
}
public static string UniqueIdentifier(this ClaimsIdentity identity)
{
if (identity != null)
{
var stableSid = string.Empty;
var ver = identity.FindFirst("ver")?.Value;
// the NameIdentifier claim is not stable.
if (string.Compare(ver, "3", StringComparison.OrdinalIgnoreCase) == 0)
{
// the NameIdentifier claim is not stable.
stableSid = identity.FindFirst("stable_sid")?.Value;
}
else if (string.Compare(ver, "4", StringComparison.OrdinalIgnoreCase) == 0)
{
// the NameIdentifier claim is stable.
stableSid = identity.FindFirst(ClaimTypes.NameIdentifier)?.Value;
}
var provider = identity.FindFirst("http://schemas.microsoft.com/identity/claims/identityprovider")?.Value;
if (string.IsNullOrEmpty(stableSid) || string.IsNullOrEmpty(provider))
{
return null;
}
return $"{provider}|{stableSid}";
}
return null;
}
public static Uri UriFromIssuerClaim(this ClaimsIdentity identity)
{
return new Uri(identity?.FindFirst(JwtRegisteredClaimNamesIss)?.Value);
}
public static void ConfigureClientForUserDetails(this HttpClient client, HttpRequestMessage req)
{
var zumoAuthHeader = req.Headers.GetValues(zumoAuthHeaderKey).FirstOrDefault();
client.DefaultRequestHeaders.Remove(zumoAuthHeaderKey);
client.DefaultRequestHeaders.Add(zumoAuthHeaderKey, zumoAuthHeader);
}
public static ClaimsIdentity GetClaimsIdentity(this IPrincipal currentPricipal)
{
if (currentPricipal?.Identity != null
&& currentPricipal.Identity.IsAuthenticated
&& currentPricipal is ClaimsPrincipal principal
&& principal.Identity is ClaimsIdentity identity)
{
return identity;
}
return null;
}
}
}

Просмотреть файл

@ -0,0 +1,23 @@
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
using System.Threading.Tasks;
namespace csharp
{
public static class TaskExtensions
{
/* TaskStatus enum
* Created, = 0
* WaitingForActivation, = 1
* WaitingToRun, = 2
* Running, = 3
* WaitingForChildrenToComplete, = 4
* RanToCompletion, = 5
* Canceled, = 6
* Faulted = 7 */
public static bool IsNullFinishCanceledOrFaulted(this Task task) => task == null || (int)task.Status >= 5;
}
}

Просмотреть файл

@ -0,0 +1,132 @@
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Documents;
using Microsoft.Azure.Documents.Client;
using Microsoft.Azure.KeyVault;
using Microsoft.Azure.KeyVault.Models;
using Microsoft.Azure.Services.AppAuthentication;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Host;
namespace csharp
{
public static class GetDataToken
{
const string AnonymousId = "anonymous-user";
const int TokenDurationSeconds = 18000; // 5 hours
const double TokenRefreshSeconds = 600; // 10 minutes
static DocumentClient _documentClient;
static DocumentClient DocumentClient => _documentClient ?? (_documentClient = new DocumentClient(EnvironmentVariables.DocumentDbUri, EnvironmentVariables.DocumentDbKey));
static AzureServiceTokenProvider _azureServiceTokenProvider;
static AzureServiceTokenProvider AzureServiceTokenProvider => _azureServiceTokenProvider ?? (_azureServiceTokenProvider = new AzureServiceTokenProvider());
static KeyVaultClient _keyVaultClient;
static KeyVaultClient KeyVaultClient => _keyVaultClient ?? (_keyVaultClient = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(AzureServiceTokenProvider.KeyVaultTokenCallback)));
[Authorize]
[FunctionName(nameof(GetDataToken))]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "api/data/{databaseId}/{collectionId}/token")]
HttpRequest req, string databaseId, string collectionId, TraceWriter log)
{
try
{
SecretBundle secretBundle = null;
var userId = Thread.CurrentPrincipal.GetClaimsIdentity()?.UniqueIdentifier() ?? AnonymousId;
log.Info($" ... userId: {userId}");
var secretId = GetSecretName(databaseId, collectionId, userId);
log.Info($" ... secretId: {secretId} ({secretId.Length})");
try
{
secretBundle = await KeyVaultClient.GetSecretAsync(EnvironmentVariables.KeyVaultUrl, secretId);
}
catch (KeyVaultErrorException kvex)
{
if (kvex.Body.Error.Code != "SecretNotFound")
{
throw;
}
log.Info($" ... existing secret not found");
}
// if the token is still valid for longer than TokenRefreshSeconds, return it
if (secretBundle != null && secretBundle.Attributes.Expires.HasValue
&& secretBundle.Attributes.Expires.Value.Subtract(DateTime.UtcNow).TotalSeconds > TokenRefreshSeconds)
{
log.Info($" ... existing secret found with greater than {TokenRefreshSeconds} seconds remaining before expiration");
return new OkObjectResult(secretBundle.Value);
}
log.Info($" ... getting new permission token for user");
// simply getting the user permission will refresh the token
var userPermission = await DocumentClient.GetOrCreatePermission((databaseId, collectionId), userId, PermissionMode.All, TokenDurationSeconds, log);
if (!string.IsNullOrEmpty(userPermission?.Token))
{
log.Info($" ... saving new permission token to key vault");
secretBundle = await KeyVaultClient.SetSecretAsync(EnvironmentVariables.KeyVaultUrl, secretId, userPermission.Token, secretAttributes: new SecretAttributes(expires: DateTime.UtcNow.AddSeconds(TokenDurationSeconds)));
return new OkObjectResult(secretBundle.Value);
}
log.Info($" ... failed to get new permission token for user");
return new StatusCodeResult(500);
}
catch (Exception ex)
{
log.Error(ex.Message, ex);
return new StatusCodeResult(500);
}
}
// The name must be a string 1-127 characters in length containing only 0-9, a-z, A-Z, and -.
// example userId: google|sid:uir7d29343a3gufe414098b063199430
static string GetSecretName(string databaseId, string collectionId, string userId)
{
const char pipe = '|', colon = ':', hyphen = '-', underscore = '_';
var normalizedUserId = userId.Replace(pipe, hyphen)
.Replace(colon, hyphen)
.Replace(underscore, hyphen);
return $"{databaseId}-{collectionId}-{normalizedUserId}";
}
}
}

Просмотреть файл

@ -0,0 +1,21 @@
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
using System;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Host;
namespace csharp
{
public static class WarmTimerTrigger
{
[FunctionName(nameof(WarmTimerTrigger))]
public static void Run([TimerTrigger("0 */4 * * * *")]TimerInfo myTimer, TraceWriter log)
{
log.Info($"C# Timer trigger function executed at: {DateTime.Now}");
}
}
}

Просмотреть файл

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<AzureFunctionsVersion>v2</AzureFunctionsVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Azure.DocumentDB" Version="1.19.1" />
<PackageReference Include="Microsoft.Azure.DocumentDB.ChangeFeedProcessor" Version="1.2.0" />
<PackageReference Include="Microsoft.Azure.KeyVault" Version="2.4.0-preview" />
<PackageReference Include="Microsoft.Azure.Services.AppAuthentication" Version="1.1.0-preview" />
<PackageReference Include="Microsoft.Azure.WebJobs.Extensions.DocumentDB" Version="1.1.0-beta4" />
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="1.0.7" />
</ItemGroup>
<ItemGroup>
<None Update="host.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="local.settings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
</None>
</ItemGroup>
</Project>

6
csharp/csharp/host.json Normal file
Просмотреть файл

@ -0,0 +1,6 @@
{
"http": {
"routePrefix": ""
},
"watchDirectories": [ "Domain" ]
}

Просмотреть файл

@ -0,0 +1,7 @@
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "",
"AzureWebJobsDashboard": ""
}
}

Просмотреть файл

@ -0,0 +1,11 @@
{
"bindings": [
{
"name": "warmTimer",
"type": "timerTrigger",
"direction": "in",
"schedule": "0 */4 * * * *"
}
],
"disabled": false
}

Просмотреть файл

@ -0,0 +1,13 @@
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
using System;
public static void Run(TimerInfo warmTimer, TraceWriter log)
{
log.Info($"C# Timer trigger function executed at: {DateTime.Now}");
}

1
csharpscript/host.json Normal file
Просмотреть файл

@ -0,0 +1 @@
{}

Просмотреть файл

@ -0,0 +1,11 @@
{
"bindings": [
{
"name": "warmTimer",
"type": "timerTrigger",
"direction": "in",
"schedule": "0 */4 * * * *"
}
],
"disabled": false
}

Просмотреть файл

@ -0,0 +1,17 @@
/**
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
module.exports = function (context, warmTimer) {
var timeStamp = new Date().toISOString();
if (warmTimer.isPastDue) {
context.log('JavaScript is running late!');
}
context.log('Warm timer trigger function ran!', timeStamp);
context.done();
};

1
javascript/host.json Normal file
Просмотреть файл

@ -0,0 +1 @@
{}