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

feat: ability to configure multiple tokendings instances #172

Merged
merged 8 commits into from
Aug 25, 2023
76 changes: 33 additions & 43 deletions controllers/jwker_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,18 @@ import (
)

const (
refreshTokenRetryInterval = 10 * time.Second
requeueInterval = 10 * time.Second
requeueInterval = 10 * time.Second
)

// JwkerReconciler reconciles a Jwker object
type JwkerReconciler struct {
client.Client
Log logr.Logger
Scheme *runtime.Scheme
Reader client.Reader
Recorder record.EventRecorder
logger logr.Logger
TokendingsToken *tokendings.TokenResponse
Config *config.Config
Log logr.Logger
Scheme *runtime.Scheme
Reader client.Reader
Recorder record.EventRecorder
logger logr.Logger
Config *config.Config
}

func (r *JwkerReconciler) appClientID(req ctrl.Request) tokendings.ClientId {
Expand All @@ -52,35 +50,19 @@ func (r *JwkerReconciler) appClientID(req ctrl.Request) tokendings.ClientId {
}
}

func (r *JwkerReconciler) CreateToken() {
jwk := r.Config.AuthProvider.ClientJwk
clientID := r.Config.AuthProvider.ClientID
endpoint := fmt.Sprintf("%s/registration/client", r.Config.Tokendings.BaseURL)

var err error

jwt, err := tokendings.ClientAssertion(jwk, clientID, endpoint)
if err != nil {
r.Log.Error(err, "failed to generate client assertion")
}
r.TokendingsToken = &tokendings.TokenResponse{
AccessToken: jwt,
TokenType: "Bearer",
ExpiresIn: 0,
Scope: "",
}
}

