wm
- A role based Webmodel Mapper
wm
solves a problem as old as multiuser data access:
How to define which fields certain users are allowed to access.
Typically users accessing REST routes are authenticated via some form of identity provider.
wm
bridges the gap between the persistence layer, User Auth and exposing models via REST (or other technologies).
With wm
it is possible to convert your DB model to a web model, without manually creating or generating web models.
To define which fields should be visible to what roles, just add struct tags.
Installation
This package requires Go 1.12 or newer. Versioned releases are available.
go get github.com/Hoss-Mobility/wm
Getting started
wm
uses struct tags and predefined authorization labels.
Just add the name of a role to your struct and set one of the following tags.
r
- Read
w
- Write
rw
- Read & Write
Multiple roles can be used in one wm
prefixed struct tag, to define fine granular access control:
type SmallExample struct {
// Staff is only allowed read the Name field
// Developers and admins are also allowed to write / change it
Name string `wm:"staff:r;developer:rw;admin:rw"`
// Staff, Developer and Admin are allowed to read and write to field Comment
Comment string `wm:"staff:rw;developer:rw;admin:rw"`
}
The actual mapping is done via three functions: ToWeb
, ToDb
and ApplyUpdate
.
ToWeb
creates a web model. A web model most often only contains a subset of
data in a model (i.e. don't expose secrets).
This is achieved by creating a copy of the source struct and
only setting values the specified user is allowed to read.
source := SmallExample{Name: "Linus the cat", Comment: "A fine boi"}
role := "staff"
webModel, err := wm.ToWeb(source, role)
// webModel.Name and webModel.Comment is populated because staff is allowed to read from the field `Name` and `Comment`
// {
// "Name": "Linus the cat",
// "Comment": "A fine boi"
// }
ToDb
creates a new instance of the type of the source struct, but only
sets fields that the user is allowed to write to.
This method can be useful to create and store a new model in the database.
source := SmallExample{Name: "Linus the cat", Comment: "A heckin' chonker"}
role := "staff"
dbModel, err := wm.ToDb(source, role)
// dbModel.Name is empty because staff is not allowed to write to the field `Name`
// dbModel.Comment is populated because staff is allowed to write to `Comment`
// {
// "Name": "",
// "Comment": "A heckin' chonker"
// }
ApplyUpdate
applies changes from a new model to an old model.
Only sets fields from the new model on the old model,
if the supplied role is allowed to write to (W or RW).
This method can be useful to update existing models in the database.
old := SmallExample{Name: "Linus the cat", Comment: "A heckin' chonker"}
new := SmallExample{Name: "New name", Comment: "MEGACHONKER"}
role := "staff"
updatedModel, err := wm.ApplyUpdate(old, new, role)
// updatedModel.Name still is "Linus the cat" because staff is not allowed to change the name
// updatedModel.Comment is set to "MEGACHONKER"
// {
// "Name": "Linus the cat",
// "Comment": "MEGACHONKER"
// }
SliceToDb
and SliceToWeb
work the same as ToDb
and ToWeb
, but take slices
instead of single structs.
cat1 := SmallExample{Name: "Linus the cat", Comment: "A heckin' chonker"}
cat2 := SmallExample{Name: "Franz", Comment: "MEGACHONKER"}
cats := []SmallExample{cat1, cat2}
role := "staff"
dbCats, err := wm.SliceToDb(cats, role)
// [
// {
// "Name": "",
// "Comment": "A heckin' chonker"
// },
// {
// "Name": "",
// "Comment": "MEGACHONKER"
// },
// ]
webCats, err := wm.SliceWeb(cats, role)
// [
// {
// "Name": "Linus the cat",
// "Comment": "A heckin' chonker"
// },
// {
// "Name": "Franz",
// "Comment": "MEGACHONKER"
// },
// ]
Please do not rely solely on wm
to "sanitize" your models before storing it in the database.
Make sure to check for SQL injections and other malicious techniques.
Real World Examples
The following pseudo API provides an endpoint to GET
, POST
and PUT
recipes.
The Recipe
struct is either served or consumed. Recipe.SecretIngredients
must
not be exposed to Staff members. Staff members are only allowed to update the Details of
a recipe, but are not allowed to write / change the name.
type Recipe struct {
Name string `json:"name" wm:"staff:r;admin:rw" `
Details string `json:"details" wm:"staff:rw;admin:rw"`
SecretIngredients string `json:"secret_ingredients,omitempty" wm:"admin:rw"`
}
func GetRecipe(database db.YourDbHandler, manager *scs.SessionManager) http.HandlerFunc {
return func (w http.ResponseWriter, r *http.Request) {
// user role is set on session via middleware
userRole := manager.GetString(r.Context(), "USER_ROLE_KEY")
// get data from db
dbRecipe, err := database.GetRecipe("crabburger")
if err != nil {...}
// convert to web model
webRecipe, err := wm.ToWeb(dbRecipe, userRole)
if err != nil {...}
// render web model
render.Status(r, http.StatusOK)
render.JSON(w, r, webRecipe)
}
}
func PostRecipe(database db.YourDbHandler, manager *scs.SessionManager) http.HandlerFunc {
return func (w http.ResponseWriter, r *http.Request) {
var webRecipe Recipe
err := httptools.ParseBodyToStruct(r.Body, &webRecipe)
if err != nil {...}
// user role is set on session via middleware
userRole := manager.GetString(r.Context(), "USER_ROLE_KEY")
// convert to db model
dbRecipe, err := wm.ToDb(webRecipe, userRole)
if err != nil {...}
// store in db
dbRecipe, err := database.AddRecipe(dbRecipe)
if err != nil {...}
// set status to ok
render.Status(r, http.StatusOK)
}
}
func PutRecipe(database db.YourDbHandler, manager *scs.SessionManager) http.HandlerFunc {
return func (w http.ResponseWriter, r *http.Request) {
var webRecipe Recipe
err := httptools.ParseBodyToStruct(r.Body, &webRecipe)
if err != nil {...}
// get already existing recipe from db
dbRecipe, err := database.GetRecipeByName(webRecipe.Name)
if err != nil {...}
// user role is set on session via middleware
userRole := manager.GetString(r.Context(), "USER_ROLE_KEY")
// apply updates from webRecipe to dbRecipe
updatedRecipe, err := wm.ApplyUpdate(dbRecipe, webRecipe, userRole)
if err != nil {...}
// store in db
dbRecipe, err = database.AddRecipe(updatedRecipe)
if err != nil {...}
// set status to ok
render.Status(r, http.StatusOK)
}
}
Showcase
The following code snippets showcase the example main.go
included in the wm
module.
Specific fields of SecretItem
are only visible to specified roles.
type SecretItem struct {
Name string `wm:"staff:r;developer:rw;admin:rw"`
Comment string `wm:"staff:rw;developer:rw;admin:rw"`
SecretInfo string `wm:"developer:r;admin:rw"`
TopSecret string `wm:"admin:rw"`
CanOnlyBeWrittenTo string `wm:"staff:w;developer:w;admin:rw"`
}
The following snippet highlights what data each role sees:
ToWeb()
---------------
staff sees:
&internal.SecretItem{Name:"Crab Burger Recipe", Comment:"The recipe of the famous crusty crab burger", SecretInfo:"", TopSecret:"", CanOnlyBeWrittenTo:""}
developer sees:
&internal.SecretItem{Name:"Crab Burger Recipe", Comment:"The recipe of the famous crusty crab burger", SecretInfo:"Bun, Pickle, Patty, Lettuce", TopSecret:"", CanOnlyBeWrittenTo:""}
admin sees:
&internal.SecretItem{Name:"Crab Burger Recipe", Comment:"The recipe of the famous crusty crab burger", SecretInfo:"Bun, Pickle, Patty, Lettuce", TopSecret:"Do not forget the tomato", CanOnlyBeWrittenTo:"Hecho en Crustáceo Cascarudo"}
unauthorized sees:
&internal.SecretItem{Name:"", Comment:"", SecretInfo:"", TopSecret:"", CanOnlyBeWrittenTo:""}
ToDb()
---------------
staff can set:
&internal.SecretItem{Name:"", Comment:"The recipe of the famous crusty crab burger", SecretInfo:"", TopSecret:"", CanOnlyBeWrittenTo:"Hecho en Crustáceo Cascarudo"}
developer can set:
&internal.SecretItem{Name:"Crab Burger Recipe", Comment:"The recipe of the famous crusty crab burger", SecretInfo:"", TopSecret:"", CanOnlyBeWrittenTo:"Hecho en Crustáceo Cascarudo"}
admin can set:
&internal.SecretItem{Name:"Crab Burger Recipe", Comment:"The recipe of the famous crusty crab burger", SecretInfo:"Bun, Pickle, Patty, Lettuce", TopSecret:"Do not forget the tomato", CanOnlyBeWrittenTo:"Hecho en Crustáceo Cascarudo"}
unauthorized can set:
&internal.SecretItem{Name:"", Comment:"", SecretInfo:"", TopSecret:"", CanOnlyBeWrittenTo:""}
Update all possible fields:
ApplyUpdate()
---------------
staff can set:
&internal.SecretItem{Name:"Crab Burger Recipe", Comment:"Updated", SecretInfo:"Bun, Pickle, Patty, Lettuce", TopSecret:"Do not forget the tomato", CanOnlyBeWrittenTo:"Updated"}
developer can set:
&internal.SecretItem{Name:"Updated", Comment:"Updated", SecretInfo:"Bun, Pickle, Patty, Lettuce", TopSecret:"Do not forget the tomato", CanOnlyBeWrittenTo:"Updated"}
admin can set:
&internal.SecretItem{Name:"Updated", Comment:"Updated", SecretInfo:"Updated", TopSecret:"Updated", CanOnlyBeWrittenTo:"Updated"}
unauthorized can set:
&internal.SecretItem{Name:"Crab Burger Recipe", Comment:"The recipe of the famous crusty crab burger", SecretInfo:"Bun, Pickle, Patty, Lettuce", TopSecret:"Do not forget the tomato", CanOnlyBeWrittenTo:"Hecho en Crustáceo Cascarudo"}
Slice operations:
SliceToWeb()
---------------
staff can set:
&[]internal.SecretItem{internal.SecretItem{Name:"Crab Burger Recipe", Comment:"The recipe of the famous crusty crab burger", SecretInfo:"", TopSecret:"", CanOnlyBeWrittenTo:""}, internal.SecretItem{Name:"Crab Burger Recipe", Comment:"The recipe of the famous crusty crab burger", SecretInfo:"", TopSecret:"", CanOnlyBeWrittenTo:""}, internal.SecretItem{Name:"Crab Burger Recipe", Comment:"The recipe of the famous crusty crab burger", SecretInfo:"", TopSecret:"", CanOnlyBeWrittenTo:""}}
developer can set:
&[]internal.SecretItem{internal.SecretItem{Name:"Crab Burger Recipe", Comment:"The recipe of the famous crusty crab burger", SecretInfo:"Bun, Pickle, Patty, Lettuce", TopSecret:"", CanOnlyBeWrittenTo:""}, internal.SecretItem{Name:"Crab Burger Recipe", Comment:"The recipe of the famous crusty crab burger", SecretInfo:"Bun, Pickle, Patty, Lettuce", TopSecret:"", CanOnlyBeWrittenTo:""}, internal.SecretItem{Name:"Crab Burger Recipe", Comment:"The recipe of the famous crusty crab burger", SecretInfo:"Bun, Pickle, Patty, Lettuce", TopSecret:"", CanOnlyBeWrittenTo:""}}
admin can set:
&[]internal.SecretItem{internal.SecretItem{Name:"Crab Burger Recipe", Comment:"The recipe of the famous crusty crab burger", SecretInfo:"Bun, Pickle, Patty, Lettuce", TopSecret:"Do not forget the tomato", CanOnlyBeWrittenTo:"Hecho en Crustáceo Cascarudo"}, internal.SecretItem{Name:"Crab Burger Recipe", Comment:"The recipe of the famous crusty crab burger", SecretInfo:"Bun, Pickle, Patty, Lettuce", TopSecret:"Do not forget the tomato", CanOnlyBeWrittenTnfo:"Bun, Pickle, Patty, Lettuce", TopSecret:"Do not forget the tomato", CanOnlyBeWrittenTo:"Hecho en Crustáceo Cascarudo"}}
unauthorized can set:
&[]internal.SecretItem{internal.SecretItem{Name:"", Comment:"", SecretInfo:"", TopSecret:"", CanOnlyBeWrittenTo:""}, internal.SecretItem{Name:"", Comment:"", SecretInfo:"", TopSecret:"", CanOnlyBeWrittenTo:""}, internal.SecretItem{Name:"", Comment:"", SecretInfo:"", TopSecret:"", CanOnlyBeWrittenTo:""}}
SliceToDb()
---------------
staff can set:
&[]internal.SecretItem{internal.SecretItem{Name:"", Comment:"The recipe of the famous crusty crab burger", SecretInfo:"", TopSecret:"", CanOnlyBeWrittenTo:"Hecho en Crustáceo Cascarudo"}, internal.SecretItem{Name:"", Comment:"The recipe of the famous crusty crab burger", SecretInfo:"", TopSecret:"", CanOnlyBeWrittenTo:"Hecho en Crustáceo Cascarudo"}, internal.SecretItem{Name:"", Comment:"The recipe of the famous crusty crab burger", SecretInfo:"", TopSecret:"", CanOnlyBeWrittenTo:"Hecho en Crustáceo Cascarudo"}}
developer can set:
&[]internal.SecretItem{internal.SecretItem{Name:"Crab Burger Recipe", Comment:"The recipe of the famous crusty crab burger", SecretInfo:"", TopSecret:"", CanOnlyBeWrittenTo:"Hecho en Crustáceo Cascarudo"}, internal.SecretItem{Name:"Crab Burger Recipe", Comment:"The recipe of the famous crusty crab burger", SecretInfo:"", TopSecret:"", CanOnlyBeWrittenTo:"Hecho en Crustáceo Cascarudo"}, internal.SecretItem{Name:"Crab Burger Recipe", Comment:"The recipe of the famous crusty crab burger", SecretInfo:"", TopSecret:"", CanOnlyBeWrittenTo:"Hecho en Crustáceo Cascarudo"}}
admin can set:
&[]internal.SecretItem{internal.SecretItem{Name:"Crab Burger Recipe", Comment:"The recipe of the famous crusty crab burger", SecretInfo:"Bun, Pickle, Patty, Lettuce", TopSecret:"Do not forget the tomato", CanOnlyBeWrittenTo:"Hecho en Crustáceo Cascarudo"}, internal.SecretItem{Name:"Crab Burger Recipe", Comment:"The recipe of the famous crusty crab burger", SecretInfo:"Bun, Pickle, Patty, Lettuce", TopSecret:"Do not forget the tomato", CanOnlyBeWrittenTo:"Hecho en Crustáceo Cascarudo"}, internal.SecretItem{Name:"Crab Burger Recipe", Comment:"The recipe of the famous crusty crab burger", SecretInfo:"Bun, Pickle, Patty, Lettuce", TopSecret:"Do not forget the tomato", CanOnlyBeWrittenTo:"Hecho en Crustáceo Cascarudo"}}
unauthorized can set:
&[]internal.SecretItem{internal.SecretItem{Name:"", Comment:"", SecretInfo:"", TopSecret:"", CanOnlyBeWrittenTo:""}, internal.SecretItem{Name:"", Comment:"", SecretInfo:"", TopSecret:"", CanOnlyBeWrittenTo:""}, internal.SecretItem{Name:"", Comment:"", SecretInfo:"", TopSecret:"", CanOnlyBeWrittenTo:""}}