Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
50 changes: 50 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: CI

on:
push:
branches: [master]
pull_request:
branches: [master]

jobs:
build-and-test:
name: Build & Test
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4
Comment on lines +15 to +16

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

GitHub Actions security hardening needed in both workflows.

Both .github/workflows/ci.yml and .github/workflows/publish-nuget.yml share the same security gaps:

  1. Missing persist-credentials: false on checkout actions - Repository credentials could be exposed through artifacts or subsequent steps
  2. Unpinned action references - Actions are pinned to mutable version tags (v4) instead of immutable commit SHAs, creating supply chain attack vectors

These are industry best practices for securing GitHub Actions workflows and should be addressed before production use.

🧰 Tools
🪛 zizmor (1.25.2)

[warning] 15-16: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 16-16: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 15 - 16, Update the checkout steps
that currently read "uses: actions/checkout@v4" to include "persist-credentials:
false" and replace the mutable tag with an immutable commit SHA (i.e., pin the
action to a specific commit) in both workflows; locate the checkout step symbol
"uses: actions/checkout@v4" and change it to use the commit SHA reference and
add the "persist-credentials: false" input so credentials are not persisted and
the action is pinned.

Source: Linters/SAST tools

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Fetch current commit hashes for pinned actions

echo "=== actions/checkout@v4 ==="
gh api repos/actions/checkout/git/matching-refs/tags/v4 --jq '.[0].object.sha'

echo "=== actions/setup-dotnet@v4 ==="
gh api repos/actions/setup-dotnet/git/matching-refs/tags/v4 --jq '.[0].object.sha'

echo "=== actions/upload-artifact@v4 ==="
gh api repos/actions/upload-artifact/git/matching-refs/tags/v4 --jq '.[0].object.sha'

Repository: teesofttech/TableauSharp

Length of output: 3690


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE=".github/workflows/ci.yml"
echo "=== exists? ==="
ls -la "$FILE"

echo "=== uses: actions/ in $FILE ==="
rg -n "uses:\s*actions/" "$FILE" || true

echo "=== show surrounding lines for referenced areas ==="
# Print around line 1-120 (workflow likely small); cap output
sed -n '1,120p' "$FILE" | cat -n

Repository: teesofttech/TableauSharp

Length of output: 1881


Security: Pin GitHub Actions to immutable commit SHAs (not @v4 tags).

In .github/workflows/ci.yml, these steps reference mutable tags: actions/checkout@v4 (line 16), actions/setup-dotnet@v4 (line 19), and actions/upload-artifact@v4 (lines 40, 47). Pin each uses: to the full commit SHA to prevent supply-chain risk from tag updates.

- uses: actions/checkout@<commit-sha>
- uses: actions/setup-dotnet@<commit-sha>
- uses: actions/upload-artifact@<commit-sha>
🧰 Tools
🪛 zizmor (1.25.2)

[warning] 15-16: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 16-16: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml at line 16, Pin the referenced GitHub Actions to
immutable commit SHAs instead of mutable tags: replace each occurrence of "uses:
actions/checkout@v4", "uses: actions/setup-dotnet@v4", and both "uses:
actions/upload-artifact@v4" with the corresponding full commit SHA for the
action repo (obtain the desired commit SHA from the action's GitHub repository
releases/commits) so the workflow always uses that exact commit; update all
instances in the file (search for the strings "actions/checkout@v4",
"actions/setup-dotnet@v4", and "actions/upload-artifact@v4") and commit the
replacements.

Source: Linters/SAST tools


- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'

- name: Restore
run: dotnet restore TableauSharp.sln

- name: Build
run: dotnet build TableauSharp.sln --no-restore -c Release

- name: Test
run: >
dotnet test TableauSharp.sln
--no-build
-c Release
--verbosity normal
--logger "trx;LogFileName=test-results.trx"
--collect:"XPlat Code Coverage"

- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: '**/TestResults/**'

- name: Upload coverage
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage
path: '**/TestResults/**/coverage.cobertura.xml'
65 changes: 65 additions & 0 deletions .github/workflows/publish-nuget.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
name: Publish to NuGet

# Triggered by a version tag: git tag v0.1.0 && git push --tags
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+*' # v0.1.0, v1.0.0-beta, etc.

