feat(admin-console): Stage 5 — operators (admin accounts)
Tests · Go / test (push) Successful in 1m59s
Tests · Go / test (push) Successful in 1m59s
Add the operator-management page over *admin.Service (no new business logic).
- GET/POST /_gm/operators list + create operator
- POST /_gm/operators/{user}/disable|enable toggle access
- POST /_gm/operators/{user}/reset-password set a new password
Console depends on an OperatorAdmin interface (satisfied by *admin.Service) so
the page renders in tests without a database. Create POST is mounted on the
collection path; per-row disable/enable/reset are guarded by the CSRF middleware
and redirect back. Passwords are never logged.
Tests: list render, create (+ username/password assertions), username-taken
conflict, disable/enable, reset (+ password assertion), missing-password 400,
bad-CSRF 403, and unavailable 503.
Docs: backend/docs/admin-console.md page inventory extended.
This commit is contained in:
@@ -381,6 +381,7 @@ func run(ctx context.Context) (err error) {
|
|||||||
Games: lobbySvc,
|
Games: lobbySvc,
|
||||||
Runtime: runtimeSvc,
|
Runtime: runtimeSvc,
|
||||||
EngineVersions: engineVersionSvc,
|
EngineVersions: engineVersionSvc,
|
||||||
|
Operators: adminSvc,
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -101,6 +101,10 @@ changes.
|
|||||||
| `/_gm/games/{id}/runtime/force-next-turn` | POST | Force the next turn now. |
|
| `/_gm/games/{id}/runtime/force-next-turn` | POST | Force the next turn now. |
|
||||||
| `/_gm/engine-versions` | GET/POST | Version registry; POST registers a version. |
|
| `/_gm/engine-versions` | GET/POST | Version registry; POST registers a version. |
|
||||||
| `/_gm/engine-versions/{ver}/disable` | POST | Disable a registered version. |
|
| `/_gm/engine-versions/{ver}/disable` | POST | Disable a registered version. |
|
||||||
|
| `/_gm/operators` | GET/POST | Admin-account list; POST creates an operator. |
|
||||||
|
| `/_gm/operators/{user}/disable` | POST | Disable an operator. |
|
||||||
|
| `/_gm/operators/{user}/enable` | POST | Re-enable an operator. |
|
||||||
|
| `/_gm/operators/{user}/reset-password` | POST | Reset an operator's password. |
|
||||||
|
|
||||||
Each page reuses the same service layer as the corresponding `/api/v1/admin/*`
|
Each page reuses the same service layer as the corresponding `/api/v1/admin/*`
|
||||||
JSON endpoint; the console adds no business logic. Collection-mutating POSTs are
|
JSON endpoint; the console adds no business logic. Collection-mutating POSTs are
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package adminconsole
|
||||||
|
|
||||||
|
// OperatorRow is one line in the operators (admin accounts) table.
|
||||||
|
type OperatorRow struct {
|
||||||
|
Username string
|
||||||
|
CreatedAt string
|
||||||
|
LastUsedAt string
|
||||||
|
Disabled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// OperatorsData is the view model for the operators page.
|
||||||
|
type OperatorsData struct {
|
||||||
|
Items []OperatorRow
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{{define "content" -}}
|
||||||
|
{{$csrf := .CSRFToken}}
|
||||||
|
<h1>Operators</h1>
|
||||||
|
{{with .Data}}
|
||||||
|
<table class="list">
|
||||||
|
<thead><tr><th>Username</th><th>Status</th><th>Created</th><th>Last used</th><th>Actions</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
{{range .Items}}
|
||||||
|
<tr>
|
||||||
|
<td>{{.Username}}</td>
|
||||||
|
<td>{{if .Disabled}}<span class="bad">disabled</span>{{else}}<span class="ok">active</span>{{end}}</td>
|
||||||
|
<td>{{.CreatedAt}}</td>
|
||||||
|
<td>{{if .LastUsedAt}}{{.LastUsedAt}}{{else}}—{{end}}</td>
|
||||||
|
<td>
|
||||||
|
<div class="actions">
|
||||||
|
{{if .Disabled}}
|
||||||
|
<form method="post" action="/_gm/operators/{{.Username}}/enable"><input type="hidden" name="_csrf" value="{{$csrf}}"><button type="submit">Enable</button></form>
|
||||||
|
{{else}}
|
||||||
|
<form method="post" action="/_gm/operators/{{.Username}}/disable" onsubmit="return confirm('Disable {{.Username}}?');"><input type="hidden" name="_csrf" value="{{$csrf}}"><button type="submit" class="danger">Disable</button></form>
|
||||||
|
{{end}}
|
||||||
|
<form method="post" action="/_gm/operators/{{.Username}}/reset-password" class="form"><input type="hidden" name="_csrf" value="{{$csrf}}"><input type="password" name="password" placeholder="new password" required><button type="submit">Reset</button></form>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{else}}<tr><td colspan="5"><span class="note">no operators</span></td></tr>{{end}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{{end}}
|
||||||
|
<section class="panel">
|
||||||
|
<h2>Create operator</h2>
|
||||||
|
<form method="post" action="/_gm/operators" class="form">
|
||||||
|
<input type="hidden" name="_csrf" value="{{$csrf}}">
|
||||||
|
<label>Username <input type="text" name="username" required></label>
|
||||||
|
<label>Password <input type="password" name="password" required></label>
|
||||||
|
<button type="submit">Create</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
{{- end}}
|
||||||
@@ -32,6 +32,7 @@ type AdminConsoleHandlers struct {
|
|||||||
games GameAdmin
|
games GameAdmin
|
||||||
runtime RuntimeAdmin
|
runtime RuntimeAdmin
|
||||||
engineVersions EngineVersionAdmin
|
engineVersions EngineVersionAdmin
|
||||||
|
operators OperatorAdmin
|
||||||
logger *zap.Logger
|
logger *zap.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,6 +50,7 @@ type AdminConsoleDeps struct {
|
|||||||
Games GameAdmin
|
Games GameAdmin
|
||||||
Runtime RuntimeAdmin
|
Runtime RuntimeAdmin
|
||||||
EngineVersions EngineVersionAdmin
|
EngineVersions EngineVersionAdmin
|
||||||
|
Operators OperatorAdmin
|
||||||
Logger *zap.Logger
|
Logger *zap.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,6 +91,7 @@ func NewAdminConsoleHandlers(deps AdminConsoleDeps) *AdminConsoleHandlers {
|
|||||||
games: deps.Games,
|
games: deps.Games,
|
||||||
runtime: deps.Runtime,
|
runtime: deps.Runtime,
|
||||||
engineVersions: deps.EngineVersions,
|
engineVersions: deps.EngineVersions,
|
||||||
|
operators: deps.Operators,
|
||||||
logger: logger.Named("http.admin.console"),
|
logger: logger.Named("http.admin.console"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"galaxy/backend/internal/admin"
|
||||||
|
"galaxy/backend/internal/adminconsole"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OperatorAdmin is the subset of the admin-account service the console uses.
|
||||||
|
// *admin.Service satisfies it.
|
||||||
|
type OperatorAdmin interface {
|
||||||
|
List(ctx context.Context) ([]admin.Admin, error)
|
||||||
|
Create(ctx context.Context, in admin.CreateInput) (admin.Admin, error)
|
||||||
|
Disable(ctx context.Context, username string) (admin.Admin, error)
|
||||||
|
Enable(ctx context.Context, username string) (admin.Admin, error)
|
||||||
|
ResetPassword(ctx context.Context, username, password string) (admin.Admin, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OperatorsList renders GET /_gm/operators.
|
||||||
|
func (h *AdminConsoleHandlers) OperatorsList() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
if h.operators == nil {
|
||||||
|
h.renderMessage(c, http.StatusServiceUnavailable, "operators", "Operators", "Operator administration is not available.", "bad", "/_gm/")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
admins, err := h.operators.List(c.Request.Context())
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Error("admin console: list operators", zap.Error(err))
|
||||||
|
h.renderMessage(c, http.StatusInternalServerError, "operators", "Operators", "Failed to load operators.", "bad", "/_gm/")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.render(c, http.StatusOK, "operators", "operators", "Operators", toOperatorsData(admins))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OperatorCreate handles POST /_gm/operators.
|
||||||
|
func (h *AdminConsoleHandlers) OperatorCreate() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
if h.operators == nil {
|
||||||
|
h.renderMessage(c, http.StatusServiceUnavailable, "operators", "Operators", "Operator administration is not available.", "bad", "/_gm/")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, err := h.operators.Create(c.Request.Context(), admin.CreateInput{
|
||||||
|
Username: strings.TrimSpace(c.PostForm("username")),
|
||||||
|
Password: c.PostForm("password"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, admin.ErrUsernameTaken):
|
||||||
|
h.renderMessage(c, http.StatusConflict, "operators", "Username taken", "That username is already in use.", "bad", "/_gm/operators")
|
||||||
|
case errors.Is(err, admin.ErrInvalidInput):
|
||||||
|
h.renderMessage(c, http.StatusBadRequest, "operators", "Invalid input", "Username and password are required.", "bad", "/_gm/operators")
|
||||||
|
default:
|
||||||
|
h.logger.Error("admin console: create operator", zap.Error(err))
|
||||||
|
h.renderMessage(c, http.StatusInternalServerError, "operators", "Create failed", "Failed to create the operator.", "bad", "/_gm/operators")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Redirect(http.StatusSeeOther, "/_gm/operators")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OperatorDisable handles POST /_gm/operators/:username/disable.
|
||||||
|
func (h *AdminConsoleHandlers) OperatorDisable() gin.HandlerFunc {
|
||||||
|
return h.operatorAction("disable", func(ctx context.Context, username string) error {
|
||||||
|
_, err := h.operators.Disable(ctx, username)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// OperatorEnable handles POST /_gm/operators/:username/enable.
|
||||||
|
func (h *AdminConsoleHandlers) OperatorEnable() gin.HandlerFunc {
|
||||||
|
return h.operatorAction("enable", func(ctx context.Context, username string) error {
|
||||||
|
_, err := h.operators.Enable(ctx, username)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// OperatorResetPassword handles POST /_gm/operators/:username/reset-password.
|
||||||
|
func (h *AdminConsoleHandlers) OperatorResetPassword() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
if h.operators == nil {
|
||||||
|
h.renderMessage(c, http.StatusServiceUnavailable, "operators", "Operators", "Operator administration is not available.", "bad", "/_gm/")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
username := c.Param("username")
|
||||||
|
password := c.PostForm("password")
|
||||||
|
if strings.TrimSpace(password) == "" {
|
||||||
|
h.renderMessage(c, http.StatusBadRequest, "operators", "Invalid input", "A new password is required.", "bad", "/_gm/operators")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := h.operators.ResetPassword(c.Request.Context(), username, password); err != nil {
|
||||||
|
if errors.Is(err, admin.ErrNotFound) {
|
||||||
|
h.renderMessage(c, http.StatusNotFound, "operators", "Operator not found", "No such operator.", "bad", "/_gm/operators")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if errors.Is(err, admin.ErrInvalidInput) {
|
||||||
|
h.renderMessage(c, http.StatusBadRequest, "operators", "Invalid input", "The password was rejected.", "bad", "/_gm/operators")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.logger.Error("admin console: reset operator password", zap.Error(err))
|
||||||
|
h.renderMessage(c, http.StatusInternalServerError, "operators", "Reset failed", "Failed to reset the password.", "bad", "/_gm/operators")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Redirect(http.StatusSeeOther, "/_gm/operators")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// operatorAction is the shared shape for operator POST actions that take only
|
||||||
|
// the username and redirect back to the list.
|
||||||
|
func (h *AdminConsoleHandlers) operatorAction(label string, run func(context.Context, string) error) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
if h.operators == nil {
|
||||||
|
h.renderMessage(c, http.StatusServiceUnavailable, "operators", "Operators", "Operator administration is not available.", "bad", "/_gm/")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := run(c.Request.Context(), c.Param("username")); err != nil {
|
||||||
|
if errors.Is(err, admin.ErrNotFound) {
|
||||||
|
h.renderMessage(c, http.StatusNotFound, "operators", "Operator not found", "No such operator.", "bad", "/_gm/operators")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.logger.Error("admin console: operator "+label, zap.Error(err))
|
||||||
|
h.renderMessage(c, http.StatusInternalServerError, "operators", "Action failed", "The "+label+" action failed.", "bad", "/_gm/operators")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Redirect(http.StatusSeeOther, "/_gm/operators")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// toOperatorsData maps admin accounts into the operators view model.
|
||||||
|
func toOperatorsData(admins []admin.Admin) adminconsole.OperatorsData {
|
||||||
|
data := adminconsole.OperatorsData{Items: make([]adminconsole.OperatorRow, 0, len(admins))}
|
||||||
|
for _, a := range admins {
|
||||||
|
data.Items = append(data.Items, adminconsole.OperatorRow{
|
||||||
|
Username: a.Username,
|
||||||
|
CreatedAt: fmtConsoleTime(a.CreatedAt),
|
||||||
|
LastUsedAt: fmtConsoleTimePtr(a.LastUsedAt),
|
||||||
|
Disabled: a.DisabledAt != nil,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"galaxy/backend/internal/admin"
|
||||||
|
"galaxy/backend/internal/adminconsole"
|
||||||
|
"galaxy/backend/internal/server/middleware/basicauth"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type fakeOperatorAdmin struct {
|
||||||
|
list []admin.Admin
|
||||||
|
createErr error
|
||||||
|
|
||||||
|
created admin.CreateInput
|
||||||
|
createCalls int
|
||||||
|
disableCalls int
|
||||||
|
enableCalls int
|
||||||
|
resetCalls int
|
||||||
|
lastResetUser string
|
||||||
|
lastResetPass string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeOperatorAdmin) List(context.Context) ([]admin.Admin, error) { return f.list, nil }
|
||||||
|
func (f *fakeOperatorAdmin) Create(_ context.Context, in admin.CreateInput) (admin.Admin, error) {
|
||||||
|
f.createCalls++
|
||||||
|
f.created = in
|
||||||
|
if f.createErr != nil {
|
||||||
|
return admin.Admin{}, f.createErr
|
||||||
|
}
|
||||||
|
return admin.Admin{Username: in.Username}, nil
|
||||||
|
}
|
||||||
|
func (f *fakeOperatorAdmin) Disable(_ context.Context, username string) (admin.Admin, error) {
|
||||||
|
f.disableCalls++
|
||||||
|
return admin.Admin{Username: username}, nil
|
||||||
|
}
|
||||||
|
func (f *fakeOperatorAdmin) Enable(_ context.Context, username string) (admin.Admin, error) {
|
||||||
|
f.enableCalls++
|
||||||
|
return admin.Admin{Username: username}, nil
|
||||||
|
}
|
||||||
|
func (f *fakeOperatorAdmin) ResetPassword(_ context.Context, username, password string) (admin.Admin, error) {
|
||||||
|
f.resetCalls++
|
||||||
|
f.lastResetUser = username
|
||||||
|
f.lastResetPass = password
|
||||||
|
return admin.Admin{Username: username}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func operatorsRouter(t *testing.T, operators OperatorAdmin) (http.Handler, *adminconsole.CSRF) {
|
||||||
|
t.Helper()
|
||||||
|
csrf := adminconsole.NewCSRF([]byte("test-key"))
|
||||||
|
handler, err := NewRouter(RouterDependencies{
|
||||||
|
Logger: zap.NewNop(),
|
||||||
|
AdminVerifier: basicauth.NewStaticVerifier("secret"),
|
||||||
|
AdminConsole: NewAdminConsoleHandlers(AdminConsoleDeps{CSRF: csrf, Operators: operators}),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewRouter: %v", err)
|
||||||
|
}
|
||||||
|
return handler, csrf
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConsoleOperatorsList(t *testing.T) {
|
||||||
|
fake := &fakeOperatorAdmin{list: []admin.Admin{{Username: "root"}}}
|
||||||
|
router, _ := operatorsRouter(t, fake)
|
||||||
|
|
||||||
|
rec := consoleGet(t, router, "/_gm/operators")
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
for _, want := range []string{"root", "Create operator", "Reset"} {
|
||||||
|
if !strings.Contains(rec.Body.String(), want) {
|
||||||
|
t.Errorf("operators page missing %q", want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConsoleOperatorCreate(t *testing.T) {
|
||||||
|
fake := &fakeOperatorAdmin{}
|
||||||
|
router, csrf := operatorsRouter(t, fake)
|
||||||
|
|
||||||
|
rec := consolePost(t, router, "/_gm/operators", "_csrf="+csrf.Token("ops")+"&username=mod&password=s3cret")
|
||||||
|
if rec.Code != http.StatusSeeOther {
|
||||||
|
t.Fatalf("status = %d, want 303; body=%s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
if fake.createCalls != 1 || fake.created.Username != "mod" || fake.created.Password != "s3cret" {
|
||||||
|
t.Errorf("create recorded %d username=%q", fake.createCalls, fake.created.Username)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConsoleOperatorCreateConflict(t *testing.T) {
|
||||||
|
fake := &fakeOperatorAdmin{createErr: admin.ErrUsernameTaken}
|
||||||
|
router, csrf := operatorsRouter(t, fake)
|
||||||
|
|
||||||
|
rec := consolePost(t, router, "/_gm/operators", "_csrf="+csrf.Token("ops")+"&username=root&password=x")
|
||||||
|
if rec.Code != http.StatusConflict {
|
||||||
|
t.Fatalf("status = %d, want 409", rec.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConsoleOperatorDisableEnable(t *testing.T) {
|
||||||
|
fake := &fakeOperatorAdmin{}
|
||||||
|
router, csrf := operatorsRouter(t, fake)
|
||||||
|
|
||||||
|
if rec := consolePost(t, router, "/_gm/operators/root/disable", "_csrf="+csrf.Token("ops")); rec.Code != http.StatusSeeOther {
|
||||||
|
t.Fatalf("disable status = %d, want 303", rec.Code)
|
||||||
|
}
|
||||||
|
if rec := consolePost(t, router, "/_gm/operators/root/enable", "_csrf="+csrf.Token("ops")); rec.Code != http.StatusSeeOther {
|
||||||
|
t.Fatalf("enable status = %d, want 303", rec.Code)
|
||||||
|
}
|
||||||
|
if fake.disableCalls != 1 || fake.enableCalls != 1 {
|
||||||
|
t.Errorf("disable=%d enable=%d, want 1/1", fake.disableCalls, fake.enableCalls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConsoleOperatorResetPassword(t *testing.T) {
|
||||||
|
fake := &fakeOperatorAdmin{}
|
||||||
|
router, csrf := operatorsRouter(t, fake)
|
||||||
|
|
||||||
|
rec := consolePost(t, router, "/_gm/operators/root/reset-password", "_csrf="+csrf.Token("ops")+"&password=newpass")
|
||||||
|
if rec.Code != http.StatusSeeOther {
|
||||||
|
t.Fatalf("status = %d, want 303", rec.Code)
|
||||||
|
}
|
||||||
|
if fake.resetCalls != 1 || fake.lastResetUser != "root" || fake.lastResetPass != "newpass" {
|
||||||
|
t.Errorf("reset recorded %d user=%q", fake.resetCalls, fake.lastResetUser)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConsoleOperatorResetPasswordMissing(t *testing.T) {
|
||||||
|
fake := &fakeOperatorAdmin{}
|
||||||
|
router, csrf := operatorsRouter(t, fake)
|
||||||
|
|
||||||
|
rec := consolePost(t, router, "/_gm/operators/root/reset-password", "_csrf="+csrf.Token("ops"))
|
||||||
|
if rec.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("status = %d, want 400", rec.Code)
|
||||||
|
}
|
||||||
|
if fake.resetCalls != 0 {
|
||||||
|
t.Error("reset must not run without a password")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConsoleOperatorRejectsBadCSRF(t *testing.T) {
|
||||||
|
fake := &fakeOperatorAdmin{}
|
||||||
|
router, _ := operatorsRouter(t, fake)
|
||||||
|
|
||||||
|
rec := consolePost(t, router, "/_gm/operators/root/disable", "")
|
||||||
|
if rec.Code != http.StatusForbidden {
|
||||||
|
t.Fatalf("status = %d, want 403", rec.Code)
|
||||||
|
}
|
||||||
|
if fake.disableCalls != 0 {
|
||||||
|
t.Error("disable must not run without a CSRF token")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConsoleOperatorsUnavailable(t *testing.T) {
|
||||||
|
router, _ := operatorsRouter(t, nil)
|
||||||
|
|
||||||
|
rec := consoleGet(t, router, "/_gm/operators")
|
||||||
|
if rec.Code != http.StatusServiceUnavailable {
|
||||||
|
t.Fatalf("status = %d, want 503", rec.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -408,6 +408,12 @@ func registerAdminConsoleRoutes(router *gin.Engine, deps RouterDependencies) {
|
|||||||
group.GET("/engine-versions", deps.AdminConsole.EngineVersionsList())
|
group.GET("/engine-versions", deps.AdminConsole.EngineVersionsList())
|
||||||
group.POST("/engine-versions", deps.AdminConsole.EngineVersionRegister())
|
group.POST("/engine-versions", deps.AdminConsole.EngineVersionRegister())
|
||||||
group.POST("/engine-versions/:version/disable", deps.AdminConsole.EngineVersionDisable())
|
group.POST("/engine-versions/:version/disable", deps.AdminConsole.EngineVersionDisable())
|
||||||
|
|
||||||
|
group.GET("/operators", deps.AdminConsole.OperatorsList())
|
||||||
|
group.POST("/operators", deps.AdminConsole.OperatorCreate())
|
||||||
|
group.POST("/operators/:username/disable", deps.AdminConsole.OperatorDisable())
|
||||||
|
group.POST("/operators/:username/enable", deps.AdminConsole.OperatorEnable())
|
||||||
|
group.POST("/operators/:username/reset-password", deps.AdminConsole.OperatorResetPassword())
|
||||||
}
|
}
|
||||||
|
|
||||||
// allowedMethodsForPath returns the comma-separated list of methods
|
// allowedMethodsForPath returns the comma-separated list of methods
|
||||||
|
|||||||
Reference in New Issue
Block a user