Remove MS365 external user from access review

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
Bryan Frimin
2026-05-07 17:15:11 +02:00
parent e73aa469a3
commit 4336b8eb48
2 changed files with 34 additions and 12 deletions

View File

@@ -4,6 +4,10 @@ All notable changes to `probod` (the server, including the bundled `@probo/conso
## Unreleased ## Unreleased
### Changed
- Microsoft 365 access review driver now fetches only internal members from Microsoft Graph (`$filter=userType eq 'Member'`), so guest (B2B) accounts are no longer pulled into access review
## [0.184.0] - 2026-05-07 ## [0.184.0] - 2026-05-07
### Added ### Added

View File

@@ -20,6 +20,8 @@ import (
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"net/url"
"strconv"
"time" "time"
"go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/coredata"
@@ -35,10 +37,13 @@ type Microsoft365Driver struct {
var _ Driver = (*Microsoft365Driver)(nil) var _ Driver = (*Microsoft365Driver)(nil)
const ( const (
microsoft365GraphBaseURL = "https://graph.microsoft.com/v1.0" microsoft365GraphBaseURL = "https://graph.microsoft.com/v1.0"
microsoft365UsersSelect = "id,userPrincipalName,mail,displayName,givenName,surname,accountEnabled,jobTitle,department,createdDateTime" microsoft365UsersSelect = "id,userPrincipalName,mail,displayName,givenName,surname,accountEnabled,jobTitle,department,createdDateTime"
microsoft365UsersPageSize = 999 // microsoft365UserTypeMemberFilter restricts /users to internal members
microsoft365MaxPaginationOK = maxPaginationPages // so guest (B2B) accounts are not pulled into access review.
microsoft365UserTypeMemberFilter = "userType eq 'Member'"
microsoft365UsersPageSize = 999
microsoft365MaxPaginationOK = maxPaginationPages
) )
// adminRoleDisplayNames lists the directory role display names that the // adminRoleDisplayNames lists the directory role display names that the
@@ -213,29 +218,42 @@ func pickHighestRole(roles []string) string {
} }
func (d *Microsoft365Driver) listUsers(ctx context.Context) ([]microsoft365User, error) { func (d *Microsoft365Driver) listUsers(ctx context.Context) ([]microsoft365User, error) {
url := fmt.Sprintf( pageURL, err := buildMicrosoft365UsersURL()
"%s/users?$select=%s&$top=%d", if err != nil {
microsoft365GraphBaseURL, return nil, err
microsoft365UsersSelect, }
microsoft365UsersPageSize,
)
var all []microsoft365User var all []microsoft365User
for range microsoft365MaxPaginationOK { for range microsoft365MaxPaginationOK {
var page microsoft365UsersPage var page microsoft365UsersPage
if err := d.fetchJSON(ctx, url, &page); err != nil { if err := d.fetchJSON(ctx, pageURL, &page); err != nil {
return nil, err return nil, err
} }
all = append(all, page.Value...) all = append(all, page.Value...)
if page.NextLink == "" { if page.NextLink == "" {
return all, nil return all, nil
} }
url = page.NextLink pageURL = page.NextLink
} }
return nil, fmt.Errorf("cannot list all microsoft 365 users: %w", ErrPaginationLimitReached) return nil, fmt.Errorf("cannot list all microsoft 365 users: %w", ErrPaginationLimitReached)
} }
func buildMicrosoft365UsersURL() (string, error) {
u, err := url.Parse(microsoft365GraphBaseURL + "/users")
if err != nil {
return "", fmt.Errorf("cannot parse graph users URL: %w", err)
}
q := u.Query()
q.Set("$select", microsoft365UsersSelect)
q.Set("$filter", microsoft365UserTypeMemberFilter)
q.Set("$top", strconv.Itoa(microsoft365UsersPageSize))
u.RawQuery = q.Encode()
return u.String(), nil
}
func (d *Microsoft365Driver) listDirectoryRoles(ctx context.Context) ([]microsoft365DirectoryRole, error) { func (d *Microsoft365Driver) listDirectoryRoles(ctx context.Context) ([]microsoft365DirectoryRole, error) {
url := fmt.Sprintf("%s/directoryRoles", microsoft365GraphBaseURL) url := fmt.Sprintf("%s/directoryRoles", microsoft365GraphBaseURL)