556 lines
22 KiB
C#
556 lines
22 KiB
C#
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF
|
|
// ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO
|
|
// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
|
|
// PARTICULAR PURPOSE.
|
|
//
|
|
// Copyright (c) Microsoft Corporation. All rights reserved
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.Specialized;
|
|
using System.Globalization;
|
|
using System.Diagnostics;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Linq;
|
|
using System.Runtime.InteropServices;
|
|
using System.Runtime.Serialization;
|
|
using System.Security.Principal;
|
|
using System.Text.RegularExpressions;
|
|
|
|
namespace Microsoft.Samples.DynamicAccessControl
|
|
{
|
|
using Utility;
|
|
|
|
using USHORT = System.UInt16;
|
|
using ULONG = System.UInt32;
|
|
using ULONG64 = System.UInt64;
|
|
|
|
using AUTHZ_CLIENT_CONTEXT_HANDLE = System.IntPtr;
|
|
using PAUTHZ_SECURITY_ATTRIBUTE_V1 = System.IntPtr;
|
|
|
|
/// <summary>
|
|
/// Exception raised when value(s) of a claim value type is invalid.
|
|
/// </summary>
|
|
[Serializable]
|
|
public class BadValueException : Exception, ISerializable
|
|
{
|
|
#region Constructors
|
|
public BadValueException()
|
|
{ }
|
|
|
|
public BadValueException(string message)
|
|
: base(message)
|
|
{ }
|
|
|
|
public BadValueException(string message, Exception innerException)
|
|
: base(message, innerException)
|
|
{ }
|
|
|
|
protected BadValueException(SerializationInfo info, StreamingContext context)
|
|
: base(info, context)
|
|
{ }
|
|
#endregion
|
|
}
|
|
|
|
/// <summary>
|
|
/// Possible types of values supported in claims.
|
|
/// </summary>
|
|
/// <remarks>These maps to the value type that can be specified from
|
|
/// a claim type </remarks>
|
|
internal enum ClaimValueType
|
|
{
|
|
Integer,
|
|
Boolean,
|
|
String,
|
|
MultiValuedString
|
|
}
|
|
|
|
/// <summary>
|
|
/// Class to represent the type of claims values held, the value(s) and
|
|
/// obtain native (unmanaged) pointers to the value as they are stored in
|
|
/// the union members of AUTHZ_SECURITY_ATTRIBUTE_V1 structure's 'Values'
|
|
/// field.
|
|
/// </summary>
|
|
internal class ClaimValue
|
|
{
|
|
#region Constructor
|
|
/// <summary>
|
|
/// Constructor to initialize a object given the ValueType and a list of
|
|
/// string, each representing one value in string form.
|
|
/// </summary>
|
|
/// <param name="valType">Type of the claim value</param>
|
|
/// <param name="vals"></param>
|
|
public ClaimValue(ClaimValueType valueType, string value)
|
|
{
|
|
this.valueType = valueType;
|
|
|
|
//
|
|
// Attempt to get the raw values so that any ill-formated value would throw an exception
|
|
// and get rejected right away.
|
|
//
|
|
rawValues = GetRawValues(valueType, value, out valueCount);
|
|
}
|
|
#endregion
|
|
|
|
#region Public properties
|
|
/// <summary>
|
|
/// Gets the Microsoft.Samples.Cbac.ClaimValueType of the current
|
|
/// instance.
|
|
/// </summary>
|
|
public ClaimValueType ValueType
|
|
{
|
|
get { return valueType; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the number of values contained in the
|
|
/// Microsoft.Samples.Cbac.ClaimValue
|
|
/// </summary>
|
|
public ULONG ValueCount
|
|
{
|
|
get { return valueCount; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the native (unmanaged) representation of the values.
|
|
/// </summary>
|
|
/// <remarks>The returned native (unmanaged) representation is meant for
|
|
/// use in 'Values' field of AUTHZ_SECURITY_ATTRUBUTE_V1.</remarks>
|
|
public SafeHGlobalHandle RawValues
|
|
{
|
|
get { return rawValues; }
|
|
}
|
|
#endregion
|
|
|
|
#region Private implementation
|
|
/// <summary>
|
|
/// Get the native (unmanaged) representation of the list of values in
|
|
/// the Microsoft.Samples.Cbac.ClaimValue instance.
|
|
/// </summary>
|
|
/// <param name="valueType">Type of the value(s) in this instance</param>
|
|
/// <param name="values">The collection of values each in string format</param>
|
|
/// <param name="valueCount">Return the count of unique values</param>
|
|
/// <returns>SafeHGlobalHandle that references a native (unmanaged)
|
|
/// pointer to values that can be used in the 'Values' field of the
|
|
/// CLAIM_SECURITY_ATTRIBUTE_V1 structure.</returns>
|
|
static SafeHGlobalHandle GetRawValues(ClaimValueType valueType,
|
|
string value,
|
|
out ULONG valueCount)
|
|
{
|
|
const int BASE_OCTAL = 8;
|
|
const int BASE_DECIMAL = 10;
|
|
const int BASE_HEX = 16;
|
|
|
|
const string OCTAL_REGEX = "^[+]?0[0-7]+$";
|
|
const string HEX_REGEX = "^[+]?0[xX][0-9a-fA-f]+$";
|
|
|
|
var stringValues = new StringCollection();
|
|
|
|
valueCount = 1;
|
|
|
|
//
|
|
// As part of formulating the values in native format, verify that
|
|
// we do not have duplicates. AuthzModifyClaims fails with
|
|
// ERROR_ALREADY_EXISTS when duplicate values are specified.
|
|
//
|
|
switch (valueType)
|
|
{
|
|
case ClaimValueType.Integer:
|
|
{
|
|
long[] values = new long[1];
|
|
try
|
|
{
|
|
int fromBase = BASE_DECIMAL;
|
|
|
|
if (Regex.Match(value, OCTAL_REGEX).Success)
|
|
{
|
|
fromBase = BASE_OCTAL;
|
|
}
|
|
else if (Regex.Match(value, HEX_REGEX).Success)
|
|
{
|
|
fromBase = BASE_HEX;
|
|
}
|
|
|
|
values[0] = Convert.ToInt64(value, fromBase);
|
|
|
|
return SafeHGlobalHandle.AllocHGlobal(values);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
throw new BadValueException(string.Format(CultureInfo.CurrentCulture,
|
|
"Invalid Int value - {0}",
|
|
value),
|
|
e);
|
|
}
|
|
}
|
|
|
|
case ClaimValueType.Boolean:
|
|
{
|
|
long[] values = new long[1];
|
|
|
|
try
|
|
{
|
|
string strValue = value;
|
|
|
|
if (string.Compare(value, "true", StringComparison.OrdinalIgnoreCase) == 0)
|
|
{
|
|
strValue = "1";
|
|
}
|
|
else if (string.Compare(value, "false", StringComparison.OrdinalIgnoreCase) == 0)
|
|
{
|
|
strValue = "0";
|
|
}
|
|
|
|
values[0] = Convert.ToInt64(strValue, CultureInfo.InvariantCulture);
|
|
|
|
return SafeHGlobalHandle.AllocHGlobal(values);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
throw new BadValueException(string.Format(CultureInfo.CurrentCulture,
|
|
"Invalid Boolean value - {0}",
|
|
value), e);
|
|
}
|
|
}
|
|
case ClaimValueType.MultiValuedString:
|
|
{
|
|
char[] bracketChars = { '[', ']' };
|
|
const string CSV_REGEX = @"# Parse CVS line. Capture next value in named group: 'val'
|
|
\s* # Ignore leading whitespace.
|
|
(?: # Group of value alternatives.
|
|
"" # Either a double quoted string,
|
|
(?<val> # Capture contents between quotes.
|
|
[^""]*(""""[^""]*)* # Zero or more non-quotes, allowing
|
|
) # doubled "" quotes within string.
|
|
""\s* # Ignore whitespace following quote.
|
|
| (?<val>[^,]+) # Or... One or more non-commas.
|
|
) # End value alternatives group.
|
|
(?:,|$) # Match end is comma or EOS";
|
|
|
|
if (!value.StartsWith("[", StringComparison.Ordinal) ||
|
|
!value.EndsWith("]", StringComparison.Ordinal))
|
|
{
|
|
throw new BadValueException(string.Format(CultureInfo.CurrentCulture,
|
|
"Multi-valued String is not enclosed within square brackets: '{0}'",
|
|
value));
|
|
}
|
|
|
|
MatchCollection splitResult = Regex.Matches(value.Trim(bracketChars),
|
|
CSV_REGEX,
|
|
RegexOptions.Multiline
|
|
| RegexOptions.IgnorePatternWhitespace);
|
|
|
|
if (splitResult.Count == 0)
|
|
{
|
|
throw new BadValueException(string.Format(CultureInfo.CurrentCulture,
|
|
"Ill-formed Multi-valued String: '{0}'",
|
|
value));
|
|
}
|
|
else
|
|
{
|
|
foreach (Match literal in splitResult)
|
|
{
|
|
string strVal = literal.Groups["val"].Value.Trim();
|
|
if (!stringValues.Contains(strVal))
|
|
{
|
|
if (!string.IsNullOrEmpty(strVal))
|
|
{
|
|
stringValues.Add(strVal);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Helper.ReportDuplicateValue(ClaimValueType.MultiValuedString,
|
|
literal.Groups["val"].Value);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (stringValues.Count == 0)
|
|
{
|
|
throw new BadValueException(string.Format(CultureInfo.CurrentCulture,
|
|
"No non-empty strings in : '{0}'",
|
|
value));
|
|
}
|
|
|
|
valueCount = (ULONG)stringValues.Count;
|
|
|
|
goto case ClaimValueType.String;
|
|
}
|
|
case ClaimValueType.String:
|
|
{
|
|
if (stringValues.Count == 0)
|
|
{
|
|
string strVal = value.Trim();
|
|
|
|
if (!string.IsNullOrEmpty(strVal))
|
|
{
|
|
stringValues.Add(strVal);
|
|
}
|
|
}
|
|
|
|
var strings = new List<SafeHGlobalHandle>(stringValues.Count);
|
|
|
|
foreach (var stringValue in stringValues)
|
|
{
|
|
SafeHGlobalHandle nativeString = SafeHGlobalHandle.AllocHGlobal(stringValue);
|
|
|
|
strings.Add(nativeString);
|
|
}
|
|
|
|
SafeHGlobalHandle result = SafeHGlobalHandle.AllocHGlobal(
|
|
strings.Select(n => n.ToIntPtr())
|
|
.ToArray());
|
|
|
|
//
|
|
// Since the native (managed) representation is an array
|
|
// of pointers to strings, ensure that these pointers
|
|
// are being referenced in the uber SafeHGlobalHandle
|
|
// that represents the array of pointers.
|
|
//
|
|
result.AddSubReference(strings);
|
|
|
|
valueCount = (ULONG)strings.Count;
|
|
|
|
return result;
|
|
}
|
|
default:
|
|
{
|
|
valueCount = 0;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return SafeHGlobalHandle.InvalidHandle;
|
|
}
|
|
#endregion
|
|
|
|
#region Private members
|
|
ClaimValueType valueType;
|
|
ULONG valueCount;
|
|
|
|
/// <summary>
|
|
/// Holds the native (unmanaged) representation of the values
|
|
/// </summary>
|
|
SafeHGlobalHandle rawValues;
|
|
#endregion
|
|
}
|
|
|
|
/// <summary>
|
|
/// Enumeration used to identify if a ClaimValueDictionary comprised of user
|
|
/// or device claims.
|
|
/// </summary>
|
|
internal enum ClaimDefinitionType
|
|
{
|
|
User,
|
|
Device
|
|
}
|
|
|
|
/// <summary>
|
|
/// Class to represent a set of claim values(s) and to facilitate applying
|
|
/// these to an Authz client context
|
|
/// </summary>
|
|
[Serializable]
|
|
internal class ClaimValueDictionary : Dictionary<string, ClaimValue>, ISerializable
|
|
{
|
|
#region Constructor
|
|
/// <summary>
|
|
/// Identifies if this instance represents user's claims or device's
|
|
/// claims
|
|
/// </summary>
|
|
/// <param name="type">ClaimDefinitionType.User to indicate user's
|
|
/// claims and ClaimDefinitionType.Device to indicate device's claims.
|
|
/// </param>
|
|
/// <remarks>When ClaimDefinitionType.User, AithzModifyClaims in invoked
|
|
/// with SidClass AuthzContextInfoUserClaims and when
|
|
/// ClaimDefinitionType.Device with SidClass
|
|
/// AuthzContextInfoDeviceClaims.</remarks>
|
|
public ClaimValueDictionary(ClaimDefinitionType type)
|
|
{
|
|
claimDefnType = type;
|
|
}
|
|
#endregion
|
|
|
|
#region Public methods
|
|
/// <summary>
|
|
/// Adds or replaces claims in the specified Authz Client Context.
|
|
/// </summary>
|
|
/// <remarks>This method invokes AuthzModifyClaims, modifying the claims
|
|
/// using AUTHZ_SECURITY_ATTRIBUTE_OPERATION_REPLACE. This ensures that
|
|
/// the values of a claims that already exists are replaces and the ones
|
|
/// not present are added.</remarks>
|
|
/// <param name="handleClientContext">Handle to the Authz Client Context to be modified</param>
|
|
/// <returns>Win32Error.ERROR_SUCCESS on success and Win32 error code otherwise.</returns>
|
|
public int ApplyClaims(AUTHZ_CLIENT_CONTEXT_HANDLE handleClientContext)
|
|
{
|
|
NativeMethods.AuthzSecurityAttributeOperation[] claimOps = null;
|
|
var claims = new List<NativeMethods.AUTHZ_SECURITY_ATTRIBUTE_V1>(this.Count);
|
|
|
|
foreach (var claim in this)
|
|
{
|
|
//
|
|
// If all of the value specified turned out invalid, ignore the claim altogether.
|
|
//
|
|
if (claim.Value.ValueCount == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var attribute = new NativeMethods.AUTHZ_SECURITY_ATTRIBUTE_V1();
|
|
|
|
attribute.Name = claim.Key;
|
|
attribute.Flags = 0;
|
|
attribute.Values = claim.Value.RawValues.ToIntPtr();
|
|
attribute.ValueCount = claim.Value.ValueCount;
|
|
|
|
switch(claim.Value.ValueType)
|
|
{
|
|
case ClaimValueType.Integer:
|
|
{
|
|
Debug.Assert(attribute.ValueCount == 1);
|
|
attribute.Type = NativeMethods.AuthzSecurityAttributeValueType.Int;
|
|
break;
|
|
}
|
|
case ClaimValueType.Boolean:
|
|
{
|
|
Debug.Assert(attribute.ValueCount == 1);
|
|
attribute.Type = NativeMethods.AuthzSecurityAttributeValueType.Boolean;
|
|
break;
|
|
}
|
|
case ClaimValueType.String:
|
|
{
|
|
Debug.Assert(attribute.ValueCount == 1);
|
|
goto case ClaimValueType.MultiValuedString;
|
|
}
|
|
case ClaimValueType.MultiValuedString:
|
|
{
|
|
attribute.Type = NativeMethods.AuthzSecurityAttributeValueType.String;
|
|
break;
|
|
}
|
|
}
|
|
|
|
claims.Add(attribute);
|
|
}
|
|
|
|
var claimInfo = new NativeMethods.AUTHZ_SECURITY_ATTRIBUTES_INFORMATION();
|
|
|
|
claimInfo.Version = 1; // AUTHZ_SECURITY_ATTRIBUTES_INFORMATION_VERSION_V1
|
|
claimInfo.Reserved = 0;
|
|
claimInfo.AttributeCount = (ULONG)claims.Count;
|
|
|
|
SafeHGlobalHandle v1Attributes = SafeHGlobalHandle.InvalidHandle;
|
|
|
|
if (claimInfo.AttributeCount != 0)
|
|
{
|
|
v1Attributes = SafeHGlobalHandle.AllocHGlobal(claims);
|
|
|
|
claimOps = new NativeMethods.AuthzSecurityAttributeOperation[claimInfo.AttributeCount];
|
|
for (ULONG Idx = 0; Idx < claimInfo.AttributeCount; ++Idx)
|
|
{
|
|
claimOps[Idx] = NativeMethods.AuthzSecurityAttributeOperation.Replace;
|
|
}
|
|
}
|
|
|
|
claimInfo.pAttributeV1 = v1Attributes.ToIntPtr();
|
|
|
|
if (!NativeMethods.AuthzModifyClaims(handleClientContext,
|
|
claimDefnType == ClaimDefinitionType.User
|
|
? NativeMethods.AuthzContextInformationClass.AuthzContextInfoUserClaims
|
|
: NativeMethods.AuthzContextInformationClass.AuthzContextInfoDeviceClaims,
|
|
claimOps,
|
|
ref claimInfo))
|
|
{
|
|
return Marshal.GetLastWin32Error();
|
|
}
|
|
|
|
return Win32Error.ERROR_SUCCESS;
|
|
}
|
|
#endregion
|
|
|
|
#region ISerialization implementation
|
|
protected ClaimValueDictionary(SerializationInfo info, StreamingContext context)
|
|
: base(info, context)
|
|
{ }
|
|
|
|
public override void GetObjectData(SerializationInfo info, StreamingContext context)
|
|
{
|
|
base.GetObjectData(info, context);
|
|
|
|
if (info != null)
|
|
{
|
|
info.AddValue("claimDefnType", this.claimDefnType);
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region Private members
|
|
ClaimDefinitionType claimDefnType;
|
|
#endregion
|
|
|
|
#region Nested class for P/Invokes and native (Win32) structures
|
|
static class NativeMethods
|
|
{
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
public struct AUTHZ_SECURITY_ATTRIBUTES_INFORMATION
|
|
{
|
|
public USHORT Version;
|
|
public USHORT Reserved;
|
|
public ULONG AttributeCount;
|
|
public PAUTHZ_SECURITY_ATTRIBUTE_V1 pAttributeV1;
|
|
}
|
|
|
|
public enum AuthzSecurityAttributeValueType : ushort
|
|
{
|
|
Invalid = 0x0,
|
|
Int = 0x1,
|
|
String = 0x3,
|
|
Boolean = 0x6,
|
|
}
|
|
|
|
[Flags]
|
|
public enum AuthzSecurityAttributeFlags : uint // ULONG
|
|
{
|
|
None = 0x0,
|
|
NonInheritable = 0x1,
|
|
ValueCaseSensitive = 0x2,
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
public struct AUTHZ_SECURITY_ATTRIBUTE_V1
|
|
{
|
|
[MarshalAs(UnmanagedType.LPWStr)]
|
|
public string Name;
|
|
public AuthzSecurityAttributeValueType Type;
|
|
public USHORT Reserved;
|
|
public AuthzSecurityAttributeFlags Flags;
|
|
public ULONG ValueCount;
|
|
public IntPtr Values;
|
|
}
|
|
|
|
public enum AuthzContextInformationClass : uint
|
|
{
|
|
AuthzContextInfoUserClaims = 13,
|
|
AuthzContextInfoDeviceClaims,
|
|
};
|
|
|
|
public enum AuthzSecurityAttributeOperation : uint
|
|
{
|
|
None = 0,
|
|
ReplaceAll,
|
|
Add,
|
|
Delete,
|
|
Replace
|
|
}
|
|
|
|
[DllImport(Win32.AUTHZ_DLL, CharSet = CharSet.Unicode, SetLastError = true)]
|
|
[return: MarshalAs(UnmanagedType.Bool)]
|
|
public static extern bool AuthzModifyClaims(
|
|
AUTHZ_CLIENT_CONTEXT_HANDLE handleClientContext,
|
|
AuthzContextInformationClass infoClass,
|
|
AuthzSecurityAttributeOperation[] claimOperation,
|
|
ref AUTHZ_SECURITY_ATTRIBUTES_INFORMATION claims);
|
|
}
|
|
#endregion
|
|
}
|
|
}
|