mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-05 19:47:47 +01:00
* 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).
295 lines
8.8 KiB
Go
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(),
|
|
}
|
|
}
|