Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Thread FulcioConfig through from main via ctx #249

Merged
merged 1 commit into from
Dec 1, 2021
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ SERVER_LDFLAGS="-X $(SERVER_PKG).gitVersion=$(GIT_VERSION) -X $(SERVER_PKG).gitC
SWAGGER := $(TOOLS_BIN_DIR)/swagger

$(GENSRC): $(SWAGGER) $(OPENAPIDEPS)
$(SWAGGER) generate server -f openapi.yaml -q -r COPYRIGHT.txt -t pkg/generated --exclude-main -A fulcio_server --exclude-spec --flag-strategy=pflag -P github.com/coreos/go-oidc/v3/oidc.IDToken --additional-initialism=SCT
$(SWAGGER) generate client -f openapi.yaml -q -r COPYRIGHT.txt -t pkg/generated -P github.com/coreos/go-oidc/v3/oidc.IDToken
$(SWAGGER) generate server -f openapi.yaml -q -r COPYRIGHT.txt -t pkg/generated --exclude-main -A fulcio_server --exclude-spec --flag-strategy=pflag --principal github.com/coreos/go-oidc/v3/oidc.IDToken --additional-initialism=SCT
$(SWAGGER) generate client -f openapi.yaml -q -r COPYRIGHT.txt -t pkg/generated --principal github.com/coreos/go-oidc/v3/oidc.IDToken
Comment on lines +52 to +53
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did this because it wasn't clear to me what -P was.


# this exists to override pattern match rule above since this file is in the generated directory but should not be treated as generated code
pkg/generated/restapi/configure_fulcio_server.go: $(OPENAPIDEPS)
Expand Down
5 changes: 3 additions & 2 deletions cmd/app/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package app

