Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 61 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Designed with developer experience in mind, the CLI makes it easy to integrate *
- 🏗️ Automate policy operations in CI/CD with IaC and GitOps
- ✨ Generate policies from natural language using AI
- 🔐 Manage users, roles, and permissions directly from your terminal
- 🌍 Multi-region support for US and EU deployments

> :bulb: The CLI is fully open source and is built with Pastel, using TypeScript and a React-style architecture. Contributions welcome!

Expand Down Expand Up @@ -146,13 +147,48 @@ The `login` command will take you to the browser to perform user authentication

- `--api-key <string>` - store a Permit API key in your workstation keychain instead of running browser authentication
- `--workspace <string>` - predefined workspace key to skip the workspace selection step
- `--region <us | eu>` - specify the Permit region to use (`default: us`). The region determines which Permit.io API endpoints the CLI will communicate with.

**Example:**
**Examples:**

Login with default US region:

```bash
$ permit login
```

Login with EU region:

```bash
$ permit login --region eu
```

Login with API key and EU region:

```bash
$ permit login --api-key permit_key_abc123 --region eu
```

**Region Support:**

Permit.io operates in multiple regions. When you log in with a specific region, the CLI will:

- Store your region preference in your system keychain
- Use the appropriate regional endpoints for all subsequent commands
- Generate Terraform configurations with the correct regional API URLs

Available regions:

- `us` (default) - United States region (`https://api.permit.io`)
- `eu` - European Union region (`https://api.eu.permit.io`)

You can also set the region using the `PERMIT_REGION` environment variable:

```bash
export PERMIT_REGION=eu
permit login
```

---

#### `permit logout`
Expand Down Expand Up @@ -387,6 +423,8 @@ Export your Permit environment configuration as a Terraform HCL file.

This is useful for users who want to start working with Terraform after configuring their Permit settings through the UI or API. The command exports all environment content (resources, roles, user sets, resource sets, condition sets) in the Permit Terraform provider format.

**Note:** The export includes all roles, including default roles (admin, editor, viewer) with their actual permissions. This allows you to manage role permissions consistently across environments using Infrastructure as Code. The Terraform provider will update existing roles or create them if they don't exist.

**Arguments (Optional)**

- `--api-key <string>` - a Permit API key to authenticate the operation. If not provided, the command will use the AuthProvider to get the API key you logged in with.
Expand All @@ -412,6 +450,28 @@ Print out the output to the console -
$ permit env export terraform
```

**Region Support:**

The generated Terraform configuration will automatically use the correct API URL based on your configured region:

- **US region**: `api_url = "https://api.permit.io"`
- **EU region**: `api_url = "https://api.eu.permit.io"`

The region is determined by:

1. The `PERMIT_REGION` environment variable (if set)
2. The region stored from your last `permit login --region <region>` command
3. Defaults to `us` if no region is specified

Example for EU region:

```bash
$ export PERMIT_REGION=eu
$ permit env export terraform --file permit-eu-config.tf
```

This ensures that when you run `terraform apply`, the Terraform provider will communicate with the correct regional Permit.io API.

## Fine-Grained Authorization Configuration

Use natural language commands with AI to instantly set up and enforce fine-grained authorization policies.
Expand Down
17 changes: 2 additions & 15 deletions source/commands/env/export/generators/RoleGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,6 @@ import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

// Define default global roles that should be excluded from creation
// These roles already exist in Permit by default
const DEFAULT_GLOBAL_ROLES = ['admin', 'editor', 'viewer'];

interface RoleData {
key: string;
terraformId: string;
Expand Down Expand Up @@ -183,10 +179,8 @@ export class RoleGenerator implements HCLGenerator {
): string {
let terraformId = roleKey;

const isDefaultRole = DEFAULT_GLOBAL_ROLES.includes(roleKey);

// For duplicate roles or default roles, use resource__role format
if (isDuplicate || isDefaultRole || this.usedTerraformIds.has(roleKey)) {
// For duplicate roles, use resource__role format
if (isDuplicate || this.usedTerraformIds.has(roleKey)) {
terraformId = resourceKey
? `${resourceKey}__${roleKey}`
: `global_${roleKey}`;
Expand Down Expand Up @@ -300,13 +294,6 @@ export class RoleGenerator implements HCLGenerator {
const validRoles: RoleData[] = [];

for (const role of roles) {
// Skip default global roles that already exist in the system
if (DEFAULT_GLOBAL_ROLES.includes(role.key)) {
// Still add to the ID map for role derivations
this.roleIdMap.set(role.key, role.key);
continue;
}

// Generate Terraform ID
const terraformId = this.generateTerraformId(role.key);

Expand Down
3 changes: 2 additions & 1 deletion source/commands/env/export/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { WarningCollector } from './types.js';
import { getPermitApiUrl } from '../../../config.js';

export function createSafeId(...parts: string[]): string {
return parts
Expand Down Expand Up @@ -36,7 +37,7 @@ variable "PERMIT_API_KEY" {
}

provider "permitio" {
api_url = "https://api.permit.io"
api_url = "${getPermitApiUrl()}"
api_key = var.PERMIT_API_KEY
}
`;
Expand Down
25 changes: 23 additions & 2 deletions source/commands/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import React, { useCallback, useEffect, useState } from 'react';
import { Text } from 'ink';
import { type infer as zInfer, object, string } from 'zod';
import { option } from 'pastel';
import { saveAuthToken } from '../lib/auth.js';
import { saveAuthToken, saveRegion } from '../lib/auth.js';
import { setRegion } from '../config.js';
import LoginFlow from '../components/LoginFlow.js';
import EnvironmentSelection, {
ActiveState,
Expand All @@ -25,6 +26,14 @@ export const options = object({
description: 'Use predefined workspace to Login',
}),
),
region: string()
.optional()
.describe(
option({
description: 'Permit region: us or eu (default: us)',
alias: 'r',
}),
),
});

