From 76a2214377dcd36d6018925fe59c0f6793c46ebd Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Wed, 22 Jan 2025 10:39:23 -0500 Subject: [PATCH] Able to use store in rbac policy finally --- pkg/alerting/alerting.go | 4 +- pkg/auth/apikey.go | 6 +- pkg/auth/jwt.go | 4 +- pkg/calls/call.go | 4 +- pkg/calls/callstore/store.go | 11 +- pkg/database/mocks/Store.go | 117 ++++++++++++++ pkg/database/models.go | 226 ++++++++++++++-------------- pkg/database/partman/partman.go | 4 +- pkg/database/querier.go | 1 + pkg/database/share.sql.go | 34 +++++ pkg/incidents/incident.go | 4 +- pkg/incidents/incstore/store.go | 11 +- pkg/nexus/nexus.go | 4 +- pkg/rbac/entities.go | 23 --- pkg/rbac/entities/entities.go | 79 ++++++++++ pkg/rbac/mocks/RBAC.go | 18 ++- pkg/rbac/{ => policy}/conditions.go | 6 +- pkg/rbac/{ => policy}/policy.go | 106 ++++++------- pkg/rbac/rbac.go | 71 ++------- pkg/rbac/rbac_test.go | 42 +++--- pkg/rest/share.go | 4 +- pkg/server/server.go | 3 +- pkg/shares/service.go | 4 +- pkg/shares/share.go | 7 +- pkg/shares/store.go | 3 +- pkg/sources/http.go | 3 +- pkg/talkgroups/filter/filter.go | 1 + pkg/talkgroups/talkgroup.go | 4 +- pkg/talkgroups/tgstore/store.go | 19 +-- pkg/users/user.go | 11 +- sql/postgres/queries/share.sql | 8 + 31 files changed, 510 insertions(+), 332 deletions(-) delete mode 100644 pkg/rbac/entities.go create mode 100644 pkg/rbac/entities/entities.go rename pkg/rbac/{ => policy}/conditions.go (96%) rename pkg/rbac/{ => policy}/policy.go (66%) diff --git a/pkg/alerting/alerting.go b/pkg/alerting/alerting.go index d78a0cf..e580aed 100644 --- a/pkg/alerting/alerting.go +++ b/pkg/alerting/alerting.go @@ -14,7 +14,7 @@ import ( "dynatron.me/x/stillbox/pkg/config" "dynatron.me/x/stillbox/pkg/database" "dynatron.me/x/stillbox/pkg/notify" - "dynatron.me/x/stillbox/pkg/rbac" + "dynatron.me/x/stillbox/pkg/rbac/entities" "dynatron.me/x/stillbox/pkg/sinks" "dynatron.me/x/stillbox/pkg/talkgroups" "dynatron.me/x/stillbox/pkg/talkgroups/tgstore" @@ -124,7 +124,7 @@ func New(cfg config.Alerting, tgCache tgstore.Store, opts ...AlertOption) Alerte // Go is the alerting loop. It does not start a goroutine. func (as *alerter) Go(ctx context.Context) { - ctx = rbac.CtxWithSubject(ctx, &rbac.SystemServiceSubject{Name: "alerter"}) + ctx = entities.CtxWithSubject(ctx, &entities.SystemServiceSubject{Name: "alerter"}) err := as.startBackfill(ctx) if err != nil { diff --git a/pkg/auth/apikey.go b/pkg/auth/apikey.go index f88c8f9..7b11b1f 100644 --- a/pkg/auth/apikey.go +++ b/pkg/auth/apikey.go @@ -7,7 +7,7 @@ import ( "time" "dynatron.me/x/stillbox/pkg/database" - "dynatron.me/x/stillbox/pkg/rbac" + "dynatron.me/x/stillbox/pkg/rbac/entities" "github.com/google/uuid" "github.com/rs/zerolog/log" @@ -16,10 +16,10 @@ import ( type apiKeyAuth interface { // CheckAPIKey validates the provided key and returns the API owner's users.UserID. // An error is returned if validation fails for any reason. - CheckAPIKey(ctx context.Context, key string) (rbac.Subject, error) + CheckAPIKey(ctx context.Context, key string) (entities.Subject, error) } -func (a *Auth) CheckAPIKey(ctx context.Context, key string) (rbac.Subject, error) { +func (a *Auth) CheckAPIKey(ctx context.Context, key string) (entities.Subject, error) { keyUuid, err := uuid.Parse(key) if err != nil { log.Error().Str("apikey", key).Msg("cannot parse key") diff --git a/pkg/auth/jwt.go b/pkg/auth/jwt.go index 97867b1..896918c 100644 --- a/pkg/auth/jwt.go +++ b/pkg/auth/jwt.go @@ -10,7 +10,7 @@ import ( "golang.org/x/crypto/bcrypt" "dynatron.me/x/stillbox/pkg/database" - "dynatron.me/x/stillbox/pkg/rbac" + "dynatron.me/x/stillbox/pkg/rbac/entities" "dynatron.me/x/stillbox/pkg/users" "github.com/go-chi/chi/v5" @@ -104,7 +104,7 @@ func (a *Auth) AuthMiddleware() func(http.Handler) http.Handler { return } - ctx = rbac.CtxWithSubject(ctx, sub) + ctx = entities.CtxWithSubject(ctx, sub) next.ServeHTTP(w, r.WithContext(ctx)) diff --git a/pkg/calls/call.go b/pkg/calls/call.go index 1f0e338..3e7ab4c 100644 --- a/pkg/calls/call.go +++ b/pkg/calls/call.go @@ -8,7 +8,7 @@ import ( "dynatron.me/x/stillbox/internal/audio" "dynatron.me/x/stillbox/internal/jsontypes" "dynatron.me/x/stillbox/pkg/pb" - "dynatron.me/x/stillbox/pkg/rbac" + "dynatron.me/x/stillbox/pkg/rbac/entities" "dynatron.me/x/stillbox/pkg/talkgroups" "dynatron.me/x/stillbox/pkg/users" @@ -76,7 +76,7 @@ type Call struct { } func (c *Call) GetResourceName() string { - return rbac.ResourceCall + return entities.ResourceCall } func (c *Call) String() string { diff --git a/pkg/calls/callstore/store.go b/pkg/calls/callstore/store.go index 72dc684..85aed7a 100644 --- a/pkg/calls/callstore/store.go +++ b/pkg/calls/callstore/store.go @@ -11,6 +11,7 @@ import ( "dynatron.me/x/stillbox/pkg/calls" "dynatron.me/x/stillbox/pkg/database" "dynatron.me/x/stillbox/pkg/rbac" + "dynatron.me/x/stillbox/pkg/rbac/entities" "dynatron.me/x/stillbox/pkg/talkgroups/tgstore" "dynatron.me/x/stillbox/pkg/users" @@ -85,7 +86,7 @@ func toAddCallParams(call *calls.Call) database.AddCallParams { } func (s *store) AddCall(ctx context.Context, call *calls.Call) error { - _, err := rbac.Check(ctx, call, rbac.WithActions(rbac.ActionCreate)) + _, err := rbac.Check(ctx, call, rbac.WithActions(entities.ActionCreate)) if err != nil { return err } @@ -123,7 +124,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, &calls.Call{ID: id}, rbac.WithActions(rbac.ActionRead)) + _, err := rbac.Check(ctx, &calls.Call{ID: id}, rbac.WithActions(entities.ActionRead)) if err != nil { return nil, err } @@ -144,7 +145,7 @@ func (s *store) CallAudio(ctx context.Context, id uuid.UUID) (*calls.CallAudio, } func (s *store) Call(ctx context.Context, id uuid.UUID) (*calls.Call, error) { - _, err := rbac.Check(ctx, rbac.UseResource(rbac.ResourceCall), rbac.WithActions(rbac.ActionRead)) + _, err := rbac.Check(ctx, rbac.UseResource(entities.ResourceCall), rbac.WithActions(entities.ActionRead)) if err != nil { return nil, err } @@ -193,7 +194,7 @@ type CallsParams struct { } func (s *store) Calls(ctx context.Context, p CallsParams) (rows []database.ListCallsPRow, totalCount int, err error) { - _, err = rbac.Check(ctx, rbac.UseResource(rbac.ResourceCall), rbac.WithActions(rbac.ActionRead)) + _, err = rbac.Check(ctx, rbac.UseResource(entities.ResourceCall), rbac.WithActions(entities.ActionRead)) if err != nil { return nil, 0, err } @@ -256,7 +257,7 @@ func (s *store) Delete(ctx context.Context, id uuid.UUID) error { return err } - _, err = rbac.Check(ctx, &callOwn, rbac.WithActions(rbac.ActionDelete)) + _, err = rbac.Check(ctx, &callOwn, rbac.WithActions(entities.ActionDelete)) if err != nil { return err } diff --git a/pkg/database/mocks/Store.go b/pkg/database/mocks/Store.go index 3f6c509..ad4ad0a 100644 --- a/pkg/database/mocks/Store.go +++ b/pkg/database/mocks/Store.go @@ -278,6 +278,64 @@ func (_c *Store_BulkSetTalkgroupTags_Call) RunAndReturn(run func(context.Context return _c } +// CallInIncident provides a mock function with given fields: ctx, incidentID, callID +func (_m *Store) CallInIncident(ctx context.Context, incidentID uuid.UUID, callID uuid.UUID) (bool, error) { + ret := _m.Called(ctx, incidentID, callID) + + if len(ret) == 0 { + panic("no return value specified for CallInIncident") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID, uuid.UUID) (bool, error)); ok { + return rf(ctx, incidentID, callID) + } + if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID, uuid.UUID) bool); ok { + r0 = rf(ctx, incidentID, callID) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(context.Context, uuid.UUID, uuid.UUID) error); ok { + r1 = rf(ctx, incidentID, callID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Store_CallInIncident_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CallInIncident' +type Store_CallInIncident_Call struct { + *mock.Call +} + +// CallInIncident is a helper method to define mock.On call +// - ctx context.Context +// - incidentID uuid.UUID +// - callID uuid.UUID +func (_e *Store_Expecter) CallInIncident(ctx interface{}, incidentID interface{}, callID interface{}) *Store_CallInIncident_Call { + return &Store_CallInIncident_Call{Call: _e.mock.On("CallInIncident", ctx, incidentID, callID)} +} + +func (_c *Store_CallInIncident_Call) Run(run func(ctx context.Context, incidentID uuid.UUID, callID uuid.UUID)) *Store_CallInIncident_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(uuid.UUID), args[2].(uuid.UUID)) + }) + return _c +} + +func (_c *Store_CallInIncident_Call) Return(_a0 bool, _a1 error) *Store_CallInIncident_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *Store_CallInIncident_Call) RunAndReturn(run func(context.Context, uuid.UUID, uuid.UUID) (bool, error)) *Store_CallInIncident_Call { + _c.Call.Return(run) + return _c +} + // CleanupSweptCalls provides a mock function with given fields: ctx, rangeStart, rangeEnd func (_m *Store) CleanupSweptCalls(ctx context.Context, rangeStart pgtype.Timestamptz, rangeEnd pgtype.Timestamptz) (int64, error) { ret := _m.Called(ctx, rangeStart, rangeEnd) @@ -1750,6 +1808,65 @@ func (_c *Store_GetIncidentOwner_Call) RunAndReturn(run func(context.Context, uu return _c } +// GetIncidentTalkgroups provides a mock function with given fields: ctx, incidentID +func (_m *Store) GetIncidentTalkgroups(ctx context.Context, incidentID uuid.UUID) ([]database.GetIncidentTalkgroupsRow, error) { + ret := _m.Called(ctx, incidentID) + + if len(ret) == 0 { + panic("no return value specified for GetIncidentTalkgroups") + } + + var r0 []database.GetIncidentTalkgroupsRow + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) ([]database.GetIncidentTalkgroupsRow, error)); ok { + return rf(ctx, incidentID) + } + if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) []database.GetIncidentTalkgroupsRow); ok { + r0 = rf(ctx, incidentID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]database.GetIncidentTalkgroupsRow) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, uuid.UUID) error); ok { + r1 = rf(ctx, incidentID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Store_GetIncidentTalkgroups_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetIncidentTalkgroups' +type Store_GetIncidentTalkgroups_Call struct { + *mock.Call +} + +// GetIncidentTalkgroups is a helper method to define mock.On call +// - ctx context.Context +// - incidentID uuid.UUID +func (_e *Store_Expecter) GetIncidentTalkgroups(ctx interface{}, incidentID interface{}) *Store_GetIncidentTalkgroups_Call { + return &Store_GetIncidentTalkgroups_Call{Call: _e.mock.On("GetIncidentTalkgroups", ctx, incidentID)} +} + +func (_c *Store_GetIncidentTalkgroups_Call) Run(run func(ctx context.Context, incidentID uuid.UUID)) *Store_GetIncidentTalkgroups_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(uuid.UUID)) + }) + return _c +} + +func (_c *Store_GetIncidentTalkgroups_Call) Return(_a0 []database.GetIncidentTalkgroupsRow, _a1 error) *Store_GetIncidentTalkgroups_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *Store_GetIncidentTalkgroups_Call) RunAndReturn(run func(context.Context, uuid.UUID) ([]database.GetIncidentTalkgroupsRow, error)) *Store_GetIncidentTalkgroups_Call { + _c.Call.Return(run) + return _c +} + // GetShare provides a mock function with given fields: ctx, id func (_m *Store) GetShare(ctx context.Context, id string) (database.Share, error) { ret := _m.Called(ctx, id) diff --git a/pkg/database/models.go b/pkg/database/models.go index 257acee..585f860 100644 --- a/pkg/database/models.go +++ b/pkg/database/models.go @@ -14,150 +14,150 @@ import ( ) type Alert struct { - ID int `json:"id"` - Time pgtype.Timestamptz `json:"time"` - TGID int `json:"tgid"` - SystemID int `json:"system_id"` - Weight *float32 `json:"weight"` - Score *float32 `json:"score"` - OrigScore *float32 `json:"orig_score"` - Notified bool `json:"notified"` - Metadata []byte `json:"metadata"` + ID int `json:"id,omitempty"` + Time pgtype.Timestamptz `json:"time,omitempty"` + TGID int `json:"tgid,omitempty"` + SystemID int `json:"system_id,omitempty"` + Weight *float32 `json:"weight,omitempty"` + Score *float32 `json:"score,omitempty"` + OrigScore *float32 `json:"orig_score,omitempty"` + Notified bool `json:"notified,omitempty"` + Metadata []byte `json:"metadata,omitempty"` } type ApiKey struct { - ID int `json:"id"` - Owner int `json:"owner"` - CreatedAt time.Time `json:"created_at"` - Expires pgtype.Timestamp `json:"expires"` - Disabled *bool `json:"disabled"` - ApiKey string `json:"api_key"` + ID int `json:"id,omitempty"` + Owner int `json:"owner,omitempty"` + CreatedAt time.Time `json:"created_at,omitempty"` + Expires pgtype.Timestamp `json:"expires,omitempty"` + Disabled *bool `json:"disabled,omitempty"` + ApiKey string `json:"api_key,omitempty"` } type Call struct { - ID uuid.UUID `json:"id"` - Submitter *int32 `json:"submitter"` - System int `json:"system"` - Talkgroup int `json:"talkgroup"` - CallDate pgtype.Timestamptz `json:"call_date"` - AudioName *string `json:"audio_name"` - AudioBlob []byte `json:"audio_blob"` - Duration *int32 `json:"duration"` - AudioType *string `json:"audio_type"` - AudioUrl *string `json:"audio_url"` - Frequency int `json:"frequency"` - Frequencies []int `json:"frequencies"` - Patches []int `json:"patches"` - TGLabel *string `json:"tg_label"` - TGAlphaTag *string `json:"tg_alpha_tag"` - TGGroup *string `json:"tg_group"` - Source int `json:"source"` - Transcript *string `json:"transcript"` + ID uuid.UUID `json:"id,omitempty"` + Submitter *int32 `json:"submitter,omitempty"` + System int `json:"system,omitempty"` + Talkgroup int `json:"talkgroup,omitempty"` + CallDate pgtype.Timestamptz `json:"call_date,omitempty"` + AudioName *string `json:"audio_name,omitempty"` + AudioBlob []byte `json:"audio_blob,omitempty"` + Duration *int32 `json:"duration,omitempty"` + AudioType *string `json:"audio_type,omitempty"` + AudioUrl *string `json:"audio_url,omitempty"` + Frequency int `json:"frequency,omitempty"` + Frequencies []int `json:"frequencies,omitempty"` + Patches []int `json:"patches,omitempty"` + TGLabel *string `json:"tg_label,omitempty"` + TGAlphaTag *string `json:"tg_alpha_tag,omitempty"` + TGGroup *string `json:"tg_group,omitempty"` + Source int `json:"source,omitempty"` + Transcript *string `json:"transcript,omitempty"` } type Incident struct { - ID uuid.UUID `json:"id"` - Name string `json:"name"` - Owner int `json:"owner"` - Description *string `json:"description"` - StartTime pgtype.Timestamptz `json:"start_time"` - EndTime pgtype.Timestamptz `json:"end_time"` - Location []byte `json:"location"` - Metadata jsontypes.Metadata `json:"metadata"` + ID uuid.UUID `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Owner int `json:"owner,omitempty"` + Description *string `json:"description,omitempty"` + StartTime pgtype.Timestamptz `json:"start_time,omitempty"` + EndTime pgtype.Timestamptz `json:"end_time,omitempty"` + Location []byte `json:"location,omitempty"` + Metadata jsontypes.Metadata `json:"metadata,omitempty"` } type IncidentsCall struct { - IncidentID uuid.UUID `json:"incident_id"` - CallID uuid.UUID `json:"call_id"` - CallsTblID pgtype.UUID `json:"calls_tbl_id"` - SweptCallID pgtype.UUID `json:"swept_call_id"` - CallDate pgtype.Timestamptz `json:"call_date"` - Notes []byte `json:"notes"` + IncidentID uuid.UUID `json:"incident_id,omitempty"` + CallID uuid.UUID `json:"call_id,omitempty"` + CallsTblID pgtype.UUID `json:"calls_tbl_id,omitempty"` + SweptCallID pgtype.UUID `json:"swept_call_id,omitempty"` + CallDate pgtype.Timestamptz `json:"call_date,omitempty"` + Notes []byte `json:"notes,omitempty"` } type Setting struct { - Name string `json:"name"` - UpdatedBy *int32 `json:"updated_by"` - Value []byte `json:"value"` + Name string `json:"name,omitempty"` + UpdatedBy *int32 `json:"updated_by,omitempty"` + Value []byte `json:"value,omitempty"` } type Share struct { - ID string `json:"id"` - EntityType string `json:"entity_type"` - EntityID uuid.UUID `json:"entity_id"` - EntityDate pgtype.Timestamptz `json:"entity_date"` - Owner int `json:"owner"` - Expiration pgtype.Timestamptz `json:"expiration"` + ID string `json:"id,omitempty"` + EntityType string `json:"entity_type,omitempty"` + EntityID uuid.UUID `json:"entity_id,omitempty"` + EntityDate pgtype.Timestamptz `json:"entity_date,omitempty"` + Owner int `json:"owner,omitempty"` + Expiration pgtype.Timestamptz `json:"expiration,omitempty"` } type SweptCall struct { - ID uuid.UUID `json:"id"` - Submitter *int32 `json:"submitter"` - System int `json:"system"` - Talkgroup int `json:"talkgroup"` - CallDate pgtype.Timestamptz `json:"call_date"` - AudioName *string `json:"audio_name"` - AudioBlob []byte `json:"audio_blob"` - Duration *int32 `json:"duration"` - AudioType *string `json:"audio_type"` - AudioUrl *string `json:"audio_url"` - Frequency int `json:"frequency"` - Frequencies []int `json:"frequencies"` - Patches []int `json:"patches"` - TGLabel *string `json:"tg_label"` - TGAlphaTag *string `json:"tg_alpha_tag"` - TGGroup *string `json:"tg_group"` - Source int `json:"source"` - Transcript *string `json:"transcript"` + ID uuid.UUID `json:"id,omitempty"` + Submitter *int32 `json:"submitter,omitempty"` + System int `json:"system,omitempty"` + Talkgroup int `json:"talkgroup,omitempty"` + CallDate pgtype.Timestamptz `json:"call_date,omitempty"` + AudioName *string `json:"audio_name,omitempty"` + AudioBlob []byte `json:"audio_blob,omitempty"` + Duration *int32 `json:"duration,omitempty"` + AudioType *string `json:"audio_type,omitempty"` + AudioUrl *string `json:"audio_url,omitempty"` + Frequency int `json:"frequency,omitempty"` + Frequencies []int `json:"frequencies,omitempty"` + Patches []int `json:"patches,omitempty"` + TGLabel *string `json:"tg_label,omitempty"` + TGAlphaTag *string `json:"tg_alpha_tag,omitempty"` + TGGroup *string `json:"tg_group,omitempty"` + Source int `json:"source,omitempty"` + Transcript *string `json:"transcript,omitempty"` } type System struct { - ID int `json:"id"` - Name string `json:"name"` + ID int `json:"id,omitempty"` + Name string `json:"name,omitempty"` } type Talkgroup struct { - ID int `json:"id"` - SystemID int32 `json:"system_id"` - TGID int32 `json:"tgid"` - Name *string `json:"name"` - AlphaTag *string `json:"alpha_tag"` - TGGroup *string `json:"tg_group"` - Frequency *int32 `json:"frequency"` - Metadata jsontypes.Metadata `json:"metadata"` - Tags []string `json:"tags"` - Alert bool `json:"alert"` - AlertRules rules.AlertRules `json:"alert_rules"` - Weight float32 `json:"weight"` - Learned bool `json:"learned"` - Ignored bool `json:"ignored"` + ID int `json:"id,omitempty"` + SystemID int32 `json:"system_id,omitempty"` + TGID int32 `json:"tgid,omitempty"` + Name *string `json:"name,omitempty"` + AlphaTag *string `json:"alpha_tag,omitempty"` + TGGroup *string `json:"tg_group,omitempty"` + Frequency *int32 `json:"frequency,omitempty"` + Metadata jsontypes.Metadata `json:"metadata,omitempty"` + Tags []string `json:"tags,omitempty"` + Alert bool `json:"alert,omitempty"` + AlertRules rules.AlertRules `json:"alert_rules,omitempty"` + Weight float32 `json:"weight,omitempty"` + Learned bool `json:"learned,omitempty"` + Ignored bool `json:"ignored,omitempty"` } type TalkgroupVersion struct { - ID int `json:"id"` - Time pgtype.Timestamptz `json:"time"` - CreatedBy *int32 `json:"created_by"` - Deleted *bool `json:"deleted"` - SystemID *int32 `json:"system_id"` - TGID *int32 `json:"tgid"` - Name *string `json:"name"` - AlphaTag *string `json:"alpha_tag"` - TGGroup *string `json:"tg_group"` - Frequency *int32 `json:"frequency"` - Metadata []byte `json:"metadata"` - Tags []string `json:"tags"` - Alert *bool `json:"alert"` - AlertRules []byte `json:"alert_rules"` - Weight *float32 `json:"weight"` - Learned *bool `json:"learned"` - Ignored *bool `json:"ignored"` + ID int `json:"id,omitempty"` + Time pgtype.Timestamptz `json:"time,omitempty"` + CreatedBy *int32 `json:"created_by,omitempty"` + Deleted *bool `json:"deleted,omitempty"` + SystemID *int32 `json:"system_id,omitempty"` + TGID *int32 `json:"tgid,omitempty"` + Name *string `json:"name,omitempty"` + AlphaTag *string `json:"alpha_tag,omitempty"` + TGGroup *string `json:"tg_group,omitempty"` + Frequency *int32 `json:"frequency,omitempty"` + Metadata []byte `json:"metadata,omitempty"` + Tags []string `json:"tags,omitempty"` + Alert *bool `json:"alert,omitempty"` + AlertRules []byte `json:"alert_rules,omitempty"` + Weight *float32 `json:"weight,omitempty"` + Learned *bool `json:"learned,omitempty"` + Ignored *bool `json:"ignored,omitempty"` } type User struct { - ID int `json:"id"` - Username string `json:"username"` - Password string `json:"password"` - Email string `json:"email"` - IsAdmin bool `json:"is_admin"` - Prefs []byte `json:"prefs"` + ID int `json:"id,omitempty"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + Email string `json:"email,omitempty"` + IsAdmin bool `json:"is_admin,omitempty"` + Prefs []byte `json:"prefs,omitempty"` } diff --git a/pkg/database/partman/partman.go b/pkg/database/partman/partman.go index d19683b..391e284 100644 --- a/pkg/database/partman/partman.go +++ b/pkg/database/partman/partman.go @@ -13,7 +13,7 @@ import ( "dynatron.me/x/stillbox/internal/isoweek" "dynatron.me/x/stillbox/pkg/config" "dynatron.me/x/stillbox/pkg/database" - "dynatron.me/x/stillbox/pkg/rbac" + "dynatron.me/x/stillbox/pkg/rbac/entities" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" @@ -135,7 +135,7 @@ func New(db database.Store, cfg config.Partition) (*partman, error) { var _ PartitionManager = (*partman)(nil) func (pm *partman) Go(ctx context.Context) { - ctx = rbac.CtxWithSubject(ctx, &rbac.SystemServiceSubject{Name: "partman"}) + ctx = entities.CtxWithSubject(ctx, &entities.SystemServiceSubject{Name: "partman"}) tick := time.NewTicker(CheckInterval) select { diff --git a/pkg/database/querier.go b/pkg/database/querier.go index dfd32b1..d2cd4a9 100644 --- a/pkg/database/querier.go +++ b/pkg/database/querier.go @@ -40,6 +40,7 @@ type Querier interface { GetIncident(ctx context.Context, id uuid.UUID) (Incident, error) GetIncidentCalls(ctx context.Context, id uuid.UUID) ([]GetIncidentCallsRow, error) GetIncidentOwner(ctx context.Context, id uuid.UUID) (int, error) + GetIncidentTalkgroups(ctx context.Context, incidentID uuid.UUID) ([]GetIncidentTalkgroupsRow, error) GetShare(ctx context.Context, id string) (Share, error) GetSystemName(ctx context.Context, systemID int) (string, error) GetTalkgroup(ctx context.Context, systemID int32, tGID int32) (GetTalkgroupRow, error) diff --git a/pkg/database/share.sql.go b/pkg/database/share.sql.go index fbd2829..53ab241 100644 --- a/pkg/database/share.sql.go +++ b/pkg/database/share.sql.go @@ -53,6 +53,40 @@ func (q *Queries) DeleteShare(ctx context.Context, id string) error { return err } +const getIncidentTalkgroups = `-- name: GetIncidentTalkgroups :many +SELECT DISTINCT + c.system, + c.talkgroup +FROM incidents_calls ic +JOIN calls c ON (c.id = ic.call_id AND c.call_date = ic.call_date) +WHERE ic.incident_id = $1 +` + +type GetIncidentTalkgroupsRow struct { + System int `json:"system"` + Talkgroup int `json:"talkgroup"` +} + +func (q *Queries) GetIncidentTalkgroups(ctx context.Context, incidentID uuid.UUID) ([]GetIncidentTalkgroupsRow, error) { + rows, err := q.db.Query(ctx, getIncidentTalkgroups, incidentID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetIncidentTalkgroupsRow + for rows.Next() { + var i GetIncidentTalkgroupsRow + if err := rows.Scan(&i.System, &i.Talkgroup); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getShare = `-- name: GetShare :one SELECT id, diff --git a/pkg/incidents/incident.go b/pkg/incidents/incident.go index b48f152..896c083 100644 --- a/pkg/incidents/incident.go +++ b/pkg/incidents/incident.go @@ -5,7 +5,7 @@ import ( "dynatron.me/x/stillbox/internal/jsontypes" "dynatron.me/x/stillbox/pkg/calls" - "dynatron.me/x/stillbox/pkg/rbac" + "dynatron.me/x/stillbox/pkg/rbac/entities" "dynatron.me/x/stillbox/pkg/users" "github.com/google/uuid" ) @@ -23,7 +23,7 @@ type Incident struct { } func (inc *Incident) GetResourceName() string { - return rbac.ResourceIncident + return entities.ResourceIncident } type IncidentCall struct { diff --git a/pkg/incidents/incstore/store.go b/pkg/incidents/incstore/store.go index c2fa0a8..45880c8 100644 --- a/pkg/incidents/incstore/store.go +++ b/pkg/incidents/incstore/store.go @@ -10,6 +10,7 @@ import ( "dynatron.me/x/stillbox/pkg/database" "dynatron.me/x/stillbox/pkg/incidents" "dynatron.me/x/stillbox/pkg/rbac" + "dynatron.me/x/stillbox/pkg/rbac/entities" "dynatron.me/x/stillbox/pkg/users" "github.com/google/uuid" "github.com/jackc/pgx/v5" @@ -143,7 +144,7 @@ func (s *store) AddRemoveIncidentCalls(ctx context.Context, incidentID uuid.UUID return err } - _, err = rbac.Check(ctx, &inc, rbac.WithActions(rbac.ActionUpdate)) + _, err = rbac.Check(ctx, &inc, rbac.WithActions(entities.ActionUpdate)) if err != nil { return err } @@ -176,7 +177,7 @@ func (s *store) AddRemoveIncidentCalls(ctx context.Context, incidentID uuid.UUID } func (s *store) Incidents(ctx context.Context, p IncidentsParams) (incs []Incident, totalCount int, err error) { - _, err = rbac.Check(ctx, new(incidents.Incident), rbac.WithActions(rbac.ActionRead)) + _, err = rbac.Check(ctx, new(incidents.Incident), rbac.WithActions(entities.ActionRead)) if err != nil { return nil, 0, err } @@ -281,7 +282,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, &incidents.Incident{ID: id}, rbac.WithActions(rbac.ActionRead)) + _, err := rbac.Check(ctx, &incidents.Incident{ID: id}, rbac.WithActions(entities.ActionRead)) if err != nil { return nil, err } @@ -337,7 +338,7 @@ func (s *store) UpdateIncident(ctx context.Context, id uuid.UUID, p UpdateIncide return nil, err } - _, err = rbac.Check(ctx, &ckinc, rbac.WithActions(rbac.ActionUpdate)) + _, err = rbac.Check(ctx, &ckinc, rbac.WithActions(entities.ActionUpdate)) if err != nil { return nil, err } @@ -360,7 +361,7 @@ func (s *store) DeleteIncident(ctx context.Context, id uuid.UUID) error { return err } - _, err = rbac.Check(ctx, &inc, rbac.WithActions(rbac.ActionDelete)) + _, err = rbac.Check(ctx, &inc, rbac.WithActions(entities.ActionDelete)) if err != nil { return err } diff --git a/pkg/nexus/nexus.go b/pkg/nexus/nexus.go index fcfe056..84d4020 100644 --- a/pkg/nexus/nexus.go +++ b/pkg/nexus/nexus.go @@ -6,7 +6,7 @@ import ( "dynatron.me/x/stillbox/pkg/calls" "dynatron.me/x/stillbox/pkg/pb" - "dynatron.me/x/stillbox/pkg/rbac" + "dynatron.me/x/stillbox/pkg/rbac/entities" "github.com/rs/zerolog/log" ) @@ -39,7 +39,7 @@ func New() *Nexus { } func (n *Nexus) Go(ctx context.Context) { - ctx = rbac.CtxWithSubject(ctx, &rbac.SystemServiceSubject{Name: "nexus"}) + ctx = entities.CtxWithSubject(ctx, &entities.SystemServiceSubject{Name: "nexus"}) for { select { case call, ok := <-n.callCh: diff --git a/pkg/rbac/entities.go b/pkg/rbac/entities.go deleted file mode 100644 index 9c710eb..0000000 --- a/pkg/rbac/entities.go +++ /dev/null @@ -1,23 +0,0 @@ -package rbac - -const ( - RoleUser = "User" - RoleSubmitter = "Submitter" - RoleAdmin = "Admin" - RoleSystem = "System" - RolePublic = "Public" - RoleShareGuest = "ShareGuest" - - ResourceCall = "Call" - ResourceIncident = "Incident" - ResourceTalkgroup = "Talkgroup" - ResourceAlert = "Alert" - ResourceShare = "Share" - ResourceAPIKey = "APIKey" - - ActionRead = "read" - ActionCreate = "create" - ActionUpdate = "update" - ActionDelete = "delete" - ActionShare = "share" -) diff --git a/pkg/rbac/entities/entities.go b/pkg/rbac/entities/entities.go new file mode 100644 index 0000000..eca0217 --- /dev/null +++ b/pkg/rbac/entities/entities.go @@ -0,0 +1,79 @@ +package entities + +import ( + "context" + + "github.com/el-mike/restrict/v2" +) + +const ( + RoleUser = "User" + RoleSubmitter = "Submitter" + RoleAdmin = "Admin" + RoleSystem = "System" + RolePublic = "Public" + RoleShareGuest = "ShareGuest" + + ResourceCall = "Call" + ResourceIncident = "Incident" + ResourceTalkgroup = "Talkgroup" + ResourceAlert = "Alert" + ResourceShare = "Share" + ResourceAPIKey = "APIKey" + + ActionRead = "read" + ActionCreate = "create" + ActionUpdate = "update" + ActionDelete = "delete" + ActionShare = "share" +) + +func SubjectFrom(ctx context.Context) Subject { + sub, ok := ctx.Value(SubjectCtxKey).(Subject) + if ok { + return sub + } + + return new(PublicSubject) +} + +type Subject interface { + restrict.Subject + GetName() string +} + +func CtxWithSubject(ctx context.Context, sub Subject) context.Context { + return context.WithValue(ctx, SubjectCtxKey, sub) +} + +type subjectContextKey string + +const SubjectCtxKey subjectContextKey = "sub" + +type Resource interface { + restrict.Resource +} + +type PublicSubject struct { + RemoteAddr string +} + +func (s *PublicSubject) GetName() string { + return "PUBLIC:" + s.RemoteAddr +} + +func (s *PublicSubject) GetRoles() []string { + return []string{RolePublic} +} + +type SystemServiceSubject struct { + Name string +} + +func (s *SystemServiceSubject) GetName() string { + return "SYSTEM:" + s.Name +} + +func (s *SystemServiceSubject) GetRoles() []string { + return []string{RoleSystem} +} diff --git a/pkg/rbac/mocks/RBAC.go b/pkg/rbac/mocks/RBAC.go index d7de98b..6735737 100644 --- a/pkg/rbac/mocks/RBAC.go +++ b/pkg/rbac/mocks/RBAC.go @@ -5,9 +5,11 @@ package mocks import ( context "context" - rbac "dynatron.me/x/stillbox/pkg/rbac" + entities "dynatron.me/x/stillbox/pkg/rbac/entities" mock "github.com/stretchr/testify/mock" + rbac "dynatron.me/x/stillbox/pkg/rbac" + restrict "github.com/el-mike/restrict/v2" ) @@ -25,7 +27,7 @@ func (_m *RBAC) EXPECT() *RBAC_Expecter { } // Check provides a mock function with given fields: ctx, res, opts -func (_m *RBAC) Check(ctx context.Context, res restrict.Resource, opts ...rbac.CheckOption) (rbac.Subject, error) { +func (_m *RBAC) Check(ctx context.Context, res restrict.Resource, opts ...rbac.CheckOption) (entities.Subject, error) { _va := make([]interface{}, len(opts)) for _i := range opts { _va[_i] = opts[_i] @@ -39,16 +41,16 @@ func (_m *RBAC) Check(ctx context.Context, res restrict.Resource, opts ...rbac.C panic("no return value specified for Check") } - var r0 rbac.Subject + var r0 entities.Subject var r1 error - if rf, ok := ret.Get(0).(func(context.Context, restrict.Resource, ...rbac.CheckOption) (rbac.Subject, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, restrict.Resource, ...rbac.CheckOption) (entities.Subject, error)); ok { return rf(ctx, res, opts...) } - if rf, ok := ret.Get(0).(func(context.Context, restrict.Resource, ...rbac.CheckOption) rbac.Subject); ok { + if rf, ok := ret.Get(0).(func(context.Context, restrict.Resource, ...rbac.CheckOption) entities.Subject); ok { r0 = rf(ctx, res, opts...) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(rbac.Subject) + r0 = ret.Get(0).(entities.Subject) } } @@ -88,12 +90,12 @@ func (_c *RBAC_Check_Call) Run(run func(ctx context.Context, res restrict.Resour return _c } -func (_c *RBAC_Check_Call) Return(_a0 rbac.Subject, _a1 error) *RBAC_Check_Call { +func (_c *RBAC_Check_Call) Return(_a0 entities.Subject, _a1 error) *RBAC_Check_Call { _c.Call.Return(_a0, _a1) return _c } -func (_c *RBAC_Check_Call) RunAndReturn(run func(context.Context, restrict.Resource, ...rbac.CheckOption) (rbac.Subject, error)) *RBAC_Check_Call { +func (_c *RBAC_Check_Call) RunAndReturn(run func(context.Context, restrict.Resource, ...rbac.CheckOption) (entities.Subject, error)) *RBAC_Check_Call { _c.Call.Return(run) return _c } diff --git a/pkg/rbac/conditions.go b/pkg/rbac/policy/conditions.go similarity index 96% rename from pkg/rbac/conditions.go rename to pkg/rbac/policy/conditions.go index ddebd69..77b8428 100644 --- a/pkg/rbac/conditions.go +++ b/pkg/rbac/policy/conditions.go @@ -1,4 +1,4 @@ -package rbac +package policy import ( "context" @@ -6,7 +6,7 @@ import ( "fmt" "reflect" - "dynatron.me/x/stillbox/pkg/database" + "dynatron.me/x/stillbox/pkg/incidents/incstore" "github.com/el-mike/restrict/v2" "github.com/google/uuid" @@ -54,7 +54,7 @@ func (c *CallInIncidentCondition) Check(r *restrict.AccessRequest) error { return restrict.NewConditionNotSatisfiedError(c, r, errors.New("call ID is not UUID")) } - inCall, err := database.FromCtx(ctx).CallInIncident(ctx, incID, callID) + inCall, err := incstore.FromCtx(ctx).CallIn(ctx, incID, callID) if err != nil { return restrict.NewConditionNotSatisfiedError(c, r, err) } diff --git a/pkg/rbac/policy.go b/pkg/rbac/policy/policy.go similarity index 66% rename from pkg/rbac/policy.go rename to pkg/rbac/policy/policy.go index 0e76ca2..16798a6 100644 --- a/pkg/rbac/policy.go +++ b/pkg/rbac/policy/policy.go @@ -1,6 +1,8 @@ -package rbac +package policy import ( + "dynatron.me/x/stillbox/pkg/rbac/entities" + "github.com/el-mike/restrict/v2" ) @@ -19,90 +21,90 @@ const ( var Policy = &restrict.PolicyDefinition{ Roles: restrict.Roles{ - RoleUser: { + entities.RoleUser: { Description: "An authenticated user", Grants: restrict.GrantsMap{ - ResourceIncident: { - &restrict.Permission{Action: ActionRead}, - &restrict.Permission{Action: ActionCreate}, + entities.ResourceIncident: { + &restrict.Permission{Action: entities.ActionRead}, + &restrict.Permission{Action: entities.ActionCreate}, &restrict.Permission{Preset: PresetUpdateOwn}, &restrict.Permission{Preset: PresetDeleteOwn}, &restrict.Permission{Preset: PresetShareOwn}, }, - ResourceCall: { - &restrict.Permission{Action: ActionRead}, - &restrict.Permission{Action: ActionCreate}, + entities.ResourceCall: { + &restrict.Permission{Action: entities.ActionRead}, + &restrict.Permission{Action: entities.ActionCreate}, &restrict.Permission{Preset: PresetUpdateSubmitter}, &restrict.Permission{Preset: PresetDeleteSubmitter}, - &restrict.Permission{Action: ActionShare}, + &restrict.Permission{Action: entities.ActionShare}, }, - ResourceTalkgroup: { - &restrict.Permission{Action: ActionRead}, + entities.ResourceTalkgroup: { + &restrict.Permission{Action: entities.ActionRead}, }, - ResourceShare: { - &restrict.Permission{Action: ActionRead}, - &restrict.Permission{Action: ActionCreate}, + entities.ResourceShare: { + &restrict.Permission{Action: entities.ActionRead}, + &restrict.Permission{Action: entities.ActionCreate}, &restrict.Permission{Preset: PresetUpdateOwn}, &restrict.Permission{Preset: PresetDeleteOwn}, }, }, }, - RoleSubmitter: { + entities.RoleSubmitter: { Description: "A role that can submit calls", Grants: restrict.GrantsMap{ - ResourceCall: { - &restrict.Permission{Action: ActionCreate}, + entities.ResourceCall: { + &restrict.Permission{Action: entities.ActionCreate}, }, - ResourceTalkgroup: { + entities.ResourceTalkgroup: { // for learning TGs - &restrict.Permission{Action: ActionCreate}, - &restrict.Permission{Action: ActionUpdate}, + &restrict.Permission{Action: entities.ActionCreate}, + &restrict.Permission{Action: entities.ActionUpdate}, }, }, }, - RoleShareGuest: { + entities.RoleShareGuest: { Description: "Someone who has a valid share link", Grants: restrict.GrantsMap{ - ResourceCall: { + entities.ResourceCall: { &restrict.Permission{Preset: PresetReadShared}, &restrict.Permission{Preset: PresetReadInSharedIncident}, }, - ResourceIncident: { + entities.ResourceIncident: { &restrict.Permission{Preset: PresetReadShared}, }, - ResourceTalkgroup: { - &restrict.Permission{Action: ActionRead}, + entities.ResourceTalkgroup: { + &restrict.Permission{Action: entities.ActionRead}, }, }, }, - RoleAdmin: { - Parents: []string{RoleUser}, + entities.RoleAdmin: { + Parents: []string{entities.RoleUser}, Grants: restrict.GrantsMap{ - ResourceIncident: { - &restrict.Permission{Action: ActionUpdate}, - &restrict.Permission{Action: ActionDelete}, - &restrict.Permission{Action: ActionShare}, + entities.ResourceIncident: { + &restrict.Permission{Action: entities.ActionUpdate}, + &restrict.Permission{Action: entities.ActionDelete}, + &restrict.Permission{Action: entities.ActionShare}, }, - ResourceCall: { - &restrict.Permission{Action: ActionUpdate}, - &restrict.Permission{Action: ActionDelete}, - &restrict.Permission{Action: ActionShare}, + entities.ResourceCall: { + &restrict.Permission{Action: entities.ActionUpdate}, + &restrict.Permission{Action: entities.ActionDelete}, + &restrict.Permission{Action: entities.ActionShare}, }, - ResourceTalkgroup: { - &restrict.Permission{Action: ActionUpdate}, - &restrict.Permission{Action: ActionCreate}, - &restrict.Permission{Action: ActionDelete}, + entities.ResourceTalkgroup: { + &restrict.Permission{Action: entities.ActionUpdate}, + &restrict.Permission{Action: entities.ActionCreate}, + &restrict.Permission{Action: entities.ActionDelete}, }, }, }, - RoleSystem: { - Parents: []string{RoleSystem}, + entities.RoleSystem: { + Parents: []string{entities.RoleSystem}, }, - RolePublic: { + entities.RolePublic: { /* Grants: restrict.GrantsMap{ - ResourceShare: { - &restrict.Permission{Action: ActionRead}, + entities.ResourceShare: { + &restrict.Permission{Action: entities.ActionRead}, }, }, */ @@ -110,7 +112,7 @@ var Policy = &restrict.PolicyDefinition{ }, PermissionPresets: restrict.PermissionPresets{ PresetUpdateOwn: &restrict.Permission{ - Action: ActionUpdate, + Action: entities.ActionUpdate, Conditions: restrict.Conditions{ &restrict.EqualCondition{ ID: "isOwner", @@ -126,7 +128,7 @@ var Policy = &restrict.PolicyDefinition{ }, }, PresetDeleteOwn: &restrict.Permission{ - Action: ActionDelete, + Action: entities.ActionDelete, Conditions: restrict.Conditions{ &restrict.EqualCondition{ ID: "isOwner", @@ -142,7 +144,7 @@ var Policy = &restrict.PolicyDefinition{ }, }, PresetShareOwn: &restrict.Permission{ - Action: ActionShare, + Action: entities.ActionShare, Conditions: restrict.Conditions{ &restrict.EqualCondition{ ID: "isOwner", @@ -158,7 +160,7 @@ var Policy = &restrict.PolicyDefinition{ }, }, PresetUpdateSubmitter: &restrict.Permission{ - Action: ActionUpdate, + Action: entities.ActionUpdate, Conditions: restrict.Conditions{ &SubmitterEqualCondition{ ID: "isSubmitter", @@ -174,7 +176,7 @@ var Policy = &restrict.PolicyDefinition{ }, }, PresetDeleteSubmitter: &restrict.Permission{ - Action: ActionDelete, + Action: entities.ActionDelete, Conditions: restrict.Conditions{ &SubmitterEqualCondition{ ID: "isSubmitter", @@ -190,7 +192,7 @@ var Policy = &restrict.PolicyDefinition{ }, }, PresetShareSubmitter: &restrict.Permission{ - Action: ActionShare, + Action: entities.ActionShare, Conditions: restrict.Conditions{ &SubmitterEqualCondition{ ID: "isSubmitter", @@ -206,7 +208,7 @@ var Policy = &restrict.PolicyDefinition{ }, }, PresetReadShared: &restrict.Permission{ - Action: ActionRead, + Action: entities.ActionRead, Conditions: restrict.Conditions{ &restrict.EqualCondition{ ID: "isOwner", @@ -222,7 +224,7 @@ var Policy = &restrict.PolicyDefinition{ }, }, PresetReadInSharedIncident: &restrict.Permission{ - Action: ActionRead, + Action: entities.ActionRead, Conditions: restrict.Conditions{ &CallInIncidentCondition{ ID: "callInIncident", diff --git a/pkg/rbac/rbac.go b/pkg/rbac/rbac.go index 172b634..58da816 100644 --- a/pkg/rbac/rbac.go +++ b/pkg/rbac/rbac.go @@ -4,6 +4,8 @@ import ( "context" "errors" + "dynatron.me/x/stillbox/pkg/rbac/entities" + "github.com/el-mike/restrict/v2" "github.com/el-mike/restrict/v2/adapters" ) @@ -12,14 +14,6 @@ var ( ErrBadSubject = errors.New("bad subject in token") ) -type subjectContextKey string - -const SubjectCtxKey subjectContextKey = "sub" - -func CtxWithSubject(ctx context.Context, sub Subject) context.Context { - return context.WithValue(ctx, SubjectCtxKey, sub) -} - func ErrAccessDenied(err error) *restrict.AccessDeniedError { if accessErr, ok := err.(*restrict.AccessDeniedError); ok { return accessErr @@ -28,15 +22,6 @@ func ErrAccessDenied(err error) *restrict.AccessDeniedError { return nil } -func SubjectFrom(ctx context.Context) Subject { - sub, ok := ctx.Value(SubjectCtxKey).(Subject) - if ok { - return sub - } - - return new(PublicSubject) -} - type rbacCtxKey string const RBACCtxKey rbacCtxKey = "rbac" @@ -81,17 +66,8 @@ func UseResource(rsc string) restrict.Resource { return restrict.UseResource(rsc) } -type Subject interface { - restrict.Subject - GetName() string -} - -type Resource interface { - restrict.Resource -} - type RBAC interface { - Check(ctx context.Context, res restrict.Resource, opts ...CheckOption) (Subject, error) + Check(ctx context.Context, res restrict.Resource, opts ...CheckOption) (entities.Subject, error) } type rbac struct { @@ -99,8 +75,8 @@ type rbac struct { access *restrict.AccessManager } -func New() (*rbac, error) { - adapter := adapters.NewInMemoryAdapter(Policy) +func New(pol *restrict.PolicyDefinition) (*rbac, error) { + adapter := adapters.NewInMemoryAdapter(pol) polMan, err := restrict.NewPolicyManager(adapter, true) if err != nil { return nil, err @@ -114,12 +90,12 @@ func New() (*rbac, error) { } // Check is a convenience function to pull the RBAC instance out of ctx and Check. -func Check(ctx context.Context, res restrict.Resource, opts ...CheckOption) (Subject, error) { +func Check(ctx context.Context, res restrict.Resource, opts ...CheckOption) (entities.Subject, error) { return FromCtx(ctx).Check(ctx, res, opts...) } -func (r *rbac) Check(ctx context.Context, res restrict.Resource, opts ...CheckOption) (Subject, error) { - sub := SubjectFrom(ctx) +func (r *rbac) Check(ctx context.Context, res restrict.Resource, opts ...CheckOption) (entities.Subject, error) { + sub := entities.SubjectFrom(ctx) o := checkOptions{} for _, opt := range opts { @@ -139,34 +115,5 @@ func (r *rbac) Check(ctx context.Context, res restrict.Resource, opts ...CheckOp Context: o.context, } - err := r.access.Authorize(req) - if err != nil { - return nil, err - } - - return sub, nil -} - -type PublicSubject struct { - RemoteAddr string -} - -func (s *PublicSubject) GetName() string { - return "PUBLIC:" + s.RemoteAddr -} - -func (s *PublicSubject) GetRoles() []string { - return []string{RolePublic} -} - -type SystemServiceSubject struct { - Name string -} - -func (s *SystemServiceSubject) GetName() string { - return "SYSTEM:" + s.Name -} - -func (s *SystemServiceSubject) GetRoles() []string { - return []string{RoleSystem} + return sub, r.access.Authorize(req) } diff --git a/pkg/rbac/rbac_test.go b/pkg/rbac/rbac_test.go index 55fd81b..4ffbd73 100644 --- a/pkg/rbac/rbac_test.go +++ b/pkg/rbac/rbac_test.go @@ -10,6 +10,8 @@ import ( "dynatron.me/x/stillbox/pkg/calls" "dynatron.me/x/stillbox/pkg/incidents" "dynatron.me/x/stillbox/pkg/rbac" + "dynatron.me/x/stillbox/pkg/rbac/entities" + "dynatron.me/x/stillbox/pkg/rbac/policy" "dynatron.me/x/stillbox/pkg/talkgroups" "dynatron.me/x/stillbox/pkg/users" "github.com/el-mike/restrict/v2" @@ -20,8 +22,8 @@ import ( func TestRBAC(t *testing.T) { tests := []struct { name string - subject rbac.Subject - resource rbac.Resource + subject entities.Subject + resource entities.Resource action string expectErr error }{ @@ -32,7 +34,7 @@ func TestRBAC(t *testing.T) { IsAdmin: true, }, resource: &talkgroups.Talkgroup{}, - action: rbac.ActionUpdate, + action: entities.ActionUpdate, expectErr: nil, }, { @@ -45,7 +47,7 @@ func TestRBAC(t *testing.T) { Name: "test incident", Owner: 4, }, - action: rbac.ActionUpdate, + action: entities.ActionUpdate, expectErr: nil, }, { @@ -57,7 +59,7 @@ func TestRBAC(t *testing.T) { Name: "test incident", Owner: 4, }, - action: rbac.ActionUpdate, + action: entities.ActionUpdate, expectErr: errors.New(`access denied for Action: "update" on Resource: "Incident"`), }, { @@ -69,7 +71,7 @@ func TestRBAC(t *testing.T) { Name: "test incident", Owner: 2, }, - action: rbac.ActionUpdate, + action: entities.ActionUpdate, expectErr: nil, }, { @@ -81,7 +83,7 @@ func TestRBAC(t *testing.T) { Name: "test incident", Owner: 6, }, - action: rbac.ActionDelete, + action: entities.ActionDelete, expectErr: errors.New(`access denied for Action: "delete" on Resource: "Incident"`), }, { @@ -93,7 +95,7 @@ func TestRBAC(t *testing.T) { resource: &calls.Call{ Submitter: common.PtrTo(users.UserID(4)), }, - action: rbac.ActionUpdate, + action: entities.ActionUpdate, expectErr: nil, }, { @@ -104,7 +106,7 @@ func TestRBAC(t *testing.T) { resource: &calls.Call{ Submitter: common.PtrTo(users.UserID(4)), }, - action: rbac.ActionUpdate, + action: entities.ActionUpdate, expectErr: errors.New(`access denied for Action: "update" on Resource: "Call"`), }, { @@ -115,7 +117,7 @@ func TestRBAC(t *testing.T) { resource: &calls.Call{ Submitter: common.PtrTo(users.UserID(2)), }, - action: rbac.ActionUpdate, + action: entities.ActionUpdate, expectErr: nil, }, { @@ -126,7 +128,7 @@ func TestRBAC(t *testing.T) { resource: &calls.Call{ Submitter: nil, }, - action: rbac.ActionUpdate, + action: entities.ActionUpdate, expectErr: errors.New(`access denied for Action: "update" on Resource: "Call"`), }, { @@ -137,7 +139,7 @@ func TestRBAC(t *testing.T) { resource: &calls.Call{ Submitter: common.PtrTo(users.UserID(6)), }, - action: rbac.ActionDelete, + action: entities.ActionDelete, expectErr: errors.New(`access denied for Action: "delete" on Resource: "Call"`), }, { @@ -148,7 +150,7 @@ func TestRBAC(t *testing.T) { resource: &calls.Call{ Submitter: common.PtrTo(users.UserID(6)), }, - action: rbac.ActionShare, + action: entities.ActionShare, expectErr: nil, }, { @@ -160,7 +162,7 @@ func TestRBAC(t *testing.T) { resource: &calls.Call{ Submitter: common.PtrTo(users.UserID(6)), }, - action: rbac.ActionShare, + action: entities.ActionShare, expectErr: nil, }, { @@ -171,7 +173,7 @@ func TestRBAC(t *testing.T) { resource: &calls.Call{ Submitter: common.PtrTo(users.UserID(6)), }, - action: rbac.ActionShare, + action: entities.ActionShare, expectErr: nil, }, { @@ -182,7 +184,7 @@ func TestRBAC(t *testing.T) { resource: &incidents.Incident{ Owner: users.UserID(6), }, - action: rbac.ActionShare, + action: entities.ActionShare, expectErr: errors.New(`access denied for Action: "share" on Resource: "Incident"`), }, { @@ -194,7 +196,7 @@ func TestRBAC(t *testing.T) { resource: &incidents.Incident{ Owner: users.UserID(6), }, - action: rbac.ActionShare, + action: entities.ActionShare, expectErr: nil, }, { @@ -205,15 +207,15 @@ func TestRBAC(t *testing.T) { resource: &incidents.Incident{ Owner: users.UserID(6), }, - action: rbac.ActionShare, + action: entities.ActionShare, expectErr: nil, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - ctx := rbac.CtxWithSubject(context.Background(), tc.subject) - rb, err := rbac.New() + ctx := entities.CtxWithSubject(context.Background(), tc.subject) + rb, err := rbac.New(policy.Policy) require.NoError(t, err) sub, err := rb.Check(ctx, tc.resource, rbac.WithActions(tc.action)) if tc.expectErr != nil { diff --git a/pkg/rest/share.go b/pkg/rest/share.go index 8406533..86e33b8 100644 --- a/pkg/rest/share.go +++ b/pkg/rest/share.go @@ -7,7 +7,7 @@ import ( "time" "dynatron.me/x/stillbox/internal/forms" - "dynatron.me/x/stillbox/pkg/rbac" + "dynatron.me/x/stillbox/pkg/rbac/entities" "dynatron.me/x/stillbox/pkg/shares" "github.com/go-chi/chi/v5" @@ -133,7 +133,7 @@ func (sa *shareAPI) routeShare(w http.ResponseWriter, r *http.Request) { return } - ctx = rbac.CtxWithSubject(ctx, sh) + ctx = entities.CtxWithSubject(ctx, sh) r = r.WithContext(ctx) if params.SubType != nil { diff --git a/pkg/server/server.go b/pkg/server/server.go index 5d144a8..9aa0efb 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -17,6 +17,7 @@ import ( "dynatron.me/x/stillbox/pkg/nexus" "dynatron.me/x/stillbox/pkg/notify" "dynatron.me/x/stillbox/pkg/rbac" + "dynatron.me/x/stillbox/pkg/rbac/policy" "dynatron.me/x/stillbox/pkg/rest" "dynatron.me/x/stillbox/pkg/shares" "dynatron.me/x/stillbox/pkg/sinks" @@ -80,7 +81,7 @@ func New(ctx context.Context, cfg *config.Configuration) (*Server, error) { tgCache := tgstore.NewCache(db) api := rest.New(cfg.BaseURL.URL()) - rbacSvc, err := rbac.New() + rbacSvc, err := rbac.New(policy.Policy) if err != nil { return nil, err } diff --git a/pkg/shares/service.go b/pkg/shares/service.go index 376a17e..245b61d 100644 --- a/pkg/shares/service.go +++ b/pkg/shares/service.go @@ -4,7 +4,7 @@ import ( "context" "time" - "dynatron.me/x/stillbox/pkg/rbac" + "dynatron.me/x/stillbox/pkg/rbac/entities" "github.com/rs/zerolog/log" ) @@ -23,7 +23,7 @@ type service struct { } func (s *service) Go(ctx context.Context) { - ctx = rbac.CtxWithSubject(ctx, &rbac.SystemServiceSubject{Name: "share"}) + ctx = entities.CtxWithSubject(ctx, &entities.SystemServiceSubject{Name: "share"}) tick := time.NewTicker(PruneInterval) diff --git a/pkg/shares/share.go b/pkg/shares/share.go index 17350c1..6184c02 100644 --- a/pkg/shares/share.go +++ b/pkg/shares/share.go @@ -10,6 +10,7 @@ import ( "dynatron.me/x/stillbox/pkg/calls/callstore" "dynatron.me/x/stillbox/pkg/incidents/incstore" "dynatron.me/x/stillbox/pkg/rbac" + "dynatron.me/x/stillbox/pkg/rbac/entities" "dynatron.me/x/stillbox/pkg/users" "github.com/google/uuid" @@ -57,11 +58,11 @@ func (s *Share) GetName() string { } func (s *Share) GetRoles() []string { - return []string{rbac.RoleShareGuest} + return []string{entities.RoleShareGuest} } func (s *Share) GetResourceName() string { - return rbac.ResourceShare + return entities.ResourceShare } type CreateShareParams struct { @@ -88,7 +89,7 @@ func (s *service) checkEntity(ctx context.Context, sh *CreateShareParams) (*time if err != nil { return nil, err } - _, err = rbac.Check(ctx, &i, rbac.WithActions(rbac.ActionShare)) + _, err = rbac.Check(ctx, &i, rbac.WithActions(entities.ActionShare)) if err != nil { return nil, err } diff --git a/pkg/shares/store.go b/pkg/shares/store.go index 35faa04..d0fbad0 100644 --- a/pkg/shares/store.go +++ b/pkg/shares/store.go @@ -7,6 +7,7 @@ import ( "dynatron.me/x/stillbox/internal/jsontypes" "dynatron.me/x/stillbox/pkg/database" "dynatron.me/x/stillbox/pkg/rbac" + "dynatron.me/x/stillbox/pkg/rbac/entities" "dynatron.me/x/stillbox/pkg/users" "github.com/jackc/pgx/v5" ) @@ -79,7 +80,7 @@ func (s *postgresStore) Create(ctx context.Context, share *Share) error { } func (s *postgresStore) Delete(ctx context.Context, id string) error { - _, err := rbac.Check(ctx, new(Share), rbac.WithActions(rbac.ActionDelete)) + _, err := rbac.Check(ctx, new(Share), rbac.WithActions(entities.ActionDelete)) if err != nil { return err } diff --git a/pkg/sources/http.go b/pkg/sources/http.go index dfd8df5..dbc51f7 100644 --- a/pkg/sources/http.go +++ b/pkg/sources/http.go @@ -10,6 +10,7 @@ import ( "dynatron.me/x/stillbox/pkg/auth" "dynatron.me/x/stillbox/pkg/calls" "dynatron.me/x/stillbox/pkg/rbac" + "dynatron.me/x/stillbox/pkg/rbac/entities" "dynatron.me/x/stillbox/pkg/users" "github.com/go-chi/chi/v5" "github.com/rs/zerolog/log" @@ -131,7 +132,7 @@ func (h *RdioHTTP) routeCallUpload(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusBadRequest) return } - err = h.ing.Ingest(rbac.CtxWithSubject(ctx, submitterSub), call) + err = h.ing.Ingest(entities.CtxWithSubject(ctx, submitterSub), call) if err != nil { if rbac.ErrAccessDenied(err) != nil { log.Error().Err(err).Msg("ingest failed") diff --git a/pkg/talkgroups/filter/filter.go b/pkg/talkgroups/filter/filter.go index 11c38c7..68a6b01 100644 --- a/pkg/talkgroups/filter/filter.go +++ b/pkg/talkgroups/filter/filter.go @@ -122,6 +122,7 @@ func (f *TalkgroupFilter) compile(ctx context.Context) error { if f.hasTags() { // don't bother with DB if no tags db := database.FromCtx(ctx) + // TODO: change this to use tgstore, and make sure the context is no longer a system subject (see nexus.Go) tagTGs, err := db.GetTalkgroupIDsByTags(ctx, f.TalkgroupTagsAny, f.TalkgroupTagsAll, f.TalkgroupTagsNot) if err != nil { return err diff --git a/pkg/talkgroups/talkgroup.go b/pkg/talkgroups/talkgroup.go index 7965e98..8082ffd 100644 --- a/pkg/talkgroups/talkgroup.go +++ b/pkg/talkgroups/talkgroup.go @@ -9,7 +9,7 @@ import ( "strings" "dynatron.me/x/stillbox/pkg/database" - "dynatron.me/x/stillbox/pkg/rbac" + "dynatron.me/x/stillbox/pkg/rbac/entities" ) type Talkgroup struct { @@ -19,7 +19,7 @@ type Talkgroup struct { } func (t *Talkgroup) GetResourceName() string { - return rbac.ResourceTalkgroup + return entities.ResourceTalkgroup } func (t Talkgroup) String() string { diff --git a/pkg/talkgroups/tgstore/store.go b/pkg/talkgroups/tgstore/store.go index 64a010e..ba86f06 100644 --- a/pkg/talkgroups/tgstore/store.go +++ b/pkg/talkgroups/tgstore/store.go @@ -12,6 +12,7 @@ import ( "dynatron.me/x/stillbox/pkg/config" "dynatron.me/x/stillbox/pkg/database" "dynatron.me/x/stillbox/pkg/rbac" + "dynatron.me/x/stillbox/pkg/rbac/entities" tgsp "dynatron.me/x/stillbox/pkg/talkgroups" "dynatron.me/x/stillbox/pkg/users" @@ -327,7 +328,7 @@ func addToRowList[T rowType](t *cache, tgRecords []T) []*tgsp.Talkgroup { } func (t *cache) TGs(ctx context.Context, tgs tgsp.IDs, opts ...Option) ([]*tgsp.Talkgroup, error) { - _, err := rbac.Check(ctx, rbac.UseResource(rbac.ResourceTalkgroup), rbac.WithActions(rbac.ActionRead)) + _, err := rbac.Check(ctx, rbac.UseResource(entities.ResourceTalkgroup), rbac.WithActions(entities.ActionRead)) if err != nil { return nil, err } @@ -430,7 +431,7 @@ func (t *cache) Weight(ctx context.Context, id tgsp.ID, tm time.Time) float64 { } func (t *cache) SystemTGs(ctx context.Context, systemID int, opts ...Option) ([]*tgsp.Talkgroup, error) { - _, err := rbac.Check(ctx, rbac.UseResource(rbac.ResourceTalkgroup), rbac.WithActions(rbac.ActionRead)) + _, err := rbac.Check(ctx, rbac.UseResource(entities.ResourceTalkgroup), rbac.WithActions(entities.ActionRead)) if err != nil { return nil, err } @@ -486,7 +487,7 @@ func (t *cache) SystemTGs(ctx context.Context, systemID int, opts ...Option) ([] } func (t *cache) TG(ctx context.Context, tg tgsp.ID) (*tgsp.Talkgroup, error) { - _, err := rbac.Check(ctx, rbac.UseResource(rbac.ResourceTalkgroup), rbac.WithActions(rbac.ActionRead)) + _, err := rbac.Check(ctx, rbac.UseResource(entities.ResourceTalkgroup), rbac.WithActions(entities.ActionRead)) if err != nil { return nil, err } @@ -513,7 +514,7 @@ func (t *cache) TG(ctx context.Context, tg tgsp.ID) (*tgsp.Talkgroup, error) { } func (t *cache) SystemName(ctx context.Context, id int) (name string, has bool) { - _, err := rbac.Check(ctx, rbac.UseResource(rbac.ResourceTalkgroup), rbac.WithActions(rbac.ActionRead)) + _, err := rbac.Check(ctx, rbac.UseResource(entities.ResourceTalkgroup), rbac.WithActions(entities.ActionRead)) if err != nil { return "", false } @@ -587,7 +588,7 @@ func (t *cache) UpdateTG(ctx context.Context, input database.UpdateTalkgroupPara } func (t *cache) DeleteSystem(ctx context.Context, id int) error { - _, err := rbac.Check(ctx, rbac.UseResource(rbac.ResourceTalkgroup), rbac.WithActions(rbac.ActionDelete)) + _, err := rbac.Check(ctx, rbac.UseResource(entities.ResourceTalkgroup), rbac.WithActions(entities.ActionDelete)) if err != nil { return err } @@ -609,7 +610,7 @@ func (t *cache) DeleteSystem(ctx context.Context, id int) error { } func (t *cache) DeleteTG(ctx context.Context, id tgsp.ID) error { - _, err := rbac.Check(ctx, rbac.UseResource(rbac.ResourceTalkgroup), rbac.WithActions(rbac.ActionDelete)) + _, err := rbac.Check(ctx, rbac.UseResource(entities.ResourceTalkgroup), rbac.WithActions(entities.ActionDelete)) if err != nil { return err } @@ -645,7 +646,7 @@ func (t *cache) DeleteTG(ctx context.Context, id tgsp.ID) error { } func (t *cache) LearnTG(ctx context.Context, c *calls.Call) (*tgsp.Talkgroup, error) { - _, err := rbac.Check(ctx, rbac.UseResource(rbac.ResourceTalkgroup), rbac.WithActions(rbac.ActionCreate, rbac.ActionUpdate)) + _, err := rbac.Check(ctx, rbac.UseResource(entities.ResourceTalkgroup), rbac.WithActions(entities.ActionCreate, entities.ActionUpdate)) if err != nil { return nil, err } @@ -764,7 +765,7 @@ func (t *cache) UpsertTGs(ctx context.Context, system int, input []database.Upse } func (t *cache) CreateSystem(ctx context.Context, id int, name string) error { - _, err := rbac.Check(ctx, rbac.UseResource(rbac.ResourceTalkgroup), rbac.WithActions(rbac.ActionCreate)) + _, err := rbac.Check(ctx, rbac.UseResource(entities.ResourceTalkgroup), rbac.WithActions(entities.ActionCreate)) if err != nil { return err } @@ -778,7 +779,7 @@ func (t *cache) CreateSystem(ctx context.Context, id int, name string) error { } func (t *cache) Tags(ctx context.Context) ([]string, error) { - _, err := rbac.Check(ctx, rbac.UseResource(rbac.ResourceTalkgroup), rbac.WithActions(rbac.ActionRead)) + _, err := rbac.Check(ctx, rbac.UseResource(entities.ResourceTalkgroup), rbac.WithActions(entities.ActionRead)) if err != nil { return nil, err } diff --git a/pkg/users/user.go b/pkg/users/user.go index d4904a7..9860be1 100644 --- a/pkg/users/user.go +++ b/pkg/users/user.go @@ -7,6 +7,7 @@ import ( "dynatron.me/x/stillbox/pkg/database" "dynatron.me/x/stillbox/pkg/rbac" + "dynatron.me/x/stillbox/pkg/rbac/entities" ) type UserID int @@ -30,11 +31,11 @@ func (u UserID) IsValid() bool { } func From(ctx context.Context) (*User, error) { - sub := rbac.SubjectFrom(ctx) + sub := entities.SubjectFrom(ctx) return FromSubject(sub) } -func UserCheck(ctx context.Context, rsc rbac.Resource, actions string) (*User, error) { +func UserCheck(ctx context.Context, rsc entities.Resource, actions string) (*User, error) { acts := strings.Split(actions, "+") subj, err := rbac.FromCtx(ctx).Check(ctx, rsc, rbac.WithActions(acts...)) if err != nil { @@ -44,7 +45,7 @@ func UserCheck(ctx context.Context, rsc rbac.Resource, actions string) (*User, e return FromSubject(subj) } -func FromSubject(sub rbac.Subject) (*User, error) { +func FromSubject(sub entities.Subject) (*User, error) { if sub == nil { return nil, rbac.ErrBadSubject } @@ -73,10 +74,10 @@ func (u *User) GetName() string { func (u *User) GetRoles() []string { r := make([]string, 1, 2) - r[0] = rbac.RoleUser + r[0] = entities.RoleUser if u.IsAdmin { - r = append(r, rbac.RoleAdmin) + r = append(r, entities.RoleAdmin) } return r diff --git a/sql/postgres/queries/share.sql b/sql/postgres/queries/share.sql index 7854c48..afbf1b6 100644 --- a/sql/postgres/queries/share.sql +++ b/sql/postgres/queries/share.sql @@ -24,3 +24,11 @@ DELETE FROM shares WHERE id = @id; -- name: PruneShares :exec DELETE FROM shares WHERE expiration < NOW(); + +-- name: GetIncidentTalkgroups :many +SELECT DISTINCT + c.system, + c.talkgroup +FROM incidents_calls ic +JOIN calls c ON (c.id = ic.call_id AND c.call_date = ic.call_date) +WHERE ic.incident_id = @incident_id;