package connector
import (
"context"
"fmt"
"example/baton-junction/pkg/client"
config "github.com/conductorone/baton-sdk/pb/c1/config/v1"
v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2"
"github.com/conductorone/baton-sdk/pkg/actions"
"github.com/conductorone/baton-sdk/pkg/annotations"
"github.com/conductorone/baton-sdk/pkg/connectorbuilder"
"github.com/conductorone/baton-sdk/pkg/types/resource"
"github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap"
"go.uber.org/zap"
)
type userBuilder struct {
client *client.Client
}
func newUserBuilder(c *client.Client) *userBuilder {
return &userBuilder{client: c}
}
func (o *userBuilder) ResourceType(_ context.Context) *v2.ResourceType {
return userResourceType
}
func (o *userBuilder) List(
ctx context.Context,
_ *v2.ResourceId,
attrs resource.SyncOpAttrs,
) ([]*v2.Resource, *resource.SyncOpResults, error) {
l := ctxzap.Extract(ctx)
bag, cursor, err := parsePageToken(attrs.PageToken.Token, &v2.ResourceId{ResourceType: userResourceType.Id})
if err != nil {
return nil, nil, err
}
users, nextCursor, err := o.client.GetUsers(ctx, cursor)
if err != nil {
return nil, nil, fmt.Errorf("baton-junction: failed to list users: %w", err)
}
l.Debug("listed users", zap.Int("count", len(users)), zap.String("next_cursor", nextCursor))
var resources []*v2.Resource
for _, u := range users {
r, err := userResource(&u)
if err != nil {
return nil, nil, err
}
resources = append(resources, r)
}
if err := bag.Next(nextCursor); err != nil {
return nil, nil, fmt.Errorf("baton-junction: failed to advance pagination: %w", err)
}
nextPageToken, err := bag.Marshal()
if err != nil {
return nil, nil, fmt.Errorf("baton-junction: failed to marshal pagination bag: %w", err)
}
return resources, &resource.SyncOpResults{NextPageToken: nextPageToken}, nil
}
func (o *userBuilder) Entitlements(_ context.Context, _ *v2.Resource, _ resource.SyncOpAttrs) ([]*v2.Entitlement, *resource.SyncOpResults, error) {
return nil, nil, nil
}
func (o *userBuilder) Grants(_ context.Context, _ *v2.Resource, _ resource.SyncOpAttrs) ([]*v2.Grant, *resource.SyncOpResults, error) {
return nil, nil, nil
}
func (o *userBuilder) Get(
ctx context.Context,
resourceId *v2.ResourceId,
_ *v2.ResourceId,
) (*v2.Resource, annotations.Annotations, error) {
l := ctxzap.Extract(ctx)
user, err := o.client.GetUser(ctx, resourceId.Resource)
if err != nil {
return nil, nil, fmt.Errorf("baton-junction: failed to get user %s: %w", resourceId.Resource, err)
}
l.Debug("fetched user", zap.String("user_id", resourceId.Resource))
r, err := userResource(user)
if err != nil {
return nil, nil, err
}
return r, nil, nil
}
func (o *userBuilder) CreateAccountCapabilityDetails(
_ context.Context,
) (*v2.CredentialDetailsAccountProvisioning, annotations.Annotations, error) {
return v2.CredentialDetailsAccountProvisioning_builder{
SupportedCredentialOptions: []v2.CapabilityDetailCredentialOption{
v2.CapabilityDetailCredentialOption_CAPABILITY_DETAIL_CREDENTIAL_OPTION_NO_PASSWORD,
},
PreferredCredentialOption: v2.CapabilityDetailCredentialOption_CAPABILITY_DETAIL_CREDENTIAL_OPTION_NO_PASSWORD,
}.Build(), nil, nil
}
func (o *userBuilder) CreateAccount(
ctx context.Context,
accountInfo *v2.AccountInfo,
_ *v2.LocalCredentialOptions,
) (connectorbuilder.CreateAccountResponse, []*v2.PlaintextData, annotations.Annotations, error) {
l := ctxzap.Extract(ctx)
login := accountInfo.GetLogin()
emails := accountInfo.GetEmails()
var email string
if len(emails) > 0 {
email = emails[0].GetAddress()
}
req := &client.CreateUserRequest{
Email: email,
FirstName: accountInfo.GetProfile().GetFields()["first_name"].GetStringValue(),
LastName: accountInfo.GetProfile().GetFields()["last_name"].GetStringValue(),
Username: login,
}
user, err := o.client.CreateUser(ctx, req)
if err != nil {
return nil, nil, nil, fmt.Errorf("baton-junction: failed to create account: %w", err)
}
l.Info("created user account",
zap.String("user_id", user.ID),
zap.String("email", user.Email),
)
result := v2.CreateAccountResponse_SuccessResult_builder{
IsCreateAccountResult: true,
}.Build()
return result, nil, nil, nil
}
func (o *userBuilder) ResourceActions(ctx context.Context, registry actions.ActionRegistry) error {
updateProfileSchema := &v2.BatonActionSchema{
Name: "update_profile",
DisplayName: "Update User Profile",
Description: "Update a user's profile attributes",
ActionType: []v2.ActionType{v2.ActionType_ACTION_TYPE_ACCOUNT_UPDATE_PROFILE},
Arguments: []*config.Field{
{
Name: "user_id",
DisplayName: "User",
Description: "The user to update",
IsRequired: true,
Field: &config.Field_ResourceIdField{
ResourceIdField: &config.ResourceIdField{
Rules: &config.ResourceIDRules{
AllowedResourceTypeIds: []string{"user"},
},
},
},
},
{Name: "first_name", DisplayName: "First Name", Description: "The user's first name", Field: &config.Field_StringField{}},
{Name: "last_name", DisplayName: "Last Name", Description: "The user's last name", Field: &config.Field_StringField{}},
{Name: "email", DisplayName: "Email", Description: "The user's email address", Field: &config.Field_StringField{}},
{Name: "department", DisplayName: "Department", Description: "The user's department", Field: &config.Field_StringField{}},
},
ReturnTypes: []*config.Field{
{Name: "success", DisplayName: "Success", Field: &config.Field_BoolField{}},
{Name: "user_id", DisplayName: "User ID", Field: &config.Field_StringField{}},
{Name: "first_name", DisplayName: "First Name", Field: &config.Field_StringField{}},
{Name: "last_name", DisplayName: "Last Name", Field: &config.Field_StringField{}},
{Name: "email", DisplayName: "Email", Field: &config.Field_StringField{}},
{Name: "department", DisplayName: "Department", Field: &config.Field_StringField{}},
},
}
return registry.Register(ctx, updateProfileSchema, o.updateUserProfile)
}
func userResource(u *client.User) (*v2.Resource, error) {
profile := map[string]interface{}{
"first_name": u.FirstName,
"last_name": u.LastName,
}
if u.Department != "" {
profile["department"] = u.Department
}
opts := []resource.UserTraitOption{
resource.WithEmail(u.Email, true),
resource.WithUserLogin(u.Username),
resource.WithStatus(mapUserStatus(u.Status)),
resource.WithUserProfile(profile),
}
if u.CreatedAt.Unix() > 0 {
opts = append(opts, resource.WithCreatedAt(u.CreatedAt))
}
if u.LastLogin != nil {
opts = append(opts, resource.WithLastLogin(*u.LastLogin))
}
r, err := resource.NewUserResource(
displayName(u.FirstName, u.LastName, u.Email, u.ID),
userResourceType,
u.ID,
opts,
resource.WithAnnotation(&v2.RawId{Id: u.ID}),
)
if err != nil {
return nil, fmt.Errorf("baton-junction: failed to build user resource %s: %w", u.ID, err)
}
return r, nil
}