View bearer token in human readable format #1498

Open
opened 2024-11-14 10:50:02 +00:00 by potyarkin · 2 comments
Member

Bearer tokens are often saved in raw protobuf binary format, especially since frostfs-http-gw does not support JSON serialized tokens yet. This format is completely inaccessible for humans to review. Even JSON tokens contain base64 encoded protobuf binaries for access rule chains, making them hard to reason about.

Describe the solution you'd like

For my personal use I wrote a simple viewer tool that unpacks nested base64 chains and spits out indented JSON. May be we should add frostfs-cli bearer view that does something similar?

cmd/viewtoken/main.go (for those who do not have access to repo)

// Print human readable representation of binary FrostFS bearer token.
package main

import (
	"encoding/base64"
	"encoding/json"
	"fmt"
	"os"

	"git.frostfs.info/TrueCloudLab/frostfs-gw-cert/pkg/bearer"
	apeChain "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
)

func main() {
	rc := 0
	for _, path := range os.Args[1:] {
		fmt.Fprintf(os.Stderr, "\n# %s\n", path)
		token, err := bearer.FromFile(path)
		if err != nil {
			fmt.Fprintln(os.Stderr, err)
			rc = 1
			continue
		}
		readable, err := makeHumanReadable(token)
		if err != nil {
			fmt.Fprintln(os.Stderr, err)
			rc = 1
			continue
		}
		fmt.Println(string(readable))
	}
	os.Exit(rc)
}

// Unpack deeply nested policy chains for humans to review
func makeHumanReadable(token any) ([]byte, error) {
	interimJson, err := json.Marshal(token)
	if err != nil {
		return nil, err
	}
	var interim map[string]any
	err = json.Unmarshal(interimJson, &interim)
	if err != nil {
		return nil, err
	}
	unpackChains(interim)
	return json.MarshalIndent(interim, "", "  ")
}

func unpackChains(token map[string]any) {
	chains, ok := get(token, "body", "apeOverride", "chains").([]any)
	if !ok || chains == nil {
		return
	}
	for index := range chains {
		packed, ok := chains[index].(map[string]any)
		if !ok {
			continue
		}
		encoded, ok := get(packed, "raw").(string)
		if !ok {
			continue
		}
		raw, err := base64.StdEncoding.DecodeString(encoded)
		if err != nil {
			continue
		}
		chain := new(apeChain.Chain)
		err = chain.DecodeBytes(raw)
		if err != nil {
			continue
		}
		set(packed, chain, "raw")
	}
}

func get(tree map[string]any, key ...string) any {
	if len(key) == 1 {
		return tree[key[0]]
	}
	if len(key) == 0 {
		return tree
	}
	var ok bool
	tree, ok = tree[key[0]].(map[string]any)
	if !ok {
		return nil
	}
	return get(tree, key[1:]...)
}

func set(tree map[string]any, value any, key ...string) {
	if len(key) == 1 {
		tree[key[0]] = value
	}
	if len(key) == 0 {
		return
	}
	var ok bool
	tree, ok = tree[key[0]].(map[string]any)
	if !ok {
		return
	}
	set(tree, value, key[1:]...)
}

Sample output from viewtoken tool

$ bin/viewtoken@linux-amd64 /tmp/bearer/rw-02aac9fcb56c933ffef20d7efac793ce77e4093e66b641a11a50ae45328fb52be3.token

