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
57 changes: 8 additions & 49 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ 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 @@ -147,46 +146,26 @@ 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.
- `--region <us>` - specify the Permit region to use (`default: us`). `us` is the only supported region.

**Examples:**

Login with default US region:
Login with the default 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
All Permit.io accounts are served from the US region (`https://api.permit.io`).

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:
The EU region (`eu`) was retired on 2026-09-28. If you previously logged in with `--region eu`, or have `PERMIT_REGION=eu` set, the CLI stops with an error before making any request. To fix it, unset `PERMIT_REGION` and log in again:

```bash
export PERMIT_REGION=eu
permit login
$ unset PERMIT_REGION
$ permit logout
$ permit login
```

---
Expand Down Expand Up @@ -450,27 +429,7 @@ 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.
The generated Terraform configuration uses `api_url = "https://api.permit.io"`.

## Fine-Grained Authorization Configuration

Expand Down
13 changes: 13 additions & 0 deletions source/cli.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
#!/usr/bin/env node
import Pastel from 'pastel';
import { getRegion, RetiredRegionError } from './config.js';

// Fail fast, before any command runs or any request is made, when
// PERMIT_REGION points at a retired region (e.g. 'eu').
try {
getRegion();
} catch (error) {
if (error instanceof RetiredRegionError) {
console.error(error.message);
process.exit(1);
}
throw error;
}

