Skip to content

PaloAltoNetworks/scm-go

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

38 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

scm-go

SDK for interacting with Strata Cloud Manager.

GoDoc

NOTE: This sdk code is auto-generated.


Beta Release Disclaimer

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.

By using this software, you agree to these terms.

Warranty

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.

Using scm-go

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))
		}
	}
}

JWT Token Caching for Concurrent Operations

Overview

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.

How It Works

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.

Example Token Caching Implementations

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.

Architecture Overview

┌─────────────────────────────────────────────────────────────────────────┐
│                       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:

  1. 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)
  2. Shared Config File (/var/cache/scm-config.json or similar)

    • Contains client_id, client_secret, and cached jwt fields
    • Updated atomically by token cache service
    • Read by all SDK client instances
  3. 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.

Example 1: Go Token Cache Service

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

Example 2: Python 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

Example 3: Simple Bash Token Cache

#!/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"

Best Practices

  1. Security Isolation: Each unique client_id/client_secret pair should have its own token cache file
  2. File Permissions: Restrict cache file access (e.g., chmod 600)
  3. Expiration Buffer: Always subtract 60-120 seconds from token lifetime to avoid race conditions
  4. Atomic Writes: Write to temporary file then rename to avoid partial reads
  5. Error Handling: Have fallback logic if cache is corrupted or inaccessible
  6. Monitoring: Log token refreshes to detect authentication issues early

Additional Resources

About

SDK for Strata Cloud Manager

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Packages

No packages published

Contributors 5

Languages