WIP
This commit is contained in:
parent
a9f64f74fb
commit
b171e8431a
12 changed files with 426 additions and 397 deletions
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
94
pkg/rbac/conditions.go
Normal 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
211
pkg/rbac/policy.go
Normal 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",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
265
pkg/rbac/rbac.go
265
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)
|
||||
}
|
||||
|
|
|
@ -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:"-"`
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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) {
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue