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
This commit is contained in:
Jay Brown
2026-02-19 20:22:59 +00:00
parent 58912a66d4
commit 6dccf494f8
70 changed files with 785 additions and 2221 deletions
@@ -0,0 +1,602 @@
// 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
}