import (
"context"
"os"

"github.com/spf13/cobra"
Expand All @@ -37,8 +38,8 @@ var rootCmd = &cobra.Command{

// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
if err := rootCmd.Execute(); err != nil {
func Execute(ctx context.Context) {
if err := rootCmd.ExecuteContext(ctx); err != nil {
log.Logger.Error(err)
os.Exit(1)
}
Expand Down
19 changes: 17 additions & 2 deletions cmd/app/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,28 @@ var serveCmd = &cobra.Command{
}
}()

// This will panic if we can't parse the config correctly
config.Config()
cfg, err := config.Load()
if err != nil {
log.Logger.Fatalf("error loading config: %v", err)
}

server.EnabledListeners = []string{"http"}

server.ConfigureAPI()

h := server.GetHandler()
server.SetHandler(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()

// For each request, infuse context with our snapshot of the FulcioConfig.
// TODO(mattmoor): Consider periodically (every minute?) refreshing the ConfigMap
// from disk, so that we don't need to cycle pods to pick up config updates.
// Alternately we could take advantage of Knative's configmap watcher.
ctx = config.With(ctx, cfg)

h.ServeHTTP(rw, r.WithContext(ctx))
}))

http.Handle("/metrics", promhttp.Handler())
go func() {
_ = http.ListenAndServe(":2112", nil)
Expand Down
8 changes: 6 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@

package main

import "github.com/sigstore/fulcio/cmd/app"
import (
"context"

"github.com/sigstore/fulcio/cmd/app"
)

func main() {
app.Execute()
app.Execute(context.Background())
}
3 changes: 1 addition & 2 deletions pkg/api/ca.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,7 @@ func SigningCertHandler(params operations.SigningCertParams, principal *oidc.IDT
}

func ExtractSubject(ctx context.Context, tok *oidc.IDToken, publicKey crypto.PublicKey, challenge []byte) (*challenges.ChallengeResult, error) {
cfg := config.Config()
iss, ok := cfg.GetIssuer(tok.Issuer)
iss, ok := config.FromContext(ctx).GetIssuer(tok.Issuer)
if !ok {
return nil, fmt.Errorf("configuration can not be loaded for issuer %v", tok.Issuer)
}
Expand Down
12 changes: 4 additions & 8 deletions pkg/challenges/challenges.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,7 @@ func Email(ctx context.Context, principal *oidc.IDToken, pubKey crypto.PublicKey
return nil, err
}

globalCfg := config.Config()
cfg, ok := globalCfg.GetIssuer(principal.Issuer)
cfg, ok := config.FromContext(ctx).GetIssuer(principal.Issuer)
if !ok {
return nil, errors.New("invalid configuration for OIDC ID Token issuer")
}
Expand All @@ -103,8 +102,7 @@ func Spiffe(ctx context.Context, principal *oidc.IDToken, pubKey crypto.PublicKe

spiffeID := principal.Subject

globalCfg := config.Config()
cfg, ok := globalCfg.GetIssuer(principal.Issuer)
cfg, ok := config.FromContext(ctx).GetIssuer(principal.Issuer)
if !ok {
return nil, errors.New("invalid configuration for OIDC ID Token issuer")
}
Expand Down Expand Up @@ -150,8 +148,7 @@ func Kubernetes(ctx context.Context, principal *oidc.IDToken, pubKey crypto.Publ
return nil, err
}

globalCfg := config.Config()
cfg, ok := globalCfg.GetIssuer(principal.Issuer)
cfg, ok := config.FromContext(ctx).GetIssuer(principal.Issuer)
if !ok {
return nil, errors.New("invalid configuration for OIDC ID Token issuer")
}
Expand Down Expand Up @@ -185,8 +182,7 @@ func GithubWorkflow(ctx context.Context, principal *oidc.IDToken, pubKey crypto.
return nil, err
}

globalCfg := config.Config()
cfg, ok := globalCfg.GetIssuer(principal.Issuer)
cfg, ok := config.FromContext(ctx).GetIssuer(principal.Issuer)
if !ok {
return nil, errors.New("invalid configuration for OIDC ID Token issuer")
}
Expand Down
52 changes: 29 additions & 23 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import (
"os"
"regexp"
"strings"
"sync"

"github.com/coreos/go-oidc/v3/oidc"
lru "github.com/hashicorp/golang-lru"
Expand Down Expand Up @@ -172,40 +171,47 @@ var DefaultConfig = &FulcioConfig{
},
}

var (
once sync.Once
config *FulcioConfig
originalTransport = http.DefaultTransport
)
var originalTransport = http.DefaultTransport

func Config() *FulcioConfig {
once.Do(func() {
if err := load(viper.GetString("config-path")); err != nil {
log.Logger.Panic(err)
}
})
return config
type configKey struct{}

func Load() (*FulcioConfig, error) {
return load(viper.GetString("config-path"))
}

func With(ctx context.Context, cfg *FulcioConfig) context.Context {
ctx = context.WithValue(ctx, configKey{}, cfg)
return ctx
}

func FromContext(ctx context.Context) *FulcioConfig {
untyped := ctx.Value(configKey{})
if untyped == nil {
return nil
}
return untyped.(*FulcioConfig)
}

// Load a config from disk, or use defaults
func load(configPath string) error {
func load(configPath string) (*FulcioConfig, error) {
var config *FulcioConfig
if _, err := os.Stat(configPath); os.IsNotExist(err) {
log.Logger.Infof("No config at %s, using defaults: %v", configPath, DefaultConfig)
config = DefaultConfig
cache, err := lru.New2Q(100 /* size */)
if err != nil {
return err
return nil, err
}
config.lru = cache
return nil
return config, nil
}
b, err := ioutil.ReadFile(configPath)
if err != nil {
return err
return nil, err
}
cfg, err := parseConfig(b)
if err != nil {
return err
return nil, err
}

if _, ok := cfg.GetIssuer("https://kubernetes.default.svc"); ok {
Expand All @@ -218,10 +224,10 @@ func load(configPath string) error {
const k8sCA = "/var/run/fulcio/ca.crt"
certs, err := ioutil.ReadFile(k8sCA)
if err != nil {
return err
return nil, err
}
if ok := rootCAs.AppendCertsFromPEM(certs); !ok {
return err
return nil, err
}

t := originalTransport.(*http.Transport).Clone()
Expand All @@ -239,19 +245,19 @@ func load(configPath string) error {
for _, iss := range cfg.OIDCIssuers {
provider, err := oidc.NewProvider(context.Background(), iss.IssuerURL)
if err != nil {
return err
return nil, err
}
verifier := provider.Verifier(&oidc.Config{ClientID: iss.ClientID})
cfg.verifiers[iss.IssuerURL] = verifier
}

cache, err := lru.New2Q(100 /* size */)
if err != nil {
return err
return nil, err
}
cfg.lru = cache

config = cfg
log.Logger.Infof("Loaded config %v from %s", cfg, configPath)
return nil
return config, nil
}
19 changes: 15 additions & 4 deletions pkg/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package config

import (
"context"
"io/ioutil"
"path/filepath"
"testing"
Expand Down Expand Up @@ -101,7 +102,7 @@ func TestLoad(t *testing.T) {
}
viper.GetViper().Set("config-path", cfgPath)

cfg := Config()
cfg, _ := Load()
got, ok := cfg.GetIssuer("https://accounts.google.com")
if !ok {
t.Error("expected true, got false")
Expand Down Expand Up @@ -137,13 +138,23 @@ func TestLoadDefaults(t *testing.T) {

// Don't put anything here!
cfgPath := filepath.Join(td, "config.json")
if err := load(cfgPath); err != nil {
cfg, err := load(cfgPath)
if err != nil {
t.Fatal(err)
}

cfg := Config()

if diff := cmp.Diff(DefaultConfig, cfg, cmpopts.IgnoreUnexported(FulcioConfig{})); diff != "" {
t.Errorf("DefaultConfig(): -want +got: %s", diff)
}

ctx := context.Background()

if got := FromContext(ctx); nil != got {
t.Errorf("FromContext(): %#v, wanted nil", got)
}

ctx = With(ctx, cfg)
if diff := cmp.Diff(cfg, FromContext(ctx), cmpopts.IgnoreUnexported(FulcioConfig{})); diff != "" {
t.Errorf("FromContext(): -want +got: %s", diff)
}
}
35 changes: 26 additions & 9 deletions pkg/generated/restapi/configure_fulcio_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ import (
// using embed to add the static html page duing build time
_ "embed"

"github.com/coreos/go-oidc/v3/oidc"
"github.com/go-chi/chi/middleware"
goaerrors "github.com/go-openapi/errors"
"github.com/go-openapi/runtime"
"github.com/go-openapi/runtime/security"
"github.com/mitchellh/mapstructure"
"github.com/rs/cors"

Expand Down Expand Up @@ -88,29 +88,47 @@ func configureAPI(api *operations.FulcioServerAPI) http.Handler {
api.JSONConsumer = runtime.JSONConsumer()
api.ApplicationPemCertificateChainProducer = runtime.TextProducer()

// OIDC objects used for authentication
fulcioCfg := config.Config()

api.BearerAuth = func(token string) (*oidc.IDToken, error) {
authenticate := func(ctx context.Context, token string) (interface{}, error) {
token = strings.Replace(token, "Bearer ", "", 1)

issuer, err := extractIssuer(token)
if err != nil {
return nil, goaerrors.New(http.StatusBadRequest, err.Error())
}

verifier, ok := fulcioCfg.GetVerifier(issuer)
verifier, ok := config.FromContext(ctx).GetVerifier(issuer)
if !ok {
return nil, goaerrors.New(http.StatusBadRequest, fmt.Sprintf("unsupported issuer: %s", issuer))
}

idToken, err := verifier.Verify(context.Background(), token)
idToken, err := verifier.Verify(ctx, token)
if err != nil {
return nil, goaerrors.New(http.StatusUnauthorized, err.Error())
}
return idToken, nil
}

api.APIKeyAuthenticator = func(name, in string, _ security.TokenAuthentication) runtime.Authenticator {
return security.HttpAuthenticator(func(r *http.Request) (bool, interface{}, error) {
var token string
switch strings.ToLower(in) {
case "header":
token = r.Header.Get(name)
case "query":
token = r.URL.Query().Get(name)
default:
return false, nil, goaerrors.New(500, "api key auth: in value needs to be either \"query\" or \"header\".")
}

if token == "" {
return false, nil, nil
}

p, err := authenticate(r.Context(), token)
return true, p, err
})
}

// Select which CA / KMS system to use
// Currently supported:
// googleca: Google Certficate Authority Service
Expand Down Expand Up @@ -204,10 +222,9 @@ func logAndServeError(w http.ResponseWriter, r *http.Request, err error) {
// errors should always be in JSON
w.Header()["Content-Type"] = []string{"application/json"}
if e, ok := err.(goaerrors.Error); ok && e.Code() == http.StatusUnauthorized {
fulcioCfg := config.Config()
// this is set directly so the header name is not canonicalized
issuers := []string{}
for iss := range fulcioCfg.OIDCIssuers {
for iss := range config.FromContext(r.Context()).OIDCIssuers {
issuers = append(issuers, "Bearer realm=\""+iss+"\",scope=\"openid email\"")
}
w.Header()["WWW-Authenticate"] = []string{strings.Join(issuers, ", ")}
Expand Down