# /tmp/bearer/rw-02aac9fcb56c933ffef20d7efac793ce77e4093e66b641a11a50ae45328fb52be3.token
{
  "body": {
    "allowImpersonate": false,
    "apeOverride": {
      "chains": [
        {
          "raw": {
            "ID": null,
            "Rules": [
              {
                "Status": "Allow",
                "Actions": {
                  "Inverted": false,
                  "Names": [
                    "SearchObject"
                  ]
                },
                "Resources": {
                  "Inverted": false,
                  "Names": [
                    "native:object/*"
                  ]
                },
                "Any": false,
                "Condition": null
              },
              {
                "Status": "Allow",
                "Actions": {
                  "Inverted": false,
                  "Names": [
                    "GetObject",
                    "HeadObject"
                  ]
                },
                "Resources": {
                  "Inverted": false,
                  "Names": [
                    "native:object/*"
                  ]
                },
                "Any": false,
                "Condition": [
                  {
                    "Op": "StringNotEquals",
                    "Kind": "Resource",
                    "Key": "ACME",
                    "Value": ""
                  }
                ]
              },
              {
                "Status": "Allow",
                "Actions": {
                  "Inverted": false,
                  "Names": [
                    "PutObject"
                  ]
                },
                "Resources": {
                  "Inverted": false,
                  "Names": [
                    "native:object/*"
                  ]
                },
                "Any": false,
                "Condition": [
                  {
                    "Op": "StringNotEquals",
                    "Kind": "Resource",
                    "Key": "ACME",
                    "Value": ""
                  }
                ]
              },
              {
                "Status": "Allow",
                "Actions": {
                  "Inverted": false,
                  "Names": [
                    "GetObject",
                    "HeadObject"
                  ]
                },
                "Resources": {
                  "Inverted": false,
                  "Names": [
                    "native:object/*"
                  ]
                },
                "Any": false,
                "Condition": [
                  {
                    "Op": "StringNotEquals",
                    "Kind": "Resource",
                    "Key": "CERTBOX-Recepient-02aac9fcb56c933ffef20d7efac793ce77e4093e66b641a11a50ae45328fb52be3",
                    "Value": ""
                  }
                ]
              },
              {
                "Status": "Allow",
                "Actions": {
                  "Inverted": false,
                  "Names": [
                    "PutObject"
                  ]
                },
                "Resources": {
                  "Inverted": false,
                  "Names": [
                    "native:object/*"
                  ]
                },
                "Any": false,
                "Condition": [
                  {
                    "Op": "StringNotEquals",
                    "Kind": "Resource",
                    "Key": "CERTBOX-Recepient-02aac9fcb56c933ffef20d7efac793ce77e4093e66b641a11a50ae45328fb52be3",
                    "Value": ""
                  }
                ]
              },
              {
                "Status": "Allow",
                "Actions": {
                  "Inverted": false,
                  "Names": [
                    "GetObject",
                    "HeadObject"
                  ]
                },
                "Resources": {
                  "Inverted": false,
                  "Names": [
                    "native:object/*"
                  ]
                },
                "Any": false,
                "Condition": [
                  {
                    "Op": "StringNotEquals",
                    "Kind": "Resource",
                    "Key": "CERTBOX-PeerList",
                    "Value": ""
                  }
                ]
              }
            ],
            "MatchType": "FirstMatch"
          }
        }
      ],
      "target": {
        "name": "A8wycwWu2sS3KgcJZeWHXjmvPgoYX3vSQ5aHxAHWTAHa",
        "type": "CONTAINER"
      }
    },
    "eaclTable": null,
    "lifetime": {
      "exp": "16533",
      "iat": "332",
      "nbf": "332"
    },
    "ownerID": {
      "value": "NXTYVxz19uWG3X7HGu5rzheAdTtT9GnG9A=="
    }
  },
  "signature": {
    "key": "A9HmdSzJqgzeUUu+exZ41aEehIZGKSAXfy2s7LUWCNuW",
    "scheme": "ECDSA_SHA512",
    "signature": "BKqAL1pk+PmzYkFGkZQ98rESwjq0AniE1YHK4SFCUzXC2gfOUHWHuspMWfElo3WiT/yLIOhKIxC3vX8QEfenFzg="
  }
}

My tool is not perfect:
  • It's JSON is only for humans to read and pretty much invalid for any non-trivial deserialization. If we decide to implement frostfs-cli bearer view we should probably switch output to a bespoke key-value format to avoid implying any machine-readability (similar to how netinfo is printed).
  • UX is basically non-existent

