// permitio.go handles Permit.io API operations for user management and role-based access control (RBAC). // This file provides functions to create, delete, search, and manage users in Permit.io, as well as // assign/unassign roles and validate role definitions. package usermanagement import ( "bytes" "encoding/json" "fmt" "io" "net/http" "net/url" "strings" ) const ( permitRoleAssignmentsPerPage = 100 maxPermitRoleAssignmentPages = 100 ) // CreatePermitUser creates a new user in Permit.io using data from a Cognito user. // The Cognito Subject ID is used as the Permit.io user key for consistency across systems. // // Parameters: // - client: HTTP client for making API requests // - config: Permit.io configuration containing API credentials and project details // - cognitoUser: The Cognito user response containing user details and Subject ID // // Returns: // - error: Error if the user creation fails. Returns nil if user is created or already exists. func CreatePermitUser(client *http.Client, config *PermitConfig, cognitoUser *CognitoUserResponse) error { projectID, err := config.GetProjectID(client) if err != nil { return fmt.Errorf("failed to get project ID: %w", err) } envID, err := config.GetEnvID(client) if err != nil { return fmt.Errorf("failed to get environment ID: %w", err) } endpoint := fmt.Sprintf("%s/v2/facts/%s/%s/users", config.BaseURL, projectID, envID) attrs := map[string]interface{}{ "cognito_username": cognitoUser.Username, "cognito_status": cognitoUser.Status, } if cognitoUser.EnvironmentID != "" { attrs["environment_id"] = cognitoUser.EnvironmentID } userObj := PermitUser{ Key: cognitoUser.SubjectID, // Use Cognito Subject ID as the key Email: cognitoUser.Email, FirstName: cognitoUser.FirstName, LastName: cognitoUser.LastName, Attributes: attrs, } jsonData, err := json.Marshal(userObj) if err != nil { return fmt.Errorf("error marshaling user data: %w", err) } req, err := http.NewRequest("POST", endpoint, bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("error creating request: %w", err) } req.Header.Set("Authorization", "Bearer "+config.APIKey) req.Header.Set("Content-Type", "application/json") resp, err := client.Do(req) if err != nil { return fmt.Errorf("error making request: %w", err) } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) // If we get a 404, provide helpful debugging info if resp.StatusCode == http.StatusNotFound { var errResp map[string]interface{} if err := json.Unmarshal(body, &errResp); err == nil { return fmt.Errorf("API error (status 404): %v", errResp["message"]) } } if resp.StatusCode == http.StatusConflict || resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusCreated { // User already exists or was created successfully return nil } if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { var errResp map[string]interface{} if err := json.Unmarshal(body, &errResp); err == nil { if errorObj, ok := errResp["error"].(map[string]interface{}); ok { if msg, ok := errorObj["message"].(string); ok { return fmt.Errorf("API error (status %d): %s", resp.StatusCode, msg) } } } return fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(body)) } return nil } // GetPermitUser retrieves a user from Permit.io by their user key, returning the full user // object including custom attributes. This is useful for verifying that attributes like // environment_id were correctly stored when the user was created. // // Parameters: // - client: HTTP client for making API requests // - config: Permit.io configuration containing API credentials and project details // - userKey: The Permit.io user key (typically Cognito SubjectID) // // Returns: // - *PermitUser: The full user object including attributes // - error: Error if the retrieval fails or user is not found func GetPermitUser(client *http.Client, config *PermitConfig, userKey string) (*PermitUser, error) { projectID, err := config.GetProjectID(client) if err != nil { return nil, fmt.Errorf("failed to get project ID: %w", err) } envID, err := config.GetEnvID(client) if err != nil { return nil, fmt.Errorf("failed to get environment ID: %w", err) } endpoint := fmt.Sprintf("%s/v2/facts/%s/%s/users/%s", config.BaseURL, projectID, envID, userKey) req, err := http.NewRequest("GET", endpoint, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Authorization", "Bearer "+config.APIKey) resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("error making request: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("error reading response: %w", err) } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(body)) } var user PermitUser if err := json.Unmarshal(body, &user); err != nil { return nil, fmt.Errorf("error parsing response: %w", err) } return &user, nil } // DeletePermitUser permanently deletes a user from Permit.io by their user key. // // Parameters: // - client: HTTP client for making API requests // - config: Permit.io configuration containing API credentials and project details // - userKey: The Permit.io user key (typically Cognito Subject ID) // // Returns: // - error: Error if the deletion fails func DeletePermitUser(client *http.Client, config *PermitConfig, userKey string) error { projectID, err := config.GetProjectID(client) if err != nil { return fmt.Errorf("failed to get project ID: %w", err) } envID, err := config.GetEnvID(client) if err != nil { return fmt.Errorf("failed to get environment ID: %w", err) } endpoint := fmt.Sprintf("%s/v2/facts/%s/%s/users/%s", config.BaseURL, projectID, envID, userKey) req, err := http.NewRequest("DELETE", endpoint, nil) if err != nil { return fmt.Errorf("error creating request: %w", err) } req.Header.Set("Authorization", "Bearer "+config.APIKey) resp, err := client.Do(req) if err != nil { return fmt.Errorf("error making request: %w", err) } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent { var errResp map[string]interface{} if err := json.Unmarshal(body, &errResp); err == nil { if errorObj, ok := errResp["error"].(map[string]interface{}); ok { if msg, ok := errorObj["message"].(string); ok { return fmt.Errorf("API error (status %d): %s", resp.StatusCode, msg) } } } return fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(body)) } return nil } // FindPermitUserByEmail searches for a user in Permit.io by their email address. // Returns the user's key (typically Cognito Subject ID) if found. // // Parameters: // - client: HTTP client for making API requests // - config: Permit.io configuration containing API credentials and project details // - email: The email address to search for // // Returns: // - string: The Permit.io user key if found, empty string if not found // - error: Error if the search fails func FindPermitUserByEmail(client *http.Client, config *PermitConfig, email string) (string, error) { projectID, err := config.GetProjectID(client) if err != nil { return "", fmt.Errorf("failed to get project ID: %w", err) } envID, err := config.GetEnvID(client) if err != nil { return "", fmt.Errorf("failed to get environment ID: %w", err) } // Search for user by email endpoint := fmt.Sprintf("%s/v2/facts/%s/%s/users?search=%s", config.BaseURL, projectID, envID, email) req, err := http.NewRequest("GET", endpoint, nil) if err != nil { return "", fmt.Errorf("error creating request: %w", err) } req.Header.Set("Authorization", "Bearer "+config.APIKey) req.Header.Set("Content-Type", "application/json") resp, err := client.Do(req) if err != nil { return "", fmt.Errorf("error making request: %w", err) } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(body)) } // Parse response to find user var response map[string]interface{} if err := json.Unmarshal(body, &response); err != nil { return "", fmt.Errorf("error parsing response: %w", err) } // Look for users in common pagination structures var users []interface{} if data, ok := response["data"].([]interface{}); ok { users = data } else if items, ok := response["items"].([]interface{}); ok { users = items } else if usersList, ok := response["users"].([]interface{}); ok { users = usersList } else if _, ok := response["key"]; ok { // Single user object returned users = []interface{}{response} } else { // Try direct array unmarshaling as fallback var directUsers []interface{} if err := json.Unmarshal(body, &directUsers); err == nil { users = directUsers } else { return "", fmt.Errorf("unexpected response format") } } // Find user with matching email for _, u := range users { if user, ok := u.(map[string]interface{}); ok { if userEmail, ok := user["email"].(string); ok && userEmail == email { if key, ok := user["key"].(string); ok { return key, nil } } } } return "", nil // User not found } // UserExistsInPermit checks if a user exists in Permit.io by email. // // Parameters: // - client: HTTP client for making API requests // - config: Permit.io configuration containing API credentials and project details // - email: The email address to check // // Returns: // - bool: true if the user exists, false otherwise // - string: The user's key if found, empty string otherwise // - error: Error if the check fails func UserExistsInPermit(client *http.Client, config *PermitConfig, email string) (bool, string, error) { userKey, err := FindPermitUserByEmail(client, config, email) if err != nil { return false, "", err } return userKey != "", userKey, nil } // AssignRole assigns a role to a user in Permit.io for the configured tenant. // // Parameters: // - client: HTTP client for making API requests // - config: Permit.io configuration containing API credentials, project details, and default tenant // - userID: The Permit.io user key (typically Cognito Subject ID) // - role: The role key to assign // // Returns: // - error: Error if the role assignment fails. Returns nil if role is assigned or already assigned. func AssignRole(client *http.Client, config *PermitConfig, userID, role string) error { projectID, err := config.GetProjectID(client) if err != nil { return fmt.Errorf("failed to get project ID: %w", err) } envID, err := config.GetEnvID(client) if err != nil { return fmt.Errorf("failed to get environment ID: %w", err) } endpoint := fmt.Sprintf("%s/v2/facts/%s/%s/role_assignments", config.BaseURL, projectID, envID) tenant := config.DefaultTenant if tenant == "" { tenant = "default" } assignment := RoleAssignment{ User: userID, Role: role, Tenant: tenant, } jsonData, err := json.Marshal(assignment) if err != nil { return fmt.Errorf("error marshaling role assignment: %w", err) } req, err := http.NewRequest("POST", endpoint, bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("error creating request: %w", err) } req.Header.Set("Authorization", "Bearer "+config.APIKey) req.Header.Set("Content-Type", "application/json") resp, err := client.Do(req) if err != nil { return fmt.Errorf("error making request: %w", err) } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) if resp.StatusCode == http.StatusConflict || resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusCreated { // Role already assigned or was assigned successfully return nil } if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { var errResp map[string]interface{} if err := json.Unmarshal(body, &errResp); err == nil { if errorObj, ok := errResp["error"].(map[string]interface{}); ok { if msg, ok := errorObj["message"].(string); ok { return fmt.Errorf("API error (status %d): %s", resp.StatusCode, msg) } } } return fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(body)) } return nil } // UnassignRole removes a role assignment from a user in Permit.io. // // Parameters: // - client: HTTP client for making API requests // - config: Permit.io configuration containing API credentials, project details, and default tenant // - userID: The Permit.io user key (typically Cognito Subject ID) // - role: The role key to unassign // // Returns: // - error: Error if the role unassignment fails. Returns nil if role is unassigned or was already not assigned. func UnassignRole(client *http.Client, config *PermitConfig, userID, role string) error { projectID, err := config.GetProjectID(client) if err != nil { return fmt.Errorf("failed to get project ID: %w", err) } envID, err := config.GetEnvID(client) if err != nil { return fmt.Errorf("failed to get environment ID: %w", err) } endpoint := fmt.Sprintf("%s/v2/facts/%s/%s/role_assignments", config.BaseURL, projectID, envID) tenant := config.DefaultTenant if tenant == "" { tenant = "default" } assignment := RoleAssignment{ User: userID, Role: role, Tenant: tenant, } jsonData, err := json.Marshal(assignment) if err != nil { return fmt.Errorf("error marshaling role assignment: %w", err) } req, err := http.NewRequest("DELETE", endpoint, bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("error creating request: %w", err) } req.Header.Set("Authorization", "Bearer "+config.APIKey) req.Header.Set("Content-Type", "application/json") resp, err := client.Do(req) if err != nil { return fmt.Errorf("error making request: %w", err) } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusNoContent || resp.StatusCode == http.StatusNotFound { // Role was unassigned, already unassigned, or didn't exist return nil } if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent { var errResp map[string]interface{} if err := json.Unmarshal(body, &errResp); err == nil { if errorObj, ok := errResp["error"].(map[string]interface{}); ok { if msg, ok := errorObj["message"].(string); ok { return fmt.Errorf("API error (status %d): %s", resp.StatusCode, msg) } } } return fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(body)) } return nil } // GetPermitUserRoles retrieves all roles assigned to a user in Permit.io. // // Parameters: // - client: HTTP client for making API requests // - config: Permit.io configuration containing API credentials and project details // - userKey: The Permit.io user key (typically Cognito Subject ID) // // Returns: // - []string: List of role keys assigned to the user // - error: Error if the retrieval fails func GetPermitUserRoles(client *http.Client, config *PermitConfig, userKey string) ([]string, error) { projectID, err := config.GetProjectID(client) if err != nil { return nil, fmt.Errorf("failed to get project ID: %w", err) } envID, err := config.GetEnvID(client) if err != nil { return nil, fmt.Errorf("failed to get environment ID: %w", err) } endpoint := fmt.Sprintf("%s/v2/facts/%s/%s/role_assignments?user=%s", config.BaseURL, projectID, envID, url.QueryEscape(userKey)) req, err := http.NewRequest("GET", endpoint, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Authorization", "Bearer "+config.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() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(body)) } body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("error reading response: %w", err) } assignments, err := decodePermitRoleAssignments(body) if err != nil { return nil, err } roles := make([]string, 0, len(assignments)) for _, assignment := range assignments { if assignment.Role != "" { roles = append(roles, assignment.Role) } } return roles, nil } // ListAllPermitRoleAssignments retrieves all role assignments in the configured Permit.io environment. func ListAllPermitRoleAssignments(client *http.Client, config *PermitConfig) ([]RoleAssignment, error) { projectID, err := config.GetProjectID(client) if err != nil { return nil, fmt.Errorf("failed to get project ID: %w", err) } envID, err := config.GetEnvID(client) if err != nil { return nil, fmt.Errorf("failed to get environment ID: %w", err) } assignments := []RoleAssignment{} for page := 1; page <= maxPermitRoleAssignmentPages; page++ { endpoint := fmt.Sprintf("%s/v2/facts/%s/%s/role_assignments?page=%d&per_page=%d", config.BaseURL, projectID, envID, page, permitRoleAssignmentsPerPage) req, err := http.NewRequest("GET", endpoint, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Authorization", "Bearer "+config.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) } body, readErr := io.ReadAll(resp.Body) closeErr := resp.Body.Close() if readErr != nil { return nil, fmt.Errorf("error reading response: %w", readErr) } if closeErr != nil { return nil, fmt.Errorf("error closing response body: %w", closeErr) } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(body)) } pageAssignments, err := decodePermitRoleAssignments(body) if err != nil { return nil, err } assignments = append(assignments, pageAssignments...) if len(pageAssignments) < permitRoleAssignmentsPerPage { return assignments, nil } } return nil, fmt.Errorf("Permit.io role assignment listing exceeded %d pages", maxPermitRoleAssignmentPages) } func decodePermitRoleAssignments(body []byte) ([]RoleAssignment, error) { var directAssignments []RoleAssignment if err := json.Unmarshal(body, &directAssignments); err == nil { return directAssignments, nil } var response struct { Data []RoleAssignment `json:"data"` Items []RoleAssignment `json:"items"` Results []RoleAssignment `json:"results"` } if err := json.Unmarshal(body, &response); err != nil { return nil, fmt.Errorf("error parsing response as object or array: %w", err) } switch { case response.Data != nil: return response.Data, nil case response.Items != nil: return response.Items, nil case response.Results != nil: return response.Results, nil default: return nil, fmt.Errorf("unexpected Permit.io role assignments response format") } } // GetExistingRoles retrieves all role definitions from the specified Permit.io project/environment. // // Parameters: // - client: HTTP client for making API requests // - config: Permit.io configuration containing API credentials and project details // // Returns: // - []string: List of role keys that exist in the Permit.io environment // - error: Error if the retrieval fails func GetExistingRoles(client *http.Client, config *PermitConfig) ([]string, error) { projectID, err := config.GetProjectID(client) if err != nil { return nil, fmt.Errorf("failed to get project ID: %w", err) } envID, err := config.GetEnvID(client) if err != nil { return nil, fmt.Errorf("failed to get environment ID: %w", err) } // Construct API URL for roles endpoint endpoint := fmt.Sprintf("%s/v2/schema/%s/%s/roles", config.BaseURL, projectID, envID) // Create HTTP request req, err := http.NewRequest("GET", endpoint, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } // Set authorization header req.Header.Set("Authorization", "Bearer "+config.APIKey) req.Header.Set("Content-Type", "application/json") // Execute request resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("error making request: %w", err) } defer resp.Body.Close() // Check for successful response if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(body)) } // Read and parse response body body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("error reading response: %w", err) } // Parse JSON response - try different response structures var roles []string var rolesData []interface{} // First try to unmarshal as direct array var directRoles []interface{} if err := json.Unmarshal(body, &directRoles); err == nil { rolesData = directRoles } else { // If that fails, try as object with data/items/etc var response map[string]interface{} if err := json.Unmarshal(body, &response); err != nil { return nil, fmt.Errorf("error parsing response as object or array: %w", err) } // Look for roles in common response structures if data, ok := response["data"].([]interface{}); ok { rolesData = data } else if items, ok := response["items"].([]interface{}); ok { rolesData = items } else if results, ok := response["results"].([]interface{}); ok { rolesData = results } else { return nil, fmt.Errorf("unexpected response format: unable to find roles array") } } // Extract role names from the data for _, roleData := range rolesData { if roleObj, ok := roleData.(map[string]interface{}); ok { // Try different possible field names for role identifier if key, ok := roleObj["key"].(string); ok { roles = append(roles, key) } else if name, ok := roleObj["name"].(string); ok { roles = append(roles, name) } else if id, ok := roleObj["id"].(string); ok { roles = append(roles, id) } } } return roles, nil } // GetUserRoles is an alias for GetPermitUserRoles for convenience. // Retrieves all roles assigned to a user in Permit.io. // // Parameters: // - client: HTTP client for making API requests // - config: Permit.io configuration containing API credentials and project details // - userKey: The Permit.io user key (typically Cognito Subject ID) // // Returns: // - []string: List of role keys assigned to the user // - error: Error if the retrieval fails func GetUserRoles(client *http.Client, config *PermitConfig, userKey string) ([]string, error) { return GetPermitUserRoles(client, config, userKey) } // ValidateRoles validates that all requested roles exist in the Permit.io environment. // // Parameters: // - client: HTTP client for making API requests // - config: Permit.io configuration containing API credentials and project details // - requestedRoles: List of role keys to validate // // Returns: // - []string: List of missing roles (empty if all roles exist) // - error: Error if the validation check fails func ValidateRoles(client *http.Client, config *PermitConfig, requestedRoles []string) ([]string, error) { if len(requestedRoles) == 0 { return []string{}, nil // No roles to validate } // Get all existing roles from Permit.io existingRoles, err := GetExistingRoles(client, config) if err != nil { return requestedRoles, fmt.Errorf("failed to retrieve existing roles from Permit.io: %w", err) } // Create a set of existing roles for efficient lookup existingSet := make(map[string]bool) for _, role := range existingRoles { existingSet[role] = true } // Find requested roles that don't exist var missingRoles []string for _, requestedRole := range requestedRoles { if !existingSet[requestedRole] { missingRoles = append(missingRoles, requestedRole) } } if len(missingRoles) > 0 { return missingRoles, fmt.Errorf("the following roles do not exist in Permit.io: %s", strings.Join(missingRoles, ", ")) } return []string{}, nil } // RoleDetails contains the details of a role including its permissions. type RoleDetails struct { Key string // The role key/identifier Name string // Human-readable role name Description string // Optional description of the role Permissions []string // List of permissions granted by this role } // GetAPIKeyScope retrieves the scope information for a Permit.io API key. // This allows deriving the project and environment IDs from the API key itself, // eliminating the need for separate PERMIT_IO_PROJECT_ID and PERMIT_IO_ENV_ID // environment variables. // // Parameters: // - client: HTTP client for making API requests // - apiKey: The Permit.io API key // - baseURL: The Permit.io API base URL (e.g., https://api.permit.io) // // Returns: // - *APIKeyScope: The scope containing organization, project, and environment IDs // - error: Error if the API call fails or response cannot be parsed func GetAPIKeyScope(client *http.Client, apiKey, baseURL string) (*APIKeyScope, error) { endpoint := fmt.Sprintf("%s/v2/api-key/scope", baseURL) req, err := http.NewRequest("GET", endpoint, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Authorization", "Bearer "+apiKey) resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("error making request: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("error reading response: %w", err) } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(body)) } var scope APIKeyScope if err := json.Unmarshal(body, &scope); err != nil { return nil, fmt.Errorf("error parsing response: %w", err) } return &scope, nil } // GetRoleDetails retrieves the details of a role from Permit.io, including its permissions. // Uses the schema API endpoint to get role definitions. // // Parameters: // - client: HTTP client for making API requests // - config: Permit.io configuration containing API credentials and project details // - roleKey: The role key to retrieve details for // // Returns: // - *RoleDetails: The role details including permissions // - error: Error if the retrieval fails func GetRoleDetails(client *http.Client, config *PermitConfig, roleKey string) (*RoleDetails, error) { projectID, err := config.GetProjectID(client) if err != nil { return nil, fmt.Errorf("failed to get project ID: %w", err) } envID, err := config.GetEnvID(client) if err != nil { return nil, fmt.Errorf("failed to get environment ID: %w", err) } endpoint := fmt.Sprintf("%s/v2/schema/%s/%s/roles/%s", config.BaseURL, projectID, envID, roleKey) req, err := http.NewRequest("GET", endpoint, nil) if err != nil { return nil, fmt.Errorf("error creating request: %w", err) } req.Header.Set("Authorization", "Bearer "+config.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, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("error reading response: %w", err) } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(body)) } var roleData struct { Key string `json:"key"` Name string `json:"name"` Description string `json:"description"` Permissions []string `json:"permissions"` } if err := json.Unmarshal(body, &roleData); err != nil { return nil, fmt.Errorf("error parsing response: %w", err) } return &RoleDetails{ Key: roleData.Key, Name: roleData.Name, Description: roleData.Description, Permissions: roleData.Permissions, }, nil }