Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,10 @@
cq-source-jumpcloud

dist
*.log
*.log
.vscode
.envrc
.cq
data
cloudquery
__debug*
85 changes: 81 additions & 4 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -32,17 +47,79 @@ 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 != "" {
return envVar
}
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
}
3 changes: 2 additions & 1 deletion docs/tables/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
## Tables

- [jumpcloud_systems](jumpcloud_systems.md)
- [jumpcloud_users](jumpcloud_users.md)
- [jumpcloud_users](jumpcloud_users.md)
- [jumpcloud_users_bound_to_system](jumpcloud_users_bound_to_system.md)
6 changes: 5 additions & 1 deletion docs/tables/jumpcloud_systems.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,8 @@ The primary key for this table is **_id**.
|connection_history|`list<item: utf8, nullable>`|
|sshd_params|`json`|
|network_interfaces|`json`|
|tags|`list<item: utf8, nullable>`|
|tags|`list<item: utf8, nullable>`|
|hardware_model|`utf8`|
|hardware_serial|`utf8`|
|hardware_vendor|`utf8`|
|hardware_version|`utf8`|
26 changes: 26 additions & 0 deletions docs/tables/jumpcloud_users_bound_to_system.md
Original file line number Diff line number Diff line change
@@ -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`|
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down
1 change: 1 addition & 0 deletions plugin/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ func Plugin() *source.Plugin {
schema.Tables{
resources.UsersTable(),
resources.SystemsTable(),
resources.UsersBoundToSystemTable(),
},
client.New,
)
Expand Down
101 changes: 98 additions & 3 deletions resources/systems.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,124 @@ 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")),
}
}

func fetchSystems(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'\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/<system_id>/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
}
Loading