From e552b58dfc93dd9292ff2efcfa0589d7df67a3b7 Mon Sep 17 00:00:00 2001 From: Alejandro Bernal Date: Wed, 28 Jan 2026 11:33:06 -0500 Subject: [PATCH 1/7] add dashboard resource type --- pkg/connector/connector.go | 6 + pkg/connector/group.go | 16 ++- pkg/connector/site.go | 1 + pkg/connector/view.go | 239 +++++++++++++++++++++++++++++++++++++ pkg/tableau/client.go | 157 ++++++++++++++++++++++++ pkg/tableau/models.go | 29 +++++ 6 files changed, 447 insertions(+), 1 deletion(-) create mode 100644 pkg/connector/view.go diff --git a/pkg/connector/connector.go b/pkg/connector/connector.go index fe55d10a..ba5dd4df 100644 --- a/pkg/connector/connector.go +++ b/pkg/connector/connector.go @@ -39,6 +39,11 @@ var ( v2.ResourceType_TRAIT_ROLE, }, } + resourceTypeView = &v2.ResourceType{ + Id: "view", + DisplayName: "View", + Description: "A Tableau dashboard/view", + } ) type Tableau struct { @@ -116,5 +121,6 @@ func (tb *Tableau) ResourceSyncers(ctx context.Context) []connectorbuilder.Resou siteBuilder(tb.client), groupBuilder(tb.client), licenseBuilder(tb.client), + viewBuilder(tb.client), } } diff --git a/pkg/connector/group.go b/pkg/connector/group.go index 32af0ef3..453c793f 100644 --- a/pkg/connector/group.go +++ b/pkg/connector/group.go @@ -15,7 +15,10 @@ import ( "go.uber.org/zap" ) -const memberEntitlement = "member" +const ( + memberEntitlement = "member" + siteRoleServerAdmin = "ServerAdministrator" +) type groupResourceType struct { resourceType *v2.ResourceType @@ -105,7 +108,18 @@ func (g *groupResourceType) Grants(ctx context.Context, resource *v2.Resource, t return nil, "", nil, err } + l := ctxzap.Extract(ctx) for _, user := range users { + if user.SiteRole == siteRoleServerAdmin { + l.Debug( + "baton-tableau: skipping server administrator in group membership (server-level admins are not site-scoped users)", + zap.String("group_id", groupId), + zap.String("user_id", user.ID), + zap.String("user_email", user.Email), + ) + continue + } + userCopy := user ur, err := userResource(&userCopy, resource.Id) if err != nil { diff --git a/pkg/connector/site.go b/pkg/connector/site.go index 44d17f80..996c0984 100644 --- a/pkg/connector/site.go +++ b/pkg/connector/site.go @@ -58,6 +58,7 @@ func siteResource(site tableau.Site) (*v2.Resource, error) { rs.WithAnnotation( &v2.ChildResourceType{ResourceTypeId: resourceTypeUser.Id}, &v2.ChildResourceType{ResourceTypeId: resourceTypeGroup.Id}, + &v2.ChildResourceType{ResourceTypeId: resourceTypeView.Id}, ), } ret, err := rs.NewResource(site.Name, resourceTypeSite, site.ID, siteOptions...) diff --git a/pkg/connector/view.go b/pkg/connector/view.go new file mode 100644 index 00000000..c3b63565 --- /dev/null +++ b/pkg/connector/view.go @@ -0,0 +1,239 @@ +package connector + +import ( + "context" + "fmt" + "strings" + + 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" + rs "github.com/conductorone/baton-sdk/pkg/types/resource" + "github.com/conductorone/baton-tableau/pkg/tableau" + "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" + "go.uber.org/zap" +) + +const ( + viewRead = "Read" + viewFilter = "Filter" + viewViewComments = "ViewComments" + viewAddComment = "AddComment" + viewExportImage = "ExportImage" + viewExportData = "ExportData" + viewShareView = "ShareView" + viewViewUnderlyingData = "ViewUnderlyingData" + viewWebAuthoring = "WebAuthoring" +) + +var viewCapabilities = map[string]string{ + viewRead: "view", + viewFilter: "filter", + viewViewComments: "view comments", + viewAddComment: "add comment", + viewExportImage: "export image", + viewExportData: "export data", + viewShareView: "share view", + viewViewUnderlyingData: "view underlying data", + viewWebAuthoring: "web authoring", +} + +type viewResourceType struct { + resourceType *v2.ResourceType + client *tableau.Client +} + +func (v *viewResourceType) ResourceType(_ context.Context) *v2.ResourceType { + return v.resourceType +} + +func viewResource(view *tableau.View, parentResourceID *v2.ResourceId) (*v2.Resource, error) { + ret, err := rs.NewResource( + view.Name, + resourceTypeView, + view.ID, + rs.WithParentResourceID(parentResourceID), + ) + if err != nil { + return nil, err + } + + return ret, nil +} + +func (v *viewResourceType) List(ctx context.Context, parentId *v2.ResourceId, token *pagination.Token) ([]*v2.Resource, string, annotations.Annotations, error) { + if parentId == nil { + return nil, "", nil, nil + } + + views, err := v.client.GetPaginatedViews(ctx) + if err != nil { + return nil, "", nil, fmt.Errorf("tableau-connector: failed to list views: %w", err) + } + + var rv []*v2.Resource + for _, view := range views { + viewCopy := view + vr, err := viewResource(&viewCopy, parentId) + if err != nil { + return nil, "", nil, err + } + rv = append(rv, vr) + } + + return rv, "", nil, nil +} + +func (v *viewResourceType) Entitlements(_ context.Context, resource *v2.Resource, _ *pagination.Token) ([]*v2.Entitlement, string, annotations.Annotations, error) { + var rv []*v2.Entitlement + + for capabilityName, displayName := range viewCapabilities { + permissionOptions := []ent.EntitlementOption{ + ent.WithGrantableTo(resourceTypeUser, resourceTypeGroup), + ent.WithDescription(fmt.Sprintf("%s permission for %s View", displayName, resource.DisplayName)), + ent.WithDisplayName(fmt.Sprintf("%s permission %s View", capabilityName, resource.DisplayName)), + } + + en := ent.NewPermissionEntitlement(resource, capabilityName, permissionOptions...) + rv = append(rv, en) + } + + return rv, "", nil, nil +} + +func (v *viewResourceType) Grants(ctx context.Context, resource *v2.Resource, token *pagination.Token) ([]*v2.Grant, string, annotations.Annotations, error) { + l := ctxzap.Extract(ctx) + var rv []*v2.Grant + + viewID := resource.Id.Resource + + permissions, err := v.client.GetViewPermissions(ctx, viewID) + if err != nil { + l.Warn( + "baton-tableau: failed to get view permissions, skipping grants for this view", + zap.String("view_id", viewID), + zap.String("view_name", resource.DisplayName), + zap.Error(err), + ) + return rv, "", nil, nil + } + + for _, grantee := range permissions { + for _, capability := range grantee.Capabilities.Capability { + if capability.Mode != "Allow" { + continue + } + + if grantee.User != nil { + principalID, err := rs.NewResourceID(resourceTypeUser, grantee.User.ID) + if err != nil { + return nil, "", nil, err + } + g := grant.NewGrant(resource, capability.Name, principalID) + rv = append(rv, g) + } + + if grantee.Group != nil { + groupID := grantee.Group.ID + principalID, err := rs.NewResourceID(resourceTypeGroup, groupID) + if err != nil { + return nil, "", nil, err + } + g := grant.NewGrant( + resource, + capability.Name, + principalID, + grant.WithAnnotation(&v2.GrantExpandable{ + EntitlementIds: []string{fmt.Sprintf("group:%s:%s", groupID, memberEntitlement)}, + Shallow: true, + }), + ) + rv = append(rv, g) + } + } + } + + return rv, "", nil, nil +} + +func (v *viewResourceType) Grant(ctx context.Context, principal *v2.Resource, entitlement *v2.Entitlement) (annotations.Annotations, error) { + l := ctxzap.Extract(ctx) + + viewID := entitlement.Resource.Id.Resource + principalID := principal.Id.Resource + capabilityName, err := parseCapabilityFromEntitlementID(entitlement.Id) + if err != nil { + return nil, fmt.Errorf("failed to parse capability from entitlement ID: %w", err) + } + + switch principal.Id.ResourceType { + case resourceTypeUser.Id: + err = v.client.AddViewPermission(ctx, viewID, principalID, capabilityName, "Allow") + case resourceTypeGroup.Id: + err = v.client.AddViewGroupPermission(ctx, viewID, principalID, capabilityName, "Allow") + default: + l.Warn( + "baton-tableau: only users and groups can be granted view permissions", + zap.String("principal_type", principal.Id.ResourceType), + zap.String("principal_id", principal.Id.Resource), + ) + return nil, fmt.Errorf("baton-tableau: only users and groups can be granted view permissions") + } + + if err != nil { + return nil, fmt.Errorf("baton-tableau: failed to grant view permission: %w", err) + } + + return nil, nil +} + +func (v *viewResourceType) Revoke(ctx context.Context, grant *v2.Grant) (annotations.Annotations, error) { + l := ctxzap.Extract(ctx) + + entitlement := grant.Entitlement + principal := grant.Principal + + viewID := entitlement.Resource.Id.Resource + principalID := principal.Id.Resource + capabilityName, err := parseCapabilityFromEntitlementID(entitlement.Id) + if err != nil { + return nil, fmt.Errorf("failed to parse capability from entitlement ID: %w", err) + } + + switch principal.Id.ResourceType { + case resourceTypeUser.Id: + err = v.client.DeleteViewPermission(ctx, viewID, principalID, capabilityName, "Allow") + case resourceTypeGroup.Id: + err = v.client.DeleteViewGroupPermission(ctx, viewID, principalID, capabilityName, "Allow") + default: + l.Warn( + "baton-tableau: only users and groups can have view permissions revoked", + zap.String("principal_type", principal.Id.ResourceType), + zap.String("principal_id", principal.Id.Resource), + ) + return nil, fmt.Errorf("baton-tableau: only users and groups can have view permissions revoked") + } + + if err != nil { + return nil, fmt.Errorf("baton-tableau: failed to revoke view permission: %w", err) + } + + return nil, nil +} + +func viewBuilder(client *tableau.Client) *viewResourceType { + return &viewResourceType{ + resourceType: resourceTypeView, + client: client, + } +} + +func parseCapabilityFromEntitlementID(entitlementID string) (string, error) { + parts := strings.Split(entitlementID, ":") + if len(parts) != 3 { + return "", fmt.Errorf("invalid entitlement ID: %s", entitlementID) + } + return parts[2], nil +} diff --git a/pkg/tableau/client.go b/pkg/tableau/client.go index d02fc703..814605ee 100644 --- a/pkg/tableau/client.go +++ b/pkg/tableau/client.go @@ -457,6 +457,163 @@ func (c *Client) UpdateUserSiteRole(ctx context.Context, userId string, siteRole return nil } +func (c *Client) GetViews(ctx context.Context, pageSize int, pageNumber int) ([]View, Pagination, error) { + url := fmt.Sprint(c.baseUrl, "/sites/", c.siteId, "/views") + q := paginationQuery(pageSize, pageNumber) + + var res struct { + Pagination Pagination `json:"pagination"` + Views struct { + View []View `json:"view"` + } `json:"views"` + } + + if err := c.doRequest(ctx, url, &res, q, nil, http.MethodGet); err != nil { + return nil, Pagination{}, err + } + + return res.Views.View, res.Pagination, nil +} + +func (c *Client) GetPaginatedViews(ctx context.Context) ([]View, error) { + var views []View + pageNumber := defaultPageNumber + totalReturned := 0 + + for { + allViews, paginationData, err := c.GetViews(ctx, defaultPageSize, pageNumber) + if err != nil { + return nil, fmt.Errorf("tableau-connector: failed to list views: %w", err) + } + + pageSizeInt, err := strconv.Atoi(paginationData.PageSize) + if err != nil { + return nil, err + } + + totalReturned += pageSizeInt + totalAvailableInt, err := strconv.Atoi(paginationData.TotalAvailable) + if err != nil { + return nil, err + } + + views = append(views, allViews...) + + if totalReturned >= totalAvailableInt { + break + } + pageNumber += 1 + } + + return views, nil +} + +func (c *Client) GetViewPermissions(ctx context.Context, viewID string) ([]GranteeCapabilities, error) { + url := fmt.Sprint(c.baseUrl, "/sites/", c.siteId, "/views/", viewID, "/permissions") + + var res struct { + Permissions struct { + GranteeCapabilities []GranteeCapabilities `json:"granteeCapabilities"` + } `json:"permissions"` + } + + if err := c.doRequest(ctx, url, &res, nil, nil, http.MethodGet); err != nil { + return nil, err + } + + return res.Permissions.GranteeCapabilities, nil +} + +func (c *Client) AddViewPermission(ctx context.Context, viewID, userID, capabilityName, capabilityMode string) error { + url := fmt.Sprint(c.baseUrl, "/sites/", c.siteId, "/views/", viewID, "/permissions") + + requestBody, err := json.Marshal(map[string]interface{}{ + "permissions": map[string]interface{}{ + "granteeCapabilities": []map[string]interface{}{ + { + "user": map[string]interface{}{ + "id": userID, + }, + "capabilities": map[string]interface{}{ + "capability": []map[string]interface{}{ + { + "name": capabilityName, + "mode": capabilityMode, + }, + }, + }, + }, + }, + }, + }) + + if err != nil { + return err + } + + var res struct{} + if err := c.doRequest(ctx, url, &res, nil, requestBody, http.MethodPut); err != nil { + return err + } + + return nil +} + +func (c *Client) DeleteViewPermission(ctx context.Context, viewID, userID, capabilityName, capabilityMode string) error { + url := fmt.Sprint(c.baseUrl, "/sites/", c.siteId, "/views/", viewID, "/permissions/users/", userID, "/", capabilityName, "/", capabilityMode) + + if err := c.doRequest(ctx, url, nil, nil, nil, http.MethodDelete); err != nil { + return err + } + + return nil +} + +func (c *Client) AddViewGroupPermission(ctx context.Context, viewID, groupID, capabilityName, capabilityMode string) error { + url := fmt.Sprint(c.baseUrl, "/sites/", c.siteId, "/views/", viewID, "/permissions") + + requestBody, err := json.Marshal(map[string]interface{}{ + "permissions": map[string]interface{}{ + "granteeCapabilities": []map[string]interface{}{ + { + "group": map[string]interface{}{ + "id": groupID, + }, + "capabilities": map[string]interface{}{ + "capability": []map[string]interface{}{ + { + "name": capabilityName, + "mode": capabilityMode, + }, + }, + }, + }, + }, + }, + }) + + if err != nil { + return err + } + + var res struct{} + if err := c.doRequest(ctx, url, &res, nil, requestBody, http.MethodPut); err != nil { + return err + } + + return nil +} + +func (c *Client) DeleteViewGroupPermission(ctx context.Context, viewID, groupID, capabilityName, capabilityMode string) error { + url := fmt.Sprint(c.baseUrl, "/sites/", c.siteId, "/views/", viewID, "/permissions/groups/", groupID, "/", capabilityName, "/", capabilityMode) + + if err := c.doRequest(ctx, url, nil, nil, nil, http.MethodDelete); err != nil { + return err + } + + return nil +} + func buildResourceURL(baseURL string, endpoint string, elems ...string) (string, error) { joined, err := url.JoinPath(baseURL, append([]string{endpoint}, elems...)...) if err != nil { diff --git a/pkg/tableau/models.go b/pkg/tableau/models.go index af106f9a..d31232a4 100644 --- a/pkg/tableau/models.go +++ b/pkg/tableau/models.go @@ -34,3 +34,32 @@ type Group struct { ID string `json:"id"` Name string `json:"name"` } + +type View struct { + ID string `json:"id"` + Name string `json:"name"` + ContentURL string `json:"contentUrl"` +} + +type GranteeCapabilities struct { + User *UserRef `json:"user,omitempty"` + Group *GroupRef `json:"group,omitempty"` + Capabilities Capabilities `json:"capabilities"` +} + +type UserRef struct { + ID string `json:"id"` +} + +type GroupRef struct { + ID string `json:"id"` +} + +type Capabilities struct { + Capability []Capability `json:"capability"` +} + +type Capability struct { + Name string `json:"name"` + Mode string `json:"mode"` +} From 176efd43d90caa699575fbdc28aaaee5306f99be Mon Sep 17 00:00:00 2001 From: Alejandro Bernal Date: Thu, 29 Jan 2026 12:16:55 -0500 Subject: [PATCH 2/7] update readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 2b72439b..619f60e3 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ baton resources - Users - Groups - Licenses +- Views (Dashboards) # Contributing, Support and Issues From c88d18b7527e6b12a9b72b45636bf0925fd22368 Mon Sep 17 00:00:00 2001 From: Alejandro Bernal Date: Thu, 29 Jan 2026 14:02:33 -0500 Subject: [PATCH 3/7] fix lint --- pkg/connector/group.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/connector/group.go b/pkg/connector/group.go index 453c793f..5d256e62 100644 --- a/pkg/connector/group.go +++ b/pkg/connector/group.go @@ -16,8 +16,8 @@ import ( ) const ( - memberEntitlement = "member" - siteRoleServerAdmin = "ServerAdministrator" + memberEntitlement = "member" + siteRoleServerAdmin = "ServerAdministrator" ) type groupResourceType struct { From 4d717e4d5a637ce72c9e49f3c1af14ed5edd7f05 Mon Sep 17 00:00:00 2001 From: Alejandro Bernal Date: Thu, 29 Jan 2026 14:17:29 -0500 Subject: [PATCH 4/7] change to url.JoinPath --- pkg/connector/view.go | 2 +- pkg/tableau/client.go | 42 ++++++++++++++++++++++++++++++------------ 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/pkg/connector/view.go b/pkg/connector/view.go index c3b63565..7836f74e 100644 --- a/pkg/connector/view.go +++ b/pkg/connector/view.go @@ -111,7 +111,7 @@ func (v *viewResourceType) Grants(ctx context.Context, resource *v2.Resource, to permissions, err := v.client.GetViewPermissions(ctx, viewID) if err != nil { - l.Warn( + l.Debug( "baton-tableau: failed to get view permissions, skipping grants for this view", zap.String("view_id", viewID), zap.String("view_name", resource.DisplayName), diff --git a/pkg/tableau/client.go b/pkg/tableau/client.go index bbbc49f4..97499f17 100644 --- a/pkg/tableau/client.go +++ b/pkg/tableau/client.go @@ -464,7 +464,10 @@ func (c *Client) UpdateUserSiteRole(ctx context.Context, userId string, siteRole } func (c *Client) GetViews(ctx context.Context, pageSize int, pageNumber int) ([]View, Pagination, error) { - url := fmt.Sprint(c.baseUrl, "/sites/", c.siteId, "/views") + endpoint, err := url.JoinPath(c.baseUrl, "sites", c.siteId, "views") + if err != nil { + return nil, Pagination{}, fmt.Errorf("failed to build URL: %w", err) + } q := paginationQuery(pageSize, pageNumber) var res struct { @@ -474,7 +477,7 @@ func (c *Client) GetViews(ctx context.Context, pageSize int, pageNumber int) ([] } `json:"views"` } - if err := c.doRequest(ctx, url, &res, q, nil, http.MethodGet); err != nil { + if err := c.doRequest(ctx, endpoint, &res, q, nil, http.MethodGet); err != nil { return nil, Pagination{}, err } @@ -515,7 +518,10 @@ func (c *Client) GetPaginatedViews(ctx context.Context) ([]View, error) { } func (c *Client) GetViewPermissions(ctx context.Context, viewID string) ([]GranteeCapabilities, error) { - url := fmt.Sprint(c.baseUrl, "/sites/", c.siteId, "/views/", viewID, "/permissions") + endpoint, err := url.JoinPath(c.baseUrl, "sites", c.siteId, "views", viewID, "permissions") + if err != nil { + return nil, fmt.Errorf("failed to build URL: %w", err) + } var res struct { Permissions struct { @@ -523,7 +529,7 @@ func (c *Client) GetViewPermissions(ctx context.Context, viewID string) ([]Grant } `json:"permissions"` } - if err := c.doRequest(ctx, url, &res, nil, nil, http.MethodGet); err != nil { + if err := c.doRequest(ctx, endpoint, &res, nil, nil, http.MethodGet); err != nil { return nil, err } @@ -531,7 +537,10 @@ func (c *Client) GetViewPermissions(ctx context.Context, viewID string) ([]Grant } func (c *Client) AddViewPermission(ctx context.Context, viewID, userID, capabilityName, capabilityMode string) error { - url := fmt.Sprint(c.baseUrl, "/sites/", c.siteId, "/views/", viewID, "/permissions") + endpoint, err := url.JoinPath(c.baseUrl, "sites", c.siteId, "views", viewID, "permissions") + if err != nil { + return fmt.Errorf("failed to build URL: %w", err) + } requestBody, err := json.Marshal(map[string]interface{}{ "permissions": map[string]interface{}{ @@ -558,7 +567,7 @@ func (c *Client) AddViewPermission(ctx context.Context, viewID, userID, capabili } var res struct{} - if err := c.doRequest(ctx, url, &res, nil, requestBody, http.MethodPut); err != nil { + if err := c.doRequest(ctx, endpoint, &res, nil, requestBody, http.MethodPut); err != nil { return err } @@ -566,9 +575,12 @@ func (c *Client) AddViewPermission(ctx context.Context, viewID, userID, capabili } func (c *Client) DeleteViewPermission(ctx context.Context, viewID, userID, capabilityName, capabilityMode string) error { - url := fmt.Sprint(c.baseUrl, "/sites/", c.siteId, "/views/", viewID, "/permissions/users/", userID, "/", capabilityName, "/", capabilityMode) + endpoint, err := url.JoinPath(c.baseUrl, "sites", c.siteId, "views", viewID, "permissions", "users", userID, capabilityName, capabilityMode) + if err != nil { + return fmt.Errorf("failed to build URL: %w", err) + } - if err := c.doRequest(ctx, url, nil, nil, nil, http.MethodDelete); err != nil { + if err := c.doRequest(ctx, endpoint, nil, nil, nil, http.MethodDelete); err != nil { return err } @@ -576,7 +588,10 @@ func (c *Client) DeleteViewPermission(ctx context.Context, viewID, userID, capab } func (c *Client) AddViewGroupPermission(ctx context.Context, viewID, groupID, capabilityName, capabilityMode string) error { - url := fmt.Sprint(c.baseUrl, "/sites/", c.siteId, "/views/", viewID, "/permissions") + endpoint, err := url.JoinPath(c.baseUrl, "sites", c.siteId, "views", viewID, "permissions") + if err != nil { + return fmt.Errorf("failed to build URL: %w", err) + } requestBody, err := json.Marshal(map[string]interface{}{ "permissions": map[string]interface{}{ @@ -603,7 +618,7 @@ func (c *Client) AddViewGroupPermission(ctx context.Context, viewID, groupID, ca } var res struct{} - if err := c.doRequest(ctx, url, &res, nil, requestBody, http.MethodPut); err != nil { + if err := c.doRequest(ctx, endpoint, &res, nil, requestBody, http.MethodPut); err != nil { return err } @@ -611,9 +626,12 @@ func (c *Client) AddViewGroupPermission(ctx context.Context, viewID, groupID, ca } func (c *Client) DeleteViewGroupPermission(ctx context.Context, viewID, groupID, capabilityName, capabilityMode string) error { - url := fmt.Sprint(c.baseUrl, "/sites/", c.siteId, "/views/", viewID, "/permissions/groups/", groupID, "/", capabilityName, "/", capabilityMode) + endpoint, err := url.JoinPath(c.baseUrl, "sites", c.siteId, "views", viewID, "permissions", "groups", groupID, capabilityName, capabilityMode) + if err != nil { + return fmt.Errorf("failed to build URL: %w", err) + } - if err := c.doRequest(ctx, url, nil, nil, nil, http.MethodDelete); err != nil { + if err := c.doRequest(ctx, endpoint, nil, nil, nil, http.MethodDelete); err != nil { return err } From 2edbf2fbd17f0bc681c09473abfad5f79912fb1f Mon Sep 17 00:00:00 2001 From: Alejandro Bernal Date: Fri, 30 Jan 2026 08:54:06 -0500 Subject: [PATCH 5/7] add error messages --- pkg/connector/view.go | 14 +++++++------- pkg/tableau/client.go | 32 ++++++++++++++++---------------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/pkg/connector/view.go b/pkg/connector/view.go index 7836f74e..091ff640 100644 --- a/pkg/connector/view.go +++ b/pkg/connector/view.go @@ -57,7 +57,7 @@ func viewResource(view *tableau.View, parentResourceID *v2.ResourceId) (*v2.Reso rs.WithParentResourceID(parentResourceID), ) if err != nil { - return nil, err + return nil, fmt.Errorf("tableau-connector: failed to create view resource: %w", err) } return ret, nil @@ -78,7 +78,7 @@ func (v *viewResourceType) List(ctx context.Context, parentId *v2.ResourceId, to viewCopy := view vr, err := viewResource(&viewCopy, parentId) if err != nil { - return nil, "", nil, err + return nil, "", nil, fmt.Errorf("tableau-connector: failed to create view resource for %s: %w", view.Name, err) } rv = append(rv, vr) } @@ -129,7 +129,7 @@ func (v *viewResourceType) Grants(ctx context.Context, resource *v2.Resource, to if grantee.User != nil { principalID, err := rs.NewResourceID(resourceTypeUser, grantee.User.ID) if err != nil { - return nil, "", nil, err + return nil, "", nil, fmt.Errorf("tableau-connector: failed to create user resource ID: %w", err) } g := grant.NewGrant(resource, capability.Name, principalID) rv = append(rv, g) @@ -139,7 +139,7 @@ func (v *viewResourceType) Grants(ctx context.Context, resource *v2.Resource, to groupID := grantee.Group.ID principalID, err := rs.NewResourceID(resourceTypeGroup, groupID) if err != nil { - return nil, "", nil, err + return nil, "", nil, fmt.Errorf("tableau-connector: failed to create group resource ID: %w", err) } g := grant.NewGrant( resource, @@ -165,7 +165,7 @@ func (v *viewResourceType) Grant(ctx context.Context, principal *v2.Resource, en principalID := principal.Id.Resource capabilityName, err := parseCapabilityFromEntitlementID(entitlement.Id) if err != nil { - return nil, fmt.Errorf("failed to parse capability from entitlement ID: %w", err) + return nil, fmt.Errorf("tableau-connector: failed to parse capability from entitlement ID: %w", err) } switch principal.Id.ResourceType { @@ -199,7 +199,7 @@ func (v *viewResourceType) Revoke(ctx context.Context, grant *v2.Grant) (annotat principalID := principal.Id.Resource capabilityName, err := parseCapabilityFromEntitlementID(entitlement.Id) if err != nil { - return nil, fmt.Errorf("failed to parse capability from entitlement ID: %w", err) + return nil, fmt.Errorf("tableau-connector: failed to parse capability from entitlement ID: %w", err) } switch principal.Id.ResourceType { @@ -233,7 +233,7 @@ func viewBuilder(client *tableau.Client) *viewResourceType { func parseCapabilityFromEntitlementID(entitlementID string) (string, error) { parts := strings.Split(entitlementID, ":") if len(parts) != 3 { - return "", fmt.Errorf("invalid entitlement ID: %s", entitlementID) + return "", fmt.Errorf("tableau-connector: invalid entitlement ID: %s", entitlementID) } return parts[2], nil } diff --git a/pkg/tableau/client.go b/pkg/tableau/client.go index 97499f17..2f6fb6e4 100644 --- a/pkg/tableau/client.go +++ b/pkg/tableau/client.go @@ -466,7 +466,7 @@ func (c *Client) UpdateUserSiteRole(ctx context.Context, userId string, siteRole func (c *Client) GetViews(ctx context.Context, pageSize int, pageNumber int) ([]View, Pagination, error) { endpoint, err := url.JoinPath(c.baseUrl, "sites", c.siteId, "views") if err != nil { - return nil, Pagination{}, fmt.Errorf("failed to build URL: %w", err) + return nil, Pagination{}, fmt.Errorf("tableau-connector: failed to build URL: %w", err) } q := paginationQuery(pageSize, pageNumber) @@ -478,7 +478,7 @@ func (c *Client) GetViews(ctx context.Context, pageSize int, pageNumber int) ([] } if err := c.doRequest(ctx, endpoint, &res, q, nil, http.MethodGet); err != nil { - return nil, Pagination{}, err + return nil, Pagination{}, fmt.Errorf("tableau-connector: failed to get views: %w", err) } return res.Views.View, res.Pagination, nil @@ -497,13 +497,13 @@ func (c *Client) GetPaginatedViews(ctx context.Context) ([]View, error) { pageSizeInt, err := strconv.Atoi(paginationData.PageSize) if err != nil { - return nil, err + return nil, fmt.Errorf("tableau-connector: failed to parse page size: %w", err) } totalReturned += pageSizeInt totalAvailableInt, err := strconv.Atoi(paginationData.TotalAvailable) if err != nil { - return nil, err + return nil, fmt.Errorf("tableau-connector: failed to parse total available: %w", err) } views = append(views, allViews...) @@ -520,7 +520,7 @@ func (c *Client) GetPaginatedViews(ctx context.Context) ([]View, error) { func (c *Client) GetViewPermissions(ctx context.Context, viewID string) ([]GranteeCapabilities, error) { endpoint, err := url.JoinPath(c.baseUrl, "sites", c.siteId, "views", viewID, "permissions") if err != nil { - return nil, fmt.Errorf("failed to build URL: %w", err) + return nil, fmt.Errorf("tableau-connector: failed to build URL: %w", err) } var res struct { @@ -530,7 +530,7 @@ func (c *Client) GetViewPermissions(ctx context.Context, viewID string) ([]Grant } if err := c.doRequest(ctx, endpoint, &res, nil, nil, http.MethodGet); err != nil { - return nil, err + return nil, fmt.Errorf("tableau-connector: failed to get view permissions: %w", err) } return res.Permissions.GranteeCapabilities, nil @@ -539,7 +539,7 @@ func (c *Client) GetViewPermissions(ctx context.Context, viewID string) ([]Grant func (c *Client) AddViewPermission(ctx context.Context, viewID, userID, capabilityName, capabilityMode string) error { endpoint, err := url.JoinPath(c.baseUrl, "sites", c.siteId, "views", viewID, "permissions") if err != nil { - return fmt.Errorf("failed to build URL: %w", err) + return fmt.Errorf("tableau-connector: failed to build URL: %w", err) } requestBody, err := json.Marshal(map[string]interface{}{ @@ -563,12 +563,12 @@ func (c *Client) AddViewPermission(ctx context.Context, viewID, userID, capabili }) if err != nil { - return err + return fmt.Errorf("tableau-connector: failed to marshal view permission request: %w", err) } var res struct{} if err := c.doRequest(ctx, endpoint, &res, nil, requestBody, http.MethodPut); err != nil { - return err + return fmt.Errorf("tableau-connector: failed to add view permission: %w", err) } return nil @@ -577,11 +577,11 @@ func (c *Client) AddViewPermission(ctx context.Context, viewID, userID, capabili func (c *Client) DeleteViewPermission(ctx context.Context, viewID, userID, capabilityName, capabilityMode string) error { endpoint, err := url.JoinPath(c.baseUrl, "sites", c.siteId, "views", viewID, "permissions", "users", userID, capabilityName, capabilityMode) if err != nil { - return fmt.Errorf("failed to build URL: %w", err) + return fmt.Errorf("tableau-connector: failed to build URL: %w", err) } if err := c.doRequest(ctx, endpoint, nil, nil, nil, http.MethodDelete); err != nil { - return err + return fmt.Errorf("tableau-connector: failed to delete view permission: %w", err) } return nil @@ -590,7 +590,7 @@ func (c *Client) DeleteViewPermission(ctx context.Context, viewID, userID, capab func (c *Client) AddViewGroupPermission(ctx context.Context, viewID, groupID, capabilityName, capabilityMode string) error { endpoint, err := url.JoinPath(c.baseUrl, "sites", c.siteId, "views", viewID, "permissions") if err != nil { - return fmt.Errorf("failed to build URL: %w", err) + return fmt.Errorf("tableau-connector: failed to build URL: %w", err) } requestBody, err := json.Marshal(map[string]interface{}{ @@ -614,12 +614,12 @@ func (c *Client) AddViewGroupPermission(ctx context.Context, viewID, groupID, ca }) if err != nil { - return err + return fmt.Errorf("tableau-connector: failed to marshal view group permission request: %w", err) } var res struct{} if err := c.doRequest(ctx, endpoint, &res, nil, requestBody, http.MethodPut); err != nil { - return err + return fmt.Errorf("tableau-connector: failed to add view group permission: %w", err) } return nil @@ -628,11 +628,11 @@ func (c *Client) AddViewGroupPermission(ctx context.Context, viewID, groupID, ca func (c *Client) DeleteViewGroupPermission(ctx context.Context, viewID, groupID, capabilityName, capabilityMode string) error { endpoint, err := url.JoinPath(c.baseUrl, "sites", c.siteId, "views", viewID, "permissions", "groups", groupID, capabilityName, capabilityMode) if err != nil { - return fmt.Errorf("failed to build URL: %w", err) + return fmt.Errorf("tableau-connector: failed to build URL: %w", err) } if err := c.doRequest(ctx, endpoint, nil, nil, nil, http.MethodDelete); err != nil { - return err + return fmt.Errorf("tableau-connector: failed to delete view group permission: %w", err) } return nil From b6d5ad59a6fcff24cc78e3411fd38f53a5305912 Mon Sep 17 00:00:00 2001 From: Alejandro Bernal Date: Mon, 2 Feb 2026 18:04:15 -0500 Subject: [PATCH 6/7] fix pagination --- pkg/tableau/client.go | 28 ++++------------------------ 1 file changed, 4 insertions(+), 24 deletions(-) diff --git a/pkg/tableau/client.go b/pkg/tableau/client.go index 2f6fb6e4..25a52b53 100644 --- a/pkg/tableau/client.go +++ b/pkg/tableau/client.go @@ -220,12 +220,7 @@ func (c *Client) GetPaginatedUsers(ctx context.Context) ([]User, error) { return nil, fmt.Errorf("tableau-connector: failed to list users: %w", err) } - pageSizeInt, err := strconv.Atoi(paginationData.PageSize) - if err != nil { - return nil, err - } - - totalReturned += pageSizeInt + totalReturned += len(allUsers) totalAvailableInt, err := strconv.Atoi(paginationData.TotalAvailable) if err != nil { return nil, err @@ -254,12 +249,7 @@ func (c *Client) GetPaginatedGroups(ctx context.Context) ([]Group, error) { return nil, fmt.Errorf("tableau-connector: failed to list groups: %w", err) } - pageSizeInt, err := strconv.Atoi(paginationData.PageSize) - if err != nil { - return nil, err - } - - totalReturned += pageSizeInt + totalReturned += len(allGroups) totalAvailableInt, err := strconv.Atoi(paginationData.TotalAvailable) if err != nil { return nil, err @@ -288,12 +278,7 @@ func (c *Client) GetPaginatedGroupUsers(ctx context.Context, groupId string) ([] return nil, fmt.Errorf("tableau-connector: failed to list group users: %w", err) } - pageSizeInt, err := strconv.Atoi(paginationData.PageSize) - if err != nil { - return nil, err - } - - totalReturned += pageSizeInt + totalReturned += len(allUsers) totalAvailableInt, err := strconv.Atoi(paginationData.TotalAvailable) if err != nil { return nil, err @@ -495,12 +480,7 @@ func (c *Client) GetPaginatedViews(ctx context.Context) ([]View, error) { return nil, fmt.Errorf("tableau-connector: failed to list views: %w", err) } - pageSizeInt, err := strconv.Atoi(paginationData.PageSize) - if err != nil { - return nil, fmt.Errorf("tableau-connector: failed to parse page size: %w", err) - } - - totalReturned += pageSizeInt + totalReturned += len(allViews) totalAvailableInt, err := strconv.Atoi(paginationData.TotalAvailable) if err != nil { return nil, fmt.Errorf("tableau-connector: failed to parse total available: %w", err) From 668ccc9462612a0d5531326b087015362556a171 Mon Sep 17 00:00:00 2001 From: Alejandro Bernal Date: Tue, 3 Feb 2026 16:38:09 -0500 Subject: [PATCH 7/7] update logs --- pkg/connector/group.go | 5 ++--- pkg/connector/site.go | 2 +- pkg/connector/view.go | 4 ++-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/pkg/connector/group.go b/pkg/connector/group.go index 5d256e62..dd4fae5f 100644 --- a/pkg/connector/group.go +++ b/pkg/connector/group.go @@ -115,7 +115,6 @@ func (g *groupResourceType) Grants(ctx context.Context, resource *v2.Resource, t "baton-tableau: skipping server administrator in group membership (server-level admins are not site-scoped users)", zap.String("group_id", groupId), zap.String("user_id", user.ID), - zap.String("user_email", user.Email), ) continue } @@ -137,7 +136,7 @@ func (o *groupResourceType) Grant(ctx context.Context, principal *v2.Resource, e l := ctxzap.Extract(ctx) if principal.Id.ResourceType != resourceTypeUser.Id { - l.Warn( + l.Debug( "baton-tableau: only users can be granted group membership", zap.String("principal_type", principal.Id.ResourceType), zap.String("principal_id", principal.Id.Resource), @@ -160,7 +159,7 @@ func (o *groupResourceType) Revoke(ctx context.Context, grant *v2.Grant) (annota principal := grant.Principal if principal.Id.ResourceType != resourceTypeUser.Id { - l.Warn( + l.Debug( "baton-tableau: only users can have group membership revoked", zap.String("principal_type", principal.Id.ResourceType), zap.String("principal_id", principal.Id.Resource), diff --git a/pkg/connector/site.go b/pkg/connector/site.go index 996c0984..dca884d9 100644 --- a/pkg/connector/site.go +++ b/pkg/connector/site.go @@ -108,7 +108,7 @@ func (o *siteResourceType) Grants(ctx context.Context, resource *v2.Resource, pt for _, user := range users { roleName := roles[user.SiteRole] if roleName == "" { - ctxzap.Extract(ctx).Warn("Unknown Tableau Role Name", + ctxzap.Extract(ctx).Debug("Unknown Tableau Role Name", zap.String("role_name", user.SiteRole), zap.String("user", user.FullName), ) diff --git a/pkg/connector/view.go b/pkg/connector/view.go index 091ff640..98fa596c 100644 --- a/pkg/connector/view.go +++ b/pkg/connector/view.go @@ -174,7 +174,7 @@ func (v *viewResourceType) Grant(ctx context.Context, principal *v2.Resource, en case resourceTypeGroup.Id: err = v.client.AddViewGroupPermission(ctx, viewID, principalID, capabilityName, "Allow") default: - l.Warn( + l.Debug( "baton-tableau: only users and groups can be granted view permissions", zap.String("principal_type", principal.Id.ResourceType), zap.String("principal_id", principal.Id.Resource), @@ -208,7 +208,7 @@ func (v *viewResourceType) Revoke(ctx context.Context, grant *v2.Grant) (annotat case resourceTypeGroup.Id: err = v.client.DeleteViewGroupPermission(ctx, viewID, principalID, capabilityName, "Allow") default: - l.Warn( + l.Debug( "baton-tableau: only users and groups can have view permissions revoked", zap.String("principal_type", principal.Id.ResourceType), zap.String("principal_id", principal.Id.Resource),