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:
Umputun
2018-09-02 13:00:20 -05:00
committed by GitHub
parent 78c27caa7d
commit 0cd76dfd2f
14 changed files with 123 additions and 41 deletions
+2 -2
View File
@@ -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 |
+3
View File
@@ -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
View File
@@ -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":
+1 -1
View File
@@ -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
View File
@@ -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"`
+4 -3
View File
@@ -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()
+4 -2
View File
@@ -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,
+9 -6
View File
@@ -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},
+4 -3
View File
@@ -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)
}
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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 }
+28
View File
@@ -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
}
+17
View File
@@ -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)
}
+17 -7
View File
@@ -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
}
}