Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ref: Use Rate Limits #10

Merged
merged 13 commits into from
Aug 6, 2024
464 changes: 278 additions & 186 deletions pkg/connector/client/confluence.go

Large diffs are not rendered by default.

56 changes: 44 additions & 12 deletions pkg/connector/client/models.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,44 @@
package client

type ConfluenceLink struct {
Base string `json:"base"`
Next string `json:"next,omitempty"`
}

type ConfluenceUser struct {
AccountId string
AccountType string
DisplayName string
Email string
AccountId string `json:"accountId"`
AccountType string `json:"accountType"`
DisplayName string `json:"displayName"`
Email string `json:"email,omitempty"`
Operations []ConfluenceOperation `json:"operations,omitempty"`
}

type ConfluenceOperation struct {
Operation string `json:"operation"`
TargetType string `json:"targetType"`
}

type confluenceUserList struct {
Start int
Limit int
Size int
Results []ConfluenceUser
Start int `json:"start"`
Limit int `json:"limit"`
Size int `json:"size"`
Links ConfluenceLink `json:"_links"`
Results []ConfluenceUser `json:"results"`
}

type ConfluenceSearch struct {
EntityType string `json:"entityType"`
Score float64 `json:"score"`
Title string `json:"title"`
User ConfluenceUser `json:"user"`
}

type ConfluenceSearchList struct {
Start int `json:"start"`
Limit int `json:"limit"`
TotalSize int `json:"totalSize"`
Size int `json:"size"`
Results []ConfluenceSearch `json:"results"`
}

