diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 0000000000..e600602a27
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ $(MSBuildThisFileDirectory)linting\codeanalysis.tests.ruleset
+
+
+
+
+ $(MSBuildThisFileDirectory)linting\codeanalysis.ruleset
+
+
+
+
+
diff --git a/Directory.Build.targets b/Directory.Build.targets
new file mode 100644
index 0000000000..e9ffa15255
--- /dev/null
+++ b/Directory.Build.targets
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/linting/.editorconfig b/linting/.editorconfig
new file mode 100644
index 0000000000..66967e074f
--- /dev/null
+++ b/linting/.editorconfig
@@ -0,0 +1,447 @@
+# Version: 1.6.2 (Using https://semver.org/)
+# Updated: 2020-11-02
+# See https://github.com/RehanSaeed/EditorConfig/releases for release notes.
+# See https://github.com/RehanSaeed/EditorConfig for updates to this file.
+# See http://EditorConfig.org for more information about .editorconfig files.
+
+##########################################
+# Common Settings
+##########################################
+
+# This file is the top-most EditorConfig file
+root = true
+
+# All Files
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 4
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+##########################################
+# File Extension Settings
+##########################################
+
+# Visual Studio Solution Files
+[*.sln]
+indent_style = tab
+
+# Visual Studio XML Project Files
+[*.{csproj,vbproj,vcxproj.filters,proj,projitems,shproj}]
+indent_size = 2
+
+# XML Configuration Files
+[*.{xml,config,props,targets,nuspec,resx,ruleset,vsixmanifest,vsct}]
+indent_size = 2
+
+# JSON Files
+[*.{json,json5,webmanifest}]
+indent_size = 2
+
+# YAML Files
+[*.{yml,yaml}]
+indent_size = 2
+
+# Markdown Files
+[*.md]
+trim_trailing_whitespace = false
+
+# Web Files
+[*.{htm,html,js,jsm,ts,tsx,css,sass,scss,less,svg,vue}]
+indent_size = 2
+
+# Batch Files
+[*.{cmd,bat}]
+end_of_line = crlf
+
+# Bash Files
+[*.sh]
+end_of_line = lf
+
+# Makefiles
+[Makefile]
+indent_style = tab
+
+##########################################
+# File Header (Uncomment to support file headers)
+# https://docs.microsoft.com/visualstudio/ide/reference/add-file-header
+##########################################
+
+# [*.{cs,csx,cake,vb,vbx}]
+file_header_template = Copyright (c) Umbraco.\nSee LICENSE for more details.
+
+# SA1636: File header copyright text should match
+# Justification: .editorconfig supports file headers. If this is changed to a value other than "none", a stylecop.json file will need to added to the project.
+# dotnet_diagnostic.SA1636.severity = none
+
+##########################################
+# .NET Language Conventions
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions
+##########################################
+
+# .NET Code Style Settings
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#net-code-style-settings
+[*.{cs,csx,cake,vb,vbx}]
+# "this." and "Me." qualifiers
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#this-and-me
+dotnet_style_qualification_for_field = false:suggestion
+dotnet_style_qualification_for_property = false:suggestion
+dotnet_style_qualification_for_method = false:suggestion
+dotnet_style_qualification_for_event = false:suggestion
+# Language keywords instead of framework type names for type references
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#language-keywords
+dotnet_style_predefined_type_for_locals_parameters_members = true:warning
+dotnet_style_predefined_type_for_member_access = true:warning
+# Modifier preferences
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#normalize-modifiers
+dotnet_style_require_accessibility_modifiers = always:warning
+csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:warning
+visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:warning
+dotnet_style_readonly_field = true:warning
+# Parentheses preferences
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#parentheses-preferences
+dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:warning
+dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:warning
+dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:warning
+dotnet_style_parentheses_in_other_operators = never_if_unnecessary:suggestion
+# Expression-level preferences
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#expression-level-preferences
+dotnet_style_object_initializer = true:warning
+dotnet_style_collection_initializer = true:warning
+dotnet_style_explicit_tuple_names = true:warning
+dotnet_style_prefer_inferred_tuple_names = true:warning
+dotnet_style_prefer_inferred_anonymous_type_member_names = true:warning
+dotnet_style_prefer_auto_properties = true:warning
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning
+dotnet_style_prefer_conditional_expression_over_assignment = false:suggestion
+dotnet_style_prefer_conditional_expression_over_return = false:suggestion
+dotnet_style_prefer_compound_assignment = true:warning
+# Null-checking preferences
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#null-checking-preferences
+dotnet_style_coalesce_expression = true:warning
+dotnet_style_null_propagation = true:warning
+# Parameter preferences
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#parameter-preferences
+dotnet_code_quality_unused_parameters = all:warning
+# More style options (Undocumented)
+# https://github.com/MicrosoftDocs/visualstudio-docs/issues/3641
+dotnet_style_operator_placement_when_wrapping = end_of_line
+# https://github.com/dotnet/roslyn/pull/40070
+dotnet_style_prefer_simplified_interpolation = true:warning
+
+# C# Code Style Settings
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#c-code-style-settings
+[*.{cs,csx,cake}]
+# Implicit and explicit types
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#implicit-and-explicit-types
+csharp_style_var_for_built_in_types = never
+csharp_style_var_when_type_is_apparent = true:warning
+csharp_style_var_elsewhere = false:warning
+# Expression-bodied members
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#expression-bodied-members
+csharp_style_expression_bodied_methods = true:warning
+csharp_style_expression_bodied_constructors = true:warning
+csharp_style_expression_bodied_operators = true:warning
+csharp_style_expression_bodied_properties = true:warning
+csharp_style_expression_bodied_indexers = true:warning
+csharp_style_expression_bodied_accessors = true:warning
+csharp_style_expression_bodied_lambdas = true:warning
+csharp_style_expression_bodied_local_functions = true:warning
+# Pattern matching
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#pattern-matching
+csharp_style_pattern_matching_over_is_with_cast_check = true:warning
+csharp_style_pattern_matching_over_as_with_null_check = true:warning
+# Inlined variable declarations
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#inlined-variable-declarations
+csharp_style_inlined_variable_declaration = true:warning
+# Expression-level preferences
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#expression-level-preferences
+csharp_prefer_simple_default_expression = true:warning
+# "Null" checking preferences
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#c-null-checking-preferences
+csharp_style_throw_expression = true:warning
+csharp_style_conditional_delegate_call = true:warning
+# Code block preferences
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#code-block-preferences
+csharp_prefer_braces = true:warning
+# Unused value preferences
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#unused-value-preferences
+csharp_style_unused_value_expression_statement_preference = discard_variable:suggestion
+csharp_style_unused_value_assignment_preference = discard_variable:suggestion
+# Index and range preferences
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#index-and-range-preferences
+csharp_style_prefer_index_operator = true:warning
+csharp_style_prefer_range_operator = true:warning
+# Miscellaneous preferences
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#miscellaneous-preferences
+csharp_style_deconstructed_variable_declaration = true:warning
+csharp_style_pattern_local_over_anonymous_function = true:warning
+csharp_using_directive_placement = outside_namespace:warning
+csharp_prefer_static_local_function = true:warning
+csharp_prefer_simple_using_statement = true:suggestion
+
+##########################################
+# .NET Formatting Conventions
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-code-style-settings-reference#formatting-conventions
+##########################################
+
+# Organize usings
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-formatting-conventions#organize-using-directives
+dotnet_sort_system_directives_first = true
+# Newline options
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-formatting-conventions#new-line-options
+csharp_new_line_before_open_brace = all
+csharp_new_line_before_else = true
+csharp_new_line_before_catch = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_members_in_object_initializers = true
+csharp_new_line_before_members_in_anonymous_types = true
+csharp_new_line_between_query_expression_clauses = true
+# Indentation options
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-formatting-conventions#indentation-options
+csharp_indent_case_contents = true
+csharp_indent_switch_labels = true
+csharp_indent_labels = no_change
+csharp_indent_block_contents = true
+csharp_indent_braces = false
+csharp_indent_case_contents_when_block = false
+# Spacing options
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-formatting-conventions#spacing-options
+csharp_space_after_cast = false
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_between_parentheses = false
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_around_binary_operators = before_and_after
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
+csharp_space_between_method_declaration_name_and_open_parenthesis = false
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_method_call_empty_parameter_list_parentheses = false
+csharp_space_between_method_call_name_and_opening_parenthesis = false
+csharp_space_after_comma = true
+csharp_space_before_comma = false
+csharp_space_after_dot = false
+csharp_space_before_dot = false
+csharp_space_after_semicolon_in_for_statement = true
+csharp_space_before_semicolon_in_for_statement = false
+csharp_space_around_declaration_statements = false
+csharp_space_before_open_square_brackets = false
+csharp_space_between_empty_square_brackets = false
+csharp_space_between_square_brackets = false
+# Wrapping options
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-formatting-conventions#wrap-options
+csharp_preserve_single_line_statements = false
+csharp_preserve_single_line_blocks = true
+
+##########################################
+# .NET Naming Conventions
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-naming-conventions
+##########################################
+
+[*.{cs,csx,cake,vb,vbx}]
+
+##########################################
+# Styles
+##########################################
+
+# camel_case_style - Define the camelCase style
+dotnet_naming_style.camel_case_style.capitalization = camel_case
+# pascal_case_style - Define the PascalCase style
+dotnet_naming_style.pascal_case_style.capitalization = pascal_case
+# first_upper_style - The first character must start with an upper-case character
+dotnet_naming_style.first_upper_style.capitalization = first_word_upper
+# prefix_interface_with_i_style - Interfaces must be PascalCase and the first character of an interface must be an 'I'
+dotnet_naming_style.prefix_interface_with_i_style.capitalization = pascal_case
+dotnet_naming_style.prefix_interface_with_i_style.required_prefix = I
+# prefix_type_parameters_with_t_style - Generic Type Parameters must be PascalCase and the first character must be a 'T'
+dotnet_naming_style.prefix_type_parameters_with_t_style.capitalization = pascal_case
+dotnet_naming_style.prefix_type_parameters_with_t_style.required_prefix = T
+# disallowed_style - Anything that has this style applied is marked as disallowed
+dotnet_naming_style.disallowed_style.capitalization = pascal_case
+dotnet_naming_style.disallowed_style.required_prefix = ____RULE_VIOLATION____
+dotnet_naming_style.disallowed_style.required_suffix = ____RULE_VIOLATION____
+# internal_error_style - This style should never occur... if it does, it indicates a bug in file or in the parser using the file
+dotnet_naming_style.internal_error_style.capitalization = pascal_case
+dotnet_naming_style.internal_error_style.required_prefix = ____INTERNAL_ERROR____
+dotnet_naming_style.internal_error_style.required_suffix = ____INTERNAL_ERROR____
+
+##########################################
+# .NET Design Guideline Field Naming Rules
+# Naming rules for fields follow the .NET Framework design guidelines
+# https://docs.microsoft.com/dotnet/standard/design-guidelines/index
+##########################################
+
+# All public/protected/protected_internal constant fields must be PascalCase
+# https://docs.microsoft.com/dotnet/standard/design-guidelines/field
+dotnet_naming_symbols.public_protected_constant_fields_group.applicable_accessibilities = public, protected, protected_internal
+dotnet_naming_symbols.public_protected_constant_fields_group.required_modifiers = const
+dotnet_naming_symbols.public_protected_constant_fields_group.applicable_kinds = field
+dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.symbols = public_protected_constant_fields_group
+dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.style = pascal_case_style
+dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.severity = warning
+
+# All public/protected/protected_internal static readonly fields must be PascalCase
+# https://docs.microsoft.com/dotnet/standard/design-guidelines/field
+dotnet_naming_symbols.public_protected_static_readonly_fields_group.applicable_accessibilities = public, protected, protected_internal
+dotnet_naming_symbols.public_protected_static_readonly_fields_group.required_modifiers = static, readonly
+dotnet_naming_symbols.public_protected_static_readonly_fields_group.applicable_kinds = field
+dotnet_naming_rule.public_protected_static_readonly_fields_must_be_pascal_case_rule.symbols = public_protected_static_readonly_fields_group
+dotnet_naming_rule.public_protected_static_readonly_fields_must_be_pascal_case_rule.style = pascal_case_style
+dotnet_naming_rule.public_protected_static_readonly_fields_must_be_pascal_case_rule.severity = warning
+
+# No other public/protected/protected_internal fields are allowed
+# https://docs.microsoft.com/dotnet/standard/design-guidelines/field
+dotnet_naming_symbols.other_public_protected_fields_group.applicable_accessibilities = public, protected, protected_internal
+dotnet_naming_symbols.other_public_protected_fields_group.applicable_kinds = field
+dotnet_naming_rule.other_public_protected_fields_disallowed_rule.symbols = other_public_protected_fields_group
+dotnet_naming_rule.other_public_protected_fields_disallowed_rule.style = disallowed_style
+dotnet_naming_rule.other_public_protected_fields_disallowed_rule.severity = error
+
+##########################################
+# StyleCop Field Naming Rules
+# Naming rules for fields follow the StyleCop analyzers
+# This does not override any rules using disallowed_style above
+# https://github.com/DotNetAnalyzers/StyleCopAnalyzers
+##########################################
+
+# All constant fields must be PascalCase
+# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1303.md
+dotnet_naming_symbols.stylecop_constant_fields_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected, private
+dotnet_naming_symbols.stylecop_constant_fields_group.required_modifiers = const
+dotnet_naming_symbols.stylecop_constant_fields_group.applicable_kinds = field
+dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.symbols = stylecop_constant_fields_group
+dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.style = pascal_case_style
+dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.severity = warning
+
+# All static readonly fields must be PascalCase
+# Ajusted to ignore private fields.
+# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1311.md
+dotnet_naming_symbols.stylecop_static_readonly_fields_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected
+dotnet_naming_symbols.stylecop_static_readonly_fields_group.required_modifiers = static, readonly
+dotnet_naming_symbols.stylecop_static_readonly_fields_group.applicable_kinds = field
+dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.symbols = stylecop_static_readonly_fields_group
+dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.style = pascal_case_style
+dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.severity = warning
+
+# No non-private instance fields are allowed
+# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1401.md
+dotnet_naming_symbols.stylecop_fields_must_be_private_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected
+dotnet_naming_symbols.stylecop_fields_must_be_private_group.applicable_kinds = field
+dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.symbols = stylecop_fields_must_be_private_group
+dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.style = disallowed_style
+dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.severity = error
+
+# Local variables must be camelCase
+# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1312.md
+dotnet_naming_symbols.stylecop_local_fields_group.applicable_accessibilities = local
+dotnet_naming_symbols.stylecop_local_fields_group.applicable_kinds = local
+dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.symbols = stylecop_local_fields_group
+dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.style = camel_case_style
+dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.severity = silent
+
+# This rule should never fire. However, it's included for at least two purposes:
+# First, it helps to understand, reason about, and root-case certain types of issues, such as bugs in .editorconfig parsers.
+# Second, it helps to raise immediate awareness if a new field type is added (as occurred recently in C#).
+dotnet_naming_symbols.sanity_check_uncovered_field_case_group.applicable_accessibilities = *
+dotnet_naming_symbols.sanity_check_uncovered_field_case_group.applicable_kinds = field
+dotnet_naming_rule.sanity_check_uncovered_field_case_rule.symbols = sanity_check_uncovered_field_case_group
+dotnet_naming_rule.sanity_check_uncovered_field_case_rule.style = internal_error_style
+dotnet_naming_rule.sanity_check_uncovered_field_case_rule.severity = error
+
+
+##########################################
+# Other Naming Rules
+##########################################
+
+# All of the following must be PascalCase:
+# - Namespaces
+# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-namespaces
+# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1300.md
+# - Classes and Enumerations
+# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces
+# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1300.md
+# - Delegates
+# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces#names-of-common-types
+# - Constructors, Properties, Events, Methods
+# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-type-members
+dotnet_naming_symbols.element_group.applicable_kinds = namespace, class, enum, struct, delegate, event, method, property
+dotnet_naming_rule.element_rule.symbols = element_group
+dotnet_naming_rule.element_rule.style = pascal_case_style
+dotnet_naming_rule.element_rule.severity = warning
+
+# Interfaces use PascalCase and are prefixed with uppercase 'I'
+# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces
+dotnet_naming_symbols.interface_group.applicable_kinds = interface
+dotnet_naming_rule.interface_rule.symbols = interface_group
+dotnet_naming_rule.interface_rule.style = prefix_interface_with_i_style
+dotnet_naming_rule.interface_rule.severity = warning
+
+# Generics Type Parameters use PascalCase and are prefixed with uppercase 'T'
+# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces
+dotnet_naming_symbols.type_parameter_group.applicable_kinds = type_parameter
+dotnet_naming_rule.type_parameter_rule.symbols = type_parameter_group
+dotnet_naming_rule.type_parameter_rule.style = prefix_type_parameters_with_t_style
+dotnet_naming_rule.type_parameter_rule.severity = warning
+
+# Function parameters use camelCase
+# https://docs.microsoft.com/dotnet/standard/design-guidelines/naming-parameters
+dotnet_naming_symbols.parameters_group.applicable_kinds = parameter
+dotnet_naming_rule.parameters_rule.symbols = parameters_group
+dotnet_naming_rule.parameters_rule.style = camel_case_style
+dotnet_naming_rule.parameters_rule.severity = warning
+
+# Private static fields use camelCase and start with s_
+dotnet_naming_symbols.private_static_field_symbols.applicable_accessibilities = private
+dotnet_naming_symbols.private_static_field_symbols.required_modifiers = static, shared
+dotnet_naming_symbols.private_static_field_symbols.applicable_kinds = field
+dotnet_naming_rule.private_static_fields_must_be_camel_cased_and_prefixed_with_s_underscore.symbols = private_static_field_symbols
+dotnet_naming_rule.private_static_fields_must_be_camel_cased_and_prefixed_with_s_underscore.style = camel_case_and_prefix_with_s_underscore_style
+dotnet_naming_rule.private_static_fields_must_be_camel_cased_and_prefixed_with_s_underscore.severity = warning
+dotnet_naming_style.camel_case_and_prefix_with_s_underscore_style.required_prefix = s_
+dotnet_naming_style.camel_case_and_prefix_with_s_underscore_style.capitalization = camel_case
+
+# Instance fields use camelCase and are prefixed with '_'
+dotnet_naming_symbols.private_field_symbols.applicable_accessibilities = private
+dotnet_naming_symbols.private_field_symbols.applicable_kinds = field
+dotnet_naming_rule.private_instance_fields_must_be_camel_cased_and_prefixed_with_underscore.symbols = private_field_symbols
+dotnet_naming_rule.private_instance_fields_must_be_camel_cased_and_prefixed_with_underscore.style = camel_case_and_prefix_with_underscore_style
+dotnet_naming_rule.private_instance_fields_must_be_camel_cased_and_prefixed_with_underscore.severity = warning
+dotnet_naming_style.camel_case_and_prefix_with_underscore_style.required_prefix = _
+dotnet_naming_style.camel_case_and_prefix_with_underscore_style.capitalization = camel_case
+
+##########################################
+# License
+##########################################
+# The following applies as to the .editorconfig file ONLY, and is
+# included below for reference, per the requirements of the license
+# corresponding to this .editorconfig file.
+# See: https://github.com/RehanSaeed/EditorConfig
+#
+# MIT License
+#
+# Copyright (c) 2017-2019 Muhammad Rehan Saeed
+# Copyright (c) 2019 Henry Gabryjelski
+#
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish, distribute,
+# sublicense, and/or sell copies of the Software, and to permit
+# persons to whom the Software is furnished to do so, subject
+# to the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+# OTHER DEALINGS IN THE SOFTWARE.
+##########################################
diff --git a/linting/codeanalysis.ruleset b/linting/codeanalysis.ruleset
new file mode 100644
index 0000000000..4fde2bef8d
--- /dev/null
+++ b/linting/codeanalysis.ruleset
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/linting/codeanalysis.tests.ruleset b/linting/codeanalysis.tests.ruleset
new file mode 100644
index 0000000000..0ac055b328
--- /dev/null
+++ b/linting/codeanalysis.tests.ruleset
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/linting/stylecop.json b/linting/stylecop.json
new file mode 100644
index 0000000000..b2f7771470
--- /dev/null
+++ b/linting/stylecop.json
@@ -0,0 +1,16 @@
+{
+ "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json",
+ "settings": {
+ "orderingRules": {
+ "usingDirectivesPlacement": "outsideNamespace",
+ "elementOrder": [
+ "kind"
+ ]
+ },
+ "documentationRules": {
+ "xmlHeader": false,
+ "documentInternalElements": false,
+ "copyrightText": "Copyright (c) Umbraco.\nSee LICENSE for more details."
+ }
+ }
+}
diff --git a/src/.editorconfig b/src/.editorconfig
new file mode 100644
index 0000000000..66967e074f
--- /dev/null
+++ b/src/.editorconfig
@@ -0,0 +1,447 @@
+# Version: 1.6.2 (Using https://semver.org/)
+# Updated: 2020-11-02
+# See https://github.com/RehanSaeed/EditorConfig/releases for release notes.
+# See https://github.com/RehanSaeed/EditorConfig for updates to this file.
+# See http://EditorConfig.org for more information about .editorconfig files.
+
+##########################################
+# Common Settings
+##########################################
+
+# This file is the top-most EditorConfig file
+root = true
+
+# All Files
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 4
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+##########################################
+# File Extension Settings
+##########################################
+
+# Visual Studio Solution Files
+[*.sln]
+indent_style = tab
+
+# Visual Studio XML Project Files
+[*.{csproj,vbproj,vcxproj.filters,proj,projitems,shproj}]
+indent_size = 2
+
+# XML Configuration Files
+[*.{xml,config,props,targets,nuspec,resx,ruleset,vsixmanifest,vsct}]
+indent_size = 2
+
+# JSON Files
+[*.{json,json5,webmanifest}]
+indent_size = 2
+
+# YAML Files
+[*.{yml,yaml}]
+indent_size = 2
+
+# Markdown Files
+[*.md]
+trim_trailing_whitespace = false
+
+# Web Files
+[*.{htm,html,js,jsm,ts,tsx,css,sass,scss,less,svg,vue}]
+indent_size = 2
+
+# Batch Files
+[*.{cmd,bat}]
+end_of_line = crlf
+
+# Bash Files
+[*.sh]
+end_of_line = lf
+
+# Makefiles
+[Makefile]
+indent_style = tab
+
+##########################################
+# File Header (Uncomment to support file headers)
+# https://docs.microsoft.com/visualstudio/ide/reference/add-file-header
+##########################################
+
+# [*.{cs,csx,cake,vb,vbx}]
+file_header_template = Copyright (c) Umbraco.\nSee LICENSE for more details.
+
+# SA1636: File header copyright text should match
+# Justification: .editorconfig supports file headers. If this is changed to a value other than "none", a stylecop.json file will need to added to the project.
+# dotnet_diagnostic.SA1636.severity = none
+
+##########################################
+# .NET Language Conventions
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions
+##########################################
+
+# .NET Code Style Settings
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#net-code-style-settings
+[*.{cs,csx,cake,vb,vbx}]
+# "this." and "Me." qualifiers
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#this-and-me
+dotnet_style_qualification_for_field = false:suggestion
+dotnet_style_qualification_for_property = false:suggestion
+dotnet_style_qualification_for_method = false:suggestion
+dotnet_style_qualification_for_event = false:suggestion
+# Language keywords instead of framework type names for type references
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#language-keywords
+dotnet_style_predefined_type_for_locals_parameters_members = true:warning
+dotnet_style_predefined_type_for_member_access = true:warning
+# Modifier preferences
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#normalize-modifiers
+dotnet_style_require_accessibility_modifiers = always:warning
+csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:warning
+visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:warning
+dotnet_style_readonly_field = true:warning
+# Parentheses preferences
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#parentheses-preferences
+dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:warning
+dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:warning
+dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:warning
+dotnet_style_parentheses_in_other_operators = never_if_unnecessary:suggestion
+# Expression-level preferences
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#expression-level-preferences
+dotnet_style_object_initializer = true:warning
+dotnet_style_collection_initializer = true:warning
+dotnet_style_explicit_tuple_names = true:warning
+dotnet_style_prefer_inferred_tuple_names = true:warning
+dotnet_style_prefer_inferred_anonymous_type_member_names = true:warning
+dotnet_style_prefer_auto_properties = true:warning
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning
+dotnet_style_prefer_conditional_expression_over_assignment = false:suggestion
+dotnet_style_prefer_conditional_expression_over_return = false:suggestion
+dotnet_style_prefer_compound_assignment = true:warning
+# Null-checking preferences
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#null-checking-preferences
+dotnet_style_coalesce_expression = true:warning
+dotnet_style_null_propagation = true:warning
+# Parameter preferences
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#parameter-preferences
+dotnet_code_quality_unused_parameters = all:warning
+# More style options (Undocumented)
+# https://github.com/MicrosoftDocs/visualstudio-docs/issues/3641
+dotnet_style_operator_placement_when_wrapping = end_of_line
+# https://github.com/dotnet/roslyn/pull/40070
+dotnet_style_prefer_simplified_interpolation = true:warning
+
+# C# Code Style Settings
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#c-code-style-settings
+[*.{cs,csx,cake}]
+# Implicit and explicit types
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#implicit-and-explicit-types
+csharp_style_var_for_built_in_types = never
+csharp_style_var_when_type_is_apparent = true:warning
+csharp_style_var_elsewhere = false:warning
+# Expression-bodied members
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#expression-bodied-members
+csharp_style_expression_bodied_methods = true:warning
+csharp_style_expression_bodied_constructors = true:warning
+csharp_style_expression_bodied_operators = true:warning
+csharp_style_expression_bodied_properties = true:warning
+csharp_style_expression_bodied_indexers = true:warning
+csharp_style_expression_bodied_accessors = true:warning
+csharp_style_expression_bodied_lambdas = true:warning
+csharp_style_expression_bodied_local_functions = true:warning
+# Pattern matching
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#pattern-matching
+csharp_style_pattern_matching_over_is_with_cast_check = true:warning
+csharp_style_pattern_matching_over_as_with_null_check = true:warning
+# Inlined variable declarations
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#inlined-variable-declarations
+csharp_style_inlined_variable_declaration = true:warning
+# Expression-level preferences
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#expression-level-preferences
+csharp_prefer_simple_default_expression = true:warning
+# "Null" checking preferences
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#c-null-checking-preferences
+csharp_style_throw_expression = true:warning
+csharp_style_conditional_delegate_call = true:warning
+# Code block preferences
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#code-block-preferences
+csharp_prefer_braces = true:warning
+# Unused value preferences
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#unused-value-preferences
+csharp_style_unused_value_expression_statement_preference = discard_variable:suggestion
+csharp_style_unused_value_assignment_preference = discard_variable:suggestion
+# Index and range preferences
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#index-and-range-preferences
+csharp_style_prefer_index_operator = true:warning
+csharp_style_prefer_range_operator = true:warning
+# Miscellaneous preferences
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#miscellaneous-preferences
+csharp_style_deconstructed_variable_declaration = true:warning
+csharp_style_pattern_local_over_anonymous_function = true:warning
+csharp_using_directive_placement = outside_namespace:warning
+csharp_prefer_static_local_function = true:warning
+csharp_prefer_simple_using_statement = true:suggestion
+
+##########################################
+# .NET Formatting Conventions
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-code-style-settings-reference#formatting-conventions
+##########################################
+
+# Organize usings
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-formatting-conventions#organize-using-directives
+dotnet_sort_system_directives_first = true
+# Newline options
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-formatting-conventions#new-line-options
+csharp_new_line_before_open_brace = all
+csharp_new_line_before_else = true
+csharp_new_line_before_catch = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_members_in_object_initializers = true
+csharp_new_line_before_members_in_anonymous_types = true
+csharp_new_line_between_query_expression_clauses = true
+# Indentation options
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-formatting-conventions#indentation-options
+csharp_indent_case_contents = true
+csharp_indent_switch_labels = true
+csharp_indent_labels = no_change
+csharp_indent_block_contents = true
+csharp_indent_braces = false
+csharp_indent_case_contents_when_block = false
+# Spacing options
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-formatting-conventions#spacing-options
+csharp_space_after_cast = false
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_between_parentheses = false
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_around_binary_operators = before_and_after
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
+csharp_space_between_method_declaration_name_and_open_parenthesis = false
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_method_call_empty_parameter_list_parentheses = false
+csharp_space_between_method_call_name_and_opening_parenthesis = false
+csharp_space_after_comma = true
+csharp_space_before_comma = false
+csharp_space_after_dot = false
+csharp_space_before_dot = false
+csharp_space_after_semicolon_in_for_statement = true
+csharp_space_before_semicolon_in_for_statement = false
+csharp_space_around_declaration_statements = false
+csharp_space_before_open_square_brackets = false
+csharp_space_between_empty_square_brackets = false
+csharp_space_between_square_brackets = false
+# Wrapping options
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-formatting-conventions#wrap-options
+csharp_preserve_single_line_statements = false
+csharp_preserve_single_line_blocks = true
+
+##########################################
+# .NET Naming Conventions
+# https://docs.microsoft.com/visualstudio/ide/editorconfig-naming-conventions
+##########################################
+
+[*.{cs,csx,cake,vb,vbx}]
+
+##########################################
+# Styles
+##########################################
+
+# camel_case_style - Define the camelCase style
+dotnet_naming_style.camel_case_style.capitalization = camel_case
+# pascal_case_style - Define the PascalCase style
+dotnet_naming_style.pascal_case_style.capitalization = pascal_case
+# first_upper_style - The first character must start with an upper-case character
+dotnet_naming_style.first_upper_style.capitalization = first_word_upper
+# prefix_interface_with_i_style - Interfaces must be PascalCase and the first character of an interface must be an 'I'
+dotnet_naming_style.prefix_interface_with_i_style.capitalization = pascal_case
+dotnet_naming_style.prefix_interface_with_i_style.required_prefix = I
+# prefix_type_parameters_with_t_style - Generic Type Parameters must be PascalCase and the first character must be a 'T'
+dotnet_naming_style.prefix_type_parameters_with_t_style.capitalization = pascal_case
+dotnet_naming_style.prefix_type_parameters_with_t_style.required_prefix = T
+# disallowed_style - Anything that has this style applied is marked as disallowed
+dotnet_naming_style.disallowed_style.capitalization = pascal_case
+dotnet_naming_style.disallowed_style.required_prefix = ____RULE_VIOLATION____
+dotnet_naming_style.disallowed_style.required_suffix = ____RULE_VIOLATION____
+# internal_error_style - This style should never occur... if it does, it indicates a bug in file or in the parser using the file
+dotnet_naming_style.internal_error_style.capitalization = pascal_case
+dotnet_naming_style.internal_error_style.required_prefix = ____INTERNAL_ERROR____
+dotnet_naming_style.internal_error_style.required_suffix = ____INTERNAL_ERROR____
+
+##########################################
+# .NET Design Guideline Field Naming Rules
+# Naming rules for fields follow the .NET Framework design guidelines
+# https://docs.microsoft.com/dotnet/standard/design-guidelines/index
+##########################################
+
+# All public/protected/protected_internal constant fields must be PascalCase
+# https://docs.microsoft.com/dotnet/standard/design-guidelines/field
+dotnet_naming_symbols.public_protected_constant_fields_group.applicable_accessibilities = public, protected, protected_internal
+dotnet_naming_symbols.public_protected_constant_fields_group.required_modifiers = const
+dotnet_naming_symbols.public_protected_constant_fields_group.applicable_kinds = field
+dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.symbols = public_protected_constant_fields_group
+dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.style = pascal_case_style
+dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.severity = warning
+
+# All public/protected/protected_internal static readonly fields must be PascalCase
+# https://docs.microsoft.com/dotnet/standard/design-guidelines/field
+dotnet_naming_symbols.public_protected_static_readonly_fields_group.applicable_accessibilities = public, protected, protected_internal
+dotnet_naming_symbols.public_protected_static_readonly_fields_group.required_modifiers = static, readonly
+dotnet_naming_symbols.public_protected_static_readonly_fields_group.applicable_kinds = field
+dotnet_naming_rule.public_protected_static_readonly_fields_must_be_pascal_case_rule.symbols = public_protected_static_readonly_fields_group
+dotnet_naming_rule.public_protected_static_readonly_fields_must_be_pascal_case_rule.style = pascal_case_style
+dotnet_naming_rule.public_protected_static_readonly_fields_must_be_pascal_case_rule.severity = warning
+
+# No other public/protected/protected_internal fields are allowed
+# https://docs.microsoft.com/dotnet/standard/design-guidelines/field
+dotnet_naming_symbols.other_public_protected_fields_group.applicable_accessibilities = public, protected, protected_internal
+dotnet_naming_symbols.other_public_protected_fields_group.applicable_kinds = field
+dotnet_naming_rule.other_public_protected_fields_disallowed_rule.symbols = other_public_protected_fields_group
+dotnet_naming_rule.other_public_protected_fields_disallowed_rule.style = disallowed_style
+dotnet_naming_rule.other_public_protected_fields_disallowed_rule.severity = error
+
+##########################################
+# StyleCop Field Naming Rules
+# Naming rules for fields follow the StyleCop analyzers
+# This does not override any rules using disallowed_style above
+# https://github.com/DotNetAnalyzers/StyleCopAnalyzers
+##########################################
+
+# All constant fields must be PascalCase
+# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1303.md
+dotnet_naming_symbols.stylecop_constant_fields_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected, private
+dotnet_naming_symbols.stylecop_constant_fields_group.required_modifiers = const
+dotnet_naming_symbols.stylecop_constant_fields_group.applicable_kinds = field
+dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.symbols = stylecop_constant_fields_group
+dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.style = pascal_case_style
+dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.severity = warning
+
+# All static readonly fields must be PascalCase
+# Ajusted to ignore private fields.
+# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1311.md
+dotnet_naming_symbols.stylecop_static_readonly_fields_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected
+dotnet_naming_symbols.stylecop_static_readonly_fields_group.required_modifiers = static, readonly
+dotnet_naming_symbols.stylecop_static_readonly_fields_group.applicable_kinds = field
+dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.symbols = stylecop_static_readonly_fields_group
+dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.style = pascal_case_style
+dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.severity = warning
+
+# No non-private instance fields are allowed
+# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1401.md
+dotnet_naming_symbols.stylecop_fields_must_be_private_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected
+dotnet_naming_symbols.stylecop_fields_must_be_private_group.applicable_kinds = field
+dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.symbols = stylecop_fields_must_be_private_group
+dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.style = disallowed_style
+dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.severity = error
+
+# Local variables must be camelCase
+# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1312.md
+dotnet_naming_symbols.stylecop_local_fields_group.applicable_accessibilities = local
+dotnet_naming_symbols.stylecop_local_fields_group.applicable_kinds = local
+dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.symbols = stylecop_local_fields_group
+dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.style = camel_case_style
+dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.severity = silent
+
+# This rule should never fire. However, it's included for at least two purposes:
+# First, it helps to understand, reason about, and root-case certain types of issues, such as bugs in .editorconfig parsers.
+# Second, it helps to raise immediate awareness if a new field type is added (as occurred recently in C#).
+dotnet_naming_symbols.sanity_check_uncovered_field_case_group.applicable_accessibilities = *
+dotnet_naming_symbols.sanity_check_uncovered_field_case_group.applicable_kinds = field
+dotnet_naming_rule.sanity_check_uncovered_field_case_rule.symbols = sanity_check_uncovered_field_case_group
+dotnet_naming_rule.sanity_check_uncovered_field_case_rule.style = internal_error_style
+dotnet_naming_rule.sanity_check_uncovered_field_case_rule.severity = error
+
+
+##########################################
+# Other Naming Rules
+##########################################
+
+# All of the following must be PascalCase:
+# - Namespaces
+# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-namespaces
+# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1300.md
+# - Classes and Enumerations
+# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces
+# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1300.md
+# - Delegates
+# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces#names-of-common-types
+# - Constructors, Properties, Events, Methods
+# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-type-members
+dotnet_naming_symbols.element_group.applicable_kinds = namespace, class, enum, struct, delegate, event, method, property
+dotnet_naming_rule.element_rule.symbols = element_group
+dotnet_naming_rule.element_rule.style = pascal_case_style
+dotnet_naming_rule.element_rule.severity = warning
+
+# Interfaces use PascalCase and are prefixed with uppercase 'I'
+# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces
+dotnet_naming_symbols.interface_group.applicable_kinds = interface
+dotnet_naming_rule.interface_rule.symbols = interface_group
+dotnet_naming_rule.interface_rule.style = prefix_interface_with_i_style
+dotnet_naming_rule.interface_rule.severity = warning
+
+# Generics Type Parameters use PascalCase and are prefixed with uppercase 'T'
+# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces
+dotnet_naming_symbols.type_parameter_group.applicable_kinds = type_parameter
+dotnet_naming_rule.type_parameter_rule.symbols = type_parameter_group
+dotnet_naming_rule.type_parameter_rule.style = prefix_type_parameters_with_t_style
+dotnet_naming_rule.type_parameter_rule.severity = warning
+
+# Function parameters use camelCase
+# https://docs.microsoft.com/dotnet/standard/design-guidelines/naming-parameters
+dotnet_naming_symbols.parameters_group.applicable_kinds = parameter
+dotnet_naming_rule.parameters_rule.symbols = parameters_group
+dotnet_naming_rule.parameters_rule.style = camel_case_style
+dotnet_naming_rule.parameters_rule.severity = warning
+
+# Private static fields use camelCase and start with s_
+dotnet_naming_symbols.private_static_field_symbols.applicable_accessibilities = private
+dotnet_naming_symbols.private_static_field_symbols.required_modifiers = static, shared
+dotnet_naming_symbols.private_static_field_symbols.applicable_kinds = field
+dotnet_naming_rule.private_static_fields_must_be_camel_cased_and_prefixed_with_s_underscore.symbols = private_static_field_symbols
+dotnet_naming_rule.private_static_fields_must_be_camel_cased_and_prefixed_with_s_underscore.style = camel_case_and_prefix_with_s_underscore_style
+dotnet_naming_rule.private_static_fields_must_be_camel_cased_and_prefixed_with_s_underscore.severity = warning
+dotnet_naming_style.camel_case_and_prefix_with_s_underscore_style.required_prefix = s_
+dotnet_naming_style.camel_case_and_prefix_with_s_underscore_style.capitalization = camel_case
+
+# Instance fields use camelCase and are prefixed with '_'
+dotnet_naming_symbols.private_field_symbols.applicable_accessibilities = private
+dotnet_naming_symbols.private_field_symbols.applicable_kinds = field
+dotnet_naming_rule.private_instance_fields_must_be_camel_cased_and_prefixed_with_underscore.symbols = private_field_symbols
+dotnet_naming_rule.private_instance_fields_must_be_camel_cased_and_prefixed_with_underscore.style = camel_case_and_prefix_with_underscore_style
+dotnet_naming_rule.private_instance_fields_must_be_camel_cased_and_prefixed_with_underscore.severity = warning
+dotnet_naming_style.camel_case_and_prefix_with_underscore_style.required_prefix = _
+dotnet_naming_style.camel_case_and_prefix_with_underscore_style.capitalization = camel_case
+
+##########################################
+# License
+##########################################
+# The following applies as to the .editorconfig file ONLY, and is
+# included below for reference, per the requirements of the license
+# corresponding to this .editorconfig file.
+# See: https://github.com/RehanSaeed/EditorConfig
+#
+# MIT License
+#
+# Copyright (c) 2017-2019 Muhammad Rehan Saeed
+# Copyright (c) 2019 Henry Gabryjelski
+#
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish, distribute,
+# sublicense, and/or sell copies of the Software, and to permit
+# persons to whom the Software is furnished to do so, subject
+# to the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+# OTHER DEALINGS IN THE SOFTWARE.
+##########################################
diff --git a/src/Umbraco.Core/BackOffice/ClaimsPrincipalExtensions.cs b/src/Umbraco.Core/BackOffice/ClaimsPrincipalExtensions.cs
index 6a792ce69b..7cbca0428a 100644
--- a/src/Umbraco.Core/BackOffice/ClaimsPrincipalExtensions.cs
+++ b/src/Umbraco.Core/BackOffice/ClaimsPrincipalExtensions.cs
@@ -17,6 +17,8 @@ namespace Umbraco.Extensions
///
public static UmbracoBackOfficeIdentity GetUmbracoIdentity(this IPrincipal user)
{
+ // TODO: It would be nice to get rid of this and only rely on Claims, not a strongly typed identity instance
+
//If it's already a UmbracoBackOfficeIdentity
if (user.Identity is UmbracoBackOfficeIdentity backOfficeIdentity) return backOfficeIdentity;
@@ -53,10 +55,10 @@ namespace Umbraco.Extensions
///
public static double GetRemainingAuthSeconds(this IPrincipal user, DateTimeOffset now)
{
- var umbIdentity = user.GetUmbracoIdentity();
- if (umbIdentity == null) return 0;
+ var claimsPrincipal = user as ClaimsPrincipal;
+ if (claimsPrincipal == null) return 0;
- var ticketExpires = umbIdentity.FindFirstValue(Constants.Security.TicketExpiresClaimType);
+ var ticketExpires = claimsPrincipal.FindFirst(Constants.Security.TicketExpiresClaimType)?.Value;
if (ticketExpires.IsNullOrWhiteSpace()) return 0;
var utcExpired = DateTimeOffset.Parse(ticketExpires, null, DateTimeStyles.RoundtripKind);
diff --git a/src/Umbraco.Core/BackOffice/UmbracoBackOfficeIdentity.cs b/src/Umbraco.Core/BackOffice/UmbracoBackOfficeIdentity.cs
index 93acb279dc..9a60c5d64f 100644
--- a/src/Umbraco.Core/BackOffice/UmbracoBackOfficeIdentity.cs
+++ b/src/Umbraco.Core/BackOffice/UmbracoBackOfficeIdentity.cs
@@ -12,6 +12,10 @@ namespace Umbraco.Core.BackOffice
[Serializable]
public class UmbracoBackOfficeIdentity : ClaimsIdentity
{
+ // TODO: Ideally we remove this class and only deal with ClaimsIdentity as a best practice. All things relevant to our own
+ // identity are part of claims. This class would essentially become extension methods on a ClaimsIdentity for resolving
+ // values from it.
+
public static bool FromClaimsIdentity(ClaimsIdentity identity, out UmbracoBackOfficeIdentity backOfficeIdentity)
{
//validate that all claims exist
diff --git a/src/Umbraco.Core/Configuration/ICronTabParser.cs b/src/Umbraco.Core/Configuration/ICronTabParser.cs
index 2238be4a4c..7124a098b3 100644
--- a/src/Umbraco.Core/Configuration/ICronTabParser.cs
+++ b/src/Umbraco.Core/Configuration/ICronTabParser.cs
@@ -1,10 +1,28 @@
-using System;
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using System;
namespace Umbraco.Core.Configuration
{
+ ///
+ /// Defines the contract for that allows the parsing of chrontab expressions.
+ ///
public interface ICronTabParser
{
+ ///
+ /// Returns a value indicating whether a given chrontab expression is valid.
+ ///
+ /// The chrontab expression to parse.
+ /// The result.
bool IsValidCronTab(string cronTab);
+
+ ///
+ /// Returns the next occurence for the given chrontab expression from the given time.
+ ///
+ /// The chrontab expression to parse.
+ /// The date and time to start from.
+ /// The representing the next occurence.
DateTime GetNextOccurrence(string cronTab, DateTime time);
}
}
diff --git a/src/Umbraco.Core/Properties/AssemblyInfo.cs b/src/Umbraco.Core/Properties/AssemblyInfo.cs
index f129ca7731..ede9e49a7d 100644
--- a/src/Umbraco.Core/Properties/AssemblyInfo.cs
+++ b/src/Umbraco.Core/Properties/AssemblyInfo.cs
@@ -1,4 +1,4 @@
-using System.Reflection;
+using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -28,7 +28,3 @@ using System.Runtime.InteropServices;
// Umbraco Headless
[assembly: InternalsVisibleTo("Umbraco.Cloud.Headless")]
-
-// code analysis
-// IDE1006 is broken, wants _value syntax for consts, etc - and it's even confusing ppl at MS, kill it
-[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "~_~")]
diff --git a/src/Umbraco.Core/Security/IBackofficeSecurity.cs b/src/Umbraco.Core/Security/IBackofficeSecurity.cs
index 4ba20f7bfa..8d0e0df6d8 100644
--- a/src/Umbraco.Core/Security/IBackofficeSecurity.cs
+++ b/src/Umbraco.Core/Security/IBackofficeSecurity.cs
@@ -9,48 +9,38 @@ namespace Umbraco.Core.Security
///
/// Gets the current user.
///
- /// The current user.
+ /// The current user that has been authenticated for the request.
+ /// If authentication hasn't taken place this will be null.
+ // TODO: This is used a lot but most of it can be refactored to not use this at all since the IUser instance isn't
+ // needed in most cases. Where an IUser is required this could be an ext method on the ClaimsIdentity/ClaimsPrincipal that passes in
+ // an IUserService, like HttpContext.User.GetUmbracoUser(_userService);
+ // This one isn't as easy to remove as the others below.
IUser CurrentUser { get; }
///
/// Gets the current user's id.
///
- ///
+ /// The current user's Id that has been authenticated for the request.
+ /// If authentication hasn't taken place this will be unsuccessful.
+ // TODO: This should just be an extension method on ClaimsIdentity
Attempt GetUserId();
- ///
- /// Validates the currently logged in user and ensures they are not timed out
- ///
- ///
- bool ValidateCurrentUser();
-
- ///
- /// Validates the current user assigned to the request and ensures the stored user data is valid
- ///
- /// set to true if you want exceptions to be thrown if failed
- /// If true requires that the user is approved to be validated
- ///
- ValidateRequestAttempt ValidateCurrentUser(bool throwExceptions, bool requiresApproval = true);
-
- ///
- /// Authorizes the full request, checks for SSL and validates the current user
- ///
- /// set to true if you want exceptions to be thrown if failed
- ///
- ValidateRequestAttempt AuthorizeRequest(bool throwExceptions = false);
-
///
/// Checks if the specified user as access to the app
///
///
///
///
+ /// If authentication hasn't taken place this will be unsuccessful.
+ // TODO: Should be part of IBackOfficeUserManager
bool UserHasSectionAccess(string section, IUser user);
///
/// Ensures that a back office user is logged in
///
///
+ /// This does not force authentication, that must be done before calls to this are made.
+ // TODO: Should be removed, this should not be necessary
bool IsAuthenticated();
}
}
diff --git a/src/Umbraco.Core/Security/ValidateRequestAttempt.cs b/src/Umbraco.Core/Security/ValidateRequestAttempt.cs
deleted file mode 100644
index a88e18d463..0000000000
--- a/src/Umbraco.Core/Security/ValidateRequestAttempt.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-namespace Umbraco.Core.Security
-{
- public enum ValidateRequestAttempt
- {
- Success = 0,
-
- FailedNoPrivileges = 100,
-
- //FailedTimedOut,
-
- FailedNoContextId = 101,
- FailedNoSsl = 102
- }
-}
diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj
index 2b3efc9349..300dedc1c6 100644
--- a/src/Umbraco.Core/Umbraco.Core.csproj
+++ b/src/Umbraco.Core/Umbraco.Core.csproj
@@ -15,13 +15,13 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserStore.cs b/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserStore.cs
index 7ac3701c5c..b271f5aa41 100644
--- a/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserStore.cs
+++ b/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserStore.cs
@@ -12,6 +12,7 @@ using Umbraco.Core.Mapping;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Identity;
using Umbraco.Core.Models.Membership;
+using Umbraco.Core.Scoping;
using Umbraco.Core.Services;
namespace Umbraco.Core.BackOffice
@@ -22,15 +23,17 @@ namespace Umbraco.Core.BackOffice
IUserLoginStore,
IUserRoleStore,
IUserSecurityStampStore,
- IUserLockoutStore,
- IUserTwoFactorStore,
+ IUserLockoutStore,
IUserSessionStore
- // TODO: This would require additional columns/tables for now people will need to implement this on their own
- //IUserPhoneNumberStore,
- // TODO: To do this we need to implement IQueryable - we'll have an IQuerable implementation soon with the UmbracoLinqPadDriver implementation
- //IQueryableUserStore
+ // TODO: This would require additional columns/tables and then a lot of extra coding support to make this happen natively within umbraco
+ //IUserTwoFactorStore,
+ // TODO: This would require additional columns/tables for now people will need to implement this on their own
+ //IUserPhoneNumberStore,
+ // TODO: To do this we need to implement IQueryable - we'll have an IQuerable implementation soon with the UmbracoLinqPadDriver implementation
+ //IQueryableUserStore
{
+ private readonly IScopeProvider _scopeProvider;
private readonly IUserService _userService;
private readonly IEntityService _entityService;
private readonly IExternalLoginService _externalLoginService;
@@ -38,8 +41,9 @@ namespace Umbraco.Core.BackOffice
private readonly UmbracoMapper _mapper;
private bool _disposed = false;
- public BackOfficeUserStore(IUserService userService, IEntityService entityService, IExternalLoginService externalLoginService, IOptions globalSettings, UmbracoMapper mapper)
+ public BackOfficeUserStore(IScopeProvider scopeProvider, IUserService userService, IEntityService entityService, IExternalLoginService externalLoginService, IOptions globalSettings, UmbracoMapper mapper)
{
+ _scopeProvider = scopeProvider;
_userService = userService;
_entityService = entityService;
_externalLoginService = externalLoginService;
@@ -156,7 +160,7 @@ namespace Umbraco.Core.BackOffice
///
///
///
- public async Task UpdateAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
+ public Task UpdateAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
ThrowIfDisposed();
@@ -168,31 +172,34 @@ namespace Umbraco.Core.BackOffice
throw new InvalidOperationException("The user id must be an integer to work with the Umbraco");
}
- // TODO: Wrap this in a scope!
-
- var found = _userService.GetUserById(asInt.Result);
- if (found != null)
+ using (var scope = _scopeProvider.CreateScope())
{
- // we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it.
- var isLoginsPropertyDirty = user.IsPropertyDirty(nameof(BackOfficeIdentityUser.Logins));
-
- if (UpdateMemberProperties(found, user))
+ var found = _userService.GetUserById(asInt.Result);
+ if (found != null)
{
- _userService.Save(found);
+ // we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it.
+ var isLoginsPropertyDirty = user.IsPropertyDirty(nameof(BackOfficeIdentityUser.Logins));
+
+ if (UpdateMemberProperties(found, user))
+ {
+ _userService.Save(found);
+ }
+
+ if (isLoginsPropertyDirty)
+ {
+ _externalLoginService.Save(
+ found.Id,
+ user.Logins.Select(x => new ExternalLogin(
+ x.LoginProvider,
+ x.ProviderKey,
+ x.UserData)));
+ }
}
- if (isLoginsPropertyDirty)
- {
- _externalLoginService.Save(
- found.Id,
- user.Logins.Select(x => new ExternalLogin(
- x.LoginProvider,
- x.ProviderKey,
- x.UserData)));
- }
+ scope.Complete();
}
- return IdentityResult.Success;
+ return Task.FromResult(IdentityResult.Success);
}
///
@@ -627,35 +634,6 @@ namespace Umbraco.Core.BackOffice
return user;
}
- ///
- /// Sets whether two factor authentication is enabled for the user
- ///
- ///
- ///
- ///
- ///
- public virtual Task SetTwoFactorEnabledAsync(BackOfficeIdentityUser user, bool enabled, CancellationToken cancellationToken = default(CancellationToken))
- {
- cancellationToken.ThrowIfCancellationRequested();
- ThrowIfDisposed();
-
- user.TwoFactorEnabled = false;
- return Task.CompletedTask;
- }
-
- ///
- /// Returns whether two factor authentication is enabled for the user
- ///
- ///
- ///
- public virtual Task GetTwoFactorEnabledAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
- {
- cancellationToken.ThrowIfCancellationRequested();
- ThrowIfDisposed();
-
- return Task.FromResult(false);
- }
-
#region IUserLockoutStore
///
diff --git a/src/Umbraco.Infrastructure/BackOffice/IBackOfficeUserManager.cs b/src/Umbraco.Infrastructure/BackOffice/IBackOfficeUserManager.cs
index ca22567418..c026c256f5 100644
--- a/src/Umbraco.Infrastructure/BackOffice/IBackOfficeUserManager.cs
+++ b/src/Umbraco.Infrastructure/BackOffice/IBackOfficeUserManager.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Security.Claims;
using System.Security.Principal;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
@@ -15,6 +16,12 @@ namespace Umbraco.Core.BackOffice
public interface IBackOfficeUserManager: IDisposable
where TUser : BackOfficeIdentityUser
{
+ Task GetUserIdAsync(TUser user);
+
+ Task GetUserAsync(ClaimsPrincipal principal);
+
+ string GetUserId(ClaimsPrincipal principal);
+
Task> GetLoginsAsync(TUser user);
Task DeleteAsync(TUser user);
@@ -304,13 +311,14 @@ namespace Umbraco.Core.BackOffice
///
Task GetPhoneNumberAsync(TUser user);
+ // TODO: These are raised from outside the signinmanager and usermanager in the auth and user controllers,
+ // let's see if there's a way to avoid that and only have these called within signinmanager and usermanager
+ // which means we can remove these from the interface (things like invite seems like they cannot be moved)
void RaiseForgotPasswordRequestedEvent(IPrincipal currentUser, int userId);
void RaiseForgotPasswordChangedSuccessEvent(IPrincipal currentUser, int userId);
SignOutAuditEventArgs RaiseLogoutSuccessEvent(IPrincipal currentUser, int userId);
UserInviteEventArgs RaiseSendingUserInvite(IPrincipal currentUser, UserInvite invite, IUser createdUser);
-
- void RaiseLoginSuccessEvent(TUser currentUser, int userId);
-
bool HasSendingUserInviteEventHandler { get; }
+
}
}
diff --git a/src/Umbraco.Tests.Integration/TestServerTest/TestAuthHandler.cs b/src/Umbraco.Tests.Integration/TestServerTest/TestAuthHandler.cs
index 2cafaf913f..b9acd9529c 100644
--- a/src/Umbraco.Tests.Integration/TestServerTest/TestAuthHandler.cs
+++ b/src/Umbraco.Tests.Integration/TestServerTest/TestAuthHandler.cs
@@ -16,11 +16,11 @@ namespace Umbraco.Tests.Integration.TestServerTest
{
public const string TestAuthenticationScheme = "Test";
- private readonly BackOfficeSignInManager _backOfficeSignInManager;
+ private readonly IBackOfficeSignInManager _backOfficeSignInManager;
private readonly BackOfficeIdentityUser _fakeUser;
public TestAuthHandler(IOptionsMonitor options,
- ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, BackOfficeSignInManager backOfficeSignInManager, IUserService userService, UmbracoMapper umbracoMapper)
+ ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, IBackOfficeSignInManager backOfficeSignInManager, IUserService userService, UmbracoMapper umbracoMapper)
: base(options, logger, encoder, clock)
{
_backOfficeSignInManager = backOfficeSignInManager;
diff --git a/src/Umbraco.Tests.UnitTests/AutoFixture/AutoMoqDataAttribute.cs b/src/Umbraco.Tests.UnitTests/AutoFixture/AutoMoqDataAttribute.cs
index 5f258bcb87..78d5d5554c 100644
--- a/src/Umbraco.Tests.UnitTests/AutoFixture/AutoMoqDataAttribute.cs
+++ b/src/Umbraco.Tests.UnitTests/AutoFixture/AutoMoqDataAttribute.cs
@@ -18,6 +18,7 @@ using Umbraco.Tests.Common.Builders;
using Umbraco.Web.BackOffice.Controllers;
using Umbraco.Web.BackOffice.Routing;
using Umbraco.Web.Common.Install;
+using Umbraco.Web.Common.Security;
using Umbraco.Web.WebApi;
namespace Umbraco.Tests.UnitTests.AutoFixture
diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Security/BackOfficeCookieManagerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Security/BackOfficeCookieManagerTests.cs
index e1a8ff9c58..d45887b3c3 100644
--- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Security/BackOfficeCookieManagerTests.cs
+++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Security/BackOfficeCookieManagerTests.cs
@@ -28,8 +28,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Backoffice.Security
runtime,
Mock.Of(),
globalSettings,
- Mock.Of(),
- Mock.Of());
+ Mock.Of());
var result = mgr.ShouldAuthenticateRequest(new Uri("http://localhost/umbraco"));
@@ -47,8 +46,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Backoffice.Security
runtime,
Mock.Of(x => x.ApplicationVirtualPath == "/" && x.ToAbsolute(globalSettings.UmbracoPath) == "/umbraco"),
globalSettings,
- Mock.Of(),
- Mock.Of());
+ Mock.Of());
var result = mgr.ShouldAuthenticateRequest(new Uri("http://localhost/umbraco"));
@@ -62,13 +60,13 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Backoffice.Security
var runtime = Mock.Of(x => x.Level == RuntimeLevel.Run);
+ GetMockLinkGenerator(out var remainingTimeoutSecondsPath, out var isAuthPath);
var mgr = new BackOfficeCookieManager(
Mock.Of(),
runtime,
Mock.Of(x => x.ApplicationVirtualPath == "/" && x.ToAbsolute(globalSettings.UmbracoPath) == "/umbraco" && x.ToAbsolute(Constants.SystemDirectories.Install) == "/install"),
globalSettings,
- Mock.Of(),
- GetMockLinkGenerator(out var remainingTimeoutSecondsPath, out var isAuthPath));
+ Mock.Of());
var result = mgr.ShouldAuthenticateRequest(new Uri($"http://localhost{remainingTimeoutSecondsPath}"));
Assert.IsTrue(result);
@@ -89,8 +87,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Backoffice.Security
runtime,
Mock.Of(x => x.ApplicationVirtualPath == "/" && x.ToAbsolute(globalSettings.UmbracoPath) == "/umbraco" && x.ToAbsolute(Constants.SystemDirectories.Install) == "/install"),
globalSettings,
- Mock.Of(x => x.IsAvailable == true && x.Get(Constants.Security.ForceReAuthFlag) == "not null"),
- GetMockLinkGenerator(out var remainingTimeoutSecondsPath, out var isAuthPath));
+ Mock.Of(x => x.IsAvailable == true && x.Get(Constants.Security.ForceReAuthFlag) == "not null"));
var result = mgr.ShouldAuthenticateRequest(new Uri($"http://localhost/notbackoffice"));
Assert.IsTrue(result);
@@ -108,8 +105,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Backoffice.Security
runtime,
Mock.Of(x => x.ApplicationVirtualPath == "/" && x.ToAbsolute(globalSettings.UmbracoPath) == "/umbraco" && x.ToAbsolute(Constants.SystemDirectories.Install) == "/install"),
globalSettings,
- Mock.Of(),
- GetMockLinkGenerator(out var remainingTimeoutSecondsPath, out var isAuthPath));
+ Mock.Of());
var result = mgr.ShouldAuthenticateRequest(new Uri($"http://localhost/notbackoffice"));
Assert.IsFalse(result);
diff --git a/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs b/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs
index 664b00b513..23f7e09f5d 100644
--- a/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs
+++ b/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs
@@ -120,8 +120,6 @@ namespace Umbraco.Tests.TestHelpers.ControllerTesting
.Returns(mockUser.Object);
//mock Validate
- backofficeSecurity.Setup(x => x.ValidateCurrentUser())
- .Returns(() => true);
backofficeSecurity.Setup(x => x.UserHasSectionAccess(It.IsAny(), It.IsAny()))
.Returns(() => true);
diff --git a/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs b/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs
index 5048d43905..8b9d5d7a4a 100644
--- a/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs
+++ b/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs
@@ -116,7 +116,6 @@ namespace Umbraco.Tests.Testing.TestingTests
var memberService = Mock.Of();
var memberTypeService = Mock.Of();
var membershipProvider = new MembersMembershipProvider(memberService, memberTypeService, Mock.Of(), TestHelper.GetHostingEnvironment(), TestHelper.GetIpResolver());
- var membershipHelper = new MembershipHelper(Mock.Of(), Mock.Of(), membershipProvider, Mock.Of(), memberService, memberTypeService, Mock.Of(), AppCaches.Disabled, NullLoggerFactory.Instance, ShortStringHelper, Mock.Of());
var umbracoMapper = new UmbracoMapper(new MapDefinitionCollection(new[] { Mock.Of() }));
var umbracoApiController = new FakeUmbracoApiController(new GlobalSettings(), Mock.Of(), Mock.Of(), Mock.Of(), ServiceContext.CreatePartial(), AppCaches.NoCache, profilingLogger , Mock.Of(), umbracoMapper, Mock.Of());
diff --git a/src/Umbraco.Web.BackOffice/Authorization/BackOfficeHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/BackOfficeHandler.cs
index 6cee04deae..065f60b3db 100644
--- a/src/Umbraco.Web.BackOffice/Authorization/BackOfficeHandler.cs
+++ b/src/Umbraco.Web.BackOffice/Authorization/BackOfficeHandler.cs
@@ -22,18 +22,17 @@ namespace Umbraco.Web.BackOffice.Authorization
protected override Task IsAuthorized(AuthorizationHandlerContext context, BackOfficeRequirement requirement)
{
- try
+ // if not configured (install or upgrade) then we can continue
+ // otherwise we need to ensure that a user is logged in
+
+ switch (_runtimeState.Level)
{
- // if not configured (install or upgrade) then we can continue
- // otherwise we need to ensure that a user is logged in
- var isAuth = _runtimeState.Level == RuntimeLevel.Install
- || _runtimeState.Level == RuntimeLevel.Upgrade
- || _backOfficeSecurity.BackOfficeSecurity?.ValidateCurrentUser(false, requirement.RequireApproval) == ValidateRequestAttempt.Success;
- return Task.FromResult(isAuth);
- }
- catch (Exception)
- {
- return Task.FromResult(false);
+ case RuntimeLevel.Install:
+ case RuntimeLevel.Upgrade:
+ return Task.FromResult(true);
+ default:
+ var userApprovalSucceeded = !requirement.RequireApproval || (_backOfficeSecurity.BackOfficeSecurity.CurrentUser?.IsApproved ?? false);
+ return Task.FromResult(userApprovalSucceeded);
}
}
diff --git a/src/Umbraco.Web.BackOffice/Authorization/DenyLocalLoginHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/DenyLocalLoginHandler.cs
index 771f462b8c..96341c5b1f 100644
--- a/src/Umbraco.Web.BackOffice/Authorization/DenyLocalLoginHandler.cs
+++ b/src/Umbraco.Web.BackOffice/Authorization/DenyLocalLoginHandler.cs
@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Authorization;
using System.Threading.Tasks;
+using Umbraco.Web.BackOffice.Security;
using Umbraco.Web.Common.Security;
namespace Umbraco.Web.BackOffice.Authorization
diff --git a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs
index e78395321d..efe28763f1 100644
--- a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs
+++ b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs
@@ -2,8 +2,10 @@
using System.Collections.Generic;
using System.Linq;
using System.Net;
+using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
@@ -21,6 +23,7 @@ using Umbraco.Core.Services;
using Umbraco.Extensions;
using Umbraco.Net;
using Umbraco.Web.BackOffice.Filters;
+using Umbraco.Web.BackOffice.Security;
using Umbraco.Web.Common.ActionsResults;
using Umbraco.Web.Common.Attributes;
using Umbraco.Web.Common.Controllers;
@@ -45,12 +48,15 @@ namespace Umbraco.Web.BackOffice.Controllers
[PluginController(Constants.Web.Mvc.BackOfficeApiArea)] // TODO: Maybe this could be applied with our Application Model conventions
//[ValidationFilter] // TODO: I don't actually think this is required with our custom Application Model conventions applied
[AngularJsonOnlyConfiguration] // TODO: This could be applied with our Application Model conventions
- [IsBackOffice] // TODO: This could be applied with our Application Model conventions
+ [IsBackOffice]
public class AuthenticationController : UmbracoApiControllerBase
{
+ // NOTE: Each action must either be explicitly authorized or explicitly [AllowAnonymous], the latter is optional because
+ // this controller itself doesn't require authz but it's more clear what the intention is.
+
private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor;
private readonly IBackOfficeUserManager _userManager;
- private readonly BackOfficeSignInManager _signInManager;
+ private readonly IBackOfficeSignInManager _signInManager;
private readonly IUserService _userService;
private readonly ILocalizedTextService _textService;
private readonly UmbracoMapper _umbracoMapper;
@@ -65,14 +71,14 @@ namespace Umbraco.Web.BackOffice.Controllers
private readonly IRequestAccessor _requestAccessor;
private readonly LinkGenerator _linkGenerator;
private readonly IBackOfficeExternalLoginProviders _externalAuthenticationOptions;
+ private readonly IBackOfficeTwoFactorOptions _backOfficeTwoFactorOptions;
- // TODO: We need to import the logic from Umbraco.Web.Editors.AuthenticationController
// TODO: We need to review all _userManager.Raise calls since many/most should be on the usermanager or signinmanager, very few should be here
public AuthenticationController(
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
IBackOfficeUserManager backOfficeUserManager,
- BackOfficeSignInManager signInManager,
+ IBackOfficeSignInManager signInManager,
IUserService userService,
ILocalizedTextService textService,
UmbracoMapper umbracoMapper,
@@ -86,7 +92,8 @@ namespace Umbraco.Web.BackOffice.Controllers
Core.Hosting.IHostingEnvironment hostingEnvironment,
IRequestAccessor requestAccessor,
LinkGenerator linkGenerator,
- IBackOfficeExternalLoginProviders externalAuthenticationOptions)
+ IBackOfficeExternalLoginProviders externalAuthenticationOptions,
+ IBackOfficeTwoFactorOptions backOfficeTwoFactorOptions)
{
_backofficeSecurityAccessor = backofficeSecurityAccessor;
_userManager = backOfficeUserManager;
@@ -105,6 +112,7 @@ namespace Umbraco.Web.BackOffice.Controllers
_requestAccessor = requestAccessor;
_linkGenerator = linkGenerator;
_externalAuthenticationOptions = externalAuthenticationOptions;
+ _backOfficeTwoFactorOptions = backOfficeTwoFactorOptions;
}
///
@@ -164,7 +172,6 @@ namespace Umbraco.Web.BackOffice.Controllers
var user = await _userManager.FindByIdAsync(User.Identity.GetUserId());
if (user == null) throw new InvalidOperationException("Could not find user");
- ExternalSignInAutoLinkOptions autoLinkOptions = null;
var authType = (await _signInManager.GetExternalAuthenticationSchemesAsync())
.FirstOrDefault(x => x.Name == unlinkLoginModel.LoginProvider);
@@ -174,11 +181,18 @@ namespace Umbraco.Web.BackOffice.Controllers
}
else
{
- autoLinkOptions = _externalAuthenticationOptions.Get(authType.Name);
- if (!autoLinkOptions.AllowManualLinking)
+ var opt = _externalAuthenticationOptions.Get(authType.Name);
+ if (opt == null)
{
- // If AllowManualLinking is disabled for this provider we cannot unlink
- return BadRequest();
+ return BadRequest($"Could not find external authentication options registered for provider {unlinkLoginModel.LoginProvider}");
+ }
+ else
+ {
+ if (!opt.Options.AutoLinkOptions.AllowManualLinking)
+ {
+ // If AllowManualLinking is disabled for this provider we cannot unlink
+ return BadRequest();
+ }
}
}
@@ -200,18 +214,27 @@ namespace Umbraco.Web.BackOffice.Controllers
}
[HttpGet]
- public double GetRemainingTimeoutSeconds()
+ [AllowAnonymous]
+ public async Task GetRemainingTimeoutSeconds()
{
- var backOfficeIdentity = HttpContext.User.GetUmbracoIdentity();
- var remainingSeconds = HttpContext.User.GetRemainingAuthSeconds();
- if (remainingSeconds <= 30 && backOfficeIdentity != null)
+ // force authentication to occur since this is not an authorized endpoint
+ var result = await this.AuthenticateBackOfficeAsync();
+ if (!result.Succeeded)
{
+ return 0;
+ }
+
+ var remainingSeconds = result.Principal.GetRemainingAuthSeconds();
+ if (remainingSeconds <= 30)
+ {
+ var username = result.Principal.FindFirst(ClaimTypes.Name)?.Value;
+
//NOTE: We are using 30 seconds because that is what is coded into angular to force logout to give some headway in
// the timeout process.
_logger.LogInformation(
"User logged will be logged out due to timeout: {Username}, IP Address: {IPAddress}",
- backOfficeIdentity.Name,
+ username ?? "unknown",
_ipResolver.GetCurrentRequestIpAddress());
}
@@ -223,14 +246,12 @@ namespace Umbraco.Web.BackOffice.Controllers
///
///
[HttpGet]
- public bool IsAuthenticated()
+ [AllowAnonymous]
+ public async Task IsAuthenticated()
{
- var attempt = _backofficeSecurityAccessor.BackOfficeSecurity.AuthorizeRequest();
- if (attempt == ValidateRequestAttempt.Success)
- {
- return true;
- }
- return false;
+ // force authentication to occur since this is not an authorized endpoint
+ var result = await this.AuthenticateBackOfficeAsync();
+ return result.Succeeded;
}
///
@@ -244,7 +265,7 @@ namespace Umbraco.Web.BackOffice.Controllers
///
[Authorize(Policy = AuthorizationPolicies.BackOfficeAccess)]
[SetAngularAntiForgeryTokens]
- //[CheckIfUserTicketDataIsStale] // TODO: Migrate this, though it will need to be done differently at the cookie auth level
+ [CheckIfUserTicketDataIsStale]
public UserDetail GetCurrentUser()
{
var user = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser;
@@ -291,7 +312,7 @@ namespace Umbraco.Web.BackOffice.Controllers
///
[SetAngularAntiForgeryTokens]
[Authorize(Policy = AuthorizationPolicies.DenyLocalLoginIfConfigured)]
- public async Task PostLogin(LoginModel loginModel)
+ public async Task> PostLogin(LoginModel loginModel)
{
// Sign the user in with username/password, this also gives a chance for developers to
// custom verify the credentials and auto-link user accounts with a custom IBackOfficePasswordChecker
@@ -306,42 +327,25 @@ namespace Umbraco.Web.BackOffice.Controllers
if (result.RequiresTwoFactor)
{
- throw new NotImplementedException("Implement MFA/2FA, we need to have some IOptions or similar to configure this");
+ var twofactorView = _backOfficeTwoFactorOptions.GetTwoFactorView(loginModel.Username);
+ if (twofactorView.IsNullOrWhiteSpace())
+ {
+ return new ValidationErrorResult($"The registered {typeof(IBackOfficeTwoFactorOptions)} of type {_backOfficeTwoFactorOptions.GetType()} did not return a view for two factor auth ");
+ }
- //var twofactorOptions = UserManager as IUmbracoBackOfficeTwoFactorOptions;
- //if (twofactorOptions == null)
- //{
- // throw new HttpResponseException(
- // Request.CreateErrorResponse(
- // HttpStatusCode.BadRequest,
- // "UserManager does not implement " + typeof(IUmbracoBackOfficeTwoFactorOptions)));
- //}
+ var attemptedUser = _userService.GetByUsername(loginModel.Username);
- //var twofactorView = twofactorOptions.GetTwoFactorView(
- // owinContext,
- // UmbracoContext,
- // loginModel.Username);
+ // create a with information to display a custom two factor send code view
+ var verifyResponse = new ObjectResult(new
+ {
+ twoFactorView = twofactorView,
+ userId = attemptedUser.Id
+ })
+ {
+ StatusCode = StatusCodes.Status402PaymentRequired
+ };
- //if (twofactorView.IsNullOrWhiteSpace())
- //{
- // throw new HttpResponseException(
- // Request.CreateErrorResponse(
- // HttpStatusCode.BadRequest,
- // typeof(IUmbracoBackOfficeTwoFactorOptions) + ".GetTwoFactorView returned an empty string"));
- //}
-
- //var attemptedUser = Services.UserService.GetByUsername(loginModel.Username);
-
- //// create a with information to display a custom two factor send code view
- //var verifyResponse = Request.CreateResponse(HttpStatusCode.PaymentRequired, new
- //{
- // twoFactorView = twofactorView,
- // userId = attemptedUser.Id
- //});
-
- //_userManager.RaiseLoginRequiresVerificationEvent(User, attemptedUser.Id);
-
- //return verifyResponse;
+ return verifyResponse;
}
// return BadRequest (400), we don't want to return a 401 because that get's intercepted
@@ -400,6 +404,7 @@ namespace Umbraco.Web.BackOffice.Controllers
///
///
[SetAngularAntiForgeryTokens]
+ [AllowAnonymous]
public async Task>> Get2FAProviders()
{
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
@@ -414,6 +419,7 @@ namespace Umbraco.Web.BackOffice.Controllers
}
[SetAngularAntiForgeryTokens]
+ [AllowAnonymous]
public async Task PostSend2FACode([FromBody] string provider)
{
if (provider.IsNullOrWhiteSpace())
@@ -459,6 +465,7 @@ namespace Umbraco.Web.BackOffice.Controllers
}
[SetAngularAntiForgeryTokens]
+ [AllowAnonymous]
public async Task> PostVerify2FACode(Verify2FACodeModel model)
{
if (ModelState.IsValid == false)
@@ -481,14 +488,14 @@ namespace Umbraco.Web.BackOffice.Controllers
if (result.IsLockedOut)
{
- throw HttpResponseException.CreateValidationErrorResponse("User is locked out");
+ return new ValidationErrorResult("User is locked out");
}
if (result.IsNotAllowed)
{
- throw HttpResponseException.CreateValidationErrorResponse("User is not allowed");
+ return new ValidationErrorResult("User is not allowed");
}
- throw HttpResponseException.CreateValidationErrorResponse("Invalid code");
+ return new ValidationErrorResult("Invalid code");
}
///
@@ -496,6 +503,7 @@ namespace Umbraco.Web.BackOffice.Controllers
///
///
[SetAngularAntiForgeryTokens]
+ [AllowAnonymous]
public async Task PostSetPassword(SetPasswordModel model)
{
var identityUser = await _userManager.FindByIdAsync(model.UserId.ToString());
@@ -560,13 +568,18 @@ namespace Umbraco.Web.BackOffice.Controllers
///
///
[ValidateAngularAntiForgeryToken]
- public IActionResult PostLogout()
+ [AllowAnonymous]
+ public async Task PostLogout()
{
- HttpContext.SignOutAsync(Constants.Security.BackOfficeAuthenticationType);
+ // force authentication to occur since this is not an authorized endpoint
+ var result = await this.AuthenticateBackOfficeAsync();
+ if (!result.Succeeded) return Ok();
+
+ await _signInManager.SignOutAsync();
_logger.LogInformation("User {UserName} from IP address {RemoteIpAddress} has logged out", User.Identity == null ? "UNKNOWN" : User.Identity.Name, HttpContext.Connection.RemoteIpAddress);
- var userId = int.Parse(User.Identity.GetUserId());
+ var userId = int.Parse(result.Principal.Identity.GetUserId());
var args = _userManager.RaiseLogoutSuccessEvent(User, userId);
if (!args.SignOutRedirectUrl.IsNullOrWhiteSpace())
{
@@ -580,7 +593,6 @@ namespace Umbraco.Web.BackOffice.Controllers
}
-
///
/// Return the for the given
///
diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs
index 249de5458c..1ce0831502 100644
--- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs
+++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs
@@ -5,7 +5,6 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
-using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Logging;
@@ -33,17 +32,23 @@ using Constants = Umbraco.Core.Constants;
using Microsoft.AspNetCore.Identity;
using System.Security.Claims;
using Microsoft.AspNetCore.Http;
-using Umbraco.Web.Security;
+using Umbraco.Web.BackOffice.Security;
+using Umbraco.Web.Common.ActionsResults;
using Microsoft.AspNetCore.Authorization;
using Umbraco.Web.Common.Authorization;
+using Microsoft.AspNetCore.Authentication;
namespace Umbraco.Web.BackOffice.Controllers
{
- [DisableBrowserCache] //TODO Reintroduce
- //[UmbracoRequireHttps] //TODO Reintroduce
+ [DisableBrowserCache]
+ [UmbracoRequireHttps]
[PluginController(Constants.Web.Mvc.BackOfficeArea)]
+ [IsBackOffice]
public class BackOfficeController : UmbracoController
{
+ // NOTE: Each action must either be explicitly authorized or explicitly [AllowAnonymous], the latter is optional because
+ // this controller itself doesn't require authz but it's more clear what the intention is.
+
private readonly IBackOfficeUserManager _userManager;
private readonly IRuntimeMinifier _runtimeMinifier;
private readonly GlobalSettings _globalSettings;
@@ -52,12 +57,13 @@ namespace Umbraco.Web.BackOffice.Controllers
private readonly IGridConfig _gridConfig;
private readonly BackOfficeServerVariables _backOfficeServerVariables;
private readonly AppCaches _appCaches;
- private readonly BackOfficeSignInManager _signInManager;
+ private readonly IBackOfficeSignInManager _signInManager;
private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor;
private readonly ILogger _logger;
private readonly IJsonSerializer _jsonSerializer;
private readonly IBackOfficeExternalLoginProviders _externalLogins;
private readonly IHttpContextAccessor _httpContextAccessor;
+ private readonly IBackOfficeTwoFactorOptions _backOfficeTwoFactorOptions;
public BackOfficeController(
IBackOfficeUserManager userManager,
@@ -68,12 +74,13 @@ namespace Umbraco.Web.BackOffice.Controllers
IGridConfig gridConfig,
BackOfficeServerVariables backOfficeServerVariables,
AppCaches appCaches,
- BackOfficeSignInManager signInManager,
+ IBackOfficeSignInManager signInManager,
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
ILogger logger,
IJsonSerializer jsonSerializer,
IBackOfficeExternalLoginProviders externalLogins,
- IHttpContextAccessor httpContextAccessor)
+ IHttpContextAccessor httpContextAccessor,
+ IBackOfficeTwoFactorOptions backOfficeTwoFactorOptions)
{
_userManager = userManager;
_runtimeMinifier = runtimeMinifier;
@@ -89,26 +96,35 @@ namespace Umbraco.Web.BackOffice.Controllers
_jsonSerializer = jsonSerializer;
_externalLogins = externalLogins;
_httpContextAccessor = httpContextAccessor;
+ _backOfficeTwoFactorOptions = backOfficeTwoFactorOptions;
}
[HttpGet]
+ [AllowAnonymous]
public async Task Default()
{
+ // force authentication to occur since this is not an authorized endpoint
+ var result = await this.AuthenticateBackOfficeAsync();
+
var viewPath = Path.Combine(_globalSettings.UmbracoPath , Constants.Web.Mvc.BackOfficeArea, nameof(Default) + ".cshtml")
.Replace("\\", "/"); // convert to forward slashes since it's a virtual path
return await RenderDefaultOrProcessExternalLoginAsync(
+ result,
() => View(viewPath),
() => View(viewPath));
}
[HttpGet]
+ [AllowAnonymous]
public async Task VerifyInvite(string invite)
{
+ var authenticate = await this.AuthenticateBackOfficeAsync();
+
//if you are hitting VerifyInvite, you're already signed in as a different user, and the token is invalid
//you'll exit on one of the return RedirectToAction(nameof(Default)) but you're still logged in so you just get
//dumped at the default admin view with no detail
- if (_backofficeSecurityAccessor.BackOfficeSecurity.IsAuthenticated())
+ if (authenticate.Succeeded)
{
await _signInManager.SignOutAsync();
}
@@ -170,10 +186,16 @@ namespace Umbraco.Web.BackOffice.Controllers
///
[HttpGet]
[StatusCodeResult(System.Net.HttpStatusCode.ServiceUnavailable)]
+ [AllowAnonymous]
public async Task AuthorizeUpgrade()
{
- var viewPath = Path.Combine(_globalSettings.UmbracoPath, Umbraco.Core.Constants.Web.Mvc.BackOfficeArea, nameof(AuthorizeUpgrade) + ".cshtml");
+ // force authentication to occur since this is not an authorized endpoint
+ var result = await this.AuthenticateBackOfficeAsync();
+
+ var viewPath = Path.Combine(_globalSettings.UmbracoPath, Constants.Web.Mvc.BackOfficeArea, nameof(AuthorizeUpgrade) + ".cshtml");
+
return await RenderDefaultOrProcessExternalLoginAsync(
+ result,
//The default view to render when there is no external login info or errors
() => View(viewPath),
//The IActionResult to perform if external login is successful
@@ -186,6 +208,7 @@ namespace Umbraco.Web.BackOffice.Controllers
///
[MinifyJavaScriptResult(Order = 0)]
[HttpGet]
+ [AllowAnonymous]
public async Task Application()
{
var result = await _runtimeMinifier.GetScriptForLoadingBackOfficeAsync(_globalSettings, _hostingEnvironment);
@@ -199,6 +222,7 @@ namespace Umbraco.Web.BackOffice.Controllers
///
///
[HttpGet]
+ [AllowAnonymous]
public Dictionary> LocalizedText(string culture = null)
{
var isAuthenticated = _backofficeSecurityAccessor.BackOfficeSecurity.IsAuthenticated();
@@ -261,6 +285,7 @@ namespace Umbraco.Web.BackOffice.Controllers
}
[HttpPost]
+ [AllowAnonymous]
public ActionResult ExternalLogin(string provider, string redirectUrl = null)
{
if (redirectUrl == null)
@@ -268,10 +293,9 @@ namespace Umbraco.Web.BackOffice.Controllers
redirectUrl = Url.Action(nameof(Default), this.GetControllerName());
}
+ // Configures the redirect URL and user identifier for the specified external login
var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
- // TODO: I believe we will have to fill in our own XsrfKey like we use to do since I think
- // we validate against that key?
- // see https://github.com/umbraco/Umbraco-CMS/blob/v8/contrib/src/Umbraco.Web/Editors/ChallengeResult.cs#L48
+
return Challenge(properties, provider);
}
@@ -286,14 +310,15 @@ namespace Umbraco.Web.BackOffice.Controllers
{
// Request a redirect to the external login provider to link a login for the current user
var redirectUrl = Url.Action(nameof(ExternalLinkLoginCallback), this.GetControllerName());
- var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl, User.Identity.GetUserId());
- // TODO: I believe we will have to fill in our own XsrfKey like we use to do since I think
- // we validate against that key?
- // see https://github.com/umbraco/Umbraco-CMS/blob/v8/contrib/src/Umbraco.Web/Editors/ChallengeResult.cs#L48
+
+ // Configures the redirect URL and user identifier for the specified external login including xsrf data
+ var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl, _userManager.GetUserId(User));
+
return Challenge(properties, provider);
}
[HttpGet]
+ [AllowAnonymous]
public async Task ValidatePasswordResetCode([Bind(Prefix = "u")]int userId, [Bind(Prefix = "r")]string resetCode)
{
var user = await _userManager.FindByIdAsync(userId.ToString());
@@ -320,12 +345,15 @@ namespace Umbraco.Web.BackOffice.Controllers
[HttpGet]
public async Task ExternalLinkLoginCallback()
{
- // TODO: Do we need/want to tell it an expected xsrf.
- // In v8 the xsrf used to be set to the user id which was verified manually, in this case I think we don't specify
- // the key and that is up to the underlying sign in manager to set so we'd just tell it to expect the user id,
- // the XSRF value used to be set in our ChallengeResult but now we don't have that so this needs to be set in the
- // BackOfficeController when we issue a Challenge, see TODO notes there.
- var loginInfo = await _signInManager.GetExternalLoginInfoAsync();
+ var user = await _userManager.GetUserAsync(User);
+ if (user == null)
+ {
+ // ... this should really not happen
+ TempData[ViewDataExtensions.TokenExternalSignInError] = new[] { "Local user does not exist" };
+ return RedirectToLocal(Url.Action(nameof(Default), this.GetControllerName()));
+ }
+
+ var loginInfo = await _signInManager.GetExternalLoginInfoAsync(await _userManager.GetUserIdAsync(user));
if (loginInfo == null)
{
@@ -334,14 +362,6 @@ namespace Umbraco.Web.BackOffice.Controllers
return RedirectToLocal(Url.Action(nameof(Default), this.GetControllerName()));
}
- var user = await _userManager.FindByIdAsync(User.Identity.GetUserId());
- if (user == null)
- {
- // ... this should really not happen
- TempData[ViewDataExtensions.TokenExternalSignInError] = new[] { "Local user does not exist" };
- return RedirectToLocal(Url.Action(nameof(Default), this.GetControllerName()));
- }
-
var addLoginResult = await _userManager.AddLoginAsync(user, loginInfo);
if (addLoginResult.Succeeded)
{
@@ -364,6 +384,7 @@ namespace Umbraco.Web.BackOffice.Controllers
///
///
private async Task RenderDefaultOrProcessExternalLoginAsync(
+ AuthenticateResult authenticateResult,
Func defaultResponse,
Func externalSignInResponse)
{
@@ -384,7 +405,7 @@ namespace Umbraco.Web.BackOffice.Controllers
if (loginInfo == null || loginInfo.Principal == null)
{
// if the user is not logged in, check if there's any auto login redirects specified
- if (!_backofficeSecurityAccessor.BackOfficeSecurity.ValidateCurrentUser())
+ if (!authenticateResult.Succeeded)
{
var oauthRedirectAuthProvider = _externalLogins.GetAutoLoginProvider();
if (!oauthRedirectAuthProvider.IsNullOrWhiteSpace())
@@ -404,186 +425,83 @@ namespace Umbraco.Web.BackOffice.Controllers
{
if (loginInfo == null) throw new ArgumentNullException(nameof(loginInfo));
if (response == null) throw new ArgumentNullException(nameof(response));
- ExternalSignInAutoLinkOptions autoLinkOptions = null;
- var authType = (await _signInManager.GetExternalAuthenticationSchemesAsync())
- .FirstOrDefault(x => x.Name == loginInfo.LoginProvider);
+ // Sign in the user with this external login provider (which auto links, etc...)
+ var result = await _signInManager.ExternalLoginSignInAsync(loginInfo, isPersistent: false);
- if (authType == null)
+ var errors = new List();
+
+ if (result == Microsoft.AspNetCore.Identity.SignInResult.Success)
{
- _logger.LogWarning("Could not find external authentication provider registered: {LoginProvider}", loginInfo.LoginProvider);
- }
- else
- {
- autoLinkOptions = _externalLogins.Get(authType.Name);
- }
- // Sign in the user with this external login provider if the user already has a login
-
- var user = await _userManager.FindByLoginAsync(loginInfo.LoginProvider, loginInfo.ProviderKey);
- if (user != null)
+ }
+ else if (result == Microsoft.AspNetCore.Identity.SignInResult.TwoFactorRequired)
{
- var shouldSignIn = true;
- if (autoLinkOptions != null && autoLinkOptions.OnExternalLogin != null)
+
+ var attemptedUser = await _userManager.FindByLoginAsync(loginInfo.LoginProvider, loginInfo.ProviderKey);
+ if (attemptedUser == null)
{
- shouldSignIn = autoLinkOptions.OnExternalLogin(user, loginInfo);
- if (shouldSignIn == false)
- {
- _logger.LogWarning("The AutoLinkOptions of the external authentication provider '{LoginProvider}' have refused the login based on the OnExternalLogin method. Affected user id: '{UserId}'", loginInfo.LoginProvider, user.Id);
- }
+ return new ValidationErrorResult($"No local user found for the login provider {loginInfo.LoginProvider} - {loginInfo.ProviderKey}");
}
- if (shouldSignIn)
+ var twofactorView = _backOfficeTwoFactorOptions.GetTwoFactorView(attemptedUser.UserName);
+ if (twofactorView.IsNullOrWhiteSpace())
{
- //sign in
- await _signInManager.SignInAsync(user, false);
- }
- }
- else
- {
- if (await AutoLinkAndSignInExternalAccount(loginInfo, autoLinkOptions) == false)
- {
- ViewData.SetExternalSignInProviderErrors(
- new BackOfficeExternalLoginProviderErrors(
- loginInfo.LoginProvider,
- new[] { "The requested provider (" + loginInfo.LoginProvider + ") has not been linked to an account" }));
+ return new ValidationErrorResult($"The registered {typeof(IBackOfficeTwoFactorOptions)} of type {_backOfficeTwoFactorOptions.GetType()} did not return a view for two factor auth ");
}
- //Remove the cookie otherwise this message will keep appearing
- Response.Cookies.Delete(Constants.Security.BackOfficeExternalCookieName);
+ // create a with information to display a custom two factor send code view
+ var verifyResponse = new ObjectResult(new
+ {
+ twoFactorView = twofactorView,
+ userId = attemptedUser.Id
+ })
+ {
+ StatusCode = StatusCodes.Status402PaymentRequired
+ };
+
+ return verifyResponse;
+
+ }
+ else if (result == Microsoft.AspNetCore.Identity.SignInResult.LockedOut)
+ {
+ errors.Add($"The local user {loginInfo.Principal.Identity.Name} for the external provider {loginInfo.ProviderDisplayName} is locked out.");
+ }
+ else if (result == Microsoft.AspNetCore.Identity.SignInResult.NotAllowed)
+ {
+ // This occurs when SignInManager.CanSignInAsync fails which is when RequireConfirmedEmail , RequireConfirmedPhoneNumber or RequireConfirmedAccount fails
+ // however since we don't enforce those rules (yet) this shouldn't happen.
+ errors.Add($"The user {loginInfo.Principal.Identity.Name} for the external provider {loginInfo.ProviderDisplayName} has not confirmed their details and cannot sign in.");
+ }
+ else if (result == Microsoft.AspNetCore.Identity.SignInResult.Failed)
+ {
+ // Failed only occurs when the user does not exist
+ errors.Add("The requested provider (" + loginInfo.LoginProvider + ") has not been linked to an account, the provider must be linked from the back office.");
+ }
+ else if (result == AutoLinkSignInResult.FailedNotLinked)
+ {
+ errors.Add("The requested provider (" + loginInfo.LoginProvider + ") has not been linked to an account, the provider must be linked from the back office.");
+ }
+ else if (result == AutoLinkSignInResult.FailedNoEmail)
+ {
+ errors.Add($"The requested provider ({loginInfo.LoginProvider}) has not provided the email claim {ClaimTypes.Email}, the account cannot be linked.");
+ }
+ else if (result is AutoLinkSignInResult autoLinkSignInResult && autoLinkSignInResult.Errors.Count > 0)
+ {
+ errors.AddRange(autoLinkSignInResult.Errors);
+ }
+
+ if (errors.Count > 0)
+ {
+ ViewData.SetExternalSignInProviderErrors(
+ new BackOfficeExternalLoginProviderErrors(
+ loginInfo.LoginProvider,
+ errors));
}
return response();
}
- private async Task AutoLinkAndSignInExternalAccount(ExternalLoginInfo loginInfo, ExternalSignInAutoLinkOptions autoLinkOptions)
- {
- if (autoLinkOptions == null)
- return false;
-
- if (autoLinkOptions.AutoLinkExternalAccount == false)
- return true; // TODO: This seems weird to return true, but it was like that before so must be a reason?
-
- var email = loginInfo.Principal.FindFirstValue(ClaimTypes.Email);
-
- //we are allowing auto-linking/creating of local accounts
- if (email.IsNullOrWhiteSpace())
- {
- ViewData.SetExternalSignInProviderErrors(
- new BackOfficeExternalLoginProviderErrors(
- loginInfo.LoginProvider,
- new[] { $"The requested provider ({loginInfo.LoginProvider}) has not provided the email claim {ClaimTypes.Email}, the account cannot be linked." }));
- }
- else
- {
- //Now we need to perform the auto-link, so first we need to lookup/create a user with the email address
- var autoLinkUser = await _userManager.FindByEmailAsync(email);
- if (autoLinkUser != null)
- {
- try
- {
- //call the callback if one is assigned
- autoLinkOptions.OnAutoLinking?.Invoke(autoLinkUser, loginInfo);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Could not link login provider {LoginProvider}.", loginInfo.LoginProvider);
- ViewData.SetExternalSignInProviderErrors(
- new BackOfficeExternalLoginProviderErrors(
- loginInfo.LoginProvider,
- new[] { "Could not link login provider " + loginInfo.LoginProvider + ". " + ex.Message }));
- return true;
- }
-
- await LinkUser(autoLinkUser, loginInfo);
- }
- else
- {
- var name = loginInfo.Principal?.Identity?.Name;
- if (name.IsNullOrWhiteSpace()) throw new InvalidOperationException("The Name value cannot be null");
-
- autoLinkUser = BackOfficeIdentityUser.CreateNew(_globalSettings, email, email, autoLinkOptions.GetUserAutoLinkCulture(_globalSettings), name);
-
- foreach (var userGroup in autoLinkOptions.DefaultUserGroups)
- {
- autoLinkUser.AddRole(userGroup);
- }
-
- //call the callback if one is assigned
- try
- {
- autoLinkOptions.OnAutoLinking?.Invoke(autoLinkUser, loginInfo);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Could not link login provider {LoginProvider}.", loginInfo.LoginProvider);
- ViewData.SetExternalSignInProviderErrors(
- new BackOfficeExternalLoginProviderErrors(
- loginInfo.LoginProvider,
- new[] { "Could not link login provider " + loginInfo.LoginProvider + ". " + ex.Message }));
- return true;
- }
-
- var userCreationResult = await _userManager.CreateAsync(autoLinkUser);
-
- if (userCreationResult.Succeeded == false)
- {
- ViewData.SetExternalSignInProviderErrors(
- new BackOfficeExternalLoginProviderErrors(
- loginInfo.LoginProvider,
- userCreationResult.Errors.Select(x => x.Description).ToList()));
- }
- else
- {
- await LinkUser(autoLinkUser, loginInfo);
- }
- }
- }
- return true;
- }
-
- private async Task LinkUser(BackOfficeIdentityUser autoLinkUser, ExternalLoginInfo loginInfo)
- {
- var existingLogins = await _userManager.GetLoginsAsync(autoLinkUser);
- var exists = existingLogins.FirstOrDefault(x => x.LoginProvider == loginInfo.LoginProvider && x.ProviderKey == loginInfo.ProviderKey);
-
- // if it already exists (perhaps it was added in the AutoLink callbak) then we just continue
- if (exists != null)
- {
- //sign in
- await _signInManager.SignInAsync(autoLinkUser, isPersistent: false);
- return;
- }
-
- var linkResult = await _userManager.AddLoginAsync(autoLinkUser, loginInfo);
- if (linkResult.Succeeded)
- {
- //we're good! sign in
- await _signInManager.SignInAsync(autoLinkUser, isPersistent: false);
- return;
- }
-
- ViewData.SetExternalSignInProviderErrors(
- new BackOfficeExternalLoginProviderErrors(
- loginInfo.LoginProvider,
- linkResult.Errors.Select(x => x.Description).ToList()));
-
- //If this fails, we should really delete the user since it will be in an inconsistent state!
- var deleteResult = await _userManager.DeleteAsync(autoLinkUser);
- if (!deleteResult.Succeeded)
- {
- //DOH! ... this isn't good, combine all errors to be shown
- ViewData.SetExternalSignInProviderErrors(
- new BackOfficeExternalLoginProviderErrors(
- loginInfo.LoginProvider,
- linkResult.Errors.Concat(deleteResult.Errors).Select(x => x.Description).ToList()));
- }
- }
-
- // Used for XSRF protection when adding external logins
- // TODO: This is duplicated in BackOfficeSignInManager
- private const string XsrfKey = "XsrfId";
-
private IActionResult RedirectToLocal(string returnUrl)
{
if (Url.IsLocalUrl(returnUrl))
diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs
index c0cf999e8b..07f20e1e03 100644
--- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs
+++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs
@@ -17,9 +17,9 @@ using Umbraco.Web.BackOffice.HealthCheck;
using Umbraco.Web.BackOffice.Profiling;
using Umbraco.Web.BackOffice.PropertyEditors;
using Umbraco.Web.BackOffice.Routing;
+using Umbraco.Web.BackOffice.Security;
using Umbraco.Web.BackOffice.Trees;
using Umbraco.Web.Common.Attributes;
-using Umbraco.Web.Common.Security;
using Umbraco.Web.Features;
using Umbraco.Web.Models.ContentEditing;
using Umbraco.Web.Trees;
@@ -135,7 +135,7 @@ namespace Umbraco.Web.BackOffice.Controllers
/// Returns the server variables for authenticated users
///
///
- internal async Task> GetServerVariablesAsync()
+ internal Task> GetServerVariablesAsync()
{
var globalSettings = _globalSettings;
var backOfficeControllerName = ControllerExtensions.GetControllerName();
@@ -149,8 +149,8 @@ namespace Umbraco.Web.BackOffice.Controllers
// having each URL defined here explicitly - we can do that in v8! for now
// for umbraco services we'll stick to explicitly defining the endpoints.
- // {"externalLoginsUrl", _linkGenerator.GetPathByAction(nameof(BackOfficeController.ExternalLogin), backOfficeControllerName, new { area = Constants.Web.Mvc.BackOfficeArea })},
- // {"externalLinkLoginsUrl", _linkGenerator.GetPathByAction(nameof(BackOfficeController.LinkLogin), backOfficeControllerName, new { area = Constants.Web.Mvc.BackOfficeArea })},
+ {"externalLoginsUrl", _linkGenerator.GetPathByAction(nameof(BackOfficeController.ExternalLogin), backOfficeControllerName, new { area = Constants.Web.Mvc.BackOfficeArea })},
+ {"externalLinkLoginsUrl", _linkGenerator.GetPathByAction(nameof(BackOfficeController.LinkLogin), backOfficeControllerName, new { area = Constants.Web.Mvc.BackOfficeArea })},
{"gridConfig", _linkGenerator.GetPathByAction(nameof(BackOfficeController.GetGridConfig), backOfficeControllerName, new { area = Constants.Web.Mvc.BackOfficeArea })},
// TODO: This is ultra confusing! this same key is used for different things, when returning the full app when authenticated it is this URL but when not auth'd it's actually the ServerVariables address
{"serverVarsJs", _linkGenerator.GetPathByAction(nameof(BackOfficeController.Application), backOfficeControllerName, new { area = Constants.Web.Mvc.BackOfficeArea })},
@@ -418,11 +418,14 @@ namespace Umbraco.Web.BackOffice.Controllers
"externalLogins", new Dictionary
{
{
+ // TODO: It would be nicer to not have to manually translate these properties
+ // but then needs to be changed in quite a few places in angular
"providers", _externalLogins.GetBackOfficeProviders()
.Select(p => new
{
- authType = p.AuthenticationType, caption = p.Name,
- properties = p.Properties
+ authType = p.AuthenticationType,
+ caption = p.Name,
+ properties = p.Options
})
.ToArray()
}
@@ -441,7 +444,7 @@ namespace Umbraco.Web.BackOffice.Controllers
}
}
};
- return defaultVals;
+ return Task.FromResult(defaultVals);
}
[DataContract]
diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs
index 742838c224..10eefcc47a 100644
--- a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs
+++ b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs
@@ -135,7 +135,7 @@ namespace Umbraco.Web.BackOffice.Controllers
///
///
[HttpGet]
- // TODO: Does this override work? What is best practices for this?
+ // TODO: We need to move this since we are going to delete OverrideAuthorization
[Authorize(Policy = AuthorizationPolicies.BackOfficeAccess), OverrideAuthorization]
public bool AllowsCultureVariation()
{
diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentTypeController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentTypeController.cs
index b00c1e2d33..8d6adee475 100644
--- a/src/Umbraco.Web.BackOffice/Controllers/ContentTypeController.cs
+++ b/src/Umbraco.Web.BackOffice/Controllers/ContentTypeController.cs
@@ -5,7 +5,6 @@ using System.Linq;
using System.Net;
using System.Net.Mime;
using System.Text;
-using System.Threading.Tasks;
using System.Xml;
using System.Xml.Linq;
using Microsoft.Extensions.Logging;
@@ -38,18 +37,15 @@ using Umbraco.Web.Common.Authorization;
namespace Umbraco.Web.BackOffice.Controllers
{
- // TODO: We'll need to be careful about the security on this controller, when we start implementing
- // methods to modify content types we'll need to enforce security on the individual methods, we
- // cannot put security on the whole controller because things like
- // GetAllowedChildren, GetPropertyTypeScaffold, GetAllPropertyTypeAliases are required for content editing.
-
///
/// An API controller used for dealing with content types
///
- [PluginController(Constants.Web.Mvc.BackOfficeApiArea)]
- [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)]
+ [PluginController(Constants.Web.Mvc.BackOfficeApiArea)]
public class ContentTypeController : ContentTypeControllerBase
{
+ // TODO: Split this controller apart so that authz is consistent, currently we need to authz each action individually.
+ // It would be possible to have something like a ContentTypeInfoController for the GetAllPropertyTypeAliases/GetCount/GetAllowedChildren/etc... actions
+
private readonly IEntityXmlSerializer _serializer;
private readonly GlobalSettings _globalSettings;
private readonly PropertyEditorCollection _propertyEditors;
@@ -126,6 +122,7 @@ namespace Umbraco.Web.BackOffice.Controllers
_jsonSerializer = jsonSerializer;
}
+ [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)]
public int GetCount()
{
return _contentTypeService.Count();
@@ -144,6 +141,7 @@ namespace Umbraco.Web.BackOffice.Controllers
///
///
[DetermineAmbiguousActionByPassingParameters]
+ [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)]
public DocumentTypeDisplay GetById(int id)
{
var ct = _contentTypeService.Get(id);
@@ -162,6 +160,7 @@ namespace Umbraco.Web.BackOffice.Controllers
///
///
[DetermineAmbiguousActionByPassingParameters]
+ [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)]
public DocumentTypeDisplay GetById(Guid id)
{
var contentType = _contentTypeService.Get(id);
@@ -180,6 +179,7 @@ namespace Umbraco.Web.BackOffice.Controllers
///
///
[DetermineAmbiguousActionByPassingParameters]
+ [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)]
public DocumentTypeDisplay GetById(Udi id)
{
var guidUdi = id as GuidUdi;
@@ -203,6 +203,7 @@ namespace Umbraco.Web.BackOffice.Controllers
///
[HttpDelete]
[HttpPost]
+ [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)]
public IActionResult DeleteById(int id)
{
var foundType = _contentTypeService.Get(id);
@@ -243,6 +244,7 @@ namespace Umbraco.Web.BackOffice.Controllers
///
///
[HttpPost]
+ [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)]
public IActionResult GetAvailableCompositeContentTypes(GetAvailableCompositionsFilter filter)
{
var result = PerformGetAvailableCompositeContentTypes(filter.ContentTypeId, UmbracoObjectTypes.DocumentType, filter.FilterContentTypes, filter.FilterPropertyTypes, filter.IsElement)
@@ -260,6 +262,7 @@ namespace Umbraco.Web.BackOffice.Controllers
///
///
[HttpPost]
+ [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)]
public IActionResult GetWhereCompositionIsUsedInContentTypes(GetAvailableCompositionsFilter filter)
{
var result = PerformGetWhereCompositionIsUsedInContentTypes(filter.ContentTypeId, UmbracoObjectTypes.DocumentType)
@@ -299,6 +302,7 @@ namespace Umbraco.Web.BackOffice.Controllers
///
[HttpDelete]
[HttpPost]
+ [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)]
public IActionResult DeleteContainer(int id)
{
_contentTypeService.DeleteContainer(id, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id);
@@ -306,6 +310,7 @@ namespace Umbraco.Web.BackOffice.Controllers
return Ok();
}
+ [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)]
public IActionResult PostCreateContainer(int parentId, string name)
{
var result = _contentTypeService.CreateContainer(parentId, name, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id);
@@ -315,6 +320,7 @@ namespace Umbraco.Web.BackOffice.Controllers
: throw HttpResponseException.CreateNotificationValidationErrorResponse(result.Exception.Message);
}
+ [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)]
public IActionResult PostRenameContainer(int id, string name)
{
var result = _contentTypeService.RenameContainer(id, name, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id);
@@ -324,6 +330,7 @@ namespace Umbraco.Web.BackOffice.Controllers
: throw HttpResponseException.CreateNotificationValidationErrorResponse(result.Exception.Message);
}
+ [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)]
public CreatedContentTypeCollectionResult PostCreateCollection(int parentId, string collectionName, bool collectionCreateTemplate, string collectionItemName, bool collectionItemCreateTemplate, string collectionIcon, string collectionItemIcon)
{
// create item doctype
@@ -380,6 +387,7 @@ namespace Umbraco.Web.BackOffice.Controllers
};
}
+ [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)]
public DocumentTypeDisplay PostSave(DocumentTypeSave contentTypeSave)
{
//Before we send this model into this saving/mapping pipeline, we need to do some cleanup on variations.
@@ -432,6 +440,7 @@ namespace Umbraco.Web.BackOffice.Controllers
return display;
}
+ [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)]
public ActionResult PostCreateDefaultTemplate(int id)
{
var contentType = _contentTypeService.Get(id);
@@ -472,6 +481,7 @@ namespace Umbraco.Web.BackOffice.Controllers
///
///
///
+ [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)]
public DocumentTypeDisplay GetEmpty(int parentId)
{
IContentType ct;
@@ -493,6 +503,7 @@ namespace Umbraco.Web.BackOffice.Controllers
///
/// Returns all content type objects
///
+ [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)]
public IEnumerable GetAll()
{
var types = _contentTypeService.GetAll();
@@ -565,6 +576,7 @@ namespace Umbraco.Web.BackOffice.Controllers
///
///
///
+ [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)]
public IActionResult PostMove(MoveOrCopy move)
{
return PerformMove(
@@ -578,6 +590,7 @@ namespace Umbraco.Web.BackOffice.Controllers
///
///
///
+ [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)]
public IActionResult PostCopy(MoveOrCopy copy)
{
return PerformCopy(
@@ -587,6 +600,7 @@ namespace Umbraco.Web.BackOffice.Controllers
}
[HttpGet]
+ [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)]
public IActionResult Export(int id)
{
var contentType = _contentTypeService.Get(id);
@@ -603,6 +617,7 @@ namespace Umbraco.Web.BackOffice.Controllers
}
[HttpPost]
+ [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)]
public IActionResult Import(string file)
{
var filePath = Path.Combine(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Data), file);
@@ -635,7 +650,8 @@ namespace Umbraco.Web.BackOffice.Controllers
}
[HttpPost]
- public async Task> Upload(List file)
+ [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)]
+ public ActionResult Upload(List file)
{
var model = new ContentTypeImportModel();
diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentTypeControllerBase.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentTypeControllerBase.cs
index 969400e213..88a83ed217 100644
--- a/src/Umbraco.Web.BackOffice/Controllers/ContentTypeControllerBase.cs
+++ b/src/Umbraco.Web.BackOffice/Controllers/ContentTypeControllerBase.cs
@@ -15,6 +15,7 @@ using Umbraco.Core.Mapping;
using Umbraco.Core.Models;
using Umbraco.Core.Services;
using Umbraco.Extensions;
+using Umbraco.Web.BackOffice.Filters;
using Umbraco.Web.Common.Attributes;
using Umbraco.Web.Common.Exceptions;
using Umbraco.Web.Editors;
@@ -26,7 +27,7 @@ namespace Umbraco.Web.BackOffice.Controllers
/// Am abstract API controller providing functionality used for dealing with content and media types
///
[PluginController(Constants.Web.Mvc.BackOfficeApiArea)]
- //[PrefixlessBodyModelValidator] //TODO reintroduce
+ [PrefixlessBodyModelValidator]
public abstract class ContentTypeControllerBase : UmbracoAuthorizedJsonController
where TContentType : class, IContentTypeComposition
{
diff --git a/src/Umbraco.Web.BackOffice/Controllers/MediaTypeController.cs b/src/Umbraco.Web.BackOffice/Controllers/MediaTypeController.cs
index 334b1adbe8..32dc2ef888 100644
--- a/src/Umbraco.Web.BackOffice/Controllers/MediaTypeController.cs
+++ b/src/Umbraco.Web.BackOffice/Controllers/MediaTypeController.cs
@@ -17,21 +17,18 @@ using Umbraco.Web.Common.Authorization;
using Umbraco.Web.Common.Exceptions;
using Umbraco.Web.Editors;
using Umbraco.Web.Models.ContentEditing;
-using Umbraco.Web.Security;
namespace Umbraco.Web.BackOffice.Controllers
{
- // TODO: We'll need to be careful about the security on this controller, when we start implementing
- // methods to modify content types we'll need to enforce security on the individual methods, we
- // cannot put security on the whole controller because things like GetAllowedChildren are required for content editing.
-
///
- /// An API controller used for dealing with content types
+ /// An API controller used for dealing with content types
///
[PluginController(Constants.Web.Mvc.BackOfficeApiArea)]
- [Authorize(Policy = AuthorizationPolicies.TreeAccessMediaTypes)]
public class MediaTypeController : ContentTypeControllerBase
{
+ // TODO: Split this controller apart so that authz is consistent, currently we need to authz each action individually.
+ // It would be possible to have something like a MediaTypeInfoController for the GetById/GetAllowedChildren/etc... actions
+
private readonly IContentTypeService _contentTypeService;
private readonly IEntityService _entityService;
private readonly ILocalizedTextService _localizedTextService;
@@ -142,6 +139,7 @@ namespace Umbraco.Web.BackOffice.Controllers
///
[HttpDelete]
[HttpPost]
+ [Authorize(Policy = AuthorizationPolicies.TreeAccessMediaTypes)]
public IActionResult DeleteById(int id)
{
var foundType = _mediaTypeService.Get(id);
@@ -177,6 +175,7 @@ namespace Umbraco.Web.BackOffice.Controllers
///
///
[HttpPost]
+ [Authorize(Policy = AuthorizationPolicies.TreeAccessMediaTypes)]
public IActionResult GetAvailableCompositeMediaTypes(GetAvailableCompositionsFilter filter)
{
var result = PerformGetAvailableCompositeContentTypes(filter.ContentTypeId, UmbracoObjectTypes.MediaType,
@@ -197,6 +196,7 @@ namespace Umbraco.Web.BackOffice.Controllers
///
///
[HttpPost]
+ [Authorize(Policy = AuthorizationPolicies.TreeAccessMediaTypes)]
public IActionResult GetWhereCompositionIsUsedInContentTypes(GetAvailableCompositionsFilter filter)
{
var result =
@@ -208,6 +208,7 @@ namespace Umbraco.Web.BackOffice.Controllers
return Ok(result);
}
+ [Authorize(Policy = AuthorizationPolicies.TreeAccessMediaTypes)]
public MediaTypeDisplay GetEmpty(int parentId)
{
IMediaType mt;
@@ -229,19 +230,21 @@ namespace Umbraco.Web.BackOffice.Controllers
///
- /// Returns all media types
+ /// Returns all media types
///
+ [Authorize(Policy = AuthorizationPolicies.TreeAccessMediaTypes)]
public IEnumerable GetAll() =>
_mediaTypeService.GetAll()
.Select(_umbracoMapper.Map);
///
- /// Deletes a media type container with a given ID
+ /// Deletes a media type container with a given ID
///
///
///
[HttpDelete]
[HttpPost]
+ [Authorize(Policy = AuthorizationPolicies.TreeAccessMediaTypes)]
public IActionResult DeleteContainer(int id)
{
_mediaTypeService.DeleteContainer(id, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id);
@@ -249,6 +252,7 @@ namespace Umbraco.Web.BackOffice.Controllers
return Ok();
}
+ [Authorize(Policy = AuthorizationPolicies.TreeAccessMediaTypes)]
public IActionResult PostCreateContainer(int parentId, string name)
{
var result = _mediaTypeService.CreateContainer(parentId, name, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id);
@@ -258,6 +262,7 @@ namespace Umbraco.Web.BackOffice.Controllers
: throw HttpResponseException.CreateNotificationValidationErrorResponse(result.Exception.Message);
}
+ [Authorize(Policy = AuthorizationPolicies.TreeAccessMediaTypes)]
public IActionResult PostRenameContainer(int id, string name)
{
var result = _mediaTypeService.RenameContainer(id, name, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id);
@@ -267,6 +272,7 @@ namespace Umbraco.Web.BackOffice.Controllers
: throw HttpResponseException.CreateNotificationValidationErrorResponse(result.Exception.Message);
}
+ [Authorize(Policy = AuthorizationPolicies.TreeAccessMediaTypes)]
public MediaTypeDisplay PostSave(MediaTypeSave contentTypeSave)
{
var savedCt = PerformPostSave(
@@ -284,10 +290,11 @@ namespace Umbraco.Web.BackOffice.Controllers
}
///
- /// Move the media type
+ /// Move the media type
///
///
///
+ [Authorize(Policy = AuthorizationPolicies.TreeAccessMediaTypes)]
public IActionResult PostMove(MoveOrCopy move)
{
return PerformMove(
@@ -297,10 +304,11 @@ namespace Umbraco.Web.BackOffice.Controllers
}
///
- /// Copy the media type
+ /// Copy the media type
///
///
///
+ [Authorize(Policy = AuthorizationPolicies.TreeAccessMediaTypes)]
public IActionResult PostCopy(MoveOrCopy copy)
{
return PerformCopy(
@@ -313,7 +321,7 @@ namespace Umbraco.Web.BackOffice.Controllers
#region GetAllowedChildren
///
- /// Returns the allowed child content type objects for the content item id passed in - based on an INT id
+ /// Returns the allowed child content type objects for the content item id passed in - based on an INT id
///
///
[Authorize(Policy = AuthorizationPolicies.TreeAccessMediaOrMediaTypes)]
diff --git a/src/Umbraco.Web.BackOffice/Controllers/PublishedSnapshotCacheStatusController.cs b/src/Umbraco.Web.BackOffice/Controllers/PublishedSnapshotCacheStatusController.cs
index 1a0e3457ca..c9c420f254 100644
--- a/src/Umbraco.Web.BackOffice/Controllers/PublishedSnapshotCacheStatusController.cs
+++ b/src/Umbraco.Web.BackOffice/Controllers/PublishedSnapshotCacheStatusController.cs
@@ -8,8 +8,7 @@ using Umbraco.Web.PublishedCache;
namespace Umbraco.Web.BackOffice.Controllers
{
- [PluginController(Constants.Web.Mvc.BackOfficeApiArea)]
- [IsBackOffice]
+ [PluginController(Constants.Web.Mvc.BackOfficeApiArea)]
public class PublishedSnapshotCacheStatusController : UmbracoAuthorizedApiController
{
private readonly IPublishedSnapshotService _publishedSnapshotService;
diff --git a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs
index 1fb3010a7d..38bf69721a 100644
--- a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs
+++ b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs
@@ -41,7 +41,6 @@ using IUser = Umbraco.Core.Models.Membership.IUser;
using Task = System.Threading.Tasks.Task;
using Umbraco.Net;
using Umbraco.Web.Common.ActionsResults;
-using Umbraco.Web.Common.Security;
using Microsoft.AspNetCore.Authorization;
using Umbraco.Web.Common.Authorization;
diff --git a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeApplicationBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeApplicationBuilderExtensions.cs
index d32351fdc6..a097ead4a1 100644
--- a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeApplicationBuilderExtensions.cs
+++ b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeApplicationBuilderExtensions.cs
@@ -2,8 +2,10 @@ using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using SixLabors.ImageSharp.Web.DependencyInjection;
+using Umbraco.Core.BackOffice;
using Umbraco.Web.BackOffice.Middleware;
using Umbraco.Web.BackOffice.Routing;
+using Umbraco.Web.Common.Security;
namespace Umbraco.Extensions
{
@@ -30,6 +32,8 @@ namespace Umbraco.Extensions
{
if (app == null) throw new ArgumentNullException(nameof(app));
+ app.UseBackOfficeUserManagerAuditing();
+
// Important we handle image manipulations before the static files, otherwise the querystring is just ignored.
// TODO: Since we are dependent on these we need to register them but what happens when we call this multiple times since we are dependent on this for UseUmbracoBackOffice too?
app.UseImageSharp();
@@ -64,5 +68,12 @@ namespace Umbraco.Extensions
return app;
}
+
+ private static IApplicationBuilder UseBackOfficeUserManagerAuditing(this IApplicationBuilder app)
+ {
+ var auditer = app.ApplicationServices.GetRequiredService();
+ auditer.Start();
+ return app;
+ }
}
}
diff --git a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs
index d13a908034..413a54a28b 100644
--- a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs
+++ b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeServiceCollectionExtensions.cs
@@ -35,7 +35,7 @@ namespace Umbraco.Extensions
.AddDefaultTokenProviders()
.AddUserStore()
.AddUserManager()
- .AddSignInManager()
+ .AddSignInManager()
.AddClaimsPrincipalFactory>();
// Configure the options specifically for the UmbracoBackOfficeIdentityOptions instance
@@ -66,7 +66,9 @@ namespace Umbraco.Extensions
services.TryAddScoped();
services.TryAddScoped();
services.TryAddScoped();
- services.TryAddSingleton();
+ services.TryAddSingleton();
+ services.TryAddSingleton();
+ services.TryAddSingleton();
/*
* IdentityBuilderExtensions.AddUserManager adds UserManager to service collection
diff --git a/src/Umbraco.Web.BackOffice/Extensions/HtmlHelperBackOfficeExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/HtmlHelperBackOfficeExtensions.cs
index 9828931198..555ed5bb90 100644
--- a/src/Umbraco.Web.BackOffice/Extensions/HtmlHelperBackOfficeExtensions.cs
+++ b/src/Umbraco.Web.BackOffice/Extensions/HtmlHelperBackOfficeExtensions.cs
@@ -13,13 +13,13 @@ using Umbraco.Core.Configuration.UmbracoSettings;
using Umbraco.Core.Hosting;
using Umbraco.Core.WebAssets;
using Umbraco.Web.BackOffice.Controllers;
-using Umbraco.Web.Common.Security;
using Umbraco.Web.Features;
using Umbraco.Web.Models;
using Umbraco.Web.WebApi;
using Umbraco.Web.WebAssets;
using Umbraco.Core;
using Umbraco.Core.Security;
+using Umbraco.Web.BackOffice.Security;
namespace Umbraco.Extensions
{
@@ -75,7 +75,7 @@ namespace Umbraco.Extensions
{
authType = p.AuthenticationType,
caption = p.Name,
- properties = p.Properties
+ properties = p.Options
})
.ToArray();
diff --git a/src/Umbraco.Web.BackOffice/Extensions/IdentityBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/IdentityBuilderExtensions.cs
index eab7142665..ddf46a24a7 100644
--- a/src/Umbraco.Web.BackOffice/Extensions/IdentityBuilderExtensions.cs
+++ b/src/Umbraco.Web.BackOffice/Extensions/IdentityBuilderExtensions.cs
@@ -7,7 +7,7 @@ namespace Umbraco.Extensions
public static class IdentityBuilderExtensions
{
///
- /// Adds a for the .
+ /// Adds a implementation for
///
/// The type of the user manager to add.
///
@@ -18,5 +18,19 @@ namespace Umbraco.Extensions
identityBuilder.Services.AddScoped(typeof(TInterface), typeof(TUserManager));
return identityBuilder;
}
+
+ ///
+ /// Adds a implementation for
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static IdentityBuilder AddSignInManager(this IdentityBuilder identityBuilder) where TSignInManager : SignInManager, TInterface
+ {
+ identityBuilder.AddSignInManager();
+ identityBuilder.Services.AddScoped(typeof(TInterface), typeof(TSignInManager));
+ return identityBuilder;
+ }
}
}
diff --git a/src/Umbraco.Web.BackOffice/Extensions/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/UmbracoBuilderExtensions.cs
index 7262966615..35b7e8e859 100644
--- a/src/Umbraco.Web.BackOffice/Extensions/UmbracoBuilderExtensions.cs
+++ b/src/Umbraco.Web.BackOffice/Extensions/UmbracoBuilderExtensions.cs
@@ -33,17 +33,24 @@ namespace Umbraco.Extensions
{
builder.Services.AddAntiforgery();
builder.Services.AddSingleton();
+
builder.Services
- .AddAuthentication(Core.Constants.Security.BackOfficeAuthenticationType)
+ .AddAuthentication() // This just creates a builder, nothing more
+ // Add our custom schemes which are cookie handlers
.AddCookie(Core.Constants.Security.BackOfficeAuthenticationType)
.AddCookie(Core.Constants.Security.BackOfficeExternalAuthenticationType, o =>
{
o.Cookie.Name = Core.Constants.Security.BackOfficeExternalAuthenticationType;
o.ExpireTimeSpan = TimeSpan.FromMinutes(5);
+ })
+ // Although we don't natively support this, we add it anyways so that if end-users implement the required logic
+ // they don't have to worry about manually adding this scheme or modifying the sign in manager
+ .AddCookie(Core.Constants.Security.BackOfficeTwoFactorAuthenticationType, o =>
+ {
+ o.Cookie.Name = Core.Constants.Security.BackOfficeTwoFactorAuthenticationType;
+ o.ExpireTimeSpan = TimeSpan.FromMinutes(5);
});
- // TODO: Need to add more cookie options, see https://github.com/dotnet/aspnetcore/blob/3.0/src/Identity/Core/src/IdentityServiceCollectionExtensions.cs#L45
-
builder.Services.ConfigureOptions();
return builder;
}
diff --git a/src/Umbraco.Web.BackOffice/Filters/CheckIfUserTicketDataIsStaleAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/CheckIfUserTicketDataIsStaleAttribute.cs
index cde2f77bf7..9cfaae6980 100644
--- a/src/Umbraco.Web.BackOffice/Filters/CheckIfUserTicketDataIsStaleAttribute.cs
+++ b/src/Umbraco.Web.BackOffice/Filters/CheckIfUserTicketDataIsStaleAttribute.cs
@@ -34,7 +34,7 @@ namespace Umbraco.Web.BackOffice.Filters
private readonly IEntityService _entityService;
private readonly ILocalizedTextService _localizedTextService;
private readonly IOptions _globalSettings;
- private readonly BackOfficeSignInManager _backOfficeSignInManager;
+ private readonly IBackOfficeSignInManager _backOfficeSignInManager;
private readonly IBackOfficeAntiforgery _backOfficeAntiforgery;
public CheckIfUserTicketDataIsStaleFilter(
@@ -44,7 +44,7 @@ namespace Umbraco.Web.BackOffice.Filters
IEntityService entityService,
ILocalizedTextService localizedTextService,
IOptions globalSettings,
- BackOfficeSignInManager backOfficeSignInManager,
+ IBackOfficeSignInManager backOfficeSignInManager,
IBackOfficeAntiforgery backOfficeAntiforgery)
{
_requestCache = requestCache;
diff --git a/src/Umbraco.Web.BackOffice/Filters/OverrideAuthorizationAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/OverrideAuthorizationAttribute.cs
index e4aef2a3bc..3d7b68cc80 100644
--- a/src/Umbraco.Web.BackOffice/Filters/OverrideAuthorizationAttribute.cs
+++ b/src/Umbraco.Web.BackOffice/Filters/OverrideAuthorizationAttribute.cs
@@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc.Filters;
namespace Umbraco.Web.BackOffice.Filters
{
+ // TODO: This should probably be deleted, anything requiring this should move to a different controller
public class OverrideAuthorizationAttribute : ActionFilterAttribute
{
///
diff --git a/src/Umbraco.Web.BackOffice/Filters/OverrideAuthorizationFilterProvider.cs b/src/Umbraco.Web.BackOffice/Filters/OverrideAuthorizationFilterProvider.cs
index 9546412270..39f691e190 100644
--- a/src/Umbraco.Web.BackOffice/Filters/OverrideAuthorizationFilterProvider.cs
+++ b/src/Umbraco.Web.BackOffice/Filters/OverrideAuthorizationFilterProvider.cs
@@ -4,7 +4,7 @@ using Umbraco.Core;
namespace Umbraco.Web.BackOffice.Filters
{
- // TODO: Need to figure out if we need this and what we should be doing
+ // TODO: This should be deleted, anything requiring this should move to a different controller
public class OverrideAuthorizationFilterProvider : IFilterProvider, IFilterMetadata
{
public void OnProvidersExecuted(FilterProviderContext context)
diff --git a/src/Umbraco.Web.BackOffice/Security/AuthenticationBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/Security/AuthenticationBuilderExtensions.cs
new file mode 100644
index 0000000000..e2a7aeccaf
--- /dev/null
+++ b/src/Umbraco.Web.BackOffice/Security/AuthenticationBuilderExtensions.cs
@@ -0,0 +1,15 @@
+using System;
+using Umbraco.Core.Builder;
+
+namespace Umbraco.Web.BackOffice.Security
+{
+ public static class AuthenticationBuilderExtensions
+ {
+ public static IUmbracoBuilder AddBackOfficeExternalLogins(this IUmbracoBuilder umbracoBuilder, Action builder)
+ {
+ builder(new BackOfficeExternalLoginsBuilder(umbracoBuilder.Services));
+ return umbracoBuilder;
+ }
+ }
+
+}
diff --git a/src/Umbraco.Web.BackOffice/Security/AutoLinkSignInResult.cs b/src/Umbraco.Web.BackOffice/Security/AutoLinkSignInResult.cs
new file mode 100644
index 0000000000..54f409e6f8
--- /dev/null
+++ b/src/Umbraco.Web.BackOffice/Security/AutoLinkSignInResult.cs
@@ -0,0 +1,48 @@
+using System;
+using System.Collections.Generic;
+using Microsoft.AspNetCore.Identity;
+
+namespace Umbraco.Web.Common.Security
+{
+ ///
+ /// Result returned from signing in when auto-linking takes place
+ ///
+ public class AutoLinkSignInResult : SignInResult
+ {
+ public static AutoLinkSignInResult FailedNotLinked => new AutoLinkSignInResult()
+ {
+ Succeeded = false
+ };
+
+ public static AutoLinkSignInResult FailedNoEmail => new AutoLinkSignInResult()
+ {
+ Succeeded = false
+ };
+
+ public static AutoLinkSignInResult FailedException(string error) => new AutoLinkSignInResult(new[] { error })
+ {
+ Succeeded = false
+ };
+
+ public static AutoLinkSignInResult FailedCreatingUser(IReadOnlyCollection errors) => new AutoLinkSignInResult(errors)
+ {
+ Succeeded = false
+ };
+
+ public static AutoLinkSignInResult FailedLinkingUser(IReadOnlyCollection errors) => new AutoLinkSignInResult(errors)
+ {
+ Succeeded = false
+ };
+
+ public AutoLinkSignInResult(IReadOnlyCollection errors)
+ {
+ Errors = errors ?? throw new ArgumentNullException(nameof(errors));
+ }
+
+ public AutoLinkSignInResult()
+ {
+ }
+
+ public IReadOnlyCollection Errors { get; } = Array.Empty();
+ }
+}
diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficeAuthenticationBuilder.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeAuthenticationBuilder.cs
new file mode 100644
index 0000000000..b3418697e2
--- /dev/null
+++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeAuthenticationBuilder.cs
@@ -0,0 +1,64 @@
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Options;
+using System;
+using Umbraco.Core;
+
+namespace Umbraco.Web.BackOffice.Security
+{
+ ///
+ /// Custom used to associate external logins with umbraco external login options
+ ///
+ public class BackOfficeAuthenticationBuilder : AuthenticationBuilder
+ {
+ private readonly BackOfficeExternalLoginProviderOptions _loginProviderOptions;
+
+ public BackOfficeAuthenticationBuilder(IServiceCollection services, BackOfficeExternalLoginProviderOptions loginProviderOptions)
+ : base(services)
+ {
+ _loginProviderOptions = loginProviderOptions;
+ }
+
+ public string SchemeForBackOffice(string scheme)
+ {
+ return Constants.Security.BackOfficeExternalAuthenticationTypePrefix + scheme;
+ }
+
+ ///
+ /// Overridden to track the final authenticationScheme being registered for the external login
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public override AuthenticationBuilder AddRemoteScheme(string authenticationScheme, string displayName, Action configureOptions)
+ {
+ // Validate that the prefix is set
+ if (!authenticationScheme.StartsWith(Constants.Security.BackOfficeExternalAuthenticationTypePrefix))
+ {
+ throw new InvalidOperationException($"The {nameof(authenticationScheme)} is not prefixed with {Constants.Security.BackOfficeExternalAuthenticationTypePrefix}. The scheme must be created with a call to the method {nameof(SchemeForBackOffice)}");
+ }
+
+ // add our login provider to the container along with a custom options configuration
+ Services.AddSingleton(x => new BackOfficeExternalLoginProvider(displayName, authenticationScheme, _loginProviderOptions));
+ Services.TryAddEnumerable(ServiceDescriptor.Singleton, EnsureBackOfficeScheme>());
+
+ return base.AddRemoteScheme(authenticationScheme, displayName, configureOptions);
+ }
+
+ // TODO: We could override and throw NotImplementedException for other methods?
+
+ // Ensures that the sign in scheme is always the Umbraco back office external type
+ private class EnsureBackOfficeScheme : IPostConfigureOptions where TOptions : RemoteAuthenticationOptions
+ {
+ public void PostConfigure(string name, TOptions options)
+ {
+ options.SignInScheme = Constants.Security.BackOfficeExternalAuthenticationType;
+ }
+ }
+ }
+
+}
diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficeCookieManager.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeCookieManager.cs
index 60bdc9c8ff..8664713c72 100644
--- a/src/Umbraco.Web.BackOffice/Security/BackOfficeCookieManager.cs
+++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeCookieManager.cs
@@ -1,10 +1,9 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
-using Microsoft.AspNetCore.Routing;
using Umbraco.Core;
using Umbraco.Core.Cache;
using Umbraco.Core.Configuration.Models;
@@ -13,8 +12,6 @@ using Umbraco.Extensions;
namespace Umbraco.Web.BackOffice.Security
{
- using ICookieManager = Microsoft.AspNetCore.Authentication.Cookies.ICookieManager;
-
///
/// A custom cookie manager that is used to read the cookie from the request.
///
@@ -22,7 +19,7 @@ namespace Umbraco.Web.BackOffice.Security
/// Umbraco's back office cookie needs to be read on two paths: /umbraco and /install, therefore we cannot just set the cookie path to be /umbraco,
/// instead we'll specify our own cookie manager and return null if the request isn't for an acceptable path.
///
- public class BackOfficeCookieManager : ChunkingCookieManager, ICookieManager
+ public class BackOfficeCookieManager : ChunkingCookieManager, Microsoft.AspNetCore.Authentication.Cookies.ICookieManager
{
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
private readonly IRuntimeState _runtime;
@@ -36,9 +33,8 @@ namespace Umbraco.Web.BackOffice.Security
IRuntimeState runtime,
IHostingEnvironment hostingEnvironment,
GlobalSettings globalSettings,
- IRequestCache requestCache,
- LinkGenerator linkGenerator)
- : this(umbracoContextAccessor, runtime, hostingEnvironment, globalSettings, requestCache, linkGenerator, null)
+ IRequestCache requestCache)
+ : this(umbracoContextAccessor, runtime, hostingEnvironment, globalSettings, requestCache, null)
{ }
public BackOfficeCookieManager(
@@ -47,7 +43,6 @@ namespace Umbraco.Web.BackOffice.Security
IHostingEnvironment hostingEnvironment,
GlobalSettings globalSettings,
IRequestCache requestCache,
- LinkGenerator linkGenerator,
IEnumerable explicitPaths)
{
_umbracoContextAccessor = umbracoContextAccessor;
@@ -61,9 +56,9 @@ namespace Umbraco.Web.BackOffice.Security
///
/// Determines if we should authenticate the request
///
- ///
- ///
- ///
+ /// The to check
+ /// true to check if the has been assigned in the request.
+ /// true if the request should be authenticated
///
/// We auth the request when:
/// * it is a back office request
@@ -79,19 +74,27 @@ namespace Umbraco.Web.BackOffice.Security
// was: app.IsConfigured == false (equiv to !Run) && dbContext.IsDbConfigured == false (equiv to Install)
// so, we handle .Install here and NOT .Upgrade
if (_runtime.Level == RuntimeLevel.Install)
+ {
return false;
+ }
- //check the explicit paths
+ // check the explicit paths
if (_explicitPaths != null)
+ {
return _explicitPaths.Any(x => x.InvariantEquals(requestUri.AbsolutePath));
+ }
- if (//check the explicit flag
- checkForceAuthTokens && _requestCache.IsAvailable && _requestCache.Get(Constants.Security.ForceReAuthFlag) != null
- //check back office
+ if (// check the explicit flag
+ (checkForceAuthTokens && _requestCache.IsAvailable && _requestCache.Get(Constants.Security.ForceReAuthFlag) != null)
+
+ // check back office
|| requestUri.IsBackOfficeRequest(_globalSettings, _hostingEnvironment)
- //check installer
+
+ // check installer
|| requestUri.IsInstallerRequest(_hostingEnvironment))
+ {
return true;
+ }
return false;
}
@@ -99,20 +102,20 @@ namespace Umbraco.Web.BackOffice.Security
///
/// Explicitly implement this so that we filter the request
///
- ///
- ///
- ///
- string ICookieManager.GetRequestCookie(HttpContext context, string key)
+ ///
+ string Microsoft.AspNetCore.Authentication.Cookies.ICookieManager.GetRequestCookie(HttpContext context, string key)
{
var requestUri = new Uri(context.Request.GetEncodedUrl(), UriKind.RelativeOrAbsolute);
if (_umbracoContextAccessor.UmbracoContext == null || requestUri.IsClientSideRequest())
+ {
return null;
+ }
return ShouldAuthenticateRequest(requestUri) == false
- //Don't auth request, don't return a cookie
+ // Don't auth request, don't return a cookie
? null
- //Return the default implementation
+ // Return the default implementation
: GetRequestCookie(context, key);
}
diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficeExternalLoginProvider.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeExternalLoginProvider.cs
new file mode 100644
index 0000000000..18e5b066dc
--- /dev/null
+++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeExternalLoginProvider.cs
@@ -0,0 +1,39 @@
+using System;
+
+namespace Umbraco.Web.BackOffice.Security
+{
+ ///
+ /// An external login (OAuth) provider for the back office
+ ///
+ public class BackOfficeExternalLoginProvider : IEquatable
+ {
+ public BackOfficeExternalLoginProvider(string name, string authenticationType, BackOfficeExternalLoginProviderOptions properties)
+ {
+ Name = name ?? throw new ArgumentNullException(nameof(name));
+ AuthenticationType = authenticationType ?? throw new ArgumentNullException(nameof(authenticationType));
+ Options = properties ?? throw new ArgumentNullException(nameof(properties));
+ }
+
+ public string Name { get; }
+ public string AuthenticationType { get; }
+ public BackOfficeExternalLoginProviderOptions Options { get; }
+
+ public override bool Equals(object obj)
+ {
+ return Equals(obj as BackOfficeExternalLoginProvider);
+ }
+
+ public bool Equals(BackOfficeExternalLoginProvider other)
+ {
+ return other != null &&
+ Name == other.Name &&
+ AuthenticationType == other.AuthenticationType;
+ }
+
+ public override int GetHashCode()
+ {
+ return HashCode.Combine(Name, AuthenticationType);
+ }
+ }
+
+}
diff --git a/src/Umbraco.Web.Common/Security/BackOfficeExternalLoginProviderOptions.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeExternalLoginProviderOptions.cs
similarity index 61%
rename from src/Umbraco.Web.Common/Security/BackOfficeExternalLoginProviderOptions.cs
rename to src/Umbraco.Web.BackOffice/Security/BackOfficeExternalLoginProviderOptions.cs
index de16d0ec14..b6c1c7f2d2 100644
--- a/src/Umbraco.Web.Common/Security/BackOfficeExternalLoginProviderOptions.cs
+++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeExternalLoginProviderOptions.cs
@@ -1,23 +1,42 @@
using System;
using System.Runtime.Serialization;
-namespace Umbraco.Web.Common.Security
+namespace Umbraco.Web.BackOffice.Security
{
+
///
/// Options used to configure back office external login providers
///
public class BackOfficeExternalLoginProviderOptions
{
+ public BackOfficeExternalLoginProviderOptions(
+ string buttonStyle, string icon,
+ ExternalSignInAutoLinkOptions autoLinkOptions = null,
+ bool denyLocalLogin = false,
+ bool autoRedirectLoginToExternalProvider = false,
+ string customBackOfficeView = null)
+ {
+ ButtonStyle = buttonStyle;
+ Icon = icon;
+ AutoLinkOptions = autoLinkOptions ?? new ExternalSignInAutoLinkOptions();
+ DenyLocalLogin = denyLocalLogin;
+ AutoRedirectLoginToExternalProvider = autoRedirectLoginToExternalProvider;
+ CustomBackOfficeView = customBackOfficeView;
+ }
+
+ public string ButtonStyle { get; }
+ public string Icon { get; }
+
///
/// Options used to control how users can be auto-linked/created/updated based on the external login provider
///
- public ExternalSignInAutoLinkOptions AutoLinkOptions { get; set; } = new ExternalSignInAutoLinkOptions();
+ public ExternalSignInAutoLinkOptions AutoLinkOptions { get; }
///
/// When set to true will disable all local user login functionality
///
- public bool DenyLocalLogin { get; set; }
+ public bool DenyLocalLogin { get; }
///
/// When specified this will automatically redirect to the OAuth login provider instead of prompting the user to click on the OAuth button first.
@@ -26,7 +45,7 @@ namespace Umbraco.Web.Common.Security
/// This is generally used in conjunction with . If more than one OAuth provider specifies this, the last registered
/// provider's redirect settings will win.
///
- public bool AutoRedirectLoginToExternalProvider { get; set; }
+ public bool AutoRedirectLoginToExternalProvider { get; }
///
/// A virtual path to a custom angular view that is used to replace the entire UI that renders the external login button that the user interacts with
@@ -35,6 +54,6 @@ namespace Umbraco.Web.Common.Security
/// If this view is specified it is 100% up to the user to render the html responsible for rendering the link/un-link buttons along with showing any errors
/// that occur. This overrides what Umbraco normally does by default.
///
- public string CustomBackOfficeView { get; set; }
+ public string CustomBackOfficeView { get; }
}
}
diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficeExternalLoginProviders.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeExternalLoginProviders.cs
new file mode 100644
index 0000000000..21c94308dd
--- /dev/null
+++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeExternalLoginProviders.cs
@@ -0,0 +1,43 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Umbraco.Web.BackOffice.Security
+{
+ ///
+ public class BackOfficeExternalLoginProviders : IBackOfficeExternalLoginProviders
+ {
+ public BackOfficeExternalLoginProviders(IEnumerable externalLogins)
+ {
+ _externalLogins = externalLogins;
+ }
+
+ private readonly IEnumerable _externalLogins;
+
+ ///
+ public BackOfficeExternalLoginProvider Get(string authenticationType)
+ {
+ return _externalLogins.FirstOrDefault(x => x.AuthenticationType == authenticationType);
+ }
+
+ ///
+ public string GetAutoLoginProvider()
+ {
+ var found = _externalLogins.Where(x => x.Options.AutoRedirectLoginToExternalProvider).ToList();
+ return found.Count > 0 ? found[0].AuthenticationType : null;
+ }
+
+ ///
+ public IEnumerable GetBackOfficeProviders()
+ {
+ return _externalLogins;
+ }
+
+ ///
+ public bool HasDenyLocalLogin()
+ {
+ var found = _externalLogins.Where(x => x.Options.DenyLocalLogin).ToList();
+ return found.Count > 0;
+ }
+ }
+
+}
diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficeExternalLoginsBuilder.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeExternalLoginsBuilder.cs
new file mode 100644
index 0000000000..402ad8b948
--- /dev/null
+++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeExternalLoginsBuilder.cs
@@ -0,0 +1,33 @@
+using Microsoft.Extensions.DependencyInjection;
+using System;
+
+namespace Umbraco.Web.BackOffice.Security
+{
+ ///
+ /// Used to add back office login providers
+ ///
+ public class BackOfficeExternalLoginsBuilder
+ {
+ public BackOfficeExternalLoginsBuilder(IServiceCollection services)
+ {
+ _services = services;
+ }
+
+ private readonly IServiceCollection _services;
+
+ ///
+ /// Add a back office login provider with options
+ ///
+ ///
+ ///
+ ///
+ public BackOfficeExternalLoginsBuilder AddBackOfficeLogin(
+ BackOfficeExternalLoginProviderOptions loginProviderOptions,
+ Action build)
+ {
+ build(new BackOfficeAuthenticationBuilder(_services, loginProviderOptions));
+ return this;
+ }
+ }
+
+}
diff --git a/src/Umbraco.Web.Common/Security/BackOfficeSignInManager.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeSignInManager.cs
similarity index 67%
rename from src/Umbraco.Web.Common/Security/BackOfficeSignInManager.cs
rename to src/Umbraco.Web.BackOffice/Security/BackOfficeSignInManager.cs
index bef94c0ada..e17067daa0 100644
--- a/src/Umbraco.Web.Common/Security/BackOfficeSignInManager.cs
+++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeSignInManager.cs
@@ -10,44 +10,51 @@ using System.Security.Claims;
using System.Threading.Tasks;
using Umbraco.Core;
using Umbraco.Core.BackOffice;
+using Umbraco.Core.Configuration;
+using Umbraco.Core.Configuration.Models;
+using Umbraco.Core.Security;
using Umbraco.Extensions;
+using Umbraco.Net;
+using Umbraco.Web.BackOffice.Security;
namespace Umbraco.Web.Common.Security
{
+
using Constants = Umbraco.Core.Constants;
- // TODO: There's potential to extract an interface for this for only what we use and put that in Core without aspnetcore refs, but we need to wait till were done with it since there's a bit to implement
-
- public class BackOfficeSignInManager : SignInManager
+ public class BackOfficeSignInManager : SignInManager, IBackOfficeSignInManager
{
// borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs
- private const string LoginProviderKey = "LoginProvider";
+ private const string UmbracoSignInMgrLoginProviderKey = "LoginProvider";
// borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs
- private const string XsrfKey = "XsrfId"; // TODO: See BackOfficeController.XsrfKey
+ private const string UmbracoSignInMgrXsrfKey = "XsrfId";
private BackOfficeUserManager _userManager;
+ private readonly IBackOfficeExternalLoginProviders _externalLogins;
+ private readonly GlobalSettings _globalSettings;
+
public BackOfficeSignInManager(
BackOfficeUserManager userManager,
IHttpContextAccessor contextAccessor,
+ IBackOfficeExternalLoginProviders externalLogins,
IUserClaimsPrincipalFactory claimsFactory,
IOptions optionsAccessor,
+ IOptions globalSettings,
ILogger> logger,
IAuthenticationSchemeProvider schemes,
IUserConfirmation confirmation)
: base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation)
{
_userManager = userManager;
+ _externalLogins = externalLogins;
+ _globalSettings = globalSettings.Value;
}
- // TODO: Need to migrate more from Umbraco.Web.Security.BackOfficeSignInManager
- // Things like dealing with auto-linking, cookie options, and a ton of other stuff. Some might not need to be ported but it
- // will be a case by case basis.
- // Have a look into RefreshSignInAsync since we might be able to use this new functionality for auto-cookie renewal in our middleware, though
+ // TODO: Have a look into RefreshSignInAsync since we might be able to use this new functionality for auto-cookie renewal in our middleware, though
// i suspect it's taken care of already.
-
///
public override async Task PasswordSignInAsync(BackOfficeIdentityUser user, string password, bool isPersistent, bool lockoutOnFailure)
{
@@ -65,7 +72,7 @@ namespace Umbraco.Web.Common.Security
return await HandleSignIn(null, userName, SignInResult.Failed);
return await PasswordSignInAsync(user, password, isPersistent, lockoutOnFailure);
}
-
+
///
public override async Task GetTwoFactorAuthenticationUserAsync()
{
@@ -112,7 +119,7 @@ namespace Umbraco.Web.Common.Security
return await HandleSignIn(user, user?.UserName, SignInResult.Failed);
}
-
+
///
public override bool IsSignedIn(ClaimsPrincipal principal)
{
@@ -193,7 +200,7 @@ namespace Umbraco.Web.Common.Security
await Context.SignOutAsync(Constants.Security.BackOfficeTwoFactorAuthenticationType);
}
-
+
///
public override async Task IsTwoFactorClientRememberedAsync(BackOfficeIdentityUser user)
{
@@ -205,7 +212,7 @@ namespace Umbraco.Web.Common.Security
return (result?.Principal != null && result.Principal.FindFirstValue(ClaimTypes.Name) == userId);
}
-
+
///
public override async Task RememberTwoFactorClientAsync(BackOfficeIdentityUser user)
{
@@ -218,7 +225,7 @@ namespace Umbraco.Web.Common.Security
new AuthenticationProperties { IsPersistent = true });
}
-
+
///
public override Task ForgetTwoFactorClientAsync()
{
@@ -228,7 +235,7 @@ namespace Umbraco.Web.Common.Security
return Context.SignOutAsync(Constants.Security.BackOfficeTwoFactorRememberMeAuthenticationType);
}
-
+
///
public override async Task TwoFactorRecoveryCodeSignInAsync(string recoveryCode)
{
@@ -257,7 +264,7 @@ namespace Umbraco.Web.Common.Security
return SignInResult.Failed;
}
-
+
///
public override async Task GetExternalLoginInfoAsync(string expectedXsrf = null)
{
@@ -266,18 +273,18 @@ namespace Umbraco.Web.Common.Security
var auth = await Context.AuthenticateAsync(Constants.Security.BackOfficeExternalAuthenticationType);
var items = auth?.Properties?.Items;
- if (auth?.Principal == null || items == null || !items.ContainsKey(LoginProviderKey))
+ if (auth?.Principal == null || items == null || !items.ContainsKey(UmbracoSignInMgrLoginProviderKey))
{
return null;
}
if (expectedXsrf != null)
{
- if (!items.ContainsKey(XsrfKey))
+ if (!items.ContainsKey(UmbracoSignInMgrXsrfKey))
{
return null;
}
- var userId = items[XsrfKey] as string;
+ var userId = items[UmbracoSignInMgrXsrfKey];
if (userId != expectedXsrf)
{
return null;
@@ -285,7 +292,7 @@ namespace Umbraco.Web.Common.Security
}
var providerKey = auth.Principal.FindFirstValue(ClaimTypes.NameIdentifier);
- var provider = items[LoginProviderKey] as string;
+ var provider = items[UmbracoSignInMgrLoginProviderKey] as string;
if (providerKey == null || provider == null)
{
return null;
@@ -299,7 +306,72 @@ namespace Umbraco.Web.Common.Security
};
}
-
+ ///
+ /// Custom ExternalLoginSignInAsync overload for handling external sign in with auto-linking
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public async Task ExternalLoginSignInAsync(ExternalLoginInfo loginInfo, bool isPersistent, bool bypassTwoFactor = false)
+ {
+ // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs
+ // to be able to deal with auto-linking and reduce duplicate lookups
+
+ var autoLinkOptions = _externalLogins.Get(loginInfo.LoginProvider)?.Options?.AutoLinkOptions;
+ var user = await UserManager.FindByLoginAsync(loginInfo.LoginProvider, loginInfo.ProviderKey);
+ if (user == null)
+ {
+ // user doesn't exist so see if we can auto link
+ return await AutoLinkAndSignInExternalAccount(loginInfo, autoLinkOptions);
+ }
+
+ if (autoLinkOptions != null && autoLinkOptions.OnExternalLogin != null)
+ {
+ var shouldSignIn = autoLinkOptions.OnExternalLogin(user, loginInfo);
+ if (shouldSignIn == false)
+ {
+ Logger.LogWarning("The AutoLinkOptions of the external authentication provider '{LoginProvider}' have refused the login based on the OnExternalLogin method. Affected user id: '{UserId}'", loginInfo.LoginProvider, user.Id);
+ }
+ }
+
+ var error = await PreSignInCheck(user);
+ if (error != null)
+ {
+ return error;
+ }
+ return await SignInOrTwoFactorAsync(user, isPersistent, loginInfo.LoginProvider, bypassTwoFactor);
+ }
+
+ ///
+ /// Configures the redirect URL and user identifier for the specified external login .
+ ///
+ /// The provider to configure.
+ /// The external login URL users should be redirected to during the login flow.
+ /// The current user's identifier, which will be used to provide CSRF protection.
+ /// A configured .
+ public override AuthenticationProperties ConfigureExternalAuthenticationProperties(string provider, string redirectUrl, string userId = null)
+ {
+ // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs
+ // to be able to use our own XsrfKey/LoginProviderKey because the default is private :/
+
+ var properties = new AuthenticationProperties { RedirectUri = redirectUrl };
+ properties.Items[UmbracoSignInMgrLoginProviderKey] = provider;
+ if (userId != null)
+ {
+ properties.Items[UmbracoSignInMgrXsrfKey] = userId;
+ }
+ return properties;
+ }
+
+ public override Task> GetExternalAuthenticationSchemesAsync()
+ {
+ // TODO: We can filter these so that they only include the back office ones.
+ // That can be done by either checking the scheme (maybe) or comparing it to what we have registered in the collection of BackOfficeExternalLoginProvider
+ return base.GetExternalAuthenticationSchemesAsync();
+ }
+
///
protected override async Task SignInOrTwoFactorAsync(BackOfficeIdentityUser user, bool isPersistent, string loginProvider = null, bool bypassTwoFactor = false)
{
@@ -346,7 +418,7 @@ namespace Umbraco.Web.Common.Security
if (username.IsNullOrWhiteSpace())
{
username = "UNKNOWN"; // could happen in 2fa or something else weird
- }
+ }
if (result.Succeeded)
{
@@ -356,24 +428,25 @@ namespace Umbraco.Web.Common.Security
{
//we have successfully logged in, reset the AccessFailedCount
user.AccessFailedCount = 0;
- }
+ }
await UserManager.UpdateAsync(user);
Logger.LogInformation("User: {UserName} logged in from IP address {IpAddress}", username, Context.Connection.RemoteIpAddress);
if (user != null)
{
- _userManager.RaiseLoginSuccessEvent(user, user.Id);
- }
+ _userManager.RaiseLoginSuccessEvent(Context.User, user.Id);
+ }
}
else if (result.IsLockedOut)
{
- _userManager.RaiseAccountLockedEvent(user, user.Id);
+ _userManager.RaiseAccountLockedEvent(Context.User, user.Id);
Logger.LogInformation("Login attempt failed for username {UserName} from IP address {IpAddress}, the user is locked", username, Context.Connection.RemoteIpAddress);
}
else if (result.RequiresTwoFactor)
{
+ _userManager.RaiseLoginRequiresVerificationEvent(Context.User, user.Id);
Logger.LogInformation("Login attempt requires verification for username {UserName} from IP address {IpAddress}", username, Context.Connection.RemoteIpAddress);
- }
+ }
else if (!result.Succeeded || result.IsNotAllowed)
{
Logger.LogInformation("Login attempt failed for username {UserName} from IP address {IpAddress}", username, Context.Connection.RemoteIpAddress);
@@ -470,5 +543,117 @@ namespace Umbraco.Web.Common.Security
public string UserId { get; set; }
public string LoginProvider { get; set; }
}
+
+
+ ///
+ /// Used for auto linking/creating user accounts for external logins
+ ///
+ ///
+ ///
+ ///
+ private async Task AutoLinkAndSignInExternalAccount(ExternalLoginInfo loginInfo, ExternalSignInAutoLinkOptions autoLinkOptions)
+ {
+ // If there are no autolink options then the attempt is failed (user does not exist)
+ if (autoLinkOptions == null || !autoLinkOptions.AutoLinkExternalAccount)
+ {
+ return SignInResult.Failed;
+ }
+
+ var email = loginInfo.Principal.FindFirstValue(ClaimTypes.Email);
+
+ //we are allowing auto-linking/creating of local accounts
+ if (email.IsNullOrWhiteSpace())
+ {
+ return AutoLinkSignInResult.FailedNoEmail;
+ }
+ else
+ {
+ //Now we need to perform the auto-link, so first we need to lookup/create a user with the email address
+ var autoLinkUser = await UserManager.FindByEmailAsync(email);
+ if (autoLinkUser != null)
+ {
+ try
+ {
+ //call the callback if one is assigned
+ autoLinkOptions.OnAutoLinking?.Invoke(autoLinkUser, loginInfo);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError(ex, "Could not link login provider {LoginProvider}.", loginInfo.LoginProvider);
+ return AutoLinkSignInResult.FailedException(ex.Message);
+ }
+
+ return await LinkUser(autoLinkUser, loginInfo);
+ }
+ else
+ {
+ var name = loginInfo.Principal?.Identity?.Name;
+ if (name.IsNullOrWhiteSpace()) throw new InvalidOperationException("The Name value cannot be null");
+
+ autoLinkUser = BackOfficeIdentityUser.CreateNew(_globalSettings, email, email, autoLinkOptions.GetUserAutoLinkCulture(_globalSettings), name);
+
+ foreach (var userGroup in autoLinkOptions.DefaultUserGroups)
+ {
+ autoLinkUser.AddRole(userGroup);
+ }
+
+ //call the callback if one is assigned
+ try
+ {
+ autoLinkOptions.OnAutoLinking?.Invoke(autoLinkUser, loginInfo);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError(ex, "Could not link login provider {LoginProvider}.", loginInfo.LoginProvider);
+ return AutoLinkSignInResult.FailedException(ex.Message);
+ }
+
+ var userCreationResult = await _userManager.CreateAsync(autoLinkUser);
+
+ if (!userCreationResult.Succeeded)
+ {
+ return AutoLinkSignInResult.FailedCreatingUser(userCreationResult.Errors.Select(x => x.Description).ToList());
+ }
+ else
+ {
+ return await LinkUser(autoLinkUser, loginInfo);
+ }
+ }
+ }
+ }
+
+ private async Task LinkUser(BackOfficeIdentityUser autoLinkUser, ExternalLoginInfo loginInfo)
+ {
+ var existingLogins = await _userManager.GetLoginsAsync(autoLinkUser);
+ var exists = existingLogins.FirstOrDefault(x => x.LoginProvider == loginInfo.LoginProvider && x.ProviderKey == loginInfo.ProviderKey);
+
+ // if it already exists (perhaps it was added in the AutoLink callbak) then we just continue
+ if (exists != null)
+ {
+ //sign in
+ return await SignInOrTwoFactorAsync(autoLinkUser, isPersistent: false, loginInfo.LoginProvider);
+ }
+
+ var linkResult = await _userManager.AddLoginAsync(autoLinkUser, loginInfo);
+ if (linkResult.Succeeded)
+ {
+ //we're good! sign in
+ return await SignInOrTwoFactorAsync(autoLinkUser, isPersistent: false, loginInfo.LoginProvider);
+ }
+
+ //If this fails, we should really delete the user since it will be in an inconsistent state!
+ var deleteResult = await _userManager.DeleteAsync(autoLinkUser);
+ if (deleteResult.Succeeded)
+ {
+ var errors = linkResult.Errors.Select(x => x.Description).ToList();
+ return AutoLinkSignInResult.FailedLinkingUser(errors);
+ }
+ else
+ {
+ //DOH! ... this isn't good, combine all errors to be shown
+ var errors = linkResult.Errors.Concat(deleteResult.Errors).Select(x => x.Description).ToList();
+ return AutoLinkSignInResult.FailedLinkingUser(errors);
+ }
+ }
}
}
diff --git a/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserManager.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeUserManager.cs
similarity index 89%
rename from src/Umbraco.Infrastructure/BackOffice/BackOfficeUserManager.cs
rename to src/Umbraco.Web.BackOffice/Security/BackOfficeUserManager.cs
index 1e5cd8436e..464f2a38aa 100644
--- a/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserManager.cs
+++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeUserManager.cs
@@ -3,9 +3,12 @@ using System.Collections.Generic;
using System.Security.Principal;
using System.Threading;
using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
+using Umbraco.Core;
+using Umbraco.Core.BackOffice;
using Umbraco.Core.Configuration;
using Umbraco.Core.Configuration.Models;
using Umbraco.Core.Models.Membership;
@@ -14,8 +17,10 @@ using Umbraco.Extensions;
using Umbraco.Net;
using Umbraco.Web.Models.ContentEditing;
-namespace Umbraco.Core.BackOffice
+
+namespace Umbraco.Web.Common.Security
{
+
public class BackOfficeUserManager : BackOfficeUserManager, IBackOfficeUserManager
{
public BackOfficeUserManager(
@@ -28,9 +33,10 @@ namespace Umbraco.Core.BackOffice
BackOfficeLookupNormalizer keyNormalizer,
BackOfficeIdentityErrorDescriber errors,
IServiceProvider services,
+ IHttpContextAccessor httpContextAccessor,
ILogger> logger,
IOptions passwordConfiguration)
- : base(ipResolver, store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger, passwordConfiguration)
+ : base(ipResolver, store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, httpContextAccessor, logger, passwordConfiguration)
{
}
}
@@ -39,6 +45,7 @@ namespace Umbraco.Core.BackOffice
where T : BackOfficeIdentityUser
{
private PasswordGenerator _passwordGenerator;
+ private readonly IHttpContextAccessor _httpContextAccessor;
public BackOfficeUserManager(
IIpResolver ipResolver,
@@ -50,11 +57,13 @@ namespace Umbraco.Core.BackOffice
BackOfficeLookupNormalizer keyNormalizer,
BackOfficeIdentityErrorDescriber errors,
IServiceProvider services,
+ IHttpContextAccessor httpContextAccessor,
ILogger> logger,
IOptions passwordConfiguration)
: base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger)
{
IpResolver = ipResolver ?? throw new ArgumentNullException(nameof(ipResolver));
+ _httpContextAccessor = httpContextAccessor;
PasswordConfiguration = passwordConfiguration.Value ?? throw new ArgumentNullException(nameof(passwordConfiguration));
}
@@ -95,7 +104,7 @@ namespace Umbraco.Core.BackOffice
{
var userSessionStore = Store as IUserSessionStore;
//if this is not set, for backwards compat (which would be super rare), we'll just approve it
- if (userSessionStore == null) return true;
+ if (userSessionStore == null) return true;
return await userSessionStore.ValidateSessionIdAsync(userId, sessionId);
}
@@ -106,7 +115,7 @@ namespace Umbraco.Core.BackOffice
///
protected virtual IPasswordHasher GetDefaultPasswordHasher(IPasswordConfiguration passwordConfiguration)
{
- //we can use the user aware password hasher (which will be the default and preferred way)
+ // we can use the user aware password hasher (which will be the default and preferred way)
return new PasswordHasher();
}
@@ -131,16 +140,22 @@ namespace Umbraco.Core.BackOffice
///
/// Override to check the user approval value as well as the user lock out date, by default this only checks the user's locked out date
///
- ///
- ///
+ /// The user
+ /// True if the user is locked out, else false
///
/// In the ASP.NET Identity world, there is only one value for being locked out, in Umbraco we have 2 so when checking this for Umbraco we need to check both values
///
public override async Task IsLockedOutAsync(T user)
{
- if (user == null) throw new ArgumentNullException(nameof(user));
+ if (user == null)
+ {
+ throw new ArgumentNullException(nameof(user));
+ }
- if (user.IsApproved == false) return true;
+ if (user.IsApproved == false)
+ {
+ return true;
+ }
return await base.IsLockedOutAsync(user);
}
@@ -211,7 +226,9 @@ namespace Umbraco.Core.BackOffice
var result = await base.ResetPasswordAsync(user, token, newPassword);
if (result.Succeeded)
- RaisePasswordChangedEvent(null, userId); // TODO: How can we get the current user? we have not HttpContext (netstandard), we can make our own IPrincipalAccessor?
+ {
+ RaisePasswordChangedEvent(_httpContextAccessor.HttpContext?.User, userId);
+ }
return result;
}
@@ -219,7 +236,9 @@ namespace Umbraco.Core.BackOffice
{
var result = await base.ChangePasswordAsync(user, currentPassword, newPassword);
if (result.Succeeded)
- RaisePasswordChangedEvent(null, user.Id); // TODO: How can we get the current user? we have not HttpContext (netstandard), we can make our own IPrincipalAccessor?
+ {
+ RaisePasswordChangedEvent(_httpContextAccessor.HttpContext?.User, user.Id);
+ }
return result;
}
@@ -297,11 +316,11 @@ namespace Umbraco.Core.BackOffice
// The way we unlock is by setting the lockoutEnd date to the current datetime
if (result.Succeeded && lockoutEnd >= DateTimeOffset.UtcNow)
{
- RaiseAccountLockedEvent(null, user.Id); // TODO: How can we get the current user? we have not HttpContext (netstandard), we can make our own IPrincipalAccessor?
+ RaiseAccountLockedEvent(_httpContextAccessor.HttpContext?.User, user.Id);
}
else
{
- RaiseAccountUnlockedEvent(null, user.Id); // TODO: How can we get the current user? we have not HttpContext (netstandard), we can make our own IPrincipalAccessor?
+ RaiseAccountUnlockedEvent(_httpContextAccessor.HttpContext?.User, user.Id);
//Resets the login attempt fails back to 0 when unlock is clicked
await ResetAccessFailedCountAsync(user);
}
@@ -321,7 +340,7 @@ namespace Umbraco.Core.BackOffice
await lockoutStore.ResetAccessFailedCountAsync(user, CancellationToken.None);
//raise the event now that it's reset
- RaiseResetAccessFailedCountEvent(null, user.Id); // TODO: How can we get the current user? we have not HttpContext (netstandard), we can make our own IPrincipalAccessor?
+ RaiseResetAccessFailedCountEvent(_httpContextAccessor.HttpContext?.User, user.Id);
return await UpdateAsync(user);
}
@@ -357,8 +376,7 @@ namespace Umbraco.Core.BackOffice
//Slightly confusing: this will return a Success if we successfully update the AccessFailed count
if (result.Succeeded)
{
- // TODO: This may no longer be the case in netcore, we'll need to see about that
- RaiseLoginFailedEvent(null, user.Id); // TODO: How can we get the current user? we have not HttpContext (netstandard), we can make our own IPrincipalAccessor?
+ RaiseLoginFailedEvent(_httpContextAccessor.HttpContext?.User, user.Id);
}
return result;
@@ -367,7 +385,7 @@ namespace Umbraco.Core.BackOffice
private int GetCurrentUserId(IPrincipal currentUser)
{
var umbIdentity = currentUser?.GetUmbracoIdentity();
- var currentUserId = umbIdentity?.GetUserId() ?? Constants.Security.SuperUserId;
+ var currentUserId = umbIdentity?.GetUserId() ?? Core.Constants.Security.SuperUserId;
return currentUserId;
}
private IdentityAuditEventArgs CreateArgs(AuditEvent auditEvent, IPrincipal currentUser, int affectedUserId, string affectedUsername)
@@ -383,9 +401,9 @@ namespace Umbraco.Core.BackOffice
return new IdentityAuditEventArgs(auditEvent, ip, currentUserId, string.Empty, affectedUserId, affectedUsername);
}
- // TODO: Review where these are raised and see if they can be simplified and either done in the this usermanager or the signin manager, lastly we'll resort to the authentication controller
- // In some cases it will be nicer/easier to not pass in IPrincipal
- public void RaiseAccountLockedEvent(BackOfficeIdentityUser currentUser, int userId) => OnAccountLocked(CreateArgs(AuditEvent.AccountLocked, currentUser, userId, string.Empty));
+ // TODO: Review where these are raised and see if they can be simplified and either done in the this usermanager or the signin manager,
+ // lastly we'll resort to the authentication controller but we should try to remove all instances of that occuring
+ public void RaiseAccountLockedEvent(IPrincipal currentUser, int userId) => OnAccountLocked(CreateArgs(AuditEvent.AccountLocked, currentUser, userId, string.Empty));
public void RaiseAccountUnlockedEvent(IPrincipal currentUser, int userId) => OnAccountUnlocked(CreateArgs(AuditEvent.AccountUnlocked, currentUser, userId, string.Empty));
@@ -395,11 +413,9 @@ namespace Umbraco.Core.BackOffice
public void RaiseLoginFailedEvent(IPrincipal currentUser, int userId) => OnLoginFailed(CreateArgs(AuditEvent.LoginFailed, currentUser, userId, string.Empty));
- public void RaiseInvalidLoginAttemptEvent(IPrincipal currentUser, string username) => OnLoginFailed(CreateArgs(AuditEvent.LoginFailed, currentUser, Constants.Security.SuperUserId, username));
-
public void RaiseLoginRequiresVerificationEvent(IPrincipal currentUser, int userId) => OnLoginRequiresVerification(CreateArgs(AuditEvent.LoginRequiresVerification, currentUser, userId, string.Empty));
- public void RaiseLoginSuccessEvent(BackOfficeIdentityUser currentUser, int userId) => OnLoginSuccess(CreateArgs(AuditEvent.LoginSucces, currentUser, userId, string.Empty));
+ public void RaiseLoginSuccessEvent(IPrincipal currentUser, int userId) => OnLoginSuccess(CreateArgs(AuditEvent.LoginSucces, currentUser, userId, string.Empty));
public SignOutAuditEventArgs RaiseLogoutSuccessEvent(IPrincipal currentUser, int userId)
{
@@ -408,7 +424,7 @@ namespace Umbraco.Core.BackOffice
OnLogoutSuccess(args);
return args;
}
-
+
public void RaisePasswordChangedEvent(IPrincipal currentUser, int userId) => OnPasswordChanged(CreateArgs(AuditEvent.LogoutSuccess, currentUser, userId, string.Empty));
public void RaiseResetAccessFailedCountEvent(IPrincipal currentUser, int userId) => OnResetAccessFailedCount(CreateArgs(AuditEvent.ResetAccessFailedCount, currentUser, userId, string.Empty));
@@ -424,6 +440,9 @@ namespace Umbraco.Core.BackOffice
public bool HasSendingUserInviteEventHandler => SendingUserInvite != null;
+ // TODO: These static events are problematic. Moving forward we don't want static events at all but we cannot
+ // have non-static events here because the user manager is a Scoped instance not a singleton
+ // so we'll have to deal with this a diff way i.e. refactoring how events are done entirely
public static event EventHandler AccountLocked;
public static event EventHandler AccountUnlocked;
public static event EventHandler ForgotPasswordRequested;
diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficeUserManagerAuditer.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeUserManagerAuditer.cs
new file mode 100644
index 0000000000..019eed7e39
--- /dev/null
+++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeUserManagerAuditer.cs
@@ -0,0 +1,163 @@
+using Microsoft.Extensions.Options;
+using System;
+using System.Threading.Tasks;
+using Umbraco.Core;
+using Umbraco.Core.BackOffice;
+using Umbraco.Core.Compose;
+using Umbraco.Core.Configuration.Models;
+using Umbraco.Core.Models.Membership;
+using Umbraco.Core.Services;
+
+namespace Umbraco.Web.Common.Security
+{
+ ///
+ /// Binds to events to write audit logs for the
+ ///
+ internal class BackOfficeUserManagerAuditer : IDisposable
+ {
+ private readonly IAuditService _auditService;
+ private readonly IUserService _userService;
+ private readonly GlobalSettings _globalSettings;
+ private bool _disposedValue;
+
+ public BackOfficeUserManagerAuditer(IAuditService auditService, IUserService userService, IOptions globalSettings)
+ {
+ _auditService = auditService;
+ _userService = userService;
+ _globalSettings = globalSettings.Value;
+ }
+
+ ///
+ /// Binds to events to start auditing
+ ///
+ public void Start()
+ {
+ // NOTE: This was migrated as-is from v8 including these missing entries
+ // TODO: See note about static events in BackOfficeUserManager
+ //BackOfficeUserManager.AccountLocked += ;
+ //BackOfficeUserManager.AccountUnlocked += ;
+ BackOfficeUserManager.ForgotPasswordRequested += OnForgotPasswordRequest;
+ BackOfficeUserManager.ForgotPasswordChangedSuccess += OnForgotPasswordChange;
+ BackOfficeUserManager.LoginFailed += OnLoginFailed;
+ //BackOfficeUserManager.LoginRequiresVerification += ;
+ BackOfficeUserManager.LoginSuccess += OnLoginSuccess;
+ BackOfficeUserManager.LogoutSuccess += OnLogoutSuccess;
+ BackOfficeUserManager.PasswordChanged += OnPasswordChanged;
+ BackOfficeUserManager.PasswordReset += OnPasswordReset;
+ //BackOfficeUserManager.ResetAccessFailedCount += ;
+ }
+
+ private IUser GetPerformingUser(int userId)
+ {
+ var found = userId >= 0 ? _userService.GetUserById(userId) : null;
+ return found ?? AuditEventsComponent.UnknownUser(_globalSettings);
+ }
+
+ private static string FormatEmail(IMembershipUser user)
+ {
+ return user == null ? string.Empty : user.Email.IsNullOrWhiteSpace() ? "" : $"<{user.Email}>";
+ }
+
+ private void OnLoginSuccess(object sender, IdentityAuditEventArgs args)
+ {
+ var performingUser = GetPerformingUser(args.PerformingUser);
+ WriteAudit(performingUser, args.AffectedUser, args.IpAddress, "umbraco/user/sign-in/login", "login success");
+ }
+
+ private void OnLogoutSuccess(object sender, IdentityAuditEventArgs args)
+ {
+ var performingUser = GetPerformingUser(args.PerformingUser);
+ WriteAudit(performingUser, args.AffectedUser, args.IpAddress, "umbraco/user/sign-in/logout", "logout success");
+ }
+
+ private void OnPasswordReset(object sender, IdentityAuditEventArgs args)
+ {
+ WriteAudit(args.PerformingUser, args.AffectedUser, args.IpAddress, "umbraco/user/password/reset", "password reset");
+ }
+
+ private void OnPasswordChanged(object sender, IdentityAuditEventArgs args)
+ {
+ WriteAudit(args.PerformingUser, args.AffectedUser, args.IpAddress, "umbraco/user/password/change", "password change");
+ }
+
+ private void OnLoginFailed(object sender, IdentityAuditEventArgs args)
+ {
+ WriteAudit(args.PerformingUser, 0, args.IpAddress, "umbraco/user/sign-in/failed", "login failed", affectedDetails: "");
+ }
+
+ private void OnForgotPasswordChange(object sender, IdentityAuditEventArgs args)
+ {
+ WriteAudit(args.PerformingUser, args.AffectedUser, args.IpAddress, "umbraco/user/password/forgot/change", "password forgot/change");
+ }
+
+ private void OnForgotPasswordRequest(object sender, IdentityAuditEventArgs args)
+ {
+ WriteAudit(args.PerformingUser, args.AffectedUser, args.IpAddress, "umbraco/user/password/forgot/request", "password forgot/request");
+ }
+
+ private void WriteAudit(int performingId, int affectedId, string ipAddress, string eventType, string eventDetails, string affectedDetails = null)
+ {
+ var performingUser = _userService.GetUserById(performingId);
+
+ var performingDetails = performingUser == null
+ ? $"User UNKNOWN:{performingId}"
+ : $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}";
+
+ WriteAudit(performingId, performingDetails, affectedId, ipAddress, eventType, eventDetails, affectedDetails);
+ }
+
+ private void WriteAudit(IUser performingUser, int affectedId, string ipAddress, string eventType, string eventDetails)
+ {
+ var performingDetails = performingUser == null
+ ? $"User UNKNOWN"
+ : $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}";
+
+ WriteAudit(performingUser?.Id ?? 0, performingDetails, affectedId, ipAddress, eventType, eventDetails);
+ }
+
+ private void WriteAudit(int performingId, string performingDetails, int affectedId, string ipAddress, string eventType, string eventDetails, string affectedDetails = null)
+ {
+ if (affectedDetails == null)
+ {
+ var affectedUser = _userService.GetUserById(affectedId);
+ affectedDetails = affectedUser == null
+ ? $"User UNKNOWN:{affectedId}"
+ : $"User \"{affectedUser.Name}\" {FormatEmail(affectedUser)}";
+ }
+
+ _auditService.Write(performingId, performingDetails,
+ ipAddress,
+ DateTime.UtcNow,
+ affectedId, affectedDetails,
+ eventType, eventDetails);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!_disposedValue)
+ {
+ if (disposing)
+ {
+ //BackOfficeUserManager.AccountLocked -= ;
+ //BackOfficeUserManager.AccountUnlocked -= ;
+ BackOfficeUserManager.ForgotPasswordRequested -= OnForgotPasswordRequest;
+ BackOfficeUserManager.ForgotPasswordChangedSuccess -= OnForgotPasswordChange;
+ BackOfficeUserManager.LoginFailed -= OnLoginFailed;
+ //BackOfficeUserManager.LoginRequiresVerification -= ;
+ BackOfficeUserManager.LoginSuccess -= OnLoginSuccess;
+ BackOfficeUserManager.LogoutSuccess -= OnLogoutSuccess;
+ BackOfficeUserManager.PasswordChanged -= OnPasswordChanged;
+ BackOfficeUserManager.PasswordReset -= OnPasswordReset;
+ }
+ _disposedValue = true;
+ }
+ }
+
+ public void Dispose()
+ {
+ // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+ }
+}
diff --git a/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeCookieOptions.cs b/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeCookieOptions.cs
index 277e9e3dfc..bd816e9382 100644
--- a/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeCookieOptions.cs
+++ b/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeCookieOptions.cs
@@ -105,8 +105,7 @@ namespace Umbraco.Web.BackOffice.Security
_runtimeState,
_hostingEnvironment,
_globalSettings,
- _requestCache,
- _linkGenerator);
+ _requestCache);
// _explicitPaths); TODO: Implement this once we do OAuth somehow
@@ -119,13 +118,13 @@ namespace Umbraco.Web.BackOffice.Security
// It would be possible to re-use the default behavior if any of these need to be set but that must be taken into account else
// our back office requests will not function correctly. For now we don't need to set/configure any of these callbacks because
// the defaults work fine with our setup.
-
+
OnValidatePrincipal = async ctx =>
{
// We need to resolve the BackOfficeSecurityStampValidator per request as a requirement (even in aspnetcore they do this)
var securityStampValidator = ctx.HttpContext.RequestServices.GetRequiredService();
// Same goes for the signinmanager
- var signInManager = ctx.HttpContext.RequestServices.GetRequiredService();
+ var signInManager = ctx.HttpContext.RequestServices.GetRequiredService();
var backOfficeIdentity = ctx.Principal.GetUmbracoIdentity();
if (backOfficeIdentity == null)
@@ -177,7 +176,7 @@ namespace Umbraco.Web.BackOffice.Security
// occurs when sign in is successful and after the ticket is written to the outbound cookie
// When we are signed in with the cookie, assign the principal to the current HttpContext
- ctx.HttpContext.User = ctx.Principal;
+ ctx.HttpContext.User = ctx.Principal;
return Task.CompletedTask;
},
@@ -226,7 +225,7 @@ namespace Umbraco.Web.BackOffice.Security
private async Task EnsureValidSessionId(CookieValidatePrincipalContext context)
{
if (_runtimeState.Level != RuntimeLevel.Run) return;
-
+
using var scope = _serviceProvider.CreateScope();
var validator = scope.ServiceProvider.GetRequiredService();
await validator.ValidateSessionAsync(TimeSpan.FromMinutes(1), context);
diff --git a/src/Umbraco.Web.Common/Security/ExternalSignInAutoLinkOptions.cs b/src/Umbraco.Web.BackOffice/Security/ExternalSignInAutoLinkOptions.cs
similarity index 92%
rename from src/Umbraco.Web.Common/Security/ExternalSignInAutoLinkOptions.cs
rename to src/Umbraco.Web.BackOffice/Security/ExternalSignInAutoLinkOptions.cs
index 0a81a503dd..8d9f57945b 100644
--- a/src/Umbraco.Web.Common/Security/ExternalSignInAutoLinkOptions.cs
+++ b/src/Umbraco.Web.BackOffice/Security/ExternalSignInAutoLinkOptions.cs
@@ -5,9 +5,8 @@ using Umbraco.Core.BackOffice;
using Umbraco.Core.Configuration.Models;
using SecurityConstants = Umbraco.Core.Constants.Security;
-namespace Umbraco.Web.Common.Security
+namespace Umbraco.Web.BackOffice.Security
{
-
///
/// Options used to configure auto-linking external OAuth providers
///
@@ -22,10 +21,12 @@ namespace Umbraco.Web.Common.Security
public ExternalSignInAutoLinkOptions(
bool autoLinkExternalAccount = false,
string[] defaultUserGroups = null,
- string defaultCulture = null)
+ string defaultCulture = null,
+ bool allowManualLinking = true)
{
DefaultUserGroups = defaultUserGroups ?? new[] { SecurityConstants.EditorGroupAlias };
AutoLinkExternalAccount = autoLinkExternalAccount;
+ AllowManualLinking = allowManualLinking;
_defaultCulture = defaultCulture;
}
@@ -33,7 +34,7 @@ namespace Umbraco.Web.Common.Security
/// By default this is true which allows the user to manually link and unlink the external provider, if set to false the back office user
/// will not see and cannot perform manual linking or unlinking of the external provider.
///
- public bool AllowManualLinking { get; set; } = true;
+ public bool AllowManualLinking { get; }
///
/// A callback executed during account auto-linking and before the user is persisted
diff --git a/src/Umbraco.Web.BackOffice/Security/IBackOfficeExternalLoginProviders.cs b/src/Umbraco.Web.BackOffice/Security/IBackOfficeExternalLoginProviders.cs
new file mode 100644
index 0000000000..ff22b91b0a
--- /dev/null
+++ b/src/Umbraco.Web.BackOffice/Security/IBackOfficeExternalLoginProviders.cs
@@ -0,0 +1,41 @@
+using Microsoft.AspNetCore.Authentication.OAuth;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Umbraco.Web.BackOffice.Security
+{
+
+ ///
+ /// Service to return instances
+ ///
+ public interface IBackOfficeExternalLoginProviders
+ {
+ ///
+ /// Get the for the specified scheme
+ ///
+ ///
+ ///
+ BackOfficeExternalLoginProvider Get(string authenticationType);
+
+ ///
+ /// Get all registered
+ ///
+ ///
+ IEnumerable GetBackOfficeProviders();
+
+ ///
+ /// Returns the authentication type for the last registered external login (oauth) provider that specifies an auto-login redirect option
+ ///
+ ///
+ ///
+ string GetAutoLoginProvider();
+
+ ///
+ /// Returns true if there is any external provider that has the Deny Local Login option configured
+ ///
+ ///
+ bool HasDenyLocalLogin();
+ }
+
+}
diff --git a/src/Umbraco.Web.BackOffice/Security/IBackOfficeSignInManager.cs b/src/Umbraco.Web.BackOffice/Security/IBackOfficeSignInManager.cs
new file mode 100644
index 0000000000..ce87484b2c
--- /dev/null
+++ b/src/Umbraco.Web.BackOffice/Security/IBackOfficeSignInManager.cs
@@ -0,0 +1,26 @@
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Identity;
+using System.Collections.Generic;
+using System.Security.Claims;
+using System.Threading.Tasks;
+using Umbraco.Core.BackOffice;
+
+namespace Umbraco.Web.Common.Security
+{
+ ///
+ /// A for the back office with a
+ ///
+ public interface IBackOfficeSignInManager
+ {
+ AuthenticationProperties ConfigureExternalAuthenticationProperties(string provider, string redirectUrl, string userId = null);
+ Task ExternalLoginSignInAsync(ExternalLoginInfo loginInfo, bool isPersistent, bool bypassTwoFactor = false);
+ Task> GetExternalAuthenticationSchemesAsync();
+ Task GetExternalLoginInfoAsync(string expectedXsrf = null);
+ Task GetTwoFactorAuthenticationUserAsync();
+ Task PasswordSignInAsync(string userName, string password, bool isPersistent, bool lockoutOnFailure);
+ Task SignOutAsync();
+ Task SignInAsync(BackOfficeIdentityUser user, bool isPersistent, string authenticationMethod = null);
+ Task CreateUserPrincipalAsync(BackOfficeIdentityUser user);
+ Task TwoFactorSignInAsync(string provider, string code, bool isPersistent, bool rememberClient);
+ }
+}
diff --git a/src/Umbraco.Web.BackOffice/Security/IBackOfficeTwoFactorOptions.cs b/src/Umbraco.Web.BackOffice/Security/IBackOfficeTwoFactorOptions.cs
new file mode 100644
index 0000000000..a05d71f3cb
--- /dev/null
+++ b/src/Umbraco.Web.BackOffice/Security/IBackOfficeTwoFactorOptions.cs
@@ -0,0 +1,16 @@
+namespace Umbraco.Web.BackOffice.Security
+{
+ ///
+ /// Options used to control 2FA for the Umbraco back office
+ ///
+ public interface IBackOfficeTwoFactorOptions
+ {
+ ///
+ /// Returns the angular view for handling 2FA interaction
+ ///
+ ///
+ ///
+ string GetTwoFactorView(string username);
+ }
+
+}
diff --git a/src/Umbraco.Web.BackOffice/Security/NoopBackOfficeTwoFactorOptions.cs b/src/Umbraco.Web.BackOffice/Security/NoopBackOfficeTwoFactorOptions.cs
new file mode 100644
index 0000000000..bbc0b3e049
--- /dev/null
+++ b/src/Umbraco.Web.BackOffice/Security/NoopBackOfficeTwoFactorOptions.cs
@@ -0,0 +1,8 @@
+namespace Umbraco.Web.BackOffice.Security
+{
+ public class NoopBackOfficeTwoFactorOptions : IBackOfficeTwoFactorOptions
+ {
+ public string GetTwoFactorView(string username) => null;
+ }
+
+}
diff --git a/src/Umbraco.Web.Common/ApplicationModels/BackOfficeApplicationModelProvider.cs b/src/Umbraco.Web.Common/ApplicationModels/BackOfficeApplicationModelProvider.cs
index 0ad6c4ec1a..11d82d4db5 100644
--- a/src/Umbraco.Web.Common/ApplicationModels/BackOfficeApplicationModelProvider.cs
+++ b/src/Umbraco.Web.Common/ApplicationModels/BackOfficeApplicationModelProvider.cs
@@ -6,6 +6,8 @@ using Umbraco.Web.Common.Attributes;
namespace Umbraco.Web.Common.ApplicationModels
{
+ // TODO: This should just exist in the back office project
+
///
/// An application model provider for all Umbraco Back Office controllers
///
@@ -49,12 +51,7 @@ namespace Umbraco.Web.Common.ApplicationModels
}
private bool IsBackOfficeController(ControllerModel controller)
- {
- var pluginControllerAttribute = controller.Attributes.OfType().FirstOrDefault();
- return pluginControllerAttribute != null
- && (pluginControllerAttribute.AreaName == Core.Constants.Web.Mvc.BackOfficeArea
- || pluginControllerAttribute.AreaName == Core.Constants.Web.Mvc.BackOfficeApiArea
- || pluginControllerAttribute.AreaName == Core.Constants.Web.Mvc.BackOfficeTreeArea);
- }
+ => controller.Attributes.OfType().Any();
+
}
}
diff --git a/src/Umbraco.Web.Common/ApplicationModels/BackOfficeIdentityCultureConvention.cs b/src/Umbraco.Web.Common/ApplicationModels/BackOfficeIdentityCultureConvention.cs
index d3e2096dd3..0a5a1f9945 100644
--- a/src/Umbraco.Web.Common/ApplicationModels/BackOfficeIdentityCultureConvention.cs
+++ b/src/Umbraco.Web.Common/ApplicationModels/BackOfficeIdentityCultureConvention.cs
@@ -3,6 +3,9 @@ using Umbraco.Web.Common.Filters;
namespace Umbraco.Web.Common.ApplicationModels
{
+
+ // TODO: This should just exist in the back office project
+
public class BackOfficeIdentityCultureConvention : IActionModelConvention
{
public void Apply(ActionModel action)
diff --git a/src/Umbraco.Web.Common/ApplicationModels/UmbracoApiBehaviorApplicationModelProvider.cs b/src/Umbraco.Web.Common/ApplicationModels/UmbracoApiBehaviorApplicationModelProvider.cs
index 918bc3776f..be296969e7 100644
--- a/src/Umbraco.Web.Common/ApplicationModels/UmbracoApiBehaviorApplicationModelProvider.cs
+++ b/src/Umbraco.Web.Common/ApplicationModels/UmbracoApiBehaviorApplicationModelProvider.cs
@@ -81,6 +81,7 @@ namespace Umbraco.Web.Common.ApplicationModels
}
}
- private bool IsUmbracoApiController(ControllerModel controller) => controller.Attributes.OfType().Any();
+ private bool IsUmbracoApiController(ControllerModel controller)
+ => controller.Attributes.OfType().Any();
}
}
diff --git a/src/Umbraco.Web.Common/ApplicationModels/UmbracoJsonModelBinderConvention.cs b/src/Umbraco.Web.Common/ApplicationModels/UmbracoJsonModelBinderConvention.cs
index 96c60398f0..42d23b33b3 100644
--- a/src/Umbraco.Web.Common/ApplicationModels/UmbracoJsonModelBinderConvention.cs
+++ b/src/Umbraco.Web.Common/ApplicationModels/UmbracoJsonModelBinderConvention.cs
@@ -2,6 +2,9 @@
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Umbraco.Web.Common.ModelBinding;
using System.Linq;
+using Umbraco.Web.Common.Attributes;
+using Umbraco.Web.Actions;
+using Umbraco.Web.Common.Filters;
namespace Umbraco.Web.Common.ApplicationModels
{
@@ -21,4 +24,6 @@ namespace Umbraco.Web.Common.ApplicationModels
}
}
}
+
+
}
diff --git a/src/Umbraco.Web.Common/Extensions/ControllerExtensions.cs b/src/Umbraco.Web.Common/Extensions/ControllerExtensions.cs
index cc52349699..b5fa9f946c 100644
--- a/src/Umbraco.Web.Common/Extensions/ControllerExtensions.cs
+++ b/src/Umbraco.Web.Common/Extensions/ControllerExtensions.cs
@@ -1,10 +1,29 @@
using System;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc;
+using Umbraco.Core;
namespace Umbraco.Extensions
{
public static class ControllerExtensions
{
+ ///
+ /// Runs the authentication process
+ ///
+ ///
+ ///
+ public static async Task AuthenticateBackOfficeAsync(this ControllerBase controller)
+ {
+ if (controller.HttpContext == null)
+ {
+ return AuthenticateResult.NoResult();
+ }
+
+ var result = await controller.HttpContext.AuthenticateAsync(Constants.Security.BackOfficeAuthenticationType);
+ return result;
+ }
+
///
/// Return the controller name from the controller type
///
diff --git a/src/Umbraco.Web.Common/Install/InstallAuthorizeAttribute.cs b/src/Umbraco.Web.Common/Install/InstallAuthorizeAttribute.cs
index 04f743144e..0989de5ba4 100644
--- a/src/Umbraco.Web.Common/Install/InstallAuthorizeAttribute.cs
+++ b/src/Umbraco.Web.Common/Install/InstallAuthorizeAttribute.cs
@@ -13,38 +13,32 @@ namespace Umbraco.Web.Common.Install
///
public class InstallAuthorizeAttribute : TypeFilterAttribute
{
- // NOTE: This doesn't need to be an authz policy, it's only used for the installer
-
public InstallAuthorizeAttribute() : base(typeof(InstallAuthorizeFilter))
{
}
private class InstallAuthorizeFilter : IAuthorizationFilter
{
- private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private readonly IRuntimeState _runtimeState;
private readonly ILogger _logger;
public InstallAuthorizeFilter(
- IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IRuntimeState runtimeState,
ILogger logger)
{
- _backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_runtimeState = runtimeState;
_logger = logger;
}
public void OnAuthorization(AuthorizationFilterContext authorizationFilterContext)
{
- if (!IsAllowed())
+ if (!IsAllowed(authorizationFilterContext))
{
authorizationFilterContext.Result = new ForbidResult();
}
-
}
- private bool IsAllowed()
+ private bool IsAllowed(AuthorizationFilterContext authorizationFilterContext)
{
try
{
@@ -52,7 +46,7 @@ namespace Umbraco.Web.Common.Install
// otherwise we need to ensure that a user is logged in
return _runtimeState.Level == RuntimeLevel.Install
|| _runtimeState.Level == RuntimeLevel.Upgrade
- || (_backOfficeSecurityAccessor?.BackOfficeSecurity?.ValidateCurrentUser() ?? false);
+ || (authorizationFilterContext.HttpContext.User?.Identity?.IsAuthenticated ?? false);
}
catch (Exception ex)
{
diff --git a/src/Umbraco.Web.Common/Install/InstallController.cs b/src/Umbraco.Web.Common/Install/InstallController.cs
index 1854d8dfbc..1e8264a2fc 100644
--- a/src/Umbraco.Web.Common/Install/InstallController.cs
+++ b/src/Umbraco.Web.Common/Install/InstallController.cs
@@ -15,6 +15,7 @@ using Umbraco.Web.Install;
using Umbraco.Web.Security;
using Umbraco.Core.Configuration.Models;
using Microsoft.Extensions.Options;
+using Microsoft.AspNetCore.Authentication;
namespace Umbraco.Web.Common.Install
{
@@ -73,13 +74,11 @@ namespace Umbraco.Web.Common.Install
// Update ClientDependency version and delete its temp directories to make sure we get fresh caches
_runtimeMinifier.Reset();
- var result = _backofficeSecurityAccessor.BackOfficeSecurity.ValidateCurrentUser(false);
+ var authResult = await this.AuthenticateBackOfficeAsync();
- switch (result)
+ if (!authResult.Succeeded)
{
- case ValidateRequestAttempt.FailedNoPrivileges:
- case ValidateRequestAttempt.FailedNoContextId:
- return Redirect(_globalSettings.UmbracoPath + "/AuthorizeUpgrade?redir=" + Request.GetEncodedUrl());
+ return Redirect(_globalSettings.UmbracoPath + "/AuthorizeUpgrade?redir=" + Request.GetEncodedUrl());
}
}
diff --git a/src/Umbraco.Web.Common/Security/BackofficeSecurity.cs b/src/Umbraco.Web.Common/Security/BackofficeSecurity.cs
index dc312ed9ca..1ea44b1596 100644
--- a/src/Umbraco.Web.Common/Security/BackofficeSecurity.cs
+++ b/src/Umbraco.Web.Common/Security/BackofficeSecurity.cs
@@ -13,6 +13,7 @@ using Umbraco.Extensions;
namespace Umbraco.Web.Common.Security
{
+ // TODO: This is only for the back office, does it need to be in common?
public class BackOfficeSecurity : IBackOfficeSecurity
{
@@ -51,18 +52,6 @@ namespace Umbraco.Web.Common.Security
}
}
- ///
- public ValidateRequestAttempt AuthorizeRequest(bool throwExceptions = false)
- {
- // check for secure connection
- if (_globalSettings.UseHttps && !_httpContextAccessor.GetRequiredHttpContext().Request.IsHttps)
- {
- if (throwExceptions) throw new SecurityException("This installation requires a secure connection (via SSL). Please update the URL to include https://");
- return ValidateRequestAttempt.FailedNoSsl;
- }
- return ValidateCurrentUser(throwExceptions);
- }
-
///
public Attempt GetUserId()
{
@@ -83,40 +72,5 @@ namespace Umbraco.Web.Common.Security
return user.HasSectionAccess(section);
}
- ///
- public bool ValidateCurrentUser()
- {
- return ValidateCurrentUser(false, true) == ValidateRequestAttempt.Success;
- }
-
- ///
- public ValidateRequestAttempt ValidateCurrentUser(bool throwExceptions, bool requiresApproval = true)
- {
- //This will first check if the current user is already authenticated - which should be the case in nearly all circumstances
- // since the authentication happens in the Module, that authentication also checks the ticket expiry. We don't
- // need to check it a second time because that requires another decryption phase and nothing can tamper with it during the request.
-
- if (IsAuthenticated() == false)
- {
- //There is no user
- if (throwExceptions) throw new InvalidOperationException("The user has no umbraco contextid - try logging in");
- return ValidateRequestAttempt.FailedNoContextId;
- }
-
- var user = CurrentUser;
-
- // Check for console access
- if (user == null || (requiresApproval && user.IsApproved == false) || (user.IsLockedOut && RequestIsInUmbracoApplication(_httpContextAccessor, _globalSettings, _hostingEnvironment)))
- {
- if (throwExceptions) throw new ArgumentException("You have no privileges to the umbraco console. Please contact your administrator");
- return ValidateRequestAttempt.FailedNoPrivileges;
- }
- return ValidateRequestAttempt.Success;
- }
-
- private static bool RequestIsInUmbracoApplication(IHttpContextAccessor httpContextAccessor, GlobalSettings globalSettings, IHostingEnvironment hostingEnvironment)
- {
- return httpContextAccessor.GetRequiredHttpContext().Request.Path.ToString().IndexOf(hostingEnvironment.ToAbsolute(globalSettings.UmbracoPath), StringComparison.InvariantCultureIgnoreCase) > -1;
- }
}
}
diff --git a/src/Umbraco.Web.Common/Security/BackofficeSecurityFactory.cs b/src/Umbraco.Web.Common/Security/BackofficeSecurityFactory.cs
index 0a3c362971..d212f5a1e3 100644
--- a/src/Umbraco.Web.Common/Security/BackofficeSecurityFactory.cs
+++ b/src/Umbraco.Web.Common/Security/BackofficeSecurityFactory.cs
@@ -9,6 +9,8 @@ using Umbraco.Core.Services;
namespace Umbraco.Web.Common.Security
{
+ // TODO: This is only for the back office, does it need to be in common?
+
public class BackOfficeSecurityFactory: IBackOfficeSecurityFactory
{
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
diff --git a/src/Umbraco.Web.Common/Security/IBackOfficeExternalLoginProviders.cs b/src/Umbraco.Web.Common/Security/IBackOfficeExternalLoginProviders.cs
deleted file mode 100644
index 85dbf95272..0000000000
--- a/src/Umbraco.Web.Common/Security/IBackOfficeExternalLoginProviders.cs
+++ /dev/null
@@ -1,61 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-
-namespace Umbraco.Web.Common.Security
-{
- // TODO: We need to implement this and extend it to support the back office external login options
- // basically migrate things from AuthenticationManagerExtensions & AuthenticationOptionsExtensions
- // and use this to get the back office external login infos
- public interface IBackOfficeExternalLoginProviders
- {
- ExternalSignInAutoLinkOptions Get(string authenticationType);
-
- IEnumerable GetBackOfficeProviders();
-
- ///
- /// Returns the authentication type for the last registered external login (oauth) provider that specifies an auto-login redirect option
- ///
- ///
- ///
- string GetAutoLoginProvider();
-
- bool HasDenyLocalLogin();
- }
-
- // TODO: This class is just a placeholder for later
- public class NopBackOfficeExternalLoginProviders : IBackOfficeExternalLoginProviders
- {
- public ExternalSignInAutoLinkOptions Get(string authenticationType)
- {
- return null;
- }
-
- public string GetAutoLoginProvider()
- {
- return null;
- }
-
- public IEnumerable GetBackOfficeProviders()
- {
- return Enumerable.Empty();
- }
-
- public bool HasDenyLocalLogin()
- {
- return false;
- }
- }
-
- // TODO: we'll need to register these somehow
- public class BackOfficeExternalLoginProvider
- {
- public string Name { get; set; }
- public string AuthenticationType { get; set; }
-
- // TODO: I believe this should be replaced with just a reference to BackOfficeExternalLoginProviderOptions
- public IReadOnlyDictionary Properties { get; set; }
- }
-
-}
diff --git a/src/Umbraco.Web.UI.Client/gulp/tasks/watchTask.js b/src/Umbraco.Web.UI.Client/gulp/tasks/watchTask.js
index 7afb8d363f..979bdc895d 100644
--- a/src/Umbraco.Web.UI.Client/gulp/tasks/watchTask.js
+++ b/src/Umbraco.Web.UI.Client/gulp/tasks/watchTask.js
@@ -49,6 +49,7 @@ function watchTask(cb) {
console.log("copying " + group.files + " to " + destPath);
task = task.pipe( dest(destPath) );
});
+ return task;
},
js
)
diff --git a/src/Umbraco.Web.UI.Client/src/common/services/externallogininfo.service.js b/src/Umbraco.Web.UI.Client/src/common/services/externallogininfo.service.js
index 31914f4e58..b44f79dd65 100644
--- a/src/Umbraco.Web.UI.Client/src/common/services/externallogininfo.service.js
+++ b/src/Umbraco.Web.UI.Client/src/common/services/externallogininfo.service.js
@@ -14,8 +14,8 @@ function externalLoginInfoService(externalLoginInfo, umbRequestHelper) {
}
function getLoginProviderView(provider) {
- if (provider && provider.properties && provider.properties.UmbracoBackOfficeExternalLoginOptions && provider.properties.UmbracoBackOfficeExternalLoginOptions.CustomBackOfficeView) {
- return umbRequestHelper.convertVirtualToAbsolutePath(provider.properties.UmbracoBackOfficeExternalLoginOptions.CustomBackOfficeView);
+ if (provider && provider.properties && provider.properties.CustomBackOfficeView) {
+ return umbRequestHelper.convertVirtualToAbsolutePath(provider.properties.CustomBackOfficeView);
}
return null;
}
@@ -26,10 +26,10 @@ function externalLoginInfoService(externalLoginInfo, umbRequestHelper) {
*/
function hasDenyLocalLogin(provider) {
if (!provider) {
- return _.some(externalLoginInfo.providers, x => x.properties && x.properties.UmbracoBackOfficeExternalLoginOptions && (x.properties.UmbracoBackOfficeExternalLoginOptions.DenyLocalLogin === true));
+ return _.some(externalLoginInfo.providers, x => x.properties && (x.properties.DenyLocalLogin === true));
}
else {
- return provider && provider.properties && provider.properties.UmbracoBackOfficeExternalLoginOptions && (provider.properties.UmbracoBackOfficeExternalLoginOptions.DenyLocalLogin === true);
+ return provider && provider.properties && (provider.properties.DenyLocalLogin === true);
}
}
@@ -50,7 +50,7 @@ function externalLoginInfoService(externalLoginInfo, umbRequestHelper) {
return true;
}
else {
- return x.properties.ExternalSignInAutoLinkOptions.AllowManualLinking;
+ return x.properties.AutoLinkOptions.AllowManualLinking;
}
});
return providers;
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.html
index fdd2671200..330a57ab7d 100644
--- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.html
+++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.html
@@ -52,11 +52,11 @@
-