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

Add expiration seconds to binding params #1189

Merged
5 changes: 2 additions & 3 deletions cmd/broker/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,7 @@ type Config struct {

RuntimeConfigurationConfigMapName string `envconfig:"default=keb-runtime-config"`

UpdateRuntimeResourceDelay time.Duration `envconfig:"default=4s"`
BindingTokenExpirationSeconds int `envconfig:"default=600"`
UpdateRuntimeResourceDelay time.Duration `envconfig:"default=4s"`
}

type ProfilerConfig struct {
Expand Down Expand Up @@ -447,7 +446,7 @@ func createAPI(router *mux.Router, servicesConfig broker.ServicesConfig, planVal
planDefaults, logs, cfg.KymaDashboardConfig, kcBuilder, convergedCloudRegionProvider, kcpK8sClient),
GetInstanceEndpoint: broker.NewGetInstance(cfg.Broker, db.Instances(), db.Operations(), kcBuilder, logs),
LastOperationEndpoint: broker.NewLastOperation(db.Operations(), db.InstancesArchived(), logs),
BindEndpoint: broker.NewBind(cfg.Broker.Binding, db.Instances(), logs, clientProvider, kubeconfigProvider, gardenerClient, cfg.BindingTokenExpirationSeconds),
BindEndpoint: broker.NewBind(cfg.Broker.Binding, db.Instances(), logs, clientProvider, kubeconfigProvider, gardenerClient),
UnbindEndpoint: broker.NewUnbind(logs),
GetBindingEndpoint: broker.NewGetBinding(logs),
LastBindingOperationEndpoint: broker.NewLastBindingOperation(logs),
Expand Down
7 changes: 4 additions & 3 deletions docs/user/05-60-kyma-bindings.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ X-Broker-API-Version: 2.14

The Broker returns a kubeconfig file in the response body. The kubeconfig file contains the necessary information to access the managed Kyma cluster. By default, KEB uses [`shoots/adminkubeconfig`](https://github.com/gardener/gardener/blob/master/docs/usage/shoot_access.md#shootsadminkubeconfig-subresource) subresources to generate a kubeconfig that uses certificates to authenticate its user. To customize the format of the returned kubeconfig, use the `parameters` field of the request body:

| Name | Default | Description |
|---|---|---|
| **token_request** | `false` | If set to `true`, the Broker returns a kubeconfig with a JWT token used as a user authentication mechanism. The token is generated using Kubernetes TokenRequest attached to a ServiceAccount, ClusterRole, and ClusterRoleBinding, all named `kyma-binding-{{binding_id}}`. Such an approach allows for easily modifying the permissions granted to the kubeconfig. |
| Name | Default | Description |
|------------------------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **token_request** | `false` | If set to `true`, the Broker returns a kubeconfig with a JWT token used as a user authentication mechanism. The token is generated using Kubernetes TokenRequest attached to a ServiceAccount, ClusterRole, and ClusterRoleBinding, all named `kyma-binding-{{binding_id}}`. Such an approach allows for easily modifying the permissions granted to the kubeconfig. |
| **expiration_seconds** | `600` | Specifies the duration (in seconds) for which the generated kubeconfig is valid. If not provided, the default value of `600` seconds (10 minutes) is used. |
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ require (
github.com/gardener/gardener v1.104.0
github.com/go-co-op/gocron v1.37.0
github.com/gocraft/dbr v0.0.0-20190714181702-8114670a83bd
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/google/uuid v1.6.0
github.com/gorilla/handlers v1.5.2
github.com/gorilla/mux v1.8.1
Expand Down Expand Up @@ -74,7 +75,6 @@ require (
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-sql-driver/mysql v1.5.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect
github.com/google/go-cmp v0.6.0 // indirect
Expand Down
39 changes: 25 additions & 14 deletions internal/broker/bind_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ import (
)

type BindingConfig struct {
Enabled bool `envconfig:"default=false"`
BindablePlans EnablePlans `envconfig:"default=aws"`
Enabled bool `envconfig:"default=false"`
BindablePlans EnablePlans `envconfig:"default=aws"`
ExpirationSeconds int `envconfig:"default=600"`
}

type BindEndpoint struct {
Expand All @@ -34,17 +35,18 @@ type BindEndpoint struct {
}

type BindingParams struct {
TokenRequest bool `json:"token_request,omit"`
TokenRequest bool `json:"token_request,omit"`
ExpirationSeconds int `json:"expiration_seconds,omit"`
}

type Credentials struct {
Kubeconfig string `json:"kubeconfig"`
}

func NewBind(cfg BindingConfig, instanceStorage storage.Instances, log logrus.FieldLogger, clientProvider broker.ClientProvider, kubeconfigProvider broker.KubeconfigProvider, gardenerClient client.Client, tokenExpirationSeconds int) *BindEndpoint {
func NewBind(cfg BindingConfig, instanceStorage storage.Instances, log logrus.FieldLogger, clientProvider broker.ClientProvider, kubeconfigProvider broker.KubeconfigProvider, gardenerClient client.Client) *BindEndpoint {
return &BindEndpoint{config: cfg, instancesStorage: instanceStorage, log: log.WithField("service", "BindEndpoint"),
tokenRequestBindingManager: broker.NewTokenRequestBindingsManager(clientProvider, kubeconfigProvider, tokenExpirationSeconds),
gardenerBindingsManager: broker.NewGardenerBindingManager(gardenerClient, tokenExpirationSeconds),
tokenRequestBindingManager: broker.NewTokenRequestBindingsManager(clientProvider, kubeconfigProvider),
gardenerBindingsManager: broker.NewGardenerBindingManager(gardenerClient),
}
}

Expand Down Expand Up @@ -86,23 +88,32 @@ func (b *BindEndpoint) Bind(ctx context.Context, instanceID, bindingID string, d
}

var parameters BindingParams
err = json.Unmarshal(details.RawParameters, &parameters)
if err != nil {
message := fmt.Sprintf("failed to unmarshal parameters: %s", err)
return domain.Binding{}, apiresponses.NewFailureResponse(fmt.Errorf(message), http.StatusInternalServerError, message)
if len(details.RawParameters) != 0 {
err = json.Unmarshal(details.RawParameters, &parameters)
if err != nil {
message := fmt.Sprintf("failed to unmarshal parameters: %s", err)
return domain.Binding{}, apiresponses.NewFailureResponse(fmt.Errorf(message), http.StatusInternalServerError, message)
}
}

expirationSeconds := b.config.ExpirationSeconds
if parameters.ExpirationSeconds != 0 {
expirationSeconds = parameters.ExpirationSeconds
}

var kubeconfig string
if parameters.TokenRequest {
// get kubeconfig for the instance
kubeconfig, err = b.tokenRequestBindingManager.Create(ctx, instance, bindingID)
kubeconfig, err = b.tokenRequestBindingManager.Create(ctx, instance, bindingID, expirationSeconds)
if err != nil {
return domain.Binding{}, fmt.Errorf("failed to create kyma binding using token requests: %s", err)
message := fmt.Sprintf("failed to create kyma binding using token requests: %s", err)
return domain.Binding{}, apiresponses.NewFailureResponse(fmt.Errorf(message), http.StatusBadRequest, message)
}
} else {
kubeconfig, err = b.gardenerBindingsManager.Create(ctx, instance, bindingID)
kubeconfig, err = b.gardenerBindingsManager.Create(ctx, instance, bindingID, expirationSeconds)
if err != nil {
return domain.Binding{}, fmt.Errorf("failed to create kyma binding using adminkubeconfig gardener subresource: %s", err)
message := fmt.Sprintf("failed to create kyma binding using adminkubeconfig gardener subresource: %s", err)
return domain.Binding{}, apiresponses.NewFailureResponse(fmt.Errorf(message), http.StatusBadRequest, message)
}
}

Expand Down
113 changes: 95 additions & 18 deletions internal/broker/bind_create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import (
"testing"
"time"

"github.com/golang-jwt/jwt/v4"
"gopkg.in/yaml.v2"

"code.cloudfoundry.org/lager"
"github.com/gorilla/mux"
"github.com/kyma-project/kyma-environment-broker/internal"
Expand All @@ -36,6 +39,19 @@ import (
"sigs.k8s.io/controller-runtime/pkg/envtest"
)

type Kubeconfig struct {
Users []User `yaml:"users"`
}

type User struct {
Name string `yaml:"name"`
User struct {
Token string `yaml:"token"`
} `yaml:"user"`
}

const expirationSeconds = 10000

func TestCreateBindingEndpoint(t *testing.T) {
t.Log("test create binding endpoint")

Expand Down Expand Up @@ -137,10 +153,11 @@ func TestCreateBindingEndpoint(t *testing.T) {
BindablePlans: EnablePlans{
fixture.PlanName,
},
ExpirationSeconds: expirationSeconds,
}

//// api handler
bindEndpoint := NewBind(*bindingCfg, db.Instances(), logs, skrK8sClientProvider, skrK8sClientProvider, gardenerClient, 10000)
bindEndpoint := NewBind(*bindingCfg, db.Instances(), logs, skrK8sClientProvider, skrK8sClientProvider, gardenerClient)
apiHandler := handlers.NewApiHandler(KymaEnvironmentBroker{
nil,
nil,
Expand Down Expand Up @@ -172,28 +189,18 @@ func TestCreateBindingEndpoint(t *testing.T) {
}
ralikio marked this conversation as resolved.
Show resolved Hide resolved
}`, fixture.PlanId), t)

// Then
require.Equal(
t, http.StatusCreated, response.StatusCode,
)
//// parse response
content, err := io.ReadAll(response.Body)
t.Logf("response content is: %v", string(content))
assert.NoError(t, err)
defer response.Body.Close()
binding := verifyResponse(t, response)

//// verify response content
assert.Contains(t, string(content), "credentials")
credentials, ok := binding.Credentials.(map[string]interface{})
require.True(t, ok)
kubeconfig := credentials["kubeconfig"].(string)

var binding domain.Binding
err = json.Unmarshal(content, &binding)
duration, err := getTokenDuration(t, kubeconfig)
require.NoError(t, err)
t.Logf("binding: %v", binding.Credentials)
assert.Equal(t, expirationSeconds*time.Second, duration)

//// verify connectivity using kubeconfig from the generated binding
credentials, ok := binding.Credentials.(map[string]interface{})
require.True(t, ok)
newClient := kubeconfigClient(t, credentials["kubeconfig"].(string))
newClient := kubeconfigClient(t, kubeconfig)
_, err = newClient.CoreV1().Secrets("default").Get(context.Background(), "secret-to-check", v1.GetOptions{})
assert.NoError(t, err)

Expand All @@ -204,6 +211,29 @@ func TestCreateBindingEndpoint(t *testing.T) {
_, err = newClient.RbacV1().ClusterRoleBindings().Get(context.Background(), "kyma-binding-binding-id", v1.GetOptions{})
assert.NoError(t, err)
})

t.Run("should create a new service binding with custom token expiration time", func(t *testing.T) {
const customExpirationSeconds = 900

// When
response := CallAPI(httpServer, method, "v2/service_instances/1/service_bindings/binding-id2?accepts_incomplete=true", fmt.Sprintf(`{
"service_id": "123",
"plan_id": "%s",
"parameters": {
"token_request": true,
"expiration_seconds": %v
}
}`, fixture.PlanId, customExpirationSeconds), t)

binding := verifyResponse(t, response)

credentials, ok := binding.Credentials.(map[string]interface{})
require.True(t, ok)

duration, err := getTokenDuration(t, credentials["kubeconfig"].(string))
require.NoError(t, err)
assert.Equal(t, customExpirationSeconds*time.Second, duration)
})
}

func createKubeconfigFileForRestConfig(restConfig rest.Config) []byte {
Expand Down Expand Up @@ -280,3 +310,50 @@ func kubeconfigClient(t *testing.T, kubeconfig string) *kubernetes.Clientset {

return clientset
}

func verifyResponse(t *testing.T, response *http.Response) domain.Binding {
require.Equal(t, http.StatusCreated, response.StatusCode)

content, err := io.ReadAll(response.Body)
require.NoError(t, err)
defer response.Body.Close()

t.Logf("response content is: %v", string(content))

assert.Contains(t, string(content), "credentials")

var binding domain.Binding
err = json.Unmarshal(content, &binding)
require.NoError(t, err)

t.Logf("binding: %v", binding.Credentials)

return binding
}

func getTokenDuration(t *testing.T, config string) (time.Duration, error) {
var kubeconfig Kubeconfig

err := yaml.Unmarshal([]byte(config), &kubeconfig)
require.NoError(t, err)

for _, user := range kubeconfig.Users {
if user.Name == "context" {
token, _, err := new(jwt.Parser).ParseUnverified(user.User.Token, jwt.MapClaims{})
require.NoError(t, err)

if claims, ok := token.Claims.(jwt.MapClaims); ok {
iat := int64(claims["iat"].(float64))
exp := int64(claims["exp"].(float64))

issuedAt := time.Unix(iat, 0)
expiresAt := time.Unix(exp, 0)

return expiresAt.Sub(issuedAt), nil
} else {
return 0, fmt.Errorf("invalid token claims")
}
}
}
return 0, fmt.Errorf("user with name 'context' not found")
}
18 changes: 8 additions & 10 deletions internal/broker/bindings/bindings_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ type Credentials struct {
}

type BindingsManager interface {
Create(ctx context.Context, instance *internal.Instance, bindingID string) (string, error)
Create(ctx context.Context, instance *internal.Instance, bindingID string, expirationSeconds int) (string, error)
}

type ClientProvider interface {
Expand All @@ -30,20 +30,18 @@ type KubeconfigProvider interface {
}

type TokenRequestBindingsManager struct {
clientProvider ClientProvider
tokenExpirationSeconds int
kubeconfigBuilder *kubeconfig.Builder
clientProvider ClientProvider
kubeconfigBuilder *kubeconfig.Builder
}

func NewTokenRequestBindingsManager(clientProvider ClientProvider, kubeconfigProvider KubeconfigProvider, tokenExpirationSeconds int) *TokenRequestBindingsManager {
func NewTokenRequestBindingsManager(clientProvider ClientProvider, kubeconfigProvider KubeconfigProvider) *TokenRequestBindingsManager {
return &TokenRequestBindingsManager{
clientProvider: clientProvider,
tokenExpirationSeconds: tokenExpirationSeconds,
kubeconfigBuilder: kubeconfig.NewBuilder(nil, nil, kubeconfigProvider),
clientProvider: clientProvider,
kubeconfigBuilder: kubeconfig.NewBuilder(nil, nil, kubeconfigProvider),
}
}

func (c *TokenRequestBindingsManager) Create(ctx context.Context, instance *internal.Instance, bindingID string) (string, error) {
func (c *TokenRequestBindingsManager) Create(ctx context.Context, instance *internal.Instance, bindingID string, expirationSeconds int) (string, error) {
clientset, err := c.clientProvider.K8sClientSetForRuntimeID(instance.RuntimeID)

if err != nil {
Expand Down Expand Up @@ -116,7 +114,7 @@ func (c *TokenRequestBindingsManager) Create(ctx context.Context, instance *inte
Labels: map[string]string{"app.kubernetes.io/managed-by": "kcp-kyma-environment-broker"},
},
Spec: authv1.TokenRequestSpec{
ExpirationSeconds: ptr.Integer64(int64(c.tokenExpirationSeconds)),
ExpirationSeconds: ptr.Integer64(int64(expirationSeconds)),
},
}

Expand Down
12 changes: 5 additions & 7 deletions internal/broker/bindings/gardener_request_bindingmanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,16 @@ import (
)

type GardenerBindingManager struct {
tokenExpirationSeconds int
gardenerClient client.Client
gardenerClient client.Client
}

func NewGardenerBindingManager(gardenerClient client.Client, tokenExpirationSeconds int) *GardenerBindingManager {
func NewGardenerBindingManager(gardenerClient client.Client) *GardenerBindingManager {
return &GardenerBindingManager{
gardenerClient: gardenerClient,
tokenExpirationSeconds: tokenExpirationSeconds,
gardenerClient: gardenerClient,
}
}

func (c *GardenerBindingManager) Create(ctx context.Context, instance *internal.Instance, bindingID string) (string, error) {
func (c *GardenerBindingManager) Create(ctx context.Context, instance *internal.Instance, bindingID string, expirationSeconds int) (string, error) {

shoot := &shoot.Shoot{
TypeMeta: metav1.TypeMeta{APIVersion: "core.gardener.cloud/v1beta1", Kind: "Shoot"},
Expand All @@ -36,7 +34,7 @@ func (c *GardenerBindingManager) Create(ctx context.Context, instance *internal.

adminKubeconfigRequest := &authenticationv1alpha1.AdminKubeconfigRequest{
Spec: authenticationv1alpha1.AdminKubeconfigRequestSpec{
ExpirationSeconds: ptr.Integer64(int64(c.tokenExpirationSeconds)),
ExpirationSeconds: ptr.Integer64(int64(expirationSeconds)),
},
}

Expand Down
Loading