permission.go 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. package permission
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "path/filepath"
  7. "strings"
  8. "sync"
  9. "log/slog"
  10. "github.com/google/uuid"
  11. "github.com/sst/opencode/internal/config"
  12. "github.com/sst/opencode/internal/pubsub"
  13. )
  14. var ErrorPermissionDenied = errors.New("permission denied")
  15. type CreatePermissionRequest struct {
  16. SessionID string `json:"session_id"`
  17. ToolName string `json:"tool_name"`
  18. Description string `json:"description"`
  19. Action string `json:"action"`
  20. Params any `json:"params"`
  21. Path string `json:"path"`
  22. }
  23. type PermissionRequest struct {
  24. ID string `json:"id"`
  25. SessionID string `json:"session_id"`
  26. ToolName string `json:"tool_name"`
  27. Description string `json:"description"`
  28. Action string `json:"action"`
  29. Params any `json:"params"`
  30. Path string `json:"path"`
  31. }
  32. type PermissionResponse struct {
  33. Request PermissionRequest
  34. Granted bool
  35. }
  36. const (
  37. EventPermissionRequested pubsub.EventType = "permission_requested"
  38. EventPermissionGranted pubsub.EventType = "permission_granted"
  39. EventPermissionDenied pubsub.EventType = "permission_denied"
  40. EventPermissionPersisted pubsub.EventType = "permission_persisted"
  41. )
  42. type Service interface {
  43. pubsub.Subscriber[PermissionRequest]
  44. SubscribeToResponseEvents(ctx context.Context) <-chan pubsub.Event[PermissionResponse]
  45. GrantPersistant(ctx context.Context, permission PermissionRequest)
  46. Grant(ctx context.Context, permission PermissionRequest)
  47. Deny(ctx context.Context, permission PermissionRequest)
  48. Request(ctx context.Context, opts CreatePermissionRequest) bool
  49. AutoApproveSession(ctx context.Context, sessionID string)
  50. IsAutoApproved(ctx context.Context, sessionID string) bool
  51. }
  52. type permissionService struct {
  53. broker *pubsub.Broker[PermissionRequest]
  54. responseBroker *pubsub.Broker[PermissionResponse]
  55. sessionPermissions map[string][]PermissionRequest
  56. pendingRequests sync.Map
  57. autoApproveSessions map[string]bool
  58. mu sync.RWMutex
  59. }
  60. var globalPermissionService *permissionService
  61. func InitService() error {
  62. if globalPermissionService != nil {
  63. return fmt.Errorf("permission service already initialized")
  64. }
  65. globalPermissionService = &permissionService{
  66. broker: pubsub.NewBroker[PermissionRequest](),
  67. responseBroker: pubsub.NewBroker[PermissionResponse](),
  68. sessionPermissions: make(map[string][]PermissionRequest),
  69. autoApproveSessions: make(map[string]bool),
  70. }
  71. return nil
  72. }
  73. func GetService() *permissionService {
  74. if globalPermissionService == nil {
  75. panic("permission service not initialized. Call permission.InitService() first.")
  76. }
  77. return globalPermissionService
  78. }
  79. func (s *permissionService) GrantPersistant(ctx context.Context, permission PermissionRequest) {
  80. s.mu.Lock()
  81. s.sessionPermissions[permission.SessionID] = append(s.sessionPermissions[permission.SessionID], permission)
  82. s.mu.Unlock()
  83. respCh, ok := s.pendingRequests.Load(permission.ID)
  84. if ok {
  85. select {
  86. case respCh.(chan bool) <- true:
  87. case <-ctx.Done():
  88. slog.Warn("Context cancelled while sending grant persistent response", "request_id", permission.ID)
  89. }
  90. }
  91. s.responseBroker.Publish(EventPermissionPersisted, PermissionResponse{Request: permission, Granted: true})
  92. }
  93. func (s *permissionService) Grant(ctx context.Context, permission PermissionRequest) {
  94. respCh, ok := s.pendingRequests.Load(permission.ID)
  95. if ok {
  96. select {
  97. case respCh.(chan bool) <- true:
  98. case <-ctx.Done():
  99. slog.Warn("Context cancelled while sending grant response", "request_id", permission.ID)
  100. }
  101. }
  102. s.responseBroker.Publish(EventPermissionGranted, PermissionResponse{Request: permission, Granted: true})
  103. }
  104. func (s *permissionService) Deny(ctx context.Context, permission PermissionRequest) {
  105. respCh, ok := s.pendingRequests.Load(permission.ID)
  106. if ok {
  107. select {
  108. case respCh.(chan bool) <- false:
  109. case <-ctx.Done():
  110. slog.Warn("Context cancelled while sending deny response", "request_id", permission.ID)
  111. }
  112. }
  113. s.responseBroker.Publish(EventPermissionDenied, PermissionResponse{Request: permission, Granted: false})
  114. }
  115. func (s *permissionService) Request(ctx context.Context, opts CreatePermissionRequest) bool {
  116. s.mu.RLock()
  117. if s.autoApproveSessions[opts.SessionID] {
  118. s.mu.RUnlock()
  119. return true
  120. }
  121. requestPath := opts.Path
  122. if !filepath.IsAbs(requestPath) {
  123. requestPath = filepath.Join(config.WorkingDirectory(), requestPath)
  124. }
  125. requestPath = filepath.Clean(requestPath)
  126. if permissions, ok := s.sessionPermissions[opts.SessionID]; ok {
  127. for _, p := range permissions {
  128. storedPath := p.Path
  129. if !filepath.IsAbs(storedPath) {
  130. storedPath = filepath.Join(config.WorkingDirectory(), storedPath)
  131. }
  132. storedPath = filepath.Clean(storedPath)
  133. if p.ToolName == opts.ToolName && p.Action == opts.Action &&
  134. (requestPath == storedPath || strings.HasPrefix(requestPath, storedPath+string(filepath.Separator))) {
  135. s.mu.RUnlock()
  136. return true
  137. }
  138. }
  139. }
  140. s.mu.RUnlock()
  141. normalizedPath := opts.Path
  142. if !filepath.IsAbs(normalizedPath) {
  143. normalizedPath = filepath.Join(config.WorkingDirectory(), normalizedPath)
  144. }
  145. normalizedPath = filepath.Clean(normalizedPath)
  146. permissionReq := PermissionRequest{
  147. ID: uuid.New().String(),
  148. Path: normalizedPath,
  149. SessionID: opts.SessionID,
  150. ToolName: opts.ToolName,
  151. Description: opts.Description,
  152. Action: opts.Action,
  153. Params: opts.Params,
  154. }
  155. respCh := make(chan bool, 1)
  156. s.pendingRequests.Store(permissionReq.ID, respCh)
  157. defer s.pendingRequests.Delete(permissionReq.ID)
  158. s.broker.Publish(EventPermissionRequested, permissionReq)
  159. select {
  160. case resp := <-respCh:
  161. return resp
  162. case <-ctx.Done():
  163. slog.Warn("Permission request timed out or context cancelled", "request_id", permissionReq.ID, "tool", opts.ToolName)
  164. return false
  165. }
  166. }
  167. func (s *permissionService) AutoApproveSession(ctx context.Context, sessionID string) {
  168. s.mu.Lock()
  169. defer s.mu.Unlock()
  170. s.autoApproveSessions[sessionID] = true
  171. }
  172. func (s *permissionService) IsAutoApproved(ctx context.Context, sessionID string) bool {
  173. s.mu.RLock()
  174. defer s.mu.RUnlock()
  175. return s.autoApproveSessions[sessionID]
  176. }
  177. func (s *permissionService) Subscribe(ctx context.Context) <-chan pubsub.Event[PermissionRequest] {
  178. return s.broker.Subscribe(ctx)
  179. }
  180. func (s *permissionService) SubscribeToResponseEvents(ctx context.Context) <-chan pubsub.Event[PermissionResponse] {
  181. return s.responseBroker.Subscribe(ctx)
  182. }
  183. func GrantPersistant(ctx context.Context, permission PermissionRequest) {
  184. GetService().GrantPersistant(ctx, permission)
  185. }
  186. func Grant(ctx context.Context, permission PermissionRequest) {
  187. GetService().Grant(ctx, permission)
  188. }
  189. func Deny(ctx context.Context, permission PermissionRequest) {
  190. GetService().Deny(ctx, permission)
  191. }
  192. func Request(ctx context.Context, opts CreatePermissionRequest) bool {
  193. return GetService().Request(ctx, opts)
  194. }
  195. func AutoApproveSession(ctx context.Context, sessionID string) {
  196. GetService().AutoApproveSession(ctx, sessionID)
  197. }
  198. func IsAutoApproved(ctx context.Context, sessionID string) bool {
  199. return GetService().IsAutoApproved(ctx, sessionID)
  200. }
  201. func SubscribeToRequests(ctx context.Context) <-chan pubsub.Event[PermissionRequest] {
  202. return GetService().Subscribe(ctx)
  203. }
  204. func SubscribeToResponses(ctx context.Context) <-chan pubsub.Event[PermissionResponse] {
  205. return GetService().SubscribeToResponseEvents(ctx)
  206. }