This PR adds the following features:
- Allow user to provide its own keypair certificates for enable TLS in
MinIO
- Allow user to configure data encryption at rest in MinIO with KES
- Removes JWT schema for login and instead Console authentication will use
encrypted session tokens
Enable TLS between client and MinIO with user provided certificates
Instead of using AutoCert feature now the user can provide `cert` and
`key` via `tls` object, values must be valid `x509.Certificate`
formatted files encoded in `base64`
Enable encryption at rest configuring KES
User can deploy KES via Console/Operator by defining the encryption
object, AutoCert must be enabled or custom certificates for KES must be
provided, KES support 3 KMS backends: `Vault`, `AWS KMS` and `Gemalto`,
previous configuration of the KMS is necessary.
eg of body request for create-tenant
```
{
"name": "honeywell",
"access_key": "minio",
"secret_key": "minio123",
"enable_mcs": false,
"enable_ssl": false,
"service_name": "honeywell",
"zones": [
{
"name": "honeywell-zone-1",
"servers": 1,
"volumes_per_server": 4,
"volume_configuration": {
"size": 256000000,
"storage_class": "vsan-default-storage-policy"
}
}
],
"namespace": "default",
"tls": {
"tls.crt": "",
"tls.key": ""
},
"encryption": {
"server": {
"tls.crt": "",
"tls.key": ""
},
"client": {
"tls.crt": "",
"tls.key": ""
},
"vault": {
"endpoint": "http://vault:8200",
"prefix": "",
"approle": {
"id": "",
"secret": ""
}
}
}
}
```
283 lines
9.9 KiB
Go
283 lines
9.9 KiB
Go
// 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 (
|
|
"context"
|
|
"errors"
|
|
"log"
|
|
|
|
"github.com/go-openapi/runtime/middleware"
|
|
"github.com/go-openapi/swag"
|
|
"github.com/minio/console/models"
|
|
"github.com/minio/console/pkg/acl"
|
|
"github.com/minio/console/pkg/auth"
|
|
"github.com/minio/console/pkg/auth/idp/oauth2"
|
|
"github.com/minio/console/pkg/auth/utils"
|
|
"github.com/minio/console/restapi/operations"
|
|
"github.com/minio/console/restapi/operations/user_api"
|
|
)
|
|
|
|
var (
|
|
errorGeneric = errors.New("an error occurred, please try again")
|
|
errInvalidCredentials = errors.New("invalid Login")
|
|
)
|
|
|
|
func registerLoginHandlers(api *operations.ConsoleAPI) {
|
|
// get login strategy
|
|
api.UserAPILoginDetailHandler = user_api.LoginDetailHandlerFunc(func(params user_api.LoginDetailParams) middleware.Responder {
|
|
loginDetails, err := getLoginDetailsResponse()
|
|
if err != nil {
|
|
return user_api.NewLoginDetailDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
|
|
}
|
|
return user_api.NewLoginDetailOK().WithPayload(loginDetails)
|
|
})
|
|
// post login
|
|
api.UserAPILoginHandler = user_api.LoginHandlerFunc(func(params user_api.LoginParams) middleware.Responder {
|
|
loginResponse, err := getLoginResponse(params.Body)
|
|
if err != nil {
|
|
return user_api.NewLoginDefault(401).WithPayload(&models.Error{Code: 401, Message: swag.String(err.Error())})
|
|
}
|
|
return user_api.NewLoginCreated().WithPayload(loginResponse)
|
|
})
|
|
api.UserAPILoginOauth2AuthHandler = user_api.LoginOauth2AuthHandlerFunc(func(params user_api.LoginOauth2AuthParams) middleware.Responder {
|
|
loginResponse, err := getLoginOauth2AuthResponse(params.Body)
|
|
if err != nil {
|
|
return user_api.NewLoginOauth2AuthDefault(401).WithPayload(&models.Error{Code: 401, Message: swag.String(err.Error())})
|
|
}
|
|
return user_api.NewLoginOauth2AuthCreated().WithPayload(loginResponse)
|
|
})
|
|
api.UserAPILoginOperatorHandler = user_api.LoginOperatorHandlerFunc(func(params user_api.LoginOperatorParams) middleware.Responder {
|
|
loginResponse, err := getLoginOperatorResponse(params.Body)
|
|
if err != nil {
|
|
return user_api.NewLoginOperatorDefault(401).WithPayload(&models.Error{Code: 401, Message: swag.String(err.Error())})
|
|
}
|
|
return user_api.NewLoginOperatorCreated().WithPayload(loginResponse)
|
|
})
|
|
}
|
|
|
|
// login performs a check of consoleCredentials against MinIO, generates some claims and returns the jwt
|
|
// for subsequent authentication
|
|
func login(credentials ConsoleCredentials, actions []string) (*string, error) {
|
|
// try to obtain consoleCredentials,
|
|
tokens, err := credentials.Get()
|
|
if err != nil {
|
|
log.Println("error authenticating user", err)
|
|
return nil, errInvalidCredentials
|
|
}
|
|
// if we made it here, the consoleCredentials work, generate a jwt with claims
|
|
jwt, err := auth.NewEncryptedTokenForClient(&tokens, actions)
|
|
if err != nil {
|
|
log.Println("error authenticating user", err)
|
|
return nil, errInvalidCredentials
|
|
}
|
|
return &jwt, nil
|
|
}
|
|
|
|
func getConfiguredRegionForLogin(client MinioAdmin) (string, error) {
|
|
location := ""
|
|
configuration, err := getConfig(client, "region")
|
|
if err != nil {
|
|
log.Println("error obtaining MinIO region:", err)
|
|
return location, errorGeneric
|
|
}
|
|
// region is an array of 1 element
|
|
if len(configuration) > 0 {
|
|
location = configuration[0].Value
|
|
}
|
|
return location, nil
|
|
}
|
|
|
|
// getLoginResponse performs login() and serializes it to the handler's output
|
|
func getLoginResponse(lr *models.LoginRequest) (*models.LoginResponse, error) {
|
|
ctx := context.Background()
|
|
mAdmin, err := newSuperMAdminClient()
|
|
if err != nil {
|
|
log.Println("error creating Madmin Client:", err)
|
|
return nil, errorGeneric
|
|
}
|
|
adminClient := adminClient{client: mAdmin}
|
|
// obtain the configured MinIO region
|
|
// need it for user authentication
|
|
location, err := getConfiguredRegionForLogin(adminClient)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
creds, err := newConsoleCredentials(*lr.AccessKey, *lr.SecretKey, location)
|
|
if err != nil {
|
|
log.Println("error login:", err)
|
|
return nil, errInvalidCredentials
|
|
}
|
|
credentials := consoleCredentials{consoleCredentials: creds}
|
|
// obtain the current policy assigned to this user
|
|
// necessary for generating the list of allowed endpoints
|
|
userInfo, err := adminClient.getUserInfo(ctx, *lr.AccessKey)
|
|
if err != nil {
|
|
log.Println("error login:", err)
|
|
return nil, errInvalidCredentials
|
|
}
|
|
policy, _ := adminClient.getPolicy(ctx, userInfo.PolicyName)
|
|
// by default every user starts with an empty array of available actions
|
|
// therefore we would have access only to pages that doesn't require any privilege
|
|
// ie: service-account page
|
|
var actions []string
|
|
// if a policy is assigned to this user we parse the actions from there
|
|
if policy != nil {
|
|
actions = acl.GetActionsStringFromPolicy(policy)
|
|
}
|
|
sessionID, err := login(credentials, actions)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// serialize output
|
|
loginResponse := &models.LoginResponse{
|
|
SessionID: *sessionID,
|
|
}
|
|
return loginResponse, nil
|
|
}
|
|
|
|
// getLoginDetailsResponse returns information regarding the Console authentication mechanism.
|
|
func getLoginDetailsResponse() (*models.LoginDetails, error) {
|
|
ctx := context.Background()
|
|
loginStrategy := models.LoginDetailsLoginStrategyForm
|
|
redirectURL := ""
|
|
if acl.GetOperatorMode() {
|
|
loginStrategy = models.LoginDetailsLoginStrategyServiceAccount
|
|
} else if oauth2.IsIdpEnabled() {
|
|
loginStrategy = models.LoginDetailsLoginStrategyRedirect
|
|
// initialize new oauth2 client
|
|
oauth2Client, err := oauth2.NewOauth2ProviderClient(ctx, nil)
|
|
if err != nil {
|
|
log.Println("error getting new oauth2 provider client", err)
|
|
return nil, errorGeneric
|
|
}
|
|
// Validate user against IDP
|
|
identityProvider := &auth.IdentityProvider{Client: oauth2Client}
|
|
redirectURL = identityProvider.GenerateLoginURL()
|
|
}
|
|
loginDetails := &models.LoginDetails{
|
|
LoginStrategy: loginStrategy,
|
|
Redirect: redirectURL,
|
|
}
|
|
return loginDetails, nil
|
|
}
|
|
|
|
func loginOauth2Auth(ctx context.Context, provider *auth.IdentityProvider, code, state string) (*oauth2.User, error) {
|
|
userIdentity, err := provider.VerifyIdentity(ctx, code, state)
|
|
if err != nil {
|
|
log.Println("error validating user identity against idp:", err)
|
|
return nil, errorGeneric
|
|
}
|
|
return userIdentity, nil
|
|
}
|
|
|
|
func getLoginOauth2AuthResponse(lr *models.LoginOauth2AuthRequest) (*models.LoginResponse, error) {
|
|
ctx := context.Background()
|
|
if oauth2.IsIdpEnabled() {
|
|
// initialize new oauth2 client
|
|
oauth2Client, err := oauth2.NewOauth2ProviderClient(ctx, nil)
|
|
if err != nil {
|
|
log.Println("error getting new oauth2 client:", err)
|
|
return nil, errorGeneric
|
|
}
|
|
// initialize new identity provider
|
|
identityProvider := &auth.IdentityProvider{Client: oauth2Client}
|
|
// Validate user against IDP
|
|
identity, err := loginOauth2Auth(ctx, identityProvider, *lr.Code, *lr.State)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
mAdmin, err := newSuperMAdminClient()
|
|
if err != nil {
|
|
log.Println("error creating Madmin Client:", err)
|
|
return nil, errorGeneric
|
|
}
|
|
adminClient := adminClient{client: mAdmin}
|
|
accessKey := identity.Email
|
|
secretKey := utils.RandomCharString(32)
|
|
// obtain the configured MinIO region
|
|
// need it for user authentication
|
|
location, err := getConfiguredRegionForLogin(adminClient)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// create user in MinIO
|
|
if _, err := addUser(ctx, adminClient, &accessKey, &secretKey, []string{}); err != nil {
|
|
log.Println("error adding user:", err)
|
|
return nil, errorGeneric
|
|
}
|
|
// rollback user if there's an error after this point
|
|
defer func() {
|
|
if err != nil {
|
|
if errRemove := removeUser(ctx, adminClient, accessKey); errRemove != nil {
|
|
log.Println("error removing user:", errRemove)
|
|
}
|
|
}
|
|
}()
|
|
// assign the "consoleAdmin" policy to this user
|
|
policyName := oauth2.GetIDPPolicyForUser()
|
|
if err := setPolicy(ctx, adminClient, policyName, accessKey, models.PolicyEntityUser); err != nil {
|
|
log.Println("error setting policy:", err)
|
|
return nil, errorGeneric
|
|
}
|
|
// obtain the current policy details, necessary for generating the list of allowed endpoints
|
|
policy, err := adminClient.getPolicy(ctx, policyName)
|
|
if err != nil {
|
|
log.Println("error reading policy:", err)
|
|
return nil, errorGeneric
|
|
}
|
|
actions := acl.GetActionsStringFromPolicy(policy)
|
|
// User was created correctly, create a new session/JWT
|
|
creds, err := newConsoleCredentials(accessKey, secretKey, location)
|
|
if err != nil {
|
|
log.Println("error login:", err)
|
|
return nil, errorGeneric
|
|
}
|
|
credentials := consoleCredentials{consoleCredentials: creds}
|
|
jwt, err := login(credentials, actions)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// serialize output
|
|
loginResponse := &models.LoginResponse{
|
|
SessionID: *jwt,
|
|
}
|
|
return loginResponse, nil
|
|
}
|
|
return nil, errorGeneric
|
|
}
|
|
|
|
// getLoginOperatorResponse validate the provided service account token against k8s api
|
|
func getLoginOperatorResponse(lmr *models.LoginOperatorRequest) (*models.LoginResponse, error) {
|
|
creds, err := newConsoleCredentials("", *lmr.Jwt, "")
|
|
if err != nil {
|
|
log.Println("error login:", err)
|
|
return nil, errInvalidCredentials
|
|
}
|
|
credentials := consoleCredentials{consoleCredentials: creds}
|
|
var actions []string
|
|
jwt, err := login(credentials, actions)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// serialize output
|
|
loginResponse := &models.LoginResponse{
|
|
SessionID: *jwt,
|
|
}
|
|
return loginResponse, nil
|
|
}
|