Files
query-orchestration/cmd/auth_related/permit.setup/setup_permit.go
T
Jay Brown 6dccf494f8 Merged in feature/permit-policy-cleanup (pull request #210)
cleanup permit policies and correct documentation

* docs

* policy cleanup

* refactor

* Merge remote-tracking branch 'origin/feature/permit-policy-cleanup' into feature/permit-policy-cleanup

* docs and edits
2026-02-19 20:22:59 +00:00

603 lines
19 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// setup_permit.go - CLI tool to configure Permit.io roles and resources
// This tool reads a YAML configuration file and creates resources, actions, and roles
// in a Permit.io project/environment using the Permit.io REST API.
//
// Usage:
//
// PERMIT_API_KEY=<key> go run setup_permit.go -config permit_policies.yaml -project <proj_id> -env <env_id>
package main
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
"gopkg.in/yaml.v3"
)
// Config structures matching the YAML file
type Config struct {
Resources []Resource `yaml:"resources"`
Roles []Role `yaml:"roles"`
}
type Resource struct {
Name string `yaml:"name"`
Description string `yaml:"description"`
Actions []string `yaml:"actions"`
}
type Role struct {
Name string `yaml:"name"`
Description string `yaml:"description"`
Permissions map[string][]string `yaml:"permissions"`
Notes string `yaml:"notes"`
}
// Permit.io API request/response types
type ResourceCreateRequest struct {
Key string `json:"key"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Actions map[string]ActionDef `json:"actions"`
}
type ResourceUpdateRequest struct {
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
Actions map[string]ActionDef `json:"actions"`
}
type ActionDef struct {
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
}
type RoleCreateRequest struct {
Key string `json:"key"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Permissions []string `json:"permissions,omitempty"`
}
type RoleUpdateRequest struct {
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
Permissions []string `json:"permissions,omitempty"`
}
func main() {
// Command-line flags
configFile := flag.String("config", "permit_policies.yaml", "Path to YAML config file")
projectID := flag.String("project", "", "Permit.io project ID or key")
envID := flag.String("env", "", "Permit.io environment ID or key")
dryRun := flag.Bool("dry-run", false, "Print what would be done without making changes")
listProjects := flag.Bool("list", false, "List available projects and environments")
flag.Parse()
// Get API key from environment
apiKey := os.Getenv("PERMIT_API_KEY")
if apiKey == "" {
fmt.Fprintf(os.Stderr, "Error: PERMIT_API_KEY environment variable must be set\n")
os.Exit(1)
}
// Handle list command
if *listProjects {
client := &http.Client{Timeout: 30 * time.Second}
baseURL := "https://api.permit.io"
listProjectsAndEnvironments(client, baseURL, apiKey)
return
}
// Validate required parameters
if *projectID == "" || *envID == "" {
fmt.Fprintf(os.Stderr, "Error: -project and -env are required\n")
fmt.Fprintf(os.Stderr, "Usage: PERMIT_API_KEY=<key> %s -config <yaml> -project <proj_id> -env <env_id>\n", os.Args[0])
fmt.Fprintf(os.Stderr, "\nTo list available projects and environments:\n")
fmt.Fprintf(os.Stderr, " PERMIT_API_KEY=<key> %s -list\n", os.Args[0])
os.Exit(1)
}
// Read and parse YAML config
config, err := loadConfig(*configFile)
if err != nil {
fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err)
os.Exit(1)
}
// Create HTTP client
client := &http.Client{Timeout: 30 * time.Second}
baseURL := "https://api.permit.io"
fmt.Printf("Setting up Permit.io configuration for project=%s, env=%s\n", *projectID, *envID)
fmt.Printf("Dry run: %v\n\n", *dryRun)
// Step 1: Create resources and their actions
fmt.Println("=" + strings.Repeat("=", 70))
fmt.Println("STEP 1: Creating Resources and Actions")
fmt.Println("=" + strings.Repeat("=", 70))
hasErrors := false
for _, resource := range config.Resources {
if err := createResource(client, baseURL, apiKey, *projectID, *envID, resource, *dryRun); err != nil {
fmt.Fprintf(os.Stderr, "\n❌ Error creating resource '%s': %v\n", resource.Name, err)
hasErrors = true
// If we get a 404 on the first resource, the project/env is wrong - fail fast
if strings.Contains(err.Error(), "404") && strings.Contains(err.Error(), "could not find") {
fmt.Fprintf(os.Stderr, "\n💡 Project or environment not found. Use -list to see available options.\n")
os.Exit(1)
}
}
}
// Step 2: Create roles with permissions
fmt.Println("\n" + strings.Repeat("=", 70))
fmt.Println("STEP 2: Creating Roles with Permissions")
fmt.Println("=" + strings.Repeat("=", 70))
for _, role := range config.Roles {
if err := createRole(client, baseURL, apiKey, *projectID, *envID, role, *dryRun); err != nil {
fmt.Fprintf(os.Stderr, "\n❌ Error creating role '%s': %v\n", role.Name, err)
hasErrors = true
}
}
fmt.Println("\n" + strings.Repeat("=", 70))
if hasErrors {
fmt.Println("⚠️ Setup completed with errors")
fmt.Println("=" + strings.Repeat("=", 70))
os.Exit(1)
} else {
fmt.Println("✅ Setup Complete!")
fmt.Println("=" + strings.Repeat("=", 70))
if *dryRun {
fmt.Println("NOTE: This was a dry run. No changes were made.")
fmt.Println("Run without -dry-run flag to apply changes.")
}
// Print IDs for use in other scripts
fmt.Println("\n# Export these for use in other scripts:")
fmt.Printf("PERMIT_PROJECT_ID=%s\n", *projectID)
fmt.Printf("PERMIT_ENVIRONMENT_ID=%s\n", *envID)
}
}
// loadConfig reads and parses the YAML configuration file
func loadConfig(filepath string) (*Config, error) {
data, err := os.ReadFile(filepath)
if err != nil {
return nil, fmt.Errorf("failed to read config file: %w", err)
}
var config Config
if err := yaml.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("failed to parse YAML: %w", err)
}
return &config, nil
}
// createResource creates or updates a resource with its actions in Permit.io (idempotent)
func createResource(client *http.Client, baseURL, apiKey, projectID, envID string, resource Resource, dryRun bool) error {
fmt.Printf("\nConfiguring resource: %s\n", resource.Name)
fmt.Printf(" Description: %s\n", resource.Description)
fmt.Printf(" Actions: %v\n", resource.Actions)
if dryRun {
fmt.Println(" [DRY RUN] Would create/update resource")
return nil
}
// Build actions map
actions := make(map[string]ActionDef)
for _, action := range resource.Actions {
actions[action] = ActionDef{Name: action}
}
// Check if resource exists
exists, err := resourceExists(client, baseURL, apiKey, projectID, envID, resource.Name)
if err != nil {
return err
}
if exists {
return updateResourceAPI(client, baseURL, apiKey, projectID, envID, resource.Name, resource.Description, actions)
}
return createResourceAPI(client, baseURL, apiKey, projectID, envID, resource.Name, resource.Description, actions)
}
// resourceExists checks if a resource already exists in Permit.io
func resourceExists(client *http.Client, baseURL, apiKey, projectID, envID, resourceName string) (bool, error) {
url := fmt.Sprintf("%s/v2/schema/%s/%s/resources/%s", baseURL, projectID, envID, resourceName)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return false, fmt.Errorf("error creating GET request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return false, fmt.Errorf("error making GET request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
return true, nil
}
if resp.StatusCode == http.StatusNotFound {
return false, nil
}
body, _ := io.ReadAll(resp.Body)
return false, fmt.Errorf("unexpected response (status %d): %s", resp.StatusCode, string(body))
}
// updateResourceAPI updates an existing resource in Permit.io
func updateResourceAPI(client *http.Client, baseURL, apiKey, projectID, envID, name, description string, actions map[string]ActionDef) error {
fmt.Println(" ️ Resource exists, updating...")
payload := ResourceUpdateRequest{
Name: name,
Description: description,
Actions: actions,
}
jsonData, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("error marshaling update data: %w", err)
}
url := fmt.Sprintf("%s/v2/schema/%s/%s/resources/%s", baseURL, projectID, envID, name)
req, err := http.NewRequest("PATCH", url, bytes.NewBuffer(jsonData))
if err != nil {
return fmt.Errorf("error creating PATCH request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("error making PATCH request: %w", err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
return fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(body))
}
fmt.Println(" ✓ Resource updated successfully")
return nil
}
// createResourceAPI creates a new resource in Permit.io
func createResourceAPI(client *http.Client, baseURL, apiKey, projectID, envID, name, description string, actions map[string]ActionDef) error {
fmt.Println(" ️ Resource does not exist, creating...")
payload := ResourceCreateRequest{
Key: name,
Name: name,
Description: description,
Actions: actions,
}
jsonData, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("error marshaling resource data: %w", err)
}
url := fmt.Sprintf("%s/v2/schema/%s/%s/resources", baseURL, projectID, envID)
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
if err != nil {
return fmt.Errorf("error creating POST request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("error making POST request: %w", err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
return fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(body))
}
fmt.Println(" ✓ Resource created successfully")
return nil
}
// createRole creates or updates a role with its permissions in Permit.io (idempotent)
func createRole(client *http.Client, baseURL, apiKey, projectID, envID string, role Role, dryRun bool) error {
fmt.Printf("\nConfiguring role: %s\n", role.Name)
fmt.Printf(" Description: %s\n", role.Description)
// Build permissions list in format "resource:action"
permissions := buildPermissionsList(role.Permissions)
fmt.Printf(" Permissions: %v\n", permissions)
if dryRun {
fmt.Println(" [DRY RUN] Would create/update role")
return nil
}
// Check if role exists
exists, err := roleExists(client, baseURL, apiKey, projectID, envID, role.Name)
if err != nil {
return err
}
if exists {
return updateRoleAPI(client, baseURL, apiKey, projectID, envID, role.Name, role.Description, permissions)
}
return createRoleAPI(client, baseURL, apiKey, projectID, envID, role.Name, role.Description, permissions)
}
// buildPermissionsList converts role permissions map to permission strings
func buildPermissionsList(permissions map[string][]string) []string {
var result []string
for resourceName, actions := range permissions {
for _, action := range actions {
result = append(result, fmt.Sprintf("%s:%s", resourceName, action))
}
}
return result
}
// roleExists checks if a role already exists in Permit.io
func roleExists(client *http.Client, baseURL, apiKey, projectID, envID, roleName string) (bool, error) {
url := fmt.Sprintf("%s/v2/schema/%s/%s/roles/%s", baseURL, projectID, envID, roleName)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return false, fmt.Errorf("error creating GET request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return false, fmt.Errorf("error making GET request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
return true, nil
}
if resp.StatusCode == http.StatusNotFound {
return false, nil
}
body, _ := io.ReadAll(resp.Body)
return false, fmt.Errorf("unexpected response (status %d): %s", resp.StatusCode, string(body))
}
// updateRoleAPI updates an existing role in Permit.io
func updateRoleAPI(client *http.Client, baseURL, apiKey, projectID, envID, name, description string, permissions []string) error {
fmt.Println(" ️ Role exists, updating...")
payload := RoleUpdateRequest{
Name: name,
Description: description,
Permissions: permissions,
}
jsonData, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("error marshaling update data: %w", err)
}
url := fmt.Sprintf("%s/v2/schema/%s/%s/roles/%s", baseURL, projectID, envID, name)
req, err := http.NewRequest("PATCH", url, bytes.NewBuffer(jsonData))
if err != nil {
return fmt.Errorf("error creating PATCH request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("error making PATCH request: %w", err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
return fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(body))
}
fmt.Println(" ✓ Role updated successfully")
return nil
}
// createRoleAPI creates a new role in Permit.io
func createRoleAPI(client *http.Client, baseURL, apiKey, projectID, envID, name, description string, permissions []string) error {
fmt.Println(" ️ Role does not exist, creating...")
payload := RoleCreateRequest{
Key: name,
Name: name,
Description: description,
Permissions: permissions,
}
jsonData, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("error marshaling role data: %w", err)
}
url := fmt.Sprintf("%s/v2/schema/%s/%s/roles", baseURL, projectID, envID)
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
if err != nil {
return fmt.Errorf("error creating POST request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("error making POST request: %w", err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
return fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(body))
}
fmt.Println(" ✓ Role created successfully")
return nil
}
// listProjectsAndEnvironments lists all available projects and environments for the API key
func listProjectsAndEnvironments(client *http.Client, baseURL, apiKey string) {
fmt.Println("Fetching available projects and environments...")
fmt.Println(strings.Repeat("=", 70))
projects, err := fetchProjects(client, baseURL, apiKey)
if err != nil {
fmt.Fprintf(os.Stderr, "Error fetching projects: %v\n", err)
return
}
if len(projects) == 0 {
fmt.Println("No projects found or unable to list projects.")
fmt.Println("Your API key might be environment-scoped.")
return
}
fmt.Printf("\nFound %d project(s):\n\n", len(projects))
for i, proj := range projects {
printProject(i+1, proj)
printProjectEnvironments(client, baseURL, apiKey, proj)
fmt.Println(strings.Repeat("-", 60))
}
fmt.Println("\nUsage:")
fmt.Println(" Use the 'ID' or 'Key' values with -project and -env flags")
fmt.Println(" Example: go run setup_permit.go -project <proj_id> -env <env_id> -config permit_policies.yaml")
}
// fetchProjects retrieves the list of projects from Permit.io API
func fetchProjects(client *http.Client, baseURL, apiKey string) ([]map[string]interface{}, error) {
url := fmt.Sprintf("%s/v2/projects", baseURL)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, fmt.Errorf("error creating request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("error making request: %w", err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
fmt.Fprintf(os.Stderr, "API error (status %d): %s\n", resp.StatusCode, string(body))
fmt.Println("\nYour API key might be environment-scoped.")
fmt.Println("Check the Permit.io dashboard for your project and environment IDs.")
return nil, fmt.Errorf("API returned status %d", resp.StatusCode)
}
return parseProjectsResponse(body)
}
// parseProjectsResponse parses the projects API response
func parseProjectsResponse(body []byte) ([]map[string]interface{}, error) {
var projects []map[string]interface{}
if err := json.Unmarshal(body, &projects); err != nil {
// Try as paginated response
var pagedResp map[string]interface{}
if err := json.Unmarshal(body, &pagedResp); err == nil {
if data, ok := pagedResp["data"].([]interface{}); ok {
for _, item := range data {
if proj, ok := item.(map[string]interface{}); ok {
projects = append(projects, proj)
}
}
}
}
}
return projects, nil
}
// printProject prints project information
func printProject(num int, proj map[string]interface{}) {
fmt.Printf("Project #%d:\n", num)
fmt.Printf(" Name: %v\n", proj["name"])
fmt.Printf(" Key: %v\n", proj["key"])
fmt.Printf(" ID: %v\n", proj["id"])
}
// printProjectEnvironments fetches and prints environments for a project
func printProjectEnvironments(client *http.Client, baseURL, apiKey string, proj map[string]interface{}) {
projID, ok := proj["id"].(string)
if !ok {
return
}
envs, err := fetchEnvironments(client, baseURL, apiKey, projID)
if err != nil || len(envs) == 0 {
return
}
fmt.Println(" Environments:")
for _, env := range envs {
fmt.Printf(" - Name: %v\n", env["name"])
fmt.Printf(" Key: %v\n", env["key"])
fmt.Printf(" ID: %v\n", env["id"])
fmt.Println()
}
}
// fetchEnvironments retrieves environments for a project
func fetchEnvironments(client *http.Client, baseURL, apiKey, projectID string) ([]map[string]interface{}, error) {
url := fmt.Sprintf("%s/v2/projects/%s/envs", baseURL, projectID)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("status %d", resp.StatusCode)
}
body, _ := io.ReadAll(resp.Body)
var envs []map[string]interface{}
if err := json.Unmarshal(body, &envs); err != nil {
return nil, err
}
return envs, nil
}