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) { 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 { if err != nil {
return nil, err return nil, err
} }

View file

@ -16,17 +16,18 @@ type Configuration struct {
} }
type Config struct { type Config struct {
BaseURL jsontypes.URL `yaml:"baseURL"` BaseURL jsontypes.URL `yaml:"baseURL"`
DB DB `yaml:"db"` DumpRoutes bool `yaml:"dumpRoutes"`
CORS CORS `yaml:"cors"` DB DB `yaml:"db"`
Auth Auth `yaml:"auth"` CORS CORS `yaml:"cors"`
Alerting Alerting `yaml:"alerting"` Auth Auth `yaml:"auth"`
Log []Logger `yaml:"log"` Alerting Alerting `yaml:"alerting"`
Listen string `yaml:"listen"` Log []Logger `yaml:"log"`
Public bool `yaml:"public"` Listen string `yaml:"listen"`
RateLimit RateLimit `yaml:"rateLimit"` Public bool `yaml:"public"`
Notify Notify `yaml:"notify"` RateLimit RateLimit `yaml:"rateLimit"`
Relay []Relay `yaml:"relay"` Notify Notify `yaml:"notify"`
Relay []Relay `yaml:"relay"`
} }
type Auth struct { type Auth struct {

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) { 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 { if err != nil {
return nil, err 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 ( import (
"context" "context"
"errors" "errors"
"fmt"
"reflect"
"github.com/el-mike/restrict/v2" "github.com/el-mike/restrict/v2"
"github.com/el-mike/restrict/v2/adapters" "github.com/el-mike/restrict/v2/adapters"
@ -31,10 +29,11 @@ const (
ActionDelete = "delete" ActionDelete = "delete"
ActionShare = "share" ActionShare = "share"
PresetUpdateOwn = "updateOwn" PresetUpdateOwn = "updateOwn"
PresetDeleteOwn = "deleteOwn" PresetDeleteOwn = "deleteOwn"
PresetReadShared = "readShared" PresetReadShared = "readShared"
PresetShareOwn = "shareOwn" PresetReadSharedInMap = "readSharedInMap"
PresetShareOwn = "shareOwn"
PresetUpdateSubmitter = "updateSubmitter" PresetUpdateSubmitter = "updateSubmitter"
PresetDeleteSubmitter = "deleteSubmitter" PresetDeleteSubmitter = "deleteSubmitter"
@ -91,212 +90,6 @@ var (
ErrNotAuthorized = errors.New("not authorized") 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 { type checkOptions struct {
actions []string actions []string
context restrict.Context context restrict.Context
@ -398,51 +191,3 @@ func (s *SystemServiceSubject) GetName() string {
func (s *SystemServiceSubject) GetRoles() []string { func (s *SystemServiceSubject) GetRoles() []string {
return []string{RoleSystem} 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" "github.com/rs/zerolog/log"
) )
type APIRoot interface {
API
Shares() ShareAPI
ShareSubroutes(chi.Router)
}
type API interface { type API interface {
Subrouter() http.Handler Subrouter() http.Handler
} }
type ShareableAPI interface { type APIRoot interface {
API API
GETSubroutes(chi.Router) ShareRouter() http.Handler
} }
type api struct { type api struct {
baseURL *url.URL baseURL *url.URL
shares ShareAPI shares *shareAPI
tgs API tgs *talkgroupAPI
calls ShareableAPI calls *callsAPI
users API users *usersAPI
incidents ShareableAPI incidents *incidentsAPI
} }
func (a *api) Shares() ShareAPI { func (a *api) ShareRouter() http.Handler {
return a.shares return a.shares.RootRouter()
} }
func New(baseURL url.URL) *api { func New(baseURL url.URL) *api {
s := &api{ s := &api{
baseURL: &baseURL, baseURL: &baseURL,
shares: newShareAPI(&baseURL), tgs: new(talkgroupAPI),
tgs: new(talkgroupAPI), calls: new(callsAPI),
calls: new(callsAPI),
incidents: newIncidentsAPI(&baseURL), incidents: newIncidentsAPI(&baseURL),
users: new(usersAPI), 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 return s
} }
@ -71,11 +72,6 @@ func (a *api) Subrouter() http.Handler {
return r return r
} }
func (a *api) ShareSubroutes(r chi.Router) {
r.Route("/calls", a.calls.GETSubroutes)
r.Route("/incidents", a.incidents.GETSubroutes)
}
type errResponse struct { type errResponse struct {
Err error `json:"-"` Err error `json:"-"`
Code int `json:"-"` Code int `json:"-"`

View file

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

View file

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

View file

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

View file

@ -31,7 +31,6 @@ func (s *Server) setupRoutes() {
s.installPprof() s.installPprof()
r.Group(func(r chi.Router) { r.Group(func(r chi.Router) {
// authenticated routes // authenticated routes
r.Use(s.auth.VerifyMiddleware(), s.auth.AuthMiddleware()) r.Use(s.auth.VerifyMiddleware(), s.auth.AuthMiddleware())
@ -41,11 +40,6 @@ func (s *Server) setupRoutes() {
r.Mount("/api", s.rest.Subrouter()) 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) { r.Group(func(r chi.Router) {
s.rateLimit(r) s.rateLimit(r)
r.Use(render.SetContentType(render.ContentTypeJSON)) r.Use(render.SetContentType(render.ContentTypeJSON))
@ -58,6 +52,7 @@ func (s *Server) setupRoutes() {
s.rateLimit(r) s.rateLimit(r)
r.Use(render.SetContentType(render.ContentTypeJSON)) r.Use(render.SetContentType(render.ContentTypeJSON))
s.auth.PublicRoutes(r) s.auth.PublicRoutes(r)
r.Mount("/share", s.rest.ShareRouter())
}) })
r.Group(func(r chi.Router) { r.Group(func(r chi.Router) {
@ -68,10 +63,6 @@ func (s *Server) setupRoutes() {
s.clientRoute(r, clientRoot) 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. // WithCtxStores is a middleware that installs all stores in the request context.

View file

@ -2,6 +2,7 @@ package server
import ( import (
"context" "context"
"fmt"
"net/http" "net/http"
"os" "os"
"time" "time"
@ -145,6 +146,13 @@ func New(ctx context.Context, cfg *config.Configuration) (*Server, error) {
})) }))
srv.setupRoutes() 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 return srv, nil
} }