From bf112a3b7a3e4e3a7f904a8506185d6f90f24ac1 Mon Sep 17 00:00:00 2001 From: A Ibrahim Date: Thu, 2 Apr 2026 13:54:10 -0400 Subject: [PATCH] fix: use resolveAuthMethod() in IAM config helpers 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 --- src/auth/fly.ts | 19 +++++- src/auth/iam.ts | 82 +++++++++----------------- src/lib/iam/users/invite.ts | 2 +- src/lib/iam/users/list.ts | 2 +- src/lib/iam/users/remove.ts | 2 +- src/lib/iam/users/revoke-invitation.ts | 2 +- src/lib/iam/users/update-role.ts | 2 +- src/lib/organizations/create.ts | 15 +---- 8 files changed, 54 insertions(+), 72 deletions(-) diff --git a/src/auth/fly.ts b/src/auth/fly.ts index 3f485a3..54f8b5f 100644 --- a/src/auth/fly.ts +++ b/src/auth/fly.ts @@ -1,12 +1,29 @@ import axios from 'axios'; import { getAuth0Config, TIGRIS_CLAIMS_NAMESPACE } from './client.js'; -import type { OrganizationInfo } from './storage.js'; +import { getSelectedOrganization, type OrganizationInfo } from './storage.js'; export function isFlyUser(organizationId?: string): boolean { return !!organizationId?.startsWith('flyio_'); } +/** + * Check if current org is Fly.io. Prints message and returns true if so. + * @param feature - what's unavailable, e.g. "User management" or "Organization creation" + */ +export function isFlyOrganization(feature: string): boolean { + const selectedOrg = getSelectedOrganization(); + if (isFlyUser(selectedOrg ?? undefined)) { + console.log( + `${feature} is not available for Fly.io organizations.\n` + + 'Your resources are managed through Fly.io.\n\n' + + 'Visit https://fly.io to manage your organization.' + ); + return true; + } + return false; +} + export async function fetchOrganizationsFromUserInfo( accessToken: string ): Promise { diff --git a/src/auth/iam.ts b/src/auth/iam.ts index d3d3662..acb703d 100644 --- a/src/auth/iam.ts +++ b/src/auth/iam.ts @@ -1,39 +1,26 @@ /** * Shared IAM auth helpers - * Consolidates OAuth check + auth check + config building patterns + * Uses resolveAuthMethod() as the single source of truth for auth priority. */ import { failWithError } from '@utils/exit.js'; import type { MessageContext } from '@utils/messages.js'; import { getAuthClient } from './client.js'; -import { isFlyUser } from './fly.js'; -import { getCredentials, getLoginMethod, getTigrisConfig } from './provider.js'; -import { getSelectedOrganization } from './storage.js'; - -/** - * Check if current org is Fly.io. Prints message and returns true if so. - */ -export function isFlyOrganization(): boolean { - const selectedOrg = getSelectedOrganization(); - if (isFlyUser(selectedOrg ?? undefined)) { - console.log( - 'User management is not available for Fly.io organizations.\n' + - 'Your users are managed through Fly.io.\n\n' + - 'Visit https://fly.io to manage your organization members.' - ); - return true; - } - return false; -} +export { isFlyOrganization } from './fly.js'; +import { getTigrisConfig, resolveAuthMethod } from './provider.js'; +import { getLoginMethod, getSelectedOrganization } from './storage.js'; /** * OAuth-only IAM config. Exits on non-OAuth or unauthenticated. * Used by IAM policy and user commands. + * + * Checks the *stored* login method (not resolveAuthMethod) because these + * operations always require OAuth — even when env vars or AWS profile + * are set for S3. */ export async function getOAuthIAMConfig(context: MessageContext) { - const loginMethod = await getLoginMethod(); - if (loginMethod !== 'oauth') { + if (getLoginMethod() !== 'oauth') { failWithError( context, 'This operation requires OAuth login.\nRun "tigris login oauth" first.' @@ -48,12 +35,11 @@ export async function getOAuthIAMConfig(context: MessageContext) { ); } - const accessToken = await authClient.getAccessToken(); const selectedOrg = getSelectedOrganization(); const { iamEndpoint, mgmtEndpoint } = getTigrisConfig(); return { - sessionToken: accessToken, + sessionToken: await authClient.getAccessToken(), organizationId: selectedOrg ?? undefined, iamEndpoint, mgmtEndpoint, @@ -62,41 +48,31 @@ export async function getOAuthIAMConfig(context: MessageContext) { /** * Dual-mode IAM config (OAuth or credentials). + * Uses resolveAuthMethod() to follow the same priority as getStorageConfig(). * Used by access-key commands. */ export async function getIAMConfig(context: MessageContext) { - const loginMethod = await getLoginMethod(); - const tigrisConfig = getTigrisConfig(); - const selectedOrg = getSelectedOrganization(); + const method = await resolveAuthMethod(); + + switch (method.type) { + case 'oauth': + return getOAuthIAMConfig(context); + + case 'aws-profile': + case 'credentials': + case 'environment': + case 'configured': + return { + accessKeyId: method.accessKeyId, + secretAccessKey: method.secretAccessKey, + organizationId: getSelectedOrganization() ?? undefined, + iamEndpoint: getTigrisConfig().iamEndpoint, + }; - if (loginMethod === 'oauth') { - const authClient = getAuthClient(); - if (!(await authClient.isAuthenticated())) { + case 'none': failWithError( context, - 'Not authenticated. Run "tigris login oauth" first.' + 'Not authenticated. Run "tigris login" or "tigris configure" first.' ); - } - - return { - sessionToken: await authClient.getAccessToken(), - organizationId: selectedOrg ?? undefined, - iamEndpoint: tigrisConfig.iamEndpoint, - }; - } - - const credentials = getCredentials(); - if (!credentials) { - failWithError( - context, - 'Not authenticated. Run "tigris login" or "tigris configure" first.' - ); } - - return { - accessKeyId: credentials.accessKeyId, - secretAccessKey: credentials.secretAccessKey, - organizationId: selectedOrg ?? undefined, - iamEndpoint: tigrisConfig.iamEndpoint, - }; } diff --git a/src/lib/iam/users/invite.ts b/src/lib/iam/users/invite.ts index a161e66..2496bf0 100644 --- a/src/lib/iam/users/invite.ts +++ b/src/lib/iam/users/invite.ts @@ -11,7 +11,7 @@ export default async function invite(options: Record) { const format = getFormat(options); - if (isFlyOrganization()) return; + if (isFlyOrganization('User management')) return; const emailOption = getOption(options, ['email']); const roleInput = getOption(options, ['role', 'r']) ?? 'member'; diff --git a/src/lib/iam/users/list.ts b/src/lib/iam/users/list.ts index 6becb49..3473827 100644 --- a/src/lib/iam/users/list.ts +++ b/src/lib/iam/users/list.ts @@ -17,7 +17,7 @@ export default async function list(options: Record) { const format = getFormat(options); - if (isFlyOrganization()) return; + if (isFlyOrganization('User management')) return; const iamConfig = await getOAuthIAMConfig(context); diff --git a/src/lib/iam/users/remove.ts b/src/lib/iam/users/remove.ts index ca64162..f820f90 100644 --- a/src/lib/iam/users/remove.ts +++ b/src/lib/iam/users/remove.ts @@ -17,7 +17,7 @@ export default async function removeUser(options: Record) { const resourceOption = getOption(options, ['resource']); const force = getOption(options, ['yes', 'y', 'force']); - if (isFlyOrganization()) return; + if (isFlyOrganization('User management')) return; const iamConfig = await getOAuthIAMConfig(context); diff --git a/src/lib/iam/users/revoke-invitation.ts b/src/lib/iam/users/revoke-invitation.ts index 639c4fe..8246371 100644 --- a/src/lib/iam/users/revoke-invitation.ts +++ b/src/lib/iam/users/revoke-invitation.ts @@ -19,7 +19,7 @@ export default async function revokeInvitation( const resourceOption = getOption(options, ['resource']); const force = getOption(options, ['yes', 'y', 'force']); - if (isFlyOrganization()) return; + if (isFlyOrganization('User management')) return; const iamConfig = await getOAuthIAMConfig(context); diff --git a/src/lib/iam/users/update-role.ts b/src/lib/iam/users/update-role.ts index ec076ca..bc597ef 100644 --- a/src/lib/iam/users/update-role.ts +++ b/src/lib/iam/users/update-role.ts @@ -14,7 +14,7 @@ export default async function updateRole(options: Record) { const format = getFormat(options); - if (isFlyOrganization()) return; + if (isFlyOrganization('User management')) return; const validRoles = ['admin', 'member'] as const; type Role = (typeof validRoles)[number]; diff --git a/src/lib/organizations/create.ts b/src/lib/organizations/create.ts index b06523f..0da6682 100644 --- a/src/lib/organizations/create.ts +++ b/src/lib/organizations/create.ts @@ -1,6 +1,5 @@ -import { isFlyUser } from '@auth/fly.js'; +import { isFlyOrganization } from '@auth/fly.js'; import { getStorageConfig, requireOAuthLogin } from '@auth/provider.js'; -import { getSelectedOrganization } from '@auth/storage.js'; import { createOrganization } from '@tigrisdata/iam'; import { failWithError, printNextActions } from '@utils/exit.js'; import { msg, printHint, printStart, printSuccess } from '@utils/messages.js'; @@ -12,17 +11,7 @@ export default async function create(options: Record) { printStart(context); if (requireOAuthLogin('Organization creation')) return; - - // Fly users cannot create organizations - const selectedOrg = getSelectedOrganization(); - if (isFlyUser(selectedOrg ?? undefined)) { - console.log( - 'Organization creation is not available for Fly.io users.\n' + - 'Your organizations are managed through Fly.io.\n\n' + - 'Visit https://fly.io to manage your organizations.' - ); - return; - } + if (isFlyOrganization('Organization creation')) return; const name = getOption(options, ['name', 'N']);