Skip to content

Commit 9117fba

Browse files
authored
Merge pull request #12 from ConductorOne/jirwin/better-jc-admins
Opportunistically use system users to represent admins
2 parents fe58ec9 + bfe1e41 commit 9117fba

File tree

6 files changed

+169
-24
lines changed

6 files changed

+169
-24
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: Output connector capabilities
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
8+
jobs:
9+
calculate-capabilities:
10+
runs-on: ubuntu-latest
11+
12+
steps:
13+
- name: Checkout code
14+
uses: actions/checkout@v4
15+
with:
16+
token: ${{ secrets.RELENG_GITHUB_TOKEN }}
17+
18+
- name: Setup Go
19+
uses: actions/setup-go@v4
20+
with:
21+
go-version-file: 'go.mod'
22+
23+
- name: Build
24+
run: go build -o connector ./cmd/baton-jumpcloud
25+
26+
- name: Run and save output
27+
run: ./connector capabilities > baton_capabilities.json
28+
29+
- name: Commit changes
30+
uses: EndBug/add-and-commit@v9
31+
with:
32+
default_author: github_actions
33+
message: 'Updating baton capabilities.'
34+
add: 'baton_capabilities.json'

pkg/connector/apps.go

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,10 @@ type graphRequest interface {
135135
Execute() ([]jcapi2.GraphConnection, *http.Response, error)
136136
}
137137

