diff --git a/tinytodo-go/README.md b/tinytodo-go/README.md index 4de0178..12f20e7 100644 --- a/tinytodo-go/README.md +++ b/tinytodo-go/README.md @@ -22,6 +22,6 @@ You need Python3 and Go (1.22 or later). See [TinyTodo's README](../tinytodo/README.md) for more information. -## Comparison with TinyTodo +## Comparison with [TinyTodo](../tinytodo) -TinyTodo-Go is constrained by the features of [`cedar-go`](https://github.com/cedar-policy/cedar-go). Refer to [this README](https://github.com/cedar-policy/cedar-go?tab=readme-ov-file#comparison-to-the-rust-implementation) to learn about the missing features. \ No newline at end of file +TinyTodo-Go relies on [v0.3.2 of `cedar-go`](https://github.com/cedar-policy/cedar-go/releases/tag/v0.3.2). Refer to [this README](https://github.com/cedar-policy/cedar-go/tree/v0.3.2?tab=readme-ov-file#comparison-to-the-rust-implementation) to learn about the features that `cedar-go` is missing in comparison to [`cedar`](https://github.com/cedar-policy/cedar). \ No newline at end of file diff --git a/tinytodo-go/cmd/server/authorization.go b/tinytodo-go/cmd/server/authorization.go index b9a7a94..bb63695 100644 --- a/tinytodo-go/cmd/server/authorization.go +++ b/tinytodo-go/cmd/server/authorization.go @@ -12,7 +12,7 @@ const ( DefaultEntitiesFileName = "entities.json" // this is not in the Cedar entity schema, conversion required ) -func prepareCedarPolicyEntities() (*entitystore.EntityStore, cedar.PolicySet, error) { +func prepareCedarPolicyEntities() (*entitystore.EntityStore, *cedar.PolicySet, error) { entitiesFile, err := os.ReadFile(DefaultEntitiesFileName) if err != nil { @@ -29,7 +29,7 @@ func prepareCedarPolicyEntities() (*entitystore.EntityStore, cedar.PolicySet, er return nil, nil, fmt.Errorf("failed to read Cedar policy file: %w", err) } - ps, err := cedar.NewPolicySet(DefaultCedarPolicyFileName, psFile) + ps, err := cedar.NewPolicySetFromBytes(DefaultCedarPolicyFileName, psFile) if err != nil { return nil, nil, fmt.Errorf("failed to create Cedar policy set: %w", err) } diff --git a/tinytodo-go/go.mod b/tinytodo-go/go.mod index 6460d1f..764ead3 100644 --- a/tinytodo-go/go.mod +++ b/tinytodo-go/go.mod @@ -3,7 +3,7 @@ module github.com/cedar-policy/cedar-examples/tinytodo-go go 1.22 require ( - github.com/cedar-policy/cedar-go v0.0.0-20240715162045-a71e93ee6ae7 // pins the cedar-go commit + github.com/cedar-policy/cedar-go v0.3.2 github.com/go-chi/chi/v5 v5.1.0 github.com/stretchr/testify v1.9.0 ) diff --git a/tinytodo-go/go.sum b/tinytodo-go/go.sum index 889520f..bc92bd2 100644 --- a/tinytodo-go/go.sum +++ b/tinytodo-go/go.sum @@ -1,5 +1,5 @@ -github.com/cedar-policy/cedar-go v0.0.0-20240715162045-a71e93ee6ae7 h1:3WPOmm5kgn8q5kbQc2kG97RK//GTQAp79AW7pV3pa8M= -github.com/cedar-policy/cedar-go v0.0.0-20240715162045-a71e93ee6ae7/go.mod h1:pEgiK479O5dJfzXnTguOMm+bCplzy5rEEFPGdZKPWz4= +github.com/cedar-policy/cedar-go v0.3.2 h1:WKE8sW/RsnTp9hkAHSf3oGspcEoIOGCPPz1GDF3dgFc= +github.com/cedar-policy/cedar-go v0.3.2/go.mod h1:pEgiK479O5dJfzXnTguOMm+bCplzy5rEEFPGdZKPWz4= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= diff --git a/tinytodo-go/internal/app/server/context.go b/tinytodo-go/internal/app/server/context.go index 4a217c6..66a20a6 100644 --- a/tinytodo-go/internal/app/server/context.go +++ b/tinytodo-go/internal/app/server/context.go @@ -3,8 +3,8 @@ package server import ( "context" "fmt" - "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore" "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/action" + "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entityuid" "github.com/cedar-policy/cedar-go" "log/slog" ) @@ -16,9 +16,9 @@ import ( // Non-existent entities (resources) will result in an error. (TODO: we may not want this behaviour) func (s *Server) isAuthorized( ctx context.Context, - principal entitystore.EntityUID, + principal entityuid.EntityUID, action action.Action, - resource entitystore.EntityUID, + resource entityuid.EntityUID, ) (bool, cedar.Diagnostic, error) { // we have to generate entities every time, because the entities may have been updated diff --git a/tinytodo-go/internal/app/server/context_test.go b/tinytodo-go/internal/app/server/context_test.go index 851a845..4619788 100644 --- a/tinytodo-go/internal/app/server/context_test.go +++ b/tinytodo-go/internal/app/server/context_test.go @@ -3,6 +3,10 @@ package server import ( "context" "encoding/json" + "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entity/list" + "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entity/team" + "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entity/user" + "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entityuid" "os" "path" "testing" @@ -27,7 +31,7 @@ func TestServer_isAuthorized(t *testing.T) { // read policies psFile := readFile(t, path.Join("../../../", "policies.cedar")) - ps, err := cedar.NewPolicySet("policies.cedar", psFile) + ps, err := cedar.NewPolicySetFromBytes("policies.cedar", psFile) require.NoError(t, err) // read entities (will be modified later) @@ -44,25 +48,25 @@ func TestServer_isAuthorized(t *testing.T) { // extract users - userAndrew, ok := es.Users[entitystore.UserUID{ - EntityUID: entitystore.NewEntityUID(entitytype.User, "andrew"), + userAndrew, ok := es.Users[user.UserUID{ + EntityUID: entityuid.New(entitytype.User, "andrew"), }] require.True(t, ok) - userAaron, ok := es.Users[entitystore.UserUID{ - EntityUID: entitystore.NewEntityUID(entitytype.User, "aaron"), + userAaron, ok := es.Users[user.UserUID{ + EntityUID: entityuid.New(entitytype.User, "aaron"), }] require.True(t, ok) - userKesha, ok := es.Users[entitystore.UserUID{ - EntityUID: entitystore.NewEntityUID(entitytype.User, "kesha"), + userKesha, ok := es.Users[user.UserUID{ + EntityUID: entityuid.New(entitytype.User, "kesha"), }] require.True(t, ok) // extract teams - teamInterns, ok := es.Teams[entitystore.TeamUID{ - EntityUID: entitystore.NewEntityUID(entitytype.Team, "interns"), + teamInterns, ok := es.Teams[team.TeamUID{ + EntityUID: entityuid.New(entitytype.Team, "interns"), }] require.True(t, ok) @@ -100,7 +104,7 @@ func TestServer_isAuthorized(t *testing.T) { list0Readers := es.InsertNextTeam() // readers for list0 list0Editors := es.InsertNextTeam() // editors for list0 - list0 := entitystore.NewList( + list0 := list.New( list0UID, "Cedar blog post", userAndrew.EUID, @@ -157,7 +161,7 @@ func TestServer_isAuthorized(t *testing.T) { context.Background(), userAaron.EUID.EntityUID, action.GetList, - entitystore.NewEntityUID(entitytype.List, "non-existent"), + entityuid.New(entitytype.List, "non-existent"), ) require.NoError(t, err) assert.False(t, decision) diff --git a/tinytodo-go/internal/app/server/entitystore/action/action.go b/tinytodo-go/internal/app/server/entitystore/action/action.go index c6582ab..5d2c3dd 100644 --- a/tinytodo-go/internal/app/server/entitystore/action/action.go +++ b/tinytodo-go/internal/app/server/entitystore/action/action.go @@ -1,13 +1,14 @@ -// Package action contains the enum Action that represents the different actions supported by entitystore.EntityStore. +// Package action contains the enum Action that represents the different actions supported by TinyTodo. package action import ( - "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore" "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entitytype" + "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entityuid" + "github.com/cedar-policy/cedar-go/types" "strings" ) -// Action is an enum that represents the different entity types supported by Cedar. +// Action is an enum that represents the different entity types supported by TinyTodo. type Action int const ( @@ -37,17 +38,17 @@ var ( DeleteList: "Action::\"DeleteList\"", } - EntityUID = map[Action]entitystore.EntityUID{} + EntityUID = map[Action]entityuid.EntityUID{} ) func init() { // verify that all Actions are valid EUIDs for k, act := range Name { - euid, err := entitystore.ParseEntityUID(act) + euid, err := entityuid.Parse(act) if err != nil { panic(err) } - if euid.Type != entitytype.Action.String() { + if euid.Type != types.EntityType(entitytype.Action.String()) { panic(err) } EntityUID[k] = euid @@ -68,6 +69,6 @@ func Parse(act string) Action { return Unknown } -func (a Action) GetEUID() entitystore.EntityUID { +func (a Action) GetEUID() entityuid.EntityUID { return EntityUID[a] } diff --git a/tinytodo-go/internal/app/server/entitystore/app.go b/tinytodo-go/internal/app/server/entitystore/app.go deleted file mode 100644 index 113c868..0000000 --- a/tinytodo-go/internal/app/server/entitystore/app.go +++ /dev/null @@ -1,20 +0,0 @@ -package entitystore - -import ( - "github.com/cedar-policy/cedar-go" -) - -// App represents the application entity (in this case, TinyTodo). -type App struct { - EUID EntityUID `json:"euid"` -} - -// AsCedarEntity converts App into a cedar.Entity, to be passed to the Cedar authorization engine when it evaluates a -// request. -func (a *App) AsCedarEntity() *cedar.Entity { - return &cedar.Entity{ - UID: a.EUID.EntityUID, - //Parents: nil, - //Attributes: nil, - } -} diff --git a/tinytodo-go/internal/app/server/entitystore/convert.go b/tinytodo-go/internal/app/server/entitystore/convert.go index 4f43e5e..55c18bf 100644 --- a/tinytodo-go/internal/app/server/entitystore/convert.go +++ b/tinytodo-go/internal/app/server/entitystore/convert.go @@ -1,36 +1,36 @@ package entitystore import ( - "github.com/cedar-policy/cedar-go" + "github.com/cedar-policy/cedar-go/types" ) -// AsEntities converts EntityStore's native objects into cedar.Entities, to be passed to the Cedar authorization engine +// AsEntities converts EntityStore's native objects into types.Entities, to be passed to the Cedar authorization engine // when it evaluates a request. -func (e *EntityStore) AsEntities() (cedar.Entities, error) { +func (e *EntityStore) AsEntities() (types.Entities, error) { - es := make(cedar.Entities) + es := make(types.Entities) // process users for _, user := range e.Users { - es[user.EUID.EntityUID.EntityUID] = *user.AsCedarEntity() + es[user.EUID.EntityUID.EntityUID] = user.AsCedarEntity() } // process teams for _, team := range e.Teams { - es[team.UID.EntityUID.EntityUID] = *team.AsCedarEntity() + es[team.UID.EntityUID.EntityUID] = team.AsCedarEntity() } // process lists for _, list := range e.Lists { - es[list.UID.EntityUID.EntityUID] = *list.AsCedarEntity() + es[list.UID.EntityUID.EntityUID] = list.AsCedarEntity() } // process application - es[e.App.EUID.EntityUID] = *e.App.AsCedarEntity() + es[e.App.EUID.EntityUID] = e.App.AsCedarEntity() return es, nil } diff --git a/tinytodo-go/internal/app/server/entitystore/convert_test.go b/tinytodo-go/internal/app/server/entitystore/convert_test.go index 06d2cef..23f8985 100644 --- a/tinytodo-go/internal/app/server/entitystore/convert_test.go +++ b/tinytodo-go/internal/app/server/entitystore/convert_test.go @@ -3,7 +3,8 @@ package entitystore import ( "encoding/json" "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entitytype" - "github.com/cedar-policy/cedar-go" + "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entityuid" + "github.com/cedar-policy/cedar-go/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "testing" @@ -16,7 +17,7 @@ func TestEntityStore_AsEntities(t *testing.T) { require.NoError(t, json.Unmarshal(f, &es)) assert.Equal( t, - NewEntityUID(entitytype.Application, "TinyTodo"), + entityuid.New(entitytype.Application, "TinyTodo"), es.App.EUID, ) @@ -26,19 +27,28 @@ func TestEntityStore_AsEntities(t *testing.T) { assert.Contains( t, entities, - cedar.NewEntityUID(entitytype.Application.String(), "TinyTodo"), + types.NewEntityUID( + types.EntityType(entitytype.Application.String()), + "TinyTodo", + ), ) assert.Contains( t, entities, - cedar.NewEntityUID(entitytype.User.String(), "kesha"), + types.NewEntityUID( + types.EntityType(entitytype.User.String()), + "kesha", + ), ) assert.Contains( t, entities, - cedar.NewEntityUID(entitytype.Team.String(), "temp"), + types.NewEntityUID( + types.EntityType(entitytype.Team.String()), + "temp", + ), ) }) } diff --git a/tinytodo-go/internal/app/server/entitystore/entity.go b/tinytodo-go/internal/app/server/entitystore/entity.go deleted file mode 100644 index 97fed00..0000000 --- a/tinytodo-go/internal/app/server/entitystore/entity.go +++ /dev/null @@ -1,7 +0,0 @@ -package entitystore - -import "github.com/cedar-policy/cedar-go" - -type Entity interface { - AsCedarEntity() *cedar.Entity -} diff --git a/tinytodo-go/internal/app/server/entitystore/entity/app/app.go b/tinytodo-go/internal/app/server/entitystore/entity/app/app.go new file mode 100644 index 0000000..629e48a --- /dev/null +++ b/tinytodo-go/internal/app/server/entitystore/entity/app/app.go @@ -0,0 +1,21 @@ +package app + +import ( + "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entityuid" + "github.com/cedar-policy/cedar-go/types" +) + +// App represents the application entity (in this case, TinyTodo). +type App struct { + EUID entityuid.EntityUID `json:"euid"` +} + +// AsCedarEntity converts App into a types.Entity, to be passed to the Cedar authorization engine when it evaluates a +// request. +func (a *App) AsCedarEntity() *types.Entity { + return &types.Entity{ + UID: a.EUID.EntityUID, + //Parents: nil, + //Attributes: nil, + } +} diff --git a/tinytodo-go/internal/app/server/entitystore/app_test.go b/tinytodo-go/internal/app/server/entitystore/entity/app/app_test.go similarity index 74% rename from tinytodo-go/internal/app/server/entitystore/app_test.go rename to tinytodo-go/internal/app/server/entitystore/entity/app/app_test.go index 94eef9b..6a104c3 100644 --- a/tinytodo-go/internal/app/server/entitystore/app_test.go +++ b/tinytodo-go/internal/app/server/entitystore/entity/app/app_test.go @@ -1,8 +1,9 @@ -package entitystore +package app import ( "encoding/json" "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entitytype" + "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entityuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "testing" @@ -19,7 +20,7 @@ func Test_App(t *testing.T) { require.NoError(t, json.Unmarshal(marshalled, &app)) assert.Equal( t, - NewEntityUID(entitytype.Application, "TinyTodo"), + entityuid.New(entitytype.Application, "TinyTodo"), app.EUID, ) }) diff --git a/tinytodo-go/internal/app/server/entitystore/entity/entity.go b/tinytodo-go/internal/app/server/entitystore/entity/entity.go new file mode 100644 index 0000000..d85e44f --- /dev/null +++ b/tinytodo-go/internal/app/server/entitystore/entity/entity.go @@ -0,0 +1,9 @@ +package entity + +import ( + "github.com/cedar-policy/cedar-go/types" +) + +type Entity interface { + AsCedarEntity() *types.Entity +} diff --git a/tinytodo-go/internal/app/server/entitystore/list.go b/tinytodo-go/internal/app/server/entitystore/entity/list/list.go similarity index 58% rename from tinytodo-go/internal/app/server/entitystore/list.go rename to tinytodo-go/internal/app/server/entitystore/entity/list/list.go index 48ef14c..4a215cb 100644 --- a/tinytodo-go/internal/app/server/entitystore/list.go +++ b/tinytodo-go/internal/app/server/entitystore/entity/list/list.go @@ -1,9 +1,13 @@ -package entitystore +package list import ( + "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entity/task" + "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entity/team" + "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entity/user" "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entitytype" + "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entityuid" "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/taskstate" - "github.com/cedar-policy/cedar-go" + "github.com/cedar-policy/cedar-go/types" "strconv" ) @@ -13,7 +17,7 @@ import ( // // [blog post]: https://sentry.io/answers/alias-type-definitions/ type ListUID struct { - EntityUID + entityuid.EntityUID } // List represents the list entity. @@ -23,43 +27,50 @@ type ListUID struct { // // This is because List should only be created via the APIs, hence the generation of the ID is controlled. type List struct { - UID ListUID `json:"uid"` - Name string `json:"name"` - Owner UserUID `json:"owner"` - Readers TeamUID `json:"readers"` // plural because its a team of readers - Editors TeamUID `json:"editors"` // plural because its a team of editors - Tasks []*Task `json:"tasks"` + UID ListUID `json:"uid"` + Name string `json:"name"` + Owner user.UserUID `json:"owner"` + Readers team.TeamUID `json:"readers"` // plural because its a team of readers + Editors team.TeamUID `json:"editors"` // plural because its a team of editors + Tasks []*task.Task `json:"tasks"` } -// NewList creates a new List; if tasks is nil, we create an empty slice so that there will be no problems with +// New creates a new List; if tasks is nil, we create an empty slice so that there will be no problems with // client processing. -func NewList(uid ListUID, name string, owner UserUID, readers TeamUID, editors TeamUID, tasks []*Task) *List { +func New( + uid ListUID, + name string, + owner user.UserUID, + readers team.TeamUID, + editors team.TeamUID, + tasks []*task.Task, +) *List { if tasks == nil { - tasks = []*Task{} + tasks = []*task.Task{} } return &List{uid, name, owner, readers, editors, tasks} } -// AsCedarEntity converts List into a cedar.Entity, to be passed to the Cedar authorization engine when it evaluates a +// AsCedarEntity converts List into a types.Entity, to be passed to the Cedar authorization engine when it evaluates a // request. -func (l *List) AsCedarEntity() *cedar.Entity { +func (l *List) AsCedarEntity() *types.Entity { - records := make(cedar.Record) + records := make(types.Record) - // be careful - it is easy to get cedar.Value wrong + // be careful - it is easy to get types.Value wrong - records["name"] = cedar.String(l.Name) + records["name"] = types.String(l.Name) records["owner"] = l.Owner.EntityUID.EntityUID records["readers"] = l.Readers.EntityUID.EntityUID records["editors"] = l.Editors.EntityUID.EntityUID - var tasks cedar.Set + var tasks types.Set for _, t := range l.Tasks { tasks = append(tasks, t.UID.EntityUID.EntityUID) } records["tasks"] = tasks // we include tasks because this is what the Rust implementation does - return &cedar.Entity{ + return &types.Entity{ UID: l.UID.EntityUID.EntityUID, //Parents: nil, Attributes: records, @@ -72,12 +83,12 @@ func (l *List) AsCedarEntity() *cedar.Entity { // Although the task ID starts from 0, the client will adjust it with a +1 offset. func (l *List) InsertTask(name string) int { id := len(l.Tasks) // simply use the current number of tasks (non-negative integer) as the ID for the next task - l.Tasks = append(l.Tasks, &Task{ - UID: TaskUID{ - EntityUID: EntityUID{ - EntityUID: cedar.EntityUID{ - Type: entitytype.Task.String(), - ID: strconv.Itoa(id), + l.Tasks = append(l.Tasks, &task.Task{ + UID: task.TaskUID{ + EntityUID: entityuid.EntityUID{ + EntityUID: types.EntityUID{ + Type: types.EntityType(entitytype.Task.String()), + ID: types.String(strconv.Itoa(id)), }, }, }, diff --git a/tinytodo-go/internal/app/server/entitystore/entity/list/list_test.go b/tinytodo-go/internal/app/server/entitystore/entity/list/list_test.go new file mode 100644 index 0000000..d71191f --- /dev/null +++ b/tinytodo-go/internal/app/server/entitystore/entity/list/list_test.go @@ -0,0 +1,107 @@ +package list + +import ( + "encoding/json" + "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entity" + "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entity/team" + "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entity/user" + "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entitytype" + "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entityuid" + "github.com/cedar-policy/cedar-go/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" +) + +func TestList(t *testing.T) { + t.Run("check interface", func(t *testing.T) { + var e entity.Entity + l := New( + ListUID{ + EntityUID: entityuid.EntityUID{ + EntityUID: types.EntityUID{ + Type: types.EntityType(entitytype.List.String()), + ID: "1", + }, + }, + }, + "Cedar blog post", + user.UserUID{ + EntityUID: entityuid.EntityUID{ + EntityUID: types.EntityUID{ + Type: types.EntityType(entitytype.User.String()), + ID: "kesha", + }, + }, + }, + team.TeamUID{ + EntityUID: entityuid.EntityUID{ + EntityUID: types.EntityUID{ + Type: types.EntityType(entitytype.Team.String()), + ID: "temp", + }, + }, + }, + team.TeamUID{ + EntityUID: entityuid.EntityUID{ + EntityUID: types.EntityUID{ + Type: types.EntityType(entitytype.Team.String()), + ID: "admin", + }, + }, + }, + nil, + ) + e = l + require.NotNil(t, e) + }) +} + +func TestList_Marshal(t *testing.T) { + t.Run("check marshal valid case", func(t *testing.T) { + list := New( + ListUID{ + EntityUID: entityuid.EntityUID{ + EntityUID: types.EntityUID{ + Type: types.EntityType(entitytype.List.String()), + ID: "1", + }, + }, + }, + "Cedar blog post", + user.UserUID{ + EntityUID: entityuid.EntityUID{ + EntityUID: types.EntityUID{ + Type: types.EntityType(entitytype.User.String()), + ID: "kesha", + }, + }, + }, + team.TeamUID{ + EntityUID: entityuid.EntityUID{ + EntityUID: types.EntityUID{ + Type: types.EntityType(entitytype.Team.String()), + ID: "temp", + }, + }, + }, + team.TeamUID{ + EntityUID: entityuid.EntityUID{ + EntityUID: types.EntityUID{ + Type: types.EntityType(entitytype.Team.String()), + ID: "admin", + }, + }, + }, + nil, + ) + + got, err := json.MarshalIndent(list, "", " ") + require.NoError(t, err) + + var recovered List + require.NoError(t, json.Unmarshal(got, &recovered)) + + assert.Equal(t, *list, recovered) + }) +} diff --git a/tinytodo-go/internal/app/server/entitystore/task.go b/tinytodo-go/internal/app/server/entitystore/entity/task/task.go similarity index 64% rename from tinytodo-go/internal/app/server/entitystore/task.go rename to tinytodo-go/internal/app/server/entitystore/entity/task/task.go index 0ba5554..bc1ac26 100644 --- a/tinytodo-go/internal/app/server/entitystore/task.go +++ b/tinytodo-go/internal/app/server/entitystore/entity/task/task.go @@ -1,8 +1,9 @@ -package entitystore +package task import ( + "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entityuid" "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/taskstate" - "github.com/cedar-policy/cedar-go" + "github.com/cedar-policy/cedar-go/types" ) // TaskUID is a transparent wrapper around EntityUID, to make it clear that we want a Task's EntityUID. @@ -11,7 +12,7 @@ import ( // // [blog post]: https://sentry.io/answers/alias-type-definitions/ type TaskUID struct { - EntityUID + entityuid.EntityUID } // Task represents the task entity. @@ -22,15 +23,15 @@ type Task struct { State taskstate.TaskState `json:"state"` } -// AsCedarEntity converts Task into a cedar.Entity, to be passed to the Cedar authorization engine when it evaluates a +// AsCedarEntity converts Task into a types.Entity, to be passed to the Cedar authorization engine when it evaluates a // request. -func (t *Task) AsCedarEntity() *cedar.Entity { +func (t *Task) AsCedarEntity() *types.Entity { - records := make(cedar.Record) - records["name"] = cedar.String(t.Name) - records["state"] = cedar.String(t.State.String()) + records := make(types.Record) + records["name"] = types.String(t.Name) + records["state"] = types.String(t.State.String()) - return &cedar.Entity{ + return &types.Entity{ UID: t.UID.EntityUID.EntityUID, //Parents: nil, Attributes: records, diff --git a/tinytodo-go/internal/app/server/entitystore/task_test.go b/tinytodo-go/internal/app/server/entitystore/entity/task/task_test.go similarity index 63% rename from tinytodo-go/internal/app/server/entitystore/task_test.go rename to tinytodo-go/internal/app/server/entitystore/entity/task/task_test.go index 82c880f..91dd49b 100644 --- a/tinytodo-go/internal/app/server/entitystore/task_test.go +++ b/tinytodo-go/internal/app/server/entitystore/entity/task/task_test.go @@ -1,10 +1,12 @@ -package entitystore +package task import ( "encoding/json" + "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entity" "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entitytype" + "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entityuid" "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/taskstate" - "github.com/cedar-policy/cedar-go" + "github.com/cedar-policy/cedar-go/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "testing" @@ -12,12 +14,12 @@ import ( func TestTask(t *testing.T) { t.Run("check interface", func(t *testing.T) { - var e Entity + var e entity.Entity task := &Task{ UID: TaskUID{ - EntityUID: EntityUID{ - EntityUID: cedar.EntityUID{ - Type: entitytype.Task.String(), + EntityUID: entityuid.EntityUID{ + EntityUID: types.EntityUID{ + Type: types.EntityType(entitytype.Task.String()), ID: "1", }, }, @@ -35,9 +37,9 @@ func TestTask_Marshal(t *testing.T) { t.Run("check marshal valid case", func(t *testing.T) { task := Task{ UID: TaskUID{ - EntityUID: EntityUID{ - EntityUID: cedar.EntityUID{ - Type: entitytype.Task.String(), + EntityUID: entityuid.EntityUID{ + EntityUID: types.EntityUID{ + Type: types.EntityType(entitytype.Task.String()), ID: "1", }, }, diff --git a/tinytodo-go/internal/app/server/entitystore/team.go b/tinytodo-go/internal/app/server/entitystore/entity/team/team.go similarity index 57% rename from tinytodo-go/internal/app/server/entitystore/team.go rename to tinytodo-go/internal/app/server/entitystore/entity/team/team.go index 1267113..0e4eba3 100644 --- a/tinytodo-go/internal/app/server/entitystore/team.go +++ b/tinytodo-go/internal/app/server/entitystore/entity/team/team.go @@ -1,7 +1,8 @@ -package entitystore +package team import ( - "github.com/cedar-policy/cedar-go" + "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entityuid" + "github.com/cedar-policy/cedar-go/types" ) // TeamUID is a transparent wrapper around EntityUID, to make it clear that we want a Team's EntityUID. @@ -10,7 +11,7 @@ import ( // // [blog post]: https://sentry.io/answers/alias-type-definitions/ type TeamUID struct { - EntityUID + entityuid.EntityUID } // Team represents the team entity. @@ -20,27 +21,27 @@ type TeamUID struct { // // This is because List should only be created via the APIs, hence the generation of the ID is controlled. type Team struct { - UID TeamUID `json:"uid"` // note the naming - Parents []EntityUID `json:"parents"` // can be TeamUID or UserUID + UID TeamUID `json:"uid"` // note the naming + Parents []entityuid.EntityUID `json:"parents"` // can be TeamUID or UserUID } -// NewTeam creates a new Team; if parents is nil, we create an empty slice so that there will be no problems with +// New creates a new Team; if parents is nil, we create an empty slice so that there will be no problems with // client processing. -func NewTeam(uid TeamUID, parents []EntityUID) *Team { +func New(uid TeamUID, parents []entityuid.EntityUID) *Team { if parents == nil { - parents = []EntityUID{} + parents = []entityuid.EntityUID{} } return &Team{uid, parents} } -// AsCedarEntity converts Team into a cedar.Entity, to be passed to the Cedar authorization engine when it evaluates a +// AsCedarEntity converts Team into a types.Entity, to be passed to the Cedar authorization engine when it evaluates a // request. -func (t *Team) AsCedarEntity() *cedar.Entity { - var parents []cedar.EntityUID +func (t *Team) AsCedarEntity() *types.Entity { + var parents []types.EntityUID for _, parent := range t.Parents { parents = append(parents, parent.EntityUID) } - return &cedar.Entity{ + return &types.Entity{ UID: t.UID.EntityUID.EntityUID, Parents: parents, } diff --git a/tinytodo-go/internal/app/server/entitystore/team_test.go b/tinytodo-go/internal/app/server/entitystore/entity/team/team_test.go similarity index 67% rename from tinytodo-go/internal/app/server/entitystore/team_test.go rename to tinytodo-go/internal/app/server/entitystore/entity/team/team_test.go index fd5c7be..2e2fb36 100644 --- a/tinytodo-go/internal/app/server/entitystore/team_test.go +++ b/tinytodo-go/internal/app/server/entitystore/entity/team/team_test.go @@ -1,8 +1,10 @@ -package entitystore +package team import ( "encoding/json" + "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entity" "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entitytype" + "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entityuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "testing" @@ -10,10 +12,10 @@ import ( func TestTeam(t *testing.T) { t.Run("check interface", func(t *testing.T) { - var e Entity - team := NewTeam( + var e entity.Entity + team := New( TeamUID{ - EntityUID: NewEntityUID(entitytype.Team, "temp"), + EntityUID: entityuid.New(entitytype.Team, "temp"), }, nil, ) @@ -25,7 +27,7 @@ func TestTeam(t *testing.T) { func TestTeamUID_Marshal(t *testing.T) { t.Run("marshal TeamUID", func(t *testing.T) { teamUID := TeamUID{ - EntityUID: NewEntityUID(entitytype.Team, "temp"), + EntityUID: entityuid.New(entitytype.Team, "temp"), } got, err := json.MarshalIndent(teamUID, "", " ") require.NoError(t, err) @@ -48,14 +50,14 @@ func TestTeam_Unmarshal(t *testing.T) { assert.Equal( t, TeamUID{ - EntityUID: NewEntityUID(entitytype.Team, "temp"), + EntityUID: entityuid.New(entitytype.Team, "temp"), }, team.UID, ) assert.Contains( t, team.Parents, - NewEntityUID(entitytype.Application, "TinyTodo"), + entityuid.New(entitytype.Application, "TinyTodo"), ) }) } diff --git a/tinytodo-go/internal/app/server/entitystore/entity/user/user.go b/tinytodo-go/internal/app/server/entitystore/entity/user/user.go new file mode 100644 index 0000000..45c36b6 --- /dev/null +++ b/tinytodo-go/internal/app/server/entitystore/entity/user/user.go @@ -0,0 +1,54 @@ +package user + +import ( + "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entityuid" + "github.com/cedar-policy/cedar-go/types" +) + +// UserUID is a transparent wrapper around EntityUID, to make it clear that we want a User's EntityUID. +// +// Note that we use inheritance instead of alias, because we want to inherit methods. See [blog post]. +// +// [blog post]: https://sentry.io/answers/alias-type-definitions/ +type UserUID struct { + entityuid.EntityUID +} + +// User represents the user entity. +type User struct { + EUID UserUID `json:"euid"` // note the spelling + Location string `json:"location"` + JobLevel int `json:"joblevel"` + Parents []entityuid.EntityUID `json:"parents"` +} + +// New creates a new User; if parents is nil, we create an empty slice so that there will be no problems with +// client processing. +func New(uid UserUID, location string, jobLevel int, parents []entityuid.EntityUID) *User { + if parents == nil { + parents = []entityuid.EntityUID{} + } + return &User{ + EUID: uid, + Location: location, + JobLevel: jobLevel, + Parents: parents, + } +} + +// AsCedarEntity converts User into a types.Entity, to be passed to the Cedar authorization engine when it evaluates a +// request. +func (u *User) AsCedarEntity() *types.Entity { + var parents []types.EntityUID + for _, parent := range u.Parents { + parents = append(parents, parent.EntityUID) + } + return &types.Entity{ + UID: u.EUID.EntityUID.EntityUID, + Parents: parents, + Attributes: types.Record{ + "location": types.String(u.Location), + "joblevel": types.Long(u.JobLevel), + }, + } +} diff --git a/tinytodo-go/internal/app/server/entitystore/user_test.go b/tinytodo-go/internal/app/server/entitystore/entity/user/user_test.go similarity index 67% rename from tinytodo-go/internal/app/server/entitystore/user_test.go rename to tinytodo-go/internal/app/server/entitystore/entity/user/user_test.go index 6c39933..2bd525f 100644 --- a/tinytodo-go/internal/app/server/entitystore/user_test.go +++ b/tinytodo-go/internal/app/server/entitystore/entity/user/user_test.go @@ -1,8 +1,10 @@ -package entitystore +package user import ( "encoding/json" + "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entity" "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entitytype" + "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entityuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "testing" @@ -10,10 +12,10 @@ import ( func TestUser(t *testing.T) { t.Run("check interface", func(t *testing.T) { - var e Entity - u := NewUser( + var e entity.Entity + u := New( UserUID{ - EntityUID: NewEntityUID(entitytype.User, "andrew"), + EntityUID: entityuid.New(entitytype.User, "andrew"), }, "test_location", 0, @@ -42,7 +44,7 @@ func TestUser_Unmarshal(t *testing.T) { assert.Equal( t, UserUID{ - NewEntityUID(entitytype.User, "kesha"), + entityuid.New(entitytype.User, "kesha"), }, u.EUID, ) @@ -51,12 +53,12 @@ func TestUser_Unmarshal(t *testing.T) { assert.Contains( t, u.Parents, - NewEntityUID(entitytype.Application, "TinyTodo"), + entityuid.New(entitytype.Application, "TinyTodo"), ) assert.Contains( t, u.Parents, - NewEntityUID(entitytype.Team, "temp"), + entityuid.New(entitytype.Team, "temp"), ) }) } diff --git a/tinytodo-go/internal/app/server/entitystore/entitystore.go b/tinytodo-go/internal/app/server/entitystore/entitystore.go index 8013a08..f4c7ee0 100644 --- a/tinytodo-go/internal/app/server/entitystore/entitystore.go +++ b/tinytodo-go/internal/app/server/entitystore/entitystore.go @@ -6,7 +6,13 @@ package entitystore import ( "encoding/json" "fmt" + "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entity/app" + "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entity/list" + "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entity/task" + "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entity/team" + "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entity/user" "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entitytype" + "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entityuid" "strconv" ) @@ -15,7 +21,7 @@ type EntityStore struct { Users UserUIDToUserMap `json:"users"` Teams TeamUIDToTeamMap `json:"teams"` Lists ListUIDToListMap `json:"lists"` - App App `json:"app"` + App app.App `json:"app"` } func New(entitiesJSON []byte) (*EntityStore, error) { @@ -27,11 +33,11 @@ func New(entitiesJSON []byte) (*EntityStore, error) { } // GetNextListUID returns the next available ListUID (but does not create) with a monotonically increasing ID. -func (e *EntityStore) GetNextListUID() ListUID { +func (e *EntityStore) GetNextListUID() list.ListUID { var id int for { - listUID := ListUID{ - EntityUID: NewEntityUID(entitytype.List, strconv.Itoa(id)), + listUID := list.ListUID{ + EntityUID: entityuid.New(entitytype.List, strconv.Itoa(id)), } if _, found := e.Lists[listUID]; !found { return listUID @@ -41,14 +47,14 @@ func (e *EntityStore) GetNextListUID() ListUID { } // InsertNextTeam creates the next available Team with a monotonically increasing ID and returns the TeamUID. -func (e *EntityStore) InsertNextTeam() TeamUID { +func (e *EntityStore) InsertNextTeam() team.TeamUID { var id int for { - teamUID := TeamUID{ - EntityUID: NewEntityUID(entitytype.Team, strconv.Itoa(id)), + teamUID := team.TeamUID{ + EntityUID: entityuid.New(entitytype.Team, strconv.Itoa(id)), } if _, found := e.Teams[teamUID]; !found { - e.Teams[teamUID] = NewTeam(teamUID, nil) + e.Teams[teamUID] = team.New(teamUID, nil) return teamUID } id++ @@ -58,24 +64,24 @@ func (e *EntityStore) InsertNextTeam() TeamUID { // UserUIDToUserMap is a special type created to help unmarshal map[UserUID]User. // // Inspired by https://groups.google.com/g/golang-nuts/c/IxPipKwI-zQ and https://go.dev/play/p/YgUIFxT7hA. -type UserUIDToUserMap map[UserUID]*User +type UserUIDToUserMap map[user.UserUID]*user.User func (im *UserUIDToUserMap) UnmarshalJSON(bytes []byte) error { // Unmarshal the string-keyed map - sk := make(map[string]*User) + sk := make(map[string]*user.User) if err := json.Unmarshal(bytes, &sk); err != nil { return err } // Copy the values - *im = make(map[UserUID]*User) + *im = make(map[user.UserUID]*user.User) for k, v := range sk { - ki, err := ParseEntityUID(k) + ki, err := entityuid.Parse(k) if err != nil { return err } - (*im)[UserUID{ + (*im)[user.UserUID{ EntityUID: ki, }] = v } @@ -86,24 +92,24 @@ func (im *UserUIDToUserMap) UnmarshalJSON(bytes []byte) error { // TeamUIDToTeamMap is a special type created to help unmarshal map[TeamUID]Team. // // Inspired by https://groups.google.com/g/golang-nuts/c/IxPipKwI-zQ and https://go.dev/play/p/YgUIFxT7hA. -type TeamUIDToTeamMap map[TeamUID]*Team +type TeamUIDToTeamMap map[team.TeamUID]*team.Team func (im *TeamUIDToTeamMap) UnmarshalJSON(bytes []byte) error { // Unmarshal the string-keyed map - sk := make(map[string]*Team) + sk := make(map[string]*team.Team) if err := json.Unmarshal(bytes, &sk); err != nil { return err } // Copy the values - *im = make(map[TeamUID]*Team) + *im = make(map[team.TeamUID]*team.Team) for k, v := range sk { - ki, err := ParseEntityUID(k) + ki, err := entityuid.Parse(k) if err != nil { return err } - (*im)[TeamUID{EntityUID: ki}] = v + (*im)[team.TeamUID{EntityUID: ki}] = v } return nil @@ -112,24 +118,24 @@ func (im *TeamUIDToTeamMap) UnmarshalJSON(bytes []byte) error { // ListUIDToListMap is a special type created to help unmarshal map[ListUID]List. // // Inspired by https://groups.google.com/g/golang-nuts/c/IxPipKwI-zQ and https://go.dev/play/p/YgUIFxT7hA. -type ListUIDToListMap map[ListUID]*List +type ListUIDToListMap map[list.ListUID]*list.List func (im *ListUIDToListMap) UnmarshalJSON(bytes []byte) error { // Unmarshal the string-keyed map - sk := make(map[string]*List) + sk := make(map[string]*list.List) if err := json.Unmarshal(bytes, &sk); err != nil { return err } // Copy the values - *im = make(map[ListUID]*List) + *im = make(map[list.ListUID]*list.List) for k, v := range sk { - ki, err := ParseEntityUID(k) + ki, err := entityuid.Parse(k) if err != nil { return err } - (*im)[ListUID{EntityUID: ki}] = v + (*im)[list.ListUID{EntityUID: ki}] = v } return nil @@ -138,24 +144,24 @@ func (im *ListUIDToListMap) UnmarshalJSON(bytes []byte) error { // TaskUIDToTaskMap is a special type created to help unmarshal map[TaskUID]Task. // // Inspired by https://groups.google.com/g/golang-nuts/c/IxPipKwI-zQ and https://go.dev/play/p/YgUIFxT7hA. -type TaskUIDToTaskMap map[TaskUID]*Task +type TaskUIDToTaskMap map[task.TaskUID]*task.Task func (im *TaskUIDToTaskMap) UnmarshalJSON(bytes []byte) error { // Unmarshal the string-keyed map - sk := make(map[string]*Task) + sk := make(map[string]*task.Task) if err := json.Unmarshal(bytes, &sk); err != nil { return err } // Copy the values - *im = make(map[TaskUID]*Task) + *im = make(map[task.TaskUID]*task.Task) for k, v := range sk { - ki, err := ParseEntityUID(k) + ki, err := entityuid.Parse(k) if err != nil { return err } - (*im)[TaskUID{EntityUID: ki}] = v + (*im)[task.TaskUID{EntityUID: ki}] = v } return nil diff --git a/tinytodo-go/internal/app/server/entitystore/entitystore_test.go b/tinytodo-go/internal/app/server/entitystore/entitystore_test.go index 4aa81b5..755d976 100644 --- a/tinytodo-go/internal/app/server/entitystore/entitystore_test.go +++ b/tinytodo-go/internal/app/server/entitystore/entitystore_test.go @@ -2,7 +2,10 @@ package entitystore import ( "encoding/json" + "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entity/team" + "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entity/user" "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entitytype" + "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entityuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "os" @@ -23,22 +26,22 @@ func Test_EntityStore(t *testing.T) { var es EntityStore require.NoError(t, json.Unmarshal(f, &es)) - userUID := UserUID{ - EntityUID: NewEntityUID(entitytype.User, "kesha"), + userUID := user.UserUID{ + EntityUID: entityuid.New(entitytype.User, "kesha"), } - teamUID := TeamUID{ - EntityUID: NewEntityUID(entitytype.Team, "temp"), + teamUID := team.TeamUID{ + EntityUID: entityuid.New(entitytype.Team, "temp"), } - applicationEUID := NewEntityUID(entitytype.Application, "TinyTodo") + applicationEUID := entityuid.New(entitytype.Application, "TinyTodo") assert.Contains(t, es.Users, userUID) assert.Equal( t, - NewUser( + user.New( userUID, "ABC17", 5, - []EntityUID{ + []entityuid.EntityUID{ // order matters applicationEUID, teamUID.EntityUID, @@ -50,7 +53,7 @@ func Test_EntityStore(t *testing.T) { assert.Contains(t, es.Teams, teamUID) assert.Equal( t, - NewTeam(teamUID, []EntityUID{applicationEUID}), + team.New(teamUID, []entityuid.EntityUID{applicationEUID}), es.Teams[teamUID], ) assert.Equal(t, applicationEUID, es.App.EUID) diff --git a/tinytodo-go/internal/app/server/entitystore/entityuid.go b/tinytodo-go/internal/app/server/entitystore/entityuid.go deleted file mode 100644 index 255b653..0000000 --- a/tinytodo-go/internal/app/server/entitystore/entityuid.go +++ /dev/null @@ -1,114 +0,0 @@ -package entitystore - -import ( - "encoding/json" - "fmt" - "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entitytype" - "github.com/cedar-policy/cedar-go" - "strings" -) - -// EntityUID is a transparent wrapper around cedar.EntityUID used to represent entities in EntityStore, so that we -// can define our own UnmarshalJSON and MarshalJSON functions. -// -// The textual representation of an EntityUID is similar to that of a cedar.EntityUID, for example: -// -// "User::\"kesha\"" -type EntityUID struct { - cedar.EntityUID -} - -// NewEntityUID creates an EntityUID from an entitytype.EntityType and ID. -// -// It is a simple wrapper around cedar.NewEntityUID with constraints on the valid types. -func NewEntityUID(typ entitytype.EntityType, id string) EntityUID { - return EntityUID{ - EntityUID: cedar.NewEntityUID(typ.String(), id), - } -} - -// ParseEntityUID converts the textual representation of an EntityUID into an EntityUID. Additionally, it checks that -// the type of the EntityUID matches one of the enums defined in entitytype.EntityType. -// -// Example textual representation of an EntityUID: -// -// "User::\"kesha\"" -func ParseEntityUID(uid string) (EntityUID, error) { - parts := strings.Split(uid, "::") - if len(parts) != 2 { - return EntityUID{}, fmt.Errorf("wrong number of components, expected %d, got %d", 2, len(parts)) - } - - entityType := entitytype.Parse(parts[0]) - if entityType == entitytype.Unknown { - return EntityUID{}, fmt.Errorf("invalid entity type: %s", parts[0]) - } - - id := parts[1] - if len(id) > 0 && id[0] == '"' { - id = id[1:] - } - if len(id) > 0 && id[len(id)-1] == '"' { - id = id[:len(id)-1] - } - return EntityUID{EntityUID: cedar.NewEntityUID(entityType.String(), id)}, nil -} - -// UnmarshalJSON converts a textual representation of EntityUID into a EntityUID. -// -// For example, -// -// "User::\"kesha\"" -// -// Based on https://pkg.go.dev/encoding/json. -// -// Note that the pointer receiver is not an error -- UnmarshalJSON is supposed to act on pointers. -// -// See https://stackoverflow.com/a/57922284 for an explanation. -func (e *EntityUID) UnmarshalJSON(data []byte) error { - - var v interface{} - if err := json.Unmarshal(data, &v); err != nil { - return err - } - - dataParsed, ok := v.(string) - if !ok { - return fmt.Errorf("entityUID must be a string") - } - - parts := strings.Split(dataParsed, "::") - if len(parts) != 2 { - return fmt.Errorf("wrong number of components, expected %d, got %d", 2, len(parts)) - } - - entityType := entitytype.Parse(parts[0]) - if entityType == entitytype.Unknown { - return fmt.Errorf("invalid entity type: %s", parts[0]) - } - - id := parts[1] - if len(id) > 0 && id[0] == '"' { - id = id[1:] - } - if len(id) > 0 && id[len(id)-1] == '"' { - id = id[:len(id)-1] - } - - e.Type = entityType.String() - e.ID = id - - return nil -} - -// MarshalJSON converts a EntityUID into a textual representation. -// -// For example, "User::\"kesha\"" -// -// Based on https://pkg.go.dev/encoding/json. -// -// Note that the value receiver is not an error -- MarshalJSON is supposed to act on values. -func (e EntityUID) MarshalJSON() ([]byte, error) { - // TODO: make this less awkward - return []byte(fmt.Sprintf("%q", e.EntityUID.String())), nil -} diff --git a/tinytodo-go/internal/app/server/entitystore/entityuid/entityuid.go b/tinytodo-go/internal/app/server/entitystore/entityuid/entityuid.go new file mode 100644 index 0000000..99774ff --- /dev/null +++ b/tinytodo-go/internal/app/server/entitystore/entityuid/entityuid.go @@ -0,0 +1,123 @@ +package entityuid + +import ( + "encoding/json" + "fmt" + "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entitytype" + "github.com/cedar-policy/cedar-go/types" + "strings" +) + +// EntityUID is a transparent wrapper around types.EntityUID used to represent entities in EntityStore, so that we +// can define our own JSON marshaller. +// +// See EntityUID.MarshalJSON to understand why we need to define our own marshaller. +type EntityUID struct { + types.EntityUID +} + +// New creates an EntityUID from an entitytype.EntityType and ID. +// +// It is a simple wrapper around types.NewEntityUID with constraints on the valid types. +func New(typ entitytype.EntityType, id string) EntityUID { + return EntityUID{ + EntityUID: types.NewEntityUID( + types.EntityType(typ.String()), + types.String(id), + ), + } +} + +// Parse converts the Cedar language representation of a types.EntityType into an EntityUID. +// Additionally, it checks that the type of the EntityUID matches one of the enums defined in entitytype.EntityType. +// +// Example input: +// +// "User::\"kesha\"" +func Parse(uid string) (EntityUID, error) { + parts := strings.Split(uid, "::") + if len(parts) != 2 { + return EntityUID{}, fmt.Errorf("wrong number of components, expected %d, got %d", 2, len(parts)) + } + + entityType := entitytype.Parse(parts[0]) + if entityType == entitytype.Unknown { + return EntityUID{}, fmt.Errorf("invalid entity type: %s", parts[0]) + } + + id := parts[1] + if len(id) > 0 && id[0] == '"' { + id = id[1:] + } + if len(id) > 0 && id[len(id)-1] == '"' { + id = id[:len(id)-1] + } + return EntityUID{EntityUID: types.NewEntityUID( + types.EntityType(entityType.String()), + types.String(id), + )}, nil +} + +// UnmarshalJSON converts a Cedar language representation of a types.EntityUID into an EntityUID. +// +// Example input (enclosing double quotes are optional): +// +// "User::\"kesha\"" +// +// We cannot rely on types.EntityUID.UnmarshalJSON because the entities.json and Python client in tinytodo expect +// entities in the Cedar language syntax rather than the JSON syntax (also see +// [Cedar language entities and context syntax] and [this Github issue]). +// +// [Cedar language entities and context syntax]: https://docs.cedarpolicy.com/auth/entities-syntax.html +// [this Github issue]: https://github.com/cedar-policy/cedar-examples/issues/186 +func (e *EntityUID) UnmarshalJSON(data []byte) error { + + var v interface{} + if err := json.Unmarshal(data, &v); err != nil { + return err + } + + dataParsed, ok := v.(string) + if !ok { + return fmt.Errorf("entityUID must be a string") + } + + parts := strings.Split(dataParsed, "::") + if len(parts) != 2 { + return fmt.Errorf("wrong number of components, expected %d, got %d", 2, len(parts)) + } + + entityType := entitytype.Parse(parts[0]) + if entityType == entitytype.Unknown { + return fmt.Errorf("invalid entity type: %s", parts[0]) + } + + id := parts[1] + if len(id) > 0 && id[0] == '"' { + id = id[1:] + } + if len(id) > 0 && id[len(id)-1] == '"' { + id = id[:len(id)-1] + } + + e.Type = types.EntityType(entityType.String()) + e.ID = types.String(id) + + return nil +} + +// MarshalJSON converts a EntityUID into its Cedar language representation. +// +// Example output (enclosing double quotes included): +// +// "User::\"kesha\"" +// +// We cannot rely on types.EntityUID.UnmarshalJSON because the entities.json and Python client in tinytodo expect +// entities in the Cedar language syntax rather than the JSON syntax (also see +// [Cedar language entities and context syntax] and [this Github issue]). +// +// [Cedar language entities and context syntax]: https://docs.cedarpolicy.com/auth/entities-syntax.html +// [this Github issue]: https://github.com/cedar-policy/cedar-examples/issues/186 +func (e EntityUID) MarshalJSON() ([]byte, error) { + return []byte(fmt.Sprintf("%q", e.EntityUID.String())), nil +} diff --git a/tinytodo-go/internal/app/server/entitystore/entityuid_test.go b/tinytodo-go/internal/app/server/entitystore/entityuid/entityuid_test.go similarity index 59% rename from tinytodo-go/internal/app/server/entitystore/entityuid_test.go rename to tinytodo-go/internal/app/server/entitystore/entityuid/entityuid_test.go index 376cf03..fcf54f2 100644 --- a/tinytodo-go/internal/app/server/entitystore/entityuid_test.go +++ b/tinytodo-go/internal/app/server/entitystore/entityuid/entityuid_test.go @@ -1,10 +1,10 @@ -package entitystore +package entityuid import ( "encoding/json" "fmt" "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entitytype" - "github.com/cedar-policy/cedar-go" + "github.com/cedar-policy/cedar-go/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "testing" @@ -13,47 +13,53 @@ import ( func Test_EntityUID(t *testing.T) { t.Run("verify json.Marshaler interface", func(t *testing.T) { var m json.Marshaler - e := NewEntityUID(entitytype.Application, "TinyTodo") + e := New(entitytype.Application, "TinyTodo") m = e require.NotNil(t, m) }) t.Run("verify json.Unmarshaler interface", func(t *testing.T) { var m json.Unmarshaler - e := NewEntityUID(entitytype.Application, "TinyTodo") + e := New(entitytype.Application, "TinyTodo") m = &e require.NotNil(t, m) }) } func TestEntityUID_MarshalJSON(t *testing.T) { - type fields struct { - EntityUID cedar.EntityUID - } tests := []struct { - name string - fields fields - wantErr assert.ErrorAssertionFunc + name string + entityUID types.EntityUID + wantErr assert.ErrorAssertionFunc }{ { "valid user", - fields{EntityUID: cedar.NewEntityUID(entitytype.User.String(), "kesha")}, + types.NewEntityUID( + types.EntityType(entitytype.User.String()), + "kesha", + ), assert.NoError, }, { "valid team", - fields{EntityUID: cedar.NewEntityUID(entitytype.Team.String(), "temp")}, + types.NewEntityUID( + types.EntityType(entitytype.Team.String()), + "temp", + ), assert.NoError, }, { "valid application", - fields{EntityUID: cedar.NewEntityUID(entitytype.Application.String(), "TinyTodo")}, + types.NewEntityUID( + types.EntityType(entitytype.Application.String()), + "TinyTodo", + ), assert.NoError, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { e := EntityUID{ - EntityUID: tt.fields.EntityUID, + EntityUID: tt.entityUID, } got, err := e.MarshalJSON() if !tt.wantErr(t, err, fmt.Sprintf("MarshalJSON()")) { @@ -70,22 +76,19 @@ func TestEntityUID_MarshalJSON(t *testing.T) { } } -func Test_ParseEntityUID(t *testing.T) { - type args struct { - euid string - } +func Test_Parse(t *testing.T) { tests := []struct { name string - args args + euid string want EntityUID wantErr assert.ErrorAssertionFunc }{ { name: "valid user ID", - args: args{euid: "User::\"kesha\""}, + euid: "User::\"kesha\"", want: EntityUID{ - cedar.EntityUID{ - Type: entitytype.User.String(), + types.EntityUID{ + Type: types.EntityType(entitytype.User.String()), ID: "kesha", }, }, @@ -93,10 +96,10 @@ func Test_ParseEntityUID(t *testing.T) { }, { name: "valid user ID without quotes", - args: args{euid: "User::kesha"}, + euid: "User::kesha", want: EntityUID{ - cedar.EntityUID{ - Type: entitytype.User.String(), + types.EntityUID{ + Type: types.EntityType(entitytype.User.String()), ID: "kesha", }, }, @@ -104,10 +107,10 @@ func Test_ParseEntityUID(t *testing.T) { }, { name: "valid team ID", - args: args{euid: "Team::\"temp\""}, + euid: "Team::\"temp\"", want: EntityUID{ - cedar.EntityUID{ - Type: entitytype.Team.String(), + types.EntityUID{ + Type: types.EntityType(entitytype.Team.String()), ID: "temp", }, }, @@ -115,10 +118,10 @@ func Test_ParseEntityUID(t *testing.T) { }, { name: "valid application ID", - args: args{euid: "Application::\"TinyTodo\""}, + euid: "Application::\"TinyTodo\"", want: EntityUID{ - cedar.EntityUID{ - Type: entitytype.Application.String(), + types.EntityUID{ + Type: types.EntityType(entitytype.Application.String()), ID: "TinyTodo", }, }, @@ -126,24 +129,24 @@ func Test_ParseEntityUID(t *testing.T) { }, { name: "invalid ID", - args: args{euid: "::"}, + euid: "::", want: EntityUID{}, wantErr: assert.Error, }, { name: "malformed ID", - args: args{euid: "Application:\"TinyTodo\""}, + euid: "Application:\"TinyTodo\"", want: EntityUID{}, wantErr: assert.Error, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := ParseEntityUID(tt.args.euid) - if !tt.wantErr(t, err, fmt.Sprintf("ParseEntityUID(%v)", tt.args.euid)) { + got, err := Parse(tt.euid) + if !tt.wantErr(t, err, fmt.Sprintf("Parse(%v)", tt.euid)) { return } - assert.Equalf(t, tt.want, got, "ParseEntityUID(%v)", tt.args.euid) + assert.Equalf(t, tt.want, got, "Parse(%v)", tt.euid) }) } } diff --git a/tinytodo-go/internal/app/server/entitystore/list_test.go b/tinytodo-go/internal/app/server/entitystore/list_test.go deleted file mode 100644 index acdfbc1..0000000 --- a/tinytodo-go/internal/app/server/entitystore/list_test.go +++ /dev/null @@ -1,103 +0,0 @@ -package entitystore - -import ( - "encoding/json" - "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entitytype" - "github.com/cedar-policy/cedar-go" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "testing" -) - -func TestList(t *testing.T) { - t.Run("check interface", func(t *testing.T) { - var e Entity - l := NewList( - ListUID{ - EntityUID: EntityUID{ - EntityUID: cedar.EntityUID{ - Type: entitytype.List.String(), - ID: "1", - }, - }, - }, - "Cedar blog post", - UserUID{ - EntityUID: EntityUID{ - EntityUID: cedar.EntityUID{ - Type: entitytype.User.String(), - ID: "kesha", - }, - }, - }, - TeamUID{ - EntityUID: EntityUID{ - EntityUID: cedar.EntityUID{ - Type: entitytype.Team.String(), - ID: "temp", - }, - }, - }, - TeamUID{ - EntityUID: EntityUID{ - EntityUID: cedar.EntityUID{ - Type: entitytype.Team.String(), - ID: "admin", - }, - }, - }, - nil, - ) - e = l - require.NotNil(t, e) - }) -} - -func TestList_Marshal(t *testing.T) { - t.Run("check marshal valid case", func(t *testing.T) { - list := NewList( - ListUID{ - EntityUID: EntityUID{ - EntityUID: cedar.EntityUID{ - Type: entitytype.List.String(), - ID: "1", - }, - }, - }, - "Cedar blog post", - UserUID{ - EntityUID: EntityUID{ - EntityUID: cedar.EntityUID{ - Type: entitytype.User.String(), - ID: "kesha", - }, - }, - }, - TeamUID{ - EntityUID: EntityUID{ - EntityUID: cedar.EntityUID{ - Type: entitytype.Team.String(), - ID: "temp", - }, - }, - }, - TeamUID{ - EntityUID: EntityUID{ - EntityUID: cedar.EntityUID{ - Type: entitytype.Team.String(), - ID: "admin", - }, - }, - }, - nil, - ) - - got, err := json.MarshalIndent(list, "", " ") - require.NoError(t, err) - - var recovered List - require.NoError(t, json.Unmarshal(got, &recovered)) - - assert.Equal(t, *list, recovered) - }) -} diff --git a/tinytodo-go/internal/app/server/entitystore/taskstate/taskstate.go b/tinytodo-go/internal/app/server/entitystore/taskstate/taskstate.go index 72a39a2..13eec06 100644 --- a/tinytodo-go/internal/app/server/entitystore/taskstate/taskstate.go +++ b/tinytodo-go/internal/app/server/entitystore/taskstate/taskstate.go @@ -37,11 +37,10 @@ func Parse(et string) TaskState { return Unknown } -// MarshalJSON converts a TaskState into a textual representation. +// MarshalJSON converts a TaskState into its Cedar language representation. // -// Based on https://pkg.go.dev/encoding/json. -// -// Note that the value receiver is not an error -- MarshalJSON is supposed to act on values. +// We override the default MarshalJSON because we want to marshal TaskState as a string (e.g., "Checked") instead of +// an integer (e.g., 1). func (t TaskState) MarshalJSON() ([]byte, error) { return []byte(fmt.Sprintf("%q", t.String())), nil } diff --git a/tinytodo-go/internal/app/server/entitystore/user.go b/tinytodo-go/internal/app/server/entitystore/user.go deleted file mode 100644 index 7284ee5..0000000 --- a/tinytodo-go/internal/app/server/entitystore/user.go +++ /dev/null @@ -1,53 +0,0 @@ -package entitystore - -import ( - "github.com/cedar-policy/cedar-go" -) - -// UserUID is a transparent wrapper around EntityUID, to make it clear that we want a User's EntityUID. -// -// Note that we use inheritance instead of alias, because we want to inherit methods. See [blog post]. -// -// [blog post]: https://sentry.io/answers/alias-type-definitions/ -type UserUID struct { - EntityUID -} - -// User represents the user entity. -type User struct { - EUID UserUID `json:"euid"` // note the spelling - Location string `json:"location"` - JobLevel int `json:"joblevel"` - Parents []EntityUID `json:"parents"` -} - -// NewUser creates a new User; if parents is nil, we create an empty slice so that there will be no problems with -// client processing. -func NewUser(uid UserUID, location string, jobLevel int, parents []EntityUID) *User { - if parents == nil { - parents = []EntityUID{} - } - return &User{ - EUID: uid, - Location: location, - JobLevel: jobLevel, - Parents: parents, - } -} - -// AsCedarEntity converts User into a cedar.Entity, to be passed to the Cedar authorization engine when it evaluates a -// request. -func (u *User) AsCedarEntity() *cedar.Entity { - var parents []cedar.EntityUID - for _, parent := range u.Parents { - parents = append(parents, parent.EntityUID) - } - return &cedar.Entity{ - UID: u.EUID.EntityUID.EntityUID, - Parents: parents, - Attributes: cedar.Record{ - "location": cedar.String(u.Location), - "joblevel": cedar.Long(u.JobLevel), - }, - } -} diff --git a/tinytodo-go/internal/app/server/httphandlersapi.go b/tinytodo-go/internal/app/server/httphandlersapi.go index d4a234c..c259641 100644 --- a/tinytodo-go/internal/app/server/httphandlersapi.go +++ b/tinytodo-go/internal/app/server/httphandlersapi.go @@ -3,8 +3,11 @@ package server import ( "encoding/json" "fmt" - "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore" "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/action" + "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entity/list" + "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entity/team" + "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entity/user" + "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entityuid" "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/role" "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/taskstate" "log/slog" @@ -27,7 +30,7 @@ func (s *Server) handleGetHome(w http.ResponseWriter, r *http.Request) { // ] func (s *Server) handleGetAPIListsGet(w http.ResponseWriter, r *http.Request) { - userUID_, err := entitystore.ParseEntityUID(r.URL.Query().Get("uid")) + userUID_, err := entityuid.Parse(r.URL.Query().Get("uid")) if err != nil { s.logger.InfoContext( r.Context(), @@ -46,7 +49,7 @@ func (s *Server) handleGetAPIListsGet(w http.ResponseWriter, r *http.Request) { return } - userUID := entitystore.UserUID{EntityUID: userUID_} + userUID := user.UserUID{EntityUID: userUID_} s.logger.InfoContext( r.Context(), @@ -94,7 +97,7 @@ func (s *Server) handleGetAPIListsGet(w http.ResponseWriter, r *http.Request) { } // client expects allowedLists to be an empty list (vs nil), don't convert to var declaration - allowedLists := []*entitystore.List{} + allowedLists := []*list.List{} for listEUID, list := range s.es.Lists { listAllowed, _, err := s.isAuthorized( @@ -153,7 +156,7 @@ func (s *Server) handlePostAPIListCreate(w http.ResponseWriter, r *http.Request) return } - userUID_, err := entitystore.ParseEntityUID(req.UID) + userUID_, err := entityuid.Parse(req.UID) if err != nil { s.logger.InfoContext( r.Context(), @@ -172,7 +175,7 @@ func (s *Server) handlePostAPIListCreate(w http.ResponseWriter, r *http.Request) return } - userUID := entitystore.UserUID{EntityUID: userUID_} + userUID := user.UserUID{EntityUID: userUID_} s.logger.InfoContext( r.Context(), @@ -234,7 +237,7 @@ func (s *Server) handlePostAPIListCreate(w http.ResponseWriter, r *http.Request) slog.Any("newEditor", newEditor), ) - s.es.Lists[listUID] = entitystore.NewList( + s.es.Lists[listUID] = list.New( listUID, req.Name, userUID, @@ -253,7 +256,7 @@ func (s *Server) handlePostAPIListCreate(w http.ResponseWriter, r *http.Request) // // {'uid': 'List::"0"', 'owner': 'User::"andrew"', 'name': 'a', 'tasks': [], 'readers': 'Team::"1"', 'editors': 'Team::"2"'} func (s *Server) handleGetAPIListGet(w http.ResponseWriter, r *http.Request) { - userUID_, err := entitystore.ParseEntityUID(r.URL.Query().Get("uid")) + userUID_, err := entityuid.Parse(r.URL.Query().Get("uid")) if err != nil { s.logger.InfoContext( r.Context(), @@ -272,9 +275,9 @@ func (s *Server) handleGetAPIListGet(w http.ResponseWriter, r *http.Request) { return } - userUID := entitystore.UserUID{EntityUID: userUID_} + userUID := user.UserUID{EntityUID: userUID_} - listUID_, err := entitystore.ParseEntityUID(r.URL.Query().Get("list")) + listUID_, err := entityuid.Parse(r.URL.Query().Get("list")) if err != nil { s.logger.InfoContext( r.Context(), @@ -293,7 +296,7 @@ func (s *Server) handleGetAPIListGet(w http.ResponseWriter, r *http.Request) { return } - listUID := entitystore.ListUID{EntityUID: listUID_} + listUID := list.ListUID{EntityUID: listUID_} s.logger.InfoContext( r.Context(), @@ -382,7 +385,7 @@ func (s *Server) handlePostAPITaskCreate(w http.ResponseWriter, r *http.Request) return } - userUID_, err := entitystore.ParseEntityUID(req.UID) + userUID_, err := entityuid.Parse(req.UID) if err != nil { s.logger.InfoContext( r.Context(), @@ -401,9 +404,9 @@ func (s *Server) handlePostAPITaskCreate(w http.ResponseWriter, r *http.Request) return } - userUID := entitystore.UserUID{EntityUID: userUID_} + userUID := user.UserUID{EntityUID: userUID_} - listUID_, err := entitystore.ParseEntityUID(req.List) + listUID_, err := entityuid.Parse(req.List) if err != nil { s.logger.InfoContext( r.Context(), @@ -422,7 +425,7 @@ func (s *Server) handlePostAPITaskCreate(w http.ResponseWriter, r *http.Request) return } - listUID := entitystore.ListUID{EntityUID: listUID_} + listUID := list.ListUID{EntityUID: listUID_} s.logger.InfoContext( r.Context(), @@ -532,7 +535,7 @@ func (s *Server) handlePostAPITaskUpdate(w http.ResponseWriter, r *http.Request) return } - userUID_, err := entitystore.ParseEntityUID(req.UID) + userUID_, err := entityuid.Parse(req.UID) if err != nil { s.logger.InfoContext( r.Context(), @@ -551,9 +554,9 @@ func (s *Server) handlePostAPITaskUpdate(w http.ResponseWriter, r *http.Request) return } - userUID := entitystore.UserUID{EntityUID: userUID_} + userUID := user.UserUID{EntityUID: userUID_} - listUID_, err := entitystore.ParseEntityUID(req.List) + listUID_, err := entityuid.Parse(req.List) if err != nil { s.logger.InfoContext( r.Context(), @@ -572,7 +575,7 @@ func (s *Server) handlePostAPITaskUpdate(w http.ResponseWriter, r *http.Request) return } - listUID := entitystore.ListUID{EntityUID: listUID_} + listUID := list.ListUID{EntityUID: listUID_} taskState := taskstate.Parse(req.State) if taskState == taskstate.Unknown { @@ -728,7 +731,7 @@ func (s *Server) handleDeleteAPITaskDelete(w http.ResponseWriter, r *http.Reques return } - userUID_, err := entitystore.ParseEntityUID(req.UID) + userUID_, err := entityuid.Parse(req.UID) if err != nil { s.logger.InfoContext( r.Context(), @@ -747,9 +750,9 @@ func (s *Server) handleDeleteAPITaskDelete(w http.ResponseWriter, r *http.Reques return } - userUID := entitystore.UserUID{EntityUID: userUID_} + userUID := user.UserUID{EntityUID: userUID_} - listUID_, err := entitystore.ParseEntityUID(req.List) + listUID_, err := entityuid.Parse(req.List) if err != nil { s.logger.InfoContext( r.Context(), @@ -768,7 +771,7 @@ func (s *Server) handleDeleteAPITaskDelete(w http.ResponseWriter, r *http.Reques return } - listUID := entitystore.ListUID{EntityUID: listUID_} + listUID := list.ListUID{EntityUID: listUID_} s.logger.InfoContext( r.Context(), @@ -903,7 +906,7 @@ func (s *Server) handleDeleteAPIListDelete(w http.ResponseWriter, r *http.Reques return } - userUID_, err := entitystore.ParseEntityUID(req.UID) + userUID_, err := entityuid.Parse(req.UID) if err != nil { s.logger.InfoContext( r.Context(), @@ -922,9 +925,9 @@ func (s *Server) handleDeleteAPIListDelete(w http.ResponseWriter, r *http.Reques return } - userUID := entitystore.UserUID{EntityUID: userUID_} + userUID := user.UserUID{EntityUID: userUID_} - listUID_, err := entitystore.ParseEntityUID(req.List) + listUID_, err := entityuid.Parse(req.List) if err != nil { s.logger.InfoContext( r.Context(), @@ -943,7 +946,7 @@ func (s *Server) handleDeleteAPIListDelete(w http.ResponseWriter, r *http.Reques return } - listUID := entitystore.ListUID{EntityUID: listUID_} + listUID := list.ListUID{EntityUID: listUID_} s.logger.InfoContext( r.Context(), @@ -1046,7 +1049,7 @@ func (s *Server) handlePostAPIShare(w http.ResponseWriter, r *http.Request) { return } - userUID_, err := entitystore.ParseEntityUID(req.UID) + userUID_, err := entityuid.Parse(req.UID) if err != nil { s.logger.InfoContext( r.Context(), @@ -1065,9 +1068,9 @@ func (s *Server) handlePostAPIShare(w http.ResponseWriter, r *http.Request) { return } - userUID := entitystore.UserUID{EntityUID: userUID_} + userUID := user.UserUID{EntityUID: userUID_} - listUID_, err := entitystore.ParseEntityUID(req.List) + listUID_, err := entityuid.Parse(req.List) if err != nil { s.logger.InfoContext( r.Context(), @@ -1086,9 +1089,9 @@ func (s *Server) handlePostAPIShare(w http.ResponseWriter, r *http.Request) { return } - listUID := entitystore.ListUID{EntityUID: listUID_} + listUID := list.ListUID{EntityUID: listUID_} - shareWith, err := entitystore.ParseEntityUID(req.ShareWith) // can be User or Team + shareWith, err := entityuid.Parse(req.ShareWith) // can be User or Team if err != nil { s.logger.InfoContext( r.Context(), @@ -1178,7 +1181,7 @@ func (s *Server) handlePostAPIShare(w http.ResponseWriter, r *http.Request) { list := s.es.Lists[listUID] - var roleTeamUID entitystore.TeamUID + var roleTeamUID team.TeamUID switch rr { case role.Editor: @@ -1196,14 +1199,14 @@ func (s *Server) handlePostAPIShare(w http.ResponseWriter, r *http.Request) { return } - if shareWithTeam, ok := s.es.Teams[entitystore.TeamUID{EntityUID: shareWith}]; ok { + if shareWithTeam, ok := s.es.Teams[team.TeamUID{EntityUID: shareWith}]; ok { s.logger.InfoContext( r.Context(), "found share_with Team entity", slog.Any("shareWithTeam", shareWithTeam), ) shareWithTeam.Parents = append(shareWithTeam.Parents, roleTeamUID.EntityUID) - } else if shareWithUser, ok := s.es.Users[entitystore.UserUID{EntityUID: shareWith}]; ok { + } else if shareWithUser, ok := s.es.Users[user.UserUID{EntityUID: shareWith}]; ok { s.logger.InfoContext( r.Context(), "found share_with User entity", @@ -1245,7 +1248,7 @@ type apiUnshareRequest struct { UnshareWith string `json:"unshare_with"` // UserUID or TeamUID to unshare with } -func removeParent(parents []entitystore.EntityUID, remove entitystore.EntityUID) []entitystore.EntityUID { +func removeParent(parents []entityuid.EntityUID, remove entityuid.EntityUID) []entityuid.EntityUID { found := -1 @@ -1288,7 +1291,7 @@ func (s *Server) handleDeleteAPIShare(w http.ResponseWriter, r *http.Request) { return } - userUID_, err := entitystore.ParseEntityUID(req.UID) + userUID_, err := entityuid.Parse(req.UID) if err != nil { s.logger.InfoContext( r.Context(), @@ -1307,9 +1310,9 @@ func (s *Server) handleDeleteAPIShare(w http.ResponseWriter, r *http.Request) { return } - userUID := entitystore.UserUID{EntityUID: userUID_} + userUID := user.UserUID{EntityUID: userUID_} - listUID_, err := entitystore.ParseEntityUID(req.List) + listUID_, err := entityuid.Parse(req.List) if err != nil { s.logger.InfoContext( r.Context(), @@ -1328,9 +1331,9 @@ func (s *Server) handleDeleteAPIShare(w http.ResponseWriter, r *http.Request) { return } - listUID := entitystore.ListUID{EntityUID: listUID_} + listUID := list.ListUID{EntityUID: listUID_} - unshareWith, err := entitystore.ParseEntityUID(req.UnshareWith) // can be User or Team + unshareWith, err := entityuid.Parse(req.UnshareWith) // can be User or Team if err != nil { s.logger.InfoContext( r.Context(), @@ -1420,7 +1423,7 @@ func (s *Server) handleDeleteAPIShare(w http.ResponseWriter, r *http.Request) { list := s.es.Lists[listUID] - var roleTeamUID entitystore.TeamUID + var roleTeamUID team.TeamUID switch rr { case role.Editor: @@ -1438,7 +1441,7 @@ func (s *Server) handleDeleteAPIShare(w http.ResponseWriter, r *http.Request) { return } - if unshareWithTeam, ok := s.es.Teams[entitystore.TeamUID{EntityUID: unshareWith}]; ok { + if unshareWithTeam, ok := s.es.Teams[team.TeamUID{EntityUID: unshareWith}]; ok { s.logger.InfoContext( r.Context(), "found unshare_with Team entity", @@ -1451,7 +1454,7 @@ func (s *Server) handleDeleteAPIShare(w http.ResponseWriter, r *http.Request) { slog.Any("unshareWithTeam", unshareWithTeam), slog.Any("removed", roleTeamUID), ) - } else if unshareWithUser, ok := s.es.Users[entitystore.UserUID{EntityUID: unshareWith}]; ok { + } else if unshareWithUser, ok := s.es.Users[user.UserUID{EntityUID: unshareWith}]; ok { s.logger.InfoContext( r.Context(), "found unshare_with User entity", diff --git a/tinytodo-go/internal/app/server/server.go b/tinytodo-go/internal/app/server/server.go index a179d39..65a2db7 100644 --- a/tinytodo-go/internal/app/server/server.go +++ b/tinytodo-go/internal/app/server/server.go @@ -2,7 +2,9 @@ package server import ( "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore" + "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entity/app" "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entitytype" + "github.com/cedar-policy/cedar-examples/tinytodo-go/internal/app/server/entitystore/entityuid" "github.com/cedar-policy/cedar-go" "log/slog" "net" @@ -19,7 +21,7 @@ var ( }, ), ) - ApplicationEntityUID = entitystore.App{EUID: entitystore.NewEntityUID(entitytype.Application, "TinyTodo")} + ApplicationEntityUID = app.App{EUID: entityuid.New(entitytype.Application, "TinyTodo")} ) // Server represents the web server that host the booking app. @@ -29,7 +31,7 @@ type Server struct { // authorization es *entitystore.EntityStore - ps cedar.PolicySet + ps *cedar.PolicySet } // Serve starts a HTTP web server. @@ -52,7 +54,7 @@ func (s *Server) Serve() error { // New creates a new Server with the provided address (addr). // // addr follows the rules of net.Listen (https://pkg.go.dev/net#Listen). -func New(addr string, es *entitystore.EntityStore, ps cedar.PolicySet, opts ...Option) (*Server, error) { +func New(addr string, es *entitystore.EntityStore, ps *cedar.PolicySet, opts ...Option) (*Server, error) { s := &Server{ addr: addr, logger: DefaultLogger,