[#2271] added dao.CanAccessRecord() helper
This commit is contained in:
parent
e99b0627d6
commit
ec303a60ed
|
@ -1,4 +1,3 @@
|
||||||
<<<<<<< HEAD
|
|
||||||
## v0.17.0-WIP
|
## v0.17.0-WIP
|
||||||
|
|
||||||
- Added Instagram OAuth2 provider ([#2534](https://github.com/pocketbase/pocketbase/pull/2534); thanks @pnmcosta).
|
- Added Instagram OAuth2 provider ([#2534](https://github.com/pocketbase/pocketbase/pull/2534); thanks @pnmcosta).
|
||||||
|
@ -26,10 +25,11 @@
|
||||||
|
|
||||||
- Added `subscriptions.Client.Unset()` helper to remove a single cached item from the client store.
|
- Added `subscriptions.Client.Unset()` helper to remove a single cached item from the client store.
|
||||||
|
|
||||||
- (@todo docs) Added query by filter record `Dao` helpers:
|
- (@todo docs) Added rule and filter record `Dao` helpers:
|
||||||
```
|
```
|
||||||
app.Dao().FindRecordsByFilter("posts", "title ~ 'lorem ipsum' && visible = true", "-created", 10)
|
app.Dao().FindRecordsByFilter("posts", "title ~ 'lorem ipsum' && visible = true", "-created", 10)
|
||||||
app.Dao().FindFirstRecordByFilter("posts", "slug='test' && active=true")
|
app.Dao().FindFirstRecordByFilter("posts", "slug='test' && active=true")
|
||||||
|
app.Dao().CanAccessRecord(record, requestData, rule)
|
||||||
```
|
```
|
||||||
|
|
||||||
- (@todo docs) Added `Dao.WithoutHooks()` helper to create a new `Dao` from the current one but without the create/update/delete hooks.
|
- (@todo docs) Added `Dao.WithoutHooks()` helper to create a new `Dao` from the current one but without the create/update/delete hooks.
|
||||||
|
|
|
@ -475,6 +475,57 @@ func (dao *Dao) SuggestUniqueAuthRecordUsername(
|
||||||
return username
|
return username
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CanAccessRecord checks if a record is allowed to be accessed by the
|
||||||
|
// specified requestData and accessRule.
|
||||||
|
//
|
||||||
|
// Always return false on invalid access rule or db error.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// requestData := apis.RequestData(c /* echo.Context */)
|
||||||
|
// record, _ := dao.FindRecordById("example", "RECORD_ID")
|
||||||
|
// // custom rule
|
||||||
|
// // or use one of the record collection's, eg. record.Collection().ViewRule
|
||||||
|
// rule := types.Pointer("@request.auth.id != '' || status = 'public'")
|
||||||
|
//
|
||||||
|
// canAccess := dao.CanAccessRecord(record, requestData, rule)
|
||||||
|
func (dao *Dao) CanAccessRecord(record *models.Record, requestData *models.RequestData, accessRule *string) bool {
|
||||||
|
if requestData.Admin != nil {
|
||||||
|
// admins can access everything
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if accessRule == nil {
|
||||||
|
// only admins can access this record
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if *accessRule == "" {
|
||||||
|
return true // empty public rule, aka. everyone can access
|
||||||
|
}
|
||||||
|
|
||||||
|
var exists bool
|
||||||
|
|
||||||
|
query := dao.RecordQuery(record.Collection()).
|
||||||
|
Select("(1)").
|
||||||
|
AndWhere(dbx.HashExp{record.Collection().Name + ".id": record.Id})
|
||||||
|
|
||||||
|
// parse and apply the access rule filter
|
||||||
|
resolver := resolvers.NewRecordFieldResolver(dao, record.Collection(), requestData, true)
|
||||||
|
expr, err := search.FilterData(*accessRule).BuildExpr(resolver)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
resolver.UpdateQuery(query)
|
||||||
|
query.AndWhere(expr)
|
||||||
|
|
||||||
|
if err := query.Limit(1).Row(&exists); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return exists
|
||||||
|
}
|
||||||
|
|
||||||
// SaveRecord persists the provided Record model in the database.
|
// SaveRecord persists the provided Record model in the database.
|
||||||
//
|
//
|
||||||
// If record.IsNew() is true, the method will perform a create, otherwise an update.
|
// If record.IsNew() is true, the method will perform a create, otherwise an update.
|
||||||
|
|
|
@ -582,6 +582,129 @@ func TestFindFirstRecordByFilter(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCanAccessRecord(t *testing.T) {
|
||||||
|
app, _ := tests.NewTestApp()
|
||||||
|
defer app.Cleanup()
|
||||||
|
|
||||||
|
admin, err := app.Dao().FindAdminByEmail("test@example.com")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
authRecord, err := app.Dao().FindAuthRecordByEmail("users", "test@example.com")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
record, err := app.Dao().FindRecordById("demo1", "imy661ixudk5izi")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
scenarios := []struct {
|
||||||
|
name string
|
||||||
|
record *models.Record
|
||||||
|
requestData *models.RequestData
|
||||||
|
rule *string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"as admin with nil rule",
|
||||||
|
record,
|
||||||
|
&models.RequestData{
|
||||||
|
Admin: admin,
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"as admin with non-empty rule",
|
||||||
|
record,
|
||||||
|
&models.RequestData{
|
||||||
|
Admin: admin,
|
||||||
|
},
|
||||||
|
types.Pointer("id = ''"), // the filter rule should be ignored
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"as guest with nil rule",
|
||||||
|
record,
|
||||||
|
&models.RequestData{},
|
||||||
|
nil,
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"as guest with empty rule",
|
||||||
|
nil,
|
||||||
|
&models.RequestData{},
|
||||||
|
types.Pointer(""),
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"as guest with mismatched rule",
|
||||||
|
record,
|
||||||
|
&models.RequestData{},
|
||||||
|
types.Pointer("@request.auth.id != ''"),
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"as guest with matched rule",
|
||||||
|
record,
|
||||||
|
&models.RequestData{
|
||||||
|
Data: map[string]any{"test": 1},
|
||||||
|
},
|
||||||
|
types.Pointer("@request.auth.id != '' || @request.data.test = 1"),
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"as auth record with nil rule",
|
||||||
|
record,
|
||||||
|
&models.RequestData{
|
||||||
|
AuthRecord: authRecord,
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"as auth record with empty rule",
|
||||||
|
nil,
|
||||||
|
&models.RequestData{
|
||||||
|
AuthRecord: authRecord,
|
||||||
|
},
|
||||||
|
types.Pointer(""),
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"as auth record with mismatched rule",
|
||||||
|
record,
|
||||||
|
&models.RequestData{
|
||||||
|
AuthRecord: authRecord,
|
||||||
|
Data: map[string]any{"test": 1},
|
||||||
|
},
|
||||||
|
types.Pointer("@request.auth.id != '' && @request.data.test > 1"),
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"as auth record with matched rule",
|
||||||
|
record,
|
||||||
|
&models.RequestData{
|
||||||
|
AuthRecord: authRecord,
|
||||||
|
Data: map[string]any{"test": 2},
|
||||||
|
},
|
||||||
|
types.Pointer("@request.auth.id != '' && @request.data.test > 1"),
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range scenarios {
|
||||||
|
result := app.Dao().CanAccessRecord(s.record, s.requestData, s.rule)
|
||||||
|
|
||||||
|
if result != s.expected {
|
||||||
|
t.Errorf("[%s] Expected %v, got %v", s.name, s.expected, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestIsRecordValueUnique(t *testing.T) {
|
func TestIsRecordValueUnique(t *testing.T) {
|
||||||
app, _ := tests.NewTestApp()
|
app, _ := tests.NewTestApp()
|
||||||
defer app.Cleanup()
|
defer app.Cleanup()
|
||||||
|
|
Loading…
Reference in New Issue