Skip to content

Commit f226f33

Browse files
designcodeclaude
andauthored
fix: use resolveAuthMethod() in IAM config helpers (#63)
getIAMConfig() had its own auth resolution logic that diverged from resolveAuthMethod(), missing AWS profiles and env var credentials. Now delegates to resolveAuthMethod() as the single source of truth. getOAuthIAMConfig() keeps using the stored login method since IAM user/policy operations always require OAuth, even when env vars or AWS profile override S3 auth. Also moves isFlyOrganization() into auth/fly.ts where isFlyUser() lives, and deduplicates the inline check in organizations/create.ts. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 82561b1 commit f226f33

8 files changed

Lines changed: 54 additions & 72 deletions

File tree

src/auth/fly.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,29 @@
11
import axios from 'axios';
22

33
import { getAuth0Config, TIGRIS_CLAIMS_NAMESPACE } from './client.js';
4-
import type { OrganizationInfo } from './storage.js';
4+
import { getSelectedOrganization, type OrganizationInfo } from './storage.js';
55

66
export function isFlyUser(organizationId?: string): boolean {
77
return !!organizationId?.startsWith('flyio_');
88
}
99

10+
/**
11+
* Check if current org is Fly.io. Prints message and returns true if so.
12+
* @param feature - what's unavailable, e.g. "User management" or "Organization creation"
13+
*/
14+
export function isFlyOrganization(feature: string): boolean {
15+
const selectedOrg = getSelectedOrganization();
16+
if (isFlyUser(selectedOrg ?? undefined)) {
17+
console.log(
18+
`${feature} is not available for Fly.io organizations.\n` +
19+
'Your resources are managed through Fly.io.\n\n' +
20+
'Visit https://fly.io to manage your organization.'
21+
);
22+
return true;
23+
}
24+
return false;
25+
}
26+
1027
export async function fetchOrganizationsFromUserInfo(
1128
accessToken: string
1229
): Promise<OrganizationInfo[] | null> {

src/auth/iam.ts

Lines changed: 29 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,26 @@
11
/**
22
* Shared IAM auth helpers
3-
* Consolidates OAuth check + auth check + config building patterns
3+
* Uses resolveAuthMethod() as the single source of truth for auth priority.
44
*/
55

66
import { failWithError } from '@utils/exit.js';
77
import type { MessageContext } from '@utils/messages.js';
88

99
import { getAuthClient } from './client.js';
10-
import { isFlyUser } from './fly.js';
11-
import { getCredentials, getLoginMethod, getTigrisConfig } from './provider.js';
12-
import { getSelectedOrganization } from './storage.js';
13-
14-
/**
15-
* Check if current org is Fly.io. Prints message and returns true if so.
16-
*/
17-
export function isFlyOrganization(): boolean {
18-
const selectedOrg = getSelectedOrganization();
19-
if (isFlyUser(selectedOrg ?? undefined)) {
20-
console.log(
21-
'User management is not available for Fly.io organizations.\n' +
22-
'Your users are managed through Fly.io.\n\n' +
23-
'Visit https://fly.io to manage your organization members.'
24-
);
25-
return true;
26-
}
27-
return false;
28-
}
10+
export { isFlyOrganization } from './fly.js';
11+
import { getTigrisConfig, resolveAuthMethod } from './provider.js';
12+
import { getLoginMethod, getSelectedOrganization } from './storage.js';
2913

3014
/**
3115
* OAuth-only IAM config. Exits on non-OAuth or unauthenticated.
3216
* Used by IAM policy and user commands.
17+
*
18+
* Checks the *stored* login method (not resolveAuthMethod) because these
19+
* operations always require OAuth — even when env vars or AWS profile
20+
* are set for S3.
3321
*/
3422
export async function getOAuthIAMConfig(context: MessageContext) {
35-
const loginMethod = await getLoginMethod();
36-
if (loginMethod !== 'oauth') {
23+
if (getLoginMethod() !== 'oauth') {
3724
failWithError(
3825
context,
3926
'This operation requires OAuth login.\nRun "tigris login oauth" first.'
@@ -48,12 +35,11 @@ export async function getOAuthIAMConfig(context: MessageContext) {
4835
);
4936
}
5037

51-
const accessToken = await authClient.getAccessToken();
5238
const selectedOrg = getSelectedOrganization();
5339
const { iamEndpoint, mgmtEndpoint } = getTigrisConfig();
5440

5541
return {
56-
sessionToken: accessToken,
42+
sessionToken: await authClient.getAccessToken(),
5743
organizationId: selectedOrg ?? undefined,
5844
iamEndpoint,
5945
mgmtEndpoint,
@@ -62,41 +48,31 @@ export async function getOAuthIAMConfig(context: MessageContext) {
6248

6349
/**
6450
* Dual-mode IAM config (OAuth or credentials).
51+
* Uses resolveAuthMethod() to follow the same priority as getStorageConfig().
6552
* Used by access-key commands.
6653
*/
6754
export async function getIAMConfig(context: MessageContext) {
68-
const loginMethod = await getLoginMethod();
69-
const tigrisConfig = getTigrisConfig();
70-
const selectedOrg = getSelectedOrganization();
55+
const method = await resolveAuthMethod();
56+
57+
switch (method.type) {
58+
case 'oauth':
59+
return getOAuthIAMConfig(context);
60+
61+
case 'aws-profile':
62+
case 'credentials':
63+
case 'environment':
64+
case 'configured':
65+
return {
66+
accessKeyId: method.accessKeyId,
67+
secretAccessKey: method.secretAccessKey,
68+
organizationId: getSelectedOrganization() ?? undefined,
69+
iamEndpoint: getTigrisConfig().iamEndpoint,
70+
};
7171

72-
if (loginMethod === 'oauth') {
73-
const authClient = getAuthClient();
74-
if (!(await authClient.isAuthenticated())) {
72+
case 'none':
7573
failWithError(
7674
context,
77-
'Not authenticated. Run "tigris login oauth" first.'
75+
'Not authenticated. Run "tigris login" or "tigris configure" first.'
7876
);
79-
}
80-
81-
return {
82-
sessionToken: await authClient.getAccessToken(),
83-
organizationId: selectedOrg ?? undefined,
84-
iamEndpoint: tigrisConfig.iamEndpoint,
85-
};
86-
}
87-
88-
const credentials = getCredentials();
89-
if (!credentials) {
90-
failWithError(
91-
context,
92-
'Not authenticated. Run "tigris login" or "tigris configure" first.'
93-
);
9477
}
95-
96-
return {
97-
accessKeyId: credentials.accessKeyId,
98-
secretAccessKey: credentials.secretAccessKey,
99-
organizationId: selectedOrg ?? undefined,
100-
iamEndpoint: tigrisConfig.iamEndpoint,
101-
};
10278
}

src/lib/iam/users/invite.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export default async function invite(options: Record<string, unknown>) {
1111

1212
const format = getFormat(options);
1313

14-
if (isFlyOrganization()) return;
14+
if (isFlyOrganization('User management')) return;
1515

1616
const emailOption = getOption<string | string[]>(options, ['email']);
1717
const roleInput = getOption<string>(options, ['role', 'r']) ?? 'member';

src/lib/iam/users/list.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export default async function list(options: Record<string, unknown>) {
1717

1818
const format = getFormat(options);
1919

20-
if (isFlyOrganization()) return;
20+
if (isFlyOrganization('User management')) return;
2121

2222
const iamConfig = await getOAuthIAMConfig(context);
2323

src/lib/iam/users/remove.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export default async function removeUser(options: Record<string, unknown>) {
1717
const resourceOption = getOption<string | string[]>(options, ['resource']);
1818
const force = getOption<boolean>(options, ['yes', 'y', 'force']);
1919

20-
if (isFlyOrganization()) return;
20+
if (isFlyOrganization('User management')) return;
2121

2222
const iamConfig = await getOAuthIAMConfig(context);
2323

src/lib/iam/users/revoke-invitation.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export default async function revokeInvitation(
1919
const resourceOption = getOption<string | string[]>(options, ['resource']);
2020
const force = getOption<boolean>(options, ['yes', 'y', 'force']);
2121

22-
if (isFlyOrganization()) return;
22+
if (isFlyOrganization('User management')) return;
2323

2424
const iamConfig = await getOAuthIAMConfig(context);
2525

src/lib/iam/users/update-role.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export default async function updateRole(options: Record<string, unknown>) {
1414

1515
const format = getFormat(options);
1616

17-
if (isFlyOrganization()) return;
17+
if (isFlyOrganization('User management')) return;
1818

1919
const validRoles = ['admin', 'member'] as const;
2020
type Role = (typeof validRoles)[number];

src/lib/organizations/create.ts

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import { isFlyUser } from '@auth/fly.js';
1+
import { isFlyOrganization } from '@auth/fly.js';
22
import { getStorageConfig, requireOAuthLogin } from '@auth/provider.js';
3-
import { getSelectedOrganization } from '@auth/storage.js';
43
import { createOrganization } from '@tigrisdata/iam';
54
import { failWithError, printNextActions } from '@utils/exit.js';
65
import { msg, printHint, printStart, printSuccess } from '@utils/messages.js';
@@ -12,17 +11,7 @@ export default async function create(options: Record<string, unknown>) {
1211
printStart(context);
1312

1413
if (requireOAuthLogin('Organization creation')) return;
15-
16-
// Fly users cannot create organizations
17-
const selectedOrg = getSelectedOrganization();
18-
if (isFlyUser(selectedOrg ?? undefined)) {
19-
console.log(
20-
'Organization creation is not available for Fly.io users.\n' +
21-
'Your organizations are managed through Fly.io.\n\n' +
22-
'Visit https://fly.io to manage your organizations.'
23-
);
24-
return;
25-
}
14+
if (isFlyOrganization('Organization creation')) return;
2615

2716
const name = getOption<string>(options, ['name', 'N']);
2817

0 commit comments

Comments
 (0)