go-ssb-room/web/members/helper.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
}