Skip to content

Commit

Permalink
feat: reworked git-auth; added github-app support
Browse files Browse the repository at this point in the history
close #141

Signed-off-by: Christian Kotzbauer <[email protected]>
  • Loading branch information
ckotzbauer committed Sep 14, 2022
1 parent 4b9256f commit eb12f65
Show file tree
Hide file tree
Showing 14 changed files with 301 additions and 77 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,4 @@ dist
*~

work
auth
/auth
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,9 +151,11 @@ The `autoCreate` option of DT is used. You have to set the `--format` flag to `c
| `git-repository` | `true` when `git` target is used. | `""` | Git-Repository-URL (HTTPS). |
| `git-branch` | `false` | `main` | Git-Branch to checkout. |
| `git-path` | `false` | `""` | Folder-Path inside the Git-Repository. |
| `git-access-token` | `true` when `git` target is used. | `""` | Git-Personal-Access-Token with write-permissions. |
| `git-author-name` | `true` when `git` target is used. | `""` | Author name to use for Git-Commits. |
| `git-author-email` | `true` when `git` target is used. | `""` | Author email to use for Git-Commits. |
| `git-access-token` | `false` | `""` | Git-Personal-Access-Token with write-permissions. |
| `github-app-id` | `false` | `""` | GitHub App-ID. |
| `github-app-installation-id` | `false` | `""` | GitHub App-Installation-ID. |

The operator will save all files with a specific folder structure as described below. When a `git-path` is configured, all folders above this path are not touched
from the application. Assuming that `git-path` is set to `dev-cluster/sboms`. When no `git-path` is given, the structure below is directly in the repository-root.
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ require (
github.com/go-git/go-git/v5 v5.4.2
github.com/go-logr/logr v1.2.3 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.8 // indirect
github.com/google/go-containerregistry v0.11.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,8 @@ github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXP
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
Expand Down
81 changes: 43 additions & 38 deletions internal/config.go
Original file line number Diff line number Diff line change
@@ -1,47 +1,52 @@
package internal

type Config struct {
Cron string `yaml:"cron" env:"SBOM_CRON" flag:"cron"`
Format string `yaml:"format" env:"SBOM_FORMAT" flag:"format"`
Targets []string `yaml:"targets" env:"SBOM_TARGETS" flag:"targets"`
IgnoreAnnotations bool `yaml:"ignoreAnnotations" env:"SBOM_IGNORE_ANNOTATIONS" flag:"ignore-annotations"`
GitWorkingTree string `yaml:"gitWorkingTree" env:"SBOM_GIT_WORKINGTREE" flag:"git-workingtree"`
GitRepository string `yaml:"gitRepository" env:"SBOM_GIT_REPOSITORY" flag:"git-repository"`
GitBranch string `yaml:"gitBranch" env:"SBOM_GIT_BRANCH" flag:"git-branch"`
GitPath string `yaml:"gitPath" env:"SBOM_GIT_PATH" flag:"git-path"`
GitAccessToken string `yaml:"gitAccessToken" env:"SBOM_GIT_ACCESS_TOKEN" flag:"git-access-token"`
GitAuthorName string `yaml:"gitAuthorName" env:"SBOM_GIT_AUTHOR_NAME" flag:"git-author-name"`
GitAuthorEmail string `yaml:"gitAuthorEmail" env:"SBOM_GIT_AUTHOR_EMAIL" flag:"git-author-email"`
PodLabelSelector string `yaml:"podLabelSelector" env:"SBOM_POD_LABEL_SELECTOR" flag:"pod-label-selector"`
NamespaceLabelSelector string `yaml:"namespaceLabelSelector" env:"SBOM_NAMESPACE_LABEL_SELECTOR" flag:"namespace-label-selector"`
DtrackBaseUrl string `yaml:"dtrackBaseUrl" env:"SBOM_DTRACK_BASE_URL" flag:"dtrack-base-url"`
DtrackApiKey string `yaml:"dtrackApiKey" env:"SBOM_DTRACK_API_KEY" flag:"dtrack-api-key"`
KubernetesClusterId string `yaml:"kubernetesClusterId" env:"SBOM_KUBERNETES_CLUSTER_ID" flag:"kubernetes-cluster-id"`
JobImage string `yaml:"jobImage" env:"SBOM_JOB_IMAGE" flag:"job-image"`
JobImagePullSecret string `yaml:"jobImagePullSecret" env:"SBOM_JOB_IMAGE_PULL_SECRET" flag:"job-image-pull-secret"`
JobTimeout int64 `yaml:"jobTimeout" env:"SBOM_JOB_TIMEOUT" flag:"job-timeout"`
OciRegistry string `yaml:"ociRegistry" env:"SBOM_OCI_REGISTRY" flag:"oci-registry"`
OciUser string `yaml:"ociUser" env:"SBOM_OCI_USER" flag:"oci-user"`
OciToken string `yaml:"ociToken" env:"SBOM_OCI_TOKEN" flag:"oci-token"`
FallbackPullSecret string `yaml:"fallbackPullSecret" env:"SBOM_FALLBACK_PULL_SECRET" flag:"fallback-pull-secret"`
Verbosity string `env:"SBOM_VERBOSITY" flag:"verbosity"`
Cron string `yaml:"cron" env:"SBOM_CRON" flag:"cron"`
Format string `yaml:"format" env:"SBOM_FORMAT" flag:"format"`
Targets []string `yaml:"targets" env:"SBOM_TARGETS" flag:"targets"`
IgnoreAnnotations bool `yaml:"ignoreAnnotations" env:"SBOM_IGNORE_ANNOTATIONS" flag:"ignore-annotations"`
GitWorkingTree string `yaml:"gitWorkingTree" env:"SBOM_GIT_WORKINGTREE" flag:"git-workingtree"`
GitRepository string `yaml:"gitRepository" env:"SBOM_GIT_REPOSITORY" flag:"git-repository"`
GitBranch string `yaml:"gitBranch" env:"SBOM_GIT_BRANCH" flag:"git-branch"`
GitPath string `yaml:"gitPath" env:"SBOM_GIT_PATH" flag:"git-path"`
GitAccessToken string `yaml:"gitAccessToken" env:"SBOM_GIT_ACCESS_TOKEN" flag:"git-access-token"`
GitAuthorName string `yaml:"gitAuthorName" env:"SBOM_GIT_AUTHOR_NAME" flag:"git-author-name"`
GitAuthorEmail string `yaml:"gitAuthorEmail" env:"SBOM_GIT_AUTHOR_EMAIL" flag:"git-author-email"`
GitHubAppId string `yaml:"githubAppId" env:"SBOM_GITHUB_APP_ID" flag:"github-app-id"`
GitHubAppInstallationId string `yaml:"githubAppInstallationId" env:"SBOM_GITHUB_APP_INSTALLATION_ID" flag:"github-app-installation-id"`
GitHubPrivateKey string `yaml:"githubAppPrivateKey" env:"SBOM_GITHUB_APP_PRIVATE_KEY"`
PodLabelSelector string `yaml:"podLabelSelector" env:"SBOM_POD_LABEL_SELECTOR" flag:"pod-label-selector"`
NamespaceLabelSelector string `yaml:"namespaceLabelSelector" env:"SBOM_NAMESPACE_LABEL_SELECTOR" flag:"namespace-label-selector"`
DtrackBaseUrl string `yaml:"dtrackBaseUrl" env:"SBOM_DTRACK_BASE_URL" flag:"dtrack-base-url"`
DtrackApiKey string `yaml:"dtrackApiKey" env:"SBOM_DTRACK_API_KEY" flag:"dtrack-api-key"`
KubernetesClusterId string `yaml:"kubernetesClusterId" env:"SBOM_KUBERNETES_CLUSTER_ID" flag:"kubernetes-cluster-id"`
JobImage string `yaml:"jobImage" env:"SBOM_JOB_IMAGE" flag:"job-image"`
JobImagePullSecret string `yaml:"jobImagePullSecret" env:"SBOM_JOB_IMAGE_PULL_SECRET" flag:"job-image-pull-secret"`
JobTimeout int64 `yaml:"jobTimeout" env:"SBOM_JOB_TIMEOUT" flag:"job-timeout"`
OciRegistry string `yaml:"ociRegistry" env:"SBOM_OCI_REGISTRY" flag:"oci-registry"`
OciUser string `yaml:"ociUser" env:"SBOM_OCI_USER" flag:"oci-user"`
OciToken string `yaml:"ociToken" env:"SBOM_OCI_TOKEN" flag:"oci-token"`
FallbackPullSecret string `yaml:"fallbackPullSecret" env:"SBOM_FALLBACK_PULL_SECRET" flag:"fallback-pull-secret"`
Verbosity string `env:"SBOM_VERBOSITY" flag:"verbosity"`
}

var (
ConfigKeyCron = "cron"
ConfigKeyFormat = "format"
ConfigKeyTargets = "targets"
ConfigKeyIgnoreAnnotations = "ignore-annotations"
ConfigKeyGitWorkingTree = "git-workingtree"
ConfigKeyGitRepository = "git-repository"
ConfigKeyGitBranch = "git-branch"
ConfigKeyGitPath = "git-path"
ConfigKeyGitAccessToken = "git-access-token"
ConfigKeyGitAuthorName = "git-author-name"
ConfigKeyGitAuthorEmail = "git-author-email"
ConfigKeyPodLabelSelector = "pod-label-selector"
ConfigKeyNamespaceLabelSelector = "namespace-label-selector"
ConfigKeyDependencyTrackBaseUrl = "dtrack-base-url"
ConfigKeyCron = "cron"
ConfigKeyFormat = "format"
ConfigKeyTargets = "targets"
ConfigKeyIgnoreAnnotations = "ignore-annotations"
ConfigKeyGitWorkingTree = "git-workingtree"
ConfigKeyGitRepository = "git-repository"
ConfigKeyGitBranch = "git-branch"
ConfigKeyGitPath = "git-path"
ConfigKeyGitAccessToken = "git-access-token"
ConfigKeyGitAuthorName = "git-author-name"
ConfigKeyGitAuthorEmail = "git-author-email"
ConfigKeyGitHubAppId = "github-app-id"
ConfigKeyGitHubAppInstallationId = "github-app-installation-id"
ConfigKeyPodLabelSelector = "pod-label-selector"
ConfigKeyNamespaceLabelSelector = "namespace-label-selector"
ConfigKeyDependencyTrackBaseUrl = "dtrack-base-url"
/* #nosec */
ConfigKeyDependencyTrackApiKey = "dtrack-api-key"
ConfigKeyKubernetesClusterId = "kubernetes-cluster-id"
Expand Down
5 changes: 4 additions & 1 deletion internal/processor/processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,10 @@ func initTargets(k8s *kubernetes.KubeClient) []target.Target {
token := internal.OperatorConfig.GitAccessToken
name := internal.OperatorConfig.GitAuthorName
email := internal.OperatorConfig.GitAuthorEmail
t := git.NewGitTarget(workingTree, workPath, repository, branch, token, name, email, format)
githubAppId := internal.OperatorConfig.GitHubAppId
githubAppInstallationId := internal.OperatorConfig.GitHubAppInstallationId
githubAppPrivateKey := internal.OperatorConfig.GitHubPrivateKey
t := git.NewGitTarget(workingTree, workPath, repository, branch, token, name, email, githubAppId, githubAppInstallationId, githubAppPrivateKey, format)
err = t.ValidateConfig()
targets = append(targets, t)
} else if ta == "dtrack" {
Expand Down
8 changes: 8 additions & 0 deletions internal/target/git/auth/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package auth

import "github.com/go-git/go-git/v5/plumbing/transport"

type GitAuthenticator interface {
IsAvailable() bool
ResolveAuth() (transport.AuthMethod, error)
}
137 changes: 137 additions & 0 deletions internal/target/git/auth/github.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package auth

import (
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"fmt"
"io"
"net/http"
"strings"
"time"

"github.com/go-git/go-git/v5/plumbing/transport"
githttp "github.com/go-git/go-git/v5/plumbing/transport/http"
"github.com/golang-jwt/jwt"
)

var (
githubURL = "https://api.github.com/app/installations"
)

type gitHubToken struct {
Token string `json:"token"`
ExpiresAt time.Time `json:"expires_at"`
}

type GitHubAuthenticator struct {
AppID string
AppInstallationID string
PrivateKey string
currentToken *gitHubToken
}

func (g *GitHubAuthenticator) IsAvailable() bool {
return g.AppID != "" && g.AppInstallationID != "" && g.PrivateKey != ""
}

func (g *GitHubAuthenticator) ResolveAuth() (transport.AuthMethod, error) {
if g.currentToken == nil || g.currentToken.ExpiresAt.Before(time.Now().Add(1*time.Minute)) {
token, err := getGitHubToken(g.PrivateKey, g.AppID, g.AppInstallationID)
if err != nil {
return nil, err
}

g.currentToken = token
}

return &githttp.BasicAuth{Username: "<token>", Password: g.currentToken.Token}, nil
}

func issueJWTFromPEM(key *rsa.PrivateKey, appID string) (string, error) {
claims := &jwt.StandardClaims{
IssuedAt: time.Now().Add(-1 * time.Minute).Unix(),
ExpiresAt: time.Now().Add(time.Minute * 5).Unix(),
Issuer: appID,
}

token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
ss, err := token.SignedString(key)
if err != nil {
return "", fmt.Errorf("cannot retrieve signed string: %w", err)
}

return ss, nil
}

func getInstallationToken(jwtToken, appInstallationID string) (*gitHubToken, error) {
url := strings.Join([]string{githubURL, appInstallationID, "access_tokens"}, "/")
responseBody := gitHubToken{}
client := &http.Client{}
req, err := http.NewRequest("POST", url, nil)
if err != nil {
return nil, fmt.Errorf("cannot create new request: %w", err)
}

req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", jwtToken))
req.Header.Set("Accept", "application/vnd.github.v3+json")
res, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("request execution failed: %w", err)
}

b, err := io.ReadAll(res.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}

if res.StatusCode < 200 || res.StatusCode > 300 {
return nil, fmt.Errorf("unexpected http-status %v: %s", res.StatusCode, string(b))
}

if err := json.Unmarshal(b, &responseBody); err != nil {
return nil, fmt.Errorf("cannot unmarshal response: %w", err)
}

return &responseBody, nil
}

func loadPEMFromBytes(key []byte) (*rsa.PrivateKey, error) {
b, _ := pem.Decode(key)
if b != nil {
key = b.Bytes
}

parsedKey, err := x509.ParsePKCS1PrivateKey(key)
if err != nil {
return nil, fmt.Errorf("cannot parse private key: %w", err)
}

return parsedKey, nil
}

func getGitHubToken(privateKey, appID, appInstallationID string) (*gitHubToken, error) {
pemBytes, err := base64.StdEncoding.DecodeString(privateKey)
if err != nil {
return nil, fmt.Errorf("failed to decode private-key from base64: %w", err)
}

key, err := loadPEMFromBytes(pemBytes)
if err != nil {
return nil, fmt.Errorf("failed to load PEM: %w", err)
}

jwtToken, err := issueJWTFromPEM(key, appID)
if err != nil {
return nil, fmt.Errorf("failed issue a jwt from PEM: %w", err)
}

token, err := getInstallationToken(jwtToken, appInstallationID)
if err != nil {
return nil, fmt.Errorf("unable to get installation-token: %w", err)
}

return token, nil
}
20 changes: 20 additions & 0 deletions internal/target/git/auth/github_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package auth

import (
"fmt"
"testing"
)

func TestGithub(t *testing.T) {
privateKey := "LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVBM3Q0L2dRN2ZoQkFsaVlod1BRTUpwNFh5K3Nwc2VtU2JOd1J5R01HL1B4THUrUGZGCkVaZTQ0dVdOZ2NtaWJGMVNFeDUvY2ZoZXFnL1R6YjlVUkhyLzQ3MjEyL3NvZlJLVUtPbmJ5TnhaQ1pJUUNVK1EKRDdxTkVWVC93NmlGbjkxNjFyRTNFWk1Rb0JkbDdUdTZXZXlWeWE4bU45S0tXWjQvTDR2aktlbFZrNzRrUnFMUApOdG1SalhsVmpBbWZML0piTzRMTmVvWndrRlpFcWE1eXBqM0cyckFoQ1BJcS9RK0MwT1hwYk5XRWVzKzNmV1BtCkVZOHN1dGpiTjlKbnpxc1ByZjFwdkFUY0RrU3EzMlFDalJuVjBMZTFKbVRhNUV6YkFZdmYxWVdIQjBVR1VaQWIKT1Bta3JLOEJ2QVRYY0FkRFpES21qS0VuVkdkQ292c2VlZFlRVlFJREFRQUJBb0lCQVFEUlhocHNaSXVJSmg4TgpjQlo0eXFURU11c0x2U1JoYXFnQjdIUS9XNndOVmYwWCtMT3lqa1VMS3JsZ0p3c21JQmtkaXZZUkhGSGFFMXBYCm5CbE81L29DZWRNaG9IWHQ0WWs4VWsyK0NYcS9xZTdVd0dERzFlcjJVRktoL1d2aXZnOUxKMXBqY1RRYUlVRUkKdDZhZUdjcXhtSkFmdXZOcnMyVUtockRXUzdTd1FCdnllVTdpOG50WUx0MTg2TXAvWjc2NWI0UVFCNXRqUlZRSwpaZHhadGxLVUc3a2tWdlNWdFdZcXNYWkwzV2RUK0tBQy85RUFQRzYrMHRlY0lkODVhUTgwREZManhlZUowdWk1CmJCYlVveFI2U1JpUnBDYnhZRWNWTU5wWitrMW9jME1NMndkZFYrOUxzc1BmSDFYeDNLKzYzUzJOYVcrWGZqOVYKRnNNU2ZleEJBb0dCQVBoOHBWTVRnYjBYVnRUaU9ISEJyQ2NKc0ZEWGNyWCtaZGV0NEErZWJiREExUHo5bmlDcQpOSmovOGgrN0R6RnNWcFJSWXU4NzF0ZUwzakpZOERIMEJKWGtVcXorRXNkWU95MkNFMDQyTFJZUHBEanNpWVpwCmNwT2pTYmRQMUYyMWtldDdoRW4vSmdwZUlSdTRoY0pKbW1iUDV6TTA0OTBFRDJ2UjVGN0pKcThsQW9HQkFPV2IKVG5NTEZWZFZnV2wxUlRnZjMyNjB6T3ZaeHlLbnluK25qVFhKenh5MDFad0pvYUpwSWxkYUYxa0ZsUGFTTnF6QwpoK1RFanRHU042Y1E3cTNQSTdzZWNxVW9NSHJUZmJGc0ZOS3d3Wk5ISjZnYmN5ODczWUg0V1RpS01salFURExqCjIrUHp5V212RTBxTldSMXZzamMvZGs1YXJ2SG1xNm1VWnNNTGNHMXhBb0dCQUp6SnlXZEovNnJZZHltTU5DRlQKR3RUbkFDR0NVQmNWbGgwbzBTQUp6eHlnRDhReUl2eHBhUmJCS0gxdEFvRWs0dHhqSGhIbHFBSUpkd0k5ckJGWQo3eXZGRFlXcUt4OHRST25LRVhSZ0tzS295KzQvRWVlRjBZM21ralRnWWhkZTRkdTc3QlRrc1pRSklNcmsrMEZFClAvQnZibXFaRE00QUtkcldiVXFCa2E4aEFvR0FTU1ljYlJ4WDFUWUJmNmFEd2VpdGx5aDByS0w2QlRsdGt4Ty8KTWlhMEhCSWtoNHl0K1A4RG1NNno1NEdXdnFOQVZmTzN0Wk5GYU5EcmVBYzE4eHV0NTN4QXpBc3N2c1RJSTVFcQpaU0U0Mm9IdHlXbk5YWHYyQ3VVakQ1Yy9pUG43Zks4SksrdEwvS1p4UCt3ZU92bU56bm9pWjc0T1JEVFg5Yk80CnRXVGNrWkVDZ1lBdDl0dDhneHlvVFpvQjl6d3RpakcrTFAxM3JteEg4d3c1VGZMODV3U2hDMElpZ3FITVJJT2gKeURHNkFiMnJ0cVNNNWN2M05TcGFFd2UxSzhjSVYzMkx2M3EyTzVjdUswNkc2MDVMMFVweDlZZ2pCVjByZkR0SQpqY2QvOSs4QkF3alB1TVgxRmZTTXNXNTFkdjdjRVd0bkNSaGNQZlphRFU1RExzWDNqNUtYWGc9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo="
appID := "236826"
appInstallationID := "29089394"

a := GitHubAuthenticator{PrivateKey: privateKey, AppID: appID, AppInstallationID: appInstallationID}
gitAuth, err := a.ResolveAuth()
if err != nil {
panic(err)
}

fmt.Println(gitAuth.String())
}
21 changes: 21 additions & 0 deletions internal/target/git/auth/token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package auth

import (
"github.com/go-git/go-git/v5/plumbing/transport"
"github.com/go-git/go-git/v5/plumbing/transport/http"
)

type TokenGitAuthenticator struct {
Token string
}

func (t *TokenGitAuthenticator) IsAvailable() bool {
return t.Token != ""
}

func (t *TokenGitAuthenticator) ResolveAuth() (transport.AuthMethod, error) {
return &http.BasicAuth{
Username: "<token>", // this can be anything except an empty string
Password: t.Token,
}, nil
}
Loading

0 comments on commit eb12f65

Please sign in to comment.