Skip to content

Commit 02df7ad

Browse files
[BB-1588] - Add Wrap Errors With Grpc - JumpCloud Connector (#23)
* add wrap errors * refactor client layer to handle erros and pagination in a centralized way * use session storage as cache to get system users * fix client package location * address lint issues * fix inconsistent error management for list directories * address coderabbit comments * fix typos --------- Co-authored-by: Luisina Santos <luisina.santos@conductorone.com>
1 parent f0aab4c commit 02df7ad

File tree

1,355 files changed

+2195
-1889
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

1,355 files changed

+2195
-1889
lines changed

cmd/baton-jumpcloud/main.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,17 @@ import (
66
cfg "github.com/conductorone/baton-jumpcloud/pkg/config"
77
"github.com/conductorone/baton-jumpcloud/pkg/connector"
88
"github.com/conductorone/baton-sdk/pkg/config"
9+
"github.com/conductorone/baton-sdk/pkg/connectorrunner"
910
)
1011

1112
var version = "dev"
1213

1314
func main() {
1415
ctx := context.Background()
15-
config.RunConnector(ctx, "baton-jumpcloud", version, cfg.ConfigurationSchema, connector.NewLambdaConnector)
16+
config.RunConnector(ctx,
17+
"baton-jumpcloud",
18+
version,
19+
cfg.ConfigurationSchema,
20+
connector.NewLambdaConnector,
21+
connectorrunner.WithSessionStoreEnabled())
1622
}

go.sum

Lines changed: 0 additions & 72 deletions
Large diffs are not rendered by default.

pkg/client/client.go

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
package client
2+
3+
import (
4+
"context"
5+
6+
"github.com/conductorone/baton-jumpcloud/pkg/client/extension"
7+
"github.com/conductorone/baton-jumpcloud/pkg/client/jcapi1"
8+
"github.com/conductorone/baton-jumpcloud/pkg/client/jcapi2"
9+
"github.com/conductorone/baton-sdk/pkg/uhttp"
10+
)
11+
12+
type Client struct {
13+
_client1 *jcapi1.APIClient
14+
_client2 *jcapi2.APIClient
15+
extensionClient *uhttp.BaseHttpClient
16+
apiKey string
17+
orgId string
18+
}
19+
20+
func NewClient(ctx context.Context, apiKey string, orgId string) (*Client, error) {
21+
httpClient, err := uhttp.NewClient(ctx, uhttp.WithLogger(true, nil), uhttp.WithUserAgent("baton-jumpcloud/0.1.0"))
22+
if err != nil {
23+
return nil, err
24+
}
25+
26+
cc1 := jcapi1.NewConfiguration()
27+
cc1.HTTPClient = httpClient
28+
cc1.UserAgent = "baton-jumpcloud/0.1.0"
29+
30+
cc2 := jcapi2.NewConfiguration()
31+
cc2.HTTPClient = httpClient
32+
cc2.UserAgent = "baton-jumpcloud/0.1.0"
33+
34+
if orgId != "" {
35+
// optional, only needed by API keys linked to multi-tenant admins
36+
cc1.AddDefaultHeader("x-org-id", orgId)
37+
cc2.AddDefaultHeader("x-org-id", orgId)
38+
}
39+
40+
client1 := jcapi1.NewAPIClient(cc1)
41+
client2 := jcapi2.NewAPIClient(cc2)
42+
43+
baseHttpClient, err := uhttp.NewBaseHttpClientWithContext(ctx, httpClient)
44+
if err != nil {
45+
return nil, err
46+
}
47+
48+
return &Client{
49+
_client1: client1,
50+
_client2: client2,
51+
extensionClient: baseHttpClient,
52+
apiKey: apiKey,
53+
orgId: orgId,
54+
}, nil
55+
}
56+
57+
func (jc *Client) client1(ctx context.Context) (context.Context, *jcapi1.APIClient) {
58+
return context.WithValue(ctx, jcapi1.ContextAPIKeys, map[string]jcapi1.APIKey{
59+
"x-api-key": {
60+
Key: jc.apiKey,
61+
},
62+
}), jc._client1
63+
}
64+
65+
func (jc *Client) client2(ctx context.Context) (context.Context, *jcapi2.APIClient) {
66+
return context.WithValue(ctx, jcapi2.ContextAPIKeys, map[string]jcapi2.APIKey{
67+
"x-api-key": {
68+
Key: jc.apiKey,
69+
},
70+
}), jc._client2
71+
}
72+
73+
func (jc *Client) ListDirectories(ctx context.Context, opts *Options) ([]jcapi2.Directory, error) {
74+
ctx, client := jc.client2(ctx)
75+
76+
if opts == nil {
77+
opts = &Options{}
78+
}
79+
limit := opts.getLimit()
80+
page := opts.getPage()
81+
directories, resp, err := client.DirectoriesApi.DirectoriesList(ctx).Limit(limit).Skip(page).Execute()
82+
if err != nil {
83+
return nil, wrapSDKError(err, resp, "failed to list directories")
84+
}
85+
86+
defer resp.Body.Close()
87+
88+
return directories, nil
89+
}
90+
91+
func (jc *Client) GetUserByID(ctx context.Context, userID string) (*jcapi1.Userreturn, error) {
92+
userGetRequest := &extension.UserGetRequest{
93+
Client: jc.extensionClient,
94+
ApiKey: jc.apiKey,
95+
OrgId: jc.orgId,
96+
UserID: userID,
97+
}
98+
99+
user, resp, err := userGetRequest.Execute(ctx)
100+
if err != nil {
101+
return nil, wrapSDKError(err, resp, "failed to get user by ID")
102+
}
103+
defer resp.Body.Close()
104+
105+
return user, nil
106+
}
107+
108+
func (jc *Client) ListSystemUsers(ctx context.Context, opts *Options) ([]jcapi1.Systemuserreturn, string, error) {
109+
ctx, client := jc.client1(ctx)
110+
111+
if opts == nil {
112+
opts = &Options{}
113+
}
114+
limit := opts.getLimit()
115+
page := opts.getPage()
116+
systemUsers, resp, err := client.SystemusersApi.SystemusersList(ctx).Skip(page).Limit(limit).Execute()
117+
if err != nil {
118+
return nil, "", wrapSDKError(err, resp, "failed to list users")
119+
}
120+
defer resp.Body.Close()
121+
122+
pageToken := getNextPageToken(len(systemUsers.Results), page)
123+
124+
return systemUsers.Results, pageToken, nil
125+
}
126+
127+
func (jc *Client) ListAdminUsers(ctx context.Context, opts *Options) ([]jcapi1.Userreturn, string, error) {
128+
if opts == nil {
129+
opts = &Options{}
130+
}
131+
page := opts.getPage()
132+
133+
userListRequest := &extension.UserListRequest{
134+
Client: jc.extensionClient,
135+
ApiKey: jc.apiKey,
136+
OrgId: jc.orgId,
137+
}
138+
139+
users, resp, err := userListRequest.Skip(page).Execute(ctx)
140+
if err != nil {
141+
return nil, "", wrapSDKError(err, resp, "failed to list admin users")
142+
}
143+
defer resp.Body.Close()
144+
145+
pageToken := getNextPageToken(len(users), page)
146+
147+
return users, pageToken, nil
148+
}
149+
150+
func (jc *Client) GetSystemUserByID(ctx context.Context, userID string) (*jcapi1.Systemuserreturn, error) {
151+
ctx, client := jc.client1(ctx)
152+
153+
user, resp, err := client.SystemusersApi.SystemusersGet(ctx, userID).Execute()
154+
if err != nil {
155+
return nil, wrapSDKError(err, resp, "failed to fetch system user by ID")
156+
}
157+
defer resp.Body.Close()
158+
159+
return user, nil
160+
}
161+
162+
func (jc *Client) ListGroups(ctx context.Context, opts *Options) ([]jcapi2.UserGroup, string, error) {
163+
ctx, client := jc.client2(ctx)
164+
165+
if opts == nil {
166+
opts = &Options{}
167+
}
168+
limit := opts.getLimit()
169+
page := opts.getPage()
170+
groups, resp, err := client.UserGroupsApi.GroupsUserList(ctx).Skip(page).Limit(limit).Execute()
171+
if err != nil {
172+
return nil, "", wrapSDKError(err, resp, "failed to list groups")
173+
}
174+
defer resp.Body.Close()
175+
176+
pageToken := getNextPageToken(len(groups), page)
177+
return groups, pageToken, nil
178+
}
179+
180+
func (jc *Client) ListGroupMembers(ctx context.Context, groupID string, opts *Options) ([]jcapi2.GraphConnection, string, error) {
181+
ctx, client := jc.client2(ctx)
182+
183+
if opts == nil {
184+
opts = &Options{}
185+
}
186+
limit := opts.getLimit()
187+
page := opts.getPage()
188+
members, resp, err := client.UserGroupMembersMembershipApi.GraphUserGroupMembersList(ctx, groupID).Skip(page).Limit(limit).Execute()
189+
if err != nil {
190+
return nil, "", wrapSDKError(err, resp, "failed to list group members")
191+
}
192+
defer resp.Body.Close()
193+
194+
pageToken := getNextPageToken(len(members), page)
195+
return members, pageToken, nil
196+
}
197+
198+
func (jc *Client) AddGroupMember(ctx context.Context, groupID string, memberID string) error {
199+
ctx, client := jc.client2(ctx)
200+
201+
resp, err := client.UserGroupMembersMembershipApi.GraphUserGroupMembersPost(ctx, groupID).Body(jcapi2.GraphOperationUserGroupMember{
202+
Id: memberID,
203+
Op: "add",
204+
Type: "user",
205+
}).Execute()
206+
if err != nil {
207+
return wrapSDKError(err, resp, "failed to add group member")
208+
}
209+
defer resp.Body.Close()
210+
211+
return nil
212+
}
213+
214+
func (jc *Client) RemoveGroupMember(ctx context.Context, groupID string, memberID string) error {
215+
ctx, client := jc.client2(ctx)
216+
217+
resp, err := client.UserGroupMembersMembershipApi.GraphUserGroupMembersPost(ctx, groupID).Body(jcapi2.GraphOperationUserGroupMember{
218+
Id: memberID,
219+
Op: "remove",
220+
Type: "user",
221+
}).Execute()
222+
if err != nil {
223+
return wrapSDKError(err, resp, "failed to remove group member")
224+
}
225+
defer resp.Body.Close()
226+
227+
return nil
228+
}
229+
230+
func (jc *Client) ListApplications(ctx context.Context, opts *Options) ([]jcapi1.Application, string, error) {
231+
ctx, client := jc.client1(ctx)
232+
233+
if opts == nil {
234+
opts = &Options{}
235+
}
236+
limit := opts.getLimit()
237+
page := opts.getPage()
238+
applications, resp, err := client.ApplicationsApi.ApplicationsList(ctx).Skip(page).Limit(limit).Execute()
239+
if err != nil {
240+
return nil, "", wrapSDKError(err, resp, "failed to list applications")
241+
}
242+
defer resp.Body.Close()
243+
244+
pageToken := getNextPageToken(len(applications.Results), page)
245+
return applications.Results, pageToken, nil
246+
}
247+
248+
func (jc *Client) ListApplicationAssociations(ctx context.Context, applicationID string, opts *Options) ([]jcapi2.GraphConnection, string, error) {
249+
ctx, client := jc.client2(ctx)
250+
251+
if opts == nil {
252+
opts = &Options{}
253+
}
254+
limit := opts.getLimit()
255+
page := opts.getPage()
256+
targets := opts.getTargets()
257+
associations, resp, err := client.ApplicationsApi.GraphApplicationAssociationsList(ctx, applicationID).Skip(page).Limit(limit).Targets(targets).Execute()
258+
if err != nil {
259+
return nil, "", wrapSDKError(err, resp, "failed to list application associations")
260+
}
261+
defer resp.Body.Close()
262+
263+
pageToken := getNextPageToken(len(associations), page)
264+
return associations, pageToken, nil
265+
}

pkg/client/extension/extension.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package extension
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"net/url"
7+
"strconv"
8+
9+
"github.com/conductorone/baton-jumpcloud/pkg/client/jcapi1"
10+
"github.com/conductorone/baton-sdk/pkg/uhttp"
11+
)
12+
13+
type UserListRequest struct {
14+
Client *uhttp.BaseHttpClient
15+
ApiKey string
16+
OrgId string
17+
Page int32
18+
}
19+
20+
type listUserResponse struct {
21+
TotalCount int64 `json:"totalCount"`
22+
Results []jcapi1.Userreturn `json:"results"`
23+
}
24+
25+
func (ulr *UserListRequest) Skip(skip int32) *UserListRequest {
26+
ulr.Page = skip
27+
return ulr
28+
}
29+
30+
func (ulr *UserListRequest) Execute(ctx context.Context) ([]jcapi1.Userreturn, *http.Response, error) {
31+
// curl --request PUT \
32+
// --url https://console.jumpcloud.com/api/users/{id} \
33+
// --header 'content-type: application/json' \
34+
// --header 'x-api-key: REPLACE_KEY_VALUE' \
35+
// --header 'x-org-id: ' \
36+
// --data '{"email":"user@example.com","enableMultiFactor":true,"firstname":"string","growthData":{},"lastWhatsNewChecked":"2019-08-24","lastname":"string","roleName":"string"}'
37+
//
38+
// JumpCloud doesn't export the List endpoint in their
39+
// OpenAPI spec, but they do export the PUT...
40+
// .... so.. here we go!
41+
42+
qp := url.Values{}
43+
if ulr.Page != 0 {
44+
qp.Set("skip", strconv.FormatInt(int64(ulr.Page), 10))
45+
}
46+
47+
u := &url.URL{
48+
Scheme: "https",
49+
Host: "console.jumpcloud.com",
50+
Path: "/api/users",
51+
RawQuery: qp.Encode(),
52+
}
53+
54+
var reqOpts []uhttp.RequestOption
55+
reqOpts = append(reqOpts, uhttp.WithAcceptJSONHeader())
56+
reqOpts = append(reqOpts, uhttp.WithHeader("x-api-key", ulr.ApiKey))
57+
if ulr.OrgId != "" {
58+
reqOpts = append(reqOpts, uhttp.WithHeader("x-org-id", ulr.OrgId))
59+
}
60+
61+
req, err := ulr.Client.NewRequest(ctx, http.MethodGet, u, reqOpts...)
62+
if err != nil {
63+
return nil, nil, err
64+
}
65+
66+
rv := &listUserResponse{}
67+
resp, err := ulr.Client.Do(req, uhttp.WithJSONResponse(rv))
68+
if err != nil {
69+
return nil, resp, err
70+
}
71+
72+
return rv.Results, resp, nil
73+
}
74+
75+
type UserGetRequest struct {
76+
Client *uhttp.BaseHttpClient
77+
ApiKey string
78+
OrgId string
79+
UserID string
80+
}
81+
82+
// Execute fetches an admin user by ID.
83+
// Uses uhttp.Do() with WithJSONResponse for automatic JSON unmarshaling and error handling.
84+
func (ugr *UserGetRequest) Execute(ctx context.Context) (*jcapi1.Userreturn, *http.Response, error) {
85+
u := &url.URL{
86+
Scheme: "https",
87+
Host: "console.jumpcloud.com",
88+
Path: "/api/users/" + url.PathEscape(ugr.UserID),
89+
}
90+
91+
var reqOpts []uhttp.RequestOption
92+
reqOpts = append(reqOpts, uhttp.WithAcceptJSONHeader())
93+
reqOpts = append(reqOpts, uhttp.WithHeader("x-api-key", ugr.ApiKey))
94+
if ugr.OrgId != "" {
95+
reqOpts = append(reqOpts, uhttp.WithHeader("x-org-id", ugr.OrgId))
96+
}
97+
98+
req, err := ugr.Client.NewRequest(ctx, http.MethodGet, u, reqOpts...)
99+
if err != nil {
100+
return nil, nil, err
101+
}
102+
103+
rv := &jcapi1.Userreturn{}
104+
resp, err := ugr.Client.Do(req, uhttp.WithJSONResponse(rv))
105+
if err != nil {
106+
return nil, resp, err
107+
}
108+
109+
return rv, resp, nil
110+
}

0 commit comments

Comments
 (0)