github.com/decred/dcrlnd@v0.7.6/cmd/dcrlncli/cmd_macaroon.go (about)

     1  package main
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/hex"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"net"
     9  	"strconv"
    10  	"strings"
    11  	"unicode"
    12  
    13  	"github.com/decred/dcrlnd/lncfg"
    14  	"github.com/decred/dcrlnd/lnrpc"
    15  	"github.com/decred/dcrlnd/macaroons"
    16  	"github.com/golang/protobuf/proto"
    17  	"github.com/urfave/cli"
    18  	"gopkg.in/macaroon-bakery.v2/bakery"
    19  	"gopkg.in/macaroon.v2"
    20  )
    21  
    22  var bakeMacaroonCommand = cli.Command{
    23  	Name:     "bakemacaroon",
    24  	Category: "Macaroons",
    25  	Usage: "Bakes a new macaroon with the provided list of permissions " +
    26  		"and restrictions.",
    27  	ArgsUsage: "[--save_to=] [--timeout=] [--ip_address=] [--allow_external_permissions] permissions...",
    28  	Description: `
    29  	Bake a new macaroon that grants the provided permissions and
    30  	optionally adds restrictions (timeout, IP address) to it.
    31  
    32  	The new macaroon can either be shown on command line in hex serialized
    33  	format or it can be saved directly to a file using the --save_to
    34  	argument.
    35  
    36  	A permission is a tuple of an entity and an action, separated by a
    37  	colon. Multiple operations can be added as arguments, for example:
    38  
    39  	lncli bakemacaroon info:read invoices:write foo:bar
    40  
    41  	For even more fine-grained permission control, it is also possible to
    42  	specify single RPC method URIs that are allowed to be accessed by a
    43  	macaroon. This can be achieved by specifying "uri:<methodURI>" pairs,
    44  	for example:
    45  
    46  	lncli bakemacaroon uri:/lnrpc.Lightning/GetInfo uri:/verrpc.Versioner/GetVersion
    47  
    48  	The macaroon created by this command would only be allowed to use the
    49  	"lncli getinfo" and "lncli version" commands.
    50  
    51  	To get a list of all available URIs and permissions, use the
    52  	"lncli listpermissions" command.
    53  	`,
    54  	Flags: []cli.Flag{
    55  		cli.StringFlag{
    56  			Name: "save_to",
    57  			Usage: "save the created macaroon to this file " +
    58  				"using the default binary format",
    59  		},
    60  		cli.Uint64Flag{
    61  			Name: "timeout",
    62  			Usage: "the number of seconds the macaroon will be " +
    63  				"valid before it times out",
    64  		},
    65  		cli.StringFlag{
    66  			Name:  "ip_address",
    67  			Usage: "the IP address the macaroon will be bound to",
    68  		},
    69  		cli.StringFlag{
    70  			Name:  "custom_caveat_name",
    71  			Usage: "the name of the custom caveat to add",
    72  		},
    73  		cli.StringFlag{
    74  			Name: "custom_caveat_condition",
    75  			Usage: "the condition of the custom caveat to add, " +
    76  				"can be empty if custom caveat doesn't need " +
    77  				"a value",
    78  		},
    79  		cli.Uint64Flag{
    80  			Name:  "root_key_id",
    81  			Usage: "the numerical root key ID used to create the macaroon",
    82  		},
    83  		cli.BoolFlag{
    84  			Name:  "allow_external_permissions",
    85  			Usage: "whether permissions lnd is not familiar with are allowed",
    86  		},
    87  	},
    88  	Action: actionDecorator(bakeMacaroon),
    89  }
    90  
    91  func bakeMacaroon(ctx *cli.Context) error {
    92  	ctxc := getContext()
    93  	client, cleanUp := getClient(ctx)
    94  	defer cleanUp()
    95  
    96  	// Show command help if no arguments.
    97  	if ctx.NArg() == 0 {
    98  		return cli.ShowCommandHelp(ctx, "bakemacaroon")
    99  	}
   100  	args := ctx.Args()
   101  
   102  	var (
   103  		savePath          string
   104  		timeout           int64
   105  		ipAddress         net.IP
   106  		customCaveatName  string
   107  		customCaveatCond  string
   108  		rootKeyID         uint64
   109  		parsedPermissions []*lnrpc.MacaroonPermission
   110  		err               error
   111  	)
   112  
   113  	if ctx.String("save_to") != "" {
   114  		savePath = lncfg.CleanAndExpandPath(ctx.String("save_to"))
   115  	}
   116  
   117  	if ctx.IsSet("timeout") {
   118  		timeout = ctx.Int64("timeout")
   119  		if timeout <= 0 {
   120  			return fmt.Errorf("timeout must be greater than 0")
   121  		}
   122  	}
   123  
   124  	if ctx.IsSet("ip_address") {
   125  		ipAddress = net.ParseIP(ctx.String("ip_address"))
   126  		if ipAddress == nil {
   127  			return fmt.Errorf("unable to parse ip_address: %s",
   128  				ctx.String("ip_address"))
   129  		}
   130  	}
   131  
   132  	if ctx.IsSet("custom_caveat_name") {
   133  		customCaveatName = ctx.String("custom_caveat_name")
   134  		if containsWhiteSpace(customCaveatName) {
   135  			return fmt.Errorf("unexpected white space found in " +
   136  				"custom caveat name")
   137  		}
   138  		if customCaveatName == "" {
   139  			return fmt.Errorf("invalid custom caveat name")
   140  		}
   141  	}
   142  
   143  	if ctx.IsSet("custom_caveat_condition") {
   144  		customCaveatCond = ctx.String("custom_caveat_condition")
   145  		if containsWhiteSpace(customCaveatCond) {
   146  			return fmt.Errorf("unexpected white space found in " +
   147  				"custom caveat condition")
   148  		}
   149  		if customCaveatCond == "" {
   150  			return fmt.Errorf("invalid custom caveat condition")
   151  		}
   152  		if customCaveatCond != "" && customCaveatName == "" {
   153  			return fmt.Errorf("cannot set custom caveat " +
   154  				"condition without custom caveat name")
   155  		}
   156  	}
   157  
   158  	if ctx.IsSet("root_key_id") {
   159  		rootKeyID = ctx.Uint64("root_key_id")
   160  	}
   161  
   162  	// A command line argument can't be an empty string. So we'll check each
   163  	// entry if it's a valid entity:action tuple. The content itself is
   164  	// validated server side. We just make sure we can parse it correctly.
   165  	for _, permission := range args {
   166  		tuple := strings.Split(permission, ":")
   167  		if len(tuple) != 2 {
   168  			return fmt.Errorf("unable to parse "+
   169  				"permission tuple: %s", permission)
   170  		}
   171  		entity, action := tuple[0], tuple[1]
   172  		if entity == "" {
   173  			return fmt.Errorf("invalid permission [%s]. entity "+
   174  				"cannot be empty", permission)
   175  		}
   176  		if action == "" {
   177  			return fmt.Errorf("invalid permission [%s]. action "+
   178  				"cannot be empty", permission)
   179  		}
   180  
   181  		// No we can assume that we have a formally valid entity:action
   182  		// tuple. The rest of the validation happens server side.
   183  		parsedPermissions = append(
   184  			parsedPermissions, &lnrpc.MacaroonPermission{
   185  				Entity: entity,
   186  				Action: action,
   187  			},
   188  		)
   189  	}
   190  
   191  	// Now we have gathered all the input we need and can do the actual
   192  	// RPC call.
   193  	req := &lnrpc.BakeMacaroonRequest{
   194  		Permissions:              parsedPermissions,
   195  		RootKeyId:                rootKeyID,
   196  		AllowExternalPermissions: ctx.Bool("allow_external_permissions"),
   197  	}
   198  	resp, err := client.BakeMacaroon(ctxc, req)
   199  	if err != nil {
   200  		return err
   201  	}
   202  
   203  	// Now we should have gotten a valid macaroon. Unmarshal it so we can
   204  	// add first-party caveats (if necessary) to it.
   205  	macBytes, err := hex.DecodeString(resp.Macaroon)
   206  	if err != nil {
   207  		return err
   208  	}
   209  	unmarshalMac := &macaroon.Macaroon{}
   210  	if err = unmarshalMac.UnmarshalBinary(macBytes); err != nil {
   211  		return err
   212  	}
   213  
   214  	// Now apply the desired constraints to the macaroon. This will always
   215  	// create a new macaroon object, even if no constraints are added.
   216  	macConstraints := make([]macaroons.Constraint, 0)
   217  	if timeout > 0 {
   218  		macConstraints = append(
   219  			macConstraints, macaroons.TimeoutConstraint(timeout),
   220  		)
   221  	}
   222  	if ipAddress != nil {
   223  		macConstraints = append(
   224  			macConstraints,
   225  			macaroons.IPLockConstraint(ipAddress.String()),
   226  		)
   227  	}
   228  
   229  	// The custom caveat condition is optional, it could just be a marker
   230  	// tag in the macaroon with just a name. The interceptor itself doesn't
   231  	// care about the value anyway.
   232  	if customCaveatName != "" {
   233  		macConstraints = append(
   234  			macConstraints, macaroons.CustomConstraint(
   235  				customCaveatName, customCaveatCond,
   236  			),
   237  		)
   238  	}
   239  	constrainedMac, err := macaroons.AddConstraints(
   240  		unmarshalMac, macConstraints...,
   241  	)
   242  	if err != nil {
   243  		return err
   244  	}
   245  	macBytes, err = constrainedMac.MarshalBinary()
   246  	if err != nil {
   247  		return err
   248  	}
   249  
   250  	// Now we can output the result. We either write it binary serialized to
   251  	// a file or write to the standard output using hex encoding.
   252  	switch {
   253  	case savePath != "":
   254  		err = ioutil.WriteFile(savePath, macBytes, 0644)
   255  		if err != nil {
   256  			return err
   257  		}
   258  		fmt.Printf("Macaroon saved to %s\n", savePath)
   259  
   260  	default:
   261  		fmt.Printf("%s\n", hex.EncodeToString(macBytes))
   262  	}
   263  
   264  	return nil
   265  }
   266  
   267  var listMacaroonIDsCommand = cli.Command{
   268  	Name:     "listmacaroonids",
   269  	Category: "Macaroons",
   270  	Usage:    "List all macaroons root key IDs in use.",
   271  	Action:   actionDecorator(listMacaroonIDs),
   272  }
   273  
   274  func listMacaroonIDs(ctx *cli.Context) error {
   275  	ctxc := getContext()
   276  	client, cleanUp := getClient(ctx)
   277  	defer cleanUp()
   278  
   279  	req := &lnrpc.ListMacaroonIDsRequest{}
   280  	resp, err := client.ListMacaroonIDs(ctxc, req)
   281  	if err != nil {
   282  		return err
   283  	}
   284  
   285  	printRespJSON(resp)
   286  	return nil
   287  }
   288  
   289  var deleteMacaroonIDCommand = cli.Command{
   290  	Name:      "deletemacaroonid",
   291  	Category:  "Macaroons",
   292  	Usage:     "Delete a specific macaroon ID.",
   293  	ArgsUsage: "root_key_id",
   294  	Description: `
   295  	Remove a macaroon ID using the specified root key ID. For example:
   296  
   297  	lncli deletemacaroonid 1
   298  
   299  	WARNING
   300  	When the ID is deleted, all macaroons created from that root key will
   301  	be invalidated.
   302  
   303  	Note that the default root key ID 0 cannot be deleted.
   304  	`,
   305  	Action: actionDecorator(deleteMacaroonID),
   306  }
   307  
   308  func deleteMacaroonID(ctx *cli.Context) error {
   309  	ctxc := getContext()
   310  	client, cleanUp := getClient(ctx)
   311  	defer cleanUp()
   312  
   313  	// Validate args length. Only one argument is allowed.
   314  	if ctx.NArg() != 1 {
   315  		return cli.ShowCommandHelp(ctx, "deletemacaroonid")
   316  	}
   317  
   318  	rootKeyIDString := ctx.Args().First()
   319  
   320  	// Convert string into uint64.
   321  	rootKeyID, err := strconv.ParseUint(rootKeyIDString, 10, 64)
   322  	if err != nil {
   323  		return fmt.Errorf("root key ID must be a positive integer")
   324  	}
   325  
   326  	// Check that the value is not equal to DefaultRootKeyID. Note that the
   327  	// server also validates the root key ID when removing it. However, we check
   328  	// it here too so that we can give users a nice warning.
   329  	if bytes.Equal([]byte(rootKeyIDString), macaroons.DefaultRootKeyID) {
   330  		return fmt.Errorf("deleting the default root key ID 0 is not allowed")
   331  	}
   332  
   333  	// Make the actual RPC call.
   334  	req := &lnrpc.DeleteMacaroonIDRequest{
   335  		RootKeyId: rootKeyID,
   336  	}
   337  	resp, err := client.DeleteMacaroonID(ctxc, req)
   338  	if err != nil {
   339  		return err
   340  	}
   341  
   342  	printRespJSON(resp)
   343  	return nil
   344  }
   345  
   346  var listPermissionsCommand = cli.Command{
   347  	Name:     "listpermissions",
   348  	Category: "Macaroons",
   349  	Usage: "Lists all RPC method URIs and the macaroon permissions they " +
   350  		"require to be invoked.",
   351  	Action: actionDecorator(listPermissions),
   352  }
   353  
   354  func listPermissions(ctx *cli.Context) error {
   355  	ctxc := getContext()
   356  	client, cleanUp := getClient(ctx)
   357  	defer cleanUp()
   358  
   359  	request := &lnrpc.ListPermissionsRequest{}
   360  	response, err := client.ListPermissions(ctxc, request)
   361  	if err != nil {
   362  		return err
   363  	}
   364  
   365  	printRespJSON(response)
   366  
   367  	return nil
   368  }
   369  
   370  type macaroonContent struct {
   371  	Version     uint16   `json:"version"`
   372  	Location    string   `json:"location"`
   373  	RootKeyID   string   `json:"root_key_id"`
   374  	Permissions []string `json:"permissions"`
   375  	Caveats     []string `json:"caveats"`
   376  }
   377  
   378  var printMacaroonCommand = cli.Command{
   379  	Name:      "printmacaroon",
   380  	Category:  "Macaroons",
   381  	Usage:     "Print the content of a macaroon in a human readable format.",
   382  	ArgsUsage: "[macaroon_content_hex]",
   383  	Description: `
   384  	Decode a macaroon and show its content in a more human readable format.
   385  	The macaroon can either be passed as a hex encoded positional parameter
   386  	or loaded from a file.
   387  	`,
   388  	Flags: []cli.Flag{
   389  		cli.StringFlag{
   390  			Name: "macaroon_file",
   391  			Usage: "load the macaroon from a file instead of the " +
   392  				"command line directly",
   393  		},
   394  	},
   395  	Action: actionDecorator(printMacaroon),
   396  }
   397  
   398  func printMacaroon(ctx *cli.Context) error {
   399  	// Show command help if no arguments or flags are set.
   400  	if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
   401  		return cli.ShowCommandHelp(ctx, "printmacaroon")
   402  	}
   403  
   404  	var (
   405  		macBytes []byte
   406  		err      error
   407  		args     = ctx.Args()
   408  	)
   409  	switch {
   410  	case ctx.IsSet("macaroon_file"):
   411  		macPath := lncfg.CleanAndExpandPath(ctx.String("macaroon_file"))
   412  
   413  		// Load the specified macaroon file.
   414  		macBytes, err = ioutil.ReadFile(macPath)
   415  		if err != nil {
   416  			return fmt.Errorf("unable to read macaroon path %v: %v",
   417  				macPath, err)
   418  		}
   419  
   420  	case args.Present():
   421  		macBytes, err = hex.DecodeString(args.First())
   422  		if err != nil {
   423  			return fmt.Errorf("unable to hex decode macaroon: %v",
   424  				err)
   425  		}
   426  
   427  	default:
   428  		return fmt.Errorf("macaroon parameter missing")
   429  	}
   430  
   431  	// Decode the macaroon and its protobuf encoded internal identifier.
   432  	mac := &macaroon.Macaroon{}
   433  	if err = mac.UnmarshalBinary(macBytes); err != nil {
   434  		return fmt.Errorf("unable to decode macaroon: %v", err)
   435  	}
   436  	rawID := mac.Id()
   437  	if rawID[0] != byte(bakery.LatestVersion) {
   438  		return fmt.Errorf("invalid macaroon version: %x", rawID)
   439  	}
   440  	decodedID := &lnrpc.MacaroonId{}
   441  	idProto := rawID[1:]
   442  	err = proto.Unmarshal(idProto, decodedID)
   443  	if err != nil {
   444  		return fmt.Errorf("unable to decode macaroon version: %v", err)
   445  	}
   446  
   447  	// Prepare everything to be printed in a more human readable format.
   448  	content := &macaroonContent{
   449  		Version:     uint16(mac.Version()),
   450  		Location:    mac.Location(),
   451  		RootKeyID:   string(decodedID.StorageId),
   452  		Permissions: nil,
   453  		Caveats:     nil,
   454  	}
   455  
   456  	for _, caveat := range mac.Caveats() {
   457  		content.Caveats = append(content.Caveats, string(caveat.Id))
   458  	}
   459  	for _, op := range decodedID.Ops {
   460  		for _, action := range op.Actions {
   461  			permission := fmt.Sprintf("%s:%s", op.Entity, action)
   462  			content.Permissions = append(
   463  				content.Permissions, permission,
   464  			)
   465  		}
   466  	}
   467  
   468  	printJSON(content)
   469  
   470  	return nil
   471  }
   472  
   473  // containsWhiteSpace returns true if the given string contains any character
   474  // that is considered to be a white space or non-printable character such as
   475  // space, tabulator, newline, carriage return and some more exotic ones.
   476  func containsWhiteSpace(str string) bool {
   477  	return strings.IndexFunc(str, unicode.IsSpace) >= 0
   478  }