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(¬ificationPermission, &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