Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,9 @@ admin_github_id=12345 # numeric id: https://api.github.com/users
Other accounts can still log in as regular users but are never promoted. Leave both unset to keep the first-to-login behaviour.

### Managing Admins
The pin above only decides who becomes the *first* admin. After that, admins are managed from the **Users** page in the admin area (`/admin/users`), which lists everyone who has logged in. An existing admin can promote any GitHub user to admin or demote another admin (including the wizard's password account); the last remaining admin can't be demoted, so the site never ends up with none. Email-login users can't be made admin (see #565).
The pin above only decides who becomes the *first* admin. After that, admins are managed from the **Users** page in the admin area (`/admin/users`), which lists everyone who has logged in. An existing admin can promote any user (GitHub or email login) to admin or demote another admin (including the wizard's password account); the last remaining admin can't be demoted, so the site never ends up with none. An email-login admin is only as secure as that mailbox, so promote only addresses you trust.

To hand the site over to a different GitHub account: log in with the new account once so it appears in the list, promote it from your current admin account, then log in as the new account and demote the old one.
To hand the site over to a different account: log in with the new account once so it appears in the list, promote it from your current admin account, then log in as the new account and demote the old one.

### Email Login (one-time codes)
Visitors without a GitHub account can log in with an emailed 6-digit code. Add SMTP details to `.env`:
Expand All @@ -132,7 +132,7 @@ smtp_user=postmaster@example.com # omit for an unauthenticated relay
smtp_password=...
smtp_from=blog@example.com
```
When `smtp_host` and `smtp_from` are both set the login page offers "sign in with email"; otherwise it shows GitHub only. Codes expire after 10 minutes, allow 5 wrong attempts, and can be re-requested once a minute. Email users are regular users — they cannot be made admin (see above).
When `smtp_host` and `smtp_from` are both set the login page offers "sign in with email"; otherwise it shows GitHub only. Codes expire after 10 minutes, allow 5 wrong attempts, and can be re-requested once a minute. Email users start as regular users; an admin can promote them (see above).

SMTP settings are read once at startup, so restart goblog after changing any `smtp_*` value in `.env` for the change to take effect. Go's SMTP client only sends `smtp_user`/`smtp_password` over an encrypted connection (STARTTLS, or implicit TLS on port 465) unless the host is `localhost`, so if you need an unencrypted remote relay, use it without credentials.

Expand Down
2 changes: 0 additions & 2 deletions admin/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -1068,8 +1068,6 @@ func (a *Admin) PromoteAdmin(c *gin.Context) {
c.JSON(http.StatusOK, "")
case errors.Is(err, gorm.ErrRecordNotFound):
c.JSON(http.StatusNotFound, "No such user")
case errors.Is(err, auth.ErrNotPromotable):
c.JSON(http.StatusBadRequest, err.Error())
default:
log.Println("ERROR PROMOTING ADMIN: ", err)
c.JSON(http.StatusInternalServerError, "Error promoting user")
Expand Down
8 changes: 4 additions & 4 deletions admin/admin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1090,8 +1090,8 @@ func TestAdminUsers_RendersUsers(t *testing.T) {
if r := row(plain); !control("promoteAdmin", plain).MatchString(r) {
t.Errorf("github non-admin must have a promote control: %s", r)
}
if r := row(mailer); strings.Contains(r, "promoteAdmin(") || strings.Contains(r, "demoteAdmin(") {
t.Errorf("email user must have no controls: %s", r)
if r := row(mailer); !control("promoteAdmin", mailer).MatchString(r) {
t.Errorf("email non-admin must have a promote control: %s", r)
}
if !strings.Contains(body, "3 users") {
t.Errorf("expected a total in the summary")
Expand Down Expand Up @@ -1161,8 +1161,8 @@ func TestPromoteAdminAPI(t *testing.T) {
if n != 1 {
t.Fatalf("expected plain to be admin after promotion")
}
if w := adminsJSON(router, "POST", mailer.ID); w.Code != http.StatusBadRequest {
t.Errorf("promote email user: expected 400, got %d", w.Code)
if w := adminsJSON(router, "POST", mailer.ID); w.Code != http.StatusOK {
t.Errorf("promote email user: expected 200, got %d", w.Code)
}
if w := adminsJSON(router, "POST", 999); w.Code != http.StatusNotFound {
t.Errorf("promote unknown user: expected 404, got %d", w.Code)
Expand Down
14 changes: 4 additions & 10 deletions auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -342,10 +342,6 @@ func (a *Auth) EnsureAdmin(user *BlogUser) error {
})
}

// ErrNotPromotable is returned by PromoteAdmin for users who can't hold
// admin: only GitHub users can (email login is a weaker credential, see #565).
var ErrNotPromotable = errors.New("only GitHub users can be admin")

// ErrLastAdmin is returned by DemoteAdmin when the demotion would leave the
// site with no admin at all, which would also re-open the install wizard
// (IsWizardMode is "no admin_users row").
Expand Down Expand Up @@ -388,18 +384,16 @@ func (a *Auth) ListUsers(offset, limit int) ([]UserListing, int64, error) {

// PromoteAdmin makes the given user an admin. Unlike EnsureAdmin this is not
// gated on the admin_login / admin_github_id pin: that pin only governs who
// bootstraps the first admin, after which existing admins decide. Returns
// gorm.ErrRecordNotFound for an unknown user and ErrNotPromotable for a
// non-GitHub user. Idempotent.
// bootstraps the first admin, after which existing admins decide. Any provider
// can be promoted: an email login is as strong as its mailbox, which is also
// what a GitHub account falls back on for recovery (#565). Returns
// gorm.ErrRecordNotFound for an unknown user. Idempotent.
func (a *Auth) PromoteAdmin(userID int) error {
return (*a.db).Transaction(func(tx *gorm.DB) error {
var user BlogUser
if err := tx.First(&user, userID).Error; err != nil {
return err
}
if user.Provider != ProviderGitHub {
return ErrNotPromotable
}
var existing AdminUser
err := tx.Where("blog_user_id = ?", userID).First(&existing).Error
if err == nil {
Expand Down
11 changes: 5 additions & 6 deletions auth/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -395,17 +395,16 @@ func TestPromoteAdmin_Idempotent(t *testing.T) {
}
}

func TestPromoteAdmin_EmailUser_Refused(t *testing.T) {
func TestPromoteAdmin_EmailUser_AddsRow(t *testing.T) {
a, db := newAuth(t)
u := auth.BlogUser{Provider: auth.ProviderEmail, ProviderID: "a@example.com", Login: "a@example.com"}
db.Create(&u)

err := a.PromoteAdmin(u.ID)
if !errors.Is(err, auth.ErrNotPromotable) {
t.Fatalf("expected ErrNotPromotable, got %v", err)
if err := a.PromoteAdmin(u.ID); err != nil {
t.Fatalf("PromoteAdmin: %v", err)
}
if n := countAdmins(t, db); n != 0 {
t.Fatalf("expected no admin rows, got %d", n)
if n := countAdmins(t, db); n != 1 {
t.Fatalf("expected 1 admin row, got %d", n)
}
}

Expand Down
4 changes: 2 additions & 2 deletions themes/default/templates/admin_users.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<div class="admin-panel">
<h1>Users</h1>
<p class="text-muted">Page {{ .page }} of {{ .total_pages }} &middot; {{ .total }} user{{ if ne .total 1 }}s{{ end }}</p>
<p class="text-muted">Admins can manage posts, pages, comments, settings and other users. Only GitHub users can be made admin, and the last admin can't be demoted.</p>
<p class="text-muted">Admins can manage posts, pages, comments, settings and other users. An email-login admin is only as secure as that mailbox. The last admin can't be demoted.</p>

{{ if .users }}
<table class="table">
Expand All @@ -27,7 +27,7 @@ <h1>Users</h1>
<td>
{{ if .IsAdmin }}
{{ if $.can_demote }}<a href="javascript:void(0)" onclick="demoteAdmin({{ .ID }})" class="text-danger">[demote]</a>{{ end }}
{{ else if eq .Provider "github" }}
{{ else }}
<a href="javascript:void(0)" onclick="promoteAdmin({{ .ID }})">[promote to admin]</a>
{{ end }}
</td>
Expand Down
Loading