mirror of https://github.com/minio/minio.git
allow root user to be disabled via config settings (#17089)
This commit is contained in:
parent
701b89f377
commit
7ae69accc0
|
@ -0,0 +1,34 @@
|
||||||
|
name: Root lockdown tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
# This ensures that previous jobs for the PR are canceled when the PR is
|
||||||
|
# updated.
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.head_ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Go ${{ matrix.go-version }} on ${{ matrix.os }}
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
go-version: [1.20.x]
|
||||||
|
os: [ubuntu-latest]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.go-version }}
|
||||||
|
check-latest: true
|
||||||
|
- name: Start root lockdown tests
|
||||||
|
run: |
|
||||||
|
make test-root-disable
|
4
Makefile
4
Makefile
|
@ -49,6 +49,10 @@ test: verifiers build ## builds minio, runs linters, tests
|
||||||
@echo "Running unit tests"
|
@echo "Running unit tests"
|
||||||
@MINIO_API_REQUESTS_MAX=10000 CGO_ENABLED=0 go test -tags kqueue ./...
|
@MINIO_API_REQUESTS_MAX=10000 CGO_ENABLED=0 go test -tags kqueue ./...
|
||||||
|
|
||||||
|
test-root-disable: install
|
||||||
|
@echo "Running minio root lockdown tests"
|
||||||
|
@env bash $(PWD)/buildscripts/disable-root.sh
|
||||||
|
|
||||||
test-decom: install
|
test-decom: install
|
||||||
@echo "Running minio decom tests"
|
@echo "Running minio decom tests"
|
||||||
@env bash $(PWD)/docs/distributed/decom.sh
|
@env bash $(PWD)/docs/distributed/decom.sh
|
||||||
|
|
|
@ -0,0 +1,119 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -x
|
||||||
|
|
||||||
|
export MINIO_CI_CD=1
|
||||||
|
killall -9 minio
|
||||||
|
|
||||||
|
rm -rf ${HOME}/tmp/dist
|
||||||
|
|
||||||
|
scheme="http"
|
||||||
|
nr_servers=4
|
||||||
|
|
||||||
|
addr="localhost"
|
||||||
|
args=""
|
||||||
|
for ((i=0;i<$[${nr_servers}];i++)); do
|
||||||
|
args="$args $scheme://$addr:$[9100+$i]/${HOME}/tmp/dist/path1/$i"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo $args
|
||||||
|
|
||||||
|
for ((i=0;i<$[${nr_servers}];i++)); do
|
||||||
|
(minio server --address ":$[9100+$i]" $args 2>&1 > /tmp/log$i.txt) &
|
||||||
|
done
|
||||||
|
|
||||||
|
sleep 10s
|
||||||
|
|
||||||
|
if [ ! -f ./mc ]; then
|
||||||
|
wget --quiet -O ./mc https://dl.minio.io/client/mc/release/linux-amd64/./mc && \
|
||||||
|
chmod +x mc
|
||||||
|
fi
|
||||||
|
|
||||||
|
set +e
|
||||||
|
|
||||||
|
export MC_HOST_minioadm=http://minioadmin:minioadmin@localhost:9100/
|
||||||
|
|
||||||
|
./mc ls minioadm/
|
||||||
|
|
||||||
|
./mc admin config set minioadm/ api root_access=off
|
||||||
|
|
||||||
|
sleep 3s # let things settle a little
|
||||||
|
|
||||||
|
./mc ls minioadm/
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo "listing succeeded, 'minioadmin' was not disabled"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
killall -9 minio
|
||||||
|
|
||||||
|
export MINIO_API_ROOT_ACCESS=on
|
||||||
|
for ((i=0;i<$[${nr_servers}];i++)); do
|
||||||
|
(minio server --address ":$[9100+$i]" $args 2>&1 > /tmp/log$i.txt) &
|
||||||
|
done
|
||||||
|
|
||||||
|
set +e
|
||||||
|
|
||||||
|
./mc ls minioadm/
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "listing failed, 'minioadmin' should be enabled"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
killall -9 minio
|
||||||
|
|
||||||
|
rm -rf /tmp/multisitea/
|
||||||
|
rm -rf /tmp/multisiteb/
|
||||||
|
|
||||||
|
echo "Setup site-replication and then disable root credentials"
|
||||||
|
|
||||||
|
minio server --address 127.0.0.1:9001 "http://127.0.0.1:9001/tmp/multisitea/data/disterasure/xl{1...4}" \
|
||||||
|
"http://127.0.0.1:9002/tmp/multisitea/data/disterasure/xl{5...8}" >/tmp/sitea_1.log 2>&1 &
|
||||||
|
minio server --address 127.0.0.1:9002 "http://127.0.0.1:9001/tmp/multisitea/data/disterasure/xl{1...4}" \
|
||||||
|
"http://127.0.0.1:9002/tmp/multisitea/data/disterasure/xl{5...8}" >/tmp/sitea_2.log 2>&1 &
|
||||||
|
|
||||||
|
minio server --address 127.0.0.1:9003 "http://127.0.0.1:9003/tmp/multisiteb/data/disterasure/xl{1...4}" \
|
||||||
|
"http://127.0.0.1:9004/tmp/multisiteb/data/disterasure/xl{5...8}" >/tmp/siteb_1.log 2>&1 &
|
||||||
|
minio server --address 127.0.0.1:9004 "http://127.0.0.1:9003/tmp/multisiteb/data/disterasure/xl{1...4}" \
|
||||||
|
"http://127.0.0.1:9004/tmp/multisiteb/data/disterasure/xl{5...8}" >/tmp/siteb_2.log 2>&1 &
|
||||||
|
|
||||||
|
sleep 20s
|
||||||
|
|
||||||
|
export MC_HOST_sitea=http://minioadmin:minioadmin@127.0.0.1:9001
|
||||||
|
export MC_HOST_siteb=http://minioadmin:minioadmin@127.0.0.1:9004
|
||||||
|
|
||||||
|
./mc admin replicate add sitea siteb
|
||||||
|
|
||||||
|
./mc admin user add sitea foobar foo12345
|
||||||
|
|
||||||
|
./mc admin policy attach sitea/ consoleAdmin --user=foobar
|
||||||
|
|
||||||
|
./mc admin user info siteb foobar
|
||||||
|
|
||||||
|
killall -9 minio
|
||||||
|
|
||||||
|
echo "turning off root access, however site replication must continue"
|
||||||
|
export MINIO_API_ROOT_ACCESS=off
|
||||||
|
|
||||||
|
minio server --address 127.0.0.1:9001 "http://127.0.0.1:9001/tmp/multisitea/data/disterasure/xl{1...4}" \
|
||||||
|
"http://127.0.0.1:9002/tmp/multisitea/data/disterasure/xl{5...8}" >/tmp/sitea_1.log 2>&1 &
|
||||||
|
minio server --address 127.0.0.1:9002 "http://127.0.0.1:9001/tmp/multisitea/data/disterasure/xl{1...4}" \
|
||||||
|
"http://127.0.0.1:9002/tmp/multisitea/data/disterasure/xl{5...8}" >/tmp/sitea_2.log 2>&1 &
|
||||||
|
|
||||||
|
minio server --address 127.0.0.1:9003 "http://127.0.0.1:9003/tmp/multisiteb/data/disterasure/xl{1...4}" \
|
||||||
|
"http://127.0.0.1:9004/tmp/multisiteb/data/disterasure/xl{5...8}" >/tmp/siteb_1.log 2>&1 &
|
||||||
|
minio server --address 127.0.0.1:9004 "http://127.0.0.1:9003/tmp/multisiteb/data/disterasure/xl{1...4}" \
|
||||||
|
"http://127.0.0.1:9004/tmp/multisiteb/data/disterasure/xl{5...8}" >/tmp/siteb_2.log 2>&1 &
|
||||||
|
|
||||||
|
sleep 20s
|
||||||
|
|
||||||
|
export MC_HOST_sitea=http://foobar:foo12345@127.0.0.1:9001
|
||||||
|
export MC_HOST_siteb=http://foobar:foo12345@127.0.0.1:9004
|
||||||
|
|
||||||
|
./mc admin user add sitea foobar-admin foo12345
|
||||||
|
|
||||||
|
sleep 2s
|
||||||
|
|
||||||
|
./mc admin user info siteb foobar-admin
|
|
@ -44,7 +44,7 @@ func (a adminAPIHandlers) RemoveUser(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.DeleteUserAdminAction)
|
objectAPI, cred := validateAdminReq(ctx, w, r, iampolicy.DeleteUserAdminAction)
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -62,6 +62,13 @@ func (a adminAPIHandlers) RemoveUser(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When the user is root credential you are not allowed to
|
||||||
|
// remove the root user. Also you cannot delete yourself.
|
||||||
|
if accessKey == globalActiveCred.AccessKey || accessKey == cred.AccessKey {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err := globalIAMSys.DeleteUser(ctx, accessKey, true); err != nil {
|
if err := globalIAMSys.DeleteUser(ctx, accessKey, true); err != nil {
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
|
@ -239,6 +246,26 @@ func (a adminAPIHandlers) UpdateGroupMembers(w http.ResponseWriter, r *http.Requ
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reject if the group add and remove are temporary credentials, or root credential.
|
||||||
|
for _, member := range updReq.Members {
|
||||||
|
ok, _, err := globalIAMSys.IsTempUser(member)
|
||||||
|
if err != nil && err != errNoSuchUser {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// When the user is root credential you are not allowed to
|
||||||
|
// add policies for root user.
|
||||||
|
if member == globalActiveCred.AccessKey {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var updatedAt time.Time
|
var updatedAt time.Time
|
||||||
if updReq.IsRemove {
|
if updReq.IsRemove {
|
||||||
updatedAt, err = globalIAMSys.RemoveUsersFromGroup(ctx, updReq.Group, updReq.Members)
|
updatedAt, err = globalIAMSys.RemoveUsersFromGroup(ctx, updReq.Group, updReq.Members)
|
||||||
|
@ -374,7 +401,7 @@ func (a adminAPIHandlers) SetUserStatus(w http.ResponseWriter, r *http.Request)
|
||||||
|
|
||||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.EnableUserAdminAction)
|
objectAPI, creds := validateAdminReq(ctx, w, r, iampolicy.EnableUserAdminAction)
|
||||||
if objectAPI == nil {
|
if objectAPI == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -383,9 +410,9 @@ func (a adminAPIHandlers) SetUserStatus(w http.ResponseWriter, r *http.Request)
|
||||||
accessKey := vars["accessKey"]
|
accessKey := vars["accessKey"]
|
||||||
status := vars["status"]
|
status := vars["status"]
|
||||||
|
|
||||||
// This API is not allowed to lookup master access key user status
|
// you cannot enable or disable yourself.
|
||||||
if accessKey == globalActiveCred.AccessKey {
|
if accessKey == creds.AccessKey {
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errInvalidArgument), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1627,6 +1654,12 @@ func (a adminAPIHandlers) SetPolicyForUserOrGroup(w http.ResponseWriter, r *http
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL)
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// When the user is root credential you are not allowed to
|
||||||
|
// add policies for root user.
|
||||||
|
if entityName == globalActiveCred.AccessKey {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate that user or group exists.
|
// Validate that user or group exists.
|
||||||
|
@ -1771,6 +1804,13 @@ func (a adminAPIHandlers) AttachPolicyBuiltin(w http.ResponseWriter, r *http.Req
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When the user is root credential you are not allowed to
|
||||||
|
// add policies for root user.
|
||||||
|
if userOrGroup == globalActiveCred.AccessKey {
|
||||||
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Validate that user exists.
|
// Validate that user exists.
|
||||||
if globalIAMSys.GetUsersSysType() == MinIOUsersSysType {
|
if globalIAMSys.GetUsersSysType() == MinIOUsersSysType {
|
||||||
_, ok := globalIAMSys.GetUser(ctx, userOrGroup)
|
_, ok := globalIAMSys.GetUser(ctx, userOrGroup)
|
||||||
|
@ -2056,13 +2096,15 @@ func (a adminAPIHandlers) ExportIAM(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
userAccounts := make(map[string]madmin.AddOrUpdateUserReq)
|
userAccounts := make(map[string]madmin.AddOrUpdateUserReq)
|
||||||
for u, uid := range userIdentities {
|
for u, uid := range userIdentities {
|
||||||
status := madmin.AccountDisabled
|
|
||||||
if uid.Credentials.IsValid() {
|
|
||||||
status = madmin.AccountEnabled
|
|
||||||
}
|
|
||||||
userAccounts[u] = madmin.AddOrUpdateUserReq{
|
userAccounts[u] = madmin.AddOrUpdateUserReq{
|
||||||
SecretKey: uid.Credentials.SecretKey,
|
SecretKey: uid.Credentials.SecretKey,
|
||||||
Status: status,
|
Status: func() madmin.AccountStatus {
|
||||||
|
// Export current credential status
|
||||||
|
if uid.Credentials.Status == auth.AccountOff {
|
||||||
|
return madmin.AccountDisabled
|
||||||
|
}
|
||||||
|
return madmin.AccountEnabled
|
||||||
|
}(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
userData, err := json.Marshal(userAccounts)
|
userData, err := json.Marshal(userAccounts)
|
||||||
|
@ -2101,6 +2143,10 @@ func (a adminAPIHandlers) ExportIAM(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
svcAccts := make(map[string]madmin.SRSvcAccCreate)
|
svcAccts := make(map[string]madmin.SRSvcAccCreate)
|
||||||
for user, acc := range serviceAccounts {
|
for user, acc := range serviceAccounts {
|
||||||
|
if user == siteReplicatorSvcAcc {
|
||||||
|
// skip site-replication service account.
|
||||||
|
continue
|
||||||
|
}
|
||||||
claims, err := globalIAMSys.GetClaimsForSvcAcc(ctx, acc.Credentials.AccessKey)
|
claims, err := globalIAMSys.GetClaimsForSvcAcc(ctx, acc.Credentials.AccessKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
||||||
|
@ -2467,6 +2513,7 @@ func (a adminAPIHandlers) ImportIAM(w http.ResponseWriter, r *http.Request) {
|
||||||
claims: svcAcctReq.Claims,
|
claims: svcAcctReq.Claims,
|
||||||
comment: svcAcctReq.Comment,
|
comment: svcAcctReq.Comment,
|
||||||
expiration: svcAcctReq.Expiration,
|
expiration: svcAcctReq.Expiration,
|
||||||
|
allowSiteReplicatorAccount: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
// In case of LDAP we need to resolve the targetUser to a DN and
|
// In case of LDAP we need to resolve the targetUser to a DN and
|
||||||
|
|
|
@ -375,8 +375,6 @@ func TestIsReqAuthenticated(t *testing.T) {
|
||||||
|
|
||||||
initConfigSubsystem(ctx, objLayer)
|
initConfigSubsystem(ctx, objLayer)
|
||||||
|
|
||||||
globalIAMSys.Init(ctx, objLayer, globalEtcdClient, 2*time.Second)
|
|
||||||
|
|
||||||
creds, err := auth.CreateCredentials("myuser", "mypassword")
|
creds, err := auth.CreateCredentials("myuser", "mypassword")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable create credential, %s", err)
|
t.Fatalf("unable create credential, %s", err)
|
||||||
|
@ -384,6 +382,8 @@ func TestIsReqAuthenticated(t *testing.T) {
|
||||||
|
|
||||||
globalActiveCred = creds
|
globalActiveCred = creds
|
||||||
|
|
||||||
|
globalIAMSys.Init(ctx, objLayer, globalEtcdClient, 2*time.Second)
|
||||||
|
|
||||||
// List of test cases for validating http request authentication.
|
// List of test cases for validating http request authentication.
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
req *http.Request
|
req *http.Request
|
||||||
|
@ -464,9 +464,8 @@ func TestValidateAdminSignature(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
initAllSubsystems(ctx)
|
initAllSubsystems(ctx)
|
||||||
initConfigSubsystem(ctx, objLayer)
|
|
||||||
|
|
||||||
globalIAMSys.Init(ctx, objLayer, globalEtcdClient, 2*time.Second)
|
initConfigSubsystem(ctx, objLayer)
|
||||||
|
|
||||||
creds, err := auth.CreateCredentials("admin", "mypassword")
|
creds, err := auth.CreateCredentials("admin", "mypassword")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -474,6 +473,8 @@ func TestValidateAdminSignature(t *testing.T) {
|
||||||
}
|
}
|
||||||
globalActiveCred = creds
|
globalActiveCred = creds
|
||||||
|
|
||||||
|
globalIAMSys.Init(ctx, objLayer, globalEtcdClient, 2*time.Second)
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
AccessKey string
|
AccessKey string
|
||||||
SecretKey string
|
SecretKey string
|
||||||
|
|
|
@ -197,7 +197,7 @@ var (
|
||||||
globalBucketTargetSys *BucketTargetSys
|
globalBucketTargetSys *BucketTargetSys
|
||||||
// globalAPIConfig controls S3 API requests throttling,
|
// globalAPIConfig controls S3 API requests throttling,
|
||||||
// healthcheck readiness deadlines and cors settings.
|
// healthcheck readiness deadlines and cors settings.
|
||||||
globalAPIConfig = apiConfig{listQuorum: "strict"}
|
globalAPIConfig = apiConfig{listQuorum: "strict", rootAccess: true}
|
||||||
|
|
||||||
globalStorageClass storageclass.Config
|
globalStorageClass storageclass.Config
|
||||||
|
|
||||||
|
|
|
@ -51,6 +51,7 @@ type apiConfig struct {
|
||||||
deleteCleanupInterval time.Duration
|
deleteCleanupInterval time.Duration
|
||||||
disableODirect bool
|
disableODirect bool
|
||||||
gzipObjects bool
|
gzipObjects bool
|
||||||
|
rootAccess bool
|
||||||
}
|
}
|
||||||
|
|
||||||
const cgroupLimitFile = "/sys/fs/cgroup/memory/memory.limit_in_bytes"
|
const cgroupLimitFile = "/sys/fs/cgroup/memory/memory.limit_in_bytes"
|
||||||
|
@ -152,6 +153,7 @@ func (t *apiConfig) init(cfg api.Config, setDriveCounts []int) {
|
||||||
t.deleteCleanupInterval = cfg.DeleteCleanupInterval
|
t.deleteCleanupInterval = cfg.DeleteCleanupInterval
|
||||||
t.disableODirect = cfg.DisableODirect
|
t.disableODirect = cfg.DisableODirect
|
||||||
t.gzipObjects = cfg.GzipObjects
|
t.gzipObjects = cfg.GzipObjects
|
||||||
|
t.rootAccess = cfg.RootAccess
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *apiConfig) isDisableODirect() bool {
|
func (t *apiConfig) isDisableODirect() bool {
|
||||||
|
@ -168,6 +170,13 @@ func (t *apiConfig) shouldGzipObjects() bool {
|
||||||
return t.gzipObjects
|
return t.gzipObjects
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *apiConfig) permitRootAccess() bool {
|
||||||
|
t.mu.RLock()
|
||||||
|
defer t.mu.RUnlock()
|
||||||
|
|
||||||
|
return t.rootAccess
|
||||||
|
}
|
||||||
|
|
||||||
func (t *apiConfig) getListQuorum() string {
|
func (t *apiConfig) getListQuorum() string {
|
||||||
t.mu.RLock()
|
t.mu.RLock()
|
||||||
defer t.mu.RUnlock()
|
defer t.mu.RUnlock()
|
||||||
|
|
|
@ -919,6 +919,7 @@ type newServiceAccountOpts struct {
|
||||||
secretKey string
|
secretKey string
|
||||||
comment string
|
comment string
|
||||||
expiration *time.Time
|
expiration *time.Time
|
||||||
|
allowSiteReplicatorAccount bool // allow creating internal service account for site-replication.
|
||||||
|
|
||||||
claims map[string]interface{}
|
claims map[string]interface{}
|
||||||
}
|
}
|
||||||
|
@ -953,7 +954,9 @@ func (sys *IAMSys) NewServiceAccount(ctx context.Context, parentUser string, gro
|
||||||
if parentUser == opts.accessKey {
|
if parentUser == opts.accessKey {
|
||||||
return auth.Credentials{}, time.Time{}, errIAMActionNotAllowed
|
return auth.Credentials{}, time.Time{}, errIAMActionNotAllowed
|
||||||
}
|
}
|
||||||
|
if siteReplicatorSvcAcc == opts.accessKey && !opts.allowSiteReplicatorAccount {
|
||||||
|
return auth.Credentials{}, time.Time{}, errIAMActionNotAllowed
|
||||||
|
}
|
||||||
m := make(map[string]interface{})
|
m := make(map[string]interface{})
|
||||||
m[parentClaim] = parentUser
|
m[parentClaim] = parentUser
|
||||||
|
|
||||||
|
|
64
cmd/jwt.go
64
cmd/jwt.go
|
@ -18,7 +18,6 @@
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
@ -40,48 +39,15 @@ const (
|
||||||
|
|
||||||
// Inter-node JWT token expiry is 15 minutes.
|
// Inter-node JWT token expiry is 15 minutes.
|
||||||
defaultInterNodeJWTExpiry = 15 * time.Minute
|
defaultInterNodeJWTExpiry = 15 * time.Minute
|
||||||
|
|
||||||
// URL JWT token expiry is one minute (might be exposed).
|
|
||||||
defaultURLJWTExpiry = time.Minute
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
errInvalidAccessKeyID = errors.New("The access key ID you provided does not exist in our records")
|
errInvalidAccessKeyID = errors.New("The access key ID you provided does not exist in our records")
|
||||||
|
errAccessKeyDisabled = errors.New("The access key you provided is disabled")
|
||||||
errAuthentication = errors.New("Authentication failed, check your access credentials")
|
errAuthentication = errors.New("Authentication failed, check your access credentials")
|
||||||
errNoAuthToken = errors.New("JWT token missing")
|
errNoAuthToken = errors.New("JWT token missing")
|
||||||
)
|
)
|
||||||
|
|
||||||
func authenticateJWTUsers(accessKey, secretKey string, expiry time.Duration) (string, error) {
|
|
||||||
passedCredential, err := auth.CreateCredentials(accessKey, secretKey)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
expiresAt := UTCNow().Add(expiry)
|
|
||||||
return authenticateJWTUsersWithCredentials(passedCredential, expiresAt)
|
|
||||||
}
|
|
||||||
|
|
||||||
func authenticateJWTUsersWithCredentials(credentials auth.Credentials, expiresAt time.Time) (string, error) {
|
|
||||||
serverCred := globalActiveCred
|
|
||||||
if serverCred.AccessKey != credentials.AccessKey {
|
|
||||||
u, ok := globalIAMSys.GetUser(context.TODO(), credentials.AccessKey)
|
|
||||||
if !ok {
|
|
||||||
return "", errInvalidAccessKeyID
|
|
||||||
}
|
|
||||||
serverCred = u.Credentials
|
|
||||||
}
|
|
||||||
|
|
||||||
if !serverCred.Equal(credentials) {
|
|
||||||
return "", errAuthentication
|
|
||||||
}
|
|
||||||
|
|
||||||
claims := xjwt.NewMapClaims()
|
|
||||||
claims.SetExpiry(expiresAt)
|
|
||||||
claims.SetAccessKey(credentials.AccessKey)
|
|
||||||
|
|
||||||
jwt := jwtgo.NewWithClaims(jwtgo.SigningMethodHS512, claims)
|
|
||||||
return jwt.SignedString([]byte(serverCred.SecretKey))
|
|
||||||
}
|
|
||||||
|
|
||||||
// cachedAuthenticateNode will cache authenticateNode results for given values up to ttl.
|
// cachedAuthenticateNode will cache authenticateNode results for given values up to ttl.
|
||||||
func cachedAuthenticateNode(ttl time.Duration) func(accessKey, secretKey, audience string) (string, error) {
|
func cachedAuthenticateNode(ttl time.Duration) func(accessKey, secretKey, audience string) (string, error) {
|
||||||
type key struct {
|
type key struct {
|
||||||
|
@ -121,14 +87,6 @@ func authenticateNode(accessKey, secretKey, audience string) (string, error) {
|
||||||
return jwt.SignedString([]byte(secretKey))
|
return jwt.SignedString([]byte(secretKey))
|
||||||
}
|
}
|
||||||
|
|
||||||
func authenticateWeb(accessKey, secretKey string) (string, error) {
|
|
||||||
return authenticateJWTUsers(accessKey, secretKey, defaultJWTExpiry)
|
|
||||||
}
|
|
||||||
|
|
||||||
func authenticateURL(accessKey, secretKey string) (string, error) {
|
|
||||||
return authenticateJWTUsers(accessKey, secretKey, defaultURLJWTExpiry)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the request is authenticated.
|
// Check if the request is authenticated.
|
||||||
// Returns nil if the request is authenticated. errNoAuthToken if token missing.
|
// Returns nil if the request is authenticated. errNoAuthToken if token missing.
|
||||||
// Returns errAuthentication for all other errors.
|
// Returns errAuthentication for all other errors.
|
||||||
|
@ -142,15 +100,24 @@ func metricsRequestAuthenticate(req *http.Request) (*xjwt.MapClaims, []string, b
|
||||||
}
|
}
|
||||||
claims := xjwt.NewMapClaims()
|
claims := xjwt.NewMapClaims()
|
||||||
if err := xjwt.ParseWithClaims(token, claims, func(claims *xjwt.MapClaims) ([]byte, error) {
|
if err := xjwt.ParseWithClaims(token, claims, func(claims *xjwt.MapClaims) ([]byte, error) {
|
||||||
if claims.AccessKey == globalActiveCred.AccessKey {
|
if claims.AccessKey != globalActiveCred.AccessKey {
|
||||||
return []byte(globalActiveCred.SecretKey), nil
|
|
||||||
}
|
|
||||||
u, ok := globalIAMSys.GetUser(req.Context(), claims.AccessKey)
|
u, ok := globalIAMSys.GetUser(req.Context(), claims.AccessKey)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
// Credentials will be invalid but for disabled
|
||||||
|
// return a different error in such a scenario.
|
||||||
|
if u.Credentials.Status == auth.AccountOff {
|
||||||
|
return nil, errAccessKeyDisabled
|
||||||
|
}
|
||||||
return nil, errInvalidAccessKeyID
|
return nil, errInvalidAccessKeyID
|
||||||
}
|
}
|
||||||
cred := u.Credentials
|
cred := u.Credentials
|
||||||
return []byte(cred.SecretKey), nil
|
return []byte(cred.SecretKey), nil
|
||||||
|
} // this means claims.AccessKey == rootAccessKey
|
||||||
|
if !globalAPIConfig.permitRootAccess() {
|
||||||
|
// if root access is disabled, fail this request.
|
||||||
|
return nil, errAccessKeyDisabled
|
||||||
|
}
|
||||||
|
return []byte(globalActiveCred.SecretKey), nil
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return claims, nil, false, errAuthentication
|
return claims, nil, false, errAuthentication
|
||||||
}
|
}
|
||||||
|
@ -173,6 +140,11 @@ func metricsRequestAuthenticate(req *http.Request) (*xjwt.MapClaims, []string, b
|
||||||
claims.MapClaims[k] = v
|
claims.MapClaims[k] = v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if root access is disabled, disable all its service accounts and temporary credentials.
|
||||||
|
if ucred.ParentUser == globalActiveCred.AccessKey && !globalAPIConfig.permitRootAccess() {
|
||||||
|
return nil, nil, false, errAccessKeyDisabled
|
||||||
|
}
|
||||||
|
|
||||||
// Now check if we have a sessionPolicy.
|
// Now check if we have a sessionPolicy.
|
||||||
if _, ok = eclaims[iampolicy.SessionPolicyName]; ok {
|
if _, ok = eclaims[iampolicy.SessionPolicyName]; ok {
|
||||||
owner = false
|
owner = false
|
||||||
|
|
|
@ -25,78 +25,9 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
jwtgo "github.com/golang-jwt/jwt/v4"
|
jwtgo "github.com/golang-jwt/jwt/v4"
|
||||||
"github.com/minio/minio/internal/auth"
|
|
||||||
xjwt "github.com/minio/minio/internal/jwt"
|
xjwt "github.com/minio/minio/internal/jwt"
|
||||||
)
|
)
|
||||||
|
|
||||||
func testAuthenticate(authType string, t *testing.T) {
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
obj, fsDir, err := prepareFS(ctx)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(fsDir)
|
|
||||||
if err = newTestConfig(globalMinioDefaultRegion, obj); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cred, err := auth.GetNewCredentials()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Error getting new credentials: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
globalActiveCred = cred
|
|
||||||
|
|
||||||
// Define test cases.
|
|
||||||
testCases := []struct {
|
|
||||||
accessKey string
|
|
||||||
secretKey string
|
|
||||||
expectedErr error
|
|
||||||
}{
|
|
||||||
// Access key (less than 3 chrs) too small.
|
|
||||||
{"u1", cred.SecretKey, auth.ErrInvalidAccessKeyLength},
|
|
||||||
// Secret key (less than 8 chrs) too small.
|
|
||||||
{cred.AccessKey, "pass", auth.ErrInvalidSecretKeyLength},
|
|
||||||
// Authentication error.
|
|
||||||
{"myuser", "mypassword", errInvalidAccessKeyID},
|
|
||||||
// Authentication error.
|
|
||||||
{cred.AccessKey, "mypassword", errAuthentication},
|
|
||||||
// Success.
|
|
||||||
{cred.AccessKey, cred.SecretKey, nil},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run tests.
|
|
||||||
for _, testCase := range testCases {
|
|
||||||
var err error
|
|
||||||
if authType == "web" {
|
|
||||||
_, err = authenticateWeb(testCase.accessKey, testCase.secretKey)
|
|
||||||
} else if authType == "url" {
|
|
||||||
_, err = authenticateURL(testCase.accessKey, testCase.secretKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
if testCase.expectedErr != nil {
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("%+v: expected: %s, got: <nil>", testCase, testCase.expectedErr)
|
|
||||||
}
|
|
||||||
if testCase.expectedErr.Error() != err.Error() {
|
|
||||||
t.Fatalf("%+v: expected: %s, got: %s", testCase, testCase.expectedErr, err)
|
|
||||||
}
|
|
||||||
} else if err != nil {
|
|
||||||
t.Fatalf("%+v: expected: <nil>, got: %s", testCase, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAuthenticateWeb(t *testing.T) {
|
|
||||||
testAuthenticate("web", t)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAuthenticateURL(t *testing.T) {
|
|
||||||
testAuthenticate("url", t)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getTokenString(accessKey, secretKey string) (string, error) {
|
func getTokenString(accessKey, secretKey string) (string, error) {
|
||||||
claims := xjwt.NewMapClaims()
|
claims := xjwt.NewMapClaims()
|
||||||
claims.SetExpiry(UTCNow().Add(defaultJWTExpiry))
|
claims.SetExpiry(UTCNow().Add(defaultJWTExpiry))
|
||||||
|
@ -258,24 +189,3 @@ func BenchmarkAuthenticateNode(b *testing.B) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkAuthenticateWeb(b *testing.B) {
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
obj, fsDir, err := prepareFS(ctx)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatal(err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(fsDir)
|
|
||||||
if err = newTestConfig(globalMinioDefaultRegion, obj); err != nil {
|
|
||||||
b.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
creds := globalActiveCred
|
|
||||||
b.ResetTimer()
|
|
||||||
b.ReportAllocs()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
authenticateWeb(creds.AccessKey, creds.SecretKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -30,6 +30,7 @@ import (
|
||||||
"github.com/minio/minio/internal/hash/sha256"
|
"github.com/minio/minio/internal/hash/sha256"
|
||||||
xhttp "github.com/minio/minio/internal/http"
|
xhttp "github.com/minio/minio/internal/http"
|
||||||
"github.com/minio/minio/internal/logger"
|
"github.com/minio/minio/internal/logger"
|
||||||
|
iampolicy "github.com/minio/pkg/iam/policy"
|
||||||
)
|
)
|
||||||
|
|
||||||
// http Header "x-amz-content-sha256" == "UNSIGNED-PAYLOAD" indicates that the
|
// http Header "x-amz-content-sha256" == "UNSIGNED-PAYLOAD" indicates that the
|
||||||
|
@ -169,7 +170,16 @@ func checkKeyValid(r *http.Request, accessKey string) (auth.Credentials, bool, A
|
||||||
}
|
}
|
||||||
cred.Claims = claims
|
cred.Claims = claims
|
||||||
|
|
||||||
owner := cred.AccessKey == globalActiveCred.AccessKey
|
owner := cred.AccessKey == globalActiveCred.AccessKey || (cred.ParentUser == globalActiveCred.AccessKey && cred.AccessKey != siteReplicatorSvcAcc)
|
||||||
|
if owner && !globalAPIConfig.permitRootAccess() {
|
||||||
|
// We disable root access and its service accounts if asked for.
|
||||||
|
return cred, owner, ErrAccessKeyDisabled
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := claims[iampolicy.SessionPolicyName]; ok {
|
||||||
|
owner = false
|
||||||
|
}
|
||||||
|
|
||||||
return cred, owner, ErrNone
|
return cred, owner, ErrNone
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -461,6 +461,7 @@ func (c *SiteReplicationSys) AddPeerClusters(ctx context.Context, psites []madmi
|
||||||
svcCred, _, err = globalIAMSys.NewServiceAccount(ctx, sites[selfIdx].AccessKey, nil, newServiceAccountOpts{
|
svcCred, _, err = globalIAMSys.NewServiceAccount(ctx, sites[selfIdx].AccessKey, nil, newServiceAccountOpts{
|
||||||
accessKey: siteReplicatorSvcAcc,
|
accessKey: siteReplicatorSvcAcc,
|
||||||
secretKey: secretKey,
|
secretKey: secretKey,
|
||||||
|
allowSiteReplicatorAccount: true,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return madmin.ReplicateAddStatus{}, errSRServiceAccount(fmt.Errorf("unable to create local service account: %w", err))
|
return madmin.ReplicateAddStatus{}, errSRServiceAccount(fmt.Errorf("unable to create local service account: %w", err))
|
||||||
|
@ -558,8 +559,7 @@ func (c *SiteReplicationSys) AddPeerClusters(ctx context.Context, psites []madmi
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// PeerJoinReq - internal API handler to respond to a peer cluster's request
|
// PeerJoinReq - internal API handler to respond to a peer cluster's request to join.
|
||||||
// to join.
|
|
||||||
func (c *SiteReplicationSys) PeerJoinReq(ctx context.Context, arg madmin.SRPeerJoinReq) error {
|
func (c *SiteReplicationSys) PeerJoinReq(ctx context.Context, arg madmin.SRPeerJoinReq) error {
|
||||||
var ourName string
|
var ourName string
|
||||||
for d, p := range arg.Peers {
|
for d, p := range arg.Peers {
|
||||||
|
@ -577,6 +577,7 @@ func (c *SiteReplicationSys) PeerJoinReq(ctx context.Context, arg madmin.SRPeerJ
|
||||||
_, _, err = globalIAMSys.NewServiceAccount(ctx, arg.SvcAcctParent, nil, newServiceAccountOpts{
|
_, _, err = globalIAMSys.NewServiceAccount(ctx, arg.SvcAcctParent, nil, newServiceAccountOpts{
|
||||||
accessKey: arg.SvcAcctAccessKey,
|
accessKey: arg.SvcAcctAccessKey,
|
||||||
secretKey: arg.SvcAcctSecretKey,
|
secretKey: arg.SvcAcctSecretKey,
|
||||||
|
allowSiteReplicatorAccount: arg.SvcAcctAccessKey == siteReplicatorSvcAcc,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright (c) 2015-2021 MinIO, Inc.
|
// Copyright (c) 2015-2023 MinIO, Inc.
|
||||||
//
|
//
|
||||||
// This file is part of MinIO Object Storage stack
|
// This file is part of MinIO Object Storage stack
|
||||||
//
|
//
|
||||||
|
@ -45,6 +45,7 @@ const (
|
||||||
apiDeleteCleanupInterval = "delete_cleanup_interval"
|
apiDeleteCleanupInterval = "delete_cleanup_interval"
|
||||||
apiDisableODirect = "disable_odirect"
|
apiDisableODirect = "disable_odirect"
|
||||||
apiGzipObjects = "gzip_objects"
|
apiGzipObjects = "gzip_objects"
|
||||||
|
apiRootAccess = "root_access"
|
||||||
|
|
||||||
EnvAPIRequestsMax = "MINIO_API_REQUESTS_MAX"
|
EnvAPIRequestsMax = "MINIO_API_REQUESTS_MAX"
|
||||||
EnvAPIRequestsDeadline = "MINIO_API_REQUESTS_DEADLINE"
|
EnvAPIRequestsDeadline = "MINIO_API_REQUESTS_DEADLINE"
|
||||||
|
@ -61,6 +62,7 @@ const (
|
||||||
EnvDeleteCleanupInterval = "MINIO_DELETE_CLEANUP_INTERVAL"
|
EnvDeleteCleanupInterval = "MINIO_DELETE_CLEANUP_INTERVAL"
|
||||||
EnvAPIDisableODirect = "MINIO_API_DISABLE_ODIRECT"
|
EnvAPIDisableODirect = "MINIO_API_DISABLE_ODIRECT"
|
||||||
EnvAPIGzipObjects = "MINIO_API_GZIP_OBJECTS"
|
EnvAPIGzipObjects = "MINIO_API_GZIP_OBJECTS"
|
||||||
|
EnvAPIRootAccess = "MINIO_API_ROOT_ACCESS" // default "on"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Deprecated key and ENVs
|
// Deprecated key and ENVs
|
||||||
|
@ -130,6 +132,10 @@ var (
|
||||||
Key: apiGzipObjects,
|
Key: apiGzipObjects,
|
||||||
Value: "off",
|
Value: "off",
|
||||||
},
|
},
|
||||||
|
config.KV{
|
||||||
|
Key: apiRootAccess,
|
||||||
|
Value: "on",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -148,6 +154,7 @@ type Config struct {
|
||||||
DeleteCleanupInterval time.Duration `json:"delete_cleanup_interval"`
|
DeleteCleanupInterval time.Duration `json:"delete_cleanup_interval"`
|
||||||
DisableODirect bool `json:"disable_odirect"`
|
DisableODirect bool `json:"disable_odirect"`
|
||||||
GzipObjects bool `json:"gzip_objects"`
|
GzipObjects bool `json:"gzip_objects"`
|
||||||
|
RootAccess bool `json:"root_access"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalJSON - Validate SS and RRS parity when unmarshalling JSON.
|
// UnmarshalJSON - Validate SS and RRS parity when unmarshalling JSON.
|
||||||
|
@ -247,6 +254,7 @@ func LookupConfig(kvs config.KVS) (cfg Config, err error) {
|
||||||
|
|
||||||
disableODirect := env.Get(EnvAPIDisableODirect, kvs.Get(apiDisableODirect)) == config.EnableOn
|
disableODirect := env.Get(EnvAPIDisableODirect, kvs.Get(apiDisableODirect)) == config.EnableOn
|
||||||
gzipObjects := env.Get(EnvAPIGzipObjects, kvs.Get(apiGzipObjects)) == config.EnableOn
|
gzipObjects := env.Get(EnvAPIGzipObjects, kvs.Get(apiGzipObjects)) == config.EnableOn
|
||||||
|
rootAccess := env.Get(EnvAPIRootAccess, kvs.Get(apiRootAccess)) == config.EnableOn
|
||||||
|
|
||||||
return Config{
|
return Config{
|
||||||
RequestsMax: requestsMax,
|
RequestsMax: requestsMax,
|
||||||
|
@ -262,5 +270,6 @@ func LookupConfig(kvs config.KVS) (cfg Config, err error) {
|
||||||
DeleteCleanupInterval: deleteCleanupInterval,
|
DeleteCleanupInterval: deleteCleanupInterval,
|
||||||
DisableODirect: disableODirect,
|
DisableODirect: disableODirect,
|
||||||
GzipObjects: gzipObjects,
|
GzipObjects: gzipObjects,
|
||||||
|
RootAccess: rootAccess,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright (c) 2015-2021 MinIO, Inc.
|
// Copyright (c) 2015-2023 MinIO, Inc.
|
||||||
//
|
//
|
||||||
// This file is part of MinIO Object Storage stack
|
// This file is part of MinIO Object Storage stack
|
||||||
//
|
//
|
||||||
|
@ -94,7 +94,13 @@ var (
|
||||||
},
|
},
|
||||||
config.HelpKV{
|
config.HelpKV{
|
||||||
Key: apiDisableODirect,
|
Key: apiDisableODirect,
|
||||||
Description: "set to disable O_DIRECT for reads under special conditions. NOTE: it is not recommended to disable O_DIRECT without prior testing." + defaultHelpPostfix(apiDisableODirect),
|
Description: "set to disable O_DIRECT for reads under special conditions. NOTE: it is not recommended to disable O_DIRECT without prior testing" + defaultHelpPostfix(apiDisableODirect),
|
||||||
|
Optional: true,
|
||||||
|
Type: "boolean",
|
||||||
|
},
|
||||||
|
config.HelpKV{
|
||||||
|
Key: apiRootAccess,
|
||||||
|
Description: "turn 'off' root credential access for all API calls including s3, admin operations" + defaultHelpPostfix(apiRootAccess),
|
||||||
Optional: true,
|
Optional: true,
|
||||||
Type: "boolean",
|
Type: "boolean",
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue