lightning-terminal/perms/manager.go
2024-10-14 15:48:17 +02:00

300 lines
8.3 KiB
Go

package perms
import (
"regexp"
"strings"
"sync"
"github.com/lightningnetwork/lnd"
"github.com/lightningnetwork/lnd/lnrpc"
"gopkg.in/macaroon-bakery.v2/bakery"
)
const (
litPerms string = "lit"
lndPerms string = "lnd"
)
// Manager manages the permission lists that Lit requires.
type Manager struct {
// lndSubServerPerms is a map from LND subserver name to permissions
// map. This is used once the manager receives a list of build tags
// that LND has been compiled with so that the correct permissions can
// be extracted based on subservers that LND has been compiled with.
lndSubServerPerms map[string]map[string][]bakery.Op
// fixedPerms is constructed once on creation of the Manager.
// It contains all the permissions that will not change throughout the
// lifetime of the manager. It maps sub-server name to uri to permission
// operations.
fixedPerms map[string]map[string][]bakery.Op
// perms is a map containing all permissions that the manager knows
// are available for use. This map will start out not including any of
// lnd's sub-server permissions. Only when the LND build tags are
// obtained and OnLNDBuildTags is called will this map include the
// available LND sub-server permissions.
perms map[string][]bakery.Op
mu sync.RWMutex
}
// NewManager constructs a new Manager instance and collects any of the
// fixed permissions. If withAllSubServers is true, then all the LND sub-server
// permissions will be added to the available permissions set regardless of
// whether LND was compiled with those sub-servers. If it is not set, however,
// then OnLNDBuildTags can be used to specify the exact sub-servers that LND
// was compiled with and then only the corresponding permissions will be added.
func NewManager(withAllSubServers bool) (*Manager, error) {
permissions := make(map[string]map[string][]bakery.Op)
permissions[litPerms] = RequiredPermissions
permissions[lndPerms] = lnd.MainRPCServerPermissions()
for url := range whiteListedLitMethods {
permissions[litPerms][url] = []bakery.Op{}
}
for url := range whiteListedLNDMethods {
permissions[lndPerms][url] = []bakery.Op{}
}
// Collect all LND sub-server permissions along with the name of the
// sub-server that each permission is associated with.
lndSubServerPerms := make(map[string]map[string][]bakery.Op)
ss := lnrpc.RegisteredSubServers()
for _, subServer := range ss {
_, perms, err := subServer.NewGrpcHandler().CreateSubServer(
&mockConfig{},
)
if err != nil {
return nil, err
}
name := subServer.SubServerName
lndSubServerPerms[name] = make(map[string][]bakery.Op)
for key, value := range perms {
lndSubServerPerms[name][key] = value
// If this sub-server is one that we know is
// automatically compiled in LND then we add it to our
// map of active permissions. We also add the permission
// if withAllSubServers is true.
if withAllSubServers ||
lndAutoCompiledSubServers[name] {
permissions[lndPerms][key] = value
}
}
}
allPerms := make(map[string][]bakery.Op)
for _, perms := range permissions {
for k, v := range perms {
allPerms[k] = v
}
}
return &Manager{
lndSubServerPerms: lndSubServerPerms,
fixedPerms: permissions,
perms: allPerms,
}, nil
}
// IsWhiteListedURL returns true if the given URL has been whitelisted meaning
// that it does not require a macaroon for validation. A URL is considered
// white-listed if it has no operations associated with a URL.
func (pm *Manager) IsWhiteListedURL(url string) bool {
pm.mu.Lock()
defer pm.mu.Unlock()
ops, ok := pm.perms[url]
return ok && len(ops) == 0
}
// RegisterSubServer adds the permissions of a given sub-server to the set
// managed by the Manager.
func (pm *Manager) RegisterSubServer(name string,
permissions map[string][]bakery.Op, whiteListURLs map[string]struct{}) {
pm.mu.Lock()
defer pm.mu.Unlock()
pm.fixedPerms[name] = permissions
for uri, ops := range permissions {
pm.perms[uri] = ops
}
for url := range whiteListURLs {
pm.perms[url] = nil
if pm.fixedPerms[name] == nil {
pm.fixedPerms[name] = make(map[string][]bakery.Op)
}
pm.fixedPerms[name][url] = []bakery.Op{}
}
}
// OnLNDBuildTags should be called once a list of LND build tags has been
// obtained. It then uses those build tags to decide which of the LND sub-server
// permissions to add to the main permissions list. This method should only
// be called once.
func (pm *Manager) OnLNDBuildTags(lndBuildTags []string) {
pm.mu.Lock()
defer pm.mu.Unlock()
tagLookup := make(map[string]bool)
for _, t := range lndBuildTags {
tagLookup[strings.ToLower(t)] = true
}
for subServerName, perms := range pm.lndSubServerPerms {
name := subServerName
if tagName, ok := lndSubServerNameToTag[name]; ok {
name = tagName
}
if !tagLookup[strings.ToLower(name)] {
continue
}
for key, value := range perms {
pm.perms[key] = value
}
}
}
// URIPermissions returns a list of permission operations for the given URI if
// the uri is known to the manager. The second return parameter will be false
// if the URI is unknown to the manager.
func (pm *Manager) URIPermissions(uri string) ([]bakery.Op, bool) {
pm.mu.RLock()
defer pm.mu.RUnlock()
ops, ok := pm.perms[uri]
return ops, ok
}
// MatchRegexURI first checks that the given URI is in fact a regex. If it is,
// then it is used to match on the perms that the manager has. The return values
// are a list of URIs that match the regex and the boolean represents whether
// the given uri is in fact a regex.
func (pm *Manager) MatchRegexURI(uriRegex string) ([]string, bool) {
pm.mu.RLock()
defer pm.mu.RUnlock()
// If the given uri string is one of our permissions, then it is not
// a regex.
if _, ok := pm.perms[uriRegex]; ok {
return nil, false
}
// Construct the regex type from the given string.
r, err := regexp.Compile(uriRegex)
if err != nil {
return nil, false
}
// Iterate over the list of permissions and collect all permissions that
// match the given regex.
var matches []string
for uri := range pm.perms {
if !r.MatchString(uri) {
continue
}
matches = append(matches, uri)
}
return matches, true
}
// ActivePermissions returns all the available active permissions that the
// manager is aware of. Optionally, readOnly can be set to true if only the
// read-only permissions should be returned.
func (pm *Manager) ActivePermissions(readOnly bool) []bakery.Op {
pm.mu.RLock()
defer pm.mu.RUnlock()
// De-dup the permissions and optionally apply the read-only filter.
dedupMap := make(map[string]map[string]bool)
for _, methodPerms := range pm.perms {
for _, methodPerm := range methodPerms {
if methodPerm.Action == "" || methodPerm.Entity == "" {
continue
}
if readOnly && methodPerm.Action != "read" {
continue
}
if dedupMap[methodPerm.Entity] == nil {
dedupMap[methodPerm.Entity] = make(
map[string]bool,
)
}
dedupMap[methodPerm.Entity][methodPerm.Action] = true
}
}
result := make([]bakery.Op, 0, len(dedupMap))
for entity, actions := range dedupMap {
for action := range actions {
result = append(result, bakery.Op{
Entity: entity,
Action: action,
})
}
}
return result
}
// GetLitPerms returns a map of all permissions that the manager is aware of
// _except_ for any LND permissions. In other words, this returns permissions
// for which the external validator of Lit is responsible.
func (pm *Manager) GetLitPerms() map[string][]bakery.Op {
pm.mu.Lock()
defer pm.mu.Unlock()
result := make(map[string][]bakery.Op)
for subserver, ops := range pm.fixedPerms {
if subserver == lndPerms {
continue
}
for key, value := range ops {
result[key] = value
}
}
return result
}
// IsSubServerURI if the given URI belongs to the RPC of the given server.
func (pm *Manager) IsSubServerURI(name string, uri string) bool {
pm.mu.Lock()
defer pm.mu.Unlock()
if name == lndPerms {
return pm.isLndURI(uri)
}
_, ok := pm.fixedPerms[name][uri]
return ok
}
// isLndURI returns true if the given URI belongs to an RPC of lnd.
func (pm *Manager) isLndURI(uri string) bool {
var lndSubServerCall bool
for _, subserverPermissions := range pm.lndSubServerPerms {
_, found := subserverPermissions[uri]
if found {
lndSubServerCall = true
break
}
}
_, lndCall := pm.fixedPerms[lndPerms][uri]
return lndCall || lndSubServerCall
}