138+
type appAdminPrincipal interface {
139+
GetId() string
140+
}
141+
138142
func (o *appResourceType) adminGrants(ctx context.Context, resource *v2.Resource, pt *pagination.Token) ([]*v2.Grant, string, annotations.Annotations, error) {
139143
skip, b, err := unmarshalSkipToken(pt)
140144
if err != nil {
@@ -152,16 +156,24 @@ func (o *appResourceType) adminGrants(ctx context.Context, resource *v2.Resource
152156
ctx, client := o.client1(ctx)
153157

154158
var rv []*v2.Grant
155-
for _, u := range users {
156-
user, err := fetchUserByEmail(ctx, client, u.GetEmail())
157-
if err != nil {
159+
for i := range users {
160+
adminUser := &users[i]
161+
var adminPrincipal appAdminPrincipal = adminUser
162+
163+
// If the user is a system user, we need to fetch the user by email to get the ID
164+
systemUser, err := fetchUserByEmail(ctx, client, adminUser.GetEmail())
165+
if err != nil && !errors.Is(err, errUserNotFoundForEmail) {
158166
return nil, "", nil, err
159167
}
168+
if systemUser != nil {
169+
adminPrincipal = systemUser
170+
}
160171

161-
ur := &v2.Resource{Id: &v2.ResourceId{
162-
ResourceType: resourceTypeUser.Id,
163-
Resource: user.GetId(),
164-
},
172+
ur := &v2.Resource{
173+
Id: &v2.ResourceId{
174+
ResourceType: resourceTypeUser.Id,
175+
Resource: adminPrincipal.GetId(),
176+
},
165177
}
166178

167179
rv = append(rv, &v2.Grant{

pkg/connector/connector.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ func (s *Jumpcloud) ResourceSyncers(ctx context.Context) []connectorbuilder.Reso
128128
// NOTE: Jumpcloud has 'two' types of Users, "admin users" and... uh, "normal users".
129129
// So, we put each in their own resource type.
130130
// https://support.jumpcloud.com/support/s/article/getting-started-jumpcloud-admin-accounts-vs-user-accounts-2019-08-21-10-36-47
131-
newUserBuilder(s.client1, s.client2),
131+
newUserBuilder(s.client1, s.client2, s.ext),
132132
newGroupBuilder(s.client1, s.client2),
133133
newRoleBuilder(s.client1, s.ext),
134134
newAppBuilder(s.client1, s.client2, s.ext),

pkg/connector/pagination.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ func unmarshalSkipToken(token *pagination.Token) (int32, *pagination.Bag, error)
2626

2727
func marshalSkipToken(newObjects int, lastSkip int32, b *pagination.Bag) (string, error) {
2828
if newObjects == 0 {
29-
return "", nil
29+
return nextToken(b, "")
3030
}
3131
nextSkip := int64(newObjects) + int64(lastSkip)
3232
pageToken, err := nextToken(b, strconv.FormatInt(nextSkip, 10))

pkg/connector/role.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package connector
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
67
"regexp"
78
"strings"
@@ -119,6 +120,10 @@ func (o *roleResourceType) cacheAllUsers(ctx context.Context) ([]jcapi1.Userretu
119120
return rv, nil
120121
}
121122

123+
type rolePrincipal interface {
124+
GetId() string
125+
}
126+
122127
func (o *roleResourceType) Grants(
123128
ctx context.Context,
124129
resource *v2.Resource,
@@ -139,17 +144,23 @@ func (o *roleResourceType) Grants(
139144
continue
140145
}
141146

147+
var principal rolePrincipal = adminUser
148+
142149
user, err := fetchUserByEmail(ctx, client, adminUser.GetEmail())
143-
if err != nil {
150+
if err != nil && !errors.Is(err, errUserNotFoundForEmail) {
144151
return nil, "", nil, err
145152
}
146153

147-
rv = append(rv, roleGrant(resource, resourceTypeUser.Id, user))
154+
if user != nil {
155+
principal = user
156+
}
157+
158+
rv = append(rv, roleGrant(resource, resourceTypeUser.Id, principal))
148159
}
149160
return rv, "", nil, nil
150161
}
151162

152-
func roleGrant(resource *v2.Resource, resourceTypeID string, user *jcapi1.Systemuserreturn) *v2.Grant {
163+
func roleGrant(resource *v2.Resource, resourceTypeID string, user rolePrincipal) *v2.Grant {
153164
roleID := resource.Id.GetResource()
154165
ur := &v2.Resource{Id: &v2.ResourceId{ResourceType: resourceTypeID, Resource: user.GetId()}}
155166

pkg/connector/users.go

Lines changed: 100 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ package connector
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
67
"strings"
78

89
"github.com/conductorone/baton-jumpcloud/pkg/jcapi1"
910
v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2"
1011
"github.com/conductorone/baton-sdk/pkg/annotations"
1112
"github.com/conductorone/baton-sdk/pkg/pagination"
13+
sdkResources "github.com/conductorone/baton-sdk/pkg/types/resource"
1214
"github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap"
1315
"go.uber.org/zap"
1416
"google.golang.org/protobuf/types/known/structpb"
@@ -19,18 +21,20 @@ type userResourceType struct {
1921
client1 jc1Func
2022
client2 jc2Func
2123
managers map[string]*jcapi1.Systemuserreturn
24+
ext *ExtensionClient
2225
}
2326

2427
func (o *userResourceType) ResourceType(_ context.Context) *v2.ResourceType {
2528
return o.resourceType
2629
}
2730

28-
func newUserBuilder(jc1 jc1Func, jc2 jc2Func) *userResourceType {
31+
func newUserBuilder(jc1 jc1Func, jc2 jc2Func, ext *ExtensionClient) *userResourceType {
2932
return &userResourceType{
3033
resourceType: resourceTypeUser,
3134
client1: jc1,
3235
client2: jc2,
3336
managers: make(map[string]*jcapi1.Systemuserreturn),
37+
ext: ext,
3438
}
3539
}
3640

@@ -43,34 +47,118 @@ func (o *userResourceType) Grants(_ context.Context, _ *v2.Resource, _ *paginati
4347
}
4448

4549
func (o *userResourceType) List(ctx context.Context, parentResourceID *v2.ResourceId, pt *pagination.Token) ([]*v2.Resource, string, annotations.Annotations, error) {
50+
l := ctxzap.Extract(ctx)
51+
4652
ctx, client := o.client1(ctx)
4753

4854
skip, b, err := unmarshalSkipToken(pt)
4955
if err != nil {
5056
return nil, "", nil, err
5157
}
5258

53-
list, resp, err := client.SystemusersApi.SystemusersList(ctx).Skip(skip).Execute()
54-
if err != nil {
55-
return nil, "", nil, err
59+
if b.Current() == nil {
60+
// Push onto stack in reverse
61+
b.Push(pagination.PageState{
62+
ResourceTypeID: "list-admin-users",
63+
})
64+
b.Push(pagination.PageState{
65+
ResourceTypeID: "list-users",
66+
})
5667
}
57-
defer resp.Body.Close()
58-
5968
var rv []*v2.Resource
60-
for i := range list.Results {
61-
ur, err := o.userResource(ctx, &list.Results[i])
69+
var pageToken string
70+
switch b.Current().ResourceTypeID {
71+
case "list-users":
72+
list, resp, err := client.SystemusersApi.SystemusersList(ctx).Skip(skip).Execute()
73+
if err != nil {
74+
return nil, "", nil, err
75+
}
76+
defer resp.Body.Close()
77+
78+
for i := range list.Results {
79+
ur, err := o.userResource(ctx, &list.Results[i])
80+
if err != nil {
81+
return nil, "", nil, err
82+
}
83+
rv = append(rv, ur)
84+
}
85+
pageToken, err = marshalSkipToken(len(list.Results), skip, b)
86+
if err != nil {
87+
return nil, "", nil, err
88+
}
89+
case "list-admin-users":
90+
adminUsers, resp, err := o.ext.UserList().Skip(skip).Execute(ctx)
6291
if err != nil {
6392
return nil, "", nil, err
6493
}
65-
rv = append(rv, ur)
94+
defer resp.Body.Close()
95+
96+
for i := range adminUsers {
97+
adminEmail := adminUsers[i].GetEmail()
98+
adminUser, err := o.adminUserResource(ctx, &adminUsers[i])
99+
if err != nil {
100+
return nil, "", nil, err
101+
}
102+
103+
// Check if the admin user is also a system user, if so we'll use that user instead
104+
systemUser, err := fetchUserByEmail(ctx, client, adminEmail)
105+
if err != nil && !errors.Is(err, errUserNotFoundForEmail) {
106+
return nil, "", nil, err
107+
}
108+
109+
if systemUser != nil {
110+
continue
111+
}
112+
113+
l.Debug("admin user not found as system user, creating", zap.String("email", adminEmail))
114+
rv = append(rv, adminUser)
115+
}
116+
pageToken, err = marshalSkipToken(len(adminUsers), skip, b)
117+
if err != nil {
118+
return nil, "", nil, err
119+
}
120+
default:
121+
return nil, "", nil, fmt.Errorf("baton-jumpcloud: unknown page state: %s", b.Current().ResourceTypeID)
122+
}
123+
124+
return rv, pageToken, nil, nil
125+
}
126+
127+
func (o *userResourceType) adminUserResource(ctx context.Context, user *jcapi1.Userreturn) (*v2.Resource, error) {
128+
profile := map[string]interface{}{
129+
"id": user.GetId(),
130+
}
131+
132+
if user.HasOrganization() {
133+
profile["organization"] = user.GetOrganization()
134+
}
135+
136+
userTraitOps := []sdkResources.UserTraitOption{
137+
sdkResources.WithUserProfile(profile),
66138
}
67139

68-
pageToken, err := marshalSkipToken(len(list.Results), skip, b)
140+
status := v2.UserTrait_Status_STATUS_ENABLED
141+
if user.GetSuspended() {
142+
status = v2.UserTrait_Status_STATUS_DISABLED
143+
}
144+
userTraitOps = append(userTraitOps, sdkResources.WithStatus(status))
145+
146+
email := user.GetEmail()
147+
if email != "" {
148+
userTraitOps = append(userTraitOps, sdkResources.WithEmail(email, true))
149+
}
150+
151+
r, err := sdkResources.NewUserResource(
152+
fmt.Sprintf("%s %s", user.GetFirstname(), user.GetLastname()),
153+
o.resourceType,
154+
user.GetId(),
155+
userTraitOps,
156+
)
69157
if err != nil {
70-
return nil, "", nil, err
158+
return nil, err
71159
}
72160

73-
return rv, pageToken, nil, nil
161+
return r, nil
74162
}
75163

76164
func (o *userResourceType) userResource(ctx context.Context, user *jcapi1.Systemuserreturn) (*v2.Resource, error) {

0 commit comments

Comments
 (0)