diff --git a/macaroons/README.md b/macaroons/README.md index 8e379c0a..863f435b 100644 --- a/macaroons/README.md +++ b/macaroons/README.md @@ -100,8 +100,18 @@ key `0` would be created with the following command: `lncli bakemacaroon peers:read peers:write` -A full and up-to-date list of available entity/action pairs can be found by -looking at the `rpcserver.go` in the root folder of the project. +For even more fine-grained permission control, it is also possible to specify +single RPC method URIs that are allowed to be accessed by a macaroon. This can +be achieved by passing `uri:` pairs to `bakemacaroon`, for example: + +`lncli bakemacaroon uri:/lnrpc.Lightning/GetInfo uri:/verrpc.Versioner/GetVersion` + +The macaroon created by this call would only be allowed to call the `GetInfo` and +`GetVersion` methods instead of all methods that have similar permissions (like +`info:read` for example). + +A full list of available entity/action pairs and RPC method URIs can be queried +by using the `lncli listpermissions` command. ### Upgrading from v0.8.0-beta or earlier diff --git a/macaroons/service.go b/macaroons/service.go index 823ef0fc..0e84ddeb 100644 --- a/macaroons/service.go +++ b/macaroons/service.go @@ -27,6 +27,15 @@ var ( // ErrDeletionForbidden is used when attempting to delete the // DefaultRootKeyID or the encryptedKeyID. ErrDeletionForbidden = fmt.Errorf("the specified ID cannot be deleted") + + // PermissionEntityCustomURI is a special entity name for a permission + // that does not describe an entity:action pair but instead specifies a + // specific URI that needs to be granted access to. This can be used for + // more fine-grained permissions where a macaroon only grants access to + // certain methods instead of a whole list of methods that define the + // same entity:action pairs. For example: uri:/lnrpc.Lightning/GetInfo + // only gives access to the GetInfo call. + PermissionEntityCustomURI = "uri" ) // Service encapsulates bakery.Bakery and adds a Close() method that zeroes the @@ -118,12 +127,15 @@ func (svc *Service) UnaryServerInterceptor( info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { - if _, ok := permissionMap[info.FullMethod]; !ok { + uriPermissions, ok := permissionMap[info.FullMethod] + if !ok { return nil, fmt.Errorf("%s: unknown permissions "+ "required for method", info.FullMethod) } - err := svc.ValidateMacaroon(ctx, permissionMap[info.FullMethod]) + err := svc.ValidateMacaroon( + ctx, uriPermissions, info.FullMethod, + ) if err != nil { return nil, err } @@ -140,13 +152,14 @@ func (svc *Service) StreamServerInterceptor( return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { - if _, ok := permissionMap[info.FullMethod]; !ok { + uriPermissions, ok := permissionMap[info.FullMethod] + if !ok { return fmt.Errorf("%s: unknown permissions required "+ "for method", info.FullMethod) } err := svc.ValidateMacaroon( - ss.Context(), permissionMap[info.FullMethod], + ss.Context(), uriPermissions, info.FullMethod, ) if err != nil { return err @@ -161,7 +174,7 @@ func (svc *Service) StreamServerInterceptor( // expect a macaroon to be encoded as request metadata using the key // "macaroon". func (svc *Service) ValidateMacaroon(ctx context.Context, - requiredPermissions []bakery.Op) error { + requiredPermissions []bakery.Op, fullMethod string) error { // Get macaroon bytes from context and unmarshal into macaroon. md, ok := metadata.FromIncomingContext(ctx) @@ -190,6 +203,20 @@ func (svc *Service) ValidateMacaroon(ctx context.Context, // the expiration time and IP address and return the result. authChecker := svc.Checker.Auth(macaroon.Slice{mac}) _, err = authChecker.Allow(ctx, requiredPermissions...) + + // If the macaroon contains broad permissions and checks out, we're + // done. + if err == nil { + return nil + } + + // To also allow the special permission of "uri:" to be a + // valid permission, we need to check it manually in case there is no + // broader scope permission defined. + _, err = authChecker.Allow(ctx, bakery.Op{ + Entity: PermissionEntityCustomURI, + Action: fullMethod, + }) return err } diff --git a/macaroons/service_test.go b/macaroons/service_test.go index 32f2327c..90c9b573 100644 --- a/macaroons/service_test.go +++ b/macaroons/service_test.go @@ -21,6 +21,10 @@ var ( Entity: "testEntity", Action: "read", } + testOperationURI = bakery.Op{ + Entity: macaroons.PermissionEntityCustomURI, + Action: "SomeMethod", + } defaultPw = []byte("hello") ) @@ -125,6 +129,7 @@ func TestValidateMacaroon(t *testing.T) { // Then, create a new macaroon that we can serialize. macaroon, err := service.NewMacaroon( context.TODO(), macaroons.DefaultRootKeyID, testOperation, + testOperationURI, ) if err != nil { t.Fatalf("Error creating macaroon from service: %v", err) @@ -142,7 +147,18 @@ func TestValidateMacaroon(t *testing.T) { mockContext := metadata.NewIncomingContext(context.Background(), md) // Finally, validate the macaroon against the required permissions. - err = service.ValidateMacaroon(mockContext, []bakery.Op{testOperation}) + err = service.ValidateMacaroon( + mockContext, []bakery.Op{testOperation}, "FooMethod", + ) + if err != nil { + t.Fatalf("Error validating the macaroon: %v", err) + } + + // If the macaroon has the method specific URI permission, the list of + // required entity/action pairs is irrelevant. + err = service.ValidateMacaroon( + mockContext, []bakery.Op{{Entity: "irrelevant"}}, "SomeMethod", + ) if err != nil { t.Fatalf("Error validating the macaroon: %v", err) }