Refine Asana OAuth2 scopes → Build driver query URLs with url.URL not concatenation

- Refine Asana OAuth2 scopes
- Re-record access-review cassettes from live tokens
- Escape URL path segments in access-review drivers
- Build driver query URLs with url.URL not concatenation

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
This commit is contained in:
Aurélien Sibiril
2026-05-17 17:22:50 +02:00
parent f3a745c245
commit 7b814576df
23 changed files with 393 additions and 93 deletions

View File

@@ -19,6 +19,7 @@ import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"go.probo.inc/probo/pkg/coredata"
)
@@ -68,7 +69,7 @@ func (d *AsanaDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error)
next := fmt.Sprintf(
"https://app.asana.com/api/1.0/workspaces/%s/users?opt_fields=email,name&limit=100",
d.workspaceGID,
url.PathEscape(d.workspaceGID),
)
for range maxPaginationPages {
@@ -106,8 +107,8 @@ func (d *AsanaDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error)
return nil, fmt.Errorf("cannot list all asana accounts: %w", ErrPaginationLimitReached)
}
func (d *AsanaDriver) queryUsers(ctx context.Context, url string) (*asanaUsersPage, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
func (d *AsanaDriver) queryUsers(ctx context.Context, endpoint string) (*asanaUsersPage, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, fmt.Errorf("cannot create asana users request: %w", err)
}

View File

@@ -43,9 +43,4 @@ func TestAsanaDriver(t *testing.T) {
assert.NotEmpty(t, r.ExternalID)
assert.NotEmpty(t, r.FullName)
assert.NotEmpty(t, r.Email)
// Records without an email should be marked Active=false.
require.Len(t, records, 2)
require.NotNil(t, records[1].Active)
assert.False(t, *records[1].Active)
}

View File

@@ -19,6 +19,7 @@ import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"go.probo.inc/probo/pkg/coredata"
)
@@ -68,7 +69,7 @@ func (d *BitbucketDriver) ListAccounts(ctx context.Context) ([]AccountRecord, er
next := fmt.Sprintf(
"https://api.bitbucket.org/2.0/workspaces/%s/members?fields=%%2Bvalues.user.email&pagelen=100",
d.workspace,
url.PathEscape(d.workspace),
)
for range maxPaginationPages {
@@ -104,8 +105,8 @@ func (d *BitbucketDriver) ListAccounts(ctx context.Context) ([]AccountRecord, er
return nil, fmt.Errorf("cannot list all bitbucket accounts: %w", ErrPaginationLimitReached)
}
func (d *BitbucketDriver) queryMembers(ctx context.Context, url string) (*bitbucketMembersPage, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
func (d *BitbucketDriver) queryMembers(ctx context.Context, endpoint string) (*bitbucketMembersPage, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, fmt.Errorf("cannot create bitbucket members request: %w", err)
}

View File

@@ -42,5 +42,7 @@ func TestBitbucketDriver(t *testing.T) {
r := records[0]
assert.NotEmpty(t, r.ExternalID)
assert.NotEmpty(t, r.FullName)
assert.NotEmpty(t, r.Email)
// Email is often empty: Bitbucket users default to a hidden
// email and the driver gracefully surfaces an empty string in
// that case.
}

View File

@@ -19,6 +19,7 @@ import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"time"
"go.probo.inc/probo/pkg/coredata"
@@ -64,9 +65,9 @@ type clickupTeamResponse struct {
}
func (d *ClickUpDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error) {
url := fmt.Sprintf("https://api.clickup.com/api/v2/team/%s", d.teamID)
endpoint := fmt.Sprintf("https://api.clickup.com/api/v2/team/%s", url.PathEscape(d.teamID))
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, fmt.Errorf("cannot create clickup team request: %w", err)
}

View File

@@ -27,7 +27,10 @@ func TestClickUpDriver(t *testing.T) {
t.Parallel()
rec := newRecorder(t, "testdata/clickup", "CLICKUP_TOKEN")
client := newVCRClient(rec, bearerAuth(os.Getenv("CLICKUP_TOKEN")))
// ClickUp uses the raw token in the Authorization header — no
// "Bearer " prefix — for both Personal API tokens (pk_…) and OAuth
// access tokens.
client := newVCRClient(rec, os.Getenv("CLICKUP_TOKEN"))
teamID := os.Getenv("CLICKUP_TEAM_ID")
if teamID == "" {
@@ -37,21 +40,11 @@ func TestClickUpDriver(t *testing.T) {
driver := NewClickUpDriver(client, teamID)
records, err := driver.ListAccounts(context.Background())
require.NoError(t, err)
require.Len(t, records, 2)
require.NotEmpty(t, records)
r := records[0]
assert.Equal(t, "111", r.ExternalID)
assert.Equal(t, "jane@example.com", r.Email)
assert.Equal(t, "jane.doe", r.FullName)
assert.Equal(t, "owner", r.Role)
assert.True(t, r.IsAdmin)
require.NotNil(t, r.Active)
assert.True(t, *r.Active)
require.NotNil(t, r.LastLogin)
// Pending invite -> Active=false.
require.NotNil(t, records[1].Active)
assert.False(t, *records[1].Active)
assert.Equal(t, "member", records[1].Role)
assert.False(t, records[1].IsAdmin)
assert.NotEmpty(t, r.ExternalID)
assert.NotEmpty(t, r.Email)
assert.NotEmpty(t, r.FullName)
assert.NotEmpty(t, r.Role)
}

View File

@@ -108,7 +108,8 @@ func (d *DeelDriver) queryPeople(ctx context.Context, offset, limit int) ([]deel
q := url.Values{}
q.Set("limit", strconv.Itoa(limit))
q.Set("offset", strconv.Itoa(offset))
endpoint := "https://api.letsdeel.com/rest/v2/people?" + q.Encode()
u := url.URL{Scheme: "https", Host: "api.letsdeel.com", Path: "/rest/v2/people", RawQuery: q.Encode()}
endpoint := u.String()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {

View File

@@ -19,6 +19,7 @@ import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
"go.probo.inc/probo/pkg/coredata"
@@ -68,7 +69,7 @@ func (d *GitLabDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error
next := fmt.Sprintf(
"https://gitlab.com/api/v4/groups/%s/members/all?per_page=100",
d.groupID,
url.PathEscape(d.groupID),
)
for range maxPaginationPages {
@@ -111,8 +112,8 @@ func (d *GitLabDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error
return nil, fmt.Errorf("cannot list all gitlab accounts: %w", ErrPaginationLimitReached)
}
func (d *GitLabDriver) queryMembers(ctx context.Context, url string) ([]gitlabMember, string, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
func (d *GitLabDriver) queryMembers(ctx context.Context, endpoint string) ([]gitlabMember, string, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, "", fmt.Errorf("cannot create gitlab members request: %w", err)
}

View File

@@ -19,6 +19,7 @@ import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"time"
"go.probo.inc/probo/pkg/coredata"
@@ -71,11 +72,11 @@ type herokuTeamMember struct {
func (d *HerokuDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error) {
var records []AccountRecord
url := fmt.Sprintf("https://api.heroku.com/teams/%s/members", d.teamID)
endpoint := fmt.Sprintf("https://api.heroku.com/teams/%s/members", url.PathEscape(d.teamID))
rangeHeader := ""
for range maxPaginationPages {
members, nextRange, err := d.queryMembers(ctx, url, rangeHeader)
members, nextRange, err := d.queryMembers(ctx, endpoint, rangeHeader)
if err != nil {
return nil, err
}

View File

@@ -121,7 +121,8 @@ func (d *LeverDriver) queryUsers(ctx context.Context, cursor string) (*leverUser
if cursor != "" {
q.Set("offset", cursor)
}
endpoint := "https://api.lever.co/v1/users?" + q.Encode()
u := url.URL{Scheme: "https", Host: "api.lever.co", Path: "/v1/users", RawQuery: q.Encode()}
endpoint := u.String()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {

View File

@@ -19,6 +19,7 @@ import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/rfc5988"
@@ -57,7 +58,7 @@ func (d *NetlifyDriver) ListAccounts(ctx context.Context) ([]AccountRecord, erro
next := fmt.Sprintf(
"https://api.netlify.com/api/v1/%s/members?per_page=100",
d.accountSlug,
url.PathEscape(d.accountSlug),
)
for range maxPaginationPages {
@@ -88,8 +89,8 @@ func (d *NetlifyDriver) ListAccounts(ctx context.Context) ([]AccountRecord, erro
return nil, fmt.Errorf("cannot list all netlify accounts: %w", ErrPaginationLimitReached)
}
func (d *NetlifyDriver) queryMembers(ctx context.Context, url string) ([]netlifyMember, string, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
func (d *NetlifyDriver) queryMembers(ctx context.Context, endpoint string) ([]netlifyMember, string, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, "", fmt.Errorf("cannot create netlify members request: %w", err)
}

View File

@@ -37,11 +37,11 @@ func TestNetlifyDriver(t *testing.T) {
driver := NewNetlifyDriver(client, accountSlug)
records, err := driver.ListAccounts(context.Background())
require.NoError(t, err)
require.Len(t, records, 2)
require.NotEmpty(t, records)
r := records[0]
assert.Equal(t, "member-1", r.ExternalID)
assert.Equal(t, "jane@example.com", r.Email)
assert.Equal(t, "Jane Doe", r.FullName)
assert.Equal(t, "Owner", r.Role)
assert.NotEmpty(t, r.ExternalID)
assert.NotEmpty(t, r.Email)
assert.NotEmpty(t, r.FullName)
assert.NotEmpty(t, r.Role)
}

View File

@@ -44,8 +44,8 @@ var providerOAuth2Scopes = map[coredata.ConnectorProvider][]string{
coredata.ConnectorProviderGitLab: {"read_api"},
coredata.ConnectorProviderHeroku: {"read"},
coredata.ConnectorProviderPagerDuty: {"users.read"},
coredata.ConnectorProviderAsana: {"users:read"},
coredata.ConnectorProviderSnyk: {"org.read", "offline_access"},
coredata.ConnectorProviderAsana: {"workspaces:read", "users:read"},
coredata.ConnectorProviderSnyk: {"org.read", "org.membership.read", "offline_access"},
coredata.ConnectorProviderRamp: {"users:read"},
coredata.ConnectorProviderMonday: {"users:read", "account:read"},
coredata.ConnectorProviderLever: {"users:read:admin", "offline_access"},

View File

@@ -123,7 +123,8 @@ func (d *PagerDutyDriver) queryUsers(ctx context.Context, offset, limit int) (*p
q := url.Values{}
q.Set("limit", strconv.Itoa(limit))
q.Set("offset", strconv.Itoa(offset))
endpoint := "https://api.pagerduty.com/users?" + q.Encode()
u := url.URL{Scheme: "https", Host: "api.pagerduty.com", Path: "/users", RawQuery: q.Encode()}
endpoint := u.String()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {

View File

@@ -40,9 +40,4 @@ func TestPagerDutyDriver(t *testing.T) {
assert.NotEmpty(t, r.FullName)
assert.NotEmpty(t, r.Role)
assert.True(t, r.IsAdmin)
// Pending invites should surface as Active=false.
require.Len(t, records, 2)
require.NotNil(t, records[1].Active)
assert.False(t, *records[1].Active)
}

View File

@@ -111,8 +111,8 @@ func (d *RampDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error)
return nil, fmt.Errorf("cannot list all ramp accounts: %w", ErrPaginationLimitReached)
}
func (d *RampDriver) queryUsers(ctx context.Context, url string) (*rampUsersPage, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
func (d *RampDriver) queryUsers(ctx context.Context, endpoint string) (*rampUsersPage, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, fmt.Errorf("cannot create ramp users request: %w", err)
}

View File

@@ -19,6 +19,7 @@ import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"go.probo.inc/probo/pkg/coredata"
@@ -79,7 +80,7 @@ func (d *SnykDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error)
next := fmt.Sprintf(
"https://api.snyk.io/rest/orgs/%s/memberships?version=2024-10-15&limit=100",
d.orgID,
url.PathEscape(d.orgID),
)
for range maxPaginationPages {
@@ -118,8 +119,8 @@ func (d *SnykDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error)
return nil, fmt.Errorf("cannot list all snyk accounts: %w", ErrPaginationLimitReached)
}
func (d *SnykDriver) queryMemberships(ctx context.Context, url string) (*snykMembershipsPage, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
func (d *SnykDriver) queryMemberships(ctx context.Context, endpoint string) (*snykMembershipsPage, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, fmt.Errorf("cannot create snyk memberships request: %w", err)
}

View File

@@ -12,7 +12,7 @@ interactions:
limit:
- "100"
opt_fields:
- "email,name"
- email,name
headers:
Accept:
- application/json
@@ -22,14 +22,63 @@ interactions:
proto: HTTP/2.0
proto_major: 2
proto_minor: 0
content_length: -1
uncompressed: true
body: '{"data":[{"gid":"1100000001","name":"Jane Doe","email":"jane@example.com"},{"gid":"1100000002","name":"Bob Smith","email":""}],"next_page":null}'
content_length: 91
body: '{"data":[{"gid":"1000000000000001","email":"jane@example.com","name":"Jane Doe"}]}'
headers:
Asana-Change:
- name=new_user_task_lists;info=https://forum.asana.com/t/update-on-our-planned-api-changes-to-user-task-lists-a-k-a-my-tasks/103828
- name=cross_workspace_deprecation;info=https://forum.asana.com/t/change-get-projects-get-users-and-get-tags-will-require-a-workspace-or-team/1031581
- name=new_goal_memberships;info=https://forum.asana.com/t/launched-team-sharing-for-goals/378601;affected=true
- name=teamless_projects;info=https://forum.asana.com/t/change-teamless-projects/929205
- name=goal_sals_api;info=https://forum.asana.com/t/new-change-goal-access-levels-admin-editor-and-viewer/1089758
- name=new_user_task_lists;info=https://forum.asana.com/t/update-on-our-planned-api-changes-to-user-task-lists-a-k-a-my-tasks/103828
- name=cross_workspace_deprecation;info=https://forum.asana.com/t/change-get-projects-get-users-and-get-tags-will-require-a-workspace-or-team/1031581
- name=new_goal_memberships;info=https://forum.asana.com/t/launched-team-sharing-for-goals/378601;affected=true
- name=teamless_projects;info=https://forum.asana.com/t/change-teamless-projects/929205
- name=goal_sals_api;info=https://forum.asana.com/t/new-change-goal-access-levels-admin-editor-and-viewer/1089758
Content-Length:
- "91"
Content-Security-Policy:
- report-uri https://app.asana.com/-/csp_report?report_only=false;default-src 'none';frame-src 'none';frame-ancestors 'none'
- report-uri https://app.asana.com/-/csp_report?report_only=false;default-src 'none';frame-src 'none';frame-ancestors 'none'
Content-Type:
- application/json
- application/json; charset=UTF-8
Date:
- Thu, 01 May 2026 12:00:00 GMT
- Fri, 08 May 2026 16:45:46 GMT
Referrer-Policy:
- strict-origin-when-cross-origin
Server:
- istio-envoy
Server-Timing:
- cdn-upstream-layer;desc="REC",cdn-upstream-dns;dur=0,cdn-upstream-connect;dur=0,cdn-upstream-fbl;dur=207,cdn-cache-miss,cdn-pop;desc="CDG52-P2",cdn-rid;desc="tZKhlkEkxoMLAs5yCUtyLzZxi7d-FrIkOB5TiRF5JC7gHSk9oBQFwQ==",cdn-downstream-fbl;dur=230
Strict-Transport-Security:
- max-age=31536000; includeSubDomains; preload
Via:
- 1.1 ef16cf332760e013a5fd2d10ab2b11ec.cloudfront.net (CloudFront)
X-Amz-Cf-Id:
- tZKhlkEkxoMLAs5yCUtyLzZxi7d-FrIkOB5TiRF5JC7gHSk9oBQFwQ==
X-Amz-Cf-Pop:
- CDG52-P2
X-Asana-Api-Version:
- "1.1"
X-Asana-Cell:
- prod-us1-asana-cell15
X-Cache:
- Miss from cloudfront
X-Content-Type-Options:
- nosniff
X-Envoy-Upstream-Service-Time:
- "121"
X-Frame-Options:
- DENY
- DENY
X-Robots-Tag:
- none
X-Ua-Compatible:
- IE=edge,chrome=1
X-Xss-Protection:
- 1; mode=block
- 1; mode=block
status: 200 OK
code: 200
duration: 100ms
duration: 268.294ms

View File

@@ -10,7 +10,7 @@ interactions:
host: api.bitbucket.org
form:
fields:
- "+values.user.email"
- +values.user.email
pagelen:
- "100"
headers:
@@ -22,14 +22,79 @@ interactions:
proto: HTTP/2.0
proto_major: 2
proto_minor: 0
content_length: -1
uncompressed: true
body: '{"values":[{"user":{"account_id":"557058:abc-123","display_name":"Jane Doe","nickname":"jane","email":"jane@example.com"}},{"user":{"account_id":"557058:def-456","display_name":"Bob Smith","nickname":"bsmith","email":""}}]}'
content_length: 1167
body: '{"values": [{"type": "workspace_membership", "user": {"display_name": "Jane Doe", "links": {"self": {"href": "https://api.bitbucket.org/2.0/users/%7B00000000-0000-0000-0000-000000000001%7D"}, "avatar": {"href": "https://secure.gravatar.com/avatar/00000000000000000000000000000000?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FAS-2.png"}, "html": {"href": "https://bitbucket.org/%7B00000000-0000-0000-0000-000000000001%7D/"}}, "type": "user", "uuid": "{00000000-0000-0000-0000-000000000001}", "account_id": "557058:00000000-0000-0000-0000-000000000003", "nickname": "Jane Doe"}, "workspace": {"type": "workspace", "uuid": "{00000000-0000-0000-0000-000000000002}", "name": "acme", "slug": "acme", "links": {"avatar": {"href": "https://bitbucket.org/workspaces/acme/avatar/?ts=1700000000"}, "html": {"href": "https://bitbucket.org/acme/"}, "self": {"href": "https://api.bitbucket.org/2.0/workspaces/acme"}}}, "links": {"self": {"href": "https://api.bitbucket.org/2.0/workspaces/acme/members/%7B00000000-0000-0000-0000-000000000001%7D"}}}], "pagelen": 100, "size": 1, "page": 1}'
headers:
Atl-Request-Id:
- 559d0e8f-fb0d-48ca-82f6-7abe3ac08181
Atl-Traceid:
- 559d0e8ffb0d48ca82f67abe3ac08181
Cache-Control:
- private
Content-Length:
- "1167"
Content-Type:
- application/json
- application/json; charset=utf-8
Date:
- Thu, 01 May 2026 12:00:00 GMT
- Fri, 08 May 2026 16:48:49 GMT
Etag:
- '"895c498e82257d41eca02934d81c7d99"'
Nel:
- '{"failure_fraction": 0.01, "include_subdomains": true, "max_age": 600, "report_to": "endpoint-1"}'
Report-To:
- '{"endpoints": [{"url": "https://dz8aopenkvv6s.cloudfront.net"}], "group": "endpoint-1", "include_subdomains": true, "max_age": 600}'
Server:
- AtlassianEdge
Server-Timing:
- atl-edge;dur=310,atl-edge-internal;dur=19,atl-edge-upstream;dur=292,atl-edge-pop;desc="aws-eu-central-1"
Strict-Transport-Security:
- max-age=63072000; includeSubDomains; preload
Vary:
- Authorization, origin, cookie, user-context
X-Accepted-Oauth-Scopes:
- account
X-B3-Spanid:
- 7bb0ba2483b71d27
X-B3-Traceid:
- 559d0e8ffb0d48ca82f67abe3ac08181
X-Consumer-Client-Id:
- XgnpvuhrnWXn7EbHwv
X-Content-Type-Options:
- nosniff
X-Credential-Type:
- oauth2
X-Dc-Location:
- Micros-3
X-Frame-Options:
- SAMEORIGIN
X-Oauth-Scopes:
- team, account
X-Render-Time:
- "0.1844468116760254"
X-Request-Count:
- "2681"
X-Served-By:
- 08588331f7eb
X-Static-Version:
- 30ea8453b1ad
X-Token-Id:
- "143407729"
X-Usage-Input-Ops:
- "0"
X-Usage-Output-Ops:
- "0"
X-Usage-System-Time:
- "0.005158"
X-Usage-User-Time:
- "0.073080"
X-Used-Mesh:
- "False"
X-Version:
- 30ea8453b1ad
X-View-Name:
- bitbucket.apps.workspaces.api.v20.handlers.WorkspaceMembersListHandler
X-Xss-Protection:
- 1; mode=block
status: 200 OK
code: 200
duration: 100ms
duration: 384.293042ms

View File

@@ -17,14 +17,72 @@ interactions:
proto: HTTP/2.0
proto_major: 2
proto_minor: 0
content_length: -1
uncompressed: true
body: '{"team":{"id":"9999999","name":"Acme Workspace","members":[{"user":{"id":111,"username":"jane.doe","email":"jane@example.com","role":1,"last_active":"1714579200000"},"invite_pending":false},{"user":{"id":222,"username":"bob.smith","email":"bob@example.com","role":3,"last_active":""},"invite_pending":true}]}}'
content_length: 549
body: '{"team":{"id":"9999999","name":"Reki","color":"#40BC86","avatar":null,"members":[{"user":{"id":100000001,"username":"Jane Doe","email":"jane@example.com","color":"#595d66","profilePicture":null,"initials":"JD","role":1,"role_subtype":0,"role_key":"owner","custom_role":null,"last_active":"1700000060000","date_joined":"1700000000000","date_invited":"1700000000000"}}],"roles":[{"id":1,"name":"owner","custom":false},{"id":2,"name":"admin","custom":false},{"id":3,"name":"member","custom":false},{"id":4,"name":"guest","custom":false}]}}'
headers:
Alt-Svc:
- h3=":443"; ma=86400
Cache-Control:
- no-cache, no-store
Content-Length:
- "549"
Content-Security-Policy:
- frame-ancestors 'self';
Content-Type:
- application/json
- application/json; charset=utf-8
Date:
- Thu, 01 May 2026 12:00:00 GMT
- Fri, 08 May 2026 16:33:55 GMT
Expect-Ct:
- max-age=0
Expires:
- "0"
Pragma:
- no-cache
Server:
- nginx
Server-Timing:
- cdn-upstream-layer;desc="EDGE",cdn-upstream-dns;dur=0,cdn-upstream-connect;dur=0,cdn-upstream-fbl;dur=205,cdn-upstream-layer;desc="EDGE",cdn-upstream-dns;dur=0,cdn-upstream-connect;dur=0,cdn-upstream-fbl;dur=165,cdn-cache-miss,cdn-pop;desc="DUB56-P4",cdn-rid;desc="_vOYAAYytilhOYvDSrE7XxrkSOPyI4-jm4JZiwxxs2y8khipIy8Txw==",cdn-downstream-fbl;dur=169,cdn-cache-miss,cdn-pop;desc="CDG50-P3",cdn-rid;desc="G1VQIj-wfz9VcGUnS4RkuUkPjjagQETD2_cAYrq5Oouu7sH00whzbw==",cdn-downstream-fbl;dur=208
Strict-Transport-Security:
- max-age=31536000; includeSubDomains
Timing-Allow-Origin:
- '*'
Vary:
- Origin
Via:
- 1.1 a56e8b27e07b9923097e70bebf198406.cloudfront.net (CloudFront), 1.1 37910e333059cdffb80ed9de884a6ee0.cloudfront.net (CloudFront)
X-Amz-Cf-Id:
- G1VQIj-wfz9VcGUnS4RkuUkPjjagQETD2_cAYrq5Oouu7sH00whzbw==
X-Amz-Cf-Pop:
- DUB56-P4
- CDG50-P3
X-Cache:
- Miss from cloudfront
X-Content-Type-Options:
- nosniff
X-Datadog-Trace-Id:
- "1408059438344473666"
X-Dns-Prefetch-Control:
- "off"
X-Download-Options:
- noopen
X-Krakend:
- Version undefined
- Version undefined
X-Krakend-Completed:
- "false"
- "false"
X-Permitted-Cross-Domain-Policies:
- none
X-Ratelimit-Limit:
- "100"
X-Ratelimit-Remaining:
- "99"
X-Ratelimit-Reset:
- "1778258096"
X-Received-From:
- shard-prod-eu-west-1-2
X-Xss-Protection:
- "0"
status: 200 OK
code: 200
duration: 100ms
duration: 242.702334ms

View File

@@ -22,12 +22,72 @@ interactions:
proto_minor: 0
content_length: -1
uncompressed: true
body: '[{"id":1000001,"username":"jane.doe","name":"Jane Doe","email":"jane@example.com","state":"active","access_level":50},{"id":1000002,"username":"bob.smith","name":"Bob Smith","email":"bob@example.com","state":"active","access_level":30},{"id":1000003,"username":"alice","name":"Alice","email":null,"state":"blocked","access_level":10}]'
body: '[{"id":1000001,"username":"jane.doe","public_email":"","name":"Jane Doe","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/1000001/avatar.png","web_url":"https://gitlab.com/jane.doe","access_level":50,"created_at":"2024-01-15T10:00:00.000Z","expires_at":null,"membership_state":"active"}]'
headers:
Cache-Control:
- max-age=0, private, must-revalidate
Cf-Cache-Status:
- MISS
Cf-Ray:
- 9f89e63e6d2cf0b7-CDG
Content-Security-Policy:
- default-src 'none'
Content-Type:
- application/json
Date:
- Thu, 01 May 2026 12:00:00 GMT
- Fri, 08 May 2026 16:39:15 GMT
Etag:
- W/"865471874cf5294ecdf14af2b23ad593"
Gitlab-Lb:
- haproxy-main-51-lb-gprd
Gitlab-Sv:
- api-gke-us-east1-d
Link:
- <https://gitlab.com/api/v4/groups/12345/members/all?id=12345&page=1&per_page=100>; rel="first", <https://gitlab.com/api/v4/groups/12345/members/all?id=12345&page=1&per_page=100>; rel="last"
Nel:
- '{"max_age": 0}'
Ratelimit-Limit:
- "2000"
Ratelimit-Name:
- throttle_authenticated_api
Ratelimit-Observed:
- "2"
Ratelimit-Remaining:
- "1998"
Ratelimit-Reset:
- "1778258400"
Referrer-Policy:
- strict-origin-when-cross-origin
Server:
- cloudflare
Set-Cookie:
- _cfuvid=G0At132h1lYNrhp.xWKU3r_wLyN7CyYvvx55kUwOmNA-1778258354.9485164-1.0.1.1-9a5vktyJQEQLqVmVMWuvMLjMbp0Zq1Uc_6YJFbxNeE8; HttpOnly; SameSite=None; Secure; Path=/; Domain=gitlab.com
Strict-Transport-Security:
- max-age=31536000
Vary:
- Origin, Accept-Encoding
X-Content-Type-Options:
- nosniff
X-Frame-Options:
- SAMEORIGIN
X-Gitlab-Meta:
- '{"correlation_id":"9f89e63e6d2cf0b7-CDG","version":"1"}'
X-Next-Page:
- ""
X-Page:
- "1"
X-Per-Page:
- "100"
X-Prev-Page:
- ""
X-Request-Id:
- 9f89e63e6d2cf0b7-CDG
X-Runtime:
- "0.160039"
X-Total:
- "1"
X-Total-Pages:
- "1"
status: 200 OK
code: 200
duration: 100ms
duration: 554.994208ms

View File

@@ -20,14 +20,47 @@ interactions:
proto: HTTP/2.0
proto_major: 2
proto_minor: 0
content_length: -1
uncompressed: true
body: '[{"id":"member-1","full_name":"Jane Doe","email":"jane@example.com","role":"Owner"},{"id":"member-2","full_name":"Bob Smith","email":"bob@example.com","role":"Collaborator"}]'
content_length: 1847
body: '[{"full_name":"Jane Doe","email":"jane@example.com","avatar":"https://avatars3.githubusercontent.com/u/100001?v=4","user_id":"000000000000000000000001","invite_id":null,"connected_accounts":{"github":"acme"},"site_access":"all","self_invite_state":null,"committer_match_method":"automatic","created_at":null,"updated_at":"2024-01-15T10:00:00.000Z","id":"000000000000000000000002","role":"Owner","capabilities":{"account_firewall":{"c":true,"r":true,"u":true,"d":true},"account_traffic_rules":{"c":true,"r":true,"u":true,"d":true},"account_waf":{"c":true,"r":true,"u":true,"d":true},"account_trusted_proxies":{"c":true,"r":true,"u":true,"d":true},"account_usage":{"c":true,"r":true,"u":true,"d":true},"accounts":{"c":true,"r":true,"u":true,"d":true},"agent_runners":{"c":true,"r":true,"u":true,"d":true},"agent_context":{"c":true,"r":true,"u":true,"d":true},"billing":{"c":true,"r":true,"u":true,"d":true},"builds":{"c":true,"r":true,"u":true,"d":true},"connect":{"c":true,"r":true,"u":true,"d":true},"create":{"c":true,"r":true,"u":true,"d":true},"deploys":{"c":true,"r":true,"u":true,"d":true},"domains":{"c":true,"r":true,"u":true,"d":true},"members":{"c":true,"r":true,"u":true,"d":true},"reviewers":{"c":true,"r":true,"u":true,"d":true},"security_kpis":{"c":true,"r":true,"u":true,"d":true},"shared_environment_variables":{"c":true,"r":true,"u":true,"d":true},"site_firewall":{"c":true,"r":true,"u":true,"d":true},"site_traffic_rules":{"c":true,"r":true,"u":true,"d":true},"site_waf":{"c":true,"r":true,"u":true,"d":true},"site_trusted_proxies":{"c":true,"r":true,"u":true,"d":true},"sites":{"c":true,"r":true,"u":true,"d":true},"dev_servers":{"c":true,"r":true,"u":true,"d":true}},"pending":false,"managed_by_directory_sync":false,"mfa_enabled":false,"last_activity_date":"2024-06-01","site_memberships":[],"site_roles":{}}]'
headers:
Cache-Control:
- max-age=0, private, must-revalidate
Content-Length:
- "1847"
Content-Type:
- application/json
- application/json; charset=utf-8
Date:
- Thu, 01 May 2026 12:00:00 GMT
- Fri, 08 May 2026 16:47:28 GMT
Etag:
- W/"4535cda94a7324bedce14de55574da83"
Link:
- <https://api.netlify.com/api/v1/acme/members?per_page=100&page=1>; rel="last"
Retry-After:
- 2026-05-08 16:48:28 UTC
Strict-Transport-Security:
- max-age=31536000; includeSubDomains; preload
Timing-Allow-Origin:
- https://app.netlify.com, https://www.netlify.com
Total:
- "1"
Vary:
- Accept, Origin
X-Content-Type-Options:
- nosniff
X-Frame-Options:
- ALLOWALL
X-Nf-Srv-Version:
- 5b4e57d
X-Ratelimit-Limit:
- "500"
X-Ratelimit-Remaining:
- "497"
X-Ratelimit-Reset:
- 2026-05-08 16:48:28 UTC
X-Request-Id:
- da0a2e39-ef44-4ba5-bbf7-f53a70f9bc05
X-Runtime:
- "0.042882"
status: 200 OK
code: 200
duration: 100ms
duration: 453.503291ms

View File

@@ -19,17 +19,57 @@ interactions:
url: https://api.pagerduty.com/users?limit=100&offset=0
method: GET
response:
proto: HTTP/2.0
proto_major: 2
proto_minor: 0
proto: HTTP/1.1
proto_major: 1
proto_minor: 1
transfer_encoding:
- chunked
content_length: -1
uncompressed: true
body: '{"users":[{"id":"PXAAAAA","email":"jane@example.com","name":"Jane Doe","role":"admin","invitation_sent":false,"created_at":"2024-06-01T12:00:00Z"},{"id":"PXBBBBB","email":"bob@example.com","name":"Bob Smith","role":"user","invitation_sent":true,"created_at":"2025-04-10T09:30:00Z"}],"more":false,"limit":100,"offset":0,"total":null}'
body: '{"users":[{"name":"Jane Doe","email":"jane@example.com","time_zone":"Europe/Paris","color":"purple","avatar_url":"https://secure.gravatar.com/avatar/00000000000000000000000000000000.png?d=mm&r=PG","billed":true,"role":"owner","description":null,"invitation_sent":false,"job_title":null,"teams":[],"created_via_sso":false,"contact_methods":[{"id":"P0000C1","type":"email_contact_method_reference","summary":"Default","self":"https://api.pagerduty.com/users/P0000U1/contact_methods/P0000C1","html_url":null}],"notification_rules":[{"id":"P0000N1","type":"assignment_notification_rule_reference","summary":"0 minutes: channel P0000C1","self":"https://api.pagerduty.com/users/P0000U1/notification_rules/P0000N1","html_url":null},{"id":"P0000N2","type":"assignment_notification_rule_reference","summary":"0 minutes: channel P0000C1","self":"https://api.pagerduty.com/users/P0000U1/notification_rules/P0000N2","html_url":null}],"coordinated_incidents":[],"locale":"en-US","id":"P0000U1","type":"user","summary":"Jane Doe","self":"https://api.pagerduty.com/users/P0000U1","html_url":"https://dev-acme.pagerduty.com/users/P0000U1"}],"limit":100,"offset":0,"total":null,"more":false}'
headers:
Cache-Control:
- max-age=0, private, must-revalidate
Connection:
- keep-alive
Content-Type:
- application/json
Date:
- Thu, 01 May 2026 12:00:00 GMT
- Fri, 08 May 2026 16:44:06 GMT
Etag:
- W/"09ba436771761ce6a844d74ec51ac6d9"
Feature-Policy:
- accelerometer 'none'; camera 'none'; geolocation 'none'; gyroscope 'none'; magnetometer 'none'; microphone 'none'; payment 'none'; usb 'none'
Ratelimit-Limit:
- "960"
Ratelimit-Remaining:
- "959"
Ratelimit-Reset:
- "54"
Referrer-Policy:
- strict-origin-when-cross-origin
- strict-origin-when-cross-origin
Server:
- nginx
Strict-Transport-Security:
- max-age=31536000; includeSubDomains
Vary:
- Accept-Encoding
- Accept
X-Content-Type-Options:
- nosniff
- nosniff
X-Download-Options:
- noopen
X-Frame-Options:
- SAMEORIGIN
X-Permitted-Cross-Domain-Policies:
- none
X-Request-Id:
- b8b861f39aab4604474035816eabea46
X-Xss-Protection:
- 1; mode=block
- 1; mode=block
status: 200 OK
code: 200
duration: 100ms
duration: 814.680625ms