top of page

Automatizando a Conexão do SSAS com SharePoint Usando Service Principal

  • Foto do escritor: Amanda Nascimento
    Amanda Nascimento
  • 4 de set.
  • 9 min de leitura

Atualizado: 8 de set.


Quem trabalha com cubos no SQL Server Analysis Services (SSAS) conectados ao SharePoint Online provavelmente já enfrentou este problema:a autenticação via conta organizacional com MFA (duplo fator) expira a cada 1–2 horas.

Isso se torna um pesadelo quando você tem um job de processamento agendado a cada minuto. Sem intervenção manual para revalidar as credenciais, o job falha.


Neste post, vou mostrar como resolvemos esse problema usando o Azure Active Directory e a autenticação com Service Principal (App Registration), garantindo integração contínua e sem expiração de sessão. A autenticação com Service Principal é o caminho recomendado pela Microsoft para integrações automatizadas com SharePoint.


Com essa configuração:


✅ Você não dependerá mais de MFA manual.

✅ O SSAS consegue processar seu cubo automaticamente, sem expiração a cada 2h.

✅ O controle de permissões continua centralizado no Azure AD.



O Problema: MFA + SSA


O SharePoint exige autenticação via Azure AD.Quando usamos conta organizacional + MFA, o token expira rapidamente.Isso faz sentido em termos de segurança para acessos manuais, mas é inviável em cenários de integração automatizada (como jobs do SSAS).

Precisamos de algo seguro, confiável e que não dependa de intervenção humana. É aí que entra o Service Principal.



O que é um Service Principal?


Um Service Principal é basicamente uma identidade de aplicação registrada no Azure AD.Ele funciona como um “usuário técnico” que pode se autenticar em recursos da Microsoft (como SharePoint e Graph API), mas sem exigir MFA.


Com isso, conseguimos:

  • Autenticação programática (Client ID + Client Secret).

  • Controle de permissões granular.

  • Tokens que podem ser renovados automaticamente pelo SSAS Mashup Engine.


Passo a Passo da Solução


➡️Criar o App Registration no Azure AD


  1. Vá no Azure Portal.

  2. Acesse Azure Active Directory → App registrations → New registration.

  3. Defina:

    • Nome: SSAS-SharePoint

    • Tipo de conta: Accounts in this organizational directory only

    • Redirecionamento: pode deixar vazio ou usar http://localhost

  4. Clique em Register.

📌 Anote o Tenant ID e o Client ID.



➡️ Gerar as Credenciais (Client Secret)


  1. No menu do App, vá em Certificates & secrets.

  2. Clique em New client secret.

  3. Escolha validade (6, 12 ou 24 meses).

  4. Copie o Value do segredo — ele não será exibido novamente.

📌 Agora você tem:

  • Tenant ID

  • Client ID

  • Client Secret


Anote o valor que aparece em "value" pois ele só aparece na hora da criação

ree





➡️ Configurar Permissões no SharePoint


  1. No App Registration, vá em API Permissions → Add a permission.

  2. Escolha SharePoint.

  3. Selecione Sites.Selected (recomendado) ou Sites.FullControl.All.

  4. Clique em Grant admin consent.

🔑 Isso garante que seu App terá permissão para acessar os sites/arquivos necessários.



➡️ Configurar no SSAS Tabular (Power Query)


Agora vamos ajustar a fonte de dados no modelo Tabular:

  1. Abra o Visual Studio (SSDT) com seu projeto Tabular.

  2. Vá em Model → Data Sources.

  3. Selecione a fonte que aponta para o SharePoint.

  4. Edite a conexão para usar OAuth2 via Service Principal.

Exemplo em Power Query (M):


let

Fonte = OData.Feed(

"https://sua-empresa.sharepoint.com/sites/seusite/_vti_bin/listdata.svc",

null,

[

Headers = [

Authorization = "Bearer " & AccessToken

]

]

)

in

Fonte




