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
106 changes: 106 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json

name: publish
on:
workflow_dispatch: # Allow running the workflow manually from the GitHub UI
push:
branches:
- 'main' # Run the workflow when pushing to the main branch
pull_request:
branches:
- '*' # Run the workflow for all pull requests
release:
types:
- published # Run the workflow when a new GitHub release is published

env:
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1
DOTNET_NOLOGO: true
NuGetDirectory: ${{ github.workspace}}/nuget

defaults:
run:
shell: pwsh

jobs:
create_nuget:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0 # Get all history to allow automatic versioning using MinVer

# Install the .NET SDK indicated in the global.json file
- name: Setup .NET
uses: actions/setup-dotnet@v4

# Create the NuGet package in the folder from the environment variable NuGetDirectory
- run: dotnet pack RoleMining.Library/RoleMining.Library.csproj --configuration Release --output ${{ env.NuGetDirectory }}

# Publish the NuGet package as an artifact, so they can be used in the following jobs
- uses: actions/upload-artifact@v3
with:
name: nuget
if-no-files-found: error
retention-days: 7
path: ${{ env.NuGetDirectory }}/*.nupkg

validate_nuget:
runs-on: ubuntu-latest
needs: [ create_nuget ]
steps:
# Install the .NET SDK indicated in the global.json file
- name: Setup .NET
uses: actions/setup-dotnet@v4

# Download the NuGet package created in the previous job
- uses: actions/download-artifact@v3
with:
name: nuget
path: ${{ env.NuGetDirectory }}

- name: Install nuget validator
run: dotnet tool update Meziantou.Framework.NuGetPackageValidation.Tool --global

# Validate metadata and content of the NuGet package
# https://www.nuget.org/packages/Meziantou.Framework.NuGetPackageValidation.Tool#readme-body-tab
# If some rules are not applicable, you can disable them
# using the --excluded-rules or --excluded-rule-ids option
- name: Validate package
run: meziantou.validate-nuget-package (Get-ChildItem "${{ env.NuGetDirectory }}/*.nupkg")

run_test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup .NET
uses: actions/setup-dotnet@v4
- name: Run tests
run: dotnet test --configuration Release

deploy:
# Publish only when creating a GitHub Release
# https://docs.github.com/en/repositories/releasing-projects-on-github/managing-releases-in-a-repository
# You can update this logic if you want to manage releases differently
if: github.event_name == 'release'
runs-on: ubuntu-latest
needs: [ validate_nuget, run_test ]
steps:
# Download the NuGet package created in the previous job
- uses: actions/download-artifact@v3
with:
name: nuget
path: ${{ env.NuGetDirectory }}

# Install the .NET SDK indicated in the global.json file
- name: Setup .NET Core
uses: actions/setup-dotnet@v4

# Publish all NuGet packages to NuGet.org
# Use --skip-duplicate to prevent errors if a package with the same version already exists.
# If you retry a failed workflow, already published packages will be skipped without error.
- name: Publish NuGet package
run: |
foreach($file in (Get-ChildItem "${{ env.NuGetDirectory }}" -Recurse -Include *.nupkg)) {
dotnet nuget push $file --api-key "${{ secrets.NUGET_APIKEY }}" --source https://api.nuget.org/v3/index.json --skip-duplicate
}
19 changes: 0 additions & 19 deletions RoleMining.Library/ExtraService.cs

This file was deleted.

23 changes: 23 additions & 0 deletions RoleMining.Library/InputValidator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;

namespace RoleMining.Library
{
internal class InputValidator
{
public static void CheckIfEmpty(IEnumerable<object> input, string paramName)
{
if (input == null)
{
throw new ArgumentNullException(paramName);
}

if (input is IEnumerable enumerable && !enumerable.GetEnumerator().MoveNext())
{
throw new ArgumentException("Input cannot be an empty collection.", paramName);
}
}
}
}
9 changes: 9 additions & 0 deletions RoleMining.Library/LICENSE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
MIT License

Copyright (c) 2024 IdentityStream AS

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
30 changes: 26 additions & 4 deletions RoleMining.Library/RatioAccessLevel.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,33 @@
namespace RoleMining
namespace RoleMining.Library
{
/// <summary>
/// Object used to store the ratio of extra accesses by each role compared to how many there are in that role.
///
/// The type in the list returned from <seealso cref="RoleMining.MineRoles">RoleMining.MineRoles</seealso> method
/// </summary>
public class RatioAccessLevel
{
public string Role { get; set; }
public string Access { get; set; }
/// <summary>
/// The role ID, unique identifier for each role
/// </summary>
public string RoleID { get; set; }
/// <summary>
/// The access level, something that is unique to each access and access level.
/// <br/><br/>
/// Example: access: "Github", access level: "Admin". The access parameter would then be "Github - Admin"
/// </summary>
public string AccessID { get; set; }
/// <summary>
/// All users with specific role and extra access divided by the count of users with that role
/// </summary>
public double Ratio { get; set; }
public int UsersWithAccessAsExtra { get; set; }
/// <summary>
/// How many users have the specific access level as extra access, whilst also having the specific role
/// </summary>
public int UsersWithAccessAsExtra { get; set; } // Maybe return the names of the people with this as extra, maybe in a new class? Who doesnt have the access?
/// <summary>
/// Total amount of users with the specific role
/// </summary>
public int TotalUsers { get; set; }
}
}
29 changes: 29 additions & 0 deletions RoleMining.Library/RecommendedNewRole.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Text;

namespace RoleMining.Library
{
/// <summary>
/// Output for a future access-to-be-added to an existing role
/// </summary>
public class RecommendedNewAccessInRole
{
/// <summary>
/// The role ID, unique identifier for each role
/// </summary>
public string RoleID { get; set; }
/// <summary>
/// The access level, something that is unique to each access and access level. Example: access: "Github", access level: "Admin". The access parameter would then be "Github - Admin"
/// </summary>
public string AccessID { get; set; }
/// <summary>
/// The amount of users in that role
/// </summary>
public int TotalUsers { get; set; }
/// <summary>
/// The amount of users that have the access level as extra access compared to how many users there are in the role
/// </summary>
public double Ratio { get; set; }
}
}
34 changes: 28 additions & 6 deletions RoleMining.Library/RoleMining.Library.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<Title>RoleMiner.Library</Title>
<PackageTags>IAM;Role</PackageTags>
<PackageReleaseNotes>First release of the RoleMining library.
Expand All @@ -12,16 +11,39 @@ Includes the method MineRoles and some classes</PackageReleaseNotes>
<Authors>Dany Gonzalez-Teigland</Authors>
<Company>IdentityStream</Company>
<Copyright>Copyright (c) 2024 IdentityStream AS</Copyright>
<PackageProjectUrl>https://github.com/IdentityStream/RoleMining/tree/main</PackageProjectUrl>
<PackageReadmeFile>ReadMe.md</PackageReadmeFile>
<RepositoryUrl>https://github.com/IdentityStream/RoleMining/tree/main</RepositoryUrl>
<PackageProjectUrl>https://github.com/IdentityStream/RoleMining</PackageProjectUrl>
<PackageReadmeFile>README.md</PackageReadmeFile>
<RepositoryUrl>https://github.com/IdentityStream/RoleMining</RepositoryUrl>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<EnablePackageValidation>true</EnablePackageValidation>
<PackageIcon>icon.png</PackageIcon>
<PackageLicenseFile>LICENSE.md</PackageLicenseFile>
</PropertyGroup>

<ItemGroup>
<None Update="ReadMe.md">
<None Include="README.md" Pack="true" PackagePath="" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="DotNet.ReproducibleBuilds" Version="1.2.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="MinVer" Version="5.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<None Update="icon.png">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
</ItemGroup>
<None Update="LICENSE.md">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
</ItemGroup>

</Project>
55 changes: 31 additions & 24 deletions RoleMining.Library/RoleMining.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,25 @@
namespace RoleMining.Library
{
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;


/// <summary>
/// Main class for RoleMining.Library. Contains the method to mine roles.
/// </summary>
public class RoleMining
{
/// <summary>
/// Mines roles based on user accesses and users in roles
/// </summary>
/// <param name="userAccesses">A IEnumerable of <seealso cref="UserAccess">UserAccess</seealso> objects</param>
/// <param name="userInRoles"></param>
/// <returns></returns>
public static List<RatioAccessLevel> MineRoles(IEnumerable<UserAccess> userAccesses, IEnumerable<UserInRole> userInRoles)
{
if (userAccesses == null)
{
throw new ArgumentNullException("userAccesses");
}
if (userInRoles == null) {
throw new ArgumentNullException("userInRoles");
}
if (!userAccesses.Any())
{
throw new ArgumentException("User accesses cannot be empty", nameof(userAccesses));
}
if (!userInRoles.Any())
{
throw new ArgumentException("User in roles cannot be empty", nameof(userInRoles));
}
InputValidator.CheckIfEmpty(userAccesses, nameof(userAccesses));
InputValidator.CheckIfEmpty(userInRoles, nameof(userInRoles));

// Input testDataAccess and testDataUserInRole from RoleMining.Console/Program.cs
// Return a RatioAccessLevel object
Expand All @@ -46,11 +42,11 @@ public static List<RatioAccessLevel> MineRoles(IEnumerable<UserAccess> userAcces


// Convert to dictionaries
var userAccessDict = distinctUserAccesses.ToDictionary(uA => $"{uA.UserID} - {uA.RoleID} - {uA.AccessLevelID}"); // Create a dictionary of user accesses
var userAccessDict = distinctUserAccesses.ToDictionary(uA => $"{uA.UserID} - {uA.RoleID} - {uA.AccessID}"); // Create a dictionary of user accesses
var userInRoleDict = distinctUserInRoles.ToDictionary(uIR => $"{uIR.UserID} - {uIR.RoleID}"); // Create a dictionary of user in roles

var specialAccesses = userAccessDict.Where(uA => uA.Value.IsExtraAccess); // Finding all extra accesses
var distinctSpecialAccesses = ReturnDistinct(specialAccesses, uA => $"{uA.RoleID} - {uA.AccessLevelID}"); // Finding distinct extra accesses (Hopefully)
var distinctSpecialAccesses = ReturnDistinct(specialAccesses, uA => $"{uA.RoleID} - {uA.AccessID}"); // Finding distinct extra accesses (Hopefully)
// DistinctSpecialAccesses does only check for distinct accesses, not accesses per role. This means that if you have two different roles with the
// same accesses, the last one will dissappear
// Hopefully this will not turn into another bug, trying out with adding the RoleID to the selector
Expand All @@ -59,23 +55,23 @@ public static List<RatioAccessLevel> MineRoles(IEnumerable<UserAccess> userAcces
var roleSummaries = userInRoleDict.GroupBy(uIR => uIR.Value.RoleID)
.Select(group => new UserRoleSummary
{
Role = group.Key,
RoleID = group.Key,
TotalUsers = group.Count()
}).ToList();


// Create the ratio per access level for all distinct special accesses
var ratioList = distinctSpecialAccesses.Select(distinctSpecialAccess =>
{
var usersWithAccessAsExtra = specialAccesses.Count(uA => uA.Value.AccessLevelID == distinctSpecialAccess.Value.AccessLevelID && uA.Value.RoleID == distinctSpecialAccess.Value.RoleID);
var usersWithAccessAsExtra = specialAccesses.Count(uA => uA.Value.AccessID == distinctSpecialAccess.Value.AccessID && uA.Value.RoleID == distinctSpecialAccess.Value.RoleID);
var typeRole = distinctSpecialAccess.Value.RoleID;

var roleSummary = roleSummaries.FirstOrDefault(role => role.Role == $"{distinctSpecialAccess.Value.RoleID}");
var roleSummary = roleSummaries.FirstOrDefault(role => role.RoleID == $"{distinctSpecialAccess.Value.RoleID}");

return new RatioAccessLevel
{
Role = typeRole,
Access = distinctSpecialAccess.Value.AccessLevelID,
RoleID = typeRole,
AccessID = distinctSpecialAccess.Value.AccessID,
Ratio = roleSummary != null ? (double)usersWithAccessAsExtra / roleSummary.TotalUsers : 1,
UsersWithAccessAsExtra = usersWithAccessAsExtra,
TotalUsers = roleSummary != null ? roleSummary.TotalUsers : 1
Expand All @@ -86,8 +82,19 @@ public static List<RatioAccessLevel> MineRoles(IEnumerable<UserAccess> userAcces
}


// Function using Jaccard index to compare users with extra access to users in role





public static IEnumerable<KeyValuePair<string, UserAccess>> ReturnDistinct(
/// <summary>
/// Helper function to return distinct values from a dictionary containing <seealso cref="UserAccess"></seealso> objects using specific selector.
/// </summary>
/// <param name="source">Dictionary with <seealso cref="UserAccess">UserAccess</seealso> objects.</param>
/// <param name="selector">The parameter that should be distinct. This is the key in this instance.</param>
/// <returns></returns>
private static IEnumerable<KeyValuePair<string, UserAccess>> ReturnDistinct( // This can be done using LINQ functions; .groupby and .select(g => g.first)
IEnumerable<KeyValuePair<string, UserAccess>> source,
Func<UserAccess, string> selector)
{
Expand Down
Loading