@@ -24,22 +24,37 @@ import (
2424 "io"
2525 "net/http"
2626 "strings"
27+ "time"
2728
2829 corectx "github.com/apache/incubator-devlake/core/context"
2930 "github.com/apache/incubator-devlake/core/errors"
3031 helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
3132 "github.com/apache/incubator-devlake/plugins/cursor/models"
3233)
3334
35+ const (
36+ membersEndpoint = "teams/members"
37+ spendEndpoint = "teams/spend"
38+ usageEventsEndpoint = "teams/filtered-usage-events"
39+ )
40+
41+ // AdminApiPermissions reports which Cursor Admin API endpoints the key can access.
42+ type AdminApiPermissions struct {
43+ Members bool `json:"members"`
44+ Spend bool `json:"spend"`
45+ UsageEvents bool `json:"usageEvents"`
46+ }
47+
3448// TestConnectionResult represents the payload returned by the connection test endpoints.
3549type TestConnectionResult struct {
36- Success bool `json:"success"`
37- Message string `json:"message"`
38- MemberCount int `json:"memberCount,omitempty"`
39- HasEnterpriseAnalytics bool `json:"hasEnterpriseAnalytics,omitempty"`
50+ Success bool `json:"success"`
51+ Message string `json:"message"`
52+ MemberCount int `json:"memberCount,omitempty"`
53+ Permissions AdminApiPermissions `json:"permissions,omitempty"`
54+ HasEnterpriseAnalytics bool `json:"hasEnterpriseAnalytics,omitempty"`
4055}
4156
42- // TestConnection exercises the Cursor Admin API to validate credentials.
57+ // TestConnection exercises the Cursor Admin API to validate credentials and permissions .
4358func TestConnection (ctx stdctx.Context , br corectx.BasicRes , connection * models.CursorConnection ) (* TestConnectionResult , errors.Error ) {
4459 if connection == nil {
4560 return nil , errors .BadInput .New ("connection is required" )
@@ -54,6 +69,10 @@ func TestConnection(ctx stdctx.Context, br corectx.BasicRes, connection *models.
5469 if err != nil {
5570 return nil , err
5671 }
72+ apiClient .SetHeaders (map [string ]string {
73+ "Accept" : "application/json" ,
74+ "Content-Type" : "application/json" ,
75+ })
5776
5877 if userKeyErr := detectUserApiKey (apiClient ); userKeyErr != nil {
5978 return & TestConnectionResult {
@@ -62,50 +81,145 @@ func TestConnection(ctx stdctx.Context, br corectx.BasicRes, connection *models.
6281 }, nil
6382 }
6483
65- res , err := apiClient .Get ("teams/members" , nil , nil )
66- if err != nil {
67- return nil , errors .Default .Wrap (err , "failed to reach Cursor Admin API" )
84+ permissions := AdminApiPermissions {}
85+ var failures []string
86+
87+ memberCount , membersErr := probeMembers (apiClient )
88+ permissions .Members = membersErr == nil
89+ if membersErr != nil {
90+ failures = append (failures , membersErr .Error ())
6891 }
69- defer res .Body .Close ()
7092
71- if res .StatusCode == http .StatusUnauthorized {
93+ if spendErr := probeSpend (apiClient ); spendErr != nil {
94+ permissions .Spend = false
95+ failures = append (failures , spendErr .Error ())
96+ } else {
97+ permissions .Spend = true
98+ }
99+
100+ if usageErr := probeUsageEvents (apiClient ); usageErr != nil {
101+ permissions .UsageEvents = false
102+ failures = append (failures , usageErr .Error ())
103+ } else {
104+ permissions .UsageEvents = true
105+ }
106+
107+ if len (failures ) > 0 {
72108 return & TestConnectionResult {
73- Success : false ,
74- Message : "Invalid Team API key. Create a Team Admin key in Dashboard → API Keys (admin:* scope), not a User API key from Settings → Integrations." ,
109+ Success : false ,
110+ Message : buildPermissionFailureMessage (failures ),
111+ MemberCount : memberCount ,
112+ Permissions : permissions ,
75113 }, nil
76114 }
77115
116+ hasEnterpriseAnalytics := probeEnterpriseAnalytics (apiClient )
117+
118+ return & TestConnectionResult {
119+ Success : true ,
120+ Message : "Team Admin API key validated. Members, spend, and usage events are accessible." ,
121+ MemberCount : memberCount ,
122+ Permissions : permissions ,
123+ HasEnterpriseAnalytics : hasEnterpriseAnalytics ,
124+ }, nil
125+ }
126+
127+ func probeMembers (apiClient * helper.ApiClient ) (int , errors.Error ) {
128+ res , err := apiClient .Get (membersEndpoint , nil , nil )
129+ if err != nil {
130+ return 0 , errors .Default .Wrap (err , "failed to reach Cursor Admin API" )
131+ }
132+ defer res .Body .Close ()
133+
78134 if res .StatusCode != http .StatusOK {
79- body , _ := io .ReadAll (res .Body )
80- return & TestConnectionResult {
81- Success : false ,
82- Message : fmt .Sprintf ("unexpected status %d: %s" , res .StatusCode , string (body )),
83- }, nil
135+ return 0 , buildAdminApiError (membersEndpoint , "list team members" , res )
84136 }
85137
86138 body , readErr := io .ReadAll (res .Body )
87139 if readErr != nil {
88- return nil , errors .Default .Wrap (readErr , "failed to read response body " )
140+ return 0 , errors .Default .Wrap (readErr , "failed to read members response " )
89141 }
90142
91143 var response struct {
92144 TeamMembers []json.RawMessage `json:"teamMembers"`
93145 }
94146 if jsonErr := json .Unmarshal (body , & response ); jsonErr != nil {
95- return & TestConnectionResult {
96- Success : false ,
97- Message : fmt .Sprintf ("failed to parse response: %v" , jsonErr ),
98- }, nil
147+ return 0 , errors .Default .Wrap (errors .Convert (jsonErr ), "failed to parse members response" )
99148 }
100149
101- hasEnterpriseAnalytics := probeEnterpriseAnalytics (apiClient )
150+ return len (response .TeamMembers ), nil
151+ }
102152
103- return & TestConnectionResult {
104- Success : true ,
105- Message : "Connection validated successfully" ,
106- MemberCount : len (response .TeamMembers ),
107- HasEnterpriseAnalytics : hasEnterpriseAnalytics ,
108- }, nil
153+ func probeSpend (apiClient * helper.ApiClient ) errors.Error {
154+ res , err := apiClient .Post (spendEndpoint , nil , map [string ]interface {}{
155+ "page" : 1 ,
156+ "pageSize" : 1 ,
157+ }, nil )
158+ if err != nil {
159+ return errors .Default .Wrap (err , "failed to reach Cursor spend API" )
160+ }
161+ defer res .Body .Close ()
162+
163+ if res .StatusCode != http .StatusOK {
164+ return buildAdminApiError (spendEndpoint , "read team billing spend" , res )
165+ }
166+ return nil
167+ }
168+
169+ func probeUsageEvents (apiClient * helper.ApiClient ) errors.Error {
170+ endMs := time .Now ().UTC ().UnixMilli ()
171+ startMs := endMs - int64 (7 * 24 * time .Hour / time .Millisecond )
172+
173+ res , err := apiClient .Post (usageEventsEndpoint , nil , map [string ]interface {}{
174+ "startDate" : startMs ,
175+ "endDate" : endMs ,
176+ "page" : 1 ,
177+ "pageSize" : 1 ,
178+ }, nil )
179+ if err != nil {
180+ return errors .Default .Wrap (err , "failed to reach Cursor usage events API" )
181+ }
182+ defer res .Body .Close ()
183+
184+ if res .StatusCode != http .StatusOK {
185+ return buildAdminApiError (usageEventsEndpoint , "read filtered usage events" , res )
186+ }
187+ return nil
188+ }
189+
190+ func buildAdminApiError (endpoint , action string , res * http.Response ) errors.Error {
191+ body , _ := io .ReadAll (res .Body )
192+ detail := strings .TrimSpace (string (body ))
193+ if detail != "" && len (detail ) > 200 {
194+ detail = detail [:200 ] + "..."
195+ }
196+
197+ switch res .StatusCode {
198+ case http .StatusUnauthorized :
199+ return errors .BadInput .New (fmt .Sprintf (
200+ "Cannot %s (%s): unauthorized. Use a Team Admin API key from Dashboard → API Keys (admin:* scope), not a User API key from Settings → Integrations." ,
201+ action , endpoint ,
202+ ))
203+ case http .StatusForbidden :
204+ msg := fmt .Sprintf ("Cannot %s (%s): forbidden. The key may lack admin permissions for this team." , action , endpoint )
205+ if detail != "" {
206+ msg = fmt .Sprintf ("%s Details: %s" , msg , detail )
207+ }
208+ return errors .BadInput .New (msg )
209+ default :
210+ msg := fmt .Sprintf ("Cannot %s (%s): unexpected status %d" , action , endpoint , res .StatusCode )
211+ if detail != "" {
212+ msg = fmt .Sprintf ("%s. Details: %s" , msg , detail )
213+ }
214+ return errors .BadInput .New (msg )
215+ }
216+ }
217+
218+ func buildPermissionFailureMessage (failures []string ) string {
219+ if len (failures ) == 1 {
220+ return failures [0 ]
221+ }
222+ return "Team Admin API key is missing required permissions:\n - " + strings .Join (failures , "\n - " )
109223}
110224
111225func detectUserApiKey (apiClient * helper.ApiClient ) errors.Error {
0 commit comments