Manipulating User IDs in AD with c# - c#

I am seriously confused, I'm a very out of practice software engineer (Last job in the field was 1993!) doing a bit of minor coding to make my sysadmin job go easier. I've picked c# over powershell because of its better debugging. What I'm trying to do, is take a username out of a spreadsheet, if that username exists in Active Directory, check its expiry date and if the one in the spreadsheet is later than the one set in active directory then update active directory to reflect.
I'm using VS 2015 on X64 2012 Server
I've got the following segment of code
using System.DirectoryServices.AccountManagement AuthenticablePrincipal.AccountExpirationDate
foundUser is the AuthenticablePrincipal
Globals.Uservars is a global (I know... please no lectures about global variables) String Array holding data which relates to the AD user object.
Uservars[4] is the expiry date from the spreadsheet.
Uservars[7] is a feedback comment result of operation.
AccountExpires = foundUser.AccountExpirationDate.Value.ToLocalTime();
ThisDay = DateTime.FromOADate(Convert.ToDouble(Globals.UserVars[4]));
if (ThisDay!=null)
{
if (ThisDay > AccountExpires)
{
foundUser.AccountExpirationDate = ThisDay;
foundUser.Save();
Globals.UserVars[7] = "Account End Date Incremented";
OutputResult();
}
else
{
Globals.UserVars[7] = "Existing User No Changes Made";
OutputResult();
}
}`
My confusion stems from the fact that I don't get an error, if I subsequently read the value of foundUser.AccountExpirationDate.Value.ToLocalTime(); from AD it has updated with the value from the spreadsheet, If I re-run the program this new value is in place but the flippin' thing doesn't show as changed in active directory users and computers.
Also If I try to convert DateTime values to FILETIME before updating AD (which I believe I should do, as I suspect I'm actually updating AD with an incorrect value despite what I see if I read it back) it seems to throw an error.
DUH?

Related

Run C# script through scheduled task

I've developed a small script in c# that is querying SQL Server and add computer objects to some Active Directory groups based on certain criteria. The script is working fine when I run it using the account which has the necessary rights to add/remove objects from Active Directory Group.
When I try to schedule the job, so it runs automatically from server using the "SYSTEM" account it does not work, I get "Access denied" I've updated the bind account to use the credentials from an account that works but I still have the same issue.
> Error Message:
> *2020-01-13 18:32:30,984 [1] ERROR TestAD.clsActiveDirectory - Error occured when trying to add computerobject abcdefg-a10 to group. Error
> message: Access is denied.*
The only way to make it work is using the actual account as account to run the scheduled task, however, problem is that our company policy does not allow us to store passwords, so I need to have the account logged-on to run this script.
code snippet
de.Username = "testing#test.com";
de.Password = "xxxxxxxxx";
de.AuthenticationType = AuthenticationTypes.Secure;
de.AuthenticationType = AuthenticationTypes.Sealing;
de.AuthenticationType = AuthenticationTypes.Delegation;
de.Properties.Count.ToString();
ds.SearchRoot = de;
ds.Filter = "(&(objectClass=computer)(name=" + _myComputerName.ToString() + `"))";`
ds.PropertiesToLoad.Add("memberof");
ds.PropertiesToLoad.Add("distinguishedname");
ds.SizeLimit = 10;
ds.PageSize = 0;
ds.SearchScope = System.DirectoryServices.SearchScope.Subtree;
I've tried adding some "AuthenticationTypes" to see if that made difference but still same
any help would be appreciated
Thx.
Have you tried using SQL Server Agents? My company uses them as opposed to Scheduled Tasks. They may be less elegant, but it may be a good alternative to your situation.
Create a SQL Server Agent that calls the executable with or without parameters.
If you cannot call the executable from the hosting OS, you can call an SSIS package on the network to call the executable for you.
Please let me know if you need more details.
I found the issue, and in the end pretty straight forward.
The Active Directory flow is following
- Bind to active directory with my special account and search for the computer object and
validate if it needs to be added to Active Directory Group
- if it needs to be added do 2nd bind to the Active Directory group and add computer
object. ==> This piece failed when using scheduled task or run under "SYSTEM" context
Reason for failure: When I bind 2nd time I did not specify any credentials so it was
using default credentials (SYSTEM) if I run the script my account which has enough
rights to add computer objects to groups.
I updated code for 2nd bind to include binding credentials and now it's working as
expected.
I hope this will be helpful for somebody else who has to troubleshoot similar issues.
old code
try
{
DirectoryEntry ent = new DirectoryEntry(bindString);
ent.Properties["member"].Add(newMember);
ent.CommitChanges();
}
New code
try
{
DirectoryEntry ent = new DirectoryEntry(bindString);
ent.AuthenticationType = AuthenticationTypes.Secure;
ent.AuthenticationType = AuthenticationTypes.Sealing;
ent.AuthenticationType = AuthenticationTypes.Delegation;
ent.Username = "test123#test.com";
ent.Password = "test123";
ent.Properties["member"].Add(newMember);
ent.CommitChanges();
}

Get other user's Outlook availability information

I am writing an application that creates an overview of peoples Outlook calendars, i.e it will show the amount of unplanned time per week for the coming [n] weeks.
The basics are working, but there's one thing that I am having trouble with. Some users have shared their Outlook calendar in a way so that other users can only see availability information (the time and description of the appointments), but not any details.
I verified this by opening Outlook manually and opening a shared calendar; hovering the mouse over an appointment will show a popup with begin and end time, description and location, but double clicking it gives an error: "You are not authorized to display the calendar, do you want to ask [person] to share it?".
The relevant lines from my code are:
var outlook = new Application();
var mapiNamespace = outlook.GetNamespace("MAPI");
var recipient = mapiNamespace.CreateRecipient("Scott");
recipient.Resolve();
var calendarFolder = mapiNamespace.GetSharedDefaultFolder(recipient, OlDefaultFolders.olFolderCalendar);
var calendarItems = calendarFolder.Items;
Everything I now try to do with calendarItems will raise an exception. For instance, getting Count will raise a TargetInvocationException (The client process failed, but I'm not quite sure about the exact English translation). Calling Sort("[Start]") will raise a COMException with message Unknown property: Start. Both do work for fully shared calendars.
Now, for the overview, all I need is begin and end times, so I don't really want to ask everyone to change their sharing settings, especially when that shouldn't be necessary.
My questions are:
Most important: Is there another way to get availability info that I'm overlooking?
And related: Is Interop still the way to go these days, or are there alternatives? Maybe an Office365 webservice?
Instead of using GetSharedDefaultFolder and accessing the items in that folder, you can use Recipient.FreeBusy method.

Outlook Interop Exception HRESULT: 0xCA140115

I have some pretty basic code that seems to work for most people but there's at least one workstation that throws this HRESULT code when it runs these couple lines of code:
Outlook.Application _OutlookInstance = new Outlook.Application();
Outlook.Stores stores = _OutlookInstance.Session.Stores;
Any idea what HRESULT code 0xCA140115 is or what it means? I can't find it on MSDN anywhere...
The workstation that experiences the problem is at a remote call center location, so I can't do any immediate testing/debugging, or easily see what is specifically different about this workstation versus the others. I would imagine there might be more workstations at that same call center that could have the error, but this code is still in the testing phase.
Sorry for the delay, but I was able to get through several more iterations of testing and find out what the issue was. First off, my original post was incorrect. The code flow made it seem like the error was happening during those 2 initial lines, but it was actually happening a little later, when I was looping through the stores, like this:
Outlook.Stores stores = _OutlookInstance.Session.Stores;
foreach(Outlook.Store store in stores) // <----- THIS LINE
{
...
}
Each time the user ran this, he would get a different HRESULT error code:
0xCA140115
0xAF64011D
0xC1F4011D
0xC834011D
The only consistent factor was the "4011" in the middle.
When I upped the logging, I could see that the user had 18 mailboxes and the foreach() loop was getting through the first 3 but failing on the 4th. The 4th mailbox was a "Public Folders" store associated to another mailbox that was added in a different way than the rest of the mailboxes (it had something to do with it being an Outlook 365 mailbox that required different authentication).
So essentially it ended up being that any attempt to even touch that particular mailbox/store (including the "store" variable being set) would result in that COM exception.
I was able to work around it by looping through stores via numeric index so that the setting of "store" was inside my try/catch block, like this:
for(int i = 0; i < stores.Count; i++)
{
try
{
Outlook.Store store = stores[i];
...
}
catch(Exception)
{
...
}
}
Now when the loop hit that particular store, I could tell that it was Outlook saying that the server wasn't available, and the store was an online-only store, so the store couldn't be accessed.
I'm still not certain about why the error codes changed each time, but there you have it.

Get Direct Reports from Logged in user from Exchange

I need to get the direct reports from a logged in user (MVC 4)
I don't need the names of the direct reports but I do need their email addresses including their proxy addresses.
So for this reason I need to search through Exchange. I personally have never attempted to search Exchange in the past and everything I find out there tells me how to get from step 8 to the finish line but says nothing about how to go from step 1 to 8.
I can get the current users user name by simply
User.Identity.Name.Replace(#"yourdomain\", "")
and I have found this example which so far is probably the best example I have found
http://msdn.microsoft.com/en-us/library/office/ff184617(v=office.15).aspx
but even with that example the line
Outlook.AddressEntry currentUser =
Application.Session.CurrentUser.AddressEntry;
is not actually getting the current user logged into the site.
I really hope someone out there is familiar with this and can get me past this point.
I reworked the sample from the URL as the following LINQPad 4 query. I've found that LINQPad is a great way to experiment because it is very scripty, allowing quick experimentation, and you can easily view data by using the Dump() extension method. Purchasing intellisense support is totally worthwhile.
Also, I noticed there is a lot of fine print like:
The logged-on user must be online for this method to return an AddressEntries collection; otherwise, GetDirectReports returns a null reference. For production code, you must test for the user being offline by using the _NameSpace.ExchangeConnectionMode property, or the _Account.ExchangeConnectionMode property for multiple Exchange scenarios.
and
If the current user has a manager, GetDirectReports() is called to return an AddressEntries collection that represents the address entries for all the direct reports of user’s manager. If the manager has no direct reports, GetDirectReports returns an AddressEntries collection that has a count of zero.
So there are a lot of assumptions like Exchange is configured properly with Direct Report relationships, and the current user is online...which I believe brings Lync into the equation. Hopefully this LINQPad query will be useful to you. Just copy and paste it into a text editor and name it with the .linq file extension. You'll then be able to open it in LINQPad 4. BTW: You're question caught my attention because there was talk recently at my work of pulling direct reports from Active Directory. I wish I could be more helpful...good luck.
<Query Kind="Program">
<Reference><ProgramFilesX86>\Microsoft Visual Studio 12.0\Visual Studio Tools for Office\PIA\Office15\Microsoft.Office.Interop.Outlook.dll</Reference>
<Reference><ProgramFilesX86>\Microsoft Visual Studio 12.0\Visual Studio Tools for Office\PIA\Office15\Microsoft.Office.Interop.OutlookViewCtl.dll</Reference>
<Namespace>Microsoft.Office.Interop.Outlook</Namespace>
</Query>
void Main()
{
GetManagerDirectReports();
}
// Define other methods and classes here
private void GetManagerDirectReports()
{
var app = new Microsoft.Office.Interop.Outlook.Application();
AddressEntry currentUser = app.Session.CurrentUser.AddressEntry;
if (currentUser.Type == "EX")
{
ExchangeUser manager = currentUser.GetExchangeUser().GetExchangeUserManager();
manager.Dump();
if (manager != null)
{
AddressEntries addrEntries = manager.GetDirectReports();
if (addrEntries != null)
{
foreach (AddressEntry addrEntry in addrEntries)
{
ExchangeUser exchUser = addrEntry.GetExchangeUser();
StringBuilder sb = new StringBuilder();
sb.AppendLine("Name: " + exchUser.Name);
sb.AppendLine("Title: " + exchUser.JobTitle);
sb.AppendLine("Department: " + exchUser.Department);
sb.AppendLine("Location: " + exchUser.OfficeLocation);
sb.Dump();
}
}
}
}
}
I would suggest using EWS Managed API in conjunction with your code to get the direct reports for a user. As Jeremy mentioned in his response that you need to have your direct report relationships already set up. To help you get started, here some steps to get EWS Managed API up and running:
Download the latest version of EWS Managed API
Get started with EWS Managed API client applications to learn about how to reference the assembly, set the service URL, and communicate with EWS.
Start working with your code. If you need some functioning code to get you going, check out the Exchange 2013 101 Code Samples that has some authentication code already written and a bunch of examples you can modify to make your own.
If you have the email address or user name of the current user you can use the ResolveName() method to get to their mailbox to retrieve additional information. Here is an article to help with that method: How to: Resolve ambiguous names by using EWS in Exchange 2013
Essentially you want to get to the point where you can run a command similar to this:
NameResolutionCollection coll = service.ResolveName(NameToResolve, ResolveNameSearchLocation.DirectoryOnly, true, new PropertySet(BasePropertySet.FirstClassProperties));
If you give a unique enough value in the NameToResolve parameter you should only get back one item in the collection. With that, you can look at the direct reports collection within that one item and see not only the names of their direct reports, but their email addresses as well.
I hope this information helps. If this does resolve your problem, please mark the post as answered.
Thanks,
--- Bob ---

ActiveDirectory error 0x8000500c when traversing properties

I got the following snippet (SomeName/SomeDomain contains real values in my code)
var entry = new DirectoryEntry("LDAP://CN=SomeName,OU=All Groups,dc=SomeDomain,dc=com");
foreach (object property in entry.Properties)
{
Console.WriteLine(property);
}
It prints OK for the first 21 properties, but then fail with:
COMException {"Unknown error (0x8000500c)"}
at System.DirectoryServices.PropertyValueCollection.PopulateList()
at System.DirectoryServices.PropertyValueCollection..ctor(DirectoryEntry entry, String propertyName)
at System.DirectoryServices.PropertyCollection.PropertyEnumerator.get_Entry()
at System.DirectoryServices.PropertyCollection.PropertyEnumerator.get_Current()
at ActiveDirectory.Tests.IntegrationTests.ObjectFactoryTests.TestMethod1() in MyTests.cs:line 22
Why? How can I prevent it?
Update
It's a custom attribute that fails.
I've tried to use entry.RefreshCache() and entry.RefreshCache(new[]{"theAttributeName"}) before enumerating the properties (which didn't help).
Update2
entry.InvokeGet("theAttributeName") works (and without RefreshCache).
Can someone explain why?
Update3
It works if I supply the FQDN to the item: LDAP://srv00014.ssab.com/CN=SomeName,xxxx
Bounty
I'm looking for an answer which addresses the following:
Why entry.Properties["customAttributeName"] fails with the mentioned exception
Why entry.InvokeGet("customAttributeName") works
The cause of the exception
How to get both working
If one wants to access a custom attribute from a machine that is not
part of the domain where the custom attribute resides (the credentials
of the logged in user don't matter) one needs to pass the fully
qualified name of the object is trying to access otherwise the schema
cache on the client machine is not properly refreshed, nevermind all
the schema.refresh() calls you make
Found here. This sounds like your problem, given the updates made to the question.
Using the Err.exe tool here
http://www.microsoft.com/download/en/details.aspx?id=985
It spits out:
for hex 0x8000500c / decimal -2147463156 :
E_ADS_CANT_CONVERT_DATATYPE adserr.h
The directory datatype cannot be converted to/from a native
DS datatype
1 matches found for "0x8000500c"
Googled "The directory datatype cannot be converted to/from a native" and found this KB:
http://support.microsoft.com/kb/907462
I have the same failure. I´m read and saw a lot of questions about the error 0x8000500c by listing attribute from a DirectoryEntry.
I could see, with the Process Monitor (Sysinternals), that my process has read a schema file. This schema file is saved under
C:\Users\xxxx\AppData\Local\Microsoft\Windows\SchCache\xyz.sch.
Remove this file and the program works fine :)
I just encountered the issue and mine was with a web application.
I had this bit of code which pulls the user out of windows authentication in IIS and pulls their info from AD.
using (var context = new PrincipalContext(ContextType.Domain))
{
var name = UserPrincipal.Current.DisplayName;
var principal = UserPrincipal.FindByIdentity(context, this.user.Identity.Name);
if (principal != null)
{
this.fullName = principal.GivenName + " " + principal.Surname;
}
else
{
this.fullName = string.Empty;
}
}
This worked fine in my tests, but when I published the website it would come up with this error on FindByIdentity call.
I fixed the issue by using correct user for the app-pool of the website. As soon as I fixed that, this started working.
I had the same problem with a custom attribute of a weird data type. I had a utility program that would extract the value, but some more structured code in a service that would not.
The utility was working directly with a SearchResult object, while the service was using a DirectoryEntry.
It distilled out to this.
SearchResult result;
result.Properties[customProp]; // might work for you
result.Properties[customProp][0]; // works for me. see below
using (DirectoryEntry entry = result.GetDirectoryEntry())
{
entry.Properties[customProp]; // fails
entry.InvokeGet(customProp); // fails as well for the weird data
}
My gut feel is that the SearchResult is a little less of an enforcer and returns back whatever it has.
When this is converted to a DirectoryEntry, this code munges the weird data type so that even InvokeGet fails.
My actual extraction code with the extra [0] looks like:
byte[] bytes = (byte[])((result.Properties[customProp][0]));
String customValue = System.Text.Encoding.UTF8.GetString(bytes);
I picked up the second line from another posting on the site.

Categories