mirror of
https://github.com/prometheus/prometheus.git
synced 2025-12-05 09:31:06 +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:
|
[ sdk:
|
||||||
[ tenant_id: <string> ] ]
|
[ 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.
|
# WARNING: Remote write is NOT SUPPORTED by Google Cloud. This configuration is reserved for future use.
|
||||||
# Optional Google Cloud Monitoring configuration.
|
# Optional Google Cloud Monitoring configuration.
|
||||||
# Cannot be used at the same time as basic_auth, authorization, oauth2, sigv4 or azuread.
|
# 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 is the Azure cloud in which the service is running. Example: AzurePublic/AzureGovernment/AzureChina.
|
||||||
Cloud string `yaml:"cloud,omitempty"`
|
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.
|
// 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
|
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
|
// 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.
|
// 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) {
|
func newTokenProvider(cfg *AzureADConfig, cred azcore.TokenCredential) (*tokenProvider, error) {
|
||||||
audience, err := getAudience(cfg.Cloud)
|
var scopes []string
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
// 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{
|
tokenProvider := &tokenProvider{
|
||||||
credentialClient: cred,
|
credentialClient: cred,
|
||||||
options: &policy.TokenRequestOptions{Scopes: []string{audience}},
|
options: &policy.TokenRequestOptions{Scopes: scopes},
|
||||||
}
|
}
|
||||||
|
|
||||||
return tokenProvider, nil
|
return tokenProvider, nil
|
||||||
|
|||||||
@ -198,6 +198,11 @@ func TestAzureAdConfig(t *testing.T) {
|
|||||||
filename: "testdata/azuread_bad_workloadidentity_missingtenantid.yaml",
|
filename: "testdata/azuread_bad_workloadidentity_missingtenantid.yaml",
|
||||||
err: "must provide an Azure Workload Identity tenant_id in the Azure AD config",
|
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.
|
// Valid config with missing optionally cloud field.
|
||||||
{
|
{
|
||||||
filename: "testdata/azuread_good_cloudmissing.yaml",
|
filename: "testdata/azuread_good_cloudmissing.yaml",
|
||||||
@ -222,6 +227,10 @@ func TestAzureAdConfig(t *testing.T) {
|
|||||||
{
|
{
|
||||||
filename: "testdata/azuread_good_workloadidentity.yaml",
|
filename: "testdata/azuread_good_workloadidentity.yaml",
|
||||||
},
|
},
|
||||||
|
// Valid OAuth config with custom scope.
|
||||||
|
{
|
||||||
|
filename: "testdata/azuread_good_oauth_customscope.yaml",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
_, err := loadAzureAdConfig(c.filename)
|
_, err := loadAzureAdConfig(c.filename)
|
||||||
@ -387,3 +396,87 @@ func getToken() azcore.AccessToken {
|
|||||||
ExpiresOn: time.Now().Add(10 * time.Second),
|
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