diff --git a/Makefile b/Makefile
index 7829597a66..3f29f25e5c 100644
--- a/Makefile
+++ b/Makefile
@@ -22,6 +22,7 @@ assets:
test:
@(go test -race -v github.com/minio/mcs/restapi/...)
+ @(go test -race -v github.com/minio/mcs/pkg/auth)
coverage:
@(go test -v -coverprofile=coverage.out github.com/minio/mcs/restapi/... && go tool cover -html=coverage.out && open coverage.html)
diff --git a/README.md b/README.md
index 653375e80a..0f1c1728d0 100644
--- a/README.md
+++ b/README.md
@@ -54,6 +54,14 @@ $ mc admin policy set myminio mcsAdmin user=mcs
To run the server:
```
+export MCS_HMAC_JWT_SECRET=YOURJWTSIGNINGSECRET
+
+#required to encrypt jwet payload
+export MCS_PBKDF_PASSPHRASE=SECRET
+
+#required to encrypt jwet payload
+export MCS_PBKDF_SALT=SECRET
+
export MCS_ACCESS_KEY=mcs
export MCS_SECRET_KEY=YOURMCSSECRET
export MCS_MINIO_SERVER=http://localhost:9000
diff --git a/go.mod b/go.mod
index 182c6deb9c..430c2367af 100644
--- a/go.mod
+++ b/go.mod
@@ -3,6 +3,7 @@ module github.com/minio/mcs
go 1.14
require (
+ github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/elazarl/go-bindata-assetfs v1.0.0
github.com/go-openapi/errors v0.19.4
github.com/go-openapi/loads v0.19.5
@@ -12,11 +13,14 @@ require (
github.com/go-openapi/swag v0.19.8
github.com/go-openapi/validate v0.19.7
github.com/jessevdk/go-flags v1.4.0
+ github.com/json-iterator/go v1.1.9
github.com/minio/cli v1.22.0
github.com/minio/mc v0.0.0-20200415193718-68b638f2f96c
github.com/minio/minio v0.0.0-20200415191640-bde0f444dbab
github.com/minio/minio-go/v6 v6.0.53
+ github.com/satori/go.uuid v1.2.0
github.com/stretchr/testify v1.5.1
github.com/unrolled/secure v1.0.7
+ golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e
)
diff --git a/go.sum b/go.sum
index d9fd5b551c..3f7483274e 100644
--- a/go.sum
+++ b/go.sum
@@ -390,6 +390,7 @@ github.com/minio/minio v0.0.0-20200415191640-bde0f444dbab h1:9hlqghJl3e3HorXa6AD
github.com/minio/minio v0.0.0-20200415191640-bde0f444dbab/go.mod h1:v8oQPMMaTkjDwp5cOz1WCElA4Ik+X+0y4On+VMk0fis=
github.com/minio/minio-go/v6 v6.0.53 h1:8jzpwiOzZ5Iz7/goFWqNZRICbyWYShbb5rARjrnSCNI=
github.com/minio/minio-go/v6 v6.0.53/go.mod h1:DIvC/IApeHX8q1BAMVCXSXwpmrmM+I+iBvhvztQorfI=
+github.com/minio/parquet-go v0.0.0-20200414234858-838cfa8aae61 h1:pUSI/WKPdd77gcuoJkSzhJ4wdS8OMDOsOu99MtpXEQA=
github.com/minio/parquet-go v0.0.0-20200414234858-838cfa8aae61/go.mod h1:4trzEJ7N1nBTd5Tt7OCZT5SEin+WiAXpdJ/WgPkESA8=
github.com/minio/sha256-simd v0.1.1 h1:5QHSlgo3nt5yKOJrC7W8w7X+NFl8cMPZm96iu8kKUJU=
github.com/minio/sha256-simd v0.1.1/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM=
@@ -494,6 +495,7 @@ github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
+github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/secure-io/sio-go v0.3.0 h1:QKGb6rGJeiExac9wSWxnWPYo8O8OFN7lxXQvHshX6vo=
github.com/secure-io/sio-go v0.3.0/go.mod h1:D3KmXgKETffyYxBdFRN+Hpd2WzhzqS0EQwT3XWsAcBU=
diff --git a/pkg/auth/jwt.go b/pkg/auth/jwt.go
new file mode 100644
index 0000000000..7d8520b2f2
--- /dev/null
+++ b/pkg/auth/jwt.go
@@ -0,0 +1,180 @@
+// This file is part of MinIO Console Server
+// Copyright (c) 2020 MinIO, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package auth
+
+import (
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/rand"
+ "crypto/sha1"
+ "encoding/base64"
+ "errors"
+ "fmt"
+ "io"
+ "log"
+ "strings"
+
+ jwtgo "github.com/dgrijalva/jwt-go"
+ xjwt "github.com/minio/mcs/pkg/auth/jwt"
+ "github.com/minio/minio-go/v6/pkg/credentials"
+ "github.com/minio/minio/cmd"
+ uuid "github.com/satori/go.uuid"
+ "golang.org/x/crypto/pbkdf2"
+)
+
+var (
+ errAuthentication = errors.New("Authentication failed, check your access credentials")
+ errNoAuthToken = errors.New("JWT token missing")
+ errReadingToken = errors.New("JWT internal data is malformed")
+ errClaimsFormat = errors.New("encrypted jwt claims not in the right format")
+)
+
+// derivedKey is the key used to encrypt the JWT claims, its derived using pbkdf on MCS_PBKDF_PASSPHRASE with MCS_PBKDF_SALT
+var derivedKey = pbkdf2.Key([]byte(xjwt.GetPBKDFPassphrase()), []byte(xjwt.GetPBKDFSalt()), 4096, 32, sha1.New)
+
+// IsJWTValid returns true or false depending if the provided jwt is valid or not
+func IsJWTValid(token string) bool {
+ _, err := JWTAuthenticate(token)
+ return err == nil
+}
+
+type DecryptedClaims struct {
+ AccessKeyID string
+ SecretAccessKey string
+ SessionToken string
+}
+
+// JWTAuthenticate takes a jwt, decode it, extract claims and validate the signature
+// if the jwt claims.Data is valid we proceed to decrypt the information inside
+//
+// returns claims after validation in the following format:
+//
+// type DecryptedClaims struct {
+// AccessKeyID
+// SecretAccessKey
+// SessionToken
+// }
+func JWTAuthenticate(token string) (*DecryptedClaims, error) {
+ if token == "" {
+ return nil, errNoAuthToken
+ }
+ // initialize claims object
+ claims := xjwt.NewMapClaims()
+ // populate the claims object
+ if err := xjwt.ParseWithClaims(token, claims); err != nil {
+ return nil, errAuthentication
+ }
+ // decrypt the claims.Data field
+ claimTokens, err := decryptClaims(claims.Data)
+ if err != nil {
+ // we print decryption token error information for debugging purposes
+ log.Println(err)
+ // we return a generic error that doesn't give any information to attackers
+ return nil, errReadingToken
+ }
+ // claimsTokens contains the decrypted STS claims
+ return claimTokens, nil
+}
+
+// NewJWTWithClaimsForClient generates a new jwt with claims based on the provided STS credentials, first
+// encrypts the claims and the sign them
+func NewJWTWithClaimsForClient(credentials *credentials.Value, audience string) (string, error) {
+ if credentials != nil {
+ encryptedClaims, err := encryptClaims(credentials.AccessKeyID, credentials.SecretAccessKey, credentials.SessionToken)
+ if err != nil {
+ return "", err
+ }
+ claims := xjwt.NewStandardClaims()
+ claims.SetExpiry(cmd.UTCNow().Add(xjwt.GetMcsSTSAndJWTDurationTime()))
+ claims.SetSubject(uuid.NewV4().String())
+ claims.SetData(encryptedClaims)
+ claims.SetAudience(audience)
+ jwt := jwtgo.NewWithClaims(jwtgo.SigningMethodHS512, claims)
+ return jwt.SignedString([]byte(xjwt.GetHmacJWTSecret()))
+ }
+ return "", errors.New("provided credentials are empty")
+}
+
+// encryptClaims() receives the 3 STS claims, concatenate them and encrypt them using AES-GCM
+// returns a base64 encoded ciphertext
+func encryptClaims(accessKeyID, secretAccessKey, sessionToken string) (string, error) {
+ payload := []byte(fmt.Sprintf("%s:%s:%s", accessKeyID, secretAccessKey, sessionToken))
+ ciphertext, err := encrypt(payload)
+ if err != nil {
+ return "", err
+ }
+ return base64.StdEncoding.EncodeToString(ciphertext), nil
+}
+
+// decryptClaims() receives base64 encoded ciphertext, decode it, decrypt it (AES-GCM) and produces a *DecryptedClaims object
+func decryptClaims(ciphertext string) (*DecryptedClaims, error) {
+ decoded, err := base64.StdEncoding.DecodeString(ciphertext)
+ if err != nil {
+ log.Println(err)
+ return nil, errClaimsFormat
+ }
+ plaintext, err := decrypt(decoded)
+ if err != nil {
+ log.Println(err)
+ return nil, errClaimsFormat
+ }
+ s := strings.Split(string(plaintext), ":")
+ // Validate that the decrypted string has the right format "accessKeyID:secretAccessKey:sessionToken"
+ if len(s) != 3 {
+ return nil, errClaimsFormat
+ }
+ accessKeyID, secretAccessKey, sessionToken := s[0], s[1], s[2]
+ return &DecryptedClaims{
+ AccessKeyID: accessKeyID,
+ SecretAccessKey: secretAccessKey,
+ SessionToken: sessionToken,
+ }, nil
+}
+
+// Encrypt a blob of data using AEAD (AES-GCM) with a pbkdf2 derived key
+func encrypt(plaintext []byte) ([]byte, error) {
+ block, _ := aes.NewCipher(derivedKey)
+ gcm, err := cipher.NewGCM(block)
+ if err != nil {
+ return nil, err
+ }
+ nonce := make([]byte, gcm.NonceSize())
+ if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
+ return nil, err
+ }
+ cipherText := gcm.Seal(nonce, nonce, plaintext, nil)
+ return cipherText, nil
+}
+
+// Decrypts a blob of data using AEAD (AES-GCM) with a pbkdf2 derived key
+func decrypt(data []byte) ([]byte, error) {
+ block, err := aes.NewCipher(derivedKey)
+ if err != nil {
+ return nil, err
+ }
+ gcm, err := cipher.NewGCM(block)
+ if err != nil {
+ return nil, err
+ }
+ nonceSize := gcm.NonceSize()
+ nonce, cipherText := data[:nonceSize], data[nonceSize:]
+ plaintext, err := gcm.Open(nil, nonce, cipherText, nil)
+ if err != nil {
+ return nil, err
+ }
+ return plaintext, nil
+}
diff --git a/restapi/sessions/sessions.go b/pkg/auth/jwt/config.go
similarity index 54%
rename from restapi/sessions/sessions.go
rename to pkg/auth/jwt/config.go
index 1cfb74aad0..1acb6ae93c 100644
--- a/restapi/sessions/sessions.go
+++ b/pkg/auth/jwt/config.go
@@ -14,58 +14,18 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
-package sessions
+package jwt
import (
"crypto/rand"
"io"
+ "strconv"
"strings"
- "sync"
+ "time"
- mcCmd "github.com/minio/mc/cmd"
+ "github.com/minio/minio/pkg/env"
)
-type Singleton struct {
- sessions map[string]*mcCmd.Config
-}
-
-var instance *Singleton
-var once sync.Once
-
-// Returns a Singleton instance that keeps the sessions
-func GetInstance() *Singleton {
- once.Do(func() {
- //build sessions hash
- sessions := make(map[string]*mcCmd.Config)
-
- instance = &Singleton{
- sessions: sessions,
- }
- })
- return instance
-}
-
-// The delete built-in function deletes the element with the specified key (m[key]) from the map.
-// If m is nil or there is no such element, delete is a no-op. https://golang.org/pkg/builtin/#delete
-func (s *Singleton) DeleteSession(sessionID string) {
- delete(s.sessions, sessionID)
-}
-
-func (s *Singleton) NewSession(cfg *mcCmd.Config) string {
- // genereate random session id
- sessionID := RandomCharString(64)
- // store the cfg under that session id
- s.sessions[sessionID] = cfg
- return sessionID
-}
-
-func (s *Singleton) ValidSession(sessionID string) bool {
- if _, ok := s.sessions[sessionID]; ok {
- return true
- }
- return false
-}
-
// Do not use:
// https://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-go
// It relies on math/rand and therefore not on a cryptographically secure RNG => It must not be used
@@ -93,3 +53,42 @@ func RandomCharString(n int) string {
}
return s.String()
}
+
+// defaultHmacJWTPassphrase will be used by default if application is not configured with a custom MCS_HMAC_JWT_SECRET secret
+var defaultHmacJWTPassphrase = RandomCharString(64)
+
+// GetHmacJWTSecret returns the 64 bytes secret used for signing the generated JWT for the application
+func GetHmacJWTSecret() string {
+ return env.Get(McsHmacJWTSecret, defaultHmacJWTPassphrase)
+}
+
+// McsSTSAndJWTDurationSeconds returns the default session duration for the STS requested tokens and the generated JWTs.
+// Ideally both values should match so jwt and Minio sts sessions expires at the same time.
+func GetMcsSTSAndJWTDurationInSeconds() int {
+ duration, err := strconv.Atoi(env.Get(McsSTSAndJWTDurationSeconds, "3600"))
+ if err != nil {
+ duration = 3600
+ }
+ return duration
+}
+
+// GetMcsSTSAndJWTDurationTime returns GetMcsSTSAndJWTDurationInSeconds in duration format
+func GetMcsSTSAndJWTDurationTime() time.Duration {
+ duration := GetMcsSTSAndJWTDurationInSeconds()
+ return time.Duration(duration) * time.Second
+}
+
+// defaultPBKDFPassphrase
+var defaultPBKDFPassphrase = RandomCharString(64)
+
+// GetPBKDFPassphrase returns passphrase for the pbkdf2 function used to encrypt JWT payload
+func GetPBKDFPassphrase() string {
+ return env.Get(McsPBKDFPassphrase, defaultPBKDFPassphrase)
+}
+
+var defaultPBKDFSalt = RandomCharString(64)
+
+// GetPBKDFSalt returns salt for the pbkdf2 function used to encrypt JWT payload
+func GetPBKDFSalt() string {
+ return env.Get(McsPBKDFSalt, defaultPBKDFSalt)
+}
diff --git a/pkg/auth/jwt/const.go b/pkg/auth/jwt/const.go
new file mode 100644
index 0000000000..aeb52c94ae
--- /dev/null
+++ b/pkg/auth/jwt/const.go
@@ -0,0 +1,24 @@
+// This file is part of MinIO Console Server
+// Copyright (c) 2020 MinIO, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package jwt
+
+const (
+ McsHmacJWTSecret = "MCS_HMAC_JWT_SECRET"
+ McsSTSAndJWTDurationSeconds = "MCS_STS_AND_JWT_DURATION_SECONDS"
+ McsPBKDFPassphrase = "MCS_PBKDF_PASSPHRASE"
+ McsPBKDFSalt = "MCS_PBKDF_SALT"
+)
diff --git a/pkg/auth/jwt/parser.go b/pkg/auth/jwt/parser.go
new file mode 100644
index 0000000000..16fa832c0e
--- /dev/null
+++ b/pkg/auth/jwt/parser.go
@@ -0,0 +1,281 @@
+// This file is part of MinIO Console Server
+// Copyright (c) 2020 MinIO, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package jwt
+
+// This file is a re-implementation of the original code here with some
+// additional allocation tweaks reproduced using GODEBUG=allocfreetrace=1
+// original file https://github.com/dgrijalva/jwt-go/blob/master/parser.go
+// borrowed under MIT License https://github.com/dgrijalva/jwt-go/blob/master/LICENSE
+
+import (
+ "crypto"
+ "crypto/hmac"
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "strings"
+ "sync"
+ "time"
+
+ jwtgo "github.com/dgrijalva/jwt-go"
+ jsoniter "github.com/json-iterator/go"
+)
+
+const (
+ claimData = "data"
+ claimSub = "sub"
+)
+
+// SigningMethodHMAC - Implements the HMAC-SHA family of signing methods signing methods
+// Expects key type of []byte for both signing and validation
+type SigningMethodHMAC struct {
+ Name string
+ Hash crypto.Hash
+}
+
+// Specific instances for HS256, HS384, HS512
+var (
+ SigningMethodHS256 *SigningMethodHMAC
+ SigningMethodHS384 *SigningMethodHMAC
+ SigningMethodHS512 *SigningMethodHMAC
+)
+
+var (
+ base64BufPool sync.Pool
+ hmacSigners []*SigningMethodHMAC
+)
+
+func init() {
+ base64BufPool = sync.Pool{
+ New: func() interface{} {
+ buf := make([]byte, 8192)
+ return &buf
+ },
+ }
+
+ hmacSigners = []*SigningMethodHMAC{
+ {"HS256", crypto.SHA256},
+ {"HS384", crypto.SHA384},
+ {"HS512", crypto.SHA512},
+ }
+}
+
+// StandardClaims are basically standard claims with "Data"
+type StandardClaims struct {
+ Data string `json:"data,omitempty"`
+ jwtgo.StandardClaims
+}
+
+// MapClaims - implements custom unmarshaller
+type MapClaims struct {
+ Data string `json:"data,omitempty"`
+ Subject string `json:"sub,omitempty"`
+ jwtgo.MapClaims
+}
+
+// NewStandardClaims - initializes standard claims
+func NewStandardClaims() *StandardClaims {
+ return &StandardClaims{}
+}
+
+// SetIssuer sets issuer for these claims
+func (c *StandardClaims) SetIssuer(issuer string) {
+ c.Issuer = issuer
+}
+
+// SetAudience sets audience for these claims
+func (c *StandardClaims) SetAudience(aud string) {
+ c.Audience = aud
+}
+
+// SetExpiry sets expiry in unix epoch secs
+func (c *StandardClaims) SetExpiry(t time.Time) {
+ c.ExpiresAt = t.Unix()
+}
+
+// SetSubject sets unique identifier for the jwt
+func (c *StandardClaims) SetSubject(subject string) {
+ c.Subject = subject
+}
+
+// SetData sets the "Data" custom field.
+func (c *StandardClaims) SetData(data string) {
+ c.Data = data
+}
+
+// Valid - implements https://godoc.org/github.com/dgrijalva/jwt-go#Claims compatible
+// claims interface, additionally validates "Data" field.
+func (c *StandardClaims) Valid() error {
+ if err := c.StandardClaims.Valid(); err != nil {
+ return err
+ }
+
+ if c.Data == "" || c.Subject == "" {
+ return jwtgo.NewValidationError("data/sub",
+ jwtgo.ValidationErrorClaimsInvalid)
+ }
+ return nil
+}
+
+// NewMapClaims - Initializes a new map claims
+func NewMapClaims() *MapClaims {
+ return &MapClaims{MapClaims: jwtgo.MapClaims{}}
+}
+
+// Lookup returns the value and if the key is found.
+func (c *MapClaims) Lookup(key string) (value string, ok bool) {
+ var vinterface interface{}
+ vinterface, ok = c.MapClaims[key]
+ if ok {
+ value, ok = vinterface.(string)
+ }
+ return
+}
+
+// SetExpiry sets expiry in unix epoch secs
+func (c *MapClaims) SetExpiry(t time.Time) {
+ c.MapClaims["exp"] = t.Unix()
+}
+
+// SetData sets the "Data" custom field.
+func (c *MapClaims) SetData(data string) {
+ c.MapClaims[claimData] = data
+}
+
+// Valid - implements https://godoc.org/github.com/dgrijalva/jwt-go#Claims compatible
+// claims interface, additionally validates "Data" field.
+func (c *MapClaims) Valid() error {
+ if err := c.MapClaims.Valid(); err != nil {
+ return err
+ }
+
+ if c.Data == "" || c.Subject == "" {
+ return jwtgo.NewValidationError("data/subject",
+ jwtgo.ValidationErrorClaimsInvalid)
+ }
+ return nil
+}
+
+// Map returns underlying low-level map claims.
+func (c *MapClaims) Map() map[string]interface{} {
+ return c.MapClaims
+}
+
+// MarshalJSON marshals the MapClaims struct
+func (c *MapClaims) MarshalJSON() ([]byte, error) {
+ return json.Marshal(c.MapClaims)
+}
+
+// https://tools.ietf.org/html/rfc7519#page-11
+type jwtHeader struct {
+ Algorithm string `json:"alg"`
+ Type string `json:"typ"`
+}
+
+// ParseWithClaims - parse the token string, valid methods.
+func ParseWithClaims(tokenStr string, claims *MapClaims) error {
+ bufp := base64BufPool.Get().(*[]byte)
+ defer base64BufPool.Put(bufp)
+
+ signer, err := parseUnverifiedMapClaims(tokenStr, claims, *bufp)
+ if err != nil {
+ return err
+ }
+
+ i := strings.LastIndex(tokenStr, ".")
+ if i < 0 {
+ return jwtgo.ErrSignatureInvalid
+ }
+
+ n, err := base64Decode(tokenStr[i+1:], *bufp)
+ if err != nil {
+ return err
+ }
+
+ var ok bool
+
+ claims.Data, ok = claims.Lookup(claimData)
+ if !ok {
+ return jwtgo.NewValidationError("data missing",
+ jwtgo.ValidationErrorClaimsInvalid)
+ }
+
+ claims.Subject, ok = claims.Lookup(claimSub)
+ if !ok {
+ return jwtgo.NewValidationError("sub missing",
+ jwtgo.ValidationErrorClaimsInvalid)
+ }
+
+ hasher := hmac.New(signer.Hash.New, []byte(GetHmacJWTSecret()))
+ hasher.Write([]byte(tokenStr[:i]))
+ if !hmac.Equal((*bufp)[:n], hasher.Sum(nil)) {
+ return jwtgo.ErrSignatureInvalid
+ }
+
+ // Signature is valid, lets validate the claims for
+ // other fields such as expiry etc.
+ return claims.Valid()
+}
+
+// base64Decode returns the bytes represented by the base64 string s.
+func base64Decode(s string, buf []byte) (int, error) {
+ return base64.RawURLEncoding.Decode(buf, []byte(s))
+}
+
+// ParseUnverifiedMapClaims - WARNING: Don't use this method unless you know what you're doing
+//
+// This method parses the token but doesn't validate the signature. It's only
+// ever useful in cases where you know the signature is valid (because it has
+// been checked previously in the stack) and you want to extract values from
+// it.
+func parseUnverifiedMapClaims(tokenString string, claims *MapClaims, buf []byte) (*SigningMethodHMAC, error) {
+ if strings.Count(tokenString, ".") != 2 {
+ return nil, jwtgo.ErrSignatureInvalid
+ }
+
+ i := strings.Index(tokenString, ".")
+ j := strings.LastIndex(tokenString, ".")
+
+ n, err := base64Decode(tokenString[:i], buf)
+ if err != nil {
+ return nil, &jwtgo.ValidationError{Inner: err, Errors: jwtgo.ValidationErrorMalformed}
+ }
+
+ var header = jwtHeader{}
+ var json = jsoniter.ConfigCompatibleWithStandardLibrary
+ if err = json.Unmarshal(buf[:n], &header); err != nil {
+ return nil, &jwtgo.ValidationError{Inner: err, Errors: jwtgo.ValidationErrorMalformed}
+ }
+
+ n, err = base64Decode(tokenString[i+1:j], buf)
+ if err != nil {
+ return nil, &jwtgo.ValidationError{Inner: err, Errors: jwtgo.ValidationErrorMalformed}
+ }
+
+ if err = json.Unmarshal(buf[:n], &claims.MapClaims); err != nil {
+ return nil, &jwtgo.ValidationError{Inner: err, Errors: jwtgo.ValidationErrorMalformed}
+ }
+
+ for _, signer := range hmacSigners {
+ if header.Algorithm == signer.Name {
+ return signer, nil
+ }
+ }
+
+ return nil, jwtgo.NewValidationError(fmt.Sprintf("signing method (%s) is unavailable.", header.Algorithm),
+ jwtgo.ValidationErrorUnverifiable)
+}
diff --git a/pkg/auth/jwt_test.go b/pkg/auth/jwt_test.go
new file mode 100644
index 0000000000..e35628cfe4
--- /dev/null
+++ b/pkg/auth/jwt_test.go
@@ -0,0 +1,80 @@
+// This file is part of MinIO Console Server
+// Copyright (c) 2020 MinIO, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package auth
+
+import (
+ "testing"
+
+ "github.com/minio/minio-go/v6/pkg/credentials"
+ "github.com/stretchr/testify/assert"
+)
+
+var audience = ""
+var creds = &credentials.Value{
+ AccessKeyID: "fakeAccessKeyID",
+ SecretAccessKey: "fakeSecretAccessKey",
+ SessionToken: "fakeSessionToken",
+ SignerType: 0,
+}
+var goodToken = ""
+var badToken = "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoiRDMwYWE0ekQ1bWtFaFRyWm5yOWM3NWh0Yko0MkROOWNDZVQ5RHVHUkg1U25SR3RyTXZNOXBMdnlFSVJAAAE5eWxxekhYMXllck8xUXpzMlZzRVFKeUF2ZmpOaDkrTVdoUURWZ2FhK2R5emxzSjNpK0k1dUdoeW5DNWswUW83WEY0UWszY0RtUTdUQUVROVFEbWRKdjBkdVB5L25hQk5vM3dIdlRDZHFNRDJZN3kycktJbmVUbUlFNmVveW9EWmprcW5tckVoYmMrTlhTRU81WjZqa1kwZ1E2eXZLaWhUZGxBRS9zS1lBNlc4Q1R1cm1MU0E0b0dIcGtldFZWU0VXMHEzNU9TU1VaczRXNkxHdGMxSTFWVFZLWUo3ZTlHR2REQ3hMWGtiZHQwcjl0RDNMWUhWRndra0dSZit5ZHBzS1Y3L1Jtbkp3SHNqNVVGV0w5WGVHUkZVUjJQclJTN2plVzFXeGZuYitVeXoxNVpOMzZsZ01GNnBlWFd1LzJGcEtrb2Z2QzNpY2x5Rmp0SE45ZkxYTVpVSFhnV2lsQWVSa3oiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjkwMDAiLCJleHAiOjE1ODc1MTY1NzEsInN1YiI6ImZmYmY4YzljLTJlMjYtNGMwYS1iMmI0LTYyMmVhM2I1YjZhYiJ9.P392RUwzsrBeJOO3fS1xMZcF-lWiDvWZ5hM7LZOyFMmoG5QLccDU5eAPSm8obzPoznX1b7eCFLeEmKK-vKgjiQ"
+
+func TestNewJWTWithClaimsForClient(t *testing.T) {
+ funcAssert := assert.New(t)
+ // Test-1 : NewJWTWithClaimsForClient() is generated correctly without errors
+ function := "NewJWTWithClaimsForClient()"
+ jwt, err := NewJWTWithClaimsForClient(creds, audience)
+ if err != nil || jwt == "" {
+ t.Errorf("Failed on %s:, error occurred: %s", function, err)
+ }
+ // saving jwt for future tests
+ goodToken = jwt
+ // Test-2 : NewJWTWithClaimsForClient() throws error because of empty credentials
+ if _, err = NewJWTWithClaimsForClient(nil, audience); err != nil {
+ funcAssert.Equal("provided credentials are empty", err.Error())
+ }
+}
+
+func TestJWTAuthenticate(t *testing.T) {
+ funcAssert := assert.New(t)
+ // Test-1 : JWTAuthenticate() should correctly return the claims
+ function := "JWTAuthenticate()"
+ claims, err := JWTAuthenticate(goodToken)
+ if err != nil || claims == nil {
+ t.Errorf("Failed on %s:, error occurred: %s", function, err)
+ } else {
+ funcAssert.Equal(claims.AccessKeyID, creds.AccessKeyID)
+ funcAssert.Equal(claims.SecretAccessKey, creds.SecretAccessKey)
+ funcAssert.Equal(claims.SessionToken, creds.SessionToken)
+ }
+ // Test-2 : JWTAuthenticate() return an error because of a tampered jwt
+ if _, err := JWTAuthenticate(badToken); err != nil {
+ funcAssert.Equal("Authentication failed, check your access credentials", err.Error())
+ }
+ // Test-3 : JWTAuthenticate() return an error because of an empty jwt
+ if _, err := JWTAuthenticate(""); err != nil {
+ funcAssert.Equal("JWT token missing", err.Error())
+ }
+}
+
+func TestIsJWTValid(t *testing.T) {
+ funcAssert := assert.New(t)
+ // Test-1 : JWTAuthenticate() provided token is valid
+ funcAssert.Equal(true, IsJWTValid(goodToken))
+ // Test-2 : JWTAuthenticate() provided token is invalid
+ funcAssert.Equal(false, IsJWTValid(badToken))
+}
diff --git a/restapi/admin_arns.go b/restapi/admin_arns.go
index bcb8ccef36..685f7ee4d6 100644
--- a/restapi/admin_arns.go
+++ b/restapi/admin_arns.go
@@ -31,7 +31,8 @@ import (
func registerAdminArnsHandlers(api *operations.McsAPI) {
// return a list of arns
api.AdminAPIArnListHandler = admin_api.ArnListHandlerFunc(func(params admin_api.ArnListParams, principal *models.Principal) middleware.Responder {
- arnsResp, err := getArnsResponse()
+ sessionID := string(*principal)
+ arnsResp, err := getArnsResponse(sessionID)
if err != nil {
return admin_api.NewArnListDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
}
@@ -53,8 +54,8 @@ func getArns(ctx context.Context, client MinioAdmin) (*models.ArnsResponse, erro
}
// getArnsResponse returns a list of active arns in the instance
-func getArnsResponse() (*models.ArnsResponse, error) {
- mAdmin, err := newMAdminClient()
+func getArnsResponse(sessionID string) (*models.ArnsResponse, error) {
+ mAdmin, err := newMAdminClient(sessionID)
if err != nil {
log.Println("error creating Madmin Client:", err)
return nil, err
diff --git a/restapi/admin_config.go b/restapi/admin_config.go
index ecbe665880..d3cf82cadb 100644
--- a/restapi/admin_config.go
+++ b/restapi/admin_config.go
@@ -33,7 +33,8 @@ import (
func registerConfigHandlers(api *operations.McsAPI) {
// List Configurations
api.AdminAPIListConfigHandler = admin_api.ListConfigHandlerFunc(func(params admin_api.ListConfigParams, principal *models.Principal) middleware.Responder {
- configListResp, err := getListConfigResponse()
+ sessionID := string(*principal)
+ configListResp, err := getListConfigResponse(sessionID)
if err != nil {
return admin_api.NewListConfigDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
}
@@ -41,7 +42,8 @@ func registerConfigHandlers(api *operations.McsAPI) {
})
// Configuration Info
api.AdminAPIConfigInfoHandler = admin_api.ConfigInfoHandlerFunc(func(params admin_api.ConfigInfoParams, principal *models.Principal) middleware.Responder {
- config, err := getConfigResponse(params)
+ sessionID := string(*principal)
+ config, err := getConfigResponse(sessionID, params)
if err != nil {
return admin_api.NewConfigInfoDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
}
@@ -49,7 +51,8 @@ func registerConfigHandlers(api *operations.McsAPI) {
})
// Set Configuration
api.AdminAPISetConfigHandler = admin_api.SetConfigHandlerFunc(func(params admin_api.SetConfigParams, principal *models.Principal) middleware.Responder {
- if err := setConfigResponse(params.Name, params.Body); err != nil {
+ sessionID := string(*principal)
+ if err := setConfigResponse(sessionID, params.Name, params.Body); err != nil {
return admin_api.NewSetConfigDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
}
return admin_api.NewSetConfigNoContent()
@@ -75,8 +78,8 @@ func listConfig(client MinioAdmin) ([]*models.ConfigDescription, error) {
}
// getListConfigResponse performs listConfig() and serializes it to the handler's output
-func getListConfigResponse() (*models.ListConfigResponse, error) {
- mAdmin, err := newMAdminClient()
+func getListConfigResponse(sessionID string) (*models.ListConfigResponse, error) {
+ mAdmin, err := newMAdminClient(sessionID)
if err != nil {
log.Println("error creating Madmin Client:", err)
return nil, err
@@ -122,8 +125,8 @@ func getConfig(client MinioAdmin, name string) ([]*models.ConfigurationKV, error
}
// getConfigResponse performs getConfig() and serializes it to the handler's output
-func getConfigResponse(params admin_api.ConfigInfoParams) (*models.Configuration, error) {
- mAdmin, err := newMAdminClient()
+func getConfigResponse(sessionID string, params admin_api.ConfigInfoParams) (*models.Configuration, error) {
+ mAdmin, err := newMAdminClient(sessionID)
if err != nil {
log.Println("error creating Madmin Client:", err)
return nil, err
@@ -175,8 +178,8 @@ func buildConfig(configName *string, kvs []*models.ConfigurationKV) *string {
}
// setConfigResponse implements setConfig() to be used by handler
-func setConfigResponse(name string, configRequest *models.SetConfigRequest) error {
- mAdmin, err := newMAdminClient()
+func setConfigResponse(sessionID string, name string, configRequest *models.SetConfigRequest) error {
+ mAdmin, err := newMAdminClient(sessionID)
if err != nil {
log.Println("error creating Madmin Client:", err)
return err
diff --git a/restapi/admin_groups.go b/restapi/admin_groups.go
index 4aee858f85..f58d8d1be3 100644
--- a/restapi/admin_groups.go
+++ b/restapi/admin_groups.go
@@ -34,7 +34,8 @@ import (
func registerGroupsHandlers(api *operations.McsAPI) {
// List Groups
api.AdminAPIListGroupsHandler = admin_api.ListGroupsHandlerFunc(func(params admin_api.ListGroupsParams, principal *models.Principal) middleware.Responder {
- listGroupsResponse, err := getListGroupsResponse()
+ sessionID := string(*principal)
+ listGroupsResponse, err := getListGroupsResponse(sessionID)
if err != nil {
return admin_api.NewListGroupsDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
}
@@ -42,7 +43,8 @@ func registerGroupsHandlers(api *operations.McsAPI) {
})
// Group Info
api.AdminAPIGroupInfoHandler = admin_api.GroupInfoHandlerFunc(func(params admin_api.GroupInfoParams, principal *models.Principal) middleware.Responder {
- groupInfo, err := getGroupInfoResponse(params)
+ sessionID := string(*principal)
+ groupInfo, err := getGroupInfoResponse(sessionID, params)
if err != nil {
return admin_api.NewGroupInfoDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
}
@@ -50,21 +52,24 @@ func registerGroupsHandlers(api *operations.McsAPI) {
})
// Add Group
api.AdminAPIAddGroupHandler = admin_api.AddGroupHandlerFunc(func(params admin_api.AddGroupParams, principal *models.Principal) middleware.Responder {
- if err := getAddGroupResponse(params.Body); err != nil {
+ sessionID := string(*principal)
+ if err := getAddGroupResponse(sessionID, params.Body); err != nil {
return admin_api.NewAddGroupDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
}
return admin_api.NewAddGroupCreated()
})
// Remove Group
api.AdminAPIRemoveGroupHandler = admin_api.RemoveGroupHandlerFunc(func(params admin_api.RemoveGroupParams, principal *models.Principal) middleware.Responder {
- if err := getRemoveGroupResponse(params); err != nil {
+ sessionID := string(*principal)
+ if err := getRemoveGroupResponse(sessionID, params); err != nil {
return admin_api.NewRemoveGroupDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
}
return admin_api.NewRemoveGroupNoContent()
})
// Update Group
api.AdminAPIUpdateGroupHandler = admin_api.UpdateGroupHandlerFunc(func(params admin_api.UpdateGroupParams, principal *models.Principal) middleware.Responder {
- groupUpdateResp, err := getUpdateGroupResponse(params)
+ sessionID := string(*principal)
+ groupUpdateResp, err := getUpdateGroupResponse(sessionID, params)
if err != nil {
return admin_api.NewUpdateGroupDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
}
@@ -82,9 +87,9 @@ func listGroups(ctx context.Context, client MinioAdmin) (*[]string, error) {
}
// getListGroupsResponse performs listGroups() and serializes it to the handler's output
-func getListGroupsResponse() (*models.ListGroupsResponse, error) {
+func getListGroupsResponse(sessionID string) (*models.ListGroupsResponse, error) {
ctx := context.Background()
- mAdmin, err := newMAdminClient()
+ mAdmin, err := newMAdminClient(sessionID)
if err != nil {
log.Println("error creating Madmin Client:", err)
return nil, err
@@ -116,9 +121,9 @@ func groupInfo(ctx context.Context, client MinioAdmin, group string) (*madmin.Gr
}
// getGroupInfoResponse performs groupInfo() and serializes it to the handler's output
-func getGroupInfoResponse(params admin_api.GroupInfoParams) (*models.Group, error) {
+func getGroupInfoResponse(sessionID string, params admin_api.GroupInfoParams) (*models.Group, error) {
ctx := context.Background()
- mAdmin, err := newMAdminClient()
+ mAdmin, err := newMAdminClient(sessionID)
if err != nil {
log.Println("error creating Madmin Client:", err)
return nil, err
@@ -157,7 +162,7 @@ func addGroup(ctx context.Context, client MinioAdmin, group string, members []st
}
// getAddGroupResponse performs addGroup() and serializes it to the handler's output
-func getAddGroupResponse(params *models.AddGroupRequest) error {
+func getAddGroupResponse(sessionID string, params *models.AddGroupRequest) error {
ctx := context.Background()
// AddGroup request needed to proceed
if params == nil {
@@ -165,7 +170,7 @@ func getAddGroupResponse(params *models.AddGroupRequest) error {
return errors.New(500, "error AddGroup body not in request")
}
- mAdmin, err := newMAdminClient()
+ mAdmin, err := newMAdminClient(sessionID)
if err != nil {
log.Println("error creating Madmin Client:", err)
return err
@@ -196,14 +201,14 @@ func removeGroup(ctx context.Context, client MinioAdmin, group string) error {
}
// getRemoveGroupResponse performs removeGroup() and serializes it to the handler's output
-func getRemoveGroupResponse(params admin_api.RemoveGroupParams) error {
+func getRemoveGroupResponse(sessionID string, params admin_api.RemoveGroupParams) error {
ctx := context.Background()
if params.Name == "" {
log.Println("error group name not in request")
return errors.New(500, "error group name not in request")
}
- mAdmin, err := newMAdminClient()
+ mAdmin, err := newMAdminClient(sessionID)
if err != nil {
log.Println("error creating Madmin Client:", err)
return err
@@ -276,7 +281,7 @@ func setGroupStatus(ctx context.Context, client MinioAdmin, group, status string
// getUpdateGroupResponse updates a group by adding or removing it's members depending on the request,
// also sets the group's status if status in the request is different than the current one.
// Then serializes the output to be used by the handler.
-func getUpdateGroupResponse(params admin_api.UpdateGroupParams) (*models.Group, error) {
+func getUpdateGroupResponse(sessionID string, params admin_api.UpdateGroupParams) (*models.Group, error) {
ctx := context.Background()
if params.Name == "" {
log.Println("error group name not in request")
@@ -289,7 +294,7 @@ func getUpdateGroupResponse(params admin_api.UpdateGroupParams) (*models.Group,
expectedGroupUpdate := params.Body
groupName := params.Name
- mAdmin, err := newMAdminClient()
+ mAdmin, err := newMAdminClient(sessionID)
if err != nil {
log.Println("error creating Madmin Client:", err)
return nil, err
diff --git a/restapi/admin_info.go b/restapi/admin_info.go
index f19b256486..952aa938c4 100644
--- a/restapi/admin_info.go
+++ b/restapi/admin_info.go
@@ -31,7 +31,8 @@ import (
func registerAdminInfoHandlers(api *operations.McsAPI) {
// return usage stats
api.AdminAPIAdminInfoHandler = admin_api.AdminInfoHandlerFunc(func(params admin_api.AdminInfoParams, principal *models.Principal) middleware.Responder {
- infoResp, err := getAdminInfoResponse()
+ sessionID := string(*principal)
+ infoResp, err := getAdminInfoResponse(sessionID)
if err != nil {
return admin_api.NewAdminInfoDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
}
@@ -62,8 +63,8 @@ func getAdminInfo(ctx context.Context, client MinioAdmin) (*UsageInfo, error) {
}
// getAdminInfoResponse returns the response containing total buckets, objects and usage.
-func getAdminInfoResponse() (*models.AdminInfoResponse, error) {
- mAdmin, err := newMAdminClient()
+func getAdminInfoResponse(sessionID string) (*models.AdminInfoResponse, error) {
+ mAdmin, err := newMAdminClient(sessionID)
if err != nil {
log.Println("error creating Madmin Client:", err)
return nil, err
diff --git a/restapi/admin_notification_endpoints.go b/restapi/admin_notification_endpoints.go
index 9311cdbd36..6829341298 100644
--- a/restapi/admin_notification_endpoints.go
+++ b/restapi/admin_notification_endpoints.go
@@ -32,7 +32,8 @@ import (
func registerAdminNotificationEndpointsHandlers(api *operations.McsAPI) {
// return a list of notification endpoints
api.AdminAPINotificationEndpointListHandler = admin_api.NotificationEndpointListHandlerFunc(func(params admin_api.NotificationEndpointListParams, principal *models.Principal) middleware.Responder {
- notifEndpoints, err := getNotificationEndpointsResponse()
+ sessionID := string(*principal)
+ notifEndpoints, err := getNotificationEndpointsResponse(sessionID)
if err != nil {
return admin_api.NewNotificationEndpointListDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
}
@@ -40,7 +41,8 @@ func registerAdminNotificationEndpointsHandlers(api *operations.McsAPI) {
})
// add a new notification endpoints
api.AdminAPIAddNotificationEndpointHandler = admin_api.AddNotificationEndpointHandlerFunc(func(params admin_api.AddNotificationEndpointParams, principal *models.Principal) middleware.Responder {
- notifEndpoints, err := getAddNotificationEndpointResponse(¶ms)
+ sessionID := string(*principal)
+ notifEndpoints, err := getAddNotificationEndpointResponse(sessionID, ¶ms)
if err != nil {
return admin_api.NewAddNotificationEndpointDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
}
@@ -78,8 +80,8 @@ func getNotificationEndpoints(ctx context.Context, client MinioAdmin) (*models.N
}
// getNotificationEndpointsResponse returns a list of notification endpoints in the instance
-func getNotificationEndpointsResponse() (*models.NotifEndpointResponse, error) {
- mAdmin, err := newMAdminClient()
+func getNotificationEndpointsResponse(sessionID string) (*models.NotifEndpointResponse, error) {
+ mAdmin, err := newMAdminClient(sessionID)
if err != nil {
log.Println("error creating Madmin Client:", err)
return nil, err
@@ -151,8 +153,8 @@ func addNotificationEndpoint(ctx context.Context, client MinioAdmin, params *adm
}
// getNotificationEndpointsResponse returns a list of notification endpoints in the instance
-func getAddNotificationEndpointResponse(params *admin_api.AddNotificationEndpointParams) (*models.NotificationEndpoint, error) {
- mAdmin, err := newMAdminClient()
+func getAddNotificationEndpointResponse(sessionID string, params *admin_api.AddNotificationEndpointParams) (*models.NotificationEndpoint, error) {
+ mAdmin, err := newMAdminClient(sessionID)
if err != nil {
log.Println("error creating Madmin Client:", err)
return nil, err
diff --git a/restapi/admin_policies.go b/restapi/admin_policies.go
index 204dfb675f..eeac4c6336 100644
--- a/restapi/admin_policies.go
+++ b/restapi/admin_policies.go
@@ -35,7 +35,8 @@ import (
func registersPoliciesHandler(api *operations.McsAPI) {
// List Policies
api.AdminAPIListPoliciesHandler = admin_api.ListPoliciesHandlerFunc(func(params admin_api.ListPoliciesParams, principal *models.Principal) middleware.Responder {
- listPoliciesResponse, err := getListPoliciesResponse()
+ sessionID := string(*principal)
+ listPoliciesResponse, err := getListPoliciesResponse(sessionID)
if err != nil {
return admin_api.NewListPoliciesDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
}
@@ -43,7 +44,8 @@ func registersPoliciesHandler(api *operations.McsAPI) {
})
// Policy Info
api.AdminAPIPolicyInfoHandler = admin_api.PolicyInfoHandlerFunc(func(params admin_api.PolicyInfoParams, principal *models.Principal) middleware.Responder {
- policyInfo, err := getPolicyInfoResponse(params)
+ sessionID := string(*principal)
+ policyInfo, err := getPolicyInfoResponse(sessionID, params)
if err != nil {
return admin_api.NewPolicyInfoDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
}
@@ -51,7 +53,8 @@ func registersPoliciesHandler(api *operations.McsAPI) {
})
// Add Policy
api.AdminAPIAddPolicyHandler = admin_api.AddPolicyHandlerFunc(func(params admin_api.AddPolicyParams, principal *models.Principal) middleware.Responder {
- policyResponse, err := getAddPolicyResponse(params.Body)
+ sessionID := string(*principal)
+ policyResponse, err := getAddPolicyResponse(sessionID, params.Body)
if err != nil {
return admin_api.NewAddPolicyDefault(500).WithPayload(&models.Error{
Code: 500,
@@ -62,14 +65,16 @@ func registersPoliciesHandler(api *operations.McsAPI) {
})
// Remove Policy
api.AdminAPIRemovePolicyHandler = admin_api.RemovePolicyHandlerFunc(func(params admin_api.RemovePolicyParams, principal *models.Principal) middleware.Responder {
- if err := getRemovePolicyResponse(params); err != nil {
+ sessionID := string(*principal)
+ if err := getRemovePolicyResponse(sessionID, params); err != nil {
return admin_api.NewRemovePolicyDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
}
return admin_api.NewRemovePolicyNoContent()
})
// Set Policy
api.AdminAPISetPolicyHandler = admin_api.SetPolicyHandlerFunc(func(params admin_api.SetPolicyParams, principal *models.Principal) middleware.Responder {
- if err := getSetPolicyResponse(params.Name, params.Body); err != nil {
+ sessionID := string(*principal)
+ if err := getSetPolicyResponse(sessionID, params.Name, params.Body); err != nil {
return admin_api.NewSetPolicyDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
}
return admin_api.NewSetPolicyNoContent()
@@ -97,9 +102,9 @@ func listPolicies(ctx context.Context, client MinioAdmin) ([]*models.Policy, err
}
// getListPoliciesResponse performs listPolicies() and serializes it to the handler's output
-func getListPoliciesResponse() (*models.ListPoliciesResponse, error) {
+func getListPoliciesResponse(sessionID string) (*models.ListPoliciesResponse, error) {
ctx := context.Background()
- mAdmin, err := newMAdminClient()
+ mAdmin, err := newMAdminClient(sessionID)
if err != nil {
log.Println("error creating Madmin Client:", err)
return nil, err
@@ -131,13 +136,13 @@ func removePolicy(ctx context.Context, client MinioAdmin, name string) error {
}
// getRemovePolicyResponse() performs removePolicy() and serializes it to the handler's output
-func getRemovePolicyResponse(params admin_api.RemovePolicyParams) error {
+func getRemovePolicyResponse(sessionID string, params admin_api.RemovePolicyParams) error {
ctx := context.Background()
if params.Name == "" {
log.Println("error policy name not in request")
return errors.New(500, "error policy name not in request")
}
- mAdmin, err := newMAdminClient()
+ mAdmin, err := newMAdminClient(sessionID)
if err != nil {
log.Println("error creating Madmin Client:", err)
return err
@@ -173,14 +178,14 @@ func addPolicy(ctx context.Context, client MinioAdmin, name, policy string) (*mo
}
// getAddPolicyResponse performs addPolicy() and serializes it to the handler's output
-func getAddPolicyResponse(params *models.AddPolicyRequest) (*models.Policy, error) {
+func getAddPolicyResponse(sessionID string, params *models.AddPolicyRequest) (*models.Policy, error) {
ctx := context.Background()
if params == nil {
log.Println("error AddPolicy body not in request")
return nil, errors.New(500, "error AddPolicy body not in request")
}
- mAdmin, err := newMAdminClient()
+ mAdmin, err := newMAdminClient(sessionID)
if err != nil {
log.Println("error creating Madmin Client:", err)
return nil, err
@@ -213,9 +218,9 @@ func policyInfo(ctx context.Context, client MinioAdmin, name string) (*models.Po
}
// getPolicyInfoResponse performs policyInfo() and serializes it to the handler's output
-func getPolicyInfoResponse(params admin_api.PolicyInfoParams) (*models.Policy, error) {
+func getPolicyInfoResponse(sessionID string, params admin_api.PolicyInfoParams) (*models.Policy, error) {
ctx := context.Background()
- mAdmin, err := newMAdminClient()
+ mAdmin, err := newMAdminClient(sessionID)
if err != nil {
log.Println("error creating Madmin Client:", err)
return nil, err
@@ -244,13 +249,13 @@ func setPolicy(ctx context.Context, client MinioAdmin, name, entityName string,
}
// getSetPolicyResponse() performs setPolicy() and serializes it to the handler's output
-func getSetPolicyResponse(name string, params *models.SetPolicyRequest) error {
+func getSetPolicyResponse(sessionID string, name string, params *models.SetPolicyRequest) error {
ctx := context.Background()
if name == "" {
log.Println("error policy name not in request")
return errors.New(500, "error policy name not in request")
}
- mAdmin, err := newMAdminClient()
+ mAdmin, err := newMAdminClient(sessionID)
if err != nil {
log.Println("error creating Madmin Client:", err)
return err
diff --git a/restapi/admin_profiling.go b/restapi/admin_profiling.go
index 5e0c7a8741..8a26c4b948 100644
--- a/restapi/admin_profiling.go
+++ b/restapi/admin_profiling.go
@@ -35,7 +35,8 @@ import (
func registerProfilingHandler(api *operations.McsAPI) {
// Start Profiling
api.AdminAPIProfilingStartHandler = admin_api.ProfilingStartHandlerFunc(func(params admin_api.ProfilingStartParams, principal *models.Principal) middleware.Responder {
- profilingStartResponse, err := getProfilingStartResponse(params.Body)
+ sessionID := string(*principal)
+ profilingStartResponse, err := getProfilingStartResponse(sessionID, params.Body)
if err != nil {
return admin_api.NewProfilingStartDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
}
@@ -43,7 +44,8 @@ func registerProfilingHandler(api *operations.McsAPI) {
})
// Stop and download profiling data
api.AdminAPIProfilingStopHandler = admin_api.ProfilingStopHandlerFunc(func(params admin_api.ProfilingStopParams, principal *models.Principal) middleware.Responder {
- profilingStopResponse, err := getProfilingStopResponse()
+ sessionID := string(*principal)
+ profilingStopResponse, err := getProfilingStopResponse(sessionID)
if err != nil {
return admin_api.NewProfilingStopDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
}
@@ -90,13 +92,13 @@ func startProfiling(ctx context.Context, client MinioAdmin, profilerType models.
}
// getProfilingStartResponse performs startProfiling() and serializes it to the handler's output
-func getProfilingStartResponse(params *models.ProfilingStartRequest) (*models.StartProfilingList, error) {
+func getProfilingStartResponse(sessionID string, params *models.ProfilingStartRequest) (*models.StartProfilingList, error) {
ctx := context.Background()
if params == nil {
log.Println("error profiling type not in body request")
return nil, errors.New(500, "error AddPolicy body not in request")
}
- mAdmin, err := newMAdminClient()
+ mAdmin, err := newMAdminClient(sessionID)
if err != nil {
log.Println("error creating Madmin Client:", err)
return nil, err
@@ -127,9 +129,9 @@ func stopProfiling(ctx context.Context, client MinioAdmin) (io.ReadCloser, error
}
// getProfilingStopResponse() performs setPolicy() and serializes it to the handler's output
-func getProfilingStopResponse() (io.ReadCloser, error) {
+func getProfilingStopResponse(sessionID string) (io.ReadCloser, error) {
ctx := context.Background()
- mAdmin, err := newMAdminClient()
+ mAdmin, err := newMAdminClient(sessionID)
if err != nil {
log.Println("error creating Madmin Client:", err)
return nil, err
diff --git a/restapi/admin_service.go b/restapi/admin_service.go
index 000df1313c..1c755a5d9d 100644
--- a/restapi/admin_service.go
+++ b/restapi/admin_service.go
@@ -32,7 +32,8 @@ import (
func registerServiceHandlers(api *operations.McsAPI) {
// Restart Service
api.AdminAPIRestartServiceHandler = admin_api.RestartServiceHandlerFunc(func(params admin_api.RestartServiceParams, principal *models.Principal) middleware.Responder {
- if err := getRestartServiceResponse(); err != nil {
+ sessionID := string(*principal)
+ if err := getRestartServiceResponse(sessionID); err != nil {
return admin_api.NewRestartServiceDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
}
return admin_api.NewRestartServiceNoContent()
@@ -61,9 +62,9 @@ func serviceRestart(ctx context.Context, client MinioAdmin) error {
}
// getRestartServiceResponse performs serviceRestart()
-func getRestartServiceResponse() error {
+func getRestartServiceResponse(sessionID string) error {
ctx := context.Background()
- mAdmin, err := newMAdminClient()
+ mAdmin, err := newMAdminClient(sessionID)
if err != nil {
log.Println("error creating Madmin Client:", err)
return err
diff --git a/restapi/admin_users.go b/restapi/admin_users.go
index dee36eeae6..2596087954 100644
--- a/restapi/admin_users.go
+++ b/restapi/admin_users.go
@@ -34,7 +34,8 @@ import (
func registerUsersHandlers(api *operations.McsAPI) {
// List Users
api.AdminAPIListUsersHandler = admin_api.ListUsersHandlerFunc(func(params admin_api.ListUsersParams, principal *models.Principal) middleware.Responder {
- listUsersResponse, err := getListUsersResponse()
+ sessionID := string(*principal)
+ listUsersResponse, err := getListUsersResponse(sessionID)
if err != nil {
return admin_api.NewListUsersDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
}
@@ -42,7 +43,8 @@ func registerUsersHandlers(api *operations.McsAPI) {
})
// Add User
api.AdminAPIAddUserHandler = admin_api.AddUserHandlerFunc(func(params admin_api.AddUserParams, principal *models.Principal) middleware.Responder {
- userResponse, err := getUserAddResponse(params)
+ sessionID := string(*principal)
+ userResponse, err := getUserAddResponse(sessionID, params)
if err != nil {
return admin_api.NewAddUserDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
}
@@ -50,7 +52,8 @@ func registerUsersHandlers(api *operations.McsAPI) {
})
// Remove User
api.AdminAPIRemoveUserHandler = admin_api.RemoveUserHandlerFunc(func(params admin_api.RemoveUserParams, principal *models.Principal) middleware.Responder {
- err := getRemoveUserResponse(params)
+ sessionID := string(*principal)
+ err := getRemoveUserResponse(sessionID, params)
if err != nil {
return admin_api.NewRemoveUserDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
}
@@ -58,7 +61,8 @@ func registerUsersHandlers(api *operations.McsAPI) {
})
// Update User-Groups
api.AdminAPIUpdateUserGroupsHandler = admin_api.UpdateUserGroupsHandlerFunc(func(params admin_api.UpdateUserGroupsParams, principal *models.Principal) middleware.Responder {
- userUpdateResponse, err := getUpdateUserGroupsResponse(params)
+ sessionID := string(*principal)
+ userUpdateResponse, err := getUpdateUserGroupsResponse(sessionID, params)
if err != nil {
return admin_api.NewUpdateUserGroupsDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
}
@@ -67,7 +71,8 @@ func registerUsersHandlers(api *operations.McsAPI) {
})
// Get User
api.AdminAPIGetUserInfoHandler = admin_api.GetUserInfoHandlerFunc(func(params admin_api.GetUserInfoParams, principal *models.Principal) middleware.Responder {
- userInfoResponse, err := getUserInfoResponse(params)
+ sessionID := string(*principal)
+ userInfoResponse, err := getUserInfoResponse(sessionID, params)
if err != nil {
return admin_api.NewGetUserDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
}
@@ -76,7 +81,8 @@ func registerUsersHandlers(api *operations.McsAPI) {
})
// Update User
api.AdminAPIUpdateUserInfoHandler = admin_api.UpdateUserInfoHandlerFunc(func(params admin_api.UpdateUserInfoParams, principal *models.Principal) middleware.Responder {
- userUpdateResponse, err := getUpdateUserResponse(params)
+ sessionID := string(*principal)
+ userUpdateResponse, err := getUpdateUserResponse(sessionID, params)
if err != nil {
return admin_api.NewUpdateUserInfoDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
}
@@ -85,9 +91,10 @@ func registerUsersHandlers(api *operations.McsAPI) {
})
// Update User-Groups Bulk
api.AdminAPIBulkUpdateUsersGroupsHandler = admin_api.BulkUpdateUsersGroupsHandlerFunc(func(params admin_api.BulkUpdateUsersGroupsParams, principal *models.Principal) middleware.Responder {
- error := getAddUsersListToGroupsResponse(params)
- if error != nil {
- return admin_api.NewBulkUpdateUsersGroupsDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(error.Error())})
+ sessionID := string(*principal)
+ err := getAddUsersListToGroupsResponse(sessionID, params)
+ if err != nil {
+ return admin_api.NewBulkUpdateUsersGroupsDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
}
return admin_api.NewBulkUpdateUsersGroupsOK()
@@ -119,9 +126,9 @@ func listUsers(ctx context.Context, client MinioAdmin) ([]*models.User, error) {
}
// getListUsersResponse performs listUsers() and serializes it to the handler's output
-func getListUsersResponse() (*models.ListUsersResponse, error) {
+func getListUsersResponse(sessionID string) (*models.ListUsersResponse, error) {
ctx := context.Background()
- mAdmin, err := newMAdminClient()
+ mAdmin, err := newMAdminClient(sessionID)
if err != nil {
log.Println("error creating Madmin Client:", err)
return nil, err
@@ -167,9 +174,9 @@ func addUser(ctx context.Context, client MinioAdmin, accessKey, secretKey *strin
return userRet, nil
}
-func getUserAddResponse(params admin_api.AddUserParams) (*models.User, error) {
+func getUserAddResponse(sessionID string, params admin_api.AddUserParams) (*models.User, error) {
ctx := context.Background()
- mAdmin, err := newMAdminClient()
+ mAdmin, err := newMAdminClient(sessionID)
if err != nil {
log.Println("error creating Madmin Client:", err)
return nil, err
@@ -194,10 +201,10 @@ func removeUser(ctx context.Context, client MinioAdmin, accessKey string) error
return nil
}
-func getRemoveUserResponse(params admin_api.RemoveUserParams) error {
+func getRemoveUserResponse(sessionID string, params admin_api.RemoveUserParams) error {
ctx := context.Background()
- mAdmin, err := newMAdminClient()
+ mAdmin, err := newMAdminClient(sessionID)
if err != nil {
log.Println("error creating Madmin Client:", err)
return err
@@ -226,10 +233,10 @@ func getUserInfo(ctx context.Context, client MinioAdmin, accessKey string) (*mad
return &userInfo, nil
}
-func getUserInfoResponse(params admin_api.GetUserInfoParams) (*models.User, error) {
+func getUserInfoResponse(sessionID string, params admin_api.GetUserInfoParams) (*models.User, error) {
ctx := context.Background()
- mAdmin, err := newMAdminClient()
+ mAdmin, err := newMAdminClient(sessionID)
if err != nil {
log.Println("error creating Madmin Client:", err)
return nil, err
@@ -341,10 +348,10 @@ func updateUserGroups(ctx context.Context, client MinioAdmin, user string, group
return userReturn, nil
}
-func getUpdateUserGroupsResponse(params admin_api.UpdateUserGroupsParams) (*models.User, error) {
+func getUpdateUserGroupsResponse(sessionID string, params admin_api.UpdateUserGroupsParams) (*models.User, error) {
ctx := context.Background()
- mAdmin, err := newMAdminClient()
+ mAdmin, err := newMAdminClient(sessionID)
if err != nil {
log.Println("error creating Madmin Client:", err)
return nil, err
@@ -382,10 +389,10 @@ func setUserStatus(ctx context.Context, client MinioAdmin, user string, status s
return nil
}
-func getUpdateUserResponse(params admin_api.UpdateUserInfoParams) (*models.User, error) {
+func getUpdateUserResponse(sessionID string, params admin_api.UpdateUserInfoParams) (*models.User, error) {
ctx := context.Background()
- mAdmin, err := newMAdminClient()
+ mAdmin, err := newMAdminClient(sessionID)
if err != nil {
log.Println("error creating Madmin Client:", err)
return nil, err
@@ -455,10 +462,10 @@ func addUsersListToGroups(ctx context.Context, client MinioAdmin, usersToUpdate
return nil
}
-func getAddUsersListToGroupsResponse(params admin_api.BulkUpdateUsersGroupsParams) error {
+func getAddUsersListToGroupsResponse(sessionID string, params admin_api.BulkUpdateUsersGroupsParams) error {
ctx := context.Background()
- mAdmin, err := newMAdminClient()
+ mAdmin, err := newMAdminClient(sessionID)
if err != nil {
log.Println("error creating Madmin Client:", err)
return err
diff --git a/restapi/client-admin.go b/restapi/client-admin.go
index 1cf65bd6c7..017836fa3d 100644
--- a/restapi/client-admin.go
+++ b/restapi/client-admin.go
@@ -24,6 +24,8 @@ import (
mcCmd "github.com/minio/mc/cmd"
"github.com/minio/mc/pkg/probe"
+ "github.com/minio/mcs/pkg/auth"
+ "github.com/minio/minio-go/v6/pkg/credentials"
iampolicy "github.com/minio/minio/pkg/iam/policy"
"github.com/minio/minio/pkg/madmin"
)
@@ -192,14 +194,17 @@ func (ac adminClient) stopProfiling(ctx context.Context) (io.ReadCloser, error)
return ac.client.DownloadProfilingData(ctx)
}
-func newMAdminClient() (*madmin.AdminClient, error) {
- endpoint := getMinIOServer()
- accessKeyID := getAccessKey()
- secretAccessKey := getSecretKey()
-
- adminClient, pErr := NewAdminClient(endpoint, accessKeyID, secretAccessKey)
- if pErr != nil {
- return nil, pErr.Cause
+func newMAdminClient(jwt string) (*madmin.AdminClient, error) {
+ claims, err := auth.JWTAuthenticate(jwt)
+ if err != nil {
+ return nil, err
+ }
+ adminClient, err := madmin.NewWithOptions(getMinIOEndpoint(), &madmin.Options{
+ Creds: credentials.NewStaticV4(claims.AccessKeyID, claims.SecretAccessKey, claims.SessionToken),
+ Secure: getMinIOEndpointIsSecure(),
+ })
+ if err != nil {
+ return nil, err
}
return adminClient, nil
}
diff --git a/restapi/client.go b/restapi/client.go
index 85ecd5a1cf..437c5cb804 100644
--- a/restapi/client.go
+++ b/restapi/client.go
@@ -22,7 +22,10 @@ import (
mc "github.com/minio/mc/cmd"
"github.com/minio/mc/pkg/probe"
+ "github.com/minio/mcs/pkg/auth"
+ xjwt "github.com/minio/mcs/pkg/auth/jwt"
"github.com/minio/minio-go/v6"
+ "github.com/minio/minio-go/v6/pkg/credentials"
)
func init() {
@@ -107,20 +110,67 @@ func (c mcS3Client) removeNotificationConfig(arn string, event string, prefix st
return c.client.RemoveNotificationConfig(arn, event, prefix, suffix)
}
-// newMinioClient creates a new MinIO client to talk to the server
-func newMinioClient() (*minio.Client, error) {
- endpoint := getMinIOEndpoint()
- accessKeyID := getAccessKey()
- secretAccessKey := getSecretKey()
- useSSL := getMinIOEndpointIsSecure()
+// Define MCSCredentials interface with all functions to be implemented
+// by mock when testing, it should include all needed minioCredentials.Credentials api calls
+// that are used within this project.
+type MCSCredentials interface {
+ Get() (credentials.Value, error)
+ Expire()
+}
+
+// Interface implementation
+//
+// Define the structure of a mc S3Client and define the functions that are actually used
+// from mcsCredentials api.
+type mcsCredentials struct {
+ minioCredentials *credentials.Credentials
+}
+
+// implements *Credentials.Get()
+func (c mcsCredentials) Get() (credentials.Value, error) {
+ return c.minioCredentials.Get()
+}
- // Initialize minio client object.
- minioClient, err := minio.NewV4(endpoint, accessKeyID, secretAccessKey, useSSL)
+// implements *Credentials.Expire()
+func (c mcsCredentials) Expire() {
+ c.minioCredentials.Expire()
+}
+
+func newMcsCredentials(accessKey, secretKey, location string) (*credentials.Credentials, error) {
+ return credentials.NewSTSAssumeRole(getMinIOServer(), credentials.STSAssumeRoleOptions{
+ AccessKey: accessKey,
+ SecretKey: secretKey,
+ Location: location,
+ DurationSeconds: xjwt.GetMcsSTSAndJWTDurationInSeconds(),
+ })
+}
+
+// getMcsCredentialsFromJWT returns the *minioCredentials.Credentials associated to the
+// provided jwt, this is useful for running the Expire() or IsExpired() operations
+func getMcsCredentialsFromJWT(jwt string) (*credentials.Credentials, error) {
+ claims, err := auth.JWTAuthenticate(jwt)
if err != nil {
return nil, err
}
+ creds := credentials.NewStaticV4(claims.AccessKeyID, claims.SecretAccessKey, claims.SessionToken)
+ return creds, nil
+}
- return minioClient, nil
+// newMinioClient creates a new MinIO client based on the minioCredentials extracted
+// from the provided jwt
+func newMinioClient(jwt string) (*minio.Client, error) {
+ creds, err := getMcsCredentialsFromJWT(jwt)
+ if err != nil {
+ return nil, err
+ }
+ adminClient, err := minio.NewWithOptions(getMinIOEndpoint(), &minio.Options{
+ Creds: creds,
+ Secure: getMinIOEndpointIsSecure(),
+ })
+ if err != nil {
+ return nil, err
+ }
+ return adminClient, nil
}
// newS3BucketClient creates a new mc S3Client to talk to the server based on a bucket
@@ -150,7 +200,7 @@ func newS3BucketClient(bucketName *string) (*mc.S3Client, error) {
// parameters.
func newS3Config(endpoint, accessKey, secretKey string, isSecure bool) *mc.Config {
// We have a valid alias and hostConfig. We populate the
- // credentials from the match found in the config file.
+ // minioCredentials from the match found in the config file.
s3Config := new(mc.Config)
s3Config.AppName = "mcs" // TODO: make this a constant
diff --git a/restapi/config.go b/restapi/config.go
index 28108dc552..443ce7cfed 100644
--- a/restapi/config.go
+++ b/restapi/config.go
@@ -198,7 +198,6 @@ func getSecureFeaturePolicy() string {
return env.Get(McsSecureFeaturePolicy, "")
}
-// FeaturePolicy allows the Feature-Policy header with the value to be set with a custom value. Default is "".
func getSecureExpectCTHeader() string {
return env.Get(McsSecureExpectCTHeader, "")
}
diff --git a/restapi/configure_mcs.go b/restapi/configure_mcs.go
index 465b14da87..509b988b4c 100644
--- a/restapi/configure_mcs.go
+++ b/restapi/configure_mcs.go
@@ -24,9 +24,8 @@ import (
"net/http"
"strings"
- "github.com/minio/mcs/restapi/sessions"
-
"github.com/minio/mcs/models"
+ "github.com/minio/mcs/pkg/auth"
assetfs "github.com/elazarl/go-bindata-assetfs"
@@ -60,7 +59,7 @@ func configureAPI(api *operations.McsAPI) http.Handler {
// Applies when the "x-token" header is set
api.KeyAuth = func(token string, scopes []string) (*models.Principal, error) {
- if sessions.GetInstance().ValidSession(token) {
+ if auth.IsJWTValid(token) {
prin := models.Principal(token)
return &prin, nil
}
diff --git a/restapi/sessions/sessions_test.go b/restapi/sessions/sessions_test.go
deleted file mode 100644
index ac2042ccea..0000000000
--- a/restapi/sessions/sessions_test.go
+++ /dev/null
@@ -1,47 +0,0 @@
-// This file is part of MinIO Console Server
-// Copyright (c) 2020 MinIO, Inc.
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package sessions
-
-import (
- "testing"
-
- mcCmd "github.com/minio/mc/cmd"
-
- "github.com/stretchr/testify/assert"
-)
-
-// TestNewSession tests the creation of a new sesison for a valid cfg object
-func TestNewSession(t *testing.T) {
- assert := assert.New(t)
- cfg := mcCmd.Config{}
- // Test Case 1: No collision
- sessionID := GetInstance().NewSession(&cfg)
- assert.NotEmpty(sessionID, "Session ID was returned empty")
-}
-
-// TestValidateSession tests a valid sessionId on the sessions object
-func TestValidateSession(t *testing.T) {
- assert := assert.New(t)
- cfg := mcCmd.Config{}
- // Test Case 1: Valid session
- sessionID := GetInstance().NewSession(&cfg)
- isValid := GetInstance().ValidSession(sessionID)
- assert.Equal(isValid, true, "Session was not found valid")
- // Test Case 2: Invalid session
- isInvalid := GetInstance().ValidSession("random")
- assert.Equal(isInvalid, false, "Session was found valid")
-}
diff --git a/restapi/user_buckets.go b/restapi/user_buckets.go
index 7a5c335580..40e461d662 100644
--- a/restapi/user_buckets.go
+++ b/restapi/user_buckets.go
@@ -37,7 +37,8 @@ import (
func registerBucketsHandlers(api *operations.McsAPI) {
// list buckets
api.UserAPIListBucketsHandler = user_api.ListBucketsHandlerFunc(func(params user_api.ListBucketsParams, principal *models.Principal) middleware.Responder {
- listBucketsResponse, err := getListBucketsResponse()
+ sessionID := string(*principal)
+ listBucketsResponse, err := getListBucketsResponse(sessionID)
if err != nil {
return user_api.NewListBucketsDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
}
@@ -45,14 +46,16 @@ func registerBucketsHandlers(api *operations.McsAPI) {
})
// make bucket
api.UserAPIMakeBucketHandler = user_api.MakeBucketHandlerFunc(func(params user_api.MakeBucketParams, principal *models.Principal) middleware.Responder {
- if err := getMakeBucketResponse(params.Body); err != nil {
+ sessionID := string(*principal)
+ if err := getMakeBucketResponse(sessionID, params.Body); err != nil {
return user_api.NewMakeBucketDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
}
return user_api.NewMakeBucketCreated()
})
// delete bucket
api.UserAPIDeleteBucketHandler = user_api.DeleteBucketHandlerFunc(func(params user_api.DeleteBucketParams, principal *models.Principal) middleware.Responder {
- if err := getDeleteBucketResponse(params); err != nil {
+ sessionID := string(*principal)
+ if err := getDeleteBucketResponse(sessionID, params); err != nil {
return user_api.NewMakeBucketDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
}
@@ -60,7 +63,8 @@ func registerBucketsHandlers(api *operations.McsAPI) {
})
// get bucket info
api.UserAPIBucketInfoHandler = user_api.BucketInfoHandlerFunc(func(params user_api.BucketInfoParams, principal *models.Principal) middleware.Responder {
- bucketInfoResp, err := getBucketInfoResponse(params)
+ sessionID := string(*principal)
+ bucketInfoResp, err := getBucketInfoResponse(sessionID, params)
if err != nil {
return user_api.NewBucketInfoDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
}
@@ -69,7 +73,8 @@ func registerBucketsHandlers(api *operations.McsAPI) {
})
// set bucket policy
api.UserAPIBucketSetPolicyHandler = user_api.BucketSetPolicyHandlerFunc(func(params user_api.BucketSetPolicyParams, principal *models.Principal) middleware.Responder {
- bucketSetPolicyResp, err := getBucketSetPolicyResponse(params.Name, params.Body)
+ sessionID := string(*principal)
+ bucketSetPolicyResp, err := getBucketSetPolicyResponse(sessionID, params.Name, params.Body)
if err != nil {
return user_api.NewBucketSetPolicyDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
}
@@ -97,11 +102,10 @@ func listBuckets(ctx context.Context, client MinioClient) ([]*models.Bucket, err
}
// getListBucketsResponse performs listBuckets() and serializes it to the handler's output
-func getListBucketsResponse() (*models.ListBucketsResponse, error) {
+func getListBucketsResponse(sessionID string) (*models.ListBucketsResponse, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*20)
defer cancel()
-
- mClient, err := newMinioClient()
+ mClient, err := newMinioClient(sessionID)
if err != nil {
log.Println("error creating MinIO Client:", err)
return nil, err
@@ -133,7 +137,7 @@ func makeBucket(ctx context.Context, client MinioClient, bucketName string) erro
}
// getMakeBucketResponse performs makeBucket() to create a bucket with its access policy
-func getMakeBucketResponse(br *models.MakeBucketRequest) error {
+func getMakeBucketResponse(sessionID string, br *models.MakeBucketRequest) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*20)
defer cancel()
// bucket request needed to proceed
@@ -141,7 +145,7 @@ func getMakeBucketResponse(br *models.MakeBucketRequest) error {
log.Println("error bucket body not in request")
return errors.New(500, "error bucket body not in request")
}
- mClient, err := newMinioClient()
+ mClient, err := newMinioClient(sessionID)
if err != nil {
log.Println("error creating MinIO Client:", err)
return err
@@ -187,11 +191,11 @@ func setBucketAccessPolicy(ctx context.Context, client MinioClient, bucketName s
// getBucketSetPolicyResponse calls setBucketAccessPolicy() to set a access policy to a bucket
// and returns the serialized output.
-func getBucketSetPolicyResponse(bucketName string, req *models.SetBucketPolicyRequest) (*models.Bucket, error) {
+func getBucketSetPolicyResponse(sessionID string, bucketName string, req *models.SetBucketPolicyRequest) (*models.Bucket, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*20)
defer cancel()
- mClient, err := newMinioClient()
+ mClient, err := newMinioClient(sessionID)
if err != nil {
log.Println("error creating MinIO Client:", err)
return nil, err
@@ -223,14 +227,14 @@ func removeBucket(client MinioClient, bucketName string) error {
}
// getDeleteBucketResponse performs removeBucket() to delete a bucket
-func getDeleteBucketResponse(params user_api.DeleteBucketParams) error {
+func getDeleteBucketResponse(sessionID string, params user_api.DeleteBucketParams) error {
if params.Name == "" {
log.Println("error bucket name not in request")
return errors.New(500, "error bucket name not in request")
}
bucketName := params.Name
- mClient, err := newMinioClient()
+ mClient, err := newMinioClient(sessionID)
if err != nil {
log.Println("error creating MinIO Client:", err)
return err
@@ -272,8 +276,8 @@ func getBucketInfo(client MinioClient, bucketName string) (*models.Bucket, error
}
// getBucketInfoResponse calls getBucketInfo() to get the bucket's info
-func getBucketInfoResponse(params user_api.BucketInfoParams) (*models.Bucket, error) {
- mClient, err := newMinioClient()
+func getBucketInfoResponse(sessionID string, params user_api.BucketInfoParams) (*models.Bucket, error) {
+ mClient, err := newMinioClient(sessionID)
if err != nil {
log.Println("error creating MinIO Client:", err)
return nil, err
diff --git a/restapi/user_buckets_events.go b/restapi/user_buckets_events.go
index 76b9b25f9a..d78f624451 100644
--- a/restapi/user_buckets_events.go
+++ b/restapi/user_buckets_events.go
@@ -31,7 +31,8 @@ import (
func registerBucketEventsHandlers(api *operations.McsAPI) {
// list bucket events
api.UserAPIListBucketEventsHandler = user_api.ListBucketEventsHandlerFunc(func(params user_api.ListBucketEventsParams, principal *models.Principal) middleware.Responder {
- listBucketEventsResponse, err := getListBucketEventsResponse(params)
+ sessionID := string(*principal)
+ listBucketEventsResponse, err := getListBucketEventsResponse(sessionID, params)
if err != nil {
return user_api.NewListBucketEventsDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
}
@@ -124,8 +125,8 @@ func listBucketEvents(client MinioClient, bucketName string) ([]*models.Notifica
}
// getListBucketsResponse performs listBucketEvents() and serializes it to the handler's output
-func getListBucketEventsResponse(params user_api.ListBucketEventsParams) (*models.ListBucketEventsResponse, error) {
- mClient, err := newMinioClient()
+func getListBucketEventsResponse(sessionID string, params user_api.ListBucketEventsParams) (*models.ListBucketEventsResponse, error) {
+ mClient, err := newMinioClient(sessionID)
if err != nil {
log.Println("error creating MinIO Client:", err)
return nil, err
diff --git a/restapi/user_login.go b/restapi/user_login.go
index 7ee820c835..9e07f21ea3 100644
--- a/restapi/user_login.go
+++ b/restapi/user_login.go
@@ -20,31 +20,14 @@ import (
"errors"
"log"
- "github.com/minio/mc/pkg/probe"
-
- "github.com/minio/mcs/restapi/sessions"
-
"github.com/go-openapi/runtime/middleware"
"github.com/go-openapi/swag"
- mcCmd "github.com/minio/mc/cmd"
"github.com/minio/mcs/models"
+ "github.com/minio/mcs/pkg/auth"
"github.com/minio/mcs/restapi/operations"
"github.com/minio/mcs/restapi/operations/user_api"
)
-// Wraps the code at mc/cmd
-type McCmd interface {
- BuildS3Config(url, accessKey, secretKey, api, lookup string) (*mcCmd.Config, *probe.Error)
-}
-
-// Implementation of McCmd
-type mcCmdWrapper struct {
-}
-
-func (mc mcCmdWrapper) BuildS3Config(url, accessKey, secretKey, api, lookup string) (*mcCmd.Config, *probe.Error) {
- return mcCmd.BuildS3Config(url, accessKey, secretKey, api, lookup)
-}
-
func registerLoginHandlers(api *operations.McsAPI) {
// get login strategy
api.UserAPILoginDetailHandler = user_api.LoginDetailHandlerFunc(func(params user_api.LoginDetailParams) middleware.Responder {
@@ -59,28 +42,34 @@ func registerLoginHandlers(api *operations.McsAPI) {
}
return user_api.NewLoginCreated().WithPayload(loginResponse)
})
-
}
-var ErrInvalidCredentials = errors.New("invalid credentials")
+var ErrInvalidCredentials = errors.New("invalid minioCredentials")
-// login performs a check of credentials against MinIO
-func login(mc McCmd, accessKey, secretKey *string) (*string, error) {
- // Probe the credentials
- cfg, pErr := mc.BuildS3Config(getMinIOServer(), *accessKey, *secretKey, "", "auto")
- if pErr != nil {
+// login performs a check of minioCredentials against MinIO
+func login(credentials MCSCredentials) (*string, error) {
+ // try to obtain minioCredentials,
+ tokens, err := credentials.Get()
+ if err != nil {
return nil, ErrInvalidCredentials
}
- // if we made it here, the credentials work, generate a session
- sessionID := sessions.GetInstance().NewSession(cfg)
-
- return &sessionID, nil
+ // if we made it here, the minioCredentials work, generate a jwt with claims
+ jwt, err := auth.NewJWTWithClaimsForClient(&tokens, getMinIOServer())
+ if err != nil {
+ return nil, ErrInvalidCredentials
+ }
+ return &jwt, nil
}
// getLoginResponse performs login() and serializes it to the handler's output
func getLoginResponse(lr *models.LoginRequest) (*models.LoginResponse, error) {
- mc := mcCmdWrapper{}
- sessionID, err := login(&mc, lr.AccessKey, lr.SecretKey)
+ creds, err := newMcsCredentials(*lr.AccessKey, *lr.SecretKey, "")
+ if err != nil {
+ log.Println("error login:", err)
+ return nil, err
+ }
+ credentials := mcsCredentials{minioCredentials: creds}
+ sessionID, err := login(credentials)
if err != nil {
log.Println("error login:", err)
return nil, err
diff --git a/restapi/user_login_test.go b/restapi/user_login_test.go
index ff434fcd65..10cc735d1d 100644
--- a/restapi/user_login_test.go
+++ b/restapi/user_login_test.go
@@ -20,43 +20,41 @@ import (
"errors"
"testing"
- mcCmd "github.com/minio/mc/cmd"
- "github.com/minio/mc/pkg/probe"
+ "github.com/minio/minio-go/v6/pkg/credentials"
"github.com/stretchr/testify/assert"
)
-var mcBuildS3ConfigMock func(url, accessKey, secretKey, api, lookup string) (*mcCmd.Config, *probe.Error)
+// Define a mock struct of MCSCredentials interface implementation
+type mcsCredentialsMock struct{}
-type mcCmdMock struct{}
+// Common mocks
+var mcsCredentialsGetMock func() (credentials.Value, error)
-func (mc mcCmdMock) BuildS3Config(url, accessKey, secretKey, api, lookup string) (*mcCmd.Config, *probe.Error) {
- return mcBuildS3ConfigMock(url, accessKey, secretKey, api, lookup)
+// mock function of Get()
+func (ac mcsCredentialsMock) Get() (credentials.Value, error) {
+ return mcsCredentialsGetMock()
}
-// TestLogin tests the case of passing a valid and an invalid access/secret pair
func TestLogin(t *testing.T) {
- assert := assert.New(t)
- // We will write a test against play
- // Probe the credentials
- mcx := mcCmdMock{}
- access := "ABCDEFHIJK"
- secret := "ABCDEFHIJKABCDEFHIJK"
-
- // Test Case 1: Valid credentials
- mcBuildS3ConfigMock = func(url, accessKey, secretKey, api, lookup string) (config *mcCmd.Config, p *probe.Error) {
- return &mcCmd.Config{}, nil
+ funcAssert := assert.New(t)
+ mcsCredentials := mcsCredentialsMock{}
+ // Test Case 1: Valid mcsCredentials
+ mcsCredentialsGetMock = func() (credentials.Value, error) {
+ return credentials.Value{
+ AccessKeyID: "fakeAccessKeyID",
+ SecretAccessKey: "fakeSecretAccessKey",
+ SessionToken: "fakeSessionToken",
+ SignerType: 0,
+ }, nil
}
-
- sessionID, err := login(mcx, &access, &secret)
- assert.NotEmpty(sessionID, "Session ID was returned empty")
- assert.Nil(err, "error creating a session")
+ jwt, err := login(mcsCredentials)
+ funcAssert.NotEmpty(jwt, "JWT was returned empty")
+ funcAssert.Nil(err, "error creating a session")
// Test Case 2: Invalid credentials
- mcBuildS3ConfigMock = func(url, accessKey, secretKey, api, lookup string) (config *mcCmd.Config, p *probe.Error) {
- return nil, probe.NewError(errors.New("Bad credentials"))
+ mcsCredentialsGetMock = func() (credentials.Value, error) {
+ return credentials.Value{}, errors.New("")
}
-
- sessionID, err = login(mcx, &access, &secret)
- assert.Empty(sessionID, "Session ID was not returned empty")
- assert.NotNil(err, "not error returned creating a session")
+ _, err = login(mcsCredentials)
+ funcAssert.NotNil(err, "not error returned creating a session")
}
diff --git a/restapi/user_logout.go b/restapi/user_logout.go
index 99da2898b1..405e59a465 100644
--- a/restapi/user_logout.go
+++ b/restapi/user_logout.go
@@ -17,14 +17,13 @@
package restapi
import (
- "errors"
+ "log"
"github.com/go-openapi/runtime/middleware"
"github.com/go-openapi/swag"
"github.com/minio/mcs/models"
"github.com/minio/mcs/restapi/operations"
"github.com/minio/mcs/restapi/operations/user_api"
- "github.com/minio/mcs/restapi/sessions"
)
func registerLogoutHandlers(api *operations.McsAPI) {
@@ -38,21 +37,23 @@ func registerLogoutHandlers(api *operations.McsAPI) {
})
}
-// logout() deletes provided bearer token from in memory sessions map
-// then checks that the session actually got removed
-func logout(sessionID string) error {
- sessionsMap := sessions.GetInstance()
- sessionsMap.DeleteSession(sessionID)
- if sessionsMap.ValidSession(sessionID) {
- return errors.New("something went wrong deleting your session, please try again")
- }
- return nil
+// logout() call Expire() on the provided minioCredentials
+func logout(credentials MCSCredentials) {
+ credentials.Expire()
}
// getLogoutResponse performs logout() and returns nil or error
-func getLogoutResponse(sessionID string) error {
- if err := logout(sessionID); err != nil {
+func getLogoutResponse(jwt string) error {
+ creds, err := getMcsCredentialsFromJWT(jwt)
+ if err != nil {
+ log.Println(err)
+ return err
+ }
+ credentials := mcsCredentials{minioCredentials: creds}
+ if err != nil {
+ log.Println("error creating MinIO Client:", err)
return err
}
+ logout(credentials)
return nil
}
diff --git a/restapi/user_logout_test.go b/restapi/user_logout_test.go
index 21efe4fa8d..f244e1e6e9 100644
--- a/restapi/user_logout_test.go
+++ b/restapi/user_logout_test.go
@@ -1,21 +1,29 @@
+// This file is part of MinIO Console Server
+// Copyright (c) 2020 MinIO, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
package restapi
-import (
- "testing"
+import "testing"
- mcCmd "github.com/minio/mc/cmd"
- "github.com/minio/mcs/restapi/sessions"
-)
+// mock function of Get()
+func (ac mcsCredentialsMock) Expire() {
+ // Do nothing
+ // Implementing this method for the mcsCredentials interface
+}
-// TestLogout tests the case of deleting a valid session id
func TestLogout(t *testing.T) {
- cfg := mcCmd.Config{}
- // Creating a new session
- sessionID := sessions.GetInstance().NewSession(&cfg)
- // Test Case 1: Delete a session Valid sessionID
- function := "logout()"
- err := logout(sessionID)
- if err != nil {
- t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
- }
+ // There's nothing to test right now
}