From ef0217a586d9f10021e7f55977c7dbe6c6ac6e0c Mon Sep 17 00:00:00 2001 From: tusmester Date: Sat, 2 Dec 2017 17:23:47 +0100 Subject: [PATCH] [KFI] Merge changes from release branch before release 7.0.0 stable. --- README.md | 4 +- docs/images/SenseNetTokenAuthentication.png | Bin 40301 -> 34675 bytes docs/install-sn-from-nuget.md | 6 +- docs/oauth.md | 118 +++ docs/sensenet-components.md | 9 +- docs/snadmin-builtin-steps.md | 85 +- docs/snadmin-tools.md | 20 + docs/web-token-authentication.md | 92 +- src/BlobStorage/BlobStorage.nuspec | 6 +- src/BlobStorage/Properties/AssemblyInfo.cs | 2 +- src/Common/Common.nuspec | 4 +- src/Common/Properties/AssemblyInfo.cs | 2 +- src/Common/Readme.md | 4 +- src/Configuration/Properties/AssemblyInfo.cs | 2 +- .../ApplicationModel/ActionFramework.cs | 3 +- src/ContentRepository/Content.cs | 14 +- src/ContentRepository/ContentList.cs | 12 +- .../ContentRepository.csproj | 16 +- src/ContentRepository/ContentTemplate.cs | 12 +- src/ContentRepository/GenericContent.cs | 2 +- .../Packaging/Steps/SetUrl.cs | 45 + src/ContentRepository/Packaging/Steps/Step.cs | 5 + .../Packaging/Steps/XmlEditorStep.cs | 53 +- .../Preview/DocumentPreviewObserver.cs | 84 -- .../Preview/DocumentPreviewProvider.cs | 46 +- .../Properties/AssemblyInfo.cs | 2 +- src/ContentRepository/Repository.cs | 6 + src/ContentRepository/RepositoryBuilder.cs | 145 ++++ src/ContentRepository/RepositoryInstance.cs | 48 +- src/ContentRepository/Schema/ContentType.cs | 4 +- .../Schema/ContentTypeInstaller.cs | 10 +- .../Schema/ContentTypeManager.cs | 4 +- .../Security/IOAuthIdentity.cs | 49 ++ .../Security/OAuthProvider.cs | 57 ++ .../SnElevatedModificationVisibilityRule.cs | 10 +- src/ContentRepository/packages.config | 1 + src/Preview/Common.cs | 30 - src/Preview/IPreviewGenerationContext.cs | 27 - src/Preview/IPreviewImageGenerator.cs | 14 - src/Preview/Preview.csproj | 70 -- src/Preview/Preview.nuspec | 24 - src/Preview/PreviewImageGenerator.cs | 107 --- src/Preview/Properties/AssemblyInfo.cs | 26 - src/Preview/SR.cs | 19 - src/Preview/packages.config | 4 - src/SenseNet.sln | 42 +- .../ApplicationModel/CopyBatchAction.cs | 79 +- .../ApplicationModel/DeleteBatchAction.cs | 114 ++- .../ApplicationModel/MoveBatchAction.cs | 75 +- src/Services/OData/BatchActionResponse.cs | 24 + src/Services/OData/ErrorContent.cs | 13 + src/Services/OData/FieldConverter.cs | 65 ++ src/Services/OData/Json.cs | 102 ++- src/Services/OData/JsonFormatter.cs | 53 ++ src/Services/OData/ODataException.cs | 57 ++ src/Services/OData/ODataFormatter.cs | 101 ++- src/Services/OData/ODataHandler.cs | 45 +- src/Services/OData/ODataRequest.cs | 154 +++- src/Services/OData/ODataTools.cs | 55 +- .../OData/Parser/ExpressionBuilder.cs | 4 +- src/Services/OData/Parser/Globals.cs | 2 +- .../OData/Parser/ODataParserException.cs | 28 + src/Services/OData/Projector.cs | 2 +- src/Services/OData/SnJsonConverter.cs | 3 + .../OData/Typescript/TypescriptFormatter.cs | 14 + src/Services/Properties/AssemblyInfo.cs | 3 +- src/Services/SR.cs | 1 + src/Services/SenseNetGlobal.cs | 1 - src/Services/Services.Install.nuspec | 4 +- src/Services/Services.csproj | 6 + src/Services/Services.nuspec | 8 +- .../Virtualization/AuthenticationHelper.cs | 15 + .../Virtualization/HttpHeaderTools.cs | 3 +- src/Services/Virtualization/OAuthManager.cs | 183 ++++ .../PortalAuthenticationModule.cs | 271 +----- .../Virtualization/TokenAuthentication.cs | 359 ++++++++ src/Storage/Configuration/Identifiers.cs | 2 +- src/Storage/Configuration/Providers.cs | 181 +++- src/Storage/Data/DataProvider.cs | 37 +- .../SqlClient/Scripts/Install_01_Schema.sql | Bin 196838 -> 187194 bytes .../Scripts/Install_03_Data_Phase1.sql | 1 + src/Storage/DataBackingStore.cs | 19 +- src/Storage/DistributedApplication.cs | 9 +- .../ElevatedModificationVisibilityRule.cs | 16 +- src/Storage/Events/NodeObserver.cs | 3 +- src/Storage/Node.cs | 53 +- src/Storage/NodeIdentifier.cs | 20 +- src/Storage/Properties/AssemblyInfo.cs | 2 +- src/Storage/Schema/NodeType.cs | 12 +- src/Storage/Security/AccessProvider.cs | 45 +- src/Storage/Security/SecurityHandler.cs | 8 +- src/Storage/Security/StartupUser.cs | 16 +- .../Properties/AssemblyInfo.cs | 19 + .../ProvidersTests.cs | 55 ++ .../RepositoryStartTests.cs | 40 + .../SenseNet.ContentRepository.Tests.csproj | 123 +++ .../packages.config | 6 + .../Properties/AssemblyInfo.cs | 2 +- .../PackagingStorageTests.cs | 14 +- .../Properties/AssemblyInfo.cs | 2 +- .../OAuthAuthenticationTests.cs | 166 ++++ .../PortalAuthenticationModuleTests.cs | 339 -------- .../Properties/AssemblyInfo.cs | 2 +- .../SenseNet.Services.Tests.csproj | 17 +- .../TokenAuthenticationTests.cs | 814 ++++++++++++++++++ .../SenseNet.Services.Tests/packages.config | 2 + .../Properties/AssemblyInfo.cs | 2 +- .../Properties/AssemblyInfo.cs | 2 +- src/TokenAuthentication/CookieHelper.cs | 11 +- .../Properties/AssemblyInfo.cs | 2 +- src/Tools/SnAdminRuntime/App.config | 2 +- .../SnAdminRuntime/Properties/AssemblyInfo.cs | 2 +- src/Tools/SnAdminRuntime/SnAdminRuntime.cs | 40 +- .../content/Admin/tools/seturl/manifest.xml | 19 + .../(apps)/ContentList/DeleteField.Content | 3 + .../(apps)/ContentList/EditField.Content | 3 + .../import/(apps)/File/CheckPreviews.Content | 3 + .../(apps)/File/EditInMicrosoftOffice.Content | 2 +- .../import/(apps)/File/ExportToPdf.Content | 3 + .../import/(apps)/File/GetPageCount.Content | 2 +- .../(apps)/File/GetPreviewsFolder.Content | 3 + .../(apps)/File/PreviewAvailable.Content | 2 +- .../(apps)/File/RegeneratePreviews.Content | 3 + .../import/(apps)/File/SetPageCount.Content | 3 + .../(apps)/File/SetPreviewStatus.Content | 3 + .../import/(apps)/File/UploadResume.Content | 2 +- .../import/(apps)/Folder/CopyBatch.Content | 2 +- .../import/(apps)/Folder/DeleteBatch.Content | 2 +- .../import/(apps)/Folder/ExportToCsv.Content | 4 +- .../import/(apps)/Folder/MoveBatch.Content | 2 +- .../AddAllowedChildTypes.Content | 3 + .../(apps)/GenericContent/CopyTo.Content | 2 +- .../(apps)/GenericContent/Delete.Content | 2 +- .../GenericContent/FinalizeContent.Content | 2 +- .../GenericContent/ForceUndoCheckOut.Content | 3 + .../GenericContent/GetAllContentTypes.Content | 3 + .../GetAllowedChildTypesFromCTD.Content | 3 + .../GenericContent/GetAllowedUsers.Content | 6 + .../GetChildrenPermissionInfo.Content | 3 + .../GetNameFromDisplayName.Content | 3 + .../GenericContent/GetPermissionInfo.Content | 3 + .../GetPermissionOverview.Content | 3 + .../GenericContent/GetPermissions.Content | 3 + .../(apps)/GenericContent/GetQueries.Content | 2 +- .../GetQueryBuilderMetadata.Content | 2 +- .../GetRelatedIdentities.Content | 3 + .../GetRelatedIdentitiesByPermissions.Content | 3 + .../GenericContent/GetRelatedItems.Content | 3 + .../GetRelatedItemsOneLevel.Content | 3 + .../GetRelatedPermissions.Content | 3 + .../GenericContent/HasPermission.Content | 3 + .../(apps)/GenericContent/MoveTo.Content | 2 +- .../(apps)/GenericContent/Reject.Content | 3 + .../GenericContent/RemoveFields.Content | 3 + .../(apps)/GenericContent/SaveQuery.Content | 2 +- .../GenericContent/SetPermissions.Content | 3 + .../GenericContent/TakeLockOver.Content | 2 +- .../import/(apps)/Group/AddMembers.Content | 3 + .../import/(apps)/Group/RemoveMembers.Content | 3 + .../import/(apps)/Link/Browse.Content | 2 +- .../import/(apps)/User/Profile.Content | 3 + .../import/IMS/BuiltIn/Portal/Startup.Content | 24 + .../import/Localization/Exceptions.xml | 6 + .../import/System/Settings/OAuth.settings | 4 + .../System/Settings/OAuth.settings.Content | 12 + .../snadmin/install-services/manifest.xml | 4 +- tools/scripts/CreateNuGetPackages.ps1 | 36 +- 167 files changed, 4046 insertions(+), 1463 deletions(-) create mode 100644 docs/oauth.md create mode 100644 src/ContentRepository/Packaging/Steps/SetUrl.cs delete mode 100644 src/ContentRepository/Preview/DocumentPreviewObserver.cs create mode 100644 src/ContentRepository/RepositoryBuilder.cs create mode 100644 src/ContentRepository/Security/IOAuthIdentity.cs create mode 100644 src/ContentRepository/Security/OAuthProvider.cs delete mode 100644 src/Preview/Common.cs delete mode 100644 src/Preview/IPreviewGenerationContext.cs delete mode 100644 src/Preview/IPreviewImageGenerator.cs delete mode 100644 src/Preview/Preview.csproj delete mode 100644 src/Preview/Preview.nuspec delete mode 100644 src/Preview/PreviewImageGenerator.cs delete mode 100644 src/Preview/Properties/AssemblyInfo.cs delete mode 100644 src/Preview/SR.cs delete mode 100644 src/Preview/packages.config create mode 100644 src/Services/OData/BatchActionResponse.cs create mode 100644 src/Services/OData/ErrorContent.cs create mode 100644 src/Services/Virtualization/OAuthManager.cs create mode 100644 src/Services/Virtualization/TokenAuthentication.cs create mode 100644 src/Tests/SenseNet.ContentRepository.Tests/Properties/AssemblyInfo.cs create mode 100644 src/Tests/SenseNet.ContentRepository.Tests/ProvidersTests.cs create mode 100644 src/Tests/SenseNet.ContentRepository.Tests/RepositoryStartTests.cs create mode 100644 src/Tests/SenseNet.ContentRepository.Tests/SenseNet.ContentRepository.Tests.csproj create mode 100644 src/Tests/SenseNet.ContentRepository.Tests/packages.config create mode 100644 src/Tests/SenseNet.Services.Tests/OAuthAuthenticationTests.cs delete mode 100644 src/Tests/SenseNet.Services.Tests/PortalAuthenticationModuleTests.cs create mode 100644 src/Tests/SenseNet.Services.Tests/TokenAuthenticationTests.cs create mode 100644 src/nuget/content/Admin/tools/seturl/manifest.xml create mode 100644 src/nuget/snadmin/install-services/import/IMS/BuiltIn/Portal/Startup.Content create mode 100644 src/nuget/snadmin/install-services/import/System/Settings/OAuth.settings create mode 100644 src/nuget/snadmin/install-services/import/System/Settings/OAuth.settings.Content diff --git a/README.md b/README.md index 932b7aaa8..2322f881f 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ The first Open Source Enterprise Content Management platform for .NET! [![Join the chat at https://gitter.im/SenseNet/sensenet](https://badges.gitter.im/SenseNet/sensenet.svg)](https://gitter.im/SenseNet/sensenet?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -> **sensenet Services 7.0 beta** is out! Jump to the [Getting started](#GettingStarted) section below to start experimenting right away! +> **sensenet Services 7.0 stable** is out! Jump to the [Getting started](#GettingStarted) section below to start experimenting right away! If you need... - a **Content Repository** with a powerful query engine (built on [Lucene.Net](https://lucenenet.apache.org)) for storing *millions* of documents, @@ -46,7 +46,7 @@ Whether you're a community member or enterprise customer, feel free to visit our ## Getting started Currently we offer two different versions of sensenet ECM. We recommend version 7.0 for new projects as it is more lightweight and flexible. -### sensenet ECM 7.0 (beta) +### sensenet ECM 7.0 A modern ECM platform that can be integrated into existing or new web applications. We modularized sensenet ECM so that you can install only the parts you need. Take a look at the currently published [core components](/docs/sensenet-components.md)! There is also a number of other built-in and 3rd party [components and plugins](https://github.com/SenseNet/awesome-sensenet) that are built on this platform either by us or the community. diff --git a/docs/images/SenseNetTokenAuthentication.png b/docs/images/SenseNetTokenAuthentication.png index aec7773d8a34006d4b07e95dcd93faeec451fb7f..54766df621d4452a3eed2fcaaf708bbd6212a3a2 100644 GIT binary patch literal 34675 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 M`v{pQZSdxQ03c8oU;qFB literal 40301 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 diff --git a/docs/install-sn-from-nuget.md b/docs/install-sn-from-nuget.md index b35e97f5d..d7fe455ca 100644 --- a/docs/install-sn-from-nuget.md +++ b/docs/install-sn-from-nuget.md @@ -29,7 +29,7 @@ This is why we decided to publish two types of packages for our components: ## Installing sensenet Services -![Sense/Net Services](https://github.com/SenseNet/sn-resources/raw/master/images/sn-components/sn-components_services.png "Sense/Net Services") +![sensenet Services](https://github.com/SenseNet/sn-resources/raw/master/images/sn-components/sn-components_services.png "sensenet Services") ### Create a web project and pull in the package(s) @@ -40,7 +40,7 @@ This is why we decided to publish two types of packages for our components: [![NuGet](https://img.shields.io/nuget/v/SenseNet.Services.Install.svg)](https://www.nuget.org/packages/SenseNet.Services.Install) -> `Install-Package SenseNet.Services.Install -Pre` +> `Install-Package SenseNet.Services.Install` (this will install the other one too, no need to pull that in manually) @@ -48,7 +48,7 @@ This is why we decided to publish two types of packages for our components: [![NuGet](https://img.shields.io/nuget/v/SenseNet.Services.svg)](https://www.nuget.org/packages/SenseNet.Services) -> `Install-Package SenseNet.Services -Pre` +> `Install-Package SenseNet.Services` ### Web app changes > The install process described below is the same that you will see in the _readme.txt_ that appears in *Visual Studio* after adding the install package. diff --git a/docs/oauth.md b/docs/oauth.md new file mode 100644 index 000000000..23b6b125f --- /dev/null +++ b/docs/oauth.md @@ -0,0 +1,118 @@ +# OAuth in sensenet ECM +[OAuth 2.0](https://oauth.net/2/) is the industry-standard protocol for authorization. In [sensenet ECM](https://github.com/SenseNet/sensenet) we use it as an extension to our [web token authentication](https://community.sensenet.com/docs/web-token-authentication) to let users **authenticate** using well-known services (such as *Google* or *Facebook*). + +The benefit is that users are able to sign in to a sensenet ECM application with a single click, **without manual registration**. + +## How it works? +When new users come to the site, they will be able to sign in by clicking the Google or Facebook button (or a similar custom experience implemented by the developer). The workflow is the following: + +- User signs in to the 3rd party service. +- User authorizes the application with the service (e.g. let the application access basic user data like name and email). This is usually a click of a button in the Google or Facebook popup window. +- The client **receives a token from the service**. +- The client sends the token to the sensenet ECM server, where the appropriate **OAuth provider verifies the token**. +- If the token has been verified, we load or create the corresponding *User* content in the Content Repository. User content items are connected to the 3rd party service by storing the unique user identifier in a provider-specific separate field (e.g. *GoogleUserId*). +- sensenet ECM asssembles a [JWT token](https://community.sensenet.com/docs/web-token-authentication) for the client and consideres the user as correctly signed in. + +From that point on the user will be able to use the application as a regular user. + +### Configuration +You can specify where new users are created and their content type using the *OAuth* settings content in the usual global *Settings* folder. + +```json +{ + UserType: "User", + Domain: "Public" +} +``` + +New users are created under the domain above separated into organizational units named by the provider. + +## OAuth providers +A sensenet ECM OAuth provider is a small plugin that is designed to verify a token using a particular service. Out of the box we offer the following OAuth provider for sensenet ECM: + +- Google [![NuGet](https://img.shields.io/nuget/v/SenseNet.OAuth.Google.svg)](https://www.nuget.org/packages/SenseNet.OAuth.Google) + +These providers are available as nuget packages on the server side and npm packages on the client. Please follow the instructions in the nuget readme, these packages usually involve executing an install command before you can use them. + +## Custom OAuth provider +The OAuth provider feature is extendable by design, so developers may create a custom provider for any 3rd party service by implementing a simple api. For detailed explanation of the api elements to implement please refer to the source code documentation. + +```csharp +public class CustomOAuthProvider : OAuthProvider +{ + public override string IdentifierFieldName { get; } = "CustomUserId"; + public override string ProviderName { get; } = "myservicename"; + + public override IOAuthIdentity GetUserData(object tokenData) + { + return tokenData as OAuthIdentity; + } + + public override string VerifyToken(HttpRequestBase request, out object tokenData) + { + dynamic userData; + + try + { + userData = GetUserDataFromToken(request); + } + catch (Exception) + { + throw new InvalidOperationException("OAuth error: cannot parse user data from the request."); + } + + tokenData = new OAuthIdentity + { + Identifier = userData.sub, + Email = userData.email, + Username = userData.sub, + FullName = userData.name + }; + + return userData.sub; + } + + private static dynamic GetUserDataFromToken(HttpRequestBase request) + { + string body; + using (var reader = new StreamReader(request.InputStream)) + { + body = reader.ReadToEnd(); + } + + dynamic requestBody = JsonConvert.DeserializeObject(body); + string token = requestBody.token; + + //TODO: verify token and extract basic user data + // return userData + } +} +``` + +The example above assumes that there is a field on the User content type called *CustomUserId*. Registering this field is the responsibility of the provider install process. + +To start using your custom provider you only have to add a reference to your provider library and sensenet ECM will automatically discover and register your class. + +## Client api +If you are using the [JavaScript client SDK](https://github.com/SenseNet/sn-client-js) (as it is recommended), you do not have to deal with sending OAuth tokens to the server, it will do it for you. + +## REST api +As an alternative, you can use the native REST api when authenticating with a 3rd party OAuth service. After receiving the service-specific token, that token has to be sent to the server for verification. The api is the following: + +```text +/sn-oauth/login?provider=providername +``` + +For example: + +```javascript +$.ajax({ + url: "/sn-oauth/login?provider=google", + dataType: "json", + type: 'POST', + data: JSON.stringify({ 'token':id_token }), + success: function () { + console.log('Success'); + } +}); +``` \ No newline at end of file diff --git a/docs/sensenet-components.md b/docs/sensenet-components.md index 0a96fc92f..7954a08f5 100644 --- a/docs/sensenet-components.md +++ b/docs/sensenet-components.md @@ -14,8 +14,7 @@ This is a list of the main components we published so far. To see an expanded, c ###### Feature packages - [Workspaces](#Workspaces): Workspace-related items (content types and templates, workspace dashboards and views) for sensenet ECM. - [Workflow](#Workflow): Windows Workflow Foundation (WWF 4.5) integration into sensenet ECM. -- Content templates -- Notification +- [Notification](#Notification): Email notification component for the sensenet ECM platform. - ...and more! ###### Client SDKs @@ -77,4 +76,8 @@ The [Workspaces component](https://github.com/SenseNet/sn-workspaces) is useful ## Workflow -Integrating **Windows Workflow Foundation (WWF 4.5)** into sensenet ECM provides many possibilities for creating content-driven workflows. The [Workflow component](https://github.com/SenseNet/sn-workflow) adds a robust and customizable workflow engine to sensenet ECM. \ No newline at end of file +Integrating **Windows Workflow Foundation (WWF 4.5)** into sensenet ECM provides many possibilities for creating content-driven workflows. The [Workflow component](https://github.com/SenseNet/sn-workflow) adds a robust and customizable workflow engine to sensenet ECM. + + +## Notification +[Email notification component](https://github.com/SenseNet/sn-notification) for the sensenet ECM platform. Lets users subscribe to content changes and receive emails either almost immediately or in an aggregated way periodically about changes in the repository. \ No newline at end of file diff --git a/docs/snadmin-builtin-steps.md b/docs/snadmin-builtin-steps.md index faa746f30..75e0182ca 100644 --- a/docs/snadmin-builtin-steps.md +++ b/docs/snadmin-builtin-steps.md @@ -333,6 +333,15 @@ If the Path is a Content Repository path, than you can define a Field (optionall You can define either the *Template* or the *Regex* property for searching replaceable text, but **not both of them**. +### SetUrl +- Full name: `SenseNet.Packaging.Steps.SetUrl` +- Default property: `Url` +- Additional properties: `Site, AuthenticationType` + +Sets a url on a site content in the Content Repository. If the url is already assigned to another site, this step will fail. + +>Please make sure that a **StartRepository** step precedes this one to make sure that the repository is started. + ## JSON text ### EditJson - Full name: `SenseNet.Packaging.Steps.EditJson` @@ -475,6 +484,76 @@ Target XML after execution: ``` +### MoveXmlElement +- Full name: `SenseNet.Packaging.Steps.MoveXmlElement` +- Default property: - +- Additional properties: `Xpath, TargetXpath, TargetParentXpath, TargetName, File, Content, Field` + +Moves the given xml elements (selected by the Xpath property) as child elements under the xml element determined by the TargetXpath property. The xml can be in the file system (usually a .config file) or in the Content Repository (a field value of a content). + +If the target element does not exist, you can configure this step to create it by providing an xpath value of the *parent* element of the target (using the TargetParentXpath property) and the name of the target (TargetName property). + +>If the target is a content, please make sure that a **StartRepository** step precedes this one to make sure that the repository is started. + +For example we want to modify the “test.xml” file in the App\_Data directory. First here is the package step: +``` xml + +``` +The target XML before execution: +``` xml + + + + + + + + +``` +Target XML after execution: +``` xml + + + + + + + + + +``` + +Moving multiple elements to a new section: +``` xml + +``` +The target XML before execution: +``` xml + + + + + + + + + +``` +Target XML after execution: +``` xml + + + + + + + + + + + +``` + ### DeleteXmlNodes - Full name: `SenseNet.Packaging.Steps.DeleteXmlNodes` - Default property: - @@ -1131,7 +1210,7 @@ In the opposite case, when an error is detected in the environment and any furth ``` ### CreateEventLog -- Full name: `vSenseNet.Packaging.Steps.CreateEventLog` +- Full name: `SenseNet.Packaging.Steps.CreateEventLog` - Default property: - - Additional properties: `LogName, Machine, Sources` @@ -1142,8 +1221,8 @@ System step for creating the provided log and source in Windows Event log. ```xml ``` -##x DeleteEventLog -- Full name: `SenseNet.Packaging.Steps.CreateEventLog` +### DeleteEventLog +- Full name: `SenseNet.Packaging.Steps.DeleteEventLog` - Default property: - - Additional properties: `LogName, Machine, Sources` diff --git a/docs/snadmin-tools.md b/docs/snadmin-tools.md index c83f8ec53..40484239c 100644 --- a/docs/snadmin-tools.md +++ b/docs/snadmin-tools.md @@ -45,6 +45,26 @@ Exporting only selected (filtered) content items using the [Content Query syntax SnAdmin export source:"/Root/Sites/MySite/articles" target:"c:\localrepo" filter:"+TypeIs:Article +CreationDate:<@@CurrentDate+3days@@" ``` +## delete +Deletes a content from the repository. + +``` text +SnAdmin delete path:/Root/MyFolder/MyContent +``` + +## seturl +Setting a url on the default site: + +``` text +SnAdmin seturl url:demo.example.com +``` + +A more complex scenario: + +``` text +SnAdmin seturl url:demo.example.com site:MySite authenticationType:Windows +``` + ## index Re-create the index for the whole Content Repository (in case of a large repository this may take time). ``` text diff --git a/docs/web-token-authentication.md b/docs/web-token-authentication.md index 6a270d5a4..75726515f 100644 --- a/docs/web-token-authentication.md +++ b/docs/web-token-authentication.md @@ -1,6 +1,6 @@ # Configuration of Web Token Authentication # -In a Sense/Net web application (on all instances) you need to configure the token authentication in the _web.config_ file. +In a sensenet ECM web application (on all instances) you need to configure the token authentication in the _web.config_ file. Find the _SymmetricKeySecret_ parameter in the `tokenAuthentication` section of the sensenet section group. Give it a value of random string (16 - 64 in length) in order to make the authentication work. All your instances in the NLB should have the same value as their SymmetricKeySecret. Without this your authentication wouldn't work. Also very important to keep this random string a secret, otherwise someone can exploit it as a security breach. It is a good practice to encrypt the whole tokenAuthentication section in the web.config file. @@ -35,7 +35,15 @@ There are a few other parameters in the tokenAuthentication section that you can ## Web Token Authentication Protocol ## ### Protocol overview ### -The token authentication needs a username and password pair for its first move. After it was given and the user was successfully identified, the service generates an access token and a refresh token and sends it to the client. The client can use the access token to get to the content allowed only for authenticated users. Every token has its expiration time, so when the access token is expired the client cannot access protected content. The client has to use the refresh token to obtain a new access token. When it is received, the client can use it to access content again. The refresh token could be expired too. In that case the client has to re-authenticate with a username and password and regain access to protected content. +The token authentication needs a username and password pair for its first move. After it was +given and the user was successfully identified, the service generates an access token and a +refresh token and sends it to the client. The client can use the access token to get to the +content allowed only for authenticated users. Every token has its expiration time, so when +the access token is expired the client cannot access protected content. The same applies after +a client issued a successful logout request. The client has to use the refresh token to obtain +a new access token. When it is received, the client can use it to +access content again. The refresh token could be expired too. In that case the client has to +re-authenticate with a username and password and regain access to protected content. ### Protocol use cases in detail ### @@ -51,7 +59,7 @@ _Steps of a token refresh process from the clients' point of view:_ All the communication are sent through SSL (https). The used cookies are all HtmlOnly and Secure. There are two types of communication: header marked and uri marked (without header mark). Either of them can be choosen freely by a client developer. However the two could be mixed, but we advice to choose one and stick to it. -![web token authentication protocol](images/SenseNetTokenAuthentication.png) _figure 1: web token authentication protocol_ +![web token authentication protocol](images/SensenetTokenAuthentication.png) _figure 1: web token authentication protocol_ **LoginRequest with header mark:** _uri:_ @@ -69,36 +77,60 @@ Authorization: Basic `````` **LoginResponse:** _cookies:_ Set-Cookie: rs=`````` +Set-Cookie: ahp=`````` Set-Cookie: as=`````` _body:_ ```json {"access":"", "refresh":""} ``` +***LogoutRequest with header mark:*** +_uri:_ +```https:///``` +_headers:_ +X-Authentication-Action: ```TokenLogout``` +X-Access-Data: `````` +_cookies:_ +Cookie: as=``` ``` +Cookie: ahp=`````` +Cookie: rs=`````` + +***LogoutRequest with uri mark:*** +_uri:_ +```https:////sn-token/logout``` +_headers:_ +_cookies:_ +Cookie: as=`````` +Cookie: ahp=`````` +Cookie: rs=`````` **AuthenticatedServiceRequest with header mark:** _uri:_ ```https:///``` headers: -```X-Authentication-Type: Token``` -```X-Access-Data: ``` +X-Authentication-Action: ```TokenAccess``` +X-Access-Data: `````` _cookies:_ +Cookie: as=`````` +Cookie: ahp=`````` Cookie: rs=`````` -Cookie: as=`````` **AuthenticatedServiceRequest without header mark:** _uri:_ ```https:///``` _headers:_ -```X-Access-Data: ``` _cookies:_ -Cookie: rs=`````` -Cookie: as=`````` +Cookie: as=`````` +Cookie: ahp=`````` +Cookie: rs=`````` **UnauthenticatedServiceRequest:** _uri:_ ```https:///``` _headers:_ -X-Access-Data: `````` +_cookies:_ +Cookie: as=`````` +Cookie: ahp=`````` +Cookie: rs=`````` **ServiceResponse:** _body:_ @@ -108,11 +140,12 @@ _body:_ _uri:_ ```https:///``` _headers:_ -X-Authentication-Type: Token +X-Authentication-Action: ```TokenRefresh``` X-Refresh-Data: `````` _cookies:_ -Cookie: rs=`````` -Cookie: as=`````` +Cookie: as=`````` +Cookie: ahp=`````` +Cookie: rs=`````` **RefreshRequest with uri mark:** _uri:_ @@ -120,12 +153,14 @@ _uri:_ _headers:_ X-Refresh-Data: `````` _cookies:_ +Cookie: as=`````` +Cookie: ahp=`````` Cookie: rs=`````` -Cookie: as=`````` **RefreshResponse:** _cookies:_ Set-Cookie: as=`````` +Set-Cookie: ahp=`````` _body:_ ```json {"access":""} @@ -140,24 +175,24 @@ HTTP response with status 401 (Unauthorized). On the diagram it is used to sign ### The used headers in detail ### **_Authorization_**: this header is a standard HTTP header and tells the service, that a client would like to authenticate. Its value always begins with "Basic ", that signes a basic type authentication requires a valid username and password. **_X-Access-Data_**: this header tells the service, that a client tries to access a content with a token. Its value is an access token head and payload. -**_X-Authentication-Type_**: this header tells the service in case of header marked communication, that a token authentication is requested. Its value is always "Token". +**_X-Authentication-Action_**: this header tells the service in case of header marked communication, that a token authentication action is requested. Its value can be "TokenLogin", "TokenLogout", "TokenAccess", "TokenRefresh". **_X-Refresh-Data_**: this header tells the service, that a client tries to refresh its expired access token. Its value is a refresh token head and payload. ### The used cookies ### -**_as, rs_**: technical HttpOnly and Secure cookies for token authentication. They are emitted by token authentication service. The client does not need them and they are not subjects of change. +**_as, ahp, rs_**: technical HttpOnly and Secure cookies for token authentication. They are emitted by token authentication service. The client does not need them and they are not subjects of change. ```, ```: signature strings used by the authentication service. ```, ```: base64 and URL encoded strings. The access head and payload are the public part of a token, that consists of two parts separated by a full stop. -The first one is a technical like header that you do not have to care about. The second one - the payload - contains claims about the authenticated user and about some authentication concerning data. Once the payload has been decoded from base64 it will be a string representation of a JSON object, so it can be easily use in Javascript. +The first one is a technical like header that you do not have to care about. The second one - the payload - contains claims about the authenticated user and about some authentication concerning data. Once the payload has been decoded from base64 it will be a string representation of a JSON object, so it can be easily used in Javascript. **Example of a typical payload:** ```json {"iss":"sensenet-token-service","sub":"sensenet","aud":"client","exp":1490577801,"iat":1490577501,"nbf":1490577501,"name":"Joe"} ``` -### The used claims in the Sense/Net tokens: +### The used claims in the sensenet ECM tokens: **_iss_**: `issuer` identifies the principal that issued the token **_sub_**: `subject` identifies the principal that is the subject of the token **_aud_**: `audience` identifies the recipients that the token is intended for @@ -170,4 +205,23 @@ The _iss, sub, aud_ claims can be configured and remains the same unless you cha ## Considerations for client developers ## -Once the client application has got the access token and the refresh token, it has to persist them preferably in some local browser storage for later usage. However the refresh token also contains the same claims as the access token, its claims - at least _iat, nbf_ and _exp_ - have different values. It happens because of their different use. An access token will be immediately valid and accepted after its creation, but the refresh token is not. The refresh token will be valid and accepted by the service only when the access token is expired. Therefore the client should extract the expiration time of the tokens into an application lifetime variable and constantly check it when the client try to access a content. Content access request have to include the access token into the according HTTP header (specified as _AuthenticatedServiceRequest_ earlier). In case when the access tokens expiration check results true the client must check the refresh token's expiration. If this results false, the client have to send a _RefreshRequest_ (specified earlier) to the service. A _RefreshRequest_ will reply with a new access token, that must replace the old one. If the check results true, the client cannot access protected content unless sending a new _LoginRequest_ to the service with the username and password of the user. Because of the sensitive nature of user's credentials, we do not recommend the client to persist them. As the lifetime of both tokens can be changed in the service's configuration, it is very important to choose them wisely to support the fluent communication between the two part. Wrong settings can disrupt efficiency of turn arounds and slow down the whole system. +Once the client application has got the access token and the refresh token, +it has to persist them preferably in some local browser storage for later usage. +(The access token will have any use if the client applies header mark communication.) +However the refresh token also contains the same claims as the access token, its claims - +at least _iat, nbf_ and _exp_ - have different values. It happens because of their different +use. An access token will be immediately valid and accepted after its creation, but the +refresh token is not. The refresh token will be valid and accepted by the service only when +the access token is expired. Therefore the client should extract the expiration time of the +tokens into an application lifetime variable and constantly check it when the client tries to +access a content. Content access request have to include the access token into the according +HTTP header (specified as _AuthenticatedServiceRequest_ earlier). In case when the access +tokens expiration check results true the client must check the refresh token's expiration. +If this results false, the client have to send a _RefreshRequest_ (specified earlier) to the +service. A _RefreshRequest_ will reply with a new access token, that must replace the old one. +If the check results true, the client cannot access protected content unless sending a new +_LoginRequest_ to the service with the username and password of the user. Because of the +sensitive nature of user's credentials, we do not recommend the client to persist them. +As the lifetime of both tokens can be changed in the service's configuration, it is very +important to choose them wisely to support the fluent communication between the two part. +Wrong settings can disrupt efficiency of turn arounds and slow down the whole system. diff --git a/src/BlobStorage/BlobStorage.nuspec b/src/BlobStorage/BlobStorage.nuspec index 49270ecea..4c6f3c7b0 100644 --- a/src/BlobStorage/BlobStorage.nuspec +++ b/src/BlobStorage/BlobStorage.nuspec @@ -2,7 +2,7 @@ SenseNet.BlobStorage - 7.0.0-beta2 + 7.0.0 sensenet ECM BlobStorage library kavics,aniko,laci,borsi,lajos,tusmester Sense/Net @@ -11,12 +11,12 @@ https://raw.githubusercontent.com/SenseNet/sn-resources/master/images/sn-icon/sensenet-icon-64.png false BlobStorage library for the sensenet ECM platform containing the base API for handling binaries directly. - Initial release. + First stable release. Copyright © Sense/Net Inc. sensenet ecm ecms - + diff --git a/src/BlobStorage/Properties/AssemblyInfo.cs b/src/BlobStorage/Properties/AssemblyInfo.cs index f0c5a0f30..c6445659e 100644 --- a/src/BlobStorage/Properties/AssemblyInfo.cs +++ b/src/BlobStorage/Properties/AssemblyInfo.cs @@ -19,4 +19,4 @@ [assembly: AssemblyCulture("")] [assembly: AssemblyVersion("7.0.0.0")] [assembly: AssemblyFileVersion("7.0.0.0")] -[assembly: AssemblyInformationalVersion("7.0.0-beta2")] +[assembly: AssemblyInformationalVersion("7.0.0")] diff --git a/src/Common/Common.nuspec b/src/Common/Common.nuspec index 3fe0c1bb8..8c47fe3df 100644 --- a/src/Common/Common.nuspec +++ b/src/Common/Common.nuspec @@ -2,7 +2,7 @@ SenseNet.Common - 7.0.0-beta2 + 7.0.0 sensenet ECM Common library kavics,aniko,laci,borsi,lajos,tusmester Sense/Net @@ -11,7 +11,7 @@ https://raw.githubusercontent.com/SenseNet/sn-resources/master/images/sn-icon/sensenet-icon-64.png false Common library for the sensenet ECM platform containing storage and configuration-related components. - Initial release. + First stable release. Copyright © Sense/Net Inc. sensenet ecm ecms diff --git a/src/Common/Properties/AssemblyInfo.cs b/src/Common/Properties/AssemblyInfo.cs index 5d0a38457..9d84731a9 100644 --- a/src/Common/Properties/AssemblyInfo.cs +++ b/src/Common/Properties/AssemblyInfo.cs @@ -21,4 +21,4 @@ [assembly: AssemblyCulture("")] [assembly: AssemblyVersion("7.0.0.0")] [assembly: AssemblyFileVersion("7.0.0.0")] -[assembly: AssemblyInformationalVersion("7.0.0-beta2")] +[assembly: AssemblyInformationalVersion("7.0.0")] diff --git a/src/Common/Readme.md b/src/Common/Readme.md index 6d058ab73..1cc0c5429 100644 --- a/src/Common/Readme.md +++ b/src/Common/Readme.md @@ -1,3 +1,3 @@ -# Markdown file +# Common library -Sense/Net Product level common knowledge strictly. +A library containing sensenet ECM product-level common knowledge. diff --git a/src/Configuration/Properties/AssemblyInfo.cs b/src/Configuration/Properties/AssemblyInfo.cs index 314a0aa15..f3f47ad70 100644 --- a/src/Configuration/Properties/AssemblyInfo.cs +++ b/src/Configuration/Properties/AssemblyInfo.cs @@ -16,7 +16,7 @@ [assembly: AssemblyCulture("")] [assembly: AssemblyVersion("7.0.0.0")] [assembly: AssemblyFileVersion("7.0.0.0")] -[assembly: AssemblyInformationalVersion("7.0.0-beta4")] +[assembly: AssemblyInformationalVersion("7.0.0")] [assembly: ComVisible(false)] [assembly: Guid("dfbdb163-d9bb-481c-b3fd-e9eb0e37d27d")] diff --git a/src/ContentRepository/ApplicationModel/ActionFramework.cs b/src/ContentRepository/ApplicationModel/ActionFramework.cs index 7467f203a..ffb05dcf2 100644 --- a/src/ContentRepository/ApplicationModel/ActionFramework.cs +++ b/src/ContentRepository/ApplicationModel/ActionFramework.cs @@ -175,9 +175,10 @@ public static IEnumerable GetActions(Content context, string scenari // if the scenario name is given, try to load actions in that scenario var sc = ScenarioManager.GetScenario(scenario, scenarioParameters); if (sc != null) + { return sc.GetActions(context, backUri); + } } - return GetActionsFromContentRepository(context, scenario, backUri); } diff --git a/src/ContentRepository/Content.cs b/src/ContentRepository/Content.cs index bb8ca5ad5..825582a30 100644 --- a/src/ContentRepository/Content.cs +++ b/src/ContentRepository/Content.cs @@ -1527,13 +1527,8 @@ public static Content Import( { SnLog.WriteException(ex); - var console = RepositoryInstance.Instance != null && RepositoryInstance.Instance.StartSettings != null - ? RepositoryInstance.Instance.StartSettings.Console - : null; - // log this to the screen or log file if exists - if (console != null) - console.WriteLine("---------- Reference skipped: " + field.Name); + RepositoryInstance.Instance?.Console?.WriteLine("---------- Reference skipped: " + field.Name); } else { @@ -1646,13 +1641,8 @@ public bool ImportFieldData(ImportContext context, bool saveContent) { SnLog.WriteException(ex); - var console = RepositoryInstance.Instance != null && RepositoryInstance.Instance.StartSettings != null - ? RepositoryInstance.Instance.StartSettings.Console - : null; - // log this to the screen or log file if exists - if (console != null) - console.WriteLine("---------- Reference skipped: " + refField.Name); + RepositoryInstance.Instance?.Console?.WriteLine("---------- Reference skipped: " + refField.Name); } else { diff --git a/src/ContentRepository/ContentList.cs b/src/ContentRepository/ContentList.cs index 4e3e2f621..5e4c8d1e0 100644 --- a/src/ContentRepository/ContentList.cs +++ b/src/ContentRepository/ContentList.cs @@ -1073,12 +1073,12 @@ private void StartSubscription() } // reflection: because we do not have access to the workflow engine here - var t = TypeResolver.GetType("SenseNet.Workflow.InstanceManager"); - if (t != null) - { - var m = t.GetMethod("Start", BindingFlags.Static | BindingFlags.Public); - m.Invoke(null, new object[] { workflowC.ContentHandler }); - } + var t = TypeResolver.GetType("SenseNet.Workflow.InstanceManager", false); + if (t == null) + return; + + var m = t.GetMethod("Start", BindingFlags.Static | BindingFlags.Public); + m.Invoke(null, new object[] { workflowC.ContentHandler }); } private static Node GetMailProcessorWorkflowContainer(Node contextNode) diff --git a/src/ContentRepository/ContentRepository.csproj b/src/ContentRepository/ContentRepository.csproj index eddcc5c9c..071afc062 100644 --- a/src/ContentRepository/ContentRepository.csproj +++ b/src/ContentRepository/ContentRepository.csproj @@ -97,6 +97,9 @@ ..\packages\OpenPop.NET.2.0.6.1120\lib\net40\OpenPop.dll True + + ..\packages\SenseNet.Preview.7.0.0\lib\net451\SenseNet.Preview.dll + ..\packages\SenseNet.Security.2.3.0.0\lib\net45\SenseNet.Security.dll True @@ -180,6 +183,7 @@ + @@ -233,9 +237,12 @@ + + + @@ -277,7 +284,6 @@ - @@ -507,7 +513,9 @@ Designer - + + Designer + ContentTypeDefinition.xsd @@ -556,10 +564,6 @@ {A453E920-29C0-45CD-984C-0D8E3631B1E3} Common - - {01ae54af-db4e-431e-a3f9-fbcd2ad73d03} - Preview - {5DB4DDBA-81F6-4D81-943A-18F3178B3355} Storage diff --git a/src/ContentRepository/ContentTemplate.cs b/src/ContentRepository/ContentTemplate.cs index 40e9416da..928c3fa5b 100644 --- a/src/ContentRepository/ContentTemplate.cs +++ b/src/ContentRepository/ContentTemplate.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using SenseNet.Configuration; -using SenseNet.Preview; using SenseNet.ContentRepository.Storage; using SenseNet.ContentRepository.Storage.Search; using SenseNet.ContentRepository.Storage.Security; @@ -11,6 +10,7 @@ using SenseNet.Search; using SenseNet.ContentRepository.Storage.Schema; using SenseNet.ContentRepository.Fields; +using SenseNet.ContentRepository.Storage.Events; using SenseNet.Security; using SenseNet.Tools; @@ -18,7 +18,9 @@ namespace SenseNet.ContentRepository { public sealed class ContentTemplate { - public static readonly string NOTIFOBSERVERNAME = "SenseNet.Messaging.NotificationObserver"; + [Obsolete("Use the SenseNet.ContentRepository.Storage.Events.NodeObserverNames class instead.", true)] + public static readonly string NOTIFOBSERVERNAME = "SenseNet.Notification.NotificationObserver"; + [Obsolete("Use the SenseNet.ContentRepository.Storage.Events.NodeObserverNames class instead.", true)] public static readonly string WFOBSERVERNAME = "SenseNet.Workflow.WorkflowNotificationObserver"; /// @@ -525,9 +527,9 @@ private static void UpdateLocalReferences(Node templateRoot, Node targetRoot) continue; content.ContentHandler.NodeOperation = NodeOperation.TemplateCreation; - content.ContentHandler.DisableObserver(typeof(DocumentPreviewObserver)); - content.ContentHandler.DisableObserver(TypeResolver.GetType(WFOBSERVERNAME)); - content.ContentHandler.DisableObserver(TypeResolver.GetType(NOTIFOBSERVERNAME)); + content.ContentHandler.DisableObserver(TypeResolver.GetType(NodeObserverNames.DOCUMENTPREVIEW, false)); + content.ContentHandler.DisableObserver(TypeResolver.GetType(NodeObserverNames.WORKFLOWNOTIFICATION, false)); + content.ContentHandler.DisableObserver(TypeResolver.GetType(NodeObserverNames.NOTIFICATION, false)); content.SaveSameVersion(); } diff --git a/src/ContentRepository/GenericContent.cs b/src/ContentRepository/GenericContent.cs index 2d8c27136..6916abfd4 100644 --- a/src/ContentRepository/GenericContent.cs +++ b/src/ContentRepository/GenericContent.cs @@ -1311,7 +1311,7 @@ public override void Save(NodeSaveSettings settings) public void KeepWorkflowsAlive() { _keepWorkflowsAlive = true; - DisableObserver(TypeResolver.GetType(NodeObserverNames.WORKFLOWNOTIFICATION)); + DisableObserver(TypeResolver.GetType(NodeObserverNames.WORKFLOWNOTIFICATION, false)); } private void UpdateRelatedWorkflows() diff --git a/src/ContentRepository/Packaging/Steps/SetUrl.cs b/src/ContentRepository/Packaging/Steps/SetUrl.cs new file mode 100644 index 000000000..f5782b778 --- /dev/null +++ b/src/ContentRepository/Packaging/Steps/SetUrl.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using SenseNet.ContentRepository; + +namespace SenseNet.Packaging.Steps +{ + public class SetUrl : Step + { + public string Site { get; set; } = "Default_Site"; + [DefaultProperty] + public string Url { get; set; } + + public string AuthenticationType { get; set; } = "Forms"; + + public override void Execute(ExecutionContext context) + { + context.AssertRepositoryStarted(); + + var siteName = context.ResolveVariable(Site) as string; + if (string.IsNullOrEmpty(siteName)) + throw new InvalidParameterException("Site name cannot be empty."); + + var url = context.ResolveVariable(Url) as string; + if (string.IsNullOrEmpty(url)) + throw new InvalidParameterException("Site url cannot be empty."); + + var site = Content.All.FirstOrDefault(c => c.InTree("/Root/Sites") && c.TypeIs("Site") && c.Name == siteName); + if (site == null) + throw new InvalidOperationException($"Site not found: {siteName}"); + + var authType = context.ResolveVariable(AuthenticationType) as string; + if (string.IsNullOrEmpty(authType)) + throw new InvalidOperationException("Authentication type cannot be empty."); + + Logger.LogMessage("Setting url {0} on site {1} with auth type {2}.", url, siteName, authType); + + var urlList = (IDictionary) site["UrlList"]; + urlList[url] = authType; + + site["UrlList"] = urlList; + site.SaveSameVersion(); + } + } +} diff --git a/src/ContentRepository/Packaging/Steps/Step.cs b/src/ContentRepository/Packaging/Steps/Step.cs index 5d87160a8..6be009cf8 100644 --- a/src/ContentRepository/Packaging/Steps/Step.cs +++ b/src/ContentRepository/Packaging/Steps/Step.cs @@ -212,6 +212,11 @@ protected XmlNodeList SelectXmlNodes(XmlDocument doc, string xpath) return doc.SelectNodes(xpath, nsmgr); } + protected XmlNode SelectXmlNode(XmlDocument doc, string xpath) + { + return SelectXmlNodes(doc, xpath)?.Cast().FirstOrDefault(); + } + #region =========================================================== Public instance part =========================================================== /// Returns the XML name of the step element in the manifest. Default: simple or fully qualified name of the class. public virtual string ElementName { get { return this.GetType().Name; } } diff --git a/src/ContentRepository/Packaging/Steps/XmlEditorStep.cs b/src/ContentRepository/Packaging/Steps/XmlEditorStep.cs index fe34ad589..f38653f4d 100644 --- a/src/ContentRepository/Packaging/Steps/XmlEditorStep.cs +++ b/src/ContentRepository/Packaging/Steps/XmlEditorStep.cs @@ -89,7 +89,14 @@ private void ExecuteOnFile(ExecutionContext context) if (!EditXml(doc, path)) return; - using (var writer = XmlWriter.Create(path, new XmlWriterSettings { Indent = true, OmitXmlDeclaration = omitXmlDeclaration })) + var settings = new XmlWriterSettings + { + Indent = true, + OmitXmlDeclaration = omitXmlDeclaration, + CloseOutput = true + }; + + using (var writer = XmlWriter.Create(path, settings)) doc.Save(writer); } } @@ -292,4 +299,48 @@ protected override bool EditXml(XmlDocument doc, string path) } } + public class MoveXmlElement : XmlEditorStep + { + public string TargetXpath { get; set; } + public string TargetParentXpath { get; set; } + public string TargetName { get; set; } + + protected override bool EditXml(XmlDocument doc, string path) + { + var target = SelectXmlNode(doc, TargetXpath); + if (target == null) + { + if (string.IsNullOrEmpty(TargetParentXpath) || string.IsNullOrEmpty(TargetName)) + { + throw new InvalidStepParameterException( + "Target xml node does not exist. If you want it to be created, please provide the TargetParentXpath and TargetName properties."); + } + + var targetParent = SelectXmlNode(doc, TargetParentXpath); + if (targetParent == null) + throw new InvalidStepParameterException("Parent of target xml node does not exist."); + + target = doc.CreateElement(TargetName); + targetParent.AppendChild(target); + } + + var moveCount = 0; + + foreach (XmlNode node in SelectXmlNodes(doc, this.Xpath)) + { + // this will move the element from its original place to the new parent + target.AppendChild(node); + moveCount++; + } + + switch (moveCount) + { + case 0: Logger.LogMessage("Nothing to move."); break; + case 1: Logger.LogMessage("Moving 1 element."); break; + default: Logger.LogMessage("Moving {0} elements.", moveCount); break; + } + + return moveCount > 0; + } + } } diff --git a/src/ContentRepository/Preview/DocumentPreviewObserver.cs b/src/ContentRepository/Preview/DocumentPreviewObserver.cs deleted file mode 100644 index 04a1db8b2..000000000 --- a/src/ContentRepository/Preview/DocumentPreviewObserver.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System; -using System.Linq; -using SenseNet.ContentRepository; -using SenseNet.ContentRepository.Storage.Events; -using SenseNet.ContentRepository.Storage; -using SenseNet.BackgroundOperations; -using System.Web; -using SenseNet.TaskManagement.Core; - -namespace SenseNet.Preview -{ - public class DocumentPreviewObserver : NodeObserver - { - private static readonly string[] MONITORED_FIELDS = new[] { "Binary", "Version", "Locked", "SavingState" }; - - // ================================================================================= Observer methods - - protected override void OnNodeCreating(object sender, CancellableNodeEventArgs e) - { - base.OnNodeCreating(sender, e); - - if (SkipWhenCreation(e.SourceNode)) - return; - - DocumentPreviewProvider.InitializePreviewGeneration(e.SourceNode); - } - protected override void OnNodeCreated(object sender, NodeEventArgs e) - { - base.OnNodeCreated(sender, e); - - if (SkipWhenCreation(e.SourceNode)) - return; - - DocumentPreviewProvider.StartPreviewGeneration(e.SourceNode, GetPriority(e.SourceNode as File)); - } - private bool SkipWhenCreation(Node sourceNode) - { - if (sourceNode.CopyInProgress) - return true; - return false; - } - - - protected override void OnNodeModified(object sender, NodeEventArgs e) - { - base.OnNodeModified(sender, e); - - // check: fire only when the relevant fields had been modified (binary, version, ...) - if (!e.ChangedData.Any(d => MONITORED_FIELDS.Contains(d.Name))) - return; - - var versionData = e.ChangedData.FirstOrDefault(d => d.Name.Equals("Version", StringComparison.OrdinalIgnoreCase)); - if (versionData != null) - { - var originalVersion = VersionNumber.Parse(versionData.Original.ToString()); - - // if the status changed from Locked to not locked, and the version number has been decreesed: undo or checkin - if (originalVersion.Status == VersionStatus.Locked && - e.SourceNode.Version.Status != VersionStatus.Locked && - originalVersion > e.SourceNode.Version && - DocumentPreviewProvider.Current.IsContentSupported(e.SourceNode)) - { - // Undo or Checkin: we will start to delete unnecessary images for the - // removed version (w/o waiting for the delete operations to complete). - DocumentPreviewProvider.Current.RemovePreviewImagesAsync(e.SourceNode.Id, originalVersion); - - // This was an UNDO operation, it is unnecessary to start a preview generator process - if (string.Compare(NodeOperation.UndoCheckOut, e.SourceNode.NodeOperation, StringComparison.OrdinalIgnoreCase) == 0) - return; - } - } - - DocumentPreviewProvider.StartPreviewGeneration(e.SourceNode, GetPriority(e.SourceNode as File)); - } - - private static TaskPriority GetPriority(File file) - { - if (file != null) - return file.PreviewGenerationPriority; - - return TaskPriority.Normal; - } - } -} diff --git a/src/ContentRepository/Preview/DocumentPreviewProvider.cs b/src/ContentRepository/Preview/DocumentPreviewProvider.cs index fb8def7fe..67515881f 100644 --- a/src/ContentRepository/Preview/DocumentPreviewProvider.cs +++ b/src/ContentRepository/Preview/DocumentPreviewProvider.cs @@ -344,12 +344,12 @@ protected static void SavePageCount(File file, int pageCount) using (new SystemAccount()) { - var x = Retrier.Retry(3, 100, + Retrier.Retry(3, 100, () => { // try file.PageCount = pageCount; - file.DisableObserver(typeof(DocumentPreviewObserver)); + file.DisableObserver(TypeResolver.GetType(NodeObserverNames.DOCUMENTPREVIEW, false)); file.DisableObserver(TypeResolver.GetType(NodeObserverNames.NOTIFICATION, false)); file.KeepWorkflowsAlive(); @@ -446,11 +446,15 @@ protected static void CheckPreviewImages(Content content, int start, int end) protected static IEnumerable QueryPreviewImages(string path) { - return NodeQuery.QueryNodesByTypeAndPath(NodeType.GetByName(PREVIEWIMAGE_CONTENTTYPE), false, path + "/", false) + var previewType = ActiveSchema.NodeTypes[PREVIEWIMAGE_CONTENTTYPE]; + if (previewType == null) + return new Node[0]; + + return NodeQuery.QueryNodesByTypeAndPath(previewType, false, path + "/", false) .Identifiers - .Select(i => NodeHead.Get(i)) + .Select(NodeHead.Get) .Where(h => (h != null) && h.Name.StartsWith("preview", StringComparison.OrdinalIgnoreCase)) - .Select(h => Node.LoadNode(h)) + .Select(Node.LoadNode) .Where(x => x != null) .OrderBy(p => p.Index); } @@ -585,17 +589,29 @@ protected static IEnumerable BreakTextIntoLines(WatermarkDrawingInfo inf public virtual bool IsPreviewOrThumbnailImage(NodeHead imageHead) { - return (imageHead != null && - imageHead.GetNodeType().IsInstaceOfOrDerivedFrom(ActiveSchema.NodeTypes[PREVIEWIMAGE_CONTENTTYPE]) && - imageHead.Path.Contains(RepositoryPath.PathSeparator + PREVIEWS_FOLDERNAME + RepositoryPath.PathSeparator)) && - new Regex(PREVIEW_THUMBNAIL_REGEX).IsMatch(imageHead.Name); + if (imageHead == null) + return false; + + var previewType = ActiveSchema.NodeTypes[PREVIEWIMAGE_CONTENTTYPE]; + if (previewType == null) + return false; + + return imageHead.GetNodeType().IsInstaceOfOrDerivedFrom(previewType) && + imageHead.Path.Contains(RepositoryPath.PathSeparator + PREVIEWS_FOLDERNAME + RepositoryPath.PathSeparator) && + new Regex(PREVIEW_THUMBNAIL_REGEX).IsMatch(imageHead.Name); } public virtual bool IsThumbnailImage(Image image) { - return (image != null && - image.NodeType.IsInstaceOfOrDerivedFrom(ActiveSchema.NodeTypes[PREVIEWIMAGE_CONTENTTYPE]) && - new Regex(THUMBNAIL_REGEX).IsMatch(image.Name)); + if (image == null) + return false; + + var previewType = ActiveSchema.NodeTypes[PREVIEWIMAGE_CONTENTTYPE]; + if (previewType == null) + return false; + + return image.NodeType.IsInstaceOfOrDerivedFrom(previewType) && + new Regex(THUMBNAIL_REGEX).IsMatch(image.Name); } public bool HasPreviewPermission(NodeHead nodeHead) @@ -1237,7 +1253,11 @@ protected internal virtual bool StartCopyingPreviewImages(Node previousVersion, return false; // collect all preview and thumbnail images - var previewIds = NodeQuery.QueryNodesByTypeAndPath(NodeType.GetByName(PREVIEWIMAGE_CONTENTTYPE), false, prevFolder.Path + RepositoryPath.PathSeparator, true).Identifiers.ToList(); + var previewType = NodeType.GetByName(PREVIEWIMAGE_CONTENTTYPE); + if (previewType == null) + return false; + + var previewIds = NodeQuery.QueryNodesByTypeAndPath(previewType, false, prevFolder.Path + RepositoryPath.PathSeparator, true).Identifiers.ToList(); if (previewIds.Count == 0) return false; diff --git a/src/ContentRepository/Properties/AssemblyInfo.cs b/src/ContentRepository/Properties/AssemblyInfo.cs index 92d886330..58cb9ec8a 100644 --- a/src/ContentRepository/Properties/AssemblyInfo.cs +++ b/src/ContentRepository/Properties/AssemblyInfo.cs @@ -20,4 +20,4 @@ [assembly: AssemblyCulture("")] [assembly: AssemblyVersion("7.0.0.0")] [assembly: AssemblyFileVersion("7.0.0.0")] -[assembly: AssemblyInformationalVersion("7.0.0-beta4")] +[assembly: AssemblyInformationalVersion("7.0.0")] diff --git a/src/ContentRepository/Repository.cs b/src/ContentRepository/Repository.cs index fbbc1db61..3275bc39a 100644 --- a/src/ContentRepository/Repository.cs +++ b/src/ContentRepository/Repository.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using SenseNet.ContentRepository.Storage; using SenseNet.ContentRepository.Storage.Security; using SenseNet.Configuration; @@ -57,6 +58,11 @@ public static RepositoryInstance Start(RepositoryStartSettings settings) AccessProvider.RestoreOriginalUser(); return instance; } + public static RepositoryInstance Start(RepositoryBuilder builder) + { + return builder == null ? Start() : Start((RepositoryStartSettings) builder); + } + /// /// Returns the running state of the Repository. /// diff --git a/src/ContentRepository/RepositoryBuilder.cs b/src/ContentRepository/RepositoryBuilder.cs new file mode 100644 index 000000000..05cc8a760 --- /dev/null +++ b/src/ContentRepository/RepositoryBuilder.cs @@ -0,0 +1,145 @@ +using System; +using SenseNet.ContentRepository.Storage; +using SenseNet.ContentRepository.Storage.Data; +using SenseNet.ContentRepository.Storage.Security; +using SenseNet.Security; + +namespace SenseNet.ContentRepository +{ + /// + /// Settings and provider builder class that controls the startup options and provider + /// instances when a sensenet ECM repository starts. + /// + public class RepositoryBuilder : RepositoryStartSettings + { + /// + /// Sets the data provider used for all db operations in the system. + /// + /// DataProvider instance. + public RepositoryBuilder UseDataProvider(DataProvider dataProvider) + { + Configuration.Providers.Instance.DataProvider = dataProvider; + return this; + } + /// + /// Sets the access provider responsible for user-related technical operations in the system. + /// + /// AccessProvider instance. + public RepositoryBuilder UseAccessProvider(AccessProvider accessProvider) + { + Configuration.Providers.Instance.AccessProvider = accessProvider; + return this; + } + /// + /// Sets the security data provider used for all security db operations in the system. + /// + /// ISecurityDataProvider instance. + public RepositoryBuilder UseSecurityDataProvider(ISecurityDataProvider securityDataProvider) + { + Configuration.Providers.Instance.SecurityDataProvider = securityDataProvider; + return this; + } + /// + /// Sets the elevated modification visibility rule provider. + /// + public RepositoryBuilder UseElevatedModificationVisibilityRuleProvider(ElevatedModificationVisibilityRule modificationVisibilityRuleProvider) + { + Configuration.Providers.Instance.ElevatedModificationVisibilityRuleProvider = modificationVisibilityRuleProvider; + return this; + } + + /// + /// General API for defining a provider instance that will be injected into and can be loaded + /// from the Providers.Instance store. + /// + /// Name of the provider. + /// Provider instance. + public RepositoryBuilder UseProvider(string providerName, object provider) + { + Configuration.Providers.Instance.SetProvider(providerName, provider); + return this; + } + /// + /// General API for defining a provider instance that will be injected into and can be loaded + /// from the Providers.Instance store. + /// + /// Type of the provider. + /// Provider instance. + public RepositoryBuilder UseProvider(Type providerType, object provider) + { + Configuration.Providers.Instance.SetProvider(providerType, provider); + return this; + } + + /// + /// Set this value to false if your tool does not need Content search and modification features + /// (e.g. save, move etc.). Default is true. + /// + /// + /// If your tool needs to query for content and querying is switched off by this method, + /// you may call the RepositoryInstance.StartLucene() method later. + /// + public new RepositoryBuilder StartLuceneManager(bool start = true) + { + base.StartLuceneManager = start; + return this; + } + public new RepositoryBuilder RestoreIndex(bool restore = true) + { + base.RestoreIndex = restore; + return this; + } + public new RepositoryBuilder IsWebContext(bool webContext = false) + { + base.IsWebContext = webContext; + return this; + } + /// + /// Gets or sets a value that is 'true' if the Lucene index will be backed up before your tool exits. Default: false + /// + public new RepositoryBuilder BackupIndexAtTheEnd(bool backup = false) + { + base.BackupIndexAtTheEnd = backup; + return this; + } + /// + /// Instructs the system to start the workflow engine during startup. + /// + /// + /// If your tool needs to run the workflow engine and its running is postponed (StartWorkflowEngine = false), + /// call the RepositoryInstance.StartWorkflowEngine() method. + /// + public new RepositoryBuilder StartWorkflowEngine(bool start = true) + { + base.StartWorkflowEngine = start; + return this; + } + /// + /// Sets a local directory path of plugins if it is different from your tool's path. + /// Default is null that means the plugins are placed in the appdomain's working directory. + /// + public RepositoryBuilder SetPluginsPath(string path) + { + PluginsPath = path; + return this; + } + /// + /// Sets a local directory path of index if it is different from configured path. + /// Default is empty that means the application uses the configured index path. + /// + public RepositoryBuilder SetIndexPath(string path) + { + IndexPath = path; + return this; + } + /// + /// Sets a TextWriter instance. Can be null. If it is not null, the startup sequence + /// will be traced to the provided textwriter. + /// + public RepositoryBuilder SetConsole(System.IO.TextWriter console) + { + Console = console; + return this; + } + } +} diff --git a/src/ContentRepository/RepositoryInstance.cs b/src/ContentRepository/RepositoryInstance.cs index b2fd4b8bb..b0182ca70 100644 --- a/src/ContentRepository/RepositoryInstance.cs +++ b/src/ContentRepository/RepositoryInstance.cs @@ -10,12 +10,14 @@ using SenseNet.Diagnostics; using SenseNet.ContentRepository.Storage.Data; using System.Diagnostics; +using System.IO; using System.Threading; using SenseNet.Communication.Messaging; using SenseNet.ContentRepository.Storage.Diagnostics; using SenseNet.TaskManagement.Core; using SenseNet.BackgroundOperations; using SenseNet.Configuration; +using SenseNet.ContentRepository.Security; using SenseNet.Tools; namespace SenseNet.ContentRepository @@ -74,15 +76,17 @@ public class StartupInfo /// /// Gets the startup control information. /// - public RepositoryStartSettings.ImmutableRepositoryStartSettings StartSettings - { - get { return _settings; } - } + [Obsolete("Use individual immutable properties instead.")] + public RepositoryStartSettings.ImmutableRepositoryStartSettings StartSettings => _settings; + /// /// Gets the started up instance or null. /// public static RepositoryInstance Instance { get { return _instance; } } + public TextWriter Console => _settings?.Console; + public bool BackupIndexAtTheEnd => _settings?.BackupIndexAtTheEnd ?? false; + private RepositoryInstance() { _startupInfo = new StartupInfo { Starting = DateTime.UtcNow }; @@ -150,6 +154,8 @@ internal void DoStart() LoggingSettings.SnTraceConfigurator.UpdateCategories(); + InitializeOAuthProviders(); + ConsoleWriteLine(); ConsoleWriteLine("Repository has started."); ConsoleWriteLine(); @@ -310,6 +316,38 @@ private void StartManagers() private List serviceInstances; + private static void InitializeOAuthProviders() + { + var providerTypeNames = new List(); + + foreach (var providerType in TypeResolver.GetTypesByBaseType(typeof(OAuthProvider)).Where(t => !t.IsAbstract)) + { + var provider = TypeResolver.CreateInstance(providerType.FullName) as OAuthProvider; + if (provider == null) + continue; + + if (string.IsNullOrEmpty(provider.ProviderName)) + { + SnLog.WriteWarning($"OAuth provider type {providerType.FullName} does not expose a valid ProviderName value, therefore cannot be initialized."); + continue; + } + if (string.IsNullOrEmpty(provider.IdentifierFieldName)) + { + SnLog.WriteWarning($"OAuth provider type {providerType.FullName} does not expose a valid IdentifierFieldName value, therefore cannot be initialized."); + continue; + } + + Providers.Instance.SetProvider(provider.GetProviderRegistrationName(), provider); + providerTypeNames.Add($"{providerType.FullName} ({provider.ProviderName})"); + } + + if (providerTypeNames.Any()) + { + SnLog.WriteInformation("OAuth providers registered: " + Environment.NewLine + + string.Join(Environment.NewLine, providerTypeNames)); + } + } + private static void InitializeLogger() { var logSection = ConfigurationManager.GetSection("loggingConfiguration"); @@ -425,7 +463,7 @@ internal static void Shutdown() SnTrace.Repository.Write("Shutting down {0}", DistributedApplication.ClusterChannel.GetType().Name); DistributedApplication.ClusterChannel.ShutDown(); - if (Instance.StartSettings.BackupIndexAtTheEnd) + if (Instance.BackupIndexAtTheEnd) { SnTrace.Repository.Write("Backing up the index."); if (LuceneManagerIsRunning) diff --git a/src/ContentRepository/Schema/ContentType.cs b/src/ContentRepository/Schema/ContentType.cs index 1d04f9079..d85595bda 100644 --- a/src/ContentRepository/Schema/ContentType.cs +++ b/src/ContentRepository/Schema/ContentType.cs @@ -824,9 +824,9 @@ private static void CheckFieldValidation(FieldDescriptor fieldDesc, string conte { if (fieldDesc.Analyzer != null) { - var analyzerType = TypeResolver.GetType(fieldDesc.Analyzer); + var analyzerType = TypeResolver.GetType(fieldDesc.Analyzer, false); if (analyzerType == null) - throw new RegistrationException(String.Concat("Unknown analyzer: ", fieldDesc.Analyzer, ". Field: ", fieldDesc.FieldName, ", ContentType: ", contentTypeName)); + throw new RegistrationException(string.Concat("Unknown analyzer: ", fieldDesc.Analyzer, ". Field: ", fieldDesc.FieldName, ", ContentType: ", contentTypeName)); } } // ==================================================== IFolder diff --git a/src/ContentRepository/Schema/ContentTypeInstaller.cs b/src/ContentRepository/Schema/ContentTypeInstaller.cs index 2a1e69a66..10a668643 100644 --- a/src/ContentRepository/Schema/ContentTypeInstaller.cs +++ b/src/ContentRepository/Schema/ContentTypeInstaller.cs @@ -1,14 +1,10 @@ using System; using System.Collections.Generic; -using System.Text; -using SenseNet.ContentRepository.Storage; using SenseNet.ContentRepository.Storage.Events; using SenseNet.ContentRepository.Storage.Schema; using System.IO; using System.Xml.XPath; -using System.Diagnostics; using SenseNet.Configuration; -using SenseNet.ContentRepository.Storage.Data; using SenseNet.Tools; namespace SenseNet.ContentRepository.Schema @@ -116,12 +112,10 @@ public void ExecuteBatch() private void Install(CTD ctd) { - ContentType contentType = ContentTypeManager.LoadOrCreateNew(ctd.Document); + var contentType = ContentTypeManager.LoadOrCreateNew(ctd.Document); // skip notification during content type install to avoid missing field errors - var notificationObserver = TypeResolver.GetType(NodeObserverNames.NOTIFICATION, false); - if (notificationObserver != null) - contentType.DisableObserver(notificationObserver); + contentType.DisableObserver(TypeResolver.GetType(NodeObserverNames.NOTIFICATION, false)); ContentTypeManager.ApplyChangesInEditor(contentType, _editor); contentType.Save(false); diff --git a/src/ContentRepository/Schema/ContentTypeManager.cs b/src/ContentRepository/Schema/ContentTypeManager.cs index 4511bbaaf..f96886f65 100644 --- a/src/ContentRepository/Schema/ContentTypeManager.cs +++ b/src/ContentRepository/Schema/ContentTypeManager.cs @@ -331,9 +331,9 @@ internal static void ApplyChanges(ContentType settings) internal static void ApplyChangesInEditor(ContentType contentType, SchemaEditor editor) { // Find ContentHandler - Type handlerType = TypeResolver.GetType(contentType.HandlerName); + var handlerType = TypeResolver.GetType(contentType.HandlerName, false); if (handlerType == null) - throw new RegistrationException(String.Concat( + throw new RegistrationException(string.Concat( SR.Exceptions.Registration.Msg_ContentHandlerNotFound, ": ", contentType.HandlerName)); // parent type diff --git a/src/ContentRepository/Security/IOAuthIdentity.cs b/src/ContentRepository/Security/IOAuthIdentity.cs new file mode 100644 index 000000000..4935c0a4d --- /dev/null +++ b/src/ContentRepository/Security/IOAuthIdentity.cs @@ -0,0 +1,49 @@ +namespace SenseNet.ContentRepository.Security +{ + /// + /// Defines a minimal set of fields that should be filled by an OAuth provider implementation. + /// + public interface IOAuthIdentity + { + /// + /// Full name of the user, may contain any unicode character. + /// + string FullName { get; } + /// + /// User name of the synchronized identity. Will become the name of the User content. + /// + string Username { get; } + /// + /// Unique identifier of the user provided by the external OAuth service. + /// + string Identifier { get; } + /// + /// Email address of the user. Optional. + /// + string Email { get; } + } + + /// + /// Built-in implementation of the IOAuthIdentity interface. Derived classes may + /// extend it with additional, provider-specific fields. + /// + public class OAuthIdentity : IOAuthIdentity + { + /// + /// Full name of the user, may contain any unicode character. + /// + public string FullName { get; set; } + /// + /// User name of the synchronized identity. Will become the name of the User content. + /// + public string Username { get; set; } + /// + /// Unique identifier of the user provided by the external OAuth service. + /// + public string Identifier { get; set; } + /// + /// Email address of the user. Optional. + /// + public string Email { get; set; } + } +} diff --git a/src/ContentRepository/Security/OAuthProvider.cs b/src/ContentRepository/Security/OAuthProvider.cs new file mode 100644 index 000000000..a60b2ae8c --- /dev/null +++ b/src/ContentRepository/Security/OAuthProvider.cs @@ -0,0 +1,57 @@ +using System.Web; + +namespace SenseNet.ContentRepository.Security +{ + /// + /// Base class for implementing OAuth authentication provided by a 3rd party service. + /// + public abstract class OAuthProvider + { + private const string ProviderNamePrefix = "oauth-"; + + /// + /// Name of the field that should be defined on the User content type for holding + /// the unique user identifier provided by the 3rd party OAuth service. + /// + public abstract string IdentifierFieldName { get; } + /// + /// Short name of the provider implementation (e.g. 'google', 'facebook'). It will be used + /// by the client to send OAuth requests to the server and by the server to find the + /// appropriate OAuth provider for the request. + /// + public abstract string ProviderName { get; } + + /// + /// Extracts token data from the request send by the client and tries to verify + /// the validity of the token by the 3rd party service. + /// + /// Http request containing token data. + /// Extracted and formatted token data that will be + /// provided later for the GetUserData method. + /// Unique user identifier in the 3rd party service. + public abstract string VerifyToken(HttpRequestBase request, out object tokenData); + /// + /// Assemble a user data that will be used to fill user fields + /// when a new user is created in the Content Repository. + /// It is called only when a new user is created. + /// + /// Token data object that was previously created by the VerifyToken method. + /// User identity information. + public abstract IOAuthIdentity GetUserData(object tokenData); + + //============================================================================ Helper methods + + internal string GetProviderRegistrationName() + { + return GetProviderRegistrationName(ProviderName); + } + /// + /// Central method for generating a feature-specific provider name + /// (e.g. 'oauth-google') for identifying provider instances. + /// + public static string GetProviderRegistrationName(string providerName) + { + return ProviderNamePrefix + providerName; + } + } +} diff --git a/src/ContentRepository/SnElevatedModificationVisibilityRule.cs b/src/ContentRepository/SnElevatedModificationVisibilityRule.cs index d4e94e579..39b98be2c 100644 --- a/src/ContentRepository/SnElevatedModificationVisibilityRule.cs +++ b/src/ContentRepository/SnElevatedModificationVisibilityRule.cs @@ -1,20 +1,16 @@ using SenseNet.ContentRepository.Storage; -using System; -using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace SenseNet.ContentRepository { /// - /// Implements a general rule for the Sense/Net that controls the visibility of the content modification + /// Implements a general rule that controls the visibility of content modification /// when a content is saved by the system user. /// Visible modification means: ModificationDate and ModifiedBy is updated before saving the content. /// public class SnElevatedModificationVisibilityRule : ElevatedModificationVisibilityRule { - private string[] systemFileExtensions = new[] { "aspx", "ascx", "cshtml", "vbhtml", "js", "css" }; + private readonly string[] _systemFileExtensions = { "aspx", "ascx", "cshtml", "vbhtml", "js", "css" }; /// /// Returns true if the content is file and its name extension is one of the followings: @@ -28,7 +24,7 @@ protected override bool IsModificationVisible(Node content) if (segments.Length > 1) { var ext = segments[segments.Length - 1].ToLowerInvariant(); - return this.systemFileExtensions.Contains(ext); + return this._systemFileExtensions.Contains(ext); } } return base.IsModificationVisible(content); diff --git a/src/ContentRepository/packages.config b/src/ContentRepository/packages.config index 3495666bb..4a2d7aef2 100644 --- a/src/ContentRepository/packages.config +++ b/src/ContentRepository/packages.config @@ -2,6 +2,7 @@ + diff --git a/src/Preview/Common.cs b/src/Preview/Common.cs deleted file mode 100644 index 556e63dc8..000000000 --- a/src/Preview/Common.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace SenseNet.Preview -{ - public class Common - { - public static readonly string LICENSEPATH = "Aspose.Total.lic"; - - public static readonly string PREVIEW_IMAGENAME = "preview{0}.png"; - public static readonly int PREVIEW_WIDTH = 1754; - public static readonly int PREVIEW_HEIGHT = 1754; - public static readonly int PREVIEW_POWERPOINT_WIDTH = 1280; - public static readonly int PREVIEW_POWERPOINT_HEIGHT = 960; - - public static readonly string THUMBNAIL_IMAGENAME = "thumbnail{0}.png"; - public static readonly int THUMBNAIL_WIDTH = 200; - public static readonly int THUMBNAIL_HEIGHT = 200; - - public static readonly System.Drawing.Imaging.ImageFormat PREVIEWIMAGEFORMAT = System.Drawing.Imaging.ImageFormat.Png; - - public static readonly string[] WORD_EXTENSIONS = { ".doc", ".docx", ".odt", ".rtf", ".txt", ".xml", ".csv" }; - public static readonly string[] DIAGRAM_EXTENSIONS = { ".vdw", ".vdx", ".vsd", ".vss", ".vst", ".vsx", ".vtx" }; - public static readonly string[] IMAGE_EXTENSIONS = { ".gif", ".jpg", ".jpeg", ".bmp", ".png", ".svg", ".exif", ".icon" }; - public static readonly string[] TIFF_EXTENSIONS = { ".tif", ".tiff" }; - public static readonly string[] WORKBOOK_EXTENSIONS = { ".ods", ".xls", ".xlsm", ".xlsx", ".xltm", ".xltx" }; - public static readonly string[] PDF_EXTENSIONS = { ".pdf" }; - public static readonly string[] PRESENTATION_EXTENSIONS = { ".pot", ".pps", ".ppt" }; - public static readonly string[] PRESENTATIONEX_EXTENSIONS = { ".potx", ".ppsx", ".pptx", ".odp" }; - public static readonly string[] PROJECT_EXTENSIONS = { ".mpp" }; - public static readonly string[] EMAIL_EXTENSIONS = { ".msg" }; - } -} diff --git a/src/Preview/IPreviewGenerationContext.cs b/src/Preview/IPreviewGenerationContext.cs deleted file mode 100644 index 91aa43f21..000000000 --- a/src/Preview/IPreviewGenerationContext.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Drawing; -using System.IO; - -namespace SenseNet.Preview -{ - public interface IPreviewGenerationContext - { - int ContentId { get; } - int PreviewsFolderId { get; } - int StartIndex { get; } - int MaxPreviewCount { get; } - int PreviewResolution { get; } - string Version { get; } - - void SetPageCount(int pageCount); - void SetIndexes(int pageCount, out int firstIndex, out int lastIndex); - - void SavePreviewAndThumbnail(Stream imgStream, int page); - void SaveEmptyPreview(int page); - void SaveImage(Bitmap image, int page); - - void LogInfo(int page, string message); - void LogWarning(int page, string message); - void LogError(int page, string message = null, Exception ex = null); - } -} diff --git a/src/Preview/IPreviewImageGenerator.cs b/src/Preview/IPreviewImageGenerator.cs deleted file mode 100644 index 7c28fde92..000000000 --- a/src/Preview/IPreviewImageGenerator.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.IO; - -namespace SenseNet.Preview -{ - public interface IPreviewImageGenerator - { - string[] KnownExtensions { get; } - string GetTaskNameByExtension(string extension); - string GetTaskTitleByExtension(string extension); - string[] GetSupportedTaskNames(); - void GeneratePreview(Stream docStream, IPreviewGenerationContext context); - } - -} diff --git a/src/Preview/Preview.csproj b/src/Preview/Preview.csproj deleted file mode 100644 index 7dcb918ac..000000000 --- a/src/Preview/Preview.csproj +++ /dev/null @@ -1,70 +0,0 @@ - - - - Debug - AnyCPU - {01AE54AF-DB4E-431E-A3F9-FBCD2AD73D03} - Library - Properties - SenseNet.Preview - SenseNet.Preview - v4.5.1 - 512 - SAK - SAK - SAK - SAK - - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - ..\packages\SenseNet.Tools.2.1.1\lib\net451\SenseNet.Tools.dll - True - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Preview/Preview.nuspec b/src/Preview/Preview.nuspec deleted file mode 100644 index 4a2d8fdd8..000000000 --- a/src/Preview/Preview.nuspec +++ /dev/null @@ -1,24 +0,0 @@ - - - - SenseNet.Preview - 7.0.0-beta2 - sensenet ECM Preview library - kavics,aniko,laci,borsi,lajos,tusmester - Sense/Net - https://github.com/SenseNet/sensenet/blob/master/LICENSE - https://github.com/SenseNet/sensenet - https://raw.githubusercontent.com/SenseNet/sn-resources/master/images/sn-icon/sensenet-icon-64.png - false - General document preview library for the sensenet ECM platform containing common interfaces. - Initial release. - Copyright © Sense/Net Inc. - sensenet ecm ecms - - - - - - - - \ No newline at end of file diff --git a/src/Preview/PreviewImageGenerator.cs b/src/Preview/PreviewImageGenerator.cs deleted file mode 100644 index 7a3fa6f0d..000000000 --- a/src/Preview/PreviewImageGenerator.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using SenseNet.Tools; - -namespace SenseNet.Preview -{ - public abstract class PreviewImageGenerator : IPreviewImageGenerator - { - public abstract string[] KnownExtensions { get; } - public abstract void GeneratePreview(Stream docStream, IPreviewGenerationContext context); - public virtual string GetTaskNameByExtension(string extension) - { - // means default - return null; - } - public virtual string GetTaskTitleByExtension(string extension) - { - // means default - return null; - } - public virtual string[] GetSupportedTaskNames() - { - // fallback to default task name, defined by the preview provider - return null; - } - - // ==================================================================================================================== - - private static readonly object LoaderSync = new object(); - private static Dictionary __providers; - private static Dictionary Providers - { - get - { - if (__providers == null) - lock (LoaderSync) - if (__providers == null) - __providers = CreateProviderPrototypes(); - return __providers; - } - } - private static Dictionary CreateProviderPrototypes() - { - var providers = new Dictionary(); - var providerTypesA = TypeResolver.GetTypesByInterface(typeof(IPreviewImageGenerator)); - foreach (var providerType in providerTypesA) - { - if (providerType.IsAbstract) - continue; - - var provider = (IPreviewImageGenerator)Activator.CreateInstance(providerType); - foreach (var extension in provider.KnownExtensions) - { - IPreviewImageGenerator existing; - var ext = extension.ToLowerInvariant(); - if (providers.TryGetValue(ext, out existing)) - { - if (providerType.IsInstanceOfType(existing)) - continue; - } - providers[ext] = provider; - } - } - return providers; - } - - public static string GetTaskNameByFileNameExtension(string extension) - { - IPreviewImageGenerator provider; - if (!Providers.TryGetValue(extension.ToLowerInvariant(), out provider)) - throw new ApplicationException(SR.F(SR.UnknownProvider_1, extension)); - return provider.GetTaskNameByExtension(extension); - } - public static string GetTaskTitleByFileNameExtension(string extension) - { - IPreviewImageGenerator provider; - if (!Providers.TryGetValue(extension.ToLowerInvariant(), out provider)) - throw new ApplicationException(SR.F(SR.UnknownProvider_1, extension)); - return provider.GetTaskTitleByExtension(extension); - } - - public static string[] GetSupportedCustomTaskNames() - { - // collect all suppotred task names from the different generator implementations - return Providers.Values.Select(pig => pig.GetSupportedTaskNames()) - .Where(tnames => tnames != null) - .SelectMany(tnames => tnames) - .Where(tn => !string.IsNullOrEmpty(tn)) - .Distinct().OrderBy(tn => tn).ToArray(); - } - - public static bool IsSupportedExtension(string extension) - { - return Providers.ContainsKey(extension.ToLowerInvariant()); - } - - public static void GeneratePreview(string extension, Stream docStream, IPreviewGenerationContext context) - { - IPreviewImageGenerator provider; - if (!Providers.TryGetValue(extension.ToLowerInvariant(), out provider)) - throw new ApplicationException(SR.F(SR.UnknownProvider_1, extension)); - provider.GeneratePreview(docStream, context); - } - } -} diff --git a/src/Preview/Properties/AssemblyInfo.cs b/src/Preview/Properties/AssemblyInfo.cs deleted file mode 100644 index 421b90913..000000000 --- a/src/Preview/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -#if DEBUG -[assembly: AssemblyTitle("SenseNet.Preview (Debug)")] -#else -[assembly: AssemblyTitle("SenseNet.Preview (Release)")] -#endif -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Sense/Net Inc.")] -[assembly: AssemblyProduct("sensenet ECM")] -[assembly: AssemblyCopyright("Copyright © Sense/Net Inc.")] -[assembly: AssemblyTrademark("Sense/Net Inc.")] -[assembly: AssemblyCulture("")] - -[assembly: ComVisible(false)] -[assembly: Guid("43ec6ad0-90f0-4a65-b950-3a5a8e4cc6e6")] - -[assembly: AssemblyVersion("7.0.0.0")] -[assembly: AssemblyFileVersion("7.0.0.0")] -[assembly: AssemblyInformationalVersion("7.0.0-beta2")] \ No newline at end of file diff --git a/src/Preview/SR.cs b/src/Preview/SR.cs deleted file mode 100644 index fcfbf0b9c..000000000 --- a/src/Preview/SR.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace SenseNet.Preview -{ - internal static class SR - { - internal static string F(string format, params object[] args) - { - return string.Format(format, args); - } - - internal static string UnknownProvider_1 = "Unknown IPreviewImageGenerator for file extension '{0}'."; - - } -} diff --git a/src/Preview/packages.config b/src/Preview/packages.config deleted file mode 100644 index 5e169d6c4..000000000 --- a/src/Preview/packages.config +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/src/SenseNet.sln b/src/SenseNet.sln index ace0bb5cf..47d80fd62 100644 --- a/src/SenseNet.sln +++ b/src/SenseNet.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 14 -VisualStudioVersion = 14.0.25420.1 +# Visual Studio 15 +VisualStudioVersion = 15.0.26730.16 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{875EF569-4684-473D-A2D4-A35B20B4A07C}" EndProject @@ -16,8 +16,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Configuration", "Configurat EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SnAdminRuntime", "Tools\SnAdminRuntime\SnAdminRuntime.csproj", "{891B1FD9-CA73-4129-8D1D-D6663F01866C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Preview", "Preview\Preview.csproj", "{01AE54AF-DB4E-431E-A3F9-FBCD2AD73D03}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlobStorage", "BlobStorage\BlobStorage.csproj", "{4E6722B5-AC95-494C-80A5-A4D80CC502B5}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common", "Common\Common.csproj", "{A453E920-29C0-45CD-984C-0D8E3631B1E3}" @@ -36,12 +34,15 @@ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SenseNet.Services.Tests", "Tests\SenseNet.Services.Tests\SenseNet.Services.Tests.csproj", "{CF157DCB-B973-42B4-948A-F6643EB6216D}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SenseNet.TokenAuthentication.Tests", "Tests\SenseNet.TokenAuthentication.Tests\SenseNet.TokenAuthentication.Tests.csproj", "{AFCB7986-73C5-4DEA-897B-46EC1D124965}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SenseNet.Packaging.Tests", "Tests\SenseNet.Packaging.Tests\SenseNet.Packaging.Tests.csproj", "{224FE019-A978-4390-9F77-EBEB7A384743}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SenseNet.Packaging.IntegrationTests", "Tests\SenseNet.Packaging.IntegrationTests\SenseNet.Packaging.IntegrationTests.csproj", "{CF049086-0872-4F55-AF6D-6177245CC59E}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SnAdminRuntime.Tests", "Tests\SnAdminRuntime.Tests\SnAdminRuntime.Tests.csproj", "{ECA9A043-5B86-445C-B70E-843573AAF72E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SenseNet.ContentRepository.Tests", "Tests\SenseNet.ContentRepository.Tests\SenseNet.ContentRepository.Tests.csproj", "{7DE6A7E8-5738-4436-9646-9C1179F752EA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -114,18 +115,6 @@ Global {891B1FD9-CA73-4129-8D1D-D6663F01866C}.Release|Mixed Platforms.Build.0 = Release|Any CPU {891B1FD9-CA73-4129-8D1D-D6663F01866C}.Release|x64.ActiveCfg = Release|Any CPU {891B1FD9-CA73-4129-8D1D-D6663F01866C}.Release|x86.ActiveCfg = Release|Any CPU - {01AE54AF-DB4E-431E-A3F9-FBCD2AD73D03}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {01AE54AF-DB4E-431E-A3F9-FBCD2AD73D03}.Debug|Any CPU.Build.0 = Debug|Any CPU - {01AE54AF-DB4E-431E-A3F9-FBCD2AD73D03}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {01AE54AF-DB4E-431E-A3F9-FBCD2AD73D03}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {01AE54AF-DB4E-431E-A3F9-FBCD2AD73D03}.Debug|x64.ActiveCfg = Debug|Any CPU - {01AE54AF-DB4E-431E-A3F9-FBCD2AD73D03}.Debug|x86.ActiveCfg = Debug|Any CPU - {01AE54AF-DB4E-431E-A3F9-FBCD2AD73D03}.Release|Any CPU.ActiveCfg = Release|Any CPU - {01AE54AF-DB4E-431E-A3F9-FBCD2AD73D03}.Release|Any CPU.Build.0 = Release|Any CPU - {01AE54AF-DB4E-431E-A3F9-FBCD2AD73D03}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {01AE54AF-DB4E-431E-A3F9-FBCD2AD73D03}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {01AE54AF-DB4E-431E-A3F9-FBCD2AD73D03}.Release|x64.ActiveCfg = Release|Any CPU - {01AE54AF-DB4E-431E-A3F9-FBCD2AD73D03}.Release|x86.ActiveCfg = Release|Any CPU {4E6722B5-AC95-494C-80A5-A4D80CC502B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4E6722B5-AC95-494C-80A5-A4D80CC502B5}.Debug|Any CPU.Build.0 = Debug|Any CPU {4E6722B5-AC95-494C-80A5-A4D80CC502B5}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -254,6 +243,22 @@ Global {ECA9A043-5B86-445C-B70E-843573AAF72E}.Release|x64.Build.0 = Release|Any CPU {ECA9A043-5B86-445C-B70E-843573AAF72E}.Release|x86.ActiveCfg = Release|Any CPU {ECA9A043-5B86-445C-B70E-843573AAF72E}.Release|x86.Build.0 = Release|Any CPU + {7DE6A7E8-5738-4436-9646-9C1179F752EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7DE6A7E8-5738-4436-9646-9C1179F752EA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7DE6A7E8-5738-4436-9646-9C1179F752EA}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {7DE6A7E8-5738-4436-9646-9C1179F752EA}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {7DE6A7E8-5738-4436-9646-9C1179F752EA}.Debug|x64.ActiveCfg = Debug|Any CPU + {7DE6A7E8-5738-4436-9646-9C1179F752EA}.Debug|x64.Build.0 = Debug|Any CPU + {7DE6A7E8-5738-4436-9646-9C1179F752EA}.Debug|x86.ActiveCfg = Debug|Any CPU + {7DE6A7E8-5738-4436-9646-9C1179F752EA}.Debug|x86.Build.0 = Debug|Any CPU + {7DE6A7E8-5738-4436-9646-9C1179F752EA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7DE6A7E8-5738-4436-9646-9C1179F752EA}.Release|Any CPU.Build.0 = Release|Any CPU + {7DE6A7E8-5738-4436-9646-9C1179F752EA}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {7DE6A7E8-5738-4436-9646-9C1179F752EA}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {7DE6A7E8-5738-4436-9646-9C1179F752EA}.Release|x64.ActiveCfg = Release|Any CPU + {7DE6A7E8-5738-4436-9646-9C1179F752EA}.Release|x64.Build.0 = Release|Any CPU + {7DE6A7E8-5738-4436-9646-9C1179F752EA}.Release|x86.ActiveCfg = Release|Any CPU + {7DE6A7E8-5738-4436-9646-9C1179F752EA}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -264,7 +269,6 @@ Global {B72529C8-FEB1-49F5-B08B-56055B58F296} = {2997D17C-A736-43E5-B3DD-11D11AC7DF17} {A19F707C-B1C3-45C7-AE38-E7B7F30C1161} = {2997D17C-A736-43E5-B3DD-11D11AC7DF17} {891B1FD9-CA73-4129-8D1D-D6663F01866C} = {875EF569-4684-473D-A2D4-A35B20B4A07C} - {01AE54AF-DB4E-431E-A3F9-FBCD2AD73D03} = {2997D17C-A736-43E5-B3DD-11D11AC7DF17} {4E6722B5-AC95-494C-80A5-A4D80CC502B5} = {2997D17C-A736-43E5-B3DD-11D11AC7DF17} {A453E920-29C0-45CD-984C-0D8E3631B1E3} = {2997D17C-A736-43E5-B3DD-11D11AC7DF17} {78FEBD73-8BC4-45B9-9B31-D512E1AFAD60} = {2997D17C-A736-43E5-B3DD-11D11AC7DF17} @@ -273,6 +277,10 @@ Global {224FE019-A978-4390-9F77-EBEB7A384743} = {C68D256D-7D40-4E33-8A2B-B1625538B138} {CF049086-0872-4F55-AF6D-6177245CC59E} = {C68D256D-7D40-4E33-8A2B-B1625538B138} {ECA9A043-5B86-445C-B70E-843573AAF72E} = {C68D256D-7D40-4E33-8A2B-B1625538B138} + {7DE6A7E8-5738-4436-9646-9C1179F752EA} = {C68D256D-7D40-4E33-8A2B-B1625538B138} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {7D903DEB-CA0B-43D8-BD9D-820BB1453C4C} EndGlobalSection GlobalSection(TestCaseManagementSettings) = postSolution CategoryFile = sensenet.vsmdi diff --git a/src/Services/ApplicationModel/CopyBatchAction.cs b/src/Services/ApplicationModel/CopyBatchAction.cs index 0c6cb4bc8..7127e9a61 100644 --- a/src/Services/ApplicationModel/CopyBatchAction.cs +++ b/src/Services/ApplicationModel/CopyBatchAction.cs @@ -1,10 +1,11 @@ -using SenseNet.ContentRepository.i18n; -using System; -using SenseNet.ContentRepository; +using System; using System.Collections.Generic; +using System.Linq; +using SenseNet.ContentRepository; +using SenseNet.ContentRepository.i18n; using SenseNet.ContentRepository.Storage; using SenseNet.Diagnostics; -using System.Linq; +using SenseNet.Portal; using SenseNet.Portal.OData; namespace SenseNet.ApplicationModel @@ -29,9 +30,9 @@ protected override string GetCallBackScript() // =========================================================================== OData - public override bool IsODataOperation { get { return true; } } + public override bool IsODataOperation => true; - public override ActionParameter[] ActionParameters { get; } = + public override ActionParameter[] ActionParameters { get; } = { new ActionParameter("targetPath", typeof (string), true), new ActionParameter("paths", typeof (object[]), true) @@ -40,37 +41,77 @@ protected override string GetCallBackScript() public override object Execute(Content content, params object[] parameters) { var targetPath = (string)parameters[0]; - var exceptions = new List(); - var targetNode = Node.LoadNode(targetPath); if (targetNode == null) throw new ContentNotFoundException(targetPath); - var ids = parameters[1] as object[]; - if (ids == null) + if (!(parameters[1] is object[] ids)) + { throw new InvalidOperationException("No content identifiers provided."); - - foreach (var node in Node.LoadNodes(ids.Select(NodeIdentifier.Get))) + } + + var results = new List(); + var errors = new List(); + var identifiers = ids.Select(NodeIdentifier.Get).ToList(); + var foundIdentifiers = new List(); + var nodes = Node.LoadNodes(identifiers); + + foreach (var node in nodes) { try { - node?.CopyTo(targetNode); + // Collect already found identifiers in a separate list otherwise the error list + // would contain multiple errors for the same content. + foundIdentifiers.Add(NodeIdentifier.Get(node)); + + var copy = node.CopyToAndGetCopy(targetNode); + results.Add(new { copy.Id, copy.Path, copy.Name }); } catch (Exception e) { - exceptions.Add(e); - //TODO: we should log only relevant exceptions here and skip // business logic-related errors, e.g. lack of permissions or // existing target content path. SnLog.WriteException(e); + + errors.Add(new ErrorContent + { + Content = new {node?.Id, node?.Path, node?.Name}, + Error = new Error + { + Code = "NotSpecified", + ExceptionType = e.GetType().FullName, + InnerError = new StackInfo {Trace = e.StackTrace}, + Message = new ErrorMessage + { + Lang = System.Globalization.CultureInfo.CurrentUICulture.Name.ToLower(), + Value = e.Message + } + } + }); } } - if (exceptions.Count > 0) - throw new Exception(string.Join(Environment.NewLine, exceptions.Select(e => e.Message))); + // iterating through the missing identifiers and making error items for them + errors.AddRange(identifiers.Where(id => !foundIdentifiers.Exists(f => f.Id == id.Id || f.Path == id.Path)) + .Select(missing => new ErrorContent + { + Content = new {missing?.Id, missing?.Path}, + Error = new Error + { + Code = "ResourceNotFound", + ExceptionType = "ContentNotFoundException", + InnerError = null, + Message = new ErrorMessage + { + Lang = System.Globalization.CultureInfo.CurrentUICulture.Name.ToLower(), + Value = string.Format(SNSR.GetString(SNSR.Exceptions.OData.ErrorContentNotFound), + missing?.Path) + } + } + })); - return null; + return BatchActionResponse.Create(results, errors, results.Count + errors.Count); } } -} +} \ No newline at end of file diff --git a/src/Services/ApplicationModel/DeleteBatchAction.cs b/src/Services/ApplicationModel/DeleteBatchAction.cs index ef3c3bd24..880f1ca86 100644 --- a/src/Services/ApplicationModel/DeleteBatchAction.cs +++ b/src/Services/ApplicationModel/DeleteBatchAction.cs @@ -1,12 +1,14 @@ -using SenseNet.ContentRepository; -using SenseNet.ContentRepository.Schema; -using SenseNet.Portal.Virtualization; -using SenseNet.ContentRepository.i18n; -using System; -using System.Linq; +using System; using System.Collections.Generic; +using System.Linq; +using SenseNet.ContentRepository; +using SenseNet.ContentRepository.i18n; +using SenseNet.ContentRepository.Schema; using SenseNet.ContentRepository.Storage; using SenseNet.Diagnostics; +using SenseNet.Portal; +using SenseNet.Portal.OData; +using SenseNet.Portal.Virtualization; namespace SenseNet.ApplicationModel { @@ -14,24 +16,13 @@ public class DeleteBatchAction : ClientAction { public override string Callback { - get - { - return this.Forbidden ? string.Empty : string.Format("{0};", GetCallBackScript()); - } - set - { - base.Callback = value; - } + get => this.Forbidden ? string.Empty : $"{GetCallBackScript()};"; + set => base.Callback = value; } private string _portletClientId; - public string PortletClientId - { - get - { - return _portletClientId ?? (_portletClientId = GetPortletClientId()); - } - } + public string PortletClientId => _portletClientId ?? (_portletClientId = GetPortletClientId()); + protected string GetPortletClientId() { var parameters = GetParameteres(); @@ -55,7 +46,7 @@ protected virtual string GetCallBackScript() { // original behavior for webforms action controls if (PortalContext.Current.ActionName == "Explore") - redirectPath = this.Content.Path + "?action=Explore"; + redirectPath = this.Content.Path + "?action=Explore"; } return string.Format( @@ -66,7 +57,7 @@ protected virtual string GetCallBackScript() var contextpath = '{2}'; var redirectPath = '{3}'; SN.Util.CreateServerDialog('/Root/System/WebRoot/DeleteAction.aspx','{1}', {{paths:paths,ids:ids,contextpath:contextpath,batch:true,redirectPath:redirectPath}});", - PortletClientId, + PortletClientId, SenseNetResourceManager.Current.GetString("ContentDelete", "DeleteStatusDialogTitle"), this.Content.Path, redirectPath @@ -84,42 +75,81 @@ protected virtual string GetCallBackScript() public override object Execute(Content content, params object[] parameters) { var permanent = parameters.Length > 1 && parameters[1] != null && (bool)parameters[1]; - var exceptions = new List(); - // no need to throw an exception if no ids are provided: we simply do not have to delete anything - var ids = parameters[0] as object[]; - if (ids == null) + if (!(parameters[0] is object[] ids)) return null; - foreach (var node in Node.LoadNodes(ids.Select(NodeIdentifier.Get))) + var results = new List(); + var errors = new List(); + var identifiers = ids.Select(NodeIdentifier.Get).ToList(); + var foundIdentifiers = new List(); + var nodes = Node.LoadNodes(identifiers); + + foreach (var node in nodes) { try { - var gc = node as GenericContent; - if (gc != null) - { - gc.Delete(permanent); - } - else + // Collect already found identifiers in a separate list otherwise the error list + // would contain multiple errors for the same content. + foundIdentifiers.Add(NodeIdentifier.Get(node)); + + switch (node) { - var ct = node as ContentType; - ct?.Delete(); + case GenericContent gc: + gc.Delete(permanent); + break; + case ContentType ct: + ct.Delete(); + break; } + + results.Add(new { node.Id, node.Path, node.Name }); } catch (Exception e) { - exceptions.Add(e); - //TODO: we should log only relevant exceptions here and skip // business logic-related errors, e.g. lack of permissions or // existing target content path. SnLog.WriteException(e); + + errors.Add(new ErrorContent + { + Content = new {node?.Id, node?.Path}, + Error = new Error + { + Code = "NotSpecified", + ExceptionType = e.GetType().FullName, + InnerError = new StackInfo {Trace = e.StackTrace}, + Message = new ErrorMessage + { + Lang = System.Globalization.CultureInfo.CurrentUICulture.Name.ToLower(), + Value = e.Message + } + } + }); } } - if (exceptions.Count > 0) - throw new Exception(string.Join(Environment.NewLine, exceptions.Select(e => e.Message))); - return null; + // iterating through the missing identifiers and making error items for them + errors.AddRange(identifiers.Where(id => !foundIdentifiers.Exists(f => f.Id == id.Id || f.Path == id.Path)) + .Select(missing => new ErrorContent + { + Content = new {missing?.Id, missing?.Path}, + Error = new Error + { + Code = "ResourceNotFound", + ExceptionType = "ContentNotFoundException", + InnerError = null, + Message = new ErrorMessage + { + Lang = System.Globalization.CultureInfo.CurrentUICulture.Name.ToLower(), + Value = string.Format(SNSR.GetString(SNSR.Exceptions.OData.ErrorContentNotFound), + missing?.Path) + } + } + })); + + return BatchActionResponse.Create(results, errors, results.Count + errors.Count); } } -} +} \ No newline at end of file diff --git a/src/Services/ApplicationModel/MoveBatchAction.cs b/src/Services/ApplicationModel/MoveBatchAction.cs index 5e0babe95..b573a8177 100644 --- a/src/Services/ApplicationModel/MoveBatchAction.cs +++ b/src/Services/ApplicationModel/MoveBatchAction.cs @@ -1,10 +1,11 @@ -using SenseNet.ContentRepository.i18n; -using System; -using SenseNet.ContentRepository; +using System; using System.Collections.Generic; +using System.Linq; +using SenseNet.ContentRepository; +using SenseNet.ContentRepository.i18n; using SenseNet.ContentRepository.Storage; using SenseNet.Diagnostics; -using System.Linq; +using SenseNet.Portal; using SenseNet.Portal.OData; namespace SenseNet.ApplicationModel @@ -33,7 +34,7 @@ protected override string GetCallBackScript() public override bool IsODataOperation => true; - public override ActionParameter[] ActionParameters { get; } = + public override ActionParameter[] ActionParameters { get; } = { new ActionParameter("targetPath", typeof (string), true), new ActionParameter("paths", typeof (object[]), true) @@ -42,37 +43,77 @@ protected override string GetCallBackScript() public override object Execute(Content content, params object[] parameters) { var targetPath = (string)parameters[0]; - var exceptions = new List(); - var targetNode = Node.LoadNode(targetPath); if (targetNode == null) throw new ContentNotFoundException(targetPath); - var ids = parameters[1] as object[]; - if (ids == null) + if (!(parameters[1] is object[] ids)) + { throw new InvalidOperationException("No content identifiers provided."); + } + + var results = new List(); + var errors = new List(); + var identifiers = ids.Select(NodeIdentifier.Get).ToList(); + var foundIdentifiers = new List(); + var nodes = Node.LoadNodes(identifiers); - foreach (var node in Node.LoadNodes(ids.Select(NodeIdentifier.Get))) + foreach (var node in nodes) { try { - node?.MoveTo(targetNode); + // Collect already found identifiers in a separate list otherwise the error list + // would contain multiple errors for the same content. + foundIdentifiers.Add(NodeIdentifier.Get(node)); + + node.MoveTo(targetNode); + results.Add(new { node.Id, node.Path, node.Name }); } catch (Exception e) { - exceptions.Add(e); - //TODO: we should log only relevant exceptions here and skip // business logic-related errors, e.g. lack of permissions or // existing target content path. SnLog.WriteException(e); + + errors.Add(new ErrorContent + { + Content = new {node?.Id, node?.Path, node?.Name}, + Error = new Error + { + Code = "NotSpecified", + ExceptionType = e.GetType().FullName, + InnerError = new StackInfo {Trace = e.StackTrace}, + Message = new ErrorMessage + { + Lang = System.Globalization.CultureInfo.CurrentUICulture.Name.ToLower(), + Value = e.Message + } + } + }); } } - if (exceptions.Count > 0) - throw new Exception(string.Join(Environment.NewLine, exceptions.Select(e => e.Message))); + // iterating through the missing identifiers and making error items for them + errors.AddRange(identifiers.Where(id => !foundIdentifiers.Exists(f => f.Id == id.Id || f.Path == id.Path)) + .Select(missing => new ErrorContent + { + Content = new {missing?.Id, missing?.Path}, + Error = new Error + { + Code = "ResourceNotFound", + ExceptionType = "ContentNotFoundException", + InnerError = null, + Message = new ErrorMessage + { + Lang = System.Globalization.CultureInfo.CurrentUICulture.Name.ToLower(), + Value = string.Format(SNSR.GetString(SNSR.Exceptions.OData.ErrorContentNotFound), + missing?.Path) + } + } + })); - return null; + return BatchActionResponse.Create(results, errors, results.Count + errors.Count); } } -} +} \ No newline at end of file diff --git a/src/Services/OData/BatchActionResponse.cs b/src/Services/OData/BatchActionResponse.cs new file mode 100644 index 000000000..66ea4a53c --- /dev/null +++ b/src/Services/OData/BatchActionResponse.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; + +namespace SenseNet.Portal.OData +{ + internal class BatchActionResponse + { + [JsonProperty(PropertyName = "d", Order = 1)] + public Dictionary Contents { get; private set; } + public static BatchActionResponse Create(IEnumerable results, IEnumerable errors, int count = 0) + { + var resultArray = results.ToArray(); + var errorArray = errors.ToArray(); + var dict = new Dictionary + { + {"__count", count == 0 ? resultArray.Length : count}, + {"results", resultArray}, + {"errors", errorArray} + }; + return new BatchActionResponse { Contents = dict }; + } + } +} \ No newline at end of file diff --git a/src/Services/OData/ErrorContent.cs b/src/Services/OData/ErrorContent.cs new file mode 100644 index 000000000..3e46db943 --- /dev/null +++ b/src/Services/OData/ErrorContent.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace SenseNet.Portal.OData +{ + internal class ErrorContent + { + [JsonProperty(PropertyName = "content", Order = 1)] + public object Content { get; set; } + + [JsonProperty(PropertyName = "error", Order = 2)] + public Error Error { get; set; } + } +} \ No newline at end of file diff --git a/src/Services/OData/FieldConverter.cs b/src/Services/OData/FieldConverter.cs index 6e0f4ac19..3b4105fe8 100644 --- a/src/Services/OData/FieldConverter.cs +++ b/src/Services/OData/FieldConverter.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Text; using Newtonsoft.Json; +using SenseNet.ContentRepository; using SenseNet.ContentRepository.Fields; using SenseNet.ContentRepository.Storage; using SenseNet.ContentRepository.Storage.Schema; @@ -10,31 +11,53 @@ namespace SenseNet.Portal.OData { + /// + /// Defines a base class for field converters that can convert value + /// of the 's to JSON format. + /// public abstract class FieldConverter : JsonConverter { + /// + /// Gets the type of the object that can be converted. + /// public abstract Type TargetType { get; } + /// + /// Returns true whether this instance can transform the value of the + /// configured by the given + /// public abstract bool CanConvert(FieldSetting fieldSetting); } + /// + /// Supports the serialization of the to JSON format. + /// public class ImageFieldConverter : FieldConverter { + /// + /// Returns with typeof(ImageField.ImageFieldData) in this case. public override Type TargetType { get { return typeof(ImageField.ImageFieldData); } } + /// public override bool CanConvert(Type objectType) { return objectType.IsAssignableFrom(typeof(ImageField.ImageFieldData)); } + /// public override bool CanConvert(FieldSetting fieldSetting) { return fieldSetting.FieldDataType == typeof(ImageField.ImageFieldData); } + /// + /// This method is not supported. + /// public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { throw new SnNotSupportedException(); } + /// public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { var imgData = value as ImageField.ImageFieldData; @@ -48,25 +71,36 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s } } + /// + /// Supports the serialization of the to JSON format. + /// public class VersionFieldConverter : FieldConverter { + /// + /// Returns with typeof(string) in this case. public override Type TargetType { get { return typeof(string); } } + /// public override bool CanConvert(Type objectType) { return objectType.IsAssignableFrom(typeof(VersionNumber)); } + /// public override bool CanConvert(FieldSetting fieldSetting) { return fieldSetting.FieldDataType == typeof(VersionNumber); } + /// + /// This method is not supported. + /// public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { throw new SnNotSupportedException(); } + /// public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { var versionData = value as VersionNumber; @@ -75,25 +109,36 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s writer.WriteValue(versionData.ToString()); } } + /// + /// Supports the serialization of the to JSON format. + /// public class NodeTypeFieldConverter : FieldConverter { + /// + /// Returns with typeof(string) in this case. public override Type TargetType { get { return typeof(string); } } + /// public override bool CanConvert(Type objectType) { return objectType.IsAssignableFrom(typeof(NodeType)); } + /// public override bool CanConvert(FieldSetting fieldSetting) { return fieldSetting.FieldDataType == typeof(NodeType); } + /// + /// This method is not supported. + /// public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { throw new SnNotSupportedException(); } + /// public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { var nodeType = value as NodeType; @@ -103,32 +148,52 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s } } + /// + /// Represents a key-value pair for serialization to the JSON format. + /// public class KeyValue { + /// + /// Gets or sets the key. + /// [JsonProperty("key")] public string Key; + /// + /// Gets or sets the value. + /// [JsonProperty("value")] public string Value; } + /// + /// Supports the serialization of the to JSON format. + /// public class UrlListFieldConverter : FieldConverter { + /// + /// Returns with typeof(IEnumerable<KeyValue>) in this case. public override Type TargetType { get { return typeof(IEnumerable); } } + /// public override bool CanConvert(Type objectType) { return objectType.IsAssignableFrom(typeof(Dictionary)); } + /// public override bool CanConvert(FieldSetting fieldSetting) { return fieldSetting.FieldDataType == typeof(IDictionary); } + /// + /// This method is not supported. + /// public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { throw new SnNotSupportedException(); } + /// public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { var urls = value as Dictionary; diff --git a/src/Services/OData/Json.cs b/src/Services/OData/Json.cs index 117c07022..09d38bcb7 100644 --- a/src/Services/OData/Json.cs +++ b/src/Services/OData/Json.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Text; using Newtonsoft.Json; + //TODO: Rename Json.cs to more generalized name namespace SenseNet.Portal.OData { @@ -63,6 +64,7 @@ public static ODataMultipleContent Create(IEnumerable return new ODataMultipleContent { Contents = dict }; } } + internal class ODataReference { [JsonProperty(PropertyName = "__deferred", Order = 1)] @@ -99,51 +101,125 @@ internal class ODataMediaResource public string MediaETag { get; set; } } + /// + /// Represents an item in the Actions list of the OData Content metadata. + /// public class ODataActionItem { + /// + /// Gets or sets the name of the Action. + /// public string Name { get; set; } + /// + /// Gets or sets the human readable name of the Action. + /// public string DisplayName { get; set; } + /// + /// Gets os sets the value that helps sorting the items. + /// public int Index { get; set; } + /// + /// Gets or sets the icon name of the Action. + /// public string Icon { get; set; } + /// + /// Gets or sets the URL of the Action. + /// public string Url { get; set; } + /// + /// Gets or sets a value that is true when the back URL argument is provided. + /// public int IncludeBackUrl { get; set; } + //UNDONE: XMLDOC: ODataActionItem.ClientAction public bool ClientAction { get; set; } + /// + /// Gets or sets a value that is true if the Action is an . + /// + public bool IsODataAction { get; set; } + /// + /// Gets or sets the parameter names of the Action. + /// + public string[] ActionParameters { get; set; } + /// + /// Gets or sets the scenario in which the Action was found. + /// + public string Scenario { get; set; } + /// + /// Gets or sets a value that is true if the Action is + /// visible but not executable for the current user. + /// public bool Forbidden { get; set; } } + /// + /// Represents a JSON-serializable OData error. + /// + /// + /// The general message format is the following (JSON): + /// + /// { + /// "error": { + /// "code": "NotSpecified", + /// "exceptiontype": "SenseNetSecurityException", + /// "message": { + /// "lang": "en-us", + /// "value": "Access denied. Path: /Root/... + /// }, + /// "innererror": { + /// "trace": "ODataException: Access denied. Path: /Root/... + /// } + /// } + /// } + /// + /// public class Error { - // { - // "error": { - // "code": "NotSpecified", - // "exceptiontype": "SenseNetSecurityException", - // "message": { - // "lang": "en-us", - // "value": "Access denied. Path: /Root/Sites/Default_Site/workspaces/Document/londondocumentworkspace PermissionType: RecallOldVersion User: BuiltIn\Visitor UserId: 6" - // }, - // "innererror": { - // "trace": "ODataException: Access denied. Path: /Root/Sites/Default_Site/workspaces/Document/londondocumentworkspace PermissionType: RecallOldVersion User: BuiltIn\\Visitor UserId: 6\\n\\n---- Inner Exception:\\nSenseNetSecurityException: Access denied. Path: /Root/Sites/Default_Site/workspaces/Document/londondocumentworkspace PermissionType: RecallOldVersion User: BuiltIn\\Visitor UserId: 6\\n at SenseNet.ContentRepository.Storage.Security.SecurityHandler.GetAccessDeniedException(String path, Int32 creatorId, Int32 lastModifierId, String message, PermissionType[] permissionTypes, IUser user) in C:\\Dev10\\SenseNet\\Development\\Budapest\\Source\\SenseNet\\Storage\\Security\\SecurityHandler.cs:line 1055\\n at SenseNet.ContentRepository.Storage.Security.SecurityHandler.Assert(String path, Int32 creatorId, Int32 lastModifierId, String message, PermissionType[] permissionTypes) in C:\\Dev10\\SenseNet\\Development\\Budapest\\Source\\SenseNet\\Storage\\Security\\SecurityHandler.cs:line 383\\n at SenseNet.ContentRepository.Storage.Security.SecurityHandler.Assert(Node node, PermissionType[] permissionTypes) in C:\\Dev10\\SenseNet\\Development\\Budapest\\Source\\SenseNet\\Storage\\Security\\SecurityHandler.cs:line 353\\n at SenseNet.ContentRepository.Storage.Security.SecurityHandler.Assert(PermissionType[] permissionTypes) in C:\\Dev10\\SenseNet\\Development\\Budapest\\Source\\SenseNet\\Storage\\Security\\SecurityHandler.cs:line 343\\n at SenseNet.ContentRepository.Storage.Node.LoadVersions() in C:\\Dev10\\SenseNet\\Development\\Budapest\\Source\\SenseNet\\Storage\\Node.cs:line 1772\\n at SenseNet.ContentRepository.GenericContent.get_Versions() in C:\\Dev10\\SenseNet\\Development\\Budapest\\Source\\SenseNet\\ContentRepository\\GenericContent.cs:line 1237\\n at SenseNet.ContentRepository.GenericContent.GetProperty(String name) in C:\\Dev10\\SenseNet\\Development\\Budapest\\Source\\SenseNet\\ContentRepository\\GenericContent.cs:line 579\\n at SenseNet.ContentRepository.Folder.GetProperty(String name) in C:\\Dev10\\SenseNet\\Development\\Budapest\\Source\\SenseNet\\ContentRepository\\Folder.cs:line 51\\n at SenseNet.ContentRepository.Workspaces.Workspace.GetProperty(String name) in C:\\Dev10\\SenseNet\\Development\\Budapest\\Source\\SenseNet\\ContentRepository\\Workspaces\\Workspace.cs:line 118\\n at SenseNet.ContentRepository.Field.ReadProperty(String propertyName) in C:\\Dev10\\SenseNet\\Development\\Budapest\\Source\\SenseNet\\ContentRepository\\Field.cs:line 194\\n at SenseNet.ContentRepository.Field.ReadProperties() in C:\\Dev10\\SenseNet\\Development\\Budapest\\Source\\SenseNet\\ContentRepository\\Field.cs:line 181\\n at SenseNet.ContentRepository.Field.get_Value() in C:\\Dev10\\SenseNet\\Development\\Budapest\\Source\\SenseNet\\ContentRepository\\Field.cs:line 136\\n at SenseNet.ContentRepository.Field.GetData(Boolean localized) in C:\\Dev10\\SenseNet\\Development\\Budapest\\Source\\SenseNet\\ContentRepository\\Field.cs:line 260\\n at SenseNet.Portal.OData.ODataFormatter.WriteContentProperty(String path, String propertyName, Boolean rawValue, PortalContext portalContext, ODataRequest req) in C:\\Dev10\\SenseNet\\Development\\Budapest\\Source\\SenseNet\\Portal\\OData\\ODataFormatter.cs:line 271\\n at SenseNet.Portal.OData.ODataHandler.ProcessRequest(HttpContext context, String httpMethod, Stream inputStream) in C:\\Dev10\\SenseNet\\Development\\Budapest\\Source\\SenseNet\\Portal\\OData\\ODataHandler.cs:line 116\\n=====================\\n" - // } - // } - // } + /// + /// Gets or sets the code of the error. + /// [JsonProperty(PropertyName="code", Order=1)] public string Code { get; set; } + /// + /// Gets or sets the (not fully qualified) type name of the exception. + /// [JsonProperty(PropertyName = "exceptiontype", Order = 2)] public string ExceptionType { get; set; } + /// + /// Gets or sets the message of the OData error. + /// [JsonProperty(PropertyName = "message", Order = 3)] public ErrorMessage Message { get; set; } + /// + /// Gets or sets all information for debugger users. + /// Contains the whole exception chain with messages and stack traces. + /// [JsonProperty(PropertyName = "innererror", Order = 4)] public StackInfo InnerError { get; set; } } + /// + /// Represents the message of the OData error. + /// public class ErrorMessage { + /// + /// Gets or sets the language code of the message (e.g. en-us). + /// [JsonProperty(PropertyName = "lang", Order = 1)] public string Lang { get; set; } + /// + /// Gets or sets the message of the OData error. + /// [JsonProperty(PropertyName = "value", Order = 1)] public string Value { get; set; } } + /// + /// Represents a stack trace information of the OData error. + /// public class StackInfo { + /// + /// Gets or sets the stack trace information of an OData error. + /// [JsonProperty(PropertyName = "trace", Order = 1)] public string Trace { get; set; } } diff --git a/src/Services/OData/JsonFormatter.cs b/src/Services/OData/JsonFormatter.cs index 5143f2bf7..841a63d42 100644 --- a/src/Services/OData/JsonFormatter.cs +++ b/src/Services/OData/JsonFormatter.cs @@ -8,33 +8,57 @@ namespace SenseNet.Portal.OData { + /// + /// Defines an inherited class for writing OData metadata in XML format. + /// public class XmlFormatter : ODataFormatter { + /// + /// Returns with "xml" in this case. public override string FormatName { get { return "xml"; } } + /// + /// Returns with "application/xml" in this case. public override string MimeType { get { return "application/xml"; } } + /// protected override void WriteMetadata(System.IO.TextWriter writer, Metadata.Edmx edmx) { edmx.WriteXml(writer); } + /// This method is not supported in this formatter. protected override void WriteServiceDocument(PortalContext portalContext, IEnumerable names) { throw new SnNotSupportedException(); } + /// This method is not supported in this formatter. protected override void WriteSingleContent(PortalContext portalContext, Dictionary fields) { throw new SnNotSupportedException(); } + /// This method is not supported in this formatter. protected override void WriteActionsProperty(PortalContext portalContext, ODataActionItem[] actions, bool raw) { throw new SnNotSupportedException(); } + /// This method is not supported in this formatter. protected override void WriteError(HttpContext context, Error error) { throw new SnNotSupportedException(); } + /// This method is not supported in this formatter. protected override void WriteOperationCustomResult(PortalContext portalContext, object result, int? allCount) { throw new SnNotSupportedException(); } + /// This method is not supported in this formatter. protected override void WriteMultipleContent(PortalContext portalContext, List> contents, int count) { throw new SnNotSupportedException(); } + /// protected override void WriteCount(PortalContext portalContext, int count) { WriteRaw(count, portalContext); } } + /// + /// Defines an inherited class for writing any OData response in JSON format. + /// public class JsonFormatter : ODataFormatter { + /// + /// Returns with "json" in this case. public override string FormatName { get { return "json"; } } + /// + /// Returns with "application/json" in this case. public override string MimeType { get { return "application/json"; } } + /// protected override void WriteMetadata(System.IO.TextWriter writer, Metadata.Edmx edmx) { edmx.WriteJson(writer); } + /// protected override void WriteServiceDocument(PortalContext portalContext, IEnumerable names) { var resp = portalContext.OwnerHttpContext.Response; @@ -43,14 +67,17 @@ protected override void WriteServiceDocument(PortalContext portalContext, IEnume Newtonsoft.Json.JsonSerializer.Create(new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }) .Serialize(resp.Output, x); } + /// protected override void WriteSingleContent(PortalContext portalContext, Dictionary fields) { Write(new ODataSingleContent { FieldData = fields }, portalContext); } + /// protected override void WriteMultipleContent(PortalContext portalContext, List> contents, int count) { Write(ODataMultipleContent.Create(contents, count), portalContext); } + /// protected override void WriteActionsProperty(PortalContext portalContext, ODataActionItem[] actions, bool raw) { if(raw) @@ -58,6 +85,7 @@ protected override void WriteActionsProperty(PortalContext portalContext, ODataA else Write(new ODataSingleContent { FieldData = new Dictionary { { ODataHandler.PROPERTY_ACTIONS, actions } } }, portalContext); } + /// protected override void WriteOperationCustomResult(PortalContext portalContext, object result, int? allCount) { var dictionaryList = result as List>; @@ -68,10 +96,12 @@ protected override void WriteOperationCustomResult(PortalContext portalContext, } Write(result, portalContext); } + /// protected override void WriteCount(PortalContext portalContext, int count) { WriteRaw(count, portalContext); } + /// protected override void WriteError(HttpContext context, Error error) { var settings = new JsonSerializerSettings @@ -86,19 +116,36 @@ protected override void WriteError(HttpContext context, Error error) context.Response.ContentType = "application/json;odata=verbose;charset=utf-8"; } } + /// + /// Defines an inherited class for writing any OData response in verbose JSON format. + /// public class VerbodeJsonFormatter : JsonFormatter { + /// + /// Returns with "verbosejson" in this case. public override string FormatName { get { return "verbosejson"; } } + /// + /// Returns with "application/json;odata=verbose" in this case. public override string MimeType { get { return "application/json;odata=verbose"; } } } + /// + /// Defines an inherited class for writing OData objects in a simple HTML TABLE format. + /// Designed for debug and test purposes only. + /// public class TableFormatter : ODataFormatter { + /// + /// Returns with "table" in this case. public override string FormatName { get { return "table"; } } + /// + /// Returns with "application/html" in this case. public override string MimeType { get { return "text/html"; } } + /// This method is not supported in this formatter. protected override void WriteMetadata(System.IO.TextWriter writer, Metadata.Edmx edmx) { throw new SnNotSupportedException("Table formatter does not support metadata writing."); } + /// protected override void WriteServiceDocument(PortalContext portalContext, IEnumerable names) { var resp = portalContext.OwnerHttpContext.Response; @@ -113,6 +160,7 @@ protected override void WriteServiceDocument(PortalContext portalContext, IEnume } WriteEnd(resp); } + /// protected override void WriteSingleContent(PortalContext portalContext, Dictionary fields) { var resp = portalContext.OwnerHttpContext.Response; @@ -159,6 +207,7 @@ protected override void WriteSingleContent(PortalContext portalContext, Dictiona } WriteEnd(resp); } + /// protected override void WriteMultipleContent(PortalContext portalContext, List> contents, int count) { var resp = portalContext.OwnerHttpContext.Response; @@ -254,6 +303,7 @@ protected override void WriteMultipleContent(PortalContext portalContext, List\n"); resp.Write("\n"); } + /// protected override void WriteActionsProperty(PortalContext portalContext, ODataActionItem[] actions, bool raw) { // raw parameter isn't used @@ -270,14 +320,17 @@ protected override void WriteActionsProperty(PortalContext portalContext, ODataA WriteMultipleContent(portalContext, data, actions.Length); } + /// This method is not supported in this formatter. protected override void WriteOperationCustomResult(PortalContext portalContext, object result, int? allCount) { throw new NotSupportedException("TableFormatter supports only a Content or an IEnumerable as an operation result."); } + /// protected override void WriteCount(PortalContext portalContext, int count) { WriteRaw(count, portalContext); } + /// protected override void WriteError(HttpContext context, Error error) { var resp = context.Response; diff --git a/src/Services/OData/ODataException.cs b/src/Services/OData/ODataException.cs index 835e1973e..f7f64baaa 100644 --- a/src/Services/OData/ODataException.cs +++ b/src/Services/OData/ODataException.cs @@ -5,43 +5,100 @@ namespace SenseNet.Portal.OData { + /// + /// Represents the qualification of an error in the OData response. + /// public enum ODataExceptionCode { + /// Means: error is not qualified. NotSpecified, /// General request error. RequestError, + /// Content id is in wrong format. InvalidId, + /// The value is invalid in the $top parameter. InvalidTopParameter, + /// The value is negative in the $top parameter. NegativeTopParameter, + /// The value is invalid in the $skip parameter. InvalidSkipParameter, + /// The value is negative in the $skip parameter. NegativeSkipParameter, + /// The value is invalid in the $inlinecount parameter. InvalidInlineCountParameter, + /// The value is invalid in the $format parameter. InvalidFormatParameter, + /// The value is invalid in the $orderby parameter. InvalidOrderByParameter, + /// The direction is invalid in the $orderby parameter. InvalidOrderByDirectionParameter, + /// The value is invalid in the $expand parameter. InvalidExpandParameter, + /// The value is invalid in the $select parameter. InvalidSelectParameter, + /// Means: cannot create the content because it already exists. ContentAlreadyExists, + /// The requested resource was not found. The equivalent HTTP status code: 404. ResourceNotFound, + /// The value cannot serialize to JSON format. CannotConvertToJSON, /// An exception occurs when an action is not an OData operation or an OData action is invoked with HTTP GET. IllegalInvoke, + /// The current user has not enough permission for accessing the requested resource. The equivalent HTTP status code: 403. Forbidden, + /// User is not authorized. The equivalent HTTP status code: 401. Unauthorized } + /// + /// Represents a general error in OData. + /// [Serializable] public class ODataException : Exception { + /// + /// Gets the of the error. + /// public ODataExceptionCode ODataExceptionCode { get; private set; } + /// + /// Gets the string representation of the ODataExceptionCode property. + /// The value can be extension of the enumeration. + /// public string ErrorCode { get; internal set; } + /// + /// Gets the string representation of the HTTP Status Cose (e.g. "403 Forbidden"). + /// public string HttpStatus { get; private set; } + /// + /// Gets the HTTP Status Code (e.g. 403). + /// public int HttpStatusCode { get; private set; } + /// + /// Initializes a new instance of the ODataException with a general message and the given error code. + /// + /// The of the exception. public ODataException(ODataExceptionCode code) : base(String.Format("An exception occured during the processing an OData request. Code: {0} ({1})", Convert.ToInt32(code), code.ToString())) { Initialize(code); } + /// + /// Initializes a new instance of the ODataException with the given parameters. + /// + /// The message of the error. + /// The of the exception. public ODataException(string message, ODataExceptionCode code) : base(message) { Initialize(code); } + /// + /// Initializes a new instance of the ODataException with the given parameters. + /// + /// The message of the error. + /// The of the exception. + /// The wrapped exception. public ODataException(string message, ODataExceptionCode code, Exception inner) : base(message, inner) { Initialize(code); } + /// + /// Initializes a new instance of the ODataException with the given parameters. + /// + /// The of the exception. + /// The wrapped exception. public ODataException(ODataExceptionCode code, Exception inner) : base(GetRelevantMessage(inner), inner) { Initialize(code); } + /// protected ODataException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) : base(info, context) { } private const string HTTPERROR200 = "200 Ok"; diff --git a/src/Services/OData/ODataFormatter.cs b/src/Services/OData/ODataFormatter.cs index 914db859e..59a29c8c4 100644 --- a/src/Services/OData/ODataFormatter.cs +++ b/src/Services/OData/ODataFormatter.cs @@ -21,36 +21,48 @@ namespace SenseNet.Portal.OData { + /// + /// Defines a base class for serializing the OData response object to various formats. + /// public abstract class ODataFormatter { + /// + /// Gets the name of the format that is used in the "$format" parameter of the OData webrequest. + /// public abstract string FormatName { get; } + /// + /// Gets the mime type of the converted object. + /// public abstract string MimeType { get; } internal ODataRequest ODataRequest { get; private set; } - internal protected PortalContext PortalContext { get; protected set; } + /// + /// Gets the current instance. + /// + protected internal PortalContext PortalContext { get; protected set; } private static object _formatterTypeLock = new object(); private static Dictionary _formatterTypes; - internal static Dictionary FormatterTypes - { - get + internal static Dictionary FormatterTypes + { + get { if (_formatterTypes == null) - { + { lock(_formatterTypeLock) { if (_formatterTypes == null) { _formatterTypes = LoadFormatterTypes(); - SnLog.WriteInformation("OData formatter types loaded: " + + SnLog.WriteInformation("OData formatter types loaded: " + string.Join(", ", _formatterTypes.Values.Select(t => t.FullName))); } } } return _formatterTypes; - } + } } private static Dictionary LoadFormatterTypes() @@ -87,7 +99,7 @@ internal static ODataFormatter Create(string formatName, PortalContext portalCon formatter.PortalContext = portalContext; return formatter; } - + internal void Initialize(ODataRequest odataRequest) { this.ODataRequest = odataRequest; @@ -109,6 +121,11 @@ private string[] GetTopLevelNames(ODataRequest req) return site?.Children.Select(n => n.Name).ToArray() ?? new[] {Repository.RootName}; } + /// + /// Writes the OData service document with the given root names to the webresponse. + /// + /// The current instance containing the current webresponse. + /// Root names. protected abstract void WriteServiceDocument(PortalContext portalContext, IEnumerable names); internal void WriteMetadata(HttpContext context, ODataRequest req) @@ -129,6 +146,11 @@ internal void WriteMetadataInternal(TextWriter writer, Metadata.Edmx edmx) { WriteMetadata(writer, edmx); } + /// + /// Writes the OData service metadata to the given text writer + /// + /// Output writer. + /// Metadata that will be written. protected abstract void WriteMetadata(TextWriter writer, Metadata.Edmx edmx); // --------------------------------------------------------------------------------------------------------------- contents @@ -143,6 +165,11 @@ internal void WriteSingleContent(Content content, PortalContext portalContext) var fields = CreateFieldDictionary(content, portalContext, false); WriteSingleContent(portalContext, fields); } + /// + /// Writes the given fields of a Content to the webresponse. + /// + /// The current instance containing the current webresponse. + /// A Dictionary<string, object> that will be written. protected abstract void WriteSingleContent(PortalContext portalContext, Dictionary fields); internal void WriteChildrenCollection(String path, PortalContext portalContext, ODataRequest req) @@ -197,7 +224,7 @@ private void WriteMultiRefContents(object references, PortalContext portalContex { var contents = new List>(); //TODO: ODATA: multiref item: get available types from reference property - contents.Add(CreateFieldDictionary(Content.Create(node), portalContext, projector)); + contents.Add(CreateFieldDictionary(Content.Create(node), portalContext, projector)); WriteMultipleContent(portalContext, contents, 1); } else @@ -260,7 +287,19 @@ private void WriteSingleRefContent(object references, PortalContext portalContex } } } + /// + /// Writes the given Content list to the webresponse. + /// + /// The current instance containing the current webresponse. + /// A List<Dictionary<string, object>> that will be written. + /// Count of contents. This value can be different from the count of the written content list if the request has restrictions in connection with cardinality (e.g. "$top=10") but specifies the total count of the collection ("$inlinecount=allpages"). protected abstract void WriteMultipleContent(PortalContext portalContext, List> contents, int count); + /// + /// Writes only the count of the requested resource to the webresponse. + /// Activated if the URI of the requested resource contains the "$count" segment. + /// + /// The current instance containing the current webresponse. + /// protected abstract void WriteCount(PortalContext portalContext, int count); internal void WriteContentProperty(String path, string propertyName, bool rawValue, PortalContext portalContext, ODataRequest req) @@ -274,7 +313,7 @@ internal void WriteContentProperty(String path, string propertyName, bool rawVal if (propertyName == ODataHandler.PROPERTY_ACTIONS) { - WriteActionsProperty(portalContext, ODataTools.GetHtmlActionItems(content, req).ToArray(), rawValue); + WriteActionsProperty(portalContext, ODataTools.GetActionItems(content, req).ToArray(), rawValue); return; } @@ -316,6 +355,12 @@ internal void WriteContentProperty(String path, string propertyName, bool rawVal WriteOperationResult(portalContext, req); } } + /// + /// Writes the available actions of the current to the webresponse. + /// + /// The current instance containing the current webresponse. + /// Array of that will be written. + /// protected abstract void WriteActionsProperty(PortalContext portalContext, ODataActionItem[] actions, bool raw); @@ -349,6 +394,11 @@ internal void WriteErrorResponse(HttpContext context, ODataException oe) context.Response.TrySkipIisCustomErrors = true; } + /// + /// Writes the given instance to the webresponse. + /// + /// The current instance. + /// The instance that will be written. protected abstract void WriteError(HttpContext context, Error error); // --------------------------------------------------------------------------------------------------------------- operations @@ -454,6 +504,13 @@ private void WriteOperationResult(object result, PortalContext portalContext, OD WriteOperationCustomResult(portalContext, result, odataReq.InlineCount == InlineCount.AllPages ? allCount : (int?)null); } + /// + /// Writes a custom operations's result object to the webresponse. + /// + /// The current instance containing the current webresponse. + /// The object that will be written. + /// A nullable int that contains the count of items in the result object + /// if the request specifies the total count of the collection ("$inlinecount=allpages"), otherwise the value is null. protected abstract void WriteOperationCustomResult(PortalContext portalContext, object result, int? allCount); private object ProcessOperationResponse(object response, PortalContext portalContext, ODataRequest odataReq, out int count) @@ -869,13 +926,30 @@ internal static object GetJsonObject(Field field, string selfUrl) { return ODataReference.Create(String.Concat(selfUrl, "/", field.Name)); } - data = field.GetData(); + try + { + data = field.GetData(); + } + catch (SenseNetSecurityException) + { + // The user does not have access to this field (e.g. cannot load + // a referenced content). In this case we serve a null value. + data = null; + + SnTrace.Repository.Write("PERMISSION warning: user {0} does not have access to field '{1}' of {2}.", User.LoggedInUser.Username, field.Name, field.Content.Path); + } + var nodeType = data as NodeType; if (nodeType != null) return nodeType.Name; return data; } + /// + /// Writes an object to the webresponse. + /// + /// The object that will be written. + /// The current instance containing the current webresponse. protected void Write(object response, PortalContext portalContext) { var resp = portalContext.OwnerHttpContext.Response; @@ -902,6 +976,11 @@ protected void Write(object response, PortalContext portalContext) serializer.Serialize(portalContext.OwnerHttpContext.Response.Output, response); resp.ContentType = "application/json;odata=verbose;charset=utf-8"; } + /// + /// Writes an object to the webresponse. Tipically used for writing a simple object (e.g. values). + /// + /// The object that will be written. + /// The current instance containing the current webresponse. protected void WriteRaw(object response, PortalContext portalContext) { var resp = portalContext.OwnerHttpContext.Response; diff --git a/src/Services/OData/ODataHandler.cs b/src/Services/OData/ODataHandler.cs index dcfaa9fa4..08212492b 100644 --- a/src/Services/OData/ODataHandler.cs +++ b/src/Services/OData/ODataHandler.cs @@ -18,11 +18,15 @@ using SenseNet.Search; using SenseNet.ContentRepository.Linq; using System.Linq.Expressions; +using System.Net.Http; using SenseNet.ContentRepository.Schema; using SenseNet.Tools; namespace SenseNet.Portal.OData { + /// + /// An implementation to process the OData requests. + /// public class ODataHandler : IHttpHandler { internal static IActionResolver ActionResolver { get; private set; } @@ -59,6 +63,8 @@ static ODataHandler() internal const string PROPERTY_BINARY = "Binary"; internal const int EXPANSIONLIMIT = Int32.MaxValue; + /// + /// Returns with false in this implementation. public bool IsReusable { get { return false; } @@ -66,10 +72,18 @@ public bool IsReusable internal ODataRequest ODataRequest { get; private set; } + /// + /// Processes the OData web request. public void ProcessRequest(HttpContext context) { ProcessRequest(context, context.Request.HttpMethod.ToUpper(), context.Request.InputStream); } + /// + /// Processes the OData web request. Designed for test purposes. + /// + /// An object that provides references to the intrinsic server objects (for example, , , , and ) used to service HTTP requests. + /// HTTP protocol method. + /// Request stream containing the posted JSON object. public void ProcessRequest(HttpContext context, string httpMethod, Stream inputStream) { ODataRequest odataReq = null; @@ -297,9 +311,14 @@ internal static JObject Read(Stream inputStream) return null; using (var reader = new StreamReader(inputStream)) models = reader.ReadToEnd(); - + return Read(models); } + /// + /// Helper method for deserializing the given string representation. + /// + /// JSON object that will be deserialized. + /// Deserialized JObject instance. public static JObject Read(string models) { if (string.IsNullOrEmpty(models)) @@ -353,7 +372,7 @@ internal static string GetEntityUrl(string path) { var sitePath = PortalContext.Current.Site?.Path; if (!string.IsNullOrEmpty(sitePath) && - path.StartsWith(sitePath, StringComparison.OrdinalIgnoreCase) && + path.StartsWith(sitePath, StringComparison.OrdinalIgnoreCase) && !sitePath.Equals(path, StringComparison.OrdinalIgnoreCase)) path = path.Substring(sitePath.Length); } @@ -450,7 +469,7 @@ private Content CreateContent(JObject model, ODataRequest odataRequest) var templated = ContentTemplate.CreateFromTemplate(parent, template, name); content = Content.Create(templated); } - + UpdateFields(content, model); @@ -527,8 +546,16 @@ private void UpdateContent(Content content, JObject model, ODataRequest odataReq else content.Save(); } + /// + /// Helper method for updating the given with a model represented by JObject. + /// The will not be saved. + /// + /// The that will be modified. Cannot be null. + /// The modifier JObject instance. Cannot be null. public static void UpdateFields(Content content, JObject model) { + //UNDONE: ArgumentNullException: content + //UNDONE: ArgumentNullException: model Field field; var isNew = content.Id == 0; foreach (var prop in model.Properties()) @@ -566,8 +593,8 @@ public static void UpdateFields(Content content, JObject model) } if (field is ReferenceField && jvalue.Value != null) { - var refNode = jvalue.Type == JTokenType.Integer - ? Node.LoadNode(Convert.ToInt32(jvalue.Value)) + var refNode = jvalue.Type == JTokenType.Integer + ? Node.LoadNode(Convert.ToInt32(jvalue.Value)) : Node.LoadNode(jvalue.Value.ToString()); field.SetData(refNode); @@ -649,7 +676,7 @@ private T GetPropertyValue(string name, JObject model) /// Returns an OData path that can request the entity identified by the given path. This path is part of the OData entity request. For example /// "/Root/MyFolder/MyDocument.doc" will be transformed to "/Root/MyFolder('MyDocument.doc')" /// - /// This path will be transformed + /// This path will be transformed. /// An OData path. public static string GetODataPath(string path) { @@ -659,10 +686,10 @@ public static string GetODataPath(string path) } /// /// Returns an OData path that can request the entity identified by the given path plus name. This path is part of the OData entity request. For example - /// path = "/Root/MyFolder" and name = "MyDocument.doc" will be transformed to "/Root/MyFolder('MyDocument.doc')" + /// path = "/Root/MyFolder" and name = "MyDocument.doc" will be transformed to "/Root/MyFolder('MyDocument.doc')". /// - /// A container path - /// Content's name in the given container + /// A container path. + /// Content's name in the given container. /// An OData path. public static string GetODataPath(string parentPath, string name) { diff --git a/src/Services/OData/ODataRequest.cs b/src/Services/OData/ODataRequest.cs index 1bc624a34..b75f122c4 100644 --- a/src/Services/OData/ODataRequest.cs +++ b/src/Services/OData/ODataRequest.cs @@ -16,53 +16,195 @@ namespace SenseNet.Portal.OData { internal enum OutputFormat { None, JSON, VerboseJSON, Atom, Xml } - public enum InlineCount { None, AllPages } - public enum MetadataFormat {None, Minimal, Full} + /// + /// Defines values for handling collection count. + /// + public enum InlineCount + { + /// + /// Not defined value. This is the default. + /// + None, + /// + /// Defines that the client requires the total count of collection even if the + /// request has restrictions in connection with cardinality (e.g. "$top=10"). + /// + AllPages + } + + /// + /// Defines values for metadata verbosity. + /// + public enum MetadataFormat + { + /// + /// There is no metadata writing at all. + /// + None, + /// + /// Writing metadata with minimal information. + /// + Minimal, + /// + /// Writing the whole metadata. This is the default value. + /// + Full + } + + /// + /// Represents a class that contains all OData related elements of the current webrequest. + /// public class ODataRequest { + /// + /// Gets the id of the requested . + /// public int RequestedContentId { get; private set; } + /// + /// Gets the path of the requested . + /// public string RepositoryPath { get; private set; } + /// + /// Gets the name of the requested property or null. + /// public string PropertyName { get; private set; } + /// + /// Gets the value of the "query" webrequest's parameter + /// if there is one. Otherwise returns with null. + /// public string ContentQueryText { get; private set; } + /// + /// Gets the value of the "scenario" webrequest's parameter + /// if there is one. Otherwise returns with null. + /// public string Scenario { get; private set; } + /// + /// Gets a value that is true if the requested resource is a collection. + /// public bool IsCollection { get; private set; } + /// + /// Gets a value that is true if the current webrequest is an OData metadata request + /// (the URI of the requested resource contains the "$metadata" segment). + /// public bool IsMetadataRequest { get; private set; } + /// + /// Gets true if the URI of the requested single resource refers to its member. + /// public bool IsMemberRequest { get; private set; } + /// + /// Gets true if the URI of the requested single resource refers to its member's value. + /// (the URI of the requested resource ends with the "$value" segment). + /// public bool IsRawValueRequest { get; private set; } + /// + /// Gets true if the webrequest is the service document request. + /// public bool IsServiceDocumentRequest { get; private set; } internal const string SCENARIO = "scenario"; // url param internal const string CONTENTQUERY = "query"; // url param private const string IDREQUEST_REGEX = "/content\\((?\\d+)\\)"; + /// + /// Gets the value of the "$top" OData parameter if it exists, otherwise returns with 0. + /// public int Top { get; private set; } + /// + /// Gets the value of the "$skip" OData parameter if it exists, otherwise returns with 0. + /// public int Skip { get; private set; } + /// + /// Gets the value of the "$inlinecount" OData parameter if it exists, otherwise returns with "None". + /// public InlineCount InlineCount { get; private set; } + /// + /// Gets true if the last URI segment of the requested resource is "$count". + /// public bool CountOnly { get; private set; } + /// + /// Gets the value of the "$format" OData parameter if it exists, otherwise returns with null. + /// public string Format { get; internal set; } + /// + /// Gets the collection of from the "$orderby" parameter of the webrequest. + /// public IEnumerable Sort { get; internal set; } + /// + /// Gets the string list of the projection from the "$select" parameter of the webrequest. + /// public List Select { get; private set; } + /// + /// Gets the string list of the expanded members from the "$expand" parameter of the webrequest. + /// public List Expand { get; private set; } + /// + /// Gets the parsed from the "$filter" parameter of the webrequest. + /// public Expression Filter { get; internal set; } + /// + /// Gets the value of the "enableautofilters" parameter of the webrequest if it exists, otherwise returs with FilterStatus.Default. + /// public FilterStatus AutofiltersEnabled { get; internal set; } + /// + /// Gets the value of the "enablelifespanfilter" parameter of the webrequest if it exists, otherwise returs with FilterStatus.Default. + /// public FilterStatus LifespanFilterEnabled { get; internal set; } + /// + /// Gets the value of the "queryexecutionmode" parameter of the webrequest if it exists, otherwise returs with FilterStatus.Default. + /// public QueryExecutionMode QueryExecutionMode { get; internal set; } + /// + /// Gets the value of the "metadata" parameter of the webrequest if it exists, otherwise returs with MetadataFormat.Full. + /// public MetadataFormat EntityMetadata { get; internal set; } + /// + /// Gets true if the "includebackurl" parameter of the webrequest is "true" if it exists, otherwise returs with false. + /// public bool IncludeBackUrl { get; private set; } + /// + /// Gets true if the value of the Top property is greater than 0. + /// public bool HasTop { get { return Top > 0; } } + /// + /// Gets true if the value of the Skip property is greater than 0. + /// public bool HasSkip { get { return Skip > 0; } } + /// + /// Gets true if the value of the Sort property contains any element. + /// public bool HasSort { get { return Sort.Count() > 0; } } + /// + /// Gets true if the value of the InlineCount property is not "None". + /// public bool HasInlineCount { get { return InlineCount != InlineCount.None; } } + /// + /// Gets true if the value of the Select property contains any element. + /// public bool HasSelect { get { return Select.Count > 0; } } + /// + /// Gets true if the value of the Expand property contains any element. + /// public bool HasExpand { get { return Expand.Count > 0; } } + /// + /// Gets true if the Filter is not null. + /// public bool HasFilter { get { return Filter != null; } } + /// + /// Gets true if the ContentQueryText property is not null. + /// public bool HasContentQuery { get { return !String.IsNullOrEmpty(this.ContentQueryText); } } + /// + /// Gets true if the webrequest contains the "multistepsave" parameter with "true" value. + /// public bool MultistepSave { get; private set; } + /// + /// Gets the instance if there was any request parsing error. + /// public Exception RequestError { get; private set; } private ODataRequest() @@ -100,7 +242,7 @@ internal static string GetContentPathFromODataRequest(string requestPath) // check if this is an id request instead of a path request var match = new Regex(IDREQUEST_REGEX, RegexOptions.IgnoreCase).Match(path); if (match.Success) - { + { var idString = match.Groups["id"].Value; var nodeId = 0; if (int.TryParse(idString, out nodeId)) @@ -110,7 +252,7 @@ internal static string GetContentPathFromODataRequest(string requestPath) return nodeHead.Path; } } - + // remove everything after item request characters (property accessor or action/function name) var itemLastIndex = path.LastIndexOf("')"); if (itemLastIndex > 0 && path.Length > itemLastIndex + 2) @@ -283,7 +425,8 @@ private void ParseQuery(string path, PortalContext portalContext) var x = Enum.TryParse(inlineCountStr, true, out ic); if (!x || ic < 0 || (int)ic > 1) throw new ODataException(SNSR.Exceptions.OData.InvalidInlineCountOption, ODataExceptionCode.InvalidInlineCountParameter); - } InlineCount = ic; + } + InlineCount = ic; // --------------------------------------------------------------- $select var selectStr = req["$select"]; @@ -382,6 +525,5 @@ private string ParseContentQueryText(PortalContext portalContext) { return portalContext.OwnerHttpContext.Request[CONTENTQUERY]; } - } } \ No newline at end of file diff --git a/src/Services/OData/ODataTools.cs b/src/Services/OData/ODataTools.cs index d8311ddd5..6ddd6bf16 100644 --- a/src/Services/OData/ODataTools.cs +++ b/src/Services/OData/ODataTools.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Web; using SenseNet.ApplicationModel; @@ -91,9 +92,59 @@ internal static IEnumerable GetHtmlActionItems(Content content, Index = a.Index, Url = a.Uri, IncludeBackUrl = a.GetApplication() == null ? 0 : (int)a.GetApplication().IncludeBackUrl, - ClientAction = a is ClientAction && !string.IsNullOrEmpty(((ClientAction)a).Callback), + ClientAction = !string.IsNullOrEmpty((a as ClientAction)?.Callback), Forbidden = a.Forbidden }); } + + internal static IEnumerable GetActionItems(Content content, ODataRequest request) + { + return GetActionsWithScenario(content, request).Select(a => new ODataActionItem + { + Name = a.Action.Name, + DisplayName = SNSR.GetString(a.Action.Text), + Icon = a.Action.Icon, + Index = a.Action.Index, + Url = a.Action.Uri, + IncludeBackUrl = a.Action.GetApplication() == null ? 0 : (int)a.Action.GetApplication().IncludeBackUrl, + ClientAction = !string.IsNullOrEmpty((a.Action as ClientAction)?.Callback), + Forbidden = a.Action.Forbidden, + IsODataAction = a.Action.IsODataOperation, + ActionParameters = a.Action.ActionParameters.Select(p => p.Name).ToArray(), + Scenario = a.Scenario + }); + } + + private struct ScenarioAction + { + public ActionBase Action { get; set; } + public string Scenario { get; set; } + } + + private static IEnumerable GetActionsWithScenario(Content content, ODataRequest request) + { + // Use the back url provided by the client. If it is empty, use + // the url of the caller page (the referrer provided by ASP.NET). + // The back url can be omitted (switched off) by the client if it provides the + // appropriate request parameter (includebackurl false). + var backUrl = PortalContext.Current != null && (request == null || request.IncludeBackUrl) + ? PortalContext.Current.BackUrl + : null; + + if (string.IsNullOrEmpty(backUrl) && (request == null || request.IncludeBackUrl) && + HttpContext.Current?.Request?.UrlReferrer != null) + { + backUrl = HttpContext.Current.Request.UrlReferrer.ToString(); + } + + var scenario = request?.Scenario; + var actions = ActionFramework.GetActions(content, scenario, string.IsNullOrEmpty(backUrl) ? null : backUrl); + + return actions.Select(action => new ScenarioAction + { + Action = action, + Scenario = scenario + }); + } } } diff --git a/src/Services/OData/Parser/ExpressionBuilder.cs b/src/Services/OData/Parser/ExpressionBuilder.cs index 3904b3ad5..c84acb9a5 100644 --- a/src/Services/OData/Parser/ExpressionBuilder.cs +++ b/src/Services/OData/Parser/ExpressionBuilder.cs @@ -91,8 +91,8 @@ internal Expression BuildMemberPath(List steps) int k; if (steps[0].Contains('.')) { - type = TypeResolver.GetType(steps[0]); - if(type == null) + type = TypeResolver.GetType(steps[0], false); + if (type == null) throw ODataParser.SyntaxError(this.Parser.Lexer, "Unknown type: " + steps[0]); k = 1; } diff --git a/src/Services/OData/Parser/Globals.cs b/src/Services/OData/Parser/Globals.cs index 69f251c79..5a4d99465 100644 --- a/src/Services/OData/Parser/Globals.cs +++ b/src/Services/OData/Parser/Globals.cs @@ -5,7 +5,7 @@ namespace SenseNet.Portal.OData.Parser { - public class ODataPoint + internal class ODataPoint { public double X { get; set; } public double Y { get; set; } diff --git a/src/Services/OData/Parser/ODataParserException.cs b/src/Services/OData/Parser/ODataParserException.cs index 76a10b16d..0c03ad9af 100644 --- a/src/Services/OData/Parser/ODataParserException.cs +++ b/src/Services/OData/Parser/ODataParserException.cs @@ -5,16 +5,44 @@ namespace SenseNet.Portal.OData.Parser { + /// + /// Represents a parsing error of an OData request. + /// [Serializable] public class ODataParserException : Exception { + /// + /// Gets the line number of the error position in the source. + /// public int Line { get; private set; } + /// + /// Gets the column number of the error position in the source. + /// public int Column { get; private set; } + /// + /// Initializes a new instance of the ODataParserException. + /// + /// The line number of the error position in the source. + /// The column number of the error position in the source. public ODataParserException(int line, int column) { Line = line; Column = column; } + /// + /// Initializes a new instance of the ODataParserException. + /// + /// The message of the error. + /// The line number of the error position in the source. + /// The column number of the error position in the source. public ODataParserException(string message, int line, int column) : base(message) { Line = line; Column = column; } + /// + /// Initializes a new instance of the ODataParserException. + /// + /// The message of the error. + /// The line number of the error position in the source. + /// The column number of the error position in the source. + /// The original exception that is wrapped by this instance. public ODataParserException(string message, int line, int column, Exception inner) : base(message, inner) { Line = line; Column = column; } + /// protected ODataParserException( System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) diff --git a/src/Services/OData/Projector.cs b/src/Services/OData/Projector.cs index 342830d1e..9d2ac9179 100644 --- a/src/Services/OData/Projector.cs +++ b/src/Services/OData/Projector.cs @@ -114,7 +114,7 @@ protected bool IsAllowedField(Content content, string fieldName) protected ODataActionItem[] GetActions(Content content) { - return ODataTools.GetHtmlActionItems(content, this.Request).ToArray(); + return ODataTools.GetActionItems(content, this.Request).ToArray(); } } } diff --git a/src/Services/OData/SnJsonConverter.cs b/src/Services/OData/SnJsonConverter.cs index 0ffcdd2c4..4d3a014cc 100644 --- a/src/Services/OData/SnJsonConverter.cs +++ b/src/Services/OData/SnJsonConverter.cs @@ -10,6 +10,9 @@ namespace SenseNet.Portal.OData { + /// + /// Provides helper methods for serializing OData response objects to JSON format. + /// public static class SnJsonConverter { private static List _jsonConverters; diff --git a/src/Services/OData/Typescript/TypescriptFormatter.cs b/src/Services/OData/Typescript/TypescriptFormatter.cs index cb31365b6..8af087274 100644 --- a/src/Services/OData/Typescript/TypescriptFormatter.cs +++ b/src/Services/OData/Typescript/TypescriptFormatter.cs @@ -10,11 +10,19 @@ namespace SenseNet.Portal.OData.Typescript { + /// + /// Defines an inherited class for writing OData metadata in TypeScript format. + /// public class TypescriptFormatter : JsonFormatter { + /// + /// Returns with "typescript" in this case. public override string FormatName { get { return "typescript"; } } + /// + /// Returns with "text/x-typescript" in this case. public override string MimeType { get { return "text/x-typescript"; } } + /// protected override void WriteMetadata(System.IO.TextWriter writer, Metadata.Edmx edmx) { var requestedModule = HttpContext.Current.Request["module"]?.ToLowerInvariant(); @@ -50,11 +58,17 @@ protected override void WriteMetadata(System.IO.TextWriter writer, Metadata.Edmx + ". Valid names: enums, complextypes, contenttypes, resources, schemas, fieldsettings."); } } + /// This method is not supported in this formatter. protected override void WriteServiceDocument(PortalContext portalContext, IEnumerable names) { throw new SnNotSupportedException(); } + /// This method is not supported in this formatter. protected override void WriteSingleContent(PortalContext portalContext, Dictionary fields) { throw new SnNotSupportedException(); } + /// This method is not supported in this formatter. protected override void WriteActionsProperty(PortalContext portalContext, ODataActionItem[] actions, bool raw) { throw new SnNotSupportedException(); } + /// This method is not supported in this formatter. protected override void WriteOperationCustomResult(PortalContext portalContext, object result, int? allCount) { throw new SnNotSupportedException(); } + /// This method is not supported in this formatter. protected override void WriteMultipleContent(PortalContext portalContext, List> contents, int count) { throw new SnNotSupportedException(); } + /// protected override void WriteCount(PortalContext portalContext, int count) { WriteRaw(count, portalContext); diff --git a/src/Services/Properties/AssemblyInfo.cs b/src/Services/Properties/AssemblyInfo.cs index 375ed06f7..7a5cf7302 100644 --- a/src/Services/Properties/AssemblyInfo.cs +++ b/src/Services/Properties/AssemblyInfo.cs @@ -3,6 +3,7 @@ using System.Runtime.InteropServices; [assembly: InternalsVisibleTo("SenseNet.Tests")] +[assembly: InternalsVisibleTo("SenseNet.Services.Tests")] #if DEBUG [assembly: AssemblyTitle("SenseNet.Services (Debug)")] @@ -21,4 +22,4 @@ // This attribute is used by NuGet to determine the package file name and version. // It may contain a SemVer value. -[assembly: AssemblyInformationalVersion("7.0.0-beta4")] +[assembly: AssemblyInformationalVersion("7.0.0")] diff --git a/src/Services/SR.cs b/src/Services/SR.cs index 11294d3cc..7b2ee6cc5 100644 --- a/src/Services/SR.cs +++ b/src/Services/SR.cs @@ -28,6 +28,7 @@ internal class OData public static string ResourceNotFound_2 = "$Error_Portal:OData_ResourceNotFound_2"; public static string CannotConvertToJSON_2 = "$Error_Portal:OData_CannotConvertToJSON_2"; public static string ContentAlreadyExists_1 = "$Error_Portal:OData_ContentAlreadyExists_1"; + public static string ErrorContentNotFound = "$Error_Portal:ErrorContentNotFound"; public static string RestoreExistingName = "$Error_Portal:OData_Restore_ExistingName"; public static string RestoreForbiddenContentType = "$Error_Portal:OData_Restore_ForbiddenContentType"; diff --git a/src/Services/SenseNetGlobal.cs b/src/Services/SenseNetGlobal.cs index 2bcc7f484..18324839a 100644 --- a/src/Services/SenseNetGlobal.cs +++ b/src/Services/SenseNetGlobal.cs @@ -109,7 +109,6 @@ protected virtual void Application_Start(object sender, EventArgs e, HttpApplica var runOnceMarkerPath = application.Server.MapPath("/" + RunOnceGuid); var firstRun = File.Exists(runOnceMarkerPath); var startConfig = new RepositoryStartSettings { StartLuceneManager = !firstRun, IsWebContext = true }; - startConfig.ConfigureProvider(typeof(ElevatedModificationVisibilityRule), typeof(SnElevatedModificationVisibilityRule)); RepositoryInstance.WaitForWriterLockFileIsReleased(RepositoryInstance.WaitForLockFileType.OnStart); diff --git a/src/Services/Services.Install.nuspec b/src/Services/Services.Install.nuspec index faa43be69..8a10aa9ed 100644 --- a/src/Services/Services.Install.nuspec +++ b/src/Services/Services.Install.nuspec @@ -2,7 +2,7 @@ SenseNet.Services.Install - 7.0.0-beta4 + 7.0.0 sensenet ECM Services install package kavics,aniko,laci,borsi,lajos,tusmester Sense/Net @@ -15,7 +15,7 @@ Copyright © Sense/Net Inc. sensenet ecm ecms - + diff --git a/src/Services/Services.csproj b/src/Services/Services.csproj index 7e87268a2..39fa96bdc 100644 --- a/src/Services/Services.csproj +++ b/src/Services/Services.csproj @@ -47,6 +47,8 @@ 4 AllRules.ruleset false + + pdbonly @@ -240,6 +242,8 @@ + + @@ -322,6 +326,7 @@ + @@ -332,6 +337,7 @@ + diff --git a/src/Services/Services.nuspec b/src/Services/Services.nuspec index b3d86c2ec..d6875be6b 100644 --- a/src/Services/Services.nuspec +++ b/src/Services/Services.nuspec @@ -2,7 +2,7 @@ SenseNet.Services - 7.0.0-beta4 + 7.0.0 sensenet ECM Services kavics,aniko,laci,borsi,lajos,tusmester Sense/Net @@ -20,9 +20,9 @@ - - - + + + diff --git a/src/Services/Virtualization/AuthenticationHelper.cs b/src/Services/Virtualization/AuthenticationHelper.cs index e1422568c..61b8ba1f3 100644 --- a/src/Services/Virtualization/AuthenticationHelper.cs +++ b/src/Services/Virtualization/AuthenticationHelper.cs @@ -11,6 +11,8 @@ using SenseNet.ContentRepository.Storage.Data; using SenseNet.ContentRepository.Storage.Security; using SenseNet.Diagnostics; +using System.Security.Principal; +using SenseNet.ContentRepository.Security; namespace SenseNet.Portal.Virtualization { @@ -150,5 +152,18 @@ private static string GetSessionIdCookieName() var sessionStateSection = (SessionStateSection)ConfigurationManager.GetSection("system.web/sessionState"); return sessionStateSection != null ? sessionStateSection.CookieName : string.Empty; } + + public static Func GetContext = sender => new HttpContextWrapper(((HttpApplication)sender).Context); + public static Func GetRequest = sender => new HttpRequestWrapper(((HttpApplication)sender).Context.Request); + public static Func GetResponse = sender => new HttpResponseWrapper(((HttpApplication)sender).Context.Response); + + public static Func GetVisitorPrincipal = () => new PortalPrincipal(User.Visitor); + public static Func LoadUserPrincipal = userName => new PortalPrincipal(User.Load(userName)); + + public static Func IsUserValid = (userName, password) => Membership.ValidateUser(userName, password); + + public static Func GetSystemAccount = () => new SystemAccount(); + public static Func GetBasicAuthHeader = () => PortalContext.Current.BasicAuthHeaders; + } } diff --git a/src/Services/Virtualization/HttpHeaderTools.cs b/src/Services/Virtualization/HttpHeaderTools.cs index 4d2855030..d0687a819 100644 --- a/src/Services/Virtualization/HttpHeaderTools.cs +++ b/src/Services/Virtualization/HttpHeaderTools.cs @@ -30,7 +30,8 @@ public static class HttpHeaderTools { "X-Authentication-Type", "X-Refresh-Data", "X-Access-Data", - "X-Requested-With", "Authorization", "Content-Type" + "X-Requested-With", "Authorization", "Content-Type", + "Content-Range", "Content-Disposition" }; private delegate void PurgeDelegate(IEnumerable urls); diff --git a/src/Services/Virtualization/OAuthManager.cs b/src/Services/Virtualization/OAuthManager.cs new file mode 100644 index 000000000..45ad4cf33 --- /dev/null +++ b/src/Services/Virtualization/OAuthManager.cs @@ -0,0 +1,183 @@ +using System; +using System.Linq; +using System.Net; +using System.Web; +using SenseNet.Configuration; +using SenseNet.ContentRepository; +using SenseNet.ContentRepository.Security; +using SenseNet.ContentRepository.Storage; +using SenseNet.ContentRepository.Storage.Security; +using SenseNet.Diagnostics; +using SenseNet.Portal.Virtualization; +using SenseNet.Search; + +namespace SenseNet.Services.Virtualization +{ + internal class SafeQueries : ISafeQueryHolder + { + public static string UsersByOAuthId => "+TypeIs:User +@0:@1"; + } + + internal class OAuthManager + { + private const string OAuthPathLogin = "/sn-oauth/login"; + private const string OAuthPathCallback = "/sn-oauth/callback"; + private const string SettingsName = "OAuth"; + private const string UserTypeSettingName = "UserType"; + private const string DomainSettingName = "Domain"; + + internal static OAuthManager Instance = new OAuthManager(); + + internal bool Authenticate(HttpApplication application, Portal.Virtualization.TokenAuthentication tokenAuthentication) + { + var request = AuthenticationHelper.GetRequest(application); + var isLoginRequest = IsLoginRequest(request); + var isCallbackRequest = IsCallbackRequest(request); + + // Currently only login requests are implemented. In the future + // we may implement/handle server-side callback requests too. + + if (!isLoginRequest && !isCallbackRequest) + return false; + + var providerName = GetProviderName(request); + if (string.IsNullOrEmpty(providerName)) + throw new InvalidOperationException("Provider parameter is missing from the request."); + + // Verify the token with the selected provider, and load or create + // the user in the repository if the token is valid. + var user = VerifyUser(providerName, request); + if (user == null) + return false; + + application.Context.User = new PortalPrincipal(user); + + var context = AuthenticationHelper.GetContext(application); //HttpContext.Current; + + try + { + // set the necessary JWT cookies and tokens + tokenAuthentication.TokenLogin(context, application); + } + catch (Exception ex) + { + SnLog.WriteException(ex); + context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; + return false; + } + finally + { + context.Response.Flush(); + application.CompleteRequest(); + } + + return true; + } + + internal IUser VerifyUser(string providerName, HttpRequestBase request) + { + var provider = GetProvider(providerName); + if (provider == null) + throw new InvalidOperationException("OAuth provider not found: " + providerName); + + string userId; + object tokenData; + + try + { + userId = provider.VerifyToken(request, out tokenData); + } + catch (Exception ex) + { + SnTrace.Security.Write($"Unsuccessful OAuth token verification. Provider: {providerName}. Error: {ex.Message}"); + return null; + } + + return string.IsNullOrEmpty(userId) ? null : LoadOrCreateUser(provider, tokenData, userId); + } + + /// + /// Derived classes may override this property and serve providers from a + /// different location - e.g. for testing purposes. + /// + internal Func GetProvider { get; set; } = providerName => Providers.Instance.GetProvider( + OAuthProvider.GetProviderRegistrationName(providerName)); + /// + /// Derived classes may override this property for testing purposes. + /// + internal Func LoadOrCreateUser { get; set; } = LoadOrCreateUserPrivate; + + //========================================================================================== Helper methods + + private static bool IsLoginRequest(HttpRequestBase request) + { + var uri = request?.Url?.AbsolutePath; + return string.Equals(uri, OAuthPathLogin, StringComparison.InvariantCultureIgnoreCase); + } + private static bool IsCallbackRequest(HttpRequestBase request) + { + var uri = request?.Url?.AbsolutePath; + return string.Equals(uri, OAuthPathCallback, StringComparison.InvariantCultureIgnoreCase); + } + + private static string GetProviderName(HttpRequestBase request) + { + return request?["provider"] ?? string.Empty; + } + + private static IUser LoadOrCreateUserPrivate(OAuthProvider provider, object tokenData, string userId) + { + User user; + + using (new SystemAccount()) + { + user = ContentQuery.Query(SafeQueries.UsersByOAuthId, QuerySettings.AdminSettings, provider.IdentifierFieldName, userId) + .Nodes.FirstOrDefault() as User ?? CreateUser(provider, tokenData, userId); + } + + return user; + } + private static User CreateUser(OAuthProvider provider, object tokenData, string userId) + { + var userData = provider.GetUserData(tokenData); + var parent = LoadOrCreateUserParent(provider.ProviderName); + + var userContentType = Settings.GetValue(SettingsName, UserTypeSettingName, null, "User"); + var userContent = Content.CreateNew(userContentType, parent, userData.Username); + + if (!userContent.Fields.ContainsKey(provider.IdentifierFieldName)) + { + var message = $"The {userContent.ContentType.Name} content type does not contain a field named {provider.IdentifierFieldName}. " + + $"Please register this field before using the {provider.ProviderName} OAuth provider."; + throw new InvalidOperationException(message); + } + + userContent["LoginName"] = userData.Username; + userContent[provider.IdentifierFieldName] = userId; + userContent["Enabled"] = true; + userContent["FullName"] = userData.FullName ?? userData.Username; + + if (!string.IsNullOrEmpty(userData.Email)) + userContent["Email"] = userData.Email; + + // If a user with the same name already exists, this will throw an exception + // so that the caller knows that the registration could not be completed. + userContent.Save(); + + return userContent.ContentHandler as User; + } + private static Node LoadOrCreateUserParent(string providerName) + { + // E.g. /Root/IMS/Public/facebook + var userDomain = Settings.GetValue(SettingsName, DomainSettingName, null, "Public"); + var domainPath = RepositoryPath.Combine(RepositoryStructure.ImsFolderPath, userDomain); + var dummy = Node.LoadNode(domainPath) ?? + RepositoryTools.CreateStructure(domainPath, "Domain")?.ContentHandler; + var orgUnitPath = RepositoryPath.Combine(domainPath, providerName); + var orgUnit = Node.LoadNode(orgUnitPath) ?? + RepositoryTools.CreateStructure(orgUnitPath, "OrganizationalUnit")?.ContentHandler; + + return orgUnit; + } + } +} diff --git a/src/Services/Virtualization/PortalAuthenticationModule.cs b/src/Services/Virtualization/PortalAuthenticationModule.cs index cd40a5178..174195c86 100644 --- a/src/Services/Virtualization/PortalAuthenticationModule.cs +++ b/src/Services/Virtualization/PortalAuthenticationModule.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Reflection; +using System.Security.Authentication; using System.Security.Principal; using System.Text; using System.Web; @@ -14,6 +15,7 @@ using SenseNet.Diagnostics; using Newtonsoft.Json; using SenseNet.Configuration; +using SenseNet.Services.Virtualization; using SenseNet.TokenAuthentication; namespace SenseNet.Portal.Virtualization @@ -31,55 +33,14 @@ public void Init(HttpApplication application) application.EndRequest += OnEndRequest; // Forms } - private const string AccessSignatureName = "as"; - private const string RefreshSignatureName = "rs"; - private const string AccessHeaderName = "X-Access-Data"; - private const string RefreshHeaderName = "X-Refresh-Data"; - private const string AuthenticationTypeHeaderName = "X-Authentication-Type"; - private const string TokenLoginPath = "/sn-token/login"; - private const string TokenRefreshPath = "/sn-token/refresh"; - private static readonly string[] AcceptedTokenPaths = { TokenLoginPath, TokenRefreshPath }; - private static class HttpResponseStatusCode - { - public static int Unauthorized = 401; - public static int Ok = 200; - } - private static ISecurityKey _securityKey; - private static readonly object _keyLock = new object(); - - private ISecurityKey SecurityKey - { - get - { - if (_securityKey == null) - { - lock (_keyLock) - { - if (_securityKey == null) - { - _securityKey = EncryptionHelper.CreateSymmetricKey(Configuration.TokenAuthentication.SymmetricKeySecret); - } - } - } - return _securityKey; - } - } - public Func GetContext = sender => new HttpContextWrapper(((HttpApplication) sender).Context); - public Func GetRequest = sender => new HttpRequestWrapper(((HttpApplication)sender).Context.Request); - public Func GetResponse = sender => new HttpResponseWrapper(((HttpApplication)sender).Context.Response); - public Func GetVisitorPrincipal = () => new PortalPrincipal(User.Visitor); - public Func LoadUserPrincipal = userName => new PortalPrincipal(User.Load(userName)); - public Func IsUserValid = (userName, password) => Membership.ValidateUser(userName, password); - public Func GetSystemAccount = () => new SystemAccount(); - public Func GetBasicAuthHeader = () => PortalContext.Current.BasicAuthHeaders; public bool DispatchBasicAuthentication(HttpContextBase context, out bool anonymAuthenticated) { anonymAuthenticated = false; - var authHeader = GetBasicAuthHeader(); + var authHeader = AuthenticationHelper.GetBasicAuthHeader(); if (authHeader == null || !authHeader.StartsWith("Basic ")) return false; @@ -89,7 +50,7 @@ public bool DispatchBasicAuthentication(HttpContextBase context, out bool anonym if (userPass.Length != 2) { - context.User = GetVisitorPrincipal(); + context.User = AuthenticationHelper.GetVisitorPrincipal(); anonymAuthenticated = true; return true; } @@ -99,15 +60,15 @@ public bool DispatchBasicAuthentication(HttpContextBase context, out bool anonym var password = userPass[1]; // Elevation: we need to load the user here, regardless of the current users permissions - using (GetSystemAccount()) + using (AuthenticationHelper.GetSystemAccount()) { - if (IsUserValid(username, password)) + if (AuthenticationHelper.IsUserValid(username, password)) { - context.User = LoadUserPrincipal(username); + context.User = AuthenticationHelper.LoadUserPrincipal(username); } else { - context.User = GetVisitorPrincipal(); + context.User = AuthenticationHelper.GetVisitorPrincipal(); anonymAuthenticated = true; } } @@ -115,43 +76,29 @@ public bool DispatchBasicAuthentication(HttpContextBase context, out bool anonym catch (Exception e) // logged { SnLog.WriteException(e); - context.User = GetVisitorPrincipal(); + context.User = AuthenticationHelper.GetVisitorPrincipal(); anonymAuthenticated = true; } return true; } - + public void OnAuthenticateRequest(object sender, EventArgs e) { var application = sender as HttpApplication; - var context = GetContext(sender); //HttpContext.Current; - var request = GetRequest(sender); - bool anonymAuthenticated; - - var basicAuthenticated = DispatchBasicAuthentication(context, out anonymAuthenticated); + var context = AuthenticationHelper.GetContext(sender); //HttpContext.Current; + var basicAuthenticated = DispatchBasicAuthentication(context, out var anonymAuthenticated); - if (IsTokenAuthenticationRequested(request)) - { + var tokenAuthentication = new TokenAuthentication(); + var tokenAuthenticated = tokenAuthentication.Authenticate(application, basicAuthenticated, anonymAuthenticated); - if (basicAuthenticated && anonymAuthenticated) - { - SnLog.WriteException(new UnauthorizedAccessException("Invalid user.")); - context.Response.StatusCode = HttpResponseStatusCode.Unauthorized; - context.Response.Flush(); - if (application?.Context != null) - { - application.CompleteRequest(); - } - } - else - { - TokenAuthenticate(basicAuthenticated, context, application); - } - return; + if (!tokenAuthenticated) + { + tokenAuthenticated = OAuthManager.Instance.Authenticate(application, tokenAuthentication); } - // if it is a simple basic authentication case - if (basicAuthenticated) + + // if it is a simple basic authentication case or authenticated with a token + if (basicAuthenticated || tokenAuthenticated) { return; } @@ -210,184 +157,6 @@ public void OnAuthenticateRequest(object sender, EventArgs e) } } - private string GetAuthenticationTypeHeader(HttpRequestBase request) - { - return request.Headers[AuthenticationTypeHeaderName] ?? request.Headers[AuthenticationTypeHeaderName.ToLower()]; - } - - private string GetRefreshHeader(HttpRequestBase request) - { - return request.Headers[RefreshHeaderName] ?? request.Headers[RefreshHeaderName.ToLower()]; - } - private string GetAccessHeader(HttpRequestBase request) - { - return request.Headers[AccessHeaderName] ?? request.Headers[AccessHeaderName.ToLower()]; - } - - private bool IsTokenAuthenticationRequested(HttpRequestBase request) - { - return request.IsSecureConnection && (GetAuthenticationTypeHeader(request) == "Token" - || AcceptedTokenPaths.Contains(request.Url.AbsolutePath, StringComparer.InvariantCultureIgnoreCase) - || !string.IsNullOrWhiteSpace(GetAccessHeader(request))); - } - - private void TokenAuthenticate(bool basicAuthenticated, HttpContextBase context, HttpApplication application) - { - try - { - var tokenHandler = new JwsSecurityTokenHandler(); - var validFrom = DateTime.UtcNow; - - ITokenParameters generateTokenParameter = new TokenParameters - { - Audience = Configuration.TokenAuthentication.Audience, - Issuer = Configuration.TokenAuthentication.Issuer, - Subject = Configuration.TokenAuthentication.Subject, - EncryptionAlgorithm = Configuration.TokenAuthentication.EncriptionAlgorithm, - AccessLifeTimeInMinutes = Configuration.TokenAuthentication.AccessLifeTimeInMinutes, - RefreshLifeTimeInMinutes = Configuration.TokenAuthentication.RefreshLifeTimeInMinutes, - ClockSkewInMinutes = Configuration.TokenAuthentication.ClockSkewInMinutes, - ValidFrom = validFrom, - ValidateLifeTime = true - }; - - var tokenManager = new TokenManager(SecurityKey, tokenHandler, generateTokenParameter); - - if (basicAuthenticated) - { - // user has just authenticated by basic auth, so let's emit a set of tokens and cookies in response - try - { - var userName = context.User.Identity.Name; - var roleName = string.Empty; - - // emit both access and refresh token and cookie - EmitTokensAndCookies(context, tokenManager, validFrom, userName, roleName, true); - context.Response.StatusCode = HttpResponseStatusCode.Ok; - } - catch (Exception ex) - { - SnLog.WriteException(ex); - context.Response.StatusCode = HttpResponseStatusCode.Unauthorized; - } - finally - { - context.Response.Flush(); - if (application.Context != null) - { - application.CompleteRequest(); - } - } - return; - } - - // user has not been authenticated yet, so there must be a valid token and cookie in the request - var header = GetRefreshHeader(context.Request); - if (!string.IsNullOrWhiteSpace(header)) - { - // we got a refresh token - try - { - var authCookie = CookieHelper.GetCookie(context.Request, RefreshSignatureName); - if (authCookie == null) - { - throw new UnauthorizedAccessException("Missing refresh cookie."); - } - - var refreshHeadAndPayload = header; - var refreshSignature = authCookie.Value; - var principal = tokenManager.ValidateToken(refreshHeadAndPayload + "." + refreshSignature); - var userName = principal.Identity.Name; - var roleName = string.Empty; - - // emit access token and cookie only - EmitTokensAndCookies(context, tokenManager, validFrom, userName, roleName, false); - context.Response.StatusCode = HttpResponseStatusCode.Ok; - } - catch (Exception ex) - { - SnLog.WriteException(ex); - context.Response.StatusCode = HttpResponseStatusCode.Unauthorized; - } - finally - { - context.Response.Flush(); - if (application.Context != null) - { - application.CompleteRequest(); - } - } - return; - } - - header = GetAccessHeader(context.Request); - if (!string.IsNullOrWhiteSpace(header)) - { - // we got an access token - var authCookie = CookieHelper.GetCookie(context.Request, AccessSignatureName); - if (authCookie == null) - { - throw new UnauthorizedAccessException("Missing access cookie."); - } - - var accessHeadAndPayload = header; - var accessSignature = authCookie.Value; - var principal = tokenManager.ValidateToken(accessHeadAndPayload + "." + accessSignature); - if (principal == null) - { - throw new UnauthorizedAccessException("Invalid access token."); - } - var userName = tokenManager.GetPayLoadValue(accessHeadAndPayload.Split(Convert.ToChar("."))[1], "name"); - using (new SystemAccount()) - { - context.User = LoadUserPrincipal(userName); - } - } - } - catch (Exception ex) - { - SnLog.WriteException(ex); - if (!basicAuthenticated) - { - context.User = GetVisitorPrincipal(); - } - } - } - - private void EmitTokensAndCookies(HttpContextBase context, TokenManager tokenManager, DateTime validFrom, string userName, string roleName, bool refreshTokenAsWell) - { - string refreshToken; - var token = tokenManager.GenerateToken(userName, roleName, out refreshToken, refreshTokenAsWell); - var tokenResponse = new TokenResponse(); - var accessSignatureIndex = token.LastIndexOf('.'); - var accessSignature = token.Substring(accessSignatureIndex + 1); - var accessHeadAndPayload = token.Substring(0, accessSignatureIndex); - var accessExpiration = validFrom.AddMinutes(Configuration.TokenAuthentication.AccessLifeTimeInMinutes); - - CookieHelper.InsertSecureCookie(context.Response, accessSignature, AccessSignatureName, accessExpiration); - - tokenResponse.access = accessHeadAndPayload; - - if (refreshTokenAsWell) - { - var refreshSignatureIndex = refreshToken.LastIndexOf('.'); - var refreshSignature = refreshToken.Substring(refreshSignatureIndex + 1); - var refreshHeadAndPayload = refreshToken.Substring(0, refreshSignatureIndex); - var refreshExpiration = accessExpiration.AddMinutes(Configuration.TokenAuthentication.RefreshLifeTimeInMinutes); - - CookieHelper.InsertSecureCookie(context.Response, refreshSignature, RefreshSignatureName, refreshExpiration); - - tokenResponse.refresh = refreshHeadAndPayload; - } - - context.Response.Write(JsonConvert.SerializeObject(tokenResponse, new JsonSerializerSettings {DefaultValueHandling = DefaultValueHandling.Ignore})); - } - - private class TokenResponse - { - public string access; - public string refresh; - } private static void CallInternalOnEnter(object sender, EventArgs e) { diff --git a/src/Services/Virtualization/TokenAuthentication.cs b/src/Services/Virtualization/TokenAuthentication.cs new file mode 100644 index 000000000..e6e2c0e22 --- /dev/null +++ b/src/Services/Virtualization/TokenAuthentication.cs @@ -0,0 +1,359 @@ +using System; +using System.Security.Authentication; +using System.Web; +using Newtonsoft.Json; +using SenseNet.ContentRepository.Storage.Security; +using SenseNet.Diagnostics; +using SenseNet.TokenAuthentication; +using System.Linq; + +namespace SenseNet.Portal.Virtualization +{ + public class TokenAuthentication + { + private const string AccessSignatureCookieName = "as"; + private const string RefreshSignatureCookieName = "rs"; + private const string AccessHeadAndPayloadCookieName = "ahp"; + private const string AccessHeaderName = "X-Access-Data"; + private const string RefreshHeaderName = "X-Refresh-Data"; + private const string AuthenticationActionHeaderName = "X-Authentication-Action"; + private const string LoginActionName = "TokenLogin"; + private const string LogoutActionName = "TokenLogout"; + private const string AccessActionName = "TokenAccess"; + private const string RefreshActionName = "TokenRefresh"; + private const string TokenLoginPath = "/sn-token/login"; + private const string TokenLogoutPath = "/sn-token/logout"; + private const string TokenRefreshPath = "/sn-token/refresh"; + private static readonly string[] TokenPaths = {TokenLoginPath, TokenLogoutPath, TokenRefreshPath }; + private static readonly string[] TokenActions = {LoginActionName, LogoutActionName, AccessActionName, RefreshActionName }; + private static class HttpResponseStatusCode + { + public static int Unauthorized = 401; + public static int Ok = 200; + } + + private enum TokenAction { TokenLogin, TokenLogout, TokenAccess, TokenRefresh } + private static ISecurityKey _securityKey; + private static readonly object _keyLock = new object(); + + private ISecurityKey SecurityKey + { + get + { + if (_securityKey == null) + { + lock (_keyLock) + { + if (_securityKey == null) + { + _securityKey = + EncryptionHelper.CreateSymmetricKey(Configuration.TokenAuthentication.SymmetricKeySecret); + } + } + } + return _securityKey; + } + } + + + public bool Authenticate(HttpApplication application, bool basicAuthenticated, bool anonymAuthenticated) + { + var context = AuthenticationHelper.GetContext(application); //HttpContext.Current; + var request = AuthenticationHelper.GetRequest(application); + bool headerMark, uriMark; + string actionHeader, uri, accessHeadAndPayload; + + if (IsTokenAuthenticationRequested(request, out headerMark, out uriMark, out actionHeader, out uri, out accessHeadAndPayload)) + { + + if (basicAuthenticated && anonymAuthenticated) + { + SnLog.WriteException(new UnauthorizedAccessException("Invalid user.")); + context.Response.StatusCode = HttpResponseStatusCode.Unauthorized; + context.Response.Flush(); + if (application?.Context != null) + { + application.CompleteRequest(); + } + } + else + { + TokenAuthenticate(basicAuthenticated, headerMark, uriMark, actionHeader, uri, accessHeadAndPayload, context, application); + } + return true; + } + return false; + } + + private void TokenAuthenticate(bool basicAuthenticated, bool headerMark, bool uriMark, string actionHeader, string uri, string headAndPayLoad, HttpContextBase context, HttpApplication application) + { + bool endRequest = false; + try + { + TokenAction tokenAction; + string tokenHeadAndPayload = headAndPayLoad; + if (headerMark) + { + if (!Enum.TryParse(actionHeader, true, out tokenAction)) + { + throw new AuthenticationException("Invalid action header for header mark token authentication."); + } + if (tokenAction == TokenAction.TokenAccess || tokenAction == TokenAction.TokenLogout) + { + tokenHeadAndPayload = GetAccessHeader(context.Request); + } + else if (tokenAction == TokenAction.TokenRefresh) + { + tokenHeadAndPayload = GetRefreshHeader(context.Request); + } + } + else if (uriMark) + { + switch (uri) + { + case TokenLoginPath: + tokenAction = TokenAction.TokenLogin; + break; + case TokenLogoutPath: + tokenAction = TokenAction.TokenLogout; + break; + case TokenRefreshPath: + tokenAction = TokenAction.TokenRefresh; + break; + default: + throw new AuthenticationException("Invalid login uri for token authentication."); + } + } + else if (!string.IsNullOrWhiteSpace(headAndPayLoad)) + { + tokenAction = TokenAction.TokenAccess; + } + else + { + throw new AuthenticationException("Invalid method for token authentication."); + } + + var validFrom = DateTime.UtcNow; + var tokenManager = GetTokenManager(validFrom); + + switch (tokenAction) + { + case TokenAction.TokenLogin: + endRequest = true; + TokenLogin(basicAuthenticated, validFrom, tokenManager, context); + break; + case TokenAction.TokenLogout: + endRequest = true; + TokenLogout(tokenHeadAndPayload, tokenManager, context); + break; + case TokenAction.TokenAccess: + TokenAccess(tokenHeadAndPayload, tokenManager, context); + break; + case TokenAction.TokenRefresh: + endRequest = true; + TokenRefresh(tokenHeadAndPayload, validFrom, tokenManager, context); + break; + } + } + catch (Exception ex) + { + SnLog.WriteException(ex); + if (endRequest) + { + context.Response.StatusCode = HttpResponseStatusCode.Unauthorized; + } + else + { + context.User = AuthenticationHelper.GetVisitorPrincipal(); + } + } + finally + { + if (endRequest) + { + context.Response.Flush(); + if (application.Context != null) + { + application.CompleteRequest(); + } + } + } + } + + + /// + /// Assembles the necessary artifacts (cookies and response tokens). This should be called only + /// if the user is already authenticated. + /// + internal void TokenLogin(HttpContextBase context, HttpApplication application) + { + var validFrom = DateTime.UtcNow; + var tokenManager = GetTokenManager(validFrom); + + TokenLogin(true, validFrom, tokenManager, context); + } + + private TokenManager GetTokenManager(DateTime validFrom) + { + var tokenHandler = new JwsSecurityTokenHandler(); + + ITokenParameters generateTokenParameter = new TokenParameters + { + Audience = Configuration.TokenAuthentication.Audience, + Issuer = Configuration.TokenAuthentication.Issuer, + Subject = Configuration.TokenAuthentication.Subject, + EncryptionAlgorithm = Configuration.TokenAuthentication.EncriptionAlgorithm, + AccessLifeTimeInMinutes = Configuration.TokenAuthentication.AccessLifeTimeInMinutes, + RefreshLifeTimeInMinutes = Configuration.TokenAuthentication.RefreshLifeTimeInMinutes, + ClockSkewInMinutes = Configuration.TokenAuthentication.ClockSkewInMinutes, + ValidFrom = validFrom, + ValidateLifeTime = true + }; + + return new TokenManager(SecurityKey, tokenHandler, generateTokenParameter); + } + + private void TokenLogin(bool basicAuthenticated, DateTime validFrom, TokenManager tokenManager, HttpContextBase context) + { + if (!basicAuthenticated) + { + throw new AuthenticationException("Missing basic authentication."); + } + // user has just authenticated by basic auth, so let's emit a set of tokens and cookies in response + var userName = context.User.Identity.Name; + var roleName = String.Empty; + + // emit both access and refresh token and cookie + EmitTokensAndCookies(context, tokenManager, validFrom, userName, roleName, true); + context.Response.StatusCode = HttpResponseStatusCode.Ok; + } + + private void TokenLogout(string accessHeadAndPayload, TokenManager tokenManager, HttpContextBase context) + { + if (!String.IsNullOrWhiteSpace(accessHeadAndPayload)) + { + var authCookie = CookieHelper.GetCookie(context.Request, AccessSignatureCookieName); + if (authCookie == null) + { + throw new UnauthorizedAccessException("Missing access cookie."); + } + + var accessSignature = authCookie.Value; + var principal = tokenManager.ValidateToken(accessHeadAndPayload + "." + accessSignature, false); + if (principal == null) + { + throw new UnauthorizedAccessException("Invalid access token."); + } + CookieHelper.DeleteCookie(context.Response, AccessSignatureCookieName); + CookieHelper.DeleteCookie(context.Response, AccessHeadAndPayloadCookieName); + CookieHelper.DeleteCookie(context.Response, RefreshSignatureCookieName); + context.Response.StatusCode = HttpResponseStatusCode.Ok; + } + } + + private void TokenAccess(string accessHeadAndPayload, TokenManager tokenManager, HttpContextBase context) + { + if (!string.IsNullOrWhiteSpace(accessHeadAndPayload)) + { + var authCookie = CookieHelper.GetCookie(context.Request, AccessSignatureCookieName); + if (authCookie == null) + { + throw new UnauthorizedAccessException("Missing access cookie."); + } + + var accessSignature = authCookie.Value; + var principal = tokenManager.ValidateToken(accessHeadAndPayload + "." + accessSignature); + if (principal == null) + { + throw new UnauthorizedAccessException("Invalid access token."); + } + var userName = tokenManager.GetPayLoadValue(accessHeadAndPayload.Split(Convert.ToChar("."))[1], "name"); + using (AuthenticationHelper.GetSystemAccount()) + { + context.User = AuthenticationHelper.LoadUserPrincipal(userName); + } + } + } + + private void TokenRefresh(string refreshHeadAndPayload, DateTime validFrom, TokenManager tokenManager, HttpContextBase context) + { + var authCookie = CookieHelper.GetCookie(context.Request, RefreshSignatureCookieName); + if (authCookie == null) + { + throw new UnauthorizedAccessException("Missing refresh cookie."); + } + + var refreshSignature = authCookie.Value; + var principal = tokenManager.ValidateToken(refreshHeadAndPayload + "." + refreshSignature); + var userName = principal.Identity.Name; + var roleName = String.Empty; + + // emit access token and cookie only + EmitTokensAndCookies(context, tokenManager, validFrom, userName, roleName, false); + context.Response.StatusCode = HttpResponseStatusCode.Ok; + } + + private void EmitTokensAndCookies(HttpContextBase context, TokenManager tokenManager, DateTime validFrom, string userName, string roleName, bool refreshTokenAsWell) + { + string refreshToken; + var token = tokenManager.GenerateToken(userName, roleName, out refreshToken, refreshTokenAsWell); + var tokenResponse = new TokenResponse(); + var accessSignatureIndex = token.LastIndexOf('.'); + var accessSignature = token.Substring(accessSignatureIndex + 1); + var accessHeadAndPayload = token.Substring(0, accessSignatureIndex); + var accessExpiration = validFrom.AddMinutes(Configuration.TokenAuthentication.AccessLifeTimeInMinutes); + + CookieHelper.InsertSecureCookie(context.Response, accessSignature, AccessSignatureCookieName, accessExpiration); + CookieHelper.InsertSecureCookie(context.Response, accessHeadAndPayload, AccessHeadAndPayloadCookieName, accessExpiration); + + tokenResponse.access = accessHeadAndPayload; + + if (refreshTokenAsWell) + { + var refreshSignatureIndex = refreshToken.LastIndexOf('.'); + var refreshSignature = refreshToken.Substring(refreshSignatureIndex + 1); + var refreshHeadAndPayload = refreshToken.Substring(0, refreshSignatureIndex); + var refreshExpiration = accessExpiration.AddMinutes(Configuration.TokenAuthentication.RefreshLifeTimeInMinutes); + + CookieHelper.InsertSecureCookie(context.Response, refreshSignature, RefreshSignatureCookieName, refreshExpiration); + + tokenResponse.refresh = refreshHeadAndPayload; + } + + context.Response.Write(JsonConvert.SerializeObject(tokenResponse, new JsonSerializerSettings { DefaultValueHandling = DefaultValueHandling.Ignore })); + } + + private bool IsTokenAuthenticationRequested(HttpRequestBase request, out bool headerMark, out bool uriMark, out string actionHeader, out string uri, out string headAndPayload) + { + actionHeader = GetAuthenticationActionHeader(request); + uri = request.Url.AbsolutePath; + headerMark = TokenActions.Contains(actionHeader, StringComparer.InvariantCultureIgnoreCase); + uriMark = TokenPaths.Contains(uri, StringComparer.InvariantCultureIgnoreCase); + var cookie = CookieHelper.GetCookie(request, AccessHeadAndPayloadCookieName); + headAndPayload = cookie == null ? request.Headers[RefreshHeaderName] : cookie.Value; + return request.IsSecureConnection && (headerMark || uriMark || headAndPayload != null); + } + + private string GetAuthenticationActionHeader(HttpRequestBase request) + { + return request.Headers[AuthenticationActionHeaderName] ?? + request.Headers[AuthenticationActionHeaderName.ToLower()]; + } + + private string GetRefreshHeader(HttpRequestBase request) + { + return request.Headers[RefreshHeaderName] ?? request.Headers[RefreshHeaderName.ToLower()]; + } + + private string GetAccessHeader(HttpRequestBase request) + { + return request.Headers[AccessHeaderName] ?? request.Headers[AccessHeaderName.ToLower()]; + } + + private class TokenResponse + { + public string access; + public string refresh; + } + } +} \ No newline at end of file diff --git a/src/Storage/Configuration/Identifiers.cs b/src/Storage/Configuration/Identifiers.cs index 0425f859e..2f6f6642d 100644 --- a/src/Storage/Configuration/Identifiers.cs +++ b/src/Storage/Configuration/Identifiers.cs @@ -3,7 +3,7 @@ public class Identifiers { public const int AdministratorUserId = 1; - public const int StartupUserId = -2; + public const int StartupUserId = 12; public const int SystemUserId = -1; public const int PortalRootId = 2; public const int PortalOrgUnitId = 5; diff --git a/src/Storage/Configuration/Providers.cs b/src/Storage/Configuration/Providers.cs index 6533b4b06..75ab44e1d 100644 --- a/src/Storage/Configuration/Providers.cs +++ b/src/Storage/Configuration/Providers.cs @@ -1,6 +1,16 @@ -using SenseNet.Communication.Messaging; +using System; +using System.Collections.Generic; +using SenseNet.Communication.Messaging; +using SenseNet.ContentRepository; +using SenseNet.ContentRepository.Storage; +using SenseNet.ContentRepository.Storage.Data; using SenseNet.ContentRepository.Storage.Data.SqlClient; +using SenseNet.ContentRepository.Storage.Security; +using SenseNet.Diagnostics; +using SenseNet.Security; +using SenseNet.Security.EF6SecurityStore; using SenseNet.Security.Messaging; +using SenseNet.Tools; // ReSharper disable once CheckNamespace // ReSharper disable RedundantTypeArgumentsOfMethod @@ -21,6 +31,8 @@ public class Providers : SnConfig typeof(ContentRepository.Storage.Security.Sha256PasswordHashProviderWithoutSalt).FullName); public static string SkinManagerClassName { get; internal set; } = GetProvider("SkinManager", "SenseNet.Portal.SkinManager"); public static string DirectoryProviderClassName { get; internal set; } = GetProvider("DirectoryProvider"); + public static string SecurityDataProviderClassName { get; internal set; } = GetProvider("SecurityDataProvider", + typeof(EF6SecurityDataProvider).FullName); public static string SecurityMessageProviderClassName { get; internal set; } = GetProvider("SecurityMessageProvider", typeof(DefaultMessageProvider).FullName); public static string DocumentPreviewProviderClassName { get; internal set; } = GetProvider("DocumentPreviewProvider", @@ -28,11 +40,178 @@ public class Providers : SnConfig public static string ClusterChannelProviderClassName { get; internal set; } = GetProvider("ClusterChannelProvider", typeof(VoidChannel).FullName); + public static string ElevatedModificationVisibilityRuleProviderName { get; internal set; } = + GetProvider("ElevatedModificationVisibilityRuleProvider", + "SenseNet.ContentRepository.SnElevatedModificationVisibilityRule"); + public static bool RepositoryPathProviderEnabled { get; internal set; } = GetValue(SectionName, "RepositoryPathProviderEnabled", true); private static string GetProvider(string key, string defaultValue = null) { return GetString(SectionName, key, defaultValue); } + + //===================================================================================== Instance + + /// + /// Lets you access the replaceable providers in the system. This instance may be replaced + /// by a derived special implementation that stores instances on a thread context + /// for testing purposes. + /// + public static Providers Instance { get; set; } = new Providers(); + + //===================================================================================== Named providers + + #region private Lazy _dataProvider = new Lazy + private Lazy _dataProvider = new Lazy(() => + { + DataProvider dbp; + + try + { + dbp = (DataProvider)TypeResolver.CreateInstance(DataProviderClassName); + } + catch (TypeNotFoundException) + { + throw new ConfigurationException($"{SR.Exceptions.Configuration.Msg_DataProviderImplementationDoesNotExist}: {DataProviderClassName}"); + } + catch (InvalidCastException) + { + throw new ConfigurationException(SR.Exceptions.Configuration.Msg_InvalidDataProviderImplementation); + } + + CommonComponents.TransactionFactory = dbp; + SnLog.WriteInformation("DataProvider created: " + DataProviderClassName); + + return dbp; + }); + public virtual DataProvider DataProvider + { + get { return _dataProvider.Value; } + set { _dataProvider = new Lazy(() => value); } + } + #endregion + + #region private Lazy _accessProvider = new Lazy + private Lazy _accessProvider = new Lazy(() => + { + try + { + var provider = (AccessProvider)TypeResolver.CreateInstance(AccessProviderClassName); + provider.InitializeInternal(); + + SnLog.WriteInformation("AccessProvider created: " + AccessProviderClassName); + + return provider; + } + catch (TypeNotFoundException) // rethrow + { + throw new ConfigurationException($"{SR.Exceptions.Configuration.Msg_AccessProviderImplementationDoesNotExist}: {AccessProviderClassName}"); + } + catch (InvalidCastException) // rethrow + { + throw new ConfigurationException($"{SR.Exceptions.Configuration.Msg_InvalidAccessProviderImplementation}: {AccessProviderClassName}"); + } + }); + public virtual AccessProvider AccessProvider + { + get { return _accessProvider.Value; } + set { _accessProvider = new Lazy(() => value); } + } + #endregion + + #region private Lazy _securityDataProvider = new Lazy + private Lazy _securityDataProvider = new Lazy(() => + { + ISecurityDataProvider securityDataProvider = null; + + try + { + // if other than the known implementation, create it automatically + if (string.Compare(SecurityDataProviderClassName, typeof(EF6SecurityDataProvider).FullName, StringComparison.Ordinal) != 0) + securityDataProvider = (ISecurityDataProvider)TypeResolver.CreateInstance(SecurityDataProviderClassName); + } + catch (TypeNotFoundException) + { + throw new ConfigurationException($"Security data provider implementation not found: {SecurityDataProviderClassName}"); + } + catch (InvalidCastException) + { + throw new ConfigurationException($"Invalid security data provider implementation: {SecurityDataProviderClassName}"); + } + + if (securityDataProvider == null) + { + // default implementation + securityDataProvider = new EF6SecurityDataProvider( + Security.SecurityDatabaseCommandTimeoutInSeconds, + ConnectionStrings.SecurityDatabaseConnectionString); + } + + SnLog.WriteInformation("SecurityDataProvider created: " + securityDataProvider.GetType().FullName); + + return securityDataProvider; + }); + public virtual ISecurityDataProvider SecurityDataProvider + { + get { return _securityDataProvider.Value; } + set { _securityDataProvider = new Lazy(() => value); } + } + #endregion + + #region private Lazy _elevatedModificationVisibilityRuleProvider + private Lazy _elevatedModificationVisibilityRuleProvider = + new Lazy(() => + { + try + { + return (ElevatedModificationVisibilityRule)TypeResolver.CreateInstance(ElevatedModificationVisibilityRuleProviderName); + } + catch (TypeNotFoundException) + { + throw new ConfigurationException($"Elevated modification visibility rule provider implementation not found: {ElevatedModificationVisibilityRuleProviderName}"); + } + catch (InvalidCastException) + { + throw new ConfigurationException($"Invalid Elevated modification visibility rule provider implementation: {ElevatedModificationVisibilityRuleProviderName}"); + } + }); + public virtual ElevatedModificationVisibilityRule ElevatedModificationVisibilityRuleProvider + { + get { return _elevatedModificationVisibilityRuleProvider.Value; } + set { _elevatedModificationVisibilityRuleProvider = new Lazy(() => value); } + } + #endregion + + //===================================================================================== General provider API + + private readonly Dictionary _providersByName = new Dictionary(); + private readonly Dictionary _providersByType = new Dictionary(); + + public virtual T GetProvider(string name) where T: class + { + object provider; + if (_providersByName.TryGetValue(name, out provider)) + return provider as T; + + return null; + } + public virtual T GetProvider() where T : class + { + object provider; + if (_providersByType.TryGetValue(typeof(T), out provider)) + return provider as T; + + return null; + } + + public virtual void SetProvider(string providerName, object provider) + { + _providersByName[providerName] = provider; + } + public virtual void SetProvider(Type providerType, object provider) + { + _providersByType[providerType] = provider; + } } } diff --git a/src/Storage/Data/DataProvider.cs b/src/Storage/Data/DataProvider.cs index df8489d18..789012c42 100644 --- a/src/Storage/Data/DataProvider.cs +++ b/src/Storage/Data/DataProvider.cs @@ -4,12 +4,10 @@ using System.IO; using System.Linq; using System.Data.Common; -using System.Diagnostics; using SenseNet.Configuration; using SenseNet.ContentRepository.Storage.Search.Internal; using SenseNet.ContentRepository.Storage.Schema; using SenseNet.Diagnostics; -using SenseNet.Tools; namespace SenseNet.ContentRepository.Storage.Data { @@ -18,40 +16,7 @@ public abstract class DataProvider : ITransactionFactory, IPackageStorageProvide //////////////////////////////////////// Static Access //////////////////////////////////////// - private static DataProvider _current; - private static readonly object _lock = new object(); - - public static DataProvider Current - { - [DebuggerStepThrough] - get - { - if (_current == null) - { - lock (_lock) - { - if (_current == null) - { - try - { - _current = (DataProvider)TypeResolver.CreateInstance(Providers.DataProviderClassName); - } - catch (TypeNotFoundException) // rethrow - { - throw new ConfigurationException(String.Concat(SR.Exceptions.Configuration.Msg_DataProviderImplementationDoesNotExist, ": ", Providers.DataProviderClassName)); - } - catch (InvalidCastException) // rethrow - { - throw new ConfigurationException(String.Concat(SR.Exceptions.Configuration.Msg_InvalidDataProviderImplementation, ": ", Providers.DataProviderClassName)); - } - CommonComponents.TransactionFactory = _current; - SnLog.WriteInformation("DataProvider created: " + _current); - } - } - } - return _current; - } - } + public static DataProvider Current => Providers.Instance.DataProvider; //////////////////////////////////////// For tests //////////////////////////////////////// diff --git a/src/Storage/Data/SqlClient/Scripts/Install_01_Schema.sql b/src/Storage/Data/SqlClient/Scripts/Install_01_Schema.sql index d7b0155b816fc4eeefe9a7962cf9049e4df085fc..c5ecb51e9f2c7b9db06582ecbef0d8da37d71b82 100644 GIT binary patch delta 37 tcmaFX$g^u3_l9LP%?j=93hj*B7227$m`qk^*O;ELhiTIG1J{|97KN(b4KQ#?J;bj;sld zB*y(@&ZE9&G1*4U;$#Lol%i%)60#XCOfWI=vb!-xFPyht7|;9CvQO5PkEQ9?zCStd zd7kr}=l#B&v;3AQnYnsuM+d}Ugg+5@6Ar^5Kc9vWoPdKo*9XUWZWu<%wzYPL7wW+S zKK^*14!r80lYjm2EI;?~vjO(d->x7HhkFdh4f?lJ=fQo(c8ld&EF-VkMCrGi?70$e zfwvgWAT8M475UQjK|R@?3d=X|cXIUe@u&R?x)|&d)a(> z)y&ODf7RG&=Dj20rxflq7FGmHwQaWhTm4SbPR>snZCxyr7<1af!iX@dM@8@qR?v&{ z&ce)+NM6I%?0_zO<`G3_ahK>khbFmAq$klNemH~Ai1alq-@K25#o!ow3%4lrQRQtP zjo&hvrRSWG{zKnSIW#{7&Q^$?NqoLA#HWt3D6f2k&DP8Sx;TnT#X+~`9}}0)qthVX zn_IQk&1>+cZcbxR`}h(T8TFNQnz&vyu~t>KT8-*lqRki;!yAFuVW((5jV{Uh8qUc1 zO>w~x99QIC{2a?^lK1>Oj*o~R$FY&=E4B_nlzDoCzg#V~@xugG(cbwd6gfFfB6GY* z*tHkL{?oW+_WIQ(jqd)^zOL`EkL4Ak4;pFu<$Gd$E*E_LoHQV&0IX09X6Oao8U=5Z zNmeWY#u!!d84<||bYwMFWsw)v5k}s}HoP!p-_!vCcvVWMa4*|i6i(`97ZzOwZ4i|+ zqV!WNp+BZdDZN-k!;2zB=`FHqb6zYaBO!|E1Lm)V1E0WUYJ zUj)D7w~DNHw&=>mu(R(&_gyiHki!b6UcBw GetNodeHeads(IEnumerable idArray) if (unloadHeads.Count > 0) { - var needsSorting = nodeHeads.Count > 0; - foreach (var head in DataProvider.Current.LoadNodeHeads(unloadHeads)) { if (head != null) @@ -103,17 +101,12 @@ internal static IEnumerable GetNodeHeads(IEnumerable idArray) nodeHeads.Add(head); } - // we need to sort the final list only if we have - // node heads from the cache AND the database too - if (needsSorting) - { - // sort the node heads aligned with the original list - nodeHeads = (from id in idArray - join head in nodeHeads.Where(h => h != null) - on id equals head.Id - where head != null - select head).ToList(); - } + // sort the node heads aligned with the original list + nodeHeads = (from id in idArray + join head in nodeHeads.Where(h => h != null) + on id equals head.Id + where head != null + select head).ToList(); } return nodeHeads; } diff --git a/src/Storage/DistributedApplication.cs b/src/Storage/DistributedApplication.cs index a4750bbf8..a959b3f14 100644 --- a/src/Storage/DistributedApplication.cs +++ b/src/Storage/DistributedApplication.cs @@ -1,9 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; -using System.Configuration; -using System.Web; -using System.Diagnostics; using SenseNet.ContentRepository.Storage.Caching; using SenseNet.Communication.Messaging; using SenseNet.Configuration; @@ -76,9 +71,9 @@ private static IClusterChannel GetFallbackChannel() private static Type GetChannelProviderType() { - var channelAdapterType = TypeResolver.GetType(Providers.ClusterChannelProviderClassName); + var channelAdapterType = TypeResolver.GetType(Providers.ClusterChannelProviderClassName, false); if (channelAdapterType == null) - throw new ArgumentException("ClusterChannelProvider is not correctly configured."); + throw new ArgumentException("ClusterChannelProvider is not configured correctly."); return channelAdapterType; } diff --git a/src/Storage/ElevatedModificationVisibilityRule.cs b/src/Storage/ElevatedModificationVisibilityRule.cs index 263c32609..9a48e8d0e 100644 --- a/src/Storage/ElevatedModificationVisibilityRule.cs +++ b/src/Storage/ElevatedModificationVisibilityRule.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using SenseNet.Configuration; namespace SenseNet.ContentRepository.Storage { @@ -12,17 +8,11 @@ namespace SenseNet.ContentRepository.Storage /// public class ElevatedModificationVisibilityRule { - private static ElevatedModificationVisibilityRule instance; - static ElevatedModificationVisibilityRule() - { - instance = TypeHandler.GetProviderInstance(); - } + private static ElevatedModificationVisibilityRule Instance => Providers.Instance.ElevatedModificationVisibilityRuleProvider; internal static bool EvaluateRule(Node node) { - if (instance == null) - return false; - return instance.IsModificationVisible(node); + return Instance?.IsModificationVisible(node) ?? false; } /// diff --git a/src/Storage/Events/NodeObserver.cs b/src/Storage/Events/NodeObserver.cs index 577b4e3bc..7761e4c57 100644 --- a/src/Storage/Events/NodeObserver.cs +++ b/src/Storage/Events/NodeObserver.cs @@ -7,8 +7,9 @@ namespace SenseNet.ContentRepository.Storage.Events { public static class NodeObserverNames { - public static readonly string NOTIFICATION = "SenseNet.Messaging.NotificationObserver"; + public static readonly string NOTIFICATION = "SenseNet.Notification.NotificationObserver"; public static readonly string WORKFLOWNOTIFICATION = "SenseNet.Workflow.WorkflowNotificationObserver"; + public static readonly string DOCUMENTPREVIEW = "SenseNet.Preview.DocumentPreviewObserver"; } public abstract class NodeObserver diff --git a/src/Storage/Node.cs b/src/Storage/Node.cs index 45adb36d1..8c87218d6 100644 --- a/src/Storage/Node.cs +++ b/src/Storage/Node.cs @@ -2456,7 +2456,7 @@ private void ChecksBeforeSave() var currentUser = AccessProvider.Current.GetOriginalUser(); var currentUserNode = currentUser as Node; - if (currentUserNode == null) + if (currentUserNode == null && !(currentUser is StartupUser)) throw new InvalidOperationException("Cannot save the content because the current user account representation is not a Node."); var thisList = this as IContentList; @@ -2677,7 +2677,6 @@ private void MoveTo(Node target, long sourceTimestamp, long targetTimestamp) } } - public static void MoveMore(List nodeList, string targetPath, ref List errors) { MoveMoreInternal2(new NodeList(nodeList), Node.LoadNode(targetPath), ref errors); @@ -2839,48 +2838,65 @@ private static void CopyMoreInternal(IEnumerable nodeList, string targetNod } /// - /// Copies the Node instance to another loacation. The new location is a Node instance which will be parent node. + /// Copies the Node instance to another location. The new location is a Node instance which will be parent node. /// public virtual void CopyTo(Node target) { CopyTo(target, this.Name); } /// - /// Copies the Node instance to another loacation. The new location is a Node instance which will be parent node. + /// Copies the Node instance to another location. The new location is a Node instance which will be parent node. /// public virtual void CopyTo(Node target, string newName) + { + CopyToAndGetCopy(target, newName); + } + + /// + /// Copies the Node instance to another location. The new location is a Node instance which will be parent node. + /// + public Node CopyToAndGetCopy(Node target) + { + return CopyToAndGetCopy(target, this.Name); + } + /// + /// Copies the Node instance to another location. The new location is a Node instance which will be parent node. + /// + public Node CopyToAndGetCopy(Node target, string newName) { StorageContext.Search.SearchEngine.WaitIfIndexingPaused(); using (var op = SnTrace.ContentOperation.StartOperation("Node.SaveCopied")) { if (target == null) + { throw new ArgumentNullException("target"); - + } string msg = CheckListAndItemCopyingConditions(target); if (msg != null) + { throw new InvalidOperationException(msg); - + } var originalPath = this.Path; string newPath; var correctTargetPath = RepositoryPath.Combine(target.Path, RepositoryPath.PathSeparator); var correctCurrentPath = RepositoryPath.Combine(this.Path, RepositoryPath.PathSeparator); if (correctTargetPath.IndexOf(correctCurrentPath) != -1) + { throw new InvalidOperationException("Node cannot be copied under itself."); - + } target.AssertLock(); var args = new CancellableNodeOperationEventArgs(this, target, CancellableNodeEvent.Copying); FireOnCopying(args); if (args.Cancel) + { throw new CancelNodeEventException(args.CancelMessage, args.EventType, this); - + } var customData = args.CustomData; - var targetName = newName; - int i = 0; var nodeList = target.GetChildren(); while (NameExists(nodeList, targetName)) @@ -2889,17 +2905,17 @@ public virtual void CopyTo(Node target, string newName) throw new NodeAlreadyExistsException(String.Concat("Cannot copy the content because the target folder already contains a content named '", this.Name, "'.")); targetName = GenerateCopyName(i++); } - newPath = correctTargetPath + targetName; - DoCopy(newPath, targetName); - SnTrace.ContentOperation.Write($"Node copied. NodeId:{this.Id}, Path:{this.Path}, OriginalPath:{originalPath}, NewPath:{newPath}"); + var copyOfSource = DoCopyAndGetCopy(newPath, targetName); + SnTrace.ContentOperation.Write($"Node copied. NodeId:{this.Id}, Path:{this.Path}, OriginalPath:{originalPath}, NewPath:{newPath}"); FireOnCopied(target, customData); - op.Successful = true; + return copyOfSource; } } + private string CheckListAndItemCopyingConditions(Node target) { string msg = null; @@ -2942,9 +2958,15 @@ private string GenerateCopyName(int index) return String.Concat("Copy (", index, ") of ", this.Name); } private void DoCopy(string targetPath, string newName) + { + DoCopyAndGetCopy(targetPath, newName); + } + + private Node DoCopyAndGetCopy(string targetPath, string newName) { bool first = true; var sourcePath = this.Path; + Node copyOfSource = null; if (!Node.Exists(sourcePath)) throw new ContentNotFoundException(sourcePath); foreach (var sourceNode in NodeEnumerator.GetNodes(sourcePath, ExecutionHint.ForceRelationalEngine)) @@ -2957,11 +2979,14 @@ private void DoCopy(string targetPath, string newName) CopyExplicitPermissionsTo(sourceNode, copy); if (first) { + copyOfSource = copy; newName = null; first = false; } } + return copyOfSource; } + private void CopyExplicitPermissionsTo(Node sourceNode, Node targetNode) { AccessProvider.ChangeToSystemAccount(); diff --git a/src/Storage/NodeIdentifier.cs b/src/Storage/NodeIdentifier.cs index ec50fd978..f83b32372 100644 --- a/src/Storage/NodeIdentifier.cs +++ b/src/Storage/NodeIdentifier.cs @@ -23,6 +23,18 @@ private NodeIdentifier() { } + /// + /// Gets a node identifier from the provided node. + /// + /// An existing Node. + public static NodeIdentifier Get(Node node) + { + if (node == null) + return null; + + return new NodeIdentifier{Id = node.Id, Path = node.Path}; + } + /// /// Gets a new node identifier based on the provided value. /// @@ -34,15 +46,13 @@ public static NodeIdentifier Get(object identifier) var nid = new NodeIdentifier(); - var idAsText = identifier as string; - if (idAsText != null) + if (identifier is string idAsText) { - // We received a string, that can be a path or an id as well. - int id; + // We received a string that can be a path or an id as well. if (RepositoryPath.IsValidPath(idAsText) == RepositoryPath.PathResult.Correct) nid.Path = idAsText; - else if (int.TryParse(idAsText, out id)) + else if (int.TryParse(idAsText, out var id)) nid.Id = id; else throw new SnNotSupportedException("An identifier should be either a path or an id. Invalid value: " + idAsText); diff --git a/src/Storage/Properties/AssemblyInfo.cs b/src/Storage/Properties/AssemblyInfo.cs index 796524f80..932eaf6ea 100644 --- a/src/Storage/Properties/AssemblyInfo.cs +++ b/src/Storage/Properties/AssemblyInfo.cs @@ -20,4 +20,4 @@ [assembly: AssemblyCulture("")] [assembly: AssemblyVersion("7.0.0.0")] [assembly: AssemblyFileVersion("7.0.0.0")] -[assembly: AssemblyInformationalVersion("7.0.0-beta4")] +[assembly: AssemblyInformationalVersion("7.0.0")] diff --git a/src/Storage/Schema/NodeType.cs b/src/Storage/Schema/NodeType.cs index 90b69508d..89b6c1194 100644 --- a/src/Storage/Schema/NodeType.cs +++ b/src/Storage/Schema/NodeType.cs @@ -226,14 +226,14 @@ public static Node CreateInstance(string nodeTypeName, Node parent) public Node CreateInstance(Node parent) { if (parent == null) - throw new ArgumentNullException("parent"); + throw new ArgumentNullException(nameof(parent)); if (_type == null) - _type = TypeResolver.GetType(_className); + _type = TypeResolver.GetType(_className, false); if (_type == null) { - string exceptionMessage = String.Concat("Type not found, therefore the node can't be created.", + var exceptionMessage = string.Concat("Type not found, therefore the node can't be created.", "\nClass name: ", _className, "\nNode type path: ", _nodeTypePath, "\nParent class name: ", (_parent != null ? _parent._className : "Parent is null"), "\n"); @@ -243,7 +243,7 @@ public Node CreateInstance(Node parent) // only public ctor is valid: public NodeDescendant(Node parent, string nodeTypeName) ConstructorInfo ctor = _type.GetConstructor(BindingFlags.Public | BindingFlags.Instance, null, _newArgTypes, null); if (ctor == null) - throw new TypeInitializationException(String.Concat("Constructor not found. Valid signature: ctor(Node, string).\nClassName: ", _className), null); + throw new TypeInitializationException(string.Concat("Constructor not found. Valid signature: ctor(Node, string).\nClassName: ", _className), null); Node node; try @@ -262,11 +262,11 @@ public Node CreateInstance(Node parent) internal Node CreateInstance(NodeToken token) { if (_type == null) - _type = TypeResolver.GetType(_className); + _type = TypeResolver.GetType(_className, false); if (_type == null) { - string exceptionMessage = string.Format(CultureInfo.InvariantCulture, "Type not found, therefore the node can't be created.\nClass name: {0}\nNode type path: {1}\nParent class name: {2}\n", _className, _nodeTypePath, (_parent != null ? _parent._className : "Parent type is null")); + var exceptionMessage = string.Format(CultureInfo.InvariantCulture, "Type not found, therefore the node can't be created.\nClass name: {0}\nNode type path: {1}\nParent class name: {2}\n", _className, _nodeTypePath, (_parent != null ? _parent._className : "Parent type is null")); if (token != null) exceptionMessage = string.Concat(exceptionMessage, string.Format(CultureInfo.InvariantCulture, "Token.NodeId: {0}\nToken.Path: {1}", token.NodeId, (token.NodeData != null ? token.NodeData.Path : "UNKNOWN (InnerInfo is not loaded)"))); else diff --git a/src/Storage/Security/AccessProvider.cs b/src/Storage/Security/AccessProvider.cs index e1714cace..e164ffb35 100644 --- a/src/Storage/Security/AccessProvider.cs +++ b/src/Storage/Security/AccessProvider.cs @@ -1,8 +1,5 @@ using System; using SenseNet.Configuration; -using SenseNet.ContentRepository.Storage.Data; -using SenseNet.Diagnostics; -using SenseNet.Tools; namespace SenseNet.ContentRepository.Storage.Security { @@ -10,46 +7,20 @@ public abstract class AccessProvider { protected readonly IUser StartupUser = new StartupUser(); - private static readonly object _lock = new object(); - private static AccessProvider _current; - public static AccessProvider Current - { - get - { - if(_current == null) - { - lock(_lock) - { - if(_current == null) - { - try - { - var provider = (AccessProvider)TypeResolver.CreateInstance(Providers.AccessProviderClassName); - provider.Initialize(); - _current = provider; - } - catch (TypeNotFoundException) // rethrow - { - throw new ConfigurationException(String.Concat(SR.Exceptions.Configuration.Msg_AccessProviderImplementationDoesNotExist, ": ", Providers.AccessProviderClassName)); - } - catch (InvalidCastException) // rethrow - { - throw new ConfigurationException(String.Concat(SR.Exceptions.Configuration.Msg_InvalidAccessProviderImplementation, ": ", Providers.AccessProviderClassName)); - } - SnLog.WriteInformation("AccessProvider created: " + _current); - } - } - } - return _current; - } - } + public static AccessProvider Current => Providers.Instance.AccessProvider; protected virtual void Initialize() { // do nothing } - public static bool IsInitialized { get { return _current != null; } } + internal void InitializeInternal() + { + Initialize(); + } + + [Obsolete] + public static bool IsInitialized => Current != null; public abstract IUser GetCurrentUser(); diff --git a/src/Storage/Security/SecurityHandler.cs b/src/Storage/Security/SecurityHandler.cs index cccba393a..d865c5fe2 100644 --- a/src/Storage/Security/SecurityHandler.cs +++ b/src/Storage/Security/SecurityHandler.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Configuration; using System.Diagnostics; using System.Linq; using System.Xml; @@ -8,7 +7,6 @@ using SenseNet.ContentRepository.Storage.Data; using SenseNet.Diagnostics; using SenseNet.Security; -using SenseNet.Security.EF6SecurityStore; using SenseNet.Security.Messaging; using SenseNet.Tools; @@ -1634,9 +1632,7 @@ private static void GetAllowedPermissionsRecursive(PermissionTypeBase permission public static void StartSecurity(bool isWebContext) { var dummy = PermissionType.Open; - var securityDataProvider = new EF6SecurityDataProvider( - Configuration.Security.SecurityDatabaseCommandTimeoutInSeconds, - ConnectionStrings.SecurityDatabaseConnectionString); + var securityDataProvider = Providers.Instance.SecurityDataProvider; var messageProvider = (IMessageProvider)Activator.CreateInstance(GetMessageProviderType()); messageProvider.Initialize(); @@ -1668,7 +1664,7 @@ public static void StartSecurity(bool isWebContext) private static Type GetMessageProviderType() { var messageProviderTypeName = Providers.SecurityMessageProviderClassName; - var t = TypeResolver.GetType(messageProviderTypeName); + var t = TypeResolver.GetType(messageProviderTypeName, false); if (t == null) throw new InvalidOperationException("Unknown security message provider: " + messageProviderTypeName); diff --git a/src/Storage/Security/StartupUser.cs b/src/Storage/Security/StartupUser.cs index 012259049..b998020df 100644 --- a/src/Storage/Security/StartupUser.cs +++ b/src/Storage/Security/StartupUser.cs @@ -9,7 +9,7 @@ namespace SenseNet.ContentRepository.Storage.Security { internal sealed class StartupUser : IUser { - + // ================================================================================================== IUser Members public bool Enabled @@ -30,7 +30,7 @@ public string Email } public string FullName { - get { return "STARTUP"; } + get { return "Startup User"; } set { throw new InvalidOperationException("You cannot set a property of the STARTUP user."); } } public string Password @@ -45,7 +45,7 @@ public string PasswordHash [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "value", Justification = "Interface implementation")] public string Username { - get { return "STARTUP"; } + get { return "Startup"; } set { throw new InvalidOperationException("You cannot set a property of the STARTUP user."); } } public bool IsInGroup(IGroup group) @@ -71,10 +71,7 @@ public bool IsInGroup(int securityGroupId) public int Id => Identifiers.StartupUserId; - public string Path - { - get { throw new InvalidOperationException("You cannot get the Path property of the STARTUP user."); } - } + public string Path => $"/Root/IMS/{IdentityManagement.BuiltInDomainName}/Portal/{Name}"; // ================================================================================================== IIdentity Members @@ -86,10 +83,7 @@ public bool IsAuthenticated { get { return true; } } - public string Name - { - get { return "STARTUP"; } - } + public string Name => "Startup"; // ================================================================================================== SenseNet.Security.ISecurityUser diff --git a/src/Tests/SenseNet.ContentRepository.Tests/Properties/AssemblyInfo.cs b/src/Tests/SenseNet.ContentRepository.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..1f2b853a5 --- /dev/null +++ b/src/Tests/SenseNet.ContentRepository.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,19 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("SenseNet.ContentRepository.Tests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("SenseNet.ContentRepository.Tests")] +[assembly: AssemblyCopyright("Copyright © 2017")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +[assembly: ComVisible(false)] +[assembly: Guid("7de6a7e8-5738-4436-9646-9c1179f752ea")] + +[assembly: AssemblyVersion("7.0.0.0")] +[assembly: AssemblyFileVersion("7.0.0.0")] +[assembly: AssemblyInformationalVersion("7.0.0")] diff --git a/src/Tests/SenseNet.ContentRepository.Tests/ProvidersTests.cs b/src/Tests/SenseNet.ContentRepository.Tests/ProvidersTests.cs new file mode 100644 index 000000000..06c5373df --- /dev/null +++ b/src/Tests/SenseNet.ContentRepository.Tests/ProvidersTests.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SenseNet.Configuration; + +namespace SenseNet.ContentRepository.Tests +{ + public class TestProvider + { + public int TestProperty { get; set; } + } + + [TestClass] + public class ProvidersTests + { + [TestMethod] + public void Provider_ByType() + { + // reset + Providers.Instance.SetProvider("TestProvider", null); + Providers.Instance.SetProvider(typeof(TestProvider), null); + + var p1 = Providers.Instance.GetProvider(); + Assert.IsNull(p1); + + Providers.Instance.SetProvider(typeof(TestProvider), new TestProvider { TestProperty = 123 }); + + p1 = Providers.Instance.GetProvider(); + Assert.AreEqual(123, p1.TestProperty); + } + + [TestMethod] + public void Provider_ByName() + { + // reset + Providers.Instance.SetProvider("TestProvider", null); + Providers.Instance.SetProvider(typeof(TestProvider), null); + + var p1 = Providers.Instance.GetProvider("TestProvider"); + Assert.IsNull(p1); + + Providers.Instance.SetProvider("TestProvider", new TestProvider { TestProperty = 456 }); + + // get by type: still null + p1 = Providers.Instance.GetProvider(); + Assert.IsNull(p1); + + p1 = Providers.Instance.GetProvider("TestProvider"); + Assert.AreEqual(456, p1.TestProperty); + } + } +} diff --git a/src/Tests/SenseNet.ContentRepository.Tests/RepositoryStartTests.cs b/src/Tests/SenseNet.ContentRepository.Tests/RepositoryStartTests.cs new file mode 100644 index 000000000..4735e7265 --- /dev/null +++ b/src/Tests/SenseNet.ContentRepository.Tests/RepositoryStartTests.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.Common; +using System.IO; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SenseNet.ContentRepository.Storage; +using SenseNet.ContentRepository.Storage.Data; +using SenseNet.ContentRepository.Storage.Schema; +using SenseNet.Diagnostics; +using SenseNet.Security; +using SenseNet.Security.Messaging; +using SenseNet.Security.Messaging.SecurityMessages; + +namespace SenseNet.ContentRepository.Tests +{ + [TestClass] + public class RepositoryStartTests + { + [TestMethod] + public void TestMethod1() + { + Assert.Inconclusive("Repository start test not implemented."); + + //var repoBuilder = new RepositoryBuilder() + // .UseDataProvider(new TestDataProvider()) + // .UseSecurityDataProvider(new TestSecurityDbProvider()) + // .UseElevatedModificationVisibilityRuleProvider(new ElevatedModificationVisibilityRule()) + // .StartLuceneManager(false) + // .BackupIndexAtTheEnd(false) + // .StartWorkflowEngine(false) + // .RestoreIndex(false); + + //using (var repo = Repository.Start(repoBuilder)) + //{ + + //} + } + } +} diff --git a/src/Tests/SenseNet.ContentRepository.Tests/SenseNet.ContentRepository.Tests.csproj b/src/Tests/SenseNet.ContentRepository.Tests/SenseNet.ContentRepository.Tests.csproj new file mode 100644 index 000000000..3ed7575dd --- /dev/null +++ b/src/Tests/SenseNet.ContentRepository.Tests/SenseNet.ContentRepository.Tests.csproj @@ -0,0 +1,123 @@ + + + + Debug + AnyCPU + {7DE6A7E8-5738-4436-9646-9C1179F752EA} + Library + Properties + SenseNet.ContentRepository.Tests + SenseNet.ContentRepository.Tests + v4.5.2 + 512 + {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages + False + UnitTest + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll + True + + + ..\..\packages\SenseNet.Security.2.3.0.0\lib\net45\SenseNet.Security.dll + True + + + ..\..\packages\SenseNet.Tools.2.1.1\lib\net451\SenseNet.Tools.dll + True + + + + + + + + + + + + + + + + + + + + + + + + + {4e6722b5-ac95-494c-80a5-a4d80cc502b5} + BlobStorage + + + {a453e920-29c0-45cd-984c-0d8e3631b1e3} + Common + + + {a19f707c-b1c3-45c7-ae38-e7b7f30c1161} + Configuration + + + {786e6165-ca02-45a9-bf58-207a45d7d6df} + ContentRepository + + + {5db4ddba-81f6-4d81-943a-18f3178b3355} + Storage + + + + + + + + + + False + + + False + + + False + + + False + + + + + + + + \ No newline at end of file diff --git a/src/Tests/SenseNet.ContentRepository.Tests/packages.config b/src/Tests/SenseNet.ContentRepository.Tests/packages.config new file mode 100644 index 000000000..377a75068 --- /dev/null +++ b/src/Tests/SenseNet.ContentRepository.Tests/packages.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/Tests/SenseNet.Packaging.IntegrationTests/Properties/AssemblyInfo.cs b/src/Tests/SenseNet.Packaging.IntegrationTests/Properties/AssemblyInfo.cs index a206f1844..71b42dd5c 100644 --- a/src/Tests/SenseNet.Packaging.IntegrationTests/Properties/AssemblyInfo.cs +++ b/src/Tests/SenseNet.Packaging.IntegrationTests/Properties/AssemblyInfo.cs @@ -17,4 +17,4 @@ [assembly: AssemblyVersion("7.0.0.0")] [assembly: AssemblyFileVersion("7.0.0.0")] -[assembly: AssemblyInformationalVersion("7.0.0-beta4")] +[assembly: AssemblyInformationalVersion("7.0.0")] diff --git a/src/Tests/SenseNet.Packaging.Tests/PackagingStorageTests.cs b/src/Tests/SenseNet.Packaging.Tests/PackagingStorageTests.cs index 1088e214b..a9de7922e 100644 --- a/src/Tests/SenseNet.Packaging.Tests/PackagingStorageTests.cs +++ b/src/Tests/SenseNet.Packaging.Tests/PackagingStorageTests.cs @@ -4,8 +4,8 @@ using System.Data.Common; using System.Linq; using Microsoft.VisualStudio.TestTools.UnitTesting; +using SenseNet.Configuration; using SenseNet.ContentRepository.Storage; -using SenseNet.ContentRepository.Storage.Data; using SenseNet.ContentRepository.Storage.Data.SqlClient; using SenseNet.Packaging.Tests.Implementations; @@ -25,10 +25,14 @@ public static void Initialize(TestContext context) { PackageManager.StorageFactory = new BuiltinPackageStorageProviderFactory(); - var sqlProvider = new SqlProvider(); - sqlProvider.DataProcedureFactory = Factory; - var dataProviderAcc = new PrivateType(typeof(DataProvider)); - dataProviderAcc.SetStaticField("_current", sqlProvider); + var sqlProvider = new SqlProvider + { + DataProcedureFactory = Factory + }; + + //UNDONE: set provider locally (per-thread) instead of changing + //the global value, that is used by every test. + Providers.Instance.DataProvider = sqlProvider; } /* ================================================================================================== Tests */ diff --git a/src/Tests/SenseNet.Packaging.Tests/Properties/AssemblyInfo.cs b/src/Tests/SenseNet.Packaging.Tests/Properties/AssemblyInfo.cs index 394b97fc1..c855f0c78 100644 --- a/src/Tests/SenseNet.Packaging.Tests/Properties/AssemblyInfo.cs +++ b/src/Tests/SenseNet.Packaging.Tests/Properties/AssemblyInfo.cs @@ -17,4 +17,4 @@ [assembly: AssemblyVersion("7.0.0.0")] [assembly: AssemblyFileVersion("7.0.0.0")] -[assembly: AssemblyInformationalVersion("7.0.0-beta4")] +[assembly: AssemblyInformationalVersion("7.0.0")] diff --git a/src/Tests/SenseNet.Services.Tests/OAuthAuthenticationTests.cs b/src/Tests/SenseNet.Services.Tests/OAuthAuthenticationTests.cs new file mode 100644 index 000000000..d6a1938d8 --- /dev/null +++ b/src/Tests/SenseNet.Services.Tests/OAuthAuthenticationTests.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; +using System.Web; +using Moq; +using SenseNet.ContentRepository.Security; +using SenseNet.ContentRepository.Storage.Security; +using SenseNet.Portal.Virtualization; +using SenseNet.Services.Virtualization; +using Xunit; +using Xunit.Sdk; + +namespace SenseNet.Services.Tests +{ + public class OAuthAuthenticationTests + { + private abstract class TestOAuthProvider : OAuthProvider + { + public static readonly string UserName = "user1"; + public static readonly string TestProviderName = "testprovider"; + + public override string IdentifierFieldName { get; } = "TestOAuthField1"; + public override string ProviderName { get; } = TestProviderName; + + public override IOAuthIdentity GetUserData(object tokenData) + { + return new OAuthIdentity + { + Username = "user1", + Identifier = "user1" + }; + } + } + + private class OAuthProviderValid : TestOAuthProvider + { + public override string VerifyToken(HttpRequestBase request, out object tokenData) + { + tokenData = null; + return UserName; + } + } + private class OAuthProviderInvalidUser : TestOAuthProvider + { + public override string VerifyToken(HttpRequestBase request, out object tokenData) + { + tokenData = null; + return null; + } + } + private class OAuthProviderError : TestOAuthProvider + { + public override string VerifyToken(HttpRequestBase request, out object tokenData) + { + throw new InvalidOperationException(); + } + } + + private class TestUser : IUser + { + public static TestUser Create(OAuthProvider provider, object tokenData, string userId) + { + return new TestUser + { + Username = userId + }; + } + + public int Id { get; } + public string Path { get; } + public bool IsInGroup(int securityGroupId) + { + throw new NotImplementedException(); + } + + public string Name { get; } + public string AuthenticationType { get; } + public bool IsAuthenticated { get; } + public IEnumerable GetDynamicGroups(int entityId) + { + throw new NotImplementedException(); + } + + public bool Enabled { get; set; } + public string Domain { get; } + public string Email { get; set; } + public string FullName { get; set; } + public string Password { get; set; } + public string PasswordHash { get; set; } + public string Username { get; set; } + public bool IsInGroup(IGroup @group) + { + throw new NotImplementedException(); + } + + public bool IsInOrganizationalUnit(IOrganizationalUnit orgUnit) + { + throw new NotImplementedException(); + } + + public bool IsInContainer(ISecurityContainer container) + { + throw new NotImplementedException(); + } + + public MembershipExtension MembershipExtension { get; set; } + } + + [Fact] + public void OAuth_ValidUser() + { + var oauth = new OAuthManager + { + GetProvider = s => new OAuthProviderValid(), + LoadOrCreateUser = TestUser.Create + }; + + var user = oauth.VerifyUser(TestOAuthProvider.TestProviderName, null); + + Assert.Equal(TestOAuthProvider.UserName, user.Username); + } + [Fact] + public void OAuth_NotVerifiedUser() + { + var oauth = new OAuthManager + { + GetProvider = s => new OAuthProviderInvalidUser(), + LoadOrCreateUser = TestUser.Create + }; + + // this must return null (we use the OAuthProviderInvalidUser), even if the LoadOrCreateUser method is defined above + var user = oauth.VerifyUser(TestOAuthProvider.TestProviderName, null); + + Assert.Null(user); + } + [Fact] + public void OAuth_ProviderError() + { + var oauth = new OAuthManager + { + GetProvider = s => new OAuthProviderError(), + LoadOrCreateUser = TestUser.Create + }; + + // this must return null (we use the OAuthProviderError), even if the LoadOrCreateUser method is defined above + var user = oauth.VerifyUser(TestOAuthProvider.TestProviderName, null); + + Assert.Null(user); + } + [Fact] + public void OAuth_ProviderNotFound() + { + var oauth = new OAuthManager + { + GetProvider = s => null, + LoadOrCreateUser = TestUser.Create + }; + + // this must throw an exception, because the provider is null (cannot be found) + Assert.Throws(() => oauth.VerifyUser(TestOAuthProvider.TestProviderName, null)); + } + } +} diff --git a/src/Tests/SenseNet.Services.Tests/PortalAuthenticationModuleTests.cs b/src/Tests/SenseNet.Services.Tests/PortalAuthenticationModuleTests.cs deleted file mode 100644 index 39fde4e2c..000000000 --- a/src/Tests/SenseNet.Services.Tests/PortalAuthenticationModuleTests.cs +++ /dev/null @@ -1,339 +0,0 @@ -#define TEST -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.IO; -using System.Net; -using System.Net.Http; -using System.Security.Claims; -using System.Security.Principal; -using System.Web; -using Moq; -using SenseNet.ContentRepository; -using SenseNet.Portal.Virtualization; -using Xunit; -using HttpCookie = System.Web.HttpCookie; - -namespace SenseNet.Services.Tests -{ - public class PortalAuthenticationModuleTests - { - - public PortalAuthenticationModuleTests() - { - } - - private class Disposable : IDisposable - { - public void Dispose(){} - } - - private void InitDependecies(Mock module, Mock context, Mock request, Mock response) - { - module.Object.GetRequest = (sender) => request.Object; - module.Object.GetResponse = (sender) => response.Object; - module.Object.GetContext = (sender) => context.Object; - module.Object.GetVisitorPrincipal = () => - { - var principal = new ClaimsPrincipal(); - var claims = new List(); - claims.Add(new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", "Visitor")); - principal.AddIdentity(new ClaimsIdentity(claims)); - return principal; - }; - module.Object.LoadUserPrincipal = (userName) => - { - var principal = new ClaimsPrincipal(); - var claims = new List(); - claims.Add(new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", userName)); - principal.AddIdentity(new ClaimsIdentity(claims)); - return principal; - }; - module.Object.IsUserValid = (userName, password) => true; - module.Object.GetSystemAccount = () => new Disposable(); - module.Object.GetBasicAuthHeader = () => null; - - } - - [Fact] - public void OnAuthenticateRequestTokenLoginTest() - { - var mockRequest = new Mock(); - mockRequest.SetupGet(o => o.Url).Returns(new Uri("https://sensenet.com")); - mockRequest.SetupGet(o => o.IsSecureConnection).Returns(true); - var headers = new NameValueCollection(); - headers.Add("X-Authentication-Type", "Token"); - mockRequest.SetupGet(o => o.Headers).Returns(headers); - var mockResponse = new Mock(); - var cookies = new HttpCookieCollection(); - string body = ""; - int responseStatus = 0; - mockResponse.SetupGet(o => o.Cookies).Returns(cookies); - mockResponse.Setup(o => o.Write(It.IsAny())).Callback((string t) => { body = t; }); - mockResponse.SetupSet(o => o.StatusCode = It.IsAny()).Callback((int s) => { responseStatus = s; }); - var principal = new ClaimsPrincipal(); - var claims = new List(); - claims.Add(new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", "username")); - principal.AddIdentity(new ClaimsIdentity(claims)); - - var mockContext = new Mock(); - mockContext.SetupGet(o => o.Request).Returns(mockRequest.Object); - mockContext.SetupGet(o => o.Response).Returns(mockResponse.Object); - mockContext.SetupGet(o => o.User).Returns(principal); - Configuration.TokenAuthentication.Audience= "audience"; - Configuration.TokenAuthentication.Issuer = "issuer"; - Configuration.TokenAuthentication.Subject = "subject"; - Configuration.TokenAuthentication.EncriptionAlgorithm = "HS512"; - Configuration.TokenAuthentication.AccessLifeTimeInMinutes = 5; - Configuration.TokenAuthentication.RefreshLifeTimeInMinutes = 1440; - Configuration.TokenAuthentication.ClockSkewInMinutes = 5; - Configuration.TokenAuthentication.SymmetricKeySecret = "very secrety secret"; - var application = new HttpApplication(); - var module = new Mock(); - InitDependecies(module, mockContext, mockRequest, mockResponse); - module.Object.GetBasicAuthHeader = () => "Basic dXNlcm5hbWU6cGFzc3dvcmQ="; - - module.Object.OnAuthenticateRequest(application, EventArgs.Empty); - - Assert.NotNull(mockResponse.Object.Cookies["as"]); - Assert.NotNull(mockResponse.Object.Cookies["rs"]); - Assert.Matches("\"access\":\".+\"", body); - Assert.Matches("\"refresh\":\".+\"", body); - Assert.Equal(200, responseStatus); - } - - [Fact] - public void OnAuthenticateRequestTokenLoginUrlTest() - { - var mockRequest = new Mock(); - mockRequest.SetupGet(o => o.Url).Returns(new Uri("https://sensenet.com/sn-token/login")); - mockRequest.SetupGet(o => o.IsSecureConnection).Returns(true); - var headers = new NameValueCollection(); - mockRequest.SetupGet(o => o.Headers).Returns(headers); - var mockResponse = new Mock(); - var cookies = new HttpCookieCollection(); - string body = ""; - int responseStatus = 0; - mockResponse.SetupGet(o => o.Cookies).Returns(cookies); - mockResponse.Setup(o => o.Write(It.IsAny())).Callback((string t) => { body = t; }); - mockResponse.SetupSet(o => o.StatusCode = It.IsAny()).Callback((int s) => { responseStatus = s; }); - var principal = new ClaimsPrincipal(); - var claims = new List(); - claims.Add(new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", "username")); - principal.AddIdentity(new ClaimsIdentity(claims)); - - var mockContext = new Mock(); - mockContext.SetupGet(o => o.Request).Returns(mockRequest.Object); - mockContext.SetupGet(o => o.Response).Returns(mockResponse.Object); - mockContext.SetupGet(o => o.User).Returns(principal); - Configuration.TokenAuthentication.Audience = "audience"; - Configuration.TokenAuthentication.Issuer = "issuer"; - Configuration.TokenAuthentication.Subject = "subject"; - Configuration.TokenAuthentication.EncriptionAlgorithm = "HS512"; - Configuration.TokenAuthentication.AccessLifeTimeInMinutes = 5; - Configuration.TokenAuthentication.RefreshLifeTimeInMinutes = 1440; - Configuration.TokenAuthentication.ClockSkewInMinutes = 5; - Configuration.TokenAuthentication.SymmetricKeySecret = "very secrety secret"; - var application = new HttpApplication(); - var module = new Mock(); - InitDependecies(module, mockContext, mockRequest, mockResponse); - module.Object.GetBasicAuthHeader = () => "Basic dXNlcm5hbWU6cGFzc3dvcmQ="; - - module.Object.OnAuthenticateRequest(application, EventArgs.Empty); - - Assert.NotNull(mockResponse.Object.Cookies["as"]); - Assert.NotNull(mockResponse.Object.Cookies["rs"]); - Assert.Matches("\"access\":\".+\"", body); - Assert.Matches("\"refresh\":\".+\"", body); - Assert.Equal(200, responseStatus); - } - - [Fact] - public void OnAuthenticateRequestTokenLoginWithInvalidUserTest() - { - var mockRequest = new Mock(); - mockRequest.SetupGet(o => o.Url).Returns(new Uri("https://sensenet.com")); - mockRequest.SetupGet(o => o.IsSecureConnection).Returns(true); - var headers = new NameValueCollection(); - headers.Add("X-Authentication-Type", "Token"); - mockRequest.SetupGet(o => o.Headers).Returns(headers); - var mockResponse = new Mock(); - var cookies = new HttpCookieCollection(); - string body = ""; - int responseStatus = 0; - mockResponse.SetupGet(o => o.Cookies).Returns(cookies); - mockResponse.Setup(o => o.Write(It.IsAny())).Callback((string t) => { body = t; }); - mockResponse.SetupSet(o => o.StatusCode = It.IsAny()).Callback((int s) => { responseStatus = s; }); - var principal = new ClaimsPrincipal(); - var claims = new List(); - claims.Add(new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", "username")); - principal.AddIdentity(new ClaimsIdentity(claims)); - - var mockContext = new Mock(); - mockContext.SetupGet(o => o.Request).Returns(mockRequest.Object); - mockContext.SetupGet(o => o.Response).Returns(mockResponse.Object); - mockContext.SetupGet(o => o.User).Returns(principal); - Configuration.TokenAuthentication.Audience = "audience"; - Configuration.TokenAuthentication.Issuer = "issuer"; - Configuration.TokenAuthentication.Subject = "subject"; - Configuration.TokenAuthentication.EncriptionAlgorithm = "HS512"; - Configuration.TokenAuthentication.AccessLifeTimeInMinutes = 5; - Configuration.TokenAuthentication.RefreshLifeTimeInMinutes = 1440; - Configuration.TokenAuthentication.ClockSkewInMinutes = 5; - Configuration.TokenAuthentication.SymmetricKeySecret = "very secrety secret"; - var application = new HttpApplication(); - var module = new Mock(); - InitDependecies(module, mockContext, mockRequest, mockResponse); - module.Object.GetBasicAuthHeader = () => "Basic dXNlcm5hbWU6cGFzc3dvcmQ="; - module.Object.IsUserValid = (n, p) => false; - - module.Object.OnAuthenticateRequest(application, EventArgs.Empty); - - Assert.Null(mockResponse.Object.Cookies["as"]); - Assert.Null(mockResponse.Object.Cookies["rs"]); - Assert.DoesNotContain("\"access\":", body); - Assert.DoesNotContain("\"refresh\":", body); - Assert.Equal(401, responseStatus); - } - - [Fact] - public void OnAuthenticateRequestTokenRefreshTest() - { - var mockRequest = new Mock(); - mockRequest.SetupGet(o => o.Url).Returns(new Uri("https://sensenet.com")); - mockRequest.SetupGet(o => o.IsSecureConnection).Returns(true); - var headers = new NameValueCollection(); - headers.Add("X-Authentication-Type", "Token"); - headers.Add("X-Refresh-Data", "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJpc3N1ZXIiLCJzdWIiOiJzdWJqZWN0IiwiYXVkIjoiYXVkaWVuY2UiLCJleHAiOjIwOTA1NzczOTcsImlhdCI6MTQ5MDU3NzA5NywibmJmIjoxNDkwNTc3Mzk3LCJuYW1lIjoiTXlOYW1lIn0"); - mockRequest.SetupGet(o => o.Headers).Returns(headers); - var inCookies = new HttpCookieCollection(); - inCookies.Add(new HttpCookie("rs", "_xdP6wc_Z4WgiIph-EC7O5Hh2f_aaGGZTidnK33ss-hdyw1ss7soTs7lVKrYQvmA4zSNRQ632Y-kR4TYyWdJiw"));; - mockRequest.SetupGet(o => o.Cookies).Returns(inCookies); - var mockResponse = new Mock(); - var cookies = new HttpCookieCollection(); - string body = ""; - int responseStatus = 0; - mockResponse.SetupGet(o => o.Cookies).Returns(cookies); - mockResponse.Setup(o => o.Write(It.IsAny())).Callback((string t) => { body = t; }); - mockResponse.SetupSet(o => o.StatusCode = It.IsAny()).Callback((int s) => { responseStatus = s; }); - - var mockContext = new Mock(); - mockContext.SetupGet(o => o.Request).Returns(mockRequest.Object); - mockContext.SetupGet(o => o.Response).Returns(mockResponse.Object); - Configuration.TokenAuthentication.Audience = "audience"; - Configuration.TokenAuthentication.Issuer = "issuer"; - Configuration.TokenAuthentication.Subject = "subject"; - Configuration.TokenAuthentication.EncriptionAlgorithm = "HS512"; - Configuration.TokenAuthentication.AccessLifeTimeInMinutes = 5; - Configuration.TokenAuthentication.RefreshLifeTimeInMinutes = 10000000; - Configuration.TokenAuthentication.ClockSkewInMinutes = 1; - Configuration.TokenAuthentication.SymmetricKeySecret = "very secrety secret"; - var application = new HttpApplication(); - var module = new Mock(); - InitDependecies(module, mockContext, mockRequest, mockResponse); - - module.Object.OnAuthenticateRequest(application, EventArgs.Empty); - - Assert.NotNull(mockResponse.Object.Cookies["as"]); - Assert.Null(mockResponse.Object.Cookies["rs"]); - Assert.Matches("\"access\":\".+\"", body); - Assert.DoesNotContain("\"refresh\":", body); - Assert.Equal(200, responseStatus); - } - - [Fact] - public void OnAuthenticateRequestTokenRefreshUrlTest() - { - var mockRequest = new Mock(); - mockRequest.SetupGet(o => o.Url).Returns(new Uri("https://sensenet.com/sn-token/refresh")); - mockRequest.SetupGet(o => o.IsSecureConnection).Returns(true); - var headers = new NameValueCollection(); - headers.Add("X-Refresh-Data", "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJpc3N1ZXIiLCJzdWIiOiJzdWJqZWN0IiwiYXVkIjoiYXVkaWVuY2UiLCJleHAiOjIwOTA1NzczOTcsImlhdCI6MTQ5MDU3NzA5NywibmJmIjoxNDkwNTc3Mzk3LCJuYW1lIjoiTXlOYW1lIn0"); - mockRequest.SetupGet(o => o.Headers).Returns(headers); - var inCookies = new HttpCookieCollection(); - inCookies.Add(new HttpCookie("rs", "_xdP6wc_Z4WgiIph-EC7O5Hh2f_aaGGZTidnK33ss-hdyw1ss7soTs7lVKrYQvmA4zSNRQ632Y-kR4TYyWdJiw")); ; - mockRequest.SetupGet(o => o.Cookies).Returns(inCookies); - var mockResponse = new Mock(); - var cookies = new HttpCookieCollection(); - string body = ""; - int responseStatus = 0; - mockResponse.SetupGet(o => o.Cookies).Returns(cookies); - mockResponse.Setup(o => o.Write(It.IsAny())).Callback((string t) => { body = t; }); - mockResponse.SetupSet(o => o.StatusCode = It.IsAny()).Callback((int s) => { responseStatus = s; }); - - var mockContext = new Mock(); - mockContext.SetupGet(o => o.Request).Returns(mockRequest.Object); - mockContext.SetupGet(o => o.Response).Returns(mockResponse.Object); - Configuration.TokenAuthentication.Audience = "audience"; - Configuration.TokenAuthentication.Issuer = "issuer"; - Configuration.TokenAuthentication.Subject = "subject"; - Configuration.TokenAuthentication.EncriptionAlgorithm = "HS512"; - Configuration.TokenAuthentication.AccessLifeTimeInMinutes = 5; - Configuration.TokenAuthentication.RefreshLifeTimeInMinutes = 10000000; - Configuration.TokenAuthentication.ClockSkewInMinutes = 1; - Configuration.TokenAuthentication.SymmetricKeySecret = "very secrety secret"; - var application = new HttpApplication(); - var module = new Mock(); - InitDependecies(module, mockContext, mockRequest, mockResponse); - - module.Object.OnAuthenticateRequest(application, EventArgs.Empty); - - Assert.NotNull(mockResponse.Object.Cookies["as"]); - Assert.Null(mockResponse.Object.Cookies["rs"]); - Assert.Matches("\"access\":\".+\"", body); - Assert.DoesNotContain("\"refresh\":", body); - Assert.Equal(200, responseStatus); - } - - [Fact] - public void OnAuthenticateRequestTokenRefreshWithInvalidTokenTest() - { - var mockRequest = new Mock(); - mockRequest.SetupGet(o => o.Url).Returns(new Uri("https://sensenet.com")); - mockRequest.SetupGet(o => o.IsSecureConnection).Returns(true); - var headers = new NameValueCollection(); - headers.Add("X-Authentication-Type", "Token"); - headers.Add("X-Refresh-Data", - "yJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJpc3N1ZXIiLCJzdWIiOiJzdWJqZWN0IiwiYXVkIjoiYXVkaWVuY2UiLCJleHAiOjIwOTA1NzczOTcsImlhdCI6MTQ5MDU3NzA5NywibmJmIjoxNDkwNTc3Mzk3LCJuYW1lIjoiTXlOYW1lIn0"); - mockRequest.SetupGet(o => o.Headers).Returns(headers); - var inCookies = new HttpCookieCollection(); - inCookies.Add(new HttpCookie("rs", - "_xdP6wc_Z4WgiIph-EC7O5Hh2f_aaGGZTidnK33ss-hdyw1ss7soTs7lVKrYQvmA4zSNRQ632Y-kR4TYyWdJiw")); - ; - mockRequest.SetupGet(o => o.Cookies).Returns(inCookies); - var mockResponse = new Mock(); - var cookies = new HttpCookieCollection(); - string body = ""; - int responseStatus = 0; - mockResponse.SetupGet(o => o.Cookies).Returns(cookies); - mockResponse.Setup(o => o.Write(It.IsAny())).Callback((string t) => { body = t; }); - mockResponse.SetupSet(o => o.StatusCode = It.IsAny()).Callback((int s) => {responseStatus = s;}); - - var mockContext = new Mock(); - mockContext.SetupGet(o => o.Request).Returns(mockRequest.Object); - mockContext.SetupGet(o => o.Response).Returns(mockResponse.Object); - Configuration.TokenAuthentication.Audience = "audience"; - Configuration.TokenAuthentication.Issuer = "issuer"; - Configuration.TokenAuthentication.Subject = "subject"; - Configuration.TokenAuthentication.EncriptionAlgorithm = "HS512"; - Configuration.TokenAuthentication.AccessLifeTimeInMinutes = 5; - Configuration.TokenAuthentication.RefreshLifeTimeInMinutes = 10000000; - Configuration.TokenAuthentication.ClockSkewInMinutes = 1; - Configuration.TokenAuthentication.SymmetricKeySecret = "very secrety secret"; - var application = new HttpApplication(); - var module = new Mock(); - InitDependecies(module, mockContext, mockRequest, mockResponse); - - module.Object.OnAuthenticateRequest(application, EventArgs.Empty); - - Assert.Null(mockResponse.Object.Cookies["as"]); - Assert.Null(mockResponse.Object.Cookies["rs"]); - Assert.DoesNotContain("\"access\":", body); - Assert.DoesNotContain("\"refresh\":", body); - Assert.Equal(401, responseStatus); - } - - - } - -} \ No newline at end of file diff --git a/src/Tests/SenseNet.Services.Tests/Properties/AssemblyInfo.cs b/src/Tests/SenseNet.Services.Tests/Properties/AssemblyInfo.cs index a5abf04c9..1bd6d9a5a 100644 --- a/src/Tests/SenseNet.Services.Tests/Properties/AssemblyInfo.cs +++ b/src/Tests/SenseNet.Services.Tests/Properties/AssemblyInfo.cs @@ -17,4 +17,4 @@ [assembly: AssemblyVersion("7.0.0.0")] [assembly: AssemblyFileVersion("7.0.0.0")] -[assembly: AssemblyInformationalVersion("7.0.0-beta4")] +[assembly: AssemblyInformationalVersion("7.0.0")] diff --git a/src/Tests/SenseNet.Services.Tests/SenseNet.Services.Tests.csproj b/src/Tests/SenseNet.Services.Tests/SenseNet.Services.Tests.csproj index a54e0ab5d..5ac9c9278 100644 --- a/src/Tests/SenseNet.Services.Tests/SenseNet.Services.Tests.csproj +++ b/src/Tests/SenseNet.Services.Tests/SenseNet.Services.Tests.csproj @@ -41,6 +41,12 @@ ..\..\packages\Moq.4.7.1\lib\net45\Moq.dll True + + ..\..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll + + + ..\..\packages\SenseNet.Security.2.3.0.0\lib\net45\SenseNet.Security.dll + ..\..\packages\SenseNet.Tools.2.1.1\lib\net451\SenseNet.Tools.dll True @@ -73,11 +79,14 @@ - + + - + + Designer + @@ -88,6 +97,10 @@ {b72529c8-feb1-49f5-b08b-56055b58f296} Services + + {5DB4DDBA-81F6-4D81-943A-18F3178B3355} + Storage + diff --git a/src/Tests/SenseNet.Services.Tests/TokenAuthenticationTests.cs b/src/Tests/SenseNet.Services.Tests/TokenAuthenticationTests.cs new file mode 100644 index 000000000..e4c1c43ac --- /dev/null +++ b/src/Tests/SenseNet.Services.Tests/TokenAuthenticationTests.cs @@ -0,0 +1,814 @@ +#define TEST +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Security.Claims; +using System.Security.Principal; +using System.Web; +using Moq; +using SenseNet.ContentRepository; +using SenseNet.ContentRepository.Storage; +using SenseNet.Portal; +using SenseNet.Portal.Virtualization; +using Xunit; +using HttpCookie = System.Web.HttpCookie; + +namespace SenseNet.Services.Tests +{ + public class TokenAuthenticationTests + { + + public TokenAuthenticationTests() + { + } + + private class Disposable : IDisposable + { + public void Dispose(){} + } + + private static void InitDependecies( Mock context, Mock request, Mock response) + { + AuthenticationHelper.GetRequest = (sender) => request.Object; + AuthenticationHelper.GetResponse = (sender) => response.Object; + AuthenticationHelper.GetContext = (sender) => context.Object; + AuthenticationHelper.GetVisitorPrincipal = () => + { + var principal = new ClaimsPrincipal(); + var claims = new List(); + claims.Add(new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", "Visitor")); + principal.AddIdentity(new ClaimsIdentity(claims)); + return principal; + }; + AuthenticationHelper.LoadUserPrincipal = (userName) => + { + var principal = new ClaimsPrincipal(); + var claims = new List(); + claims.Add(new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", userName)); + principal.AddIdentity(new ClaimsIdentity(claims)); + return principal; + }; + AuthenticationHelper.IsUserValid = (userName, password) => true; + AuthenticationHelper.GetSystemAccount = () => new Disposable(); + AuthenticationHelper.GetBasicAuthHeader = () => null; + } + + [Fact] + public void TokenLogoutTest() + { + var mockRequest = new Mock(); + mockRequest.SetupGet(o => o.Url).Returns(new Uri("https://sensenet.com")); + mockRequest.SetupGet(o => o.IsSecureConnection).Returns(true); + var headers = new NameValueCollection(); + headers.Add("X-Authentication-Action", "TokenLogout"); + headers.Add("X-Access-Data", "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJpc3N1ZXIiLCJzdWIiOiJzdWJqZWN0IiwiYXVkIjoiYXVkaWVuY2UiLCJleHAiOjIxMzQyMDQzMTgsImlhdCI6MTUwMzQ4NDMxOCwibmJmIjoxNTAzNDg0MzE4LCJuYW1lIjoidXNlcm5hbWUifQ"); + mockRequest.SetupGet(o => o.Headers).Returns(headers); + var requestCookies = new HttpCookieCollection(); + requestCookies.Add(new HttpCookie("ahp") + { + Value = "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJpc3N1ZXIiLCJzdWIiOiJzdWJqZWN0IiwiYXVkIjoiYXVkaWVuY2UiLCJleHAiOjIxMzQyMDQzMTgsImlhdCI6MTUwMzQ4NDMxOCwibmJmIjoxNTAzNDg0MzE4LCJuYW1lIjoidXNlcm5hbWUifQ" + ,HttpOnly = true + ,Secure = true + ,Expires = new DateTime(2047,8,23) + }); + requestCookies.Add(new HttpCookie("as") + { + Value = "4UN3ajbm74CKeTAk3VpiJR2f0VAiKydjZg0BWwpbBcWZM5uqVJ_YbazPboItaAliH5eepgvMfNbwwk9W8UIhEA" + ,HttpOnly = true + ,Secure = true + ,Expires = new DateTime(2047,8,23) + }); + requestCookies.Add(new HttpCookie("rs") + { + Value = "lL54nsEOfzZVD6vBCwMB4AxO1fwRgXadNBh9XtduaIygv_HJARlOXBISpC-sxG_96RSQ_fbnU-PcxWvj7w67Rg" + ,HttpOnly = true + ,Secure = true + ,Expires = new DateTime(2047,8,24) + }); + mockRequest.SetupGet(o => o.Cookies).Returns(requestCookies); + var mockResponse = new Mock(); + var cookies = new HttpCookieCollection(); + string body = ""; + int responseStatus = 0; + mockResponse.SetupGet(o => o.Cookies).Returns(cookies); + mockResponse.Setup(o => o.Write(It.IsAny())).Callback((string t) => { body = t; }); + mockResponse.SetupSet(o => o.StatusCode = It.IsAny()).Callback((int s) => { responseStatus = s; }); + var principal = new ClaimsPrincipal(); + var claims = new List(); + claims.Add(new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", "username")); + principal.AddIdentity(new ClaimsIdentity(claims)); + + var mockContext = new Mock(); + mockContext.SetupGet(o => o.Request).Returns(mockRequest.Object); + mockContext.SetupGet(o => o.Response).Returns(mockResponse.Object); + mockContext.SetupGet(o => o.User).Returns(principal); + Configuration.TokenAuthentication.Audience = "audience"; + Configuration.TokenAuthentication.Issuer = "issuer"; + Configuration.TokenAuthentication.Subject = "subject"; + Configuration.TokenAuthentication.EncriptionAlgorithm = "HS512"; + Configuration.TokenAuthentication.AccessLifeTimeInMinutes = 5; + Configuration.TokenAuthentication.RefreshLifeTimeInMinutes = 1440; + Configuration.TokenAuthentication.ClockSkewInMinutes = 5; + Configuration.TokenAuthentication.SymmetricKeySecret = "very secrety secret"; + var application = new HttpApplication(); + InitDependecies(mockContext, mockRequest, mockResponse); + + new TokenAuthentication().Authenticate(application, false, false); + + Assert.NotNull(mockResponse.Object.Cookies["as"]); + Assert.NotNull(mockResponse.Object.Cookies["rs"]); + Assert.NotNull(mockResponse.Object.Cookies["ahp"]); + Assert.True(mockResponse.Object.Cookies["as"].Expires < DateTime.Today); + Assert.True(mockResponse.Object.Cookies["rs"].Expires < DateTime.Today); + Assert.True(mockResponse.Object.Cookies["ahp"].Expires < DateTime.Today); + Assert.Equal(200, responseStatus); + } + + [Fact] + public void TokenLogoutUrlTest() + { + var mockRequest = new Mock(); + mockRequest.SetupGet(o => o.Url).Returns(new Uri("https://sensenet.com/sn-token/logout")); + mockRequest.SetupGet(o => o.IsSecureConnection).Returns(true); + var headers = new NameValueCollection(); + mockRequest.SetupGet(o => o.Headers).Returns(headers); + var requestCookies = new HttpCookieCollection(); + requestCookies.Add(new HttpCookie("ahp") + { + Value = "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJpc3N1ZXIiLCJzdWIiOiJzdWJqZWN0IiwiYXVkIjoiYXVkaWVuY2UiLCJleHAiOjIxMzQyMDQzMTgsImlhdCI6MTUwMzQ4NDMxOCwibmJmIjoxNTAzNDg0MzE4LCJuYW1lIjoidXNlcm5hbWUifQ" + ,HttpOnly = true + ,Secure = true + ,Expires = new DateTime(2047, 8, 23) + }); + requestCookies.Add(new HttpCookie("as") + { + Value = "4UN3ajbm74CKeTAk3VpiJR2f0VAiKydjZg0BWwpbBcWZM5uqVJ_YbazPboItaAliH5eepgvMfNbwwk9W8UIhEA" + ,HttpOnly = true + ,Secure = true + ,Expires = new DateTime(2047, 8, 23) + }); + requestCookies.Add(new HttpCookie("rs") + { + Value = "lL54nsEOfzZVD6vBCwMB4AxO1fwRgXadNBh9XtduaIygv_HJARlOXBISpC-sxG_96RSQ_fbnU-PcxWvj7w67Rg" + ,HttpOnly = true + ,Secure = true + ,Expires = new DateTime(2047, 8, 24) + }); + mockRequest.SetupGet(o => o.Cookies).Returns(requestCookies); + var mockResponse = new Mock(); + var cookies = new HttpCookieCollection(); + string body = ""; + int responseStatus = 0; + mockResponse.SetupGet(o => o.Cookies).Returns(cookies); + mockResponse.Setup(o => o.Write(It.IsAny())).Callback((string t) => { body = t; }); + mockResponse.SetupSet(o => o.StatusCode = It.IsAny()).Callback((int s) => { responseStatus = s; }); + var principal = new ClaimsPrincipal(); + var claims = new List(); + claims.Add(new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", "username")); + principal.AddIdentity(new ClaimsIdentity(claims)); + + var mockContext = new Mock(); + mockContext.SetupGet(o => o.Request).Returns(mockRequest.Object); + mockContext.SetupGet(o => o.Response).Returns(mockResponse.Object); + mockContext.SetupGet(o => o.User).Returns(principal); + Configuration.TokenAuthentication.Audience = "audience"; + Configuration.TokenAuthentication.Issuer = "issuer"; + Configuration.TokenAuthentication.Subject = "subject"; + Configuration.TokenAuthentication.EncriptionAlgorithm = "HS512"; + Configuration.TokenAuthentication.AccessLifeTimeInMinutes = 5; + Configuration.TokenAuthentication.RefreshLifeTimeInMinutes = 1440; + Configuration.TokenAuthentication.ClockSkewInMinutes = 5; + Configuration.TokenAuthentication.SymmetricKeySecret = "very secrety secret"; + var application = new HttpApplication(); + InitDependecies( mockContext, mockRequest, mockResponse); + + new TokenAuthentication().Authenticate(application, false, false); + + Assert.NotNull(mockResponse.Object.Cookies["as"]); + Assert.NotNull(mockResponse.Object.Cookies["rs"]); + Assert.NotNull(mockResponse.Object.Cookies["ahp"]); + Assert.True(mockResponse.Object.Cookies["as"].Expires < DateTime.Today); + Assert.True(mockResponse.Object.Cookies["rs"].Expires < DateTime.Today); + Assert.True(mockResponse.Object.Cookies["ahp"].Expires < DateTime.Today); + Assert.Equal(200, responseStatus); + } + + [Fact] + public void TokenLogoutWithExpiredTokenTest() + { + var mockRequest = new Mock(); + mockRequest.SetupGet(o => o.Url).Returns(new Uri("https://sensenet.com/sn-token/logout")); + mockRequest.SetupGet(o => o.IsSecureConnection).Returns(true); + var headers = new NameValueCollection(); + mockRequest.SetupGet(o => o.Headers).Returns(headers); + var requestCookies = new HttpCookieCollection(); + requestCookies.Add(new HttpCookie("ahp") + { + Value = "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJpc3N1ZXIiLCJzdWIiOiJzdWJqZWN0IiwiYXVkIjoiYXVkaWVuY2UiLCJleHAiOjE0ODMyMjkxMDAsImlhdCI6MTQ4MzIyODgwMCwibmJmIjoxNDgzMjI4ODAwLCJuYW1lIjoidXNlcm5hbWUifQ" + ,HttpOnly = true + ,Secure = true + ,Expires = new DateTime(2017, 1, 1) + }); + requestCookies.Add(new HttpCookie("as") + { + Value = "egnUVGaLt_kfj_i12z7_sOSXMNByuz7p2OEEULUanpcJk4ySYebnuNc1v7XRT2ZYALc0FQEwgtrAN_uQ4779bg" + ,HttpOnly = true + ,Secure = true + ,Expires = new DateTime(2017, 1, 1) + }); + requestCookies.Add(new HttpCookie("rs") + { + Value = "tpOjo49W0S0Dlt89-8AmYWiV2D4cBT9A5wdh8Rt0s37ZKbPt1aSN-oCywjpKlJ3nLC-Zqnd11IJ-B4dtDOsU-g" + ,HttpOnly = true + ,Secure = true + ,Expires = new DateTime(2017, 1, 1) + }); + mockRequest.SetupGet(o => o.Cookies).Returns(requestCookies); + var mockResponse = new Mock(); + var cookies = new HttpCookieCollection(); + string body = ""; + int responseStatus = 0; + mockResponse.SetupGet(o => o.Cookies).Returns(cookies); + mockResponse.Setup(o => o.Write(It.IsAny())).Callback((string t) => { body = t; }); + mockResponse.SetupSet(o => o.StatusCode = It.IsAny()).Callback((int s) => { responseStatus = s; }); + var principal = new ClaimsPrincipal(); + var claims = new List(); + claims.Add(new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", "username")); + principal.AddIdentity(new ClaimsIdentity(claims)); + + var mockContext = new Mock(); + mockContext.SetupGet(o => o.Request).Returns(mockRequest.Object); + mockContext.SetupGet(o => o.Response).Returns(mockResponse.Object); + mockContext.SetupGet(o => o.User).Returns(principal); + Configuration.TokenAuthentication.Audience = "audience"; + Configuration.TokenAuthentication.Issuer = "issuer"; + Configuration.TokenAuthentication.Subject = "subject"; + Configuration.TokenAuthentication.EncriptionAlgorithm = "HS512"; + Configuration.TokenAuthentication.AccessLifeTimeInMinutes = 5; + Configuration.TokenAuthentication.RefreshLifeTimeInMinutes = 1440; + Configuration.TokenAuthentication.ClockSkewInMinutes = 5; + Configuration.TokenAuthentication.SymmetricKeySecret = "very secrety secret"; + var application = new HttpApplication(); + InitDependecies(mockContext, mockRequest, mockResponse); + + new TokenAuthentication().Authenticate(application, false, false); + + Assert.NotNull(mockResponse.Object.Cookies["as"]); + Assert.NotNull(mockResponse.Object.Cookies["rs"]); + Assert.NotNull(mockResponse.Object.Cookies["ahp"]); + Assert.True(mockResponse.Object.Cookies["as"].Expires < DateTime.Today); + Assert.True(mockResponse.Object.Cookies["rs"].Expires < DateTime.Today); + Assert.True(mockResponse.Object.Cookies["ahp"].Expires < DateTime.Today); + Assert.Equal(200, responseStatus); + } + + [Fact] + public void TokenLogoutWithInvalidTokenTest() + { + var mockRequest = new Mock(); + mockRequest.SetupGet(o => o.Url).Returns(new Uri("https://sensenet.com/sn-token/logout")); + mockRequest.SetupGet(o => o.IsSecureConnection).Returns(true); + var headers = new NameValueCollection(); + mockRequest.SetupGet(o => o.Headers).Returns(headers); + var requestCookies = new HttpCookieCollection(); + requestCookies.Add(new HttpCookie("ahp") + { + Value = "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJpc3N1ZXIiLCJzdWIiOiJzdWJqZWN0IiwiYXVkIjoiYXVkaWVuY2UiLCJleHAiOjIxMzQyMDQzMTgsImlhdCI6MTUwMzQ4NDMxOCwibmJmIjoxNTAzNDg0MzE4LCJuYW1lIjoidXNlcm5hbWUifQ" + ,HttpOnly = true + ,Secure = true + ,Expires = new DateTime(2047, 8, 23) + }); + requestCookies.Add(new HttpCookie("as") + { + Value = "4UN3ajbm74CKeTAk3VpiJR2f0VAiKydjZg0BWwpbBcWZM5uqVJ_YbazPboItaAliH5eepgvMfNbwwk9W8UIhFB" + ,HttpOnly = true + ,Secure = true + ,Expires = new DateTime(2047, 8, 23) + }); + requestCookies.Add(new HttpCookie("rs") + { + Value = "lL54nsEOfzZVD6vBCwMB4AxO1fwRgXadNBh9XtduaIygv_HJARlOXBISpC-sxG_96RSQ_fbnU-PcxWvj7w67Rg" + ,HttpOnly = true + ,Secure = true + ,Expires = new DateTime(2047, 8, 24) + }); + mockRequest.SetupGet(o => o.Cookies).Returns(requestCookies); + var mockResponse = new Mock(); + var cookies = new HttpCookieCollection(); + string body = ""; + int responseStatus = 0; + mockResponse.SetupGet(o => o.Cookies).Returns(cookies); + mockResponse.Setup(o => o.Write(It.IsAny())).Callback((string t) => { body = t; }); + mockResponse.SetupSet(o => o.StatusCode = It.IsAny()).Callback((int s) => { responseStatus = s; }); + var principal = new ClaimsPrincipal(); + var claims = new List(); + claims.Add(new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", "username")); + principal.AddIdentity(new ClaimsIdentity(claims)); + + var mockContext = new Mock(); + mockContext.SetupGet(o => o.Request).Returns(mockRequest.Object); + mockContext.SetupGet(o => o.Response).Returns(mockResponse.Object); + mockContext.SetupGet(o => o.User).Returns(principal); + Configuration.TokenAuthentication.Audience = "audience"; + Configuration.TokenAuthentication.Issuer = "issuer"; + Configuration.TokenAuthentication.Subject = "subject"; + Configuration.TokenAuthentication.EncriptionAlgorithm = "HS512"; + Configuration.TokenAuthentication.AccessLifeTimeInMinutes = 5; + Configuration.TokenAuthentication.RefreshLifeTimeInMinutes = 1440; + Configuration.TokenAuthentication.ClockSkewInMinutes = 5; + Configuration.TokenAuthentication.SymmetricKeySecret = "very secrety secret"; + var application = new HttpApplication(); + InitDependecies(mockContext, mockRequest, mockResponse); + + new TokenAuthentication().Authenticate(application, false, false); + + Assert.Null(mockResponse.Object.Cookies["as"]); + Assert.Null(mockResponse.Object.Cookies["rs"]); + Assert.Null(mockResponse.Object.Cookies["ahp"]); + Assert.Equal(401, responseStatus); + } + + + [Fact] + public void TokenAccessTest() + { + var mockRequest = new Mock(); + mockRequest.SetupGet(o => o.Url).Returns(new Uri("https://sensenet.com")); + mockRequest.SetupGet(o => o.IsSecureConnection).Returns(true); + var headers = new NameValueCollection(); + headers.Add("X-Authentication-Action", "TokenAccess"); + headers.Add("X-Access-Data", "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJpc3N1ZXIiLCJzdWIiOiJzdWJqZWN0IiwiYXVkIjoiYXVkaWVuY2UiLCJleHAiOjIxMzQyMDQzMTgsImlhdCI6MTUwMzQ4NDMxOCwibmJmIjoxNTAzNDg0MzE4LCJuYW1lIjoidXNlcm5hbWUifQ"); + mockRequest.SetupGet(o => o.Headers).Returns(headers); + var requestCookies = new HttpCookieCollection(); + requestCookies.Add(new HttpCookie("ahp") + { + Value = "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJpc3N1ZXIiLCJzdWIiOiJzdWJqZWN0IiwiYXVkIjoiYXVkaWVuY2UiLCJleHAiOjIxMzQyMDQzMTgsImlhdCI6MTUwMzQ4NDMxOCwibmJmIjoxNTAzNDg0MzE4LCJuYW1lIjoidXNlcm5hbWUifQ" + ,HttpOnly = true + ,Secure = true + ,Expires = new DateTime(2047, 8, 23) + }); + requestCookies.Add(new HttpCookie("as") + { + Value = "4UN3ajbm74CKeTAk3VpiJR2f0VAiKydjZg0BWwpbBcWZM5uqVJ_YbazPboItaAliH5eepgvMfNbwwk9W8UIhEA" + ,HttpOnly = true + ,Secure = true + ,Expires = new DateTime(2047, 8, 23) + }); + requestCookies.Add(new HttpCookie("rs") + { + Value = "lL54nsEOfzZVD6vBCwMB4AxO1fwRgXadNBh9XtduaIygv_HJARlOXBISpC-sxG_96RSQ_fbnU-PcxWvj7w67Rg" + ,HttpOnly = true + ,Secure = true + ,Expires = new DateTime(2047, 8, 24) + }); + mockRequest.SetupGet(o => o.Cookies).Returns(requestCookies); + var mockResponse = new Mock(); + var cookies = new HttpCookieCollection(); + string body = ""; + int responseStatus = 0; + mockResponse.SetupGet(o => o.Cookies).Returns(cookies); + mockResponse.Setup(o => o.Write(It.IsAny())).Callback((string t) => { body = t; }); + mockResponse.SetupSet(o => o.StatusCode = It.IsAny()).Callback((int s) => { responseStatus = s; }); + var principal = new ClaimsPrincipal(); + var claims = new List(); + claims.Add(new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", "username")); + principal.AddIdentity(new ClaimsIdentity(claims)); + + var mockContext = new Mock(); + mockContext.SetupGet(o => o.Request).Returns(mockRequest.Object); + mockContext.SetupGet(o => o.Response).Returns(mockResponse.Object); + mockContext.SetupGet(o => o.User).Returns(principal); + Configuration.TokenAuthentication.Audience = "audience"; + Configuration.TokenAuthentication.Issuer = "issuer"; + Configuration.TokenAuthentication.Subject = "subject"; + Configuration.TokenAuthentication.EncriptionAlgorithm = "HS512"; + Configuration.TokenAuthentication.AccessLifeTimeInMinutes = 5; + Configuration.TokenAuthentication.RefreshLifeTimeInMinutes = 1440; + Configuration.TokenAuthentication.ClockSkewInMinutes = 5; + Configuration.TokenAuthentication.SymmetricKeySecret = "very secrety secret"; + var application = new HttpApplication(); + InitDependecies(mockContext, mockRequest, mockResponse); + + new TokenAuthentication().Authenticate(application, false, false); + + Assert.Equal("username", mockContext.Object.User.Identity.Name); + Assert.Null(mockResponse.Object.Cookies["as"]); + Assert.Null(mockResponse.Object.Cookies["rs"]); + Assert.Null(mockResponse.Object.Cookies["ahp"]); + Assert.Equal(0, responseStatus); + } + + [Fact] + public void TokenAccessUrlTest() + { + var mockRequest = new Mock(); + mockRequest.SetupGet(o => o.Url).Returns(new Uri("https://sensenet.com/odata.svc/content(1)")); + mockRequest.SetupGet(o => o.IsSecureConnection).Returns(true); + var headers = new NameValueCollection(); + mockRequest.SetupGet(o => o.Headers).Returns(headers); + var requestCookies = new HttpCookieCollection(); + requestCookies.Add(new HttpCookie("ahp") + { + Value = "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJpc3N1ZXIiLCJzdWIiOiJzdWJqZWN0IiwiYXVkIjoiYXVkaWVuY2UiLCJleHAiOjIxMzQyMDQzMTgsImlhdCI6MTUwMzQ4NDMxOCwibmJmIjoxNTAzNDg0MzE4LCJuYW1lIjoidXNlcm5hbWUifQ" + ,HttpOnly = true + ,Secure = true + ,Expires = new DateTime(2047, 8, 23) + }); + requestCookies.Add(new HttpCookie("as") + { + Value = "4UN3ajbm74CKeTAk3VpiJR2f0VAiKydjZg0BWwpbBcWZM5uqVJ_YbazPboItaAliH5eepgvMfNbwwk9W8UIhEA" + ,HttpOnly = true + ,Secure = true + ,Expires = new DateTime(2047, 8, 23) + }); + requestCookies.Add(new HttpCookie("rs") + { + Value = "lL54nsEOfzZVD6vBCwMB4AxO1fwRgXadNBh9XtduaIygv_HJARlOXBISpC-sxG_96RSQ_fbnU-PcxWvj7w67Rg" + ,HttpOnly = true + ,Secure = true + ,Expires = new DateTime(2047, 8, 24) + }); + mockRequest.SetupGet(o => o.Cookies).Returns(requestCookies); + var mockResponse = new Mock(); + var cookies = new HttpCookieCollection(); + string body = ""; + int responseStatus = 0; + mockResponse.SetupGet(o => o.Cookies).Returns(cookies); + mockResponse.Setup(o => o.Write(It.IsAny())).Callback((string t) => { body = t; }); + mockResponse.SetupSet(o => o.StatusCode = It.IsAny()).Callback((int s) => { responseStatus = s; }); + + var mockContext = new Mock(); + mockContext.SetupGet(o => o.Request).Returns(mockRequest.Object); + mockContext.SetupGet(o => o.Response).Returns(mockResponse.Object); + IPrincipal user = null; + mockContext.SetupSet(o => o.User = It.IsAny()).Callback((IPrincipal p) => user = p); + Configuration.TokenAuthentication.Audience = "audience"; + Configuration.TokenAuthentication.Issuer = "issuer"; + Configuration.TokenAuthentication.Subject = "subject"; + Configuration.TokenAuthentication.EncriptionAlgorithm = "HS512"; + Configuration.TokenAuthentication.AccessLifeTimeInMinutes = 5; + Configuration.TokenAuthentication.RefreshLifeTimeInMinutes = 1440; + Configuration.TokenAuthentication.ClockSkewInMinutes = 5; + Configuration.TokenAuthentication.SymmetricKeySecret = "very secrety secret"; + var application = new HttpApplication(); + InitDependecies(mockContext, mockRequest, mockResponse); + + new TokenAuthentication().Authenticate(application, false, false); + + Assert.Equal("username", user.Identity.Name); + Assert.Null(mockResponse.Object.Cookies["as"]); + Assert.Null(mockResponse.Object.Cookies["rs"]); + Assert.Null(mockResponse.Object.Cookies["ahp"]); + Assert.Equal(0, responseStatus); + } + + + [Fact] + public void TokenAccessWithInvalidTokenTest() + { + var mockRequest = new Mock(); + mockRequest.SetupGet(o => o.Url).Returns(new Uri("https://sensenet.com/odata.svc/content(1)")); + mockRequest.SetupGet(o => o.IsSecureConnection).Returns(true); + var headers = new NameValueCollection(); + mockRequest.SetupGet(o => o.Headers).Returns(headers); + var requestCookies = new HttpCookieCollection(); + requestCookies.Add(new HttpCookie("ahp") + { + Value = "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJpc3N1ZXIiLCJzdWIiOiJzdWJqZWN0IiwiYXVkIjoiYXVkaWVuY2UiLCJleHAiOjIxMzQyMDQzMTgsImlhdCI6MTUwMzQ4NDMxOCwibmJmIjoxNTAzNDg0MzE4LCJuYW1lIjoidXNlcm5hbWUifQ" + ,HttpOnly = true + ,Secure = true + ,Expires = new DateTime(2047, 8, 23) + }); + requestCookies.Add(new HttpCookie("as") + { + Value = "4UN3ajbm74CKeTAk3VpiJR2f0VAiKydjZg0BWwpbBcWZM5uqVJ_YbazPboItaAliH5eepgvMfNbwwk9W8UIhFB" + ,HttpOnly = true + ,Secure = true + ,Expires = new DateTime(2047, 8, 23) + }); + requestCookies.Add(new HttpCookie("rs") + { + Value = "lL54nsEOfzZVD6vBCwMB4AxO1fwRgXadNBh9XtduaIygv_HJARlOXBISpC-sxG_96RSQ_fbnU-PcxWvj7w67Rg" + ,HttpOnly = true + ,Secure = true + ,Expires = new DateTime(2047, 8, 24) + }); + mockRequest.SetupGet(o => o.Cookies).Returns(requestCookies); + var mockResponse = new Mock(); + var cookies = new HttpCookieCollection(); + string body = ""; + int responseStatus = 0; + mockResponse.SetupGet(o => o.Cookies).Returns(cookies); + mockResponse.Setup(o => o.Write(It.IsAny())).Callback((string t) => { body = t; }); + mockResponse.SetupSet(o => o.StatusCode = It.IsAny()).Callback((int s) => { responseStatus = s; }); + + var mockContext = new Mock(); + mockContext.SetupGet(o => o.Request).Returns(mockRequest.Object); + mockContext.SetupGet(o => o.Response).Returns(mockResponse.Object); + IPrincipal user = null; + mockContext.SetupSet(o => o.User = It.IsAny()).Callback((IPrincipal p) => user = p); + Configuration.TokenAuthentication.Audience = "audience"; + Configuration.TokenAuthentication.Issuer = "issuer"; + Configuration.TokenAuthentication.Subject = "subject"; + Configuration.TokenAuthentication.EncriptionAlgorithm = "HS512"; + Configuration.TokenAuthentication.AccessLifeTimeInMinutes = 5; + Configuration.TokenAuthentication.RefreshLifeTimeInMinutes = 1440; + Configuration.TokenAuthentication.ClockSkewInMinutes = 5; + Configuration.TokenAuthentication.SymmetricKeySecret = "very secrety secret"; + var application = new HttpApplication(); + InitDependecies(mockContext, mockRequest, mockResponse); + + new TokenAuthentication().Authenticate(application, false, false); + + Assert.Equal("Visitor", user.Identity.Name); + Assert.Null(mockResponse.Object.Cookies["as"]); + Assert.Null(mockResponse.Object.Cookies["rs"]); + Assert.Null(mockResponse.Object.Cookies["ahp"]); + Assert.Equal(0, responseStatus); + } + + [Fact] + public void TokenLoginTest() + { + var mockRequest = new Mock(); + mockRequest.SetupGet(o => o.Url).Returns(new Uri("https://sensenet.com")); + mockRequest.SetupGet(o => o.IsSecureConnection).Returns(true); + var headers = new NameValueCollection(); + headers.Add("X-Authentication-Action", "TokenLogin"); + mockRequest.SetupGet(o => o.Headers).Returns(headers); + var mockResponse = new Mock(); + var cookies = new HttpCookieCollection(); + string body = ""; + int responseStatus = 0; + mockResponse.SetupGet(o => o.Cookies).Returns(cookies); + mockResponse.Setup(o => o.Write(It.IsAny())).Callback((string t) => { body = t; }); + mockResponse.SetupSet(o => o.StatusCode = It.IsAny()).Callback((int s) => { responseStatus = s; }); + var principal = new ClaimsPrincipal(); + var claims = new List(); + claims.Add(new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", "username")); + principal.AddIdentity(new ClaimsIdentity(claims)); + + var mockContext = new Mock(); + mockContext.SetupGet(o => o.Request).Returns(mockRequest.Object); + mockContext.SetupGet(o => o.Response).Returns(mockResponse.Object); + mockContext.SetupGet(o => o.User).Returns(principal); + Configuration.TokenAuthentication.Audience= "audience"; + Configuration.TokenAuthentication.Issuer = "issuer"; + Configuration.TokenAuthentication.Subject = "subject"; + Configuration.TokenAuthentication.EncriptionAlgorithm = "HS512"; + Configuration.TokenAuthentication.AccessLifeTimeInMinutes = 5; + Configuration.TokenAuthentication.RefreshLifeTimeInMinutes = 1440; + Configuration.TokenAuthentication.ClockSkewInMinutes = 5; + Configuration.TokenAuthentication.SymmetricKeySecret = "very secrety secret"; + var application = new HttpApplication(); + InitDependecies( mockContext, mockRequest, mockResponse); + AuthenticationHelper.GetBasicAuthHeader = () => "Basic dXNlcm5hbWU6cGFzc3dvcmQ="; + + new TokenAuthentication().Authenticate(application, true, false); + + Assert.NotNull(mockResponse.Object.Cookies["as"]); + Assert.NotNull(mockResponse.Object.Cookies["rs"]); + Assert.NotNull(mockResponse.Object.Cookies["ahp"]); + Assert.Matches("\"access\":\".+\"", body); + Assert.Matches("\"refresh\":\".+\"", body); + Assert.Equal(200, responseStatus); + } + + [Fact] + public void TokenLoginUrlTest() + { + var mockRequest = new Mock(); + mockRequest.SetupGet(o => o.Url).Returns(new Uri("https://sensenet.com/sn-token/login")); + mockRequest.SetupGet(o => o.IsSecureConnection).Returns(true); + var headers = new NameValueCollection(); + mockRequest.SetupGet(o => o.Headers).Returns(headers); + var mockResponse = new Mock(); + var cookies = new HttpCookieCollection(); + string body = ""; + int responseStatus = 0; + mockResponse.SetupGet(o => o.Cookies).Returns(cookies); + mockResponse.Setup(o => o.Write(It.IsAny())).Callback((string t) => { body = t; }); + mockResponse.SetupSet(o => o.StatusCode = It.IsAny()).Callback((int s) => { responseStatus = s; }); + var principal = new ClaimsPrincipal(); + var claims = new List(); + claims.Add(new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", "username")); + principal.AddIdentity(new ClaimsIdentity(claims)); + + var mockContext = new Mock(); + mockContext.SetupGet(o => o.Request).Returns(mockRequest.Object); + mockContext.SetupGet(o => o.Response).Returns(mockResponse.Object); + mockContext.SetupGet(o => o.User).Returns(principal); + Configuration.TokenAuthentication.Audience = "audience"; + Configuration.TokenAuthentication.Issuer = "issuer"; + Configuration.TokenAuthentication.Subject = "subject"; + Configuration.TokenAuthentication.EncriptionAlgorithm = "HS512"; + Configuration.TokenAuthentication.AccessLifeTimeInMinutes = 5; + Configuration.TokenAuthentication.RefreshLifeTimeInMinutes = 1440; + Configuration.TokenAuthentication.ClockSkewInMinutes = 5; + Configuration.TokenAuthentication.SymmetricKeySecret = "very secrety secret"; + var application = new HttpApplication(); + InitDependecies( mockContext, mockRequest, mockResponse); + AuthenticationHelper.GetBasicAuthHeader = () => "Basic dXNlcm5hbWU6cGFzc3dvcmQ="; + + new TokenAuthentication().Authenticate(application, true, false); + + Assert.NotNull(mockResponse.Object.Cookies["as"]); + Assert.NotNull(mockResponse.Object.Cookies["rs"]); + Assert.NotNull(mockResponse.Object.Cookies["ahp"]); + Assert.Matches("\"access\":\".+\"", body); + Assert.Matches("\"refresh\":\".+\"", body); + Assert.Equal(200, responseStatus); + } + + [Fact] + public void TokenLoginWithInvalidUserTest() + { + var mockRequest = new Mock(); + mockRequest.SetupGet(o => o.Url).Returns(new Uri("https://sensenet.com")); + mockRequest.SetupGet(o => o.IsSecureConnection).Returns(true); + var headers = new NameValueCollection(); + headers.Add("X-Authentication-Action", "TokenLogin"); + mockRequest.SetupGet(o => o.Headers).Returns(headers); + var mockResponse = new Mock(); + var cookies = new HttpCookieCollection(); + string body = ""; + int responseStatus = 0; + mockResponse.SetupGet(o => o.Cookies).Returns(cookies); + mockResponse.Setup(o => o.Write(It.IsAny())).Callback((string t) => { body = t; }); + mockResponse.SetupSet(o => o.StatusCode = It.IsAny()).Callback((int s) => { responseStatus = s; }); + var principal = new ClaimsPrincipal(); + var claims = new List(); + claims.Add(new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", "username")); + principal.AddIdentity(new ClaimsIdentity(claims)); + + var mockContext = new Mock(); + mockContext.SetupGet(o => o.Request).Returns(mockRequest.Object); + mockContext.SetupGet(o => o.Response).Returns(mockResponse.Object); + mockContext.SetupGet(o => o.User).Returns(principal); + Configuration.TokenAuthentication.Audience = "audience"; + Configuration.TokenAuthentication.Issuer = "issuer"; + Configuration.TokenAuthentication.Subject = "subject"; + Configuration.TokenAuthentication.EncriptionAlgorithm = "HS512"; + Configuration.TokenAuthentication.AccessLifeTimeInMinutes = 5; + Configuration.TokenAuthentication.RefreshLifeTimeInMinutes = 1440; + Configuration.TokenAuthentication.ClockSkewInMinutes = 5; + Configuration.TokenAuthentication.SymmetricKeySecret = "very secrety secret"; + var application = new HttpApplication(); + InitDependecies( mockContext, mockRequest, mockResponse); + AuthenticationHelper.GetBasicAuthHeader = () => "Basic dXNlcm5hbWU6cGFzc3dvcmQ="; + AuthenticationHelper.IsUserValid = (n, p) => false; + + new TokenAuthentication().Authenticate(application, true, true); + + Assert.Null(mockResponse.Object.Cookies["as"]); + Assert.Null(mockResponse.Object.Cookies["rs"]); + Assert.Null(mockResponse.Object.Cookies["ahp"]); + Assert.DoesNotContain("\"access\":", body); + Assert.DoesNotContain("\"refresh\":", body); + Assert.Equal(401, responseStatus); + } + + [Fact] + public void TokenRefreshTest() + { + var mockRequest = new Mock(); + mockRequest.SetupGet(o => o.Url).Returns(new Uri("https://sensenet.com")); + mockRequest.SetupGet(o => o.IsSecureConnection).Returns(true); + var headers = new NameValueCollection(); + headers.Add("X-Authentication-Action", "TokenRefresh"); + headers.Add("X-Refresh-Data", "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJpc3N1ZXIiLCJzdWIiOiJzdWJqZWN0IiwiYXVkIjoiYXVkaWVuY2UiLCJleHAiOjIwOTA1NzczOTcsImlhdCI6MTQ5MDU3NzA5NywibmJmIjoxNDkwNTc3Mzk3LCJuYW1lIjoiTXlOYW1lIn0"); + mockRequest.SetupGet(o => o.Headers).Returns(headers); + var inCookies = new HttpCookieCollection(); + inCookies.Add(new HttpCookie("rs", "_xdP6wc_Z4WgiIph-EC7O5Hh2f_aaGGZTidnK33ss-hdyw1ss7soTs7lVKrYQvmA4zSNRQ632Y-kR4TYyWdJiw"));; + mockRequest.SetupGet(o => o.Cookies).Returns(inCookies); + var mockResponse = new Mock(); + var cookies = new HttpCookieCollection(); + string body = ""; + int responseStatus = 0; + mockResponse.SetupGet(o => o.Cookies).Returns(cookies); + mockResponse.Setup(o => o.Write(It.IsAny())).Callback((string t) => { body = t; }); + mockResponse.SetupSet(o => o.StatusCode = It.IsAny()).Callback((int s) => { responseStatus = s; }); + + var mockContext = new Mock(); + mockContext.SetupGet(o => o.Request).Returns(mockRequest.Object); + mockContext.SetupGet(o => o.Response).Returns(mockResponse.Object); + Configuration.TokenAuthentication.Audience = "audience"; + Configuration.TokenAuthentication.Issuer = "issuer"; + Configuration.TokenAuthentication.Subject = "subject"; + Configuration.TokenAuthentication.EncriptionAlgorithm = "HS512"; + Configuration.TokenAuthentication.AccessLifeTimeInMinutes = 5; + Configuration.TokenAuthentication.RefreshLifeTimeInMinutes = 10000000; + Configuration.TokenAuthentication.ClockSkewInMinutes = 1; + Configuration.TokenAuthentication.SymmetricKeySecret = "very secrety secret"; + var application = new HttpApplication(); + InitDependecies( mockContext, mockRequest, mockResponse); + + new TokenAuthentication().Authenticate(application, false, false); + + Assert.NotNull(mockResponse.Object.Cookies["as"]); + Assert.NotNull(mockResponse.Object.Cookies["ahp"]); + Assert.Null(mockResponse.Object.Cookies["rs"]); + Assert.Matches("\"access\":\".+\"", body); + Assert.DoesNotContain("\"refresh\":", body); + Assert.Equal(200, responseStatus); + } + + [Fact] + public void TokenRefreshUrlTest() + { + var mockRequest = new Mock(); + mockRequest.SetupGet(o => o.Url).Returns(new Uri("https://sensenet.com/sn-token/refresh")); + mockRequest.SetupGet(o => o.IsSecureConnection).Returns(true); + var headers = new NameValueCollection(); + headers.Add("X-Refresh-Data", "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJpc3N1ZXIiLCJzdWIiOiJzdWJqZWN0IiwiYXVkIjoiYXVkaWVuY2UiLCJleHAiOjIwOTA1NzczOTcsImlhdCI6MTQ5MDU3NzA5NywibmJmIjoxNDkwNTc3Mzk3LCJuYW1lIjoiTXlOYW1lIn0"); + mockRequest.SetupGet(o => o.Headers).Returns(headers); + var inCookies = new HttpCookieCollection(); + inCookies.Add(new HttpCookie("rs", "_xdP6wc_Z4WgiIph-EC7O5Hh2f_aaGGZTidnK33ss-hdyw1ss7soTs7lVKrYQvmA4zSNRQ632Y-kR4TYyWdJiw")); ; + mockRequest.SetupGet(o => o.Cookies).Returns(inCookies); + var mockResponse = new Mock(); + var cookies = new HttpCookieCollection(); + string body = ""; + int responseStatus = 0; + mockResponse.SetupGet(o => o.Cookies).Returns(cookies); + mockResponse.Setup(o => o.Write(It.IsAny())).Callback((string t) => { body = t; }); + mockResponse.SetupSet(o => o.StatusCode = It.IsAny()).Callback((int s) => { responseStatus = s; }); + + var mockContext = new Mock(); + mockContext.SetupGet(o => o.Request).Returns(mockRequest.Object); + mockContext.SetupGet(o => o.Response).Returns(mockResponse.Object); + Configuration.TokenAuthentication.Audience = "audience"; + Configuration.TokenAuthentication.Issuer = "issuer"; + Configuration.TokenAuthentication.Subject = "subject"; + Configuration.TokenAuthentication.EncriptionAlgorithm = "HS512"; + Configuration.TokenAuthentication.AccessLifeTimeInMinutes = 5; + Configuration.TokenAuthentication.RefreshLifeTimeInMinutes = 10000000; + Configuration.TokenAuthentication.ClockSkewInMinutes = 1; + Configuration.TokenAuthentication.SymmetricKeySecret = "very secrety secret"; + var application = new HttpApplication(); + InitDependecies( mockContext, mockRequest, mockResponse); + + new TokenAuthentication().Authenticate(application, false, false); + + Assert.NotNull(mockResponse.Object.Cookies["as"]); + Assert.NotNull(mockResponse.Object.Cookies["ahp"]); + Assert.Null(mockResponse.Object.Cookies["rs"]); + Assert.Matches("\"access\":\".+\"", body); + Assert.DoesNotContain("\"refresh\":", body); + Assert.Equal(200, responseStatus); + } + + [Fact] + public void TokenRefreshWithInvalidTokenTest() + { + var mockRequest = new Mock(); + mockRequest.SetupGet(o => o.Url).Returns(new Uri("https://sensenet.com")); + mockRequest.SetupGet(o => o.IsSecureConnection).Returns(true); + var headers = new NameValueCollection(); + headers.Add("X-Authentication-Action", "TokenRefresh"); + headers.Add("X-Refresh-Data", + "yJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJpc3N1ZXIiLCJzdWIiOiJzdWJqZWN0IiwiYXVkIjoiYXVkaWVuY2UiLCJleHAiOjIwOTA1NzczOTcsImlhdCI6MTQ5MDU3NzA5NywibmJmIjoxNDkwNTc3Mzk3LCJuYW1lIjoiTXlOYW1lIn0"); + mockRequest.SetupGet(o => o.Headers).Returns(headers); + var inCookies = new HttpCookieCollection(); + inCookies.Add(new HttpCookie("rs", + "_xdP6wc_Z4WgiIph-EC7O5Hh2f_aaGGZTidnK33ss-hdyw1ss7soTs7lVKrYQvmA4zSNRQ632Y-kR4TYyWdJiw")); + ; + mockRequest.SetupGet(o => o.Cookies).Returns(inCookies); + var mockResponse = new Mock(); + var cookies = new HttpCookieCollection(); + string body = ""; + int responseStatus = 0; + mockResponse.SetupGet(o => o.Cookies).Returns(cookies); + mockResponse.Setup(o => o.Write(It.IsAny())).Callback((string t) => { body = t; }); + mockResponse.SetupSet(o => o.StatusCode = It.IsAny()).Callback((int s) => {responseStatus = s;}); + + var mockContext = new Mock(); + mockContext.SetupGet(o => o.Request).Returns(mockRequest.Object); + mockContext.SetupGet(o => o.Response).Returns(mockResponse.Object); + Configuration.TokenAuthentication.Audience = "audience"; + Configuration.TokenAuthentication.Issuer = "issuer"; + Configuration.TokenAuthentication.Subject = "subject"; + Configuration.TokenAuthentication.EncriptionAlgorithm = "HS512"; + Configuration.TokenAuthentication.AccessLifeTimeInMinutes = 5; + Configuration.TokenAuthentication.RefreshLifeTimeInMinutes = 10000000; + Configuration.TokenAuthentication.ClockSkewInMinutes = 1; + Configuration.TokenAuthentication.SymmetricKeySecret = "very secrety secret"; + var application = new HttpApplication(); + InitDependecies( mockContext, mockRequest, mockResponse); + + new TokenAuthentication().Authenticate(application, false, false); + + Assert.Null(mockResponse.Object.Cookies["as"]); + Assert.Null(mockResponse.Object.Cookies["ahp"]); + Assert.Null(mockResponse.Object.Cookies["rs"]); + Assert.DoesNotContain("\"access\":", body); + Assert.DoesNotContain("\"refresh\":", body); + Assert.Equal(401, responseStatus); + } + + + } + +} \ No newline at end of file diff --git a/src/Tests/SenseNet.Services.Tests/packages.config b/src/Tests/SenseNet.Services.Tests/packages.config index 05596890c..4b95bf931 100644 --- a/src/Tests/SenseNet.Services.Tests/packages.config +++ b/src/Tests/SenseNet.Services.Tests/packages.config @@ -2,6 +2,8 @@ + + diff --git a/src/Tests/SenseNet.TokenAuthentication.Tests/Properties/AssemblyInfo.cs b/src/Tests/SenseNet.TokenAuthentication.Tests/Properties/AssemblyInfo.cs index a4c6ec37d..9155ce587 100644 --- a/src/Tests/SenseNet.TokenAuthentication.Tests/Properties/AssemblyInfo.cs +++ b/src/Tests/SenseNet.TokenAuthentication.Tests/Properties/AssemblyInfo.cs @@ -17,4 +17,4 @@ [assembly: AssemblyVersion("7.0.0.0")] [assembly: AssemblyFileVersion("7.0.0.0")] -[assembly: AssemblyInformationalVersion("7.0.0-beta4")] +[assembly: AssemblyInformationalVersion("7.0.0")] diff --git a/src/Tests/SnAdminRuntime.Tests/Properties/AssemblyInfo.cs b/src/Tests/SnAdminRuntime.Tests/Properties/AssemblyInfo.cs index b8829887b..b105c12d0 100644 --- a/src/Tests/SnAdminRuntime.Tests/Properties/AssemblyInfo.cs +++ b/src/Tests/SnAdminRuntime.Tests/Properties/AssemblyInfo.cs @@ -17,4 +17,4 @@ [assembly: AssemblyVersion("7.0.0.0")] [assembly: AssemblyFileVersion("7.0.0.0")] -[assembly: AssemblyInformationalVersion("7.0.0-beta4")] +[assembly: AssemblyInformationalVersion("7.0.0")] diff --git a/src/TokenAuthentication/CookieHelper.cs b/src/TokenAuthentication/CookieHelper.cs index 936ec574b..09339b02a 100644 --- a/src/TokenAuthentication/CookieHelper.cs +++ b/src/TokenAuthentication/CookieHelper.cs @@ -17,9 +17,18 @@ public static void InsertSecureCookie(HttpResponseBase response, string token, s response.Cookies.Add(authCookie); } + public static void DeleteCookie(HttpResponseBase response, string cookieName) + { + var sessionCookie = new HttpCookie(cookieName, string.Empty) + { + Expires = DateTime.UtcNow.AddDays(-1) + }; + response.Cookies.Add(sessionCookie); + } + public static HttpCookie GetCookie(HttpRequestBase request, string cookieName) { - return request.Cookies[cookieName]; + return request.Cookies?[cookieName]; } } } \ No newline at end of file diff --git a/src/TokenAuthentication/Properties/AssemblyInfo.cs b/src/TokenAuthentication/Properties/AssemblyInfo.cs index 231447d96..44381930a 100644 --- a/src/TokenAuthentication/Properties/AssemblyInfo.cs +++ b/src/TokenAuthentication/Properties/AssemblyInfo.cs @@ -15,4 +15,4 @@ [assembly: AssemblyCulture("")] [assembly: AssemblyVersion("7.0.0.0")] [assembly: AssemblyFileVersion("7.0.0.0")] -[assembly: AssemblyInformationalVersion("7.0.0-beta4")] +[assembly: AssemblyInformationalVersion("7.0.0")] diff --git a/src/Tools/SnAdminRuntime/App.config b/src/Tools/SnAdminRuntime/App.config index 27ffc7f48..0bd5bf3af 100644 --- a/src/Tools/SnAdminRuntime/App.config +++ b/src/Tools/SnAdminRuntime/App.config @@ -94,7 +94,7 @@ - + diff --git a/src/Tools/SnAdminRuntime/Properties/AssemblyInfo.cs b/src/Tools/SnAdminRuntime/Properties/AssemblyInfo.cs index 928ca9dc9..9da2d4c07 100644 --- a/src/Tools/SnAdminRuntime/Properties/AssemblyInfo.cs +++ b/src/Tools/SnAdminRuntime/Properties/AssemblyInfo.cs @@ -16,7 +16,7 @@ [assembly: AssemblyCulture("")] [assembly: AssemblyVersion("7.0.0.0")] [assembly: AssemblyFileVersion("7.0.0.0")] -[assembly: AssemblyInformationalVersion("7.0.0-beta4")] +[assembly: AssemblyInformationalVersion("7.0.0")] [assembly: ComVisible(false)] [assembly: Guid("1B973251-9AAE-48D2-9FFF-408AA95CA576")] diff --git a/src/Tools/SnAdminRuntime/SnAdminRuntime.cs b/src/Tools/SnAdminRuntime/SnAdminRuntime.cs index a22b2e720..c2cef77f2 100644 --- a/src/Tools/SnAdminRuntime/SnAdminRuntime.cs +++ b/src/Tools/SnAdminRuntime/SnAdminRuntime.cs @@ -9,6 +9,7 @@ using System.Diagnostics; using Ionic.Zip; using System.Configuration; +using System.Security; using System.Xml; using SenseNet.ContentRepository.Storage; using SenseNet.Tools.SnAdmin.Testability; @@ -75,11 +76,40 @@ internal static int Main(string[] args) return -1; Logger.PackageName = Path.GetFileName(packagePath); - - Logger.Create(logLevel, logFilePath); - Debug.WriteLine("##> " + Logger.Level); - - return ExecutePhase(packagePath, targetDirectory, phase, parameters, logFilePath, help, schema); + try + { + Logger.Create(logLevel, logFilePath); + Debug.WriteLine("##> " + Logger.Level); + return ExecutePhase(packagePath, targetDirectory, phase, parameters, logFilePath, help, schema); + } + catch (ReflectionTypeLoadException ex) + { + List types = new List(); + if (ex.LoaderExceptions != null) + { + foreach (var item in ex.LoaderExceptions) + { + if (item is FileLoadException flo) + { + types.Add(flo.FileName); + } + if (item is FileNotFoundException f) + { + types.Add(f.FileName); + } + if (item is BadImageFormatException b) + { + types.Add(b.FileName); + } + if (item is SecurityException s) + { + types.Add(s.Url); + } + } + } + throw new Exception( + $"ReflectionTypeLoadException: Could not load types. Affected types: {Environment.NewLine + string.Join(";" + Environment.NewLine, types) + ";" + Environment.NewLine}"); + } } internal static bool ParseParameters(string[] args, out string packagePath, out string targetDirectory, diff --git a/src/nuget/content/Admin/tools/seturl/manifest.xml b/src/nuget/content/Admin/tools/seturl/manifest.xml new file mode 100644 index 000000000..f39c8de93 --- /dev/null +++ b/src/nuget/content/Admin/tools/seturl/manifest.xml @@ -0,0 +1,19 @@ + + SenseNet.SetUrl + 2017-04-05 + 7.0.0 + + + + + Default_Site + + Forms + + + + + @url + + + \ No newline at end of file diff --git a/src/nuget/snadmin/install-services/import/(apps)/ContentList/DeleteField.Content b/src/nuget/snadmin/install-services/import/(apps)/ContentList/DeleteField.Content index 2b7da1ee7..a56339aef 100644 --- a/src/nuget/snadmin/install-services/import/(apps)/ContentList/DeleteField.Content +++ b/src/nuget/snadmin/install-services/import/(apps)/ContentList/DeleteField.Content @@ -10,5 +10,8 @@ + + Allow + \ No newline at end of file diff --git a/src/nuget/snadmin/install-services/import/(apps)/ContentList/EditField.Content b/src/nuget/snadmin/install-services/import/(apps)/ContentList/EditField.Content index 2400665b5..e65721bcd 100644 --- a/src/nuget/snadmin/install-services/import/(apps)/ContentList/EditField.Content +++ b/src/nuget/snadmin/install-services/import/(apps)/ContentList/EditField.Content @@ -10,5 +10,8 @@ + + Allow + \ No newline at end of file diff --git a/src/nuget/snadmin/install-services/import/(apps)/File/CheckPreviews.Content b/src/nuget/snadmin/install-services/import/(apps)/File/CheckPreviews.Content index fadfdccec..525b1eebf 100644 --- a/src/nuget/snadmin/install-services/import/(apps)/File/CheckPreviews.Content +++ b/src/nuget/snadmin/install-services/import/(apps)/File/CheckPreviews.Content @@ -12,5 +12,8 @@ + + Allow + \ No newline at end of file diff --git a/src/nuget/snadmin/install-services/import/(apps)/File/EditInMicrosoftOffice.Content b/src/nuget/snadmin/install-services/import/(apps)/File/EditInMicrosoftOffice.Content index 422d68f33..5a96bf9c4 100644 --- a/src/nuget/snadmin/install-services/import/(apps)/File/EditInMicrosoftOffice.Content +++ b/src/nuget/snadmin/install-services/import/(apps)/File/EditInMicrosoftOffice.Content @@ -11,7 +11,7 @@ - + Allow diff --git a/src/nuget/snadmin/install-services/import/(apps)/File/ExportToPdf.Content b/src/nuget/snadmin/install-services/import/(apps)/File/ExportToPdf.Content index 19bb74d61..9f7d26b73 100644 --- a/src/nuget/snadmin/install-services/import/(apps)/File/ExportToPdf.Content +++ b/src/nuget/snadmin/install-services/import/(apps)/File/ExportToPdf.Content @@ -37,5 +37,8 @@ + + Allow + \ No newline at end of file diff --git a/src/nuget/snadmin/install-services/import/(apps)/File/GetPageCount.Content b/src/nuget/snadmin/install-services/import/(apps)/File/GetPageCount.Content index 1d5fe869d..ec93410a3 100644 --- a/src/nuget/snadmin/install-services/import/(apps)/File/GetPageCount.Content +++ b/src/nuget/snadmin/install-services/import/(apps)/File/GetPageCount.Content @@ -11,7 +11,7 @@ - + Allow diff --git a/src/nuget/snadmin/install-services/import/(apps)/File/GetPreviewsFolder.Content b/src/nuget/snadmin/install-services/import/(apps)/File/GetPreviewsFolder.Content index ea1d6b3f6..2d5843ec5 100644 --- a/src/nuget/snadmin/install-services/import/(apps)/File/GetPreviewsFolder.Content +++ b/src/nuget/snadmin/install-services/import/(apps)/File/GetPreviewsFolder.Content @@ -12,5 +12,8 @@ + + Allow + \ No newline at end of file diff --git a/src/nuget/snadmin/install-services/import/(apps)/File/PreviewAvailable.Content b/src/nuget/snadmin/install-services/import/(apps)/File/PreviewAvailable.Content index 8bfb12526..f1770b7b4 100644 --- a/src/nuget/snadmin/install-services/import/(apps)/File/PreviewAvailable.Content +++ b/src/nuget/snadmin/install-services/import/(apps)/File/PreviewAvailable.Content @@ -37,7 +37,7 @@ - + Allow diff --git a/src/nuget/snadmin/install-services/import/(apps)/File/RegeneratePreviews.Content b/src/nuget/snadmin/install-services/import/(apps)/File/RegeneratePreviews.Content index 12f9e1f7d..3f9ed5c33 100644 --- a/src/nuget/snadmin/install-services/import/(apps)/File/RegeneratePreviews.Content +++ b/src/nuget/snadmin/install-services/import/(apps)/File/RegeneratePreviews.Content @@ -11,5 +11,8 @@ + + Allow + \ No newline at end of file diff --git a/src/nuget/snadmin/install-services/import/(apps)/File/SetPageCount.Content b/src/nuget/snadmin/install-services/import/(apps)/File/SetPageCount.Content index fb7af5802..88d0dfe24 100644 --- a/src/nuget/snadmin/install-services/import/(apps)/File/SetPageCount.Content +++ b/src/nuget/snadmin/install-services/import/(apps)/File/SetPageCount.Content @@ -12,5 +12,8 @@ + + Allow + \ No newline at end of file diff --git a/src/nuget/snadmin/install-services/import/(apps)/File/SetPreviewStatus.Content b/src/nuget/snadmin/install-services/import/(apps)/File/SetPreviewStatus.Content index 5f39179f0..ce9998f8c 100644 --- a/src/nuget/snadmin/install-services/import/(apps)/File/SetPreviewStatus.Content +++ b/src/nuget/snadmin/install-services/import/(apps)/File/SetPreviewStatus.Content @@ -12,5 +12,8 @@ + + Allow + \ No newline at end of file diff --git a/src/nuget/snadmin/install-services/import/(apps)/File/UploadResume.Content b/src/nuget/snadmin/install-services/import/(apps)/File/UploadResume.Content index 07c4c0832..ae01e9af1 100644 --- a/src/nuget/snadmin/install-services/import/(apps)/File/UploadResume.Content +++ b/src/nuget/snadmin/install-services/import/(apps)/File/UploadResume.Content @@ -12,7 +12,7 @@ - + Allow diff --git a/src/nuget/snadmin/install-services/import/(apps)/Folder/CopyBatch.Content b/src/nuget/snadmin/install-services/import/(apps)/Folder/CopyBatch.Content index 3161ec66a..861701a1a 100644 --- a/src/nuget/snadmin/install-services/import/(apps)/Folder/CopyBatch.Content +++ b/src/nuget/snadmin/install-services/import/(apps)/Folder/CopyBatch.Content @@ -39,7 +39,7 @@ - + Allow diff --git a/src/nuget/snadmin/install-services/import/(apps)/Folder/DeleteBatch.Content b/src/nuget/snadmin/install-services/import/(apps)/Folder/DeleteBatch.Content index 421f4e5f0..acc6a51fb 100644 --- a/src/nuget/snadmin/install-services/import/(apps)/Folder/DeleteBatch.Content +++ b/src/nuget/snadmin/install-services/import/(apps)/Folder/DeleteBatch.Content @@ -39,7 +39,7 @@ - + Allow diff --git a/src/nuget/snadmin/install-services/import/(apps)/Folder/ExportToCsv.Content b/src/nuget/snadmin/install-services/import/(apps)/Folder/ExportToCsv.Content index 17a64ca49..bc2b8168e 100644 --- a/src/nuget/snadmin/install-services/import/(apps)/Folder/ExportToCsv.Content +++ b/src/nuget/snadmin/install-services/import/(apps)/Folder/ExportToCsv.Content @@ -42,8 +42,8 @@ - - Allowed + + Allow \ No newline at end of file diff --git a/src/nuget/snadmin/install-services/import/(apps)/Folder/MoveBatch.Content b/src/nuget/snadmin/install-services/import/(apps)/Folder/MoveBatch.Content index 1e4995246..37af7601e 100644 --- a/src/nuget/snadmin/install-services/import/(apps)/Folder/MoveBatch.Content +++ b/src/nuget/snadmin/install-services/import/(apps)/Folder/MoveBatch.Content @@ -39,7 +39,7 @@ - + Allow diff --git a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/AddAllowedChildTypes.Content b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/AddAllowedChildTypes.Content index 50b340429..28a22e992 100644 --- a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/AddAllowedChildTypes.Content +++ b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/AddAllowedChildTypes.Content @@ -12,5 +12,8 @@ + + Allow + \ No newline at end of file diff --git a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/CopyTo.Content b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/CopyTo.Content index f2456b152..e52c1d74a 100644 --- a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/CopyTo.Content +++ b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/CopyTo.Content @@ -39,7 +39,7 @@ - + Allow diff --git a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/Delete.Content b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/Delete.Content index dfd5da69a..fb7bb46a2 100644 --- a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/Delete.Content +++ b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/Delete.Content @@ -39,7 +39,7 @@ - + Allow diff --git a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/FinalizeContent.Content b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/FinalizeContent.Content index a36f29f8d..83763cf52 100644 --- a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/FinalizeContent.Content +++ b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/FinalizeContent.Content @@ -36,7 +36,7 @@ - + Allow diff --git a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/ForceUndoCheckOut.Content b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/ForceUndoCheckOut.Content index 5c8d1f221..70cac1180 100644 --- a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/ForceUndoCheckOut.Content +++ b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/ForceUndoCheckOut.Content @@ -11,5 +11,8 @@ + + Allow + \ No newline at end of file diff --git a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetAllContentTypes.Content b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetAllContentTypes.Content index 335f1fedd..eadd8852c 100644 --- a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetAllContentTypes.Content +++ b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetAllContentTypes.Content @@ -45,5 +45,8 @@ + + Allow + \ No newline at end of file diff --git a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetAllowedChildTypesFromCTD.Content b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetAllowedChildTypesFromCTD.Content index 558727adc..002e3bd6e 100644 --- a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetAllowedChildTypesFromCTD.Content +++ b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetAllowedChildTypesFromCTD.Content @@ -45,5 +45,8 @@ + + Allow + \ No newline at end of file diff --git a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetAllowedUsers.Content b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetAllowedUsers.Content index e8bc6292b..ac5228ae7 100644 --- a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetAllowedUsers.Content +++ b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetAllowedUsers.Content @@ -14,4 +14,10 @@ false + + + + Allow + + diff --git a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetChildrenPermissionInfo.Content b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetChildrenPermissionInfo.Content index f0aaa032f..154d38b63 100644 --- a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetChildrenPermissionInfo.Content +++ b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetChildrenPermissionInfo.Content @@ -13,5 +13,8 @@ + + Allow + \ No newline at end of file diff --git a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetNameFromDisplayName.Content b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetNameFromDisplayName.Content index 7e7dca8c7..a6de00bcd 100644 --- a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetNameFromDisplayName.Content +++ b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetNameFromDisplayName.Content @@ -11,5 +11,8 @@ + + Allow + \ No newline at end of file diff --git a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetPermissionInfo.Content b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetPermissionInfo.Content index 1f558d396..11f6b7c8d 100644 --- a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetPermissionInfo.Content +++ b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetPermissionInfo.Content @@ -13,5 +13,8 @@ + + Allow + \ No newline at end of file diff --git a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetPermissionOverview.Content b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetPermissionOverview.Content index 9282e5cdc..01ef986a4 100644 --- a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetPermissionOverview.Content +++ b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetPermissionOverview.Content @@ -13,5 +13,8 @@ + + Allow + \ No newline at end of file diff --git a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetPermissions.Content b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetPermissions.Content index 7f0c3ca9c..187b5a46a 100644 --- a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetPermissions.Content +++ b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetPermissions.Content @@ -45,5 +45,8 @@ Allow Allow + + Allow + \ No newline at end of file diff --git a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetQueries.Content b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetQueries.Content index f12832944..ed7a2d496 100644 --- a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetQueries.Content +++ b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetQueries.Content @@ -39,7 +39,7 @@ - + Allow diff --git a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetQueryBuilderMetadata.Content b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetQueryBuilderMetadata.Content index 33bdc2520..84ff088d9 100644 --- a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetQueryBuilderMetadata.Content +++ b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetQueryBuilderMetadata.Content @@ -37,7 +37,7 @@ - + Allow diff --git a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetRelatedIdentities.Content b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetRelatedIdentities.Content index f6d2c073f..ecd394716 100644 --- a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetRelatedIdentities.Content +++ b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetRelatedIdentities.Content @@ -40,5 +40,8 @@ + + Allow + \ No newline at end of file diff --git a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetRelatedIdentitiesByPermissions.Content b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetRelatedIdentitiesByPermissions.Content index a24ebabf6..6a7ae0ac5 100644 --- a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetRelatedIdentitiesByPermissions.Content +++ b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetRelatedIdentitiesByPermissions.Content @@ -41,5 +41,8 @@ + + Allow + \ No newline at end of file diff --git a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetRelatedItems.Content b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetRelatedItems.Content index 1deb05306..db783a7f5 100644 --- a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetRelatedItems.Content +++ b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetRelatedItems.Content @@ -42,5 +42,8 @@ + + Allow + \ No newline at end of file diff --git a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetRelatedItemsOneLevel.Content b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetRelatedItemsOneLevel.Content index c94b245a7..433623db3 100644 --- a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetRelatedItemsOneLevel.Content +++ b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetRelatedItemsOneLevel.Content @@ -41,5 +41,8 @@ + + Allow + \ No newline at end of file diff --git a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetRelatedPermissions.Content b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetRelatedPermissions.Content index 5fc59f0fd..3c7d59cf9 100644 --- a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetRelatedPermissions.Content +++ b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/GetRelatedPermissions.Content @@ -42,5 +42,8 @@ + + Allow + \ No newline at end of file diff --git a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/HasPermission.Content b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/HasPermission.Content index b7ed245c3..d0bc5cd9b 100644 --- a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/HasPermission.Content +++ b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/HasPermission.Content @@ -36,5 +36,8 @@ + + Allow + \ No newline at end of file diff --git a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/MoveTo.Content b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/MoveTo.Content index 801368fc8..d6db0cd40 100644 --- a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/MoveTo.Content +++ b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/MoveTo.Content @@ -39,7 +39,7 @@ - + Allow diff --git a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/Reject.Content b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/Reject.Content index 000108efa..b6f3c03c2 100644 --- a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/Reject.Content +++ b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/Reject.Content @@ -34,5 +34,8 @@ + + Allow + \ No newline at end of file diff --git a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/RemoveFields.Content b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/RemoveFields.Content index 87aed4648..e03df854d 100644 --- a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/RemoveFields.Content +++ b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/RemoveFields.Content @@ -44,5 +44,8 @@ Allow Allow + + Allow + \ No newline at end of file diff --git a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/SaveQuery.Content b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/SaveQuery.Content index 2ade23e68..3b19dc741 100644 --- a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/SaveQuery.Content +++ b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/SaveQuery.Content @@ -41,7 +41,7 @@ - + Allow diff --git a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/SetPermissions.Content b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/SetPermissions.Content index cb15719cc..39da92dbd 100644 --- a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/SetPermissions.Content +++ b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/SetPermissions.Content @@ -11,5 +11,8 @@ + + Allow + \ No newline at end of file diff --git a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/TakeLockOver.Content b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/TakeLockOver.Content index 2639aa09f..6a6e0e9ec 100644 --- a/src/nuget/snadmin/install-services/import/(apps)/GenericContent/TakeLockOver.Content +++ b/src/nuget/snadmin/install-services/import/(apps)/GenericContent/TakeLockOver.Content @@ -39,7 +39,7 @@ - + Allow diff --git a/src/nuget/snadmin/install-services/import/(apps)/Group/AddMembers.Content b/src/nuget/snadmin/install-services/import/(apps)/Group/AddMembers.Content index b8c959f80..bd82c6d10 100644 --- a/src/nuget/snadmin/install-services/import/(apps)/Group/AddMembers.Content +++ b/src/nuget/snadmin/install-services/import/(apps)/Group/AddMembers.Content @@ -21,5 +21,8 @@ Allow Allow + + Allow + \ No newline at end of file diff --git a/src/nuget/snadmin/install-services/import/(apps)/Group/RemoveMembers.Content b/src/nuget/snadmin/install-services/import/(apps)/Group/RemoveMembers.Content index 35feca397..7a4ed9e57 100644 --- a/src/nuget/snadmin/install-services/import/(apps)/Group/RemoveMembers.Content +++ b/src/nuget/snadmin/install-services/import/(apps)/Group/RemoveMembers.Content @@ -21,5 +21,8 @@ Allow Allow + + Allow + \ No newline at end of file diff --git a/src/nuget/snadmin/install-services/import/(apps)/Link/Browse.Content b/src/nuget/snadmin/install-services/import/(apps)/Link/Browse.Content index 16a6a63f1..fe2a33a6c 100644 --- a/src/nuget/snadmin/install-services/import/(apps)/Link/Browse.Content +++ b/src/nuget/snadmin/install-services/import/(apps)/Link/Browse.Content @@ -39,7 +39,7 @@ - + Allow diff --git a/src/nuget/snadmin/install-services/import/(apps)/User/Profile.Content b/src/nuget/snadmin/install-services/import/(apps)/User/Profile.Content index fa4370e39..73382e59d 100644 --- a/src/nuget/snadmin/install-services/import/(apps)/User/Profile.Content +++ b/src/nuget/snadmin/install-services/import/(apps)/User/Profile.Content @@ -14,5 +14,8 @@ + + Allow + \ No newline at end of file diff --git a/src/nuget/snadmin/install-services/import/IMS/BuiltIn/Portal/Startup.Content b/src/nuget/snadmin/install-services/import/IMS/BuiltIn/Portal/Startup.Content new file mode 100644 index 000000000..06fc3ec2c --- /dev/null +++ b/src/nuget/snadmin/install-services/import/IMS/BuiltIn/Portal/Startup.Content @@ -0,0 +1,24 @@ + + + User + Startup + + true + Startup User + Startup + + startup + + + + + Deny + + + Deny + + + Deny + + + \ No newline at end of file diff --git a/src/nuget/snadmin/install-services/import/Localization/Exceptions.xml b/src/nuget/snadmin/install-services/import/Localization/Exceptions.xml index cc4d86483..45450ce1c 100644 --- a/src/nuget/snadmin/install-services/import/Localization/Exceptions.xml +++ b/src/nuget/snadmin/install-services/import/Localization/Exceptions.xml @@ -232,6 +232,9 @@ Content already exists: {0}. + + Content does not exist at {0}. + A content with this name already exists in the selected target folder. @@ -304,6 +307,9 @@ A tartalom már létezik: {0}. + + A tartalom nem létezik: {0}. + Ilyen nevű tartalom már létezik a kiválasztott mappában. diff --git a/src/nuget/snadmin/install-services/import/System/Settings/OAuth.settings b/src/nuget/snadmin/install-services/import/System/Settings/OAuth.settings new file mode 100644 index 000000000..eeaffb9bb --- /dev/null +++ b/src/nuget/snadmin/install-services/import/System/Settings/OAuth.settings @@ -0,0 +1,4 @@ +{ + UserType: "User", + Domain: "Public" +} \ No newline at end of file diff --git a/src/nuget/snadmin/install-services/import/System/Settings/OAuth.settings.Content b/src/nuget/snadmin/install-services/import/System/Settings/OAuth.settings.Content new file mode 100644 index 000000000..e3735429d --- /dev/null +++ b/src/nuget/snadmin/install-services/import/System/Settings/OAuth.settings.Content @@ -0,0 +1,12 @@ + + + Settings + OAuth.settings + + + true + + + + + \ No newline at end of file diff --git a/src/nuget/snadmin/install-services/manifest.xml b/src/nuget/snadmin/install-services/manifest.xml index 5e62ed626..828e32cc3 100644 --- a/src/nuget/snadmin/install-services/manifest.xml +++ b/src/nuget/snadmin/install-services/manifest.xml @@ -1,7 +1,7 @@ SenseNet.Services - sensenet ECM Services 7.0 (beta) - 2017-04-20 + sensenet ECM Services 7.0 + 2017-11-28 7.0.0 . diff --git a/tools/scripts/CreateNuGetPackages.ps1 b/tools/scripts/CreateNuGetPackages.ps1 index ffb774520..799bcd700 100644 --- a/tools/scripts/CreateNuGetPackages.ps1 +++ b/tools/scripts/CreateNuGetPackages.ps1 @@ -1,20 +1,24 @@ -nuget pack ..\..\src\Common\Common.csproj -properties Configuration=Release -nuget pack ..\..\src\BlobStorage\BlobStorage.csproj -properties Configuration=Release -nuget pack ..\..\src\Preview\Preview.csproj -properties Configuration=Release -nuget pack ..\..\src\Services\Services.nuspec -properties Configuration=Release +$srcPath = [System.IO.Path]::GetFullPath(($PSScriptRoot + '\..\..\src')) +$installPackagePath = "$srcPath\nuget\content\Admin\tools\install-services.zip" +$scriptsSourcePath = "$srcPath\Storage\Data\SqlClient\Scripts" -New-Item ..\..\src\nuget\snadmin\install-services\scripts -ItemType directory -Force +# delete existing packages +Remove-Item $PSScriptRoot\*.nupkg -Copy-Item ..\..\src\Storage\Data\SqlClient\Scripts\Create_SenseNet_Database.sql ..\..\src\nuget\snadmin\install-services\scripts -Force -Copy-Item ..\..\src\Storage\Data\SqlClient\Scripts\Create_SenseNet_Azure_Database.sql ..\..\src\nuget\snadmin\install-services\scripts -Force -Copy-Item ..\..\src\Storage\Data\SqlClient\Scripts\Install_Security.sql ..\..\src\nuget\snadmin\install-services\scripts -Force -Copy-Item ..\..\src\Storage\Data\SqlClient\Scripts\Install_01_Schema.sql ..\..\src\nuget\snadmin\install-services\scripts -Force -Copy-Item ..\..\src\Storage\Data\SqlClient\Scripts\Install_02_Procs.sql ..\..\src\nuget\snadmin\install-services\scripts -Force -Copy-Item ..\..\src\Storage\Data\SqlClient\Scripts\Install_03_Data_Phase1.sql ..\..\src\nuget\snadmin\install-services\scripts -Force -Copy-Item ..\..\src\Storage\Data\SqlClient\Scripts\Install_04_Data_Phase2.sql ..\..\src\nuget\snadmin\install-services\scripts -Force +nuget pack $srcPath\Common\Common.csproj -properties Configuration=Release -OutputDirectory $PSScriptRoot +nuget pack $srcPath\BlobStorage\BlobStorage.csproj -properties Configuration=Release -OutputDirectory $PSScriptRoot +nuget pack $srcPath\Services\Services.nuspec -properties Configuration=Release -OutputDirectory $PSScriptRoot -Compress-Archive -Path "..\..\src\nuget\snadmin\install-services\*" -Force -CompressionLevel Optimal -DestinationPath "..\..\src\nuget\content\Admin\tools\install-services.zip" -nuget pack ..\..\src\Services\Services.Install.nuspec -properties Configuration=Release +New-Item $srcPath\nuget\snadmin\install-services\scripts -ItemType directory -Force -# nuget.exe push -Source "SenseNet" -ApiKey VSTS SenseNet.Services.7.0.0-beta4.nupkg -# nuget.exe push -Source "SenseNet" -ApiKey VSTS SenseNet.Services.Install.7.0.0-beta4.nupkg +Copy-Item $scriptsSourcePath\Create_SenseNet_Database.sql $srcPath\nuget\snadmin\install-services\scripts -Force +Copy-Item $scriptsSourcePath\Create_SenseNet_Azure_Database.sql $srcPath\nuget\snadmin\install-services\scripts -Force +Copy-Item $scriptsSourcePath\Install_Security.sql $srcPath\nuget\snadmin\install-services\scripts -Force +Copy-Item $scriptsSourcePath\Install_01_Schema.sql $srcPath\nuget\snadmin\install-services\scripts -Force +Copy-Item $scriptsSourcePath\Install_02_Procs.sql $srcPath\nuget\snadmin\install-services\scripts -Force +Copy-Item $scriptsSourcePath\Install_03_Data_Phase1.sql $srcPath\nuget\snadmin\install-services\scripts -Force +Copy-Item $scriptsSourcePath\Install_04_Data_Phase2.sql $srcPath\nuget\snadmin\install-services\scripts -Force + +Compress-Archive -Path "$srcPath\nuget\snadmin\install-services\*" -Force -CompressionLevel Optimal -DestinationPath $installPackagePath + +nuget pack $srcPath\Services\Services.Install.nuspec -properties Configuration=Release -OutputDirectory $PSScriptRoot