mirror of
https://github.com/kopia/kopia.git
synced 2025-12-23 22:57:50 -05:00
feat(providers): Azure Federated Identity support (#4728)
Add authentication support for Azure Federated Identity (AFI) Authored-by: @alisonb-veeam Authored-by: Alison Burgess <a.burgess@veeam.com>
This commit is contained in:
@@ -26,6 +26,7 @@ func (c *storageAzureFlags) Setup(svc StorageProviderServices, cmd *kingpin.CmdC
|
||||
cmd.Flag("client-id", "Azure service principle client ID (overrides AZURE_CLIENT_ID environment variable)").Envar(svc.EnvName("AZURE_CLIENT_ID")).StringVar(&c.azOptions.ClientID)
|
||||
cmd.Flag("client-secret", "Azure service principle client secret (overrides AZURE_CLIENT_SECRET environment variable)").Envar(svc.EnvName("AZURE_CLIENT_SECRET")).StringVar(&c.azOptions.ClientSecret)
|
||||
cmd.Flag("client-cert", "Azure client certificate (overrides AZURE_CLIENT_CERTIFICATE environment variable)").Envar(svc.EnvName("AZURE_CLIENT_CERTIFICATE")).StringVar(&c.azOptions.ClientCertificate)
|
||||
cmd.Flag("azure-federated-token-file", "Path to a file containing an Azure Federated Token (overrides AZURE_FEDERATED_TOKEN_FILE environment variable)").Envar(svc.EnvName("AZURE_FEDERATED_TOKEN_FILE")).StringVar(&c.azOptions.AzureFederatedTokenFile)
|
||||
|
||||
commonThrottlingFlags(cmd, &c.azOptions.Limits)
|
||||
|
||||
|
||||
@@ -31,6 +31,9 @@ type Options struct {
|
||||
// ClientCertificate are used for creating ClientCertificateCredentials
|
||||
ClientCertificate string `json:"clientCertificate,omitempty" kopia:"sensitive"`
|
||||
|
||||
// AzureFederatedTokenFile is the path to a file containing an Azure Federated Token.
|
||||
AzureFederatedTokenFile string `json:"azureFederatedTokenFile,omitempty"`
|
||||
|
||||
StorageDomain string `json:"storageDomain,omitempty"`
|
||||
|
||||
throttling.Limits
|
||||
|
||||
@@ -403,6 +403,7 @@ func New(ctx context.Context, opt *Options, isCreate bool) (blob.Storage, error)
|
||||
return az, nil
|
||||
}
|
||||
|
||||
//nolint:gocyclo
|
||||
func getAZService(opt *Options, storageHostname string) (*azblob.Client, error) {
|
||||
var (
|
||||
service *azblob.Client
|
||||
@@ -445,9 +446,21 @@ func getAZService(opt *Options, storageHostname string) (*azblob.Client, error)
|
||||
return nil, errors.Wrap(credErr, "unable to initialize client cert credential")
|
||||
}
|
||||
|
||||
service, serviceErr = azblob.NewClient(fmt.Sprintf("https://%s/", storageHostname), cred, nil)
|
||||
// Azure Federated Token
|
||||
case opt.TenantID != "" && opt.ClientID != "" && opt.AzureFederatedTokenFile != "":
|
||||
cred, err := azidentity.NewWorkloadIdentityCredential(&azidentity.WorkloadIdentityCredentialOptions{
|
||||
ClientID: opt.ClientID,
|
||||
TenantID: opt.TenantID,
|
||||
TokenFilePath: opt.AzureFederatedTokenFile,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to initialize Azure Federated Identity workload identity credential")
|
||||
}
|
||||
|
||||
service, serviceErr = azblob.NewClient(fmt.Sprintf("https://%s/", storageHostname), cred, nil)
|
||||
default:
|
||||
return nil, errors.New("one of the storage key, SAS token, client secret or client certificate must be provided")
|
||||
return nil, errors.New("one of the storage key, SAS token, client secret, client certificate, or Azure Federated Token must be provided")
|
||||
}
|
||||
|
||||
return service, errors.Wrap(serviceErr, "unable to create azure client")
|
||||
|
||||
@@ -22,18 +22,19 @@
|
||||
)
|
||||
|
||||
const (
|
||||
testContainerEnv = "KOPIA_AZURE_TEST_CONTAINER"
|
||||
testStorageAccountEnv = "KOPIA_AZURE_TEST_STORAGE_ACCOUNT"
|
||||
testStorageKeyEnv = "KOPIA_AZURE_TEST_STORAGE_KEY"
|
||||
testStorageSASTokenEnv = "KOPIA_AZURE_TEST_SAS_TOKEN"
|
||||
testImmutableContainerEnv = "KOPIA_AZURE_TEST_IMMUTABLE_CONTAINER"
|
||||
testImmutableStorageAccountEnv = "KOPIA_AZURE_TEST_IMMUTABLE_STORAGE_ACCOUNT"
|
||||
testImmutableStorageKeyEnv = "KOPIA_AZURE_TEST_IMMUTABLE_STORAGE_KEY"
|
||||
testImmutableStorageSASTokenEnv = "KOPIA_AZURE_TEST_IMMUTABLE_SAS_TOKEN"
|
||||
testStorageTenantIDEnv = "KOPIA_AZURE_TEST_TENANT_ID"
|
||||
testStorageClientIDEnv = "KOPIA_AZURE_TEST_CLIENT_ID"
|
||||
testStorageClientSecretEnv = "KOPIA_AZURE_TEST_CLIENT_SECRET"
|
||||
testStorageClientCertEnv = "KOPIA_AZURE_TEST_CLIENT_CERTIFICATE"
|
||||
testContainerEnv = "KOPIA_AZURE_TEST_CONTAINER"
|
||||
testStorageAccountEnv = "KOPIA_AZURE_TEST_STORAGE_ACCOUNT"
|
||||
testStorageKeyEnv = "KOPIA_AZURE_TEST_STORAGE_KEY"
|
||||
testStorageSASTokenEnv = "KOPIA_AZURE_TEST_SAS_TOKEN"
|
||||
testImmutableContainerEnv = "KOPIA_AZURE_TEST_IMMUTABLE_CONTAINER"
|
||||
testImmutableStorageAccountEnv = "KOPIA_AZURE_TEST_IMMUTABLE_STORAGE_ACCOUNT"
|
||||
testImmutableStorageKeyEnv = "KOPIA_AZURE_TEST_IMMUTABLE_STORAGE_KEY"
|
||||
testImmutableStorageSASTokenEnv = "KOPIA_AZURE_TEST_IMMUTABLE_SAS_TOKEN"
|
||||
testStorageTenantIDEnv = "KOPIA_AZURE_TEST_TENANT_ID"
|
||||
testStorageClientIDEnv = "KOPIA_AZURE_TEST_CLIENT_ID"
|
||||
testStorageClientSecretEnv = "KOPIA_AZURE_TEST_CLIENT_SECRET"
|
||||
testStorageClientCertEnv = "KOPIA_AZURE_TEST_CLIENT_CERTIFICATE"
|
||||
testAzureFederatedIdentityFilePathEnv = "KOPIA_AZURE_FEDERATED_IDENTITY_FILE_PATH"
|
||||
)
|
||||
|
||||
func getEnvOrSkip(t *testing.T, name string) string {
|
||||
@@ -240,6 +241,44 @@ func TestAzureStorageClientCertificate(t *testing.T) {
|
||||
require.NoError(t, providervalidation.ValidateProvider(ctx, st, blobtesting.TestValidationOptions))
|
||||
}
|
||||
|
||||
func TestAzureFederatedIdentity(t *testing.T) {
|
||||
t.Parallel()
|
||||
testutil.ProviderTest(t)
|
||||
|
||||
container := getEnvOrSkip(t, testContainerEnv)
|
||||
storageAccount := getEnvOrSkip(t, testStorageAccountEnv)
|
||||
tenantID := getEnvOrSkip(t, testStorageTenantIDEnv)
|
||||
clientID := getEnvOrSkip(t, testStorageClientIDEnv)
|
||||
azureFederatedTokenFilePath := getEnvOrSkip(t, testAzureFederatedIdentityFilePathEnv)
|
||||
|
||||
data := make([]byte, 8)
|
||||
rand.Read(data)
|
||||
|
||||
ctx := testlogging.Context(t)
|
||||
|
||||
// use context that gets canceled after storage is initialize,
|
||||
// to verify we do not depend on the original context past initialization.
|
||||
newctx, cancel := context.WithCancel(ctx)
|
||||
st, err := azure.New(newctx, &azure.Options{
|
||||
Container: container,
|
||||
StorageAccount: storageAccount,
|
||||
TenantID: tenantID,
|
||||
ClientID: clientID,
|
||||
AzureFederatedTokenFile: azureFederatedTokenFilePath,
|
||||
Prefix: fmt.Sprintf("sastest-%v-%x/", clock.Now().Unix(), data),
|
||||
}, false)
|
||||
|
||||
require.NoError(t, err)
|
||||
cancel()
|
||||
|
||||
defer st.Close(ctx)
|
||||
defer blobtesting.CleanupOldData(ctx, t, st, 0)
|
||||
|
||||
blobtesting.VerifyStorage(ctx, t, st, blob.PutOptions{})
|
||||
blobtesting.AssertConnectionInfoRoundTrips(ctx, t, st)
|
||||
require.NoError(t, providervalidation.ValidateProvider(ctx, st, blobtesting.TestValidationOptions))
|
||||
}
|
||||
|
||||
func TestAzureStorageInvalidBlob(t *testing.T) {
|
||||
testutil.ProviderTest(t)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user