mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-18 04:25:52 +00:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcd5bcadcf | ||
|
|
4eb45ecc5e | ||
|
|
1f3df6e9ef | ||
|
|
fcd5de9710 | ||
|
|
b6f6f0187e |
@@ -51,7 +51,7 @@ metadata:
|
||||
annotations:
|
||||
"helm.sh/hook": post-install,post-upgrade
|
||||
"helm.sh/hook-weight": "-5"
|
||||
"helm.sh/hook-delete-policy": hook-succeeded
|
||||
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
|
||||
spec:
|
||||
template:
|
||||
metadata:
|
||||
@@ -92,6 +92,7 @@ spec:
|
||||
- "/bin/sh"
|
||||
- "-ec"
|
||||
- |
|
||||
set -o pipefail
|
||||
wait_for_service() {
|
||||
local url=$1
|
||||
local max_attempts=60 # 5 minutes total (5s * 60)
|
||||
@@ -117,8 +118,7 @@ spec:
|
||||
wait_for_service "http://$WEED_CLUSTER_SW_MASTER{{ .Values.master.readinessProbe.httpGet.path }}"
|
||||
wait_for_service "http://$WEED_CLUSTER_SW_FILER{{ .Values.filer.readinessProbe.httpGet.path }}"
|
||||
{{- end }}
|
||||
set -o pipefail
|
||||
{{- range $createBuckets }}
|
||||
{{- range $createBuckets }}
|
||||
{{- $bucketName := .name }}
|
||||
{{- $bucketLock := or .lock .objectLock .withLock }}
|
||||
bucket_list=$(/bin/echo 's3.bucket.list' | /usr/bin/weed shell) || { echo "Error listing s3 buckets"; exit 1; }
|
||||
|
||||
@@ -58,7 +58,6 @@ templ Layout(view ViewContext, content templ.Component) {
|
||||
<a class="navbar-brand fw-bold" href="/admin">
|
||||
<i class="fas fa-server me-2"></i>
|
||||
SeaweedFS Admin
|
||||
<span class="badge bg-warning text-dark ms-2">ALPHA</span>
|
||||
</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
@@ -245,19 +244,6 @@ templ Layout(view ViewContext, content templ.Component) {
|
||||
</div>
|
||||
}
|
||||
</li>
|
||||
<!-- Commented out for later -->
|
||||
<!--
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/metrics">
|
||||
<i class="fas fa-chart-line me-2"></i>Metrics
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/logs">
|
||||
<i class="fas fa-file-alt me-2"></i>Logs
|
||||
</a>
|
||||
</li>
|
||||
-->
|
||||
</ul>
|
||||
|
||||
<h6 class="sidebar-heading px-3 mt-4 mb-1 text-muted">
|
||||
|
||||
@@ -71,14 +71,14 @@ func Layout(view ViewContext, content templ.Component) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\"><link rel=\"icon\" href=\"/static/favicon.ico\" type=\"image/x-icon\"><!-- Bootstrap CSS --><link href=\"/static/css/bootstrap.min.css\" rel=\"stylesheet\"><!-- Font Awesome CSS --><link href=\"/static/css/fontawesome.min.css\" rel=\"stylesheet\"><!-- HTMX --><script src=\"/static/js/htmx.min.js\"></script><!-- Custom CSS --><link rel=\"stylesheet\" href=\"/static/css/admin.css\"></head><body><div class=\"container-fluid p-0\"><!-- Header --><header class=\"navbar navbar-expand-lg navbar-dark bg-primary sticky-top\"><div class=\"container-fluid\"><a class=\"navbar-brand fw-bold\" href=\"/admin\"><i class=\"fas fa-server me-2\"></i> SeaweedFS Admin <span class=\"badge bg-warning text-dark ms-2\">ALPHA</span></a> <button class=\"navbar-toggler\" type=\"button\" data-bs-toggle=\"collapse\" data-bs-target=\"#navbarNav\"><span class=\"navbar-toggler-icon\"></span></button><div class=\"collapse navbar-collapse\" id=\"navbarNav\"><ul class=\"navbar-nav ms-auto\"><li class=\"nav-item dropdown\"><a class=\"nav-link dropdown-toggle\" href=\"#\" role=\"button\" data-bs-toggle=\"dropdown\"><i class=\"fas fa-user me-1\"></i>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\"><link rel=\"icon\" href=\"/static/favicon.ico\" type=\"image/x-icon\"><!-- Bootstrap CSS --><link href=\"/static/css/bootstrap.min.css\" rel=\"stylesheet\"><!-- Font Awesome CSS --><link href=\"/static/css/fontawesome.min.css\" rel=\"stylesheet\"><!-- HTMX --><script src=\"/static/js/htmx.min.js\"></script><!-- Custom CSS --><link rel=\"stylesheet\" href=\"/static/css/admin.css\"></head><body><div class=\"container-fluid p-0\"><!-- Header --><header class=\"navbar navbar-expand-lg navbar-dark bg-primary sticky-top\"><div class=\"container-fluid\"><a class=\"navbar-brand fw-bold\" href=\"/admin\"><i class=\"fas fa-server me-2\"></i> SeaweedFS Admin</a> <button class=\"navbar-toggler\" type=\"button\" data-bs-toggle=\"collapse\" data-bs-target=\"#navbarNav\"><span class=\"navbar-toggler-icon\"></span></button><div class=\"collapse navbar-collapse\" id=\"navbarNav\"><ul class=\"navbar-nav ms-auto\"><li class=\"nav-item dropdown\"><a class=\"nav-link dropdown-toggle\" href=\"#\" role=\"button\" data-bs-toggle=\"dropdown\"><i class=\"fas fa-user me-1\"></i>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(username)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 72, Col: 73}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 71, Col: 73}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -113,7 +113,7 @@ func Layout(view ViewContext, content templ.Component) templ.Component {
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%t", isClusterPage))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 99, Col: 207}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 98, Col: 207}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -170,7 +170,7 @@ func Layout(view ViewContext, content templ.Component) templ.Component {
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%t", isStoragePage))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 124, Col: 207}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 123, Col: 207}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -254,7 +254,7 @@ func Layout(view ViewContext, content templ.Component) templ.Component {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</li><!-- Commented out for later --><!--\n <li class=\"nav-item\">\n <a class=\"nav-link\" href=\"/metrics\">\n <i class=\"fas fa-chart-line me-2\"></i>Metrics\n </a>\n </li>\n <li class=\"nav-item\">\n <a class=\"nav-link\" href=\"/logs\">\n <i class=\"fas fa-file-alt me-2\"></i>Logs\n </a>\n </li>\n --></ul><h6 class=\"sidebar-heading px-3 mt-4 mb-1 text-muted\"><span>WORKERS</span></h6><ul class=\"nav flex-column\"><li class=\"nav-item\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</li></ul><h6 class=\"sidebar-heading px-3 mt-4 mb-1 text-muted\"><span>WORKERS</span></h6><ul class=\"nav flex-column\"><li class=\"nav-item\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -344,7 +344,7 @@ func Layout(view ViewContext, content templ.Component) templ.Component {
|
||||
var templ_7745c5c3_Var14 string
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", time.Now().Year()))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 339, Col: 60}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 325, Col: 60}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -357,7 +357,7 @@ func Layout(view ViewContext, content templ.Component) templ.Component {
|
||||
var templ_7745c5c3_Var15 string
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(version.VERSION_NUMBER)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 339, Col: 102}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 325, Col: 102}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -409,7 +409,7 @@ func LoginForm(title string, errorMessage string, csrfToken string) templ.Compon
|
||||
var templ_7745c5c3_Var17 string
|
||||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 367, Col: 17}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 353, Col: 17}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -422,7 +422,7 @@ func LoginForm(title string, errorMessage string, csrfToken string) templ.Compon
|
||||
var templ_7745c5c3_Var18 string
|
||||
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 381, Col: 57}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 367, Col: 57}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -440,7 +440,7 @@ func LoginForm(title string, errorMessage string, csrfToken string) templ.Compon
|
||||
var templ_7745c5c3_Var19 string
|
||||
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(errorMessage)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 388, Col: 45}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 374, Col: 45}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -458,7 +458,7 @@ func LoginForm(title string, errorMessage string, csrfToken string) templ.Compon
|
||||
var templ_7745c5c3_Var20 string
|
||||
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(csrfToken)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 393, Col: 84}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 379, Col: 84}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -2,6 +2,7 @@ package s3api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -21,6 +22,7 @@ import (
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/s3api/policy_engine"
|
||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util/wildcard"
|
||||
@@ -48,12 +50,14 @@ type IdentityAccessManagement struct {
|
||||
accessKeyIdent map[string]*Identity
|
||||
nameToIdentity map[string]*Identity // O(1) lookup by identity name
|
||||
policies map[string]*iam_pb.Policy
|
||||
iamPolicyCache map[iamPolicyCacheKey]*policy_engine.PolicyEngine
|
||||
accounts map[string]*Account
|
||||
emailAccount map[string]*Account
|
||||
hashes map[string]*sync.Pool
|
||||
hashCounters map[string]*int32
|
||||
identityAnonymous *Identity
|
||||
hashMu sync.RWMutex
|
||||
policyCacheMu sync.RWMutex
|
||||
domain string
|
||||
externalHost string // pre-computed host for S3 signature verification (from ExternalUrl)
|
||||
isAuthEnabled bool
|
||||
@@ -131,6 +135,11 @@ type Credential struct {
|
||||
Expiration int64 // Unix timestamp when credential expires (0 = no expiration)
|
||||
}
|
||||
|
||||
type iamPolicyCacheKey struct {
|
||||
name string
|
||||
contentHash [32]byte
|
||||
}
|
||||
|
||||
// isCredentialExpired checks if a credential has expired
|
||||
func (c *Credential) isCredentialExpired() bool {
|
||||
return c.Expiration > 0 && c.Expiration < time.Now().Unix()
|
||||
@@ -205,11 +214,12 @@ func NewIdentityAccessManagementWithStore(option *S3ApiServerOption, filerClient
|
||||
}
|
||||
|
||||
iam := &IdentityAccessManagement{
|
||||
domain: option.DomainName,
|
||||
externalHost: externalHost,
|
||||
hashes: make(map[string]*sync.Pool),
|
||||
hashCounters: make(map[string]*int32),
|
||||
filerClient: filerClient,
|
||||
domain: option.DomainName,
|
||||
externalHost: externalHost,
|
||||
hashes: make(map[string]*sync.Pool),
|
||||
hashCounters: make(map[string]*int32),
|
||||
iamPolicyCache: make(map[iamPolicyCacheKey]*policy_engine.PolicyEngine),
|
||||
filerClient: filerClient,
|
||||
}
|
||||
|
||||
// Always initialize credential manager with fallback to defaults
|
||||
@@ -683,6 +693,7 @@ func (iam *IdentityAccessManagement) ReplaceS3ApiConfiguration(config *iam_pb.S3
|
||||
// Once enabled, keep it enabled (one-way toggle)
|
||||
authJustEnabled := iam.updateAuthenticationState(len(iam.identities))
|
||||
iam.m.Unlock()
|
||||
iam.clearIAMPolicyCache()
|
||||
|
||||
if authJustEnabled {
|
||||
glog.V(1).Infof("S3 authentication enabled - credentials were added dynamically")
|
||||
@@ -918,6 +929,7 @@ func (iam *IdentityAccessManagement) MergeS3ApiConfiguration(config *iam_pb.S3Ap
|
||||
// Once enabled, keep it enabled (one-way toggle)
|
||||
authJustEnabled := iam.updateAuthenticationState(len(identities))
|
||||
iam.m.Unlock()
|
||||
iam.clearIAMPolicyCache()
|
||||
|
||||
if authJustEnabled {
|
||||
glog.V(1).Infof("S3 authentication enabled because credentials were added dynamically")
|
||||
@@ -1658,6 +1670,104 @@ func determineIAMAuthPath(sessionToken, principal, principalArn string) iamAuthP
|
||||
return iamAuthPathNone
|
||||
}
|
||||
|
||||
func newIAMPolicyCacheKey(name, content string) iamPolicyCacheKey {
|
||||
return iamPolicyCacheKey{
|
||||
name: name,
|
||||
contentHash: sha256.Sum256([]byte(content)),
|
||||
}
|
||||
}
|
||||
|
||||
func (iam *IdentityAccessManagement) getOrCreateIAMPolicyEngine(policyName, content string) (*policy_engine.PolicyEngine, error) {
|
||||
cacheKey := newIAMPolicyCacheKey(policyName, content)
|
||||
|
||||
iam.policyCacheMu.RLock()
|
||||
if engine, ok := iam.iamPolicyCache[cacheKey]; ok {
|
||||
iam.policyCacheMu.RUnlock()
|
||||
return engine, nil
|
||||
}
|
||||
iam.policyCacheMu.RUnlock()
|
||||
|
||||
engine := policy_engine.NewPolicyEngine()
|
||||
if err := engine.SetBucketPolicy(policyName, content); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
iam.policyCacheMu.Lock()
|
||||
defer iam.policyCacheMu.Unlock()
|
||||
|
||||
if engine, ok := iam.iamPolicyCache[cacheKey]; ok {
|
||||
return engine, nil
|
||||
}
|
||||
if iam.iamPolicyCache == nil {
|
||||
iam.iamPolicyCache = make(map[iamPolicyCacheKey]*policy_engine.PolicyEngine)
|
||||
}
|
||||
iam.iamPolicyCache[cacheKey] = engine
|
||||
return engine, nil
|
||||
}
|
||||
|
||||
func (iam *IdentityAccessManagement) invalidateIAMPolicyCache(policyName string) {
|
||||
iam.policyCacheMu.Lock()
|
||||
defer iam.policyCacheMu.Unlock()
|
||||
|
||||
for cacheKey := range iam.iamPolicyCache {
|
||||
if cacheKey.name == policyName {
|
||||
delete(iam.iamPolicyCache, cacheKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (iam *IdentityAccessManagement) clearIAMPolicyCache() {
|
||||
iam.policyCacheMu.Lock()
|
||||
defer iam.policyCacheMu.Unlock()
|
||||
iam.iamPolicyCache = make(map[iamPolicyCacheKey]*policy_engine.PolicyEngine)
|
||||
}
|
||||
|
||||
// evaluateIAMPolicies evaluates attached IAM policies for a user identity.
|
||||
// Returns true if any matching statement explicitly allows the action.
|
||||
func (iam *IdentityAccessManagement) evaluateIAMPolicies(r *http.Request, identity *Identity, action Action, bucket, object string) bool {
|
||||
if identity == nil || len(identity.PolicyNames) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
resource := buildResourceARN(bucket, object)
|
||||
principal := buildPrincipalARN(identity, r)
|
||||
s3Action := ResolveS3Action(r, string(action), bucket, object)
|
||||
explicitAllow := false
|
||||
conditions := policy_engine.ExtractConditionValuesFromRequest(r)
|
||||
for k, v := range policy_engine.ExtractPrincipalVariables(principal) {
|
||||
conditions[k] = v
|
||||
}
|
||||
|
||||
for _, policyName := range identity.PolicyNames {
|
||||
policy, err := iam.GetPolicy(policyName)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
engine, err := iam.getOrCreateIAMPolicyEngine(policyName, policy.Content)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
result := engine.EvaluatePolicy(policyName, &policy_engine.PolicyEvaluationArgs{
|
||||
Action: s3Action,
|
||||
Resource: resource,
|
||||
Principal: principal,
|
||||
Conditions: conditions,
|
||||
Claims: identity.Claims,
|
||||
})
|
||||
|
||||
if result == policy_engine.PolicyResultDeny {
|
||||
return false
|
||||
}
|
||||
if result == policy_engine.PolicyResultAllow {
|
||||
explicitAllow = true
|
||||
}
|
||||
}
|
||||
|
||||
return explicitAllow
|
||||
}
|
||||
|
||||
// VerifyActionPermission checks if the identity is allowed to perform the action on the resource.
|
||||
// It handles both traditional identities (via Actions) and IAM/STS identities (via Policy).
|
||||
func (iam *IdentityAccessManagement) VerifyActionPermission(r *http.Request, identity *Identity, action Action, bucket, object string) s3err.ErrorCode {
|
||||
@@ -1679,6 +1789,7 @@ func (iam *IdentityAccessManagement) VerifyActionPermission(r *http.Request, ide
|
||||
return iam.authorizeWithIAM(r, identity, action, bucket, object)
|
||||
}
|
||||
|
||||
// Traditional actions-based authorization from static S3 config.
|
||||
if len(identity.Actions) > 0 {
|
||||
if !identity.CanDo(action, bucket, object) {
|
||||
return s3err.ErrAccessDenied
|
||||
@@ -1686,6 +1797,14 @@ func (iam *IdentityAccessManagement) VerifyActionPermission(r *http.Request, ide
|
||||
return s3err.ErrNone
|
||||
}
|
||||
|
||||
// IAM policy fallback for identities with attached policies but without IAM integration.
|
||||
if len(identity.PolicyNames) > 0 {
|
||||
if iam.evaluateIAMPolicies(r, identity, action, bucket, object) {
|
||||
return s3err.ErrNone
|
||||
}
|
||||
return s3err.ErrAccessDenied
|
||||
}
|
||||
|
||||
return s3err.ErrAccessDenied
|
||||
}
|
||||
|
||||
@@ -1747,11 +1866,13 @@ func (iam *IdentityAccessManagement) authorizeWithIAM(r *http.Request, identity
|
||||
// PutPolicy adds or updates a policy
|
||||
func (iam *IdentityAccessManagement) PutPolicy(name string, content string) error {
|
||||
iam.m.Lock()
|
||||
defer iam.m.Unlock()
|
||||
if iam.policies == nil {
|
||||
iam.policies = make(map[string]*iam_pb.Policy)
|
||||
}
|
||||
iam.policies[name] = &iam_pb.Policy{Name: name, Content: content}
|
||||
iam.m.Unlock()
|
||||
|
||||
iam.invalidateIAMPolicyCache(name)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1768,8 +1889,10 @@ func (iam *IdentityAccessManagement) GetPolicy(name string) (*iam_pb.Policy, err
|
||||
// DeletePolicy removes a policy
|
||||
func (iam *IdentityAccessManagement) DeletePolicy(name string) error {
|
||||
iam.m.Lock()
|
||||
defer iam.m.Unlock()
|
||||
delete(iam.policies, name)
|
||||
iam.m.Unlock()
|
||||
|
||||
iam.invalidateIAMPolicyCache(name)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package s3api
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"reflect"
|
||||
"sync"
|
||||
@@ -9,6 +11,7 @@ import (
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/credential"
|
||||
. "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util/wildcard"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
@@ -260,6 +263,260 @@ func TestMatchWildcardPattern(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyActionPermissionPolicyFallback(t *testing.T) {
|
||||
buildRequest := func(t *testing.T, method string) *http.Request {
|
||||
t.Helper()
|
||||
req, err := http.NewRequest(method, "http://s3.amazonaws.com/test-bucket/test-object", nil)
|
||||
assert.NoError(t, err)
|
||||
return req
|
||||
}
|
||||
|
||||
t.Run("policy allow grants access", func(t *testing.T) {
|
||||
iam := &IdentityAccessManagement{}
|
||||
err := iam.PutPolicy("allowGet", `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"arn:aws:s3:::test-bucket/*"}]}`)
|
||||
assert.NoError(t, err)
|
||||
|
||||
identity := &Identity{
|
||||
Name: "policy-user",
|
||||
Account: &AccountAdmin,
|
||||
PolicyNames: []string{"allowGet"},
|
||||
}
|
||||
|
||||
errCode := iam.VerifyActionPermission(buildRequest(t, http.MethodGet), identity, Action(ACTION_READ), "test-bucket", "test-object")
|
||||
assert.Equal(t, s3err.ErrNone, errCode)
|
||||
})
|
||||
|
||||
t.Run("explicit deny overrides allow", func(t *testing.T) {
|
||||
iam := &IdentityAccessManagement{}
|
||||
err := iam.PutPolicy("allowAllGet", `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"arn:aws:s3:::test-bucket/*"}]}`)
|
||||
assert.NoError(t, err)
|
||||
err = iam.PutPolicy("denySecret", `{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Action":"s3:GetObject","Resource":"arn:aws:s3:::test-bucket/secret.txt"}]}`)
|
||||
assert.NoError(t, err)
|
||||
|
||||
identity := &Identity{
|
||||
Name: "policy-user",
|
||||
Account: &AccountAdmin,
|
||||
PolicyNames: []string{"allowAllGet", "denySecret"},
|
||||
}
|
||||
|
||||
errCode := iam.VerifyActionPermission(buildRequest(t, http.MethodGet), identity, Action(ACTION_READ), "test-bucket", "secret.txt")
|
||||
assert.Equal(t, s3err.ErrAccessDenied, errCode)
|
||||
})
|
||||
|
||||
t.Run("implicit deny when no statement matches", func(t *testing.T) {
|
||||
iam := &IdentityAccessManagement{}
|
||||
err := iam.PutPolicy("allowOtherBucket", `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"arn:aws:s3:::other-bucket/*"}]}`)
|
||||
assert.NoError(t, err)
|
||||
|
||||
identity := &Identity{
|
||||
Name: "policy-user",
|
||||
Account: &AccountAdmin,
|
||||
PolicyNames: []string{"allowOtherBucket"},
|
||||
}
|
||||
|
||||
errCode := iam.VerifyActionPermission(buildRequest(t, http.MethodGet), identity, Action(ACTION_READ), "test-bucket", "test-object")
|
||||
assert.Equal(t, s3err.ErrAccessDenied, errCode)
|
||||
})
|
||||
|
||||
t.Run("invalid policy document does not allow", func(t *testing.T) {
|
||||
iam := &IdentityAccessManagement{}
|
||||
err := iam.PutPolicy("invalidPolicy", "{not-json")
|
||||
assert.NoError(t, err)
|
||||
|
||||
identity := &Identity{
|
||||
Name: "policy-user",
|
||||
Account: &AccountAdmin,
|
||||
PolicyNames: []string{"invalidPolicy"},
|
||||
}
|
||||
|
||||
errCode := iam.VerifyActionPermission(buildRequest(t, http.MethodGet), identity, Action(ACTION_READ), "test-bucket", "test-object")
|
||||
assert.Equal(t, s3err.ErrAccessDenied, errCode)
|
||||
})
|
||||
|
||||
t.Run("notresource excludes denied object", func(t *testing.T) {
|
||||
iam := &IdentityAccessManagement{}
|
||||
err := iam.PutPolicy("denyNotResource", `{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Action":"s3:GetObject","NotResource":"arn:aws:s3:::test-bucket/public/*"}]}`)
|
||||
assert.NoError(t, err)
|
||||
err = iam.PutPolicy("allowAllGet", `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"arn:aws:s3:::test-bucket/*"}]}`)
|
||||
assert.NoError(t, err)
|
||||
|
||||
identity := &Identity{
|
||||
Name: "policy-user",
|
||||
Account: &AccountAdmin,
|
||||
PolicyNames: []string{"allowAllGet", "denyNotResource"},
|
||||
}
|
||||
|
||||
errCode := iam.VerifyActionPermission(buildRequest(t, http.MethodGet), identity, Action(ACTION_READ), "test-bucket", "private/secret.txt")
|
||||
assert.Equal(t, s3err.ErrAccessDenied, errCode)
|
||||
|
||||
errCode = iam.VerifyActionPermission(buildRequest(t, http.MethodGet), identity, Action(ACTION_READ), "test-bucket", "public/readme.txt")
|
||||
assert.Equal(t, s3err.ErrNone, errCode)
|
||||
})
|
||||
|
||||
t.Run("condition securetransport enforced", func(t *testing.T) {
|
||||
iam := &IdentityAccessManagement{}
|
||||
err := iam.PutPolicy("allowTLSOnly", `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"arn:aws:s3:::test-bucket/*","Condition":{"Bool":{"aws:SecureTransport":"true"}}}]}`)
|
||||
assert.NoError(t, err)
|
||||
|
||||
identity := &Identity{
|
||||
Name: "policy-user",
|
||||
Account: &AccountAdmin,
|
||||
PolicyNames: []string{"allowTLSOnly"},
|
||||
}
|
||||
|
||||
httpReq := buildRequest(t, http.MethodGet)
|
||||
errCode := iam.VerifyActionPermission(httpReq, identity, Action(ACTION_READ), "test-bucket", "test-object")
|
||||
assert.Equal(t, s3err.ErrAccessDenied, errCode)
|
||||
|
||||
httpsReq := buildRequest(t, http.MethodGet)
|
||||
httpsReq.TLS = &tls.ConnectionState{}
|
||||
errCode = iam.VerifyActionPermission(httpsReq, identity, Action(ACTION_READ), "test-bucket", "test-object")
|
||||
assert.Equal(t, s3err.ErrNone, errCode)
|
||||
})
|
||||
|
||||
t.Run("actions based path still works", func(t *testing.T) {
|
||||
iam := &IdentityAccessManagement{}
|
||||
identity := &Identity{
|
||||
Name: "legacy-user",
|
||||
Account: &AccountAdmin,
|
||||
Actions: []Action{"Read:test-bucket"},
|
||||
}
|
||||
|
||||
errCode := iam.VerifyActionPermission(buildRequest(t, http.MethodGet), identity, Action(ACTION_READ), "test-bucket", "any-object")
|
||||
assert.Equal(t, s3err.ErrNone, errCode)
|
||||
})
|
||||
}
|
||||
|
||||
func TestVerifyActionPermissionPolicyFallbackCachesParsedPolicies(t *testing.T) {
|
||||
buildRequest := func(t *testing.T, method string) *http.Request {
|
||||
t.Helper()
|
||||
req, err := http.NewRequest(method, "http://s3.amazonaws.com/test-bucket/test-object", nil)
|
||||
assert.NoError(t, err)
|
||||
return req
|
||||
}
|
||||
|
||||
iam := &IdentityAccessManagement{}
|
||||
content := `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"arn:aws:s3:::test-bucket/*"}]}`
|
||||
assert.NoError(t, iam.PutPolicy("allowGet", content))
|
||||
|
||||
identity := &Identity{
|
||||
Name: "policy-user",
|
||||
Account: &AccountAdmin,
|
||||
PolicyNames: []string{"allowGet"},
|
||||
}
|
||||
|
||||
errCode := iam.VerifyActionPermission(buildRequest(t, http.MethodGet), identity, Action(ACTION_READ), "test-bucket", "test-object")
|
||||
assert.Equal(t, s3err.ErrNone, errCode)
|
||||
|
||||
cacheKey := newIAMPolicyCacheKey("allowGet", content)
|
||||
iam.policyCacheMu.RLock()
|
||||
firstEngine := iam.iamPolicyCache[cacheKey]
|
||||
firstCacheSize := len(iam.iamPolicyCache)
|
||||
iam.policyCacheMu.RUnlock()
|
||||
|
||||
assert.NotNil(t, firstEngine)
|
||||
assert.Equal(t, 1, firstCacheSize)
|
||||
|
||||
errCode = iam.VerifyActionPermission(buildRequest(t, http.MethodGet), identity, Action(ACTION_READ), "test-bucket", "test-object")
|
||||
assert.Equal(t, s3err.ErrNone, errCode)
|
||||
|
||||
iam.policyCacheMu.RLock()
|
||||
secondEngine := iam.iamPolicyCache[cacheKey]
|
||||
secondCacheSize := len(iam.iamPolicyCache)
|
||||
iam.policyCacheMu.RUnlock()
|
||||
|
||||
assert.Same(t, firstEngine, secondEngine)
|
||||
assert.Equal(t, 1, secondCacheSize)
|
||||
}
|
||||
|
||||
func TestPutPolicyInvalidatesCachedPolicyEngine(t *testing.T) {
|
||||
buildRequest := func(t *testing.T, method string) *http.Request {
|
||||
t.Helper()
|
||||
req, err := http.NewRequest(method, "http://s3.amazonaws.com/test-bucket/test-object", nil)
|
||||
assert.NoError(t, err)
|
||||
return req
|
||||
}
|
||||
|
||||
iam := &IdentityAccessManagement{}
|
||||
initialContent := `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"arn:aws:s3:::test-bucket/*"}]}`
|
||||
assert.NoError(t, iam.PutPolicy("allowGet", initialContent))
|
||||
|
||||
identity := &Identity{
|
||||
Name: "policy-user",
|
||||
Account: &AccountAdmin,
|
||||
PolicyNames: []string{"allowGet"},
|
||||
}
|
||||
|
||||
errCode := iam.VerifyActionPermission(buildRequest(t, http.MethodGet), identity, Action(ACTION_READ), "test-bucket", "test-object")
|
||||
assert.Equal(t, s3err.ErrNone, errCode)
|
||||
|
||||
initialCacheKey := newIAMPolicyCacheKey("allowGet", initialContent)
|
||||
iam.policyCacheMu.RLock()
|
||||
initialEngine := iam.iamPolicyCache[initialCacheKey]
|
||||
initialCacheSize := len(iam.iamPolicyCache)
|
||||
iam.policyCacheMu.RUnlock()
|
||||
|
||||
assert.NotNil(t, initialEngine)
|
||||
assert.Equal(t, 1, initialCacheSize)
|
||||
|
||||
updatedContent := `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"arn:aws:s3:::other-bucket/*"}]}`
|
||||
assert.NoError(t, iam.PutPolicy("allowGet", updatedContent))
|
||||
|
||||
iam.policyCacheMu.RLock()
|
||||
_, oldEntryStillCached := iam.iamPolicyCache[initialCacheKey]
|
||||
cacheSizeAfterUpdate := len(iam.iamPolicyCache)
|
||||
iam.policyCacheMu.RUnlock()
|
||||
|
||||
assert.False(t, oldEntryStillCached)
|
||||
assert.Equal(t, 0, cacheSizeAfterUpdate)
|
||||
|
||||
errCode = iam.VerifyActionPermission(buildRequest(t, http.MethodGet), identity, Action(ACTION_READ), "test-bucket", "test-object")
|
||||
assert.Equal(t, s3err.ErrAccessDenied, errCode)
|
||||
|
||||
updatedCacheKey := newIAMPolicyCacheKey("allowGet", updatedContent)
|
||||
iam.policyCacheMu.RLock()
|
||||
updatedEngine := iam.iamPolicyCache[updatedCacheKey]
|
||||
updatedCacheSize := len(iam.iamPolicyCache)
|
||||
iam.policyCacheMu.RUnlock()
|
||||
|
||||
assert.NotNil(t, updatedEngine)
|
||||
assert.NotSame(t, initialEngine, updatedEngine)
|
||||
assert.Equal(t, 1, updatedCacheSize)
|
||||
}
|
||||
|
||||
func TestReplaceS3ApiConfigurationClearsCachedPolicyEngines(t *testing.T) {
|
||||
t.Setenv("AWS_ACCESS_KEY_ID", "")
|
||||
t.Setenv("AWS_SECRET_ACCESS_KEY", "")
|
||||
|
||||
iam := &IdentityAccessManagement{}
|
||||
content := `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"arn:aws:s3:::test-bucket/*"}]}`
|
||||
_, err := iam.getOrCreateIAMPolicyEngine("allowGet", content)
|
||||
assert.NoError(t, err)
|
||||
|
||||
cacheKey := newIAMPolicyCacheKey("allowGet", content)
|
||||
iam.policyCacheMu.RLock()
|
||||
_, existsBeforeReload := iam.iamPolicyCache[cacheKey]
|
||||
iam.policyCacheMu.RUnlock()
|
||||
assert.True(t, existsBeforeReload)
|
||||
|
||||
config := &iam_pb.S3ApiConfiguration{
|
||||
Policies: []*iam_pb.Policy{
|
||||
{
|
||||
Name: "replacement",
|
||||
Content: `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:ListBucket","Resource":"arn:aws:s3:::test-bucket"}]}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.NoError(t, iam.ReplaceS3ApiConfiguration(config))
|
||||
|
||||
iam.policyCacheMu.RLock()
|
||||
cacheSizeAfterReload := len(iam.iamPolicyCache)
|
||||
iam.policyCacheMu.RUnlock()
|
||||
|
||||
assert.Equal(t, 0, cacheSizeAfterReload)
|
||||
}
|
||||
|
||||
type LoadS3ApiConfigurationTestCase struct {
|
||||
pbAccount *iam_pb.Account
|
||||
pbIdent *iam_pb.Identity
|
||||
|
||||
Reference in New Issue
Block a user