Allow forced server deletion

This commit is contained in:
npc0-hue
2026-08-04 15:03:49 +08:00
parent 96a1939f32
commit cf9af14eaf
17 changed files with 236 additions and 24 deletions
+9 -5
View File
@@ -23,6 +23,8 @@ var (
ErrForbidden = errors.New("forbidden")
)
const ServerDeletionForceConfirmation = "FORCE DELETE"
type ForbiddenError struct {
Reason string
}
@@ -102,7 +104,7 @@ type Core interface {
ListServerAdministratorCandidates(string, string) ([]domain.User, error)
AddServerAdministrator(string, string, string) (domain.ServerInstance, error)
RemoveServerAdministrator(string, string, string) (domain.ServerInstance, error)
DeleteServerInstanceForSession(string, string, string) (domain.ServerInstance, error)
DeleteServerInstanceForSession(string, string, domain.ServerDeletionRequest) (domain.ServerInstance, error)
GetPlatformResourceUsage() (domain.PlatformResourceUsage, error)
ListServerMetricsForSession(string) ([]domain.ServerMetrics, error)
GetProductionCapacityForSession(string) (domain.ProductionCapacitySummary, error)
@@ -2367,7 +2369,7 @@ func (svc *CoreService) RemoveServerAdministrator(sessionID string, serverInstan
return domain.CopyServerInstance(instance), nil
}
func (svc *CoreService) DeleteServerInstanceForSession(sessionID string, serverInstanceID string, password string) (domain.ServerInstance, error) {
func (svc *CoreService) DeleteServerInstanceForSession(sessionID string, serverInstanceID string, request domain.ServerDeletionRequest) (domain.ServerInstance, error) {
user, err := svc.GetCurrentUser(sessionID)
if err != nil {
return domain.ServerInstance{}, err
@@ -2379,14 +2381,16 @@ func (svc *CoreService) DeleteServerInstanceForSession(sessionID string, serverI
if !isPlatformAdmin(user) && instance.OwnerUserID != user.ID {
return domain.ServerInstance{}, ErrForbidden
}
if strings.TrimSpace(password) == "" {
if strings.TrimSpace(request.Password) == "" {
return domain.ServerInstance{}, validationError("password is required")
}
if !verifyPassword(user.PasswordHash, password) {
if !verifyPassword(user.PasswordHash, request.Password) {
return domain.ServerInstance{}, forbiddenError("password confirmation failed")
}
if instance.State == domain.ServerInstanceStateRunning || instance.State == domain.ServerInstanceStateInstalling {
return domain.ServerInstance{}, validationError("running or installing server instances must be stopped before delete")
if !request.Force || strings.TrimSpace(request.Confirmation) != ServerDeletionForceConfirmation {
return domain.ServerInstance{}, validationError("running or installing server instances require forced-delete confirmation")
}
}
if instance.State == domain.ServerInstanceStateDeleted {
return domain.CopyServerInstance(instance), nil
+33 -6
View File
@@ -580,11 +580,21 @@ func TestCoreServiceDeletesServerInstancesWithPasswordConfirmation(t *testing.T)
if err := svc.store.ServerInstances().Update(runningTarget); err != nil {
t.Fatalf("set running target state: %v", err)
}
installingTarget, err := svc.CreateServerInstanceForSession(ownerSession, domain.ServerInstance{
ID: "server-delete-installing",
PluginID: plugin.ID,
RunEndpointID: endpoint.ID,
Name: "Delete Installing Target",
State: domain.ServerInstanceStateInstalling,
})
if err != nil {
t.Fatalf("create installing target: %v", err)
}
if _, err := svc.DeleteServerInstanceForSession(otherSession, ownerInstance.ID, "secret-password"); !errors.Is(err, ErrForbidden) {
if _, err := svc.DeleteServerInstanceForSession(otherSession, ownerInstance.ID, domain.ServerDeletionRequest{Password: "secret-password"}); !errors.Is(err, ErrForbidden) {
t.Fatalf("expected non-owner delete to be forbidden, got %v", err)
}
if _, err := svc.DeleteServerInstanceForSession(ownerSession, ownerInstance.ID, ""); err == nil {
if _, err := svc.DeleteServerInstanceForSession(ownerSession, ownerInstance.ID, domain.ServerDeletionRequest{}); err == nil {
t.Fatalf("expected missing password to fail")
} else {
var validationErr validator.ValidationError
@@ -592,25 +602,25 @@ func TestCoreServiceDeletesServerInstancesWithPasswordConfirmation(t *testing.T)
t.Fatalf("expected missing password to be validation error, got %v", err)
}
}
if _, err := svc.DeleteServerInstanceForSession(ownerSession, ownerInstance.ID, "wrong-password"); !errors.Is(err, ErrForbidden) {
if _, err := svc.DeleteServerInstanceForSession(ownerSession, ownerInstance.ID, domain.ServerDeletionRequest{Password: "wrong-password"}); !errors.Is(err, ErrForbidden) {
t.Fatalf("expected wrong password to be forbidden, got %v", err)
}
deletedOwner, err := svc.DeleteServerInstanceForSession(ownerSession, ownerInstance.ID, "secret-password")
deletedOwner, err := svc.DeleteServerInstanceForSession(ownerSession, ownerInstance.ID, domain.ServerDeletionRequest{Password: "secret-password"})
if err != nil {
t.Fatalf("delete owner instance: %v", err)
}
if deletedOwner.State != domain.ServerInstanceStateDeleted {
t.Fatalf("expected deleted owner state, got %+v", deletedOwner)
}
deletedAdmin, err := svc.DeleteServerInstanceForSession(adminSession, adminTarget.ID, "secret-password")
deletedAdmin, err := svc.DeleteServerInstanceForSession(adminSession, adminTarget.ID, domain.ServerDeletionRequest{Password: "secret-password"})
if err != nil {
t.Fatalf("delete admin target: %v", err)
}
if deletedAdmin.State != domain.ServerInstanceStateDeleted {
t.Fatalf("expected deleted admin state, got %+v", deletedAdmin)
}
if _, err := svc.DeleteServerInstanceForSession(ownerSession, runningTarget.ID, "secret-password"); err == nil {
if _, err := svc.DeleteServerInstanceForSession(ownerSession, runningTarget.ID, domain.ServerDeletionRequest{Password: "secret-password"}); err == nil {
t.Fatalf("expected running instance delete to fail")
} else {
var validationErr validator.ValidationError
@@ -618,6 +628,23 @@ func TestCoreServiceDeletesServerInstancesWithPasswordConfirmation(t *testing.T)
t.Fatalf("expected running delete to be validation error, got %v", err)
}
}
if _, err := svc.DeleteServerInstanceForSession(ownerSession, installingTarget.ID, domain.ServerDeletionRequest{Password: "secret-password", Force: true, Confirmation: "not enough"}); err == nil {
t.Fatalf("expected installing force delete without exact confirmation to fail")
}
forcedRunning, err := svc.DeleteServerInstanceForSession(ownerSession, runningTarget.ID, domain.ServerDeletionRequest{Password: "secret-password", Force: true, Confirmation: ServerDeletionForceConfirmation})
if err != nil {
t.Fatalf("force delete running target: %v", err)
}
if forcedRunning.State != domain.ServerInstanceStateDeleted {
t.Fatalf("expected forced running target deleted, got %+v", forcedRunning)
}
forcedInstalling, err := svc.DeleteServerInstanceForSession(ownerSession, installingTarget.ID, domain.ServerDeletionRequest{Password: "secret-password", Force: true, Confirmation: ServerDeletionForceConfirmation})
if err != nil {
t.Fatalf("force delete installing target: %v", err)
}
if forcedInstalling.State != domain.ServerInstanceStateDeleted {
t.Fatalf("expected forced installing target deleted, got %+v", forcedInstalling)
}
}
func TestCoreServiceMetricsAndConfigReadAreRoleScoped(t *testing.T) {