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 }