// 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= go run setup_permit.go -config permit_policies.yaml -project -env 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= %s -config -project -env \n", os.Args[0]) fmt.Fprintf(os.Stderr, "\nTo list available projects and environments:\n") fmt.Fprintf(os.Stderr, " PERMIT_API_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 -env -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 }