O AccessToken é gerado automaticamente pelo Mashup Engine do SSAS, desde que você configure a conexão como Service Principal.


➡️ Autenticar com Service Principal no SSDT


No Data Source Settings do Power Query dentro do Visual Studio:

  1. Escolha Organizational Account.

  2. Clique em Sign in with service principal.

  3. Informe:

    • Client ID

    • Client Secret

    • Tenant ID

Isso só funciona nas versões mais recentes do SSDT + SSAS 2019+, que possuem suporte ao modo Service Principal para SharePoint/OData.



Script PowerShell 


  1. obter um access token (app-only / client credentials) para o SharePoint Online usando o Client ID + Client Secret;

  2. validar a conexão fora do SSAS chamando a REST API do SharePoint.

incluí duas abordagens:

  • A. MSAL + Invoke-RestMethod (mais baixo nível, ótimo para entender o fluxo OAuth2)

  • B. PnP.PowerShell (alto nível, mais simples para testes rápidos)


A) MSAL + Invoke-RestMethod (app-only)

Requisitos: PowerShell 7+ recomendado (funciona no Windows PowerShell também). Módulo MSAL.PS (Install-Module MSAL.PS -Scope CurrentUser). O App Registration já deve ter Sites.Selected (recomendado) ou Sites.FullControl.All com Admin consent. O site do SharePoint precisa conceder acesso ao app (em cenários Sites.Selected). Veja “Dicas e solução de problemas” ao final.

  • Gera um access token para o SharePoint Online usando Client Credentials (Service Principal).

  • Chama dois endpoints:

    • _api/web → pega o título/URL do site (sanidade de conexão).

    • _api/web/lists/getbytitle('...')/items → lê itens de uma lista/biblioteca (validação de permissão).


# ============================

# VARIÁVEIS - AJUSTE AQUI

# ============================

$TenantId = "<seu-tenant-guid-ou-nomedoTenant.onmicrosoft.com>"

$ClientId = "<seu-client-id>"

$ClientSecret = "<seu-client-secret>" # mantenha em KeyVault/SecretStore em produção

$SiteUrl = "https://sua-empresa.sharepoint.com/sites/seusite"

$ListTitle = "Documents" # ou o nome da lista/biblioteca que deseja testar


# Resource / Scope para SharePoint Online (OAuth2 Client Credentials)

# IMPORTANTE: use o escopo '.default' para app-only

$SPOResourceScope = "https://sua-empresa.sharepoint.com/.default"


# ============================

# 1) Obter Access Token (MSAL)

# ============================

Import-Module MSAL.PS -ErrorAction Stop


$token = Get-MsalToken `

-ClientId $ClientId `

-TenantId $TenantId `

-ClientSecret ($ClientSecret | ConvertTo-SecureString -AsPlainText -Force) `

-Scopes $SPOResourceScope


if (-not $token -or -not $token.AccessToken) {

throw "Falha ao obter o access token. Verifique Tenant, ClientId/Secret e permissões do app."

}


Write-Host "Access Token obtido com sucesso. Expira em: $($token.ExpiresOn.UtcDateTime) UTC"


# ============================

# 2) Chamar a REST API do SPO

# ============================

# Exemplo 2.1: Ler o título do site

$headers = @{

"Authorization" = "Bearer $($token.AccessToken)"

"Accept" = "application/json;odata=nometadata"

}


$siteInfoEndpoint = "$SiteUrl/_api/web?$select=Title,Url"

$siteInfo = Invoke-RestMethod -Method GET -Uri $siteInfoEndpoint -Headers $headers


Write-Host "Site Title:" $siteInfo.Title

Write-Host "Site Url :" $siteInfo.Url


# Exemplo 2.2: Listar os primeiros itens de uma lista

# Endpoint moderno (recomendado) ao invés do antigo listdata.svc

$listItemsEndpoint = "$SiteUrl/_api/web/lists/getbytitle('$([uri]::EscapeDataString($ListTitle))')/items?$top=5"

