Skip to content

Commit

Permalink
API Base path added to config, group ext work
Browse files Browse the repository at this point in the history
The group extension now allows for adding the default routes we would expect someone to use
  • Loading branch information
markdicksonjr committed Feb 16, 2020
1 parent 5c0ef0c commit 60d4db8
Show file tree
Hide file tree
Showing 14 changed files with 421 additions and 116 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
1 change: 1 addition & 0 deletions application.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ type Configuration struct {
Headers HeaderConfiguration
Port int
Raw config.Config
ApiPrefix string
StaticDirectory string
}

Expand Down
1 change: 1 addition & 0 deletions configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
15 changes: 15 additions & 0 deletions model.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
11 changes: 11 additions & 0 deletions response.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion sample/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 7 additions & 7 deletions user/auth/local/extension.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 12 additions & 10 deletions user/group/composite.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"]

Expand All @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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
}
Expand Down
78 changes: 78 additions & 0 deletions user/group/enforce.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
104 changes: 19 additions & 85 deletions user/group/extension.go
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -14,110 +15,43 @@ 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 {
nibbler.NoOpExtension
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
}

Expand Down
Loading

0 comments on commit 60d4db8

Please sign in to comment.