This commit is contained in:
Daniel Ponte 2025-01-20 20:28:25 -05:00
parent a9f64f74fb
commit b171e8431a
12 changed files with 426 additions and 397 deletions

View file

@ -123,7 +123,7 @@ func (s *store) AddCall(ctx context.Context, call *calls.Call) error {
}
func (s *store) CallAudio(ctx context.Context, id uuid.UUID) (*calls.CallAudio, error) {
_, err := rbac.Check(ctx, rbac.UseResource(rbac.ResourceCall), rbac.WithActions(rbac.ActionRead))
_, err := rbac.Check(ctx, &calls.Call{ID: id}, rbac.WithActions(rbac.ActionRead))
if err != nil {
return nil, err
}

View file

@ -17,6 +17,7 @@ type Configuration struct {
type Config struct {
BaseURL jsontypes.URL `yaml:"baseURL"`
DumpRoutes bool `yaml:"dumpRoutes"`
DB DB `yaml:"db"`
CORS CORS `yaml:"cors"`
Auth Auth `yaml:"auth"`

View file

@ -278,7 +278,7 @@ func fromDBCalls(d []database.GetIncidentCallsRow) []incidents.IncidentCall {
}
func (s *store) Incident(ctx context.Context, id uuid.UUID) (*incidents.Incident, error) {
_, err := rbac.Check(ctx, new(incidents.Incident), rbac.WithActions(rbac.ActionRead))
_, err := rbac.Check(ctx, &incidents.Incident{ID: id}, rbac.WithActions(rbac.ActionRead))
if err != nil {
return nil, err
}

94
pkg/rbac/conditions.go Normal file
View file

@ -0,0 +1,94 @@
package rbac
import (
"fmt"
"reflect"
"github.com/el-mike/restrict/v2"
)
const (
SubmitterEqualConditionType = "SUBMITTER_EQUAL"
InMapConditionType = "IN_MAP"
)
type SubmitterEqualCondition struct {
ID string `json:"name,omitempty" yaml:"name,omitempty"`
Left *restrict.ValueDescriptor `json:"left" yaml:"left"`
Right *restrict.ValueDescriptor `json:"right" yaml:"right"`
}
func (s *SubmitterEqualCondition) Type() string {
return SubmitterEqualConditionType
}
func (c *SubmitterEqualCondition) Check(r *restrict.AccessRequest) error {
left, err := c.Left.GetValue(r)
if err != nil {
return err
}
right, err := c.Right.GetValue(r)
if err != nil {
return err
}
lVal := reflect.ValueOf(left)
rVal := reflect.ValueOf(right)
// deref Left. this is the difference between us and EqualCondition
for lVal.Kind() == reflect.Pointer {
lVal = lVal.Elem()
}
if !lVal.IsValid() || !reflect.DeepEqual(rVal.Interface(), lVal.Interface()) {
return restrict.NewConditionNotSatisfiedError(c, r, fmt.Errorf("values \"%v\" and \"%v\" are not equal", left, right))
}
return nil
}
func SubmitterEqualConditionFactory() restrict.Condition {
return new(SubmitterEqualCondition)
}
type InMapCondition[K comparable, V any] struct {
ID string `json:"name,omitempty" yaml:"name,omitempty"`
Key *restrict.ValueDescriptor `json:"key" yaml:"key"`
Map *restrict.ValueDescriptor `json:"map" yaml:"map"`
}
func (s *InMapCondition[K, V]) Type() string {
return SubmitterEqualConditionType
}
func InMapConditionFactory[K comparable, V any]() restrict.Condition {
return new(InMapCondition[K, V])
}
func (c *InMapCondition[K, V]) Check(r *restrict.AccessRequest) error {
cKey, err := c.Key.GetValue(r)
if err != nil {
return err
}
cMap, err := c.Map.GetValue(r)
if err != nil {
return err
}
keyVal := reflect.ValueOf(cKey)
mapVal := reflect.ValueOf(cMap)
key := keyVal.Interface().(K)
if _, in := mapVal.Interface().(map[K]V)[key]; !in {
return restrict.NewConditionNotSatisfiedError(c, r, fmt.Errorf("key '%v' not in map", key))
}
return nil
}
func init() {
restrict.RegisterConditionFactory(SubmitterEqualConditionType, SubmitterEqualConditionFactory)
}

211
pkg/rbac/policy.go Normal file
View file

@ -0,0 +1,211 @@
package rbac
import (
"github.com/el-mike/restrict/v2"
)
var policy = &restrict.PolicyDefinition{
Roles: restrict.Roles{
RoleUser: {
Description: "An authenticated user",
Grants: restrict.GrantsMap{
ResourceIncident: {
&restrict.Permission{Action: ActionRead},
&restrict.Permission{Action: ActionCreate},
&restrict.Permission{Preset: PresetUpdateOwn},
&restrict.Permission{Preset: PresetDeleteOwn},
&restrict.Permission{Preset: PresetShareOwn},
},
ResourceCall: {
&restrict.Permission{Action: ActionRead},
&restrict.Permission{Action: ActionCreate},
&restrict.Permission{Preset: PresetUpdateSubmitter},
&restrict.Permission{Preset: PresetDeleteSubmitter},
&restrict.Permission{Action: ActionShare},
},
ResourceTalkgroup: {
&restrict.Permission{Action: ActionRead},
},
ResourceShare: {
&restrict.Permission{Action: ActionRead},
&restrict.Permission{Action: ActionCreate},
&restrict.Permission{Preset: PresetUpdateOwn},
&restrict.Permission{Preset: PresetDeleteOwn},
},
},
},
RoleSubmitter: {
Description: "A role that can submit calls",
Grants: restrict.GrantsMap{
ResourceCall: {
&restrict.Permission{Action: ActionCreate},
},
ResourceTalkgroup: {
// for learning TGs
&restrict.Permission{Action: ActionCreate},
&restrict.Permission{Action: ActionUpdate},
},
},
},
RoleShareGuest: {
Description: "Someone who has a valid share link",
Grants: restrict.GrantsMap{
ResourceCall: {
&restrict.Permission{Preset: PresetReadShared},
},
ResourceIncident: {
&restrict.Permission{Preset: PresetReadShared},
},
ResourceTalkgroup: {
&restrict.Permission{Action: ActionRead},
},
},
},
RoleAdmin: {
Parents: []string{RoleUser},
Grants: restrict.GrantsMap{
ResourceIncident: {
&restrict.Permission{Action: ActionUpdate},
&restrict.Permission{Action: ActionDelete},
&restrict.Permission{Action: ActionShare},
},
ResourceCall: {
&restrict.Permission{Action: ActionUpdate},
&restrict.Permission{Action: ActionDelete},
&restrict.Permission{Action: ActionShare},
},
ResourceTalkgroup: {
&restrict.Permission{Action: ActionUpdate},
&restrict.Permission{Action: ActionCreate},
&restrict.Permission{Action: ActionDelete},
},
},
},
RoleSystem: {
Parents: []string{RoleSystem},
},
RolePublic: {
/*
Grants: restrict.GrantsMap{
ResourceShare: {
&restrict.Permission{Action: ActionRead},
},
},
*/
},
},
PermissionPresets: restrict.PermissionPresets{
PresetUpdateOwn: &restrict.Permission{
Action: ActionUpdate,
Conditions: restrict.Conditions{
&restrict.EqualCondition{
ID: "isOwner",
Left: &restrict.ValueDescriptor{
Source: restrict.ResourceField,
Field: "Owner",
},
Right: &restrict.ValueDescriptor{
Source: restrict.SubjectField,
Field: "ID",
},
},
},
},
PresetDeleteOwn: &restrict.Permission{
Action: ActionDelete,
Conditions: restrict.Conditions{
&restrict.EqualCondition{
ID: "isOwner",
Left: &restrict.ValueDescriptor{
Source: restrict.ResourceField,
Field: "Owner",
},
Right: &restrict.ValueDescriptor{
Source: restrict.SubjectField,
Field: "ID",
},
},
},
},
PresetShareOwn: &restrict.Permission{
Action: ActionShare,
Conditions: restrict.Conditions{
&restrict.EqualCondition{
ID: "isOwner",
Left: &restrict.ValueDescriptor{
Source: restrict.ResourceField,
Field: "Owner",
},
Right: &restrict.ValueDescriptor{
Source: restrict.SubjectField,
Field: "ID",
},
},
},
},
PresetUpdateSubmitter: &restrict.Permission{
Action: ActionUpdate,
Conditions: restrict.Conditions{
&SubmitterEqualCondition{
ID: "isSubmitter",
Left: &restrict.ValueDescriptor{
Source: restrict.ResourceField,
Field: "Submitter",
},
Right: &restrict.ValueDescriptor{
Source: restrict.SubjectField,
Field: "ID",
},
},
},
},
PresetDeleteSubmitter: &restrict.Permission{
Action: ActionDelete,
Conditions: restrict.Conditions{
&SubmitterEqualCondition{
ID: "isSubmitter",
Left: &restrict.ValueDescriptor{
Source: restrict.ResourceField,
Field: "Submitter",
},
Right: &restrict.ValueDescriptor{
Source: restrict.SubjectField,
Field: "ID",
},
},
},
},
PresetShareSubmitter: &restrict.Permission{
Action: ActionShare,
Conditions: restrict.Conditions{
&SubmitterEqualCondition{
ID: "isSubmitter",
Left: &restrict.ValueDescriptor{
Source: restrict.ResourceField,
Field: "Submitter",
},
Right: &restrict.ValueDescriptor{
Source: restrict.SubjectField,
Field: "ID",
},
},
},
},
PresetReadShared: &restrict.Permission{
Action: ActionRead,
Conditions: restrict.Conditions{
&restrict.EqualCondition{
ID: "isOwner",
Left: &restrict.ValueDescriptor{
Source: restrict.ResourceField,
Field: "ID",
},
Right: &restrict.ValueDescriptor{
Source: restrict.SubjectField,
Field: "EntityID",
},
},
},
},
},
}

View file

@ -3,8 +3,6 @@ package rbac
import (
"context"
"errors"
"fmt"
"reflect"
"github.com/el-mike/restrict/v2"
"github.com/el-mike/restrict/v2/adapters"
@ -34,6 +32,7 @@ const (
PresetUpdateOwn = "updateOwn"
PresetDeleteOwn = "deleteOwn"
PresetReadShared = "readShared"
PresetReadSharedInMap = "readSharedInMap"
PresetShareOwn = "shareOwn"
PresetUpdateSubmitter = "updateSubmitter"
@ -91,212 +90,6 @@ var (
ErrNotAuthorized = errors.New("not authorized")
)
var policy = &restrict.PolicyDefinition{
Roles: restrict.Roles{
RoleUser: {
Description: "An authenticated user",
Grants: restrict.GrantsMap{
ResourceIncident: {
&restrict.Permission{Action: ActionRead},
&restrict.Permission{Action: ActionCreate},
&restrict.Permission{Preset: PresetUpdateOwn},
&restrict.Permission{Preset: PresetDeleteOwn},
&restrict.Permission{Preset: PresetShareOwn},
},
ResourceCall: {
&restrict.Permission{Action: ActionRead},
&restrict.Permission{Action: ActionCreate},
&restrict.Permission{Preset: PresetUpdateSubmitter},
&restrict.Permission{Preset: PresetDeleteSubmitter},
&restrict.Permission{Action: ActionShare},
},
ResourceTalkgroup: {
&restrict.Permission{Action: ActionRead},
},
ResourceShare: {
&restrict.Permission{Action: ActionRead},
&restrict.Permission{Action: ActionCreate},
&restrict.Permission{Preset: PresetUpdateOwn},
&restrict.Permission{Preset: PresetDeleteOwn},
},
},
},
RoleSubmitter: {
Description: "A role that can submit calls",
Grants: restrict.GrantsMap{
ResourceCall: {
&restrict.Permission{Action: ActionCreate},
},
ResourceTalkgroup: {
// for learning TGs
&restrict.Permission{Action: ActionCreate},
&restrict.Permission{Action: ActionUpdate},
},
},
},
RoleShareGuest: {
Description: "Someone who has a valid share link",
Grants: restrict.GrantsMap{
ResourceCall: {
&restrict.Permission{Preset: PresetReadShared},
},
ResourceIncident: {
&restrict.Permission{Preset: PresetReadShared},
},
ResourceTalkgroup: {
&restrict.Permission{Action: ActionRead},
},
},
},
RoleAdmin: {
Parents: []string{RoleUser},
Grants: restrict.GrantsMap{
ResourceIncident: {
&restrict.Permission{Action: ActionUpdate},
&restrict.Permission{Action: ActionDelete},
&restrict.Permission{Action: ActionShare},
},
ResourceCall: {
&restrict.Permission{Action: ActionUpdate},
&restrict.Permission{Action: ActionDelete},
&restrict.Permission{Action: ActionShare},
},
ResourceTalkgroup: {
&restrict.Permission{Action: ActionUpdate},
&restrict.Permission{Action: ActionCreate},
&restrict.Permission{Action: ActionDelete},
},
},
},
RoleSystem: {
Parents: []string{RoleSystem},
},
RolePublic: {
/*
Grants: restrict.GrantsMap{
ResourceShare: {
&restrict.Permission{Action: ActionRead},
},
},
*/
},
},
PermissionPresets: restrict.PermissionPresets{
PresetUpdateOwn: &restrict.Permission{
Action: ActionUpdate,
Conditions: restrict.Conditions{
&restrict.EqualCondition{
ID: "isOwner",
Left: &restrict.ValueDescriptor{
Source: restrict.ResourceField,
Field: "Owner",
},
Right: &restrict.ValueDescriptor{
Source: restrict.SubjectField,
Field: "ID",
},
},
},
},
PresetDeleteOwn: &restrict.Permission{
Action: ActionDelete,
Conditions: restrict.Conditions{
&restrict.EqualCondition{
ID: "isOwner",
Left: &restrict.ValueDescriptor{
Source: restrict.ResourceField,
Field: "Owner",
},
Right: &restrict.ValueDescriptor{
Source: restrict.SubjectField,
Field: "ID",
},
},
},
},
PresetShareOwn: &restrict.Permission{
Action: ActionShare,
Conditions: restrict.Conditions{
&restrict.EqualCondition{
ID: "isOwner",
Left: &restrict.ValueDescriptor{
Source: restrict.ResourceField,
Field: "Owner",
},
Right: &restrict.ValueDescriptor{
Source: restrict.SubjectField,
Field: "ID",
},
},
},
},
PresetUpdateSubmitter: &restrict.Permission{
Action: ActionUpdate,
Conditions: restrict.Conditions{
&SubmitterEqualCondition{
ID: "isSubmitter",
Left: &restrict.ValueDescriptor{
Source: restrict.ResourceField,
Field: "Submitter",
},
Right: &restrict.ValueDescriptor{
Source: restrict.SubjectField,
Field: "ID",
},
},
},
},
PresetDeleteSubmitter: &restrict.Permission{
Action: ActionDelete,
Conditions: restrict.Conditions{
&SubmitterEqualCondition{
ID: "isSubmitter",
Left: &restrict.ValueDescriptor{
Source: restrict.ResourceField,
Field: "Submitter",
},
Right: &restrict.ValueDescriptor{
Source: restrict.SubjectField,
Field: "ID",
},
},
},
},
PresetShareSubmitter: &restrict.Permission{
Action: ActionShare,
Conditions: restrict.Conditions{
&SubmitterEqualCondition{
ID: "isSubmitter",
Left: &restrict.ValueDescriptor{
Source: restrict.ResourceField,
Field: "Submitter",
},
Right: &restrict.ValueDescriptor{
Source: restrict.SubjectField,
Field: "ID",
},
},
},
},
PresetReadShared: &restrict.Permission{
Action: ActionRead,
Conditions: restrict.Conditions{
&restrict.EqualCondition{
ID: "isOwner",
Left: &restrict.ValueDescriptor{
Source: restrict.ContextField,
Field: "Owner",
},
Right: &restrict.ValueDescriptor{
Source: restrict.SubjectField,
Field: "ID",
},
},
},
},
},
}
type checkOptions struct {
actions []string
context restrict.Context
@ -398,51 +191,3 @@ func (s *SystemServiceSubject) GetName() string {
func (s *SystemServiceSubject) GetRoles() []string {
return []string{RoleSystem}
}
const (
SubmitterEqualConditionType = "SUBMITTER_EQUAL"
)
type SubmitterEqualCondition struct {
ID string `json:"name,omitempty" yaml:"name,omitempty"`
Left *restrict.ValueDescriptor `json:"left" yaml:"left"`
Right *restrict.ValueDescriptor `json:"right" yaml:"right"`
}
func (s *SubmitterEqualCondition) Type() string {
return SubmitterEqualConditionType
}
func (c *SubmitterEqualCondition) Check(r *restrict.AccessRequest) error {
left, err := c.Left.GetValue(r)
if err != nil {
return err
}
right, err := c.Right.GetValue(r)
if err != nil {
return err
}
lVal := reflect.ValueOf(left)
rVal := reflect.ValueOf(right)
// deref Left. this is the difference between us and EqualCondition
for lVal.Kind() == reflect.Pointer {
lVal = lVal.Elem()
}
if !lVal.IsValid() || !reflect.DeepEqual(rVal.Interface(), lVal.Interface()) {
return restrict.NewConditionNotSatisfiedError(c, r, fmt.Errorf("values \"%v\" and \"%v\" are not equal", left, right))
}
return nil
}
func SubmitterEqualConditionFactory() restrict.Condition {
return new(SubmitterEqualCondition)
}
func init() {
restrict.RegisterConditionFactory(SubmitterEqualConditionType, SubmitterEqualConditionFactory)
}

View file

@ -18,43 +18,44 @@ import (
"github.com/rs/zerolog/log"
)
type APIRoot interface {
API
Shares() ShareAPI
ShareSubroutes(chi.Router)
}
type API interface {
Subrouter() http.Handler
}
type ShareableAPI interface {
type APIRoot interface {
API
GETSubroutes(chi.Router)
ShareRouter() http.Handler
}
type api struct {
baseURL *url.URL
shares ShareAPI
tgs API
calls ShareableAPI
users API
incidents ShareableAPI
shares *shareAPI
tgs *talkgroupAPI
calls *callsAPI
users *usersAPI
incidents *incidentsAPI
}
func (a *api) Shares() ShareAPI {
return a.shares
func (a *api) ShareRouter() http.Handler {
return a.shares.RootRouter()
}
func New(baseURL url.URL) *api {
s := &api{
baseURL: &baseURL,
shares: newShareAPI(&baseURL),
tgs: new(talkgroupAPI),
calls: new(callsAPI),
incidents: newIncidentsAPI(&baseURL),
users: new(usersAPI),
}
s.shares = newShareAPI(&baseURL,
ShareHandlers{
ShareRequestCall: s.calls.shareCallRoute,
ShareRequestCallDL: s.calls.shareCallDLRoute,
ShareRequestIncident: s.incidents.getIncident,
ShareRequestIncidentM3U: s.incidents.getCallsM3U,
},
)
return s
}
@ -71,11 +72,6 @@ func (a *api) Subrouter() http.Handler {
return r
}
func (a *api) ShareSubroutes(r chi.Router) {
r.Route("/calls", a.calls.GETSubroutes)
r.Route("/incidents", a.incidents.GETSubroutes)
}
type errResponse struct {
Err error `json:"-"`
Code int `json:"-"`

View file

@ -30,22 +30,20 @@ type callsAPI struct {
func (ca *callsAPI) Subrouter() http.Handler {
r := chi.NewMux()
ca.GETSubroutes(r)
r.Get(`/{call:[a-f0-9-]+}`, ca.getAudioRoute)
r.Get(`/{call:[a-f0-9-]+}/{download:download}`, ca.getAudioRoute)
r.Post(`/`, ca.listCalls)
return r
}
func (ca *callsAPI) GETSubroutes(r chi.Router) {
r.Get(`/{call:[a-f0-9-]+}`, ca.getAudio)
r.Get(`/{call:[a-f0-9-]+}/{download:download}`, ca.getAudio)
}
func (ca *callsAPI) getAudio(w http.ResponseWriter, r *http.Request) {
p := struct {
type getAudioParams struct {
CallID *uuid.UUID `param:"call"`
Download *string `param:"download"`
}{}
}
func (ca *callsAPI) getAudioRoute(w http.ResponseWriter, r *http.Request) {
p := getAudioParams{}
err := decodeParams(&p, r)
if err != nil {
@ -53,6 +51,10 @@ func (ca *callsAPI) getAudio(w http.ResponseWriter, r *http.Request) {
return
}
ca.getAudio(p, w, r)
}
func (ca *callsAPI) getAudio(p getAudioParams, w http.ResponseWriter, r *http.Request) {
if p.CallID == nil {
wErr(w, r, badRequest(ErrNoCall))
return
@ -99,6 +101,23 @@ func (ca *callsAPI) getAudio(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write(call.AudioBlob)
}
func (ca *callsAPI) shareCallRoute(id uuid.UUID, w http.ResponseWriter, r *http.Request) {
p := getAudioParams{
CallID: &id,
}
ca.getAudio(p, w, r)
}
func (ca *callsAPI) shareCallDLRoute(id uuid.UUID, w http.ResponseWriter, r *http.Request) {
p := getAudioParams{
CallID: &id,
Download: common.PtrTo("download"),
}
ca.getAudio(p, w, r)
}
func (ca *callsAPI) listCalls(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
cSt := callstore.FromCtx(ctx)

View file

@ -22,14 +22,15 @@ type incidentsAPI struct {
baseURL *url.URL
}
func newIncidentsAPI(baseURL *url.URL) ShareableAPI {
func newIncidentsAPI(baseURL *url.URL) *incidentsAPI {
return &incidentsAPI{baseURL}
}
func (ia *incidentsAPI) Subrouter() http.Handler {
r := chi.NewMux()
ia.GETSubroutes(r)
r.Get(`/{id:[a-f0-9-]+}`, ia.getIncidentRoute)
r.Get(`/{id:[a-f0-9-]+}.m3u`, ia.getCallsM3URoute)
r.Post(`/new`, ia.createIncident)
r.Post(`/`, ia.listIncidents)
r.Post(`/{id:[a-f0-9-]+}/calls`, ia.postCalls)
@ -41,11 +42,6 @@ func (ia *incidentsAPI) Subrouter() http.Handler {
return r
}
func (ia *incidentsAPI) GETSubroutes(r chi.Router) {
r.Get(`/{id:[a-f0-9-]+}`, ia.getIncident)
r.Get(`/{id:[a-f0-9-]+}.m3u`, ia.getCallsM3U)
}
func (ia *incidentsAPI) listIncidents(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
incs := incstore.FromCtx(ctx)
@ -91,15 +87,18 @@ func (ia *incidentsAPI) createIncident(w http.ResponseWriter, r *http.Request) {
respond(w, r, inc)
}
func (ia *incidentsAPI) getIncident(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
incs := incstore.FromCtx(ctx)
func (ia *incidentsAPI) getIncidentRoute(w http.ResponseWriter, r *http.Request) {
id, err := idOnlyParam(w, r)
if err != nil {
return
}
ia.getIncident(id, w, r)
}
func (ia *incidentsAPI) getIncident(id uuid.UUID, w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
incs := incstore.FromCtx(ctx)
inc, err := incs.Incident(ctx, id)
if err != nil {
wErr(w, r, autoError(err))
@ -189,16 +188,20 @@ func (ia *incidentsAPI) postCalls(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
}
func (ia *incidentsAPI) getCallsM3U(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
incs := incstore.FromCtx(ctx)
tgst := tgstore.FromCtx(ctx)
func (ia *incidentsAPI) getCallsM3URoute(w http.ResponseWriter, r *http.Request) {
id, err := idOnlyParam(w, r)
if err != nil {
return
}
ia.getCallsM3U(id, w, r)
}
func (ia *incidentsAPI) getCallsM3U(id uuid.UUID, w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
incs := incstore.FromCtx(ctx)
tgst := tgstore.FromCtx(ctx)
inc, err := incs.Incident(ctx, id)
if err != nil {
wErr(w, r, autoError(err))

View file

@ -2,10 +2,8 @@ package rest
import (
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"dynatron.me/x/stillbox/internal/forms"
@ -13,6 +11,7 @@ import (
"dynatron.me/x/stillbox/pkg/shares"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
)
var (
@ -23,6 +22,7 @@ type ShareRequestType string
const (
ShareRequestCall ShareRequestType = "call"
ShareRequestCallDL ShareRequestType = "callDL"
ShareRequestIncident ShareRequestType = "incident"
ShareRequestIncidentM3U ShareRequestType = "m3u"
)
@ -36,39 +36,17 @@ func (rt ShareRequestType) IsValid() bool {
return false
}
type ShareHandlers map[shares.EntityType]http.Handler
type HandlerFunc func(uuid.UUID, http.ResponseWriter, *http.Request)
type ShareHandlers map[ShareRequestType]HandlerFunc
type shareAPI struct {
baseURL *url.URL
shnd ShareHandlers
}
type ShareAPI interface {
API
ShareMiddleware() func(http.Handler) http.Handler
}
func newShareAPI(baseURL *url.URL) ShareAPI {
func newShareAPI(baseURL *url.URL, shnd ShareHandlers) *shareAPI {
return &shareAPI{
baseURL: baseURL,
}
}
func (a *shareAPI) ShareMiddleware() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
if !strings.HasPrefix(r.URL.Path, "/share/") {
next.ServeHTTP(w, r)
return
}
nr, err := a.getShare(r)
if err != nil {
wErr(w, r, autoError(err))
return
}
next.ServeHTTP(w, nr)
}
return http.HandlerFunc(fn)
shnd: shnd,
}
}
@ -81,10 +59,12 @@ func (sa *shareAPI) Subrouter() http.Handler {
return r
}
//func (sa *shareAPI) PublicRoutes(r chi.Router) http.Handler {
// r.Get(`/{type}/{id:[A-Za-z0-9_-]{20,}}`, sa.getShare)
// r.Get(`/{type}/{id:[A-Za-z0-9_-]{20,}}*`, sa.getShare)
//}
func (sa *shareAPI) RootRouter() http.Handler {
r := chi.NewMux()
r.Get("/{type}/{shareId:[A-Za-z0-9_-]{20,}}", sa.routeShare)
return r
}
func (sa *shareAPI) createShare(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
@ -107,10 +87,7 @@ func (sa *shareAPI) createShare(w http.ResponseWriter, r *http.Request) {
respond(w, r, sh)
}
func (sa *shareAPI) deleteShare(w http.ResponseWriter, r *http.Request) {
}
func (sa *shareAPI) getShare(r *http.Request) (*http.Request, error) {
func (sa *shareAPI) routeShare(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
shs := shares.FromCtx(ctx)
@ -121,50 +98,34 @@ func (sa *shareAPI) getShare(r *http.Request) (*http.Request, error) {
err := decodeParams(&params, r)
if err != nil {
return nil, err
wErr(w, r, autoError(err))
return
}
rType := ShareRequestType(params.Type)
id := params.ID
if !rType.IsValid() {
return nil, ErrBadShare
wErr(w, r, autoError(ErrBadShare))
return
}
sh, err := shs.GetShare(ctx, id)
if err != nil {
return nil, err
wErr(w, r, autoError(err))
return
}
if sh.Expiration != nil && sh.Expiration.Time().Before(time.Now()) {
return nil, shares.ErrNoShare
wErr(w, r, autoError(shares.ErrNoShare))
return
}
ctx = rbac.CtxWithSubject(ctx, sh)
r = r.WithContext(ctx)
switch rType {
case ShareRequestCall, ShareRequestIncident:
r.URL.Path += fmt.Sprintf("/%s/%s", rType, sh.EntityID.String())
case ShareRequestIncidentM3U:
r.URL.Path = fmt.Sprintf("/incident/%s.m3u", sh.EntityID.String())
sa.shnd[rType](sh.EntityID, w, r)
}
return r, nil
}
// idOnlyParam checks for a sole URL parameter, id, and writes an errorif this fails.
func shareParams(w http.ResponseWriter, r *http.Request) (rType ShareRequestType, rID string, err error) {
params := struct {
Type string `param:"type"`
ID string `param:"id"`
}{}
err = decodeParams(&params, r)
if err != nil {
wErr(w, r, badRequest(err))
return "", "", err
}
return ShareRequestType(params.Type), params.ID, nil
func (sa *shareAPI) deleteShare(w http.ResponseWriter, r *http.Request) {
}

View file

@ -31,7 +31,6 @@ func (s *Server) setupRoutes() {
s.installPprof()
r.Group(func(r chi.Router) {
// authenticated routes
r.Use(s.auth.VerifyMiddleware(), s.auth.AuthMiddleware())
@ -41,11 +40,6 @@ func (s *Server) setupRoutes() {
r.Mount("/api", s.rest.Subrouter())
})
r.Route("/share/{type}/{shareId:[A-Za-z0-9_-]{20,}}", func(r chi.Router) {
r.Use(s.rest.Shares().ShareMiddleware())
s.rest.ShareSubroutes(r)
})
r.Group(func(r chi.Router) {
s.rateLimit(r)
r.Use(render.SetContentType(render.ContentTypeJSON))
@ -58,6 +52,7 @@ func (s *Server) setupRoutes() {
s.rateLimit(r)
r.Use(render.SetContentType(render.ContentTypeJSON))
s.auth.PublicRoutes(r)
r.Mount("/share", s.rest.ShareRouter())
})
r.Group(func(r chi.Router) {
@ -68,10 +63,6 @@ func (s *Server) setupRoutes() {
s.clientRoute(r, clientRoot)
})
chi.Walk(r, func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
fmt.Printf("[%s]: '%s' has %d middlewares\n", method, route, len(middlewares))
return nil
})
}
// WithCtxStores is a middleware that installs all stores in the request context.

View file

@ -2,6 +2,7 @@ package server
import (
"context"
"fmt"
"net/http"
"os"
"time"
@ -145,6 +146,13 @@ func New(ctx context.Context, cfg *config.Configuration) (*Server, error) {
}))
srv.setupRoutes()
if os.Getenv("STILLBOX_DUMP_ROUTES") == "true" {
chi.Walk(r, func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
fmt.Printf("[%s]: '%s' has %d middlewares\n", method, route, len(middlewares))
return nil
})
}
return srv, nil
}