Merge remote-tracking branch 'origin/netcore/netcore' into feature/auth-policies-tests
This commit is contained in:
23
Directory.Build.props
Normal file
23
Directory.Build.props
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project>
|
||||
|
||||
<!-- Package references and additional files which are consumed by all projects -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="StyleCop.Analyzers" IsImplicitlyDefined="true" />
|
||||
<AdditionalFiles Include="$(MSBuildThisFileDirectory)linting\stylecop.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<Choose>
|
||||
<When Condition="$([System.Text.RegularExpressions.Regex]::IsMatch($(MSBuildProjectFile), 'Tests'))">
|
||||
<PropertyGroup>
|
||||
<CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)linting\codeanalysis.tests.ruleset</CodeAnalysisRuleSet>
|
||||
</PropertyGroup>
|
||||
</When>
|
||||
<Otherwise>
|
||||
<PropertyGroup>
|
||||
<CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)linting\codeanalysis.ruleset</CodeAnalysisRuleSet>
|
||||
</PropertyGroup>
|
||||
</Otherwise>
|
||||
</Choose>
|
||||
|
||||
</Project>
|
||||
26
Directory.Build.targets
Normal file
26
Directory.Build.targets
Normal file
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project>
|
||||
|
||||
<!-- Package versions for package references across all projects -->
|
||||
<ItemGroup>
|
||||
<!--Global Dependencies-->
|
||||
<PackageReference Update="StyleCop.Analyzers" Version="1.1.118">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!--Shared config files that have to exist at root level to work properly.-->
|
||||
<ConfigFilesToCopy Include="$(MSBuildThisFileDirectory)linting\.editorconfig" />
|
||||
</ItemGroup>
|
||||
|
||||
<!--Ensures our config files are up to date.-->
|
||||
<!--TODO Copy files to root once solution is split up into src/tests folder-->
|
||||
<Target Name="CopyFiles" BeforeTargets="Build">
|
||||
<Copy SourceFiles="@(ConfigFilesToCopy)"
|
||||
SkipUnchangedFiles = "true"
|
||||
DestinationFolder="$(MSBuildThisFileDirectory)src" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
447
linting/.editorconfig
Normal file
447
linting/.editorconfig
Normal file
@@ -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.
|
||||
##########################################
|
||||
16
linting/codeanalysis.ruleset
Normal file
16
linting/codeanalysis.ruleset
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RuleSet Name="CodeAnalysis" ToolsVersion="16.0">
|
||||
<Rules AnalyzerId="Roslynator.CSharp.Analyzers" RuleNamespace="Roslynator.CSharp">
|
||||
<Rule Id="RCS1090" Action="None" />
|
||||
</Rules>
|
||||
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers">
|
||||
<Rule Id="SA1101" Action="None" />
|
||||
<Rule Id="SA1200" Action="None" />
|
||||
<Rule Id="SA1308" Action="None" />
|
||||
<Rule Id="SA1309" Action="None" />
|
||||
<Rule Id="SA1311" Action="None" />
|
||||
<Rule Id="SA1402" Action="None" />
|
||||
<Rule Id="SA1413" Action="None" />
|
||||
<Rule Id="SA1629" Action="None" />
|
||||
</Rules>
|
||||
</RuleSet>
|
||||
26
linting/codeanalysis.tests.ruleset
Normal file
26
linting/codeanalysis.tests.ruleset
Normal file
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RuleSet Name="CodeAnalysis.Tests" ToolsVersion="16.0">
|
||||
<Include Path="codeanalysis.ruleset" Action="Default" />
|
||||
<Rules AnalyzerId="Microsoft.CodeAnalysis.CSharp" RuleNamespace="Microsoft.CodeAnalysis.CSharp">
|
||||
<Rule Id="CS1591" Action="None" />
|
||||
</Rules>
|
||||
<Rules AnalyzerId="Microsoft.CodeAnalysis.CSharp.Features" RuleNamespace="Microsoft.CodeAnalysis.CSharp.Features">
|
||||
<Rule Id="IDE0058" Action="None" />
|
||||
</Rules>
|
||||
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers">
|
||||
<Rule Id="SA0001" Action="None" />
|
||||
<Rule Id="SA1115" Action="None" />
|
||||
<Rule Id="SA1201" Action="None" />
|
||||
<Rule Id="SA1202" Action="None" />
|
||||
<Rule Id="SA1203" Action="None" />
|
||||
<Rule Id="SA1204" Action="None" />
|
||||
<Rule Id="SA1310" Action="None" />
|
||||
<Rule Id="SA1401" Action="None" />
|
||||
<Rule Id="SA1600" Action="None" />
|
||||
<Rule Id="SA1601" Action="None" />
|
||||
<Rule Id="SA1602" Action="None" />
|
||||
<Rule Id="SA1611" Action="None" />
|
||||
<Rule Id="SA1618" Action="None" />
|
||||
<Rule Id="SA1642" Action="None" />
|
||||
</Rules>
|
||||
</RuleSet>
|
||||
16
linting/stylecop.json
Normal file
16
linting/stylecop.json
Normal file
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
447
src/.editorconfig
Normal file
447
src/.editorconfig
Normal file
@@ -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.
|
||||
##########################################
|
||||
@@ -17,6 +17,8 @@ namespace Umbraco.Extensions
|
||||
/// <returns></returns>
|
||||
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
|
||||
/// <returns></returns>
|
||||
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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,10 +1,28 @@
|
||||
using System;
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Umbraco.Core.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the contract for that allows the parsing of chrontab expressions.
|
||||
/// </summary>
|
||||
public interface ICronTabParser
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns a value indicating whether a given chrontab expression is valid.
|
||||
/// </summary>
|
||||
/// <param name="cronTab">The chrontab expression to parse.</param>
|
||||
/// <returns>The <see cref="bool"/> result.</returns>
|
||||
bool IsValidCronTab(string cronTab);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the next occurence for the given chrontab expression from the given time.
|
||||
/// </summary>
|
||||
/// <param name="cronTab">The chrontab expression to parse.</param>
|
||||
/// <param name="time">The date and time to start from.</param>
|
||||
/// <returns>The <see cref="DateTime"/> representing the next occurence.</returns>
|
||||
DateTime GetNextOccurrence(string cronTab, DateTime time);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = "~_~")]
|
||||
|
||||
@@ -9,48 +9,38 @@ namespace Umbraco.Core.Security
|
||||
/// <summary>
|
||||
/// Gets the current user.
|
||||
/// </summary>
|
||||
/// <value>The current user.</value>
|
||||
/// <returns>The current user that has been authenticated for the request.</returns>
|
||||
/// <remarks>If authentication hasn't taken place this will be null.</remarks>
|
||||
// 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; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current user's id.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
/// <returns>The current user's Id that has been authenticated for the request.</returns>
|
||||
/// <remarks>If authentication hasn't taken place this will be unsuccessful.</remarks>
|
||||
// TODO: This should just be an extension method on ClaimsIdentity
|
||||
Attempt<int> GetUserId();
|
||||
|
||||
/// <summary>
|
||||
/// Validates the currently logged in user and ensures they are not timed out
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
bool ValidateCurrentUser();
|
||||
|
||||
/// <summary>
|
||||
/// Validates the current user assigned to the request and ensures the stored user data is valid
|
||||
/// </summary>
|
||||
/// <param name="throwExceptions">set to true if you want exceptions to be thrown if failed</param>
|
||||
/// <param name="requiresApproval">If true requires that the user is approved to be validated</param>
|
||||
/// <returns></returns>
|
||||
ValidateRequestAttempt ValidateCurrentUser(bool throwExceptions, bool requiresApproval = true);
|
||||
|
||||
/// <summary>
|
||||
/// Authorizes the full request, checks for SSL and validates the current user
|
||||
/// </summary>
|
||||
/// <param name="throwExceptions">set to true if you want exceptions to be thrown if failed</param>
|
||||
/// <returns></returns>
|
||||
ValidateRequestAttempt AuthorizeRequest(bool throwExceptions = false);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the specified user as access to the app
|
||||
/// </summary>
|
||||
/// <param name="section"></param>
|
||||
/// <param name="user"></param>
|
||||
/// <returns></returns>
|
||||
/// <remarks>If authentication hasn't taken place this will be unsuccessful.</remarks>
|
||||
// TODO: Should be part of IBackOfficeUserManager
|
||||
bool UserHasSectionAccess(string section, IUser user);
|
||||
|
||||
/// <summary>
|
||||
/// Ensures that a back office user is logged in
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
/// <remarks>This does not force authentication, that must be done before calls to this are made.</remarks>
|
||||
// TODO: Should be removed, this should not be necessary
|
||||
bool IsAuthenticated();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
namespace Umbraco.Core.Security
|
||||
{
|
||||
public enum ValidateRequestAttempt
|
||||
{
|
||||
Success = 0,
|
||||
|
||||
FailedNoPrivileges = 100,
|
||||
|
||||
//FailedTimedOut,
|
||||
|
||||
FailedNoContextId = 101,
|
||||
FailedNoSsl = 102
|
||||
}
|
||||
}
|
||||
@@ -15,13 +15,13 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.8" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="3.1.8" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="3.1.8" />
|
||||
<PackageReference Include="System.ComponentModel.Annotations" Version="4.7.0" />
|
||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="4.7.0" />
|
||||
<PackageReference Include="System.Reflection.Emit.Lightweight" Version="4.7.0" />
|
||||
<PackageReference Include="System.Runtime.Caching" Version="4.7.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.8" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="3.1.8" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="3.1.8" />
|
||||
<PackageReference Include="System.ComponentModel.Annotations" Version="4.7.0" />
|
||||
<PackageReference Include="System.Configuration.ConfigurationManager" Version="4.7.0" />
|
||||
<PackageReference Include="System.Reflection.Emit.Lightweight" Version="4.7.0" />
|
||||
<PackageReference Include="System.Runtime.Caching" Version="4.7.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -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<BackOfficeIdentityUser>,
|
||||
IUserRoleStore<BackOfficeIdentityUser>,
|
||||
IUserSecurityStampStore<BackOfficeIdentityUser>,
|
||||
IUserLockoutStore<BackOfficeIdentityUser>,
|
||||
IUserTwoFactorStore<BackOfficeIdentityUser>,
|
||||
IUserLockoutStore<BackOfficeIdentityUser>,
|
||||
IUserSessionStore<BackOfficeIdentityUser>
|
||||
|
||||
// TODO: This would require additional columns/tables for now people will need to implement this on their own
|
||||
//IUserPhoneNumberStore<BackOfficeIdentityUser, int>,
|
||||
// TODO: To do this we need to implement IQueryable - we'll have an IQuerable implementation soon with the UmbracoLinqPadDriver implementation
|
||||
//IQueryableUserStore<BackOfficeIdentityUser, int>
|
||||
// TODO: This would require additional columns/tables and then a lot of extra coding support to make this happen natively within umbraco
|
||||
//IUserTwoFactorStore<BackOfficeIdentityUser>,
|
||||
// TODO: This would require additional columns/tables for now people will need to implement this on their own
|
||||
//IUserPhoneNumberStore<BackOfficeIdentityUser, int>,
|
||||
// TODO: To do this we need to implement IQueryable - we'll have an IQuerable implementation soon with the UmbracoLinqPadDriver implementation
|
||||
//IQueryableUserStore<BackOfficeIdentityUser, int>
|
||||
{
|
||||
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> globalSettings, UmbracoMapper mapper)
|
||||
public BackOfficeUserStore(IScopeProvider scopeProvider, IUserService userService, IEntityService entityService, IExternalLoginService externalLoginService, IOptions<GlobalSettings> globalSettings, UmbracoMapper mapper)
|
||||
{
|
||||
_scopeProvider = scopeProvider;
|
||||
_userService = userService;
|
||||
_entityService = entityService;
|
||||
_externalLoginService = externalLoginService;
|
||||
@@ -156,7 +160,7 @@ namespace Umbraco.Core.BackOffice
|
||||
/// <param name="user"/>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns/>
|
||||
public async Task<IdentityResult> UpdateAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
|
||||
public Task<IdentityResult> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -627,35 +634,6 @@ namespace Umbraco.Core.BackOffice
|
||||
return user;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets whether two factor authentication is enabled for the user
|
||||
/// </summary>
|
||||
/// <param name="user"/>
|
||||
/// <param name="enabled"/>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns/>
|
||||
public virtual Task SetTwoFactorEnabledAsync(BackOfficeIdentityUser user, bool enabled, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
|
||||
user.TwoFactorEnabled = false;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether two factor authentication is enabled for the user
|
||||
/// </summary>
|
||||
/// <param name="user"/>
|
||||
/// <returns/>
|
||||
public virtual Task<bool> GetTwoFactorEnabledAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
#region IUserLockoutStore
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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<TUser>: IDisposable
|
||||
where TUser : BackOfficeIdentityUser
|
||||
{
|
||||
Task<string> GetUserIdAsync(TUser user);
|
||||
|
||||
Task<TUser> GetUserAsync(ClaimsPrincipal principal);
|
||||
|
||||
string GetUserId(ClaimsPrincipal principal);
|
||||
|
||||
Task<IList<UserLoginInfo>> GetLoginsAsync(TUser user);
|
||||
|
||||
Task<IdentityResult> DeleteAsync(TUser user);
|
||||
@@ -304,13 +311,14 @@ namespace Umbraco.Core.BackOffice
|
||||
/// </remarks>
|
||||
Task<string> 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; }
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<AuthenticationSchemeOptions> 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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -28,8 +28,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Backoffice.Security
|
||||
runtime,
|
||||
Mock.Of<IHostingEnvironment>(),
|
||||
globalSettings,
|
||||
Mock.Of<IRequestCache>(),
|
||||
Mock.Of<LinkGenerator>());
|
||||
Mock.Of<IRequestCache>());
|
||||
|
||||
var result = mgr.ShouldAuthenticateRequest(new Uri("http://localhost/umbraco"));
|
||||
|
||||
@@ -47,8 +46,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Backoffice.Security
|
||||
runtime,
|
||||
Mock.Of<IHostingEnvironment>(x => x.ApplicationVirtualPath == "/" && x.ToAbsolute(globalSettings.UmbracoPath) == "/umbraco"),
|
||||
globalSettings,
|
||||
Mock.Of<IRequestCache>(),
|
||||
Mock.Of<LinkGenerator>());
|
||||
Mock.Of<IRequestCache>());
|
||||
|
||||
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<IRuntimeState>(x => x.Level == RuntimeLevel.Run);
|
||||
|
||||
GetMockLinkGenerator(out var remainingTimeoutSecondsPath, out var isAuthPath);
|
||||
var mgr = new BackOfficeCookieManager(
|
||||
Mock.Of<IUmbracoContextAccessor>(),
|
||||
runtime,
|
||||
Mock.Of<IHostingEnvironment>(x => x.ApplicationVirtualPath == "/" && x.ToAbsolute(globalSettings.UmbracoPath) == "/umbraco" && x.ToAbsolute(Constants.SystemDirectories.Install) == "/install"),
|
||||
globalSettings,
|
||||
Mock.Of<IRequestCache>(),
|
||||
GetMockLinkGenerator(out var remainingTimeoutSecondsPath, out var isAuthPath));
|
||||
Mock.Of<IRequestCache>());
|
||||
|
||||
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<IHostingEnvironment>(x => x.ApplicationVirtualPath == "/" && x.ToAbsolute(globalSettings.UmbracoPath) == "/umbraco" && x.ToAbsolute(Constants.SystemDirectories.Install) == "/install"),
|
||||
globalSettings,
|
||||
Mock.Of<IRequestCache>(x => x.IsAvailable == true && x.Get(Constants.Security.ForceReAuthFlag) == "not null"),
|
||||
GetMockLinkGenerator(out var remainingTimeoutSecondsPath, out var isAuthPath));
|
||||
Mock.Of<IRequestCache>(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<IHostingEnvironment>(x => x.ApplicationVirtualPath == "/" && x.ToAbsolute(globalSettings.UmbracoPath) == "/umbraco" && x.ToAbsolute(Constants.SystemDirectories.Install) == "/install"),
|
||||
globalSettings,
|
||||
Mock.Of<IRequestCache>(),
|
||||
GetMockLinkGenerator(out var remainingTimeoutSecondsPath, out var isAuthPath));
|
||||
Mock.Of<IRequestCache>());
|
||||
|
||||
var result = mgr.ShouldAuthenticateRequest(new Uri($"http://localhost/notbackoffice"));
|
||||
Assert.IsFalse(result);
|
||||
|
||||
@@ -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<string>(), It.IsAny<IUser>()))
|
||||
.Returns(() => true);
|
||||
|
||||
|
||||
@@ -116,7 +116,6 @@ namespace Umbraco.Tests.Testing.TestingTests
|
||||
var memberService = Mock.Of<IMemberService>();
|
||||
var memberTypeService = Mock.Of<IMemberTypeService>();
|
||||
var membershipProvider = new MembersMembershipProvider(memberService, memberTypeService, Mock.Of<IUmbracoVersion>(), TestHelper.GetHostingEnvironment(), TestHelper.GetIpResolver());
|
||||
var membershipHelper = new MembershipHelper(Mock.Of<IHttpContextAccessor>(), Mock.Of<IPublishedMemberCache>(), membershipProvider, Mock.Of<RoleProvider>(), memberService, memberTypeService, Mock.Of<IPublicAccessService>(), AppCaches.Disabled, NullLoggerFactory.Instance, ShortStringHelper, Mock.Of<IEntityService>());
|
||||
var umbracoMapper = new UmbracoMapper(new MapDefinitionCollection(new[] { Mock.Of<IMapDefinition>() }));
|
||||
|
||||
var umbracoApiController = new FakeUmbracoApiController(new GlobalSettings(), Mock.Of<IUmbracoContextAccessor>(), Mock.Of<IBackOfficeSecurityAccessor>(), Mock.Of<ISqlContext>(), ServiceContext.CreatePartial(), AppCaches.NoCache, profilingLogger , Mock.Of<IRuntimeState>(), umbracoMapper, Mock.Of<IPublishedUrlProvider>());
|
||||
|
||||
@@ -22,18 +22,17 @@ namespace Umbraco.Web.BackOffice.Authorization
|
||||
|
||||
protected override Task<bool> 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -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<double> 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
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet]
|
||||
public bool IsAuthenticated()
|
||||
[AllowAnonymous]
|
||||
public async Task<bool> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -244,7 +265,7 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
/// </remarks>
|
||||
[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
|
||||
/// <returns></returns>
|
||||
[SetAngularAntiForgeryTokens]
|
||||
[Authorize(Policy = AuthorizationPolicies.DenyLocalLoginIfConfigured)]
|
||||
public async Task<UserDetail> PostLogin(LoginModel loginModel)
|
||||
public async Task<ActionResult<UserDetail>> 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
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[SetAngularAntiForgeryTokens]
|
||||
[AllowAnonymous]
|
||||
public async Task<ActionResult<IEnumerable<string>>> Get2FAProviders()
|
||||
{
|
||||
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
|
||||
@@ -414,6 +419,7 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
}
|
||||
|
||||
[SetAngularAntiForgeryTokens]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> PostSend2FACode([FromBody] string provider)
|
||||
{
|
||||
if (provider.IsNullOrWhiteSpace())
|
||||
@@ -459,6 +465,7 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
}
|
||||
|
||||
[SetAngularAntiForgeryTokens]
|
||||
[AllowAnonymous]
|
||||
public async Task<ActionResult<UserDetail>> 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");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -496,6 +503,7 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[SetAngularAntiForgeryTokens]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> PostSetPassword(SetPasswordModel model)
|
||||
{
|
||||
var identityUser = await _userManager.FindByIdAsync(model.UserId.ToString());
|
||||
@@ -560,13 +568,18 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[ValidateAngularAntiForgeryToken]
|
||||
public IActionResult PostLogout()
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> 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
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Return the <see cref="UserDetail"/> for the given <see cref="IUser"/>
|
||||
/// </summary>
|
||||
|
||||
@@ -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<BackOfficeController> _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<BackOfficeController> 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<IActionResult> 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<IActionResult> 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
|
||||
/// <returns></returns>
|
||||
[HttpGet]
|
||||
[StatusCodeResult(System.Net.HttpStatusCode.ServiceUnavailable)]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> 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
|
||||
/// <returns></returns>
|
||||
[MinifyJavaScriptResult(Order = 0)]
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> Application()
|
||||
{
|
||||
var result = await _runtimeMinifier.GetScriptForLoadingBackOfficeAsync(_globalSettings, _hostingEnvironment);
|
||||
@@ -199,6 +222,7 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
/// <param name="culture"></param>
|
||||
/// <returns></returns>
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public Dictionary<string, Dictionary<string, string>> 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<IActionResult> 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<IActionResult> 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
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private async Task<IActionResult> RenderDefaultOrProcessExternalLoginAsync(
|
||||
AuthenticateResult authenticateResult,
|
||||
Func<IActionResult> defaultResponse,
|
||||
Func<IActionResult> 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<string>();
|
||||
|
||||
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<bool> 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))
|
||||
|
||||
@@ -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
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
internal async Task<Dictionary<string, object>> GetServerVariablesAsync()
|
||||
internal Task<Dictionary<string, object>> GetServerVariablesAsync()
|
||||
{
|
||||
var globalSettings = _globalSettings;
|
||||
var backOfficeControllerName = ControllerExtensions.GetControllerName<BackOfficeController>();
|
||||
@@ -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<string, object>
|
||||
{
|
||||
{
|
||||
// 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]
|
||||
|
||||
@@ -135,7 +135,7 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[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()
|
||||
{
|
||||
|
||||
@@ -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.
|
||||
|
||||
/// <summary>
|
||||
/// An API controller used for dealing with content types
|
||||
/// </summary>
|
||||
[PluginController(Constants.Web.Mvc.BackOfficeApiArea)]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)]
|
||||
[PluginController(Constants.Web.Mvc.BackOfficeApiArea)]
|
||||
public class ContentTypeController : ContentTypeControllerBase<IContentType>
|
||||
{
|
||||
// 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
|
||||
/// <param name="id"></param>
|
||||
/// <returns></returns>
|
||||
[DetermineAmbiguousActionByPassingParameters]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)]
|
||||
public DocumentTypeDisplay GetById(int id)
|
||||
{
|
||||
var ct = _contentTypeService.Get(id);
|
||||
@@ -162,6 +160,7 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
/// <param name="id"></param>
|
||||
/// <returns></returns>
|
||||
[DetermineAmbiguousActionByPassingParameters]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)]
|
||||
public DocumentTypeDisplay GetById(Guid id)
|
||||
{
|
||||
var contentType = _contentTypeService.Get(id);
|
||||
@@ -180,6 +179,7 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
/// <param name="id"></param>
|
||||
/// <returns></returns>
|
||||
[DetermineAmbiguousActionByPassingParameters]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)]
|
||||
public DocumentTypeDisplay GetById(Udi id)
|
||||
{
|
||||
var guidUdi = id as GuidUdi;
|
||||
@@ -203,6 +203,7 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
/// <returns></returns>
|
||||
[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
|
||||
/// <param name="filter"></param>
|
||||
/// <returns></returns>
|
||||
[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
|
||||
/// <param name="filter"></param>
|
||||
/// <returns></returns>
|
||||
[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
|
||||
/// <returns></returns>
|
||||
[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<TemplateDisplay> PostCreateDefaultTemplate(int id)
|
||||
{
|
||||
var contentType = _contentTypeService.Get(id);
|
||||
@@ -472,6 +481,7 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
/// </summary>
|
||||
/// <param name="parentId"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)]
|
||||
public DocumentTypeDisplay GetEmpty(int parentId)
|
||||
{
|
||||
IContentType ct;
|
||||
@@ -493,6 +503,7 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
/// <summary>
|
||||
/// Returns all content type objects
|
||||
/// </summary>
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)]
|
||||
public IEnumerable<ContentTypeBasic> GetAll()
|
||||
{
|
||||
var types = _contentTypeService.GetAll();
|
||||
@@ -565,6 +576,7 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
/// </summary>
|
||||
/// <param name="move"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)]
|
||||
public IActionResult PostMove(MoveOrCopy move)
|
||||
{
|
||||
return PerformMove(
|
||||
@@ -578,6 +590,7 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
/// </summary>
|
||||
/// <param name="copy"></param>
|
||||
/// <returns></returns>
|
||||
[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<ActionResult<ContentTypeImportModel>> Upload(List<IFormFile> file)
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)]
|
||||
public ActionResult<ContentTypeImportModel> Upload(List<IFormFile> file)
|
||||
{
|
||||
var model = new ContentTypeImportModel();
|
||||
|
||||
|
||||
@@ -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
|
||||
/// </summary>
|
||||
[PluginController(Constants.Web.Mvc.BackOfficeApiArea)]
|
||||
//[PrefixlessBodyModelValidator] //TODO reintroduce
|
||||
[PrefixlessBodyModelValidator]
|
||||
public abstract class ContentTypeControllerBase<TContentType> : UmbracoAuthorizedJsonController
|
||||
where TContentType : class, IContentTypeComposition
|
||||
{
|
||||
|
||||
@@ -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.
|
||||
|
||||
/// <summary>
|
||||
/// An API controller used for dealing with content types
|
||||
/// An API controller used for dealing with content types
|
||||
/// </summary>
|
||||
[PluginController(Constants.Web.Mvc.BackOfficeApiArea)]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessMediaTypes)]
|
||||
public class MediaTypeController : ContentTypeControllerBase<IMediaType>
|
||||
{
|
||||
// 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
|
||||
/// <returns></returns>
|
||||
[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
|
||||
/// </param>
|
||||
/// <returns></returns>
|
||||
[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
|
||||
/// <param name="filter"></param>
|
||||
/// <returns></returns>
|
||||
[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
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns all media types
|
||||
/// Returns all media types
|
||||
/// </summary>
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessMediaTypes)]
|
||||
public IEnumerable<ContentTypeBasic> GetAll() =>
|
||||
_mediaTypeService.GetAll()
|
||||
.Select(_umbracoMapper.Map<IMediaType, ContentTypeBasic>);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a media type container with a given ID
|
||||
/// Deletes a media type container with a given ID
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <returns></returns>
|
||||
[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<MediaTypeDisplay, MediaTypeSave, PropertyTypeBasic>(
|
||||
@@ -284,10 +290,11 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Move the media type
|
||||
/// Move the media type
|
||||
/// </summary>
|
||||
/// <param name="move"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessMediaTypes)]
|
||||
public IActionResult PostMove(MoveOrCopy move)
|
||||
{
|
||||
return PerformMove(
|
||||
@@ -297,10 +304,11 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy the media type
|
||||
/// Copy the media type
|
||||
/// </summary>
|
||||
/// <param name="copy"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessMediaTypes)]
|
||||
public IActionResult PostCopy(MoveOrCopy copy)
|
||||
{
|
||||
return PerformCopy(
|
||||
@@ -313,7 +321,7 @@ namespace Umbraco.Web.BackOffice.Controllers
|
||||
#region GetAllowedChildren
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
/// <param name="contentId"></param>
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessMediaOrMediaTypes)]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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<BackOfficeUserManagerAuditer>();
|
||||
auditer.Start();
|
||||
return app;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ namespace Umbraco.Extensions
|
||||
.AddDefaultTokenProviders()
|
||||
.AddUserStore<BackOfficeUserStore>()
|
||||
.AddUserManager<IBackOfficeUserManager, BackOfficeUserManager>()
|
||||
.AddSignInManager<BackOfficeSignInManager>()
|
||||
.AddSignInManager<IBackOfficeSignInManager, BackOfficeSignInManager>()
|
||||
.AddClaimsPrincipalFactory<BackOfficeClaimsPrincipalFactory<BackOfficeIdentityUser>>();
|
||||
|
||||
// Configure the options specifically for the UmbracoBackOfficeIdentityOptions instance
|
||||
@@ -66,7 +66,9 @@ namespace Umbraco.Extensions
|
||||
services.TryAddScoped<BackOfficeLookupNormalizer>();
|
||||
services.TryAddScoped<BackOfficeIdentityErrorDescriber>();
|
||||
services.TryAddScoped<IIpResolver, AspNetCoreIpResolver>();
|
||||
services.TryAddSingleton<IBackOfficeExternalLoginProviders, NopBackOfficeExternalLoginProviders>();
|
||||
services.TryAddSingleton<IBackOfficeExternalLoginProviders, BackOfficeExternalLoginProviders>();
|
||||
services.TryAddSingleton<IBackOfficeTwoFactorOptions, NoopBackOfficeTwoFactorOptions>();
|
||||
services.TryAddSingleton<BackOfficeUserManagerAuditer>();
|
||||
|
||||
/*
|
||||
* IdentityBuilderExtensions.AddUserManager adds UserManager<BackOfficeIdentityUser> to service collection
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace Umbraco.Extensions
|
||||
public static class IdentityBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds a <see cref="UserManager{TUser}"/> for the <seealso cref="UserType"/>.
|
||||
/// Adds a <see cref="UserManager{TUser}"/> implementation for <seealso cref="BackOfficeIdentityUser"/>
|
||||
/// </summary>
|
||||
/// <typeparam name="TUserManager">The type of the user manager to add.</typeparam>
|
||||
/// <typeparam name="TInterface"></typeparam>
|
||||
@@ -18,5 +18,19 @@ namespace Umbraco.Extensions
|
||||
identityBuilder.Services.AddScoped(typeof(TInterface), typeof(TUserManager));
|
||||
return identityBuilder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a <see cref="SignInManager{TUser}"/> implementation for <seealso cref="BackOfficeIdentityUser"/>
|
||||
/// </summary>
|
||||
/// <typeparam name="TInterface"></typeparam>
|
||||
/// <typeparam name="TSignInManager"></typeparam>
|
||||
/// <param name="identityBuilder"></param>
|
||||
/// <returns></returns>
|
||||
public static IdentityBuilder AddSignInManager<TInterface, TSignInManager>(this IdentityBuilder identityBuilder) where TSignInManager : SignInManager<BackOfficeIdentityUser>, TInterface
|
||||
{
|
||||
identityBuilder.AddSignInManager<TSignInManager>();
|
||||
identityBuilder.Services.AddScoped(typeof(TInterface), typeof(TSignInManager));
|
||||
return identityBuilder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,17 +33,24 @@ namespace Umbraco.Extensions
|
||||
{
|
||||
builder.Services.AddAntiforgery();
|
||||
builder.Services.AddSingleton<IFilterProvider, OverrideAuthorizationFilterProvider>();
|
||||
|
||||
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<ConfigureBackOfficeCookieOptions>();
|
||||
return builder;
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ namespace Umbraco.Web.BackOffice.Filters
|
||||
private readonly IEntityService _entityService;
|
||||
private readonly ILocalizedTextService _localizedTextService;
|
||||
private readonly IOptions<GlobalSettings> _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> globalSettings,
|
||||
BackOfficeSignInManager backOfficeSignInManager,
|
||||
IBackOfficeSignInManager backOfficeSignInManager,
|
||||
IBackOfficeAntiforgery backOfficeAntiforgery)
|
||||
{
|
||||
_requestCache = requestCache;
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<BackOfficeExternalLoginsBuilder> builder)
|
||||
{
|
||||
builder(new BackOfficeExternalLoginsBuilder(umbracoBuilder.Services));
|
||||
return umbracoBuilder;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
48
src/Umbraco.Web.BackOffice/Security/AutoLinkSignInResult.cs
Normal file
48
src/Umbraco.Web.BackOffice/Security/AutoLinkSignInResult.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Umbraco.Web.Common.Security
|
||||
{
|
||||
/// <summary>
|
||||
/// Result returned from signing in when auto-linking takes place
|
||||
/// </summary>
|
||||
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<string> errors) => new AutoLinkSignInResult(errors)
|
||||
{
|
||||
Succeeded = false
|
||||
};
|
||||
|
||||
public static AutoLinkSignInResult FailedLinkingUser(IReadOnlyCollection<string> errors) => new AutoLinkSignInResult(errors)
|
||||
{
|
||||
Succeeded = false
|
||||
};
|
||||
|
||||
public AutoLinkSignInResult(IReadOnlyCollection<string> errors)
|
||||
{
|
||||
Errors = errors ?? throw new ArgumentNullException(nameof(errors));
|
||||
}
|
||||
|
||||
public AutoLinkSignInResult()
|
||||
{
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<string> Errors { get; } = Array.Empty<string>();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Custom <see cref="AuthenticationBuilder"/> used to associate external logins with umbraco external login options
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overridden to track the final authenticationScheme being registered for the external login
|
||||
/// </summary>
|
||||
/// <typeparam name="TOptions"></typeparam>
|
||||
/// <typeparam name="THandler"></typeparam>
|
||||
/// <param name="authenticationScheme"></param>
|
||||
/// <param name="displayName"></param>
|
||||
/// <param name="configureOptions"></param>
|
||||
/// <returns></returns>
|
||||
public override AuthenticationBuilder AddRemoteScheme<TOptions, THandler>(string authenticationScheme, string displayName, Action<TOptions> 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<IPostConfigureOptions<TOptions>, EnsureBackOfficeScheme<TOptions>>());
|
||||
|
||||
return base.AddRemoteScheme<TOptions, THandler>(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<TOptions> : IPostConfigureOptions<TOptions> where TOptions : RemoteAuthenticationOptions
|
||||
{
|
||||
public void PostConfigure(string name, TOptions options)
|
||||
{
|
||||
options.SignInScheme = Constants.Security.BackOfficeExternalAuthenticationType;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// A custom cookie manager that is used to read the cookie from the request.
|
||||
/// </summary>
|
||||
@@ -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.
|
||||
/// </remarks>
|
||||
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<string> explicitPaths)
|
||||
{
|
||||
_umbracoContextAccessor = umbracoContextAccessor;
|
||||
@@ -61,9 +56,9 @@ namespace Umbraco.Web.BackOffice.Security
|
||||
/// <summary>
|
||||
/// Determines if we should authenticate the request
|
||||
/// </summary>
|
||||
/// <param name="requestUri"></param>
|
||||
/// <param name="checkForceAuthTokens"></param>
|
||||
/// <returns></returns>
|
||||
/// <param name="requestUri">The <see cref="Uri"/> to check</param>
|
||||
/// <param name="checkForceAuthTokens">true to check if the <see cref="Constants.Security.ForceReAuthFlag"/> has been assigned in the request.</param>
|
||||
/// <returns>true if the request should be authenticated</returns>
|
||||
/// <remarks>
|
||||
/// 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
|
||||
/// <summary>
|
||||
/// Explicitly implement this so that we filter the request
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <param name="key"></param>
|
||||
/// <returns></returns>
|
||||
string ICookieManager.GetRequestCookie(HttpContext context, string key)
|
||||
/// <inheritdoc/>
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
using System;
|
||||
|
||||
namespace Umbraco.Web.BackOffice.Security
|
||||
{
|
||||
/// <summary>
|
||||
/// An external login (OAuth) provider for the back office
|
||||
/// </summary>
|
||||
public class BackOfficeExternalLoginProvider : IEquatable<BackOfficeExternalLoginProvider>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,23 +1,42 @@
|
||||
using System;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace Umbraco.Web.Common.Security
|
||||
namespace Umbraco.Web.BackOffice.Security
|
||||
{
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Options used to configure back office external login providers
|
||||
/// </summary>
|
||||
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; }
|
||||
|
||||
/// <summary>
|
||||
/// Options used to control how users can be auto-linked/created/updated based on the external login provider
|
||||
/// </summary>
|
||||
public ExternalSignInAutoLinkOptions AutoLinkOptions { get; set; } = new ExternalSignInAutoLinkOptions();
|
||||
public ExternalSignInAutoLinkOptions AutoLinkOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// When set to true will disable all local user login functionality
|
||||
/// </summary>
|
||||
public bool DenyLocalLogin { get; set; }
|
||||
public bool DenyLocalLogin { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 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 <see cref="DenyLocalLogin"/>. If more than one OAuth provider specifies this, the last registered
|
||||
/// provider's redirect settings will win.
|
||||
/// </remarks>
|
||||
public bool AutoRedirectLoginToExternalProvider { get; set; }
|
||||
public bool AutoRedirectLoginToExternalProvider { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
public string CustomBackOfficeView { get; set; }
|
||||
public string CustomBackOfficeView { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Umbraco.Web.BackOffice.Security
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class BackOfficeExternalLoginProviders : IBackOfficeExternalLoginProviders
|
||||
{
|
||||
public BackOfficeExternalLoginProviders(IEnumerable<BackOfficeExternalLoginProvider> externalLogins)
|
||||
{
|
||||
_externalLogins = externalLogins;
|
||||
}
|
||||
|
||||
private readonly IEnumerable<BackOfficeExternalLoginProvider> _externalLogins;
|
||||
|
||||
/// <inheritdoc />
|
||||
public BackOfficeExternalLoginProvider Get(string authenticationType)
|
||||
{
|
||||
return _externalLogins.FirstOrDefault(x => x.AuthenticationType == authenticationType);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetAutoLoginProvider()
|
||||
{
|
||||
var found = _externalLogins.Where(x => x.Options.AutoRedirectLoginToExternalProvider).ToList();
|
||||
return found.Count > 0 ? found[0].AuthenticationType : null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<BackOfficeExternalLoginProvider> GetBackOfficeProviders()
|
||||
{
|
||||
return _externalLogins;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool HasDenyLocalLogin()
|
||||
{
|
||||
var found = _externalLogins.Where(x => x.Options.DenyLocalLogin).ToList();
|
||||
return found.Count > 0;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
|
||||
namespace Umbraco.Web.BackOffice.Security
|
||||
{
|
||||
/// <summary>
|
||||
/// Used to add back office login providers
|
||||
/// </summary>
|
||||
public class BackOfficeExternalLoginsBuilder
|
||||
{
|
||||
public BackOfficeExternalLoginsBuilder(IServiceCollection services)
|
||||
{
|
||||
_services = services;
|
||||
}
|
||||
|
||||
private readonly IServiceCollection _services;
|
||||
|
||||
/// <summary>
|
||||
/// Add a back office login provider with options
|
||||
/// </summary>
|
||||
/// <param name="loginProviderOptions"></param>
|
||||
/// <param name="build"></param>
|
||||
/// <returns></returns>
|
||||
public BackOfficeExternalLoginsBuilder AddBackOfficeLogin(
|
||||
BackOfficeExternalLoginProviderOptions loginProviderOptions,
|
||||
Action<BackOfficeAuthenticationBuilder> build)
|
||||
{
|
||||
build(new BackOfficeAuthenticationBuilder(_services, loginProviderOptions));
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<BackOfficeIdentityUser>
|
||||
public class BackOfficeSignInManager : SignInManager<BackOfficeIdentityUser>, 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<BackOfficeIdentityUser> claimsFactory,
|
||||
IOptions<IdentityOptions> optionsAccessor,
|
||||
IOptions<GlobalSettings> globalSettings,
|
||||
ILogger<SignInManager<BackOfficeIdentityUser>> logger,
|
||||
IAuthenticationSchemeProvider schemes,
|
||||
IUserConfirmation<BackOfficeIdentityUser> 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.
|
||||
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<SignInResult> 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);
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<BackOfficeIdentityUser> GetTwoFactorAuthenticationUserAsync()
|
||||
{
|
||||
@@ -112,7 +119,7 @@ namespace Umbraco.Web.Common.Security
|
||||
return await HandleSignIn(user, user?.UserName, SignInResult.Failed);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool IsSignedIn(ClaimsPrincipal principal)
|
||||
{
|
||||
@@ -193,7 +200,7 @@ namespace Umbraco.Web.Common.Security
|
||||
await Context.SignOutAsync(Constants.Security.BackOfficeTwoFactorAuthenticationType);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<bool> IsTwoFactorClientRememberedAsync(BackOfficeIdentityUser user)
|
||||
{
|
||||
@@ -205,7 +212,7 @@ namespace Umbraco.Web.Common.Security
|
||||
return (result?.Principal != null && result.Principal.FindFirstValue(ClaimTypes.Name) == userId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task RememberTwoFactorClientAsync(BackOfficeIdentityUser user)
|
||||
{
|
||||
@@ -218,7 +225,7 @@ namespace Umbraco.Web.Common.Security
|
||||
new AuthenticationProperties { IsPersistent = true });
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task ForgetTwoFactorClientAsync()
|
||||
{
|
||||
@@ -228,7 +235,7 @@ namespace Umbraco.Web.Common.Security
|
||||
return Context.SignOutAsync(Constants.Security.BackOfficeTwoFactorRememberMeAuthenticationType);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<SignInResult> TwoFactorRecoveryCodeSignInAsync(string recoveryCode)
|
||||
{
|
||||
@@ -257,7 +264,7 @@ namespace Umbraco.Web.Common.Security
|
||||
return SignInResult.Failed;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<ExternalLoginInfo> 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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Custom ExternalLoginSignInAsync overload for handling external sign in with auto-linking
|
||||
/// </summary>
|
||||
/// <param name="loginProvider"></param>
|
||||
/// <param name="providerKey"></param>
|
||||
/// <param name="isPersistent"></param>
|
||||
/// <param name="bypassTwoFactor"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<SignInResult> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures the redirect URL and user identifier for the specified external login <paramref name="provider"/>.
|
||||
/// </summary>
|
||||
/// <param name="provider">The provider to configure.</param>
|
||||
/// <param name="redirectUrl">The external login URL users should be redirected to during the login flow.</param>
|
||||
/// <param name="userId">The current user's identifier, which will be used to provide CSRF protection.</param>
|
||||
/// <returns>A configured <see cref="AuthenticationProperties"/>.</returns>
|
||||
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<IEnumerable<AuthenticationScheme>> 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();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task<SignInResult> 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; }
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Used for auto linking/creating user accounts for external logins
|
||||
/// </summary>
|
||||
/// <param name="loginInfo"></param>
|
||||
/// <param name="autoLinkOptions"></param>
|
||||
/// <returns></returns>
|
||||
private async Task<SignInResult> 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<SignInResult> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<BackOfficeIdentityUser>, IBackOfficeUserManager
|
||||
{
|
||||
public BackOfficeUserManager(
|
||||
@@ -28,9 +33,10 @@ namespace Umbraco.Core.BackOffice
|
||||
BackOfficeLookupNormalizer keyNormalizer,
|
||||
BackOfficeIdentityErrorDescriber errors,
|
||||
IServiceProvider services,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
ILogger<UserManager<BackOfficeIdentityUser>> logger,
|
||||
IOptions<UserPasswordConfigurationSettings> 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<UserManager<T>> logger,
|
||||
IOptions<UserPasswordConfigurationSettings> 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<T>;
|
||||
//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
|
||||
/// <returns></returns>
|
||||
protected virtual IPasswordHasher<T> 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<T>();
|
||||
}
|
||||
|
||||
@@ -131,16 +140,22 @@ namespace Umbraco.Core.BackOffice
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
/// <returns></returns>
|
||||
/// <param name="user">The user</param>
|
||||
/// <returns>True if the user is locked out, else false</returns>
|
||||
/// <remarks>
|
||||
/// 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
|
||||
/// </remarks>
|
||||
public override async Task<bool> 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<int?>() ?? Constants.Security.SuperUserId;
|
||||
var currentUserId = umbIdentity?.GetUserId<int?>() ?? 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<IdentityAuditEventArgs> AccountLocked;
|
||||
public static event EventHandler<IdentityAuditEventArgs> AccountUnlocked;
|
||||
public static event EventHandler<IdentityAuditEventArgs> ForgotPasswordRequested;
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Binds to events to write audit logs for the <see cref="IBackOfficeUserManager"/>
|
||||
/// </summary>
|
||||
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> globalSettings)
|
||||
{
|
||||
_auditService = auditService;
|
||||
_userService = userService;
|
||||
_globalSettings = globalSettings.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Binds to events to start auditing
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<BackOfficeSecurityStampValidator>();
|
||||
// Same goes for the signinmanager
|
||||
var signInManager = ctx.HttpContext.RequestServices.GetRequiredService<BackOfficeSignInManager>();
|
||||
var signInManager = ctx.HttpContext.RequestServices.GetRequiredService<IBackOfficeSignInManager>();
|
||||
|
||||
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<BackOfficeSessionIdValidator>();
|
||||
await validator.ValidateSessionAsync(TimeSpan.FromMinutes(1), context);
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Options used to configure auto-linking external OAuth providers
|
||||
/// </summary>
|
||||
@@ -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.
|
||||
/// </summary>
|
||||
public bool AllowManualLinking { get; set; } = true;
|
||||
public bool AllowManualLinking { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A callback executed during account auto-linking and before the user is persisted
|
||||
@@ -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
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Service to return <see cref="BackOfficeExternalLoginProvider"/> instances
|
||||
/// </summary>
|
||||
public interface IBackOfficeExternalLoginProviders
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the <see cref="BackOfficeExternalLoginProvider"/> for the specified scheme
|
||||
/// </summary>
|
||||
/// <param name="authenticationType"></param>
|
||||
/// <returns></returns>
|
||||
BackOfficeExternalLoginProvider Get(string authenticationType);
|
||||
|
||||
/// <summary>
|
||||
/// Get all registered <see cref="BackOfficeExternalLoginProvider"/>
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
IEnumerable<BackOfficeExternalLoginProvider> GetBackOfficeProviders();
|
||||
|
||||
/// <summary>
|
||||
/// Returns the authentication type for the last registered external login (oauth) provider that specifies an auto-login redirect option
|
||||
/// </summary>
|
||||
/// <param name="manager"></param>
|
||||
/// <returns></returns>
|
||||
string GetAutoLoginProvider();
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if there is any external provider that has the Deny Local Login option configured
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
bool HasDenyLocalLogin();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="SignInManager{BackOfficeIdentityUser}"/> for the back office with a <seealso cref="BackOfficeIdentityUser"/>
|
||||
/// </summary>
|
||||
public interface IBackOfficeSignInManager
|
||||
{
|
||||
AuthenticationProperties ConfigureExternalAuthenticationProperties(string provider, string redirectUrl, string userId = null);
|
||||
Task<SignInResult> ExternalLoginSignInAsync(ExternalLoginInfo loginInfo, bool isPersistent, bool bypassTwoFactor = false);
|
||||
Task<IEnumerable<AuthenticationScheme>> GetExternalAuthenticationSchemesAsync();
|
||||
Task<ExternalLoginInfo> GetExternalLoginInfoAsync(string expectedXsrf = null);
|
||||
Task<BackOfficeIdentityUser> GetTwoFactorAuthenticationUserAsync();
|
||||
Task<SignInResult> PasswordSignInAsync(string userName, string password, bool isPersistent, bool lockoutOnFailure);
|
||||
Task SignOutAsync();
|
||||
Task SignInAsync(BackOfficeIdentityUser user, bool isPersistent, string authenticationMethod = null);
|
||||
Task<ClaimsPrincipal> CreateUserPrincipalAsync(BackOfficeIdentityUser user);
|
||||
Task<SignInResult> TwoFactorSignInAsync(string provider, string code, bool isPersistent, bool rememberClient);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace Umbraco.Web.BackOffice.Security
|
||||
{
|
||||
/// <summary>
|
||||
/// Options used to control 2FA for the Umbraco back office
|
||||
/// </summary>
|
||||
public interface IBackOfficeTwoFactorOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the angular view for handling 2FA interaction
|
||||
/// </summary>
|
||||
/// <param name="username"></param>
|
||||
/// <returns></returns>
|
||||
string GetTwoFactorView(string username);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Umbraco.Web.BackOffice.Security
|
||||
{
|
||||
public class NoopBackOfficeTwoFactorOptions : IBackOfficeTwoFactorOptions
|
||||
{
|
||||
public string GetTwoFactorView(string username) => null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,6 +6,8 @@ using Umbraco.Web.Common.Attributes;
|
||||
|
||||
namespace Umbraco.Web.Common.ApplicationModels
|
||||
{
|
||||
// TODO: This should just exist in the back office project
|
||||
|
||||
/// <summary>
|
||||
/// An application model provider for all Umbraco Back Office controllers
|
||||
/// </summary>
|
||||
@@ -49,12 +51,7 @@ namespace Umbraco.Web.Common.ApplicationModels
|
||||
}
|
||||
|
||||
private bool IsBackOfficeController(ControllerModel controller)
|
||||
{
|
||||
var pluginControllerAttribute = controller.Attributes.OfType<PluginControllerAttribute>().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<IsBackOfficeAttribute>().Any();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -81,6 +81,7 @@ namespace Umbraco.Web.Common.ApplicationModels
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsUmbracoApiController(ControllerModel controller) => controller.Attributes.OfType<UmbracoApiControllerAttribute>().Any();
|
||||
private bool IsUmbracoApiController(ControllerModel controller)
|
||||
=> controller.Attributes.OfType<UmbracoApiControllerAttribute>().Any();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Runs the authentication process
|
||||
/// </summary>
|
||||
/// <param name="controller"></param>
|
||||
/// <returns></returns>
|
||||
public static async Task<AuthenticateResult> AuthenticateBackOfficeAsync(this ControllerBase controller)
|
||||
{
|
||||
if (controller.HttpContext == null)
|
||||
{
|
||||
return AuthenticateResult.NoResult();
|
||||
}
|
||||
|
||||
var result = await controller.HttpContext.AuthenticateAsync(Constants.Security.BackOfficeAuthenticationType);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return the controller name from the controller type
|
||||
/// </summary>
|
||||
|
||||
@@ -13,38 +13,32 @@ namespace Umbraco.Web.Common.Install
|
||||
/// </summary>
|
||||
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<InstallAuthorizeFilter> _logger;
|
||||
|
||||
public InstallAuthorizeFilter(
|
||||
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
|
||||
IRuntimeState runtimeState,
|
||||
ILogger<InstallAuthorizeFilter> 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)
|
||||
{
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Attempt<int> GetUserId()
|
||||
{
|
||||
@@ -83,40 +72,5 @@ namespace Umbraco.Web.Common.Security
|
||||
return user.HasSectionAccess(section);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool ValidateCurrentUser()
|
||||
{
|
||||
return ValidateCurrentUser(false, true) == ValidateRequestAttempt.Success;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<BackOfficeExternalLoginProvider> GetBackOfficeProviders();
|
||||
|
||||
/// <summary>
|
||||
/// Returns the authentication type for the last registered external login (oauth) provider that specifies an auto-login redirect option
|
||||
/// </summary>
|
||||
/// <param name="manager"></param>
|
||||
/// <returns></returns>
|
||||
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<BackOfficeExternalLoginProvider> GetBackOfficeProviders()
|
||||
{
|
||||
return Enumerable.Empty<BackOfficeExternalLoginProvider>();
|
||||
}
|
||||
|
||||
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<string, object> Properties { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -49,6 +49,7 @@ function watchTask(cb) {
|
||||
console.log("copying " + group.files + " to " + destPath);
|
||||
task = task.pipe( dest(destPath) );
|
||||
});
|
||||
return task;
|
||||
},
|
||||
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;
|
||||
|
||||
@@ -52,11 +52,11 @@
|
||||
|
||||
<div ng-if="login.customView" ng-include="login.customView"></div>
|
||||
|
||||
<div ng-if="!login.customView && login.properties.ExternalSignInAutoLinkOptions.AllowManualLinking">
|
||||
<div ng-if="!login.customView && login.properties.AutoLinkOptions.AllowManualLinking">
|
||||
<form ng-submit="linkProvider($event)" ng-if="login.linkedProviderKey == undefined" method="POST" action="{{externalLinkLoginFormAction}}" name="oauthloginform" id="oauthloginform-{{login.authType}}">
|
||||
<input type="hidden" name="provider" value="{{login.authType}}" />
|
||||
<button class="btn btn-block btn-social"
|
||||
ng-class="login.properties.SocialStyle"
|
||||
ng-class="login.properties.ButtonStyle"
|
||||
id="{{login.authType}}">
|
||||
|
||||
<i class="fa" ng-class="login.properties.SocialIcon"></i>
|
||||
@@ -67,7 +67,7 @@
|
||||
<button ng-if="login.linkedProviderKey != undefined"
|
||||
ng-click="unlink($event, login.authType, login.linkedProviderKey)"
|
||||
class="btn btn-block btn-social"
|
||||
ng-class="login.properties.SocialStyle"
|
||||
ng-class="login.properties.ButtonStyle"
|
||||
id="{{login.authType}}"
|
||||
name="provider"
|
||||
value="{{login.authType}}">
|
||||
|
||||
@@ -126,7 +126,7 @@
|
||||
<form method="POST" action="{{vm.externalLoginFormAction}}">
|
||||
<button type="submit"
|
||||
class="btn btn-block btn-social"
|
||||
ng-class="login.properties.SocialStyle"
|
||||
ng-class="login.properties.ButtonStyle"
|
||||
id="{{login.authType}}" name="provider" value="{{login.authType}}"
|
||||
title="Log in using your {{login.caption}} account">
|
||||
<i class="fa" ng-class="login.properties.SocialIcon"></i>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@using Microsoft.Extensions.Options;
|
||||
@using Umbraco.Core
|
||||
@using Umbraco.Web.WebAssets
|
||||
@using Umbraco.Web.Common.Security
|
||||
@using Umbraco.Web.BackOffice.Security
|
||||
@using Umbraco.Core.WebAssets
|
||||
@using Umbraco.Core.Configuration
|
||||
@using Umbraco.Core.Configuration.Models
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
@using System.Globalization
|
||||
@using Umbraco.Core
|
||||
@using Umbraco.Web.WebAssets
|
||||
@using Umbraco.Web.Common.Security
|
||||
@using Umbraco.Web.BackOffice.Security
|
||||
@using Umbraco.Core.WebAssets
|
||||
@using Umbraco.Core.Configuration
|
||||
@using Umbraco.Core.Configuration.Models
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
@using Umbraco.Extensions
|
||||
@using Umbraco.Core.Logging
|
||||
@using Umbraco.Web.BackOffice.Controllers
|
||||
@inject BackOfficeSignInManager SignInManager
|
||||
@inject IBackOfficeSignInManager SignInManager
|
||||
@inject BackOfficeServerVariables BackOfficeServerVariables
|
||||
@inject IUmbracoVersion UmbracoVersion
|
||||
@inject IHostingEnvironment HostingEnvironment
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
using System;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Compose;
|
||||
using Umbraco.Core.Composing;
|
||||
using Umbraco.Core.Configuration;
|
||||
using Umbraco.Core.Configuration.Models;
|
||||
using Umbraco.Core.Models.Membership;
|
||||
using Umbraco.Core.Services;
|
||||
using Umbraco.Web.Security;
|
||||
|
||||
namespace Umbraco.Web.Compose
|
||||
{
|
||||
public sealed class BackOfficeUserAuditEventsComponent : IComponent
|
||||
{
|
||||
private readonly IAuditService _auditService;
|
||||
private readonly IUserService _userService;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
|
||||
public BackOfficeUserAuditEventsComponent(IAuditService auditService, IUserService userService, GlobalSettings globalSettings)
|
||||
{
|
||||
_auditService = auditService;
|
||||
_userService = userService;
|
||||
_globalSettings = globalSettings;
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
//BackOfficeUserManager.AccountLocked += ;
|
||||
//BackOfficeUserManager.AccountUnlocked += ;
|
||||
BackOfficeOwinUserManager.ForgotPasswordRequested += OnForgotPasswordRequest;
|
||||
BackOfficeOwinUserManager.ForgotPasswordChangedSuccess += OnForgotPasswordChange;
|
||||
BackOfficeOwinUserManager.LoginFailed += OnLoginFailed;
|
||||
//BackOfficeUserManager.LoginRequiresVerification += ;
|
||||
BackOfficeOwinUserManager.LoginSuccess += OnLoginSuccess;
|
||||
BackOfficeOwinUserManager.LogoutSuccess += OnLogoutSuccess;
|
||||
BackOfficeOwinUserManager.PasswordChanged += OnPasswordChanged;
|
||||
BackOfficeOwinUserManager.PasswordReset += OnPasswordReset;
|
||||
//BackOfficeUserManager.ResetAccessFailedCount += ;
|
||||
}
|
||||
|
||||
public void Terminate()
|
||||
{
|
||||
//BackOfficeUserManager.AccountLocked -= ;
|
||||
//BackOfficeUserManager.AccountUnlocked -= ;
|
||||
BackOfficeOwinUserManager.ForgotPasswordRequested -= OnForgotPasswordRequest;
|
||||
BackOfficeOwinUserManager.ForgotPasswordChangedSuccess -= OnForgotPasswordChange;
|
||||
BackOfficeOwinUserManager.LoginFailed -= OnLoginFailed;
|
||||
//BackOfficeUserManager.LoginRequiresVerification -= ;
|
||||
BackOfficeOwinUserManager.LoginSuccess -= OnLoginSuccess;
|
||||
BackOfficeOwinUserManager.LogoutSuccess -= OnLogoutSuccess;
|
||||
BackOfficeOwinUserManager.PasswordChanged -= OnPasswordChanged;
|
||||
BackOfficeOwinUserManager.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, EventArgs args)
|
||||
{
|
||||
if (args is IdentityAuditEventArgs identityArgs)
|
||||
{
|
||||
var performingUser = GetPerformingUser(identityArgs.PerformingUser);
|
||||
WriteAudit(performingUser, identityArgs.AffectedUser, identityArgs.IpAddress, "umbraco/user/sign-in/login", "login success");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnLogoutSuccess(object sender, EventArgs args)
|
||||
{
|
||||
if (args is IdentityAuditEventArgs identityArgs)
|
||||
{
|
||||
var performingUser = GetPerformingUser(identityArgs.PerformingUser);
|
||||
WriteAudit(performingUser, identityArgs.AffectedUser, identityArgs.IpAddress, "umbraco/user/sign-in/logout", "logout success");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPasswordReset(object sender, EventArgs args)
|
||||
{
|
||||
if (args is IdentityAuditEventArgs identityArgs && identityArgs.PerformingUser >= 0)
|
||||
{
|
||||
WriteAudit(identityArgs.PerformingUser, identityArgs.AffectedUser, identityArgs.IpAddress, "umbraco/user/password/reset", "password reset");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPasswordChanged(object sender, EventArgs args)
|
||||
{
|
||||
if (args is IdentityAuditEventArgs identityArgs && identityArgs.PerformingUser >= 0)
|
||||
{
|
||||
WriteAudit(identityArgs.PerformingUser, identityArgs.AffectedUser, identityArgs.IpAddress, "umbraco/user/password/change", "password change");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnLoginFailed(object sender, EventArgs args)
|
||||
{
|
||||
if (args is IdentityAuditEventArgs identityArgs && identityArgs.PerformingUser >= 0)
|
||||
{
|
||||
WriteAudit(identityArgs.PerformingUser, 0, identityArgs.IpAddress, "umbraco/user/sign-in/failed", "login failed", affectedDetails: "");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnForgotPasswordChange(object sender, EventArgs args)
|
||||
{
|
||||
if (args is IdentityAuditEventArgs identityArgs && identityArgs.PerformingUser >= 0)
|
||||
{
|
||||
WriteAudit(identityArgs.PerformingUser, identityArgs.AffectedUser, identityArgs.IpAddress, "umbraco/user/password/forgot/change", "password forgot/change");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnForgotPasswordRequest(object sender, EventArgs args)
|
||||
{
|
||||
if (args is IdentityAuditEventArgs identityArgs && identityArgs.PerformingUser >= 0)
|
||||
{
|
||||
WriteAudit(identityArgs.PerformingUser, identityArgs.AffectedUser, identityArgs.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
using Umbraco.Core.Composing;
|
||||
|
||||
namespace Umbraco.Web.Compose
|
||||
{
|
||||
public sealed class BackOfficeUserAuditEventsComposer : ComponentComposer<BackOfficeUserAuditEventsComponent>, ICoreComposer
|
||||
{ }
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Extensions;
|
||||
using Umbraco.Web.Models;
|
||||
using Umbraco.Web.Security;
|
||||
using IUser = Umbraco.Core.Models.Membership.IUser;
|
||||
|
||||
//Migrated to .NET CORE
|
||||
namespace Umbraco.Web.Editors
|
||||
{
|
||||
internal class PasswordChanger
|
||||
{
|
||||
private readonly ILogger<PasswordChanger> _logger;
|
||||
|
||||
public PasswordChanger(ILogger<PasswordChanger> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Changes the password for a user based on the many different rules and config options
|
||||
/// </summary>
|
||||
/// <param name="currentUser">The user performing the password save action</param>
|
||||
/// <param name="savingUser">The user who's password is being changed</param>
|
||||
/// <param name="passwordModel"></param>
|
||||
/// <param name="userMgr"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<Attempt<PasswordChangedModel>> ChangePasswordWithIdentityAsync(
|
||||
IUser currentUser,
|
||||
IUser savingUser,
|
||||
ChangingPasswordModel passwordModel,
|
||||
BackOfficeOwinUserManager userMgr)
|
||||
{
|
||||
if (passwordModel == null) throw new ArgumentNullException(nameof(passwordModel));
|
||||
if (userMgr == null) throw new ArgumentNullException(nameof(userMgr));
|
||||
|
||||
if (passwordModel.NewPassword.IsNullOrWhiteSpace())
|
||||
{
|
||||
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Cannot set an empty password", new[] { "value" }) });
|
||||
}
|
||||
|
||||
var backOfficeIdentityUser = await userMgr.FindByIdAsync(savingUser.Id.ToString());
|
||||
if (backOfficeIdentityUser == null)
|
||||
{
|
||||
//this really shouldn't ever happen... but just in case
|
||||
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Password could not be verified", new[] { "oldPassword" }) });
|
||||
}
|
||||
|
||||
//Are we just changing another user's password?
|
||||
if (passwordModel.OldPassword.IsNullOrWhiteSpace())
|
||||
{
|
||||
//if it's the current user, the current user cannot reset their own password
|
||||
if (currentUser.Username == savingUser.Username)
|
||||
{
|
||||
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Password reset is not allowed", new[] { "value" }) });
|
||||
}
|
||||
|
||||
//if the current user has access to reset/manually change the password
|
||||
if (currentUser.HasSectionAccess(Umbraco.Core.Constants.Applications.Users) == false)
|
||||
{
|
||||
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("The current user is not authorized", new[] { "value" }) });
|
||||
}
|
||||
|
||||
//ok, we should be able to reset it
|
||||
var resetToken = await userMgr.GeneratePasswordResetTokenAsync(backOfficeIdentityUser);
|
||||
|
||||
var resetResult = await userMgr.ChangePasswordWithResetAsync(savingUser.Id, resetToken, passwordModel.NewPassword);
|
||||
|
||||
if (resetResult.Succeeded == false)
|
||||
{
|
||||
var errors = resetResult.Errors.ToErrorMessage();
|
||||
_logger.LogWarning("Could not reset user password {PasswordErrors}", errors);
|
||||
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult(errors, new[] { "value" }) });
|
||||
}
|
||||
|
||||
return Attempt.Succeed(new PasswordChangedModel());
|
||||
}
|
||||
|
||||
//is the old password correct?
|
||||
var validateResult = await userMgr.CheckPasswordAsync(backOfficeIdentityUser, passwordModel.OldPassword);
|
||||
if (validateResult == false)
|
||||
{
|
||||
//no, fail with an error message for "oldPassword"
|
||||
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Incorrect password", new[] { "oldPassword" }) });
|
||||
}
|
||||
//can we change to the new password?
|
||||
var changeResult = await userMgr.ChangePasswordAsync(backOfficeIdentityUser, passwordModel.OldPassword, passwordModel.NewPassword);
|
||||
if (changeResult.Succeeded == false)
|
||||
{
|
||||
//no, fail with error messages for "password"
|
||||
var errors = changeResult.Errors.ToErrorMessage();
|
||||
_logger.LogWarning("Could not change user password {PasswordErrors}", errors);
|
||||
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult(errors, new[] { "password" }) });
|
||||
}
|
||||
return Attempt.Succeed(new PasswordChangedModel());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -76,7 +76,7 @@ namespace Umbraco.Web.Mvc
|
||||
// otherwise we need to ensure that a user is logged in
|
||||
return RuntimeState.Level == RuntimeLevel.Install
|
||||
|| RuntimeState.Level == RuntimeLevel.Upgrade
|
||||
|| BackOfficeSecurity.ValidateCurrentUser();
|
||||
|| (httpContext.User?.Identity?.IsAuthenticated ?? false);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
|
||||
@@ -52,25 +52,6 @@ namespace Umbraco.Web
|
||||
return ctx == null ? Attempt<HttpContextBase>.Fail() : Attempt.Succeed(ctx);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets the back office user manager out of OWIN
|
||||
/// </summary>
|
||||
/// <param name="owinContext"></param>
|
||||
/// <returns></returns>
|
||||
/// <remarks>
|
||||
/// This is required because to extract the user manager we need to user a custom service since owin only deals in generics and
|
||||
/// developers could register their own user manager types
|
||||
/// </remarks>
|
||||
public static BackOfficeOwinUserManager GetBackOfficeUserManager(this IOwinContext owinContext)
|
||||
{
|
||||
var marker = owinContext.Get<IBackOfficeUserManagerMarker>(BackOfficeOwinUserManager.OwinMarkerKey)
|
||||
?? throw new NullReferenceException($"No {typeof (IBackOfficeUserManagerMarker)}, i.e. no Umbraco back-office, has been registered with Owin.");
|
||||
|
||||
return marker.GetManager(owinContext)
|
||||
?? throw new NullReferenceException($"Could not resolve an instance of {typeof (BackOfficeOwinUserManager)} from the {typeof (IOwinContext)}.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adapted from Microsoft.AspNet.Identity.Owin.OwinContextExtensions
|
||||
/// </summary>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Reflection;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
@@ -31,7 +31,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 = "~_~")]
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Owin.Security.DataProtection;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.BackOffice;
|
||||
using Umbraco.Core.Configuration.Models;
|
||||
using Umbraco.Core.Mapping;
|
||||
using Umbraco.Core.Services;
|
||||
using Umbraco.Net;
|
||||
|
||||
namespace Umbraco.Web.Security
|
||||
{
|
||||
// TODO: Most of this is already migrated to netcore, there's probably not much more to go and then we can complete remove it
|
||||
public class BackOfficeOwinUserManager : BackOfficeUserManager
|
||||
{
|
||||
public const string OwinMarkerKey = "Umbraco.Web.Security.Identity.BackOfficeUserManagerMarker";
|
||||
|
||||
public BackOfficeOwinUserManager(
|
||||
IOptions<UserPasswordConfigurationSettings> passwordConfiguration,
|
||||
IIpResolver ipResolver,
|
||||
IUserStore<BackOfficeIdentityUser> store,
|
||||
IOptions<BackOfficeIdentityOptions> optionsAccessor,
|
||||
IEnumerable<IUserValidator<BackOfficeIdentityUser>> userValidators,
|
||||
IEnumerable<IPasswordValidator<BackOfficeIdentityUser>> passwordValidators,
|
||||
BackOfficeLookupNormalizer keyNormalizer,
|
||||
BackOfficeIdentityErrorDescriber errors,
|
||||
IDataProtectionProvider dataProtectionProvider,
|
||||
ILogger<UserManager<BackOfficeIdentityUser>> logger)
|
||||
: base(ipResolver, store, optionsAccessor, null, userValidators, passwordValidators, keyNormalizer, errors, null, logger, passwordConfiguration)
|
||||
{
|
||||
PasswordConfiguration = passwordConfiguration.Value;
|
||||
InitUserManager(this, dataProtectionProvider);
|
||||
}
|
||||
|
||||
#region Static Create methods
|
||||
|
||||
/// <summary>
|
||||
/// Creates a BackOfficeUserManager instance with all default options and the default BackOfficeUserManager
|
||||
/// </summary>
|
||||
public static BackOfficeOwinUserManager Create(
|
||||
IUserService userService,
|
||||
IEntityService entityService,
|
||||
IExternalLoginService externalLoginService,
|
||||
IOptions<GlobalSettings> globalSettings,
|
||||
UmbracoMapper mapper,
|
||||
IOptions<UserPasswordConfigurationSettings> passwordConfiguration,
|
||||
IIpResolver ipResolver,
|
||||
BackOfficeIdentityErrorDescriber errors,
|
||||
IDataProtectionProvider dataProtectionProvider,
|
||||
ILogger<UserManager<BackOfficeIdentityUser>> logger)
|
||||
{
|
||||
var store = new BackOfficeUserStore(userService, entityService, externalLoginService, globalSettings, mapper);
|
||||
|
||||
return Create(
|
||||
passwordConfiguration,
|
||||
ipResolver,
|
||||
store,
|
||||
errors,
|
||||
dataProtectionProvider,
|
||||
logger);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a BackOfficeUserManager instance with all default options and a custom BackOfficeUserManager instance
|
||||
/// </summary>
|
||||
public static BackOfficeOwinUserManager Create(
|
||||
IOptions<UserPasswordConfigurationSettings> passwordConfiguration,
|
||||
IIpResolver ipResolver,
|
||||
IUserStore<BackOfficeIdentityUser> customUserStore,
|
||||
BackOfficeIdentityErrorDescriber errors,
|
||||
IDataProtectionProvider dataProtectionProvider,
|
||||
ILogger<UserManager<BackOfficeIdentityUser>> logger)
|
||||
{
|
||||
var options = new BackOfficeIdentityOptions();
|
||||
|
||||
// Configure validation logic for usernames
|
||||
var userValidators = new List<UserValidator<BackOfficeIdentityUser>> { new BackOfficeUserValidator<BackOfficeIdentityUser>() };
|
||||
options.User.RequireUniqueEmail = true;
|
||||
|
||||
// Configure validation logic for passwords
|
||||
var passwordValidators = new List<IPasswordValidator<BackOfficeIdentityUser>> { new PasswordValidator<BackOfficeIdentityUser>() };
|
||||
options.Password.RequiredLength = passwordConfiguration.Value.RequiredLength;
|
||||
options.Password.RequireNonAlphanumeric = passwordConfiguration.Value.RequireNonLetterOrDigit;
|
||||
options.Password.RequireDigit = passwordConfiguration.Value.RequireDigit;
|
||||
options.Password.RequireLowercase = passwordConfiguration.Value.RequireLowercase;
|
||||
options.Password.RequireUppercase = passwordConfiguration.Value.RequireUppercase;
|
||||
|
||||
// Ensure Umbraco security stamp claim type is used
|
||||
options.ClaimsIdentity.UserIdClaimType = ClaimTypes.NameIdentifier;
|
||||
options.ClaimsIdentity.UserNameClaimType = ClaimTypes.Name;
|
||||
options.ClaimsIdentity.RoleClaimType = ClaimTypes.Role;
|
||||
options.ClaimsIdentity.SecurityStampClaimType = Constants.Security.SecurityStampClaimType;
|
||||
|
||||
options.Lockout.AllowedForNewUsers = true;
|
||||
options.Lockout.MaxFailedAccessAttempts = passwordConfiguration.Value.MaxFailedAccessAttemptsBeforeLockout;
|
||||
//NOTE: This just needs to be in the future, we currently don't support a lockout timespan, it's either they are locked
|
||||
// or they are not locked, but this determines what is set on the account lockout date which corresponds to whether they are
|
||||
// locked out or not.
|
||||
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromDays(30);
|
||||
|
||||
return new BackOfficeOwinUserManager(
|
||||
passwordConfiguration,
|
||||
ipResolver,
|
||||
customUserStore,
|
||||
new OptionsWrapper<BackOfficeIdentityOptions>(options),
|
||||
userValidators,
|
||||
passwordValidators,
|
||||
new BackOfficeLookupNormalizer(),
|
||||
errors,
|
||||
dataProtectionProvider,
|
||||
logger);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
protected void InitUserManager(BackOfficeOwinUserManager manager, IDataProtectionProvider dataProtectionProvider)
|
||||
{
|
||||
// use a custom hasher based on our membership provider
|
||||
PasswordHasher = GetDefaultPasswordHasher(PasswordConfiguration);
|
||||
|
||||
// set OWIN data protection token provider as default
|
||||
if (dataProtectionProvider != null)
|
||||
{
|
||||
manager.RegisterTokenProvider(
|
||||
TokenOptions.DefaultProvider,
|
||||
new OwinDataProtectorTokenProvider<BackOfficeIdentityUser>(dataProtectionProvider.Create("ASP.NET Identity"))
|
||||
{
|
||||
TokenLifespan = TimeSpan.FromDays(3)
|
||||
});
|
||||
}
|
||||
|
||||
// register ASP.NET Core Identity token providers
|
||||
manager.RegisterTokenProvider(TokenOptions.DefaultEmailProvider, new EmailTokenProvider<BackOfficeIdentityUser>());
|
||||
manager.RegisterTokenProvider(TokenOptions.DefaultPhoneProvider, new PhoneNumberTokenProvider<BackOfficeIdentityUser>());
|
||||
manager.RegisterTokenProvider(TokenOptions.DefaultAuthenticatorProvider, new AuthenticatorTokenProvider<BackOfficeIdentityUser>());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
using System;
|
||||
using Microsoft.Owin;
|
||||
using Umbraco.Core.BackOffice;
|
||||
|
||||
namespace Umbraco.Web.Security
|
||||
{
|
||||
/// <summary>
|
||||
/// This class is only here due to the fact that IOwinContext Get / Set only work in generics, if they worked
|
||||
/// with regular 'object' then we wouldn't have to use this work around but because of that we have to use this
|
||||
/// class to resolve the 'real' type of the registered user manager
|
||||
/// </summary>
|
||||
/// <typeparam name="TManager"></typeparam>
|
||||
/// <typeparam name="TUser"></typeparam>
|
||||
internal class BackOfficeUserManagerMarker<TManager, TUser> : IBackOfficeUserManagerMarker
|
||||
where TManager : BackOfficeUserManager<TUser>
|
||||
where TUser : BackOfficeIdentityUser
|
||||
{
|
||||
public BackOfficeOwinUserManager GetManager(IOwinContext owin)
|
||||
{
|
||||
var mgr = owin.Get<TManager>() as BackOfficeOwinUserManager;
|
||||
if (mgr == null) throw new InvalidOperationException("Could not cast the registered back office user of type " + typeof(TManager) + " to " + typeof(BackOfficeUserManager<BackOfficeIdentityUser>));
|
||||
return mgr;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,11 +11,6 @@ namespace Umbraco.Web.Security
|
||||
{
|
||||
public IUser CurrentUser => throw new NotImplementedException();
|
||||
|
||||
public ValidateRequestAttempt AuthorizeRequest(bool throwExceptions = false)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Attempt<int> GetUserId()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
@@ -31,14 +26,5 @@ namespace Umbraco.Web.Security
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public bool ValidateCurrentUser()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public ValidateRequestAttempt ValidateCurrentUser(bool throwExceptions, bool requiresApproval = true)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
using Microsoft.Owin;
|
||||
|
||||
namespace Umbraco.Web.Security
|
||||
{
|
||||
/// <summary>
|
||||
/// This interface is only here due to the fact that IOwinContext Get / Set only work in generics, if they worked
|
||||
/// with regular 'object' then we wouldn't have to use this work around but because of that we have to use this
|
||||
/// class to resolve the 'real' type of the registered user manager
|
||||
/// </summary>
|
||||
internal interface IBackOfficeUserManagerMarker
|
||||
{
|
||||
BackOfficeOwinUserManager GetManager(IOwinContext owin);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
using Microsoft.Owin;
|
||||
|
||||
namespace Umbraco.Web.Security
|
||||
{
|
||||
/// <summary>
|
||||
/// Used to display a custom view in the back office if developers choose to implement their own custom 2 factor authentication
|
||||
/// </summary>
|
||||
public interface IUmbracoBackOfficeTwoFactorOptions
|
||||
{
|
||||
string GetTwoFactorView(IOwinContext owinContext, IUmbracoContext umbracoContext, string username);
|
||||
}
|
||||
}
|
||||
@@ -18,9 +18,7 @@ using Umbraco.Web.Security.Providers;
|
||||
|
||||
namespace Umbraco.Web.Security
|
||||
{
|
||||
/// <summary>
|
||||
/// A helper class for handling Members
|
||||
/// </summary>
|
||||
// MIGRATED TO NETCORE
|
||||
public class MembershipHelper
|
||||
{
|
||||
private readonly MembersMembershipProvider _membershipProvider;
|
||||
@@ -680,37 +678,6 @@ namespace Umbraco.Web.Security
|
||||
return allowAction;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Changes password for a member/user given the membership provider name and the password change model
|
||||
/// </summary>
|
||||
/// <param name="username"></param>
|
||||
/// <param name="passwordModel"></param>
|
||||
/// <param name="membershipProviderName"></param>
|
||||
/// <returns></returns>
|
||||
public virtual Attempt<PasswordChangedModel> ChangePassword(string username, ChangingPasswordModel passwordModel, string membershipProviderName)
|
||||
{
|
||||
var provider = Membership.Providers[membershipProviderName];
|
||||
if (provider == null)
|
||||
{
|
||||
throw new InvalidOperationException("Could not find provider with name " + membershipProviderName);
|
||||
}
|
||||
|
||||
return ChangePassword(username, passwordModel, provider);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Changes password for a member/user given the membership provider and the password change model
|
||||
/// </summary>
|
||||
/// <param name="username"></param>
|
||||
/// <param name="passwordModel"></param>
|
||||
/// <param name="membershipProvider"></param>
|
||||
/// <returns></returns>
|
||||
public virtual Attempt<PasswordChangedModel> ChangePassword(string username, ChangingPasswordModel passwordModel, MembershipProvider membershipProvider)
|
||||
{
|
||||
var passwordChanger = new PasswordChanger(_loggerFactory.CreateLogger<PasswordChanger>());
|
||||
return ChangePasswordWithMembershipProvider(username, passwordModel, membershipProvider);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates a membership user with all of it's writable properties
|
||||
/// </summary>
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<Prefer32Bit>false</Prefer32Bit>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<CodeAnalysisRuleSet>..\UnusedCode.ruleset</CodeAnalysisRuleSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugType>portable</DebugType>
|
||||
@@ -135,9 +134,6 @@
|
||||
<Compile Include="AspNet\AspNetUserAgentProvider.cs" />
|
||||
<Compile Include="AspNet\FrameworkMarchal.cs" />
|
||||
<Compile Include="AspNet\AspNetUmbracoApplicationLifetime.cs" />
|
||||
<Compile Include="Compose\BackOfficeUserAuditEventsComponent.cs" />
|
||||
<Compile Include="Compose\BackOfficeUserAuditEventsComposer.cs" />
|
||||
<Compile Include="Editors\PasswordChanger.cs" />
|
||||
<Compile Include="HttpContextExtensions.cs" />
|
||||
<Compile Include="Macros\MemberUserKeyProvider.cs" />
|
||||
<Compile Include="Mvc\IRenderController.cs" />
|
||||
@@ -164,8 +160,6 @@
|
||||
<Compile Include="RoutableDocumentFilter.cs" />
|
||||
<Compile Include="Runtime\AspNetUmbracoBootPermissionChecker.cs" />
|
||||
<Compile Include="Security\BackOfficeSignInManager.cs" />
|
||||
<Compile Include="Security\BackOfficeOwinUserManager.cs" />
|
||||
<Compile Include="Security\BackOfficeUserManagerMarker.cs" />
|
||||
<Compile Include="WebAssets\CDF\ClientDependencyComponent.cs" />
|
||||
<Compile Include="WebAssets\CDF\ClientDependencyComposer.cs" />
|
||||
<Compile Include="Security\MembershipProviderBase.cs" />
|
||||
@@ -181,7 +175,6 @@
|
||||
<Compile Include="Runtime\WebInitialComposer.cs" />
|
||||
<Compile Include="Security\ActiveDirectoryBackOfficeUserPasswordChecker.cs" />
|
||||
<Compile Include="Security\BackOfficeUserPasswordCheckerResult.cs" />
|
||||
<Compile Include="Security\IBackOfficeUserManagerMarker.cs" />
|
||||
<Compile Include="Security\IBackOfficeUserPasswordChecker.cs" />
|
||||
<Compile Include="Security\IdentityAuditEventArgs.cs" />
|
||||
<Compile Include="UmbracoBuilderExtensions.cs" />
|
||||
@@ -216,7 +209,6 @@
|
||||
<Compile Include="Security\ForceRenewalCookieAuthenticationHandler.cs" />
|
||||
<Compile Include="Security\ForceRenewalCookieAuthenticationMiddleware.cs" />
|
||||
<Compile Include="Security\GetUserSecondsMiddleWare.cs" />
|
||||
<Compile Include="Security\IUmbracoBackOfficeTwoFactorOptions.cs" />
|
||||
<Compile Include="Security\WebAuthExtensions.cs" />
|
||||
<Compile Include="WebAssets\CDF\UmbracoClientDependencyLoader.cs" />
|
||||
<Compile Include="UmbracoDefaultOwinStartup.cs" />
|
||||
@@ -314,4 +306,4 @@
|
||||
</None>
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -53,17 +53,17 @@ namespace Umbraco.Web.WebApi
|
||||
return true;
|
||||
}
|
||||
|
||||
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
|
||||
return RuntimeState.Level == RuntimeLevel.Install
|
||||
|| RuntimeState.Level == RuntimeLevel.Upgrade
|
||||
|| BackOfficeSecurity.ValidateCurrentUser(false, _requireApproval) == ValidateRequestAttempt.Success;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
case RuntimeLevel.Install:
|
||||
case RuntimeLevel.Upgrade:
|
||||
return true;
|
||||
default:
|
||||
var userApprovalSucceeded = !_requireApproval || (BackOfficeSecurity.CurrentUser?.IsApproved ?? false);
|
||||
return userApprovalSucceeded;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,8 +31,6 @@ namespace Umbraco.Web.WebApi
|
||||
[EnableDetailedErrors]
|
||||
public abstract class UmbracoAuthorizedApiController : UmbracoApiController
|
||||
{
|
||||
private BackOfficeOwinUserManager _userManager;
|
||||
|
||||
protected UmbracoAuthorizedApiController()
|
||||
{
|
||||
}
|
||||
@@ -42,10 +40,5 @@ namespace Umbraco.Web.WebApi
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the user manager.
|
||||
/// </summary>
|
||||
protected BackOfficeOwinUserManager UserManager
|
||||
=> _userManager ?? (_userManager = TryGetOwinContext().Result.GetBackOfficeUserManager());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,16 @@ VisualStudioVersion = 16.0.29209.152
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{2849E9D4-3B4E-40A3-A309-F3CB4F0E125F}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
..\linting\.editorconfig = ..\linting\.editorconfig
|
||||
..\build\build-bootstrap.ps1 = ..\build\build-bootstrap.ps1
|
||||
..\build\build.ps1 = ..\build\build.ps1
|
||||
..\NuGet.Config = ..\NuGet.Config
|
||||
SolutionInfo.cs = SolutionInfo.cs
|
||||
..\linting\stylecop.json = ..\linting\stylecop.json
|
||||
..\linting\codeanalysis.ruleset = ..\linting\codeanalysis.ruleset
|
||||
..\linting\codeanalysis.tests.ruleset = ..\linting\codeanalysis.tests.ruleset
|
||||
..\Directory.Build.props = ..\Directory.Build.props
|
||||
..\Directory.Build.targets = ..\Directory.Build.targets
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docs", "Docs", "{FD962632-184C-4005-A5F3-E705D92FC645}"
|
||||
|
||||
@@ -7,10 +7,16 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.Web.UI", "Umbraco.W
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{2849E9D4-3B4E-40A3-A309-F3CB4F0E125F}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
..\linting\.editorconfig = ..\linting\.editorconfig
|
||||
..\build\build-bootstrap.ps1 = ..\build\build-bootstrap.ps1
|
||||
..\build\build.ps1 = ..\build\build.ps1
|
||||
..\NuGet.Config = ..\NuGet.Config
|
||||
SolutionInfo.cs = SolutionInfo.cs
|
||||
..\linting\stylecop.json = ..\linting\stylecop.json
|
||||
..\linting\codeanalysis.ruleset = ..\linting\codeanalysis.ruleset
|
||||
..\linting\codeanalysis.tests.ruleset = ..\linting\codeanalysis.tests.ruleset
|
||||
..\Directory.Build.props = ..\Directory.Build.props
|
||||
..\Directory.Build.targets = ..\Directory.Build.targets
|
||||
..\build\azure-pipelines.yml = ..\build\azure-pipelines.yml
|
||||
EndProjectSection
|
||||
EndProject
|
||||
|
||||
Reference in New Issue
Block a user