$listItems = Invoke-RestMethod -Method GET -Uri $listItemsEndpoint -Headers $headers


Write-Host "Itens retornados:" ($listItems.value | Measure-Object).Count

$listItems.value | Select-Object Id, Title | Format-Table -AutoSize



B) PnP.PowerShell (mais simples)

Requisitos: Install-Module PnP.PowerShell -Scope CurrentUser Permissões e consentimentos do App já configurados (Sites.Selected/Sites.FullControl.All). Para Sites.Selected, é preciso conceder o app ao site (ver dicas abaixo).


# ============================

# VARIÁVEIS - AJUSTE AQUI

# ============================

$TenantId = "<seu-tenant-guid-ou-nomedoTenant.onmicrosoft.com>"

$ClientId = "<seu-client-id>"

$ClientSecret = "<seu-client-secret>"

$SiteUrl = "https://sua-empresa.sharepoint.com/sites/seusite"


# ============================

# 1) Conectar com App-Only

# ============================

Import-Module PnP.PowerShell -ErrorAction Stop


Connect-PnPOnline -Url $SiteUrl `

-ClientId $ClientId `

-ClientSecret $ClientSecret `

-Tenant $TenantId


# ============================

# 2) Testes rápidos

# ============================

# Título do Web

(Get-PnPWeb).Title


# Listas do site (primeiras 10)

Get-PnPList | Select-Object Title, ItemCount -First 10 | Format-Table -AutoSize


# Itens de uma lista específica (primeiros 5)

Get-PnPListItem -List "Documents" -PageSize 5 -Fields "ID","Title" |

Select-Object Id, @{n="Title";e={$_.FieldValues["Title"]}} |

Format-Table -AutoSize




Dicas & solução de problemas


  1. “Sites.Selected”

    • É a permissão mais segura (o app não tem acesso a todos os sites por padrão).

    • Você precisa conceder o app ao site específico (app-only site permission).

    • Com PnP.PowerShell, um tenant admin pode executar algo como:


    # Executar como admin do tenant

    # Concede permissão de leitura (ou write) ao App no site indicado

    Grant-PnPAzureADAppSitePermission `

    -AppId "<CLIENT_ID>" `

    -DisplayName "SSAS-SharePoint" `

    -Site "https://sua-empresa.sharepoint.com/sites/seusite" `

    -Permissions Write

    • Permissões válidas: Read, Write, FullControl.Depois, confirme com Get-PnPAzureADAppSitePermission.

  2. Escopo correto do token

  3. Erros 401/403

    • Verifique se o Admin consent foi concedido no App Registration.

    • Para Sites.Selected, confirme se o site realmente concedeu permissão ao app.

    • Confira se a URL do site está correta (inclusive o caminho /sites/...).

  4. Segredo expira

    • Prefira certificado no lugar de client secret para ambientes produtivos.

    • Se usar secret, acompanhe a validade e renove proativamente.

  5. Por que não usar listdata.svc?

    • É legado (OData v3). Prefira _api/... (REST moderno) ou Microsoft Graph (quando aplicável).



  • O mesmo App Registration e mesmo método de autenticação (Service Principal) usados aqui são os que você configura no SSDT (Data Source Settings → Sign in with service principal).

  • Testando com esses scripts, você valida fora do SSAS que:

    • As permissões do app no Azure AD estão corretas;

    • O site SharePoint aceita o app (especialmente com Sites.Selected);

    • O token está sendo emitido e permite leitura das listas/bibliotecas que o seu Power Query no modelo Tabular irá acessar.




health check completo em PowerShell para monitorar sua integração SSAS ⇄ SharePoint (via Service Principal).


  • valida emissão do access token (app-only);

  • consulta o _api/web e (opcional) uma lista do site;

  • mede latência;

  • grava log (arquivo + Windows Event Log);

  • retorna códigos de saída úteis para monitoramento (0=OK, 2=Degradado, 1=Falha);

  • dispara alerta por e-mail ou Teams (Webhook) quando falhar



Script: HealthCheck-SPO-SSAS.ps1

Pré-requisitos: PowerShell 7+ (recomendado) Install-Module MSAL.PS -Scope CurrentUser -Force App Registration com Sites.Selected (recomendado) ou Sites.FullControl.All + Admin consent Se usar Sites.Selected, garanta o grant no site (Grant-PnPAzureADAppSitePermission)


<#

.SYNOPSIS

Health check para SPO (SharePoint Online) com Service Principal (Client Credentials).


.DESCRIPTION

- Obtém token com MSAL (app-only)

- Chama _api/web e (opcional) uma lista

- Mede latência

- Loga em arquivo e Windows Event Log

- Alerta por e-mail/Teams (opcional)

- ExitCode: 0=OK | 2=Degradado | 1=Falha


.NOTES

Agende com o Agendador de Tarefas (schtasks) rodando como conta de serviço.

#>


param(

[Parameter(Mandatory=$true)]

[string]$TenantId, # ex: "contoso.onmicrosoft.com" ou GUID do tenant


[Parameter(Mandatory=$true)]

[string]$ClientId, # App (client) ID


[Parameter(Mandatory=$false)]

[string]$ClientSecret, # OU deixe vazio e use $Env:SPO_CLIENT_SECRET


[Parameter(Mandatory=$true)]


[Parameter(Mandatory=$false)]

[string]$TestListTitle = "", # ex: "Documents" (vazio = não testa lista)


[int]$WarnLatencyMs = 1500, # limiar de “degradado” (ms)

[int]$HttpTimeoutSec = 30,


[string]$LogFolder = "C:\Logs\SPO-Health",

[int]$MaxLogSizeMB = 10,


# Alerta por e-mail (opcional)

[string]$SmtpServer = "",

[int]$SmtpPort = 587,

[string]$MailFrom = "",

[string]$MailTo = "", # separadas por ';'

[string]$MailSubjectPrefix = "[ALERTA] SPO Health",

[string]$MailUser = "",

[string]$MailPassword = "", # ou use SecretManagement


# Alerta por Teams Webhook (opcional)

[string]$TeamsWebhookUrl = ""

)


# ============================

# Preparação

# ============================

$ErrorActionPreference = "Stop"

Import-Module MSAL.PS -ErrorAction Stop


if ([string]::IsNullOrWhiteSpace($ClientSecret)) {

$ClientSecret = $Env:SPO_CLIENT_SECRET

}


if ([string]::IsNullOrWhiteSpace($ClientSecret)) {

Write-Error "ClientSecret não informado (param ou env var SPO_CLIENT_SECRET)."

exit 1

}


# Logs

New-Item -ItemType Directory -Path $LogFolder -Force | Out-Null

$logFile = Join-Path $LogFolder ("SPO-Health_{0:yyyyMMdd}.log" -f (Get-Date))


function Write-Log {

param([string]$Message, [string]$Level = "INFO")

$line = "[{0:u}] [{1}] {2}" -f (Get-Date), $Level, $Message

$line | Tee-Object -FilePath $logFile -Append

}


# Roda log com rotação simples por tamanho

if (Test-Path $logFile) {

$sizeMB = (Get-Item $logFile).Length / 1MB

if ($sizeMB -ge $MaxLogSizeMB) {

$arch = $logFile.Replace(".log", "_{0:HHmmss}.log" -f (Get-Date))

Move-Item $logFile $arch -Force

}

}


# Windows Event Log

$eventSource = "SPO-HealthCheck"

$eventLogName = "Application"

if (-not [System.Diagnostics.EventLog]::SourceExists($eventSource)) {

New-EventLog -LogName $eventLogName -Source $eventSource

}


function Write-Event {

param([string]$Message, [int]$EventId = 5000, [ValidateSet("Information","Warning","Error")] [string]$EntryType = "Information")

Write-EventLog -LogName $eventLogName -Source $eventSource -EventId $EventId -EntryType $EntryType -Message $Message

}


# Alerta e-mail (opcional)

function Send-MailAlert {

param([string]$Subject, [string]$Body)

if ([string]::IsNullOrWhiteSpace($SmtpServer) -or [string]::IsNullOrWhiteSpace($MailTo) -or [string]::IsNullOrWhiteSpace($MailFrom)) { return }

try {

$secure = $null

if ($MailPassword) {

$secure = (ConvertTo-SecureString $MailPassword -AsPlainText -Force)

}

$cred = $null

if ($MailUser -and $secure) {

$cred = New-Object System.Management.Automation.PSCredential($MailUser, $secure)

}

$params = @{

SmtpServer = $SmtpServer

Port = $SmtpPort

From = $MailFrom

To = $MailTo.Split(";") | Where-Object { $_ -and $_.Trim() -ne "" }

Subject = "$MailSubjectPrefix - $Subject"

Body = $Body

BodyAsHtml = $true

}

if ($cred) { $params.Credential = $cred; $params.UseSsl = $true }

Send-MailMessage @params

Write-Log "Alerta por e-mail enviado."

}

catch {

Write-Log "Falha ao enviar e-mail: $($_.Exception.Message)" "WARN"

}

}


# Alerta Teams (opcional)

function Send-TeamsAlert {

param([string]$Title, [string]$Text)

if ([string]::IsNullOrWhiteSpace($TeamsWebhookUrl)) { return }

try {

$payload = @{

"@type" = "MessageCard"

"@context" = "http://schema.org/extensions"

"summary" = $Title

"themeColor" = "C4314B"

"title" = $Title

"text" = $Text

} | ConvertTo-Json -Depth 5

Invoke-RestMethod -Method Post -Uri $TeamsWebhookUrl -ContentType "application/json" -Body $payload

Write-Log "Alerta para Teams enviado."

}

catch {

Write-Log "Falha ao enviar alerta para Teams: $($_.Exception.Message)" "WARN"

}

}


# ============================

# 1) Token (app-only)

# ============================

try {

$scope = ($SiteUrl.TrimEnd('/').Split('/')[0..2] -join '/') + "/.default"

$sw = [System.Diagnostics.Stopwatch]::StartNew()


$token = Get-MsalToken `

