Add support for AAD auth for docker-acr (#395)

* Add support for AAD auth for docker-acr

* Update go version

---------

Co-authored-by: TP Honey <tp@harness.io>
This commit is contained in:
Rutvij Mehta
2023-08-22 04:07:41 -07:00
committed by GitHub
parent 025dfb0adc
commit be7bda4431
6 changed files with 316 additions and 68 deletions
+207
View File
@@ -1,17 +1,40 @@
package main
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"os/exec"
"strings"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/joho/godotenv"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
docker "github.com/drone-plugins/drone-docker"
)
const (
acrCertPath = "/tmp/acr-cert.pem"
azSubscriptionApiVersion = "2021-04-01"
azSubscriptionBaseUrl = "https://management.azure.com/subscriptions/"
basePublicUrl = "https://portal.azure.com/#view/Microsoft_Azure_ContainerRegistries/TagMetadataBlade/registryId/"
defaultUsername = "00000000-0000-0000-0000-000000000000"
// Environment variable names for Azure Environment Credential
clientIdEnv = "AZURE_CLIENT_ID"
clientSecretKeyEnv = "AZURE_CLIENT_SECRET"
tenantKeyEnv = "AZURE_TENANT_ID"
certPathEnv = "AZURE_CLIENT_CERTIFICATE_PATH"
)
func main() {
// Load env-file if it exists first
if env := os.Getenv("PLUGIN_ENV_FILE"); env != "" {
@@ -21,8 +44,19 @@ func main() {
var (
repo = getenv("PLUGIN_REPO")
registry = getenv("PLUGIN_REGISTRY")
// If these credentials are provided, they will be directly used
// for docker login
username = getenv("SERVICE_PRINCIPAL_CLIENT_ID")
password = getenv("SERVICE_PRINCIPAL_CLIENT_SECRET")
// Service principal credentials
clientId = getenv("CLIENT_ID")
clientSecret = getenv("CLIENT_SECRET")
clientCert = getenv("CLIENT_CERTIFICATE")
tenantId = getenv("TENANT_ID")
subscriptionId = getenv("SUBSCRIPTION_ID")
publicUrl = getenv("DAEMON_REGISTRY")
)
// default registry value
@@ -30,6 +64,17 @@ func main() {
registry = "azurecr.io"
}
// Get auth if username and password is not specified
if username == "" && password == "" {
// docker login credentials are not provided
var err error
username = defaultUsername
password, publicUrl, err = getAuth(clientId, clientSecret, clientCert, tenantId, subscriptionId, registry)
if err != nil {
logrus.Fatal(err)
}
}
// must use the fully qualified repo name. If the
// repo name does not have the registry prefix we
// should prepend.
@@ -42,6 +87,11 @@ func main() {
os.Setenv("DOCKER_USERNAME", username)
os.Setenv("DOCKER_PASSWORD", password)
os.Setenv("PLUGIN_REGISTRY_TYPE", "ACR")
if publicUrl != "" {
// Set this env variable if public URL for artifact is available
// If not, we will fall back to registry url
os.Setenv("ARTIFACT_REGISTRY", publicUrl)
}
// invoke the base docker plugin binary
cmd := exec.Command(docker.GetDroneDockerExecCmd())
@@ -53,6 +103,157 @@ func main() {
}
}
func getAuth(clientId, clientSecret, clientCert, tenantId, subscriptionId, registry string) (string, string, error) {
// Verify inputs
if tenantId == "" {
return "", "", fmt.Errorf("tenantId cannot be empty for AAD authentication")
}
if clientId == "" {
return "", "", fmt.Errorf("clientId cannot be empty for AAD authentication")
}
if clientSecret == "" && clientCert == "" {
return "", "", fmt.Errorf("one of client secret or client cert should be defined")
}
// Setup cert
if clientCert != "" {
err := setupACRCert(clientCert, acrCertPath)
if err != nil {
errors.Wrap(err, "failed to push setup cert file")
}
}
// Get AZ env
if err := os.Setenv(clientIdEnv, clientId); err != nil {
return "", "", errors.Wrap(err, "failed to set env variable client Id")
}
if err := os.Setenv(clientSecretKeyEnv, clientSecret); err != nil {
return "", "", errors.Wrap(err, "failed to set env variable client secret")
}
if err := os.Setenv(tenantKeyEnv, tenantId); err != nil {
return "", "", errors.Wrap(err, "failed to set env variable tenant Id")
}
if err := os.Setenv(certPathEnv, acrCertPath); err != nil {
return "", "", errors.Wrap(err, "failed to set env variable cert path")
}
env, err := azidentity.NewEnvironmentCredential(nil)
if err != nil {
return "", "", errors.Wrap(err, "failed to get env credentials from azure")
}
os.Unsetenv(clientIdEnv)
os.Unsetenv(clientSecretKeyEnv)
os.Unsetenv(tenantKeyEnv)
os.Unsetenv(certPathEnv)
// Fetch AAD token
policy := policy.TokenRequestOptions{
Scopes: []string{"https://management.azure.com/.default"},
}
aadToken, err := env.GetToken(context.Background(), policy)
if err != nil {
return "", "", errors.Wrap(err, "failed to fetch access token")
}
// Get public URL for artifacts
publicUrl, err := getPublicUrl(aadToken.Token, registry, subscriptionId)
if err != nil {
// execution should not fail because of this error
fmt.Fprintf(os.Stderr, "failed to get public url with error: %s\n", err)
}
// Fetch token
ACRToken, err := fetchACRToken(tenantId, aadToken.Token, registry)
if err != nil {
return "", "", errors.Wrap(err, "failed to fetch ACR token")
}
return ACRToken, publicUrl, nil
}
func fetchACRToken(tenantId, token, registry string) (string, error) {
// oauth exchange
formData := url.Values{
"grant_type": {"access_token"},
"service": {registry},
"tenant": {tenantId},
"access_token": {token},
}
jsonResponse, err := http.PostForm(fmt.Sprintf("https://%s/oauth2/exchange", registry), formData)
if err != nil || jsonResponse == nil {
return "", errors.Wrap(err, "failed to fetch ACR token")
}
// fetch token from response
var response map[string]interface{}
err = json.NewDecoder(jsonResponse.Body).Decode(&response)
if err != nil {
return "", errors.Wrap(err, "failed to decode oauth exchange response")
}
// Parse the refresh_token from the response
if t, found := response["refresh_token"]; found {
if refreshToken, ok := t.(string); ok {
return refreshToken, nil
}
return "", errors.New("failed to cast refresh token from acr")
}
return "", errors.Wrap(err, "refresh token not found in response of oauth exchange call")
}
func setupACRCert(cert, certPath string) error {
decoded, err := base64.StdEncoding.DecodeString(cert)
if err != nil {
return errors.Wrap(err, "failed to base64 decode ACR certificate")
}
err = ioutil.WriteFile(certPath, decoded, 0644)
if err != nil {
return errors.Wrap(err, "failed to write ACR certificate")
}
return nil
}
func getPublicUrl(token, registryUrl, subscriptionId string) (string, error) {
if len(subscriptionId) == 0 || registryUrl == "" {
return "", nil
}
registry := strings.Split(registryUrl, ".")[0]
filter := fmt.Sprintf("resourceType eq 'Microsoft.ContainerRegistry/registries' and name eq '%s'", registry)
params := url.Values{}
params.Add("$filter", filter)
params.Add("api-version", azSubscriptionApiVersion)
params.Add("$select", "id")
url := azSubscriptionBaseUrl + subscriptionId + "/resources?" + params.Encode()
client := &http.Client{}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
fmt.Println(err)
return "", errors.Wrap(err, "failed to create request for getting container registry setting")
}
req.Header.Add("Authorization", "Bearer "+token)
res, err := client.Do(req)
if err != nil {
fmt.Println(err)
return "", errors.Wrap(err, "failed to send request for getting container registry setting")
}
defer res.Body.Close()
var response subscriptionUrlResponse
err = json.NewDecoder(res.Body).Decode(&response)
if err != nil {
return "", errors.Wrap(err, "failed to send request for getting container registry setting")
}
if len(response.Value) == 0 {
return "", errors.New("no id present for base url")
}
return basePublicUrl + encodeParam(response.Value[0].ID), nil
}
func encodeParam(s string) string {
return url.QueryEscape(s)
}
func getenv(key ...string) (s string) {
for _, k := range key {
s = os.Getenv(k)
@@ -62,3 +263,9 @@ func getenv(key ...string) (s string) {
}
return
}
type subscriptionUrlResponse struct {
Value []struct {
ID string `json:"id"`
} `json:"value"`
}
+21 -14
View File
@@ -112,6 +112,12 @@ func main() {
Usage: "don't start the docker daemon",
EnvVar: "PLUGIN_DAEMON_OFF",
},
cli.StringFlag{
Name: "artifact.registry",
Usage: "artifact registry",
Value: "https://index.docker.io/v1/",
EnvVar: "ARTIFACT_REGISTRY,PLUGIN_REGISTRY,DOCKER_REGISTRY",
},
cli.StringFlag{
Name: "dockerfile",
Usage: "build dockerfile",
@@ -346,20 +352,21 @@ func run(c *cli.Context) error {
SSHAgentKey: c.String("ssh-agent-key"),
},
Daemon: docker.Daemon{
Registry: c.String("docker.registry"),
Mirror: c.String("daemon.mirror"),
StorageDriver: c.String("daemon.storage-driver"),
StoragePath: c.String("daemon.storage-path"),
Insecure: c.Bool("daemon.insecure"),
Disabled: c.Bool("daemon.off"),
IPv6: c.Bool("daemon.ipv6"),
Debug: c.Bool("daemon.debug"),
Bip: c.String("daemon.bip"),
DNS: c.StringSlice("daemon.dns"),
DNSSearch: c.StringSlice("daemon.dns-search"),
MTU: c.String("daemon.mtu"),
Experimental: c.Bool("daemon.experimental"),
RegistryType: registryType,
Registry: c.String("docker.registry"),
Mirror: c.String("daemon.mirror"),
StorageDriver: c.String("daemon.storage-driver"),
StoragePath: c.String("daemon.storage-path"),
Insecure: c.Bool("daemon.insecure"),
Disabled: c.Bool("daemon.off"),
IPv6: c.Bool("daemon.ipv6"),
Debug: c.Bool("daemon.debug"),
Bip: c.String("daemon.bip"),
DNS: c.StringSlice("daemon.dns"),
DNSSearch: c.StringSlice("daemon.dns-search"),
MTU: c.String("daemon.mtu"),
Experimental: c.Bool("daemon.experimental"),
RegistryType: registryType,
ArtifactRegistry: c.String("artifact.registry"),
},
}