// Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you 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") fmt.Fprintf(&sb, "Subject: %v\n", rvals[0]) if len(rvals) > 1 { fmt.Fprintf(&sb, "Object: %v\n", rvals[1]) } if len(rvals) > 2 { fmt.Fprintf(&sb, "Action: %v\n", rvals[2]) } fmt.Fprintf(&sb, "\nEnforcement Result: %v\n", result) // Add matched rules if len(matchedRules) > 0 { sb.WriteString("\nMatched Policy Rules:\n") for _, rule := range matchedRules { fmt.Fprintf(&sb, "- %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 { fmt.Fprintf(&sb, "Matcher (%s): %s\n", key, ast.Value) } } if eff, ok := e.model["e"]; ok { for key, ast := range eff { fmt.Fprintf(&sb, "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 { fmt.Fprintf(&sb, "- %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 }