-ClientId $ClientId `

-TenantId $TenantId `

-ClientSecret ($ClientSecret | ConvertTo-SecureString -AsPlainText -Force) `

-Scopes $scope


$sw.Stop()

$latencyTokenMs = [int]$sw.Elapsed.TotalMilliseconds


if (-not $token.AccessToken) { throw "AccessToken vazio." }


Write-Log ("Token OK. Expira às {0:u} (UTC). Latência: {1} ms" -f $token.ExpiresOn.UtcDateTime, $latencyTokenMs)

}

catch {

$msg = "Falha ao obter token: $($_.Exception.Message)"

Write-Log $msg "ERROR"

Write-Event $msg 5001 "Error"

Send-MailAlert "Falha ao obter token" "<pre>$msg</pre>"

Send-TeamsAlert "SPO Health - Falha ao obter token" $msg

exit 1

}


# ============================

# 2) _api/web (sanidade)

# ============================

$headers = @{

"Authorization" = "Bearer $($token.AccessToken)"

"Accept" = "application/json;odata=nometadata"

}

try {

$uriWeb = "$SiteUrl/_api/web?`$select=Title,Url"

$sw.Restart()

$web = Invoke-RestMethod -Method GET -Uri $uriWeb -Headers $headers -TimeoutSec $HttpTimeoutSec

