diff --git a/.gitignore b/.gitignore index 2cd464d..3a64bae 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,10 @@ cq-source-jumpcloud dist -*.log \ No newline at end of file +*.log +.vscode +.envrc +.cq +data +cloudquery +__debug* diff --git a/client/client.go b/client/client.go index 7c92faf..6a47fe5 100644 --- a/client/client.go +++ b/client/client.go @@ -3,18 +3,33 @@ package client import ( "context" "fmt" + "log" "os" "github.com/TheJumpCloud/jcapi" + + jcapiv2 "github.com/TheJumpCloud/jcapi-go/v2" + "github.com/cloudquery/plugin-pb-go/specs" "github.com/cloudquery/plugin-sdk/v3/plugins/source" "github.com/cloudquery/plugin-sdk/v3/schema" "github.com/rs/zerolog" ) +// the following constants are used for API v2 calls: +const ( + contentType = "application/json" + accept = "application/json" + searchLimit = 100 + searchSkipInterval = 100 +) + type Client struct { - Logger zerolog.Logger - JumpCloud *jcapi.JCAPI + Logger zerolog.Logger + JumpCloud *jcapi.JCAPI + JumpCloudv2 *jcapiv2.APIClient + JumpCloudv2Auth context.Context + IsGroups bool } func (c *Client) ID() string { @@ -32,13 +47,35 @@ func New(ctx context.Context, logger zerolog.Logger, s specs.Source, opts source apiURL = config("JUMPCLOUD_API_URL", "https://console.jumpcloud.com/api") apiKey = config("JUMPCLOUD_API_KEY", "") apiClientV1 = jcapi.NewJCAPI(apiKey, apiURL) + apiClientV2 = jcapiv2.NewAPIClient(jcapiv2.NewConfiguration()) ) + // check if this org is on Groups or Tags: + isGroups, err := isGroupsOrg(apiURL, apiKey) + if err != nil { + log.Fatalf("Could not determine your org type, err='%s'\n", err) + } + // if we're on a groups org, instantiate the API client v2: + var auth context.Context + if isGroups { + // instantiate API client v2: + apiClientV2 = jcapiv2.NewAPIClient(jcapiv2.NewConfiguration()) + apiClientV2.ChangeBasePath(apiURL + "/v2") + // set up the API key via context: + auth = context.WithValue(context.TODO(), jcapiv2.ContextAPIKey, jcapiv2.APIKey{ + Key: apiKey, + }) + } + return &Client{ - Logger: logger, - JumpCloud: &apiClientV1, + Logger: logger, + JumpCloud: &apiClientV1, + JumpCloudv2: apiClientV2, + IsGroups: isGroups, + JumpCloudv2Auth: auth, }, nil } + func config(s, e string) string { envVar := os.Getenv(s) if envVar != "" { @@ -46,3 +83,43 @@ func config(s, e string) string { } return e } + +// isGroupsOrg returns true if this org is groups enabled: +func isGroupsOrg(urlBase string, apiKey string) (bool, error) { + // instantiate a new API client object: + client := jcapiv2.NewAPIClient(jcapiv2.NewConfiguration()) + client.ChangeBasePath(urlBase + "/v2") + + // set up the API key via context: + auth := context.WithValue(context.TODO(), jcapiv2.ContextAPIKey, jcapiv2.APIKey{ + Key: apiKey, + }) + + // set up optional parameters: + optionals := map[string]interface{}{ + "limit": int32(1), // limit the query to return 1 item + } + // in order to check for groups support, we just query for the list of User groups + // (we just ask to retrieve 1) and check the response status code: + _, res, err := client.UserGroupsApi.GroupsUserList(auth, contentType, accept, optionals) + + // check if we're using the API v1: + // we need to explicitly check for 404, since GroupsUserList will also return a json + // unmarshalling error (err will not be nil) if we're running this endpoint against + // a Tags org and we don't want to treat this case as an error: + if res != nil && res.StatusCode == 404 { + return false, nil + } + + // if there was any kind of other error, return that: + if err != nil { + return false, err + } + + // if we're using API v2, we're expecting a 200: + if res.StatusCode == 200 { + return true, nil + } + + return false, nil +} diff --git a/docs/tables/README.md b/docs/tables/README.md index ba1e63a..2e1b795 100644 --- a/docs/tables/README.md +++ b/docs/tables/README.md @@ -3,4 +3,5 @@ ## Tables - [jumpcloud_systems](jumpcloud_systems.md) -- [jumpcloud_users](jumpcloud_users.md) \ No newline at end of file +- [jumpcloud_users](jumpcloud_users.md) +- [jumpcloud_users_bound_to_system](jumpcloud_users_bound_to_system.md) \ No newline at end of file diff --git a/docs/tables/jumpcloud_systems.md b/docs/tables/jumpcloud_systems.md index ace372e..4b44239 100644 --- a/docs/tables/jumpcloud_systems.md +++ b/docs/tables/jumpcloud_systems.md @@ -37,4 +37,8 @@ The primary key for this table is **_id**. |connection_history|`list`| |sshd_params|`json`| |network_interfaces|`json`| -|tags|`list`| \ No newline at end of file +|tags|`list`| +|hardware_model|`utf8`| +|hardware_serial|`utf8`| +|hardware_vendor|`utf8`| +|hardware_version|`utf8`| \ No newline at end of file diff --git a/docs/tables/jumpcloud_users_bound_to_system.md b/docs/tables/jumpcloud_users_bound_to_system.md new file mode 100644 index 0000000..167d3e5 --- /dev/null +++ b/docs/tables/jumpcloud_users_bound_to_system.md @@ -0,0 +1,26 @@ +# Table: jumpcloud_users_bound_to_system + +This table shows data for Jumpcloud Users Bound To System. + +The primary key for this table is **_cq_id**. + +## Columns + +| Name | Type | +| ------------- | ------------- | +|_cq_source_name|`utf8`| +|_cq_sync_time|`timestamp[us, tz=UTC]`| +|_cq_id (PK)|`uuid`| +|_cq_parent_id|`uuid`| +|_id|`utf8`| +|display_name|`utf8`| +|hostname|`utf8`| +|active|`bool`| +|amazon_instance_id|`utf8`| +|os|`utf8`| +|version|`utf8`| +|agent_version|`utf8`| +|created|`utf8`| +|last_contact|`utf8`| +|user_name|`utf8`| +|email|`utf8`| \ No newline at end of file diff --git a/go.mod b/go.mod index f037022..81f5e45 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.18 require ( github.com/TheJumpCloud/jcapi v0.0.0-20180830175259-45efc78e5511 + github.com/TheJumpCloud/jcapi-go v3.0.0+incompatible github.com/cloudquery/plugin-pb-go v1.1.0 github.com/cloudquery/plugin-sdk/v3 v3.10.6 github.com/rs/zerolog v1.29.1 @@ -37,11 +38,13 @@ require ( golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect golang.org/x/mod v0.8.0 // indirect golang.org/x/net v0.9.0 // indirect + golang.org/x/oauth2 v0.6.0 // indirect golang.org/x/sync v0.1.0 // indirect golang.org/x/sys v0.7.0 // indirect golang.org/x/text v0.9.0 // indirect golang.org/x/tools v0.6.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect + google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect google.golang.org/grpc v1.55.0 // indirect google.golang.org/protobuf v1.30.0 // indirect diff --git a/go.sum b/go.sum index 65423ef..632e4de 100644 --- a/go.sum +++ b/go.sum @@ -35,6 +35,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/TheJumpCloud/jcapi v0.0.0-20180830175259-45efc78e5511 h1:tbtlzatMZSJPMehmw8N+Fq4vs+w4XGxMDk7vElBr7kU= github.com/TheJumpCloud/jcapi v0.0.0-20180830175259-45efc78e5511/go.mod h1:ngL7kl3Cp6oRY0BpL1wb14Dr3ZeXUINen1Us88jLxe4= +github.com/TheJumpCloud/jcapi-go v3.0.0+incompatible h1:hqcTK6ZISdip65SR792lwYJTa/axESA0889D3UlZbLo= +github.com/TheJumpCloud/jcapi-go v3.0.0+incompatible/go.mod h1:6B1nuc1MUs6c62ODZDl7hVE5Pv7O2XGSkgg2olnq34I= github.com/apache/arrow/go/v13 v13.0.0-20230622123301-12891333a850 h1:BKdugeucKQdIJOnSr+plkBs5gtL4cnAzZhZDvFzmp6o= github.com/apache/arrow/go/v13 v13.0.0-20230622123301-12891333a850/go.mod h1:W69eByFNO0ZR30q1/7Sr9d83zcVZmF2MiP3fFYAWJOc= github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= @@ -279,6 +281,8 @@ golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4Iltr golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20210413134643-5e61552d6c78/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw= +golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -408,6 +412,8 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= diff --git a/plugin/plugin.go b/plugin/plugin.go index e26c40e..92ff01d 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -19,6 +19,7 @@ func Plugin() *source.Plugin { schema.Tables{ resources.UsersTable(), resources.SystemsTable(), + resources.UsersBoundToSystemTable(), }, client.New, ) diff --git a/resources/systems.go b/resources/systems.go index d2d6be4..333229d 100644 --- a/resources/systems.go +++ b/resources/systems.go @@ -9,16 +9,55 @@ import ( "fmt" "github.com/TheJumpCloud/jcapi" + jcapiv2 "github.com/TheJumpCloud/jcapi-go/v2" "github.com/cloudquery/plugin-sdk/v3/schema" "github.com/cloudquery/plugin-sdk/v3/transformers" "github.com/virtualbeck/cq-source-jumpcloud/client" ) +type System struct { + Os string `json:"os,omitempty"` + TemplateName string `json:"templateName,omitempty"` + AllowSshRootLogin bool `json:"allowSshRootLogin"` + Id string `json:"_id"` + LastContact string `json:"lastContact,omitempty"` + RemoteIP string `json:"remoteIP,omitempty"` + Active bool `json:"active,omitempty"` + SshRootEnabled bool `json:"sshRootEnabled"` + AmazonInstanceID string `json:"amazonInstanceID,omitempty"` + SshPassEnabled bool `json:"sshPassEnabled,omitempty"` + Version string `json:"version,omitempty"` + AgentVersion string `json:"agentVersion,omitempty"` + AllowPublicKeyAuth bool `json:"allowPublicKeyAuthentication"` + Organization string `json:"organization,omitempty"` + Created string `json:"created,omitempty"` + Arch string `json:"arch,omitempty"` + SystemTimezone float64 `json:"systemTimeZone,omitempty"` + AllowSshPasswordAuthentication bool `json:"allowSshPasswordAuthentication"` + DisplayName string `json:"displayName"` + ModifySSHDConfig bool `json:"modifySSHDConfig"` + AllowMultiFactorAuthentication bool `json:"allowMultiFactorAuthentication"` + Hostname string `json:"hostname,omitempty"` + + ConnectionHistoryList []string `json:"connectionHistory,omitempty"` + SshdParams []jcapi.JCSSHDParam `json:"sshdParams,omitempty"` + NetworkInterfaces []jcapi.JCNetworkInterface `json:"networkInterfaces,omitempty"` + + // Derived by JCAPI + TagList []string `json:"tags,omitempty"` + Tags []jcapi.JCTag + + HardwareModel string `json:"hardware_model,omitempty"` + HardwareSerial string `json:"hardware_serial,omitempty"` + HardwareVendor string `json:"hardware_vendor,omitempty"` + HardwareVersion string `json:"hardware_version,omitempty"` +} + func SystemsTable() *schema.Table { return &schema.Table{ Name: "jumpcloud_systems", Resolver: fetchSystems, - Transform: transformers.TransformWithStruct(&jcapi.JCSystem{}, transformers.WithPrimaryKeys("Id")), + Transform: transformers.TransformWithStruct(&System{}, transformers.WithPrimaryKeys("Id")), } } @@ -26,12 +65,68 @@ func fetchSystems(ctx context.Context, meta schema.ClientMeta, parent *schema.Re c := meta.(*client.Client) systemsList, err := c.JumpCloud.GetSystems(false) if err != nil { - return fmt.Errorf("Could not read systems, err='%s'\n", err) + return fmt.Errorf("could not read systems, err='%s'", err) } - for _, system := range systemsList { + for _, jcSystem := range systemsList { + system := NewSystem(jcSystem) + system.getHardwareDetails(c.JumpCloudv2, c.JumpCloudv2Auth) res <- system } return nil } + +func NewSystem(jcSystem jcapi.JCSystem) System { + return System{ + Os: jcSystem.Os, + TemplateName: jcSystem.TemplateName, + AllowSshRootLogin: jcSystem.AllowSshRootLogin, + Id: jcSystem.Id, + LastContact: jcSystem.LastContact, + RemoteIP: jcSystem.RemoteIP, + Active: jcSystem.Active, + SshRootEnabled: jcSystem.SshRootEnabled, + AmazonInstanceID: jcSystem.AmazonInstanceID, + SshPassEnabled: jcSystem.SshPassEnabled, + Version: jcSystem.Version, + AgentVersion: jcSystem.AgentVersion, + AllowPublicKeyAuth: jcSystem.AllowPublicKeyAuth, + Organization: jcSystem.Organization, + Created: jcSystem.Created, + Arch: jcSystem.Arch, + SystemTimezone: jcSystem.SystemTimezone, + AllowSshPasswordAuthentication: jcSystem.AllowSshPasswordAuthentication, + DisplayName: jcSystem.DisplayName, + ModifySSHDConfig: jcSystem.ModifySSHDConfig, + AllowMultiFactorAuthentication: jcSystem.AllowMultiFactorAuthentication, + Hostname: jcSystem.Hostname, + } +} + +// getUsersBoundToSystemV2 returns the list of users associated with the given system +// for a Groups org using the /v2/systems//users endpoint: +func (system *System) getHardwareDetails(apiClientV2 *jcapiv2.APIClient, auth context.Context) (userIds []string, err error) { + var graphs []jcapiv2.GraphObjectWithPaths + for skip := 0; skip == 0 || len(graphs) == searchLimit; skip += searchSkipInterval { + // set up optional parameters: + optionals := map[string]interface{}{ + "limit": int32(searchLimit), + "skip": int32(skip), + "filter": []string{fmt.Sprintf("system_id:eq:%s", system.Id)}, + } + systemsInsightsInfo, _, err := apiClientV2.SystemInsightsApi.SysteminsightsListSystemInfo(auth, contentType, accept, optionals) + if err != nil { + fmt.Println(systemsInsightsInfo) + return userIds, fmt.Errorf("system %s, err='%s'", system.Id, err) + } + + for _, info := range systemsInsightsInfo { + system.HardwareModel = info.HardwareModel + system.HardwareSerial = info.HardwareSerial + system.HardwareVendor = info.HardwareVendor + system.HardwareVersion = info.HardwareVersion + } + } + return +} diff --git a/resources/systems_test.go b/resources/systems_test.go new file mode 100644 index 0000000..3922d8d --- /dev/null +++ b/resources/systems_test.go @@ -0,0 +1,65 @@ +// TODO: Pull in device specs ferda boyz via v2 api +// TODO: Clean up un-used columns (like passwords) +// TODO: Provide command execution history (like cloudtrail) + +package resources + +import ( + "context" + "os" + "reflect" + "testing" + + jcapiv2 "github.com/TheJumpCloud/jcapi-go/v2" + "github.com/cloudquery/plugin-pb-go/specs" + "github.com/cloudquery/plugin-sdk/v3/plugins/source" + "github.com/rs/zerolog" + "github.com/virtualbeck/cq-source-jumpcloud/client" +) + +func TestSystem_getHardwareDetails(t *testing.T) { + t.Skip() // Skip this until tests are mocked + type fields struct { + Id string + } + type args struct { + apiClientV2 *jcapiv2.APIClient + auth context.Context + } + tests := []struct { + name string + fields fields + wantUserIds []string + wantErr bool + }{ + { + name: "test", + fields: fields{ + Id: "TEST", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + ctx := context.Background() + meta, err := client.New(ctx, zerolog.New(os.Stdout), specs.Source{}, source.Options{}) + if err != nil { + t.Error(err) + } + c := meta.(*client.Client) + + system := &System{ + Id: tt.fields.Id, + } + gotUserIds, err := system.getHardwareDetails(c.JumpCloudv2, c.JumpCloudv2Auth) + if (err != nil) != tt.wantErr { + t.Errorf("System.getHardwareDetails() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(gotUserIds, tt.wantUserIds) { + t.Errorf("System.getHardwareDetails() = %v, want %v", gotUserIds, tt.wantUserIds) + } + }) + } +} diff --git a/resources/users_bound_to_system.go b/resources/users_bound_to_system.go new file mode 100644 index 0000000..071de81 --- /dev/null +++ b/resources/users_bound_to_system.go @@ -0,0 +1,144 @@ +package resources + +import ( + "context" + "fmt" + "log" + + "github.com/TheJumpCloud/jcapi" + jcapiv2 "github.com/TheJumpCloud/jcapi-go/v2" + "github.com/cloudquery/plugin-sdk/v3/schema" + "github.com/cloudquery/plugin-sdk/v3/transformers" + "github.com/virtualbeck/cq-source-jumpcloud/client" +) + +const ( + // the following constants are used for API v2 calls: + contentType = "application/json" + accept = "application/json" + searchLimit = 100 + searchSkipInterval = 100 +) + +type UsersBoundToSystem struct { + Id string `json:"_id"` + DisplayName string + Hostname string + Active bool + AmazonInstanceID string + OS string + Version string + AgentVersion string + Created string + LastContact string + UserName string + Email string +} + +func UsersBoundToSystemTable() *schema.Table { + return &schema.Table{ + Name: "jumpcloud_users_bound_to_system", + Resolver: fetchUsersBoundToSystem, + Transform: transformers.TransformWithStruct(&UsersBoundToSystem{}), + } +} + +func fetchUsersBoundToSystem(ctx context.Context, meta schema.ClientMeta, parent *schema.Resource, res chan<- any) error { + c := meta.(*client.Client) + + systemsList, err := c.JumpCloud.GetSystems(false) + if err != nil { + return fmt.Errorf("could not read systems, err='%s'", err) + } + + for _, system := range systemsList { + + outLine := []string{system.Id, system.DisplayName, system.Hostname, fmt.Sprintf("%t", system.Active), + system.AmazonInstanceID, system.Os, system.Version, system.AgentVersion, system.Created, + system.LastContact} + + var usersBoundToSystem = UsersBoundToSystem{ + Id: system.Id, + DisplayName: system.DisplayName, + Hostname: system.Hostname, + Active: system.Active, + AmazonInstanceID: system.AmazonInstanceID, + OS: system.Os, + Version: system.Version, + AgentVersion: system.AgentVersion, + Created: system.Created, + LastContact: system.LastContact, + } + + var userIds []string + + if c.IsGroups { + userIds, err = getUsersBoundToSystemV2(c.JumpCloudv2, c.JumpCloudv2Auth, system.Id) + } else { + userIds, err = getUsersBoundToSystemV1(c.JumpCloud, system.Id) + } + + if err != nil { + // if we fail to retrieve users for the current system, log a msg: + log.Printf("Failed to retrieve system user bindings: err='%s'\n", err) + // make sure we still write the system details before skipping: + res <- usersBoundToSystem + continue + } + + // get details for each bound user and append it to the current system: + for _, userId := range userIds { + user, err := c.JumpCloud.GetSystemUserById(userId, false) + if err != nil { + log.Printf("Could not retrieve system user for ID '%s', err='%s'\n", userId, err) + } else { + outLine = append(outLine, fmt.Sprintf("%s (%s)", user.UserName, user.Email)) + usersBoundToSystem.UserName = user.UserName + usersBoundToSystem.Email = user.Email + res <- usersBoundToSystem + } + } + + } + + return nil +} + +// getUsersBoundToSystemV1 returns the list of users associated with the given system +// for a Tags org using the /systems//users endpoint: +// This endpoint will return all the system-user bindings including those made +// via tags and via direct system-user binding +func getUsersBoundToSystemV1(apiClientV1 *jcapi.JCAPI, systemId string) (userIds []string, err error) { + + systemUserBindings, err := apiClientV1.GetSystemUserBindingsById(systemId) + if err != nil { + return userIds, fmt.Errorf("could not get system user bindings for system %s, err='%s'", systemId, err) + } + // add the retrieved user Ids to our userIds list: + for _, systemUserBinding := range systemUserBindings { + userIds = append(userIds, systemUserBinding.UserId) + } + return +} + +// getUsersBoundToSystemV2 returns the list of users associated with the given system +// for a Groups org using the /v2/systems//users endpoint: +func getUsersBoundToSystemV2(apiClientV2 *jcapiv2.APIClient, auth context.Context, systemId string) (userIds []string, err error) { + var graphs []jcapiv2.GraphObjectWithPaths + for skip := 0; skip == 0 || len(graphs) == searchLimit; skip += searchSkipInterval { + // set up optional parameters: + optionals := map[string]interface{}{ + "limit": int32(searchLimit), + "skip": int32(skip), + } + graphs, _, err := apiClientV2.SystemsApi.GraphSystemTraverseUser(auth, systemId, contentType, accept, optionals) + if err != nil { + return userIds, fmt.Errorf("could not retrieve users for system %s, err='%s'", systemId, err) + } + // add the retrieved user Ids to our userIds list: + for _, graph := range graphs { + userIds = append(userIds, graph.Id) + } + } + return +} diff --git a/testing/testing_config.yaml b/testing/testing_config.yaml index b6b483d..5f1b49b 100644 --- a/testing/testing_config.yaml +++ b/testing/testing_config.yaml @@ -16,6 +16,7 @@ spec: kind: destination spec: name: "file" + registry: github path: "cloudquery/file" write_mode: "append" # file only supports 'append' mode version: "v3.2.2"