Skip to content

Commit

Permalink
[GH-84] Add the concept of plugin admins (#93)
Browse files Browse the repository at this point in the history
* Add the concept of plugin admins

* Review fixes #1

Co-Authored-By: Jason Frerich <[email protected]>

* Review fixes #2

Co-authored-by: Jason Frerich <[email protected]>
  • Loading branch information
vespian and jfrerich authored Mar 23, 2020
1 parent 5e53e3f commit a10f0ee
Show file tree
Hide file tree
Showing 7 changed files with 295 additions and 26 deletions.
6 changes: 6 additions & 0 deletions plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@
"display_name": "Apply plugin to updated posts as well as new posts",
"type": "bool",
"default": false
},
{
"key": "PluginAdmins",
"display_name": "Admin User IDs",
"type": "text",
"help_text": "Comma-separated list of userIDs authorized to administer the plugin in addition to the System Admins.\n \nUser IDs can be found by navigating to **System Console** > **Users**."
}
]
}
Expand Down
2 changes: 1 addition & 1 deletion server/autolink/autolink_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (
)

func setupTestPlugin(t *testing.T, l autolink.Autolink) *autolinkplugin.Plugin {
p := &autolinkplugin.Plugin{}
p := autolinkplugin.New()
api := &plugintest.API{}

api.On("GetChannel", mock.AnythingOfType("string")).Run(func(args mock.Arguments) {
Expand Down
12 changes: 7 additions & 5 deletions server/autolinkplugin/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,17 +66,19 @@ func (ch CommandHandler) Handle(p *Plugin, c *plugin.Context, header *model.Comm
}

func (p *Plugin) ExecuteCommand(c *plugin.Context, commandArgs *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
user, appErr := p.API.GetUser(commandArgs.UserId)
if appErr != nil {
return responsef("%v", appErr.Error()), nil
isAdmin, err := p.IsAuthorizedAdmin(commandArgs.UserId)
if err != nil {
return responsef("error occured while authorizing the command: %v", err), nil
}
if !strings.Contains(user.Roles, "system_admin") {
return responsef("`/autolink` can only be executed by a system administrator."), nil
if !isAdmin {
return responsef("`/autolink` commands can only be executed by a system administrator or `autolink` plugin admins."), nil
}

args := strings.Fields(commandArgs.Command)
if len(args) == 0 || args[0] != "/autolink" {
return responsef(helpText), nil
}

return autolinkCommandHandler.Handle(p, c, commandArgs, args[1:]...), nil
}

Expand Down
58 changes: 47 additions & 11 deletions server/autolinkplugin/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,34 @@ import (
type Config struct {
EnableAdminCommand bool
EnableOnUpdate bool
PluginAdmins string
Links []autolink.Autolink

// AdminUserIds is a set of UserIds that are permitted to perform
// administrative operations on the plugin configuration (i.e. plugin
// admins). On each configuration change the contents of PluginAdmins
// config field is parsed into this field.
AdminUserIds map[string]struct{}
}

// OnConfigurationChange is invoked when configuration changes may have been made.
func (p *Plugin) OnConfigurationChange() error {
var c Config
err := p.API.LoadPluginConfiguration(&c)
if err != nil {
return err
if err := p.API.LoadPluginConfiguration(&c); err != nil {
return fmt.Errorf("failed to load configuration: %w", err)
}

for i := range c.Links {
err = c.Links[i].Compile()
if err != nil {
if err := c.Links[i].Compile(); err != nil {
mlog.Error(fmt.Sprintf("Error creating autolinker: %+v: %v", c.Links[i], err))
}
}

// Plugin admin UserId parsing and validation errors are
// not fatal, if everything fails only sysadmin will be able to manage the
// config which is still OK
c.parsePluginAdminList(p)

p.UpdateConfig(func(conf *Config) {
*conf = c
})
Expand All @@ -54,15 +64,17 @@ func (p *Plugin) OnConfigurationChange() error {
return nil
}

func (p *Plugin) getConfig() Config {
func (p *Plugin) getConfig() *Config {
p.confLock.RLock()
defer p.confLock.RUnlock()

return p.conf
}

func (p *Plugin) GetLinks() []autolink.Autolink {
p.confLock.RLock()
defer p.confLock.RUnlock()

return p.conf.Links
}

Expand All @@ -78,34 +90,58 @@ func (p *Plugin) SaveLinks(links []autolink.Autolink) error {
return nil
}

func (p *Plugin) UpdateConfig(f func(conf *Config)) Config {
func (p *Plugin) UpdateConfig(f func(conf *Config)) {
p.confLock.Lock()
defer p.confLock.Unlock()

f(&p.conf)
return p.conf
f(p.conf)
}

// ToConfig marshals Config into a tree of map[string]interface{} to pass down
// to p.API.SavePluginConfig, otherwise RPC/gob barfs at the unknown type.
func (conf Config) ToConfig() map[string]interface{} {
func (conf *Config) ToConfig() map[string]interface{} {
links := []interface{}{}
for _, l := range conf.Links {
links = append(links, l.ToConfig())
}
return map[string]interface{}{
"EnableAdminCommand": conf.EnableAdminCommand,
"EnableOnUpdate": conf.EnableOnUpdate,
"PluginAdmins": conf.PluginAdmins,
"Links": links,
}
}

// Sorted returns a clone of the Config, with links sorted alphabetically
func (conf Config) Sorted() Config {
func (conf *Config) Sorted() *Config {
sorted := conf
sorted.Links = append([]autolink.Autolink{}, conf.Links...)
sort.Slice(conf.Links, func(i, j int) bool {
return strings.Compare(conf.Links[i].DisplayName(), conf.Links[j].DisplayName()) < 0
})
return conf
}

// parsePluginAdminList parses the contents of PluginAdmins config field
func (conf *Config) parsePluginAdminList(p *Plugin) {
conf.AdminUserIds = make(map[string]struct{})

if len(conf.PluginAdmins) == 0 {
// There were no plugin admin users defined
return
}

userIDs := strings.Split(conf.PluginAdmins, ",")

for _, v := range userIDs {
userId := strings.TrimSpace(v)
// Let's verify that the given user really exists
_, appErr := p.API.GetUser(userId)
if appErr != nil {
mlog.Error(fmt.Sprintf(
"error occured while verifying userId %s: %v", v, appErr))
} else {
conf.AdminUserIds[userId] = struct{}{}
}
}
}
25 changes: 21 additions & 4 deletions server/autolinkplugin/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,41 @@ type Plugin struct {
handler *api.Handler

// configuration and a muttex to control concurrent access
conf Config
conf *Config
confLock sync.RWMutex
}

func New() *Plugin {
return &Plugin{
conf: new(Config),
}
}

func (p *Plugin) OnActivate() error {
p.handler = api.NewHandler(p, p)

return nil
}

func (p *Plugin) IsAuthorizedAdmin(mattermostID string) (bool, error) {
user, err := p.API.GetUser(mattermostID)
func (p *Plugin) IsAuthorizedAdmin(userId string) (bool, error) {
user, err := p.API.GetUser(userId)
if err != nil {
return false, err
return false, fmt.Errorf(
"failed to obtain information about user `%s`: %w", userId, err)
}
if strings.Contains(user.Roles, "system_admin") {
mlog.Info(
fmt.Sprintf("UserId `%s` is authorized basing on the sysadmin role membership", userId))
return true, nil
}

conf := p.getConfig()
if _, ok := conf.AdminUserIds[userId]; ok {
mlog.Info(
fmt.Sprintf("UserId `%s` is authorized basing on the list of plugin admins list", userId))
return true, nil
}

return false, nil
}

Expand Down
Loading

0 comments on commit a10f0ee

Please sign in to comment.