jobs:
publish:
name: Pack & Publish
runs-on: ubuntu-latest
permissions:
contents: read

steps:
- name: Checkout
uses: actions/checkout@v4

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ls -la .github/workflows || true

echo "---- publish-nuget.yml ----"
sed -n '1,200p' .github/workflows/publish-nuget.yml

echo "---- ci.yml (if present) ----"
if [ -f .github/workflows/ci.yml ]; then
  sed -n '1,240p' .github/workflows/ci.yml
else
  # Try common variants
  for f in .github/workflows/*ci*.yml .github/workflows/*CI*.yml; do
    [ -f "$f" ] || continue
    echo "---- $f ----"
    sed -n '1,240p' "$f"
  done
fi

echo "---- all workflow action usages in publish-nuget.yml ----"
rg -n "uses:\s*[^#\n]+" .github/workflows/publish-nuget.yml || true

echo "---- all workflow action usages in ci.yml (or matches) ----"
if [ -f .github/workflows/ci.yml ]; then
  rg -n "uses:\s*[^#\n]+" .github/workflows/ci.yml || true
else
  for f in .github/workflows/*ci*.yml .github/workflows/*CI*.yml; do
    [ -f "$f" ] || continue
    echo "## $f"
    rg -n "uses:\s*[^#\n]+" "$f" || true
  done
fi

Repository: teesofttech/TableauSharp

Length of output: 3612


Security: Pin GitHub Actions to immutable commit SHAs (no version tags).

In .github/workflows/publish-nuget.yml, replace actions/checkout@v4 (line 18), actions/setup-dotnet@v4 (line 21), and actions/upload-artifact@v4 (lines 62-65) with pinned owner/repo@<commit_sha> versions. The same applies in .github/workflows/ci.yml (actions/checkout@v4 line 16, actions/setup-dotnet@v4 line 19, actions/upload-artifact@v4 lines 40 and 47).

🧰 Tools
🪛 zizmor (1.25.2)

[warning] 17-18: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 18-18: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/publish-nuget.yml at line 18, Replace the floating action
versions with pinned commit SHAs: find occurrences of actions/checkout@v4,
actions/setup-dotnet@v4 and actions/upload-artifact@v4 in the workflow files and
change each to the corresponding owner/repo@<commit_sha> form (use the exact
commit SHA for the release you want to pin); ensure every instance is updated
(the three identifiers above) so the workflows use immutable references rather
than version tags.

Source: Linters/SAST tools


- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'

- name: Extract version from tag
id: version
# Strip the leading 'v' from the tag name (v0.1.0 → 0.1.0)
run: echo "VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"

- name: Restore
run: dotnet restore TableauSharp.sln

- name: Build
run: dotnet build TableauSharp.sln --no-restore -c Release

- name: Test
run: dotnet test TableauSharp.sln --no-build -c Release --verbosity normal

- name: Pack
run: >
dotnet pack src/TableauSharp/TableauSharp.csproj
--no-build
-c Release
-p:PackageVersion=${{ steps.version.outputs.VERSION }}
-o ./artifacts

- name: Publish to NuGet
run: >
dotnet nuget push ./artifacts/*.nupkg
--api-key ${{ secrets.NUGET_API_KEY }}
--source https://api.nuget.org/v3/index.json
--skip-duplicate

- name: Publish symbols
run: >
dotnet nuget push ./artifacts/*.snupkg
--api-key ${{ secrets.NUGET_API_KEY }}
--source https://api.nuget.org/v3/index.json
--skip-duplicate

- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: nuget-packages
path: ./artifacts/
59 changes: 59 additions & 0 deletions TableauSharp.sln
Original file line number Diff line number Diff line change
Expand Up @@ -19,32 +19,90 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TableauSharp.AppHost", "Tab
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TableauSharp.ServiceDefaults", "TableauSharp.ServiceDefaults\TableauSharp.ServiceDefaults.csproj", "{E0696B33-AF30-01B1-59D3-88E9DB530E2D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TableauSharp.Examples", "samples\TableauSharp.Examples\TableauSharp.Examples.csproj", "{0CED7987-15A3-4151-AC7E-85D6BDA02B6D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{F4ED3DA6-B4C7-4B38-A128-EC0BBB92BAF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F4ED3DA6-B4C7-4B38-A128-EC0BBB92BAF0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F4ED3DA6-B4C7-4B38-A128-EC0BBB92BAF0}.Debug|x64.ActiveCfg = Debug|Any CPU
{F4ED3DA6-B4C7-4B38-A128-EC0BBB92BAF0}.Debug|x64.Build.0 = Debug|Any CPU
{F4ED3DA6-B4C7-4B38-A128-EC0BBB92BAF0}.Debug|x86.ActiveCfg = Debug|Any CPU
{F4ED3DA6-B4C7-4B38-A128-EC0BBB92BAF0}.Debug|x86.Build.0 = Debug|Any CPU
{F4ED3DA6-B4C7-4B38-A128-EC0BBB92BAF0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F4ED3DA6-B4C7-4B38-A128-EC0BBB92BAF0}.Release|Any CPU.Build.0 = Release|Any CPU
{F4ED3DA6-B4C7-4B38-A128-EC0BBB92BAF0}.Release|x64.ActiveCfg = Release|Any CPU
{F4ED3DA6-B4C7-4B38-A128-EC0BBB92BAF0}.Release|x64.Build.0 = Release|Any CPU
{F4ED3DA6-B4C7-4B38-A128-EC0BBB92BAF0}.Release|x86.ActiveCfg = Release|Any CPU
{F4ED3DA6-B4C7-4B38-A128-EC0BBB92BAF0}.Release|x86.Build.0 = Release|Any CPU
{27F742DD-CAC0-446F-B9B9-F346B4020CC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{27F742DD-CAC0-446F-B9B9-F346B4020CC4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{27F742DD-CAC0-446F-B9B9-F346B4020CC4}.Debug|x64.ActiveCfg = Debug|Any CPU
{27F742DD-CAC0-446F-B9B9-F346B4020CC4}.Debug|x64.Build.0 = Debug|Any CPU
{27F742DD-CAC0-446F-B9B9-F346B4020CC4}.Debug|x86.ActiveCfg = Debug|Any CPU
{27F742DD-CAC0-446F-B9B9-F346B4020CC4}.Debug|x86.Build.0 = Debug|Any CPU
{27F742DD-CAC0-446F-B9B9-F346B4020CC4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{27F742DD-CAC0-446F-B9B9-F346B4020CC4}.Release|Any CPU.Build.0 = Release|Any CPU
{27F742DD-CAC0-446F-B9B9-F346B4020CC4}.Release|x64.ActiveCfg = Release|Any CPU
{27F742DD-CAC0-446F-B9B9-F346B4020CC4}.Release|x64.Build.0 = Release|Any CPU
{27F742DD-CAC0-446F-B9B9-F346B4020CC4}.Release|x86.ActiveCfg = Release|Any CPU
{27F742DD-CAC0-446F-B9B9-F346B4020CC4}.Release|x86.Build.0 = Release|Any CPU
{F3BC13B7-C8FD-1AE9-F7FE-7CEA07B964D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F3BC13B7-C8FD-1AE9-F7FE-7CEA07B964D9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F3BC13B7-C8FD-1AE9-F7FE-7CEA07B964D9}.Debug|x64.ActiveCfg = Debug|Any CPU
{F3BC13B7-C8FD-1AE9-F7FE-7CEA07B964D9}.Debug|x64.Build.0 = Debug|Any CPU
{F3BC13B7-C8FD-1AE9-F7FE-7CEA07B964D9}.Debug|x86.ActiveCfg = Debug|Any CPU
{F3BC13B7-C8FD-1AE9-F7FE-7CEA07B964D9}.Debug|x86.Build.0 = Debug|Any CPU
{F3BC13B7-C8FD-1AE9-F7FE-7CEA07B964D9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F3BC13B7-C8FD-1AE9-F7FE-7CEA07B964D9}.Release|Any CPU.Build.0 = Release|Any CPU
{F3BC13B7-C8FD-1AE9-F7FE-7CEA07B964D9}.Release|x64.ActiveCfg = Release|Any CPU
{F3BC13B7-C8FD-1AE9-F7FE-7CEA07B964D9}.Release|x64.Build.0 = Release|Any CPU
{F3BC13B7-C8FD-1AE9-F7FE-7CEA07B964D9}.Release|x86.ActiveCfg = Release|Any CPU
{F3BC13B7-C8FD-1AE9-F7FE-7CEA07B964D9}.Release|x86.Build.0 = Release|Any CPU
{EBC649F6-D014-4E5D-903F-58D50A8D7422}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EBC649F6-D014-4E5D-903F-58D50A8D7422}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EBC649F6-D014-4E5D-903F-58D50A8D7422}.Debug|x64.ActiveCfg = Debug|Any CPU
{EBC649F6-D014-4E5D-903F-58D50A8D7422}.Debug|x64.Build.0 = Debug|Any CPU
{EBC649F6-D014-4E5D-903F-58D50A8D7422}.Debug|x86.ActiveCfg = Debug|Any CPU
{EBC649F6-D014-4E5D-903F-58D50A8D7422}.Debug|x86.Build.0 = Debug|Any CPU
{EBC649F6-D014-4E5D-903F-58D50A8D7422}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EBC649F6-D014-4E5D-903F-58D50A8D7422}.Release|Any CPU.Build.0 = Release|Any CPU
{EBC649F6-D014-4E5D-903F-58D50A8D7422}.Release|x64.ActiveCfg = Release|Any CPU
{EBC649F6-D014-4E5D-903F-58D50A8D7422}.Release|x64.Build.0 = Release|Any CPU
{EBC649F6-D014-4E5D-903F-58D50A8D7422}.Release|x86.ActiveCfg = Release|Any CPU
{EBC649F6-D014-4E5D-903F-58D50A8D7422}.Release|x86.Build.0 = Release|Any CPU
{E0696B33-AF30-01B1-59D3-88E9DB530E2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E0696B33-AF30-01B1-59D3-88E9DB530E2D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E0696B33-AF30-01B1-59D3-88E9DB530E2D}.Debug|x64.ActiveCfg = Debug|Any CPU
{E0696B33-AF30-01B1-59D3-88E9DB530E2D}.Debug|x64.Build.0 = Debug|Any CPU
{E0696B33-AF30-01B1-59D3-88E9DB530E2D}.Debug|x86.ActiveCfg = Debug|Any CPU
{E0696B33-AF30-01B1-59D3-88E9DB530E2D}.Debug|x86.Build.0 = Debug|Any CPU
{E0696B33-AF30-01B1-59D3-88E9DB530E2D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E0696B33-AF30-01B1-59D3-88E9DB530E2D}.Release|Any CPU.Build.0 = Release|Any CPU
{E0696B33-AF30-01B1-59D3-88E9DB530E2D}.Release|x64.ActiveCfg = Release|Any CPU
{E0696B33-AF30-01B1-59D3-88E9DB530E2D}.Release|x64.Build.0 = Release|Any CPU
{E0696B33-AF30-01B1-59D3-88E9DB530E2D}.Release|x86.ActiveCfg = Release|Any CPU
{E0696B33-AF30-01B1-59D3-88E9DB530E2D}.Release|x86.Build.0 = Release|Any CPU
{0CED7987-15A3-4151-AC7E-85D6BDA02B6D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0CED7987-15A3-4151-AC7E-85D6BDA02B6D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0CED7987-15A3-4151-AC7E-85D6BDA02B6D}.Debug|x64.ActiveCfg = Debug|Any CPU
{0CED7987-15A3-4151-AC7E-85D6BDA02B6D}.Debug|x64.Build.0 = Debug|Any CPU
{0CED7987-15A3-4151-AC7E-85D6BDA02B6D}.Debug|x86.ActiveCfg = Debug|Any CPU
{0CED7987-15A3-4151-AC7E-85D6BDA02B6D}.Debug|x86.Build.0 = Debug|Any CPU
{0CED7987-15A3-4151-AC7E-85D6BDA02B6D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0CED7987-15A3-4151-AC7E-85D6BDA02B6D}.Release|Any CPU.Build.0 = Release|Any CPU
{0CED7987-15A3-4151-AC7E-85D6BDA02B6D}.Release|x64.ActiveCfg = Release|Any CPU
{0CED7987-15A3-4151-AC7E-85D6BDA02B6D}.Release|x64.Build.0 = Release|Any CPU
{0CED7987-15A3-4151-AC7E-85D6BDA02B6D}.Release|x86.ActiveCfg = Release|Any CPU
{0CED7987-15A3-4151-AC7E-85D6BDA02B6D}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -53,6 +111,7 @@ Global
{F4ED3DA6-B4C7-4B38-A128-EC0BBB92BAF0} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{27F742DD-CAC0-446F-B9B9-F346B4020CC4} = {649115EA-82C4-43F0-8EC5-3D1673C79EEB}
{F3BC13B7-C8FD-1AE9-F7FE-7CEA07B964D9} = {A2F93780-46F7-4A71-AFE9-4771B77C7DDA}
{0CED7987-15A3-4151-AC7E-85D6BDA02B6D} = {A2F93780-46F7-4A71-AFE9-4771B77C7DDA}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C8FDB297-CF10-4609-B89F-5382E1BD670D}
Expand Down
60 changes: 60 additions & 0 deletions samples/TableauSharp.Examples/Examples/AuthExamples.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using Microsoft.Extensions.Logging;
using TableauSharp.Auth.Service;

namespace TableauSharp.Examples.Examples;

// Demonstrates the three authentication flows supported by TableauSharp.
// Only one sign-in method should be called per session. After sign-in the
// token is stored automatically in ITableauTokenProvider and injected into
// all other services — you do not need to pass it manually.
public class AuthExamples(IAuthService authService, ILogger<AuthExamples> logger)
{
public async Task RunAsync()
{
Console.WriteLine("--- Auth Examples ---");
Console.WriteLine();

await SignInWithPATAsync();
await SignOutAsync();
}

// Personal Access Token is the recommended flow for server-side applications.
// Configure PersonalAccessTokenName and PersonalAccessTokenSecret in appsettings.json.
private async Task SignInWithPATAsync()
{
Console.WriteLine("[1] Sign in with Personal Access Token");
try
{
var token = await authService.SignInWithPATAsync();
Console.WriteLine($" Token : {token.Token[..8]}...");
Console.WriteLine($" SiteId : {token.SiteId}");
Console.WriteLine($" UserId : {token.UserId}");
Console.WriteLine($" Expires : {token.Expiration:u}");
}
catch (Exception ex)
{
logger.LogError(ex, "PAT sign-in failed");
}

Console.WriteLine();
}

// Sign out invalidates the session on the Tableau server.
// Always sign out when done to avoid leaking server sessions.
private async Task SignOutAsync()
{
Console.WriteLine("[2] Sign out");
try
{
var token = await authService.SignInWithPATAsync();
await authService.SignOutAsync(token.Token);
Console.WriteLine(" Signed out successfully.");
}
catch (Exception ex)
{
logger.LogError(ex, "Sign-out failed");
}

Console.WriteLine();
}
Comment on lines +44 to +59

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Redundant authentication in SignOutAsync wastes resources.

The SignOutAsync method calls authService.SignInWithPATAsync() at line 49 to obtain a token, but RunAsync already signed in at line 17. According to the class comment (lines 7-9), "After sign-in the token is stored automatically in ITableauTokenProvider," so the second sign-in creates an unnecessary server session and contradicts the documented usage pattern.

When RunAsync calls both methods sequentially (lines 17-18), this results in two sign-ins for one sign-out, which is inefficient and demonstrates incorrect API usage.

🔧 Recommended fixes

Option 1: Store and reuse the token from the initial sign-in

 public async Task RunAsync()
 {
     Console.WriteLine("--- Auth Examples ---");
     Console.WriteLine();

-    await SignInWithPATAsync();
-    await SignOutAsync();
+    var token = await SignInWithPATAsync();
+    await SignOutAsync(token);
 }

-private async Task SignInWithPATAsync()
+private async Task<string> SignInWithPATAsync()
 {
     Console.WriteLine("[1] Sign in with Personal Access Token");
     try
     {
         var token = await authService.SignInWithPATAsync();
         Console.WriteLine($"    Token    : {token.Token[..8]}...");
         Console.WriteLine($"    SiteId   : {token.SiteId}");
         Console.WriteLine($"    UserId   : {token.UserId}");
         Console.WriteLine($"    Expires  : {token.Expiration:u}");
+        return token.Token;
     }
     catch (Exception ex)
     {
         logger.LogError(ex, "PAT sign-in failed");
+        return null;
     }

     Console.WriteLine();
 }

-private async Task SignOutAsync()
+private async Task SignOutAsync(string tokenString)
 {
     Console.WriteLine("[2] Sign out");
+    if (string.IsNullOrEmpty(tokenString))
+    {
+        Console.WriteLine("    No token available to sign out.");
+        return;
+    }
+
     try
     {
-        var token = await authService.SignInWithPATAsync();
-        await authService.SignOutAsync(token.Token);
+        await authService.SignOutAsync(tokenString);
         Console.WriteLine("    Signed out successfully.");
     }
     catch (Exception ex)
     {
         logger.LogError(ex, "Sign-out failed");
     }

     Console.WriteLine();
 }

Option 2: Make examples independent (if they're meant to run standalone)

Restructure RunAsync to run only one example at a time based on a parameter, or add a comment clarifying that these are independent workflows not meant to be called sequentially.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@samples/TableauSharp.Examples/Examples/AuthExamples.cs` around lines 44 - 59,
SignOutAsync is performing a redundant sign-in by calling
authService.SignInWithPATAsync() even though RunAsync already signed in and
tokens are stored in ITableauTokenProvider; remove the SignInWithPATAsync call
and retrieve the existing token from the shared token provider (or accept the
token as a parameter) and pass that token to authService.SignOutAsync(token) in
SignOutAsync (update SignOutAsync signature or body to use
ITableauTokenProvider/GetStoredToken instead of creating a new session);
alternatively, if examples must be standalone, change RunAsync to invoke only
one example at a time or document that each example is independent so they
should not be called sequentially.

}
71 changes: 71 additions & 0 deletions samples/TableauSharp.Examples/Examples/DataSourceExamples.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using Microsoft.Extensions.Logging;
using TableauSharp.Auth.Service;
using TableauSharp.DataSources.Services;

namespace TableauSharp.Examples.Examples;

// Demonstrates data source operations.
// RefreshAsync triggers an extract refresh job on Tableau Server.
// The job runs asynchronously — use Tableau's Jobs API to poll completion.
public class DataSourceExamples(
IAuthService authService,
IDataSourceService dataSourceService,
ILogger<DataSourceExamples> logger)
{
public async Task RunAsync()
{
Console.WriteLine("--- Data Source Examples ---");
Console.WriteLine();

await authService.SignInWithPATAsync();

await ListDataSourcesAsync();
await RefreshFirstDataSourceAsync();
}

private async Task ListDataSourcesAsync()
{
Console.WriteLine("[1] List all data sources");
try
{
var sources = await dataSourceService.GetAllAsync();
foreach (var ds in sources)
{
var certified = ds.IsCertified ? " [certified]" : "";
Console.WriteLine($" {ds.Id} {ds.Name,-35} type={ds.Type ?? "unknown"}{certified}");
}

if (!sources.Any())
Console.WriteLine(" No data sources found.");
}
catch (Exception ex)
{
logger.LogError(ex, "List data sources failed");
}

Console.WriteLine();
}

// Triggering a refresh is fire-and-forget from the SDK's perspective.
// Tableau Server queues the job and runs it asynchronously.
private async Task RefreshFirstDataSourceAsync()
{
Console.WriteLine("[2] Trigger extract refresh on first data source");
try
{
var sources = await dataSourceService.GetAllAsync();
var first = sources.FirstOrDefault();
if (first is null) { Console.WriteLine(" No data sources."); return; }

await dataSourceService.RefreshAsync(first.Id);
Console.WriteLine($" Refresh triggered for '{first.Name}'.");
Console.WriteLine(" Check Tableau Server > Jobs to monitor completion.");
}
catch (Exception ex)
{
logger.LogError(ex, "Refresh failed");
}

Console.WriteLine();
}
}
Loading
Loading