From 60d4db8ce08900d3ccaeb3be6b8904b02f532109 Mon Sep 17 00:00:00 2001 From: Mark Dickson Jr Date: Sun, 16 Feb 2020 16:12:20 -0500 Subject: [PATCH] API Base path added to config, group ext work The group extension now allows for adding the default routes we would expect someone to use --- README.md | 2 +- application.go | 1 + configuration.go | 1 + model.go | 15 +++ response.go | 11 ++ sample/main.go | 2 +- user/auth/local/extension.go | 14 +-- user/group/composite.go | 22 ++-- user/group/enforce.go | 78 +++++++++++++++ user/group/extension.go | 104 ++++--------------- user/group/group.go | 36 +++++++ user/group/membership.go | 55 ++++++++++ user/group/privilege.go | 188 +++++++++++++++++++++++++++++++++-- user/message/extension.go | 8 +- 14 files changed, 421 insertions(+), 116 deletions(-) create mode 100644 user/group/enforce.go diff --git a/README.md b/README.md index 4f4601a..029ef7c 100755 --- a/README.md +++ b/README.md @@ -149,7 +149,7 @@ To use, make something like build/main.go in your app, which contains: ```go import "github.com/markdicksonjr/nibbler/build" ... -build.ProcessDefaultTargets("BigCommerceSync", "main/main.go") +build.ProcessDefaultTargets("BinaryName", "main/main.go") ``` This will build your app for a few platforms (currently, Windows, darwin, linux). diff --git a/application.go b/application.go index e2ff135..b8431db 100755 --- a/application.go +++ b/application.go @@ -42,6 +42,7 @@ type Configuration struct { Headers HeaderConfiguration Port int Raw config.Config + ApiPrefix string StaticDirectory string } diff --git a/configuration.go b/configuration.go index 88e61e1..b82f685 100755 --- a/configuration.go +++ b/configuration.go @@ -71,6 +71,7 @@ func LoadConfiguration(sources ...source.Source) (*Configuration, error) { Raw: conf, Port: primaryPort, StaticDirectory: conf.Get("nibbler", "directory", "static").String("./public/"), + ApiPrefix: conf.Get("nibbler", "api", "prefix").String("/api"), Headers: HeaderConfiguration{ AccessControlAllowOrigin: conf.Get("nibbler", "ac", "allow", "origin").String("*"), AccessControlAllowMethods: conf.Get("nibbler", "ac", "allow", "methods").String("GET, POST, OPTIONS, PUT, PATCH, DELETE"), diff --git a/model.go b/model.go index fe7c69a..59506ff 100644 --- a/model.go +++ b/model.go @@ -94,3 +94,18 @@ type GroupPrivilege struct { ResourceID string `json:"resourceID"` // e.g. "customers" ID Action string `json:"action"` // e.g. read/write/admin/etc } + +// common interfaces extensions can use if needed + +type SearchParameters struct { + Query interface{} `json:"query,omitempty"` + Offset *int `json:"offset,omitempty"` + Size *int `json:"size,omitempty"` + IncludeTotal bool `json:"includeTotal"` +} + +type SearchResults struct { + Hits interface{} `json:"hits"` + Offset *int `json:"offset,omitempty"` + Total *int `json:"total,omitempty"` +} diff --git a/response.go b/response.go index 9b4c12f..26e18fe 100644 --- a/response.go +++ b/response.go @@ -14,6 +14,17 @@ func Write200Json(w http.ResponseWriter, content string) { WriteJson(w, content, http.StatusOK) } +// Write201Json is some syntactic sugar to allow for a quick way to write JSON responses with an OK code +func Write201Json(w http.ResponseWriter, content string) { + WriteJson(w, content, http.StatusCreated) +} + +// Write204 is some syntactic sugar to allow for a quick way to write JSON responses with a no-content code +func Write204(w http.ResponseWriter, content string) { + w.WriteHeader(http.StatusNoContent) + w.Write(nil) +} + // Write401Json func Write401Json(w http.ResponseWriter) { WriteJson(w, `{"result": "not authorized"}`, http.StatusUnauthorized) diff --git a/sample/main.go b/sample/main.go index 32c606b..b0752c3 100755 --- a/sample/main.go +++ b/sample/main.go @@ -16,7 +16,7 @@ type SampleExtension struct { // PostInit just adds a REST endpoint at "/api/ok" to serve up a simple health-check type message func (s *SampleExtension) PostInit(context *nibbler.Application) error { - context.Router.HandleFunc("/api/ok", func(w http.ResponseWriter, _ *http.Request) { + context.Router.HandleFunc(context.Config.ApiPrefix + "/ok", func(w http.ResponseWriter, _ *http.Request) { nibbler.Write200Json(w, `{"result": "OK"}`) }).Methods("GET") return nil diff --git a/user/auth/local/extension.go b/user/auth/local/extension.go index 929aa71..7582fd4 100644 --- a/user/auth/local/extension.go +++ b/user/auth/local/extension.go @@ -91,17 +91,17 @@ func (s *Extension) Init(app *nibbler.Application) error { } func (s *Extension) PostInit(app *nibbler.Application) error { - app.Router.HandleFunc("/api/user", s.GetCurrentUserHandler).Methods("GET") - app.Router.HandleFunc("/api/login", s.LoginFormHandler).Methods("POST") - app.Router.HandleFunc("/api/logout", s.LogoutHandler).Methods("POST", "GET") - app.Router.HandleFunc("/api/password/reset-token", s.ResetPasswordTokenHandler).Methods("POST") - app.Router.HandleFunc("/api/password", s.ResetPasswordHandler).Methods("POST") + app.Router.HandleFunc(app.Config.ApiPrefix + "/user", s.GetCurrentUserHandler).Methods("GET") + app.Router.HandleFunc(app.Config.ApiPrefix + "/login", s.LoginFormHandler).Methods("POST") + app.Router.HandleFunc(app.Config.ApiPrefix + "/logout", s.LogoutHandler).Methods("POST", "GET") + app.Router.HandleFunc(app.Config.ApiPrefix + "/password/reset-token", s.ResetPasswordTokenHandler).Methods("POST") + app.Router.HandleFunc(app.Config.ApiPrefix + "/password", s.ResetPasswordHandler).Methods("POST") if s.RegistrationEnabled { - app.Router.HandleFunc("/api/register", s.RegisterFormHandler).Methods("POST") + app.Router.HandleFunc(app.Config.ApiPrefix + "/register", s.RegisterFormHandler).Methods("POST") if s.EmailVerificationEnabled { - app.Router.HandleFunc("/api/email/validate", s.EmailTokenVerifyHandler).Methods("POST") + app.Router.HandleFunc(app.Config.ApiPrefix + "/email/validate", s.EmailTokenVerifyHandler).Methods("POST") } } return nil diff --git a/user/group/composite.go b/user/group/composite.go index cb9d486..8119479 100644 --- a/user/group/composite.go +++ b/user/group/composite.go @@ -7,16 +7,17 @@ import ( "net/http" ) +// UserComposite is a summary of group memberships and the state of the "current" group and role for the user type UserComposite struct { - CurrentGroup *nibbler.Group `json:"currentGroup"` - Groups []nibbler.Group `json:"groups"` - RoleInCurrentGroup string `json:"roleInCurrentGroup"` + CurrentGroup *nibbler.Group `json:"currentGroup"` + Groups []nibbler.Group `json:"groups"` + RoleInCurrentGroup string `json:"roleInCurrentGroup"` } -// LoadUserCompositeRequestHandler gets the composite for the given user - you can either allow them to specify the ID +// GetUserCompositeRequestHandler gets the composite for the given user - you can either allow them to specify the ID // as a path param, or just always make it return the composite for the caller. If you allow the ID to be specified, // protect this route with a check to see if the caller can ask for that user's composite info. -func (s *Extension) LoadUserCompositeRequestHandler(w http.ResponseWriter, r *http.Request) { +func (s *Extension) GetUserCompositeRequestHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) id := vars["id"] @@ -31,7 +32,7 @@ func (s *Extension) LoadUserCompositeRequestHandler(w http.ResponseWriter, r *ht id = caller.ID } - composite, err := s.LoadUserComposite(id) + composite, err := s.GetUserComposite(id) // fail on any error if err != nil { @@ -50,12 +51,13 @@ func (s *Extension) LoadUserCompositeRequestHandler(w http.ResponseWriter, r *ht nibbler.Write200Json(w, string(compositeJson)) } -func (s *Extension) LoadUserComposite(userId string) (*UserComposite, error) { +// GetUserComposite returns the composite for the user with the given id +func (s *Extension) GetUserComposite(userId string) (*UserComposite, error) { // prepare a composite instance to return composite := UserComposite{ - Groups: make([]nibbler.Group, 0), - CurrentGroup: nil, + Groups: make([]nibbler.Group, 0), + CurrentGroup: nil, } // load the user configuration @@ -96,7 +98,7 @@ func (s *Extension) LoadUserComposite(userId string) (*UserComposite, error) { // set the current group in the composite model we're returning // TODO: check results of GetGroups above to avoid doing this load if u.CurrentGroupID != nil { - g, err := s.GetGroups([]string{ *u.CurrentGroupID }, true) + g, err := s.GetGroups([]string{*u.CurrentGroupID}, true) if err != nil { return nil, err } diff --git a/user/group/enforce.go b/user/group/enforce.go new file mode 100644 index 0000000..7503d1f --- /dev/null +++ b/user/group/enforce.go @@ -0,0 +1,78 @@ +package nibbler_user_group + +import ( + "github.com/markdicksonjr/nibbler" + "net/http" +) + +// EnforceHasPrivilege will use HasPrivilege to produce a result for the caller - it will return a 500 if something +// went wrong, a 401 if no user is authenticated, a 404 if there is no access. It will pass through to the routerFunc +// if the caller has access +func (s *Extension) EnforceHasPrivilege(action string, routerFunc func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + caller, err := s.SessionExtension.GetCaller(r) + if err != nil { + nibbler.Write500Json(w, err.Error()) + return + } + + if caller == nil { + nibbler.Write401Json(w) + return + } + + if has, err := s.HasPrivilege(caller.ID, action); err != nil { + nibbler.Write500Json(w, err.Error()) + } else if !has { + nibbler.Write404Json(w) + } else { + routerFunc(w, r) + } + } +} + +// EnforceHasPrivilegeOnResource will use HasPrivilegeOnResource to produce a result for the caller - it will return +// a 500 if something went wrong, a 401 if no user is authenticated, a 404 if there is no access. It will pass through +// to the routerFunc if the caller has access +func (s *Extension) EnforceHasPrivilegeOnResource(action string, getResourceIdFn func(r *http.Request) (string, error), routerFunc func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + caller, err := s.SessionExtension.GetCaller(r) + if err != nil { + nibbler.Write500Json(w, err.Error()) + return + } + + if caller == nil { + nibbler.Write401Json(w) + return + } + + targetGroup, err := getResourceIdFn(r) + if err != nil { + nibbler.Write500Json(w, err.Error()) + return + } + + if targetGroup == "" { + nibbler.Write404Json(w) + return + } + + // if the user does not have the privilege on the target resource, fall back to the "global" version of that privilege + if has, err := s.HasPrivilegeOnResource(caller.ID, targetGroup, action); err != nil { + nibbler.Write500Json(w, err.Error()) + } else if !has { + + // this is the check against the global privilege for this action (e.g. admins, etc) + if has, err := s.HasPrivilege(caller.ID, action); err != nil { + nibbler.Write500Json(w, err.Error()) + } else if !has { + nibbler.Write404Json(w) + } else { + routerFunc(w, r) + } + } else { + routerFunc(w, r) + } + } +} diff --git a/user/group/extension.go b/user/group/extension.go index 87ba14a..8285053 100644 --- a/user/group/extension.go +++ b/user/group/extension.go @@ -1,6 +1,7 @@ package nibbler_user_group import ( + "github.com/gorilla/mux" "github.com/markdicksonjr/nibbler" "github.com/markdicksonjr/nibbler/session" "github.com/markdicksonjr/nibbler/user" @@ -14,9 +15,12 @@ type PersistenceExtension interface { GetGroupMembershipsForUser(id string) ([]nibbler.GroupMembership, error) SetGroupMembership(groupId string, userId string, role string) (nibbler.GroupMembership, error) CreateGroup(group nibbler.Group) error + DeleteGroup(groupId string, hardDelete bool) error + SearchGroups(query nibbler.SearchParameters, includePrivileges bool) (nibbler.SearchResults, error) GetGroupsById(ids []string, includePrivileges bool) ([]nibbler.Group, error) - AddPrivilegeToGroups(groupIdList []string, targetGroupId string, action string) error + AddPrivilegeToGroups(groupIdList []string, resourceId string, action string) error GetPrivilegesForAction(groupId string, resourceId *string, action string) ([]nibbler.GroupPrivilege, error) + DeletePrivilege(id string) error } type Extension struct { @@ -24,100 +28,30 @@ type Extension struct { PersistenceExtension PersistenceExtension SessionExtension *session.Extension UserExtension *user.Extension + DisableDefaultRoutes bool } func (s *Extension) GetName() string { return "user-group" } -func (s *Extension) EnforceHasPrivilege(action string, routerFunc func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - caller, err := s.SessionExtension.GetCaller(r) - if err != nil { - nibbler.Write500Json(w, err.Error()) - return - } - - if caller == nil { - nibbler.Write404Json(w) - return - } - - userFromDb, err := s.UserExtension.GetUserById(caller.ID) - if err != nil { - nibbler.Write500Json(w, err.Error()) - return - } - - if userFromDb == nil || userFromDb.CurrentGroupID == nil { - nibbler.Write404Json(w) - return - } - - privileges, err := s.PersistenceExtension.GetPrivilegesForAction(*userFromDb.CurrentGroupID, nil, action) - if err != nil { - nibbler.Write500Json(w, err.Error()) - return - } - if len(privileges) == 0 { - nibbler.Write404Json(w) - return - } - - routerFunc(w, r) +func GetParamValueFromRequest(paramName string) func(r *http.Request) (s string, err error) { + return func(r *http.Request) (s string, err error) { + return mux.Vars(r)[paramName], nil } } -func (s *Extension) EnforceHasPrivilegeOnResource(action string, getResourceIdFn func(r *http.Request) (string, error), routerFunc func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - caller, err := s.SessionExtension.GetCaller(r) - if err != nil { - nibbler.Write500Json(w, err.Error()) - return - } - - if caller == nil { - nibbler.Write404Json(w) - return - } - - userFromDb, err := s.UserExtension.GetUserById(caller.ID) - if err != nil { - nibbler.Write500Json(w, err.Error()) - return - } - - if userFromDb == nil || userFromDb.CurrentGroupID == nil { - nibbler.Write404Json(w) - return - } - - targetGroup, err := getResourceIdFn(r) - if err != nil { - nibbler.Write500Json(w, err.Error()) - return - } - - if targetGroup == "" { - nibbler.Write404Json(w) - return - } - - privileges, err := s.PersistenceExtension.GetPrivilegesForAction(*userFromDb.CurrentGroupID, &targetGroup, action) - if err != nil { - nibbler.Write500Json(w, err.Error()) - return - } - if len(privileges) == 0 { - nibbler.Write404Json(w) - return - } - - routerFunc(w, r) +func (s *Extension) PostInit(app *nibbler.Application) error { + if !s.DisableDefaultRoutes { + app.Router.HandleFunc(app.Config.ApiPrefix+"/group/composite", s.SessionExtension.EnforceLoggedIn(s.GetUserCompositeRequestHandler)).Methods("GET") + app.Router.HandleFunc(app.Config.ApiPrefix+"/group", s.EnforceHasPrivilege(ListGroupsAction, s.QueryGroupsRequestHandler)).Methods("GET") + app.Router.HandleFunc(app.Config.ApiPrefix+"/group", s.EnforceHasPrivilege(CreateGroupAction, s.CreateGroupRequestHandler)).Methods("PUT") + app.Router.HandleFunc(app.Config.ApiPrefix+"/group/:groupId/privilege", s.EnforceHasPrivilegeOnResource(DeleteGroupPrivilegeAction, GetParamValueFromRequest("groupId"), s.DeleteGroupPrivilegeRequestHandler)).Methods("DELETE") + app.Router.HandleFunc(app.Config.ApiPrefix+"/group/:groupId/privilege", s.EnforceHasPrivilegeOnResource(CreateGroupPrivilegeAction, GetParamValueFromRequest("groupId"), s.CreateGroupPrivilegeRequestHandler)).Methods("PUT") + app.Router.HandleFunc(app.Config.ApiPrefix+"/group/:groupId", s.EnforceHasPrivilegeOnResource(DeleteGroupAction, GetParamValueFromRequest("groupId"), s.DeleteGroupRequestHandler)).Methods("DELETE") + app.Router.HandleFunc(app.Config.ApiPrefix+"/group/:groupId/membership", s.EnforceHasPrivilegeOnResource(CreateGroupMembershipAction, GetParamValueFromRequest("groupId"), s.CreateGroupMembershipRequestHandler)).Methods("PUT") + app.Router.HandleFunc(app.Config.ApiPrefix+"/group/:groupId/membership", s.EnforceHasPrivilegeOnResource(RemoveMemberFromGroupAction, GetParamValueFromRequest("groupId"), s.CreateGroupMembershipRequestHandler)).Methods("DELETE") } -} - -func (s *Extension) PostInit(context *nibbler.Application) error { return nil } diff --git a/user/group/group.go b/user/group/group.go index 9ccd17a..184c89f 100644 --- a/user/group/group.go +++ b/user/group/group.go @@ -3,6 +3,7 @@ package nibbler_user_group import ( "encoding/json" "github.com/google/uuid" + "github.com/gorilla/mux" "github.com/markdicksonjr/nibbler" "net/http" ) @@ -68,6 +69,31 @@ func (s *Extension) CreateGroupRequestHandler(w http.ResponseWriter, r *http.Req nibbler.Write200Json(w, string(groupJson)) } +// QueryGroupsRequestHandler lists groups - it does not yet support queries +func (s *Extension) QueryGroupsRequestHandler(w http.ResponseWriter, r *http.Request) { + params := nibbler.SearchParameters{ + Query: nil, + Offset: nil, + Size: nil, + IncludeTotal: true, + } + + // TODO: looks wrong - this should be a query param here + includePrivs := mux.Vars(r)["includePrivs"] == "true" + + if g, err := s.PersistenceExtension.SearchGroups(params, includePrivs); err != nil { + nibbler.Write500Json(w, err.Error()) + return + } else { + r, err := json.Marshal(g) + if err != nil { + nibbler.Write500Json(w, err.Error()) + return + } + nibbler.Write200Json(w, string(r)) + } +} + func (s *Extension) CreateGroup(name string) (nibbler.Group, error) { group := nibbler.Group{ ID: uuid.New().String(), @@ -77,6 +103,16 @@ func (s *Extension) CreateGroup(name string) (nibbler.Group, error) { return group, err } +func (s *Extension) DeleteGroupRequestHandler(w http.ResponseWriter, r *http.Request) { + // TODO: allow query param for hard delete + if err := s.PersistenceExtension.DeleteGroup(mux.Vars(r)["groupId"], false); err != nil { + nibbler.Write500Json(w, err.Error()) + return + } + + nibbler.Write200Json(w, "{\"result\":\"ok\"") +} + func (s *Extension) GetGroups(groupIds []string, includePrivileges bool) ([]nibbler.Group, error) { // load the groups for the set of memberships diff --git a/user/group/membership.go b/user/group/membership.go index 7621ef2..93e7321 100644 --- a/user/group/membership.go +++ b/user/group/membership.go @@ -1,7 +1,12 @@ package nibbler_user_group import ( + "encoding/json" + "errors" + "github.com/gorilla/mux" "github.com/markdicksonjr/nibbler" + "io/ioutil" + "net/http" ) // SetGroupMembership upserts the group membership record for a given user and group @@ -13,3 +18,53 @@ func (s *Extension) SetGroupMembership(groupId, userId string, role string) (nib func (s *Extension) GetGroupMembershipsForUser(userId string) ([]nibbler.GroupMembership, error) { return s.PersistenceExtension.GetGroupMembershipsForUser(userId) } + +func (s *Extension) CreateGroupMembershipRequestHandler(w http.ResponseWriter, r *http.Request) { + membership, err := getMembershipFromBody(r) + if err != nil { + nibbler.Write500Json(w, err.Error()) + return + } + + if membership.MemberID == "" { + nibbler.Write500Json(w, "{\"error\":\"no member ID provided\"") + return + } + + membership.GroupID = mux.Vars(r)["groupId"] + + // role is optional + + result, err := s.PersistenceExtension.SetGroupMembership(membership.GroupID, membership.MemberID, membership.Role) + if err != nil { + nibbler.Write500Json(w, err.Error()) + return + } + + resultJson, err := json.Marshal(result) + if err != nil { + nibbler.Write500Json(w, err.Error()) + return + } + + nibbler.Write200Json(w, string(resultJson)) +} + +func getMembershipFromBody(r *http.Request) (*nibbler.GroupMembership, error) { + if r.Body == nil { + return nil, errors.New("no body provided") + } + + defer r.Body.Close() + raw, err := ioutil.ReadAll(r.Body) + if err != nil { + return nil, err + } + + membership := nibbler.GroupMembership{} + if err := json.Unmarshal(raw, &membership); err != nil { + return nil, err + } + + return &membership, nil +} diff --git a/user/group/privilege.go b/user/group/privilege.go index 9f06912..db20b31 100644 --- a/user/group/privilege.go +++ b/user/group/privilege.go @@ -1,13 +1,27 @@ package nibbler_user_group -// some hopefully reusable or useful privileges - at some point, the group extension -// will likely provide a default implementation of privileges around group editing -const AddMemberToGroupPrivilege = "add-member-to-group" -const CreateGroupPrivilege = "create-group" -const DeleteGroupPrivilege = "delete-group" - -// allows all groups in the groupIdList to perform the provided action -// on the targetGroupId. If targetGroupId is blank, it means "all groups" +import ( + "encoding/json" + "errors" + "github.com/gorilla/mux" + "github.com/markdicksonjr/nibbler" + "io/ioutil" + "net/http" +) + +// some (hopefully) reusable or useful privileges +const CreateGroupMembershipAction = "add-member-to-group" +const CreateGroupAction = "create-group" +const DeleteGroupAction = "delete-group" +const CreatePrivilegeAction = "create-privilege" +const DeletePrivilegeAction = "delete-privilege" +const CreateGroupPrivilegeAction = "create-group-privilege" +const DeleteGroupPrivilegeAction = "delete-group-privilege" +const ListGroupsAction = "list-groups" +const RemoveMemberFromGroupAction = "remove-member-from-group" + +// allows all groups in the groupIdList to perform the provided action on the targetGroupId. If targetGroupId is blank, +// it means "all resources/groups" func (s *Extension) AddPrivilegeToGroups( groupIdList []string, targetGroupId string, @@ -15,3 +29,161 @@ func (s *Extension) AddPrivilegeToGroups( ) error { return s.PersistenceExtension.AddPrivilegeToGroups(groupIdList, targetGroupId, action) } + +// HasPrivilege returns whether the caller has a privilege for a resource-agnostic action. This is suitable +// for something like "create-admin" or any other "global"-type privilege +func (s *Extension) HasPrivilege(userId, action string) (bool, error) { + userFromDb, err := s.UserExtension.GetUserById(userId) + if err != nil { + return false, err + } + + if userFromDb == nil || userFromDb.CurrentGroupID == nil { + return false, nil + } + + privileges, err := s.PersistenceExtension.GetPrivilegesForAction(*userFromDb.CurrentGroupID, nil, action) + if err != nil { + return false, err + } + + return len(privileges) == 0, nil +} + +// HasPrivilegeOnResource will state whether the caller can perform an action on a specific resource. If there is no +// resource-specific privilege, it will check to see if the caller has the global privilege for that action. For +// example, some users may have "create-user" privileges for a specific group, but an admin may have a resource-agnostic +// "create-user" privilege. This function will check both. +func (s *Extension) HasPrivilegeOnResource(userId, resourceId, action string) (bool, error) { + userFromDb, err := s.UserExtension.GetUserById(userId) + if err != nil { + return false, err + } + + if userFromDb == nil || userFromDb.CurrentGroupID == nil { + return false, nil + } + + privileges, err := s.PersistenceExtension.GetPrivilegesForAction(*userFromDb.CurrentGroupID, &resourceId, action) + if err != nil { + return false, err + } + + return len(privileges) == 0, nil +} + +func (s *Extension) DeleteGroupPrivilegeRequestHandler(w http.ResponseWriter, r *http.Request) { + priv, err := getPrivilegeFromBody(r) + if err != nil { + nibbler.Write500Json(w, err.Error()) + return + } + + priv.GroupID = mux.Vars(r)["groupId"] + + // if a user passes no resource for the privilege, this is a little dangerous - we need to be sure they are allowed + // to allocate privileges that are independent of a resource (global privileges) + if priv.ResourceID == "" { + caller, err := s.SessionExtension.GetCaller(r) + if err != nil { + nibbler.Write500Json(w, err.Error()) + return + } + + if caller == nil { + nibbler.Write401Json(w) + return + } + + // if the user does not have the right to create such privileges, stop them here + if has, err := s.HasPrivilege(caller.ID, DeletePrivilegeAction); err != nil { + nibbler.Write500Json(w, err.Error()) + return + } else if !has { + nibbler.Write404Json(w) + return + } + } + + // get group/resource/action from request and delete match(es) + privileges, err := s.PersistenceExtension.GetPrivilegesForAction(priv.GroupID, &priv.ResourceID, priv.Action) + if err != nil { + nibbler.Write500Json(w, err.Error()) + return + } + + if len(privileges) == 0 { + nibbler.Write404Json(w) + return + } + + for _, p := range privileges { + if err := s.PersistenceExtension.DeletePrivilege(p.ID); err != nil { + nibbler.Write500Json(w, err.Error()) + return + } + } + + nibbler.Write200Json(w, "{\"result\":\"ok\"") +} + +func (s *Extension) CreateGroupPrivilegeRequestHandler(w http.ResponseWriter, r *http.Request) { + priv, err := getPrivilegeFromBody(r) + if err != nil { + nibbler.Write500Json(w, err.Error()) + return + } + + // override the group ID in the request body/contents with the one from the path (TODO: perhaps warn if different - could identify hack attempts) + priv.GroupID = mux.Vars(r)["groupId"] + + // if a user passes no resource for the privilege, this is a little dangerous - we need to be sure they are allowed + // to allocate privileges that are independent of a resource (global privileges) + if priv.ResourceID == "" { + caller, err := s.SessionExtension.GetCaller(r) + if err != nil { + nibbler.Write500Json(w, err.Error()) + return + } + + if caller == nil { + nibbler.Write401Json(w) + return + } + + // if the user does not have the right to create such privileges, stop them here + if has, err := s.HasPrivilege(caller.ID, CreatePrivilegeAction); err != nil { + nibbler.Write500Json(w, err.Error()) + return + } else if !has { + nibbler.Write404Json(w) + return + } + } + + if err := s.PersistenceExtension.AddPrivilegeToGroups([]string{ priv.GroupID }, priv.ResourceID, priv.Action); err != nil { + nibbler.Write500Json(w, err.Error()) + return + } + + nibbler.Write200Json(w, "{\"result\":\"ok\"") +} + +func getPrivilegeFromBody(r *http.Request) (*nibbler.GroupPrivilege, error) { + if r.Body == nil { + return nil, errors.New("no body provided") + } + + defer r.Body.Close() + raw, err := ioutil.ReadAll(r.Body) + if err != nil { + return nil, err + } + + priv := nibbler.GroupPrivilege{} + if err := json.Unmarshal(raw, &priv); err != nil { + return nil, err + } + + return &priv, nil +} diff --git a/user/message/extension.go b/user/message/extension.go index 9a0cbbb..7eba734 100644 --- a/user/message/extension.go +++ b/user/message/extension.go @@ -72,10 +72,10 @@ func (s *Extension) PostInit(app *nibbler.Application) error { s.Logger = app.Logger - app.Router.HandleFunc("/api/message", s.GetMessagesHandler).Queries("userId", "{userId}", "count", "{count}", "offset", "{offset}").Methods("GET") - app.Router.HandleFunc("/api/message", s.SendMessageToUserHandler).Methods("POST") - app.Router.HandleFunc("/api/message", s.DeleteUserMessageStateHandler).Queries("userId", "{userId}").Methods("DELETE") - app.Router.HandleFunc("/api/message/read", s.MarkUserMessageStateAsReadHandler).Methods("POST") + app.Router.HandleFunc(app.Config.ApiPrefix + "/message", s.GetMessagesHandler).Queries("userId", "{userId}", "count", "{count}", "offset", "{offset}").Methods("GET") + app.Router.HandleFunc(app.Config.ApiPrefix + "/message", s.SendMessageToUserHandler).Methods("POST") + app.Router.HandleFunc(app.Config.ApiPrefix + "/message", s.DeleteUserMessageStateHandler).Queries("userId", "{userId}").Methods("DELETE") + app.Router.HandleFunc(app.Config.ApiPrefix + "/message/read", s.MarkUserMessageStateAsReadHandler).Methods("POST") return nil }