Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/baton-tableau/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func getConnector(ctx context.Context, tc *cfg.Tableau) (types.ConnectorServer,
return nil, err
}

apiVersion := "3.19"
apiVersion := "3.27"
if tc.ApiVersion != "" {
apiVersion = tc.ApiVersion
}
Expand Down
24 changes: 23 additions & 1 deletion pkg/connector/connector.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,14 +88,36 @@ func (tb *Tableau) Metadata(ctx context.Context) (*v2.ConnectorMetadata, error)
"siteRole": {
DisplayName: "Site Role",
Required: true,
Description: `The role to assign to the user on the site. Possible values are:
Description: `The role to assign to the user on the site. Possible values are:
Creator, Explorer, ExplorerCanPublish, SiteAdministratorExplorer, SiteAdministratorCreator, Unlicensed, or Viewer.`,
Field: &v2.ConnectorAccountCreationSchema_Field_StringField{
StringField: &v2.ConnectorAccountCreationSchema_StringField{},
},
Placeholder: "Site Role",
Order: 2,
},
"withMFA": {
DisplayName: "With MFA",
Required: false,
Description: `If true, creates users with TableauIDWithMFA authentication instead of using the site's IDP configuration (SAML). Defaults to false.`,
Field: &v2.ConnectorAccountCreationSchema_Field_BoolField{
BoolField: &v2.ConnectorAccountCreationSchema_BoolField{},
},
Order: 3,
},
"idpConfigurationName": {
DisplayName: "IDP Configuration Name",
Required: false,
Description: `The name of the SAML IDP configuration to use for user authentication.
Only required when multiple SAML IDP configurations are enabled on the site.
If only one SAML IDP exists, it will be used automatically.
If no SAML IDPs exist, set withMFA=true instead.`,
Field: &v2.ConnectorAccountCreationSchema_Field_StringField{
StringField: &v2.ConnectorAccountCreationSchema_StringField{},
},
Placeholder: "IDP Configuration Name",
Order: 4,
},
},
},
}, nil
Expand Down
89 changes: 83 additions & 6 deletions pkg/connector/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package connector

