Documentation
¶
Index ¶
- Constants
- Variables
- func ConstructEndpoint(request *Request) (string, *framework.Error)
- func ExtractImplicitFilters(advancedFilters AdvancedFilters) map[string][]EntityFilter
- func IsAdvancedQuery(request *Request, endpoint string) bool
- func MarshalAdvancedFilterCursor(cursor *AdvancedFilterCursor) (string, *framework.Error)
- func NewAdapter(client Client) framework.Adapter[Config]
- func ParseResponse(body []byte) (objects []map[string]any, nextLink *string, err *framework.Error)
- type Adapter
- func (a *Adapter) GetPage(ctx context.Context, request *framework.Request[Config]) framework.Response
- func (a *Adapter) RequestPageFromDatasource(ctx context.Context, request *framework.Request[Config]) framework.Response
- func (a *Adapter) ValidateGetPageRequest(ctx context.Context, request *framework.Request[Config]) *framework.Error
- type AdvancedFilterCursor
- type AdvancedFilters
- type Client
- type Config
- type Datasource
- type DatasourceResponse
- type EntityFilter
- type EntityInfo
- type MemberFilter
- type Request
- type Response
Constants ¶
const ( User string = "User" Group string = "Group" GroupMember string = "GroupMember" Application string = "Application" Device string = "Device" Role string = "Role" RoleMember string = "RoleMember" RoleAssignment string = "RoleAssignment" // Use a combination of $top and $skip to paginate the response for these two PIM entities. RoleAssignmentScheduleRequest string = "RoleAssignmentScheduleRequest" GroupAssignmentScheduleRequest string = "GroupAssignmentScheduleRequest" )
Variables ¶
var ( // ValidEntityExternalIDs is a set of valid external IDs of entities that can be queried. ValidEntityExternalIDs = map[string]EntityInfo{ User: {}, Group: {}, GroupMember: { // contains filtered or unexported fields }, Application: {}, Device: {}, Role: {}, RoleMember: { // contains filtered or unexported fields }, RoleAssignment: {}, RoleAssignmentScheduleRequest: {}, GroupAssignmentScheduleRequest: {}, } )
Functions ¶
func ConstructEndpoint ¶
ConstructEndpoint constructs and returns the endpoint to query the datasource.
func ExtractImplicitFilters ¶
func ExtractImplicitFilters(advancedFilters AdvancedFilters) map[string][]EntityFilter
ExtractImplicitFilters extracts implicit User/Group filters from the GroupMember's advanced filters. A GroupMember advanced filter may look like:
"GroupMember": [
{
"scopeEntity": "Group",
"scopeEntityFilter": "id in ('aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee')", // Canada Group
"members": [
{
"memberEntity": "User",
"memberEntityFilter": "department eq 'engineering'"
}
]
},
{
"scopeEntity": "Group",
"scopeEntityFilter": "id in ('aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee')", // United States Group
"members": [
{
"memberEntity": "User",
"memberEntityFilter": "department eq 'engineering'"
},
{
"memberEntity": "User",
"memberEntityFilter": "department eq 'product'"
},
{
"memberEntity": "Group",
"memberEntityFilter": "startswith(displayName, 'California')"
}
]
},
] The above filter means: 1) Sync all GroupMember user nodes from the Canada group where the department is engineering. 2) Sync all GroupMember user nodes from the United States group where the department is engineering or product. 3) Sync all GroupMember group nodes from the United States group where the group name starts with California. This implies we should only sync: 1) User nodes from the Canada group where the department is engineering. 2) User nodes from the United States group where the department is engineering or product. 3) Group nodes from the United States group where the group name starts with California. 4) The Canada and United States groups themselves. Thus, this function will extract the following implicit filters (only User filters are shown for brevity) "User": [
{
"scopeEntity": "Group",
"scopeEntityFilter": "id in ('aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee')", // Canada Group
"members": [
{
"memberEntity": "User",
"memberEntityFilter": "department eq 'engineering'"
}
]
},
{
"scopeEntity": "Group",
"scopeEntityFilter": "id in ('aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee')", // United States Group
"members": [
{
"memberEntity": "User",
"memberEntityFilter": "department eq 'engineering'"
},
{
"memberEntity": "User",
"memberEntityFilter": "department eq 'product'"
},
]
},
].
func IsAdvancedQuery ¶ added in v1.53.1
nolint: lll IsAdvancedQuery determines if the request requires advanced query capabilities based on Microsoft Graph advanced query documentation. If any of the conditions are met, it returns true and a header "ConsistencyLevel: eventual" should be added to the request. See: https://learn.microsoft.com/en-us/graph/aad-advanced-queries?tabs=http#microsoft-entra-id-directory-objects-that-support-advanced-query-capabilities for more information. The ODATA logical operator NOT also needs the ConsistencyLevel header. See: https://learn.microsoft.com/en-us/graph/api/group-list?view=graph-rest-1.0&tabs=http The function checks for the following conditions: 1. If the request already has UseAdvancedFilters set to true, it returns true. 2. If the endpoint contains $count, $search, or $orderby query parameters. 3. If the endpoint contains $filter with advanced operators like endsWith, contains, startsWith. 4. If the endpoint contains $filter with 'ne' or 'not' operators as whole words. 5. If the endpoint contains $filter with 'NOT' operator as a whole word.
func MarshalAdvancedFilterCursor ¶
func MarshalAdvancedFilterCursor(cursor *AdvancedFilterCursor) (string, *framework.Error)
MarshalAdvancedFilterCursor marshals the struct and b64 encodes it.
func NewAdapter ¶
NewAdapter instantiates a new Adapter.
Types ¶
type Adapter ¶
type Adapter struct {
AzureADClient Client
}
Adapter implements the framework.Adapter interface to query pages of objects from datasources.
func (*Adapter) GetPage ¶
func (a *Adapter) GetPage(ctx context.Context, request *framework.Request[Config]) framework.Response
GetPage is called by SGNL's ingestion service to query a page of objects from a datasource.
type AdvancedFilterCursor ¶
type AdvancedFilterCursor struct {
EntityFilterIndex int `json:"entityFilterIndex"`
MemberFilterIndex int `json:"memberFilterIndex"`
Cursor *pagination.CompositeCursor[string] `json:"cursor"`
}
AdvancedFilterCursor is a simple wrapper around the existing composite cursor. The existing composite cursor helps paginate through G groups and fetch U users.
When using advanced filters for an entityExternalID (e.g. `groupMembers“),
- Iterate each scopedEntity (i-th index) (e.g. a `group`) and get its filter, a scopeFilter
- For that scopedEntity, iterate each memberEntity (j-th index) (e.g. `users`), and get its filter, a memberFilter
- For every (scopedEntity, memberEntity) pair, 3.1 the scopeFilter would filter down the /groups response to a subset of G groups 3.2 the memberFilter would filter down the /members response to a subset of U users for each group 3.3 Call GetPage with these filters 3.3 The pagination within GetPage is captured by the existing composite cursor 3.4 If composite cursor is not nil, it means there are more pages for the current (scopedEntity, memberEntity) pair. 3.5 Move to the next (scopedEntity, memberEntity) pair if the composite cursor is nil.
- Continue calling GetPage until all (scopedEntity, memberEntity) pairs are processed
This is how the advanced filter cursor wraps the existing composite cursor.
func UnmarshalAdvancedFilterCursor ¶
func UnmarshalAdvancedFilterCursor(cursor string) (*AdvancedFilterCursor, *framework.Error)
UnmarshalAdvancedFilterCursor decodes the b64 encoded string and unmarshals it.
type AdvancedFilters ¶
type AdvancedFilters struct {
// ScopedObjects is a map of entityExternalID to a list of EntityFilter configurations.
ScopedObjects map[string][]EntityFilter `json:"getObjectsByScope,omitempty"`
}
nolint: godot,lll Example of AdvancedFilters:
"advancedFilters": {
"getObjectsByScope": {
// The externalId is a key
// The value is a list of filter configurations
"GroupMember": [
{
// To obtain a subset of groupMembers, we scope the entity `Group` and apply a scope-filter.
// Observe that scopeEntityFilter is extensible because
// - native AAD filter syntax and not limited to uniqueIds
// - allows applying same user-filter/group-filter to several groups matching the scope-filter
"scopeEntity": "Group",
"scopeEntityFilter": "id in ('7df6bf7d-7b09-4399-9aed-0e345d1ea7b2', 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', '84168a05-7f94-46f5-8d2f-302f6c2ca638')",
// Fetch both users and groups belonging to the groups matching the scope-filter
// The set of users and groups are further filtered down using memberEntityFilter
"members": [
{
// All Users belonging to group matching scope-filters will be further filtered down using memberEntityFilter
"memberEntity": "User",
"memberEntityFilter": "department eq 'Architecture and Authentication'"
},
{
// All Groups belonging to group matching scope-filters will NOT be further filtered down as memberEntityFilter is absent
"memberEntity": "Group"
}
]
},
{
"scopeEntity": "Group",
"scopeEntityFilter": "id eq '94e98d95-8e04-47ee-b2d4-5a1bd96bf0a9'",
// Only fetch users belonging to the groups matching the scope-filter
"members": [
{
"memberEntity": "User"
}
]
},
{
"scopeEntity": "Group",
"scopeEntityFilter": "id eq '821d9845-3482-41c8-9a0e-d93f4d576731'",
// Fetch both users and groups belonging to the groups matching the scope-filter
"members": [
{
"memberEntity": "User"
},
{
"memberEntity": "Group"
}
]
}
],
// This ingests only those group entity nodes starting with `Test` or `SGNL`
"Group": [
{
"scopeEntity": "Group",
"scopeEntityFilter": "startswith(displayName, 'Test')"
},
{
"scopeEntity": "Group",
"scopeEntityFilter": "startswith(displayName, 'SGNL')"
}
]
}
}
type Client ¶
type Client interface {
GetPage(ctx context.Context, request *Request) (*Response, *framework.Error)
}
Client is a client that allows querying the datasource which contains JSON objects.
type Config ¶
type Config struct {
// Common configuration
*config.CommonConfig
// APIVersion is the version of the Microsoft Graph API to use.
APIVersion string `json:"apiVersion,omitempty"`
// Filters contains a map of filters for each entity associated with this
// datasource. The key is the entity's external_name, and the value is the filter string.
Filters map[string]string `json:"filters,omitempty"`
// ApplyFiltersToMembers determines whether filters applied to parent objects should be applied when querying
// member objects. For example, if true, Group filters be applied when pulling GroupMembers.
ApplyFiltersToMembers bool `json:"applyFiltersToMembers,omitempty"`
// Optional advanced filters to apply to the request.
// See advanced_filters.go for more information.
AdvancedFilters *AdvancedFilters `json:"advancedFilters,omitempty"`
}
Config is the configuration passed in each GetPage calls to the adapter. Adapter configuration example: nolint: godot
{
"requestTimeoutSeconds": 10,
"localTimeZoneOffset": 43200,
"apiVersion": "v1.0",
"filters": {
"users": "displayName ne null"
},
"applyFiltersToMembers": true
}
type Datasource ¶
Datasource directly implements a Client interface to allow querying an external datasource.
type DatasourceResponse ¶
type DatasourceResponse struct {
Values []map[string]any `json:"value"`
NextLink *string `json:"@odata.nextLink"`
}
Graph API response format example: User Response: https://learn.microsoft.com/en-us/graph/api/user-list?view=graph-rest-1.0&tabs=http#examples Paging: https://learn.microsoft.com/en-us/graph/paging?tabs=http
type EntityFilter ¶
type EntityFilter struct {
ScopeEntity string `json:"scopeEntity"`
ScopeEntityFilter string `json:"scopeEntityFilter"`
Members []MemberFilter `json:"members"`
}
type EntityInfo ¶
type EntityInfo struct {
// contains filtered or unexported fields
}
type MemberFilter ¶
type Request ¶
type Request struct {
// BaseURL is the Base URL of the datasource to query.
BaseURL string
// Token is the Bearer API token to authenticate a request.
Token string
// PageSize is the maximum number of objects to return from the entity.
PageSize int64
// EntityExternalID is the external ID of the entity.
// The external ID should match the API's resource name.
EntityExternalID string
// Cursor identifies the first object of the page to return, as returned by
// the last request for the entity.
// nil in the request for the first page.
Cursor *pagination.CompositeCursor[string]
// Skip is the number of objects to skip from the beginning of the entity.
// This is applicable to PIM entities like RoleAssignmentScheduleRequest and GroupAssignmentScheduleRequest.
Skip int64
// Filter contains the optional filter to apply to the current request.
Filter *string
// ParentFilter contains the optional filter to apply when retrieving parent objects for a member entity.
// This will only be set if the current entity has a parent and ApplyFiltersToMembers is enabled.
ParentFilter *string
// Attributes contains the list of attributes to request along with the current request.
Attributes []*framework.AttributeConfig
// APIVersion the API version to use.
APIVersion string
// RequestTimeoutSeconds is the timeout duration for requests made to datasources.
// This should be set to the number of seconds to wait before timing out.
RequestTimeoutSeconds int
// UseAdvancedFilters is a flag that indicates whether advanced filters should be used.
// Set $count=true in the query to enable advanced filters.
// See https://learn.microsoft.com/en-us/graph/aad-advanced-queries for more information.
UseAdvancedFilters bool
// AdvancedFilterMemberExternalID is the external ID of the member entity to apply advanced filters to.
AdvancedFilterMemberExternalID *string
}
Request is a request to the datasource.
type Response ¶
type Response struct {
// StatusCode is an HTTP status code.
StatusCode int
// RetryAfterHeader is the Retry-After response HTTP header, if set.
RetryAfterHeader string
// Objects is the list of
// May be empty.
Objects []map[string]any
// NextCursor is the cursor that identifies the first object of the next page.
// nil if this is the last page in this full sync.
NextCursor *pagination.CompositeCursor[string]
}
Response is a response returned by the datasource.