Feature/admin_store (#197)
* generalize admins and email info with store interface * lint: missing group comment * fix admin group with shared substruct
This commit is contained in:
@@ -92,8 +92,8 @@ _this is the recommended way to run remark42_
|
||||
| store.bolt.timeout | STORE_BOLT_TIMEOUT | `30s` | boltdb access timeout |
|
||||
| store.mongo.url | STORE_MONGO_URL | | mongo url for data store |
|
||||
| store.mongo.db | STORE_MONGO_DB | | mongo db for data store |
|
||||
| admin | ADMIN | | admin names (list of user ids), _multi_ |
|
||||
| admin-email | ADMIN_EMAIL | `admin@${REMARK_URL}` | admin email |
|
||||
| admin.shared.id | ADMIN_SHARED_ID | | admin names (list of user ids), _multi_ |
|
||||
| admin.shared.email | ADMIN_SHARED_EMAIL | `admin@${REMARK_URL}` | admin email |
|
||||
| backup | BACKUP_PATH | `./var/backup` | backups location |
|
||||
| max-back | MAX_BACKUP_FILES | `10` | max backup files to keep |
|
||||
| cache.max.items | CACHE_MAX_ITEMS | `1000` | max number of cached items, `0` - unlimited |
|
||||
|
||||
@@ -16,6 +16,9 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Revision sets from main
|
||||
var Revision = "unknown"
|
||||
|
||||
// fileParser used to convert template strings like blah-{{.SITE}}-{{.YYYYMMDD}} the final format
|
||||
type fileParser struct {
|
||||
site string
|
||||
|
||||
+31
-14
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/coreos/bbolt"
|
||||
"github.com/go-pkgz/mongo"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/umputun/remark/backend/app/store/admin"
|
||||
|
||||
"github.com/umputun/remark/backend/app/migrator"
|
||||
"github.com/umputun/remark/backend/app/rest/api"
|
||||
@@ -37,10 +38,9 @@ type ServerCommand struct {
|
||||
Cache CacheGroup `group:"cache" namespace:"cache" env-namespace:"CACHE"`
|
||||
Mongo MongoGroup `group:"mongo" namespace:"mongo" env-namespace:"MONGO"`
|
||||
Key KeyGroup `group:"key" namespace:"key" env-namespace:"KEY"`
|
||||
Admin AdminGroup `group:"admin" namespace:"admin" env-namespace:"ADMIN"`
|
||||
|
||||
Sites []string `long:"site" env:"SITE" default:"remark" description:"site names" env-delim:","`
|
||||
Admins []string `long:"admin" env:"ADMIN" description:"admin(s) names" env-delim:","`
|
||||
AdminEmail string `long:"admin-email" env:"ADMIN_EMAIL" default:"" description:"admin email"`
|
||||
DevPasswd string `long:"dev-passwd" env:"DEV_PASSWD" default:"" description:"development mode password"`
|
||||
BackupLocation string `long:"backup" env:"BACKUP_PATH" default:"./var/backup" description:"backups location"`
|
||||
MaxBackupFiles int `long:"max-back" env:"MAX_BACKUP_FILES" default:"10" description:"max backups to keep"`
|
||||
@@ -52,7 +52,6 @@ type ServerCommand struct {
|
||||
EditDuration time.Duration `long:"edit-time" env:"EDIT_TIME" default:"5m" description:"edit window"`
|
||||
Port int `long:"port" env:"REMARK_PORT" default:"8080" description:"port"`
|
||||
WebRoot string `long:"web-root" env:"REMARK_WEB_ROOT" default:"./web" description:"web root directory"`
|
||||
// Dbg bool `long:"dbg" env:"DEBUG" description:"debug mode"`
|
||||
|
||||
Auth struct {
|
||||
TTL struct {
|
||||
@@ -112,8 +111,14 @@ type KeyGroup struct {
|
||||
Type string `long:"type" env:"TYPE" description:"type of key store" choice:"shared" choice:"mongo" default:"shared"`
|
||||
}
|
||||
|
||||
// Revision sets from main
|
||||
var Revision = "unknown"
|
||||
// AdminGroup defines options group for admin params
|
||||
type AdminGroup struct {
|
||||
Type string `long:"type" env:"TYPE" description:"type of admin store" choice:"shared" choice:"mongo" default:"shared"`
|
||||
Shared struct {
|
||||
Admins []string `long:"id" env:"ID" description:"admin(s) ids" env-delim:","`
|
||||
Email string `long:"email" env:"EMAIL" default:"" description:"admin email"`
|
||||
} `group:"shared" namespace:"shared" env-namespace:"SHARED"`
|
||||
}
|
||||
|
||||
// serverApp holds all active objects
|
||||
type serverApp struct {
|
||||
@@ -174,12 +179,17 @@ func (s *ServerCommand) newServerApp() (*serverApp, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
adminStore, err := s.makeAdminStore()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dataService := &service.DataStore{
|
||||
Interface: storeEngine,
|
||||
EditDuration: s.EditDuration,
|
||||
KeyStore: keyStore,
|
||||
AdminStore: adminStore,
|
||||
MaxCommentSize: s.MaxCommentSize,
|
||||
Admins: s.Admins,
|
||||
}
|
||||
|
||||
loadingCache, err := s.makeCache()
|
||||
@@ -229,7 +239,7 @@ func (s *ServerCommand) newServerApp() (*serverApp, error) {
|
||||
SharedSecret: s.SharedSecret,
|
||||
Authenticator: auth.Authenticator{
|
||||
JWTService: jwtService,
|
||||
AdminEmail: s.AdminEmail,
|
||||
AdminStore: adminStore,
|
||||
Providers: authProviders,
|
||||
DevPasswd: s.DevPasswd,
|
||||
PermissionChecker: dataService,
|
||||
@@ -237,13 +247,6 @@ func (s *ServerCommand) newServerApp() (*serverApp, error) {
|
||||
Cache: loadingCache,
|
||||
}
|
||||
|
||||
// no admin email, use admin@domain
|
||||
if srv.Authenticator.AdminEmail == "" {
|
||||
if u, err := url.Parse(s.RemarkURL); err == nil {
|
||||
srv.Authenticator.AdminEmail = "admin@" + u.Host
|
||||
}
|
||||
}
|
||||
|
||||
srv.ScoreThresholds.Low, srv.ScoreThresholds.Critical = s.LowScore, s.CriticalScore
|
||||
|
||||
var devAuth *auth.DevAuthServer
|
||||
@@ -362,6 +365,20 @@ func (s *ServerCommand) makeKeyStore() (keys.Store, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ServerCommand) makeAdminStore() (admin.Store, error) {
|
||||
switch s.Admin.Type {
|
||||
case "shared":
|
||||
if s.Admin.Shared.Email == "" { // no admin email, use admin@domain
|
||||
if u, err := url.Parse(s.RemarkURL); err == nil {
|
||||
s.Admin.Shared.Email = "admin@" + u.Host
|
||||
}
|
||||
}
|
||||
return admin.NewStaticStore(s.Admin.Shared.Admins, s.Admin.Shared.Email), nil
|
||||
default:
|
||||
return nil, errors.Errorf("unsupported admin store type %s", s.Key.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ServerCommand) makeCache() (cache.LoadingCache, error) {
|
||||
switch s.Cache.Type {
|
||||
case "mem":
|
||||
|
||||
@@ -45,7 +45,7 @@ func TestServerApp(t *testing.T) {
|
||||
body, _ = ioutil.ReadAll(resp.Body)
|
||||
t.Log(string(body))
|
||||
|
||||
assert.Equal(t, "admin@demo.remark42.com", app.restSrv.Authenticator.AdminEmail, "default admin email")
|
||||
assert.Equal(t, "admin@demo.remark42.com", app.restSrv.Authenticator.AdminStore.Email(""), "default admin email")
|
||||
|
||||
app.Wait()
|
||||
}
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ import (
|
||||
"github.com/umputun/remark/backend/app/cmd"
|
||||
)
|
||||
|
||||
// Opts has all commands
|
||||
// Opts with all cli commands and flags
|
||||
type Opts struct {
|
||||
ServerCmd cmd.ServerCommand `command:"server"`
|
||||
ImportCmd cmd.ImportCommand `command:"import"`
|
||||
|
||||
@@ -71,9 +71,10 @@ type commentsWithInfo struct {
|
||||
func (s *Rest) Run(port int) {
|
||||
log.Printf("[INFO] activate rest server on port %d", port)
|
||||
|
||||
if s.DataService != nil && len(s.DataService.Admins) > 0 {
|
||||
log.Printf("[DEBUG] admins %+v", s.DataService.Admins)
|
||||
}
|
||||
// TODO: restore admin info dbg
|
||||
//if s.DataService != nil && len(s.DataService.AdminStore.Admins()) > 0 {
|
||||
// log.Printf("[DEBUG] admins %+v", s.DataService.Admins)
|
||||
//}
|
||||
|
||||
router := s.routes()
|
||||
|
||||
|
||||
@@ -195,6 +195,8 @@ func (s *Rest) findUserCommentsCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// GET /config?site=siteID - returns configuration
|
||||
func (s *Rest) configCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
siteID := r.URL.Query().Get("site")
|
||||
|
||||
type config struct {
|
||||
Version string `json:"version"`
|
||||
EditDuration int `json:"edit_duration"`
|
||||
@@ -211,8 +213,8 @@ func (s *Rest) configCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
Version: s.Version,
|
||||
EditDuration: int(s.DataService.EditDuration.Seconds()),
|
||||
MaxCommentSize: s.DataService.MaxCommentSize,
|
||||
Admins: s.DataService.Admins,
|
||||
AdminEmail: s.Authenticator.AdminEmail,
|
||||
Admins: s.DataService.AdminStore.Admins(siteID),
|
||||
AdminEmail: s.DataService.AdminStore.Email(siteID),
|
||||
LowScore: s.ScoreThresholds.Low,
|
||||
CriticalScore: s.ScoreThresholds.Critical,
|
||||
ReadOnlyAge: s.ReadOnlyAge,
|
||||
|
||||
@@ -14,15 +14,16 @@ import (
|
||||
"github.com/coreos/bbolt"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/umputun/remark/backend/app/store/keys"
|
||||
|
||||
"github.com/umputun/remark/backend/app/migrator"
|
||||
"github.com/umputun/remark/backend/app/rest/auth"
|
||||
"github.com/umputun/remark/backend/app/rest/cache"
|
||||
"github.com/umputun/remark/backend/app/rest/proxy"
|
||||
"github.com/umputun/remark/backend/app/store"
|
||||
adminstore "github.com/umputun/remark/backend/app/store/admin"
|
||||
"github.com/umputun/remark/backend/app/store/avatar"
|
||||
"github.com/umputun/remark/backend/app/store/engine"
|
||||
"github.com/umputun/remark/backend/app/store/keys"
|
||||
"github.com/umputun/remark/backend/app/store/service"
|
||||
)
|
||||
|
||||
@@ -90,20 +91,22 @@ func TestRest_filterComments(t *testing.T) {
|
||||
func prep(t *testing.T) (srv *Rest, ts *httptest.Server) {
|
||||
b, err := engine.NewBoltDB(bolt.Options{}, engine.BoltSite{FileName: testDb, SiteID: "radio-t"})
|
||||
require.Nil(t, err)
|
||||
|
||||
adminStore := adminstore.NewStaticStore([]string{"a1", "a2"}, "admin@remark-42.com")
|
||||
|
||||
dataStore := &service.DataStore{
|
||||
Interface: b,
|
||||
EditDuration: 5 * time.Minute,
|
||||
MaxCommentSize: 4000,
|
||||
KeyStore: keys.NewStaticStore("123456"),
|
||||
Admins: []string{"a1", "a2"},
|
||||
AdminStore: adminStore,
|
||||
}
|
||||
srv = &Rest{
|
||||
DataService: dataStore,
|
||||
Authenticator: auth.Authenticator{
|
||||
DevPasswd: "password",
|
||||
Providers: nil,
|
||||
|
||||
AdminEmail: "admin@remark-42.com",
|
||||
DevPasswd: "password",
|
||||
Providers: nil,
|
||||
AdminStore: adminStore,
|
||||
JWTService: auth.NewJWT(keys.NewStaticStore("123456"), false, time.Minute, time.Hour),
|
||||
},
|
||||
Exporter: &migrator.Remark{DataStore: dataStore},
|
||||
|
||||
@@ -9,13 +9,14 @@ import (
|
||||
|
||||
"github.com/umputun/remark/backend/app/rest"
|
||||
"github.com/umputun/remark/backend/app/store"
|
||||
"github.com/umputun/remark/backend/app/store/admin"
|
||||
)
|
||||
|
||||
// Authenticator is top level auth object providing middlewares
|
||||
type Authenticator struct {
|
||||
JWTService *JWT
|
||||
Providers []Provider
|
||||
AdminEmail string
|
||||
AdminStore admin.Store
|
||||
DevPasswd string
|
||||
PermissionChecker PermissionChecker
|
||||
}
|
||||
@@ -31,7 +32,7 @@ var devUser = store.User{
|
||||
type PermissionChecker interface {
|
||||
IsVerified(siteID, userID string) bool
|
||||
IsBlocked(siteID, userID string) bool
|
||||
IsAdmin(userID string) bool
|
||||
IsAdmin(siteID, userID string) bool
|
||||
}
|
||||
|
||||
// Auth middleware adds auth from session and populates user info
|
||||
@@ -98,7 +99,7 @@ func (a *Authenticator) Auth(reqAuth bool) func(http.Handler) http.Handler {
|
||||
|
||||
func (a *Authenticator) refreshExpiredToken(w http.ResponseWriter, claims *CustomClaims) (*CustomClaims, error) {
|
||||
if a.PermissionChecker != nil {
|
||||
claims.User.Admin = a.PermissionChecker.IsAdmin(claims.User.ID)
|
||||
claims.User.Admin = a.PermissionChecker.IsAdmin(claims.SiteID, claims.User.ID)
|
||||
claims.User.Blocked = a.PermissionChecker.IsBlocked(claims.SiteID, claims.User.ID)
|
||||
claims.User.Verified = a.PermissionChecker.IsVerified(claims.SiteID, claims.User.ID)
|
||||
}
|
||||
|
||||
@@ -203,7 +203,7 @@ func (p Provider) setAvatar(u store.User) store.User {
|
||||
|
||||
// setPermissions sets permission fields not handled by provider's MapUser, things like admin, verified and blocked
|
||||
func (p Provider) setPermissions(u store.User, siteID string) store.User {
|
||||
u.Admin = p.PermissionChecker.IsAdmin(u.ID)
|
||||
u.Admin = p.PermissionChecker.IsAdmin(siteID, u.ID)
|
||||
u.Verified = p.PermissionChecker.IsVerified(siteID, u.ID)
|
||||
u.Blocked = p.PermissionChecker.IsBlocked(siteID, u.ID)
|
||||
log.Printf("[DEBUG] set permissions for user %s, site %s - %+v", u.ID, siteID, u)
|
||||
|
||||
@@ -233,6 +233,6 @@ type mockUserPermissions struct {
|
||||
blocked string
|
||||
}
|
||||
|
||||
func (m *mockUserPermissions) IsAdmin(userID string) bool { return userID == m.admin }
|
||||
func (m *mockUserPermissions) IsAdmin(siteID, userID string) bool { return userID == m.admin }
|
||||
func (m *mockUserPermissions) IsVerified(siteID, userID string) bool { return userID == m.verified }
|
||||
func (m *mockUserPermissions) IsBlocked(siteID, userID string) bool { return userID == m.blocked }
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package admin
|
||||
|
||||
// Store defines interface returning admins info for given site
|
||||
type Store interface {
|
||||
Admins(siteID string) (ids []string)
|
||||
Email(siteID string) (email string)
|
||||
}
|
||||
|
||||
// StaticStore implements keys.Store with a single, predefined key
|
||||
type StaticStore struct {
|
||||
admins []string
|
||||
email string
|
||||
}
|
||||
|
||||
// NewStaticStore makes StaticStore instance with given key
|
||||
func NewStaticStore(admins []string, email string) *StaticStore {
|
||||
return &StaticStore{admins: admins, email: email}
|
||||
}
|
||||
|
||||
// Admins returns static list of admin's ids, the same for all sites
|
||||
func (s *StaticStore) Admins(string) (ids []string) {
|
||||
return s.admins
|
||||
}
|
||||
|
||||
// Email gets static email address
|
||||
func (s *StaticStore) Email(string) (email string) {
|
||||
return s.email
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestStaticStore_Get(t *testing.T) {
|
||||
var ks Store = NewStaticStore([]string{"123", "xyz"}, "aa@example.com")
|
||||
|
||||
a := ks.Admins("any")
|
||||
assert.Equal(t, []string{"123", "xyz"}, a)
|
||||
|
||||
email := ks.Email("blah")
|
||||
assert.Equal(t, "aa@example.com", email)
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/umputun/remark/backend/app/store/admin"
|
||||
|
||||
"github.com/umputun/remark/backend/app/store"
|
||||
"github.com/umputun/remark/backend/app/store/engine"
|
||||
@@ -17,8 +18,8 @@ type DataStore struct {
|
||||
engine.Interface
|
||||
EditDuration time.Duration
|
||||
KeyStore keys.Store
|
||||
AdminStore admin.Store
|
||||
MaxCommentSize int
|
||||
Admins []string
|
||||
|
||||
// granular locks
|
||||
scopedLocks struct {
|
||||
@@ -32,6 +33,16 @@ const defaultCommentMaxSize = 2000
|
||||
|
||||
// Create prepares comment and forward to Interface.Create
|
||||
func (s *DataStore) Create(comment store.Comment) (commentID string, err error) {
|
||||
|
||||
if comment, err = s.prepareNewComment(comment); err != nil {
|
||||
return "", errors.Wrap(err, "failed to prepare comment")
|
||||
}
|
||||
|
||||
return s.Interface.Create(comment)
|
||||
}
|
||||
|
||||
// prepareNewComment sets new comment fields, hashing and sanitizing data
|
||||
func (s *DataStore) prepareNewComment(comment store.Comment) (store.Comment, error) {
|
||||
// fill ID and time if empty
|
||||
if comment.ID == "" {
|
||||
comment.ID = uuid.New().String()
|
||||
@@ -47,11 +58,10 @@ func (s *DataStore) Create(comment store.Comment) (commentID string, err error)
|
||||
|
||||
secret, err := s.KeyStore.Get(comment.Locator.SiteID)
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "can't get secret for site %s", comment.Locator.SiteID)
|
||||
return store.Comment{}, errors.Wrapf(err, "can't get secret for site %s", comment.Locator.SiteID)
|
||||
}
|
||||
comment.User.HashIP(secret) // replace ip by hash
|
||||
|
||||
return s.Interface.Create(comment)
|
||||
return comment, nil
|
||||
}
|
||||
|
||||
// SetPin pin/un-pin comment as special
|
||||
@@ -170,9 +180,9 @@ func (s *DataStore) ValidateComment(c *store.Comment) error {
|
||||
}
|
||||
|
||||
// IsAdmin checks if usesID in the list of admins
|
||||
func (s *DataStore) IsAdmin(userID string) bool {
|
||||
for _, admin := range s.Admins {
|
||||
if admin == userID {
|
||||
func (s *DataStore) IsAdmin(siteID string, userID string) bool {
|
||||
for _, a := range s.AdminStore.Admins(siteID) {
|
||||
if a == userID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user