repo_description: Apache Casbin: an authorization library that supports access control models like ACL, RBAC, ABAC.
repo_topics: abac access-control acl apache auth authentication authn authorization authz cas casbin iam oauth oidc permission rbac saml sso
code: // Copyright 2026 The casbin Authors. All Rights Reserved.
  //
  // Licensed under the Apache License, Version 2.0 (the "License");
  // you may not use this file except in compliance with the License.
  // You may obtain a copy of the License at
  //
  //      http://www.apache.org/licenses/LICENSE-2.0
  //
  // Unless required by applicable law or agreed to in writing, software
  // distributed under the License is distributed on an "AS IS" BASIS,
  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  // See the License for the specific language governing permissions and
  // limitations under the License.
  
  package casbin
  
  import (
  	"bytes"
  	"context"
  	"encoding/json"
  	"errors"
  	"fmt"
  	"io"
  	"net/http"
  	"strings"
  	"time"
  )
  
  // AIConfig contains configuration for AI API calls.
  type AIConfig struct {
  	// Endpoint is the API endpoint (e.g., "https://api.openai.com/v1/chat/completions")
  	Endpoint string
  	// APIKey is the authentication key for the API
  	APIKey string
  	// Model is the model to use (e.g., "gpt-3.5-turbo", "gpt-4")
  	Model string
  	// Timeout for API requests (default: 30s)
  	Timeout time.Duration
  }
  
  // aiMessage represents a message in the OpenAI chat format.
  type aiMessage struct {
  	Role    string `json:"role"`
  	Content string `json:"content"`
  }
  
  // aiChatRequest represents the request to OpenAI chat completions API.
  type aiChatRequest struct {
  	Model    string      `json:"model"`
  	Messages []aiMessage `json:"messages"`
  }
  
  // aiChatResponse represents the response from OpenAI chat completions API.
  type aiChatResponse struct {
  	Choices []struct {
  		Message aiMessage `json:"message"`
  	} `json:"choices"`
  	Error *struct {
  		Message string `json:"message"`
  	} `json:"error,omitempty"`
  }
  
  // SetAIConfig sets the configuration for AI API calls.
  func (e *Enforcer) SetAIConfig(config AIConfig) {
  	if config.Timeout == 0 {
  		config.Timeout = 30 * time.Second
  	}
  	e.aiConfig = config
  }
  
  // Explain returns an AI-generated explanation of why Enforce returned a particular result.
  // It calls the configured OpenAI-compatible API to generate a natural language explanation.
  func (e *Enforcer) Explain(rvals ...interface{}) (string, error) {
  	if e.aiConfig.Endpoint == "" {
  		return "", errors.New("AI config not set, use SetAIConfig first")
  	}
  
  	// Get enforcement result and matched rules
  	result, matchedRules, err := e.EnforceEx(rvals...)
  	if err != nil {
  		return "", fmt.Errorf("failed to enforce: %w", err)
  	}
  
  	// Build context for AI
  	explainContext := e.buildExplainContext(rvals, result, matchedRules)
  
  	// Call AI API
  	explanation, err := e.callAIAPI(explainContext)
  	if err != nil {
  		return "", fmt.Errorf("failed to get AI explanation: %w", err)
  	}
  
  	return explanation, nil
  }
  
  // buildExplainContext builds the context string for AI explanation.
  func (e *Enforcer) buildExplainContext(rvals []interface{}, result bool, matchedRules []string) string {
  	var sb strings.Builder
  
  	// Add request information
  	sb.WriteString("Authorization Request:\n")
  	sb.WriteString(fmt.Sprintf("Subject: %v\n", rvals[0]))
  	if len(rvals) > 1 {
  		sb.WriteString(fmt.Sprintf("Object: %v\n", rvals[1]))
  	}
  	if len(rvals) > 2 {
  		sb.WriteString(fmt.Sprintf("Action: %v\n", rvals[2]))
  	}
  	sb.WriteString(fmt.Sprintf("\nEnforcement Result: %v\n", result))
  
  	// Add matched rules
  	if len(matchedRules) > 0 {
  		sb.WriteString("\nMatched Policy Rules:\n")
  		for _, rule := range matchedRules {
  			sb.WriteString(fmt.Sprintf("- %s\n", rule))
  		}
  	} else {
  		sb.WriteString("\nNo policy rules matched.\n")
  	}
  
  	// Add model information
  	sb.WriteString("\nAccess Control Model:\n")
  	if m, ok := e.model["m"]; ok {
  		for key, ast := range m {
  			sb.WriteString(fmt.Sprintf("Matcher (%s): %s\n", key, ast.Value))
  		}
  	}
  	if eff, ok := e.model["e"]; ok {
  		for key, ast := range eff {
  			sb.WriteString(fmt.Sprintf("Effect (%s): %s\n", key, ast.Value))
  		}
  	}
  
  	// Add all policies
  	policies, _ := e.GetPolicy()
  	if len(policies) > 0 {
  		sb.WriteString("\nAll Policy Rules:\n")
  		for _, policy := range policies {
  			sb.WriteString(fmt.Sprintf("- %s\n", strings.Join(policy, ", ")))
  		}
  	}
  
  	return sb.String()
  }
  
  // callAIAPI calls the configured AI API to get an explanation.
  func (e *Enforcer) callAIAPI(explainContext string) (string, error) {
  	// Prepare the request
  	messages := []aiMessage{
  		{
  			Role: "system",
  			Content: "You are an expert in access control and authorization systems. " +
  				"Explain why an authorization request was allowed or denied based on the " +
  				"provided access control model, policies, and enforcement result. " +
  				"Be clear, concise, and educational.",
  		},
  		{
  			Role:    "user",
  			Content: fmt.Sprintf("Please explain the following authorization decision:\n\n%s", explainContext),
  		},
  	}
  
  	reqBody := aiChatRequest{
  		Model:    e.aiConfig.Model,
  		Messages: messages,
  	}
  
  	jsonData, err := json.Marshal(reqBody)
  	if err != nil {
  		return "", fmt.Errorf("failed to marshal request: %w", err)
  	}
  
  	// Create HTTP request with context
  	reqCtx, cancel := context.WithTimeout(context.Background(), e.aiConfig.Timeout)
  	defer cancel()
  
  	req, err := http.NewRequestWithContext(reqCtx, http.MethodPost, e.aiConfig.Endpoint, bytes.NewBuffer(jsonData))
  	if err != nil {
  		return "", fmt.Errorf("failed to create request: %w", err)
  	}
  
  	req.Header.Set("Content-Type", "application/json")
  	req.Header.Set("Authorization", "Bearer "+e.aiConfig.APIKey)
  
  	// Execute request
  	client := &http.Client{}
  
  	resp, err := client.Do(req)
  	if err != nil {
  		return "", fmt.Errorf("failed to execute request: %w", err)
  	}
  	defer resp.Body.Close()
  
  	// Read response
  	body, err := io.ReadAll(resp.Body)
  	if err != nil {
  		return "", fmt.Errorf("failed to read response: %w", err)
  	}
  
  	// Parse response
  	var chatResp aiChatResponse
  	if err := json.Unmarshal(body, &chatResp); err != nil {
  		return "", fmt.Errorf("failed to parse response: %w", err)
  	}
  
  	// Check for API errors
  	if chatResp.Error != nil {
  		return "", fmt.Errorf("API error: %s", chatResp.Error.Message)
  	}
  
  	if resp.StatusCode != http.StatusOK {
  		return "", fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(body))
  	}
  
  	// Extract explanation
  	if len(chatResp.Choices) == 0 {
  		return "", errors.New("no response from AI")
  	}
  
  	return chatResp.Choices[0].Message.Content, nil
  }
  