Skip to content
This repository has been archived by the owner on Dec 22, 2023. It is now read-only.

Commit

Permalink
Make default ACL server-based
Browse files Browse the repository at this point in the history
Ref. #309

This commit moves the default ACL to server instead of on SDK level.
In addition, this commit supports different default ACL for each
record type.
  • Loading branch information
Ben Lei committed Apr 7, 2017
2 parents 23eb420 + 8a7f5be commit 57d014c
Show file tree
Hide file tree
Showing 13 changed files with 390 additions and 12 deletions.
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ func main() {
r.Map("schema:create", injector.Inject(&handler.SchemaCreateHandler{}))
r.Map("schema:fetch", injector.Inject(&handler.SchemaFetchHandler{}))
r.Map("schema:access", injector.Inject(&handler.SchemaAccessHandler{}))
r.Map("schema:default_access", injector.Inject(&handler.SchemaDefaultAccessHandler{}))

serveMux.Handle("/", r)

Expand Down
4 changes: 4 additions & 0 deletions pkg/server/handler/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,10 @@ func (conn *singleUserConn) GetRecordAccess(recordType string) (skydb.RecordACL,
return skydb.NewRecordACL([]skydb.RecordACLEntry{}), nil
}

func (conn *singleUserConn) GetRecordDefaultAccess(recordType string) (skydb.RecordACL, error) {
return nil, nil
}

func TestSignupHandlerAsAnonymous(t *testing.T) {
Convey("SignupHandler", t, func() {
tokenStore := authtokentest.SingleTokenStore{}
Expand Down
7 changes: 7 additions & 0 deletions pkg/server/handler/record_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,10 @@ func (db bogusFieldDatabaseConnection) GetRecordAccess(recordType string) (skydb
return skydb.NewRecordACL([]skydb.RecordACLEntry{}), nil
}

func (db bogusFieldDatabaseConnection) GetRecordDefaultAccess(recordType string) (skydb.RecordACL, error) {
return nil, nil
}

type bogusFieldDatabase struct {
SaveFunc func(record *skydb.Record) error
GetFunc func(id skydb.RecordID, record *skydb.Record) error
Expand Down Expand Up @@ -1295,9 +1299,11 @@ func TestRecordOwnerIDSerialization(t *testing.T) {
record: record,
recordSchema: skydb.RecordSchema{},
}
conn := skydbtest.NewMapConn()

injectDBFunc := func(payload *router.Payload) {
payload.Database = db
payload.DBConn = conn
payload.UserInfo = &skydb.UserInfo{
ID: "ownerID",
}
Expand Down Expand Up @@ -1928,6 +1934,7 @@ func TestHookExecution(t *testing.T) {

r := handlertest.NewSingleRouteRouter(test.handler, func(p *router.Payload) {
p.Database = db
p.DBConn = skydbtest.NewMapConn()
p.UserInfo = &skydb.UserInfo{
ID: "user0",
}
Expand Down
25 changes: 25 additions & 0 deletions pkg/server/handler/recordutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ type recordFetcher struct {
conn skydb.Conn
withMasterKey bool
creationAccessCacheMap map[string]skydb.RecordACL
defaultAccessCacheMap map[string]skydb.RecordACL
}

func newRecordFetcher(db skydb.Database, conn skydb.Conn, withMasterKey bool) recordFetcher {
Expand All @@ -116,6 +117,7 @@ func newRecordFetcher(db skydb.Database, conn skydb.Conn, withMasterKey bool) re
conn: conn,
withMasterKey: withMasterKey,
creationAccessCacheMap: map[string]skydb.RecordACL{},
defaultAccessCacheMap: map[string]skydb.RecordACL{},
}
}

Expand All @@ -133,6 +135,20 @@ func (f recordFetcher) getCreationAccess(recordType string) skydb.RecordACL {
return creationAccess
}

func (f recordFetcher) getDefaultAccess(recordType string) skydb.RecordACL {
defaultAccess, defaultAccessCached := f.defaultAccessCacheMap[recordType]
if defaultAccessCached == false {
var err error
defaultAccess, err = f.conn.GetRecordDefaultAccess(recordType)

if err == nil && defaultAccess != nil {
f.defaultAccessCacheMap[recordType] = defaultAccess
}
}

return defaultAccess
}

func (f recordFetcher) fetchOrCreateRecord(recordID skydb.RecordID, userInfo *skydb.UserInfo) (record *skydb.Record, err skyerr.Error) {
dbRecord := skydb.Record{}
if dbErr := f.db.Get(recordID, &dbRecord); dbErr != nil {
Expand Down Expand Up @@ -209,6 +225,15 @@ func recordSaveHandler(req *recordModifyRequest, resp *recordModifyResponse) sky
return
})

// Apply default access
records = executeRecordFunc(records, resp.ErrMap, func(record *skydb.Record) skyerr.Error {
if record.ACL == nil {
defaultACL := fetcher.getDefaultAccess(record.ID.Type)
record.ACL = defaultACL
}
return nil
})

// execute before save hooks
if req.HookRegistry != nil {
records = executeRecordFunc(records, resp.ErrMap, func(record *skydb.Record) (err skyerr.Error) {
Expand Down
102 changes: 102 additions & 0 deletions pkg/server/handler/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
pluginEvent "github.com/skygeario/skygear-server/pkg/server/plugin/event"
"github.com/skygeario/skygear-server/pkg/server/router"
"github.com/skygeario/skygear-server/pkg/server/skydb"
"github.com/skygeario/skygear-server/pkg/server/skydb/skyconv"
"github.com/skygeario/skygear-server/pkg/server/skyerr"
)

Expand Down Expand Up @@ -497,3 +498,104 @@ func (h *SchemaAccessHandler) Handle(rpayload *router.Payload, response *router.
CreateRoles: payload.RawCreateRoles,
}
}

/*
SchemaDefaultAccessHandler handles the update of creation access of record
curl -X POST -H "Content-Type: application/json" \
-d @- http://localhost:3000/schema/default_access <<EOF
{
"master_key": "MASTER_KEY",
"action": "schema:access",
"type": "note",
"default_access": [
{"public": true, "level": "write"}
]
}
EOF
*/
type SchemaDefaultAccessHandler struct {
AccessKey router.Processor `preprocessor:"accesskey"`
DevOnly router.Processor `preprocessor:"dev_only"`
DBConn router.Processor `preprocessor:"dbconn"`
InjectDB router.Processor `preprocessor:"inject_db"`
PluginReady router.Processor `preprocessor:"plugin_ready"`
preprocessors []router.Processor
}

type schemaDefaultAccessPayload struct {
Type string `mapstructure:"type"`
RawDefaultAccess []map[string]interface{} `mapstructure:"default_access"`
ACL skydb.RecordACL
}

type schemaDefaultAccessResponse struct {
Type string `json:"type"`
DefaultAccess []map[string]interface{} `json:"default_access,omitempty"`
}

func (h *SchemaDefaultAccessHandler) Setup() {
h.preprocessors = []router.Processor{
h.AccessKey,
h.DevOnly,
h.DBConn,
h.InjectDB,
h.PluginReady,
}
}

func (h *SchemaDefaultAccessHandler) GetPreprocessors() []router.Processor {
return h.preprocessors
}

func (payload *schemaDefaultAccessPayload) Decode(data map[string]interface{}) skyerr.Error {
if err := mapstructure.Decode(data, payload); err != nil {
return skyerr.NewError(skyerr.BadRequest, "fails to decode the request payload")
}

acl := skydb.RecordACL{}
for _, v := range payload.RawDefaultAccess {
ace := skydb.RecordACLEntry{}
if err := (*skyconv.MapACLEntry)(&ace).FromMap(v); err != nil {
return skyerr.NewInvalidArgument("invalid default_access entry", []string{"default_access"})
}
acl = append(acl, ace)
}

payload.ACL = acl

return payload.Validate()
}

func (payload *schemaDefaultAccessPayload) Validate() skyerr.Error {
if payload.Type == "" {
return skyerr.NewInvalidArgument("missing required fields", []string{"type"})
}

return nil
}

func (h *SchemaDefaultAccessHandler) Handle(rpayload *router.Payload, response *router.Response) {
payload := schemaDefaultAccessPayload{}
skyErr := payload.Decode(rpayload.Data)
if skyErr != nil {
response.Err = skyErr
return
}

c := rpayload.Database.Conn()
err := c.SetRecordDefaultAccess(payload.Type, payload.ACL)

if err != nil {
if skyErr, isSkyErr := err.(skyerr.Error); isSkyErr {
response.Err = skyErr
} else {
response.Err = skyerr.MakeError(err)
}
return
}

response.Result = schemaDefaultAccessResponse{
Type: payload.Type,
DefaultAccess: payload.RawDefaultAccess,
}
}
122 changes: 122 additions & 0 deletions pkg/server/handler/schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -799,3 +799,125 @@ func TestSchemaAccessHandler(t *testing.T) {
So(roleNames, ShouldContain, "Writer")
})
}

func TestSchemaDefaultAccessPayload(t *testing.T) {
Convey("SchemaDefaultAccessPayload", t, func() {
Convey("Valid Data", func() {
payload := schemaDefaultAccessPayload{}
skyErr := payload.Decode(map[string]interface{}{
"action": "schema:default_access",
"type": "note",
"default_access": []interface{}{
map[string]interface{}{
"public": true,
"level": "read",
},
map[string]interface{}{
"role": "admin",
"level": "write",
},
},
})

So(skyErr, ShouldBeNil)
So(payload.Validate(), ShouldBeNil)

admin := skydb.UserInfo{
Roles: []string{"admin"},
}
So(payload.ACL.Accessible(&admin, skydb.WriteLevel), ShouldEqual, true)
So(payload.ACL.Accessible(nil, skydb.ReadLevel), ShouldEqual, true)
})

Convey("Invalid Data", func() {
payload := schemaDefaultAccessPayload{}
err := payload.Decode(map[string]interface{}{
"action": "schema:default_access",
"default_access": []interface{}{
map[string]interface{}{
"public": true,
"level": "read",
},
map[string]interface{}{
"role": "admin",
"level": "write",
},
},
})

So(err, ShouldResemble,
skyerr.NewInvalidArgument("missing required fields", []string{"type"}))

err = payload.Decode(map[string]interface{}{
"action": "schema:default_access",
"type": "script",
"default_access": "read",
})

So(err, ShouldResemble,
skyerr.NewError(skyerr.BadRequest, "fails to decode the request payload"))
})
})
}

type mockSchemaDefaultAccessDatabase struct {
DBConn skydb.Conn

skydb.Database
}

func (db *mockSchemaDefaultAccessDatabase) Conn() skydb.Conn {
return db.DBConn
}

type mockSchemaDefaultAccessDatabaseConnection struct {
recordType string
acl skydb.RecordACL

skydb.Conn
}

func (c *mockSchemaDefaultAccessDatabaseConnection) SetRecordDefaultAccess(recordType string, acl skydb.RecordACL) error {
c.recordType = recordType
c.acl = acl

return nil
}

func TestSchemaDefaultAccessHandler(t *testing.T) {
Convey("TestSchemaDefaultAccessHandler", t, func() {
mockConn := &mockSchemaDefaultAccessDatabaseConnection{}
mockDB := &mockSchemaDefaultAccessDatabase{}
mockDB.DBConn = mockConn

handler := handlertest.NewSingleRouteRouter(&SchemaDefaultAccessHandler{}, func(p *router.Payload) {
p.Database = mockDB
})

resp := handler.POST(`{
"type": "script",
"default_access": [
{"public": true, "level": "read"},
{"role": "admin", "level": "write"}
]
}`)

So(resp.Body.Bytes(), ShouldEqualJSON, `{
"result": {
"type": "script",
"default_access": [
{"public": true, "level": "read"},
{"role": "admin", "level": "write"}
]
}
}`)

So(mockConn.recordType, ShouldEqual, "script")

admin := skydb.UserInfo{
Roles: []string{"admin"},
}
So(mockConn.acl.Accessible(&admin, skydb.WriteLevel), ShouldEqual, true)
So(mockConn.acl.Accessible(nil, skydb.ReadLevel), ShouldEqual, true)
})
}
8 changes: 7 additions & 1 deletion pkg/server/skydb/conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,15 @@ type Conn interface {
// SetRecordAccess sets default record access of a specific type
SetRecordAccess(recordType string, acl RecordACL) error

// GetRecordAccess returns default record access of a specific type
// SetRecordDefaultAccess sets default record access of a specific type
SetRecordDefaultAccess(recordType string, acl RecordACL) error

// GetRecordAccess returns the record creation access of a specific type
GetRecordAccess(recordType string) (RecordACL, error)

// GetRecordDefaultAccess returns default record access of a specific type
GetRecordDefaultAccess(recordType string) (RecordACL, error)

// GetAsset retrieves Asset information by its name
GetAsset(name string, asset *Asset) error

Expand Down
13 changes: 13 additions & 0 deletions pkg/server/skydb/mock_skydb/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,13 @@ func (_m *MockConn) GetRecordAccess(_param0 string) (skydb.RecordACL, error) {
return ret0, ret1
}

func (_m *MockConn) GetRecordDefaultAccess(_param0 string) (skydb.RecordACL, error) {
ret := _m.ctrl.Call(_m, "GetRecordDefaultAccess", _param0)
ret0, _ := ret[0].(skydb.RecordACL)
ret1, _ := ret[1].(error)
return ret0, ret1
}

func (_mr *_MockConnRecorder) GetRecordAccess(arg0 interface{}) *gomock.Call {
return _mr.mock.ctrl.RecordCall(_mr.mock, "GetRecordAccess", arg0)
}
Expand Down Expand Up @@ -324,6 +331,12 @@ func (_m *MockConn) SetRecordAccess(_param0 string, _param1 skydb.RecordACL) err
return ret0
}

func (_m *MockConn) SetRecordDefaultAccess(_param0 string, _param1 skydb.RecordACL) error {
ret := _m.ctrl.Call(_m, "SetRecordDefaultAccess", _param0, _param1)
ret0, _ := ret[0].(error)
return ret0
}

func (_mr *_MockConnRecorder) SetRecordAccess(arg0, arg1 interface{}) *gomock.Call {
return _mr.mock.ctrl.RecordCall(_mr.mock, "SetRecordAccess", arg0, arg1)
}
Expand Down
Loading

0 comments on commit 57d014c

Please sign in to comment.