type Props = {
Expand All @@ -38,9 +47,14 @@ type Props = {
};

export default function Login({
options: { apiKey, workspace },
options: { apiKey, workspace, region },
loginSuccess,
}: Props) {
// Set region IMMEDIATELY before anything else (synchronously)
if (region && (region === 'us' || region === 'eu')) {
setRegion(region as 'us' | 'eu');
}

const [state, setState] = useState<'login' | 'signup' | 'env' | 'done'>(
'login',
);
Expand All @@ -51,6 +65,13 @@ export default function Login({
const [organization, setOrganization] = useState<string>('');
const [environment, setEnvironment] = useState<string>('');

// Save region to keystore after successful login
useEffect(() => {
if (region && (region === 'us' || region === 'eu')) {
saveRegion(region as 'us' | 'eu');
}
}, [region]);

const onEnvironmentSelectSuccess = useCallback(
async (
organisation: ActiveState,
Expand Down
12 changes: 11 additions & 1 deletion source/components/AuthProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import React, {
useState,
} from 'react';
import { Text, Newline } from 'ink';
import { loadAuthToken } from '../lib/auth.js';
import { loadAuthToken, loadRegion } from '../lib/auth.js';
import Login from '../commands/login.js';
import {
ApiKeyCreate,
Expand Down Expand Up @@ -131,6 +131,11 @@ export function AuthProvider({
redirect_scope: 'organization' | 'project' | 'login',
) => {
try {
// Load region from storage BEFORE validating API key
await loadRegion().catch(() => {
// Ignore errors - will default to 'us'
});

const token = await loadAuthToken();
const {
valid,
Expand Down Expand Up @@ -188,6 +193,11 @@ export function AuthProvider({
useEffect(() => {
if (state === 'validate') {
(async () => {
// Load region from storage BEFORE validating API key
await loadRegion().catch(() => {
// Ignore errors - will default to 'us'
});

const {
valid,
scope: keyScope,
Expand Down
28 changes: 22 additions & 6 deletions source/components/policy/CreateSimpleWizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ export default function CreateSimpleWizard({
const parsedActions = useParseActions(presentActions);
const parsedRoles = useParseRoles(presentRoles);

// Track if preset data has been processed
const [hasProcessedPresetData, setHasProcessedPresetData] =
React.useState(false);

// Initialize step based on preset values
const getInitialStep = () => {
if (presentResources && presentActions && presentRoles) return 'complete';
Expand Down Expand Up @@ -109,10 +113,13 @@ export default function CreateSimpleWizard({
};

const handleRolesComplete = useCallback(
async (roles: components['schemas']['RoleCreate'][]) => {
async (
roles: components['schemas']['RoleCreate'][],
resourcesToCreate?: components['schemas']['ResourceCreate'][],
) => {
setStatus('processing');
try {
await createBulkResources(resources);
await createBulkResources(resourcesToCreate || resources);
await createBulkRoles(roles);
setStatus('success');
setResources([]);
Expand All @@ -125,6 +132,9 @@ export default function CreateSimpleWizard({
);

useEffect(() => {
// Only process preset data once
if (hasProcessedPresetData) return;

const processPresetData = async () => {
if (presentResources && presentActions && presentRoles) {
try {
Expand All @@ -133,7 +143,8 @@ export default function CreateSimpleWizard({
actions: parsedActions,
}));
setResources(resourcesWithActions);
await handleRolesComplete(parsedRoles);
setHasProcessedPresetData(true);
await handleRolesComplete(parsedRoles, resourcesWithActions);
} catch (err) {
handleError((err as Error).message);
}
Expand All @@ -143,19 +154,24 @@ export default function CreateSimpleWizard({
actions: parsedActions,
}));
setResources(resourcesWithActions);
setHasProcessedPresetData(true);
}
};

processPresetData();
if (
(presentResources && presentActions && presentRoles) ||
(presentResources && presentActions)
) {
processPresetData();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
presentResources,
presentActions,
presentRoles,
parsedResources,
parsedActions,
parsedRoles,
handleRolesComplete,
handleError,
]);

return (
Expand Down
3 changes: 2 additions & 1 deletion source/components/policy/create/TerraformGenerator.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { PolicyData } from './types.js';
import { getPermitApiUrl } from '../../../config.js';

interface TerraformGeneratorProps {
tableData: PolicyData;
Expand Down Expand Up @@ -37,7 +38,7 @@ export const generateTerraform = ({
}

provider "permitio" {
api_url = "https://api.permit.io"
api_url = "${getPermitApiUrl()}"
api_key = "${authToken}"
}

Expand Down
Loading
Loading