Set LogonHours Active Directory

julierme

Member
Joined
Jun 13, 2021
Messages
14
Programming Experience
3-5
Dear,
I'm developing a system to restrict user login times in AD, I can view login times and even change. But for example, I'm not able to establish this login time from 17:00 to midnight, the system appears to be zero, another issue is to establish the time from 00:00 to 00:00, when this done in the system in AD, it displays as denied.

I'm using as a base the classes available here: LogonTime.cs
Observe the days of Friday and Saturday, the system is showing up empty, but in AD is marked the allowed times.
I need help getting these details right.

1623625475735.png


1623625508749.png
 
Seeking to understand the position of bytes in the LogonHours mask, I noticed that it is shown as if it were inverted.
Is it possible to be making this spin?
1623925805678.png
 
Is the console output showing low bits to high bits for each byte?
 
Sorry. That's the downside of looking at screenshots on small devices like a phone. Now that I'm at a PC, I can see that the console output on the right is showing the bits low to high order which corresponds to how the data about logon hours is represented.

Everything is correct. The data in the bytes on the console output on the right correctly matches up with the the graphical representation on the left. The thing that you don't seem to be taking into account is that the byte data is in UTC while the graphical display is in local time. Notice how for Sunday, the first active hour is on bit 7 of the 2nd byte. That corresponds to 15:00 UTC (7 + (2-1) * 8 == 15). In your GUI, that first active hour is at 12:00. So that would imply that your local time zone is at -3 hours offset. That is completely consistent with the Tuesday that you highlighted. The first active hour is at 03:00 UTC, but your GUI is showing the active hour at 00:00 local. Since the user is on a 24 shift, notice how there are 3 active hours on Wednesday from 00:00-03:00 UTC in the console which should be 21:00-00:00 local on your GUI. Also on the Saturday, notice the leading 3 inactive hours in UTC, but it is active on in your local time.

Personally, despite the folksy easy to read style of Anlai's blogpost, it is also got some subtle errors in it. I think that this is a better blog post that explains how the data is stored:
 
Just wanted to share. With less than 80 lines of code here:
LogonHours.cs:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using AXFSoftware.Utilities;

namespace AXFSoftware.Utilities.ActiveDirectory.LogonHours
{
    public class LogonHours
    {
        // One bit per hour over an entire week
        const int BitArraySize = 24 * 7;

        public BitArray Bits { get; }

        public byte[] Bitmask
        {
            get
            {
                var bytes = new byte[BitArraySize / 8];
                Bits.CopyTo(bytes, 0);
                return bytes;
            }
        }

        public TimeZoneInfo TimeZoneInfo { get; }
        public bool IsUtc => TimeZoneInfo.Id == TimeZoneInfo.Utc.Id;

        Lazy<Week> _days;
        public Week Days => _days.Value;

        public bool this[int hour]
        {
            get => Bits[hour];
            set => Bits[hour] = value;
        }

        public LogonHours(byte [] bitmask)
            => new LogonHours(new BitArray(bitmask), TimeZoneInfo.Utc);

        public LogonHours(BitArray bits)
            => new LogonHours(new BitArray(bits), TimeZoneInfo.Utc);

        LogonHours(BitArray bits, TimeZoneInfo timeZoneInfo)
        {
            if (bits.Length != BitArraySize)
                throw new ArgumentException("Must hold exact 168 bits for 24 hours over 7 days.", nameof(bits));

            Bits = bits;
            TimeZoneInfo = timeZoneInfo;
            _days = new Lazy<Week>(() => new Week(this));
        }

        public LogonHours ToTimeZone(TimeZoneInfo timeZoneInfo)
            => ToTimeZone(timeZoneInfo, DateTime.Now);

        public LogonHours ToTimeZone(TimeZoneInfo timeZoneInfo, DateTime dateTime)
        {
            var srcOffset = TimeZoneInfo.GetUtcOffset(dateTime);
            var dstOffset = timeZoneInfo.GetUtcOffset(dateTime);
            var newOffset = dstOffset - srcOffset;

            int dstHour = (int)Math.Round(newOffset.TotalHours);
            var bits = new BitArray(Bits);
            if (dstHour < 0)
                bits.LeftRotate(-dstHour);
            else if (dstHour > 0)
                bits.RightRotate(dstHour);
            return new LogonHours(bits, timeZoneInfo);
        }

        public LogonHours ToUtc()
            => ToUtc(DateTime.Now);

