Coder Social home page Coder Social logo

wm's Introduction

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:""}}

wm's People

Contributors

ddibiasi avatar

Stargazers

 avatar Stephan Kast avatar

Watchers

 avatar  avatar

wm's Issues

Add more info to ImproperFormatErr

Rn the ImproperFormatErr just shows an example how it should look like. It should also tell where the error happened for faster debugging.

Add possibility "update" models

For PUT (update) routes, the use case arises to update existing DB models. An ApplyUpdate or ApplyDiff method should be created that updates the existing model with a new web model and only sets the fields, the user with role XYZ is allowed to alter.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.