When serializing to JSON, how to omit certain fields based on a run-time condition?
Asked Answered
N

5

6

In a web service implemented in Go, I want to be able to restrict fields returned in a JSON response based on a user's role.

For example I may have a currently logged in user who has a role of guest and another with the role of admin

For an admin I want json to have all the keys eg

{
  id: 1,
  name: "John",
  role: "admin"
}

and for a guest to not have the role key eg

{
  id: 1,
  name: "John"
}

I can currently marshal the json and it returns all fields. I need to be able to restrict it.

Necker answered 3/4, 2014 at 11:13 Comment(2)
Well, seems some coding is needed. Maybe clear the fields and use omitempty?Trailblazer
This is a use case for sets e.g. intersection thereof.Christmas
G
7

You can go by the suggestion @Volker made and clear struct fields for which the user has no permissions. This is probably the easiest to implement.

A second option in a similar vein is to create a custom JSON encoder. One which encodes fields only if a role struct tag matches the current user's role. Here is some pseudo code to illustrate:

type T struct {
    currentRole Role   `json:"-"`
    FieldA      string `json:"field_a,omitempty", role:"guest"`
    FieldB      string `json:"field_b,omitempty", role:"guest"`
    FieldC      int    `json:"field_c,omitempty", role:"admin"`
}

// Have T implement the encoding/json.Marshaler interface.
func (t *T) MarshalJSON() ([]byte, error) {
    var buf bytes.Buffer

    // Use some reflection magic to iterate over struct fields.
    for _, field := range getStructFields(t) {
        // More reflection magic to extract field tag data.
        role := getFieldTag(field, "role")

        // If the field tag's role matches our current role,
        // we are good to go. otherwise, skip this field.
        if !matchingRole(role, t.currentRole) {
            continue // skip this field 
        }

        data, err := json.Marshal(fieldValue(field))
        ...
        _, err = buf.Write(data)
        ...
    }

    return buf.Bytes(), nil
}

This is going to be a pain to maintain if you need new roles though. So this would not be something I would lightly consider doing.

Security concerns

I am not entirely sure that what you are looking for is the right solution to your problem. This depends on the context in which you use your code, which is not clear from your question. But if this concerns a website where a user's abilities on the website are defined solely by the value of the role JSON field, then you are looking at a security hole. They can simply go into a browser debugger and change the value of this JSON object to include the "role: "admin" field. And presto! Instant administrative powers. Whether or not to render certain parts of a page, based on user role, should really be handled by the server, during template processing. Just like any and all data posted to the server should be checked and checked again to ensure it came from a trusted source.

If none of this is applicable to you, then by all means, disregard this paragraph.

Gammon answered 3/4, 2014 at 11:43 Comment(3)
For performance reasons - if you're using this method, make sure not to call the reflection code in real time. You can actually create closures that do the real validations, and read the reflect values only when creating them.Calculating
Also as a security consideration, you are basically blacklisting fields. If possible try not retrieving privileged fields in the first place instead of clearing them.Ejection
@Calculating can you give an example of the closure implementation you suggest?Hellenistic
D
3

This question seems old, but I recently wanted to do the same thing. Maybe this will help someone in the future. Here is another method: you can define your own Marshal interface and use anonymous structs.

//User holds all variables
//even private ones
type User struct {
    ID      int64
    Name    string
    Role    string
}

//MarshalJSON gives back json user
//but only the public fields!
func (u *User) MarshalJSON() ([]byte, error) {
    return json.Marshal(struct {
        ID string `json:"id"`
        Name string `json:"name"`
    }{u.ID, u.Name})
}

it would be pretty easy to put an if u.Role == "admin" statement in the block to decide whether to marshal the rest.

Daydream answered 13/7, 2015 at 3:23 Comment(0)
M
3

Another option that also works to define the set of fields in the output for a list of struct that comes from an appengine datastore query.

    // Setting different JSON output field for the same struct, using anonymous
    // fields (inspired by inspired by http://choly.ca/post/go-json-marshalling/)

    // This alternative could be used to load a resultset from an appengine datastore
    // query and returned a custom field combination for the list items.

    package main

    import (
        "encoding/json"
        "fmt"
    )

    type User struct {
        ID         string `json:"id"`
        Name       string `json:"name"`
        Role       string `json:"-"`
        LaunchCode string `json:"-"`
    }

    type AdminOutputUser User

    func (user *AdminOutputUser) MarshalJSON() ([]byte, error) {
        type Alias AdminOutputUser
        return json.Marshal(&struct {
            *Alias
            Role string `json:"role"`
        }{
            (*Alias)(user),
            user.Role,
        })
    }

    type SuperadminOutputUser User

    func (user *SuperadminOutputUser) MarshalJSON() ([]byte, error) {
        type Alias SuperadminOutputUser
        return json.Marshal(&struct {
            *Alias
            Role       string `json:"role"`
            LaunchCode string `json:"code"`
        }{
            (*Alias)(user),
            user.Role,
            user.LaunchCode,
        })
    }

    func main() {
        user := User{"007", "James Bond", "admin", "12345678"}
        adminOutput := AdminOutputUser(user)
        superadminOutput := SuperadminOutputUser(user)

        b, _ := json.Marshal(&user)
        fmt.Printf("%s\n\n", string(b))
        // {"id":"007","name":"James Bond"}

        b, _ = json.Marshal(&adminOutput)
        fmt.Printf("%s\n\n", string(b))
        // {"id":"007","name":"James Bond","role":"admin"}

        b, _ = json.Marshal(&superadminOutput)
        fmt.Printf("%s\n\n", string(b))
        // {"id":"007","name":"James Bond","role":"admin","code":"12345678"}
    }

    // for appengine could do something like
    // ...
    // var users []AdminOutputUser // or User or SuperadminOutputUser
    // q := datastore.NewQuery("User")
    // keys, err := q.GetAll(ctx, &users)
    // ...

https://play.golang.org/p/ignIz0hP0z

Marrowfat answered 21/11, 2016 at 18:4 Comment(0)
B
0

You might just define your struct like this

type User struct {
    ID   int64 `json:"id"`
    Name string `json:"name"`
    Role string `json:"role,omitempty"`
}

And then set them like this

normalUser := User{ID: "boring", Name: "Rubber"}
adminUser := User{ID: "powers", Name: "Ruler", Role: "admin"}

Then json.Marshal() or json.NewEncoder().Encode() as usual

Found in How To Use Struct Tags in Go

Note: I know that omitempty was mentioned in a comment and is even part of @jimt's code example and mentioned as first option, albeit without a simple example. To me being still pretty new to Go wasn't clear that that would just work as expected. So I figured it might help others as well 🤓

Bjorn answered 21/10, 2021 at 16:51 Comment(0)
D
0

Following the answers above, I have created a small library that provides control for field visibility based on roles.

https://github.com/fattymango/visible

import (
    "fmt"

    "github.com/fattymango/visible"
)

type User struct {
    ID        int         `json:"id"`
    Name      string      `json:"name"`
    AdminData interface{} `json:"admin_data" visible:"admin"`
}

func main() {
    user := User{
        ID:        1,
        Name:      "John Doe",
        AdminData: "Admin Data",
    }

    res, err := visible.CleanStruct(user, "admin")
    if err != nil {
        panic(err)
    }
    fmt.Println(res)
    // Output: {id:1 name:John Doe admin_data:Admin Data}

    res, err = visible.CleanStruct(user, "user")
    if err != nil {
        panic(err)
    }
    fmt.Println(res)
    // Output: {id:1 name:John Doe}
}

Disconsider answered 10/8 at 9:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.