mirror of
https://github.com/prometheus/prometheus.git
synced 2025-12-05 17:41:21 +01:00
RW2: Allow custom scope in azuread (#17483)
Signed-off-by: Ben Edmunds <sammybenblue2@gmail.com>
This commit is contained in:
parent
8a1086a128
commit
0e682a70a6
@ -3277,6 +3277,14 @@ azuread:
|
||||
[ sdk:
|
||||
[ tenant_id: <string> ] ]
|
||||
|
||||
# Optional custom OAuth 2.0 scope to request when acquiring tokens.
|
||||
# If not specified, defaults to the appropriate monitoring scope for the cloud:
|
||||
# - AzurePublic: https://monitor.azure.com//.default
|
||||
# - AzureGovernment: https://monitor.azure.us//.default
|
||||
# - AzureChina: https://monitor.azure.cn//.default
|
||||
# Use this to authenticate against custom Azure applications or non-standard endpoints.
|
||||
[ scope: <string> ]
|
||||
|
||||
# WARNING: Remote write is NOT SUPPORTED by Google Cloud. This configuration is reserved for future use.
|
||||
# Optional Google Cloud Monitoring configuration.
|
||||
# Cannot be used at the same time as basic_auth, authorization, oauth2, sigv4 or azuread.
|
||||
|
||||
@ -103,6 +103,9 @@ type AzureADConfig struct { //nolint:revive // exported.
|
||||
|
||||
// Cloud is the Azure cloud in which the service is running. Example: AzurePublic/AzureGovernment/AzureChina.
|
||||
Cloud string `yaml:"cloud,omitempty"`
|
||||
|
||||
// Scope is the custom OAuth 2.0 scope to request when acquiring tokens.
|
||||
Scope string `yaml:"scope,omitempty"`
|
||||
}
|
||||
|
||||
// azureADRoundTripper is used to store the roundtripper and the tokenprovider.
|
||||
@ -211,6 +214,12 @@ func (c *AzureADConfig) Validate() error {
|
||||
}
|
||||
}
|
||||
|
||||
if c.Scope != "" {
|
||||
if matched, err := regexp.MatchString("^[\\w\\s:/.\\-]+$", c.Scope); err != nil || !matched {
|
||||
return errors.New("the provided scope contains invalid characters")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -360,14 +369,22 @@ func newSDKTokenCredential(clientOpts *azcore.ClientOptions, sdkConfig *SDKConfi
|
||||
// newTokenProvider helps to fetch accessToken for different types of credential. This also takes care of
|
||||
// refreshing the accessToken before expiry. This accessToken is attached to the Authorization header while making requests.
|
||||
func newTokenProvider(cfg *AzureADConfig, cred azcore.TokenCredential) (*tokenProvider, error) {
|
||||
var scopes []string
|
||||
|
||||
// Use custom scope if provided, otherwise fallback to cloud-specific audience
|
||||
if cfg.Scope != "" {
|
||||
scopes = []string{cfg.Scope}
|
||||
} else {
|
||||
audience, err := getAudience(cfg.Cloud)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
scopes = []string{audience}
|
||||
}
|
||||
|
||||
tokenProvider := &tokenProvider{
|
||||
credentialClient: cred,
|
||||
options: &policy.TokenRequestOptions{Scopes: []string{audience}},
|
||||
options: &policy.TokenRequestOptions{Scopes: scopes},
|
||||
}
|
||||
|
||||
return tokenProvider, nil
|
||||
|
||||
@ -198,6 +198,11 @@ func TestAzureAdConfig(t *testing.T) {
|
||||
filename: "testdata/azuread_bad_workloadidentity_missingtenantid.yaml",
|
||||
err: "must provide an Azure Workload Identity tenant_id in the Azure AD config",
|
||||
},
|
||||
// Invalid scope validation.
|
||||
{
|
||||
filename: "testdata/azuread_bad_scope_invalid.yaml",
|
||||
err: "the provided scope contains invalid characters",
|
||||
},
|
||||
// Valid config with missing optionally cloud field.
|
||||
{
|
||||
filename: "testdata/azuread_good_cloudmissing.yaml",
|
||||
@ -222,6 +227,10 @@ func TestAzureAdConfig(t *testing.T) {
|
||||
{
|
||||
filename: "testdata/azuread_good_workloadidentity.yaml",
|
||||
},
|
||||
// Valid OAuth config with custom scope.
|
||||
{
|
||||
filename: "testdata/azuread_good_oauth_customscope.yaml",
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
_, err := loadAzureAdConfig(c.filename)
|
||||
@ -387,3 +396,87 @@ func getToken() azcore.AccessToken {
|
||||
ExpiresOn: time.Now().Add(10 * time.Second),
|
||||
}
|
||||
}
|
||||
|
||||
func TestCustomScopeSupport(t *testing.T) {
|
||||
mockCredential := new(mockCredential)
|
||||
testToken := &azcore.AccessToken{
|
||||
Token: testTokenString,
|
||||
ExpiresOn: testTokenExpiry(),
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
cfg *AzureADConfig
|
||||
expectedScope string
|
||||
}{
|
||||
{
|
||||
name: "Custom scope with OAuth",
|
||||
cfg: &AzureADConfig{
|
||||
Cloud: "AzurePublic",
|
||||
OAuth: &OAuthConfig{
|
||||
ClientID: dummyClientID,
|
||||
ClientSecret: dummyClientSecret,
|
||||
TenantID: dummyTenantID,
|
||||
},
|
||||
Scope: "https://custom-app.com/.default",
|
||||
},
|
||||
expectedScope: "https://custom-app.com/.default",
|
||||
},
|
||||
{
|
||||
name: "Custom scope with Managed Identity",
|
||||
cfg: &AzureADConfig{
|
||||
Cloud: "AzurePublic",
|
||||
ManagedIdentity: &ManagedIdentityConfig{
|
||||
ClientID: dummyClientID,
|
||||
},
|
||||
Scope: "https://monitor.azure.com//.default",
|
||||
},
|
||||
expectedScope: "https://monitor.azure.com//.default",
|
||||
},
|
||||
{
|
||||
name: "Default scope fallback with OAuth",
|
||||
cfg: &AzureADConfig{
|
||||
Cloud: "AzurePublic",
|
||||
OAuth: &OAuthConfig{
|
||||
ClientID: dummyClientID,
|
||||
ClientSecret: dummyClientSecret,
|
||||
TenantID: dummyTenantID,
|
||||
},
|
||||
},
|
||||
expectedScope: IngestionPublicAudience,
|
||||
},
|
||||
{
|
||||
name: "Default scope fallback with China cloud",
|
||||
cfg: &AzureADConfig{
|
||||
Cloud: "AzureChina",
|
||||
OAuth: &OAuthConfig{
|
||||
ClientID: dummyClientID,
|
||||
ClientSecret: dummyClientSecret,
|
||||
TenantID: dummyTenantID,
|
||||
},
|
||||
},
|
||||
expectedScope: IngestionChinaAudience,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
// Set up mock to capture the actual scopes used
|
||||
mockCredential.On("GetToken", mock.Anything, mock.MatchedBy(func(options policy.TokenRequestOptions) bool {
|
||||
return len(options.Scopes) == 1 && options.Scopes[0] == c.expectedScope
|
||||
})).Return(*testToken, nil).Once()
|
||||
|
||||
tokenProvider, err := newTokenProvider(c.cfg, mockCredential)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, tokenProvider)
|
||||
|
||||
// Verify that the token provider uses the expected scope
|
||||
token, err := tokenProvider.getAccessToken(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, testTokenString, token)
|
||||
|
||||
// Reset mock for next test
|
||||
mockCredential.ExpectedCalls = nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
6
storage/remote/azuread/testdata/azuread_bad_scope_invalid.yaml
vendored
Normal file
6
storage/remote/azuread/testdata/azuread_bad_scope_invalid.yaml
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
cloud: AzurePublic
|
||||
oauth:
|
||||
client_id: 00000000-0000-0000-0000-000000000000
|
||||
client_secret: Cl1ent$ecret!
|
||||
tenant_id: 00000000-a12b-3cd4-e56f-000000000000
|
||||
scope: "invalid<>scope*chars"
|
||||
6
storage/remote/azuread/testdata/azuread_good_oauth_customscope.yaml
vendored
Normal file
6
storage/remote/azuread/testdata/azuread_good_oauth_customscope.yaml
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
cloud: AzurePublic
|
||||
oauth:
|
||||
client_id: 00000000-0000-0000-0000-000000000000
|
||||
client_secret: Cl1ent$ecret!
|
||||
tenant_id: 00000000-a12b-3cd4-e56f-000000000000
|
||||
scope: "https://custom-app.com/.default"
|
||||
Loading…
x
Reference in New Issue
Block a user