diff --git a/annotation.go b/annotation.go index 6eb15c5b33..3056a0d44f 100644 --- a/annotation.go +++ b/annotation.go @@ -68,9 +68,9 @@ type AnnotationService interface { // CreateAnnotations creates annotations. CreateAnnotations(ctx context.Context, orgID platform.ID, create []AnnotationCreate) ([]AnnotationEvent, error) // ListAnnotations lists all annotations matching the filter. - ListAnnotations(ctx context.Context, orgID platform.ID, filter AnnotationListFilter) (ReadAnnotations, error) + ListAnnotations(ctx context.Context, orgID platform.ID, filter AnnotationListFilter) ([]StoredAnnotation, error) // GetAnnotation gets an annotation by id. - GetAnnotation(ctx context.Context, id platform.ID) (*AnnotationEvent, error) + GetAnnotation(ctx context.Context, id platform.ID) (*StoredAnnotation, error) // DeleteAnnotations deletes annotations matching the filter. DeleteAnnotations(ctx context.Context, orgID platform.ID, delete AnnotationDeleteFilter) error // DeleteAnnotation deletes an annotation by id. @@ -79,13 +79,16 @@ type AnnotationService interface { UpdateAnnotation(ctx context.Context, id platform.ID, update AnnotationCreate) (*AnnotationEvent, error) // ListStreams lists all streams matching the filter. - ListStreams(ctx context.Context, orgID platform.ID, filter StreamListFilter) ([]ReadStream, error) + ListStreams(ctx context.Context, orgID platform.ID, filter StreamListFilter) ([]StoredStream, error) // CreateOrUpdateStream creates or updates the matching stream by name. - CreateOrUpdateStream(ctx context.Context, stream Stream) (*ReadStream, error) + CreateOrUpdateStream(ctx context.Context, orgID platform.ID, stream Stream) (*ReadStream, error) + // GetStream gets a stream by id. Currently this is only used for authorization, and there are no + // API routes for getting a single stream by ID. + GetStream(ctx context.Context, id platform.ID) (*StoredStream, error) // UpdateStream updates the stream by the ID. UpdateStream(ctx context.Context, id platform.ID, stream Stream) (*ReadStream, error) // DeleteStreams deletes one or more streams by name. - DeleteStreams(ctx context.Context, delete BasicStream) error + DeleteStreams(ctx context.Context, orgID platform.ID, delete BasicStream) error // DeleteStreamByID deletes the stream metadata by id. DeleteStreamByID(ctx context.Context, id platform.ID) error } @@ -116,7 +119,7 @@ type StoredAnnotation struct { Message string `db:"message"` // Message is a longer description of the annotated event. Stickers []string `db:"stickers"` // Stickers are additional labels to group annotations by. Duration string `db:"duration"` // Duration is the time range (with zone) of an annotated event. - Lower string `db:"lower"` // Lower is the time an annotated event beings. + Lower string `db:"lower"` // Lower is the time an annotated event begins. Upper string `db:"upper"` // Upper is the time an annotated event ends. } diff --git a/annotations/transport/annotations_router.go b/annotations/transport/annotations_router.go index ccd25a5178..63f8095dd9 100644 --- a/annotations/transport/annotations_router.go +++ b/annotations/transport/annotations_router.go @@ -3,6 +3,7 @@ package transport import ( "encoding/json" "net/http" + "strings" "time" "github.com/go-chi/chi" @@ -65,7 +66,13 @@ func (h *AnnotationHandler) handleGetAnnotations(w http.ResponseWriter, r *http. return } - l, err := h.annotationService.ListAnnotations(ctx, *o, *f) + s, err := h.annotationService.ListAnnotations(ctx, *o, *f) + if err != nil { + h.api.Err(w, r, err) + return + } + + l, err := storedAnnotationsToReadAnnotations(s) if err != nil { h.api.Err(w, r, err) return @@ -106,13 +113,19 @@ func (h *AnnotationHandler) handleGetAnnotation(w http.ResponseWriter, r *http.R return } - a, err := h.annotationService.GetAnnotation(ctx, *id) + s, err := h.annotationService.GetAnnotation(ctx, *id) if err != nil { h.api.Err(w, r, err) return } - h.api.Respond(w, r, http.StatusOK, a) + c, err := storedAnnotationToEvent(s) + if err != nil { + h.api.Err(w, r, err) + return + } + + h.api.Respond(w, r, http.StatusOK, c) } func (h *AnnotationHandler) handleDeleteAnnotation(w http.ResponseWriter, r *http.Request) { @@ -234,3 +247,68 @@ func decodeUpdateAnnotationRequest(r *http.Request) (*influxdb.AnnotationCreate, return u, nil } + +func storedAnnotationsToReadAnnotations(s []influxdb.StoredAnnotation) (influxdb.ReadAnnotations, error) { + r := influxdb.ReadAnnotations{} + + for _, val := range s { + stickers, err := stickerSliceToMap(val.Stickers) + if err != nil { + return nil, err + } + + r[val.StreamTag] = append(r[val.StreamTag], influxdb.ReadAnnotation{ + ID: val.ID, + Summary: val.Summary, + Message: val.Message, + Stickers: stickers, + StartTime: val.Lower, + EndTime: val.Upper, + }) + } + + return r, nil +} + +func storedAnnotationToEvent(s *influxdb.StoredAnnotation) (*influxdb.AnnotationEvent, error) { + st, err := tStringToPointer(s.Lower) + if err != nil { + return nil, err + } + + et, err := tStringToPointer(s.Upper) + if err != nil { + return nil, err + } + + stickers, err := stickerSliceToMap(s.Stickers) + if err != nil { + return nil, err + } + + return &influxdb.AnnotationEvent{ + ID: s.ID, + AnnotationCreate: influxdb.AnnotationCreate{ + StreamTag: s.StreamTag, + Summary: s.Summary, + Message: s.Message, + Stickers: stickers, + EndTime: et, + StartTime: st, + }, + }, nil +} + +func stickerSliceToMap(stickers []string) (map[string]string, error) { + stickerMap := map[string]string{} + + for i := range stickers { + sticks := strings.SplitN(stickers[i], "=", 2) + if len(sticks) < 2 { + return nil, invalidStickerError(stickers[i]) + } + stickerMap[sticks[0]] = sticks[1] + } + + return stickerMap, nil +} diff --git a/annotations/transport/annotations_router_test.go b/annotations/transport/annotations_router_test.go index 13c9f190c4..5231c5e0de 100644 --- a/annotations/transport/annotations_router_test.go +++ b/annotations/transport/annotations_router_test.go @@ -16,6 +16,8 @@ var ( testCreateAnnotation = influxdb.AnnotationCreate{ StreamTag: "sometag", Summary: "testing the api", + Message: "stored annotation message", + Stickers: map[string]string{"val1": "sticker1", "val2": "sticker2"}, EndTime: &now, StartTime: &now, } @@ -32,6 +34,31 @@ var ( testReadAnnotation2 = influxdb.ReadAnnotation{ ID: *influxdbtesting.IDPtr(2), } + + testStoredAnnotation = influxdb.StoredAnnotation{ + ID: *id, + OrgID: *orgID, + StreamID: *influxdbtesting.IDPtr(3), + StreamTag: "sometag", + Summary: "testing the api", + Message: "stored annotation message", + Stickers: []string{"val1=sticker1", "val2=sticker2"}, + Lower: now.Format(time.RFC3339), + Upper: now.Format(time.RFC3339), + } + + testReadAnnotations = influxdb.ReadAnnotations{ + "sometag": []influxdb.ReadAnnotation{ + { + ID: testStoredAnnotation.ID, + Summary: testStoredAnnotation.Summary, + Message: testStoredAnnotation.Message, + Stickers: map[string]string{"val1": "sticker1", "val2": "sticker2"}, + EndTime: testStoredAnnotation.Lower, + StartTime: testStoredAnnotation.Upper, + }, + }, + } ) func TestAnnotationRouter(t *testing.T) { @@ -72,9 +99,15 @@ func TestAnnotationRouter(t *testing.T) { EndTime: &now, }, }). - Return(influxdb.ReadAnnotations{ - "stream1": []influxdb.ReadAnnotation{testReadAnnotation1}, - "stream2": []influxdb.ReadAnnotation{testReadAnnotation2}, + Return([]influxdb.StoredAnnotation{ + { + ID: testReadAnnotation1.ID, + StreamTag: "stream1", + }, + { + ID: testReadAnnotation2.ID, + StreamTag: "stream2", + }, }, nil) res := doTestRequest(t, req, http.StatusOK, true) @@ -143,7 +176,7 @@ func TestAnnotationRouter(t *testing.T) { svc.EXPECT(). GetAnnotation(gomock.Any(), *id). - Return(&testEvent, nil) + Return(&testStoredAnnotation, nil) res := doTestRequest(t, req, http.StatusOK, true) @@ -216,3 +249,55 @@ func TestAnnotationRouter(t *testing.T) { } }) } + +func TestStoredAnnotationsToReadAnnotations(t *testing.T) { + t.Parallel() + + got, err := storedAnnotationsToReadAnnotations([]influxdb.StoredAnnotation{testStoredAnnotation}) + require.NoError(t, err) + require.Equal(t, got, testReadAnnotations) +} + +func TestStoredAnnotationToEvent(t *testing.T) { + t.Parallel() + + got, err := storedAnnotationToEvent(&testStoredAnnotation) + require.NoError(t, err) + require.Equal(t, got, &testEvent) +} + +func TestStickerSliceToMap(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + stickers []string + want map[string]string + wantErr error + }{ + { + "good stickers", + []string{"good1=val1", "good2=val2"}, + map[string]string{"good1": "val1", "good2": "val2"}, + nil, + }, + { + "bad stickers", + []string{"this is an invalid sticker", "shouldbe=likethis"}, + nil, + invalidStickerError("this is an invalid sticker"), + }, + { + "no stickers", + []string{}, + map[string]string{}, + nil, + }, + } + + for _, tt := range tests { + got, err := stickerSliceToMap(tt.stickers) + require.Equal(t, tt.want, got) + require.Equal(t, tt.wantErr, err) + } +} diff --git a/annotations/transport/http.go b/annotations/transport/http.go index f603e9b7b4..857472abfa 100644 --- a/annotations/transport/http.go +++ b/annotations/transport/http.go @@ -1,6 +1,7 @@ package transport import ( + "fmt" "net/http" "time" @@ -40,6 +41,13 @@ var ( } ) +func invalidStickerError(s string) error { + return &errors.Error{ + Code: errors.EInternal, + Msg: fmt.Sprintf("invalid sticker: %q", s), + } +} + // AnnotationsHandler is the handler for the annotation service type AnnotationHandler struct { chi.Router diff --git a/annotations/transport/streams_router.go b/annotations/transport/streams_router.go index 470fd949ed..2ff90a533f 100644 --- a/annotations/transport/streams_router.go +++ b/annotations/transport/streams_router.go @@ -28,13 +28,19 @@ func (h *AnnotationHandler) streamsRouter() http.Handler { func (h *AnnotationHandler) handleCreateOrUpdateStream(w http.ResponseWriter, r *http.Request) { ctx := r.Context() + o, err := platform.IDFromString(r.URL.Query().Get("orgID")) + if err != nil { + h.api.Err(w, r, errBadOrg) + return + } + u, err := decodeCreateOrUpdateStreamRequest(r) if err != nil { h.api.Err(w, r, err) return } - s, err := h.annotationService.CreateOrUpdateStream(ctx, *u) + s, err := h.annotationService.CreateOrUpdateStream(ctx, *o, *u) if err != nil { h.api.Err(w, r, err) return @@ -58,19 +64,25 @@ func (h *AnnotationHandler) handleGetStreams(w http.ResponseWriter, r *http.Requ return } - l, err := h.annotationService.ListStreams(ctx, *o, *f) + s, err := h.annotationService.ListStreams(ctx, *o, *f) if err != nil { h.api.Err(w, r, err) return } - h.api.Respond(w, r, http.StatusOK, l) + h.api.Respond(w, r, http.StatusOK, storedStreamsToReadStreams(s)) } // Delete stream(s) by name, capable of handling a list of names func (h *AnnotationHandler) handleDeleteStreams(w http.ResponseWriter, r *http.Request) { ctx := r.Context() + o, err := platform.IDFromString(r.URL.Query().Get("orgID")) + if err != nil { + h.api.Err(w, r, errBadOrg) + return + } + f, err := decodeDeleteStreamsRequest(r) if err != nil { h.api.Err(w, r, err) @@ -79,7 +91,7 @@ func (h *AnnotationHandler) handleDeleteStreams(w http.ResponseWriter, r *http.R // delete all of the streams according to the filter. annotations associated with the stream // will be deleted by the ON DELETE CASCADE relationship between streams and annotations. - if err = h.annotationService.DeleteStreams(ctx, *f); err != nil { + if err = h.annotationService.DeleteStreams(ctx, *o, *f); err != nil { h.api.Err(w, r, err) return } @@ -176,3 +188,19 @@ func decodeDeleteStreamsRequest(r *http.Request) (*influxdb.BasicStream, error) return f, nil } + +func storedStreamsToReadStreams(stored []influxdb.StoredStream) []influxdb.ReadStream { + r := make([]influxdb.ReadStream, 0, len(stored)) + + for _, s := range stored { + r = append(r, influxdb.ReadStream{ + ID: s.ID, + Name: s.Name, + Description: s.Description, + CreatedAt: s.CreatedAt, + UpdatedAt: s.UpdatedAt, + }) + } + + return r +} diff --git a/annotations/transport/streams_router_test.go b/annotations/transport/streams_router_test.go index 8b5893ce4a..ece4806d48 100644 --- a/annotations/transport/streams_router_test.go +++ b/annotations/transport/streams_router_test.go @@ -30,6 +30,24 @@ var ( CreatedAt: now, UpdatedAt: now, } + + testStoredStream1 = influxdb.StoredStream{ + ID: testReadStream1.ID, + OrgID: *orgID, + Name: testReadStream1.Name, + Description: testReadStream1.Description, + CreatedAt: testReadStream1.CreatedAt, + UpdatedAt: testReadStream1.UpdatedAt, + } + + testStoredStream2 = influxdb.StoredStream{ + ID: testReadStream2.ID, + OrgID: *orgID, + Name: testReadStream2.Name, + Description: testReadStream2.Description, + CreatedAt: testReadStream2.CreatedAt, + UpdatedAt: testReadStream2.UpdatedAt, + } ) func TestStreamsRouter(t *testing.T) { @@ -46,7 +64,7 @@ func TestStreamsRouter(t *testing.T) { req.URL.RawQuery = q.Encode() svc.EXPECT(). - CreateOrUpdateStream(gomock.Any(), testCreateStream). + CreateOrUpdateStream(gomock.Any(), *orgID, testCreateStream). Return(testReadStream1, nil) res := doTestRequest(t, req, http.StatusOK, true) @@ -70,8 +88,6 @@ func TestStreamsRouter(t *testing.T) { q.Add("streamIncludes", "stream2") req.URL.RawQuery = q.Encode() - want := []influxdb.ReadStream{*testReadStream1, *testReadStream2} - svc.EXPECT(). ListStreams(gomock.Any(), *orgID, influxdb.StreamListFilter{ StreamIncludes: []string{"stream1", "stream2"}, @@ -80,14 +96,14 @@ func TestStreamsRouter(t *testing.T) { EndTime: &now, }, }). - Return(want, nil) + Return([]influxdb.StoredStream{testStoredStream1, testStoredStream2}, nil) res := doTestRequest(t, req, http.StatusOK, true) got := []influxdb.ReadStream{} err := json.NewDecoder(res.Body).Decode(&got) require.NoError(t, err) - require.ElementsMatch(t, want, got) + require.ElementsMatch(t, []influxdb.ReadStream{*testReadStream1, *testReadStream2}, got) }) t.Run("delete streams (by name) happy path", func(t *testing.T) { @@ -96,12 +112,13 @@ func TestStreamsRouter(t *testing.T) { req := newTestRequest(t, "DELETE", ts.URL+"/streams", nil) q := req.URL.Query() + q.Add("orgID", orgStr) q.Add("stream", "stream1") q.Add("stream", "stream2") req.URL.RawQuery = q.Encode() svc.EXPECT(). - DeleteStreams(gomock.Any(), influxdb.BasicStream{ + DeleteStreams(gomock.Any(), *orgID, influxdb.BasicStream{ Names: []string{"stream1", "stream2"}, }). Return(nil) @@ -141,7 +158,7 @@ func TestStreamsRouter(t *testing.T) { }) t.Run("invalid org ids return 400 when required", func(t *testing.T) { - methods := []string{"GET"} + methods := []string{"GET", "PUT", "DELETE"} for _, m := range methods { t.Run(m, func(t *testing.T) { @@ -172,3 +189,10 @@ func TestStreamsRouter(t *testing.T) { } }) } + +func TestStoredStreamsToReadStreams(t *testing.T) { + t.Parallel() + + got := storedStreamsToReadStreams([]influxdb.StoredStream{testStoredStream1, testStoredStream2}) + require.Equal(t, got, []influxdb.ReadStream{*testReadStream1, *testReadStream2}) +} diff --git a/authorizer/annotation.go b/authorizer/annotation.go new file mode 100644 index 0000000000..ef4545a99d --- /dev/null +++ b/authorizer/annotation.go @@ -0,0 +1,189 @@ +package authorizer + +import ( + "context" + "fmt" + + "github.com/influxdata/influxdb/v2" + "github.com/influxdata/influxdb/v2/kit/platform" + "github.com/influxdata/influxdb/v2/kit/platform/errors" +) + +var _ influxdb.AnnotationService = (*AnnotationService)(nil) + +// AnnotationService wraps an influxdb.AnnotationService and authorizes actions +// against it appropriately. +type AnnotationService struct { + s influxdb.AnnotationService +} + +// NewAnnotationService constructs an instance of an authorizing check service +func NewAnnotationService(s influxdb.AnnotationService) *AnnotationService { + return &AnnotationService{ + s: s, + } +} + +// CreateAnnotations checks to see if the authorizer on context has write access for annotations for the provided orgID +func (s *AnnotationService) CreateAnnotations(ctx context.Context, orgID platform.ID, create []influxdb.AnnotationCreate) ([]influxdb.AnnotationEvent, error) { + if _, _, err := AuthorizeCreate(ctx, influxdb.AnnotationsResourceType, orgID); err != nil { + return nil, err + } + + return s.s.CreateAnnotations(ctx, orgID, create) +} + +// ListAnnotations checks to see if the authorizer on context has read access for annotations for the provided orgID +// and then filters the list down to only the resources that are authorized +func (s *AnnotationService) ListAnnotations(ctx context.Context, orgID platform.ID, filter influxdb.AnnotationListFilter) ([]influxdb.StoredAnnotation, error) { + if _, _, err := AuthorizeOrgReadResource(ctx, influxdb.AnnotationsResourceType, orgID); err != nil { + return nil, err + } + + as, err := s.s.ListAnnotations(ctx, orgID, filter) + if err != nil { + return nil, err + } + + as, _, err = AuthorizeFindAnnotations(ctx, as) + return as, err +} + +// GetAnnotation checks to see if the authorizer on context has read access to the requested annotation +func (s *AnnotationService) GetAnnotation(ctx context.Context, id platform.ID) (*influxdb.StoredAnnotation, error) { + a, err := s.s.GetAnnotation(ctx, id) + if err != nil { + return nil, err + } + if _, _, err := AuthorizeRead(ctx, influxdb.AnnotationsResourceType, id, a.OrgID); err != nil { + return nil, err + } + return a, nil +} + +// DeleteAnnotations checks to see if the authorizer on context has write access to the provided orgID +func (s *AnnotationService) DeleteAnnotations(ctx context.Context, orgID platform.ID, delete influxdb.AnnotationDeleteFilter) error { + if _, _, err := AuthorizeOrgWriteResource(ctx, influxdb.AnnotationsResourceType, orgID); err != nil { + return err + } + return s.s.DeleteAnnotations(ctx, orgID, delete) +} + +// DeleteAnnotation checks to see if the authorizer on context has write access to the requested annotation +func (s *AnnotationService) DeleteAnnotation(ctx context.Context, id platform.ID) error { + a, err := s.s.GetAnnotation(ctx, id) + if err != nil { + return err + } + if _, _, err := AuthorizeWrite(ctx, influxdb.AnnotationsResourceType, id, a.OrgID); err != nil { + return err + } + return s.s.DeleteAnnotation(ctx, id) +} + +// UpdateAnnotation checks to see if the authorizer on context has write access to the requested annotation +func (s *AnnotationService) UpdateAnnotation(ctx context.Context, id platform.ID, update influxdb.AnnotationCreate) (*influxdb.AnnotationEvent, error) { + a, err := s.s.GetAnnotation(ctx, id) + if err != nil { + return nil, err + } + if _, _, err := AuthorizeWrite(ctx, influxdb.AnnotationsResourceType, id, a.OrgID); err != nil { + return nil, err + } + return s.s.UpdateAnnotation(ctx, id, update) +} + +// ListStreams checks to see if the authorizer on context has read access for streams for the provided orgID +// and then filters the list down to only the resources that are authorized +func (s *AnnotationService) ListStreams(ctx context.Context, orgID platform.ID, filter influxdb.StreamListFilter) ([]influxdb.StoredStream, error) { + if _, _, err := AuthorizeOrgReadResource(ctx, influxdb.AnnotationsResourceType, orgID); err != nil { + return nil, err + } + + ss, err := s.s.ListStreams(ctx, orgID, filter) + if err != nil { + return nil, err + } + + ss, _, err = AuthorizeFindStreams(ctx, ss) + return ss, err +} + +// GetStream checks to see if the authorizer on context has read access to the requested stream +func (s *AnnotationService) GetStream(ctx context.Context, id platform.ID) (*influxdb.StoredStream, error) { + st, err := s.s.GetStream(ctx, id) + if err != nil { + return nil, err + } + if _, _, err := AuthorizeRead(ctx, influxdb.AnnotationsResourceType, id, st.OrgID); err != nil { + return nil, err + } + return st, nil +} + +func (s *AnnotationService) CreateOrUpdateStream(ctx context.Context, orgID platform.ID, stream influxdb.Stream) (*influxdb.ReadStream, error) { + // We need to know if the request is creating a new stream, or updating an existing stream to check + // permissions appropriately + + // Get the stream by name. An empty slice will be returned if the stream doesn't exist + // note: a given org can only have one stream by the same name. this constraint is enforced in the database schema + streams, err := s.s.ListStreams(ctx, orgID, influxdb.StreamListFilter{ + StreamIncludes: []string{stream.Name}, + }) + if err != nil { + return nil, err + } + + // update an already existing stream + if len(streams) == 1 { + return s.UpdateStream(ctx, streams[0].ID, stream) + } + + // create a new stream if one doesn't already exist + if len(streams) == 0 { + if _, _, err := AuthorizeCreate(ctx, influxdb.AnnotationsResourceType, orgID); err != nil { + return nil, err + } + + return s.s.CreateOrUpdateStream(ctx, orgID, stream) + } + + // if multiple streams were returned somehow, return an error + // this should never happen, so return a server error + return nil, &errors.Error{ + Code: errors.EInternal, + Msg: fmt.Sprintf("more than one stream named %q for org %q", streams[0].Name, orgID), + } +} + +// UpdateStream checks to see if the authorizer on context has write access to the requested stream +func (s *AnnotationService) UpdateStream(ctx context.Context, id platform.ID, stream influxdb.Stream) (*influxdb.ReadStream, error) { + st, err := s.s.GetStream(ctx, id) + if err != nil { + return nil, err + } + if _, _, err := AuthorizeWrite(ctx, influxdb.AnnotationsResourceType, id, st.OrgID); err != nil { + return nil, err + } + return s.s.UpdateStream(ctx, id, stream) +} + +// DeleteStreams checks to see if the authorizer on context has write access to the provided orgID +func (s *AnnotationService) DeleteStreams(ctx context.Context, orgID platform.ID, delete influxdb.BasicStream) error { + if _, _, err := AuthorizeOrgWriteResource(ctx, influxdb.AnnotationsResourceType, orgID); err != nil { + return err + } + return s.s.DeleteStreams(ctx, orgID, delete) +} + +// DeleteStreamByID checks to see if the authorizer on context has write access to the requested stream +func (s *AnnotationService) DeleteStreamByID(ctx context.Context, id platform.ID) error { + st, err := s.s.GetStream(ctx, id) + if err != nil { + return err + } + if _, _, err := AuthorizeWrite(ctx, influxdb.AnnotationsResourceType, id, st.OrgID); err != nil { + return err + } + return s.s.DeleteStreamByID(ctx, id) +} diff --git a/authorizer/annotation_test.go b/authorizer/annotation_test.go new file mode 100644 index 0000000000..803d5b54c6 --- /dev/null +++ b/authorizer/annotation_test.go @@ -0,0 +1,725 @@ +package authorizer_test + +import ( + "context" + "fmt" + "testing" + + "github.com/golang/mock/gomock" + "github.com/influxdata/influxdb/v2" + "github.com/influxdata/influxdb/v2/authorizer" + influxdbcontext "github.com/influxdata/influxdb/v2/context" + "github.com/influxdata/influxdb/v2/kit/platform" + "github.com/influxdata/influxdb/v2/kit/platform/errors" + "github.com/influxdata/influxdb/v2/mock" + influxdbtesting "github.com/influxdata/influxdb/v2/testing" + "github.com/stretchr/testify/require" +) + +var ( + annOrgID1 = influxdbtesting.IDPtr(1) + annOrgID2 = influxdbtesting.IDPtr(10) + rID = influxdbtesting.IDPtr(2) +) + +func Test_CreateAnnotations(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + wantRet []influxdb.AnnotationEvent + wantErr error + }{ + { + "authorized to create annotation(s) with the specified org", + []influxdb.AnnotationEvent{{ID: *rID}}, + nil, + }, + { + "not authorized to create annotation(s) with the specified org", + nil, + &errors.Error{ + Msg: fmt.Sprintf("write:orgs/%s/annotations is unauthorized", annOrgID1), + Code: errors.EUnauthorized, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrlr := gomock.NewController(t) + svc := mock.NewMockAnnotationService(ctrlr) + s := authorizer.NewAnnotationService(svc) + + var perm influxdb.Permission + if tt.wantErr == nil { + perm = newTestAnnotationsPermission(influxdb.WriteAction, annOrgID1) + svc.EXPECT(). + CreateAnnotations(gomock.Any(), *annOrgID1, []influxdb.AnnotationCreate{{}}). + Return(tt.wantRet, nil) + } else { + perm = newTestAnnotationsPermission(influxdb.ReadAction, annOrgID1) + } + + ctx := influxdbcontext.SetAuthorizer(context.Background(), mock.NewMockAuthorizer(false, []influxdb.Permission{perm})) + got, err := s.CreateAnnotations(ctx, *annOrgID1, []influxdb.AnnotationCreate{{}}) + require.Equal(t, tt.wantErr, err) + require.Equal(t, tt.wantRet, got) + }) + } +} + +func Test_ListAnnotations(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + wantRet []influxdb.StoredAnnotation + wantErr error + }{ + { + "authorized to list annotations for the specified org", + []influxdb.StoredAnnotation{}, + nil, + }, + { + "not authorized to list annotations for the specified org", + nil, + &errors.Error{ + Msg: fmt.Sprintf("read:orgs/%s/annotations is unauthorized", annOrgID1), + Code: errors.EUnauthorized, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrlr := gomock.NewController(t) + svc := mock.NewMockAnnotationService(ctrlr) + s := authorizer.NewAnnotationService(svc) + + var perm influxdb.Permission + if tt.wantErr == nil { + perm = newTestAnnotationsPermission(influxdb.ReadAction, annOrgID1) + svc.EXPECT(). + ListAnnotations(gomock.Any(), *annOrgID1, influxdb.AnnotationListFilter{}). + Return(tt.wantRet, nil) + } + + ctx := influxdbcontext.SetAuthorizer(context.Background(), mock.NewMockAuthorizer(false, []influxdb.Permission{perm})) + got, err := s.ListAnnotations(ctx, *annOrgID1, influxdb.AnnotationListFilter{}) + require.Equal(t, tt.wantErr, err) + require.Equal(t, tt.wantRet, got) + }) + } +} + +func Test_GetAnnotation(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + permissionOrg *platform.ID + wantRet *influxdb.StoredAnnotation + wantErr error + }{ + { + "authorized to access annotation by id", + annOrgID1, + &influxdb.StoredAnnotation{ + ID: *rID, + OrgID: *annOrgID1, + }, + nil, + }, + { + "not authorized to access annotation by id", + annOrgID2, + nil, + &errors.Error{ + Msg: fmt.Sprintf("read:orgs/%s/annotations/%s is unauthorized", annOrgID1, rID), + Code: errors.EUnauthorized, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrlr := gomock.NewController(t) + svc := mock.NewMockAnnotationService(ctrlr) + s := authorizer.NewAnnotationService(svc) + + svc.EXPECT(). + GetAnnotation(gomock.Any(), *rID). + Return(&influxdb.StoredAnnotation{ + ID: *rID, + OrgID: *annOrgID1, + }, nil) + + perm := newTestAnnotationsPermission(influxdb.ReadAction, tt.permissionOrg) + + ctx := context.Background() + ctx = influxdbcontext.SetAuthorizer(ctx, mock.NewMockAuthorizer(false, []influxdb.Permission{perm})) + got, err := s.GetAnnotation(ctx, *rID) + require.Equal(t, tt.wantErr, err) + require.Equal(t, tt.wantRet, got) + }) + } +} + +func Test_DeleteAnnotations(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + wantErr error + }{ + { + "authorized to delete annotations with the specified org", + nil, + }, + { + "not authorized to delete annotations with the specified org", + &errors.Error{ + Msg: fmt.Sprintf("write:orgs/%s/annotations is unauthorized", annOrgID1), + Code: errors.EUnauthorized, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrlr := gomock.NewController(t) + svc := mock.NewMockAnnotationService(ctrlr) + s := authorizer.NewAnnotationService(svc) + + var perm influxdb.Permission + if tt.wantErr == nil { + perm = newTestAnnotationsPermission(influxdb.WriteAction, annOrgID1) + svc.EXPECT(). + DeleteAnnotations(gomock.Any(), *annOrgID1, influxdb.AnnotationDeleteFilter{}). + Return(nil) + } else { + perm = newTestAnnotationsPermission(influxdb.ReadAction, annOrgID1) + } + + ctx := influxdbcontext.SetAuthorizer(context.Background(), mock.NewMockAuthorizer(false, []influxdb.Permission{perm})) + err := s.DeleteAnnotations(ctx, *annOrgID1, influxdb.AnnotationDeleteFilter{}) + require.Equal(t, tt.wantErr, err) + }) + } +} + +func Test_DeleteAnnotation(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + permissionOrg *platform.ID + wantErr error + }{ + { + "authorized to delete annotation by id", + annOrgID1, + nil, + }, + { + "not authorized to delete annotation by id", + annOrgID2, + &errors.Error{ + Msg: fmt.Sprintf("write:orgs/%s/annotations/%s is unauthorized", annOrgID1, rID), + Code: errors.EUnauthorized, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrlr := gomock.NewController(t) + svc := mock.NewMockAnnotationService(ctrlr) + s := authorizer.NewAnnotationService(svc) + + svc.EXPECT(). + GetAnnotation(gomock.Any(), *rID). + Return(&influxdb.StoredAnnotation{ + ID: *rID, + OrgID: *annOrgID1, + }, nil) + + perm := newTestAnnotationsPermission(influxdb.WriteAction, tt.permissionOrg) + + if tt.wantErr == nil { + svc.EXPECT(). + DeleteAnnotation(gomock.Any(), *rID). + Return(nil) + } + + ctx := context.Background() + ctx = influxdbcontext.SetAuthorizer(ctx, mock.NewMockAuthorizer(false, []influxdb.Permission{perm})) + err := s.DeleteAnnotation(ctx, *rID) + require.Equal(t, tt.wantErr, err) + }) + } +} + +func Test_UpdateAnnotation(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + permissionOrg *platform.ID + wantRet *influxdb.AnnotationEvent + wantErr error + }{ + { + "authorized to update annotation by id", + annOrgID1, + &influxdb.AnnotationEvent{}, + nil, + }, + { + "not authorized to update annotation by id", + annOrgID2, + nil, + &errors.Error{ + Msg: fmt.Sprintf("write:orgs/%s/annotations/%s is unauthorized", annOrgID1, rID), + Code: errors.EUnauthorized, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrlr := gomock.NewController(t) + svc := mock.NewMockAnnotationService(ctrlr) + s := authorizer.NewAnnotationService(svc) + + svc.EXPECT(). + GetAnnotation(gomock.Any(), *rID). + Return(&influxdb.StoredAnnotation{ + ID: *rID, + OrgID: *annOrgID1, + }, nil) + + perm := newTestAnnotationsPermission(influxdb.WriteAction, tt.permissionOrg) + + if tt.wantErr == nil { + svc.EXPECT(). + UpdateAnnotation(gomock.Any(), *rID, influxdb.AnnotationCreate{}). + Return(tt.wantRet, nil) + } + + ctx := context.Background() + ctx = influxdbcontext.SetAuthorizer(ctx, mock.NewMockAuthorizer(false, []influxdb.Permission{perm})) + got, err := s.UpdateAnnotation(ctx, *rID, influxdb.AnnotationCreate{}) + require.Equal(t, tt.wantErr, err) + require.Equal(t, tt.wantRet, got) + }) + } +} + +func Test_ListStreams(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + wantRet []influxdb.StoredStream + wantErr error + }{ + { + "authorized to list streams for the specified org", + []influxdb.StoredStream{}, + nil, + }, + { + "not authorized to list streams for the specified org", + nil, + &errors.Error{ + Msg: fmt.Sprintf("read:orgs/%s/annotations is unauthorized", annOrgID1), + Code: errors.EUnauthorized, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrlr := gomock.NewController(t) + svc := mock.NewMockAnnotationService(ctrlr) + s := authorizer.NewAnnotationService(svc) + + var perm influxdb.Permission + if tt.wantErr == nil { + perm = newTestAnnotationsPermission(influxdb.ReadAction, annOrgID1) + svc.EXPECT(). + ListStreams(gomock.Any(), *annOrgID1, influxdb.StreamListFilter{}). + Return(tt.wantRet, nil) + } + + ctx := influxdbcontext.SetAuthorizer(context.Background(), mock.NewMockAuthorizer(false, []influxdb.Permission{perm})) + got, err := s.ListStreams(ctx, *annOrgID1, influxdb.StreamListFilter{}) + require.Equal(t, tt.wantErr, err) + require.Equal(t, tt.wantRet, got) + }) + } +} + +func Test_GetStream(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + permissionOrg *platform.ID + wantRet *influxdb.StoredStream + wantErr error + }{ + { + "authorized to access stream by id", + annOrgID1, + &influxdb.StoredStream{ + ID: *rID, + OrgID: *annOrgID1, + }, + nil, + }, + { + "not authorized to access stream by id", + annOrgID2, + nil, + &errors.Error{ + Msg: fmt.Sprintf("read:orgs/%s/annotations/%s is unauthorized", annOrgID1, rID), + Code: errors.EUnauthorized, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrlr := gomock.NewController(t) + svc := mock.NewMockAnnotationService(ctrlr) + s := authorizer.NewAnnotationService(svc) + + svc.EXPECT(). + GetStream(gomock.Any(), *rID). + Return(&influxdb.StoredStream{ + ID: *rID, + OrgID: *annOrgID1, + }, nil) + + perm := newTestAnnotationsPermission(influxdb.ReadAction, tt.permissionOrg) + + ctx := context.Background() + ctx = influxdbcontext.SetAuthorizer(ctx, mock.NewMockAuthorizer(false, []influxdb.Permission{perm})) + got, err := s.GetStream(ctx, *rID) + require.Equal(t, tt.wantErr, err) + require.Equal(t, tt.wantRet, got) + }) + } +} + +func Test_CreateOrUpdateStream(t *testing.T) { + t.Parallel() + + var ( + testStreamName = "test stream" + testStream = influxdb.Stream{ + Name: testStreamName, + } + ) + + t.Run("updating a stream", func(t *testing.T) { + tests := []struct { + name string + permissionOrg *platform.ID + existingStreams []influxdb.StoredStream + getStreamRet *influxdb.StoredStream + wantRet *influxdb.ReadStream + wantErr error + }{ + { + "authorized to update an existing stream", + annOrgID1, + []influxdb.StoredStream{{ID: *rID, OrgID: *annOrgID1}}, + &influxdb.StoredStream{ID: *rID, OrgID: *annOrgID1}, + &influxdb.ReadStream{ID: *rID}, + nil, + }, + { + "not authorized to update an existing stream", + annOrgID2, + []influxdb.StoredStream{{ID: *rID, OrgID: *annOrgID1}}, + &influxdb.StoredStream{ID: *rID, OrgID: *annOrgID1}, + nil, + &errors.Error{ + Msg: fmt.Sprintf("write:orgs/%s/annotations/%s is unauthorized", annOrgID1, rID), + Code: errors.EUnauthorized, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrlr := gomock.NewController(t) + svc := mock.NewMockAnnotationService(ctrlr) + s := authorizer.NewAnnotationService(svc) + + svc.EXPECT(). + ListStreams(gomock.Any(), *tt.permissionOrg, influxdb.StreamListFilter{ + StreamIncludes: []string{testStreamName}, + }). + Return(tt.existingStreams, nil) + + svc.EXPECT(). + GetStream(gomock.Any(), tt.existingStreams[0].ID). + Return(tt.getStreamRet, nil) + + if tt.wantErr == nil { + svc.EXPECT(). + UpdateStream(gomock.Any(), tt.existingStreams[0].ID, testStream). + Return(tt.wantRet, tt.wantErr) + } + + perm := newTestAnnotationsPermission(influxdb.WriteAction, tt.permissionOrg) + ctx := influxdbcontext.SetAuthorizer(context.Background(), mock.NewMockAuthorizer(false, []influxdb.Permission{perm})) + got, err := s.CreateOrUpdateStream(ctx, *tt.permissionOrg, testStream) + require.Equal(t, tt.wantErr, err) + require.Equal(t, tt.wantRet, got) + }) + } + }) + + t.Run("creating a stream", func(t *testing.T) { + tests := []struct { + name string + existingStreams []influxdb.StoredStream + wantRet *influxdb.ReadStream + wantErr error + }{ + { + "authorized to create a stream with the specified org", + []influxdb.StoredStream{}, + &influxdb.ReadStream{}, + nil, + }, + { + "not authorized to create a stream with the specified org", + []influxdb.StoredStream{}, + nil, + &errors.Error{ + Msg: fmt.Sprintf("write:orgs/%s/annotations is unauthorized", annOrgID1), + Code: errors.EUnauthorized, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrlr := gomock.NewController(t) + svc := mock.NewMockAnnotationService(ctrlr) + s := authorizer.NewAnnotationService(svc) + + svc.EXPECT(). + ListStreams(gomock.Any(), *annOrgID1, influxdb.StreamListFilter{ + StreamIncludes: []string{testStreamName}, + }). + Return(tt.existingStreams, nil) + + var perm influxdb.Permission + if tt.wantErr == nil { + perm = newTestAnnotationsPermission(influxdb.WriteAction, annOrgID1) + svc.EXPECT(). + CreateOrUpdateStream(gomock.Any(), *annOrgID1, testStream). + Return(tt.wantRet, nil) + } else { + perm = newTestAnnotationsPermission(influxdb.ReadAction, annOrgID1) + } + + ctx := influxdbcontext.SetAuthorizer(context.Background(), mock.NewMockAuthorizer(false, []influxdb.Permission{perm})) + got, err := s.CreateOrUpdateStream(ctx, *annOrgID1, testStream) + require.Equal(t, tt.wantErr, err) + require.Equal(t, tt.wantRet, got) + }) + } + }) + + t.Run("stream list longer than 1 returns a server error", func(t *testing.T) { + ctrlr := gomock.NewController(t) + svc := mock.NewMockAnnotationService(ctrlr) + s := authorizer.NewAnnotationService(svc) + + svc.EXPECT(). + ListStreams(gomock.Any(), *annOrgID1, influxdb.StreamListFilter{ + StreamIncludes: []string{testStreamName}, + }). + Return([]influxdb.StoredStream{{Name: testStreamName}, {Name: testStreamName}}, nil) + + wantErr := &errors.Error{ + Code: errors.EInternal, + Msg: fmt.Sprintf("more than one stream named %q for org %q", testStreamName, annOrgID1), + } + + got, err := s.CreateOrUpdateStream(context.Background(), *annOrgID1, testStream) + require.Nil(t, got) + require.Equal(t, err, wantErr) + }) +} + +func Test_UpdateStream(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + permissionOrg *platform.ID + wantRet *influxdb.ReadStream + wantErr error + }{ + { + "authorized to update stream by id", + annOrgID1, + &influxdb.ReadStream{}, + nil, + }, + { + "not authorized to update stream by id", + annOrgID2, + nil, + &errors.Error{ + Msg: fmt.Sprintf("write:orgs/%s/annotations/%s is unauthorized", annOrgID1, rID), + Code: errors.EUnauthorized, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrlr := gomock.NewController(t) + svc := mock.NewMockAnnotationService(ctrlr) + s := authorizer.NewAnnotationService(svc) + + svc.EXPECT(). + GetStream(gomock.Any(), *rID). + Return(&influxdb.StoredStream{ + ID: *rID, + OrgID: *annOrgID1, + }, nil) + + perm := newTestAnnotationsPermission(influxdb.WriteAction, tt.permissionOrg) + + if tt.wantErr == nil { + svc.EXPECT(). + UpdateStream(gomock.Any(), *rID, influxdb.Stream{}). + Return(tt.wantRet, nil) + } + + ctx := context.Background() + ctx = influxdbcontext.SetAuthorizer(ctx, mock.NewMockAuthorizer(false, []influxdb.Permission{perm})) + got, err := s.UpdateStream(ctx, *rID, influxdb.Stream{}) + require.Equal(t, tt.wantErr, err) + require.Equal(t, tt.wantRet, got) + }) + } +} + +func Test_DeleteStreams(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + wantErr error + }{ + { + "authorized to delete streams with the specified org", + nil, + }, + { + "not authorized to delete streams with the specified org", + &errors.Error{ + Msg: fmt.Sprintf("write:orgs/%s/annotations is unauthorized", annOrgID1), + Code: errors.EUnauthorized, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrlr := gomock.NewController(t) + svc := mock.NewMockAnnotationService(ctrlr) + s := authorizer.NewAnnotationService(svc) + + var perm influxdb.Permission + if tt.wantErr == nil { + perm = newTestAnnotationsPermission(influxdb.WriteAction, annOrgID1) + svc.EXPECT(). + DeleteStreams(gomock.Any(), *annOrgID1, influxdb.BasicStream{}). + Return(nil) + } else { + perm = newTestAnnotationsPermission(influxdb.ReadAction, annOrgID1) + } + + ctx := influxdbcontext.SetAuthorizer(context.Background(), mock.NewMockAuthorizer(false, []influxdb.Permission{perm})) + err := s.DeleteStreams(ctx, *annOrgID1, influxdb.BasicStream{}) + require.Equal(t, tt.wantErr, err) + }) + } +} + +func Test_DeleteStreamByID(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + permissionOrg *platform.ID + wantErr error + }{ + { + "authorized to delete stream by id", + annOrgID1, + nil, + }, + { + "not authorized to delete stream by id", + annOrgID2, + &errors.Error{ + Msg: fmt.Sprintf("write:orgs/%s/annotations/%s is unauthorized", annOrgID1, rID), + Code: errors.EUnauthorized, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrlr := gomock.NewController(t) + svc := mock.NewMockAnnotationService(ctrlr) + s := authorizer.NewAnnotationService(svc) + + svc.EXPECT(). + GetStream(gomock.Any(), *rID). + Return(&influxdb.StoredStream{ + ID: *rID, + OrgID: *annOrgID1, + }, nil) + + perm := newTestAnnotationsPermission(influxdb.WriteAction, tt.permissionOrg) + + if tt.wantErr == nil { + svc.EXPECT(). + DeleteStreamByID(gomock.Any(), *rID). + Return(nil) + } + + ctx := context.Background() + ctx = influxdbcontext.SetAuthorizer(ctx, mock.NewMockAuthorizer(false, []influxdb.Permission{perm})) + err := s.DeleteStreamByID(ctx, *rID) + require.Equal(t, tt.wantErr, err) + }) + } +} + +func newTestAnnotationsPermission(action influxdb.Action, orgID *platform.ID) influxdb.Permission { + return influxdb.Permission{ + Action: action, + Resource: influxdb.Resource{ + Type: influxdb.AnnotationsResourceType, + OrgID: orgID, + }, + } +} diff --git a/authorizer/authorize_find.go b/authorizer/authorize_find.go index b4aa615259..ba50e5b427 100644 --- a/authorizer/authorize_find.go +++ b/authorizer/authorize_find.go @@ -92,6 +92,42 @@ func AuthorizeFindDashboards(ctx context.Context, rs []*influxdb.Dashboard) ([]* return rrs, len(rrs), nil } +// AuthorizeFindAnnotations takes the given items and returns only the ones that the user is authorized to read. +func AuthorizeFindAnnotations(ctx context.Context, rs []influxdb.StoredAnnotation) ([]influxdb.StoredAnnotation, int, error) { + // This filters without allocating + // https://github.com/golang/go/wiki/SliceTricks#filtering-without-allocating + rrs := rs[:0] + for _, r := range rs { + _, _, err := AuthorizeRead(ctx, influxdb.AnnotationsResourceType, r.ID, r.OrgID) + if err != nil && errors.ErrorCode(err) != errors.EUnauthorized { + return nil, 0, err + } + if errors.ErrorCode(err) == errors.EUnauthorized { + continue + } + rrs = append(rrs, r) + } + return rrs, len(rrs), nil +} + +// AuthorizeFindStreams takes the given items and returns only the ones that the user is authorized to read. +func AuthorizeFindStreams(ctx context.Context, rs []influxdb.StoredStream) ([]influxdb.StoredStream, int, error) { + // This filters without allocating + // https://github.com/golang/go/wiki/SliceTricks#filtering-without-allocating + rrs := rs[:0] + for _, r := range rs { + _, _, err := AuthorizeRead(ctx, influxdb.AnnotationsResourceType, r.ID, r.OrgID) + if err != nil && errors.ErrorCode(err) != errors.EUnauthorized { + return nil, 0, err + } + if errors.ErrorCode(err) == errors.EUnauthorized { + continue + } + rrs = append(rrs, r) + } + return rrs, len(rrs), nil +} + // AuthorizeFindOrganizations takes the given items and returns only the ones that the user is authorized to read. func AuthorizeFindOrganizations(ctx context.Context, rs []*influxdb.Organization) ([]*influxdb.Organization, int, error) { // This filters without allocating diff --git a/authorizer/notebook_test.go b/authorizer/notebook_test.go index 0ff97b76b0..7842ec3e91 100644 --- a/authorizer/notebook_test.go +++ b/authorizer/notebook_test.go @@ -61,7 +61,7 @@ func Test_GetNotebook(t *testing.T) { GetNotebook(gomock.Any(), *nbID). Return(newTestNotebook(*orgID1), nil) - perm := newTestPermission(influxdb.ReadAction, tt.permissionOrg) + perm := newTestNotebooksPermission(influxdb.ReadAction, tt.permissionOrg) ctx := context.Background() ctx = influxdbcontext.SetAuthorizer(ctx, mock.NewMockAuthorizer(false, []influxdb.Permission{perm})) @@ -107,7 +107,7 @@ func Test_CreateNotebook(t *testing.T) { svc := mock.NewMockNotebookService(ctrlr) s := authorizer.NewNotebookService(svc) - perm := newTestPermission(influxdb.WriteAction, tt.permissionOrg) + perm := newTestNotebooksPermission(influxdb.WriteAction, tt.permissionOrg) nb := newTestReqBody(*tt.notebookOrg) if tt.wantErr == nil { @@ -163,7 +163,7 @@ func Test_UpdateNotebook(t *testing.T) { GetNotebook(gomock.Any(), *nbID). Return(newTestNotebook(*tt.notebookOrg), nil) - perm := newTestPermission(influxdb.WriteAction, tt.permissionOrg) + perm := newTestNotebooksPermission(influxdb.WriteAction, tt.permissionOrg) nb := newTestReqBody(*tt.notebookOrg) if tt.wantErr == nil { @@ -216,7 +216,7 @@ func Test_DeleteNotebook(t *testing.T) { GetNotebook(gomock.Any(), *nbID). Return(newTestNotebook(*tt.notebookOrg), nil) - perm := newTestPermission(influxdb.WriteAction, tt.permissionOrg) + perm := newTestNotebooksPermission(influxdb.WriteAction, tt.permissionOrg) if tt.wantErr == nil { svc.EXPECT(). @@ -266,7 +266,7 @@ func Test_ListNotebooks(t *testing.T) { svc := mock.NewMockNotebookService(ctrlr) s := authorizer.NewNotebookService(svc) - perm := newTestPermission(influxdb.ReadAction, tt.permissionOrg) + perm := newTestNotebooksPermission(influxdb.ReadAction, tt.permissionOrg) filter := influxdb.NotebookListFilter{OrgID: *tt.notebookOrg} if tt.wantErr == nil { @@ -304,7 +304,7 @@ func newTestReqBody(orgID platform.ID) *influxdb.NotebookReqBody { } } -func newTestPermission(action influxdb.Action, orgID *platform.ID) influxdb.Permission { +func newTestNotebooksPermission(action influxdb.Action, orgID *platform.ID) influxdb.Permission { return influxdb.Permission{ Action: action, Resource: influxdb.Resource{ diff --git a/authz.go b/authz.go index 8926b9d69e..81e7208676 100644 --- a/authz.go +++ b/authz.go @@ -137,6 +137,8 @@ const ( DBRPResourceType = ResourceType("dbrp") // 17 // NotebooksResourceType gives permission to one or more notebooks. NotebooksResourceType = ResourceType("notebooks") // 18 + // AnnotationsResourceType gives permission to one or more annotations. + AnnotationsResourceType = ResourceType("annotations") // 19 ) // AllResourceTypes is the list of all known resource types. @@ -160,6 +162,7 @@ var AllResourceTypes = []ResourceType{ ChecksResourceType, // 16 DBRPResourceType, // 17 NotebooksResourceType, // 18 + AnnotationsResourceType, // 19 // NOTE: when modifying this list, please update the swagger for components.schemas.Permission resource enum. } @@ -179,6 +182,7 @@ var OrgResourceTypes = []ResourceType{ ChecksResourceType, // 16 DBRPResourceType, // 17 NotebooksResourceType, // 18 + AnnotationsResourceType, // 19 } // Valid checks if the resource type is a member of the ResourceType enum. @@ -208,6 +212,7 @@ func (t ResourceType) Valid() (err error) { case ChecksResourceType: // 16 case DBRPResourceType: // 17 case NotebooksResourceType: // 18 + case AnnotationsResourceType: // 19 default: err = ErrInvalidResourceType } diff --git a/authz_test.go b/authz_test.go index 30514cf382..f92a9ea37f 100644 --- a/authz_test.go +++ b/authz_test.go @@ -325,7 +325,8 @@ func TestPermissionAllResources_Valid(t *testing.T) { platform.BucketsResourceType, platform.DashboardsResourceType, platform.SourcesResourceType, - platform.DashboardsResourceType, + platform.NotebooksResourceType, + platform.AnnotationsResourceType, } for _, rt := range resources { diff --git a/mock/annotation_service.go b/mock/annotation_service.go index c83eda5269..b7b5fd4999 100644 --- a/mock/annotation_service.go +++ b/mock/annotation_service.go @@ -6,37 +6,36 @@ package mock import ( context "context" - reflect "reflect" - gomock "github.com/golang/mock/gomock" influxdb "github.com/influxdata/influxdb/v2" platform "github.com/influxdata/influxdb/v2/kit/platform" + reflect "reflect" ) -// MockAnnotationService is a mock of AnnotationService interface. +// MockAnnotationService is a mock of AnnotationService interface type MockAnnotationService struct { ctrl *gomock.Controller recorder *MockAnnotationServiceMockRecorder } -// MockAnnotationServiceMockRecorder is the mock recorder for MockAnnotationService. +// MockAnnotationServiceMockRecorder is the mock recorder for MockAnnotationService type MockAnnotationServiceMockRecorder struct { mock *MockAnnotationService } -// NewMockAnnotationService creates a new mock instance. +// NewMockAnnotationService creates a new mock instance func NewMockAnnotationService(ctrl *gomock.Controller) *MockAnnotationService { mock := &MockAnnotationService{ctrl: ctrl} mock.recorder = &MockAnnotationServiceMockRecorder{mock} return mock } -// EXPECT returns an object that allows the caller to indicate expected use. +// EXPECT returns an object that allows the caller to indicate expected use func (m *MockAnnotationService) EXPECT() *MockAnnotationServiceMockRecorder { return m.recorder } -// CreateAnnotations mocks base method. +// CreateAnnotations mocks base method func (m *MockAnnotationService) CreateAnnotations(ctx context.Context, orgID platform.ID, create []influxdb.AnnotationCreate) ([]influxdb.AnnotationEvent, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CreateAnnotations", ctx, orgID, create) @@ -45,42 +44,43 @@ func (m *MockAnnotationService) CreateAnnotations(ctx context.Context, orgID pla return ret0, ret1 } -// CreateAnnotations indicates an expected call of CreateAnnotations. +// CreateAnnotations indicates an expected call of CreateAnnotations func (mr *MockAnnotationServiceMockRecorder) CreateAnnotations(ctx, orgID, create interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateAnnotations", reflect.TypeOf((*MockAnnotationService)(nil).CreateAnnotations), ctx, orgID, create) } -// CreateOrUpdateStream mocks base method. -func (m *MockAnnotationService) CreateOrUpdateStream(ctx context.Context, stream influxdb.Stream) (*influxdb.ReadStream, error) { +// ListAnnotations mocks base method +func (m *MockAnnotationService) ListAnnotations(ctx context.Context, orgID platform.ID, filter influxdb.AnnotationListFilter) ([]influxdb.StoredAnnotation, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateOrUpdateStream", ctx, stream) - ret0, _ := ret[0].(*influxdb.ReadStream) + ret := m.ctrl.Call(m, "ListAnnotations", ctx, orgID, filter) + ret0, _ := ret[0].([]influxdb.StoredAnnotation) ret1, _ := ret[1].(error) return ret0, ret1 } -// CreateOrUpdateStream indicates an expected call of CreateOrUpdateStream. -func (mr *MockAnnotationServiceMockRecorder) CreateOrUpdateStream(ctx, stream interface{}) *gomock.Call { +// ListAnnotations indicates an expected call of ListAnnotations +func (mr *MockAnnotationServiceMockRecorder) ListAnnotations(ctx, orgID, filter interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOrUpdateStream", reflect.TypeOf((*MockAnnotationService)(nil).CreateOrUpdateStream), ctx, stream) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAnnotations", reflect.TypeOf((*MockAnnotationService)(nil).ListAnnotations), ctx, orgID, filter) } -// DeleteAnnotation mocks base method. -func (m *MockAnnotationService) DeleteAnnotation(ctx context.Context, id platform.ID) error { +// GetAnnotation mocks base method +func (m *MockAnnotationService) GetAnnotation(ctx context.Context, id platform.ID) (*influxdb.StoredAnnotation, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteAnnotation", ctx, id) - ret0, _ := ret[0].(error) - return ret0 + ret := m.ctrl.Call(m, "GetAnnotation", ctx, id) + ret0, _ := ret[0].(*influxdb.StoredAnnotation) + ret1, _ := ret[1].(error) + return ret0, ret1 } -// DeleteAnnotation indicates an expected call of DeleteAnnotation. -func (mr *MockAnnotationServiceMockRecorder) DeleteAnnotation(ctx, id interface{}) *gomock.Call { +// GetAnnotation indicates an expected call of GetAnnotation +func (mr *MockAnnotationServiceMockRecorder) GetAnnotation(ctx, id interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAnnotation", reflect.TypeOf((*MockAnnotationService)(nil).DeleteAnnotation), ctx, id) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAnnotation", reflect.TypeOf((*MockAnnotationService)(nil).GetAnnotation), ctx, id) } -// DeleteAnnotations mocks base method. +// DeleteAnnotations mocks base method func (m *MockAnnotationService) DeleteAnnotations(ctx context.Context, orgID platform.ID, delete influxdb.AnnotationDeleteFilter) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "DeleteAnnotations", ctx, orgID, delete) @@ -88,86 +88,27 @@ func (m *MockAnnotationService) DeleteAnnotations(ctx context.Context, orgID pla return ret0 } -// DeleteAnnotations indicates an expected call of DeleteAnnotations. +// DeleteAnnotations indicates an expected call of DeleteAnnotations func (mr *MockAnnotationServiceMockRecorder) DeleteAnnotations(ctx, orgID, delete interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAnnotations", reflect.TypeOf((*MockAnnotationService)(nil).DeleteAnnotations), ctx, orgID, delete) } -// DeleteStreamByID mocks base method. -func (m *MockAnnotationService) DeleteStreamByID(ctx context.Context, id platform.ID) error { +// DeleteAnnotation mocks base method +func (m *MockAnnotationService) DeleteAnnotation(ctx context.Context, id platform.ID) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteStreamByID", ctx, id) + ret := m.ctrl.Call(m, "DeleteAnnotation", ctx, id) ret0, _ := ret[0].(error) return ret0 } -// DeleteStreamByID indicates an expected call of DeleteStreamByID. -func (mr *MockAnnotationServiceMockRecorder) DeleteStreamByID(ctx, id interface{}) *gomock.Call { +// DeleteAnnotation indicates an expected call of DeleteAnnotation +func (mr *MockAnnotationServiceMockRecorder) DeleteAnnotation(ctx, id interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteStreamByID", reflect.TypeOf((*MockAnnotationService)(nil).DeleteStreamByID), ctx, id) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAnnotation", reflect.TypeOf((*MockAnnotationService)(nil).DeleteAnnotation), ctx, id) } -// DeleteStreams mocks base method. -func (m *MockAnnotationService) DeleteStreams(ctx context.Context, delete influxdb.BasicStream) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteStreams", ctx, delete) - ret0, _ := ret[0].(error) - return ret0 -} - -// DeleteStreams indicates an expected call of DeleteStreams. -func (mr *MockAnnotationServiceMockRecorder) DeleteStreams(ctx, delete interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteStreams", reflect.TypeOf((*MockAnnotationService)(nil).DeleteStreams), ctx, delete) -} - -// GetAnnotation mocks base method. -func (m *MockAnnotationService) GetAnnotation(ctx context.Context, id platform.ID) (*influxdb.AnnotationEvent, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetAnnotation", ctx, id) - ret0, _ := ret[0].(*influxdb.AnnotationEvent) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetAnnotation indicates an expected call of GetAnnotation. -func (mr *MockAnnotationServiceMockRecorder) GetAnnotation(ctx, id interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAnnotation", reflect.TypeOf((*MockAnnotationService)(nil).GetAnnotation), ctx, id) -} - -// ListAnnotations mocks base method. -func (m *MockAnnotationService) ListAnnotations(ctx context.Context, orgID platform.ID, filter influxdb.AnnotationListFilter) (influxdb.ReadAnnotations, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListAnnotations", ctx, orgID, filter) - ret0, _ := ret[0].(influxdb.ReadAnnotations) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ListAnnotations indicates an expected call of ListAnnotations. -func (mr *MockAnnotationServiceMockRecorder) ListAnnotations(ctx, orgID, filter interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAnnotations", reflect.TypeOf((*MockAnnotationService)(nil).ListAnnotations), ctx, orgID, filter) -} - -// ListStreams mocks base method. -func (m *MockAnnotationService) ListStreams(ctx context.Context, orgID platform.ID, filter influxdb.StreamListFilter) ([]influxdb.ReadStream, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListStreams", ctx, orgID, filter) - ret0, _ := ret[0].([]influxdb.ReadStream) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ListStreams indicates an expected call of ListStreams. -func (mr *MockAnnotationServiceMockRecorder) ListStreams(ctx, orgID, filter interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListStreams", reflect.TypeOf((*MockAnnotationService)(nil).ListStreams), ctx, orgID, filter) -} - -// UpdateAnnotation mocks base method. +// UpdateAnnotation mocks base method func (m *MockAnnotationService) UpdateAnnotation(ctx context.Context, id platform.ID, update influxdb.AnnotationCreate) (*influxdb.AnnotationEvent, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "UpdateAnnotation", ctx, id, update) @@ -176,13 +117,58 @@ func (m *MockAnnotationService) UpdateAnnotation(ctx context.Context, id platfor return ret0, ret1 } -// UpdateAnnotation indicates an expected call of UpdateAnnotation. +// UpdateAnnotation indicates an expected call of UpdateAnnotation func (mr *MockAnnotationServiceMockRecorder) UpdateAnnotation(ctx, id, update interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAnnotation", reflect.TypeOf((*MockAnnotationService)(nil).UpdateAnnotation), ctx, id, update) } -// UpdateStream mocks base method. +// ListStreams mocks base method +func (m *MockAnnotationService) ListStreams(ctx context.Context, orgID platform.ID, filter influxdb.StreamListFilter) ([]influxdb.StoredStream, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListStreams", ctx, orgID, filter) + ret0, _ := ret[0].([]influxdb.StoredStream) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListStreams indicates an expected call of ListStreams +func (mr *MockAnnotationServiceMockRecorder) ListStreams(ctx, orgID, filter interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListStreams", reflect.TypeOf((*MockAnnotationService)(nil).ListStreams), ctx, orgID, filter) +} + +// CreateOrUpdateStream mocks base method +func (m *MockAnnotationService) CreateOrUpdateStream(ctx context.Context, orgID platform.ID, stream influxdb.Stream) (*influxdb.ReadStream, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateOrUpdateStream", ctx, orgID, stream) + ret0, _ := ret[0].(*influxdb.ReadStream) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateOrUpdateStream indicates an expected call of CreateOrUpdateStream +func (mr *MockAnnotationServiceMockRecorder) CreateOrUpdateStream(ctx, orgID, stream interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOrUpdateStream", reflect.TypeOf((*MockAnnotationService)(nil).CreateOrUpdateStream), ctx, orgID, stream) +} + +// GetStream mocks base method +func (m *MockAnnotationService) GetStream(ctx context.Context, id platform.ID) (*influxdb.StoredStream, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetStream", ctx, id) + ret0, _ := ret[0].(*influxdb.StoredStream) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetStream indicates an expected call of GetStream +func (mr *MockAnnotationServiceMockRecorder) GetStream(ctx, id interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetStream", reflect.TypeOf((*MockAnnotationService)(nil).GetStream), ctx, id) +} + +// UpdateStream mocks base method func (m *MockAnnotationService) UpdateStream(ctx context.Context, id platform.ID, stream influxdb.Stream) (*influxdb.ReadStream, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "UpdateStream", ctx, id, stream) @@ -191,8 +177,36 @@ func (m *MockAnnotationService) UpdateStream(ctx context.Context, id platform.ID return ret0, ret1 } -// UpdateStream indicates an expected call of UpdateStream. +// UpdateStream indicates an expected call of UpdateStream func (mr *MockAnnotationServiceMockRecorder) UpdateStream(ctx, id, stream interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateStream", reflect.TypeOf((*MockAnnotationService)(nil).UpdateStream), ctx, id, stream) } + +// DeleteStreams mocks base method +func (m *MockAnnotationService) DeleteStreams(ctx context.Context, orgID platform.ID, delete influxdb.BasicStream) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteStreams", ctx, orgID, delete) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteStreams indicates an expected call of DeleteStreams +func (mr *MockAnnotationServiceMockRecorder) DeleteStreams(ctx, orgID, delete interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteStreams", reflect.TypeOf((*MockAnnotationService)(nil).DeleteStreams), ctx, orgID, delete) +} + +// DeleteStreamByID mocks base method +func (m *MockAnnotationService) DeleteStreamByID(ctx context.Context, id platform.ID) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteStreamByID", ctx, id) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteStreamByID indicates an expected call of DeleteStreamByID +func (mr *MockAnnotationServiceMockRecorder) DeleteStreamByID(ctx, id interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteStreamByID", reflect.TypeOf((*MockAnnotationService)(nil).DeleteStreamByID), ctx, id) +} diff --git a/tenant/service_onboarding_test.go b/tenant/service_onboarding_test.go index 0a82e2475b..6c993fda0d 100644 --- a/tenant/service_onboarding_test.go +++ b/tenant/service_onboarding_test.go @@ -162,6 +162,8 @@ func TestOnboardAuth(t *testing.T) { {Action: influxdb.WriteAction, Resource: influxdb.Resource{OrgID: &onboard.Org.ID, Type: influxdb.DBRPResourceType}}, {Action: influxdb.ReadAction, Resource: influxdb.Resource{OrgID: &onboard.Org.ID, Type: influxdb.NotebooksResourceType}}, {Action: influxdb.WriteAction, Resource: influxdb.Resource{OrgID: &onboard.Org.ID, Type: influxdb.NotebooksResourceType}}, + {Action: influxdb.ReadAction, Resource: influxdb.Resource{OrgID: &onboard.Org.ID, Type: influxdb.AnnotationsResourceType}}, + {Action: influxdb.WriteAction, Resource: influxdb.Resource{OrgID: &onboard.Org.ID, Type: influxdb.AnnotationsResourceType}}, {Action: influxdb.ReadAction, Resource: influxdb.Resource{ID: &onboard.User.ID, Type: influxdb.UsersResourceType}}, {Action: influxdb.WriteAction, Resource: influxdb.Resource{ID: &onboard.User.ID, Type: influxdb.UsersResourceType}}, } diff --git a/tenant/service_user_test.go b/tenant/service_user_test.go index 2c3ff1b64b..6677ab2ccf 100644 --- a/tenant/service_user_test.go +++ b/tenant/service_user_test.go @@ -157,6 +157,7 @@ func TestFindPermissionsFromUser(t *testing.T) { influxdb.Permission{Action: influxdb.ReadAction, Resource: influxdb.Resource{OrgID: &orgID, Type: influxdb.ChecksResourceType}}, influxdb.Permission{Action: influxdb.ReadAction, Resource: influxdb.Resource{OrgID: &orgID, Type: influxdb.DBRPResourceType}}, influxdb.Permission{Action: influxdb.ReadAction, Resource: influxdb.Resource{OrgID: &orgID, Type: influxdb.NotebooksResourceType}}, + influxdb.Permission{Action: influxdb.ReadAction, Resource: influxdb.Resource{OrgID: &orgID, Type: influxdb.AnnotationsResourceType}}, influxdb.Permission{Action: influxdb.ReadAction, Resource: influxdb.Resource{Type: influxdb.UsersResourceType, ID: &u.ID}}, influxdb.Permission{Action: influxdb.WriteAction, Resource: influxdb.Resource{Type: influxdb.UsersResourceType, ID: &u.ID}}, }