Skip to content

Commit 56c2c3b

Browse files
authored
Initial implementation of codespaces API (#2803)
1 parent 4f74d0c commit 56c2c3b

File tree

10 files changed

+3028
-2
lines changed

10 files changed

+3028
-2
lines changed
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
// Copyright 2023 The go-github AUTHORS. All rights reserved.
2+
//
3+
// Use of this source code is governed by a BSD-style
4+
// license that can be found in the LICENSE file.
5+
6+
// newreposecretwithxcrypto creates a new secret in GitHub for a given owner/repo.
7+
// newreposecretwithxcrypto uses x/crypto/nacl/box instead of sodium.
8+
// It does not depend on any native libraries and is easier to cross-compile for different platforms.
9+
// Quite possibly there is a performance penalty due to this.
10+
//
11+
// newreposecretwithxcrypto has two required flags for owner and repo, and takes in one argument for the name of the secret to add.
12+
// The secret value is pulled from an environment variable based on the secret name.
13+
// To authenticate with GitHub, provide your token via an environment variable GITHUB_AUTH_TOKEN.
14+
//
15+
// To verify the new secret, navigate to GitHub Repository > Settings > left side options bar > Secrets.
16+
//
17+
// Usage:
18+
//
19+
// export GITHUB_AUTH_TOKEN=<auth token from github that has secret create rights>
20+
// export SECRET_VARIABLE=<secret value of the secret variable>
21+
// go run main.go -owner <owner name> -repo <repository name> SECRET_VARIABLE
22+
//
23+
// Example:
24+
//
25+
// export GITHUB_AUTH_TOKEN=0000000000000000
26+
// export SECRET_VARIABLE="my-secret"
27+
// go run main.go -owner google -repo go-github SECRET_VARIABLE
28+
package main
29+
30+
import (
31+
"context"
32+
crypto_rand "crypto/rand"
33+
"encoding/base64"
34+
"flag"
35+
"fmt"
36+
"log"
37+
"os"
38+
39+
"github.com/google/go-github/v53/github"
40+
"golang.org/x/crypto/nacl/box"
41+
)
42+
43+
var (
44+
repo = flag.String("repo", "", "The repo that the secret should be added to, ex. go-github")
45+
owner = flag.String("owner", "", "The owner of there repo this should be added to, ex. google")
46+
)
47+
48+
func main() {
49+
flag.Parse()
50+
51+
token := os.Getenv("GITHUB_AUTH_TOKEN")
52+
if token == "" {
53+
log.Fatal("please provide a GitHub API token via env variable GITHUB_AUTH_TOKEN")
54+
}
55+
56+
if *repo == "" {
57+
log.Fatal("please provide required flag --repo to specify GitHub repository ")
58+
}
59+
60+
if *owner == "" {
61+
log.Fatal("please provide required flag --owner to specify GitHub user/org owner")
62+
}
63+
64+
secretName, err := getSecretName()
65+
if err != nil {
66+
log.Fatal(err)
67+
}
68+
69+
secretValue, err := getSecretValue(secretName)
70+
if err != nil {
71+
log.Fatal(err)
72+
}
73+
74+
ctx := context.Background()
75+
client := github.NewTokenClient(ctx, token)
76+
77+
if err := addRepoSecret(ctx, client, *owner, *repo, secretName, secretValue); err != nil {
78+
log.Fatal(err)
79+
}
80+
81+
fmt.Printf("Added secret %q to the repo %v/%v\n", secretName, *owner, *repo)
82+
}
83+
84+
func getSecretName() (string, error) {
85+
secretName := flag.Arg(0)
86+
if secretName == "" {
87+
return "", fmt.Errorf("missing argument secret name")
88+
}
89+
return secretName, nil
90+
}
91+
92+
func getSecretValue(secretName string) (string, error) {
93+
secretValue := os.Getenv(secretName)
94+
if secretValue == "" {
95+
return "", fmt.Errorf("secret value not found under env variable %q", secretName)
96+
}
97+
return secretValue, nil
98+
}
99+
100+
// addRepoSecret will add a secret to a GitHub repo for use in GitHub Codespaces.
101+
//
102+
// The secretName and secretValue will determine the name of the secret added and it's corresponding value.
103+
//
104+
// The actual transmission of the secret value to GitHub using the api requires that the secret value is encrypted
105+
// using the public key of the target repo. This encryption is done using x/crypto/nacl/box.
106+
//
107+
// First, the public key of the repo is retrieved. The public key comes base64
108+
// encoded, so it must be decoded prior to use.
109+
//
110+
// Second, the decode key is converted into a fixed size byte array.
111+
//
112+
// Third, the secret value is converted into a slice of bytes.
113+
//
114+
// Fourth, the secret is encrypted with box.SealAnonymous using the repo's decoded public key.
115+
//
116+
// Fifth, the encrypted secret is encoded as a base64 string to be used in a github.EncodedSecret type.
117+
//
118+
// Sixth, The other two properties of the github.EncodedSecret type are determined. The name of the secret to be added
119+
// (string not base64), and the KeyID of the public key used to encrypt the secret.
120+
// This can be retrieved via the public key's GetKeyID method.
121+
//
122+
// Finally, the github.EncodedSecret is passed into the GitHub client.Codespaces.CreateOrUpdateRepoSecret method to
123+
// populate the secret in the GitHub repo.
124+
func addRepoSecret(ctx context.Context, client *github.Client, owner string, repo, secretName string, secretValue string) error {
125+
publicKey, _, err := client.Codespaces.GetRepoPublicKey(ctx, owner, repo)
126+
if err != nil {
127+
return err
128+
}
129+
130+
encryptedSecret, err := encryptSecretWithPublicKey(publicKey, secretName, secretValue)
131+
if err != nil {
132+
return err
133+
}
134+
135+
if _, err := client.Codespaces.CreateOrUpdateRepoSecret(ctx, owner, repo, encryptedSecret); err != nil {
136+
return fmt.Errorf("Codespaces.CreateOrUpdateRepoSecret returned error: %v", err)
137+
}
138+
139+
return nil
140+
}
141+
142+
func encryptSecretWithPublicKey(publicKey *github.PublicKey, secretName string, secretValue string) (*github.EncryptedSecret, error) {
143+
decodedPublicKey, err := base64.StdEncoding.DecodeString(publicKey.GetKey())
144+
if err != nil {
145+
return nil, fmt.Errorf("base64.StdEncoding.DecodeString was unable to decode public key: %v", err)
146+
}
147+
148+
var boxKey [32]byte
149+
copy(boxKey[:], decodedPublicKey)
150+
secretBytes := []byte(secretValue)
151+
encryptedBytes, err := box.SealAnonymous([]byte{}, secretBytes, &boxKey, crypto_rand.Reader)
152+
if err != nil {
153+
return nil, fmt.Errorf("box.SealAnonymous failed with error %w", err)
154+
}
155+
156+
encryptedString := base64.StdEncoding.EncodeToString(encryptedBytes)
157+
keyID := publicKey.GetKeyID()
158+
encryptedSecret := &github.EncryptedSecret{
159+
Name: secretName,
160+
KeyID: keyID,
161+
EncryptedValue: encryptedString,
162+
}
163+
return encryptedSecret, nil
164+
}
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
// Copyright 2023 The go-github AUTHORS. All rights reserved.
2+
//
3+
// Use of this source code is governed by a BSD-style
4+
// license that can be found in the LICENSE file.
5+
6+
// newusersecretwithxcrypto creates a new secret in GitHub for a given user.
7+
// newusersecretwithxcrypto uses x/crypto/nacl/box instead of sodium.
8+
// It does not depend on any native libraries and is easier to cross-compile for different platforms.
9+
// Quite possibly there is a performance penalty due to this.
10+
//
11+
// newusersecretwithxcrypto takes in one argument for the name of the secret to add, and 2 flags owner, repo.
12+
// If owner/repo are defined then it adds the secret to that repository
13+
// The secret value is pulled from an environment variable based on the secret name.
14+
// To authenticate with GitHub, provide your token via an environment variable GITHUB_AUTH_TOKEN.
15+
//
16+
// To verify the new secret, navigate to GitHub User > Settings > left side options bar > Codespaces > Secrets.
17+
//
18+
// Usage:
19+
//
20+
// export GITHUB_AUTH_TOKEN=<auth token from github that has secret create rights>
21+
// export SECRET_VARIABLE=<secret value of the secret variable>
22+
// go run main.go SECRET_VARIABLE
23+
//
24+
// Example:
25+
//
26+
// export GITHUB_AUTH_TOKEN=0000000000000000
27+
// export SECRET_VARIABLE="my-secret"
28+
// go run main.go SECRET_VARIABLE
29+
package main
30+
31+
import (
32+
"context"
33+
crypto_rand "crypto/rand"
34+
"encoding/base64"
35+
"flag"
36+
"fmt"
37+
"log"
38+
"os"
39+
40+
"github.com/google/go-github/v53/github"
41+
"golang.org/x/crypto/nacl/box"
42+
)
43+
44+
var (
45+
repo = flag.String("repo", "", "The repo that the secret should be added to, ex. go-github")
46+
owner = flag.String("owner", "", "The owner of there repo this should be added to, ex. google")
47+
)
48+
49+
func main() {
50+
flag.Parse()
51+
52+
token := os.Getenv("GITHUB_AUTH_TOKEN")
53+
if token == "" {
54+
log.Fatal("please provide a GitHub API token via env variable GITHUB_AUTH_TOKEN")
55+
}
56+
57+
secretName, err := getSecretName()
58+
if err != nil {
59+
log.Fatal(err)
60+
}
61+
62+
secretValue, err := getSecretValue(secretName)
63+
if err != nil {
64+
log.Fatal(err)
65+
}
66+
67+
ctx := context.Background()
68+
client := github.NewTokenClient(ctx, token)
69+
70+
if err := addUserSecret(ctx, client, secretName, secretValue, *owner, *repo); err != nil {
71+
log.Fatal(err)
72+
}
73+
74+
fmt.Printf("Added secret %q to the authenticated user\n", secretName)
75+
}
76+
77+
func getSecretName() (string, error) {
78+
secretName := flag.Arg(0)
79+
if secretName == "" {
80+
return "", fmt.Errorf("missing argument secret name")
81+
}
82+
return secretName, nil
83+
}
84+
85+
func getSecretValue(secretName string) (string, error) {
86+
secretValue := os.Getenv(secretName)
87+
if secretValue == "" {
88+
return "", fmt.Errorf("secret value not found under env variable %q", secretName)
89+
}
90+
return secretValue, nil
91+
}
92+
93+
// addUserSecret will add a secret to a GitHub user for use in GitHub Codespaces.
94+
//
95+
// The secretName and secretValue will determine the name of the secret added and it's corresponding value.
96+
//
97+
// The actual transmission of the secret value to GitHub using the api requires that the secret value is encrypted
98+
// using the public key of the target user. This encryption is done using x/crypto/nacl/box.
99+
//
100+
// First, the public key of the user is retrieved. The public key comes base64
101+
// encoded, so it must be decoded prior to use.
102+
//
103+
// Second, the decode key is converted into a fixed size byte array.
104+
//
105+
// Third, the secret value is converted into a slice of bytes.
106+
//
107+
// Fourth, the secret is encrypted with box.SealAnonymous using the user's decoded public key.
108+
//
109+
// Fifth, the encrypted secret is encoded as a base64 string to be used in a github.EncodedSecret type.
110+
//
111+
// Sixth, The other two properties of the github.EncodedSecret type are determined. The name of the secret to be added
112+
// (string not base64), and the KeyID of the public key used to encrypt the secret.
113+
// This can be retrieved via the public key's GetKeyID method.
114+
//
115+
// Seventh, the github.EncodedSecret is passed into the GitHub client.Codespaces.CreateOrUpdateUserSecret method to
116+
// populate the secret in the GitHub user.
117+
//
118+
// Finally, if a repo and owner are passed in, it adds the repo to the user secret.
119+
func addUserSecret(ctx context.Context, client *github.Client, secretName, secretValue, owner, repo string) error {
120+
publicKey, _, err := client.Codespaces.GetUserPublicKey(ctx)
121+
if err != nil {
122+
return err
123+
}
124+
125+
encryptedSecret, err := encryptSecretWithPublicKey(publicKey, secretName, secretValue)
126+
if err != nil {
127+
return err
128+
}
129+
130+
if _, err := client.Codespaces.CreateOrUpdateUserSecret(ctx, encryptedSecret); err != nil {
131+
return fmt.Errorf("Codespaces.CreateOrUpdateUserSecret returned error: %v", err)
132+
}
133+
134+
if owner != "" && repo != "" {
135+
r, _, err := client.Repositories.Get(ctx, owner, repo)
136+
if err != nil {
137+
return fmt.Errorf("Repositories.Get returned error: %v", err)
138+
}
139+
_, err = client.Codespaces.AddSelectedRepoToUserSecret(ctx, encryptedSecret.Name, r)
140+
if err != nil {
141+
return fmt.Errorf("Codespaces.AddSelectedRepoToUserSecret returned error: %v", err)
142+
}
143+
fmt.Printf("Added secret %q to %v/%v\n", secretName, owner, repo)
144+
}
145+
146+
return nil
147+
}
148+
149+
func encryptSecretWithPublicKey(publicKey *github.PublicKey, secretName string, secretValue string) (*github.EncryptedSecret, error) {
150+
decodedPublicKey, err := base64.StdEncoding.DecodeString(publicKey.GetKey())
151+
if err != nil {
152+
return nil, fmt.Errorf("base64.StdEncoding.DecodeString was unable to decode public key: %v", err)
153+
}
154+
155+
var boxKey [32]byte
156+
copy(boxKey[:], decodedPublicKey)
157+
secretBytes := []byte(secretValue)
158+
encryptedBytes, err := box.SealAnonymous([]byte{}, secretBytes, &boxKey, crypto_rand.Reader)
159+
if err != nil {
160+
return nil, fmt.Errorf("box.SealAnonymous failed with error %w", err)
161+
}
162+
163+
encryptedString := base64.StdEncoding.EncodeToString(encryptedBytes)
164+
keyID := publicKey.GetKeyID()
165+
encryptedSecret := &github.EncryptedSecret{
166+
Name: secretName,
167+
KeyID: keyID,
168+
EncryptedValue: encryptedString,
169+
}
170+
return encryptedSecret, nil
171+
}

example/newreposecretwithxcrypto/main.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ func getSecretValue(secretName string) (string, error) {
9999

100100
// addRepoSecret will add a secret to a GitHub repo for use in GitHub Actions.
101101
//
102-
// Finally, the secretName and secretValue will determine the name of the secret added and it's corresponding value.
102+
// The secretName and secretValue will determine the name of the secret added and it's corresponding value.
103103
//
104104
// The actual transmission of the secret value to GitHub using the api requires that the secret value is encrypted
105105
// using the public key of the target repo. This encryption is done using x/crypto/nacl/box.
@@ -115,7 +115,7 @@ func getSecretValue(secretName string) (string, error) {
115115
//
116116
// Fifth, the encrypted secret is encoded as a base64 string to be used in a github.EncodedSecret type.
117117
//
118-
// Sixt, The other two properties of the github.EncodedSecret type are determined. The name of the secret to be added
118+
// Sixth, The other two properties of the github.EncodedSecret type are determined. The name of the secret to be added
119119
// (string not base64), and the KeyID of the public key used to encrypt the secret.
120120
// This can be retrieved via the public key's GetKeyID method.
121121
//

0 commit comments

Comments
 (0)