From b171e8431afe128bb33127f35a861f20b56af276 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Mon, 20 Jan 2025 20:28:25 -0500 Subject: [PATCH] WIP --- pkg/calls/callstore/store.go | 2 +- pkg/config/config.go | 23 +-- pkg/incidents/incstore/store.go | 2 +- pkg/rbac/conditions.go | 94 +++++++++++ pkg/rbac/policy.go | 211 +++++++++++++++++++++++++ pkg/rbac/rbac.go | 265 +------------------------------- pkg/rest/api.go | 48 +++--- pkg/rest/calls.go | 37 +++-- pkg/rest/incidents.go | 35 +++-- pkg/rest/share.go | 87 +++-------- pkg/server/routes.go | 11 +- pkg/server/server.go | 8 + 12 files changed, 426 insertions(+), 397 deletions(-) create mode 100644 pkg/rbac/conditions.go create mode 100644 pkg/rbac/policy.go diff --git a/pkg/calls/callstore/store.go b/pkg/calls/callstore/store.go index 3de46d4..72dc684 100644 --- a/pkg/calls/callstore/store.go +++ b/pkg/calls/callstore/store.go @@ -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 } diff --git a/pkg/config/config.go b/pkg/config/config.go index 61dc344..63a4f78 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -16,17 +16,18 @@ type Configuration struct { } type Config struct { - BaseURL jsontypes.URL `yaml:"baseURL"` - DB DB `yaml:"db"` - CORS CORS `yaml:"cors"` - Auth Auth `yaml:"auth"` - Alerting Alerting `yaml:"alerting"` - Log []Logger `yaml:"log"` - Listen string `yaml:"listen"` - Public bool `yaml:"public"` - RateLimit RateLimit `yaml:"rateLimit"` - Notify Notify `yaml:"notify"` - Relay []Relay `yaml:"relay"` + BaseURL jsontypes.URL `yaml:"baseURL"` + DumpRoutes bool `yaml:"dumpRoutes"` + DB DB `yaml:"db"` + CORS CORS `yaml:"cors"` + Auth Auth `yaml:"auth"` + Alerting Alerting `yaml:"alerting"` + Log []Logger `yaml:"log"` + Listen string `yaml:"listen"` + Public bool `yaml:"public"` + RateLimit RateLimit `yaml:"rateLimit"` + Notify Notify `yaml:"notify"` + Relay []Relay `yaml:"relay"` } type Auth struct { diff --git a/pkg/incidents/incstore/store.go b/pkg/incidents/incstore/store.go index 195837a..e2f396f 100644 --- a/pkg/incidents/incstore/store.go +++ b/pkg/incidents/incstore/store.go @@ -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 } diff --git a/pkg/rbac/conditions.go b/pkg/rbac/conditions.go new file mode 100644 index 0000000..721e83d --- /dev/null +++ b/pkg/rbac/conditions.go @@ -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) +} diff --git a/pkg/rbac/policy.go b/pkg/rbac/policy.go new file mode 100644 index 0000000..32e20da --- /dev/null +++ b/pkg/rbac/policy.go @@ -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", + }, + }, + }, + }, + }, +} diff --git a/pkg/rbac/rbac.go b/pkg/rbac/rbac.go index 7ad53ab..9a9525c 100644 --- a/pkg/rbac/rbac.go +++ b/pkg/rbac/rbac.go @@ -3,8 +3,6 @@ package rbac import ( "context" "errors" - "fmt" - "reflect" "github.com/el-mike/restrict/v2" "github.com/el-mike/restrict/v2/adapters" @@ -31,10 +29,11 @@ const ( ActionDelete = "delete" ActionShare = "share" - PresetUpdateOwn = "updateOwn" - PresetDeleteOwn = "deleteOwn" - PresetReadShared = "readShared" - PresetShareOwn = "shareOwn" + PresetUpdateOwn = "updateOwn" + PresetDeleteOwn = "deleteOwn" + PresetReadShared = "readShared" + PresetReadSharedInMap = "readSharedInMap" + PresetShareOwn = "shareOwn" PresetUpdateSubmitter = "updateSubmitter" PresetDeleteSubmitter = "deleteSubmitter" @@ -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) -} diff --git a/pkg/rest/api.go b/pkg/rest/api.go index 6b0c7ee..9e7f066 100644 --- a/pkg/rest/api.go +++ b/pkg/rest/api.go @@ -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 + baseURL *url.URL + 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), + baseURL: &baseURL, + tgs: new(talkgroupAPI), + calls: new(callsAPI), 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 } @@ -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:"-"` diff --git a/pkg/rest/calls.go b/pkg/rest/calls.go index e6b411b..285a761 100644 --- a/pkg/rest/calls.go +++ b/pkg/rest/calls.go @@ -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) +type getAudioParams struct { + CallID *uuid.UUID `param:"call"` + Download *string `param:"download"` } -func (ca *callsAPI) getAudio(w http.ResponseWriter, r *http.Request) { - p := 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) diff --git a/pkg/rest/incidents.go b/pkg/rest/incidents.go index 802d256..c7effb5 100644 --- a/pkg/rest/incidents.go +++ b/pkg/rest/incidents.go @@ -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)) diff --git a/pkg/rest/share.go b/pkg/rest/share.go index 08f0ef1..aee47aa 100644 --- a/pkg/rest/share.go +++ b/pkg/rest/share.go @@ -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(¶ms, 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()) - } - - return r, nil + sa.shnd[rType](sh.EntityID, w, r) } -// 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(¶ms, 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) { } diff --git a/pkg/server/routes.go b/pkg/server/routes.go index 9d4ae96..43fa02f 100644 --- a/pkg/server/routes.go +++ b/pkg/server/routes.go @@ -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. diff --git a/pkg/server/server.go b/pkg/server/server.go index a18aec4..5d144a8 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -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 }