Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions command/report/report.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ type ReportOptions struct {
ValueFile string
SkipCertificateVerification bool
DSN string
UseOIDC bool
OIDCRequestToken string // id token to manually get an OIDC token
OIDCRequestUrl string // url to manually get an OIDC token
DeepSourceHostEndpoint string // DeepSource host endpoint where the app is running. Defaults to the cloud endpoint https://app.deepsource.com
OIDCProvider string // OIDC provider to use for authentication
}

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

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

cmd.Flags().BoolVar(&opts.UseOIDC, "use-oidc", false, "use OIDC to authenticate with DeepSource")

cmd.Flags().StringVar(&opts.OIDCRequestToken, "oidc-request-token", "", "request ID token to fetch an OIDC token from OIDC provider")

cmd.Flags().StringVar(&opts.OIDCRequestUrl, "oidc-request-url", "", "OIDC provider's request URL to fetch an OIDC token")
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")
cmd.Flags().StringVar(&opts.OIDCProvider, "oidc-provider", "", "OIDC provider to use for authentication. Supported providers: github-actions")

// --skip-verify flag to skip SSL certificate verification while reporting test coverage data.
cmd.Flags().BoolVar(&opts.SkipCertificateVerification, "skip-verify", false, "skip SSL certificate verification while sending the test coverage data")

Expand All @@ -80,6 +93,9 @@ func (opts *ReportOptions) sanitize() {
opts.Value = strings.TrimSpace(opts.Value)
opts.ValueFile = strings.TrimSpace(opts.ValueFile)
opts.DSN = strings.TrimSpace(os.Getenv("DEEPSOURCE_DSN"))
opts.OIDCRequestToken = strings.TrimSpace(opts.OIDCRequestToken)
opts.OIDCRequestUrl = strings.TrimSpace(opts.OIDCRequestUrl)
opts.DeepSourceHostEndpoint = strings.TrimSpace(opts.DeepSourceHostEndpoint)
}

func (opts *ReportOptions) validateKey() error {
Expand Down Expand Up @@ -107,6 +123,15 @@ func (opts *ReportOptions) validateKey() error {

func (opts *ReportOptions) Run() int {
opts.sanitize()
if opts.UseOIDC {
dsn, err := utils.GetDSNFromOIDC(opts.OIDCRequestToken, opts.OIDCRequestUrl, opts.DeepSourceHostEndpoint, opts.OIDCProvider)
if err != nil {
fmt.Fprintln(os.Stderr, "DeepSource | Error | Failed to get DSN using OIDC:", err)
return 1
}
opts.DSN = dsn
}

if opts.DSN == "" {
fmt.Fprintln(os.Stderr, "DeepSource | Error | Environment variable DEEPSOURCE_DSN not set (or) is empty. You can find it under the repository settings page")
return 1
Expand Down
135 changes: 135 additions & 0 deletions utils/fetch_oidc_token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package utils

import (
"encoding/json"
"fmt"
"net/http"
"os"
)

var (
DEEPSOURCE_AUDIENCE = "DeepSource"
ALLOWED_PROVIDERS = map[string]bool{
"github-actions": true,
}
)

// FetchOIDCTokenFromProvider fetches the OIDC token from the OIDC token provider.
// It takes the request ID and the request URL as input and returns the OIDC token as a string.
func FetchOIDCTokenFromProvider(requestId, requestUrl string) (string, error) {
// requestid is the bearer token that needs to be sent to the request url
req, err := http.NewRequest("GET", requestUrl, nil)
if err != nil {
return "", err
}
req.Header.Set("Authorization", "Bearer "+requestId)
// set the expected audiences as the audience parameter
q := req.URL.Query()
q.Set("audience", DEEPSOURCE_AUDIENCE)
req.URL.RawQuery = q.Encode()

// send the request
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()

// check if the response is 200
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("failed to fetch OIDC token: %s", resp.Status)
}

// extract the token from the json response. The token is sent under the key `value`
// and the response is a json object
var tokenResponse struct {
Value string `json:"value"`
}
if err := json.NewDecoder(resp.Body).Decode(&tokenResponse); err != nil {
return "", err
}
// check if the token is empty
if tokenResponse.Value == "" {
return "", fmt.Errorf("failed to fetch OIDC token: empty token")
}
// return the token
return tokenResponse.Value, nil
}

// ExchangeOIDCTokenForTempDSN exchanges the OIDC token for a temporary DSN.
// It sends the OIDC token to the respective DeepSource API endpoint and returns the temp DSN as string.
func ExchangeOIDCTokenForTempDSN(oidcToken, dsEndpoint, provider string) (string, error) {
apiEndpoint := fmt.Sprintf("%s/services/oidc/%s/", dsEndpoint, provider)
req, err := http.NewRequest("POST", apiEndpoint, nil)
if err != nil {
return "", err
}
req.Header.Set("Authorization", "Bearer "+oidcToken)

type ExchangeResponse struct {
DSN string `json:"access_token"`
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("failed to exchange OIDC token for DSN: %s", resp.Status)
}
var exchangeResponse ExchangeResponse
if err := json.NewDecoder(resp.Body).Decode(&exchangeResponse); err != nil {
return "", err
}
// check if the token is empty
if exchangeResponse.DSN == "" {
return "", fmt.Errorf("failed to exchange OIDC token for DSN: empty token")
}
// return the token
return exchangeResponse.DSN, nil
}

func GetDSNFromOIDC(requestId, requestUrl, dsEndpoint, provider string) (string, error) {
// infer provider from environment variables.
// Github actions sets the GITHUB_ACTIONS environment variable to true by default.
if os.Getenv("GITHUB_ACTIONS") == "true" {
provider = "github-actions"
}

if dsEndpoint == "" {
return "", fmt.Errorf("--deepsource-host-endpoint can not be empty")
}

if provider == "" {
return "", fmt.Errorf("--oidc-provider can not be empty")
}

isSupported := ALLOWED_PROVIDERS[provider]
if !isSupported {
return "", fmt.Errorf("provider %s is not supported for OIDC Token exchange (Supported Providers: %v)", provider, ALLOWED_PROVIDERS)
}
if requestId == "" || requestUrl == "" {
var foundIDToken, foundRequestURL bool
// try to fetch the token from the environment variables.
// skipcq: CRT-A0014
switch provider {
case "github-actions":
requestId, foundIDToken = os.LookupEnv("ACTIONS_ID_TOKEN_REQUEST_TOKEN")
requestUrl, foundRequestURL = os.LookupEnv("ACTIONS_ID_TOKEN_REQUEST_URL")
if !(foundIDToken && foundRequestURL) {
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`
return "", fmt.Errorf("%s", errMsg)
}
}
}
oidcToken, err := FetchOIDCTokenFromProvider(requestId, requestUrl)
if err != nil {
return "", err
}
tempDSN, err := ExchangeOIDCTokenForTempDSN(oidcToken, dsEndpoint, provider)
if err != nil {
return "", err
}
return tempDSN, nil
}
Loading