RaspberryIO/Unosquare.Swan/Networking/Ldap/LdapEntry.cs
2019-12-03 18:44:25 +01:00

734 lines
25 KiB
C#

using System;
using System.Linq;
using System.Collections.Generic;
using System.Text;
namespace Unosquare.Swan.Networking.Ldap {
/// <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) {
this.DN = dn ?? String.Empty;
this._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) => this._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() => this._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) => this._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) {
this.Name = attrName ?? throw new ArgumentNullException(nameof(attrName));
this._baseName = GetBaseName(attrName);
this._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) => this.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(this._values == null) {
return new SByte[0][];
}
Int32 size = this._values.Length;
SByte[][] bva = new SByte[size][];
// Deep copy so application cannot change values
for(Int32 i = 0, u = size; i < u; i++) {
bva[i] = new SByte[((SByte[])this._values[i]).Length];
Array.Copy((Array)this._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(this._values == null) {
return new String[0];
}
Int32 size = this._values.Length;
String[] sva = new String[size];
for(Int32 j = 0; j < size; j++) {
sva[j] = Encoding.UTF8.GetString((SByte[])this._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 => this._values == null ? null : Encoding.UTF8.GetString((SByte[])this._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(this._values == null) {
return null;
}
// Deep copy so app can't change the value
SByte[] bva = new SByte[((SByte[])this._values[0]).Length];
Array.Copy((Array)this._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 => this._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 {
this._values = null;
this.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");
}
Tokenizer st = new Tokenizer(attrName, ";");
String[] subTypes = null;
Int32 cnt = st.Count;
if(cnt > 0) {
_ = st.NextToken(); // skip over basename
subTypes = new String[cnt - 1];
Int32 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");
}
Int32 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() {
Object newObj = this.MemberwiseClone();
if(this._values != null) {
Array.Copy(this._values, 0, ((LdapAttribute)newObj)._values, 0, this._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");
}
this.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");
}
this.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");
}
this.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, Int32 start, Int32 end) {
if(attrString == null) {
throw new ArgumentNullException(nameof(attrString));
}
this.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));
}
this.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() => this._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() => this._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 Boolean HasSubtype(String subtype) {
if(subtype == null) {
throw new ArgumentNullException(nameof(subtype));
}
return this._subTypes != null && this._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 Boolean HasSubtypes(String[] subtypes) {
if(subtypes == null) {
throw new ArgumentNullException(nameof(subtypes));
}
for(Int32 i = 0; i < subtypes.Length; i++) {
foreach(String sub in this._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));
}
this.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(Int32 i = 0; i < this._values.Length; i++) {
if(!Equals(attrBytes, (SByte[])this._values[i])) {
continue;
}
if(i == 0 && this._values.Length == 1) {
// Optimize if first element of a single valued attr
this._values = null;
return;
}
if(this._values.Length == 1) {
this._values = null;
} else {
Int32 moved = this._values.Length - i - 1;
Object[] tmp = new Object[this._values.Length - 1];
if(i != 0) {
Array.Copy(this._values, 0, tmp, 0, i);
}
if(moved != 0) {
Array.Copy(this._values, i + 1, tmp, i, moved);
}
this._values = tmp;
}
break;
}
}
/// <summary>
/// Returns the number of values in the attribute.
/// </summary>
/// <returns>
/// The number of values in the attribute.
/// </returns>
public Int32 Size() => this._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 Int32 CompareTo(Object attribute)
=> String.Compare(this.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() {
StringBuilder result = new StringBuilder("LdapAttribute: ");
_ = result.Append("{type='" + this.Name + "'");
if(this._values != null) {
_ = result
.Append(", ")
.Append(this._values.Length == 1 ? "value='" : "values='");
for(Int32 i = 0; i < this._values.Length; i++) {
if(i != 0) {
_ = result.Append("','");
}
if(((SByte[])this._values[i]).Length == 0) {
continue;
}
String sval = Encoding.UTF8.GetString((SByte[])this._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(this._values == null) {
this._values = new Object[] { bytes };
} else {
// Duplicate attribute values not allowed
if(this._values.Any(t => Equals(bytes, (SByte[])t))) {
return; // Duplicate, don't add
}
Object[] tmp = new Object[this._values.Length + 1];
Array.Copy(this._values, 0, tmp, 0, this._values.Length);
tmp[this._values.Length] = bytes;
this._values = tmp;
}
}
private static Boolean 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
Int32 length = e1.Length;
if(e2.Length != length) {
return false;
}
// If any of the bytes are different, they compare false
for(Int32 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
LdapAttributeSet tempAttributeSet = new LdapAttributeSet();
foreach(KeyValuePair<String, LdapAttribute> 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 Boolean Contains(Object attr) => this.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 Boolean Add(LdapAttribute attr) {
String name = attr.Name;
if(this.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 Boolean Remove(LdapAttribute entry) => this.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() {
StringBuilder retValue = new StringBuilder("LdapAttributeSet: ");
Boolean first = true;
foreach(KeyValuePair<String, LdapAttribute> attr in this) {
if(!first) {
_ = retValue.Append(" ");
}
first = false;
_ = retValue.Append(attr);
}
return retValue.ToString();
}
}
}