diff --git a/pkg/connector/connector.go b/pkg/connector/connector.go index 0cfaf39e..0b69d7bb 100644 --- a/pkg/connector/connector.go +++ b/pkg/connector/connector.go @@ -21,6 +21,7 @@ func (d *MongoDB) ResourceSyncers(ctx context.Context) []connectorbuilder.Resour newUserBuilder(d.client), newTeamBuilder(d.client), newProjectBuilder(d.client), + newDatabaseUserBuilder(d.client), } } diff --git a/pkg/connector/database_users.go b/pkg/connector/database_users.go new file mode 100644 index 00000000..f7b573f6 --- /dev/null +++ b/pkg/connector/database_users.go @@ -0,0 +1,103 @@ +package connector + +import ( + "context" + + v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" + "github.com/conductorone/baton-sdk/pkg/annotations" + "github.com/conductorone/baton-sdk/pkg/pagination" + rs "github.com/conductorone/baton-sdk/pkg/types/resource" + "go.mongodb.org/atlas-sdk/v20231001002/admin" +) + +type databaseUserBuilder struct { + resourceType *v2.ResourceType + client *admin.APIClient +} + +func (o *databaseUserBuilder) ResourceType(ctx context.Context) *v2.ResourceType { + return databaseUserResourceType +} + +func newDatabaseUserResource(ctx context.Context, projectId *v2.ResourceId, user admin.CloudDatabaseUser) (*v2.Resource, error) { + profile := map[string]interface{}{ + "username": user.Username, + "login": user.Username, + "database_name": user.DatabaseName, + } + + userTraits := []rs.UserTraitOption{ + rs.WithUserProfile(profile), + rs.WithUserLogin(user.Username), + rs.WithStatus(v2.UserTrait_Status_STATUS_UNSPECIFIED), + } + + resource, err := rs.NewUserResource( + user.Username, + databaseUserResourceType, + user.Username, + userTraits, + rs.WithParentResourceID(projectId), + ) + if err != nil { + return nil, err + } + + return resource, nil +} + +func newDatabaseUserBuilder(client *admin.APIClient) *databaseUserBuilder { + return &databaseUserBuilder{ + resourceType: databaseUserResourceType, + client: client, + } +} + +// List returns all the users from the database as resource objects. +// Users include a UserTrait because they are the 'shape' of a standard user. +func (o *databaseUserBuilder) List(ctx context.Context, parentResourceID *v2.ResourceId, pToken *pagination.Token) ([]*v2.Resource, string, annotations.Annotations, error) { + if parentResourceID == nil { + return nil, "", nil, nil + } + + bag, page, err := parsePageToken(pToken.Token, &v2.ResourceId{ResourceType: o.resourceType.Id}) + if err != nil { + return nil, "", nil, err + } + + users, _, err := o.client.DatabaseUsersApi.ListDatabaseUsers(ctx, parentResourceID.Resource).IncludeCount(true).PageNum(page).ItemsPerPage(resourcePageSize).Execute() + if err != nil { + return nil, "", nil, wrapError(err, "failed to list database users") + } + + var resources []*v2.Resource + for _, user := range users.Results { + resource, err := newDatabaseUserResource(ctx, parentResourceID, user) + if err != nil { + return nil, "", nil, wrapError(err, "failed to create database user resource") + } + + resources = append(resources, resource) + } + + if isLastPage(*users.TotalCount, resourcePageSize) { + return resources, "", nil, nil + } + + nextPage, err := getPageTokenFromPage(bag, page+1) + if err != nil { + return nil, "", nil, err + } + + return resources, nextPage, nil, nil +} + +// Entitlements always returns an empty slice for users. +func (o *databaseUserBuilder) Entitlements(_ context.Context, resource *v2.Resource, _ *pagination.Token) ([]*v2.Entitlement, string, annotations.Annotations, error) { + return nil, "", nil, nil +} + +// Grants always returns an empty slice for users since they don't have any entitlements. +func (o *databaseUserBuilder) Grants(ctx context.Context, resource *v2.Resource, pToken *pagination.Token) ([]*v2.Grant, string, annotations.Annotations, error) { + return nil, "", nil, nil +} diff --git a/pkg/connector/projects.go b/pkg/connector/projects.go index 1d256545..a3d29339 100644 --- a/pkg/connector/projects.go +++ b/pkg/connector/projects.go @@ -40,6 +40,9 @@ func newProjectResource(ctx context.Context, organizationId *v2.ResourceId, proj projectId, projectTraits, rs.WithParentResourceID(organizationId), + rs.WithAnnotation( + &v2.ChildResourceType{ResourceTypeId: databaseUserResourceType.Id}, + ), ) if err != nil { return nil, err @@ -56,6 +59,10 @@ func newProjectBuilder(client *admin.APIClient) *projectBuilder { } func (p *projectBuilder) List(ctx context.Context, parentResourceID *v2.ResourceId, pToken *pagination.Token) ([]*v2.Resource, string, annotations.Annotations, error) { + if parentResourceID == nil { + return nil, "", nil, nil + } + bag, page, err := parsePageToken(pToken.Token, &v2.ResourceId{ResourceType: p.resourceType.Id}) if err != nil { return nil, "", nil, err @@ -101,39 +108,96 @@ func (p *projectBuilder) Entitlements(_ context.Context, resource *v2.Resource, entitlement := ent.NewAssignmentEntitlement(resource, memberEntitlement, assigmentOptions...) rv = append(rv, entitlement) + assigmentOptions = []ent.EntitlementOption{ + ent.WithGrantableTo(databaseUserResourceType), + ent.WithDescription(fmt.Sprintf("Member of %s team", resource.DisplayName)), + ent.WithDisplayName(fmt.Sprintf("%s team %s", resource.DisplayName, memberEntitlement)), + } + + entitlement = ent.NewAssignmentEntitlement(resource, memberEntitlement, assigmentOptions...) + rv = append(rv, entitlement) + return rv, "", nil, nil } // Grants always returns an empty slice for users since they don't have any entitlements. func (p *projectBuilder) Grants(ctx context.Context, resource *v2.Resource, pToken *pagination.Token) ([]*v2.Grant, string, annotations.Annotations, error) { - bag, page, err := parsePageToken(pToken.Token, &v2.ResourceId{ResourceType: p.resourceType.Id}) + bag, page, err := parsePageToken(pToken.Token, &v2.ResourceId{ResourceType: databaseUserResourceType.Id}, &v2.ResourceId{ResourceType: userResourceType.Id}) if err != nil { return nil, "", nil, err } + var rv []*v2.Grant + var count int + switch bag.Current().ResourceTypeID { + case databaseUserResourceType.Id: + grants, c, err := p.GrantDatabaseUsers(ctx, resource, page) + if err != nil { + return nil, "", nil, err + } + count = c + rv = append(rv, grants...) + case userResourceType.Id: + grants, c, err := p.GrantUsers(ctx, resource, page) + if err != nil { + return nil, "", nil, err + } + count = c + rv = append(rv, grants...) + } + + if isLastPage(count, resourcePageSize) { + nextPage, err := bag.NextToken("") + if err != nil { + return nil, "", nil, err + } + + // Process the next resource type. + return rv, nextPage, nil, nil + } + + nextPage, err := getPageTokenFromPage(bag, page+1) + if err != nil { + return nil, "", nil, err + } + + return rv, nextPage, nil, nil +} + +func (p *projectBuilder) GrantUsers(ctx context.Context, resource *v2.Resource, page int) ([]*v2.Grant, int, error) { members, _, err := p.client.ProjectsApi.ListProjectUsers(ctx, resource.Id.Resource).PageNum(page).ItemsPerPage(resourcePageSize).IncludeCount(true).Execute() if err != nil { - return nil, "", nil, wrapError(err, "failed to list team members") + return nil, 0, wrapError(err, "failed to list project users") } var rv []*v2.Grant for _, member := range members.Results { userResource, err := newUserResource(ctx, resource.ParentResourceId, member) if err != nil { - return nil, "", nil, wrapError(err, "failed to create user resource") + return nil, *members.TotalCount, wrapError(err, "failed to create user resource") } rv = append(rv, grant.NewGrant(resource, memberEntitlement, userResource.Id)) } - if isLastPage(*members.TotalCount, resourcePageSize) { - return rv, "", nil, nil - } + return rv, *members.TotalCount, nil +} - nextPage, err := getPageTokenFromPage(bag, page+1) +func (p *projectBuilder) GrantDatabaseUsers(ctx context.Context, resource *v2.Resource, page int) ([]*v2.Grant, int, error) { + members, _, err := p.client.DatabaseUsersApi.ListDatabaseUsers(ctx, resource.Id.Resource).PageNum(page).ItemsPerPage(resourcePageSize).IncludeCount(true).Execute() if err != nil { - return nil, "", nil, err + return nil, 0, wrapError(err, "failed to list project database users") + } + + var rv []*v2.Grant + for _, member := range members.Results { + userResource, err := newDatabaseUserResource(ctx, resource.ParentResourceId, member) + if err != nil { + return nil, *members.TotalCount, wrapError(err, "failed to create database user resource") + } + + rv = append(rv, grant.NewGrant(resource, memberEntitlement, userResource.Id)) } - return nil, nextPage, nil, nil + return rv, *members.TotalCount, nil } diff --git a/pkg/connector/resource_types.go b/pkg/connector/resource_types.go index 5609a452..33a4f546 100644 --- a/pkg/connector/resource_types.go +++ b/pkg/connector/resource_types.go @@ -30,4 +30,11 @@ var ( Description: "A MongoDB Atlas Project", Traits: []v2.ResourceType_Trait{v2.ResourceType_TRAIT_GROUP}, } + databaseUserResourceType = &v2.ResourceType{ + Id: "database_user", + DisplayName: "Database User", + Description: "A MongoDB Atlas Database User", + Traits: []v2.ResourceType_Trait{v2.ResourceType_TRAIT_USER}, + Annotations: getSkippEntitlementsAndGrantsAnnotations(), + } )