        public LogonHours ToUtc(DateTime dateTime)
            => IsUtc ? new LogonHours(new BitArray(Bits), TimeZoneInfo.Utc) : ToTimeZone(TimeZoneInfo.Utc, dateTime);
    }
}
and the supporting 43 lines of code here:
BitArrayExtensions.cs:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace AXFSoftware.Utilities
{
    public static class BitArrayExtensions
    {
        public static BitArray RightRotate(this BitArray bits, int count)
        {
            if (count < 0)
                throw new ArgumentOutOfRangeException(nameof(count), count, "Must be greater than or equal zero");

            count %= bits.Count;

            if (count == 0)
                return bits;

            var lowBits = (BitArray) bits.Clone();
            lowBits.LeftShift(bits.Count - count);
            bits.RightShift(count);
            return bits.Or(lowBits);
        }

        public static BitArray LeftRotate(this BitArray bits, int count)
        {
            if (count < 0)
                throw new ArgumentOutOfRangeException(nameof(count), count, "Must be greater than or equal zero");

            count %= bits.Count;

            if (count == 0)
                return bits;

            var highBits = (BitArray)bits.Clone();
            highBits.RightShift(bits.Count - count);
            bits.LeftShift(count);
            return bits.Or(highBits);
        }
    }
}

It is already enough to be able load the AD logon hours bitmask, convert to your desired timezone, and query and set the availability hours using the indexer, and the convert back to UTC, get the appropriate bitmask to put back into AD.

For the sake convenience and just a mere 80 extra lines, these helper classes let you index into the hours by day and hour (instead of computing the hour index yourself with the above classes):
Day.cs:
using System;
using System.Collections;
using System.Collections.Generic;

namespace AXFSoftware.Utilities.ActiveDirectory.LogonHours
{
    public struct Day : IEnumerable<bool>
    {
        readonly int _startOfDayHour;

        public LogonHours LogonHours { get; }
        public TimeZoneInfo TimeZoneInfo => LogonHours.TimeZoneInfo;

        public bool this[int hour]
        {
            get
            {
                ValidateHour(hour);
                return LogonHours.Bits[_startOfDayHour + hour];
            }

            set
            {
                ValidateHour(hour);
                LogonHours.Bits[_startOfDayHour + hour] = value;
            }
        }

        internal Day(LogonHours logonHours, int day)
        {
            LogonHours = logonHours;
            _startOfDayHour = day * 24;
        }

        void ValidateHour(int hour)
        {
            if (!(0 <= hour && hour <= 23))
                throw new IndexOutOfRangeException("Hour value must be between 0 and 23 inclusively.");
        }

        public IEnumerator<bool> GetEnumerator()
        {
            for (int hour = 0; hour < 24; hour++)
                yield return this[hour];
        }

        IEnumerator IEnumerable.GetEnumerator()
            => GetEnumerator();
    }
}

Week.cs:
using System;
using System.Collections;
using System.Collections.Generic;

namespace AXFSoftware.Utilities.ActiveDirectory.LogonHours
{
    public struct Week : IEnumerable<Day>
    {
        readonly Day[] _days;

        public LogonHours LogonHours { get; }
        public TimeZoneInfo TimeZoneInfo => LogonHours.TimeZoneInfo;
        public Day this[DayOfWeek day] => _days[(int) day];

        internal Week(LogonHours logonHours)
        {
            LogonHours = logonHours;

            _days = new Day[7];
            for (int i = 0; i < 7; i++)
                _days[i] = new Day(LogonHours, i);
        }

        public IEnumerator<Day> GetEnumerator()
            => ((IEnumerable<Day>)_days).GetEnumerator();

        IEnumerator IEnumerable.GetEnumerator()
            => _days.GetEnumerator();
    }
}

Compare that to the over 300+ lines used in Anlai's code.
 
Just wanted to share. With less than 80 lines of code here:
LogonHours.cs:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using AXFSoftware.Utilities;

namespace AXFSoftware.Utilities.ActiveDirectory.LogonHours
{
    public class LogonHours
    {
        // One bit per hour over an entire week
        const int BitArraySize = 24 * 7;

        public BitArray Bits { get; }

        public byte[] Bitmask
        {
            get
            {
                var bytes = new byte[BitArraySize / 8];
                Bits.CopyTo(bytes, 0);
                return bytes;
            }
        }

        public TimeZoneInfo TimeZoneInfo { get; }
        public bool IsUtc => TimeZoneInfo.Id == TimeZoneInfo.Utc.Id;

        Lazy<Week> _days;
        public Week Days => _days.Value;

        public bool this[int hour]
        {
            get => Bits[hour];
            set => Bits[hour] = value;
        }

        public LogonHours(byte [] bitmask)
            => new LogonHours(new BitArray(bitmask), TimeZoneInfo.Utc);

        public LogonHours(BitArray bits)
            => new LogonHours(new BitArray(bits), TimeZoneInfo.Utc);

        LogonHours(BitArray bits, TimeZoneInfo timeZoneInfo)
        {
            if (bits.Length != BitArraySize)
                throw new ArgumentException("Must hold exact 168 bits for 24 hours over 7 days.", nameof(bits));

            Bits = bits;
            TimeZoneInfo = timeZoneInfo;
            _days = new Lazy<Week>(() => new Week(this));
        }

        public LogonHours ToTimeZone(TimeZoneInfo timeZoneInfo)
            => ToTimeZone(timeZoneInfo, DateTime.Now);

