mirror of
https://github.com/minio/minio.git
synced 2024-12-24 06:05:55 -05:00
allow root user to be disabled via config settings (#17089)
This commit is contained in:
parent
701b89f377
commit
7ae69accc0
34
.github/workflows/root-disable.yml
vendored
Normal file
34
.github/workflows/root-disable.yml
vendored
Normal file
@ -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"
|
||||
@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
|
||||
@echo "Running minio decom tests"
|
||||
@env bash $(PWD)/docs/distributed/decom.sh
|
||||
|
119
buildscripts/disable-root.sh
Executable file
119
buildscripts/disable-root.sh
Executable file
@ -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))
|
||||
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.DeleteUserAdminAction)
|
||||
objectAPI, cred := validateAdminReq(ctx, w, r, iampolicy.DeleteUserAdminAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
@ -62,6 +62,13 @@ func (a adminAPIHandlers) RemoveUser(w http.ResponseWriter, r *http.Request) {
|
||||
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 {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
@ -239,6 +246,26 @@ func (a adminAPIHandlers) UpdateGroupMembers(w http.ResponseWriter, r *http.Requ
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
||||
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
|
||||
if updReq.IsRemove {
|
||||
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))
|
||||
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.EnableUserAdminAction)
|
||||
objectAPI, creds := validateAdminReq(ctx, w, r, iampolicy.EnableUserAdminAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
@ -383,9 +410,9 @@ func (a adminAPIHandlers) SetUserStatus(w http.ResponseWriter, r *http.Request)
|
||||
accessKey := vars["accessKey"]
|
||||
status := vars["status"]
|
||||
|
||||
// This API is not allowed to lookup master access key user status
|
||||
if accessKey == globalActiveCred.AccessKey {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
||||
// you cannot enable or disable yourself.
|
||||
if accessKey == creds.AccessKey {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errInvalidArgument), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
@ -1627,6 +1654,12 @@ func (a adminAPIHandlers) SetPolicyForUserOrGroup(w http.ResponseWriter, r *http
|
||||
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 entityName == globalActiveCred.AccessKey {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Validate that user or group exists.
|
||||
@ -1771,6 +1804,13 @@ func (a adminAPIHandlers) AttachPolicyBuiltin(w http.ResponseWriter, r *http.Req
|
||||
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.
|
||||
if globalIAMSys.GetUsersSysType() == MinIOUsersSysType {
|
||||
_, 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)
|
||||
for u, uid := range userIdentities {
|
||||
status := madmin.AccountDisabled
|
||||
if uid.Credentials.IsValid() {
|
||||
status = madmin.AccountEnabled
|
||||
}
|
||||
userAccounts[u] = madmin.AddOrUpdateUserReq{
|
||||
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)
|
||||
@ -2101,6 +2143,10 @@ func (a adminAPIHandlers) ExportIAM(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
svcAccts := make(map[string]madmin.SRSvcAccCreate)
|
||||
for user, acc := range serviceAccounts {
|
||||
if user == siteReplicatorSvcAcc {
|
||||
// skip site-replication service account.
|
||||
continue
|
||||
}
|
||||
claims, err := globalIAMSys.GetClaimsForSvcAcc(ctx, acc.Credentials.AccessKey)
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
|
||||
@ -2461,12 +2507,13 @@ func (a adminAPIHandlers) ImportIAM(w http.ResponseWriter, r *http.Request) {
|
||||
continue
|
||||
}
|
||||
opts := newServiceAccountOpts{
|
||||
accessKey: user,
|
||||
secretKey: svcAcctReq.SecretKey,
|
||||
sessionPolicy: sp,
|
||||
claims: svcAcctReq.Claims,
|
||||
comment: svcAcctReq.Comment,
|
||||
expiration: svcAcctReq.Expiration,
|
||||
accessKey: user,
|
||||
secretKey: svcAcctReq.SecretKey,
|
||||
sessionPolicy: sp,
|
||||
claims: svcAcctReq.Claims,
|
||||
comment: svcAcctReq.Comment,
|
||||
expiration: svcAcctReq.Expiration,
|
||||
allowSiteReplicatorAccount: false,
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
globalIAMSys.Init(ctx, objLayer, globalEtcdClient, 2*time.Second)
|
||||
|
||||
creds, err := auth.CreateCredentials("myuser", "mypassword")
|
||||
if err != nil {
|
||||
t.Fatalf("unable create credential, %s", err)
|
||||
@ -384,6 +382,8 @@ func TestIsReqAuthenticated(t *testing.T) {
|
||||
|
||||
globalActiveCred = creds
|
||||
|
||||
globalIAMSys.Init(ctx, objLayer, globalEtcdClient, 2*time.Second)
|
||||
|
||||
// List of test cases for validating http request authentication.
|
||||
testCases := []struct {
|
||||
req *http.Request
|
||||
@ -464,9 +464,8 @@ func TestValidateAdminSignature(t *testing.T) {
|
||||
}
|
||||
|
||||
initAllSubsystems(ctx)
|
||||
initConfigSubsystem(ctx, objLayer)
|
||||
|
||||
globalIAMSys.Init(ctx, objLayer, globalEtcdClient, 2*time.Second)
|
||||
initConfigSubsystem(ctx, objLayer)
|
||||
|
||||
creds, err := auth.CreateCredentials("admin", "mypassword")
|
||||
if err != nil {
|
||||
@ -474,6 +473,8 @@ func TestValidateAdminSignature(t *testing.T) {
|
||||
}
|
||||
globalActiveCred = creds
|
||||
|
||||
globalIAMSys.Init(ctx, objLayer, globalEtcdClient, 2*time.Second)
|
||||
|
||||
testCases := []struct {
|
||||
AccessKey string
|
||||
SecretKey string
|
||||
|
@ -197,7 +197,7 @@ var (
|
||||
globalBucketTargetSys *BucketTargetSys
|
||||
// globalAPIConfig controls S3 API requests throttling,
|
||||
// healthcheck readiness deadlines and cors settings.
|
||||
globalAPIConfig = apiConfig{listQuorum: "strict"}
|
||||
globalAPIConfig = apiConfig{listQuorum: "strict", rootAccess: true}
|
||||
|
||||
globalStorageClass storageclass.Config
|
||||
|
||||
|
@ -51,6 +51,7 @@ type apiConfig struct {
|
||||
deleteCleanupInterval time.Duration
|
||||
disableODirect bool
|
||||
gzipObjects bool
|
||||
rootAccess bool
|
||||
}
|
||||
|
||||
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.disableODirect = cfg.DisableODirect
|
||||
t.gzipObjects = cfg.GzipObjects
|
||||
t.rootAccess = cfg.RootAccess
|
||||
}
|
||||
|
||||
func (t *apiConfig) isDisableODirect() bool {
|
||||
@ -168,6 +170,13 @@ func (t *apiConfig) shouldGzipObjects() bool {
|
||||
return t.gzipObjects
|
||||
}
|
||||
|
||||
func (t *apiConfig) permitRootAccess() bool {
|
||||
t.mu.RLock()
|
||||
defer t.mu.RUnlock()
|
||||
|
||||
return t.rootAccess
|
||||
}
|
||||
|
||||
func (t *apiConfig) getListQuorum() string {
|
||||
t.mu.RLock()
|
||||
defer t.mu.RUnlock()
|
||||
|
15
cmd/iam.go
15
cmd/iam.go
@ -914,11 +914,12 @@ func (sys *IAMSys) notifyForServiceAccount(ctx context.Context, accessKey string
|
||||
}
|
||||
|
||||
type newServiceAccountOpts struct {
|
||||
sessionPolicy *iampolicy.Policy
|
||||
accessKey string
|
||||
secretKey string
|
||||
comment string
|
||||
expiration *time.Time
|
||||
sessionPolicy *iampolicy.Policy
|
||||
accessKey string
|
||||
secretKey string
|
||||
comment string
|
||||
expiration *time.Time
|
||||
allowSiteReplicatorAccount bool // allow creating internal service account for site-replication.
|
||||
|
||||
claims map[string]interface{}
|
||||
}
|
||||
@ -953,7 +954,9 @@ func (sys *IAMSys) NewServiceAccount(ctx context.Context, parentUser string, gro
|
||||
if parentUser == opts.accessKey {
|
||||
return auth.Credentials{}, time.Time{}, errIAMActionNotAllowed
|
||||
}
|
||||
|
||||
if siteReplicatorSvcAcc == opts.accessKey && !opts.allowSiteReplicatorAccount {
|
||||
return auth.Credentials{}, time.Time{}, errIAMActionNotAllowed
|
||||
}
|
||||
m := make(map[string]interface{})
|
||||
m[parentClaim] = parentUser
|
||||
|
||||
|
74
cmd/jwt.go
74
cmd/jwt.go
@ -18,7 +18,6 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
@ -40,48 +39,15 @@ const (
|
||||
|
||||
// Inter-node JWT token expiry is 15 minutes.
|
||||
defaultInterNodeJWTExpiry = 15 * time.Minute
|
||||
|
||||
// URL JWT token expiry is one minute (might be exposed).
|
||||
defaultURLJWTExpiry = time.Minute
|
||||
)
|
||||
|
||||
var (
|
||||
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")
|
||||
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.
|
||||
func cachedAuthenticateNode(ttl time.Duration) func(accessKey, secretKey, audience string) (string, error) {
|
||||
type key struct {
|
||||
@ -121,14 +87,6 @@ func authenticateNode(accessKey, secretKey, audience string) (string, error) {
|
||||
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.
|
||||
// Returns nil if the request is authenticated. errNoAuthToken if token missing.
|
||||
// Returns errAuthentication for all other errors.
|
||||
@ -142,15 +100,24 @@ func metricsRequestAuthenticate(req *http.Request) (*xjwt.MapClaims, []string, b
|
||||
}
|
||||
claims := xjwt.NewMapClaims()
|
||||
if err := xjwt.ParseWithClaims(token, claims, func(claims *xjwt.MapClaims) ([]byte, error) {
|
||||
if claims.AccessKey == globalActiveCred.AccessKey {
|
||||
return []byte(globalActiveCred.SecretKey), nil
|
||||
if claims.AccessKey != globalActiveCred.AccessKey {
|
||||
u, ok := globalIAMSys.GetUser(req.Context(), claims.AccessKey)
|
||||
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
|
||||
}
|
||||
cred := u.Credentials
|
||||
return []byte(cred.SecretKey), nil
|
||||
} // this means claims.AccessKey == rootAccessKey
|
||||
if !globalAPIConfig.permitRootAccess() {
|
||||
// if root access is disabled, fail this request.
|
||||
return nil, errAccessKeyDisabled
|
||||
}
|
||||
u, ok := globalIAMSys.GetUser(req.Context(), claims.AccessKey)
|
||||
if !ok {
|
||||
return nil, errInvalidAccessKeyID
|
||||
}
|
||||
cred := u.Credentials
|
||||
return []byte(cred.SecretKey), nil
|
||||
return []byte(globalActiveCred.SecretKey), nil
|
||||
}); err != nil {
|
||||
return claims, nil, false, errAuthentication
|
||||
}
|
||||
@ -173,6 +140,11 @@ func metricsRequestAuthenticate(req *http.Request) (*xjwt.MapClaims, []string, b
|
||||
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.
|
||||
if _, ok = eclaims[iampolicy.SessionPolicyName]; ok {
|
||||
owner = false
|
||||
|
@ -25,78 +25,9 @@ import (
|
||||
"time"
|
||||
|
||||
jwtgo "github.com/golang-jwt/jwt/v4"
|
||||
"github.com/minio/minio/internal/auth"
|
||||
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) {
|
||||
claims := xjwt.NewMapClaims()
|
||||
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"
|
||||
xhttp "github.com/minio/minio/internal/http"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
iampolicy "github.com/minio/pkg/iam/policy"
|
||||
)
|
||||
|
||||
// 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
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -459,8 +459,9 @@ func (c *SiteReplicationSys) AddPeerClusters(ctx context.Context, psites []madmi
|
||||
return madmin.ReplicateAddStatus{}, errSRServiceAccount(fmt.Errorf("unable to create local service account: %w", err))
|
||||
}
|
||||
svcCred, _, err = globalIAMSys.NewServiceAccount(ctx, sites[selfIdx].AccessKey, nil, newServiceAccountOpts{
|
||||
accessKey: siteReplicatorSvcAcc,
|
||||
secretKey: secretKey,
|
||||
accessKey: siteReplicatorSvcAcc,
|
||||
secretKey: secretKey,
|
||||
allowSiteReplicatorAccount: true,
|
||||
})
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
// PeerJoinReq - internal API handler to respond to a peer cluster's request
|
||||
// to join.
|
||||
// PeerJoinReq - internal API handler to respond to a peer cluster's request to join.
|
||||
func (c *SiteReplicationSys) PeerJoinReq(ctx context.Context, arg madmin.SRPeerJoinReq) error {
|
||||
var ourName string
|
||||
for d, p := range arg.Peers {
|
||||
@ -575,8 +575,9 @@ func (c *SiteReplicationSys) PeerJoinReq(ctx context.Context, arg madmin.SRPeerJ
|
||||
_, _, err := globalIAMSys.GetServiceAccount(ctx, arg.SvcAcctAccessKey)
|
||||
if err == errNoSuchServiceAccount {
|
||||
_, _, err = globalIAMSys.NewServiceAccount(ctx, arg.SvcAcctParent, nil, newServiceAccountOpts{
|
||||
accessKey: arg.SvcAcctAccessKey,
|
||||
secretKey: arg.SvcAcctSecretKey,
|
||||
accessKey: arg.SvcAcctAccessKey,
|
||||
secretKey: arg.SvcAcctSecretKey,
|
||||
allowSiteReplicatorAccount: arg.SvcAcctAccessKey == siteReplicatorSvcAcc,
|
||||
})
|
||||
}
|
||||
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
|
||||
//
|
||||
@ -45,6 +45,7 @@ const (
|
||||
apiDeleteCleanupInterval = "delete_cleanup_interval"
|
||||
apiDisableODirect = "disable_odirect"
|
||||
apiGzipObjects = "gzip_objects"
|
||||
apiRootAccess = "root_access"
|
||||
|
||||
EnvAPIRequestsMax = "MINIO_API_REQUESTS_MAX"
|
||||
EnvAPIRequestsDeadline = "MINIO_API_REQUESTS_DEADLINE"
|
||||
@ -61,6 +62,7 @@ const (
|
||||
EnvDeleteCleanupInterval = "MINIO_DELETE_CLEANUP_INTERVAL"
|
||||
EnvAPIDisableODirect = "MINIO_API_DISABLE_ODIRECT"
|
||||
EnvAPIGzipObjects = "MINIO_API_GZIP_OBJECTS"
|
||||
EnvAPIRootAccess = "MINIO_API_ROOT_ACCESS" // default "on"
|
||||
)
|
||||
|
||||
// Deprecated key and ENVs
|
||||
@ -130,6 +132,10 @@ var (
|
||||
Key: apiGzipObjects,
|
||||
Value: "off",
|
||||
},
|
||||
config.KV{
|
||||
Key: apiRootAccess,
|
||||
Value: "on",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@ -148,6 +154,7 @@ type Config struct {
|
||||
DeleteCleanupInterval time.Duration `json:"delete_cleanup_interval"`
|
||||
DisableODirect bool `json:"disable_odirect"`
|
||||
GzipObjects bool `json:"gzip_objects"`
|
||||
RootAccess bool `json:"root_access"`
|
||||
}
|
||||
|
||||
// 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
|
||||
gzipObjects := env.Get(EnvAPIGzipObjects, kvs.Get(apiGzipObjects)) == config.EnableOn
|
||||
rootAccess := env.Get(EnvAPIRootAccess, kvs.Get(apiRootAccess)) == config.EnableOn
|
||||
|
||||
return Config{
|
||||
RequestsMax: requestsMax,
|
||||
@ -262,5 +270,6 @@ func LookupConfig(kvs config.KVS) (cfg Config, err error) {
|
||||
DeleteCleanupInterval: deleteCleanupInterval,
|
||||
DisableODirect: disableODirect,
|
||||
GzipObjects: gzipObjects,
|
||||
RootAccess: rootAccess,
|
||||
}, 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
|
||||
//
|
||||
@ -94,7 +94,13 @@ var (
|
||||
},
|
||||
config.HelpKV{
|
||||
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,
|
||||
Type: "boolean",
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user