From d4ade9a88b5ca823cb853600a561aac33a5fbabd Mon Sep 17 00:00:00 2001 From: Dirk-jan Mollema Date: Tue, 1 Apr 2025 15:51:02 +0200 Subject: [PATCH] roadrecon: add group members in roles overview and add links to entra portal on users and groups --- .../app/appmain/config/config.component.html | 4 +++ .../app/appmain/config/config.component.ts | 2 ++ .../groupsdialog/groupsdialog.component.html | 1 + .../groupsdialog/groupsdialog.component.ts | 32 ++++++++++++------- .../usersdialog/usersdialog.component.html | 1 + .../usersdialog/usersdialog.component.ts | 24 ++++++++++---- roadrecon/roadtools/roadrecon/server.py | 32 +++++++++++++++++-- 7 files changed, 77 insertions(+), 19 deletions(-) diff --git a/roadrecon/frontend/src/app/appmain/config/config.component.html b/roadrecon/frontend/src/app/appmain/config/config.component.html index e87f32a..c630b7b 100644 --- a/roadrecon/frontend/src/app/appmain/config/config.component.html +++ b/roadrecon/frontend/src/app/appmain/config/config.component.html @@ -5,5 +5,9 @@ Show MFA columns (only accurate if you collected this data) +
+ + Show management links to Entra portal on objects + diff --git a/roadrecon/frontend/src/app/appmain/config/config.component.ts b/roadrecon/frontend/src/app/appmain/config/config.component.ts index a62dd6f..a42b959 100644 --- a/roadrecon/frontend/src/app/appmain/config/config.component.ts +++ b/roadrecon/frontend/src/app/appmain/config/config.component.ts @@ -9,6 +9,8 @@ import { LocalStorage } from 'ngx-webstorage'; export class ConfigComponent implements OnInit { @LocalStorage() public mfa; + @LocalStorage() + public portallinks; constructor() { } diff --git a/roadrecon/frontend/src/app/appmain/groups/groupsdialog/groupsdialog.component.html b/roadrecon/frontend/src/app/appmain/groups/groupsdialog/groupsdialog.component.html index fca181e..6dd4ab2 100644 --- a/roadrecon/frontend/src/app/appmain/groups/groupsdialog/groupsdialog.component.html +++ b/roadrecon/frontend/src/app/appmain/groups/groupsdialog/groupsdialog.component.html @@ -13,6 +13,7 @@ Can be assigned to roles{{ group.isAssignableToRole? 'Yes':'No' }} Created{{ group.createdDateTime }} Group source{{ group.dirSyncEnabled? 'Synced with AD':'Cloud-only' }} + Manage in Entra portalopen_in_new diff --git a/roadrecon/frontend/src/app/appmain/groups/groupsdialog/groupsdialog.component.ts b/roadrecon/frontend/src/app/appmain/groups/groupsdialog/groupsdialog.component.ts index fadcbc7..cd7d037 100644 --- a/roadrecon/frontend/src/app/appmain/groups/groupsdialog/groupsdialog.component.ts +++ b/roadrecon/frontend/src/app/appmain/groups/groupsdialog/groupsdialog.component.ts @@ -1,41 +1,46 @@ import { Component, OnInit, Inject, ViewChild } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; -import { GroupsItem } from '../../aadobjects.service' +import { GroupsItem } from '../../aadobjects.service'; import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { MatSort } from '@angular/material/sort'; import { Location } from '@angular/common'; +import { LocalStorageService } from 'ngx-webstorage'; + @Component({ template: '' }) export class GroupsdialogInitComponent implements OnInit { user: GroupsItem; myurl: string; + showPortalLink: boolean; + constructor( private route: ActivatedRoute, private router: Router, public dialog: MatDialog, - private location: Location + private location: Location, + private localSt: LocalStorageService ) { this.myurl = this.router.url; + this.showPortalLink = this.localSt.retrieve('portallinks'); } ngOnInit() { this.route.data .subscribe((data: { user: GroupsItem }) => { const dialogRef = this.dialog.open(GroupsdialogComponent, { - data: data.user + data: { + group: data.user, + showPortalLink: this.showPortalLink + } }); dialogRef.afterClosed().subscribe(result => { - console.log(this.router.url); - console.log(this.myurl); if(this.router.url == this.myurl){ this.location.back(); } - }); }); } - } @Component({ @@ -49,11 +54,16 @@ export class GroupsdialogComponent { public displayedColumnsServicePrincipal: string[] = ['displayName', 'servicePrincipalType'] public displayedColumnsOwners: string[] = ['displayName', 'userPrincipalName'] public displayedColumnsDevices: string[] = ['displayName', 'deviceModel', 'deviceOSType', 'deviceTrustType']; + public showPortalLink: boolean; + public group: GroupsItem; + + @ViewChild(MatSort, { static: true }) sort: MatSort; - @ViewChild(MatSort, {static: true}) sort: MatSort; constructor( public dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public group: GroupsItem - ) { } - + @Inject(MAT_DIALOG_DATA) public data: { group: GroupsItem, showPortalLink: boolean } + ) { + this.group = data.group; + this.showPortalLink = data.showPortalLink; + } } diff --git a/roadrecon/frontend/src/app/appmain/users/usersdialog/usersdialog.component.html b/roadrecon/frontend/src/app/appmain/users/usersdialog/usersdialog.component.html index 2216a98..d00b09e 100644 --- a/roadrecon/frontend/src/app/appmain/users/usersdialog/usersdialog.component.html +++ b/roadrecon/frontend/src/app/appmain/users/usersdialog/usersdialog.component.html @@ -15,6 +15,7 @@ Account source{{ user.dirSyncEnabled? 'Synced with AD':'Cloud-only' }} Account type{{ user.userType }} Status{{ user.accountEnabled? 'Enabled':'Disabled' }} + Manage in Entra portalopen_in_new diff --git a/roadrecon/frontend/src/app/appmain/users/usersdialog/usersdialog.component.ts b/roadrecon/frontend/src/app/appmain/users/usersdialog/usersdialog.component.ts index 0fe4162..be7254b 100644 --- a/roadrecon/frontend/src/app/appmain/users/usersdialog/usersdialog.component.ts +++ b/roadrecon/frontend/src/app/appmain/users/usersdialog/usersdialog.component.ts @@ -4,26 +4,34 @@ import { UsersItem } from '../../aadobjects.service' import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { MatSort } from '@angular/material/sort'; import { Location } from '@angular/common'; +import { LocalStorageService } from 'ngx-webstorage'; + @Component({ template: '' }) export class UsersdialogInitComponent implements OnInit { user: UsersItem; myurl: string; + showPortalLink: boolean; constructor( private route: ActivatedRoute, private router: Router, public dialog: MatDialog, - private location: Location + private location: Location, + private localSt:LocalStorageService ) { this.myurl = this.router.url; + this.showPortalLink = this.localSt.retrieve('portallinks'); } ngOnInit() { this.route.data .subscribe((data: { user: UsersItem }) => { - const dialogRef = this.dialog.open(UsersdialogComponent, { - data: data.user + const dialogRef = this.dialog.open(UsersdialogComponent,{ + data: { + user: data.user, + showPortalLink: this.showPortalLink + } }); dialogRef.afterClosed().subscribe(result => { if(this.router.url == this.myurl){ @@ -46,11 +54,15 @@ export class UsersdialogComponent { public displayedColumnsServicePrincipals: string[] = ['displayName', 'publisherName', 'microsoftFirstParty', 'passwordCredentials', 'keyCredentials', 'appRoles', 'oauth2Permissions']; public displayedColumnsDevices: string[] = ['displayName', 'deviceManufacturer', 'accountEnabled', 'deviceModel', 'deviceOSType', 'deviceOSVersion', 'deviceTrustType', 'isCompliant', 'isManaged', 'isRooted']; public displayedColumnsApplications: string[] = ['displayName', 'passwordCredentials', 'keyCredentials', 'appRoles', 'oauth2Permissions']; - + public showPortalLink: boolean; + public user: UsersItem; @ViewChild(MatSort, {static: true}) sort: MatSort; constructor( public dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public user: UsersItem - ) { } + @Inject(MAT_DIALOG_DATA) public data: {user: UsersItem, showPortalLink: boolean} + ) { + this.user = data.user; + this.showPortalLink = data.showPortalLink; + } } diff --git a/roadrecon/roadtools/roadrecon/server.py b/roadrecon/roadtools/roadrecon/server.py index 180af81..1b0c6ff 100644 --- a/roadrecon/roadtools/roadrecon/server.py +++ b/roadrecon/roadtools/roadrecon/server.py @@ -507,9 +507,24 @@ def get_allroles(): 'scopeNames': snames, 'scopeIds': sids } - _, principal = resolve_objectid(assignment.principalId) + principalType, principal = resolve_objectid(assignment.principalId) aobj['principal'] = principal + roleobj['assignments'].append(aobj) + if principalType == 'Group': + group = db.session.get(Group, assignment.principalId) + for member in group.memberUsers: + mp = users_schema.dump([member])[0] + mp['displayName'] = f"{principal['displayName']} member: {mp['displayName']}" + roleobj['assignments'].append({ + 'type': 'assignment', + 'scope': assignment.resourceScopes, + 'scopeTypes': stypes, + 'scopeNames': snames, + 'scopeIds': sids, + 'principal': mp + }) + for assignment in role.eligibleAssignments: stypes, snames, sids = translate_rolescopes(assignment.resourceScopes) aobj = { @@ -519,9 +534,22 @@ def get_allroles(): 'scopeNames': snames, 'scopeIds': sids } - _, principal = resolve_objectid(assignment.principalId) + principalType, principal = resolve_objectid(assignment.principalId) aobj['principal'] = principal roleobj['assignments'].append(aobj) + if principalType == 'Group': + group = db.session.get(Group, assignment.principalId) + for member in group.memberUsers: + mp = users_schema.dump([member])[0] + mp['displayName'] = f"{principal['displayName']} member: {mp['displayName']}" + roleobj['assignments'].append({ + 'type': 'eligible', + 'scope': assignment.resourceScopes, + 'scopeTypes': stypes, + 'scopeNames': snames, + 'scopeIds': sids, + 'principal': mp + }) allroles.append(roleobj) return jsonify(allroles)