2024-07-16 10:03:03 -04:00
|
|
|
// Copyright (c) 2015-2024 MinIO, Inc.
|
|
|
|
//
|
|
|
|
// This file is part of MinIO Object Storage stack
|
|
|
|
//
|
|
|
|
// 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 cmd
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
|
|
|
"net/http/httptest"
|
|
|
|
"net/url"
|
|
|
|
"strings"
|
|
|
|
"testing"
|
|
|
|
|
|
|
|
"github.com/minio/madmin-go/v3"
|
|
|
|
"github.com/minio/minio/internal/kms"
|
|
|
|
"github.com/minio/pkg/v3/policy"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
// KMS API paths
|
|
|
|
// For example: /minio/kms/v1/key/list?pattern=*
|
|
|
|
kmsURL = kmsPathPrefix + kmsAPIVersionPrefix
|
|
|
|
kmsStatusPath = kmsURL + "/status"
|
|
|
|
kmsMetricsPath = kmsURL + "/metrics"
|
|
|
|
kmsAPIsPath = kmsURL + "/apis"
|
|
|
|
kmsVersionPath = kmsURL + "/version"
|
|
|
|
kmsKeyCreatePath = kmsURL + "/key/create"
|
|
|
|
kmsKeyListPath = kmsURL + "/key/list"
|
|
|
|
kmsKeyStatusPath = kmsURL + "/key/status"
|
|
|
|
|
|
|
|
// Admin API paths
|
|
|
|
// For example: /minio/admin/v3/kms/status
|
|
|
|
adminURL = adminPathPrefix + adminAPIVersionPrefix
|
|
|
|
kmsAdminStatusPath = adminURL + "/kms/status"
|
|
|
|
kmsAdminKeyStatusPath = adminURL + "/kms/key/status"
|
|
|
|
kmsAdminKeyCreate = adminURL + "/kms/key/create"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
userAccessKey = "miniofakeuseraccesskey"
|
|
|
|
userSecretKey = "miniofakeusersecret"
|
|
|
|
)
|
|
|
|
|
|
|
|
type kmsTestCase struct {
|
|
|
|
name string
|
|
|
|
method string
|
|
|
|
path string
|
|
|
|
query map[string]string
|
|
|
|
|
|
|
|
// User credentials and policy for request
|
|
|
|
policy string
|
|
|
|
asRoot bool
|
|
|
|
|
|
|
|
// Wanted in response.
|
|
|
|
wantStatusCode int
|
|
|
|
wantKeyNames []string
|
|
|
|
wantResp []string
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestKMSHandlersCreateKey(t *testing.T) {
|
|
|
|
adminTestBed, tearDown := setupKMSTest(t, true)
|
|
|
|
defer tearDown()
|
|
|
|
|
|
|
|
tests := []kmsTestCase{
|
|
|
|
// Create key test
|
|
|
|
{
|
|
|
|
name: "create key as user with no policy want forbidden",
|
|
|
|
method: http.MethodPost,
|
|
|
|
path: kmsKeyCreatePath,
|
|
|
|
query: map[string]string{"key-id": "new-test-key"},
|
|
|
|
asRoot: false,
|
|
|
|
|
|
|
|
wantStatusCode: http.StatusForbidden,
|
|
|
|
wantResp: []string{"AccessDenied"},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "create key as user with no resources specified want success",
|
|
|
|
method: http.MethodPost,
|
|
|
|
path: kmsKeyCreatePath,
|
|
|
|
query: map[string]string{"key-id": "new-test-key"},
|
|
|
|
asRoot: false,
|
|
|
|
|
|
|
|
policy: `{"Effect": "Allow",
|
|
|
|
"Action": ["kms:CreateKey"] }`,
|
|
|
|
|
|
|
|
wantStatusCode: http.StatusOK,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "create key as user set policy to allow want success",
|
|
|
|
method: http.MethodPost,
|
|
|
|
path: kmsKeyCreatePath,
|
|
|
|
query: map[string]string{"key-id": "second-new-test-key"},
|
|
|
|
asRoot: false,
|
|
|
|
|
|
|
|
policy: `{"Effect": "Allow",
|
|
|
|
"Action": ["kms:CreateKey"],
|
|
|
|
"Resource": ["arn:minio:kms:::second-new-test-*"] }`,
|
|
|
|
|
|
|
|
wantStatusCode: http.StatusOK,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "create key as user set policy to non matching resource want forbidden",
|
|
|
|
method: http.MethodPost,
|
|
|
|
path: kmsKeyCreatePath,
|
|
|
|
query: map[string]string{"key-id": "third-new-test-key"},
|
|
|
|
asRoot: false,
|
|
|
|
|
|
|
|
policy: `{"Effect": "Allow",
|
|
|
|
"Action": ["kms:CreateKey"],
|
|
|
|
"Resource": ["arn:minio:kms:::non-matching-key-name"] }`,
|
|
|
|
|
|
|
|
wantStatusCode: http.StatusForbidden,
|
|
|
|
wantResp: []string{"AccessDenied"},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
for testNum, test := range tests {
|
|
|
|
t.Run(fmt.Sprintf("%d %s", testNum+1, test.name), func(t *testing.T) {
|
|
|
|
execKMSTest(t, test, adminTestBed)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestKMSHandlersKeyStatus(t *testing.T) {
|
|
|
|
adminTestBed, tearDown := setupKMSTest(t, true)
|
|
|
|
defer tearDown()
|
|
|
|
|
|
|
|
tests := []kmsTestCase{
|
|
|
|
{
|
|
|
|
name: "create a first key root user",
|
|
|
|
method: http.MethodPost,
|
|
|
|
path: kmsKeyCreatePath,
|
|
|
|
query: map[string]string{"key-id": "abc-test-key"},
|
|
|
|
asRoot: true,
|
|
|
|
|
|
|
|
wantStatusCode: http.StatusOK,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "key status as root want success",
|
|
|
|
method: http.MethodGet,
|
|
|
|
path: kmsKeyStatusPath,
|
|
|
|
query: map[string]string{"key-id": "abc-test-key"},
|
|
|
|
asRoot: true,
|
|
|
|
|
|
|
|
wantStatusCode: http.StatusOK,
|
|
|
|
wantResp: []string{"abc-test-key"},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "key status as user no policy want forbidden",
|
|
|
|
method: http.MethodGet,
|
|
|
|
path: kmsKeyStatusPath,
|
|
|
|
query: map[string]string{"key-id": "abc-test-key"},
|
|
|
|
asRoot: false,
|
|
|
|
|
|
|
|
wantStatusCode: http.StatusForbidden,
|
|
|
|
wantResp: []string{"AccessDenied"},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "key status as user legacy no resources specified want success",
|
|
|
|
method: http.MethodGet,
|
|
|
|
path: kmsKeyStatusPath,
|
|
|
|
query: map[string]string{"key-id": "abc-test-key"},
|
|
|
|
asRoot: false,
|
|
|
|
|
|
|
|
policy: `{"Effect": "Allow",
|
|
|
|
"Action": ["kms:KeyStatus"] }`,
|
|
|
|
|
|
|
|
wantStatusCode: http.StatusOK,
|
|
|
|
wantResp: []string{"abc-test-key"},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "key status as user set policy to allow only one key",
|
|
|
|
method: http.MethodGet,
|
|
|
|
path: kmsKeyStatusPath,
|
|
|
|
query: map[string]string{"key-id": "abc-test-key"},
|
|
|
|
asRoot: false,
|
|
|
|
|
|
|
|
policy: `{"Effect": "Allow",
|
|
|
|
"Action": ["kms:KeyStatus"],
|
|
|
|
"Resource": ["arn:minio:kms:::abc-test-*"] }`,
|
|
|
|
|
|
|
|
wantStatusCode: http.StatusOK,
|
|
|
|
wantResp: []string{"abc-test-key"},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "key status as user set policy to allow non-matching key",
|
|
|
|
method: http.MethodGet,
|
|
|
|
path: kmsKeyStatusPath,
|
|
|
|
query: map[string]string{"key-id": "abc-test-key"},
|
|
|
|
asRoot: false,
|
|
|
|
|
|
|
|
policy: `{"Effect": "Allow",
|
|
|
|
"Action": ["kms:KeyStatus"],
|
|
|
|
"Resource": ["arn:minio:kms:::xyz-test-key"] }`,
|
|
|
|
|
|
|
|
wantStatusCode: http.StatusForbidden,
|
|
|
|
wantResp: []string{"AccessDenied"},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
for testNum, test := range tests {
|
|
|
|
t.Run(fmt.Sprintf("%d %s", testNum+1, test.name), func(t *testing.T) {
|
|
|
|
execKMSTest(t, test, adminTestBed)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestKMSHandlersAPIs(t *testing.T) {
|
|
|
|
adminTestBed, tearDown := setupKMSTest(t, true)
|
|
|
|
defer tearDown()
|
|
|
|
|
|
|
|
tests := []kmsTestCase{
|
|
|
|
// Version test
|
|
|
|
{
|
|
|
|
name: "version as root want success",
|
|
|
|
method: http.MethodGet,
|
|
|
|
path: kmsVersionPath,
|
|
|
|
asRoot: true,
|
|
|
|
|
|
|
|
wantStatusCode: http.StatusOK,
|
|
|
|
wantResp: []string{"version"},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "version as user with no policy want forbidden",
|
|
|
|
method: http.MethodGet,
|
|
|
|
path: kmsVersionPath,
|
|
|
|
asRoot: false,
|
|
|
|
|
|
|
|
wantStatusCode: http.StatusForbidden,
|
|
|
|
wantResp: []string{"AccessDenied"},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "version as user with policy ignores resource want success",
|
|
|
|
method: http.MethodGet,
|
|
|
|
path: kmsVersionPath,
|
|
|
|
asRoot: false,
|
|
|
|
|
|
|
|
policy: `{"Effect": "Allow",
|
|
|
|
"Action": ["kms:Version"],
|
|
|
|
"Resource": ["arn:minio:kms:::does-not-matter-it-is-ignored"] }`,
|
|
|
|
|
|
|
|
wantStatusCode: http.StatusOK,
|
|
|
|
wantResp: []string{"version"},
|
|
|
|
},
|
|
|
|
|
|
|
|
// APIs test
|
|
|
|
{
|
|
|
|
name: "apis as root want success",
|
|
|
|
method: http.MethodGet,
|
|
|
|
path: kmsAPIsPath,
|
|
|
|
asRoot: true,
|
|
|
|
|
|
|
|
wantStatusCode: http.StatusOK,
|
|
|
|
wantResp: []string{"stub/path"},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "apis as user with no policy want forbidden",
|
|
|
|
method: http.MethodGet,
|
|
|
|
path: kmsAPIsPath,
|
|
|
|
asRoot: false,
|
|
|
|
|
|
|
|
wantStatusCode: http.StatusForbidden,
|
|
|
|
wantResp: []string{"AccessDenied"},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "apis as user with policy ignores resource want success",
|
|
|
|
method: http.MethodGet,
|
|
|
|
path: kmsAPIsPath,
|
|
|
|
asRoot: false,
|
|
|
|
|
|
|
|
policy: `{"Effect": "Allow",
|
|
|
|
"Action": ["kms:API"],
|
|
|
|
"Resource": ["arn:minio:kms:::does-not-matter-it-is-ignored"] }`,
|
|
|
|
|
|
|
|
wantStatusCode: http.StatusOK,
|
|
|
|
wantResp: []string{"stub/path"},
|
|
|
|
},
|
|
|
|
|
|
|
|
// Metrics test
|
|
|
|
{
|
|
|
|
name: "metrics as root want success",
|
|
|
|
method: http.MethodGet,
|
|
|
|
path: kmsMetricsPath,
|
|
|
|
asRoot: true,
|
|
|
|
|
|
|
|
wantStatusCode: http.StatusOK,
|
|
|
|
wantResp: []string{"kms"},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "metrics as user with no policy want forbidden",
|
|
|
|
method: http.MethodGet,
|
|
|
|
path: kmsMetricsPath,
|
|
|
|
asRoot: false,
|
|
|
|
|
|
|
|
wantStatusCode: http.StatusForbidden,
|
|
|
|
wantResp: []string{"AccessDenied"},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "metrics as user with policy ignores resource want success",
|
|
|
|
method: http.MethodGet,
|
|
|
|
path: kmsMetricsPath,
|
|
|
|
asRoot: false,
|
|
|
|
|
|
|
|
policy: `{"Effect": "Allow",
|
|
|
|
"Action": ["kms:Metrics"],
|
|
|
|
"Resource": ["arn:minio:kms:::does-not-matter-it-is-ignored"] }`,
|
|
|
|
|
|
|
|
wantStatusCode: http.StatusOK,
|
|
|
|
wantResp: []string{"kms"},
|
|
|
|
},
|
|
|
|
|
|
|
|
// Status tests
|
|
|
|
{
|
|
|
|
name: "status as root want success",
|
|
|
|
method: http.MethodGet,
|
|
|
|
path: kmsStatusPath,
|
|
|
|
asRoot: true,
|
|
|
|
|
|
|
|
wantStatusCode: http.StatusOK,
|
|
|
|
wantResp: []string{"MinIO builtin"},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "status as user with no policy want forbidden",
|
|
|
|
method: http.MethodGet,
|
|
|
|
path: kmsStatusPath,
|
|
|
|
asRoot: false,
|
|
|
|
|
|
|
|
wantStatusCode: http.StatusForbidden,
|
|
|
|
wantResp: []string{"AccessDenied"},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "status as user with policy ignores resource want success",
|
|
|
|
method: http.MethodGet,
|
|
|
|
path: kmsStatusPath,
|
|
|
|
asRoot: false,
|
|
|
|
|
|
|
|
policy: `{"Effect": "Allow",
|
|
|
|
"Action": ["kms:Status"],
|
|
|
|
"Resource": ["arn:minio:kms:::does-not-matter-it-is-ignored"]}`,
|
|
|
|
|
|
|
|
wantStatusCode: http.StatusOK,
|
|
|
|
wantResp: []string{"MinIO builtin"},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
for testNum, test := range tests {
|
|
|
|
t.Run(fmt.Sprintf("%d %s", testNum+1, test.name), func(t *testing.T) {
|
|
|
|
execKMSTest(t, test, adminTestBed)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestKMSHandlersListKeys(t *testing.T) {
|
|
|
|
adminTestBed, tearDown := setupKMSTest(t, true)
|
|
|
|
defer tearDown()
|
|
|
|
|
|
|
|
tests := []kmsTestCase{
|
|
|
|
{
|
|
|
|
name: "create a first key root user",
|
|
|
|
method: http.MethodPost,
|
|
|
|
path: kmsKeyCreatePath,
|
|
|
|
query: map[string]string{"key-id": "abc-test-key"},
|
|
|
|
asRoot: true,
|
|
|
|
|
|
|
|
wantStatusCode: http.StatusOK,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "create a second key root user",
|
|
|
|
method: http.MethodPost,
|
|
|
|
path: kmsKeyCreatePath,
|
|
|
|
query: map[string]string{"key-id": "xyz-test-key"},
|
|
|
|
asRoot: true,
|
|
|
|
|
|
|
|
wantStatusCode: http.StatusOK,
|
|
|
|
},
|
|
|
|
|
|
|
|
// List keys tests
|
|
|
|
{
|
|
|
|
name: "list keys as root want all to be returned",
|
|
|
|
method: http.MethodGet,
|
|
|
|
path: kmsKeyListPath,
|
|
|
|
query: map[string]string{"pattern": "*"},
|
|
|
|
asRoot: true,
|
|
|
|
|
|
|
|
wantStatusCode: http.StatusOK,
|
|
|
|
wantKeyNames: []string{"default-test-key", "abc-test-key", "xyz-test-key"},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "list keys as user with no policy want forbidden",
|
|
|
|
method: http.MethodGet,
|
|
|
|
path: kmsKeyListPath,
|
|
|
|
query: map[string]string{"pattern": "*"},
|
|
|
|
asRoot: false,
|
|
|
|
|
|
|
|
wantStatusCode: http.StatusForbidden,
|
|
|
|
wantResp: []string{"AccessDenied"},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "list keys as user with no resources specified want success",
|
|
|
|
method: http.MethodGet,
|
|
|
|
path: kmsKeyListPath,
|
|
|
|
query: map[string]string{"pattern": "*"},
|
|
|
|
asRoot: false,
|
|
|
|
|
|
|
|
policy: `{"Effect": "Allow",
|
|
|
|
"Action": ["kms:ListKeys"]
|
|
|
|
}`,
|
|
|
|
|
|
|
|
wantStatusCode: http.StatusOK,
|
|
|
|
wantKeyNames: []string{"default-test-key", "abc-test-key", "xyz-test-key"},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "list keys as user set policy resource to allow only one key",
|
|
|
|
method: http.MethodGet,
|
|
|
|
path: kmsKeyListPath,
|
|
|
|
query: map[string]string{"pattern": "*"},
|
|
|
|
asRoot: false,
|
|
|
|
|
|
|
|
policy: `{"Effect": "Allow",
|
|
|
|
"Action": ["kms:ListKeys"],
|
|
|
|
"Resource": ["arn:minio:kms:::abc*"]}`,
|
|
|
|
|
|
|
|
wantStatusCode: http.StatusOK,
|
|
|
|
wantKeyNames: []string{"abc-test-key"},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "list keys as user set policy to allow only one key, use pattern that includes correct key",
|
|
|
|
method: http.MethodGet,
|
|
|
|
path: kmsKeyListPath,
|
|
|
|
query: map[string]string{"pattern": "abc*"},
|
|
|
|
|
|
|
|
policy: `{"Effect": "Allow",
|
|
|
|
"Action": ["kms:ListKeys"],
|
|
|
|
"Resource": ["arn:minio:kms:::abc*"]}`,
|
|
|
|
|
|
|
|
wantStatusCode: http.StatusOK,
|
|
|
|
wantKeyNames: []string{"abc-test-key"},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "list keys as user set policy to allow only one key, use pattern that excludes correct key",
|
|
|
|
method: http.MethodGet,
|
|
|
|
path: kmsKeyListPath,
|
|
|
|
query: map[string]string{"pattern": "xyz*"},
|
|
|
|
asRoot: false,
|
|
|
|
|
|
|
|
policy: `{"Effect": "Allow",
|
|
|
|
"Action": ["kms:ListKeys"],
|
|
|
|
"Resource": ["arn:minio:kms:::abc*"]}`,
|
|
|
|
|
|
|
|
wantStatusCode: http.StatusOK,
|
|
|
|
wantKeyNames: []string{},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "list keys as user set policy that has no matching key resources",
|
|
|
|
method: http.MethodGet,
|
|
|
|
path: kmsKeyListPath,
|
|
|
|
query: map[string]string{"pattern": "*"},
|
|
|
|
asRoot: false,
|
|
|
|
|
|
|
|
policy: `{"Effect": "Allow",
|
|
|
|
"Action": ["kms:ListKeys"],
|
|
|
|
"Resource": ["arn:minio:kms:::nonematch*"]}`,
|
|
|
|
|
|
|
|
wantStatusCode: http.StatusOK,
|
|
|
|
wantKeyNames: []string{},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "list keys as user set policy that allows listing but denies specific keys",
|
|
|
|
method: http.MethodGet,
|
|
|
|
path: kmsKeyListPath,
|
|
|
|
query: map[string]string{"pattern": "*"},
|
|
|
|
asRoot: false,
|
|
|
|
|
|
|
|
// It looks like this should allow listing any key that isn't "default-test-key", however
|
|
|
|
// the policy engine matches all Deny statements first, without regard to Resources (for KMS).
|
|
|
|
// This is for backwards compatibility where historically KMS statements ignored Resources.
|
|
|
|
policy: `{
|
|
|
|
"Effect": "Allow",
|
|
|
|
"Action": ["kms:ListKeys"]
|
|
|
|
},{
|
|
|
|
"Effect": "Deny",
|
|
|
|
"Action": ["kms:ListKeys"],
|
|
|
|
"Resource": ["arn:minio:kms:::default-test-key"]
|
|
|
|
}`,
|
|
|
|
|
|
|
|
wantStatusCode: http.StatusForbidden,
|
|
|
|
wantResp: []string{"AccessDenied"},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
for testNum, test := range tests {
|
|
|
|
t.Run(fmt.Sprintf("%d %s", testNum+1, test.name), func(t *testing.T) {
|
|
|
|
execKMSTest(t, test, adminTestBed)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestKMSHandlerAdminAPI(t *testing.T) {
|
|
|
|
adminTestBed, tearDown := setupKMSTest(t, true)
|
|
|
|
defer tearDown()
|
|
|
|
|
|
|
|
tests := []kmsTestCase{
|
|
|
|
// Create key tests
|
|
|
|
{
|
|
|
|
name: "create a key root user",
|
|
|
|
method: http.MethodPost,
|
|
|
|
path: kmsAdminKeyCreate,
|
|
|
|
query: map[string]string{"key-id": "abc-test-key"},
|
|
|
|
asRoot: true,
|
|
|
|
|
|
|
|
wantStatusCode: http.StatusOK,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "create key as user with no policy want forbidden",
|
|
|
|
method: http.MethodPost,
|
|
|
|
path: kmsAdminKeyCreate,
|
|
|
|
query: map[string]string{"key-id": "new-test-key"},
|
|
|
|
asRoot: false,
|
|
|
|
|
|
|
|
wantStatusCode: http.StatusForbidden,
|
|
|
|
wantResp: []string{"AccessDenied"},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "create key as user with no resources specified want success",
|
|
|
|
method: http.MethodPost,
|
|
|
|
path: kmsAdminKeyCreate,
|
|
|
|
query: map[string]string{"key-id": "new-test-key"},
|
|
|
|
asRoot: false,
|
|
|
|
|
|
|
|
policy: `{"Effect": "Allow",
|
|
|
|
"Action": ["admin:KMSCreateKey"] }`,
|
|
|
|
|
|
|
|
wantStatusCode: http.StatusOK,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "create key as user set policy to non matching resource want success",
|
|
|
|
method: http.MethodPost,
|
|
|
|
path: kmsAdminKeyCreate,
|
|
|
|
query: map[string]string{"key-id": "third-new-test-key"},
|
|
|
|
asRoot: false,
|
|
|
|
|
|
|
|
// Admin actions ignore Resources
|
|
|
|
policy: `{"Effect": "Allow",
|
|
|
|
"Action": ["admin:KMSCreateKey"],
|
|
|
|
"Resource": ["arn:minio:kms:::this-is-disregarded"] }`,
|
|
|
|
|
|
|
|
wantStatusCode: http.StatusOK,
|
|
|
|
},
|
|
|
|
|
|
|
|
// Status tests
|
|
|
|
{
|
|
|
|
name: "status as root want success",
|
|
|
|
method: http.MethodPost,
|
|
|
|
path: kmsAdminStatusPath,
|
|
|
|
asRoot: true,
|
|
|
|
|
|
|
|
wantStatusCode: http.StatusOK,
|
|
|
|
wantResp: []string{"MinIO builtin"},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "status as user with no policy want forbidden",
|
|
|
|
method: http.MethodPost,
|
|
|
|
path: kmsAdminStatusPath,
|
|
|
|
asRoot: false,
|
|
|
|
|
|
|
|
wantStatusCode: http.StatusForbidden,
|
|
|
|
wantResp: []string{"AccessDenied"},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "status as user with policy ignores resource want success",
|
|
|
|
method: http.MethodPost,
|
|
|
|
path: kmsAdminStatusPath,
|
|
|
|
asRoot: false,
|
|
|
|
|
|
|
|
policy: `{"Effect": "Allow",
|
|
|
|
"Action": ["admin:KMSKeyStatus"],
|
|
|
|
"Resource": ["arn:minio:kms:::does-not-matter-it-is-ignored"] }`,
|
|
|
|
|
|
|
|
wantStatusCode: http.StatusOK,
|
|
|
|
wantResp: []string{"MinIO builtin"},
|
|
|
|
},
|
|
|
|
|
|
|
|
// Key status tests
|
|
|
|
{
|
|
|
|
name: "key status as root want success",
|
|
|
|
method: http.MethodGet,
|
|
|
|
path: kmsAdminKeyStatusPath,
|
|
|
|
asRoot: true,
|
|
|
|
|
|
|
|
wantStatusCode: http.StatusOK,
|
|
|
|
wantResp: []string{"key-id"},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "key status as user with no policy want forbidden",
|
|
|
|
method: http.MethodGet,
|
|
|
|
path: kmsAdminKeyStatusPath,
|
|
|
|
asRoot: false,
|
|
|
|
|
|
|
|
wantStatusCode: http.StatusForbidden,
|
|
|
|
wantResp: []string{"AccessDenied"},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "key status as user with policy ignores resource want success",
|
|
|
|
method: http.MethodGet,
|
|
|
|
path: kmsAdminKeyStatusPath,
|
|
|
|
asRoot: false,
|
|
|
|
|
|
|
|
policy: `{"Effect": "Allow",
|
|
|
|
"Action": ["admin:KMSKeyStatus"],
|
|
|
|
"Resource": ["arn:minio:kms:::does-not-matter-it-is-ignored"] }`,
|
|
|
|
|
|
|
|
wantStatusCode: http.StatusOK,
|
|
|
|
wantResp: []string{"key-id"},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
for testNum, test := range tests {
|
|
|
|
t.Run(fmt.Sprintf("%d %s", testNum+1, test.name), func(t *testing.T) {
|
|
|
|
execKMSTest(t, test, adminTestBed)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// execKMSTest runs a single test case for KMS handlers
|
|
|
|
func execKMSTest(t *testing.T, test kmsTestCase, adminTestBed *adminErasureTestBed) {
|
|
|
|
var accessKey, secretKey string
|
|
|
|
if test.asRoot {
|
|
|
|
accessKey, secretKey = globalActiveCred.AccessKey, globalActiveCred.SecretKey
|
|
|
|
} else {
|
|
|
|
setupKMSUser(t, userAccessKey, userSecretKey, test.policy)
|
|
|
|
accessKey = userAccessKey
|
|
|
|
secretKey = userSecretKey
|
|
|
|
}
|
|
|
|
|
|
|
|
req := buildKMSRequest(t, test.method, test.path, accessKey, secretKey, test.query)
|
|
|
|
rec := httptest.NewRecorder()
|
|
|
|
adminTestBed.router.ServeHTTP(rec, req)
|
|
|
|
|
|
|
|
t.Logf("HTTP req: %s, resp code: %d, resp body: %s", req.URL.String(), rec.Code, rec.Body.String())
|
|
|
|
|
|
|
|
// Check status code
|
|
|
|
if rec.Code != test.wantStatusCode {
|
|
|
|
t.Errorf("want status code %d, got %d", test.wantStatusCode, rec.Code)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check returned key list is correct
|
|
|
|
if test.wantKeyNames != nil {
|
2024-08-18 02:43:03 -04:00
|
|
|
keys := []madmin.KMSKeyInfo{}
|
|
|
|
err := json.Unmarshal(rec.Body.Bytes(), &keys)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
if len(keys) != len(test.wantKeyNames) {
|
|
|
|
t.Fatalf("want %d keys, got %d", len(test.wantKeyNames), len(keys))
|
2024-07-16 10:03:03 -04:00
|
|
|
}
|
2024-08-18 02:43:03 -04:00
|
|
|
|
|
|
|
for i, want := range keys {
|
|
|
|
if want.CreatedBy != kms.StubCreatedBy {
|
|
|
|
t.Fatalf("want key created by %s, got %s", kms.StubCreatedBy, want.CreatedBy)
|
|
|
|
}
|
|
|
|
if want.CreatedAt != kms.StubCreatedAt {
|
|
|
|
t.Fatalf("want key created at %s, got %s", kms.StubCreatedAt, want.CreatedAt)
|
|
|
|
}
|
|
|
|
if test.wantKeyNames[i] != want.Name {
|
|
|
|
t.Fatalf("want key name %s, got %s", test.wantKeyNames[i], want.Name)
|
2024-07-16 10:03:03 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check generic text in the response
|
|
|
|
if test.wantResp != nil {
|
|
|
|
for _, want := range test.wantResp {
|
|
|
|
if !strings.Contains(rec.Body.String(), want) {
|
|
|
|
t.Fatalf("want response to contain %s, got %s", want, rec.Body.String())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// TestKMSHandlerNotConfiguredOrInvalidCreds tests KMS handlers for situations where KMS is not configured
|
|
|
|
// or invalid credentials are provided.
|
|
|
|
func TestKMSHandlerNotConfiguredOrInvalidCreds(t *testing.T) {
|
|
|
|
adminTestBed, tearDown := setupKMSTest(t, false)
|
|
|
|
defer tearDown()
|
|
|
|
|
|
|
|
tests := []struct {
|
|
|
|
name string
|
|
|
|
method string
|
|
|
|
path string
|
|
|
|
query map[string]string
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
name: "GET status",
|
|
|
|
method: http.MethodGet,
|
|
|
|
path: kmsStatusPath,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "GET metrics",
|
|
|
|
method: http.MethodGet,
|
|
|
|
path: kmsMetricsPath,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "GET apis",
|
|
|
|
method: http.MethodGet,
|
|
|
|
path: kmsAPIsPath,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "GET version",
|
|
|
|
method: http.MethodGet,
|
|
|
|
path: kmsVersionPath,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "POST key create",
|
|
|
|
method: http.MethodPost,
|
|
|
|
path: kmsKeyCreatePath,
|
|
|
|
query: map[string]string{"key-id": "master-key-id"},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "GET key list",
|
|
|
|
method: http.MethodGet,
|
|
|
|
path: kmsKeyListPath,
|
|
|
|
query: map[string]string{"pattern": "*"},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "GET key status",
|
|
|
|
method: http.MethodGet,
|
|
|
|
path: kmsKeyStatusPath,
|
|
|
|
query: map[string]string{"key-id": "master-key-id"},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
// Test when the GlobalKMS is not configured
|
|
|
|
for _, test := range tests {
|
|
|
|
t.Run(test.name+" not configured", func(t *testing.T) {
|
|
|
|
req := buildKMSRequest(t, test.method, test.path, "", "", test.query)
|
|
|
|
rec := httptest.NewRecorder()
|
|
|
|
adminTestBed.router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusNotImplemented {
|
|
|
|
t.Errorf("want status code %d, got %d", http.StatusNotImplemented, rec.Code)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// Test when the GlobalKMS is configured but the credentials are invalid
|
|
|
|
GlobalKMS = kms.NewStub("default-test-key")
|
|
|
|
for _, test := range tests {
|
|
|
|
t.Run(test.name+" invalid credentials", func(t *testing.T) {
|
|
|
|
req := buildKMSRequest(t, test.method, test.path, userAccessKey, userSecretKey, test.query)
|
|
|
|
rec := httptest.NewRecorder()
|
|
|
|
adminTestBed.router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusForbidden {
|
|
|
|
t.Errorf("want status code %d, got %d", http.StatusForbidden, rec.Code)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func setupKMSTest(t *testing.T, enableKMS bool) (*adminErasureTestBed, func()) {
|
|
|
|
adminTestBed, err := prepareAdminErasureTestBed(context.Background())
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
registerKMSRouter(adminTestBed.router)
|
|
|
|
|
|
|
|
if enableKMS {
|
|
|
|
GlobalKMS = kms.NewStub("default-test-key")
|
|
|
|
}
|
|
|
|
|
|
|
|
tearDown := func() {
|
|
|
|
adminTestBed.TearDown()
|
|
|
|
GlobalKMS = nil
|
|
|
|
}
|
|
|
|
return adminTestBed, tearDown
|
|
|
|
}
|
|
|
|
|
|
|
|
func buildKMSRequest(t *testing.T, method, path, accessKey, secretKey string, query map[string]string) *http.Request {
|
|
|
|
if len(query) > 0 {
|
|
|
|
queryVal := url.Values{}
|
|
|
|
for k, v := range query {
|
|
|
|
queryVal.Add(k, v)
|
|
|
|
}
|
|
|
|
path = path + "?" + queryVal.Encode()
|
|
|
|
}
|
|
|
|
|
|
|
|
if accessKey == "" && secretKey == "" {
|
|
|
|
accessKey = globalActiveCred.AccessKey
|
|
|
|
secretKey = globalActiveCred.SecretKey
|
|
|
|
}
|
|
|
|
|
|
|
|
req, err := newTestSignedRequestV4(method, path, 0, nil, accessKey, secretKey, nil)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
return req
|
|
|
|
}
|
|
|
|
|
|
|
|
// setupKMSUser is a test helper that creates a new user with the provided access key and secret key
|
|
|
|
// and applies the given policy to the user.
|
|
|
|
func setupKMSUser(t *testing.T, accessKey, secretKey, p string) {
|
|
|
|
ctx := context.Background()
|
|
|
|
createUserParams := madmin.AddOrUpdateUserReq{
|
|
|
|
SecretKey: secretKey,
|
|
|
|
Status: madmin.AccountEnabled,
|
|
|
|
}
|
|
|
|
_, err := globalIAMSys.CreateUser(ctx, accessKey, createUserParams)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
testKMSPolicyName := "testKMSPolicy"
|
|
|
|
if p != "" {
|
|
|
|
p = `{"Version":"2012-10-17","Statement":[` + p + `]}`
|
|
|
|
policyData, err := policy.ParseConfig(strings.NewReader(p))
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
_, err = globalIAMSys.SetPolicy(ctx, testKMSPolicyName, *policyData)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
_, err = globalIAMSys.PolicyDBSet(ctx, accessKey, testKMSPolicyName, regUser, false)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
err = globalIAMSys.DeletePolicy(ctx, testKMSPolicyName, false)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
_, err = globalIAMSys.PolicyDBSet(ctx, accessKey, "", regUser, false)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|