Files
seaweedfs/weed/admin/handlers/policy_handlers.go
T
Chris Lu 8782749f26 admin: reject IAM policy deletion while still attached to a user/group (#11230)
* admin: add IsPolicyAttached helper to detect user/group attachments

Introduces AdminServer.IsPolicyAttached, which lists the users and groups
that still have a managed policy attached, reusing the existing credential
manager ListUsers / ListAttachedUserPolicies / ListGroups / GetGroup
methods. This is the building block for rejecting policy deletion while a
policy is still referenced, so deleted policy names stop lingering in a
user attached policy names list (issue #11225).

* admin: reject IAM policy deletion while still attached

Guards AdminServer.DeletePolicy with the new IsPolicyAttached check and
returns the typed ErrPolicyStillAttached error when the policy is still
referenced by a user or group. This matches AWS IAM and the existing IAM
API handler behavior, fixing the stale reference where a deleted policy
name kept showing up in a user attached policy names list (issue #11225).

* admin: return 409 Conflict when deleting an attached IAM policy

The admin UI DeletePolicy handler now maps ErrPolicyStillAttached to HTTP
409 Conflict instead of 500, so the dashboard can surface the attachment
conflict to the user rather than reporting a generic server error.

* admin: skip vanished groups when checking policy attachments

IsPolicyAttached now treats a group that disappears between ListGroups
and GetGroup (credential.ErrGroupNotFound) as no longer attached instead
of failing the whole deletion with HTTP 500, matching the IAM API handler
which skips vanished groups.

* test: assert policy state after deletion paths

Strengthen GetPolicy assertions in the policy deletion tests to check the
returned policy is non-nil after a rejected deletion and nil after a
successful one, not just that no lookup error occurred (GetPolicy returns
nil, nil when a policy is absent).
2026-09-08 16:16:01 -07:00

295 lines
8.8 KiB
Go

package handlers
import (
"errors"
"fmt"
"net/http"
"time"
"github.com/gorilla/mux"
"github.com/seaweedfs/seaweedfs/weed/admin/dash"
"github.com/seaweedfs/seaweedfs/weed/admin/view/app"
"github.com/seaweedfs/seaweedfs/weed/admin/view/layout"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/s3api/policy_engine"
)
// PolicyHandlers contains all the HTTP handlers for policy management
type PolicyHandlers struct {
adminServer *dash.AdminServer
}
// NewPolicyHandlers creates a new instance of PolicyHandlers
func NewPolicyHandlers(adminServer *dash.AdminServer) *PolicyHandlers {
return &PolicyHandlers{
adminServer: adminServer,
}
}
// ShowPolicies renders the policies management page
func (h *PolicyHandlers) ShowPolicies(w http.ResponseWriter, r *http.Request) {
// Get policies data from the server
policiesData := h.getPoliciesData(r)
// Render HTML template
w.Header().Set("Content-Type", "text/html")
policiesComponent := app.Policies(policiesData)
viewCtx := layout.NewViewContext(r, dash.UsernameFromContext(r.Context()), dash.CSRFTokenFromContext(r.Context()))
layoutComponent := layout.Layout(viewCtx, policiesComponent)
if err := layoutComponent.Render(r.Context(), w); err != nil {
writeJSONError(w, http.StatusInternalServerError, "Failed to render template: "+err.Error())
return
}
}
// GetPolicies returns the list of policies as JSON
func (h *PolicyHandlers) GetPolicies(w http.ResponseWriter, r *http.Request) {
policies, err := h.adminServer.GetPolicies()
if err != nil {
writeJSONError(w, http.StatusInternalServerError, "Failed to get policies: "+err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{"policies": policies})
}
// GetPrincipalSuggestions returns candidate ARNs (existing users and IAM
// roles) for the policy editor's Principal/NotPrincipal autocomplete.
func (h *PolicyHandlers) GetPrincipalSuggestions(w http.ResponseWriter, r *http.Request) {
suggestions, err := h.adminServer.GetPrincipalSuggestions(r.Context())
if err != nil {
writeJSONError(w, http.StatusInternalServerError, "Failed to get principal suggestions: "+err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{"principals": suggestions})
}
// CreatePolicy handles policy creation
func (h *PolicyHandlers) CreatePolicy(w http.ResponseWriter, r *http.Request) {
var req dash.CreatePolicyRequest
if err := decodeJSONBody(newJSONMaxReader(w, r), &req); err != nil {
writeJSONError(w, http.StatusBadRequest, "Invalid request: "+err.Error())
return
}
// Validate policy name
if req.Name == "" {
writeJSONError(w, http.StatusBadRequest, "Policy name is required")
return
}
// Check if policy already exists
existingPolicy, err := h.adminServer.GetPolicy(req.Name)
if err != nil {
writeJSONError(w, http.StatusInternalServerError, "Failed to check existing policy: "+err.Error())
return
}
if existingPolicy != nil {
writeJSONError(w, http.StatusConflict, "Policy with this name already exists")
return
}
// Create the policy
err = h.adminServer.CreatePolicy(req.Name, req.Document)
if err != nil {
glog.Errorf("Failed to create policy %s: %v", req.Name, err)
writeJSONError(w, http.StatusInternalServerError, "Failed to create policy: "+err.Error())
return
}
writeJSON(w, http.StatusCreated, map[string]interface{}{
"success": true,
"message": "Policy created successfully",
"policy": req.Name,
})
}
// GetPolicy returns a specific policy
func (h *PolicyHandlers) GetPolicy(w http.ResponseWriter, r *http.Request) {
policyName := mux.Vars(r)["name"]
if policyName == "" {
writeJSONError(w, http.StatusBadRequest, "Policy name is required")
return
}
policy, err := h.adminServer.GetPolicy(policyName)
if err != nil {
writeJSONError(w, http.StatusInternalServerError, "Failed to get policy: "+err.Error())
return
}
if policy == nil {
writeJSONError(w, http.StatusNotFound, "Policy not found")
return
}
writeJSON(w, http.StatusOK, policy)
}
// UpdatePolicy handles policy updates
func (h *PolicyHandlers) UpdatePolicy(w http.ResponseWriter, r *http.Request) {
policyName := mux.Vars(r)["name"]
if policyName == "" {
writeJSONError(w, http.StatusBadRequest, "Policy name is required")
return
}
var req dash.UpdatePolicyRequest
if err := decodeJSONBody(newJSONMaxReader(w, r), &req); err != nil {
writeJSONError(w, http.StatusBadRequest, "Invalid request: "+err.Error())
return
}
// Check if policy exists
existingPolicy, err := h.adminServer.GetPolicy(policyName)
if err != nil {
writeJSONError(w, http.StatusInternalServerError, "Failed to check existing policy: "+err.Error())
return
}
if existingPolicy == nil {
writeJSONError(w, http.StatusNotFound, "Policy not found")
return
}
// Update the policy
err = h.adminServer.UpdatePolicy(policyName, req.Document)
if err != nil {
glog.Errorf("Failed to update policy %s: %v", policyName, err)
writeJSONError(w, http.StatusInternalServerError, "Failed to update policy: "+err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"success": true,
"message": "Policy updated successfully",
"policy": policyName,
})
}
// DeletePolicy handles policy deletion
func (h *PolicyHandlers) DeletePolicy(w http.ResponseWriter, r *http.Request) {
policyName := mux.Vars(r)["name"]
if policyName == "" {
writeJSONError(w, http.StatusBadRequest, "Policy name is required")
return
}
// Check if policy exists
existingPolicy, err := h.adminServer.GetPolicy(policyName)
if err != nil {
writeJSONError(w, http.StatusInternalServerError, "Failed to check existing policy: "+err.Error())
return
}
if existingPolicy == nil {
writeJSONError(w, http.StatusNotFound, "Policy not found")
return
}
// Delete the policy
err = h.adminServer.DeletePolicy(policyName)
if err != nil {
status := http.StatusInternalServerError
if errors.Is(err, dash.ErrPolicyStillAttached) {
status = http.StatusConflict
} else {
glog.Errorf("Failed to delete policy %s: %v", policyName, err)
}
writeJSONError(w, status, "Failed to delete policy: "+err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"success": true,
"message": "Policy deleted successfully",
"policy": policyName,
})
}
// ValidatePolicy validates a policy document without saving it
func (h *PolicyHandlers) ValidatePolicy(w http.ResponseWriter, r *http.Request) {
var req struct {
Document policy_engine.PolicyDocument `json:"document"`
}
if err := decodeJSONBody(newJSONMaxReader(w, r), &req); err != nil {
writeJSONError(w, http.StatusBadRequest, "Invalid request: "+err.Error())
return
}
// Basic validation
if req.Document.Version == "" {
writeJSONError(w, http.StatusBadRequest, "Policy version is required")
return
}
if len(req.Document.Statement) == 0 {
writeJSONError(w, http.StatusBadRequest, "Policy must have at least one statement")
return
}
// Validate each statement
for i, statement := range req.Document.Statement {
if statement.Effect != "Allow" && statement.Effect != "Deny" {
writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("Statement %d: Effect must be 'Allow' or 'Deny'", i+1))
return
}
if len(statement.Action.Strings()) == 0 {
writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("Statement %d: Action is required", i+1))
return
}
if len(statement.Resource.Strings()) == 0 && len(statement.NotResource.Strings()) == 0 {
writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("Statement %d: Resource or NotResource is required", i+1))
return
}
if statement.Resource != nil && statement.NotResource != nil {
writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("Statement %d: cannot specify both Resource and NotResource", i+1))
return
}
if statement.Principal != nil && statement.NotPrincipal != nil {
writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("Statement %d: cannot specify both Principal and NotPrincipal", i+1))
return
}
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"valid": true,
"message": "Policy document is valid",
})
}
// getPoliciesData retrieves policies data from the server
func (h *PolicyHandlers) getPoliciesData(r *http.Request) dash.PoliciesData {
username := dash.UsernameFromContext(r.Context())
if username == "" {
username = "admin"
}
// Get policies
policies, err := h.adminServer.GetPolicies()
if err != nil {
glog.Errorf("Failed to get policies: %v", err)
// Return empty data on error
return dash.PoliciesData{
Username: username,
Policies: []dash.IAMPolicy{},
TotalPolicies: 0,
LastUpdated: time.Now(),
}
}
// Ensure policies is never nil
if policies == nil {
policies = []dash.IAMPolicy{}
}
return dash.PoliciesData{
Username: username,
Policies: policies,
TotalPolicies: len(policies),
LastUpdated: time.Now(),
}
}