$sw.Stop()

$latencyWebMs = [int]$sw.Elapsed.TotalMilliseconds


Write-Log ("_api/web OK. Title='{0}', Latência={1} ms" -f $web.Title, $latencyWebMs)

}

catch {

$msg = "Falha em _api/web: $($_.Exception.Message)"

Write-Log $msg "ERROR"

Write-Event $msg 5002 "Error"

Send-MailAlert "Falha em _api/web" "<pre>$msg</pre>"

Send-TeamsAlert "SPO Health - Falha em _api/web" $msg

exit 1

}


# ============================

# 3) Lista (opcional)

# ============================

$latencyListMs = $null

if ($TestListTitle) {

try {

$uriList = "$SiteUrl/_api/web/lists/getbytitle('$([uri]::EscapeDataString($TestListTitle))')/items?`$top=1&`$select=Id"

$sw.Restart()

$items = Invoke-RestMethod -Method GET -Uri $uriList -Headers $headers -TimeoutSec $HttpTimeoutSec

$sw.Stop()

$latencyListMs = [int]$sw.Elapsed.TotalMilliseconds

$count = ($items.value | Measure-Object).Count

Write-Log ("Lista '{0}' OK. Itens={1}, Latência={2} ms" -f $TestListTitle, $count, $latencyListMs)

}

catch {

$msg = "Falha ao consultar lista '$TestListTitle': $($_.Exception.Message)"

Write-Log $msg "ERROR"

Write-Event $msg 5003 "Error"

Send-MailAlert "Falha ao consultar lista" "<pre>$msg</pre>"

Send-TeamsAlert "SPO Health - Falha na lista" $msg

exit 1

}

}