type ConfluenceGroup struct {
Expand All @@ -21,8 +48,13 @@ type ConfluenceGroup struct {
}

type confluenceGroupList struct {
Start int
Limit int
Size int
Results []ConfluenceGroup
Start int `json:"start"`
Limit int `json:"limit"`
Size int `json:"size"`
Links ConfluenceLink `json:"_links"`
Results []ConfluenceGroup `json:"results"`
}

type AddUserToGroupRequestBody struct {
AccountId string `json:"accountId"`
}
92 changes: 92 additions & 0 deletions pkg/connector/client/path.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package client

import (
"fmt"
"net/url"
"strconv"
)

const (
CurrentUserUrlPath = "/wiki/rest/api/user/current"
GroupsListUrlPath = "/wiki/rest/api/group"
getUsersByGroupIdUrlPath = "/wiki/rest/api/group/%s/membersByGroupId"
groupBaseUrlPath = "/wiki/rest/api/group/userByGroupId"
spacePermissionsCreateUrlPath = "/wiki/rest/api/space/%s/permissions"
spacePermissionsUpdateUrlPath = "/wiki/rest/api/space/%s/permissions/%s"
SpacesListUrlPath = "/wiki/api/v2/spaces"
spacesGetUrlPath = "/wiki/api/v2/spaces/%s"
SpacePermissionsListUrlPath = "/wiki/api/v2/spaces/%s/permissions"
SearchUrlPath = "/wiki/rest/api/search/user"
addUsersToGroupUrlPath = "/wiki/rest/api/group/userByGroupId?groupId=%s"
removeUsersFromGroupUrlPath = "/wiki/rest/api/group/userByGroupId?groupId=%s&accountId=%s"
)

type Option = func(*url.URL) (*url.URL, error)

func withQueryParameters(parameters map[string]interface{}) Option {
return func(url *url.URL) (*url.URL, error) {
query := url.Query()
for key, interfaceValue := range parameters {
var stringValue string
switch actualValue := interfaceValue.(type) {
case string:
stringValue = actualValue
case int:
stringValue = strconv.Itoa(actualValue)
case bool:
if actualValue {
stringValue = "1"
} else {
stringValue = "0"
}
default:
return nil, fmt.Errorf("invalid query parameter type %s", actualValue)
}
query.Set(key, stringValue)
}
url.RawQuery = query.Encode()
return url, nil
}
}

// withLimitAndOffset adds `start` and `limit` query parameters to a URL. This
// pagination parameter is only used by the v1 REST API.
func withLimitAndOffset(pageToken string, pageSize int) Option {
return withQueryParameters(map[string]interface{}{
"limit": pageSize,
"start": pageToken,
})
}

// WithPaginationCursor uses Confluence Cloud's REST API v2 pagination scheme.
func WithPaginationCursor(
pageSize int,
paginationCursor string,
) Option {
parameters := map[string]interface{}{
"limit": pageSize,
}
if paginationCursor != "" {
parameters["cursor"] = paginationCursor
}

return withQueryParameters(parameters)
}

func (c *ConfluenceClient) parse(
path string,
options ...Option,
) (*url.URL, error) {
parsed, err := url.Parse(path)
if err != nil {
return nil, fmt.Errorf("failed to parse request path '%s': %w", path, err)
}
parsedUrl := c.apiBase.ResolveReference(parsed)
for _, option := range options {
parsedUrl, err = option(parsedUrl)
if err != nil {
return nil, err
}
}
return parsedUrl, nil
}
1 change: 1 addition & 0 deletions pkg/connector/connector.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

const (
accountTypeAtlassian = "atlassian" // user account type
accountTypeApp = "app" // bot account type
)

var (
Expand Down
113 changes: 85 additions & 28 deletions pkg/connector/group.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,9 @@ import (
v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2"
"github.com/conductorone/baton-sdk/pkg/annotations"
"github.com/conductorone/baton-sdk/pkg/pagination"
ent "github.com/conductorone/baton-sdk/pkg/types/entitlement"
grant "github.com/conductorone/baton-sdk/pkg/types/grant"
res "github.com/conductorone/baton-sdk/pkg/types/resource"
"github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap"
"go.uber.org/zap"
"github.com/conductorone/baton-sdk/pkg/types/entitlement"
"github.com/conductorone/baton-sdk/pkg/types/grant"
"github.com/conductorone/baton-sdk/pkg/types/resource"
)

const (
Expand All @@ -35,9 +33,9 @@ func groupResource(ctx context.Context, group *client.ConfluenceGroup) (*v2.Reso
"group_type": group.Type,
}

groupTraitOptions := []res.GroupTraitOption{res.WithGroupProfile(profile)}
groupTraitOptions := []resource.GroupTraitOption{resource.WithGroupProfile(profile)}

resource, err := res.NewGroupResource(
newGroupResource, err := resource.NewGroupResource(
group.Name,
resourceTypeGroup,
group.Id,
Expand All @@ -47,10 +45,19 @@ func groupResource(ctx context.Context, group *client.ConfluenceGroup) (*v2.Reso
return nil, err
}

return resource, nil
return newGroupResource, nil
}

func (o *groupResourceType) List(ctx context.Context, resourceId *v2.ResourceId, pt *pagination.Token) ([]*v2.Resource, string, annotations.Annotations, error) {
func (o *groupResourceType) List(
ctx context.Context,
resourceId *v2.ResourceId,
pt *pagination.Token,
) (
[]*v2.Resource,
string,
annotations.Annotations,
error,
) {
bag := &pagination.Bag{}
err := bag.Unmarshal(pt.Token)
if err != nil {
Expand All @@ -61,9 +68,10 @@ func (o *groupResourceType) List(ctx context.Context, resourceId *v2.ResourceId,
ResourceTypeID: resourceTypeGroup.Id,
})
}
groups, token, err := o.client.GetGroups(ctx, bag.PageToken(), ResourcesPageSize)
groups, token, ratelimitData, err := o.client.GetGroups(ctx, bag.PageToken(), ResourcesPageSize)
outputAnnotations := WithRateLimitAnnotations(ratelimitData)
if err != nil {
return nil, "", nil, err
return nil, "", outputAnnotations, err
}

rv := make([]*v2.Resource, 0, len(groups))
Expand All @@ -72,30 +80,39 @@ func (o *groupResourceType) List(ctx context.Context, resourceId *v2.ResourceId,

gr, err := groupResource(ctx, &groupCopy)
if err != nil {
return nil, "", nil, err
return nil, "", outputAnnotations, err
}

rv = append(rv, gr)
}

nextPage, err := bag.NextToken(token)
if err != nil {
return nil, "", nil, err
return nil, "", outputAnnotations, err
}

return rv, nextPage, nil, nil
return rv, nextPage, outputAnnotations, nil
}

func (o *groupResourceType) Entitlements(ctx context.Context, resource *v2.Resource, _ *pagination.Token) ([]*v2.Entitlement, string, annotations.Annotations, error) {
func (o *groupResourceType) Entitlements(
ctx context.Context,
resource *v2.Resource,
_ *pagination.Token,
) (
[]*v2.Entitlement,
string,
annotations.Annotations,
error,
) {
var rv []*v2.Entitlement

assignmentOptions := []ent.EntitlementOption{
ent.WithGrantableTo(resourceTypeUser),
ent.WithDisplayName(fmt.Sprintf("%s Group Member", resource.DisplayName)),
ent.WithDescription(fmt.Sprintf("Is member of the %s group in Confluence", resource.DisplayName)),
assignmentOptions := []entitlement.EntitlementOption{
entitlement.WithGrantableTo(resourceTypeUser),
entitlement.WithDisplayName(fmt.Sprintf("%s Group Member", resource.DisplayName)),
entitlement.WithDescription(fmt.Sprintf("Is member of the %s group in Confluence", resource.DisplayName)),
}

rv = append(rv, ent.NewAssignmentEntitlement(
rv = append(rv, entitlement.NewAssignmentEntitlement(
resource,
groupMemberEntitlement,
assignmentOptions...,
Expand All @@ -104,8 +121,16 @@ func (o *groupResourceType) Entitlements(ctx context.Context, resource *v2.Resou
return rv, "", nil, nil
}

func (o *groupResourceType) Grants(ctx context.Context, resource *v2.Resource, pt *pagination.Token) ([]*v2.Grant, string, annotations.Annotations, error) {
l := ctxzap.Extract(ctx)
func (o *groupResourceType) Grants(
ctx context.Context,
resource *v2.Resource,
pt *pagination.Token,
) (
[]*v2.Grant,
string,
annotations.Annotations,
error,
) {
bag := &pagination.Bag{}
err := bag.Unmarshal(pt.Token)
if err != nil {
Expand All @@ -117,15 +142,20 @@ func (o *groupResourceType) Grants(ctx context.Context, resource *v2.Resource, p
})
}

users, token, err := o.client.GetGroupMembers(ctx, bag.PageToken(), ResourcesPageSize, resource.DisplayName)
users, token, ratelimitData, err := o.client.GetGroupMembers(
ctx,
bag.PageToken(),
ResourcesPageSize,
resource.Id.Resource,
)
outputAnnotations := WithRateLimitAnnotations(ratelimitData)
if err != nil {
return nil, "", nil, err
return nil, "", outputAnnotations, err
}

var rv []*v2.Grant
for _, user := range users {
if user.AccountType != accountTypeAtlassian {
l.Debug("confluence: user is not of type atlassian", zap.Any("user", user))
if !shouldIncludeUser(ctx, user) {
continue
}

Expand All @@ -141,9 +171,36 @@ func (o *groupResourceType) Grants(ctx context.Context, resource *v2.Resource, p

nextPage, err := bag.NextToken(token)
if err != nil {
return nil, "", nil, err
return nil, "", outputAnnotations, err
}
return rv, nextPage, nil, nil
return rv, nextPage, outputAnnotations, nil
}

func (o *groupResourceType) Grant(
ctx context.Context,
principal *v2.Resource,
entitlement *v2.Entitlement,
) (annotations.Annotations, error) {
ratelimitData, err := o.client.AddUserToGroup(
ctx,
entitlement.Resource.Id.Resource,
principal.Id.Resource,
)
outputAnnotations := WithRateLimitAnnotations(ratelimitData)
return outputAnnotations, err
}

func (o *groupResourceType) Revoke(
ctx context.Context,
grant *v2.Grant,
) (annotations.Annotations, error) {
ratelimitData, err := o.client.RemoveUserFromGroup(
ctx,
grant.Entitlement.Resource.Id.Resource,
grant.Principal.Id.Resource,
)
outputAnnotations := WithRateLimitAnnotations(ratelimitData)
return outputAnnotations, err
Comment on lines +193 to +203
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tip

Codebase Verification

Comprehensive testing needed for Revoke method

The Revoke method in groupResourceType does not appear to have any direct unit tests. To ensure its functionality and reliability, it is crucial to add comprehensive tests for this method.

  • File: pkg/connector/group.go
  • Lines: 193-203
Analysis chain

New method looks good! Ensure comprehensive testing.

The Revoke method encapsulates the logic for removing users from groups and handles rate limit data. Ensure that the method is thoroughly tested.

Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify the functionality of the `Revoke` method.

# Test: Search for the unit test function. Expect: Unit tests for `Revoke` method.
rg --type go 'func TestGroupResourceType_Revoke' --context 5

Length of output: 62


Script:

#!/bin/bash
# Description: Verify the functionality of the `Revoke` method by searching for references in test files.

# Search for any references to the `Revoke` method in Go test files.
rg 'Revoke' --type go -A 5

Length of output: 141145


Script:

#!/bin/bash
# Description: Verify the functionality of the `Revoke` method by searching for references in the test directory.

# Search for any references to the `Revoke` method in the test directory.
rg 'Revoke' --type go -A 5 --glob 'test/**'

Length of output: 43

}

func groupBuilder(client *client.ConfluenceClient) *groupResourceType {
Expand Down
Loading
Loading