Migrate config to KV data format (#8392)

- adding oauth support to MinIO browser (#8400) by @kanagaraj
- supports multi-line get/set/del for all config fields
- add support for comments, allow toggle
- add extensive validation of config before saving
- support MinIO browser to support proper claims, using STS tokens
- env support for all config parameters, legacy envs are also
  supported with all documentation now pointing to latest ENVs
- preserve accessKey/secretKey from FS mode setups
- add history support implements three APIs
  - ClearHistory
  - RestoreHistory
  - ListHistory
- add help command support for each config parameters
- all the bug fixes after migration to KV, and other bug
  fixes encountered during testing.
This commit is contained in:
Harshavardhana
2019-10-22 22:59:13 -07:00
committed by kannappanr
parent 8836d57e3c
commit ee4a6a823d
185 changed files with 8228 additions and 3597 deletions

View File

@@ -68,6 +68,20 @@ func IsSecretKeyValid(secretKey string) bool {
return len(secretKey) >= secretKeyMinLen
}
// Default access and secret keys.
const (
DefaultAccessKey = "minioadmin"
DefaultSecretKey = "minioadmin"
)
// Default access credentials
var (
DefaultCredentials = Credentials{
AccessKey: DefaultAccessKey,
SecretKey: DefaultSecretKey,
}
)
// Credentials holds access and secret keys.
type Credentials struct {
AccessKey string `xml:"AccessKeyId" json:"accessKey,omitempty"`
@@ -77,6 +91,22 @@ type Credentials struct {
Status string `xml:"-" json:"status,omitempty"`
}
func (cred Credentials) String() string {
var s strings.Builder
s.WriteString(cred.AccessKey)
s.WriteString(":")
s.WriteString(cred.SecretKey)
if cred.SessionToken != "" {
s.WriteString("\n")
s.WriteString(cred.SessionToken)
}
if !cred.Expiration.IsZero() && cred.Expiration != timeSentinel {
s.WriteString("\n")
s.WriteString(cred.Expiration.String())
}
return s.String()
}
// IsExpired - returns whether Credential is expired or not.
func (cred Credentials) IsExpired() bool {
if cred.Expiration.IsZero() || cred.Expiration == timeSentinel {
@@ -89,10 +119,10 @@ func (cred Credentials) IsExpired() bool {
// IsValid - returns whether credential is valid or not.
func (cred Credentials) IsValid() bool {
// Verify credentials if its enabled or not set.
if cred.Status == "enabled" || cred.Status == "" {
return IsAccessKeyValid(cred.AccessKey) && IsSecretKeyValid(cred.SecretKey) && !cred.IsExpired()
if cred.Status == "off" {
return false
}
return false
return IsAccessKeyValid(cred.AccessKey) && IsSecretKeyValid(cred.SecretKey) && !cred.IsExpired()
}
// Equal - returns whether two credentials are equal or not.
@@ -156,7 +186,7 @@ func GetNewCredentialsWithMetadata(m map[string]interface{}, tokenSecret string)
return cred, err
}
cred.SecretKey = strings.Replace(string([]byte(base64.StdEncoding.EncodeToString(keyBytes))[:secretKeyMaxLen]), "/", "+", -1)
cred.Status = "enabled"
cred.Status = "on"
expiry, err := expToInt64(m["exp"])
if err != nil {
@@ -196,6 +226,6 @@ func CreateCredentials(accessKey, secretKey string) (cred Credentials, err error
cred.AccessKey = accessKey
cred.SecretKey = secretKey
cred.Expiration = timeSentinel
cred.Status = "enabled"
cred.Status = "on"
return cred, nil
}

View File

@@ -40,9 +40,21 @@ var (
}
return fmt.Sprintf
}()
Green = func() func(a ...interface{}) string {
if IsTerminal() {
return color.New(color.FgGreen).SprintFunc()
}
return fmt.Sprint
}()
GreenBold = func() func(a ...interface{}) string {
if IsTerminal() {
return color.New(color.FgGreen, color.Bold).SprintFunc()
}
return fmt.Sprint
}()
CyanBold = func() func(a ...interface{}) string {
if IsTerminal() {
color.New(color.FgCyan, color.Bold).SprintFunc()
return color.New(color.FgCyan, color.Bold).SprintFunc()
}
return fmt.Sprint
}()
@@ -52,6 +64,12 @@ var (
}
return fmt.Sprintf
}()
BlueBold = func() func(format string, a ...interface{}) string {
if IsTerminal() {
return color.New(color.FgBlue, color.Bold).SprintfFunc()
}
return fmt.Sprintf
}()
BgYellow = func() func(format string, a ...interface{}) string {
if IsTerminal() {
return color.New(color.BgYellow).SprintfFunc()

View File

@@ -167,6 +167,13 @@ func (q *Queue) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
// Validate - checks whether queue has valid values or not.
func (q Queue) Validate(region string, targetList *TargetList) error {
if q.ARN.region == "" {
if !targetList.Exists(q.ARN.TargetID) {
return &ErrARNNotFound{q.ARN}
}
return nil
}
if region != "" && q.ARN.region != region {
return &ErrUnknownRegion{q.ARN.region}
}

View File

@@ -49,6 +49,43 @@ type AMQPArgs struct {
QueueLimit uint64 `json:"queueLimit"`
}
// AMQP input constants.
const (
AmqpQueueDir = "queue_dir"
AmqpQueueLimit = "queue_limit"
AmqpURL = "url"
AmqpExchange = "exchange"
AmqpRoutingKey = "routing_key"
AmqpExchangeType = "exchange_type"
AmqpDeliveryMode = "delivery_mode"
AmqpMandatory = "mandatory"
AmqpImmediate = "immediate"
AmqpDurable = "durable"
AmqpInternal = "internal"
AmqpNoWait = "no_wait"
AmqpAutoDeleted = "auto_deleted"
AmqpArguments = "arguments"
AmqpPublishingHeaders = "publishing_headers"
EnvAMQPState = "MINIO_NOTIFY_AMQP_STATE"
EnvAMQPURL = "MINIO_NOTIFY_AMQP_URL"
EnvAMQPExchange = "MINIO_NOTIFY_AMQP_EXCHANGE"
EnvAMQPRoutingKey = "MINIO_NOTIFY_AMQP_ROUTING_KEY"
EnvAMQPExchangeType = "MINIO_NOTIFY_AMQP_EXCHANGE_TYPE"
EnvAMQPDeliveryMode = "MINIO_NOTIFY_AMQP_DELIVERY_MODE"
EnvAMQPMandatory = "MINIO_NOTIFY_AMQP_MANDATORY"
EnvAMQPImmediate = "MINIO_NOTIFY_AMQP_IMMEDIATE"
EnvAMQPDurable = "MINIO_NOTIFY_AMQP_DURABLE"
EnvAMQPInternal = "MINIO_NOTIFY_AMQP_INTERNAL"
EnvAMQPNoWait = "MINIO_NOTIFY_AMQP_NO_WAIT"
EnvAMQPAutoDeleted = "MINIO_NOTIFY_AMQP_AUTO_DELETED"
EnvAMQPArguments = "MINIO_NOTIFY_AMQP_ARGUMENTS"
EnvAMQPPublishingHeaders = "MINIO_NOTIFY_AMQP_PUBLISHING_HEADERS"
EnvAMQPQueueDir = "MINIO_NOTIFY_AMQP_QUEUE_DIR"
EnvAMQPQueueLimit = "MINIO_NOTIFY_AMQP_QUEUE_LIMIT"
)
// Validate AMQP arguments
func (a *AMQPArgs) Validate() error {
if !a.Enable {

View File

@@ -31,6 +31,22 @@ import (
"gopkg.in/olivere/elastic.v5"
)
// Elastic constants
const (
ElasticFormat = "format"
ElasticURL = "url"
ElasticIndex = "index"
ElasticQueueDir = "queue_dir"
ElasticQueueLimit = "queue_limit"
EnvElasticState = "MINIO_NOTIFY_ELASTICSEARCH_STATE"
EnvElasticFormat = "MINIO_NOTIFY_ELASTICSEARCH_FORMAT"
EnvElasticURL = "MINIO_NOTIFY_ELASTICSEARCH_URL"
EnvElasticIndex = "MINIO_NOTIFY_ELASTICSEARCH_INDEX"
EnvElasticQueueDir = "MINIO_NOTIFY_ELASTICSEARCH_QUEUE_DIR"
EnvElasticQueueLimit = "MINIO_NOTIFY_ELASTICSEARCH_QUEUE_LIMIT"
)
// ElasticsearchArgs - Elasticsearch target arguments.
type ElasticsearchArgs struct {
Enable bool `json:"enable"`

View File

@@ -33,6 +33,32 @@ import (
sarama "gopkg.in/Shopify/sarama.v1"
)
// MQTT input constants
const (
KafkaBrokers = "brokers"
KafkaTopic = "topic"
KafkaQueueDir = "queue_dir"
KafkaQueueLimit = "queue_limit"
KafkaTLSEnable = "tls_enable"
KafkaTLSSkipVerify = "tls_skip_verify"
KafkaTLSClientAuth = "tls_client_auth"
KafkaSASLEnable = "sasl_enable"
KafkaSASLUsername = "sasl_username"
KafkaSASLPassword = "sasl_password"
EnvKafkaState = "MINIO_NOTIFY_KAFKA_STATE"
EnvKafkaBrokers = "MINIO_NOTIFY_KAFKA_BROKERS"
EnvKafkaTopic = "MINIO_NOTIFY_KAFKA_TOPIC"
EnvKafkaQueueDir = "MINIO_NOTIFY_KAFKA_QUEUE_DIR"
EnvKafkaQueueLimit = "MINIO_NOTIFY_KAFKA_QUEUE_LIMIT"
EnvKafkaTLSEnable = "MINIO_NOTIFY_KAFKA_TLS_ENABLE"
EnvKafkaTLSSkipVerify = "MINIO_NOTIFY_KAFKA_TLS_SKIP_VERIFY"
EnvKafkaTLSClientAuth = "MINIO_NOTIFY_KAFKA_TLS_CLIENT_AUTH"
EnvKafkaSASLEnable = "MINIO_NOTIFY_KAFKA_SASL_ENABLE"
EnvKafkaSASLUsername = "MINIO_NOTIFY_KAFKA_SASL_USERNAME"
EnvKafkaSASLPassword = "MINIO_NOTIFY_KAFKA_SASL_PASSWORD"
)
// KafkaArgs - Kafka target arguments.
type KafkaArgs struct {
Enable bool `json:"enable"`

View File

@@ -38,6 +38,30 @@ const (
storePrefix = "minio"
)
// MQTT input constants
const (
MqttBroker = "broker"
MqttTopic = "topic"
MqttQoS = "qos"
MqttUsername = "username"
MqttPassword = "password"
MqttReconnectInterval = "reconnect_interval"
MqttKeepAliveInterval = "keep_alive_interval"
MqttQueueDir = "queue_dir"
MqttQueueLimit = "queue_limit"
EnvMQTTState = "MINIO_NOTIFY_MQTT_STATE"
EnvMQTTBroker = "MINIO_NOTIFY_MQTT_BROKER"
EnvMQTTTopic = "MINIO_NOTIFY_MQTT_TOPIC"
EnvMQTTQoS = "MINIO_NOTIFY_MQTT_QOS"
EnvMQTTUsername = "MINIO_NOTIFY_MQTT_USERNAME"
EnvMQTTPassword = "MINIO_NOTIFY_MQTT_PASSWORD"
EnvMQTTReconnectInterval = "MINIO_NOTIFY_MQTT_RECONNECT_INTERVAL"
EnvMQTTKeepAliveInterval = "MINIO_NOTIFY_MQTT_KEEP_ALIVE_INTERVAL"
EnvMQTTQueueDir = "MINIO_NOTIFY_MQTT_QUEUE_DIR"
EnvMQTTQueueLimit = "MINIO_NOTIFY_MQTT_QUEUE_LIMIT"
)
// MQTTArgs - MQTT target arguments.
type MQTTArgs struct {
Enable bool `json:"enable"`

View File

@@ -81,6 +81,32 @@ const (
mysqlInsertRow = `INSERT INTO %s (event_time, event_data) VALUES (?, ?);`
)
// MySQL related constants
const (
MySQLFormat = "format"
MySQLDSNString = "dsn_string"
MySQLTable = "table"
MySQLHost = "host"
MySQLPort = "port"
MySQLUsername = "username"
MySQLPassword = "password"
MySQLDatabase = "database"
MySQLQueueLimit = "queue_limit"
MySQLQueueDir = "queue_dir"
EnvMySQLState = "MINIO_NOTIFY_MYSQL_STATE"
EnvMySQLFormat = "MINIO_NOTIFY_MYSQL_FORMAT"
EnvMySQLDSNString = "MINIO_NOTIFY_MYSQL_DSN_STRING"
EnvMySQLTable = "MINIO_NOTIFY_MYSQL_TABLE"
EnvMySQLHost = "MINIO_NOTIFY_MYSQL_HOST"
EnvMySQLPort = "MINIO_NOTIFY_MYSQL_PORT"
EnvMySQLUsername = "MINIO_NOTIFY_MYSQL_USERNAME"
EnvMySQLPassword = "MINIO_NOTIFY_MYSQL_PASSWORD"
EnvMySQLDatabase = "MINIO_NOTIFY_MYSQL_DATABASE"
EnvMySQLQueueLimit = "MINIO_NOTIFY_MYSQL_QUEUE_LIMIT"
EnvMySQLQueueDir = "MINIO_NOTIFY_MYSQL_QUEUE_DIR"
)
// MySQLArgs - MySQL target arguments.
type MySQLArgs struct {
Enable bool `json:"enable"`

View File

@@ -30,6 +30,42 @@ import (
"github.com/nats-io/stan.go"
)
// NATS related constants
const (
NATSAddress = "address"
NATSSubject = "subject"
NATSUsername = "username"
NATSPassword = "password"
NATSToken = "token"
NATSSecure = "secure"
NATSPingInterval = "ping_interval"
NATSQueueDir = "queue_dir"
NATSQueueLimit = "queue_limit"
// Streaming constants
NATSStreamingEnable = "streaming_enable"
NATSStreamingClusterID = "streaming_cluster_id"
NATSStreamingAsync = "streaming_async"
NATSStreamingMaxPubAcksInFlight = "streaming_max_pub_acks_in_flight"
EnvNATSState = "MINIO_NOTIFY_NATS_STATE"
EnvNATSAddress = "MINIO_NOTIFY_NATS_ADDRESS"
EnvNATSSubject = "MINIO_NOTIFY_NATS_SUBJECT"
EnvNATSUsername = "MINIO_NOTIFY_NATS_USERNAME"
EnvNATSPassword = "MINIO_NOTIFY_NATS_PASSWORD"
EnvNATSToken = "MINIO_NOTIFY_NATS_TOKEN"
EnvNATSSecure = "MINIO_NOTIFY_NATS_SECURE"
EnvNATSPingInterval = "MINIO_NOTIFY_NATS_PING_INTERVAL"
EnvNATSQueueDir = "MINIO_NOTIFY_NATS_QUEUE_DIR"
EnvNATSQueueLimit = "MINIO_NOTIFY_NATS_QUEUE_LIMIT"
// Streaming constants
EnvNATSStreamingEnable = "MINIO_NOTIFY_NATS_STREAMING_ENABLE"
EnvNATSStreamingClusterID = "MINIO_NOTIFY_NATS_STREAMING_CLUSTER_ID"
EnvNATSStreamingAsync = "MINIO_NOTIFY_NATS_STREAMING_ASYNC"
EnvNATSStreamingMaxPubAcksInFlight = "MINIO_NOTIFY_NATS_STREAMING_MAX_PUB_ACKS_IN_FLIGHT"
)
// NATSArgs - NATS target arguments.
type NATSArgs struct {
Enable bool `json:"enable"`

View File

@@ -31,6 +31,24 @@ import (
xnet "github.com/minio/minio/pkg/net"
)
// NSQ constants
const (
NSQAddress = "nsqd_address"
NSQTopic = "topic"
NSQTLSEnable = "tls_enable"
NSQTLSSkipVerify = "tls_skip_verify"
NSQQueueDir = "queue_dir"
NSQQueueLimit = "queue_limit"
EnvNSQState = "MINIO_NOTIFY_NSQ"
EnvNSQAddress = "MINIO_NOTIFY_NSQ_NSQD_ADDRESS"
EnvNSQTopic = "MINIO_NOTIFY_NSQ_TOPIC"
EnvNSQTLSEnable = "MINIO_NOTIFY_NSQ_TLS_ENABLE"
EnvNSQTLSSkipVerify = "MINIO_NOTIFY_NSQ_TLS_SKIP_VERIFY"
EnvNSQQueueDir = "MINIO_NOTIFY_NSQ_QUEUE_DIR"
EnvNSQQueueLimit = "MINIO_NOTIFY_NSQ_QUEUE_LIMIT"
)
// NSQArgs - NSQ target arguments.
type NSQArgs struct {
Enable bool `json:"enable"`

View File

@@ -82,6 +82,32 @@ const (
psqlInsertRow = `INSERT INTO %s (event_time, event_data) VALUES ($1, $2);`
)
// Postgres constants
const (
PostgresFormat = "format"
PostgresConnectionString = "connection_string"
PostgresTable = "table"
PostgresHost = "host"
PostgresPort = "port"
PostgresUsername = "username"
PostgresPassword = "password"
PostgresDatabase = "database"
PostgresQueueDir = "queue_dir"
PostgresQueueLimit = "queue_limit"
EnvPostgresState = "MINIO_NOTIFY_POSTGRES_STATE"
EnvPostgresFormat = "MINIO_NOTIFY_POSTGRES_FORMAT"
EnvPostgresConnectionString = "MINIO_NOTIFY_POSTGRES_CONNECTION_STRING"
EnvPostgresTable = "MINIO_NOTIFY_POSTGRES_TABLE"
EnvPostgresHost = "MINIO_NOTIFY_POSTGRES_HOST"
EnvPostgresPort = "MINIO_NOTIFY_POSTGRES_PORT"
EnvPostgresUsername = "MINIO_NOTIFY_POSTGRES_USERNAME"
EnvPostgresPassword = "MINIO_NOTIFY_POSTGRES_PASSWORD"
EnvPostgresDatabase = "MINIO_NOTIFY_POSTGRES_DATABASE"
EnvPostgresQueueDir = "MINIO_NOTIFY_POSTGRES_QUEUE_DIR"
EnvPostgresQueueLimit = "MINIO_NOTIFY_POSTGRES_QUEUE_LIMIT"
)
// PostgreSQLArgs - PostgreSQL target arguments.
type PostgreSQLArgs struct {
Enable bool `json:"enable"`

View File

@@ -32,6 +32,24 @@ import (
xnet "github.com/minio/minio/pkg/net"
)
// Redis constants
const (
RedisFormat = "format"
RedisAddress = "address"
RedisPassword = "password"
RedisKey = "key"
RedisQueueDir = "queue_dir"
RedisQueueLimit = "queue_limit"
EnvRedisState = "MINIO_NOTIFY_REDIS_STATE"
EnvRedisFormat = "MINIO_NOTIFY_REDIS_FORMAT"
EnvRedisAddress = "MINIO_NOTIFY_REDIS_ADDRESS"
EnvRedisPassword = "MINIO_NOTIFY_REDIS_PASSWORD"
EnvRedisKey = "MINIO_NOTIFY_REDIS_KEY"
EnvRedisQueueDir = "MINIO_NOTIFY_REDIS_QUEUE_DIR"
EnvRedisQueueLimit = "MINIO_NOTIFY_REDIS_QUEUE_LIMIT"
)
// RedisArgs - Redis target arguments.
type RedisArgs struct {
Enable bool `json:"enable"`

View File

@@ -37,10 +37,25 @@ import (
xnet "github.com/minio/minio/pkg/net"
)
// Webhook constants
const (
WebhookEndpoint = "endpoint"
WebhookAuthToken = "auth_token"
WebhookQueueDir = "queue_dir"
WebhookQueueLimit = "queue_limit"
EnvWebhookState = "MINIO_NOTIFY_WEBHOOK_STATE"
EnvWebhookEndpoint = "MINIO_NOTIFY_WEBHOOK_ENDPOINT"
EnvWebhookAuthToken = "MINIO_NOTIFY_WEBHOOK_AUTH_TOKEN"
EnvWebhookQueueDir = "MINIO_NOTIFY_WEBHOOK_QUEUE_DIR"
EnvWebhookQueueLimit = "MINIO_NOTIFY_WEBHOOK_QUEUE_LIMIT"
)
// WebhookArgs - Webhook target arguments.
type WebhookArgs struct {
Enable bool `json:"enable"`
Endpoint xnet.URL `json:"endpoint"`
AuthToken string `json:"authToken"`
RootCAs *x509.CertPool `json:"-"`
QueueDir string `json:"queueDir"`
QueueLimit uint64 `json:"queueLimit"`
@@ -114,6 +129,10 @@ func (target *WebhookTarget) send(eventData event.Event) error {
return err
}
if target.args.AuthToken != "" {
req.Header.Set("Authorization", "Bearer "+target.args.AuthToken)
}
req.Header.Set("Content-Type", "application/json")
resp, err := target.httpClient.Do(req)
@@ -172,15 +191,15 @@ func (target *WebhookTarget) Close() error {
}
// NewWebhookTarget - creates new Webhook target.
func NewWebhookTarget(id string, args WebhookArgs, doneCh <-chan struct{}, loggerOnce func(ctx context.Context, err error, id interface{}, kind ...interface{})) *WebhookTarget {
func NewWebhookTarget(id string, args WebhookArgs, doneCh <-chan struct{}, loggerOnce func(ctx context.Context, err error, id interface{}, kind ...interface{})) (*WebhookTarget, error) {
var store Store
if args.QueueDir != "" {
queueDir := filepath.Join(args.QueueDir, storePrefix+"-webhook-"+id)
store = NewQueueStore(queueDir, args.QueueLimit)
if oErr := store.Open(); oErr != nil {
return nil
if err := store.Open(); err != nil {
return nil, err
}
}
@@ -209,5 +228,5 @@ func NewWebhookTarget(id string, args WebhookArgs, doneCh <-chan struct{}, logge
go sendEvents(target, eventKeyCh, doneCh, loggerOnce)
}
return target
return target, nil
}

View File

@@ -1,137 +0,0 @@
/*
* MinIO Cloud Storage, (C) 2018 MinIO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package openid
import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rsa"
"encoding/base64"
"encoding/binary"
"errors"
"fmt"
"math/big"
"strings"
)
// JWKS - https://tools.ietf.org/html/rfc7517
type JWKS struct {
Keys []*JWKS `json:"keys,omitempty"`
Kty string `json:"kty"`
Use string `json:"use,omitempty"`
Kid string `json:"kid,omitempty"`
Alg string `json:"alg,omitempty"`
Crv string `json:"crv,omitempty"`
X string `json:"x,omitempty"`
Y string `json:"y,omitempty"`
D string `json:"d,omitempty"`
N string `json:"n,omitempty"`
E string `json:"e,omitempty"`
K string `json:"k,omitempty"`
}
func safeDecode(str string) ([]byte, error) {
lenMod4 := len(str) % 4
if lenMod4 > 0 {
str = str + strings.Repeat("=", 4-lenMod4)
}
return base64.URLEncoding.DecodeString(str)
}
var (
errMalformedJWKRSAKey = errors.New("malformed JWK RSA key")
errMalformedJWKECKey = errors.New("malformed JWK EC key")
)
// DecodePublicKey - decodes JSON Web Key (JWK) as public key
func (key *JWKS) DecodePublicKey() (crypto.PublicKey, error) {
switch key.Kty {
case "RSA":
if key.N == "" || key.E == "" {
return nil, errMalformedJWKRSAKey
}
// decode exponent
data, err := safeDecode(key.E)
if err != nil {
return nil, errMalformedJWKRSAKey
}
if len(data) < 4 {
ndata := make([]byte, 4)
copy(ndata[4-len(data):], data)
data = ndata
}
pubKey := &rsa.PublicKey{
N: &big.Int{},
E: int(binary.BigEndian.Uint32(data[:])),
}
data, err = safeDecode(key.N)
if err != nil {
return nil, errMalformedJWKRSAKey
}
pubKey.N.SetBytes(data)
return pubKey, nil
case "EC":
if key.Crv == "" || key.X == "" || key.Y == "" {
return nil, errMalformedJWKECKey
}
var curve elliptic.Curve
switch key.Crv {
case "P-224":
curve = elliptic.P224()
case "P-256":
curve = elliptic.P256()
case "P-384":
curve = elliptic.P384()
case "P-521":
curve = elliptic.P521()
default:
return nil, fmt.Errorf("Unknown curve type: %s", key.Crv)
}
pubKey := &ecdsa.PublicKey{
Curve: curve,
X: &big.Int{},
Y: &big.Int{},
}
data, err := safeDecode(key.X)
if err != nil {
return nil, errMalformedJWKECKey
}
pubKey.X.SetBytes(data)
data, err = safeDecode(key.Y)
if err != nil {
return nil, errMalformedJWKECKey
}
pubKey.Y.SetBytes(data)
return pubKey, nil
default:
return nil, fmt.Errorf("Unknown JWK key type %s", key.Kty)
}
}

View File

@@ -1,103 +0,0 @@
/*
* MinIO Cloud Storage, (C) 2018 MinIO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package openid
import (
"bytes"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rsa"
"encoding/json"
"testing"
)
// A.1 - Example public keys
func TestPublicKey(t *testing.T) {
const jsonkey = `{"keys":
[
{"kty":"EC",
"crv":"P-256",
"x":"MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4",
"y":"4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM",
"use":"enc",
"kid":"1"},
{"kty":"RSA",
"n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw",
"e":"AQAB",
"alg":"RS256",
"kid":"2011-04-29"}
]
}`
var jk JWKS
if err := json.Unmarshal([]byte(jsonkey), &jk); err != nil {
t.Fatal("Unmarshal: ", err)
} else if len(jk.Keys) != 2 {
t.Fatalf("Expected 2 keys, got %d", len(jk.Keys))
}
keys := make([]crypto.PublicKey, len(jk.Keys))
for ii, jks := range jk.Keys {
var err error
keys[ii], err = jks.DecodePublicKey()
if err != nil {
t.Fatalf("Failed to decode key %d: %v", ii, err)
}
}
if key0, ok := keys[0].(*ecdsa.PublicKey); !ok {
t.Fatalf("Expected ECDSA key[0], got %T", keys[0])
} else if key1, ok := keys[1].(*rsa.PublicKey); !ok {
t.Fatalf("Expected RSA key[1], got %T", keys[1])
} else if key0.Curve != elliptic.P256() {
t.Fatal("Key[0] is not using P-256 curve")
} else if !bytes.Equal(key0.X.Bytes(), []byte{0x30, 0xa0, 0x42, 0x4c, 0xd2,
0x1c, 0x29, 0x44, 0x83, 0x8a, 0x2d, 0x75, 0xc9, 0x2b, 0x37, 0xe7, 0x6e, 0xa2,
0xd, 0x9f, 0x0, 0x89, 0x3a, 0x3b, 0x4e, 0xee, 0x8a, 0x3c, 0xa, 0xaf, 0xec, 0x3e}) {
t.Fatalf("Bad key[0].X, got %v", key0.X.Bytes())
} else if !bytes.Equal(key0.Y.Bytes(), []byte{0xe0, 0x4b, 0x65, 0xe9, 0x24,
0x56, 0xd9, 0x88, 0x8b, 0x52, 0xb3, 0x79, 0xbd, 0xfb, 0xd5, 0x1e, 0xe8,
0x69, 0xef, 0x1f, 0xf, 0xc6, 0x5b, 0x66, 0x59, 0x69, 0x5b, 0x6c, 0xce,
0x8, 0x17, 0x23}) {
t.Fatalf("Bad key[0].Y, got %v", key0.Y.Bytes())
} else if key1.E != 0x10001 {
t.Fatalf("Bad key[1].E: %d", key1.E)
} else if !bytes.Equal(key1.N.Bytes(), []byte{0xd2, 0xfc, 0x7b, 0x6a, 0xa, 0x1e,
0x6c, 0x67, 0x10, 0x4a, 0xeb, 0x8f, 0x88, 0xb2, 0x57, 0x66, 0x9b, 0x4d, 0xf6,
0x79, 0xdd, 0xad, 0x9, 0x9b, 0x5c, 0x4a, 0x6c, 0xd9, 0xa8, 0x80, 0x15, 0xb5,
0xa1, 0x33, 0xbf, 0xb, 0x85, 0x6c, 0x78, 0x71, 0xb6, 0xdf, 0x0, 0xb, 0x55,
0x4f, 0xce, 0xb3, 0xc2, 0xed, 0x51, 0x2b, 0xb6, 0x8f, 0x14, 0x5c, 0x6e, 0x84,
0x34, 0x75, 0x2f, 0xab, 0x52, 0xa1, 0xcf, 0xc1, 0x24, 0x40, 0x8f, 0x79, 0xb5,
0x8a, 0x45, 0x78, 0xc1, 0x64, 0x28, 0x85, 0x57, 0x89, 0xf7, 0xa2, 0x49, 0xe3,
0x84, 0xcb, 0x2d, 0x9f, 0xae, 0x2d, 0x67, 0xfd, 0x96, 0xfb, 0x92, 0x6c, 0x19,
0x8e, 0x7, 0x73, 0x99, 0xfd, 0xc8, 0x15, 0xc0, 0xaf, 0x9, 0x7d, 0xde, 0x5a,
0xad, 0xef, 0xf4, 0x4d, 0xe7, 0xe, 0x82, 0x7f, 0x48, 0x78, 0x43, 0x24, 0x39,
0xbf, 0xee, 0xb9, 0x60, 0x68, 0xd0, 0x47, 0x4f, 0xc5, 0xd, 0x6d, 0x90, 0xbf,
0x3a, 0x98, 0xdf, 0xaf, 0x10, 0x40, 0xc8, 0x9c, 0x2, 0xd6, 0x92, 0xab, 0x3b,
0x3c, 0x28, 0x96, 0x60, 0x9d, 0x86, 0xfd, 0x73, 0xb7, 0x74, 0xce, 0x7, 0x40,
0x64, 0x7c, 0xee, 0xea, 0xa3, 0x10, 0xbd, 0x12, 0xf9, 0x85, 0xa8, 0xeb, 0x9f,
0x59, 0xfd, 0xd4, 0x26, 0xce, 0xa5, 0xb2, 0x12, 0xf, 0x4f, 0x2a, 0x34, 0xbc,
0xab, 0x76, 0x4b, 0x7e, 0x6c, 0x54, 0xd6, 0x84, 0x2, 0x38, 0xbc, 0xc4, 0x5, 0x87,
0xa5, 0x9e, 0x66, 0xed, 0x1f, 0x33, 0x89, 0x45, 0x77, 0x63, 0x5c, 0x47, 0xa,
0xf7, 0x5c, 0xf9, 0x2c, 0x20, 0xd1, 0xda, 0x43, 0xe1, 0xbf, 0xc4, 0x19, 0xe2,
0x22, 0xa6, 0xf0, 0xd0, 0xbb, 0x35, 0x8c, 0x5e, 0x38, 0xf9, 0xcb, 0x5, 0xa, 0xea,
0xfe, 0x90, 0x48, 0x14, 0xf1, 0xac, 0x1a, 0xa4, 0x9c, 0xca, 0x9e, 0xa0, 0xca, 0x83}) {
t.Fatalf("Bad key[1].N, got %v", key1.N.Bytes())
}
}

View File

@@ -1,235 +0,0 @@
/*
* MinIO Cloud Storage, (C) 2018 MinIO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package openid
import (
"crypto"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strconv"
"time"
jwtgo "github.com/dgrijalva/jwt-go"
"github.com/minio/minio/pkg/env"
xnet "github.com/minio/minio/pkg/net"
)
// JWKSArgs - RSA authentication target arguments
type JWKSArgs struct {
URL *xnet.URL `json:"url"`
publicKeys map[string]crypto.PublicKey
transport *http.Transport
closeRespFn func(io.ReadCloser)
}
// PopulatePublicKey - populates a new publickey from the JWKS URL.
func (r *JWKSArgs) PopulatePublicKey() error {
if r.URL == nil {
return nil
}
client := &http.Client{}
if r.transport != nil {
client.Transport = r.transport
}
resp, err := client.Get(r.URL.String())
if err != nil {
return err
}
defer r.closeRespFn(resp.Body)
if resp.StatusCode != http.StatusOK {
return errors.New(resp.Status)
}
var jwk JWKS
if err = json.NewDecoder(resp.Body).Decode(&jwk); err != nil {
return err
}
for _, key := range jwk.Keys {
r.publicKeys[key.Kid], err = key.DecodePublicKey()
if err != nil {
return err
}
}
return nil
}
// UnmarshalJSON - decodes JSON data.
func (r *JWKSArgs) UnmarshalJSON(data []byte) error {
// subtype to avoid recursive call to UnmarshalJSON()
type subJWKSArgs JWKSArgs
var sr subJWKSArgs
if err := json.Unmarshal(data, &sr); err != nil {
return err
}
ar := JWKSArgs(sr)
if ar.URL == nil || ar.URL.String() == "" {
*r = ar
return nil
}
*r = ar
return nil
}
// JWT - rs client grants provider details.
type JWT struct {
args JWKSArgs
}
func expToInt64(expI interface{}) (expAt int64, err error) {
switch exp := expI.(type) {
case float64:
expAt = int64(exp)
case int64:
expAt = exp
case json.Number:
expAt, err = exp.Int64()
if err != nil {
return 0, err
}
default:
return 0, ErrInvalidDuration
}
return expAt, nil
}
// GetDefaultExpiration - returns the expiration seconds expected.
func GetDefaultExpiration(dsecs string) (time.Duration, error) {
defaultExpiryDuration := time.Duration(60) * time.Minute // Defaults to 1hr.
if dsecs != "" {
expirySecs, err := strconv.ParseInt(dsecs, 10, 64)
if err != nil {
return 0, ErrInvalidDuration
}
// The duration, in seconds, of the role session.
// The value can range from 900 seconds (15 minutes)
// to 12 hours.
if expirySecs < 900 || expirySecs > 43200 {
return 0, ErrInvalidDuration
}
defaultExpiryDuration = time.Duration(expirySecs) * time.Second
}
return defaultExpiryDuration, nil
}
// Validate - validates the access token.
func (p *JWT) Validate(token, dsecs string) (map[string]interface{}, error) {
jp := new(jwtgo.Parser)
jp.ValidMethods = []string{"RS256", "RS384", "RS512", "ES256", "ES384", "ES512"}
keyFuncCallback := func(jwtToken *jwtgo.Token) (interface{}, error) {
kid, ok := jwtToken.Header["kid"].(string)
if !ok {
return nil, fmt.Errorf("Invalid kid value %v", jwtToken.Header["kid"])
}
return p.args.publicKeys[kid], nil
}
var claims jwtgo.MapClaims
jwtToken, err := jp.ParseWithClaims(token, &claims, keyFuncCallback)
if err != nil {
if err = p.args.PopulatePublicKey(); err != nil {
return nil, err
}
jwtToken, err = jwtgo.ParseWithClaims(token, &claims, keyFuncCallback)
if err != nil {
return nil, err
}
}
if !jwtToken.Valid {
return nil, ErrTokenExpired
}
expAt, err := expToInt64(claims["exp"])
if err != nil {
return nil, err
}
defaultExpiryDuration, err := GetDefaultExpiration(dsecs)
if err != nil {
return nil, err
}
if time.Unix(expAt, 0).UTC().Sub(time.Now().UTC()) < defaultExpiryDuration {
defaultExpiryDuration = time.Unix(expAt, 0).UTC().Sub(time.Now().UTC())
}
expiry := time.Now().UTC().Add(defaultExpiryDuration).Unix()
if expAt < expiry {
claims["exp"] = strconv.FormatInt(expAt, 64)
}
return claims, nil
}
// ID returns the provider name and authentication type.
func (p *JWT) ID() ID {
return "jwt"
}
// JWKS url
const (
EnvIAMJWKSURL = "MINIO_IAM_JWKS_URL"
)
// LookupConfig lookup jwks from config, override with any ENVs.
func LookupConfig(args JWKSArgs, transport *http.Transport, closeRespFn func(io.ReadCloser)) (JWKSArgs, error) {
var urlStr string
if args.URL != nil {
urlStr = args.URL.String()
}
jwksURL := env.Get(EnvIAMJWKSURL, urlStr)
if jwksURL == "" {
return args, nil
}
u, err := xnet.ParseURL(jwksURL)
if err != nil {
return args, err
}
args = JWKSArgs{
URL: u,
publicKeys: make(map[string]crypto.PublicKey),
transport: transport,
closeRespFn: closeRespFn,
}
if err = args.PopulatePublicKey(); err != nil {
return args, err
}
return args, nil
}
// NewJWT - initialize new jwt authenticator.
func NewJWT(args JWKSArgs) *JWT {
return &JWT{
args: args,
}
}

View File

@@ -1,120 +0,0 @@
/*
* MinIO Cloud Storage, (C) 2018 MinIO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package openid
import (
"crypto"
"encoding/json"
"net/url"
"testing"
"time"
xnet "github.com/minio/minio/pkg/net"
)
func TestJWT(t *testing.T) {
const jsonkey = `{"keys":
[
{"kty":"RSA",
"n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw",
"e":"AQAB",
"alg":"RS256",
"kid":"2011-04-29"}
]
}`
var jk JWKS
if err := json.Unmarshal([]byte(jsonkey), &jk); err != nil {
t.Fatal("Unmarshal: ", err)
} else if len(jk.Keys) != 1 {
t.Fatalf("Expected 1 keys, got %d", len(jk.Keys))
}
keys := make(map[string]crypto.PublicKey, len(jk.Keys))
for ii, jks := range jk.Keys {
var err error
keys[jks.Kid], err = jks.DecodePublicKey()
if err != nil {
t.Fatalf("Failed to decode key %d: %v", ii, err)
}
}
u1, err := xnet.ParseURL("http://localhost:8443")
if err != nil {
t.Fatal(err)
}
jwt := NewJWT(JWKSArgs{
URL: u1,
publicKeys: keys,
})
if jwt.ID() != "jwt" {
t.Fatalf("Uexpected id %s for the validator", jwt.ID())
}
u, err := url.Parse("http://localhost:8443/?Token=invalid")
if err != nil {
t.Fatal(err)
}
if _, err := jwt.Validate(u.Query().Get("Token"), ""); err == nil {
t.Fatal(err)
}
}
func TestDefaultExpiryDuration(t *testing.T) {
testCases := []struct {
reqURL string
duration time.Duration
expectErr bool
}{
{
reqURL: "http://localhost:8443/?Token=xxxxx",
duration: time.Duration(60) * time.Minute,
},
{
reqURL: "http://localhost:8443/?DurationSeconds=9s",
expectErr: true,
},
{
reqURL: "http://localhost:8443/?DurationSeconds=43201",
expectErr: true,
},
{
reqURL: "http://localhost:8443/?DurationSeconds=800",
expectErr: true,
},
{
reqURL: "http://localhost:8443/?DurationSeconds=901",
duration: time.Duration(901) * time.Second,
},
}
for i, testCase := range testCases {
u, err := url.Parse(testCase.reqURL)
if err != nil {
t.Fatal(err)
}
d, err := GetDefaultExpiration(u.Query().Get("DurationSeconds"))
gotErr := (err != nil)
if testCase.expectErr != gotErr {
t.Errorf("Test %d: Expected %v, got %v with error %s", i+1, testCase.expectErr, gotErr, err)
}
if d != testCase.duration {
t.Errorf("Test %d: Expected duration %d, got %d", i+1, testCase.duration, d)
}
}
}

View File

@@ -1,92 +0,0 @@
/*
* MinIO Cloud Storage, (C) 2018 MinIO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package openid
import (
"errors"
"fmt"
"sync"
)
// ID - holds identification name authentication validator target.
type ID string
// Validator interface describes basic implementation
// requirements of various authentication providers.
type Validator interface {
// Validate is a custom validator function for this provider,
// each validation is authenticationType or provider specific.
Validate(token string, duration string) (map[string]interface{}, error)
// ID returns provider name of this provider.
ID() ID
}
// ErrTokenExpired - error token expired
var (
ErrTokenExpired = errors.New("token expired")
ErrInvalidDuration = errors.New("duration higher than token expiry")
)
// Validators - holds list of providers indexed by provider id.
type Validators struct {
sync.RWMutex
providers map[ID]Validator
}
// Add - adds unique provider to provider list.
func (list *Validators) Add(provider Validator) error {
list.Lock()
defer list.Unlock()
if _, ok := list.providers[provider.ID()]; ok {
return fmt.Errorf("provider %v already exists", provider.ID())
}
list.providers[provider.ID()] = provider
return nil
}
// List - returns available provider IDs.
func (list *Validators) List() []ID {
list.RLock()
defer list.RUnlock()
keys := []ID{}
for k := range list.providers {
keys = append(keys, k)
}
return keys
}
// Get - returns the provider for the given providerID, if not found
// returns an error.
func (list *Validators) Get(id ID) (p Validator, err error) {
list.RLock()
defer list.RUnlock()
var ok bool
if p, ok = list.providers[id]; !ok {
return nil, fmt.Errorf("provider %v doesn't exist", id)
}
return p, nil
}
// NewValidators - creates Validators.
func NewValidators() *Validators {
return &Validators{providers: make(map[ID]Validator)}
}

View File

@@ -1,104 +0,0 @@
/*
* MinIO Cloud Storage, (C) 2018 MinIO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package openid
import (
"net/http"
"net/http/httptest"
"testing"
xnet "github.com/minio/minio/pkg/net"
)
type errorValidator struct{}
func (e errorValidator) Validate(token, dsecs string) (map[string]interface{}, error) {
return nil, ErrTokenExpired
}
func (e errorValidator) ID() ID {
return "err"
}
func TestValidators(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("content-type", "application/json")
w.Write([]byte(`{
"keys" : [ {
"kty" : "RSA",
"kid" : "1438289820780",
"use" : "sig",
"alg" : "RS256",
"n" : "idWPro_QiAFOdMsJD163lcDIPogOwXogRo3Pct2MMyeE2GAGqV20Sc8QUbuLDfPl-7Hi9IfFOz--JY6QL5l92eV-GJXkTmidUEooZxIZSp3ghRxLCqlyHeF5LuuM5LPRFDeF4YWFQT_D2eNo_w95g6qYSeOwOwGIfaHa2RMPcQAiM6LX4ot-Z7Po9z0_3ztFa02m3xejEFr2rLRqhFl3FZJaNnwTUk6an6XYsunxMk3Ya3lRaKJReeXeFtfTpShgtPiAl7lIfLJH9h26h2OAlww531DpxHSm1gKXn6bjB0NTC55vJKft4wXoc_0xKZhnWmjQE8d9xE8e1Z3Ll1LYbw",
"e" : "AQAB"
}, {
"kty" : "RSA",
"kid" : "1438289856256",
"use" : "sig",
"alg" : "RS256",
"n" : "zo5cKcbFECeiH8eGx2D-DsFSpjSKbTVlXD6uL5JAy9rYIv7eYEP6vrKeX-x1z70yEdvgk9xbf9alc8siDfAz3rLCknqlqL7XGVAQL0ZP63UceDmD60LHOzMrx4eR6p49B3rxFfjvX2SWSV3-1H6XNyLk_ALbG6bGCFGuWBQzPJB4LMKCrOFq-6jtRKOKWBXYgkYkaYs5dG-3e2ULbq-y2RdgxYh464y_-MuxDQfvUgP787XKfcXP_XjJZvyuOEANjVyJYZSOyhHUlSGJapQ8ztHdF-swsnf7YkePJ2eR9fynWV2ZoMaXOdidgZtGTa4R1Z4BgH2C0hKJiqRy9fB7Gw",
"e" : "AQAB"
} ]
}
`))
w.(http.Flusher).Flush()
}))
defer ts.Close()
vrs := NewValidators()
if err := vrs.Add(&errorValidator{}); err != nil {
t.Fatal(err)
}
if err := vrs.Add(&errorValidator{}); err == nil {
t.Fatal("Unexpected should return error for double inserts")
}
if _, err := vrs.Get("unknown"); err == nil {
t.Fatal("Unexpected should return error for unknown validators")
}
v, err := vrs.Get("err")
if err != nil {
t.Fatal(err)
}
if _, err = v.Validate("", ""); err != ErrTokenExpired {
t.Fatalf("Expected error %s, got %s", ErrTokenExpired, err)
}
vids := vrs.List()
if len(vids) == 0 || len(vids) > 1 {
t.Fatalf("Unexpected number of vids %v", vids)
}
u, err := xnet.ParseURL(ts.URL)
if err != nil {
t.Fatal(err)
}
if err = vrs.Add(NewJWT(JWKSArgs{
URL: u,
})); err != nil {
t.Fatal(err)
}
if _, err = vrs.Get("jwt"); err != nil {
t.Fatal(err)
}
}

View File

@@ -1,193 +0,0 @@
/*
* MinIO Cloud Storage, (C) 2018 MinIO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package iampolicy
import (
"bytes"
"encoding/json"
"io"
"io/ioutil"
"net/http"
"github.com/minio/minio/pkg/env"
xnet "github.com/minio/minio/pkg/net"
)
// Env IAM OPA URL
const (
EnvIAMOPAURL = "MINIO_IAM_OPA_URL"
EnvIAMOPAAuthToken = "MINIO_IAM_OPA_AUTHTOKEN"
)
// OpaArgs opa general purpose policy engine configuration.
type OpaArgs struct {
URL *xnet.URL `json:"url"`
AuthToken string `json:"authToken"`
Transport http.RoundTripper `json:"-"`
CloseRespFn func(r io.ReadCloser) `json:"-"`
}
// Validate - validate opa configuration params.
func (a *OpaArgs) Validate() error {
req, err := http.NewRequest("POST", a.URL.String(), bytes.NewReader([]byte("")))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
if a.AuthToken != "" {
req.Header.Set("Authorization", a.AuthToken)
}
client := &http.Client{Transport: a.Transport}
resp, err := client.Do(req)
if err != nil {
return err
}
defer a.CloseRespFn(resp.Body)
return nil
}
// UnmarshalJSON - decodes JSON data.
func (a *OpaArgs) UnmarshalJSON(data []byte) error {
// subtype to avoid recursive call to UnmarshalJSON()
type subOpaArgs OpaArgs
var so subOpaArgs
if err := json.Unmarshal(data, &so); err != nil {
return err
}
oa := OpaArgs(so)
if oa.URL == nil || oa.URL.String() == "" {
*a = oa
return nil
}
*a = oa
return nil
}
// Opa - implements opa policy agent calls.
type Opa struct {
args OpaArgs
client *http.Client
}
// LookupConfig lookup Opa from config, override with any ENVs.
func LookupConfig(args OpaArgs, transport *http.Transport, closeRespFn func(io.ReadCloser)) (OpaArgs, error) {
var urlStr string
if args.URL != nil {
urlStr = args.URL.String()
}
opaURL := env.Get(EnvIAMOPAURL, urlStr)
if opaURL == "" {
return args, nil
}
u, err := xnet.ParseURL(opaURL)
if err != nil {
return args, err
}
args = OpaArgs{
URL: u,
AuthToken: env.Get(EnvIAMOPAAuthToken, ""),
Transport: transport,
CloseRespFn: closeRespFn,
}
if err = args.Validate(); err != nil {
return args, err
}
return args, nil
}
// NewOpa - initializes opa policy engine connector.
func NewOpa(args OpaArgs) *Opa {
// No opa args.
if args.URL == nil || args.URL.Scheme == "" && args.AuthToken == "" {
return nil
}
return &Opa{
args: args,
client: &http.Client{Transport: args.Transport},
}
}
// IsAllowed - checks given policy args is allowed to continue the REST API.
func (o *Opa) IsAllowed(args Args) (bool, error) {
if o == nil {
return false, nil
}
// OPA input
body := make(map[string]interface{})
body["input"] = args
inputBytes, err := json.Marshal(body)
if err != nil {
return false, err
}
req, err := http.NewRequest("POST", o.args.URL.String(), bytes.NewReader(inputBytes))
if err != nil {
return false, err
}
req.Header.Set("Content-Type", "application/json")
if o.args.AuthToken != "" {
req.Header.Set("Authorization", o.args.AuthToken)
}
resp, err := o.client.Do(req)
if err != nil {
return false, err
}
defer o.args.CloseRespFn(resp.Body)
// Read the body to be saved later.
opaRespBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
return false, err
}
// Handle large OPA responses when OPA URL is of
// form http://localhost:8181/v1/data/httpapi/authz
type opaResultAllow struct {
Result struct {
Allow bool `json:"allow"`
} `json:"result"`
}
// Handle simpler OPA responses when OPA URL is of
// form http://localhost:8181/v1/data/httpapi/authz/allow
type opaResult struct {
Result bool `json:"result"`
}
respBody := bytes.NewReader(opaRespBytes)
var result opaResult
if err = json.NewDecoder(respBody).Decode(&result); err != nil {
respBody.Seek(0, 0)
var resultAllow opaResultAllow
if err = json.NewDecoder(respBody).Decode(&resultAllow); err != nil {
return false, err
}
return resultAllow.Result.Allow, nil
}
return result.Result, nil
}

View File

@@ -617,8 +617,8 @@ __Example__
### GetKeyStatus(keyID string) (*KMSKeyStatus, error)
Requests status information about one particular KMS master key
from a MinIO server. The keyID is optional and the server will
use the default master key (configured via `MINIO_SSE_VAULT_KEY_NAME`
or `MINIO_SSE_MASTER_KEY`) if the keyID is empty.
use the default master key (configured via `MINIO_KMS_VAULT_KEY_NAME`
or `MINIO_KMS_MASTER_KEY`) if the keyID is empty.
__Example__

View File

@@ -1,5 +1,5 @@
/*
* MinIO Cloud Storage, (C) 2017 MinIO, Inc.
* MinIO Cloud Storage, (C) 2017-2019 MinIO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,18 +19,14 @@ package madmin
import (
"bytes"
"encoding/json"
"errors"
"io"
"net/http"
"github.com/minio/minio/pkg/quick"
)
// GetConfig - returns the config.json of a minio setup, incoming data is encrypted.
func (adm *AdminClient) GetConfig() ([]byte, error) {
// Execute GET on /minio/admin/v2/config to get config of a setup.
resp, err := adm.executeMethod("GET",
resp, err := adm.executeMethod(http.MethodGet,
requestData{relPath: adminAPIPrefix + "/config"})
defer closeResponse(resp)
if err != nil {
@@ -40,7 +36,6 @@ func (adm *AdminClient) GetConfig() ([]byte, error) {
if resp.StatusCode != http.StatusOK {
return nil, httpRespToErrorResponse(resp)
}
defer closeResponse(resp)
return DecryptData(adm.secretAccessKey, resp.Body)
}
@@ -59,26 +54,6 @@ func (adm *AdminClient) SetConfig(config io.Reader) (err error) {
return err
}
configBytes := configBuf[:n]
type configVersion struct {
Version string `json:"version,omitempty"`
}
var cfg configVersion
// Check if read data is in json format
if err = json.Unmarshal(configBytes, &cfg); err != nil {
return errors.New("Invalid JSON format: " + err.Error())
}
// Check if the provided json file has "version" key set
if cfg.Version == "" {
return errors.New("Missing or unset \"version\" key in json file")
}
// Validate there are no duplicate keys in the JSON
if err = quick.CheckDuplicateKeys(string(configBytes)); err != nil {
return errors.New("Duplicate key in json file: " + err.Error())
}
econfigBytes, err := EncryptData(adm.secretAccessKey, configBytes)
if err != nil {
return err
@@ -90,7 +65,7 @@ func (adm *AdminClient) SetConfig(config io.Reader) (err error) {
}
// Execute PUT on /minio/admin/v2/config to set config.
resp, err := adm.executeMethod("PUT", reqData)
resp, err := adm.executeMethod(http.MethodPut, reqData)
defer closeResponse(resp)
if err != nil {

View File

@@ -0,0 +1,49 @@
/*
* MinIO Cloud Storage, (C) 2019 MinIO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package madmin
import (
"io"
"net/http"
"net/url"
)
// HelpConfigKV - return help for a given sub-system.
func (adm *AdminClient) HelpConfigKV(subSys, key string) (io.ReadCloser, error) {
v := url.Values{}
v.Set("subSys", subSys)
v.Set("key", key)
reqData := requestData{
relPath: adminAPIPrefix + "/help-config-kv",
queryValues: v,
}
// Execute GET on /minio/admin/v2/help-config-kv
resp, err := adm.executeMethod(http.MethodGet, reqData)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
defer closeResponse(resp)
return nil, httpRespToErrorResponse(resp)
}
return resp.Body, nil
}

View File

@@ -0,0 +1,107 @@
/*
* MinIO Cloud Storage, (C) 2019 MinIO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package madmin
import (
"encoding/json"
"net/http"
"net/url"
"time"
)
// ClearConfigHistoryKV - clears the config entry represented by restoreID.
// optionally allows setting `all` as a special keyword to automatically
// erase all config set history entires.
func (adm *AdminClient) ClearConfigHistoryKV(restoreID string) (err error) {
v := url.Values{}
v.Set("restoreId", restoreID)
reqData := requestData{
relPath: adminAPIPrefix + "/clear-config-history-kv",
queryValues: v,
}
// Execute DELETE on /minio/admin/v2/clear-config-history-kv
resp, err := adm.executeMethod(http.MethodDelete, reqData)
defer closeResponse(resp)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return httpRespToErrorResponse(resp)
}
return nil
}
// RestoreConfigHistoryKV - Restore a previous config set history.
// Input is a unique id which represents the previous setting.
func (adm *AdminClient) RestoreConfigHistoryKV(restoreID string) (err error) {
v := url.Values{}
v.Set("restoreId", restoreID)
reqData := requestData{
relPath: adminAPIPrefix + "/restore-config-history-kv",
queryValues: v,
}
// Execute PUT on /minio/admin/v2/set-config-kv to set config key/value.
resp, err := adm.executeMethod(http.MethodPut, reqData)
defer closeResponse(resp)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return httpRespToErrorResponse(resp)
}
return nil
}
// ConfigHistoryEntry - captures config set history with a unique
// restore ID and createTime
type ConfigHistoryEntry struct {
RestoreID string `json:"restoreId"`
CreateTime time.Time `json:"createTime"`
}
// ListConfigHistoryKV - lists a slice of ConfigHistoryEntries sorted by createTime.
func (adm *AdminClient) ListConfigHistoryKV() ([]ConfigHistoryEntry, error) {
// Execute GET on /minio/admin/v2/list-config-history-kv
resp, err := adm.executeMethod(http.MethodGet,
requestData{
relPath: adminAPIPrefix + "/list-config-history-kv",
})
defer closeResponse(resp)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, httpRespToErrorResponse(resp)
}
var chEntries []ConfigHistoryEntry
d := json.NewDecoder(resp.Body)
d.DisallowUnknownFields()
if err = d.Decode(&chEntries); err != nil {
return chEntries, err
}
return chEntries, nil
}

View File

@@ -0,0 +1,100 @@
/*
* MinIO Cloud Storage, (C) 2019 MinIO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package madmin
import (
"net/http"
"net/url"
)
// DelConfigKV - delete key from server config.
func (adm *AdminClient) DelConfigKV(k string) (err error) {
econfigBytes, err := EncryptData(adm.secretAccessKey, []byte(k))
if err != nil {
return err
}
reqData := requestData{
relPath: adminAPIPrefix + "/del-config-kv",
content: econfigBytes,
}
// Execute DELETE on /minio/admin/v2/del-config-kv to delete config key.
resp, err := adm.executeMethod(http.MethodDelete, reqData)
defer closeResponse(resp)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return httpRespToErrorResponse(resp)
}
return nil
}
// SetConfigKV - set key value config to server.
func (adm *AdminClient) SetConfigKV(kv string) (err error) {
econfigBytes, err := EncryptData(adm.secretAccessKey, []byte(kv))
if err != nil {
return err
}
reqData := requestData{
relPath: adminAPIPrefix + "/set-config-kv",
content: econfigBytes,
}
// Execute PUT on /minio/admin/v2/set-config-kv to set config key/value.
resp, err := adm.executeMethod(http.MethodPut, reqData)
defer closeResponse(resp)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return httpRespToErrorResponse(resp)
}
return nil
}
// GetConfigKV - returns the key, value of the requested key, incoming data is encrypted.
func (adm *AdminClient) GetConfigKV(key string) ([]byte, error) {
v := url.Values{}
v.Set("key", key)
// Execute GET on /minio/admin/v2/get-config-kv?key={key} to get value of key.
resp, err := adm.executeMethod(http.MethodGet,
requestData{
relPath: adminAPIPrefix + "/get-config-kv",
queryValues: v,
})
defer closeResponse(resp)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, httpRespToErrorResponse(resp)
}
return DecryptData(adm.secretAccessKey, resp.Body)
}

View File

@@ -1,54 +0,0 @@
// +build ignore
/*
* MinIO Cloud Storage, (C) 2017 MinIO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package main
import (
"bytes"
"encoding/json"
"log"
"github.com/minio/minio/pkg/madmin"
)
func main() {
// Note: YOUR-ACCESSKEYID, YOUR-SECRETACCESSKEY are
// dummy values, please replace them with original values.
// API requests are secure (HTTPS) if secure=true and insecure (HTTP) otherwise.
// New returns an MinIO Admin client object.
madmClnt, err := madmin.New("your-minio.example.com:9000", "YOUR-ACCESSKEYID", "YOUR-SECRETACCESSKEY", true)
if err != nil {
log.Fatalln(err)
}
configBytes, err := madmClnt.GetConfig()
if err != nil {
log.Fatalf("failed due to: %v", err)
}
// Pretty-print config received as json.
var buf bytes.Buffer
err = json.Indent(&buf, configBytes, "", "\t")
if err != nil {
log.Fatalf("failed due to: %v", err)
}
log.Println("config received successfully: ", string(buf.Bytes()))
}

View File

@@ -1,155 +0,0 @@
// +build ignore
/*
* MinIO Cloud Storage, (C) 2017 MinIO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package main
import (
"bytes"
"encoding/json"
"fmt"
"log"
"github.com/minio/minio/pkg/madmin"
)
var configJSON = []byte(`{
"version": "13",
"credential": {
"accessKey": "minio",
"secretKey": "minio123"
},
"region": "us-west-1",
"logger": {
"console": {
"enable": true,
"level": "fatal"
},
"file": {
"enable": false,
"fileName": "",
"level": ""
}
},
"notify": {
"amqp": {
"1": {
"enable": false,
"url": "",
"exchange": "",
"routingKey": "",
"exchangeType": "",
"mandatory": false,
"immediate": false,
"durable": false,
"internal": false,
"noWait": false,
"autoDeleted": false
}
},
"nats": {
"1": {
"enable": false,
"address": "",
"subject": "",
"username": "",
"password": "",
"token": "",
"secure": false,
"pingInterval": 0,
"streaming": {
"enable": false,
"clusterID": "",
"async": false,
"maxPubAcksInflight": 0
}
}
},
"elasticsearch": {
"1": {
"enable": false,
"url": "",
"index": ""
}
},
"redis": {
"1": {
"enable": false,
"address": "",
"password": "",
"key": ""
}
},
"postgresql": {
"1": {
"enable": false,
"connectionString": "",
"table": "",
"host": "",
"port": "",
"user": "",
"password": "",
"database": ""
}
},
"kafka": {
"1": {
"enable": false,
"brokers": null,
"topic": ""
}
},
"webhook": {
"1": {
"enable": false,
"endpoint": ""
}
}
}
}`)
func main() {
// Note: YOUR-ACCESSKEYID, YOUR-SECRETACCESSKEY are
// dummy values, please replace them with original values.
// Note: YOUR-ACCESSKEYID, YOUR-SECRETACCESSKEY are
// dummy values, please replace them with original values.
// API requests are secure (HTTPS) if secure=true and insecure (HTTP) otherwise.
// New returns an MinIO Admin client object.
madmClnt, err := madmin.New("your-minio.example.com:9000", "YOUR-ACCESSKEYID", "YOUR-SECRETACCESSKEY", true)
if err != nil {
log.Fatalln(err)
}
result, err := madmClnt.SetConfig(bytes.NewReader(configJSON))
if err != nil {
log.Fatalln(err)
}
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetEscapeHTML(false)
enc.SetIndent("", "\t")
err = enc.Encode(result)
if err != nil {
log.Fatalln(err)
}
fmt.Println("SetConfig: ", string(buf.Bytes()))
}

View File

@@ -187,10 +187,6 @@ func loadFileConfig(filename string, v interface{}) error {
fileData = []byte(strings.Replace(string(fileData), "\r\n", "\n", -1))
}
if err = checkDupJSONKeys(string(fileData)); err != nil {
return err
}
// Unmarshal file's content
return toUnmarshaller(filepath.Ext(filename))(fileData, v)
}

View File

@@ -21,12 +21,10 @@ package quick
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"github.com/cheggaaa/pb"
"github.com/tidwall/gjson"
)
const errorFmt = "%5d: %s <<<<"
@@ -82,59 +80,3 @@ func FormatJSONSyntaxError(data io.Reader, offset int64) (highlight string) {
return fmt.Sprintf(errorFmt, errLine, readLine.String()[idx:])
}
// doCheckDupJSONKeys recursively detects duplicate json keys
func doCheckDupJSONKeys(key, value gjson.Result) error {
// Key occurrences map of the current scope to count
// if there is any duplicated json key.
keysOcc := make(map[string]int)
// Holds the found error
var checkErr error
// Iterate over keys in the current json scope
value.ForEach(func(k, v gjson.Result) bool {
// If current key is not null, check if its
// value contains some duplicated keys.
if k.Type != gjson.Null {
keysOcc[k.String()]++
checkErr = doCheckDupJSONKeys(k, v)
}
return checkErr == nil
})
// Check found err
if checkErr != nil {
return errors.New(key.String() + " => " + checkErr.Error())
}
// Check for duplicated keys
for k, v := range keysOcc {
if v > 1 {
return errors.New(key.String() + " => `" + k + "` entry is duplicated")
}
}
return nil
}
// Check recursively if a key is duplicated in the same json scope
// e.g.:
// `{ "key" : { "key" ..` is accepted
// `{ "key" : { "subkey" : "val1", "subkey": "val2" ..` throws subkey duplicated error
func checkDupJSONKeys(json string) error {
// Parse config with gjson library
config := gjson.Parse(json)
// Create a fake rootKey since root json doesn't seem to have representation
// in gjson library.
rootKey := gjson.Result{Type: gjson.String, Str: "config.json"}
// Check if loaded json contains any duplicated keys
return doCheckDupJSONKeys(rootKey, config)
}
// CheckDuplicateKeys - checks for duplicate entries in a JSON file
func CheckDuplicateKeys(json string) error {
return checkDupJSONKeys(json)
}

View File

@@ -27,8 +27,6 @@ import (
"runtime"
"strings"
"testing"
"github.com/tidwall/gjson"
)
func TestReadVersion(t *testing.T) {
@@ -506,31 +504,3 @@ func TestDeepDiff(t *testing.T) {
// fmt.Printf("DeepDiff[%d]: %s=%v\n", i, field.Name(), field.Value())
// }
}
func TestCheckDupJSONKeys(t *testing.T) {
testCases := []struct {
json string
shouldPass bool
}{
{`{}`, true},
{`{"version" : "13"}`, true},
{`{"version" : "13", "version": "14"}`, false},
{`{"version" : "13", "credential": {"accessKey": "12345"}}`, true},
{`{"version" : "13", "credential": {"accessKey": "12345", "accessKey":"12345"}}`, false},
{`{"version" : "13", "notify": {"amqp": {"1"}, "webhook":{"3"}}}`, true},
{`{"version" : "13", "notify": {"amqp": {"1"}, "amqp":{"3"}}}`, false},
{`{"version" : "13", "notify": {"amqp": {"1":{}, "2":{}}}}`, true},
{`{"version" : "13", "notify": {"amqp": {"1":{}, "1":{}}}}`, false},
}
for i, testCase := range testCases {
err := doCheckDupJSONKeys(gjson.Result{}, gjson.Parse(testCase.json))
if testCase.shouldPass && err != nil {
t.Errorf("Test %d, should pass but it failed with err = %v", i+1, err)
}
if !testCase.shouldPass && err == nil {
t.Errorf("Test %d, should fail but it succeed.", i+1)
}
}
}