RaspberryIO/Unosquare.Swan/Networking/Ldap/LdapEntry.cs

794 lines
28 KiB
C#
Raw Normal View History

2019-02-17 14:08:57 +01:00
namespace Unosquare.Swan.Networking.Ldap
{
using System.Linq;
using System;
using System.Collections.Generic;
using System.Text;
/// <summary>
/// Represents a single entry in a directory, consisting of
/// a distinguished name (DN) and zero or more attributes.
/// An instance of
/// LdapEntry is created in order to add an entry to a directory, and
/// instances of LdapEntry are returned on a search by enumerating an
/// LdapSearchResults.
/// </summary>
/// <seealso cref="LdapAttribute"></seealso>
/// <seealso cref="LdapAttributeSet"></seealso>
public class LdapEntry
{
private readonly LdapAttributeSet _attrs;
/// <summary>
/// Initializes a new instance of the <see cref="LdapEntry" /> class.
/// Constructs a new entry with the specified distinguished name and set
/// of attributes.
/// </summary>
/// <param name="dn">The distinguished name of the new entry. The
/// value is not validated. An invalid distinguished
/// name will cause operations using this entry to fail.</param>
/// <param name="attrs">The initial set of attributes assigned to the
/// entry.</param>
public LdapEntry(string dn = null, LdapAttributeSet attrs = null)
{
DN = dn ?? string.Empty;
_attrs = attrs ?? new LdapAttributeSet();
}
/// <summary>
/// Returns the distinguished name of the entry.
/// </summary>
/// <value>
/// The dn.
/// </value>
public string DN { get; }
/// <summary>
/// Returns the attributes matching the specified attrName.
/// </summary>
/// <param name="attrName">The name of the attribute or attributes to return.</param>
/// <returns>
/// The attribute matching the name.
/// </returns>
public LdapAttribute GetAttribute(string attrName) => _attrs[attrName];
/// <summary>
/// Returns the attribute set of the entry.
/// All base and subtype variants of all attributes are
/// returned. The LdapAttributeSet returned may be
/// empty if there are no attributes in the entry.
/// </summary>
/// <returns>
/// The attribute set of the entry.
/// </returns>
public LdapAttributeSet GetAttributeSet() => _attrs;
/// <summary>
/// Returns an attribute set from the entry, consisting of only those
/// attributes matching the specified subtypes.
/// The getAttributeSet method can be used to extract only
/// a particular language variant subtype of each attribute,
/// if it exists. The "subtype" may be, for example, "lang-ja", "binary",
/// or "lang-ja;phonetic". If more than one subtype is specified, separated
/// with a semicolon, only those attributes with all of the named
/// subtypes will be returned. The LdapAttributeSet returned may be
/// empty if there are no matching attributes in the entry.
/// </summary>
/// <param name="subtype">One or more subtype specification(s), separated
/// with semicolons. The "lang-ja" and
/// "lang-en;phonetic" are valid subtype
/// specifications.</param>
/// <returns>
/// An attribute set from the entry with the attributes that
/// match the specified subtypes or an empty set if no attributes
/// match.
/// </returns>
public LdapAttributeSet GetAttributeSet(string subtype) => _attrs.GetSubset(subtype);
}
/// <summary>
/// The name and values of one attribute of a directory entry.
/// LdapAttribute objects are used when searching for, adding,
/// modifying, and deleting attributes from the directory.
/// LdapAttributes are often used in conjunction with an
/// LdapAttributeSet when retrieving or adding multiple
/// attributes to an entry.
/// </summary>
public class LdapAttribute
{
private readonly string _baseName; // cn of cn;lang-ja;phonetic
private readonly string[] _subTypes; // lang-ja of cn;lang-ja
private object[] _values; // Array of byte[] attribute values
/// <summary>
/// Initializes a new instance of the <see cref="LdapAttribute"/> class.
/// Constructs an attribute with no values.
/// </summary>
/// <param name="attrName">Name of the attribute.</param>
/// <exception cref="ArgumentException">Attribute name cannot be null.</exception>
public LdapAttribute(string attrName)
{
Name = attrName ?? throw new ArgumentNullException(nameof(attrName));
_baseName = GetBaseName(attrName);
_subTypes = GetSubtypes(attrName);
}
/// <summary>
/// Initializes a new instance of the <see cref="LdapAttribute" /> class.
/// Constructs an attribute with a single <see cref="System.String" /> value.
/// </summary>
/// <param name="attrName">Name of the attribute.</param>
/// <param name="attrString">Value of the attribute as a string.</param>
/// <exception cref="ArgumentException">Attribute value cannot be null.</exception>
public LdapAttribute(string attrName, string attrString)
: this(attrName)
{
Add(Encoding.UTF8.GetSBytes(attrString));
}
/// <summary>
/// Returns the values of the attribute as an array of bytes.
/// </summary>
/// <value>
/// The byte value array.
/// </value>
public sbyte[][] ByteValueArray
{
get
{
if (_values == null)
return new sbyte[0][];
var size = _values.Length;
var bva = new sbyte[size][];
// Deep copy so application cannot change values
for (int i = 0, u = size; i < u; i++)
{
bva[i] = new sbyte[((sbyte[])_values[i]).Length];
Array.Copy((Array)_values[i], 0, bva[i], 0, bva[i].Length);
}
return bva;
}
}
/// <summary>
/// Returns the values of the attribute as an array of strings.
/// </summary>
/// <value>
/// The string value array.
/// </value>
public string[] StringValueArray
{
get
{
if (_values == null)
return new string[0];
var size = _values.Length;
var sva = new string[size];
for (var j = 0; j < size; j++)
{
sva[j] = Encoding.UTF8.GetString((sbyte[])_values[j]);
}
return sva;
}
}
/// <summary>
/// Returns the the first value of the attribute as an UTF-8 string.
/// </summary>
/// <value>
/// The string value.
/// </value>
public string StringValue => _values == null ? null : Encoding.UTF8.GetString((sbyte[])_values[0]);
/// <summary>
/// Returns the first value of the attribute as a byte array or null.
/// </summary>
/// <value>
/// The byte value.
/// </value>
public sbyte[] ByteValue
{
get
{
if (_values == null) return null;
// Deep copy so app can't change the value
var bva = new sbyte[((sbyte[])_values[0]).Length];
Array.Copy((Array)_values[0], 0, bva, 0, bva.Length);
return bva;
}
}
/// <summary>
/// Returns the language subtype of the attribute, if any.
/// For example, if the attribute name is cn;lang-ja;phonetic,
/// this method returns the string, lang-ja.
/// </summary>
/// <value>
/// The language subtype.
/// </value>
public string LangSubtype => _subTypes?.FirstOrDefault(t => t.StartsWith("lang-"));
/// <summary>
/// Returns the name of the attribute.
/// </summary>
/// <value>
/// The name.
/// </value>
public string Name { get; }
internal string Value
{
set
{
_values = null;
Add(Encoding.UTF8.GetSBytes(value));
}
}
/// <summary>
/// Extracts the subtypes from the specified attribute name.
/// For example, if the attribute name is cn;lang-ja;phonetic,
/// this method returns an array containing lang-ja and phonetic.
/// </summary>
/// <param name="attrName">Name of the attribute from which to extract
/// the subtypes.</param>
/// <returns>
/// An array subtypes or null if the attribute has none.
/// </returns>
/// <exception cref="ArgumentException">Attribute name cannot be null.</exception>
public static string[] GetSubtypes(string attrName)
{
if (attrName == null)
{
throw new ArgumentException("Attribute name cannot be null");
}
var st = new Tokenizer(attrName, ";");
string[] subTypes = null;
var cnt = st.Count;
if (cnt > 0)
{
st.NextToken(); // skip over basename
subTypes = new string[cnt - 1];
var i = 0;
while (st.HasMoreTokens())
{
subTypes[i++] = st.NextToken();
}
}
return subTypes;
}
/// <summary>
/// Returns the base name of the specified attribute name.
/// For example, if the attribute name is cn;lang-ja;phonetic,
/// this method returns cn.
/// </summary>
/// <param name="attrName">Name of the attribute from which to extract the
/// base name.</param>
/// <returns> The base name of the attribute. </returns>
/// <exception cref="ArgumentException">Attribute name cannot be null.</exception>
public static string GetBaseName(string attrName)
{
if (attrName == null)
{
throw new ArgumentException("Attribute name cannot be null");
}
var idx = attrName.IndexOf(';');
return idx == -1 ? attrName : attrName.Substring(0, idx - 0);
}
/// <summary>
/// Clones this instance.
/// </summary>
/// <returns>A cloned instance.</returns>
public LdapAttribute Clone()
{
var newObj = MemberwiseClone();
if (_values != null)
{
Array.Copy(_values, 0, ((LdapAttribute)newObj)._values, 0, _values.Length);
}
return (LdapAttribute) newObj;
}
/// <summary>
/// Adds a <see cref="System.String" /> value to the attribute.
/// </summary>
/// <param name="attrString">Value of the attribute as a String.</param>
/// <exception cref="ArgumentException">Attribute value cannot be null.</exception>
public void AddValue(string attrString)
{
if (attrString == null)
{
throw new ArgumentException("Attribute value cannot be null");
}
Add(Encoding.UTF8.GetSBytes(attrString));
}
/// <summary>
/// Adds a byte-formatted value to the attribute.
/// </summary>
/// <param name="attrBytes">Value of the attribute as raw bytes.
/// Note: If attrBytes represents a string it should be UTF-8 encoded.</param>
/// <exception cref="ArgumentException">Attribute value cannot be null.</exception>
public void AddValue(sbyte[] attrBytes)
{
if (attrBytes == null)
{
throw new ArgumentException("Attribute value cannot be null");
}
Add(attrBytes);
}
/// <summary>
/// Adds a base64 encoded value to the attribute.
/// The value will be decoded and stored as bytes. String
/// data encoded as a base64 value must be UTF-8 characters.
/// </summary>
/// <param name="attrString">The base64 value of the attribute as a String.</param>
/// <exception cref="ArgumentException">Attribute value cannot be null.</exception>
public void AddBase64Value(string attrString)
{
if (attrString == null)
{
throw new ArgumentException("Attribute value cannot be null");
}
Add(Convert.FromBase64String(attrString).ToSByteArray());
}
/// <summary>
/// Adds a base64 encoded value to the attribute.
/// The value will be decoded and stored as bytes. Character
/// data encoded as a base64 value must be UTF-8 characters.
/// </summary>
/// <param name="attrString">The base64 value of the attribute as a StringBuffer.</param>
/// <param name="start">The start index of base64 encoded part, inclusive.</param>
/// <param name="end">The end index of base encoded part, exclusive.</param>
/// <exception cref="ArgumentNullException">attrString.</exception>
public void AddBase64Value(StringBuilder attrString, int start, int end)
{
if (attrString == null)
{
throw new ArgumentNullException(nameof(attrString));
}
Add(Convert.FromBase64String(attrString.ToString(start, end)).ToSByteArray());
}
/// <summary>
/// Adds a base64 encoded value to the attribute.
/// The value will be decoded and stored as bytes. Character
/// data encoded as a base64 value must be UTF-8 characters.
/// </summary>
/// <param name="attrChars">The base64 value of the attribute as an array of
/// characters.</param>
/// <exception cref="ArgumentNullException">attrChars.</exception>
public void AddBase64Value(char[] attrChars)
{
if (attrChars == null)
{
throw new ArgumentNullException(nameof(attrChars));
}
Add(Convert.FromBase64CharArray(attrChars, 0, attrChars.Length).ToSByteArray());
}
/// <summary>
/// Returns the base name of the attribute.
/// For example, if the attribute name is cn;lang-ja;phonetic,
/// this method returns cn.
/// </summary>
/// <returns>
/// The base name of the attribute.
/// </returns>
public string GetBaseName() => _baseName;
/// <summary>
/// Extracts the subtypes from the attribute name.
/// For example, if the attribute name is cn;lang-ja;phonetic,
/// this method returns an array containing lang-ja and phonetic.
/// </summary>
/// <returns>
/// An array subtypes or null if the attribute has none.
/// </returns>
public string[] GetSubtypes() => _subTypes;
/// <summary>
/// Reports if the attribute name contains the specified subtype.
/// For example, if you check for the subtype lang-en and the
/// attribute name is cn;lang-en, this method returns true.
/// </summary>
/// <param name="subtype">
/// The single subtype to check for.
/// </param>
/// <returns>
/// True, if the attribute has the specified subtype;
/// false, if it doesn't.
/// </returns>
public bool HasSubtype(string subtype)
{
if (subtype == null)
{
throw new ArgumentNullException(nameof(subtype));
}
return _subTypes != null && _subTypes.Any(t => string.Equals(t, subtype, StringComparison.OrdinalIgnoreCase));
}
/// <summary>
/// Reports if the attribute name contains all the specified subtypes.
/// For example, if you check for the subtypes lang-en and phonetic
/// and if the attribute name is cn;lang-en;phonetic, this method
/// returns true. If the attribute name is cn;phonetic or cn;lang-en,
/// this method returns false.
/// </summary>
/// <param name="subtypes">
/// An array of subtypes to check for.
/// </param>
/// <returns>
/// True, if the attribute has all the specified subtypes;
/// false, if it doesn't have all the subtypes.
/// </returns>
public bool HasSubtypes(string[] subtypes)
{
if (subtypes == null)
{
throw new ArgumentNullException(nameof(subtypes));
}
for (var i = 0; i < subtypes.Length; i++)
{
foreach (var sub in _subTypes)
{
if (sub == null)
{
throw new ArgumentException($"subtype at array index {i} cannot be null");
}
if (string.Equals(sub, subtypes[i], StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
}
return false;
}
/// <summary>
/// Removes a string value from the attribute.
/// </summary>
/// <param name="attrString">Value of the attribute as a string.
/// Note: Removing a value which is not present in the attribute has
/// no effect.</param>
/// <exception cref="ArgumentNullException">attrString.</exception>
public void RemoveValue(string attrString)
{
if (attrString == null)
{
throw new ArgumentNullException(nameof(attrString));
}
RemoveValue(Encoding.UTF8.GetSBytes(attrString));
}
/// <summary>
/// Removes a byte-formatted value from the attribute.
/// </summary>
/// <param name="attrBytes">Value of the attribute as raw bytes.
/// Note: If attrBytes represents a string it should be UTF-8 encoded.
/// Note: Removing a value which is not present in the attribute has
/// no effect.
/// </param>
/// <exception cref="ArgumentNullException">attrBytes.</exception>
public void RemoveValue(sbyte[] attrBytes)
{
if (attrBytes == null)
{
throw new ArgumentNullException(nameof(attrBytes));
}
for (var i = 0; i < _values.Length; i++)
{
if (!Equals(attrBytes, (sbyte[])_values[i])) continue;
if (i == 0 && _values.Length == 1)
{
// Optimize if first element of a single valued attr
_values = null;
return;
}
if (_values.Length == 1)
{
_values = null;
}
else
{
var moved = _values.Length - i - 1;
var tmp = new object[_values.Length - 1];
if (i != 0)
{
Array.Copy(_values, 0, tmp, 0, i);
}
if (moved != 0)
{
Array.Copy(_values, i + 1, tmp, i, moved);
}
_values = tmp;
}
break;
}
}
/// <summary>
/// Returns the number of values in the attribute.
/// </summary>
/// <returns>
/// The number of values in the attribute.
/// </returns>
public int Size() => _values?.Length ?? 0;
/// <summary>
/// Compares this object with the specified object for order.
/// Ordering is determined by comparing attribute names using the method Compare() of the String class.
/// </summary>
/// <param name="attribute">The LdapAttribute to be compared to this object.</param>
/// <returns>
/// Returns a negative integer, zero, or a positive
/// integer as this object is less than, equal to, or greater than the
/// specified object.
/// </returns>
public int CompareTo(object attribute)
=> string.Compare(Name, ((LdapAttribute)attribute).Name, StringComparison.Ordinal);
/// <summary>
/// Returns a string representation of this LdapAttribute.
/// </summary>
/// <returns>
/// a string representation of this LdapAttribute.
/// </returns>
/// <exception cref="Exception">NullReferenceException.</exception>
public override string ToString()
{
var result = new StringBuilder("LdapAttribute: ");
result.Append("{type='" + Name + "'");
if (_values != null)
{
result
.Append(", ")
.Append(_values.Length == 1 ? "value='" : "values='");
for (var i = 0; i < _values.Length; i++)
{
if (i != 0)
{
result.Append("','");
}
if (((sbyte[])_values[i]).Length == 0)
{
continue;
}
var sval = Encoding.UTF8.GetString((sbyte[])_values[i]);
if (sval.Length == 0)
{
// didn't decode well, must be binary
result.Append("<binary value, length:" + sval.Length);
continue;
}
result.Append(sval);
}
result.Append("'");
}
result.Append("}");
return result.ToString();
}
/// <summary>
/// Adds an object to this object's list of attribute values.
/// </summary>
/// <param name="bytes">Ultimately all of this attribute's values are treated
/// as binary data so we simplify the process by requiring
/// that all data added to our list is in binary form.
/// Note: If attrBytes represents a string it should be UTF-8 encoded.</param>
private void Add(sbyte[] bytes)
{
if (_values == null)
{
_values = new object[] { bytes };
}
else
{
// Duplicate attribute values not allowed
if (_values.Any(t => Equals(bytes, (sbyte[])t)))
{
return; // Duplicate, don't add
}
var tmp = new object[_values.Length + 1];
Array.Copy(_values, 0, tmp, 0, _values.Length);
tmp[_values.Length] = bytes;
_values = tmp;
}
}
private static bool Equals(sbyte[] e1, sbyte[] e2)
{
// If same object, they compare true
if (e1 == e2)
return true;
// If either but not both are null, they compare false
if (e1 == null || e2 == null)
return false;
// If arrays have different length, they compare false
var length = e1.Length;
if (e2.Length != length)
return false;
// If any of the bytes are different, they compare false
for (var i = 0; i < length; i++)
{
if (e1[i] != e2[i])
return false;
}
return true;
}
}
/// <summary>
/// A set of LdapAttribute objects.
/// An LdapAttributeSet is a collection of LdapAttribute
/// classes as returned from an LdapEntry on a search or read
/// operation. LdapAttributeSet may be also used to construct an entry
/// to be added to a directory.
/// </summary>
/// <seealso cref="LdapAttribute"></seealso>
/// <seealso cref="LdapEntry"></seealso>
public class LdapAttributeSet : Dictionary<string, LdapAttribute>
{
/// <summary>
/// Initializes a new instance of the <see cref="LdapAttributeSet"/> class.
/// </summary>
public LdapAttributeSet()
: base(StringComparer.OrdinalIgnoreCase)
{
// placeholder
}
/// <summary>
/// Creates a new attribute set containing only the attributes that have
/// the specified subtypes.
/// For example, suppose an attribute set contains the following
/// attributes:
/// <ul><li> cn</li><li> cn;lang-ja</li><li> sn;phonetic;lang-ja</li><li> sn;lang-us</li></ul>
/// Calling the <c>getSubset</c> method and passing lang-ja as the
/// argument, the method returns an attribute set containing the following
/// attributes:.
/// <ul><li>cn;lang-ja</li><li>sn;phonetic;lang-ja</li></ul>
/// </summary>
/// <param name="subtype">Semi-colon delimited list of subtypes to include. For
/// example:
/// <ul><li> "lang-ja" specifies only Japanese language subtypes</li><li> "binary" specifies only binary subtypes</li><li>
/// "binary;lang-ja" specifies only Japanese language subtypes
/// which also are binary
/// </li></ul>
/// Note: Novell eDirectory does not currently support language subtypes.
/// It does support the "binary" subtype.</param>
/// <returns>
/// An attribute set containing the attributes that match the
/// specified subtype.
/// </returns>
public LdapAttributeSet GetSubset(string subtype)
{
// Create a new tempAttributeSet
var tempAttributeSet = new LdapAttributeSet();
foreach (var kvp in this)
{
if (kvp.Value.HasSubtype(subtype))
tempAttributeSet.Add(kvp.Value.Clone());
}
return tempAttributeSet;
}
/// <summary>
/// Returns <c>true</c> if this set contains an attribute of the same name
/// as the specified attribute.
/// </summary>
/// <param name="attr">Object of type <c>LdapAttribute</c>.</param>
/// <returns>
/// true if this set contains the specified attribute.
/// </returns>
public bool Contains(object attr) => ContainsKey(((LdapAttribute)attr).Name);
/// <summary>
/// Adds the specified attribute to this set if it is not already present.
/// If an attribute with the same name already exists in the set then the
/// specified attribute will not be added.
/// </summary>
/// <param name="attr">Object of type <c>LdapAttribute</c>.</param>
/// <returns>
/// <c>true</c> if the attribute was added.
/// </returns>
public bool Add(LdapAttribute attr)
{
var name = attr.Name;
if (ContainsKey(name))
return false;
this[name] = attr;
return true;
}
/// <summary>
/// Removes the specified object from this set if it is present.
/// If the specified object is of type <c>LdapAttribute</c>, the
/// specified attribute will be removed. If the specified object is of type
/// string, the attribute with a name that matches the string will
/// be removed.
/// </summary>
/// <param name="entry">The entry.</param>
/// <returns>
/// true if the object was removed.
/// </returns>
public bool Remove(LdapAttribute entry) => Remove(entry.Name);
/// <summary>
/// Returns a <see cref="System.String" /> that represents this instance.
/// </summary>
/// <returns>
/// A <see cref="System.String" /> that represents this instance.
/// </returns>
public override string ToString()
{
var retValue = new StringBuilder("LdapAttributeSet: ");
var first = true;
foreach (var attr in this)
{
if (!first)
{
retValue.Append(" ");
}
first = false;
retValue.Append(attr);
}
return retValue.ToString();
}
}
}