Cascading Privilege View Sets for Go Web APIs

11 August 2021

For an extremely long time, I’ve wrestled with one issue that seems to crop up almost continuously when moving from a dynamic language like Ruby or Python to a static language like Go. That is, the easiest way to map a database table (e.g., a users table) to an object (or, in Go, a struct). When rendering what is otherwise a database record, you will regularly want to add, remove, or change fields based on, among other things, the user’s permission level. For instance, you may not wish to show the IP address of a user to any non-administrators.

I’ve tried many different approaches, a lot of them attempting to use reflect in order to dynamically add or remove fields at rendering time. None of them worked perfectly and, commonly, they fell down because of nested structs.

This is the best approach I’ve found, which was somewhat heavily adapted from the book “Go Programming Blueprints” by Mat Ryer.

type PublicViewer interface {
	PublicView() interface{}
}

// PublicView checks to see if the interface as a PublicView function. If so,
// it defers to that. Otherwise, it returns nil, as the interface has no
// safeguards on data rendered.
func PublicView(o interface{}) interface{} {
	if p, ok := o.(PublicViewer); ok {
		return p.PublicView()
	}

	return nil
}

type ModeratorViewer interface {
	ModeratorView() interface{}
}

// ModeratorView checks to see if the interface has an ModeratorView function.
// If so, it defers to that. Otherwise, it defers to PublicView.
func ModeratorView(o interface{}) interface{} {
	if p, ok := o.(ModeratorViewer); ok {
		return p.ModeratorView()
	}

	return PublicView(o)
}

type AdministratorViewer interface {
	AdministratorView() interface{}
}

// AdministratorView checks to see if the interface has an AdministratorView
// function. If so, it defers to that.
// Otherwise, it defers to ModeratorView.
func AdministratorView(o interface{}) interface{} {
	if p, ok := o.(AdministratorViewer); ok {
		return p.AdministratorView()
	}

	return ModeratorView(o)
}

In this setup, PublicView is the “safest” representation of your struct or “object” that will go out into the world. As these funtions can (should ideally) return a map[string]interface{}, you fully control the content going on. Additionally, it means you don’t have to use the JSON tagging mechanism which, honestly, is just far more cognitive load for a new developer.

type Product struct {
	gorm.Model
	Name string `db:"name"`
}

func (p Product) PublicView() interface{} {
	return map[string]interface{} {
		"name": p.Name,
	}
}

No matter the role of the individual to whom this struct will be rendered as JSON, they will only ever see the name as this struct does not have an AdministratorView or a ModeratorView. You can add a ModeratorView that perhaps includes the created_at attribute, an AdministratorView that includes the IP of the creating user, and so forth.

All of this without touching ‘magic’ - that is, without touching tags.

Rendering this is pretty straightforward, but it does require a middleware for role checking. You probably have this already, so it actually doesn’t require anything at all.

func (s *Server) respondJSON(w http.ResponseWriter, r *http.Request, data interface{}) error {
	// Check user role here, use correct View.
	// ...
	return json.NewEncoder(w).Encode(app.ModeratorView(data))
	// ...
}

// userRoleMiddleware checks the user role and stores it in the context.
// This is useful for functions such as 'respondJSON'.
func userRoleMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Check user role from cookie or whatever
		// Save in context, probably just as an iota.
		// ...
		next.ServeHTTP(w, r)
	})
}

What this doesn’t cover, though, is cases where a user may be able to see more information about something in certain circumstances. For instance, a user might be able to see the IP address from which a post was made if they are the author. I haven’t put too much thought into this particular use case, as it’s not overly important for my work, however I imagine that the method that most appropriately fits the dynamic made here would be to extend the PublicView function to accept a single boolean value: owner. If set, the fields that should be added are added. Otherwise, they are not.

So, why three separate interfaces? Why not just one - Renderer? This was a design choice. You can simply use one and cascade down, but you end up with more code that is not at the left margin, and checking interface inheritance is much easier than attempting to discern whether or not an object can be presented to a particular user role when it’s not solidified as an inherited trait. It provides two level of safeguards:

It does this while being semi-generic, easy to read and parse, and self-contained, without cluttering the business logic of the HTTP server with the how, what, and why of structs.