STS integration, JWT auth and Stateless MCS (#70)

This commit changes the authentication mechanism between mcs and minio to an sts
(security token service) schema using the user provided credentials, previously
mcs was using master credentials. With that said in order for you to
login to MCS as an admin your user must exists first on minio and have enough
privileges to do administrative operations.

```
./mc admin user add myminio alevsk alevsk12345
```

```
cat admin.json

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "admin:*",
        "s3:*"
      ],
      "Resource": [
        "arn:aws:s3:::*"
      ]
    }
  ]
}

./mc admin policy add myminio admin admin.json
```

```
./mc admin policy set myminio admin user=alevsk
```
This commit is contained in:
Lenin Alevski
2020-04-22 23:43:17 -07:00
committed by GitHub
parent 605b80037a
commit 0f52136fd2
29 changed files with 916 additions and 303 deletions

View File

@@ -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)

View File

@@ -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

4
go.mod
View File

@@ -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
)

2
go.sum
View File

@@ -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=

180
pkg/auth/jwt.go Normal file
View File

@@ -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 <http://www.gnu.org/licenses/>.
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
}

View File

@@ -14,58 +14,18 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
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)
}

24
pkg/auth/jwt/const.go Normal file
View File

@@ -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 <http://www.gnu.org/licenses/>.
package jwt
const (
McsHmacJWTSecret = "MCS_HMAC_JWT_SECRET"
McsSTSAndJWTDurationSeconds = "MCS_STS_AND_JWT_DURATION_SECONDS"
McsPBKDFPassphrase = "MCS_PBKDF_PASSPHRASE"
McsPBKDFSalt = "MCS_PBKDF_SALT"
)

281
pkg/auth/jwt/parser.go Normal file
View File

@@ -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 <http://www.gnu.org/licenses/>.
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)
}

80
pkg/auth/jwt_test.go Normal file
View File

@@ -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 <http://www.gnu.org/licenses/>.
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))
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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(&params)
sessionID := string(*principal)
notifEndpoints, err := getAddNotificationEndpointResponse(sessionID, &params)
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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
}

View File

@@ -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()
}
// Initialize minio client object.
minioClient, err := minio.NewV4(endpoint, accessKeyID, secretAccessKey, useSSL)
// 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()
}
// 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

View File

@@ -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, "")
}

View File

@@ -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
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
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")
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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")
}

View File

@@ -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
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
package restapi
import (
"testing"
import "testing"
mcCmd "github.com/minio/mc/cmd"
"github.com/minio/mcs/restapi/sessions"
)
// 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())
}
// mock function of Get()
func (ac mcsCredentialsMock) Expire() {
// Do nothing
// Implementing this method for the mcsCredentials interface
}
func TestLogout(t *testing.T) {
// There's nothing to test right now
}