Describe alternatives you've considered

Keep things as they are and leave bearer tokens unreadable.

## Is your feature request related to a problem? Please describe. Bearer tokens are often saved in raw protobuf binary format, especially since frostfs-http-gw [does not support](https://git.frostfs.info/TrueCloudLab/frostfs-http-gw/issues/163) JSON serialized tokens yet. This format is completely inaccessible for humans to review. Even JSON tokens contain base64 encoded protobuf binaries for access rule chains, making them hard to reason about. ## Describe the solution you'd like For my personal use I wrote a simple [viewer tool](https://git.frostfs.info/potyarkin/frostfs-gw-cert/src/commit/cd508a41f6d39ef85af08fff2b85e5b802f67e49/cmd/viewtoken/main.go) that unpacks nested base64 chains and spits out indented JSON. May be we should add `frostfs-cli bearer view` that does something similar? <details><summary>cmd/viewtoken/main.go (for those who do not have access to repo)</summary><p> ```go // Print human readable representation of binary FrostFS bearer token. package main import ( "encoding/base64" "encoding/json" "fmt" "os" "git.frostfs.info/TrueCloudLab/frostfs-gw-cert/pkg/bearer" apeChain "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" ) func main() { rc := 0 for _, path := range os.Args[1:] { fmt.Fprintf(os.Stderr, "\n# %s\n", path) token, err := bearer.FromFile(path) if err != nil { fmt.Fprintln(os.Stderr, err) rc = 1 continue } readable, err := makeHumanReadable(token) if err != nil { fmt.Fprintln(os.Stderr, err) rc = 1 continue } fmt.Println(string(readable)) } os.Exit(rc) } // Unpack deeply nested policy chains for humans to review func makeHumanReadable(token any) ([]byte, error) { interimJson, err := json.Marshal(token) if err != nil { return nil, err } var interim map[string]any err = json.Unmarshal(interimJson, &interim) if err != nil { return nil, err } unpackChains(interim) return json.MarshalIndent(interim, "", " ") } func unpackChains(token map[string]any) { chains, ok := get(token, "body", "apeOverride", "chains").([]any) if !ok || chains == nil { return } for index := range chains { packed, ok := chains[index].(map[string]any) if !ok { continue } encoded, ok := get(packed, "raw").(string) if !ok { continue } raw, err := base64.StdEncoding.DecodeString(encoded) if err != nil { continue } chain := new(apeChain.Chain) err = chain.DecodeBytes(raw) if err != nil { continue } set(packed, chain, "raw") } } func get(tree map[string]any, key ...string) any { if len(key) == 1 { return tree[key[0]] } if len(key) == 0 { return tree } var ok bool tree, ok = tree[key[0]].(map[string]any) if !ok { return nil } return get(tree, key[1:]...) } func set(tree map[string]any, value any, key ...string) { if len(key) == 1 { tree[key[0]] = value } if len(key) == 0 { return } var ok bool tree, ok = tree[key[0]].(map[string]any) if !ok { return } set(tree, value, key[1:]...) } ``` </p></details> <details><summary>Sample output from viewtoken tool</summary><p> ```console $ bin/viewtoken@linux-amd64 /tmp/bearer/rw-02aac9fcb56c933ffef20d7efac793ce77e4093e66b641a11a50ae45328fb52be3.token # /tmp/bearer/rw-02aac9fcb56c933ffef20d7efac793ce77e4093e66b641a11a50ae45328fb52be3.token { "body": { "allowImpersonate": false, "apeOverride": { "chains": [ { "raw": { "ID": null, "Rules": [ { "Status": "Allow", "Actions": { "Inverted": false, "Names": [ "SearchObject" ] }, "Resources": { "Inverted": false, "Names": [ "native:object/*" ] }, "Any": false, "Condition": null }, { "Status": "Allow", "Actions": { "Inverted": false, "Names": [ "GetObject", "HeadObject" ] }, "Resources": { "Inverted": false, "Names": [ "native:object/*" ] }, "Any": false, "Condition": [ { "Op": "StringNotEquals", "Kind": "Resource", "Key": "ACME", "Value": "" } ] }, { "Status": "Allow", "Actions": { "Inverted": false, "Names": [ "PutObject" ] }, "Resources": { "Inverted": false, "Names": [ "native:object/*" ] }, "Any": false, "Condition": [ { "Op": "StringNotEquals", "Kind": "Resource", "Key": "ACME", "Value": "" } ] }, { "Status": "Allow", "Actions": { "Inverted": false, "Names": [ "GetObject", "HeadObject" ] }, "Resources": { "Inverted": false, "Names": [ "native:object/*" ] }, "Any": false, "Condition": [ { "Op": "StringNotEquals", "Kind": "Resource", "Key": "CERTBOX-Recepient-02aac9fcb56c933ffef20d7efac793ce77e4093e66b641a11a50ae45328fb52be3", "Value": "" } ] }, { "Status": "Allow", "Actions": { "Inverted": false, "Names": [ "PutObject" ] }, "Resources": { "Inverted": false, "Names": [ "native:object/*" ] }, "Any": false, "Condition": [ { "Op": "StringNotEquals", "Kind": "Resource", "Key": "CERTBOX-Recepient-02aac9fcb56c933ffef20d7efac793ce77e4093e66b641a11a50ae45328fb52be3", "Value": "" } ] }, { "Status": "Allow", "Actions": { "Inverted": false, "Names": [ "GetObject", "HeadObject" ] }, "Resources": { "Inverted": false, "Names": [ "native:object/*" ] }, "Any": false, "Condition": [ { "Op": "StringNotEquals", "Kind": "Resource", "Key": "CERTBOX-PeerList", "Value": "" } ] } ], "MatchType": "FirstMatch" } } ], "target": { "name": "A8wycwWu2sS3KgcJZeWHXjmvPgoYX3vSQ5aHxAHWTAHa", "type": "CONTAINER" } }, "eaclTable": null, "lifetime": { "exp": "16533", "iat": "332", "nbf": "332" }, "ownerID": { "value": "NXTYVxz19uWG3X7HGu5rzheAdTtT9GnG9A==" } }, "signature": { "key": "A9HmdSzJqgzeUUu+exZ41aEehIZGKSAXfy2s7LUWCNuW", "scheme": "ECDSA_SHA512", "signature": "BKqAL1pk+PmzYkFGkZQ98rESwjq0AniE1YHK4SFCUzXC2gfOUHWHuspMWfElo3WiT/yLIOhKIxC3vX8QEfenFzg=" } } ```` </p></details> My tool is not perfect: - It's JSON is only for humans to read and pretty much invalid for any non-trivial deserialization. If we decide to implement `frostfs-cli bearer view` we should probably switch output to a bespoke key-value format to avoid implying any machine-readability (similar to how netinfo is printed). - UX is basically non-existent ## Describe alternatives you've considered Keep things as they are and leave bearer tokens unreadable.
potyarkin added the
enhancement
discussion
frostfs-cli
P3
triage
labels 2024-11-14 10:50:02 +00:00
Member

I wrote a simple viewer tool

The link you provided is broken. Could you please fix it?

> I wrote a simple viewer tool The link you provided is broken. Could you please fix it?
Author
Member

The link you provided is broken. Could you please fix it?

It's a private repo. I've added you to access list, but the full code listing for the tool is also available in the collapsible section below the link.

> The link you provided is broken. Could you please fix it? It's a private repo. I've added you to access list, but the full code listing for the tool is also available in the collapsible section below the link.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: TrueCloudLab/frostfs-node#1498
No description provided.