const app = new Pastel({
importMeta: import.meta,
Expand Down
39 changes: 31 additions & 8 deletions source/commands/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import { Text } from 'ink';
import { type infer as zInfer, object, string } from 'zod';
import { option } from 'pastel';
import { saveAuthToken, saveRegion } from '../lib/auth.js';
import { setRegion } from '../config.js';
import {
isRetiredRegion,
setRegion,
EU_REGION_RETIRED_MESSAGE,
} from '../config.js';
import LoginFlow from '../components/LoginFlow.js';
import EnvironmentSelection, {
ActiveState,
Expand All @@ -30,7 +34,7 @@ export const options = object({
.optional()
.describe(
option({
description: 'Permit region: us or eu (default: us)',
description: 'Permit region: us (default: us)',
alias: 'r',
}),
),
Expand All @@ -50,9 +54,12 @@ export default function Login({
options: { apiKey, workspace, region },
loginSuccess,
}: Props) {
// A retired region (e.g. 'eu') is rejected before any request is made.
const retiredRegion = isRetiredRegion(region);

// Set region IMMEDIATELY before anything else (synchronously)
if (region && (region === 'us' || region === 'eu')) {
setRegion(region as 'us' | 'eu');
if (region === 'us') {
setRegion(region);
}

const [state, setState] = useState<'login' | 'signup' | 'env' | 'done'>(
Expand All @@ -65,12 +72,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');
if (retiredRegion) {
setTimeout(() => {
process.exit(1);
}, 100);
}
}, [region]);
}, [retiredRegion]);

const onEnvironmentSelectSuccess = useCallback(
async (
Expand All @@ -82,6 +90,17 @@ export default function Login({
setOrganization(organisation.label);
setEnvironment(environment.label);
await saveAuthToken(secret);
// Save region to keystore after successful login. 'us' is the only
// region, so this also replaces a retired region (e.g. 'eu') left in
// the keychain by an earlier login.
try {
await saveRegion('us');
} catch (err) {
setError(
`Failed to save the region: ${err instanceof Error ? err.message : String(err)}`,
);
return;
}
if (loginSuccess) {
loginSuccess(organisation, project, environment, secret);
return;
Expand Down Expand Up @@ -113,6 +132,10 @@ export default function Login({
setState('env');
}, []);

if (retiredRegion) {
return <Text>{EU_REGION_RETIRED_MESSAGE}</Text>;
}

return (
<>
{state == 'login' && (
Expand Down
36 changes: 30 additions & 6 deletions source/components/AuthProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import React, {
} from 'react';
import { Text, Newline } from 'ink';
import { loadAuthToken, loadRegion } from '../lib/auth.js';
import { RetiredRegionError } from '../config.js';
import Login from '../commands/login.js';
import {
ApiKeyCreate,
Expand All @@ -30,6 +31,21 @@ import {
globalTokenGetterSetter,
} from '../hooks/useClient.js';

// Loads the saved region. Keychain read failures still default to 'us' (as
// before), but a retired region such as 'eu' is rethrown so the caller can
// stop with a clear error instead of silently sending old-region credentials
// to the US endpoints.
const loadStoredRegion = async (): Promise<void> => {
try {
await loadRegion();
} catch (err) {
if (err instanceof RetiredRegionError) {
throw err;
}
// Ignore other errors - will default to 'us'
}
};

// Define the AuthContext type
export type AuthContextType = {
authToken: string;
Expand Down Expand Up @@ -132,10 +148,15 @@ export function AuthProvider({
) => {
try {
// Load region from storage BEFORE validating API key
await loadRegion().catch(() => {
// Ignore errors - will default to 'us'
});
await loadStoredRegion();
} catch (err) {
// A retired region (e.g. 'eu') must stop here, before any request,
// instead of falling through to the login flow or to US.
setError(err instanceof Error ? err.message : String(err));
return;
}

try {
const token = await loadAuthToken();
const {
valid,
Expand Down Expand Up @@ -194,9 +215,12 @@ export function AuthProvider({
if (state === 'validate') {
(async () => {
// Load region from storage BEFORE validating API key
await loadRegion().catch(() => {
// Ignore errors - will default to 'us'
});
try {
await loadStoredRegion();
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
return;
}

const {
valid,
Expand Down
4 changes: 2 additions & 2 deletions source/components/pdp/PDPRunComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import { exec } from 'node:child_process';
import { promisify } from 'node:util';
import { loadAuthToken } from '../../lib/auth.js';
import { API_PDPS_CONFIG_URL } from '../../config.js';
import { getApiPdpsConfigUrl } from '../../config.js';
import { useAuth } from '../AuthProvider.js';
import SelectInput from 'ink-select-input';

Expand Down Expand Up @@ -66,7 +66,7 @@
}

// Fetch PDP configuration
const response = await fetch(API_PDPS_CONFIG_URL, {
const response = await fetch(getApiPdpsConfigUrl(), {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
Expand Down Expand Up @@ -163,7 +163,7 @@
};

generateDockerCommand();
}, [

Check warning on line 166 in source/components/pdp/PDPRunComponent.tsx

View workflow job for this annotation

GitHub Actions / build (22.x)

React Hook useEffect has a missing dependency: 'tag'. Either include it or remove the dependency array

Check warning on line 166 in source/components/pdp/PDPRunComponent.tsx

View workflow job for this annotation

GitHub Actions / build (18.x)

React Hook useEffect has a missing dependency: 'tag'. Either include it or remove the dependency array

Check warning on line 166 in source/components/pdp/PDPRunComponent.tsx

View workflow job for this annotation

GitHub Actions / build (20.x)

React Hook useEffect has a missing dependency: 'tag'. Either include it or remove the dependency array
opa,
dryRun,
authToken,
Expand Down
4 changes: 2 additions & 2 deletions source/components/pdp/PDPStatComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Box, Newline, Text } from 'ink';
import Spinner from 'ink-spinner';
import { useAuth } from '../AuthProvider.js';
import { PDPStatsProps } from '../../commands/pdp/stats.js';
import { PERMIT_API_STATISTICS_URL } from '../../config.js';
import { getPermitApiStatisticsUrl } from '../../config.js';
import TableComponent from '../ui/Table.js';
import { fetchUtil, MethodE } from '../../utils/fetchUtil.js';

Expand All @@ -22,7 +22,7 @@ export default function PDPStatComponent({ options }: PDPStatsProps) {
// State To Store Statistics URL
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [statisticsURL, _] = useState<string>(
`${options.statsUrl || PERMIT_API_STATISTICS_URL}/${auth.scope.project_id || options.projectKey}/${auth.scope.environment_id || options.environmentKey}/pdps`,
`${options.statsUrl || getPermitApiStatisticsUrl()}/${auth.scope.project_id || options.projectKey}/${auth.scope.environment_id || options.environmentKey}/pdps`,
);

// State to store API response data
Expand Down
Loading
Loading