        public LogonHours ToTimeZone(TimeZoneInfo timeZoneInfo, DateTime dateTime)
        {
            var srcOffset = TimeZoneInfo.GetUtcOffset(dateTime);
            var dstOffset = timeZoneInfo.GetUtcOffset(dateTime);
            var newOffset = dstOffset - srcOffset;

            int dstHour = (int)Math.Round(newOffset.TotalHours);
            var bits = new BitArray(Bits);
            if (dstHour < 0)
                bits.LeftRotate(-dstHour);
            else if (dstHour > 0)
                bits.RightRotate(dstHour);
            return new LogonHours(bits, timeZoneInfo);
        }

        public LogonHours ToUtc()
            => ToUtc(DateTime.Now);

        public LogonHours ToUtc(DateTime dateTime)
            => IsUtc ? new LogonHours(new BitArray(Bits), TimeZoneInfo.Utc) : ToTimeZone(TimeZoneInfo.Utc, dateTime);
    }
}
and the supporting 43 lines of code here:
BitArrayExtensions.cs:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace AXFSoftware.Utilities
{
    public static class BitArrayExtensions
    {
        public static BitArray RightRotate(this BitArray bits, int count)
        {
            if (count < 0)
                throw new ArgumentOutOfRangeException(nameof(count), count, "Must be greater than or equal zero");

            count %= bits.Count;

            if (count == 0)
                return bits;

            var lowBits = (BitArray) bits.Clone();
            lowBits.LeftShift(bits.Count - count);
            bits.RightShift(count);
            return bits.Or(lowBits);
        }

        public static BitArray LeftRotate(this BitArray bits, int count)
        {
            if (count < 0)
                throw new ArgumentOutOfRangeException(nameof(count), count, "Must be greater than or equal zero");

            count %= bits.Count;

            if (count == 0)
                return bits;

            var highBits = (BitArray)bits.Clone();
            highBits.RightShift(bits.Count - count);
            bits.LeftShift(count);
            return bits.Or(highBits);
        }
    }
}

It is already enough to be able load the AD logon hours bitmask, convert to your desired timezone, and query and set the availability hours using the indexer, and the convert back to UTC, get the appropriate bitmask to put back into AD.

For the sake convenience and just a mere 80 extra lines, these helper classes let you index into the hours by day and hour (instead of computing the hour index yourself with the above classes):
Day.cs:
using System;
using System.Collections;
using System.Collections.Generic;

namespace AXFSoftware.Utilities.ActiveDirectory.LogonHours
{
    public struct Day : IEnumerable<bool>
    {
        readonly int _startOfDayHour;

        public LogonHours LogonHours { get; }
        public TimeZoneInfo TimeZoneInfo => LogonHours.TimeZoneInfo;

        public bool this[int hour]
        {
            get
            {
                ValidateHour(hour);
                return LogonHours.Bits[_startOfDayHour + hour];
            }

            set
            {
                ValidateHour(hour);
                LogonHours.Bits[_startOfDayHour + hour] = value;
            }
        }

        internal Day(LogonHours logonHours, int day)
        {
            LogonHours = logonHours;
            _startOfDayHour = day * 24;
        }

        void ValidateHour(int hour)
        {
            if (!(0 <= hour && hour <= 23))
                throw new IndexOutOfRangeException("Hour value must be between 0 and 23 inclusively.");
        }

        public IEnumerator<bool> GetEnumerator()
        {
            for (int hour = 0; hour < 24; hour++)
                yield return this[hour];
        }

        IEnumerator IEnumerable.GetEnumerator()
            => GetEnumerator();
    }
}

Week.cs:
using System;
using System.Collections;
using System.Collections.Generic;

namespace AXFSoftware.Utilities.ActiveDirectory.LogonHours
{
    public struct Week : IEnumerable<Day>
    {
        readonly Day[] _days;

        public LogonHours LogonHours { get; }
        public TimeZoneInfo TimeZoneInfo => LogonHours.TimeZoneInfo;
        public Day this[DayOfWeek day] => _days[(int) day];

        internal Week(LogonHours logonHours)
        {
            LogonHours = logonHours;

            _days = new Day[7];
            for (int i = 0; i < 7; i++)
                _days[i] = new Day(LogonHours, i);
        }

        public IEnumerator<Day> GetEnumerator()
            => ((IEnumerable<Day>)_days).GetEnumerator();

        IEnumerator IEnumerable.GetEnumerator()
            => _days.GetEnumerator();
    }
}

Compare that to the over 300+ lines used in Anlai's code.

Thanks.
I will try to test.
 
Dear,
I'm developing a system to restrict user login times in AD, I can view login times and even change. But for example, I'm not able to establish this login time from 17:00 to midnight, the system appears to be zero, another issue is to establish the time from 00:00 to 00:00, when this done in the system in AD, it displays as denied.

I'm using as a base the classes available here: LogonTime.cs
Observe the days of Friday and Saturday, the system is showing up empty, but in AD is marked the allowed times.
I need help getting these details right.

View attachment 1573

View attachment 1574

Can you send me your code please ?
 
See posts #33 and #34.
 
Back
Top Bottom