280 lines
7.9 KiB
Go
280 lines
7.9 KiB
Go
// SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021
|
|
//
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
// Package members implements helpers for accessing the currently logged in admin or moderator of an active request.
|
|
package members
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"go.mindeco.de/http/auth"
|
|
"go.mindeco.de/http/render"
|
|
|
|
"github.com/ssbc/go-ssb-room/v2/roomdb"
|
|
weberrors "github.com/ssbc/go-ssb-room/v2/web/errors"
|
|
authWithSSB "github.com/ssbc/go-ssb-room/v2/web/handlers/auth"
|
|
)
|
|
|
|
type roomMemberContextKeyType string
|
|
|
|
var roomMemberContextKey roomMemberContextKeyType = "ssb:room:httpcontext:member"
|
|
|
|
type Middleware func(next http.Handler) http.Handler
|
|
|
|
// AuthenticateFromContext calls the next http handler if there is a member stored in the context
|
|
// otherwise it will call r.Error
|
|
func AuthenticateFromContext(r *render.Renderer) Middleware {
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
|
if FromContext(req.Context()) == nil {
|
|
r.Error(w, req, http.StatusUnauthorized, weberrors.ErrNotAuthorized)
|
|
return
|
|
}
|
|
next.ServeHTTP(w, req)
|
|
})
|
|
}
|
|
}
|
|
|
|
// FromContext returns the member or nil if not logged in
|
|
func FromContext(ctx context.Context) *roomdb.Member {
|
|
v := ctx.Value(roomMemberContextKey)
|
|
|
|
m, ok := v.(*roomdb.Member)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
return m
|
|
}
|
|
|
|
// ContextInjecter returns middleware for injecting a member into the context of the request.
|
|
// Retreive it using FromContext(ctx)
|
|
func ContextInjecter(mdb roomdb.MembersService, withPassword *auth.Handler, withSSB *authWithSSB.WithSSBHandler) Middleware {
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
|
var (
|
|
member *roomdb.Member
|
|
|
|
errWithPassword, errWithSSB error
|
|
)
|
|
|
|
v, errWithPassword := withPassword.AuthenticateRequest(req)
|
|
if errWithPassword == nil {
|
|
mid, ok := v.(int64)
|
|
if !ok {
|
|
next.ServeHTTP(w, req)
|
|
return
|
|
}
|
|
|
|
m, err := mdb.GetByID(req.Context(), mid)
|
|
if err != nil {
|
|
next.ServeHTTP(w, req)
|
|
return
|
|
}
|
|
member = &m
|
|
}
|
|
|
|
m, errWithSSB := withSSB.AuthenticateRequest(req)
|
|
if errWithSSB == nil {
|
|
member = m
|
|
}
|
|
|
|
// if both methods failed, don't update the context
|
|
if errWithPassword != nil && errWithSSB != nil {
|
|
next.ServeHTTP(w, req)
|
|
return
|
|
}
|
|
|
|
ctx := context.WithValue(req.Context(), roomMemberContextKey, member)
|
|
next.ServeHTTP(w, req.WithContext(ctx))
|
|
})
|
|
}
|
|
}
|
|
|
|
// TemplateHelpers returns functions to be used with the go.mindeco.de/http/render package.
|
|
// Each helper has to return a function twice because the first is evaluated with the request before it gets passed onto html/template's FuncMap.
|
|
//
|
|
// {{ is_logged_in }} returns true or false depending on if the user is logged in
|
|
//
|
|
// {{ member_has_role "string" }} returns a boolean which confrms wether the member has a certain role (RoleMemeber, RoleAdmin, etc)
|
|
//
|
|
// {{ member_is_admin }} is a shortcut for {{ member_has_role "RoleAdmin" }}
|
|
//
|
|
// {{ member_is_elevated }} is a shortcut for {{ or member_has_role "RoleAdmin" member_has_role "RoleModerator"}}
|
|
//
|
|
// {{ member_can "action" }} returns true if a member can execute a certain action. Actions are "invite" and "remove-denied-key". See allowedActions to add more.
|
|
func TemplateHelpers(roomCfg roomdb.RoomConfig) []render.Option {
|
|
|
|
return []render.Option{
|
|
render.InjectTemplateFunc("is_logged_in", func(r *http.Request) interface{} {
|
|
no := func() *roomdb.Member { return nil }
|
|
|
|
member := FromContext(r.Context())
|
|
if member == nil {
|
|
return no
|
|
}
|
|
|
|
yes := func() *roomdb.Member { return member }
|
|
return yes
|
|
}),
|
|
|
|
render.InjectTemplateFunc("member_has_role", func(r *http.Request) interface{} {
|
|
no := func(_ string) bool { return false }
|
|
|
|
member := FromContext(r.Context())
|
|
if member == nil {
|
|
return no
|
|
}
|
|
|
|
return func(has string) bool {
|
|
var r roomdb.Role
|
|
if err := r.UnmarshalText([]byte(has)); err != nil {
|
|
return false
|
|
}
|
|
return member.Role == r
|
|
}
|
|
}),
|
|
|
|
render.InjectTemplateFunc("member_is_admin", func(r *http.Request) interface{} {
|
|
no := func() bool { return false }
|
|
|
|
member := FromContext(r.Context())
|
|
if member == nil {
|
|
return no
|
|
}
|
|
|
|
return func() bool {
|
|
return member.Role == roomdb.RoleAdmin
|
|
}
|
|
}),
|
|
|
|
// shorthand for is admin || mod (used for editing notices, managing users, managing aliases)
|
|
render.InjectTemplateFunc("member_is_elevated", func(r *http.Request) interface{} {
|
|
no := func() bool { return false }
|
|
|
|
member := FromContext(r.Context())
|
|
if member == nil {
|
|
return no
|
|
}
|
|
|
|
return func() bool {
|
|
return member.Role == roomdb.RoleAdmin || member.Role == roomdb.RoleModerator
|
|
}
|
|
}),
|
|
|
|
render.InjectTemplateFunc("member_can", func(r *http.Request) interface{} {
|
|
// evaluate member and privacy mode first to reduce some churn for multiple calls to this helper
|
|
// works fine since they are not changing during one request
|
|
member := FromContext(r.Context())
|
|
if member == nil {
|
|
return func(_ string) bool { return false }
|
|
}
|
|
|
|
pm, err := roomCfg.GetPrivacyMode(r.Context())
|
|
if err != nil {
|
|
return func(_ string) (bool, error) { return false, err }
|
|
}
|
|
|
|
// now return the template func which closes over pm and the member
|
|
return func(what string) (bool, error) {
|
|
actionCheck, has := allowedActionsMap[what]
|
|
if !has {
|
|
return false, fmt.Errorf("unrecognized action: %s", what)
|
|
}
|
|
|
|
return actionCheck(pm, member.Role), nil
|
|
}
|
|
}),
|
|
}
|
|
}
|
|
|
|
// AllowedFunc returns true if a member role is allowed to do a thing under the passed mode
|
|
type AllowedFunc func(mode roomdb.PrivacyMode, role roomdb.Role) bool
|
|
|
|
// AllowedActions exposes check function by name. It exists to protected against changes of the map
|
|
func AllowedActions(name string) (AllowedFunc, bool) {
|
|
fn, has := allowedActionsMap[name]
|
|
return fn, has
|
|
}
|
|
|
|
// member actions
|
|
const (
|
|
ActionInviteMember = "invite"
|
|
ActionChangeDeniedKeys = "change-denied-keys"
|
|
ActionRemoveMember = "remove-member"
|
|
ActionChangeNotice = "change-notice"
|
|
)
|
|
|
|
var allowedActionsMap = map[string]AllowedFunc{
|
|
ActionInviteMember: func(pm roomdb.PrivacyMode, role roomdb.Role) bool {
|
|
switch pm {
|
|
case roomdb.ModeOpen:
|
|
return true
|
|
case roomdb.ModeCommunity:
|
|
return role > roomdb.RoleUnknown && role <= roomdb.RoleAdmin
|
|
case roomdb.ModeRestricted:
|
|
return role == roomdb.RoleAdmin || role == roomdb.RoleModerator
|
|
default:
|
|
return false
|
|
}
|
|
},
|
|
|
|
ActionChangeDeniedKeys: func(pm roomdb.PrivacyMode, role roomdb.Role) bool {
|
|
switch pm {
|
|
case roomdb.ModeCommunity:
|
|
return true
|
|
case roomdb.ModeOpen:
|
|
fallthrough
|
|
case roomdb.ModeRestricted:
|
|
return role == roomdb.RoleAdmin || role == roomdb.RoleModerator
|
|
default:
|
|
return false
|
|
}
|
|
},
|
|
|
|
ActionRemoveMember: func(_ roomdb.PrivacyMode, role roomdb.Role) bool {
|
|
return role == roomdb.RoleAdmin || role == roomdb.RoleModerator
|
|
},
|
|
|
|
ActionChangeNotice: func(pm roomdb.PrivacyMode, role roomdb.Role) bool {
|
|
switch pm {
|
|
case roomdb.ModeCommunity:
|
|
return true
|
|
case roomdb.ModeOpen:
|
|
fallthrough
|
|
case roomdb.ModeRestricted:
|
|
return role == roomdb.RoleAdmin || role == roomdb.RoleModerator
|
|
default:
|
|
return false
|
|
}
|
|
},
|
|
}
|
|
|
|
// CheckAllowed retreives the member from the passed context and lookups the current privacy mode from the passed cfg to determain if the action is okay or not.
|
|
// If it's not it returns an error. For convenience it also returns the member if the action is okay.
|
|
func CheckAllowed(ctx context.Context, cfg roomdb.RoomConfig, action string) (*roomdb.Member, error) {
|
|
member := FromContext(ctx)
|
|
if member == nil {
|
|
return nil, weberrors.ErrNotAuthorized
|
|
}
|
|
|
|
pm, err := cfg.GetPrivacyMode(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
allowed, ok := AllowedActions(action)
|
|
if !ok {
|
|
return nil, fmt.Errorf("unknown action: %s: %w", action, weberrors.ErrNotAuthorized)
|
|
}
|
|
|
|
if !allowed(pm, member.Role) {
|
|
return nil, weberrors.ErrNotAuthorized
|
|
}
|
|
|
|
return member, nil
|
|
}
|