SDK for interacting with Strata Cloud Manager.
NOTE: This sdk code is auto-generated.
This software is a pre-release version and is not ready for production use.
- No Warranty: This software is provided "as is," without any warranty of any kind, either expressed or implied, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose.
- Instability: The beta software may contain defects, may not operate correctly, and may be substantially modified or withdrawn at any time.
- Limitation of Liability: In no event shall the authors or copyright holders be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the beta software or the use or other dealings in the beta software.
- Feedback: We encourage and appreciate your feedback and bug reports. However, you acknowledge that any feedback you provide is non-confidential.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
THIS SOFTWARE IS RELEASED AS A PROOF OF CONCEPT FOR EXPERIMENTAL PURPOSES ONLY. USE IT AT OWN RISK. THIS SOFTWARE IS NOT SUPPORTED.
In the project root scm-go, populate scm-config.json with the relevant parameters for auth_url, client_id, client_secret, host, protocol, scope etc.
{
"auth_url": "",
"client_id": "",
"client_secret": "",
"host": "",
"logging": "quiet",
"protocol": "https",
"scope": "",
"skip_verify_certificate": false
}
Then you can write a go program to test out the authentication. There are tests provided in the tests directory for convenience. Error handling below is omitted for brevity.
package main
import (
"context"
"fmt"
"net/http"
setup "github.com/paloaltonetworks/scm-go"
"github.com/paloaltonetworks/scm-go/common"
"github.com/paloaltonetworks/scm-go/generated/objects"
)
func main() {
configPath := common.GetConfigPath()
setupClient := &setup.Client{
AuthFile: configPath,
CheckEnvironment: false,
}
fmt.Printf("Using config file: %s\n", setupClient.AuthFile)
// Setup the client configuration
err := setupClient.Setup()
if err != nil {
fmt.Printf("Error setting up client: %v\n", err)
return
}
// Refresh JWT token
ctx := context.Background()
err = setupClient.RefreshJwt(ctx)
if setupClient.Jwt != "" {
fmt.Printf("JWT token obtained (first 20 chars): %s...\n", setupClient.Jwt[:min(20, len(setupClient.Jwt))])
} else {
fmt.Println("WARNING: JWT token is empty!")
}
// Create the network_services API client
config := objects.NewConfiguration()
config.Host = setupClient.GetHost()
config.Scheme = "https"
// Create a custom HTTP client that includes the JWT token and logging
if setupClient.HttpClient == nil {
setupClient.HttpClient = &http.Client{}
}
// Wrap the transport with our logging transport
if setupClient.HttpClient.Transport == nil {
setupClient.HttpClient.Transport = http.DefaultTransport
}
setupClient.HttpClient.Transport = &common.LoggingRoundTripper{
Wrapped: setupClient.HttpClient.Transport,
}
config.HTTPClient = setupClient.HttpClient
// Set up the default header with JWT
config.DefaultHeader = make(map[string]string)
config.DefaultHeader["Authorization"] = "Bearer " + setupClient.Jwt
config.DefaultHeader["x-auth-jwt"] = setupClient.Jwt
fmt.Printf("Authorization header: Bearer %s...\n", setupClient.Jwt[:min(10, len(setupClient.Jwt))])
fmt.Printf("Host: %s\n", config.Host)
apiClient := objects.NewAPIClient(config)
reqCreate := apiClient.AddressesAPI.ListAddresses(context.Background()).Folder("All")
createRes, httpResp, err := reqCreate.Execute()
if err != nil {
fmt.Printf("Error Listing Addresses: %v\n", err)
if httpResp != nil {
fmt.Printf("HTTP Status: %d\n", httpResp.StatusCode)
}
return
}
if httpResp != nil {
fmt.Printf("HTTP Status: %d\n", httpResp.StatusCode)
}
// Print the first address from the response
if createRes != nil && createRes.Data != nil && len(createRes.Data) > 0 {
firstAddress := createRes.Data[0]
fmt.Printf("First address found:\n")
fmt.Printf(" Name: %s\n", firstAddress.Name)
if firstAddress.Fqdn != nil {
fmt.Printf(" FQDN: %s\n", *firstAddress.Fqdn)
}
if firstAddress.IpNetmask != nil {
fmt.Printf(" IP/Netmask: %s\n", *firstAddress.IpNetmask)
}
if firstAddress.IpRange != nil {
fmt.Printf(" IP Range: %s\n", *firstAddress.IpRange)
}
} else {
fmt.Println("No addresses found in the response")
if createRes != nil {
fmt.Printf("Total addresses returned: %d\n", len(createRes.Data))
}
}
}The Strata Cloud Manager authentication API has rate limits on token requests (approximately 10 concurrent requests per tenant). When running multiple concurrent operations (e.g., parallel Terraform runs, CI/CD pipelines), these rate limits can cause authentication failures.
To work around this limitation, you can implement a token caching solution that allows multiple client instances to share the same JWT token.
Starting from version 1.0.8, the scm-go client supports passing JWT tokens via the configuration file. The following fields can now be included in your scm-config.json:
{
"client_id": "your-client-id",
"client_secret": "your-client-secret",
"host": "api.strata.paloaltonetworks.com",
"protocol": "https",
"scope": "tsg_id:1234567890",
"jwt": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"jwt_expires_at": "2026-01-21T10:30:00Z",
"jwt_lifetime": 900
}Important Security Note: Only share JWT tokens among client instances that use the same client_id and client_secret. Different service principals with different RBAC permissions should never share tokens, as this would be a privilege escalation risk.
Below are sample implementations of token caching services. These are provided as examples only and should be adapted to your specific security requirements and infrastructure.
┌─────────────────────────────────────────────────────────────────────────┐
│ Token Caching Architecture │
└─────────────────────────────────────────────────────────────────────────┘
┌──────────────────────┐
│ SCM Auth API │
│ (Rate Limited ~10 │
│ concurrent requests)│
└──────────┬───────────┘
│
│ 1. Fetch JWT Token
│ (Once every 10-12 min)
│
┌──────────▼───────────┐
│ Token Cache Service │
│ (Cron Job/Timer) │
│ │
│ • Checks expiration │
│ • Fetches new token │
│ • Updates auth file │
└──────────┬───────────┘
│
│ 2. Write (Atomic)
│ jwt + jwt_expires_at
│
┌───────────────▼────────────────┐
│ Shared Config File │
│ /var/cache/scm-config.json │
│ │
│ { │
│ "client_id": "...", │
│ "client_secret": "...", │
│ "jwt": "eyJ...", │
│ "jwt_expires_at": "...", │
│ "jwt_lifetime": 900 │
│ } │
└─┬────────┬────────┬────────┬───┘
│ │ │ │
3. Read JWT │ │ │ │ 3. Read JWT
(No API │ │ │ │ (No API
call) │ │ │ │ call)
│ │ │ │
┌─────────────▼──┐ ┌──▼────────▼──┐ ┌─▼─────────────┐
│ SDK Client │ │ SDK Client │ │ SDK Client │
│ Instance #1 │ │ Instance #2 │ │ Instance #N │
│ │ │ │ │ │
│ Go App / │ │ Python App /│ │ Terraform / │
│ Script / │ │ Script / │ │ CI-CD Job / │
│ Terraform │ │ Terraform │ │ Parallel Run │
└─────────────────┘ └──────────────┘ └───────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ Benefits: │
│ • Only 1 auth request per tenant every 10-12 minutes │
│ • Supports unlimited concurrent SDK client instances │
│ • No rate limit errors during parallel operations │
│ • Cached token shared safely (read-only for client instances) │
└─────────────────────────────────────────────────────────────────────────┘
How It Works:
-
Token Cache Service (cron job/systemd timer) runs every 10-12 minutes
- Checks if cached token is expired or expiring soon (60s buffer)
- Fetches new JWT token from SCM Auth API if needed
- Writes updated token to shared config file (atomic write operation)
-
Shared Config File (
/var/cache/scm-config.jsonor similar)- Contains
client_id,client_secret, and cachedjwtfields - Updated atomically by token cache service
- Read by all SDK client instances
- Contains
-
Multiple SDK Client Instances (concurrent operations)
- Each instance reads the shared config file on initialization
- Uses cached JWT token (no API call needed)
- Can run unlimited concurrent operations without hitting rate limits
- All instances must use the same
client_id/client_secret
Disclaimer: This example code is provided "as is" without warranty. It is intended as a reference implementation only. You are responsible for ensuring it meets your organization's security and operational requirements.
package main
import (
"context"
"encoding/json"
"log"
"os"
"time"
scm "github.com/paloaltonetworks/scm-go"
)
// Config represents the SCM configuration with JWT caching
type Config struct {
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
Host string `json:"host"`
Protocol string `json:"protocol"`
Scope string `json:"scope"`
JWT string `json:"jwt,omitempty"`
JWTExpiresAt time.Time `json:"jwt_expires_at,omitempty"`
JWTLifetime int64 `json:"jwt_lifetime,omitempty"`
AuthUrl string `json:"auth_url"`
}
func main() {
// Use absolute path for cron compatibility
configPath := os.Getenv("SCM_CONFIG_PATH")
if configPath == "" {
configPath := "/path/to/scm-config.json"
}
// Load existing config
config, err := loadConfig(configPath)
if err != nil {
log.Fatalf("Failed to load config: %v", err)
}
// Check if token needs refresh (5 minute buffer)
needsRefresh := config.JWT == "" || time.Now().After(config.JWTExpiresAt.Add(-300*time.Second))
if needsRefresh {
log.Println("Token expired or missing, fetching new token...")
// Create SCM client (will fetch new token)
client := &scm.Client{
ClientId: config.ClientID,
ClientSecret: config.ClientSecret,
Host: config.Host,
Protocol: config.Protocol,
Scope: config.Scope,
AuthUrl: config.AuthUrl,
CheckEnvironment: false, // Don't check env vars
}
if err := client.Setup(); err != nil {
log.Fatalf("Failed to setup client: %v", err)
}
// Get JWT token
if err := client.RefreshJwt(context.Background()); err != nil {
log.Fatalf("Failed to refresh JWT: %v", err)
}
// Update config with new token
config.JWT = client.Jwt
config.JWTExpiresAt = client.JwtExpiresAt
config.JWTLifetime = client.JwtLifetime
// Save updated config atomically
if err := saveConfigAtomic(configPath, config); err != nil {
log.Fatalf("Failed to save config: %v", err)
}
log.Printf("Token refreshed, expires at: %s\n", config.JWTExpiresAt)
} else {
log.Printf("Using cached token, expires at: %s\n", config.JWTExpiresAt)
}
}
func loadConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var config Config
if err := json.Unmarshal(data, &config); err != nil {
return nil, err
}
return &config, nil
}
func saveConfigAtomic(path string, config *Config) error {
data, err := json.MarshalIndent(config, "", " ")
if err != nil {
return err
}
// Write to temp file first
tmpPath := path + ".tmp"
if err := os.WriteFile(tmpPath, data, 0600); err != nil {
return err
}
// Atomic rename
return os.Rename(tmpPath, path)
}Usage: Run this as a cron job or systemd timer every 10-12 minutes to keep tokens fresh.
# Run every 10 minutes
*/10 * * * * /path/to/token-cache-service#!/usr/bin/env python3
"""
SCM Token Cache Service
Fetches and caches JWT tokens for concurrent SCM operations
"""
import json
import os
import sys
import tempfile
from datetime import datetime, timedelta
from pathlib import Path
from scm import Scm
def load_config(config_path: str) -> dict:
"""Load configuration from JSON file"""
with open(config_path, 'r') as f:
return json.load(f)
def save_config_atomic(config_path: str, config: dict) -> None:
"""Save configuration atomically with proper permissions"""
config_dir = os.path.dirname(config_path)
# Write to temporary file first
with tempfile.NamedTemporaryFile(
mode='w',
dir=config_dir,
delete=False,
prefix='.scm-config-',
suffix='.tmp'
) as tmp_file:
json.dump(config, tmp_file, indent=2)
tmp_path = tmp_file.name
# Set restrictive permissions
os.chmod(tmp_path, 0o600)
# Atomic rename
os.rename(tmp_path, config_path)
def needs_refresh(config: dict, buffer_seconds: int = 60) -> bool:
"""Check if token needs refresh"""
if not config.get('jwt') or not config.get('jwt_expires_at'):
return True
try:
expires_at = datetime.fromisoformat(
config['jwt_expires_at'].replace('Z', '+00:00')
)
# Refresh if expiring within buffer period
return datetime.now(expires_at.tzinfo) >= (
expires_at - timedelta(seconds=buffer_seconds)
)
except (ValueError, TypeError):
return True
def main():
config_path = os.path.expanduser("~/.scm/config.json")
# Ensure config directory exists
Path(config_path).parent.mkdir(parents=True, exist_ok=True, mode=0o700)
# Load config
try:
config = load_config(config_path)
except FileNotFoundError:
print(f"Error: Config file not found: {config_path}", file=sys.stderr)
sys.exit(1)
# Check if refresh needed
if needs_refresh(config):
print("Token expired or missing, fetching new token...")
# Create SCM client (will fetch new token)
client = Scm(
client_id=config.get('client_id'),
client_secret=config.get('client_secret'),
tsg_id=config.get('tsg_id') or config.get('scope', '').replace('tsg_id:', ''),
host=config.get('host', 'api.sase.paloaltonetworks.com'),
)
# Get token details
token = client.access_token
expires_at = client._token_expires_at
lifetime = client._jwt_lifetime
# Update config
config['jwt'] = token
config['jwt_expires_at'] = expires_at.isoformat() if expires_at else None
config['jwt_lifetime'] = lifetime
# Save atomically
save_config_atomic(config_path, config)
print(f"Token refreshed, expires at: {expires_at}")
else:
print(f"Using cached token, expires at: {config['jwt_expires_at']}")
if __name__ == '__main__':
main()Usage: Run this as a cron job every 10-12 minutes to keep tokens fresh.
# Run every 10 minutes
*/10 * * * * /usr/bin/python3 /path/to/token_cache_service.py#!/bin/bash
# Simple token cache updater using scm-go binary
CONFIG_FILE="${HOME}/.scm/scm-config.json"
GO_CACHE_TOOL="/path/to/scm-token-refresh" # Build from Example 1
# Check if token needs refresh
if [ -f "$CONFIG_FILE" ]; then
EXPIRES_AT=$(jq -r '.jwt_expires_at // empty' "$CONFIG_FILE")
if [ -n "$EXPIRES_AT" ]; then
EXPIRES_EPOCH=$(date -d "$EXPIRES_AT" +%s 2>/dev/null || date -j -f "%Y-%m-%dT%H:%M:%S" "${EXPIRES_AT:0:19}" +%s 2>/dev/null)
NOW_EPOCH=$(date +%s)
BUFFER=120 # 2 minute buffer
if [ $((EXPIRES_EPOCH - NOW_EPOCH)) -gt $BUFFER ]; then
echo "Token still valid, expires at: $EXPIRES_AT"
exit 0
fi
fi
fi
echo "Refreshing token..."
"$GO_CACHE_TOOL"- Security Isolation: Each unique
client_id/client_secretpair should have its own token cache file - File Permissions: Restrict cache file access (e.g.,
chmod 600) - Expiration Buffer: Always subtract 60-120 seconds from token lifetime to avoid race conditions
- Atomic Writes: Write to temporary file then rename to avoid partial reads
- Error Handling: Have fallback logic if cache is corrupted or inaccessible
- Monitoring: Log token refreshes to detect authentication issues early