server admin panel is now supports multiple operators profiles
This commit is contained in:
parent
e48160ac3a
commit
280321b902
25 changed files with 2197 additions and 128 deletions
125
cmd/telesrv-admin/adminusers.go
Normal file
125
cmd/telesrv-admin/adminusers.go
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
// AdminConsoleUser is one named panel operator. It deliberately never carries
|
||||
// the password hash outside authentication: everything that renders or returns
|
||||
// a user uses this shape, so a hash cannot leak into an API response by
|
||||
// someone adding a field to a JSON struct.
|
||||
type AdminConsoleUser struct {
|
||||
ID int64 `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Permissions []string `json:"permissions"`
|
||||
Enabled bool `json:"enabled"`
|
||||
TokenEpoch int32 `json:"token_epoch"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
LastLoginAt *time.Time `json:"last_login_at,omitempty"`
|
||||
}
|
||||
|
||||
// adminConsoleCredential is the authentication-only view: the hash plus the
|
||||
// few fields a login decision needs. Kept unexported and separate from
|
||||
// AdminConsoleUser so the hash has exactly one reason to be read.
|
||||
type adminConsoleCredential struct {
|
||||
ID int64
|
||||
Username string
|
||||
PasswordHash string
|
||||
Permissions []string
|
||||
Enabled bool
|
||||
TokenEpoch int32
|
||||
}
|
||||
|
||||
// errAdminUserNotFound is returned instead of pgx.ErrNoRows so callers can
|
||||
// treat "no such operator" without importing pgx.
|
||||
var errAdminUserNotFound = errors.New("admin console user not found")
|
||||
|
||||
const adminConsoleUserColumns = `id, username, permissions, enabled, token_epoch, created_at, updated_at, last_login_at`
|
||||
|
||||
// AdminConsoleCredentialByUsername loads the authentication view for a login
|
||||
// attempt. The lookup is case-insensitive to match the unique index, so an
|
||||
// operator cannot be shadowed by a differently-cased duplicate.
|
||||
func (s *readStore) AdminConsoleCredentialByUsername(ctx context.Context, username string) (adminConsoleCredential, error) {
|
||||
var out adminConsoleCredential
|
||||
err := s.pool.QueryRow(ctx, `
|
||||
SELECT id, username, password_hash, permissions, enabled, token_epoch
|
||||
FROM admin_console_users
|
||||
WHERE lower(username) = lower($1)`, strings.TrimSpace(username)).Scan(
|
||||
&out.ID, &out.Username, &out.PasswordHash, &out.Permissions, &out.Enabled, &out.TokenEpoch)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return adminConsoleCredential{}, errAdminUserNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return adminConsoleCredential{}, fmt.Errorf("load admin console credential: %w", err)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// AdminConsoleSessionState re-reads the two things a live session depends on.
|
||||
// requireAuthAPI calls it per request so that disabling an operator, editing
|
||||
// their rights or changing their password takes effect immediately rather than
|
||||
// whenever their signed cookie happens to expire.
|
||||
func (s *readStore) AdminConsoleSessionState(ctx context.Context, id int64) (enabled bool, epoch int32, permissions []string, err error) {
|
||||
err = s.pool.QueryRow(ctx, `
|
||||
SELECT enabled, token_epoch, permissions FROM admin_console_users WHERE id = $1`, id).
|
||||
Scan(&enabled, &epoch, &permissions)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return false, 0, nil, errAdminUserNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return false, 0, nil, fmt.Errorf("load admin console session state: %w", err)
|
||||
}
|
||||
return enabled, epoch, permissions, nil
|
||||
}
|
||||
|
||||
// ListAdminConsoleUsers returns every operator, newest last so the list reads
|
||||
// like the order they were added.
|
||||
func (s *readStore) ListAdminConsoleUsers(ctx context.Context) ([]AdminConsoleUser, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT `+adminConsoleUserColumns+` FROM admin_console_users ORDER BY id`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list admin console users: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
out := []AdminConsoleUser{}
|
||||
for rows.Next() {
|
||||
var u AdminConsoleUser
|
||||
if err := rows.Scan(&u.ID, &u.Username, &u.Permissions, &u.Enabled,
|
||||
&u.TokenEpoch, &u.CreatedAt, &u.UpdatedAt, &u.LastLoginAt); err != nil {
|
||||
return nil, fmt.Errorf("scan admin console user: %w", err)
|
||||
}
|
||||
if u.Permissions == nil {
|
||||
u.Permissions = []string{}
|
||||
}
|
||||
out = append(out, u)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("iterate admin console users: %w", err)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// CountEnabledAdminConsoleUsersWith reports how many enabled operators hold a
|
||||
// given permission, counting the '*' wildcard as holding everything. It exists
|
||||
// for the last-administrator guard: the panel refuses the edit that would
|
||||
// leave nobody able to manage operators.
|
||||
func (s *readStore) CountEnabledAdminConsoleUsersWith(ctx context.Context, permission string, excludeID int64) (int, error) {
|
||||
var n int
|
||||
if err := s.pool.QueryRow(ctx, `
|
||||
SELECT count(*)::int FROM admin_console_users
|
||||
WHERE enabled
|
||||
AND id <> $2
|
||||
AND (permissions @> ARRAY[$1]::text[] OR permissions @> ARRAY['*']::text[])`,
|
||||
permission, excludeID).Scan(&n); err != nil {
|
||||
return 0, fmt.Errorf("count admin console users with permission: %w", err)
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue