Skip to content

Commit 757a756

Browse files
authored
ci-9739 - workload identity/OIDC support for GAR/GCR (drone-plugins#413)
* adds support for oidc access tokens gar/gcr
1 parent c354cd6 commit 757a756

File tree

6 files changed

+314
-41
lines changed

6 files changed

+314
-41
lines changed

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,26 @@ steps:
133133
from_secret: gcr_json_key
134134
```
135135
136+
### GAR (Google Artifact Registry) using workload identity (OIDC)
137+
138+
```yaml
139+
steps:
140+
- name: push-to-gar
141+
image: plugins/gcr
142+
pull: never
143+
settings:
144+
tag: latest
145+
repo: project-id/repo/image-name
146+
registry_type: GAR
147+
location: europe
148+
project_number: project-number
149+
pool_id: workload identity pool id
150+
provider_id: workload identity provider id
151+
service_account_email: service account email
152+
oidc_token_id:
153+
from_secret: token
154+
```
155+
136156
## Developer Notes
137157
138158
- When updating the base image, you will need to update for each architecture and OS.

cmd/drone-docker/main.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,11 @@ func main() {
292292
Usage: "registry type",
293293
EnvVar: "PLUGIN_REGISTRY_TYPE",
294294
},
295+
cli.StringFlag{
296+
Name: "access-token",
297+
Usage: "access token",
298+
EnvVar: "ACCESS_TOKEN",
299+
},
295300
}
296301

297302
if err := app.Run(os.Args); err != nil {
@@ -309,11 +314,12 @@ func run(c *cli.Context) error {
309314
Dryrun: c.Bool("dry-run"),
310315
Cleanup: c.BoolT("docker.purge"),
311316
Login: docker.Login{
312-
Registry: c.String("docker.registry"),
313-
Username: c.String("docker.username"),
314-
Password: c.String("docker.password"),
315-
Email: c.String("docker.email"),
316-
Config: c.String("docker.config"),
317+
Registry: c.String("docker.registry"),
318+
Username: c.String("docker.username"),
319+
Password: c.String("docker.password"),
320+
Email: c.String("docker.email"),
321+
Config: c.String("docker.config"),
322+
AccessToken: c.String("access-token"),
317323
},
318324
CardPath: c.String("drone-card-path"),
319325
ArtifactFile: c.String("artifact-file"),

cmd/drone-gcr/main.go

Lines changed: 99 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,15 @@ import (
1111
"strconv"
1212
"strings"
1313

14+
docker "github.com/drone-plugins/drone-docker"
15+
1416
"github.com/joho/godotenv"
1517
"github.com/sirupsen/logrus"
18+
"golang.org/x/oauth2"
1619
"golang.org/x/oauth2/google"
17-
18-
docker "github.com/drone-plugins/drone-docker"
20+
"google.golang.org/api/iamcredentials/v1"
21+
"google.golang.org/api/option"
22+
"google.golang.org/api/sts/v1"
1923
)
2024

2125
type Config struct {
@@ -25,11 +29,21 @@ type Config struct {
2529
WorkloadIdentity bool
2630
Username string
2731
RegistryType string
32+
AccessToken string
33+
}
34+
35+
type staticTokenSource struct {
36+
token *oauth2.Token
37+
}
38+
39+
func (s *staticTokenSource) Token() (*oauth2.Token, error) {
40+
return s.token, nil
2841
}
2942

3043
func loadConfig() Config {
3144
// Default username
3245
username := "_json_key"
46+
var config Config
3347

3448
// Load env-file if it exists
3549
if env := os.Getenv("PLUGIN_ENV_FILE"); env != "" {
@@ -38,18 +52,36 @@ func loadConfig() Config {
3852
}
3953
}
4054

55+
idToken := getenv("PLUGIN_OIDC_TOKEN_ID")
56+
projectId := getenv("PLUGIN_PROJECT_NUMBER")
57+
poolId := getenv("PLUGIN_POOL_ID")
58+
providerId := getenv("PLUGIN_PROVIDER_ID")
59+
serviceAccountEmail := getenv("PLUGIN_SERVICE_ACCOUNT_EMAIL")
60+
61+
if idToken != "" && projectId != "" && poolId != "" && providerId != "" && serviceAccountEmail != "" {
62+
federalToken, err := getFederalToken(idToken, projectId, poolId, providerId)
63+
if err != nil {
64+
logrus.Fatalf("Error (getFederalToken): %s", err)
65+
}
66+
accessToken, err := getGoogleCloudAccessToken(federalToken, serviceAccountEmail)
67+
if err != nil {
68+
logrus.Fatalf("Error (getGoogleCloudAccessToken): %s", err)
69+
}
70+
config.AccessToken = accessToken
71+
} else {
72+
password := getenv(
73+
"PLUGIN_JSON_KEY",
74+
"GCR_JSON_KEY",
75+
"GOOGLE_CREDENTIALS",
76+
"TOKEN",
77+
)
78+
config.WorkloadIdentity = parseBoolOrDefault(false, getenv("PLUGIN_WORKLOAD_IDENTITY"))
79+
config.Username, config.Password = setUsernameAndPassword(username, password, config.WorkloadIdentity)
80+
}
81+
4182
location := getenv("PLUGIN_LOCATION")
4283
repo := getenv("PLUGIN_REPO")
4384

44-
password := getenv(
45-
"PLUGIN_JSON_KEY",
46-
"GCR_JSON_KEY",
47-
"GOOGLE_CREDENTIALS",
48-
"TOKEN",
49-
)
50-
workloadIdentity := parseBoolOrDefault(false, getenv("PLUGIN_WORKLOAD_IDENTITY"))
51-
username, password = setUsernameAndPassword(username, password, workloadIdentity)
52-
5385
registryType := getenv("PLUGIN_REGISTRY_TYPE")
5486
if registryType == "" {
5587
registryType = "GCR"
@@ -73,24 +105,23 @@ func loadConfig() Config {
73105
if !strings.HasPrefix(repo, registry) {
74106
repo = path.Join(registry, repo)
75107
}
76-
77-
return Config{
78-
Repo: repo,
79-
Registry: registry,
80-
Password: password,
81-
WorkloadIdentity: workloadIdentity,
82-
Username: username,
83-
RegistryType: registryType,
84-
}
108+
config.Repo = repo
109+
config.Registry = registry
110+
config.RegistryType = registryType
111+
return config
85112
}
86113

87114
func main() {
88115
config := loadConfig()
116+
if config.AccessToken != "" {
117+
os.Setenv("ACCESS_TOKEN", config.AccessToken)
118+
} else if config.Username != "" && config.Password != "" {
119+
os.Setenv("DOCKER_USERNAME", config.Username)
120+
os.Setenv("DOCKER_PASSWORD", config.Password)
121+
}
89122

90123
os.Setenv("PLUGIN_REPO", config.Repo)
91124
os.Setenv("PLUGIN_REGISTRY", config.Registry)
92-
os.Setenv("DOCKER_USERNAME", config.Username)
93-
os.Setenv("DOCKER_PASSWORD", config.Password)
94125
os.Setenv("PLUGIN_REGISTRY_TYPE", config.RegistryType)
95126

96127
// invoke the base docker plugin binary
@@ -152,3 +183,49 @@ func getenv(key ...string) (s string) {
152183
}
153184
return
154185
}
186+
187+
func getFederalToken(idToken, projectNumber, poolId, providerId string) (string, error) {
188+
ctx := context.Background()
189+
stsService, err := sts.NewService(ctx, option.WithoutAuthentication())
190+
if err != nil {
191+
return "", err
192+
}
193+
audience := fmt.Sprintf("//iam.googleapis.com/projects/%s/locations/global/workloadIdentityPools/%s/providers/%s", projectNumber, poolId, providerId)
194+
tokenRequest := &sts.GoogleIdentityStsV1ExchangeTokenRequest{
195+
GrantType: "urn:ietf:params:oauth:grant-type:token-exchange",
196+
SubjectToken: idToken,
197+
Audience: audience,
198+
Scope: "https://www.googleapis.com/auth/cloud-platform",
199+
RequestedTokenType: "urn:ietf:params:oauth:token-type:access_token",
200+
SubjectTokenType: "urn:ietf:params:oauth:token-type:id_token",
201+
}
202+
tokenResponse, err := stsService.V1.Token(tokenRequest).Do()
203+
if err != nil {
204+
return "", err
205+
}
206+
207+
return tokenResponse.AccessToken, nil
208+
}
209+
210+
func getGoogleCloudAccessToken(federatedToken string, serviceAccountEmail string) (string, error) {
211+
ctx := context.Background()
212+
tokenSource := &staticTokenSource{
213+
token: &oauth2.Token{AccessToken: federatedToken},
214+
}
215+
service, err := iamcredentials.NewService(ctx, option.WithTokenSource(tokenSource))
216+
if err != nil {
217+
return "", err
218+
}
219+
220+
name := "projects/-/serviceAccounts/" + serviceAccountEmail
221+
rb := &iamcredentials.GenerateAccessTokenRequest{
222+
Scope: []string{"https://www.googleapis.com/auth/cloud-platform"},
223+
}
224+
225+
resp, err := service.Projects.ServiceAccounts.GenerateAccessToken(name, rb).Do()
226+
if err != nil {
227+
return "", err
228+
}
229+
230+
return resp.AccessToken, nil
231+
}

docker.go

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,12 @@ type (
3434

3535
// Login defines Docker login parameters.
3636
Login struct {
37-
Registry string // Docker registry address
38-
Username string // Docker registry username
39-
Password string // Docker registry password
40-
Email string // Docker registry email
41-
Config string // Docker Auth Config
37+
Registry string // Docker registry address
38+
Username string // Docker registry username
39+
Password string // Docker registry password
40+
Email string // Docker registry email
41+
Config string // Docker Auth Config
42+
AccessToken string // External Access Token
4243
}
4344

4445
// Build defines Docker build parameters.
@@ -113,7 +114,6 @@ type (
113114

114115
// Exec executes the plugin step
115116
func (p Plugin) Exec() error {
116-
117117
// start the Docker daemon server
118118
if !p.Daemon.Disabled {
119119
p.startDaemon()
@@ -143,6 +143,8 @@ func (p Plugin) Exec() error {
143143
fmt.Println("Detected registry credentials")
144144
case p.Login.Config != "":
145145
fmt.Println("Detected registry credentials file")
146+
case p.Login.AccessToken != "":
147+
fmt.Println("Detected access token")
146148
default:
147149
fmt.Println("Registry credentials or Docker config not provided. Guest mode enabled.")
148150
}
@@ -166,7 +168,18 @@ func (p Plugin) Exec() error {
166168
out := string(raw)
167169
out = strings.Replace(out, "WARNING! Using --password via the CLI is insecure. Use --password-stdin.", "", -1)
168170
fmt.Println(out)
169-
return fmt.Errorf("Error authenticating: exit status 1")
171+
return fmt.Errorf("error authenticating: exit status 1")
172+
}
173+
} else if p.Login.AccessToken != "" {
174+
cmd := commandLoginAccessToken(p.Login, p.Login.AccessToken)
175+
output, err := cmd.CombinedOutput()
176+
if err != nil {
177+
return fmt.Errorf("error logging in to Docker registry: %s", err)
178+
}
179+
if strings.Contains(string(output), "Login Succeeded") {
180+
fmt.Println("Login successful")
181+
} else {
182+
return fmt.Errorf("login did not succeed")
170183
}
171184
}
172185

@@ -270,6 +283,17 @@ func commandLogin(login Login) *exec.Cmd {
270283
)
271284
}
272285

286+
func commandLoginAccessToken(login Login, accessToken string) *exec.Cmd {
287+
cmd := exec.Command(dockerExe,
288+
"login",
289+
"-u",
290+
"oauth2accesstoken",
291+
"--password-stdin",
292+
login.Registry)
293+
cmd.Stdin = strings.NewReader(accessToken)
294+
return cmd
295+
}
296+
273297
// helper to check if args match "docker pull <image>"
274298
func isCommandPull(args []string) bool {
275299
return len(args) > 2 && args[1] == "pull"

go.mod

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,31 @@ require (
1010
github.com/joho/godotenv v1.3.0
1111
github.com/sirupsen/logrus v1.9.0
1212
github.com/urfave/cli v1.22.2
13-
golang.org/x/oauth2 v0.8.0
13+
golang.org/x/oauth2 v0.13.0
1414
)
1515

1616
require (
17-
cloud.google.com/go/compute/metadata v0.2.0 // indirect
17+
cloud.google.com/go/compute v1.23.1 // indirect
18+
cloud.google.com/go/compute/metadata v0.2.3 // indirect
1819
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
19-
github.com/golang/protobuf v1.5.2 // indirect
20+
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
21+
github.com/golang/protobuf v1.5.3 // indirect
22+
github.com/google/s2a-go v0.1.7 // indirect
23+
github.com/google/uuid v1.3.1 // indirect
24+
github.com/googleapis/enterprise-certificate-proxy v0.3.1 // indirect
25+
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
2026
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af // indirect
2127
github.com/russross/blackfriday/v2 v2.1.0 // indirect
22-
golang.org/x/net v0.10.0 // indirect
23-
golang.org/x/sys v0.8.0 // indirect
24-
google.golang.org/appengine v1.6.7 // indirect
25-
google.golang.org/protobuf v1.28.0 // indirect
28+
go.opencensus.io v0.24.0 // indirect
29+
golang.org/x/crypto v0.14.0 // indirect
30+
golang.org/x/net v0.17.0 // indirect
31+
golang.org/x/sys v0.13.0 // indirect
32+
golang.org/x/text v0.13.0 // indirect
33+
google.golang.org/api v0.146.0 // indirect
34+
google.golang.org/appengine v1.6.8 // indirect
35+
google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b // indirect
36+
google.golang.org/grpc v1.59.0 // indirect
37+
google.golang.org/protobuf v1.31.0 // indirect
2638
gopkg.in/yaml.v2 v2.2.8 // indirect
2739
gopkg.in/yaml.v3 v3.0.1 // indirect
2840
)

0 commit comments

Comments
 (0)