diff --git a/daos/record.go b/daos/record.go index 8013ec0d..e2909cd7 100644 --- a/daos/record.go +++ b/daos/record.go @@ -230,7 +230,7 @@ func (dao *Dao) FindFirstRecordByData( // If the limit argument is <= 0, no limit is applied to the query and // all matching records are returned. // -// NB Don't put untrusted user input in the filter string as it +// NB! Don't put untrusted user input in the filter string as it // practically would allow the users to inject their own custom filter. // // Example: @@ -294,7 +294,7 @@ func (dao *Dao) FindRecordsByFilter( // FindFirstRecordByFilter returns the first available record matching the provided filter. // -// NB Don't put untrusted user input in the filter string as it +// NB! Don't put untrusted user input in the filter string as it // practically would allow the users to inject their own custom filter. // // Example: @@ -484,30 +484,35 @@ func (dao *Dao) SuggestUniqueAuthRecordUsername( // 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. +// Rule and db checks are ignored in case requestData.Admin is set. +// +// The returned error indicate that something unexpected happened during +// the check (eg. invalid rule or db error). +// +// The method 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'") +// // ... or use one of the record collection's rule, eg. record.Collection().ViewRule // -// canAccess := dao.CanAccessRecord(record, requestData, rule) -func (dao *Dao) CanAccessRecord(record *models.Record, requestData *models.RequestData, accessRule *string) bool { +// if ok, _ := dao.CanAccessRecord(record, requestData, rule); ok { ... } +func (dao *Dao) CanAccessRecord(record *models.Record, requestData *models.RequestData, accessRule *string) (bool, error) { if requestData.Admin != nil { // admins can access everything - return true + return true, nil } if accessRule == nil { // only admins can access this record - return false + return false, nil } if *accessRule == "" { - return true // empty public rule, aka. everyone can access + // empty public rule, aka. everyone can access + return true, nil } var exists bool @@ -520,16 +525,16 @@ func (dao *Dao) CanAccessRecord(record *models.Record, requestData *models.Reque resolver := resolvers.NewRecordFieldResolver(dao, record.Collection(), requestData, true) expr, err := search.FilterData(*accessRule).BuildExpr(resolver) if err != nil { - return false + return false, err } resolver.UpdateQuery(query) query.AndWhere(expr) - if err := query.Limit(1).Row(&exists); err != nil { - return false + if err := query.Limit(1).Row(&exists); err != nil && !errors.Is(err, sql.ErrNoRows) { + return false, err } - return exists + return exists, nil } // SaveRecord persists the provided Record model in the database. diff --git a/daos/record_test.go b/daos/record_test.go index cf18368b..a9b8f8e3 100644 --- a/daos/record_test.go +++ b/daos/record_test.go @@ -607,6 +607,7 @@ func TestCanAccessRecord(t *testing.T) { requestData *models.RequestData rule *string expected bool + expectError bool }{ { "as admin with nil rule", @@ -616,6 +617,7 @@ func TestCanAccessRecord(t *testing.T) { }, nil, true, + false, }, { "as admin with non-empty rule", @@ -625,6 +627,17 @@ func TestCanAccessRecord(t *testing.T) { }, types.Pointer("id = ''"), // the filter rule should be ignored true, + false, + }, + { + "as admin with invalid rule", + record, + &models.RequestData{ + Admin: admin, + }, + types.Pointer("id ?!@ 1"), // the filter rule should be ignored + true, + false, }, { "as guest with nil rule", @@ -632,13 +645,23 @@ func TestCanAccessRecord(t *testing.T) { &models.RequestData{}, nil, false, + false, }, { "as guest with empty rule", - nil, + record, &models.RequestData{}, types.Pointer(""), true, + false, + }, + { + "as guest with invalid rule", + record, + &models.RequestData{}, + types.Pointer("id ?!@ 1"), + false, + true, }, { "as guest with mismatched rule", @@ -646,6 +669,7 @@ func TestCanAccessRecord(t *testing.T) { &models.RequestData{}, types.Pointer("@request.auth.id != ''"), false, + false, }, { "as guest with matched rule", @@ -655,6 +679,7 @@ func TestCanAccessRecord(t *testing.T) { }, types.Pointer("@request.auth.id != '' || @request.data.test = 1"), true, + false, }, { "as auth record with nil rule", @@ -664,15 +689,27 @@ func TestCanAccessRecord(t *testing.T) { }, nil, false, + false, }, { "as auth record with empty rule", - nil, + record, &models.RequestData{ AuthRecord: authRecord, }, types.Pointer(""), true, + false, + }, + { + "as auth record with invalid rule", + record, + &models.RequestData{ + AuthRecord: authRecord, + }, + types.Pointer("id ?!@ 1"), + false, + true, }, { "as auth record with mismatched rule", @@ -683,6 +720,7 @@ func TestCanAccessRecord(t *testing.T) { }, types.Pointer("@request.auth.id != '' && @request.data.test > 1"), false, + false, }, { "as auth record with matched rule", @@ -693,15 +731,21 @@ func TestCanAccessRecord(t *testing.T) { }, types.Pointer("@request.auth.id != '' && @request.data.test > 1"), true, + false, }, } for _, s := range scenarios { - result := app.Dao().CanAccessRecord(s.record, s.requestData, s.rule) + result, err := 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) } + + hasErr := err != nil + if hasErr != s.expectError { + t.Errorf("[%s] Expected hasErr %v, got %v (%v)", s.name, s.expectError, hasErr, err) + } } } diff --git a/go.mod b/go.mod index c9bf0c4b..0bf5d2a8 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.18 require ( github.com/AlecAivazis/survey/v2 v2.3.7 - github.com/aws/aws-sdk-go v1.44.298 + github.com/aws/aws-sdk-go v1.44.299 github.com/disintegration/imaging v1.6.2 github.com/domodwyer/mailyak/v3 v3.6.0 github.com/dop251/goja v0.0.0-20230707174833-636fdf960de1 @@ -79,7 +79,7 @@ require ( golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/api v0.130.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20230706204954-ccb25ca9f130 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230710151506-e685fd7b542b // indirect google.golang.org/grpc v1.56.2 // indirect google.golang.org/protobuf v1.31.0 // indirect lukechampine.com/uint128 v1.3.0 // indirect diff --git a/go.sum b/go.sum index f9dbb11a..a3aa5fef 100644 --- a/go.sum +++ b/go.sum @@ -773,8 +773,8 @@ github.com/aws/aws-sdk-go v1.43.31/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4 github.com/aws/aws-sdk-go v1.44.156/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= github.com/aws/aws-sdk-go v1.44.245/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= github.com/aws/aws-sdk-go v1.44.284/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= -github.com/aws/aws-sdk-go v1.44.298 h1:5qTxdubgV7PptZJmp/2qDwD2JL187ePL7VOxsSh1i3g= -github.com/aws/aws-sdk-go v1.44.298/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/aws/aws-sdk-go v1.44.299 h1:HVD9lU4CAFHGxleMJp95FV/sRhtg7P4miHD1v88JAQk= +github.com/aws/aws-sdk-go v1.44.299/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= github.com/aws/aws-sdk-go-v2 v1.9.1/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4= github.com/aws/aws-sdk-go-v2 v1.18.1 h1:+tefE750oAb7ZQGzla6bLkOwfcQCEtC5y2RqoqCeqKo= github.com/aws/aws-sdk-go-v2 v1.18.1/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= @@ -3091,7 +3091,7 @@ google.golang.org/genproto v0.0.0-20230403163135-c38d8f061ccd/go.mod h1:UUQDJDOl google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= google.golang.org/genproto v0.0.0-20230525234025-438c736192d0/go.mod h1:9ExIQyXL5hZrHzQceCwuSYwZZ5QZBazOcprJ5rgs3lY= google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:xZnkP7mREFX5MORlOPEzLMr+90PPZQ2QWzrVTWfAq64= -google.golang.org/genproto v0.0.0-20230629202037-9506855d4529 h1:9JucMWR7sPvCxUFd6UsOUNmA5kCcWOfORaT3tpAsKQs= +google.golang.org/genproto v0.0.0-20230706204954-ccb25ca9f130 h1:Au6te5hbKUV8pIYWHqOUZ1pva5qK/rwbIhoXEUB9Lu8= google.golang.org/genproto/googleapis/api v0.0.0-20230525234020-1aefcd67740a/go.mod h1:ts19tUU+Z0ZShN1y3aPyq2+O3d5FUNNgT6FtOzmrNn8= google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= google.golang.org/genproto/googleapis/api v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= @@ -3102,8 +3102,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234015-3fc162c6f38a/go. google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= google.golang.org/genproto/googleapis/rpc v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230706204954-ccb25ca9f130 h1:2FZP5XuJY9zQyGM5N0rtovnoXjiMUEIUMvw0m9wlpLc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230706204954-ccb25ca9f130/go.mod h1:8mL13HKkDa+IuJ8yruA3ci0q+0vsUz4m//+ottjwS5o= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230710151506-e685fd7b542b h1:BC7Q0uXfp6VFXnNWp5RqATIN/viqCGkqBO8+HxzH/jY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230710151506-e685fd7b542b/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM= google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=