# ============================

# 4) Avaliação & saída

# ============================

$worstLatency = @($latencyTokenMs, $latencyWebMs, ($latencyListMs | ForEach-Object { $_ })) | Where-Object { $_ } | Measure-Object -Maximum | Select-Object -ExpandProperty Maximum

$status = 0

$statusText = "OK"


if ($worstLatency -ge $WarnLatencyMs) {

$status = 2

$statusText = "DEGRADADO"

}


$summary = "SPO Health: $statusText | token=${latencyTokenMs}ms | web=${latencyWebMs}ms" + ($(if ($latencyListMs) { " | list=${latencyListMs}ms" } else { "" }))

Write-Log $summary

Write-Event $summary 5000 ($(if ($status -eq 0) { "Information" } elseif ($status -eq 2) { "Warning" } else { "Error" }))


# alerta só em falha total (exit 1). Em degradado, apenas event/warn + log.

if ($status -eq 1) {

Send-MailAlert "Falha geral" "<pre>$summary</pre>"

Send-TeamsAlert "SPO Health - Falha" $summary

}


exit $status



Como agendar no Windows (Agendador de Tarefas)


  1. Salvar o script:C:\Scripts\HealthCheck-SPO-SSAS.ps1

  2. (Opcional) Guardar segredo em variável de ambiente para não deixar no comando:

[Environment]::SetEnvironmentVariable("SPO_CLIENT_SECRET", "<seu-secret>", "Machine")
  1. Criar a tarefa (rodando como conta de serviço que tenha rede/perm):

schtasks /Create /TN "SPO-Health" /SC MINUTE /MO 5 ^
  /TR "pwsh -NoLogo -NonInteractive -File C:\Scripts\HealthCheck-SPO-SSAS.ps1 -TenantId contoso.onmicrosoft.com -ClientId <CLIENT_ID> -SiteUrl https://contoso.sharepoint.com/sites/finance -TestListTitle Documents -WarnLatencyMs 2000 -LogFolder C:\Logs\SPO-Health" ^
  /RU "DOMINIO\ContaDeServico" /RP "SenhaDaConta"
Dica: em produção, prefira Managed Service Account (gMSA) e armazene segredos no Windows Credential Manager, SecretManagement ou Azure Key Vault.

Como usar o retorno no seu monitor


  • ExitCode 0 → OK

  • ExitCode 2 → Degradado (latência alta)

  • ExitCode 1 → Falha (token ou chamadas HTTP)

Você pode capturar o código no seu orquestrador/SSIS/agent para acionar alertas.


© 2017-2025  Criado e desenvolvido por Amanda Nascimento

  • Discord
  • GitHub
  • youtube
  • LinkedIn Amanda
bottom of page