diff --git a/lib/msgraph/client.go b/lib/msgraph/client.go index 5764b1459f4bb..e5a2f987936e7 100644 --- a/lib/msgraph/client.go +++ b/lib/msgraph/client.go @@ -360,6 +360,19 @@ func (c *Client) UpdateApplication(ctx context.Context, appObjectID string, app return trace.Wrap(c.patch(ctx, uri.String(), app)) } +// GetApplication returns the application with the given app client ID. +// Note that appID here is the app the application "client ID" ([Application.AppID]) not "object ID" ([Application.ID]). +// Ref: [https://learn.microsoft.com/en-us/graph/api/application-get]. +func (c *Client) GetApplication(ctx context.Context, applicationID string) (*Application, error) { + applicationIDFilter := fmt.Sprintf("applications(appId='%s')", applicationID) + uri := c.endpointURI(applicationIDFilter) + out, err := roundtrip[*Application](ctx, c, http.MethodGet, uri.String(), nil) + if err != nil { + return nil, trace.Wrap(err) + } + return out, nil +} + // UpdateServicePrincipal issues a partial update for a [ServicePrincipal]. // Ref: [https://learn.microsoft.com/en-us/graph/api/serviceprincipal-update]. func (c *Client) UpdateServicePrincipal(ctx context.Context, spID string, sp *ServicePrincipal) error { diff --git a/lib/msgraph/client_test.go b/lib/msgraph/client_test.go index 8e49692249c0a..f9e35f56425a9 100644 --- a/lib/msgraph/client_test.go +++ b/lib/msgraph/client_test.go @@ -19,6 +19,7 @@ package msgraph import ( "context" "encoding/json" + "fmt" "io" "net/http" "net/http/httptest" @@ -474,3 +475,79 @@ func TestIterateGroupMembers(t *testing.T) { require.Equal(t, "Test Group 1", *group.DisplayName) } } + +const getApplicationPayload = ` +{ + "id": "aeee7e9f-57ad-4ea6-a236-cd10b2dbc0b4", + "appId": "d2a39a2a-1636-457f-82f9-c2d76527e20e", + "displayName": "test SAML App", + "groupMembershipClaims": "SecurityGroup", + "identifierUris": [ + "goteleport.com" + ], + "optionalClaims": { + "accessToken": [], + "idToken": [], + "saml2Token": [ + { + "additionalProperties": [ + "sam_account_name" + ], + "essential": false, + "name": "groups", + "source": null + } + ] + } + }` + +func TestGetApplication(t *testing.T) { + + mux := http.NewServeMux() + appID := "d2a39a2a-1636-457f-82f9-c2d76527e20e" + mux.Handle(fmt.Sprintf("GET /applications(appId='%s')", appID), + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(getApplicationPayload)) + })) + + srv := httptest.NewServer(mux) + t.Cleanup(func() { srv.Close() }) + + uri, err := url.Parse(srv.URL) + require.NoError(t, err) + client := &Client{ + httpClient: &http.Client{}, + tokenProvider: &fakeTokenProvider{}, + retryConfig: retryConfig, + baseURL: uri, + pageSize: 2, // smaller page size so we actually fetch multiple pages with our small test payload + } + + app, err := client.GetApplication(context.Background(), appID) + require.NoError(t, err) + require.Equal(t, "aeee7e9f-57ad-4ea6-a236-cd10b2dbc0b4", *app.ID) + + expectation := &Application{ + AppID: toPtr("d2a39a2a-1636-457f-82f9-c2d76527e20e"), + DirectoryObject: DirectoryObject{ + DisplayName: toPtr("test SAML App"), + ID: toPtr("aeee7e9f-57ad-4ea6-a236-cd10b2dbc0b4"), + }, + GroupMembershipClaims: toPtr("SecurityGroup"), + IdentifierURIs: &[]string{"goteleport.com"}, + OptionalClaims: &OptionalClaims{ + SAML2Token: []OptionalClaim{ + { + AdditionalProperties: &[]string{"sam_account_name"}, + Essential: toPtr(false), + Name: toPtr("groups"), + Source: nil, + }, + }, + }, + } + require.EqualValues(t, expectation, app) + +} + +func toPtr[T any](s T) *T { return &s } diff --git a/lib/msgraph/models.go b/lib/msgraph/models.go index 1150c8943e7d1..294cdd1fe9156 100644 --- a/lib/msgraph/models.go +++ b/lib/msgraph/models.go @@ -35,7 +35,14 @@ type DirectoryObject struct { type Group struct { DirectoryObject + // GroupTypes is a list of group type strings. GroupTypes []string `json:"groupTypes,omitempty"` + // OnPremisesDomainName is the on-premises domain name of the group. + OnPremisesDomainName *string `json:"onPremisesDomainName,omitempty"` + // OnPremisesNetBiosName is the on-premises NetBIOS name of the group. + OnPremisesNetBiosName *string `json:"onPremisesNetBiosName,omitempty"` + // OnPremisesSamAccountName is the on-premises SAM account name of the group. + OnPremisesSamAccountName *string `json:"onPremisesSamAccountName,omitempty"` } func (g *Group) IsOffice365Group() bool { @@ -64,6 +71,28 @@ type Application struct { IdentifierURIs *[]string `json:"identifierUris,omitempty"` Web *WebApplication `json:"web,omitempty"` GroupMembershipClaims *string `json:"groupMembershipClaims,omitempty"` + OptionalClaims *OptionalClaims `json:"optionalClaims,omitempty"` +} + +// OptionalClaim represents an optional claim in a token. +type OptionalClaim struct { + // AdditionalProperties is a list of additional properties. + // Possible values: + // - sam_account_name: sAMAccountName + // - dns_domain_and_sam_account_name: dnsDomainName\sAMAccountName + // - netbios_domain_and_sam_account_name: netbiosDomainName\sAMAccountName + // - emit_as_roles + // - cloud_displayname + AdditionalProperties *[]string `json:"additionalProperties,omitempty"` + Essential *bool `json:"essential,omitempty"` + Name *string `json:"name,omitempty"` + Source *string `json:"source,omitempty"` +} + +// OptionalClaims represents optional claims in a token. +// Currently, only SAML2 tokens are supported. +type OptionalClaims struct { + SAML2Token []OptionalClaim `json:"saml2Token,omitempty"` } type WebApplication struct {