@@ -98,6 +98,7 @@ func (d *AsanaDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error)
|
||||
if page.NextPage == nil || page.NextPage.URI == "" {
|
||||
return records, nil
|
||||
}
|
||||
|
||||
next = page.NextPage.URI
|
||||
}
|
||||
|
||||
@@ -109,12 +110,14 @@ func (d *AsanaDriver) queryUsers(ctx context.Context, endpoint string) (*asanaUs
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot create asana users request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
httpResp, err := d.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot execute asana users request: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = httpResp.Body.Close() }()
|
||||
|
||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||
|
||||
@@ -110,12 +110,14 @@ func (d *BitbucketDriver) queryMembers(ctx context.Context, endpoint string) (*b
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot create bitbucket members request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
httpResp, err := d.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot execute bitbucket members request: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = httpResp.Body.Close() }()
|
||||
|
||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||
|
||||
@@ -84,6 +84,7 @@ func (d *BrexDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error)
|
||||
if resp.NextCursor == "" {
|
||||
return records, nil
|
||||
}
|
||||
|
||||
nextCursor := resp.NextCursor
|
||||
cursor = &nextCursor
|
||||
}
|
||||
@@ -110,6 +111,7 @@ func (d *BrexDriver) queryUsers(ctx context.Context, cursor *string) (*brexUsers
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot execute brex users request: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = httpResp.Body.Close()
|
||||
}()
|
||||
|
||||
@@ -82,6 +82,7 @@ func TestCassettesUseSyntheticEmails(t *testing.T) {
|
||||
if seen[email] {
|
||||
continue
|
||||
}
|
||||
|
||||
seen[email] = true
|
||||
|
||||
domain := email[strings.IndexByte(email, '@')+1:]
|
||||
@@ -90,6 +91,7 @@ func TestCassettesUseSyntheticEmails(t *testing.T) {
|
||||
}
|
||||
|
||||
ok := false
|
||||
|
||||
for _, suffix := range allowedDomainSuffixes {
|
||||
if strings.HasSuffix("."+domain, suffix) || domain == strings.TrimPrefix(suffix, ".") {
|
||||
ok = true
|
||||
|
||||
@@ -77,12 +77,14 @@ func (d *ClickUpDriver) ListAccounts(ctx context.Context) ([]AccountRecord, erro
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot create clickup team request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
httpResp, err := d.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot execute clickup team request: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = httpResp.Body.Close() }()
|
||||
|
||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||
@@ -159,5 +161,6 @@ func parseClickUpTime(raw string) (time.Time, error) {
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf("cannot parse clickup time %q: %w", raw, err)
|
||||
}
|
||||
|
||||
return time.UnixMilli(ms).UTC(), nil
|
||||
}
|
||||
|
||||
@@ -131,6 +131,7 @@ func (d *CloudflareDriver) queryAccounts(ctx context.Context, page int) (*cloudf
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot execute cloudflare accounts request: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = httpResp.Body.Close()
|
||||
}()
|
||||
@@ -168,6 +169,7 @@ func (d *CloudflareDriver) queryAllMembers(ctx context.Context, accountID string
|
||||
}
|
||||
|
||||
isAdmin := false
|
||||
|
||||
for _, r := range m.Roles {
|
||||
if r.Name == "Super Administrator - All Privileges" || r.Name == "Administrator" {
|
||||
isAdmin = true
|
||||
@@ -224,6 +226,7 @@ func (d *CloudflareDriver) queryMembers(ctx context.Context, accountID string, p
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot execute cloudflare members request: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = httpResp.Body.Close()
|
||||
}()
|
||||
|
||||
@@ -51,6 +51,7 @@ func (d *CSVDriver) ListAccounts(_ context.Context) ([]AccountRecord, error) {
|
||||
for i, col := range header {
|
||||
colIndex[strings.TrimSpace(strings.ToLower(col))] = i
|
||||
}
|
||||
|
||||
if _, ok := colIndex["email"]; !ok {
|
||||
return nil, fmt.Errorf("cannot parse CSV: missing required column email")
|
||||
}
|
||||
@@ -62,6 +63,7 @@ func (d *CSVDriver) ListAccounts(_ context.Context) ([]AccountRecord, error) {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot read CSV row: %w", err)
|
||||
}
|
||||
@@ -75,24 +77,31 @@ func (d *CSVDriver) ListAccounts(_ context.Context) ([]AccountRecord, error) {
|
||||
if idx, ok := colIndex["email"]; ok && idx < len(row) {
|
||||
record.Email = strings.TrimSpace(row[idx])
|
||||
}
|
||||
|
||||
if idx, ok := colIndex["full_name"]; ok && idx < len(row) {
|
||||
record.FullName = strings.TrimSpace(row[idx])
|
||||
}
|
||||
|
||||
if idx, ok := colIndex["role"]; ok && idx < len(row) {
|
||||
record.Role = strings.TrimSpace(row[idx])
|
||||
}
|
||||
|
||||
if idx, ok := colIndex["job_title"]; ok && idx < len(row) {
|
||||
record.JobTitle = strings.TrimSpace(row[idx])
|
||||
}
|
||||
|
||||
if idx, ok := colIndex["is_admin"]; ok && idx < len(row) {
|
||||
record.IsAdmin = strings.TrimSpace(strings.ToLower(row[idx])) == "true"
|
||||
}
|
||||
|
||||
if idx, ok := colIndex["active"]; ok && idx < len(row) {
|
||||
record.Active = new(strings.TrimSpace(strings.ToLower(row[idx])) == "true")
|
||||
}
|
||||
|
||||
if idx, ok := colIndex["external_id"]; ok && idx < len(row) {
|
||||
record.ExternalID = strings.TrimSpace(row[idx])
|
||||
}
|
||||
|
||||
if idx, ok := colIndex["account_type"]; ok && idx < len(row) {
|
||||
if strings.TrimSpace(strings.ToUpper(row[idx])) == "SERVICE_ACCOUNT" {
|
||||
record.AccountType = coredata.AccessEntryAccountTypeServiceAccount
|
||||
|
||||
@@ -24,6 +24,7 @@ func TestCSVDriverRequiresEmailHeader(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
driver := NewCSVDriver(strings.NewReader("full_name,role\nJane Doe,Admin\n"))
|
||||
|
||||
_, err := driver.ListAccounts(context.Background())
|
||||
if err == nil {
|
||||
t.Fatalf("expected error when email header is missing")
|
||||
@@ -36,16 +37,20 @@ func TestCSVDriverParsesRequiredAndOptionalColumns(t *testing.T) {
|
||||
driver := NewCSVDriver(strings.NewReader(
|
||||
"email,full_name,role,external_id\njane@example.com,Jane Doe,Admin,42\n",
|
||||
))
|
||||
|
||||
records, err := driver.ListAccounts(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if len(records) != 1 {
|
||||
t.Fatalf("expected 1 record, got %d", len(records))
|
||||
}
|
||||
|
||||
if records[0].Email != "jane@example.com" {
|
||||
t.Fatalf("unexpected email: %s", records[0].Email)
|
||||
}
|
||||
|
||||
if records[0].ExternalID != "42" {
|
||||
t.Fatalf("unexpected external id: %s", records[0].ExternalID)
|
||||
}
|
||||
|
||||
@@ -79,6 +79,7 @@ func (d *DocuSignDriver) ListAccounts(ctx context.Context) ([]AccountRecord, err
|
||||
}
|
||||
|
||||
var records []AccountRecord
|
||||
|
||||
startPosition := 0
|
||||
|
||||
for range maxPaginationPages {
|
||||
@@ -143,12 +144,14 @@ func (d *DocuSignDriver) discoverAccount(ctx context.Context) (accountID string,
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("cannot create docusign userinfo request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
httpResp, err := d.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("cannot execute docusign userinfo request: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = httpResp.Body.Close()
|
||||
}()
|
||||
@@ -183,12 +186,14 @@ func (d *DocuSignDriver) queryUsers(ctx context.Context, baseURI string, account
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot create docusign users request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
httpResp, err := d.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot execute docusign users request: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = httpResp.Body.Close()
|
||||
}()
|
||||
|
||||
@@ -85,6 +85,7 @@ func (d *GitHubDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error
|
||||
d.logger.WarnCtx(ctx, "cannot fetch github membership, skipping member",
|
||||
log.Error(err),
|
||||
)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -93,6 +94,7 @@ func (d *GitHubDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error
|
||||
d.logger.WarnCtx(ctx, "cannot fetch github user profile, skipping member",
|
||||
log.Error(err),
|
||||
)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -107,6 +109,7 @@ func (d *GitHubDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error
|
||||
}
|
||||
|
||||
mfaStatus := coredata.MFAStatusUnknown
|
||||
|
||||
if no2FASet != nil {
|
||||
if no2FASet[m.Login] {
|
||||
mfaStatus = coredata.MFAStatusDisabled
|
||||
@@ -158,6 +161,7 @@ func (d *GitHubDriver) fetchAllMembers(ctx context.Context) ([]githubMember, err
|
||||
if nextURL == "" {
|
||||
return members, nil
|
||||
}
|
||||
|
||||
url = nextURL
|
||||
}
|
||||
|
||||
@@ -176,6 +180,7 @@ func (d *GitHubDriver) fetchMembersPage(ctx context.Context, url string) ([]gith
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("cannot execute github members request: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = httpResp.Body.Close()
|
||||
}()
|
||||
@@ -215,6 +220,7 @@ func (d *GitHubDriver) fetchAll2FADisabledLogins(ctx context.Context) (map[strin
|
||||
if nextURL == "" {
|
||||
return set, nil
|
||||
}
|
||||
|
||||
url = nextURL
|
||||
}
|
||||
|
||||
@@ -239,6 +245,7 @@ func (d *GitHubDriver) fetchMembership(ctx context.Context, login string) (*gith
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot execute github membership request: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = httpResp.Body.Close()
|
||||
}()
|
||||
@@ -269,6 +276,7 @@ func (d *GitHubDriver) fetchUserProfile(ctx context.Context, login string) (*git
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot execute github user profile request: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = httpResp.Body.Close()
|
||||
}()
|
||||
|
||||
@@ -117,12 +117,14 @@ func (d *GitLabDriver) queryMembers(ctx context.Context, endpoint string) ([]git
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("cannot create gitlab members request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
httpResp, err := d.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("cannot execute gitlab members request: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = httpResp.Body.Close() }()
|
||||
|
||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||
|
||||
@@ -59,6 +59,7 @@ func (rt *retryRoundTripper) RoundTrip(req *http.Request) (*http.Response, error
|
||||
}
|
||||
|
||||
var lastResp *http.Response
|
||||
|
||||
for attempt := range rt.maxRetries {
|
||||
resp, err := transport.RoundTrip(req)
|
||||
if err != nil {
|
||||
@@ -94,6 +95,7 @@ func (d *GoogleWorkspaceDriver) ListAccounts(ctx context.Context) ([]AccountReco
|
||||
}
|
||||
|
||||
var records []AccountRecord
|
||||
|
||||
pageToken := ""
|
||||
|
||||
for range maxPaginationPages {
|
||||
|
||||
@@ -124,6 +124,7 @@ func (d *HerokuDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error
|
||||
if nextRange == "" {
|
||||
return records, nil
|
||||
}
|
||||
|
||||
rangeHeader = nextRange
|
||||
}
|
||||
|
||||
@@ -135,7 +136,9 @@ func (d *HerokuDriver) queryMembers(ctx context.Context, endpoint, rangeHeader s
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("cannot create heroku members request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/vnd.heroku+json; version=3")
|
||||
|
||||
if rangeHeader != "" {
|
||||
req.Header.Set("Range", rangeHeader)
|
||||
}
|
||||
@@ -144,6 +147,7 @@ func (d *HerokuDriver) queryMembers(ctx context.Context, endpoint, rangeHeader s
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("cannot execute heroku members request: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = httpResp.Body.Close() }()
|
||||
|
||||
// Heroku returns 206 Partial Content for ranged responses with more
|
||||
|
||||
@@ -83,6 +83,7 @@ func (d *HubSpotDriver) ListAccounts(ctx context.Context) ([]AccountRecord, erro
|
||||
|
||||
for _, u := range resp.Results {
|
||||
role := "User"
|
||||
|
||||
if roleMap != nil && u.RoleID != "" {
|
||||
if name, ok := roleMap[u.RoleID]; ok {
|
||||
role = name
|
||||
@@ -114,6 +115,7 @@ func (d *HubSpotDriver) ListAccounts(ctx context.Context) ([]AccountRecord, erro
|
||||
if resp.Paging == nil || resp.Paging.Next == nil || resp.Paging.Next.After == "" {
|
||||
return records, nil
|
||||
}
|
||||
|
||||
after = resp.Paging.Next.After
|
||||
}
|
||||
|
||||
@@ -128,9 +130,11 @@ func (d *HubSpotDriver) fetchUsers(ctx context.Context, after string) (*hubspotU
|
||||
|
||||
q := req.URL.Query()
|
||||
q.Set("limit", "100")
|
||||
|
||||
if after != "" {
|
||||
q.Set("after", after)
|
||||
}
|
||||
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
@@ -139,6 +143,7 @@ func (d *HubSpotDriver) fetchUsers(ctx context.Context, after string) (*hubspotU
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot execute hubspot users request: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = httpResp.Body.Close()
|
||||
}()
|
||||
@@ -167,6 +172,7 @@ func (d *HubSpotDriver) fetchRoles(ctx context.Context) (map[string]string, erro
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot execute hubspot roles request: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = httpResp.Body.Close()
|
||||
}()
|
||||
|
||||
@@ -61,6 +61,7 @@ func (d *IntercomDriver) ListAccounts(ctx context.Context) ([]AccountRecord, err
|
||||
}
|
||||
|
||||
var records []AccountRecord
|
||||
|
||||
for _, a := range resp.Admins {
|
||||
record := AccountRecord{
|
||||
Email: a.Email,
|
||||
@@ -95,6 +96,7 @@ func (d *IntercomDriver) fetchAdmins(ctx context.Context) (*intercomAdminsRespon
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot execute intercom admins request: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = httpResp.Body.Close()
|
||||
}()
|
||||
@@ -119,5 +121,6 @@ func intercomRole(hasInboxSeat bool) string {
|
||||
if hasInboxSeat {
|
||||
return "Agent"
|
||||
}
|
||||
|
||||
return "Viewer"
|
||||
}
|
||||
|
||||
@@ -125,6 +125,7 @@ func (d *LinearDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error
|
||||
if !resp.Data.Users.PageInfo.HasNextPage || resp.Data.Users.PageInfo.EndCursor == "" {
|
||||
return records, nil
|
||||
}
|
||||
|
||||
nextCursor := resp.Data.Users.PageInfo.EndCursor
|
||||
after = &nextCursor
|
||||
}
|
||||
@@ -170,6 +171,7 @@ query AccessReviewLinearUsers($after: String) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot create linear users request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
@@ -177,6 +179,7 @@ query AccessReviewLinearUsers($after: String) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot execute linear users request: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = httpResp.Body.Close()
|
||||
}()
|
||||
@@ -189,6 +192,7 @@ query AccessReviewLinearUsers($after: String) {
|
||||
if err := json.NewDecoder(httpResp.Body).Decode(&resp); err != nil {
|
||||
return nil, fmt.Errorf("cannot decode linear users response: %w", err)
|
||||
}
|
||||
|
||||
if len(resp.Errors) > 0 {
|
||||
return nil, fmt.Errorf("linear graphql error: %s", resp.Errors[0].Message)
|
||||
}
|
||||
|
||||
@@ -120,15 +120,18 @@ func (d *Microsoft365Driver) ListAccounts(ctx context.Context) ([]AccountRecord,
|
||||
}
|
||||
|
||||
rolesByUser := make(map[string][]string)
|
||||
|
||||
for _, role := range roles {
|
||||
members, err := d.listRoleMembers(ctx, role.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot list members of role %q: %w", role.DisplayName, err)
|
||||
}
|
||||
|
||||
for _, m := range members {
|
||||
if m.ODataType != "" && m.ODataType != "#microsoft.graph.user" {
|
||||
continue
|
||||
}
|
||||
|
||||
rolesByUser[m.ID] = append(rolesByUser[m.ID], role.DisplayName)
|
||||
}
|
||||
}
|
||||
@@ -147,6 +150,7 @@ func (d *Microsoft365Driver) ListAccounts(ctx context.Context) ([]AccountRecord,
|
||||
|
||||
userRoles := rolesByUser[u.ID]
|
||||
isAdmin := false
|
||||
|
||||
for _, r := range userRoles {
|
||||
if adminRoleDisplayNames[r] {
|
||||
isAdmin = true
|
||||
@@ -214,6 +218,7 @@ func pickHighestRole(roles []string) string {
|
||||
if len(roles) > 0 {
|
||||
return roles[0]
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -224,15 +229,18 @@ func (d *Microsoft365Driver) listUsers(ctx context.Context) ([]microsoft365User,
|
||||
}
|
||||
|
||||
var all []microsoft365User
|
||||
|
||||
for range microsoft365MaxPaginationOK {
|
||||
var page microsoft365UsersPage
|
||||
if err := d.fetchJSON(ctx, pageURL, &page); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
all = append(all, page.Value...)
|
||||
if page.NextLink == "" {
|
||||
return all, nil
|
||||
}
|
||||
|
||||
pageURL = page.NextLink
|
||||
}
|
||||
|
||||
@@ -258,15 +266,18 @@ func (d *Microsoft365Driver) listDirectoryRoles(ctx context.Context) ([]microsof
|
||||
url := fmt.Sprintf("%s/directoryRoles", microsoft365GraphBaseURL)
|
||||
|
||||
var all []microsoft365DirectoryRole
|
||||
|
||||
for range microsoft365MaxPaginationOK {
|
||||
var page microsoft365RolesPage
|
||||
if err := d.fetchJSON(ctx, url, &page); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
all = append(all, page.Value...)
|
||||
if page.NextLink == "" {
|
||||
return all, nil
|
||||
}
|
||||
|
||||
url = page.NextLink
|
||||
}
|
||||
|
||||
@@ -277,15 +288,18 @@ func (d *Microsoft365Driver) listRoleMembers(ctx context.Context, roleID string)
|
||||
url := fmt.Sprintf("%s/directoryRoles/%s/members", microsoft365GraphBaseURL, roleID)
|
||||
|
||||
var all []microsoft365RoleMember
|
||||
|
||||
for range microsoft365MaxPaginationOK {
|
||||
var page microsoft365MembersPage
|
||||
if err := d.fetchJSON(ctx, url, &page); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
all = append(all, page.Value...)
|
||||
if page.NextLink == "" {
|
||||
return all, nil
|
||||
}
|
||||
|
||||
url = page.NextLink
|
||||
}
|
||||
|
||||
@@ -297,12 +311,14 @@ func (d *Microsoft365Driver) fetchJSON(ctx context.Context, url string, dst any)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create graph request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
resp, err := d.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot execute graph request: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
|
||||
@@ -144,6 +144,7 @@ func (d *MondayDriver) queryUsers(ctx context.Context, page int) ([]mondayUser,
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot create monday users request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
@@ -151,6 +152,7 @@ func (d *MondayDriver) queryUsers(ctx context.Context, page int) ([]mondayUser,
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot execute monday users request: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = httpResp.Body.Close() }()
|
||||
|
||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||
|
||||
@@ -69,6 +69,7 @@ func ProviderDisplayName(provider coredata.ConnectorProvider) string {
|
||||
if name, ok := providerDisplayNames[provider]; ok {
|
||||
return name
|
||||
}
|
||||
|
||||
return string(provider)
|
||||
}
|
||||
|
||||
@@ -91,6 +92,7 @@ func (r *slackNameResolver) ResolveInstanceName(ctx context.Context) (string, er
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot execute slack auth.test request: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = httpResp.Body.Close() }()
|
||||
|
||||
var resp struct {
|
||||
@@ -156,6 +158,7 @@ func (r *linearNameResolver) ResolveInstanceName(ctx context.Context) (string, e
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot create linear organization request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
@@ -163,6 +166,7 @@ func (r *linearNameResolver) ResolveInstanceName(ctx context.Context) (string, e
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot execute linear organization request: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = httpResp.Body.Close() }()
|
||||
|
||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||
@@ -182,6 +186,7 @@ func (r *linearNameResolver) ResolveInstanceName(ctx context.Context) (string, e
|
||||
if err := json.NewDecoder(httpResp.Body).Decode(&resp); err != nil {
|
||||
return "", fmt.Errorf("cannot decode linear organization response: %w", err)
|
||||
}
|
||||
|
||||
if len(resp.Errors) > 0 {
|
||||
return "", fmt.Errorf("linear graphql error: %s", resp.Errors[0].Message)
|
||||
}
|
||||
@@ -208,12 +213,14 @@ func (r *cloudflareNameResolver) ResolveInstanceName(ctx context.Context) (strin
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot create cloudflare accounts request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
httpResp, err := r.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot execute cloudflare accounts request: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = httpResp.Body.Close() }()
|
||||
|
||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||
@@ -255,12 +262,14 @@ func (r *brexNameResolver) ResolveInstanceName(ctx context.Context) (string, err
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot create brex company request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
httpResp, err := r.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot execute brex company request: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = httpResp.Body.Close() }()
|
||||
|
||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||
@@ -297,12 +306,14 @@ func (r *tallyNameResolver) ResolveInstanceName(ctx context.Context) (string, er
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot create tally organization request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
httpResp, err := r.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot execute tally organization request: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = httpResp.Body.Close() }()
|
||||
|
||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||
@@ -338,12 +349,14 @@ func (r *hubspotNameResolver) ResolveInstanceName(ctx context.Context) (string,
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot create hubspot account-info request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
httpResp, err := r.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot execute hubspot account-info request: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = httpResp.Body.Close() }()
|
||||
|
||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||
@@ -375,12 +388,14 @@ func (r *docusignNameResolver) ResolveInstanceName(ctx context.Context) (string,
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot create docusign userinfo request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
httpResp, err := r.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot execute docusign userinfo request: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = httpResp.Body.Close() }()
|
||||
|
||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||
@@ -429,12 +444,14 @@ func (r *openaiNameResolver) ResolveInstanceName(ctx context.Context) (string, e
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot create openai organization request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
httpResp, err := r.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot execute openai organization request: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = httpResp.Body.Close() }()
|
||||
|
||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||
@@ -473,12 +490,14 @@ func (r *sentryNameResolver) ResolveInstanceName(ctx context.Context) (string, e
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot create sentry organization request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
httpResp, err := r.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot execute sentry organization request: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = httpResp.Body.Close() }()
|
||||
|
||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||
@@ -512,12 +531,14 @@ func (r *githubNameResolver) ResolveInstanceName(ctx context.Context) (string, e
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot create github organization request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/vnd.github+json")
|
||||
|
||||
httpResp, err := r.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot execute github organization request: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = httpResp.Body.Close() }()
|
||||
|
||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||
@@ -565,6 +586,7 @@ func (r *intercomNameResolver) ResolveInstanceName(ctx context.Context) (string,
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot create intercom me request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Intercom-Version", "2.11")
|
||||
|
||||
@@ -572,6 +594,7 @@ func (r *intercomNameResolver) ResolveInstanceName(ctx context.Context) (string,
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot execute intercom me request: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = httpResp.Body.Close() }()
|
||||
|
||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||
@@ -622,12 +645,14 @@ func (r *gitlabNameResolver) ResolveInstanceName(ctx context.Context) (string, e
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot create gitlab group request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
httpResp, err := r.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot execute gitlab group request: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = httpResp.Body.Close() }()
|
||||
|
||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||
@@ -645,6 +670,7 @@ func (r *gitlabNameResolver) ResolveInstanceName(ctx context.Context) (string, e
|
||||
if resp.Name != "" {
|
||||
return resp.Name, nil
|
||||
}
|
||||
|
||||
return resp.FullPath, nil
|
||||
}
|
||||
|
||||
@@ -669,12 +695,14 @@ func (r *bitbucketNameResolver) ResolveInstanceName(ctx context.Context) (string
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot create bitbucket workspace request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
httpResp, err := r.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot execute bitbucket workspace request: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = httpResp.Body.Close() }()
|
||||
|
||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||
@@ -692,6 +720,7 @@ func (r *bitbucketNameResolver) ResolveInstanceName(ctx context.Context) (string
|
||||
if resp.Name != "" {
|
||||
return resp.Name, nil
|
||||
}
|
||||
|
||||
return resp.Slug, nil
|
||||
}
|
||||
|
||||
@@ -716,12 +745,14 @@ func (r *herokuNameResolver) ResolveInstanceName(ctx context.Context) (string, e
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot create heroku team request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/vnd.heroku+json; version=3")
|
||||
|
||||
httpResp, err := r.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot execute heroku team request: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = httpResp.Body.Close() }()
|
||||
|
||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||
@@ -774,12 +805,14 @@ func (r *asanaNameResolver) ResolveInstanceName(ctx context.Context) (string, er
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot create asana workspace request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
httpResp, err := r.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot execute asana workspace request: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = httpResp.Body.Close() }()
|
||||
|
||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||
@@ -819,12 +852,14 @@ func (r *netlifyNameResolver) ResolveInstanceName(ctx context.Context) (string,
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot create netlify account request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
httpResp, err := r.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot execute netlify account request: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = httpResp.Body.Close() }()
|
||||
|
||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||
@@ -862,12 +897,14 @@ func (r *clickupNameResolver) ResolveInstanceName(ctx context.Context) (string,
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot create clickup team request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
httpResp, err := r.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot execute clickup team request: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = httpResp.Body.Close() }()
|
||||
|
||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||
@@ -905,16 +942,19 @@ func (r *vercelNameResolver) ResolveInstanceName(ctx context.Context) (string, e
|
||||
}
|
||||
|
||||
teamURL := fmt.Sprintf("https://api.vercel.com/v2/teams/%s", url.PathEscape(r.teamID))
|
||||
|
||||
teamReq, err := http.NewRequestWithContext(ctx, http.MethodGet, teamURL, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot create vercel team request: %w", err)
|
||||
}
|
||||
|
||||
teamReq.Header.Set("Accept", "application/json")
|
||||
|
||||
teamResp, err := r.httpClient.Do(teamReq)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot execute vercel team request: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = teamResp.Body.Close() }()
|
||||
|
||||
if teamResp.StatusCode == http.StatusOK {
|
||||
@@ -925,9 +965,11 @@ func (r *vercelNameResolver) ResolveInstanceName(ctx context.Context) (string, e
|
||||
if err := json.NewDecoder(teamResp.Body).Decode(&body); err != nil {
|
||||
return "", fmt.Errorf("cannot decode vercel team response: %w", err)
|
||||
}
|
||||
|
||||
if body.Name != "" {
|
||||
return body.Name, nil
|
||||
}
|
||||
|
||||
return body.Slug, nil
|
||||
}
|
||||
|
||||
@@ -941,9 +983,11 @@ func (r *vercelNameResolver) ResolveInstanceName(ctx context.Context) (string, e
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if user.Username != "" {
|
||||
return user.Username, nil
|
||||
}
|
||||
|
||||
return user.Name, nil
|
||||
}
|
||||
|
||||
@@ -972,6 +1016,7 @@ func (r *mondayNameResolver) ResolveInstanceName(ctx context.Context) (string, e
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot create monday account request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
@@ -979,6 +1024,7 @@ func (r *mondayNameResolver) ResolveInstanceName(ctx context.Context) (string, e
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot execute monday account request: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = httpResp.Body.Close() }()
|
||||
|
||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||
@@ -1023,6 +1069,7 @@ func (r *notionNameResolver) ResolveInstanceName(ctx context.Context) (string, e
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot create notion users/me request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Notion-Version", notionAPIVersion)
|
||||
|
||||
@@ -1030,6 +1077,7 @@ func (r *notionNameResolver) ResolveInstanceName(ctx context.Context) (string, e
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot execute notion users/me request: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = httpResp.Body.Close() }()
|
||||
|
||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||
@@ -1068,12 +1116,14 @@ func (r *microsoft365NameResolver) ResolveInstanceName(ctx context.Context) (str
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot create microsoft 365 organization request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
httpResp, err := r.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot execute microsoft 365 organization request: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = httpResp.Body.Close() }()
|
||||
|
||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||
@@ -1101,13 +1151,16 @@ func (r *microsoft365NameResolver) ResolveInstanceName(ctx context.Context) (str
|
||||
if org.DisplayName != "" {
|
||||
return org.DisplayName, nil
|
||||
}
|
||||
|
||||
for _, d := range org.VerifiedDomains {
|
||||
if d.IsDefault {
|
||||
return d.Name, nil
|
||||
}
|
||||
}
|
||||
|
||||
if len(org.VerifiedDomains) > 0 {
|
||||
return org.VerifiedDomains[0].Name, nil
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
@@ -37,9 +37,11 @@ func (h *hostRewriter) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r2 := r.Clone(r.Context())
|
||||
r2.URL.Scheme = u.Scheme
|
||||
r2.URL.Host = u.Host
|
||||
|
||||
return http.DefaultTransport.RoundTrip(r2)
|
||||
}
|
||||
|
||||
@@ -88,11 +90,13 @@ func TestNotionNameResolver(t *testing.T) {
|
||||
defer srv.Close()
|
||||
|
||||
client := &http.Client{Transport: &hostRewriter{target: srv.URL}}
|
||||
|
||||
got, err := NewNotionNameResolver(client).ResolveInstanceName(context.Background())
|
||||
if tc.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.want, got)
|
||||
})
|
||||
|
||||
@@ -94,12 +94,14 @@ func (d *NetlifyDriver) queryMembers(ctx context.Context, endpoint string) ([]ne
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("cannot create netlify members request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
httpResp, err := d.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("cannot execute netlify members request: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = httpResp.Body.Close() }()
|
||||
|
||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||
|
||||
@@ -96,6 +96,7 @@ func (d *NotionDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error
|
||||
if !resp.HasMore || resp.NextCursor == "" {
|
||||
return records, nil
|
||||
}
|
||||
|
||||
nextCursor := resp.NextCursor
|
||||
startCursor = &nextCursor
|
||||
}
|
||||
@@ -114,15 +115,18 @@ func (d *NotionDriver) queryUsers(ctx context.Context, startCursor *string) (*no
|
||||
|
||||
q := req.URL.Query()
|
||||
q.Set("page_size", "100")
|
||||
|
||||
if startCursor != nil {
|
||||
q.Set("start_cursor", *startCursor)
|
||||
}
|
||||
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
httpResp, err := d.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot execute notion users request: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = httpResp.Body.Close()
|
||||
}()
|
||||
|
||||
@@ -71,6 +71,7 @@ func NewOnePasswordDriver(httpClient *http.Client, baseURL string) *OnePasswordD
|
||||
|
||||
func (d *OnePasswordDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error) {
|
||||
var records []AccountRecord
|
||||
|
||||
startIndex := 1
|
||||
|
||||
for range maxPaginationPages {
|
||||
@@ -103,6 +104,7 @@ func (d *OnePasswordDriver) ListAccounts(ctx context.Context) ([]AccountRecord,
|
||||
if record.FullName == "" && u.Name.Formatted != "" {
|
||||
record.FullName = u.Name.Formatted
|
||||
}
|
||||
|
||||
if record.FullName == "" && (u.Name.GivenName != "" || u.Name.FamilyName != "") {
|
||||
record.FullName = u.Name.GivenName + " " + u.Name.FamilyName
|
||||
}
|
||||
@@ -128,6 +130,7 @@ func (d *OnePasswordDriver) ListAccounts(ctx context.Context) ([]AccountRecord,
|
||||
if len(resp.Resources) == 0 || resp.ItemsPerPage <= 0 || startIndex+resp.ItemsPerPage > resp.TotalResults {
|
||||
return records, nil
|
||||
}
|
||||
|
||||
startIndex += resp.ItemsPerPage
|
||||
}
|
||||
|
||||
@@ -139,6 +142,7 @@ func (d *OnePasswordDriver) queryUsers(ctx context.Context, startIndex int) (*on
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot parse 1password base url: %w", err)
|
||||
}
|
||||
|
||||
u = u.JoinPath("scim", "v2", "Users")
|
||||
q := u.Query()
|
||||
q.Set("startIndex", strconv.Itoa(startIndex))
|
||||
@@ -149,12 +153,14 @@ func (d *OnePasswordDriver) queryUsers(ctx context.Context, startIndex int) (*on
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot create 1password users request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/scim+json")
|
||||
|
||||
httpResp, err := d.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot execute 1password users request: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = httpResp.Body.Close()
|
||||
}()
|
||||
|
||||
@@ -107,6 +107,7 @@ func (d *OnePasswordUsersAPIDriver) ListAccounts(ctx context.Context) ([]Account
|
||||
if resp.NextPageToken == "" {
|
||||
return records, nil
|
||||
}
|
||||
|
||||
pageToken = resp.NextPageToken
|
||||
}
|
||||
|
||||
@@ -118,6 +119,7 @@ func (d *OnePasswordUsersAPIDriver) queryUsers(ctx context.Context, pageToken st
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot parse 1password users api base url: %w", err)
|
||||
}
|
||||
|
||||
u = u.JoinPath("v1beta1", "accounts", d.accountID, "users")
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
|
||||
@@ -127,9 +129,11 @@ func (d *OnePasswordUsersAPIDriver) queryUsers(ctx context.Context, pageToken st
|
||||
|
||||
q := req.URL.Query()
|
||||
q.Set("max_page_size", "100")
|
||||
|
||||
if pageToken != "" {
|
||||
q.Set("page_token", pageToken)
|
||||
}
|
||||
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
@@ -138,6 +142,7 @@ func (d *OnePasswordUsersAPIDriver) queryUsers(ctx context.Context, pageToken st
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot execute 1password users api request: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = httpResp.Body.Close()
|
||||
}()
|
||||
|
||||
@@ -89,6 +89,7 @@ func (d *OpenAIDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error
|
||||
if !resp.HasMore || resp.LastID == "" {
|
||||
return records, nil
|
||||
}
|
||||
|
||||
after = resp.LastID
|
||||
}
|
||||
|
||||
@@ -103,9 +104,11 @@ func (d *OpenAIDriver) fetchUsers(ctx context.Context, after string) (*openaiUse
|
||||
|
||||
q := req.URL.Query()
|
||||
q.Set("limit", "100")
|
||||
|
||||
if after != "" {
|
||||
q.Set("after", after)
|
||||
}
|
||||
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
@@ -114,6 +117,7 @@ func (d *OpenAIDriver) fetchUsers(ctx context.Context, after string) (*openaiUse
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot execute openai users request: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = httpResp.Body.Close()
|
||||
}()
|
||||
|
||||
@@ -38,12 +38,14 @@ func ListGitHubOrganizations(ctx context.Context, httpClient *http.Client) ([]Or
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot create github organizations request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot fetch github organizations: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
@@ -64,8 +66,10 @@ func ListGitHubOrganizations(ctx context.Context, httpClient *http.Client) ([]Or
|
||||
if displayName == "" {
|
||||
displayName = org.Login
|
||||
}
|
||||
|
||||
result[i] = Organization{Slug: org.Login, DisplayName: displayName}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -81,12 +85,14 @@ func ListSentryOrganizations(ctx context.Context, httpClient *http.Client) ([]Or
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot create sentry organizations request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot fetch sentry organizations: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
@@ -107,8 +113,10 @@ func ListSentryOrganizations(ctx context.Context, httpClient *http.Client) ([]Or
|
||||
if displayName == "" {
|
||||
displayName = org.Slug
|
||||
}
|
||||
|
||||
result[i] = Organization{Slug: org.Slug, DisplayName: displayName}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -125,12 +133,14 @@ func ListGitLabOrganizations(ctx context.Context, httpClient *http.Client) ([]Or
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot create gitlab organizations request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot fetch gitlab organizations: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
@@ -152,11 +162,13 @@ func ListGitLabOrganizations(ctx context.Context, httpClient *http.Client) ([]Or
|
||||
if displayName == "" {
|
||||
displayName = g.FullPath
|
||||
}
|
||||
|
||||
result[i] = Organization{
|
||||
Slug: strconv.FormatInt(g.ID, 10),
|
||||
DisplayName: displayName,
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -174,6 +186,7 @@ func ListBitbucketOrganizations(ctx context.Context, httpClient *http.Client) ([
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot create bitbucket organizations request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
@@ -203,6 +216,7 @@ func ListBitbucketOrganizations(ctx context.Context, httpClient *http.Client) ([
|
||||
_ = resp.Body.Close()
|
||||
return nil, fmt.Errorf("cannot decode bitbucket organizations response: %w", err)
|
||||
}
|
||||
|
||||
_ = resp.Body.Close()
|
||||
|
||||
for _, v := range body.Values {
|
||||
@@ -211,18 +225,22 @@ func ListBitbucketOrganizations(ctx context.Context, httpClient *http.Client) ([
|
||||
slug = v.Workspace.Slug
|
||||
name = v.Workspace.Name
|
||||
}
|
||||
|
||||
displayName := name
|
||||
if displayName == "" {
|
||||
displayName = slug
|
||||
}
|
||||
|
||||
result = append(result, Organization{Slug: slug, DisplayName: displayName})
|
||||
}
|
||||
|
||||
if body.Next == "" {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
pageURL = body.Next
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("cannot list all bitbucket organizations: %w", ErrPaginationLimitReached)
|
||||
}
|
||||
|
||||
@@ -233,12 +251,14 @@ func ListHerokuOrganizations(ctx context.Context, httpClient *http.Client) ([]Or
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot create heroku organizations request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/vnd.heroku+json; version=3")
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot fetch heroku organizations: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
@@ -259,8 +279,10 @@ func ListHerokuOrganizations(ctx context.Context, httpClient *http.Client) ([]Or
|
||||
if displayName == "" {
|
||||
displayName = t.ID
|
||||
}
|
||||
|
||||
result[i] = Organization{Slug: t.ID, DisplayName: displayName}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -276,12 +298,14 @@ func ListAsanaOrganizations(ctx context.Context, httpClient *http.Client) ([]Org
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot create asana organizations request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot fetch asana organizations: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
@@ -304,8 +328,10 @@ func ListAsanaOrganizations(ctx context.Context, httpClient *http.Client) ([]Org
|
||||
if displayName == "" {
|
||||
displayName = w.GID
|
||||
}
|
||||
|
||||
result[i] = Organization{Slug: w.GID, DisplayName: displayName}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -321,12 +347,14 @@ func ListNetlifyOrganizations(ctx context.Context, httpClient *http.Client) ([]O
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot create netlify organizations request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot fetch netlify organizations: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
@@ -348,8 +376,10 @@ func ListNetlifyOrganizations(ctx context.Context, httpClient *http.Client) ([]O
|
||||
if displayName == "" {
|
||||
displayName = a.Slug
|
||||
}
|
||||
|
||||
result[i] = Organization{Slug: a.Slug, DisplayName: displayName}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -365,12 +395,14 @@ func ListClickUpOrganizations(ctx context.Context, httpClient *http.Client) ([]O
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot create clickup organizations request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot fetch clickup organizations: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
@@ -393,7 +425,9 @@ func ListClickUpOrganizations(ctx context.Context, httpClient *http.Client) ([]O
|
||||
if displayName == "" {
|
||||
displayName = t.ID
|
||||
}
|
||||
|
||||
result[i] = Organization{Slug: t.ID, DisplayName: displayName}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ func (d *PagerDutyDriver) ListAccounts(ctx context.Context) ([]AccountRecord, er
|
||||
var records []AccountRecord
|
||||
|
||||
const limit = 100
|
||||
|
||||
offset := 0
|
||||
|
||||
for range maxPaginationPages {
|
||||
@@ -113,6 +114,7 @@ func (d *PagerDutyDriver) ListAccounts(ctx context.Context) ([]AccountRecord, er
|
||||
if pageSize <= 0 {
|
||||
pageSize = limit
|
||||
}
|
||||
|
||||
offset += pageSize
|
||||
}
|
||||
|
||||
@@ -130,12 +132,14 @@ func (d *PagerDutyDriver) queryUsers(ctx context.Context, offset, limit int) (*p
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot create pagerduty users request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/vnd.pagerduty+json;version=2")
|
||||
|
||||
httpResp, err := d.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot execute pagerduty users request: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = httpResp.Body.Close() }()
|
||||
|
||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||
|
||||
@@ -54,6 +54,7 @@ func (d *ResendDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error
|
||||
}
|
||||
|
||||
var records []AccountRecord
|
||||
|
||||
for _, k := range resp.Data {
|
||||
record := AccountRecord{
|
||||
FullName: k.Name,
|
||||
@@ -96,6 +97,7 @@ func (d *ResendDriver) fetchAPIKeys(ctx context.Context) (*resendAPIKeysResponse
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot execute resend api-keys request: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = httpResp.Body.Close()
|
||||
}()
|
||||
|
||||
@@ -67,12 +67,14 @@ func (d *SentryDriver) resolveOrgSlug(ctx context.Context) (string, error) {
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot create sentry organizations request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
resp, err := d.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot fetch sentry organizations: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
@@ -100,6 +102,7 @@ func (d *SentryDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot resolve sentry organization slug: %w", err)
|
||||
}
|
||||
|
||||
orgSlug = slug
|
||||
}
|
||||
|
||||
@@ -130,6 +133,7 @@ func (d *SentryDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error
|
||||
isAdmin := m.OrgRole == "admin" || m.OrgRole == "owner"
|
||||
|
||||
mfaStatus := coredata.MFAStatusUnknown
|
||||
|
||||
if m.User != nil {
|
||||
if m.User.Has2FA {
|
||||
mfaStatus = coredata.MFAStatusEnabled
|
||||
@@ -188,6 +192,7 @@ func (d *SentryDriver) queryMembers(ctx context.Context, url string) ([]sentryMe
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("cannot execute sentry members request: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = httpResp.Body.Close()
|
||||
}()
|
||||
@@ -221,8 +226,10 @@ func sentryAuthMethod(flags map[string]bool, user *sentryUser) coredata.AccessEn
|
||||
if flags["sso:linked"] {
|
||||
return coredata.AccessEntryAuthMethodSSO
|
||||
}
|
||||
|
||||
if user != nil && user.HasPasswordAuth {
|
||||
return coredata.AccessEntryAuthMethodPassword
|
||||
}
|
||||
|
||||
return coredata.AccessEntryAuthMethodUnknown
|
||||
}
|
||||
|
||||
@@ -120,6 +120,7 @@ func (d *SlackDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error)
|
||||
if resp.ResponseMetadata.NextCursor == "" {
|
||||
return records, nil
|
||||
}
|
||||
|
||||
cursor = resp.ResponseMetadata.NextCursor
|
||||
}
|
||||
|
||||
@@ -134,15 +135,18 @@ func (d *SlackDriver) queryUsers(ctx context.Context, cursor string) (*slackUser
|
||||
|
||||
q := req.URL.Query()
|
||||
q.Set("limit", "200")
|
||||
|
||||
if cursor != "" {
|
||||
q.Set("cursor", cursor)
|
||||
}
|
||||
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
httpResp, err := d.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot execute slack users.list request: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = httpResp.Body.Close()
|
||||
}()
|
||||
@@ -180,5 +184,6 @@ func slackMFAStatus(has2FA bool) coredata.MFAStatus {
|
||||
if has2FA {
|
||||
return coredata.MFAStatusEnabled
|
||||
}
|
||||
|
||||
return coredata.MFAStatusDisabled
|
||||
}
|
||||
|
||||
@@ -36,12 +36,14 @@ func TestSlackDriver(t *testing.T) {
|
||||
|
||||
// Find the first human user (bots may not have email).
|
||||
var r AccountRecord
|
||||
|
||||
for _, rec := range records {
|
||||
if rec.Email != "" {
|
||||
r = rec
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
require.NotEmpty(t, r.Email, "expected at least one record with an email")
|
||||
assert.NotEmpty(t, r.ExternalID)
|
||||
assert.NotEmpty(t, r.Role)
|
||||
|
||||
@@ -53,6 +53,7 @@ func (d *SupabaseDriver) ListAccounts(ctx context.Context) ([]AccountRecord, err
|
||||
}
|
||||
|
||||
var records []AccountRecord
|
||||
|
||||
for _, m := range members {
|
||||
mfaStatus := coredata.MFAStatusDisabled
|
||||
if m.MFAEnabled {
|
||||
@@ -96,6 +97,7 @@ func (d *SupabaseDriver) queryMembers(ctx context.Context) ([]supabaseMember, er
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot execute supabase members request: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = httpResp.Body.Close()
|
||||
}()
|
||||
|
||||
@@ -89,6 +89,7 @@ func (d *TallyDriver) listUsers(ctx context.Context) ([]AccountRecord, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot execute tally users request: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = httpResp.Body.Close()
|
||||
}()
|
||||
@@ -106,6 +107,7 @@ func (d *TallyDriver) listUsers(ctx context.Context) ([]AccountRecord, error) {
|
||||
}
|
||||
|
||||
var records []AccountRecord
|
||||
|
||||
for _, u := range users {
|
||||
mfaStatus := coredata.MFAStatusDisabled
|
||||
if u.HasTwoFactorEnabled {
|
||||
@@ -149,6 +151,7 @@ func (d *TallyDriver) listInvites(ctx context.Context) ([]AccountRecord, error)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot execute tally invites request: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = httpResp.Body.Close()
|
||||
}()
|
||||
@@ -166,6 +169,7 @@ func (d *TallyDriver) listInvites(ctx context.Context) ([]AccountRecord, error)
|
||||
}
|
||||
|
||||
var records []AccountRecord
|
||||
|
||||
for _, inv := range invites {
|
||||
record := AccountRecord{
|
||||
Email: inv.Email,
|
||||
|
||||
@@ -57,6 +57,7 @@ func newRecorder(t *testing.T, cassettePath string, envVar string) *recorder.Rec
|
||||
if mode == recorder.ModeReplayOnly {
|
||||
t.Skipf("cassette not found (record with %s env var): %v", envVar, err)
|
||||
}
|
||||
|
||||
t.Fatalf("cannot create vcr recorder: %v", err)
|
||||
}
|
||||
|
||||
@@ -81,6 +82,7 @@ func (rt *authRoundTripper) RoundTrip(req *http.Request) (*http.Response, error)
|
||||
if rt.authValue != "" {
|
||||
req.Header.Set("Authorization", rt.authValue)
|
||||
}
|
||||
|
||||
return rt.transport.RoundTrip(req)
|
||||
}
|
||||
|
||||
@@ -89,6 +91,7 @@ func bearerAuth(token string) string {
|
||||
if token == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
return "Bearer " + token
|
||||
}
|
||||
|
||||
@@ -104,5 +107,6 @@ func newVCRClient(rec *recorder.Recorder, authValue string) *http.Client {
|
||||
transport: transport,
|
||||
}
|
||||
}
|
||||
|
||||
return &http.Client{Transport: transport}
|
||||
}
|
||||
|
||||
@@ -111,6 +111,7 @@ func (d *VercelDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error
|
||||
if page.Pagination.Next == nil {
|
||||
return records, nil
|
||||
}
|
||||
|
||||
cursor = strconv.FormatInt(*page.Pagination.Next, 10)
|
||||
}
|
||||
|
||||
@@ -120,9 +121,11 @@ func (d *VercelDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error
|
||||
func (d *VercelDriver) queryMembers(ctx context.Context, cursor string) (*vercelMembersPage, error) {
|
||||
q := url.Values{}
|
||||
q.Set("limit", "100")
|
||||
|
||||
if cursor != "" {
|
||||
q.Set("until", cursor)
|
||||
}
|
||||
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "api.vercel.com",
|
||||
@@ -134,12 +137,14 @@ func (d *VercelDriver) queryMembers(ctx context.Context, cursor string) (*vercel
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot create vercel members request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
httpResp, err := d.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot execute vercel members request: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = httpResp.Body.Close() }()
|
||||
|
||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||
|
||||
Reference in New Issue
Block a user