Skip to content

Commit 697bc4c

Browse files
authored
Refactor for session management (#193)
Previously every Handler function was receiving the session token in the form of a jwt string, in consequence every time we want to access the encrypted claims of the jwt we needed to run a decryption process, additionally we were decrypting the jwt twice, first at the session validation then inside each handler function, this was also causing a lot of using related to the merge between m3 and mcs What changed: Now we validate and decrypt the jwt once in `configure_mcs.go`, this works for both, mcs (console) and operator sessions, and then pass the decrypted claims to all the functions that need it, so no further token validation or decryption is need it.
1 parent 93e1168 commit 697bc4c

34 files changed

+618
-605
lines changed

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,6 @@ clean:
5959
@find . -name '*.test' | xargs rm -fv
6060
@find . -name '*~' | xargs rm -fv
6161
@rm -vf mcs
62+
63+
docker:
64+
@docker build -t $(TAG) --build-arg build_version=$(BUILD_VERSION) --build-arg build_time='$(BUILD_TIME)' .

cluster/cluster.go

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,28 @@ import (
2323
certutil "k8s.io/client-go/util/cert"
2424
)
2525

26-
func GetK8sConfig(token string) *rest.Config {
27-
// if console is running inside k8s by default he will have access to the ca cert from the k8s local authority
28-
const (
29-
rootCAFile = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
30-
)
31-
tlsClientConfig := rest.TLSClientConfig{Insecure: getK8sAPIServerInsecure()}
32-
if _, err := certutil.NewPool(rootCAFile); err == nil {
33-
tlsClientConfig.CAFile = rootCAFile
26+
// getTLSClientConfig will return the right TLS configuration for the K8S client based on the configured TLS certificate
27+
func getTLSClientConfig() rest.TLSClientConfig {
28+
var defaultRootCAFile = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
29+
var customRootCAFile = getK8sAPIServerTLSRootCA()
30+
tlsClientConfig := rest.TLSClientConfig{}
31+
// if console is running inside k8s by default he will have access to the CA Cert from the k8s local authority
32+
if _, err := certutil.NewPool(defaultRootCAFile); err == nil {
33+
tlsClientConfig.CAFile = defaultRootCAFile
34+
}
35+
// if the user explicitly define a custom CA certificate, instead, we will use that
36+
if customRootCAFile != "" {
37+
if _, err := certutil.NewPool(customRootCAFile); err == nil {
38+
tlsClientConfig.CAFile = customRootCAFile
39+
}
3440
}
41+
return tlsClientConfig
42+
}
43+
44+
// This operation will run only once at console startup
45+
var tlsClientConfig = getTLSClientConfig()
46+
47+
func GetK8sConfig(token string) *rest.Config {
3548
config := &rest.Config{
3649
Host: GetK8sAPIServer(),
3750
TLSClientConfig: tlsClientConfig,

cluster/config.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,10 @@ func GetK8sAPIServer() string {
4848
return env.Get(McsK8sAPIServer, apiServerAddress)
4949
}
5050

51-
// getK8sAPIServerInsecure allow to tell the k8s client to skip TLS certificate verification, ie: when connecting to a k8s cluster
52-
// that uses certificate not trusted by your machine
53-
func getK8sAPIServerInsecure() bool {
54-
return strings.ToLower(env.Get(McsK8SAPIServerInsecure, "off")) == "on"
51+
// If MCS_K8S_API_SERVER_TLS_ROOT_CA is true mcs will load the certificate into the
52+
// http.client rootCAs pool, this is useful for testing an k8s ApiServer or when working with self-signed certificates
53+
func getK8sAPIServerTLSRootCA() string {
54+
return strings.TrimSpace(env.Get(McsK8SAPIServerTLSRootCA, ""))
5555
}
5656

5757
// GetNsFromFile assumes console is running inside a k8s pod and extract the current namespace from the

cluster/const.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@
1717
package cluster
1818

1919
const (
20-
McsK8sAPIServer = "MCS_K8S_API_SERVER"
21-
McsK8SAPIServerInsecure = "MCS_K8S_API_SERVER_INSECURE"
22-
McsMinioImage = "MCS_MINIO_IMAGE"
23-
McsMCImage = "MCS_MC_IMAGE"
24-
McsNamespace = "MCS_NAMESPACE"
20+
McsK8sAPIServer = "MCS_K8S_API_SERVER"
21+
McsK8SAPIServerTLSRootCA = "MCS_K8S_API_SERVER_TLS_ROOT_CA"
22+
McsMinioImage = "MCS_MINIO_IMAGE"
23+
McsMCImage = "MCS_MC_IMAGE"
24+
McsNamespace = "MCS_NAMESPACE"
2525
)

docs/mcs_operator_mode.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Running MCS in Operator mode
2+
3+
`MCS` will authenticate against `Kubernetes`using bearer tokens via HTTP `Authorization` header. The user will provide this token once
4+
in the login form, MCS will validate it against Kubernetes (list apis) and if valid will generate and return a new MCS sessions
5+
with encrypted claims (the user Service account token will be inside the JWT in the data field)
6+
7+
# Kubernetes
8+
9+
The provided `JWT token` corresponds to the `Kubernetes service account` that `MCS` will use to run tasks on behalf of the
10+
user, ie: list, create, edit, delete tenants, storage class, etc.
11+
12+
13+
# Development
14+
15+
If console is running inside a k8s pod `KUBERNETES_SERVICE_HOST` and `KUBERNETES_SERVICE_PORT` will contain the k8s api server apiServerAddress
16+
if console is not running inside k8s by default will look for the k8s api server on `localhost:8001` (kubectl proxy)
17+
18+
If you are running mcs in your local environment and wish to make request to `Kubernetes` you can set `MCS_K8S_API_SERVER`, if
19+
the environment variable is not present by default `MCS` will use `"http://localhost:8001"`, additionally you will need to set the
20+
`MCS_OPERATOR_MODE=on` variable to make MCS display the Operator UI.
21+
22+
NOTE: using `kubectl` proxy is for local development only, since every request send to localhost:8001 will bypass service account authentication
23+
more info here: https://kubernetes.io/docs/tasks/access-application-cluster/access-cluster/#directly-accessing-the-rest-api
24+
you can override this using `MCS_K8S_API_SERVER`, ie use the k8s cluster from `kubectl config view`
25+
26+
## Extract the Service account token and use it with MCS
27+
28+
For local development you can use the jwt associated to the `m3-sa` service account, you can get the token running
29+
the following command in your terminal:
30+
31+
```
32+
kubectl get secret $(kubectl get serviceaccount mcs-sa -o jsonpath="{.secrets[0].name}") -o jsonpath="{.data.token}" | base64 --decode
33+
```
34+
35+
Then run the mcs server
36+
37+
```
38+
MCS_OPERATOR_MODE=on ./mcs server
39+
```

docs/mcs_service_account_mkube.md

Lines changed: 0 additions & 40 deletions
This file was deleted.

models/principal.go

Lines changed: 34 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/auth/jwt.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import (
3232

3333
jwtgo "github.com/dgrijalva/jwt-go"
3434
"github.com/go-openapi/swag"
35+
"github.com/minio/mcs/models"
3536
xjwt "github.com/minio/mcs/pkg/auth/jwt"
3637
"github.com/minio/minio-go/v6/pkg/credentials"
3738
uuid "github.com/satori/go.uuid"
@@ -210,7 +211,7 @@ func GetTokenFromRequest(r *http.Request) (*string, error) {
210211
return swag.String(reqToken), nil
211212
}
212213

213-
func GetClaimsFromTokenInRequest(req *http.Request) (*DecryptedClaims, error) {
214+
func GetClaimsFromTokenInRequest(req *http.Request) (*models.Principal, error) {
214215
sessionID, err := GetTokenFromRequest(req)
215216
if err != nil {
216217
return nil, err
@@ -221,5 +222,10 @@ func GetClaimsFromTokenInRequest(req *http.Request) (*DecryptedClaims, error) {
221222
if err != nil {
222223
return nil, err
223224
}
224-
return claims, nil
225+
return &models.Principal{
226+
AccessKeyID: claims.AccessKeyID,
227+
Actions: claims.Actions,
228+
SecretAccessKey: claims.SecretAccessKey,
229+
SessionToken: claims.SessionToken,
230+
}, nil
225231
}

pkg/auth/mkube_test.go

Lines changed: 0 additions & 77 deletions
This file was deleted.

pkg/auth/mkube.go renamed to pkg/auth/operator.go

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@
1717
package auth
1818

1919
import (
20-
"fmt"
20+
"context"
2121
"log"
22-
"net/http"
2322

2423
"github.com/minio/mcs/cluster"
2524
"github.com/minio/minio-go/v6/pkg/credentials"
25+
operatorClientset "github.com/minio/minio-operator/pkg/client/clientset/versioned"
2626
)
2727

2828
// operatorCredentialsProvider is an struct to hold the JWT (service account token)
@@ -44,16 +44,31 @@ func (s operatorCredentialsProvider) IsExpired() bool {
4444
return false
4545
}
4646

47-
// isServiceAccountTokenValid will make an authenticated request (using bearer token) against kubernetes api, if the
47+
// OperatorClient interface with all functions to be implemented
48+
// by mock when testing, it should include all OperatorClient respective api calls
49+
// that are used within this project.
50+
type OperatorClient interface {
51+
Authenticate(context.Context) ([]byte, error)
52+
}
53+
54+
// Interface implementation
55+
//
56+
// Define the structure of a operator client and define the functions that are actually used
57+
// from the minio-operator.
58+
type operatorClient struct {
59+
client *operatorClientset.Clientset
60+
}
61+
62+
// Authenticate implements the operator authenticate function via REST /api
63+
func (c *operatorClient) Authenticate(ctx context.Context) ([]byte, error) {
64+
return c.client.RESTClient().Verb("GET").RequestURI("/api").DoRaw(ctx)
65+
}
66+
67+
// isServiceAccountTokenValid will make an authenticated request against kubernetes api, if the
4868
// request success means the provided jwt its a valid service account token and the MCS user can use it for future
49-
// requests until it fails
50-
func isServiceAccountTokenValid(client *http.Client, jwt string) bool {
51-
//# Explore the API with TOKEN
52-
//curl -X GET $APISERVER/api --header "Authorization: Bearer $TOKEN" --insecure
53-
apiURL := fmt.Sprintf("%s/api", cluster.GetK8sAPIServer())
54-
req, _ := http.NewRequest("GET", apiURL, nil)
55-
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", jwt))
56-
_, err := client.Do(req)
69+
// requests until it expires
70+
func isServiceAccountTokenValid(ctx context.Context, operatorClient OperatorClient) bool {
71+
_, err := operatorClient.Authenticate(ctx)
5772
if err != nil {
5873
log.Println(err)
5974
return false
@@ -63,8 +78,15 @@ func isServiceAccountTokenValid(client *http.Client, jwt string) bool {
6378

6479
// GetMcsCredentialsForOperator will validate the provided JWT (service account token) and return it in the form of credentials.Credentials
6580
func GetMcsCredentialsForOperator(jwt string) (*credentials.Credentials, error) {
66-
client := http.Client{}
67-
if isServiceAccountTokenValid(&client, jwt) {
81+
ctx := context.Background()
82+
opClientClientSet, err := cluster.OperatorClient(jwt)
83+
if err != nil {
84+
return nil, err
85+
}
86+
opClient := &operatorClient{
87+
client: opClientClientSet,
88+
}
89+
if isServiceAccountTokenValid(ctx, opClient) {
6890
return credentials.New(operatorCredentialsProvider{serviceAccountJWT: jwt}), nil
6991
}
7092
return nil, errInvalidCredentials

0 commit comments

Comments
 (0)