Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 9 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ _**Note:**_ This is an unofficial Onspring integration. It was not built in cons
- [Activating and Deactivating Users](#activating-and-deactivating-users)
- [Custom Mappings](#custom-mappings)
- [Validating Mappings](#validating-mappings)
- [Group Filters](#group-filters)
- [Group Filter](#group-filter)
- [Options](#options)
- [Output](#output)
- [Log](#log)
Expand All @@ -51,14 +51,14 @@ The OnspringAzureADSyncer app is meant to help fill this gap and provide Onsprin

✅ Allow Azure Active Directory to serve as the system of record for managing users and groups.

✅ Synchronize all users and groups in Azure Active Directory with Onspring.
✅ Synchronize groups and their members in Azure Active Directory with Onspring.

✅ Activate and deactivate Onspring users based on group membership in specific Azure Active Directory groups.

✅ Map Azure Active Directory user properties to Onspring user fields.

✅ Map Azure Active Directory group properties to Onspring group fields.

✅ Map Azure Active Directory user properties to Onspring user fields.

## Requirements

- You must have an Onspring instance.
Expand Down Expand Up @@ -313,9 +313,9 @@ Prior to the app actually attempting to sync groups or users it will validate th
| `DateTimeOffset` | `Text` |
| `DateTimeOffset` | `Date/Time` |

### Group Filters
### Group Filter

The app can be configured to filter the groups that are synced from Azure Active Directory to Onspring by adding the `GroupFilters` property to the `Azure` section of the configuration file. The `GroupFilters` property should be an array of objects that contain a `Property` and `Pattern` property. The `Property` value should be the name of the property on the Azure Active Directory group that you want to filter on and the `Pattern` value should be a regular expression that the value of the property should match. See below for an example:
The app can be configured to filter the groups that are synced from Azure Active Directory to Onspring by adding the `GroupFilter` property to the `Azure` section of the configuration file. The `GroupFilter` property should be a string that is a valid OData filter. It will be passed as the `$filter` query param when retreiving groups from the Microsoft Graph API. You can find more informationa about valid filters [here](https://learn.microsoft.com/en-us/graph/filter-query-parameter?tabs=http)

```json
{
Expand All @@ -324,26 +324,15 @@ The app can be configured to filter the groups that are synced from Azure Active
"TenantId": "00000000-0000-0000-0000-000000000000",
"ClientId": "0a000aa0-1b11-2222-3c33-444444d44d44",
"ClientSecret": "00000~00000--000000000-00000000000000000",
"GroupFilters": [
{
"Property": "displayName",
"Pattern": ".*-Onspring$"
}
]
"GroupFilter": "displayName eq 'My Group'"
},
...
}
```

_**Note:**_ These filters are ANDed together. So a group must match all the filters to be synced to Onspring.

_**Note:**_ The `Property` value is case insensitive, but it should be a property of type `String` on the Azure Active Directory group.

_**Note:**_ The `Pattern` value is a regular expression that will be used to match the value of the property on the Azure Active Directory group. The regular expression should be a valid .NET regular expression pattern.

_**Note:**_ The app will only sync groups from Azure Active Directory to Onspring that match the filters you've set. If you have not set any filters the app will sync all groups from Azure Active Directory to Onspring.
_**Note:**_ The app will only sync groups from Azure Active Directory to Onspring that match the filter you've set. If you have not set a filter the app will sync all groups from Azure Active Directory to Onspring.

_**Note:**_ Prior to running the syncer will validate the group filters to ensure the property is a valid property on the Azure Active Directory group with the type string and that the pattern is a valid regular expression. Note the regular expression uses the .NET regular expression engine. See the [Microsoft .NET Regular Expression Language](https://learn.microsoft.com/en-us/dotnet/standard/base-types/regular-expression-language-quick-reference) for more information.
_**Note:**_ The app will also only sync users from Azure Active Directory to Onspring that are members of groups that match the filter you've set. If you have not set a filter than the app will sync all members of all groups from Azure Active Directory to Onspring.

## Options

Expand Down
71 changes: 31 additions & 40 deletions src/Extensions/HostBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,31 @@ public static class HostBuilderExtensions
public static IHostBuilder AddGraphClient(this IHostBuilder hostBuilder)
{
return hostBuilder
.ConfigureServices(
(context, services) =>
{
var settings = services.BuildServiceProvider().GetRequiredService<ISettings>();
.ConfigureServices(static (context, services) =>
{
var settings = services.BuildServiceProvider().GetRequiredService<ISettings>();

var graphServiceClient = new GraphServiceClient(
new ClientSecretCredential(
settings.Azure.TenantId,
settings.Azure.ClientId,
settings.Azure.ClientSecret,
new TokenCredentialOptions
{
AuthorityHost = AzureAuthorityHosts.AzurePublicCloud
}
),
new[] { "https://graph.microsoft.com/.default" }
);
var graphServiceClient = new GraphServiceClient(
new ClientSecretCredential(
settings.Azure.TenantId,
settings.Azure.ClientId,
settings.Azure.ClientSecret,
new TokenCredentialOptions
{
AuthorityHost = AzureAuthorityHosts.AzurePublicCloud
}
),
["https://graph.microsoft.com/.default"]
);

services.AddSingleton(graphServiceClient);
}
);
services.AddSingleton(graphServiceClient);
});
}

public static IHostBuilder AddOnspringClient(this IHostBuilder hostBuilder)
{
return hostBuilder
.ConfigureServices(
(context, services) =>
.ConfigureServices(static (context, services) =>
{
var settings = services.BuildServiceProvider().GetRequiredService<ISettings>();

Expand All @@ -42,39 +39,33 @@ public static IHostBuilder AddOnspringClient(this IHostBuilder hostBuilder)
);

services.AddSingleton<IOnspringClient>(onspringClient);
}
);
});
}

public static IHostBuilder AddSerilog(this IHostBuilder hostBuilder)
{
return hostBuilder
.UseSerilog(
(context, services, config) =>
static (context, services, config) =>
{
var options = services.GetRequiredService<IOptions<AppOptions>>().Value;
var azureGroupDestructPolicy = services.GetRequiredService<IAzureGroupDestructuringPolicy>();
var azureUserDestructPolicy = services.GetRequiredService<IAzureUserDestructuringPolicy>();

var logPath = Path.Combine(
AppContext.BaseDirectory,
$"{DateTime.Now:yyyy_MM_dd_HHmmss}_output",
"log.json"
);

config
.Destructure.With(
new IDestructuringPolicy[]
{
.Destructure.With([
azureGroupDestructPolicy,
azureUserDestructPolicy
}
)
.MinimumLevel.Debug()
.Enrich.FromLogContext()
.WriteTo.File(
new CompactJsonFormatter(),
Path.Combine(
AppContext.BaseDirectory,
$"{DateTime.Now:yyyy_MM_dd_HHmmss}_output",
"log.json"
),
options.LogLevel
);
])
.MinimumLevel.Debug()
.Enrich.FromLogContext()
.WriteTo.File(new CompactJsonFormatter(), logPath, options.LogLevel);
}
);
}
Expand Down
3 changes: 2 additions & 1 deletion src/Interfaces/IGraphService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ namespace OnspringAzureADSyncer.Interfaces;

public interface IGraphService
{
Task<PageIterator<DirectoryObject, DirectoryObjectCollectionResponse>?> GetGroupMembersIterator(string groupId, List<User> groupMembers, int pageSize);
Task<List<Group>> GetUserGroups(User azureUser);
Task<PageIterator<Group, GroupCollectionResponse>?> GetGroupsIterator(List<Group> groups, int pageSize);
Task<PageIterator<User, UserCollectionResponse>?> GetUsersIterator(List<User> azureUsers, int pageSize);
Task<bool> IsConnected();
Task<bool> CanGetGroups();
Task<(bool IsSuccessful, string ResultMessage)> CanGetGroups(string? groupFilter = null);
Task<bool> CanGetUsers();
}
7 changes: 3 additions & 4 deletions src/Interfaces/IMsGraph.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
using GroupFilter = OnspringAzureADSyncer.Models.GroupFilter;

namespace OnspringAzureADSyncer.Interfaces;

public interface IMsGraph
{
GraphServiceClient GraphServiceClient { get; init; }
Task<DirectoryObjectCollectionResponse?> GetUserGroups(string? userId);
Task<GroupCollectionResponse?> GetGroups();
Task<GroupCollectionResponse?> GetGroupsForIterator(Dictionary<int, string> groupFieldMappings, List<GroupFilter> groupFilters);
Task<GroupCollectionResponse?> GetGroups(string? groupFilter = null);
Task<GroupCollectionResponse?> GetGroupsForIterator(Dictionary<int, string> groupFieldMappings, string? groupFilter = null);
Task<UserCollectionResponse?> GetUsers();
Task<UserCollectionResponse?> GetUsersForIterator(Dictionary<int, string> usersFieldMappings);
Task<DirectoryObjectCollectionResponse?> GetGroupMembersForIterator(string groupId, Dictionary<int, string> usersFieldMappings);
}
7 changes: 5 additions & 2 deletions src/Interfaces/IProcessor.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
using Group = Microsoft.Graph.Models.Group;

namespace OnspringAzureADSyncer.Interfaces;

public interface IProcessor
{
Task SyncGroupMembers(Group azureGroup);
Task SyncUsers();
Task SyncGroups();
Task<List<Group>> SyncGroups();
bool FieldMappingsAreValid();
Task GetOnspringUserFields();
Task GetOnspringGroupFields();
void SetDefaultGroupsFieldMappings();
void SetDefaultUsersFieldMappings();
Task<bool> VerifyConnections();
bool HasValidGroupFilters();
Task<(bool IsSuccessful, string ResultMessage)> HasValidGroupFilter();
}
11 changes: 1 addition & 10 deletions src/Models/AzureGroupDestructuringPolicy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,10 @@ public bool TryDestructure(
.Values
.ToList();

var groupFilterProperties = _settings
.Azure
.GroupFilters
.Select(f => f.Property)
.ToList();

List<string> groupProperties = [.. groupFilterProperties, .. mappedGroupProperties];
var distinctGroupProperties = groupProperties.Distinct();

var props = value
.GetType()
.GetProperties(BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.Public)
.Where(p => distinctGroupProperties.Contains(p.Name, StringComparer.OrdinalIgnoreCase));
.Where(p => mappedGroupProperties.Contains(p.Name, StringComparer.OrdinalIgnoreCase));

var logEventProperties = new List<LogEventProperty>();

Expand Down
4 changes: 2 additions & 2 deletions src/Models/AzureSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,5 @@ public class AzureSettings
public string[] OnspringActiveGroups { get; init; } = [];
public PropertyInfo[] UsersProperties { get; } = typeof(User).GetProperties();
public PropertyInfo[] GroupsProperties { get; } = typeof(Group).GetProperties();
public GroupFilter[] GroupFilters { get; init; } = [];
}
public string GroupFilter { get; init; } = string.Empty;
}
66 changes: 0 additions & 66 deletions src/Models/GroupFilter.cs

This file was deleted.

Loading