Automatizando a Conexão do SSAS com SharePoint Usando Service Principal
- 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
Vá no Azure Portal.
Acesse Azure Active Directory → App registrations → New registration.
Defina:
Nome: SSAS-SharePoint
Tipo de conta: Accounts in this organizational directory only
Redirecionamento: pode deixar vazio ou usar http://localhost
Clique em Register.
📌 Anote o Tenant ID e o Client ID.
➡️ Gerar as Credenciais (Client Secret)
No menu do App, vá em Certificates & secrets.
Clique em New client secret.
Escolha validade (6, 12 ou 24 meses).
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

➡️ Configurar Permissões no SharePoint
No App Registration, vá em API Permissions → Add a permission.
Escolha SharePoint.
Selecione Sites.Selected (recomendado) ou Sites.FullControl.All.
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:
Abra o Visual Studio (SSDT) com seu projeto Tabular.
Vá em Model → Data Sources.
Selecione a fonte que aponta para o SharePoint.
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:
Escolha Organizational Account.
Clique em Sign in with service principal.
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
obter um access token (app-only / client credentials) para o SharePoint Online usando o Client ID + Client Secret;
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
“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.
Escopo correto do token
Para SharePoint Online, use https://<sua-org>.sharepoint.com/.default no Get-MsalToken.
Se usar Graph somente (https://graph.microsoft.com/.default), muitos endpoints do SPO não funcionarão.
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/...).
Segredo expira
Prefira certificado no lugar de client secret para ambientes produtivos.
Se usar secret, acompanhe a validade e renove proativamente.
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)]
[string]$SiteUrl, # ex: https://contoso.sharepoint.com/sites/finance
[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)
Salvar o script:C:\Scripts\HealthCheck-SPO-SSAS.ps1
(Opcional) Guardar segredo em variável de ambiente para não deixar no comando:
[Environment]::SetEnvironmentVariable("SPO_CLIENT_SECRET", "<seu-secret>", "Machine")
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.


