Skip to content

Commit f14576d

Browse files
feat: add ability to use OIDC insted of DSN (#251)
1 parent 65a4d41 commit f14576d

File tree

3 files changed

+506
-0
lines changed

3 files changed

+506
-0
lines changed

command/report/report.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ type ReportOptions struct {
2525
ValueFile string
2626
SkipCertificateVerification bool
2727
DSN string
28+
UseOIDC bool
29+
OIDCRequestToken string // id token to manually get an OIDC token
30+
OIDCRequestUrl string // url to manually get an OIDC token
31+
DeepSourceHostEndpoint string // DeepSource host endpoint where the app is running. Defaults to the cloud endpoint https://app.deepsource.com
32+
OIDCProvider string // OIDC provider to use for authentication
2833
}
2934

3035
// NewCmdVersion returns the current version of cli being used
@@ -67,6 +72,14 @@ func NewCmdReport() *cobra.Command {
6772

6873
cmd.Flags().StringVar(&opts.ValueFile, "value-file", "", "path to the artifact value file")
6974

75+
cmd.Flags().BoolVar(&opts.UseOIDC, "use-oidc", false, "use OIDC to authenticate with DeepSource")
76+
77+
cmd.Flags().StringVar(&opts.OIDCRequestToken, "oidc-request-token", "", "request ID token to fetch an OIDC token from OIDC provider")
78+
79+
cmd.Flags().StringVar(&opts.OIDCRequestUrl, "oidc-request-url", "", "OIDC provider's request URL to fetch an OIDC token")
80+
cmd.Flags().StringVar(&opts.DeepSourceHostEndpoint, "deepsource-host-endpoint", "https://app.deepsource.com", "DeepSource host endpoint where the app is running. Defaults to the cloud endpoint https://app.deepsource.com")
81+
cmd.Flags().StringVar(&opts.OIDCProvider, "oidc-provider", "", "OIDC provider to use for authentication. Supported providers: github-actions")
82+
7083
// --skip-verify flag to skip SSL certificate verification while reporting test coverage data.
7184
cmd.Flags().BoolVar(&opts.SkipCertificateVerification, "skip-verify", false, "skip SSL certificate verification while sending the test coverage data")
7285

@@ -80,6 +93,9 @@ func (opts *ReportOptions) sanitize() {
8093
opts.Value = strings.TrimSpace(opts.Value)
8194
opts.ValueFile = strings.TrimSpace(opts.ValueFile)
8295
opts.DSN = strings.TrimSpace(os.Getenv("DEEPSOURCE_DSN"))
96+
opts.OIDCRequestToken = strings.TrimSpace(opts.OIDCRequestToken)
97+
opts.OIDCRequestUrl = strings.TrimSpace(opts.OIDCRequestUrl)
98+
opts.DeepSourceHostEndpoint = strings.TrimSpace(opts.DeepSourceHostEndpoint)
8399
}
84100

85101
func (opts *ReportOptions) validateKey() error {
@@ -107,6 +123,15 @@ func (opts *ReportOptions) validateKey() error {
107123

108124
func (opts *ReportOptions) Run() int {
109125
opts.sanitize()
126+
if opts.UseOIDC {
127+
dsn, err := utils.GetDSNFromOIDC(opts.OIDCRequestToken, opts.OIDCRequestUrl, opts.DeepSourceHostEndpoint, opts.OIDCProvider)
128+
if err != nil {
129+
fmt.Fprintln(os.Stderr, "DeepSource | Error | Failed to get DSN using OIDC:", err)
130+
return 1
131+
}
132+
opts.DSN = dsn
133+
}
134+
110135
if opts.DSN == "" {
111136
fmt.Fprintln(os.Stderr, "DeepSource | Error | Environment variable DEEPSOURCE_DSN not set (or) is empty. You can find it under the repository settings page")
112137
return 1

utils/fetch_oidc_token.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package utils
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"net/http"
7+
"os"
8+
)
9+
10+
var (
11+
DEEPSOURCE_AUDIENCE = "DeepSource"
12+
ALLOWED_PROVIDERS = map[string]bool{
13+
"github-actions": true,
14+
}
15+
)
16+
17+
// FetchOIDCTokenFromProvider fetches the OIDC token from the OIDC token provider.
18+
// It takes the request ID and the request URL as input and returns the OIDC token as a string.
19+
func FetchOIDCTokenFromProvider(requestId, requestUrl string) (string, error) {
20+
// requestid is the bearer token that needs to be sent to the request url
21+
req, err := http.NewRequest("GET", requestUrl, nil)
22+
if err != nil {
23+
return "", err
24+
}
25+
req.Header.Set("Authorization", "Bearer "+requestId)
26+
// set the expected audiences as the audience parameter
27+
q := req.URL.Query()
28+
q.Set("audience", DEEPSOURCE_AUDIENCE)
29+
req.URL.RawQuery = q.Encode()
30+
31+
// send the request
32+
client := &http.Client{}
33+
resp, err := client.Do(req)
34+
if err != nil {
35+
return "", err
36+
}
37+
defer resp.Body.Close()
38+
39+
// check if the response is 200
40+
if resp.StatusCode != http.StatusOK {
41+
return "", fmt.Errorf("failed to fetch OIDC token: %s", resp.Status)
42+
}
43+
44+
// extract the token from the json response. The token is sent under the key `value`
45+
// and the response is a json object
46+
var tokenResponse struct {
47+
Value string `json:"value"`
48+
}
49+
if err := json.NewDecoder(resp.Body).Decode(&tokenResponse); err != nil {
50+
return "", err
51+
}
52+
// check if the token is empty
53+
if tokenResponse.Value == "" {
54+
return "", fmt.Errorf("failed to fetch OIDC token: empty token")
55+
}
56+
// return the token
57+
return tokenResponse.Value, nil
58+
}
59+
60+
// ExchangeOIDCTokenForTempDSN exchanges the OIDC token for a temporary DSN.
61+
// It sends the OIDC token to the respective DeepSource API endpoint and returns the temp DSN as string.
62+
func ExchangeOIDCTokenForTempDSN(oidcToken, dsEndpoint, provider string) (string, error) {
63+
apiEndpoint := fmt.Sprintf("%s/services/oidc/%s/", dsEndpoint, provider)
64+
req, err := http.NewRequest("POST", apiEndpoint, nil)
65+
if err != nil {
66+
return "", err
67+
}
68+
req.Header.Set("Authorization", "Bearer "+oidcToken)
69+
70+
type ExchangeResponse struct {
71+
DSN string `json:"access_token"`
72+
}
73+
resp, err := http.DefaultClient.Do(req)
74+
if err != nil {
75+
return "", err
76+
}
77+
defer resp.Body.Close()
78+
if resp.StatusCode != http.StatusOK {
79+
return "", fmt.Errorf("failed to exchange OIDC token for DSN: %s", resp.Status)
80+
}
81+
var exchangeResponse ExchangeResponse
82+
if err := json.NewDecoder(resp.Body).Decode(&exchangeResponse); err != nil {
83+
return "", err
84+
}
85+
// check if the token is empty
86+
if exchangeResponse.DSN == "" {
87+
return "", fmt.Errorf("failed to exchange OIDC token for DSN: empty token")
88+
}
89+
// return the token
90+
return exchangeResponse.DSN, nil
91+
}
92+
93+
func GetDSNFromOIDC(requestId, requestUrl, dsEndpoint, provider string) (string, error) {
94+
// infer provider from environment variables.
95+
// Github actions sets the GITHUB_ACTIONS environment variable to true by default.
96+
if os.Getenv("GITHUB_ACTIONS") == "true" {
97+
provider = "github-actions"
98+
}
99+
100+
if dsEndpoint == "" {
101+
return "", fmt.Errorf("--deepsource-host-endpoint can not be empty")
102+
}
103+
104+
if provider == "" {
105+
return "", fmt.Errorf("--oidc-provider can not be empty")
106+
}
107+
108+
isSupported := ALLOWED_PROVIDERS[provider]
109+
if !isSupported {
110+
return "", fmt.Errorf("provider %s is not supported for OIDC Token exchange (Supported Providers: %v)", provider, ALLOWED_PROVIDERS)
111+
}
112+
if requestId == "" || requestUrl == "" {
113+
var foundIDToken, foundRequestURL bool
114+
// try to fetch the token from the environment variables.
115+
// skipcq: CRT-A0014
116+
switch provider {
117+
case "github-actions":
118+
requestId, foundIDToken = os.LookupEnv("ACTIONS_ID_TOKEN_REQUEST_TOKEN")
119+
requestUrl, foundRequestURL = os.LookupEnv("ACTIONS_ID_TOKEN_REQUEST_URL")
120+
if !(foundIDToken && foundRequestURL) {
121+
errMsg := `failed to fetch "ACTIONS_ID_TOKEN_REQUEST_TOKEN" and "ACTIONS_ID_TOKEN_REQUEST_URL" from environment variables. Please make sure you are running this in a GitHub Actions environment with the required permissions. Or, use '--oidc-request-token' and '--oidc-request-url' flags to pass the token and request URL`
122+
return "", fmt.Errorf("%s", errMsg)
123+
}
124+
}
125+
}
126+
oidcToken, err := FetchOIDCTokenFromProvider(requestId, requestUrl)
127+
if err != nil {
128+
return "", err
129+
}
130+
tempDSN, err := ExchangeOIDCTokenForTempDSN(oidcToken, dsEndpoint, provider)
131+
if err != nil {
132+
return "", err
133+
}
134+
return tempDSN, nil
135+
}

0 commit comments

Comments
 (0)