// delete all associated objects
// TODO: needs finalizer
func (r *JwkerReconciler) purge(ctx context.Context, req ctrl.Request) error {
aid := r.appClientID(req)

r.logger.Info(fmt.Sprintf("Jwker resource %s in namespace: %s has been deleted. Cleaning up resources", req.Name, req.Namespace))

r.logger.Info(fmt.Sprintf("Deleting resource %s in namespace %s from tokendings", req.Name, req.Namespace))
if err := tokendings.DeleteClient(ctx, r.TokendingsToken.AccessToken, r.Config.Tokendings.BaseURL, aid); err != nil {
return fmt.Errorf("deleting resource from Tokendings: %s", err)
r.logger.Info(fmt.Sprintf("Deleting resource %s in namespace %s from %d tokendings instances", req.Name, req.Namespace, len(r.Config.TokendingsInstances)))
for _, instance := range r.Config.TokendingsInstances {
r.logger.Info(fmt.Sprintf("Deleting client from tokendings instance %s", instance.BaseURL))
if err := instance.DeleteClient(ctx, aid); err != nil {
return fmt.Errorf("deleting resource from Tokendings instance '%s': %w", instance.BaseURL, err)
}
}

r.logger.Info(fmt.Sprintf("Deleting application %s jwker secrets in namespace %s from cluster", req.Name, req.Namespace))
Expand Down Expand Up @@ -158,7 +140,7 @@ func (r *JwkerReconciler) create(tx transaction) error {
app := r.appClientID(tx.req)

cr, err := tokendings.MakeClientRegistration(
r.Config.AuthProvider.ClientJwk,
r.Config.ClientJwk,
&tx.keyset.Public,
app,
tx.jwker,
Expand All @@ -168,12 +150,18 @@ func (r *JwkerReconciler) create(tx transaction) error {
return fmt.Errorf("create client registration payload: %s", err)
}

r.logger.Info(fmt.Sprintf("Registering app %s with tokendings", app.String()))
err = tokendings.RegisterClient(
*cr,
r.TokendingsToken.AccessToken,
r.Config.Tokendings.BaseURL,
)
instances := r.Config.TokendingsInstances
if len(instances) == 0 {
return fmt.Errorf("no tokendings instances configured, cannot create resources")
}
tommytroen marked this conversation as resolved.
Show resolved Hide resolved
r.logger.Info(fmt.Sprintf("Registering app %s with %d tokendings instances", app.String(), len(instances)))

for _, instance := range instances {
r.logger.Info(fmt.Sprintf("Registering client with tokendings instance %s", instance.BaseURL))
if err := instance.RegisterClient(*cr); err != nil {
return fmt.Errorf("registering client with Tokendings instance '%s': %w", instance.BaseURL, err)
}
}

if err != nil {
return fmt.Errorf("failed registering client: %s", err)
tommytroen marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -187,9 +175,13 @@ func (r *JwkerReconciler) create(tx transaction) error {
}

secretData := secret.PodSecretData{
ClientId: app,
Jwk: *jwk,
TokendingsConfig: r.Config.Tokendings,
ClientId: app,
Jwk: *jwk,
TokendingsConfig: config.Tokendings{
BaseURL: instances[0].BaseURL,
Metadata: instances[0].Metadata,
WellKnownURL: instances[0].WellKnownURL,
},
}

if err := secret.CreateAppSecret(r.Client, tx.ctx, tx.jwker.Spec.SecretName, secretData); err != nil {
Expand All @@ -209,8 +201,6 @@ func (r *JwkerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl

jwkermetrics.JwkersProcessedCount.Inc()

r.CreateToken()

r.logger = r.Log.WithValues(
"jwker", req.NamespacedName,
"jwker_name", req.Name,
Expand Down
43 changes: 22 additions & 21 deletions controllers/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"net/http/httptest"
"testing"
Expand All @@ -13,7 +14,6 @@ import (
nais_io_v1 "github.com/nais/liberator/pkg/apis/nais.io/v1"
"github.com/nais/liberator/pkg/crd"
"github.com/nais/liberator/pkg/events"
"github.com/nais/liberator/pkg/oauth"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
Expand Down Expand Up @@ -217,15 +217,16 @@ func TestReconciler(t *testing.T) {

tokendingsServer := httptest.NewServer(&tokendingsHandler{})
cfg, err := makeConfig(tokendingsServer.URL)
assert.NoError(t, err)
if err != nil {
log.Fatalf("unable to create tokendings instances: %+v", err)
}

jwker := &controllers.JwkerReconciler{
Client: cli,
Log: ctrl.Log.WithName("controllers").WithName("Jwker"),
Recorder: mgr.GetEventRecorderFor("jwker"),
Scheme: mgr.GetScheme(),
TokendingsToken: &tokendings.TokenResponse{},
Config: cfg,
Client: cli,
Log: ctrl.Log.WithName("controllers").WithName("Jwker"),
Recorder: mgr.GetEventRecorderFor("jwker"),
Scheme: mgr.GetScheme(),
Config: cfg,
}

err = jwker.SetupWithManager(mgr)
Expand Down Expand Up @@ -256,6 +257,15 @@ func TestReconciler(t *testing.T) {
// secret must have data
assert.NotEmpty(t, sec.Data[secret.TokenXPrivateJwkKey])

t.Run("should contain secret data", func(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, "local:default:app1", string(sec.Data[secret.TokenXClientIdKey]))
assert.Equal(t, fmt.Sprintf("%s/.well-known/oauth-authorization-server", tokendingsServer.URL), string(sec.Data[secret.TokenXWellKnownUrlKey]))
assert.Equal(t, fmt.Sprintf("%s", tokendingsServer.URL), string(sec.Data[secret.TokenXIssuerKey]))
assert.Equal(t, fmt.Sprintf("%s/jwks", tokendingsServer.URL), string(sec.Data[secret.TokenXJwksUriKey]))
assert.Equal(t, fmt.Sprintf("%s/token", tokendingsServer.URL), string(sec.Data[secret.TokenXTokenEndpointKey]))
})

// existing, in-use secret should be preserved
sec, err = getSecret(ctx, cli, namespace, alreadyInUseSecret)
assert.NoError(t, err)
Expand Down Expand Up @@ -367,20 +377,11 @@ func makeConfig(tokendingsURL string) (*config.Config, error) {
}

return &config.Config{
AuthProvider: config.AuthProvider{
ClientJwk: &jwk,
},
ClientID: "jwker",
ClientJwk: &jwk,
ClusterName: "local",
Tokendings: config.Tokendings{
BaseURL: tokendingsURL,
Metadata: &oauth.MetadataOAuth{
MetadataCommon: oauth.MetadataCommon{
Issuer: tokendingsURL,
JwksURI: tokendingsURL + "/jwks",
TokenEndpoint: tokendingsURL + "/token",
},
},
WellKnownURL: tokendingsURL + "/.well-known/oauth/authorization-server",
TokendingsInstances: []*tokendings.Instance{
tokendings.NewInstance(tokendingsURL, "jwker", &jwk),
},
}, nil
}
55 changes: 25 additions & 30 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,68 +3,63 @@ package config
import (
"flag"
"fmt"
"github.com/nais/jwker/pkg/tokendings"
"net/url"
"os"
"path"
"strings"

"github.com/nais/jwker/jwkutils"
"github.com/nais/liberator/pkg/oauth"
"gopkg.in/square/go-jose.v2"
)

type Config struct {
AuthProvider AuthProvider
ClusterName string
LogLevel string
MetricsAddr string
Tokendings Tokendings
ClientID string
ClientJwk *jose.JSONWebKey
ClusterName string
LogLevel string
MetricsAddr string
TokendingsInstances []*tokendings.Instance
}

type Tokendings struct {
BaseURL string
ClientID string
Metadata *oauth.MetadataOAuth
WellKnownURL string
}

type AuthProvider struct {
ClientID string
ClientJwkFile string
ClientJwk *jose.JSONWebKey
Metadata *oauth.MetadataOpenID
WellKnownURL string
}

func New() (*Config, error) {
cfg := &Config{}
cfg.Tokendings.Metadata = &oauth.MetadataOAuth{}
var clientJwkJson string
var instanceString string
var tokendingsURL string
flag.StringVar(&clientJwkJson, "client-jwk-json", os.Getenv("JWKER_PRIVATE_JWK"), "json with private JWK credential")
flag.StringVar(&cfg.AuthProvider.ClientID, "client-id", os.Getenv("JWKER_CLIENT_ID"), "Client ID of Jwker at Auth Provider.")
flag.StringVar(&cfg.AuthProvider.WellKnownURL, "auth-provider-well-known-url", os.Getenv("AUTH_PROVIDER_WELL_KNOWN_URL"), "Well-known URL to Auth Provider.")
flag.StringVar(&cfg.ClientID, "client-id", os.Getenv("JWKER_CLIENT_ID"), "Client ID of Jwker at Auth Provider.")
flag.StringVar(&cfg.ClusterName, "cluster-name", os.Getenv("CLUSTER_NAME"), "nais cluster")
flag.StringVar(&cfg.MetricsAddr, "metrics-addr", ":8181", "The address the metric endpoint binds to.")
flag.StringVar(&cfg.Tokendings.BaseURL, "tokendings-base-url", os.Getenv("TOKENDINGS_URL"), "Base URL to Tokendings.")
flag.StringVar(&cfg.Tokendings.ClientID, "tokendings-client-id", os.Getenv("TOKENDINGS_CLIENT_ID"), "Client ID of Tokendings at Auth Provider")
flag.StringVar(&tokendingsURL, "tokendings-base-url", os.Getenv("TOKENDINGS_URL"), "The base URL to Tokendings.")
flag.StringVar(&instanceString, "tokendings-instances", os.Getenv("TOKENDINGS_INSTANCES"), "Comma separated list of baseUrls to Tokendings instances.")
flag.StringVar(&cfg.LogLevel, "log-level", "info", "Log level for jwker")
flag.Parse()

j, err := jwkutils.ParseJWK([]byte(clientJwkJson))
if err != nil {
return nil, err
}
cfg.AuthProvider.ClientJwk = j

cfg.Tokendings.Metadata.Issuer = cfg.Tokendings.BaseURL
cfg.Tokendings.Metadata.JwksURI = fmt.Sprintf("%s/jwks", cfg.Tokendings.BaseURL)
cfg.Tokendings.Metadata.TokenEndpoint = fmt.Sprintf("%s/token", cfg.Tokendings.BaseURL)
cfg.ClientJwk = j

tokendingsWellKnownURL, err := url.Parse(cfg.Tokendings.BaseURL)
if err != nil {
return nil, fmt.Errorf("invalid base url for tokendings: %w", err)
instances := make([]*tokendings.Instance, 0)
raw := strings.TrimSpace(instanceString)
if raw == "" {
raw = tokendingsURL
}
for _, u := range strings.Split(raw, ",") {
_, err := url.Parse(strings.TrimSpace(u))
if err != nil {
return nil, fmt.Errorf("invalid base url for tokendings instance: %w", err)
}
instances = append(instances, tokendings.NewInstance(u, cfg.ClientID, cfg.ClientJwk))
}
tokendingsWellKnownURL.Path = path.Join(tokendingsWellKnownURL.Path, oauth.WellKnownOAuthPath)
cfg.Tokendings.WellKnownURL = tokendingsWellKnownURL.String()

return cfg, nil
}
Loading
Loading