forked from pocketbase/pocketbase
-
Notifications
You must be signed in to change notification settings - Fork 0
/
middlewares.go
396 lines (343 loc) · 11.7 KB
/
middlewares.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
package apis
import (
"fmt"
"log"
"net"
"net/http"
"strings"
"time"
"github.com/labstack/echo/v5"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/tokens"
"github.com/pocketbase/pocketbase/tools/list"
"github.com/pocketbase/pocketbase/tools/routine"
"github.com/pocketbase/pocketbase/tools/security"
"github.com/pocketbase/pocketbase/tools/types"
"github.com/spf13/cast"
)
// Common request context keys used by the middlewares and api handlers.
const (
ContextAdminKey string = "admin"
ContextAuthRecordKey string = "authRecord"
ContextCollectionKey string = "collection"
)
// RequireGuestOnly middleware requires a request to NOT have a valid
// Authorization header.
//
// This middleware is the opposite of [apis.RequireAdminOrRecordAuth()].
func RequireGuestOnly() echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
err := NewBadRequestError("The request can be accessed only by guests.", nil)
record, _ := c.Get(ContextAuthRecordKey).(*models.Record)
if record != nil {
return err
}
admin, _ := c.Get(ContextAdminKey).(*models.Admin)
if admin != nil {
return err
}
return next(c)
}
}
}
// RequireRecordAuth middleware requires a request to have
// a valid record auth Authorization header.
//
// The auth record could be from any collection.
//
// You can further filter the allowed record auth collections by
// specifying their names.
//
// Example:
// apis.RequireRecordAuth()
// Or:
// apis.RequireRecordAuth("users", "supervisors")
//
// To restrict the auth record only to the loaded context collection,
// use [apis.RequireSameContextRecordAuth()] instead.
func RequireRecordAuth(optCollectionNames ...string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
record, _ := c.Get(ContextAuthRecordKey).(*models.Record)
if record == nil {
return NewUnauthorizedError("The request requires valid record authorization token to be set.", nil)
}
// check record collection name
if len(optCollectionNames) > 0 && !list.ExistInSlice(record.Collection().Name, optCollectionNames) {
return NewForbiddenError("The authorized record model is not allowed to perform this action.", nil)
}
return next(c)
}
}
}
//
// RequireSameContextRecordAuth middleware requires a request to have
// a valid record Authorization header.
//
// The auth record must be from the same collection already loaded in the context.
func RequireSameContextRecordAuth() echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
record, _ := c.Get(ContextAuthRecordKey).(*models.Record)
if record == nil {
return NewUnauthorizedError("The request requires valid record authorization token to be set.", nil)
}
collection, _ := c.Get(ContextCollectionKey).(*models.Collection)
if collection == nil || record.Collection().Id != collection.Id {
return NewForbiddenError(fmt.Sprintf("The request requires auth record from %s collection.", record.Collection().Name), nil)
}
return next(c)
}
}
}
// RequireAdminAuth middleware requires a request to have
// a valid admin Authorization header.
func RequireAdminAuth() echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
admin, _ := c.Get(ContextAdminKey).(*models.Admin)
if admin == nil {
return NewUnauthorizedError("The request requires valid admin authorization token to be set.", nil)
}
return next(c)
}
}
}
// RequireAdminAuthOnlyIfAny middleware requires a request to have
// a valid admin Authorization header ONLY if the application has
// at least 1 existing Admin model.
func RequireAdminAuthOnlyIfAny(app core.App) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
totalAdmins, err := app.Dao().TotalAdmins()
if err != nil {
return NewBadRequestError("Failed to fetch admins info.", err)
}
admin, _ := c.Get(ContextAdminKey).(*models.Admin)
if admin != nil || totalAdmins == 0 {
return next(c)
}
return NewUnauthorizedError("The request requires valid admin authorization token to be set.", nil)
}
}
}
// RequireAdminOrRecordAuth middleware requires a request to have
// a valid admin or record Authorization header set.
//
// You can further filter the allowed auth record collections by providing their names.
//
// This middleware is the opposite of [apis.RequireGuestOnly()].
func RequireAdminOrRecordAuth(optCollectionNames ...string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
admin, _ := c.Get(ContextAdminKey).(*models.Admin)
record, _ := c.Get(ContextAuthRecordKey).(*models.Record)
if admin == nil && record == nil {
return NewUnauthorizedError("The request requires admin or record authorization token to be set.", nil)
}
if record != nil && len(optCollectionNames) > 0 && !list.ExistInSlice(record.Collection().Name, optCollectionNames) {
return NewForbiddenError("The authorized record model is not allowed to perform this action.", nil)
}
return next(c)
}
}
}
// RequireAdminOrOwnerAuth middleware requires a request to have
// a valid admin or auth record owner Authorization header set.
//
// This middleware is similar to [apis.RequireAdminOrRecordAuth()] but
// for the auth record token expects to have the same id as the path
// parameter ownerIdParam (default to "id" if empty).
func RequireAdminOrOwnerAuth(ownerIdParam string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
admin, _ := c.Get(ContextAdminKey).(*models.Admin)
if admin != nil {
return next(c)
}
record, _ := c.Get(ContextAuthRecordKey).(*models.Record)
if record == nil {
return NewUnauthorizedError("The request requires admin or record authorization token to be set.", nil)
}
if ownerIdParam == "" {
ownerIdParam = "id"
}
ownerId := c.PathParam(ownerIdParam)
// note: it is "safe" to compare only the record id since the auth
// record ids are treated as unique across all auth collections
if record.Id != ownerId {
return NewForbiddenError("You are not allowed to perform this request.", nil)
}
return next(c)
}
}
}
// LoadAuthContext middleware reads the Authorization request header
// and loads the token related record or admin instance into the
// request's context.
//
// This middleware is expected to be already registered by default for all routes.
func LoadAuthContext(app core.App) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
token := c.Request().Header.Get("Authorization")
if token == "" {
return next(c)
}
// the schema is not required and it is only for
// compatibility with the defaults of some HTTP clients
token = strings.TrimPrefix(token, "Bearer ")
claims, _ := security.ParseUnverifiedJWT(token)
tokenType := cast.ToString(claims["type"])
switch tokenType {
case tokens.TypeAdmin:
admin, err := app.Dao().FindAdminByToken(
token,
app.Settings().AdminAuthToken.Secret,
)
if err == nil && admin != nil {
c.Set(ContextAdminKey, admin)
}
case tokens.TypeAuthRecord:
record, err := app.Dao().FindAuthRecordByToken(
token,
app.Settings().RecordAuthToken.Secret,
)
if err == nil && record != nil {
c.Set(ContextAuthRecordKey, record)
}
}
return next(c)
}
}
}
// LoadCollectionContext middleware finds the collection with related
// path identifier and loads it into the request context.
//
// Set optCollectionTypes to further filter the found collection by its type.
func LoadCollectionContext(app core.App, optCollectionTypes ...string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
if param := c.PathParam("collection"); param != "" {
collection, err := app.Dao().FindCollectionByNameOrId(param)
if err != nil || collection == nil {
return NewNotFoundError("", err)
}
if len(optCollectionTypes) > 0 && !list.ExistInSlice(collection.Type, optCollectionTypes) {
return NewBadRequestError("Invalid collection type.", nil)
}
c.Set(ContextCollectionKey, collection)
}
return next(c)
}
}
}
// ActivityLogger middleware takes care to save the request information
// into the logs database.
//
// The middleware does nothing if the app logs retention period is zero
// (aka. app.Settings().Logs.MaxDays = 0).
func ActivityLogger(app core.App) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
err := next(c)
// no logs retention
if app.Settings().Logs.MaxDays == 0 {
return err
}
httpRequest := c.Request()
httpResponse := c.Response()
status := httpResponse.Status
meta := types.JsonMap{}
if err != nil {
switch v := err.(type) {
case *echo.HTTPError:
status = v.Code
meta["errorMessage"] = v.Message
meta["errorDetails"] = fmt.Sprint(v.Internal)
case *ApiError:
status = v.Code
meta["errorMessage"] = v.Message
meta["errorDetails"] = fmt.Sprint(v.RawData())
default:
status = http.StatusBadRequest
meta["errorMessage"] = v.Error()
}
}
requestAuth := models.RequestAuthGuest
if c.Get(ContextAuthRecordKey) != nil {
requestAuth = models.RequestAuthRecord
} else if c.Get(ContextAdminKey) != nil {
requestAuth = models.RequestAuthAdmin
}
ip, _, _ := net.SplitHostPort(httpRequest.RemoteAddr)
model := &models.Request{
Url: httpRequest.URL.RequestURI(),
Method: strings.ToLower(httpRequest.Method),
Status: status,
Auth: requestAuth,
UserIp: realUserIp(httpRequest, ip),
RemoteIp: ip,
Referer: httpRequest.Referer(),
UserAgent: httpRequest.UserAgent(),
Meta: meta,
}
// set timestamp fields before firing a new go routine
model.RefreshCreated()
model.RefreshUpdated()
routine.FireAndForget(func() {
attempts := 1
BeginSave:
logErr := app.LogsDao().SaveRequest(model)
if logErr != nil {
// try one more time after 10s in case of SQLITE_BUSY or "database is locked" error
if attempts <= 2 {
attempts++
time.Sleep(10 * time.Second)
goto BeginSave
} else if app.IsDebug() {
log.Println("Log save failed:", logErr)
}
}
// Delete old request logs
// ---
now := time.Now()
lastLogsDeletedAt := cast.ToTime(app.Cache().Get("lastLogsDeletedAt"))
daysDiff := now.Sub(lastLogsDeletedAt).Hours() * 24
if daysDiff > float64(app.Settings().Logs.MaxDays) {
deleteErr := app.LogsDao().DeleteOldRequests(now.AddDate(0, 0, -1*app.Settings().Logs.MaxDays))
if deleteErr == nil {
app.Cache().Set("lastLogsDeletedAt", now)
} else if app.IsDebug() {
log.Println("Logs delete failed:", deleteErr)
}
}
})
return err
}
}
}
// Returns the "real" user IP from common proxy headers (or fallbackIp if none is found).
//
// The returned IP value shouldn't be trusted if not behind a trusted reverse proxy!
func realUserIp(r *http.Request, fallbackIp string) string {
if ip := r.Header.Get("CF-Connecting-IP"); ip != "" {
return ip
}
if ip := r.Header.Get("X-Real-IP"); ip != "" {
return ip
}
if ipsList := r.Header.Get("X-Forwarded-For"); ipsList != "" {
ips := strings.Split(ipsList, ",")
// extract the rightmost ip
for i := len(ips) - 1; i >= 0; i-- {
ip := strings.TrimSpace(ips[i])
if ip != "" {
return ip
}
}
}
return fallbackIp
}