import (
"context"
"errors"
"fmt"
"strings"

Expand All @@ -10,7 +11,9 @@ import (
"github.com/conductorone/baton-sdk/pkg/connectorbuilder"
"github.com/conductorone/baton-sdk/pkg/pagination"
rs "github.com/conductorone/baton-sdk/pkg/types/resource"
"github.com/conductorone/baton-sdk/pkg/uhttp"
"github.com/conductorone/baton-tableau/pkg/tableau"
"google.golang.org/grpc/codes"
)

var _ connectorbuilder.AccountManager = &userResourceType{}
Expand All @@ -37,10 +40,11 @@ func userResource(user *tableau.User, parentResourceID *v2.ResourceId) (*v2.Reso
}

profile := map[string]interface{}{
"first_name": firstName,
"last_name": lastName,
"login": user.Email,
"user_id": user.ID,
"first_name": firstName,
"last_name": lastName,
"login": user.Email,
"user_id": user.ID,
"auth_setting": user.AuthSetting,
}

userTraitOptions := []rs.UserTraitOption{
Expand Down Expand Up @@ -121,9 +125,26 @@ func (o *userResourceType) CreateAccount(ctx context.Context, accountInfo *v2.Ac
return nil, nil, nil, fmt.Errorf("baton-tableau: siteRole not found in profile")
}

withMFA, _ := pMap["withMFA"].(bool)

var authSetting string
var idpConfigId string

if withMFA {
authSetting = "TableauIDWithMFA"
} else {
id, err := o.selectIDPConfiguration(ctx, pMap)
if err != nil {
return nil, nil, nil, err
}
idpConfigId = id
}

user, err := o.client.AddUserToSite(ctx, tableau.CreateUserRequest{
Email: email,
SiteRole: siteRole,
Email: email,
SiteRole: siteRole,
AuthSetting: authSetting,
IdpConfigurationId: idpConfigId,
})
if err != nil {
return nil, nil, nil, fmt.Errorf("baton-tableau: failed to create user %s: %w", email, err)
Expand All @@ -139,6 +160,41 @@ func (o *userResourceType) CreateAccount(ctx context.Context, accountInfo *v2.Ac
}, nil, nil, nil
}

func (o *userResourceType) selectIDPConfiguration(ctx context.Context, pMap map[string]interface{}) (string, error) {
idpConfigName, _ := pMap["idpConfigurationName"].(string)

idpConfigs, err := o.client.ListIdpConfigurations(ctx)
if err != nil {
return "", fmt.Errorf("baton-tableau: failed to list IDP configurations: %w", err)
}

var enabledSAMLConfigs []tableau.IdpConfiguration
for i := range idpConfigs {
if idpConfigs[i].AuthSetting == "SAML" && idpConfigs[i].Enabled {
enabledSAMLConfigs = append(enabledSAMLConfigs, idpConfigs[i])
}
}

if len(enabledSAMLConfigs) == 0 {
return "", fmt.Errorf("baton-tableau: you need to pass the MFA flag since no IDP is configured in Tableau")
}

if len(enabledSAMLConfigs) == 1 {
return enabledSAMLConfigs[0].IdpConfigurationId, nil
}

if idpConfigName == "" {
return "", uhttp.WrapErrors(codes.InvalidArgument, "multiple SAML IDPs available", buildMultipleIDPError(enabledSAMLConfigs))
}

selectedConfig, err := findIDPByName(enabledSAMLConfigs, idpConfigName)
if err != nil {
return "", uhttp.WrapErrors(codes.InvalidArgument, fmt.Sprintf("IDP configuration '%s' not found", idpConfigName), buildMultipleIDPError(enabledSAMLConfigs))
}

return selectedConfig.IdpConfigurationId, nil
}

func (o *userResourceType) Delete(ctx context.Context, resourceId *v2.ResourceId) (annotations.Annotations, error) {
userID := resourceId.Resource
err := o.client.RemoveUserFromSite(ctx, userID)
Expand All @@ -155,3 +211,24 @@ func userBuilder(client *tableau.Client) *userResourceType {
client: client,
}
}

func findIDPByName(configs []tableau.IdpConfiguration, name string) (*tableau.IdpConfiguration, error) {
lowerName := strings.ToLower(name)
for i := range configs {
if strings.ToLower(configs[i].IdpConfigurationName) == lowerName {
return &configs[i], nil
}
}
return nil, fmt.Errorf("IDP configuration with name '%s' not found", name)
}

func buildMultipleIDPError(configs []tableau.IdpConfiguration) error {
msg := fmt.Sprintf(`baton-tableau: multiple SAML IDP configurations found (%d available). Please specify idpConfigurationName in the account profile. Available IDPs:
`, len(configs))

for _, config := range configs {
msg += fmt.Sprintf(" - \"%s\" (ID: %s)\n", config.IdpConfigurationName, config.IdpConfigurationId)
}

return errors.New(msg)
}
31 changes: 27 additions & 4 deletions pkg/tableau/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -400,11 +400,19 @@ func (c *Client) AddUserToSite(ctx context.Context, user CreateUserRequest) (*Us
User *User `json:"user"`
}

userMap := map[string]interface{}{
"name": user.Email,
"siteRole": user.SiteRole,
}

if user.IdpConfigurationId != "" {
userMap["idpConfigurationId"] = user.IdpConfigurationId
} else if user.AuthSetting != "" {
userMap["authSetting"] = user.AuthSetting
}

requestBody, err := json.Marshal(map[string]interface{}{
"user": map[string]interface{}{
"name": user.Email,
"siteRole": user.SiteRole,
},
"user": userMap,
})

if err != nil {
Expand Down Expand Up @@ -457,6 +465,21 @@ func (c *Client) UpdateUserSiteRole(ctx context.Context, userId string, siteRole
return nil
}

func (c *Client) ListIdpConfigurations(ctx context.Context) ([]IdpConfiguration, error) {
url := fmt.Sprint(c.baseUrl, "/sites/", c.siteId, "/site-auth-configurations")
var res struct {
SiteAuthConfigurations struct {
SiteAuthConfiguration []IdpConfiguration `json:"siteAuthConfiguration"`
} `json:"siteAuthConfigurations"`
}

if err := c.doRequest(ctx, url, &res, nil, nil, http.MethodGet); err != nil {
return nil, err
}

return res.SiteAuthConfigurations.SiteAuthConfiguration, nil
}

func buildResourceURL(baseURL string, endpoint string, elems ...string) (string, error) {
joined, err := url.JoinPath(baseURL, append([]string{endpoint}, elems...)...)
if err != nil {
Expand Down
26 changes: 18 additions & 8 deletions pkg/tableau/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,28 @@ type Site struct {
}

type User struct {
Email string `json:"email"`
ID string `json:"id"`
FullName string `json:"fullName"`
Name string `json:"name"`
SiteRole string `json:"siteRole"`
LastLogin time.Time `json:"lastLogin"`
Email string `json:"email"`
ID string `json:"id"`
FullName string `json:"fullName"`
Name string `json:"name"`
SiteRole string `json:"siteRole"`
AuthSetting string `json:"authSetting"`
LastLogin time.Time `json:"lastLogin"`
}

type CreateUserRequest struct {
// has to be name in the payload
Email string `json:"name"`
SiteRole string `json:"siteRole"`
Email string `json:"name"`
SiteRole string `json:"siteRole"`
AuthSetting string `json:"authSetting,omitempty"`
IdpConfigurationId string `json:"idpConfigurationId,omitempty"`
}

type IdpConfiguration struct {
IdpConfigurationId string `json:"idpConfigurationId"`
IdpConfigurationName string `json:"idpConfigurationName"`
AuthSetting string `json:"authSetting"`
Enabled bool `json:"enabled"`
}

type Group struct {
Expand Down