permissions.go raw

   1  package permissions
   2  
   3  import (
   4  	"fmt"
   5  	"slices"
   6  	"time"
   7  
   8  	"github.com/getAlby/hub/constants"
   9  	"github.com/getAlby/hub/db"
  10  	"github.com/getAlby/hub/events"
  11  	"github.com/getAlby/hub/lnclient"
  12  	"github.com/getAlby/hub/logger"
  13  	"github.com/getAlby/hub/nip47/models"
  14  	"github.com/getAlby/hub/utils"
  15  	"github.com/sirupsen/logrus"
  16  	"gorm.io/gorm"
  17  )
  18  
  19  type permissionsService struct {
  20  	db             *gorm.DB
  21  	eventPublisher events.EventPublisher
  22  }
  23  
  24  // TODO: does this need to be a service?
  25  type PermissionsService interface {
  26  	HasPermission(app *db.App, requestMethod string) (result bool, code string, message string)
  27  	GetPermittedMethods(app *db.App, lnClient lnclient.LNClient) []string
  28  	PermitsNotifications(app *db.App) bool
  29  }
  30  
  31  func NewPermissionsService(db *gorm.DB, eventPublisher events.EventPublisher) *permissionsService {
  32  	return &permissionsService{
  33  		db:             db,
  34  		eventPublisher: eventPublisher,
  35  	}
  36  }
  37  
  38  func (svc *permissionsService) HasPermission(app *db.App, scope string) (result bool, code string, message string) {
  39  	appPermission := db.AppPermission{}
  40  	findPermissionResult := svc.db.Limit(1).Find(&appPermission, &db.AppPermission{
  41  		AppId: app.ID,
  42  		Scope: scope,
  43  	})
  44  	if findPermissionResult.RowsAffected == 0 {
  45  		// No permission for this request method
  46  		return false, constants.ERROR_RESTRICTED, fmt.Sprintf("This app does not have the %s scope", scope)
  47  	}
  48  	expiresAt := appPermission.ExpiresAt
  49  	if expiresAt != nil && expiresAt.Before(time.Now()) {
  50  		logger.Logger.WithFields(logrus.Fields{
  51  			"scope":     scope,
  52  			"expiresAt": expiresAt.Unix(),
  53  			"appId":     app.ID,
  54  			"pubkey":    app.AppPubkey,
  55  		}).Info("This pubkey is expired")
  56  
  57  		return false, constants.ERROR_EXPIRED, "This app has expired"
  58  	}
  59  
  60  	return true, "", ""
  61  }
  62  
  63  func (svc *permissionsService) GetPermittedMethods(app *db.App, lnClient lnclient.LNClient) []string {
  64  	if lnClient == nil {
  65  		return []string{}
  66  	}
  67  
  68  	appPermissions := []db.AppPermission{}
  69  	svc.db.Where("app_id = ?", app.ID).Find(&appPermissions)
  70  	scopes := make([]string, 0, len(appPermissions))
  71  	for _, appPermission := range appPermissions {
  72  		scopes = append(scopes, appPermission.Scope)
  73  	}
  74  
  75  	requestMethods := scopesToRequestMethods(scopes)
  76  
  77  	for _, method := range GetAlwaysGrantedMethods() {
  78  		if !slices.Contains(requestMethods, method) {
  79  			requestMethods = append(requestMethods, method)
  80  		}
  81  	}
  82  
  83  	// only return methods supported by the lnClient
  84  	lnClientSupportedMethods := lnClient.GetSupportedNIP47Methods()
  85  	requestMethods = utils.Filter(requestMethods, func(requestMethod string) bool {
  86  		// TODO: better way to exclude methods unrelated to the lnclient
  87  		if requestMethod == models.CREATE_CONNECTION_METHOD {
  88  			return true
  89  		}
  90  
  91  		return slices.Contains(lnClientSupportedMethods, requestMethod)
  92  	})
  93  
  94  	return requestMethods
  95  }
  96  
  97  func (svc *permissionsService) PermitsNotifications(app *db.App) bool {
  98  	notificationPermission := db.AppPermission{}
  99  	result := svc.db.Limit(1).Find(&notificationPermission, &db.AppPermission{
 100  		AppId: app.ID,
 101  		Scope: constants.NOTIFICATIONS_SCOPE,
 102  	})
 103  
 104  	return result.Error == nil && result.RowsAffected > 0
 105  }
 106  
 107  func scopesToRequestMethods(scopes []string) []string {
 108  	requestMethods := []string{}
 109  
 110  	for _, scope := range scopes {
 111  		scopeRequestMethods := scopeToRequestMethods(scope)
 112  		requestMethods = append(requestMethods, scopeRequestMethods...)
 113  	}
 114  	return requestMethods
 115  }
 116  
 117  func scopeToRequestMethods(scope string) []string {
 118  	switch scope {
 119  	case constants.PAY_INVOICE_SCOPE:
 120  		return []string{models.PAY_INVOICE_METHOD, models.PAY_KEYSEND_METHOD, models.MULTI_PAY_INVOICE_METHOD, models.MULTI_PAY_KEYSEND_METHOD}
 121  	case constants.GET_BALANCE_SCOPE:
 122  		return []string{models.GET_BALANCE_METHOD}
 123  	case constants.GET_INFO_SCOPE:
 124  		return []string{models.GET_INFO_METHOD}
 125  	case constants.MAKE_INVOICE_SCOPE:
 126  		return []string{models.MAKE_INVOICE_METHOD, models.MAKE_HOLD_INVOICE_METHOD, models.SETTLE_HOLD_INVOICE_METHOD, models.CANCEL_HOLD_INVOICE_METHOD}
 127  	case constants.LOOKUP_INVOICE_SCOPE:
 128  		return []string{models.LOOKUP_INVOICE_METHOD}
 129  	case constants.LIST_TRANSACTIONS_SCOPE:
 130  		return []string{models.LIST_TRANSACTIONS_METHOD}
 131  	case constants.SIGN_MESSAGE_SCOPE:
 132  		return []string{models.SIGN_MESSAGE_METHOD}
 133  	case constants.SUPERUSER_SCOPE:
 134  		return []string{models.CREATE_CONNECTION_METHOD}
 135  	}
 136  	return []string{}
 137  }
 138  
 139  func RequestMethodsToScopes(requestMethods []string) ([]string, error) {
 140  	scopes := []string{}
 141  
 142  	for _, requestMethod := range requestMethods {
 143  		scope, err := RequestMethodToScope(requestMethod)
 144  		if err != nil {
 145  			return nil, err
 146  		}
 147  		if scope != "" && !slices.Contains(scopes, scope) {
 148  			scopes = append(scopes, scope)
 149  		}
 150  	}
 151  	return scopes, nil
 152  }
 153  
 154  func RequestMethodToScope(requestMethod string) (string, error) {
 155  	switch requestMethod {
 156  	case models.PAY_INVOICE_METHOD, models.PAY_KEYSEND_METHOD, models.MULTI_PAY_INVOICE_METHOD, models.MULTI_PAY_KEYSEND_METHOD:
 157  		return constants.PAY_INVOICE_SCOPE, nil
 158  	case models.GET_BALANCE_METHOD:
 159  		return constants.GET_BALANCE_SCOPE, nil
 160  	case models.GET_BUDGET_METHOD:
 161  		return "", nil
 162  	case models.GET_INFO_METHOD:
 163  		return constants.GET_INFO_SCOPE, nil
 164  	case models.MAKE_INVOICE_METHOD:
 165  		return constants.MAKE_INVOICE_SCOPE, nil
 166  	case models.LOOKUP_INVOICE_METHOD:
 167  		return constants.LOOKUP_INVOICE_SCOPE, nil
 168  	case models.LIST_TRANSACTIONS_METHOD:
 169  		return constants.LIST_TRANSACTIONS_SCOPE, nil
 170  	case models.SIGN_MESSAGE_METHOD:
 171  		return constants.SIGN_MESSAGE_SCOPE, nil
 172  	case models.MAKE_HOLD_INVOICE_METHOD, models.SETTLE_HOLD_INVOICE_METHOD, models.CANCEL_HOLD_INVOICE_METHOD:
 173  		return constants.MAKE_INVOICE_SCOPE, nil
 174  	case models.CREATE_CONNECTION_METHOD:
 175  		return constants.SUPERUSER_SCOPE, nil
 176  	}
 177  	logger.Logger.WithField("request_method", requestMethod).Error("Unsupported request method")
 178  	return "", fmt.Errorf("unsupported request method: %s", requestMethod)
 179  }
 180  
 181  func AllScopes() []string {
 182  	return []string{
 183  		constants.PAY_INVOICE_SCOPE,
 184  		constants.GET_BALANCE_SCOPE,
 185  		constants.GET_INFO_SCOPE,
 186  		constants.MAKE_INVOICE_SCOPE,
 187  		constants.LOOKUP_INVOICE_SCOPE,
 188  		constants.LIST_TRANSACTIONS_SCOPE,
 189  		constants.SIGN_MESSAGE_SCOPE,
 190  		constants.NOTIFICATIONS_SCOPE,
 191  		constants.SUPERUSER_SCOPE,
 192  	}
 193  }
 194  
 195  func GetAlwaysGrantedMethods() []string {
 196  	return []string{models.GET_INFO_METHOD, models.GET_BUDGET_METHOD}
 197  }
 198