[#943] exposed apis.EnrichRecord and apis.EnrichRecords

This commit is contained in:
Gani Georgiev
2022-11-17 14:17:10 +02:00
parent 6e9cf986c5
commit 39408f135b
16 changed files with 297 additions and 212 deletions
+3 -9
View File
@@ -251,16 +251,10 @@ func (api *realtimeApi) canAccessRecord(client subscriptions.Client, record *mod
}
// emulate request data
requestData := map[string]any{
"method": "GET",
"query": map[string]any{},
"data": map[string]any{},
"auth": nil,
}
authRecord, _ := client.Get(ContextAuthRecordKey).(*models.Record)
if authRecord != nil {
requestData["auth"] = authRecord.PublicExport()
requestData := &models.FilterRequestData{
Method: "GET",
}
requestData.AuthRecord, _ = client.Get(ContextAuthRecordKey).(*models.Record)
resolver := resolvers.NewRecordFieldResolver(api.app.Dao(), record.Collection(), requestData, true)
expr, err := search.FilterData(*accessRule).BuildExpr(resolver)
+7 -8
View File
@@ -65,20 +65,19 @@ func (api *recordAuthApi) authResponse(c echo.Context, authRecord *models.Record
}
return api.app.OnRecordAuthRequest().Trigger(event, func(e *core.RecordAuthEvent) error {
admin, _ := e.HttpContext.Get(ContextAdminKey).(*models.Admin)
// allow always returning the email address of the authenticated account
e.Record.IgnoreEmailVisibility(true)
// expand record relations
expands := strings.Split(c.QueryParam(expandQueryParam), ",")
if len(expands) > 0 {
requestData := exportRequestData(e.HttpContext)
requestData["auth"] = e.Record.PublicExport()
requestData := GetRequestData(e.HttpContext)
requestData.Admin = nil
requestData.AuthRecord = e.Record
failed := api.app.Dao().ExpandRecord(
e.Record,
expands,
expandFetch(api.app.Dao(), admin != nil, requestData),
expandFetch(api.app.Dao(), requestData),
)
if len(failed) > 0 && api.app.IsDebug() {
log.Println("Failed to expand relations: ", failed)
@@ -204,8 +203,8 @@ func (api *recordAuthApi) authWithOAuth2(c echo.Context) error {
record, authData, submitErr := form.Submit(func(createForm *forms.RecordUpsert, authRecord *models.Record, authUser *auth.AuthUser) error {
return createForm.DrySubmit(func(txDao *daos.Dao) error {
requestData := exportRequestData(c)
requestData["data"] = form.CreateData
requestData := GetRequestData(c)
requestData.Data = form.CreateData
createRuleFunc := func(q *dbx.SelectQuery) error {
admin, _ := c.Get(ContextAdminKey).(*models.Admin)
@@ -422,7 +421,7 @@ func (api *recordAuthApi) listExternalAuths(c echo.Context) error {
ExternalAuths: externalAuths,
}
return api.app.OnRecordListExternalAuths().Trigger(event, func(e *core.RecordListExternalAuthsEvent) error {
return api.app.OnRecordListExternalAuthsRequest().Trigger(event, func(e *core.RecordListExternalAuthsEvent) error {
return e.HttpContext.JSON(http.StatusOK, e.ExternalAuths)
})
}
+4 -4
View File
@@ -886,7 +886,7 @@ func TestRecordAuthListExternalsAuths(t *testing.T) {
},
ExpectedStatus: 200,
ExpectedContent: []string{`[]`},
ExpectedEvents: map[string]int{"OnRecordListExternalAuths": 1},
ExpectedEvents: map[string]int{"OnRecordListExternalAuthsRequest": 1},
},
{
Name: "admin + existing user id and 2 external auths",
@@ -902,7 +902,7 @@ func TestRecordAuthListExternalsAuths(t *testing.T) {
`"recordId":"4q1xlclmfloku33"`,
`"collectionId":"_pb_users_auth_"`,
},
ExpectedEvents: map[string]int{"OnRecordListExternalAuths": 1},
ExpectedEvents: map[string]int{"OnRecordListExternalAuthsRequest": 1},
},
{
Name: "auth record + trying to list another user external auths",
@@ -933,7 +933,7 @@ func TestRecordAuthListExternalsAuths(t *testing.T) {
},
ExpectedStatus: 200,
ExpectedContent: []string{`[]`},
ExpectedEvents: map[string]int{"OnRecordListExternalAuths": 1},
ExpectedEvents: map[string]int{"OnRecordListExternalAuthsRequest": 1},
},
{
Name: "authorized as user - owner with 2 external auths",
@@ -949,7 +949,7 @@ func TestRecordAuthListExternalsAuths(t *testing.T) {
`"recordId":"4q1xlclmfloku33"`,
`"collectionId":"_pb_users_auth_"`,
},
ExpectedEvents: map[string]int{"OnRecordListExternalAuths": 1},
ExpectedEvents: map[string]int{"OnRecordListExternalAuthsRequest": 1},
},
}
+47 -115
View File
@@ -46,31 +46,30 @@ func (api *recordApi) list(c echo.Context) error {
return NewNotFoundError("", "Missing collection context.")
}
admin, _ := c.Get(ContextAdminKey).(*models.Admin)
if admin == nil && collection.ListRule == nil {
// only admins can access if the rule is nil
return NewForbiddenError("Only admins can perform this action.", nil)
}
// forbid users and guests to query special filter/sort fields
if err := api.checkForForbiddenQueryFields(c); err != nil {
return err
}
requestData := exportRequestData(c)
requestData := GetRequestData(c)
if requestData.Admin == nil && collection.ListRule == nil {
// only admins can access if the rule is nil
return NewForbiddenError("Only admins can perform this action.", nil)
}
fieldsResolver := resolvers.NewRecordFieldResolver(
api.app.Dao(),
collection,
requestData,
// hidden fields are searchable only by admins
admin != nil,
requestData.Admin != nil,
)
searchProvider := search.NewProvider(fieldsResolver).
Query(api.app.Dao().RecordQuery(collection))
if admin == nil && collection.ListRule != nil {
if requestData.Admin == nil && collection.ListRule != nil {
searchProvider.AddFilter(search.FilterData(*collection.ListRule))
}
@@ -82,28 +81,6 @@ func (api *recordApi) list(c echo.Context) error {
records := models.NewRecordsFromNullStringMaps(collection, rawRecords)
// expand records relations
expands := strings.Split(c.QueryParam(expandQueryParam), ",")
if len(expands) > 0 {
failed := api.app.Dao().ExpandRecords(
records,
expands,
expandFetch(api.app.Dao(), admin != nil, requestData),
)
if len(failed) > 0 && api.app.IsDebug() {
log.Println("Failed to expand relations: ", failed)
}
}
if collection.IsAuth() {
err := autoIgnoreAuthRecordsEmailVisibility(
api.app.Dao(), records, admin != nil, requestData,
)
if err != nil && api.app.IsDebug() {
log.Println("IgnoreEmailVisibility failure:", err)
}
}
result.Items = records
event := &core.RecordsListEvent{
@@ -114,6 +91,10 @@ func (api *recordApi) list(c echo.Context) error {
}
return api.app.OnRecordsListRequest().Trigger(event, func(e *core.RecordsListEvent) error {
if err := EnrichRecords(e.HttpContext, api.app.Dao(), e.Records); err != nil && api.app.IsDebug() {
log.Println(err)
}
return e.HttpContext.JSON(http.StatusOK, e.Result)
})
}
@@ -124,21 +105,20 @@ func (api *recordApi) view(c echo.Context) error {
return NewNotFoundError("", "Missing collection context.")
}
admin, _ := c.Get(ContextAdminKey).(*models.Admin)
if admin == nil && collection.ViewRule == nil {
// only admins can access if the rule is nil
return NewForbiddenError("Only admins can perform this action.", nil)
}
recordId := c.PathParam("id")
if recordId == "" {
return NewNotFoundError("", nil)
}
requestData := exportRequestData(c)
requestData := GetRequestData(c)
if requestData.Admin == nil && collection.ViewRule == nil {
// only admins can access if the rule is nil
return NewForbiddenError("Only admins can perform this action.", nil)
}
ruleFunc := func(q *dbx.SelectQuery) error {
if admin == nil && collection.ViewRule != nil && *collection.ViewRule != "" {
if requestData.Admin == nil && collection.ViewRule != nil && *collection.ViewRule != "" {
resolver := resolvers.NewRecordFieldResolver(api.app.Dao(), collection, requestData, true)
expr, err := search.FilterData(*collection.ViewRule).BuildExpr(resolver)
if err != nil {
@@ -155,31 +135,16 @@ func (api *recordApi) view(c echo.Context) error {
return NewNotFoundError("", fetchErr)
}
// expand record relations
failed := api.app.Dao().ExpandRecord(
record,
strings.Split(c.QueryParam(expandQueryParam), ","),
expandFetch(api.app.Dao(), admin != nil, requestData),
)
if len(failed) > 0 && api.app.IsDebug() {
log.Println("Failed to expand relations: ", failed)
}
if collection.IsAuth() {
err := autoIgnoreAuthRecordsEmailVisibility(
api.app.Dao(), []*models.Record{record}, admin != nil, requestData,
)
if err != nil && api.app.IsDebug() {
log.Println("IgnoreEmailVisibility failure:", err)
}
}
event := &core.RecordViewEvent{
HttpContext: c,
Record: record,
}
return api.app.OnRecordViewRequest().Trigger(event, func(e *core.RecordViewEvent) error {
if err := EnrichRecord(e.HttpContext, api.app.Dao(), e.Record); err != nil && api.app.IsDebug() {
log.Println(err)
}
return e.HttpContext.JSON(http.StatusOK, e.Record)
})
}
@@ -190,18 +155,17 @@ func (api *recordApi) create(c echo.Context) error {
return NewNotFoundError("", "Missing collection context.")
}
admin, _ := c.Get(ContextAdminKey).(*models.Admin)
if admin == nil && collection.CreateRule == nil {
requestData := GetRequestData(c)
if requestData.Admin == nil && collection.CreateRule == nil {
// only admins can access if the rule is nil
return NewForbiddenError("Only admins can perform this action.", nil)
}
requestData := exportRequestData(c)
hasFullManageAccess := admin != nil
hasFullManageAccess := requestData.Admin != nil
// temporary save the record and check it against the create rule
if admin == nil && collection.CreateRule != nil {
if requestData.Admin == nil && collection.CreateRule != nil {
createRuleFunc := func(q *dbx.SelectQuery) error {
if *collection.CreateRule == "" {
return nil // no create rule to resolve
@@ -260,23 +224,8 @@ func (api *recordApi) create(c echo.Context) error {
return NewBadRequestError("Failed to create record.", err)
}
// expand record relations
failed := api.app.Dao().ExpandRecord(
e.Record,
strings.Split(e.HttpContext.QueryParam(expandQueryParam), ","),
expandFetch(api.app.Dao(), admin != nil, requestData),
)
if len(failed) > 0 && api.app.IsDebug() {
log.Println("Failed to expand relations: ", failed)
}
if collection.IsAuth() {
err := autoIgnoreAuthRecordsEmailVisibility(
api.app.Dao(), []*models.Record{e.Record}, admin != nil, requestData,
)
if err != nil && api.app.IsDebug() {
log.Println("IgnoreEmailVisibility failure:", err)
}
if err := EnrichRecord(e.HttpContext, api.app.Dao(), e.Record); err != nil && api.app.IsDebug() {
log.Println(err)
}
return e.HttpContext.JSON(http.StatusOK, e.Record)
@@ -297,21 +246,20 @@ func (api *recordApi) update(c echo.Context) error {
return NewNotFoundError("", "Missing collection context.")
}
admin, _ := c.Get(ContextAdminKey).(*models.Admin)
if admin == nil && collection.UpdateRule == nil {
// only admins can access if the rule is nil
return NewForbiddenError("Only admins can perform this action.", nil)
}
recordId := c.PathParam("id")
if recordId == "" {
return NewNotFoundError("", nil)
}
requestData := exportRequestData(c)
requestData := GetRequestData(c)
if requestData.Admin == nil && collection.UpdateRule == nil {
// only admins can access if the rule is nil
return NewForbiddenError("Only admins can perform this action.", nil)
}
ruleFunc := func(q *dbx.SelectQuery) error {
if admin == nil && collection.UpdateRule != nil && *collection.UpdateRule != "" {
if requestData.Admin == nil && collection.UpdateRule != nil && *collection.UpdateRule != "" {
resolver := resolvers.NewRecordFieldResolver(api.app.Dao(), collection, requestData, true)
expr, err := search.FilterData(*collection.UpdateRule).BuildExpr(resolver)
if err != nil {
@@ -330,7 +278,7 @@ func (api *recordApi) update(c echo.Context) error {
}
form := forms.NewRecordUpsert(api.app, record)
form.SetFullManageAccess(admin != nil || hasAuthManageAccess(api.app.Dao(), record, requestData))
form.SetFullManageAccess(requestData.Admin != nil || hasAuthManageAccess(api.app.Dao(), record, requestData))
// load request
if err := form.LoadRequest(c.Request(), ""); err != nil {
@@ -350,23 +298,8 @@ func (api *recordApi) update(c echo.Context) error {
return NewBadRequestError("Failed to update record.", err)
}
// expand record relations
failed := api.app.Dao().ExpandRecord(
e.Record,
strings.Split(e.HttpContext.QueryParam(expandQueryParam), ","),
expandFetch(api.app.Dao(), admin != nil, requestData),
)
if len(failed) > 0 && api.app.IsDebug() {
log.Println("Failed to expand relations: ", failed)
}
if collection.IsAuth() {
err := autoIgnoreAuthRecordsEmailVisibility(
api.app.Dao(), []*models.Record{e.Record}, admin != nil, requestData,
)
if err != nil && api.app.IsDebug() {
log.Println("IgnoreEmailVisibility failure:", err)
}
if err := EnrichRecord(e.HttpContext, api.app.Dao(), e.Record); err != nil && api.app.IsDebug() {
log.Println(err)
}
return e.HttpContext.JSON(http.StatusOK, e.Record)
@@ -387,21 +320,20 @@ func (api *recordApi) delete(c echo.Context) error {
return NewNotFoundError("", "Missing collection context.")
}
admin, _ := c.Get(ContextAdminKey).(*models.Admin)
if admin == nil && collection.DeleteRule == nil {
// only admins can access if the rule is nil
return NewForbiddenError("Only admins can perform this action.", nil)
}
recordId := c.PathParam("id")
if recordId == "" {
return NewNotFoundError("", nil)
}
requestData := exportRequestData(c)
requestData := GetRequestData(c)
if requestData.Admin == nil && collection.DeleteRule == nil {
// only admins can access if the rule is nil
return NewForbiddenError("Only admins can perform this action.", nil)
}
ruleFunc := func(q *dbx.SelectQuery) error {
if admin == nil && collection.DeleteRule != nil && *collection.DeleteRule != "" {
if requestData.Admin == nil && collection.DeleteRule != nil && *collection.DeleteRule != "" {
resolver := resolvers.NewRecordFieldResolver(api.app.Dao(), collection, requestData, true)
expr, err := search.FilterData(*collection.DeleteRule).BuildExpr(resolver)
if err != nil {
+65 -33
View File
@@ -2,6 +2,7 @@ package apis
import (
"fmt"
"strings"
"github.com/labstack/echo/v5"
"github.com/pocketbase/dbx"
@@ -10,45 +11,77 @@ import (
"github.com/pocketbase/pocketbase/resolvers"
"github.com/pocketbase/pocketbase/tools/rest"
"github.com/pocketbase/pocketbase/tools/search"
"github.com/spf13/cast"
)
// exportRequestData exports a map with common request fields.
//
// @todo consider changing the map to a typed struct after v0.8 and the
// IN operator support.
func exportRequestData(c echo.Context) map[string]any {
result := map[string]any{}
queryParams := map[string]any{}
bodyData := map[string]any{}
method := c.Request().Method
const ContextRequestDataKey = "requestData"
echo.BindQueryParams(c, &queryParams)
rest.BindBody(c, &bodyData)
result["method"] = method
result["query"] = queryParams
result["data"] = bodyData
result["auth"] = nil
auth, _ := c.Get(ContextAuthRecordKey).(*models.Record)
if auth != nil {
result["auth"] = auth.PublicExport()
// GetRequestData exports common request data fields
// (query, body, logged auth state, etc.) from the provided context.
func GetRequestData(c echo.Context) *models.FilterRequestData {
// return cached to avoid reading the body multiple times
if v := c.Get(ContextRequestDataKey); v != nil {
if data, ok := v.(*models.FilterRequestData); ok {
return data
}
}
result := &models.FilterRequestData{
Method: c.Request().Method,
Query: map[string]any{},
Data: map[string]any{},
}
result.AuthRecord, _ = c.Get(ContextAuthRecordKey).(*models.Record)
result.Admin, _ = c.Get(ContextAdminKey).(*models.Admin)
echo.BindQueryParams(c, &result.Query)
rest.BindBody(c, &result.Data)
c.Set(ContextRequestDataKey, result)
return result
}
// EnrichRecord parses the request context and enrich the provided record:
// - expands relations (if defaultExpands and/or ?expand query param is set)
// - ensures that the emails of the auth record and its expanded auth relations
// are visibe only for the current logged admin, record owner or record with manage access
func EnrichRecord(c echo.Context, dao *daos.Dao, record *models.Record, defaultExpands ...string) error {
return EnrichRecords(c, dao, []*models.Record{record}, defaultExpands...)
}
// EnrichRecords parses the request context and enriches the provided records:
// - expands relations (if defaultExpands and/or ?expand query param is set)
// - ensures that the emails of the auth records and their expanded auth relations
// are visibe only for the current logged admin, record owner or record with manage access
func EnrichRecords(c echo.Context, dao *daos.Dao, records []*models.Record, defaultExpands ...string) error {
requestData := GetRequestData(c)
if err := autoIgnoreAuthRecordsEmailVisibility(dao, records, requestData); err != nil {
return fmt.Errorf("Failed to resolve email visibility: %v", err)
}
expands := defaultExpands
expands = append(expands, strings.Split(c.QueryParam(expandQueryParam), ",")...)
if len(expands) == 0 {
return nil // nothing to expand
}
errs := dao.ExpandRecords(records, expands, expandFetch(dao, requestData))
if len(errs) > 0 {
return fmt.Errorf("Failed to expand: %v", errs)
}
return nil
}
// expandFetch is the records fetch function that is used to expand related records.
func expandFetch(
dao *daos.Dao,
isAdmin bool,
requestData map[string]any,
requestData *models.FilterRequestData,
) daos.ExpandFetchFunc {
return func(relCollection *models.Collection, relIds []string) ([]*models.Record, error) {
records, err := dao.FindRecordsByIds(relCollection.Id, relIds, func(q *dbx.SelectQuery) error {
if isAdmin {
if requestData.Admin != nil {
return nil // admins can access everything
}
@@ -70,7 +103,7 @@ func expandFetch(
})
if err == nil && len(records) > 0 {
autoIgnoreAuthRecordsEmailVisibility(dao, records, isAdmin, requestData)
autoIgnoreAuthRecordsEmailVisibility(dao, records, requestData)
}
return records, err
@@ -84,14 +117,13 @@ func expandFetch(
func autoIgnoreAuthRecordsEmailVisibility(
dao *daos.Dao,
records []*models.Record,
isAdmin bool,
requestData map[string]any,
requestData *models.FilterRequestData,
) error {
if len(records) == 0 || !records[0].Collection().IsAuth() {
return nil // nothing to check
}
if isAdmin {
if requestData.Admin != nil {
for _, rec := range records {
rec.IgnoreEmailVisibility(true)
}
@@ -107,8 +139,8 @@ func autoIgnoreAuthRecordsEmailVisibility(
recordIds = append(recordIds, rec.Id)
}
if auth, ok := requestData["auth"].(map[string]any); ok && mappedRecords[cast.ToString(auth["id"])] != nil {
mappedRecords[cast.ToString(auth["id"])].IgnoreEmailVisibility(true)
if requestData != nil && requestData.AuthRecord != nil && mappedRecords[requestData.AuthRecord.Id] != nil {
mappedRecords[requestData.AuthRecord.Id].IgnoreEmailVisibility(true)
}
authOptions := collection.AuthOptions()
@@ -153,7 +185,7 @@ func autoIgnoreAuthRecordsEmailVisibility(
func hasAuthManageAccess(
dao *daos.Dao,
record *models.Record,
requestData map[string]any,
requestData *models.FilterRequestData,
) bool {
if !record.Collection().IsAuth() {
return false
@@ -165,7 +197,7 @@ func hasAuthManageAccess(
return false // only for admins (manageRule can't be empty)
}
if auth, ok := requestData["auth"].(map[string]any); !ok || cast.ToString(auth["id"]) == "" {
if requestData == nil || requestData.AuthRecord == nil {
return false // no auth record
}
+101
View File
@@ -0,0 +1,101 @@
package apis_test
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/labstack/echo/v5"
"github.com/pocketbase/pocketbase/apis"
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/tests"
)
func TestGetRequestData(t *testing.T) {
e := echo.New()
req := httptest.NewRequest(http.MethodPost, "/?test=123", strings.NewReader(`{"test":456}`))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
dummyRecord := &models.Record{}
dummyRecord.Id = "id1"
c.Set(apis.ContextAuthRecordKey, dummyRecord)
dummyAdmin := &models.Admin{}
dummyAdmin.Id = "id2"
c.Set(apis.ContextAdminKey, dummyAdmin)
result := apis.GetRequestData(c)
if result == nil {
t.Fatal("Expected *models.FilterRequestData instance, got nil")
}
if result.Method != http.MethodPost {
t.Fatalf("Expected Method %v, got %v", http.MethodPost, result.Method)
}
rawQuery, _ := json.Marshal(result.Query)
expectedQuery := `{"test":"123"}`
if v := string(rawQuery); v != expectedQuery {
t.Fatalf("Expected Query %v, got %v", expectedQuery, v)
}
rawData, _ := json.Marshal(result.Data)
expectedData := `{"test":456}`
if v := string(rawData); v != expectedData {
t.Fatalf("Expected Data %v, got %v", expectedData, v)
}
if result.AuthRecord == nil || result.AuthRecord.Id != dummyRecord.Id {
t.Fatalf("Expected AuthRecord %v, got %v", dummyRecord, result.AuthRecord)
}
if result.Admin == nil || result.Admin.Id != dummyAdmin.Id {
t.Fatalf("Expected Admin %v, got %v", dummyAdmin, result.Admin)
}
}
func TestEnrichRecords(t *testing.T) {
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/?expand=rel_many", nil)
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
dummyAdmin := &models.Admin{}
dummyAdmin.Id = "test_id"
c.Set(apis.ContextAdminKey, dummyAdmin)
app, _ := tests.NewTestApp()
defer app.Cleanup()
records, err := app.Dao().FindRecordsByIds("demo1", []string{"al1h9ijdeojtsjy", "84nmscqy84lsi1t"})
if err != nil {
t.Fatal(err)
}
apis.EnrichRecords(c, app.Dao(), records, "rel_one")
for _, record := range records {
expand := record.Expand()
if len(expand) == 0 {
t.Fatalf("Expected non-empty expand, got nil for record %v", record)
}
if len(record.GetStringSlice("rel_one")) != 0 {
if _, ok := expand["rel_one"]; !ok {
t.Fatalf("Expected rel_one to be expanded for record %v, got \n%v", record, expand)
}
}
if len(record.GetStringSlice("rel_many")) != 0 {
if _, ok := expand["rel_many"]; !ok {
t.Fatalf("Expected rel_many to be expanded for record %v, got \n%v", record, expand)
}
}
}
}