Skip to content

Commit fe2dd13

Browse files
committed
Merge branch 'feature/preps-for-2.0-rc3' into 'master'
Preps for 2.0 RC3 See merge request lightmeter/controlcenter!891
2 parents 8639e74 + 977bcd6 commit fe2dd13

File tree

29 files changed

+1049
-779
lines changed

29 files changed

+1049
-779
lines changed

api/detective.go

Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"gitlab.com/lightmeter/controlcenter/metadata"
1515
"gitlab.com/lightmeter/controlcenter/pkg/httperror"
1616
detectivesettings "gitlab.com/lightmeter/controlcenter/settings/detective"
17+
"gitlab.com/lightmeter/controlcenter/util/emailutil"
1718
"gitlab.com/lightmeter/controlcenter/util/errorutil"
1819
"gitlab.com/lightmeter/controlcenter/util/httputil"
1920
"gitlab.com/lightmeter/controlcenter/util/timeutil"
@@ -22,34 +23,72 @@ import (
2223
"time"
2324
)
2425

26+
func isUserAuthenticated(auth *httpauth.Authenticator, r *http.Request) bool {
27+
sessionData, err := httpauth.GetSessionData(auth, r)
28+
if err != nil {
29+
return false
30+
}
31+
32+
return sessionData.IsAuthenticated()
33+
}
34+
35+
func checkQueryParameters(r *http.Request, isAuthenticated bool) error {
36+
err := r.ParseForm()
37+
if err != nil {
38+
return httperror.NewHTTPStatusCodeError(http.StatusUnprocessableEntity, errorutil.Wrap(err))
39+
}
40+
41+
// Parameters mail_from and mail_to can be partial (even void), but only for authenticated users
42+
paramOk := func(value string, isAuthenticated bool) int {
43+
switch {
44+
case len(value) == 0 && isAuthenticated:
45+
return http.StatusOK
46+
case len(value) == 0 && !isAuthenticated:
47+
return http.StatusUnauthorized
48+
}
49+
50+
_, _, partial, err := emailutil.SplitPartial(value)
51+
if err != nil {
52+
return http.StatusUnprocessableEntity
53+
}
54+
55+
if partial && !isAuthenticated {
56+
return http.StatusUnauthorized
57+
}
58+
59+
return http.StatusOK
60+
}
61+
62+
if fromOk := paramOk(r.Form.Get("mail_from"), isAuthenticated); fromOk != http.StatusOK {
63+
return httperror.NewHTTPStatusCodeError(fromOk, errors.New("Partial from or to parameter not allowed"))
64+
}
65+
66+
if toOk := paramOk(r.Form.Get("mail_to"), isAuthenticated); toOk != http.StatusOK {
67+
return httperror.NewHTTPStatusCodeError(toOk, errors.New("Partial from or to parameter not allowed"))
68+
}
69+
70+
return nil
71+
}
72+
2573
func requireDetectiveAuth(auth *httpauth.Authenticator, settingsReader metadata.Reader) httpmiddleware.Middleware {
2674
return func(h httpmiddleware.CustomHTTPHandler) httpmiddleware.CustomHTTPHandler {
2775
return httpmiddleware.CustomHTTPHandler(func(w http.ResponseWriter, r *http.Request) error {
2876
/* The detective handler can be accessed if authenticated
2977
* or if the 'open to end users' setting is enabled.
3078
*/
79+
isAuthenticated := isUserAuthenticated(auth, r)
3180

3281
settings := detectivesettings.Settings{}
3382
err := settingsReader.RetrieveJson(r.Context(), detectivesettings.SettingKey, &settings)
3483
if err != nil && !errors.Is(err, metadata.ErrNoSuchKey) {
3584
return httperror.NewHTTPStatusCodeError(http.StatusInternalServerError, errorutil.Wrap(err))
3685
}
3786

38-
if settings.EndUsersEnabled {
39-
if err := h.ServeHTTP(w, r); err != nil {
40-
return httperror.NewHTTPStatusCodeError(http.StatusInternalServerError, errorutil.Wrap(err))
41-
}
42-
43-
return nil
87+
if err := checkQueryParameters(r, isAuthenticated); err != nil {
88+
return err // NOTE: already an XHTTPError
4489
}
4590

46-
// If setting is disabled, user must be authenticated
47-
sessionData, err := httpauth.GetSessionData(auth, r)
48-
if err != nil {
49-
return httperror.NewHTTPStatusCodeError(http.StatusUnauthorized, errorutil.Wrap(err))
50-
}
51-
52-
if !sessionData.IsAuthenticated() {
91+
if !isAuthenticated && !settings.EndUsersEnabled {
5392
return httperror.NewHTTPStatusCodeError(http.StatusUnauthorized, httpauth.ErrUnauthenticated)
5493
}
5594

@@ -75,6 +114,8 @@ type Interval string
75114
// @Param mail_to query string true "Recipient email address"
76115
// @Param timestamp_from query string true "Initial timestamp in the format 1999-12-23 12:00:00"
77116
// @Param timestamp_to query string true "Final timestamp in the format 1999-12-23 14:00:00"
117+
// @Param status query string true "A status to filter messages (-1: all, 0: sent... see smtp.go)"
118+
// @Param page query string true "Page number to return results"
78119
// @Produce json
79120
// @Success 200 {object} []detective.MessageDelivery "desc"
80121
// @Failure 422 {string} string "desc"
@@ -89,12 +130,17 @@ func (h checkMessageDeliveryHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
89130
return httperror.NewHTTPStatusCodeError(http.StatusUnprocessableEntity, err)
90131
}
91132

133+
status, err := strconv.Atoi(r.Form.Get("status"))
134+
if err != nil {
135+
return httperror.NewHTTPStatusCodeError(http.StatusUnprocessableEntity, err)
136+
}
137+
92138
page, err := strconv.Atoi(r.Form.Get("page"))
93139
if err != nil {
94140
return httperror.NewHTTPStatusCodeError(http.StatusUnprocessableEntity, err)
95141
}
96142

97-
messages, err := h.detective.CheckMessageDelivery(r.Context(), r.Form.Get("mail_from"), r.Form.Get("mail_to"), interval, page)
143+
messages, err := h.detective.CheckMessageDelivery(r.Context(), r.Form.Get("mail_from"), r.Form.Get("mail_to"), interval, status, page)
98144

99145
if err != nil {
100146
return httperror.NewHTTPStatusCodeError(http.StatusUnprocessableEntity, err)
@@ -130,7 +176,7 @@ func (h oldestAvailableTimeHandler) ServeHTTP(w http.ResponseWriter, r *http.Req
130176

131177
func HttpDetective(auth *auth.Authenticator, mux *http.ServeMux, timezone *time.Location, detective detective.Detective, escalator escalator.Requester, settingsReader metadata.Reader, isBehindReverseProxy bool) {
132178
publicIfEnabled := httpmiddleware.New(
133-
httpmiddleware.RequestWithRateLimit(10*time.Minute, 20, isBehindReverseProxy, httpmiddleware.BlockQuery),
179+
httpmiddleware.RequestWithRateLimit(10*time.Minute, 50, isBehindReverseProxy, httpmiddleware.BlockQuery),
134180
httpmiddleware.RequestWithTimeout(httpmiddleware.DefaultTimeout),
135181
requireDetectiveAuth(auth, settingsReader),
136182
)

api/detective_auth_test.go

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -81,23 +81,30 @@ func buildTestEnv(t *testing.T) (*httptest.Server, *mock_detective.MockDetective
8181

8282
func TestDetectiveAuth(t *testing.T) {
8383
Convey("Detective auth", t, func() {
84-
detectiveURL := "/api/v0/checkMessageDeliveryStatus?mail_from=a@b.c&mail_to=d@e.f&from=2020-01-01&to=2020-12-31&page=1"
84+
detectiveURL := "/api/v0/checkMessageDeliveryStatus?mail_from=a@b.c&mail_to=d@e.f&from=2020-01-01&to=2020-12-31&status=-1&page=1"
85+
detectiveURLPartialMailFrom := "/api/v0/checkMessageDeliveryStatus?mail_from=b.c&mail_to=d@e.f&from=2020-01-01&to=2020-12-31&status=-1&page=1"
86+
detectiveURLPartialMailTo := "/api/v0/checkMessageDeliveryStatus?mail_from=a@b.c&mail_to=e.f&from=2020-01-01&to=2020-12-31&status=-1&page=1"
87+
detectiveURLEmptyMailFrom := "/api/v0/checkMessageDeliveryStatus?mail_to=d@e.f&from=2020-01-01&to=2020-12-31&status=-1&page=1"
88+
detectiveURLEmptyMailTo := "/api/v0/checkMessageDeliveryStatus?mail_from=a@b.c&from=2020-01-01&to=2020-12-31&status=-1&page=1"
8589

8690
c := buildCookieClient()
8791

8892
s, d, settingsWriter, clear := buildTestEnv(t)
8993
defer clear()
9094

91-
d.EXPECT().
92-
CheckMessageDelivery(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
93-
Return(&detective.MessagesPage{}, nil)
95+
expect := func(d *mock_detective.MockDetective) {
96+
d.EXPECT().
97+
CheckMessageDelivery(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
98+
Return(&detective.MessagesPage{}, nil)
99+
}
94100

95101
Convey("Detective API not accessible to non-authenticated user", func() {
96102
r, err := c.Get(s.URL + detectiveURL)
97103
So(err, ShouldBeNil)
98104
So(r.StatusCode, ShouldEqual, http.StatusUnauthorized)
99105

100106
Convey("Once we are logged in, detective API is accessible", func() {
107+
expect(d)
101108
r, err = c.PostForm(s.URL+"/login", url.Values{"email": {"alice@example.com"}, "password": {"super-secret"}})
102109
So(err, ShouldBeNil)
103110
So(r.StatusCode, ShouldEqual, http.StatusOK)
@@ -107,6 +114,32 @@ func TestDetectiveAuth(t *testing.T) {
107114
So(r.StatusCode, ShouldEqual, http.StatusOK)
108115
}
109116
})
117+
118+
Convey("Partial searches available to authenticated users", func() {
119+
r, err = c.PostForm(s.URL+"/login", url.Values{"email": {"alice@example.com"}, "password": {"super-secret"}})
120+
So(err, ShouldBeNil)
121+
So(r.StatusCode, ShouldEqual, http.StatusOK)
122+
123+
expect(d)
124+
r, err := c.Get(s.URL + detectiveURLPartialMailFrom)
125+
So(err, ShouldBeNil)
126+
So(r.StatusCode, ShouldEqual, http.StatusOK)
127+
128+
expect(d)
129+
r, err = c.Get(s.URL + detectiveURLPartialMailTo)
130+
So(err, ShouldBeNil)
131+
So(r.StatusCode, ShouldEqual, http.StatusOK)
132+
133+
expect(d)
134+
r, err = c.Get(s.URL + detectiveURLEmptyMailFrom)
135+
So(err, ShouldBeNil)
136+
So(r.StatusCode, ShouldEqual, http.StatusOK)
137+
138+
expect(d)
139+
r, err = c.Get(s.URL + detectiveURLEmptyMailTo)
140+
So(err, ShouldBeNil)
141+
So(r.StatusCode, ShouldEqual, http.StatusOK)
142+
})
110143
})
111144

112145
Convey("Detective API only accessible to end-users if setting is enabled", func() {
@@ -119,11 +152,28 @@ func TestDetectiveAuth(t *testing.T) {
119152
settings.EndUsersEnabled = true
120153
detectivesettings.SetSettings(context.Background(), settingsWriter, settings)
121154

122-
{
123-
r, err := c.Get(s.URL + detectiveURL)
155+
expect(d)
156+
r, err := c.Get(s.URL + detectiveURL)
157+
So(err, ShouldBeNil)
158+
So(r.StatusCode, ShouldEqual, http.StatusOK)
159+
160+
Convey("Partial searches unavailable to unauthenticated users", func() {
161+
r, err := c.Get(s.URL + detectiveURLPartialMailFrom)
124162
So(err, ShouldBeNil)
125-
So(r.StatusCode, ShouldEqual, http.StatusOK)
126-
}
163+
So(r.StatusCode, ShouldEqual, http.StatusUnauthorized)
164+
165+
r, err = c.Get(s.URL + detectiveURLPartialMailTo)
166+
So(err, ShouldBeNil)
167+
So(r.StatusCode, ShouldEqual, http.StatusUnauthorized)
168+
169+
r, err = c.Get(s.URL + detectiveURLEmptyMailFrom)
170+
So(err, ShouldBeNil)
171+
So(r.StatusCode, ShouldEqual, http.StatusUnauthorized)
172+
173+
r, err = c.Get(s.URL + detectiveURLEmptyMailTo)
174+
So(err, ShouldBeNil)
175+
So(r.StatusCode, ShouldEqual, http.StatusUnauthorized)
176+
})
127177
})
128178
})
129179
})

api/detective_test.go

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -47,36 +47,36 @@ func TestDetectiveCheckMessageDeliveryHandler(t *testing.T) {
4747
})
4848

4949
Convey("No Time Interval", func() {
50-
r, err := http.Get(fmt.Sprintf("%s?mail_from=user1@example.org&mail_to=user2@example.org&page=1", s.URL))
50+
r, err := http.Get(fmt.Sprintf("%s?mail_from=user1@example.org&mail_to=user2@example.org&status=-1&page=1", s.URL))
5151
So(err, ShouldBeNil)
5252
So(r.StatusCode, ShouldEqual, http.StatusUnprocessableEntity)
5353
})
5454

5555
emptyResult := detective.MessagesPage{}
5656

5757
Convey("No Sender", func() {
58-
m.EXPECT().CheckMessageDelivery(gomock.Any(), "", "user2@example.org", interval, 1).Return(&emptyResult, emailutil.ErrInvalidEmail)
59-
r, err := http.Get(fmt.Sprintf("%s?from=1999-01-01&to=1999-12-31&mail_to=user2@example.org&page=1", s.URL))
58+
m.EXPECT().CheckMessageDelivery(gomock.Any(), "", "user2@example.org", interval, -1, 1).Return(&emptyResult, emailutil.ErrInvalidEmail)
59+
r, err := http.Get(fmt.Sprintf("%s?from=1999-01-01&to=1999-12-31&mail_to=user2@example.org&status=-1&page=1", s.URL))
6060
So(err, ShouldBeNil)
6161
So(r.StatusCode, ShouldEqual, http.StatusUnprocessableEntity)
6262
})
6363

6464
Convey("No Recipient", func() {
65-
m.EXPECT().CheckMessageDelivery(gomock.Any(), "user1@example.org", "", interval, 1).Return(&emptyResult, emailutil.ErrInvalidEmail)
66-
r, err := http.Get(fmt.Sprintf("%s?from=1999-01-01&to=1999-12-31&mail_from=user1@example.org&page=1", s.URL))
65+
m.EXPECT().CheckMessageDelivery(gomock.Any(), "user1@example.org", "", interval, -1, 1).Return(&emptyResult, emailutil.ErrInvalidEmail)
66+
r, err := http.Get(fmt.Sprintf("%s?from=1999-01-01&to=1999-12-31&mail_from=user1@example.org&status=-1&page=1", s.URL))
6767
So(err, ShouldBeNil)
6868
So(r.StatusCode, ShouldEqual, http.StatusUnprocessableEntity)
6969
})
7070

7171
Convey("Dates out of order", func() {
7272
// "from" comes after "to"
73-
r, err := http.Get(fmt.Sprintf("%s?to=1999-01-01&from=1999-12-31&mail_from=user1@example.org&mail_to=user2&page=1", s.URL))
73+
r, err := http.Get(fmt.Sprintf("%s?to=1999-01-01&from=1999-12-31&mail_from=user1@example.org&mail_to=user2&status=-1&page=1", s.URL))
7474
So(err, ShouldBeNil)
7575
So(r.StatusCode, ShouldEqual, http.StatusUnprocessableEntity)
7676
})
7777

7878
Convey("No page", func() {
79-
r, err := http.Get(fmt.Sprintf("%s?from=1999-01-01&to=1999-12-31&mail_from=user1@example.org&mail_to=user2@example.org", s.URL))
79+
r, err := http.Get(fmt.Sprintf("%s?from=1999-01-01&to=1999-12-31&mail_from=user1@example.org&mail_to=user2@example.org&status=-1", s.URL))
8080
So(err, ShouldBeNil)
8181
So(r.StatusCode, ShouldEqual, http.StatusUnprocessableEntity)
8282
})
@@ -97,15 +97,17 @@ func TestDetectiveCheckMessageDeliveryHandler(t *testing.T) {
9797
detective.Status(parser.SentStatus),
9898
"2.0.0",
9999
nil,
100+
"user1@example.org",
101+
"user2@example.org",
100102
},
101103
},
102104
},
103105
},
104106
}
105107

106-
m.EXPECT().CheckMessageDelivery(gomock.Any(), "user1@example.org", "user2@example.org", interval, 1).Return(&messages, nil)
108+
m.EXPECT().CheckMessageDelivery(gomock.Any(), "user1@example.org", "user2@example.org", interval, -1, 1).Return(&messages, nil)
107109

108-
r, err := http.Get(fmt.Sprintf("%s?from=1999-01-01&to=1999-12-31&mail_from=user1@example.org&mail_to=user2@example.org&page=1", s.URL))
110+
r, err := http.Get(fmt.Sprintf("%s?from=1999-01-01&to=1999-12-31&mail_from=user1@example.org&mail_to=user2@example.org&status=-1&page=1", s.URL))
109111
So(err, ShouldBeNil)
110112
So(r.StatusCode, ShouldEqual, http.StatusOK)
111113

@@ -177,7 +179,7 @@ func TestEscalation(t *testing.T) {
177179
s := httptest.NewServer(httpmiddleware.New().WithEndpoint(detectiveEscalatorHandler{requester: e, detective: d}))
178180

179181
Convey("No message escalated", func() {
180-
d.EXPECT().CheckMessageDelivery(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(&detective.MessagesPage{}, nil)
182+
d.EXPECT().CheckMessageDelivery(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(&detective.MessagesPage{}, nil)
181183

182184
r, err := http.PostForm(s.URL, url.Values{
183185
"from": []string{"2000-01-01"},
@@ -192,7 +194,7 @@ func TestEscalation(t *testing.T) {
192194
})
193195

194196
Convey("Internal error if detective check fails", func() {
195-
d.EXPECT().CheckMessageDelivery(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(&detective.MessagesPage{}, errors.New(`Some error`))
197+
d.EXPECT().CheckMessageDelivery(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(&detective.MessagesPage{}, errors.New(`Some error`))
196198

197199
r, err := http.PostForm(s.URL, url.Values{
198200
"from": []string{"2000-01-01"},
@@ -220,7 +222,7 @@ func TestEscalation(t *testing.T) {
220222
})
221223

222224
Convey("Escalate issue", func() {
223-
d.EXPECT().CheckMessageDelivery(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(
225+
d.EXPECT().CheckMessageDelivery(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(
224226
&detective.MessagesPage{
225227
PageNumber: 1,
226228
FirstPage: 1,
@@ -231,11 +233,13 @@ func TestEscalation(t *testing.T) {
231233
Queue: "AAA",
232234
Entries: []detective.MessageDelivery{
233235
{
234-
TimeMin: timeutil.MustParseTime(`2000-01-01 10:00:00 +0000`),
235-
TimeMax: timeutil.MustParseTime(`2000-01-01 10:00:00 +0000`),
236-
Status: detective.Status(parser.BouncedStatus),
237-
Dsn: "3.4.6",
238-
Expired: nil,
236+
TimeMin: timeutil.MustParseTime(`2000-01-01 10:00:00 +0000`),
237+
TimeMax: timeutil.MustParseTime(`2000-01-01 10:00:00 +0000`),
238+
Status: detective.Status(parser.BouncedStatus),
239+
Dsn: "3.4.6",
240+
Expired: nil,
241+
MailFrom: "user1@example.com",
242+
MailTo: "user2@example.com",
239243
},
240244
},
241245
},
@@ -261,11 +265,13 @@ func TestEscalation(t *testing.T) {
261265
Queue: "AAA",
262266
Entries: []detective.MessageDelivery{
263267
{
264-
TimeMin: timeutil.MustParseTime(`2000-01-01 10:00:00 +0000`),
265-
TimeMax: timeutil.MustParseTime(`2000-01-01 10:00:00 +0000`),
266-
Status: detective.Status(parser.BouncedStatus),
267-
Dsn: "3.4.6",
268-
Expired: nil,
268+
TimeMin: timeutil.MustParseTime(`2000-01-01 10:00:00 +0000`),
269+
TimeMax: timeutil.MustParseTime(`2000-01-01 10:00:00 +0000`),
270+
Status: detective.Status(parser.BouncedStatus),
271+
Dsn: "3.4.6",
272+
Expired: nil,
273+
MailFrom: "user1@example.com",
274+
MailTo: "user2@example.com",
269275
},
270276
},
271277
},

api/status_message.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@ type statusMessageHandler struct {
1818
accessor *collector.Accessor
1919
}
2020

21-
// NOTE: for swagger only
22-
type Event receptor.Event
21+
type MessageNotification struct {
22+
ID string `json:"id"`
23+
Notification *receptor.MessageNotification `json:"notification,omitempty"`
24+
}
2325

2426
// @Summary
25-
// @Success 200 {object} receptor.Event "desc"
27+
// @Success 200 {object} MessageNotification "desc"
2628
// @Failure 422 {string} string "desc"
2729
// @Failure 500 {string} string "desc"
2830
// @Router /api/v0/intelstatus [post]
@@ -40,7 +42,7 @@ func (h statusMessageHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
4042
return httputil.WriteJson(w, statusMessage, http.StatusOK)
4143
}
4244

43-
return httputil.WriteJson(w, statusMessage.MessageNotification, http.StatusOK)
45+
return httputil.WriteJson(w, MessageNotification{ID: statusMessage.ID, Notification: statusMessage.MessageNotification}, http.StatusOK)
4446
}
4547

4648
func HttpStatusMessage(auth *auth.Authenticator, mux *http.ServeMux, accessor *collector.Accessor) {

0 commit comments

Comments
 (0)