1
0
mirror of https://github.com/Infisical/infisical.git synced 2025-03-25 14:05:03 +00:00

Compare commits

..

1 Commits

Author SHA1 Message Date
4b9f409ea5 fix: scim improvements and ui fixes 2025-03-25 07:12:56 +04:00
19 changed files with 187 additions and 81 deletions
backend/src/ee/services
cli/packages/util
frontend
src
hooks
api/secretImports
utils
pages
organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection
project/AccessControlPage/components/MembersTab/components
secret-manager/OverviewPage
OverviewPage.tsx
components
SecretOverviewImportListView
SecretOverviewTableRow
vite.config.ts

@ -21,12 +21,7 @@ const generateUsername = () => {
export const CassandraProvider = (): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => {
const providerInputs = await DynamicSecretCassandraSchema.parseAsync(inputs);
const hostIps = await Promise.all(
providerInputs.host
.split(",")
.filter(Boolean)
.map((el) => verifyHostInputValidity(el).then((ip) => ip[0]))
);
const [hostIp] = await verifyHostInputValidity(providerInputs.host);
validateHandlebarTemplate("Cassandra creation", providerInputs.creationStatement, {
allowedExpressions: (val) => ["username", "password", "expiration", "keyspace"].includes(val)
});
@ -39,10 +34,10 @@ export const CassandraProvider = (): TDynamicProviderFns => {
allowedExpressions: (val) => ["username"].includes(val)
});
return { ...providerInputs, hostIps };
return { ...providerInputs, host: hostIp };
};
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretCassandraSchema> & { hostIps: string[] }) => {
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretCassandraSchema>) => {
const sslOptions = providerInputs.ca ? { rejectUnauthorized: false, ca: providerInputs.ca } : undefined;
const client = new cassandra.Client({
sslOptions,
@ -55,7 +50,7 @@ export const CassandraProvider = (): TDynamicProviderFns => {
},
keyspace: providerInputs.keyspace,
localDataCenter: providerInputs?.localDataCenter,
contactPoints: providerInputs.hostIps
contactPoints: providerInputs.host.split(",").filter(Boolean)
});
return client;
};

@ -20,13 +20,13 @@ export const ElasticSearchProvider = (): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => {
const providerInputs = await DynamicSecretElasticSearchSchema.parseAsync(inputs);
const [hostIp] = await verifyHostInputValidity(providerInputs.host);
return { ...providerInputs, hostIp };
return { ...providerInputs, host: hostIp };
};
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretElasticSearchSchema> & { hostIp: string }) => {
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretElasticSearchSchema>) => {
const connection = new ElasticSearchClient({
node: {
url: new URL(`${providerInputs.hostIp}:${providerInputs.port}`),
url: new URL(`${providerInputs.host}:${providerInputs.port}`),
...(providerInputs.ca && {
ssl: {
rejectUnauthorized: false,

@ -20,14 +20,14 @@ export const MongoDBProvider = (): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => {
const providerInputs = await DynamicSecretMongoDBSchema.parseAsync(inputs);
const [hostIp] = await verifyHostInputValidity(providerInputs.host);
return { ...providerInputs, hostIp };
return { ...providerInputs, host: hostIp };
};
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretMongoDBSchema> & { hostIp: string }) => {
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretMongoDBSchema>) => {
const isSrv = !providerInputs.port;
const uri = isSrv
? `mongodb+srv://${providerInputs.hostIp}`
: `mongodb://${providerInputs.hostIp}:${providerInputs.port}`;
? `mongodb+srv://${providerInputs.host}`
: `mongodb://${providerInputs.host}:${providerInputs.port}`;
const client = new MongoClient(uri, {
auth: {

@ -3,6 +3,7 @@ import https from "https";
import { customAlphabet } from "nanoid";
import { z } from "zod";
import { removeTrailingSlash } from "@app/lib/fn";
import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid";
@ -79,12 +80,12 @@ export const RabbitMqProvider = (): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => {
const providerInputs = await DynamicSecretRabbitMqSchema.parseAsync(inputs);
const [hostIp] = await verifyHostInputValidity(providerInputs.host);
return { ...providerInputs, hostIp };
return { ...providerInputs, host: hostIp };
};
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretRabbitMqSchema> & { hostIp: string }) => {
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretRabbitMqSchema>) => {
const axiosInstance = axios.create({
baseURL: `${providerInputs.hostIp}:${providerInputs.port}/api`,
baseURL: `${removeTrailingSlash(providerInputs.host)}:${providerInputs.port}/api`,
auth: {
username: providerInputs.username,
password: providerInputs.password

@ -65,15 +65,15 @@ export const RedisDatabaseProvider = (): TDynamicProviderFns => {
allowedExpressions: (val) => ["username"].includes(val)
});
return { ...providerInputs, hostIp };
return { ...providerInputs, host: hostIp };
};
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretRedisDBSchema> & { hostIp: string }) => {
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretRedisDBSchema>) => {
let connection: Redis | null = null;
try {
connection = new Redis({
username: providerInputs.username,
host: providerInputs.hostIp,
host: providerInputs.host,
port: providerInputs.port,
password: providerInputs.password,
...(providerInputs.ca && {

@ -37,16 +37,13 @@ export const SapAseProvider = (): TDynamicProviderFns => {
allowedExpressions: (val) => ["username"].includes(val)
});
}
return { ...providerInputs, hostIp };
return { ...providerInputs, host: hostIp };
};
const $getClient = async (
providerInputs: z.infer<typeof DynamicSecretSapAseSchema> & { hostIp: string },
useMaster?: boolean
) => {
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretSapAseSchema>, useMaster?: boolean) => {
const connectionString =
`DRIVER={FreeTDS};` +
`SERVER=${providerInputs.hostIp};` +
`SERVER=${providerInputs.host};` +
`PORT=${providerInputs.port};` +
`DATABASE=${useMaster ? "master" : providerInputs.database};` +
`UID=${providerInputs.username};` +

@ -41,12 +41,12 @@ export const SapHanaProvider = (): TDynamicProviderFns => {
validateHandlebarTemplate("SAP Hana revoke", providerInputs.revocationStatement, {
allowedExpressions: (val) => ["username"].includes(val)
});
return { ...providerInputs, hostIp };
return { ...providerInputs, host: hostIp };
};
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretSapHanaSchema> & { hostIp: string }) => {
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretSapHanaSchema>) => {
const client = hdb.createClient({
host: providerInputs.hostIp,
host: providerInputs.host,
port: providerInputs.port,
user: providerInputs.username,
password: providerInputs.password,

@ -132,7 +132,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
allowedExpressions: (val) => ["username", "database"].includes(val)
});
return { ...providerInputs, hostIp };
return { ...providerInputs, host: hostIp };
};
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretSqlDBSchema>) => {
@ -193,7 +193,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
const validateConnection = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
let isConnected = false;
const gatewayCallback = async (host = providerInputs.hostIp, port = providerInputs.port) => {
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
const db = await $getClient({ ...providerInputs, port, host });
// oracle needs from keyword
const testStatement = providerInputs.client === SqlProviders.Oracle ? "SELECT 1 FROM DUAL" : "SELECT 1";

@ -583,6 +583,8 @@ export const scimServiceFactory = ({
const serverCfg = await getServerCfg();
await userDAL.transaction(async (tx) => {
const { role, roleId } = await getDefaultOrgMembershipRole(org.defaultMembershipRole);
await userAliasDAL.update(
{
orgId,
@ -594,10 +596,15 @@ export const scimServiceFactory = ({
},
tx
);
await orgMembershipDAL.updateById(
membership.id,
{
isActive: active
isActive: active,
...(active && {
role,
roleId
})
},
tx
);

@ -56,7 +56,6 @@ func WriteInitalConfig(userCredentials *models.UserCredentials) error {
LoggedInUsers: existingConfigFile.LoggedInUsers,
VaultBackendType: existingConfigFile.VaultBackendType,
VaultBackendPassphrase: existingConfigFile.VaultBackendPassphrase,
Domains: existingConfigFile.Domains,
}
configFileMarshalled, err := json.Marshal(configFile)

@ -182,8 +182,7 @@ export const useGetImportedSecretsAllEnvs = ({
comment: encSecret.secretComment,
createdAt: encSecret.createdAt,
updatedAt: encSecret.updatedAt,
version: encSecret.version,
sourceEnv: env
version: encSecret.version
};
})
})),

@ -86,5 +86,13 @@ export const useSecretOverview = (secrets: DashboardProjectSecretsOverview["secr
[secrets]
);
return { secKeys, getEnvSecretKeyCount };
const getSecretByKey = useCallback(
(env: string, key: string) => {
const sec = secrets?.find((s) => s.env === env && s.key === key);
return sec;
},
[secrets]
);
return { secKeys, getSecretByKey, getEnvSecretKeyCount };
};

@ -258,7 +258,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
</Th>
<Th className="w-1/3">
<div className="flex items-center">
Email
Username
<IconButton
variant="plain"
className={`ml-2 ${orderBy === OrgMembersOrderBy.Email ? "" : "opacity-30"}`}
@ -478,7 +478,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro
onClick={(e) => {
e.stopPropagation();
if (currentOrg?.scimEnabled) {
if (currentOrg?.scimEnabled && isActive) {
createNotification({
text: "You cannot manage users from Infisical when org-level auth is enforced for your organization",
type: "error"

@ -94,8 +94,29 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
});
} else {
const inviteeEmails = selectedMembers
.map((member) => member?.user.username as string)
.filter(Boolean);
.map((member) => {
if (!member) return null;
if (member.user.email) {
return member.user.email;
}
if (member.user.username) {
return member.user.username;
}
return null;
})
.filter(Boolean) as string[];
if (inviteeEmails.length !== selectedMembers.length) {
createNotification({
text: "Failed to add users to project. One or more users were invalid.",
type: "error"
});
return;
}
if (inviteeEmails.length || newInvitees.length) {
await addMembersToProject({
inviteeEmails: [...inviteeEmails, ...newInvitees],

@ -81,6 +81,7 @@ import { CreateSecretForm } from "./components/CreateSecretForm";
import { FolderBreadCrumbs } from "./components/FolderBreadCrumbs";
import { SecretOverviewDynamicSecretRow } from "./components/SecretOverviewDynamicSecretRow";
import { SecretOverviewFolderRow } from "./components/SecretOverviewFolderRow";
import { SecretOverviewImportListView } from "./components/SecretOverviewImportListView";
import {
SecretNoAccessOverviewTableRow,
SecretOverviewTableRow
@ -202,16 +203,12 @@ export const OverviewPage = () => {
setVisibleEnvs(userAvailableEnvs);
}, [userAvailableEnvs]);
const {
secretImports,
isImportedSecretPresentInEnv,
getImportedSecretByKey,
getEnvImportedSecretKeyCount
} = useGetImportedSecretsAllEnvs({
projectId: workspaceId,
path: secretPath,
environments: (userAvailableEnvs || []).map(({ slug }) => slug)
});
const { isImportedSecretPresentInEnv, getImportedSecretByKey, getEnvImportedSecretKeyCount } =
useGetImportedSecretsAllEnvs({
projectId: workspaceId,
path: secretPath,
environments: (userAvailableEnvs || []).map(({ slug }) => slug)
});
const { isPending: isOverviewLoading, data: overview } = useGetProjectSecretsOverview(
{
@ -235,6 +232,7 @@ export const OverviewPage = () => {
secrets,
folders,
dynamicSecrets,
imports,
totalFolderCount,
totalSecretCount,
totalDynamicSecretCount,
@ -246,20 +244,16 @@ export const OverviewPage = () => {
totalUniqueDynamicSecretsInPage
} = overview ?? {};
const secretImportsShaped = secretImports
?.flatMap(({ data }) => data)
.filter(Boolean)
.flatMap((item) => item?.secrets || []);
const handleIsImportedSecretPresentInEnv = (envSlug: string, secretName: string) => {
if (secrets?.some((s) => s.key === secretName && s.env === envSlug)) {
return false;
}
if (secretImportsShaped.some((s) => s.key === secretName && s.sourceEnv === envSlug)) {
return true;
}
return isImportedSecretPresentInEnv(envSlug, secretName);
};
const importsShaped = imports
?.filter((el) => !el.isReserved)
?.map(({ importPath, importEnv }) => ({ importPath, importEnv }))
.filter(
(el, index, self) =>
index ===
self.findIndex(
(item) => item.importPath === el.importPath && item.importEnv.slug === el.importEnv.slug
)
);
useResetPageHelper({
totalCount,
@ -273,18 +267,7 @@ export const OverviewPage = () => {
const { dynamicSecretNames, isDynamicSecretPresentInEnv } =
useDynamicSecretOverview(dynamicSecrets);
const { secKeys, getEnvSecretKeyCount } = useSecretOverview(
secrets?.concat(secretImportsShaped) || []
);
const getSecretByKey = useCallback(
(env: string, key: string) => {
const sec = secrets?.find((s) => s.env === env && s.key === key);
return sec;
},
[secrets]
);
const { secKeys, getSecretByKey, getEnvSecretKeyCount } = useSecretOverview(secrets);
const { data: tags } = useGetWsTags(
permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags) ? workspaceId : ""
);
@ -1141,13 +1124,24 @@ export const OverviewPage = () => {
key={`overview-${dynamicSecretName}-${index + 1}`}
/>
))}
{filter.import &&
importsShaped &&
importsShaped?.length > 0 &&
importsShaped?.map((item, index) => (
<SecretOverviewImportListView
secretImport={item}
environments={visibleEnvs}
key={`overview-secret-input-${index + 1}`}
allSecretImports={imports}
/>
))}
{secKeys.map((key, index) => (
<SecretOverviewTableRow
isSelected={Boolean(selectedEntries.secret[key])}
onToggleSecretSelect={() => toggleSelectedEntry(EntryType.SECRET, key)}
secretPath={secretPath}
getImportedSecretByKey={getImportedSecretByKey}
isImportedSecretPresentInEnv={handleIsImportedSecretPresentInEnv}
isImportedSecretPresentInEnv={isImportedSecretPresentInEnv}
onSecretCreate={handleSecretCreate}
onSecretDelete={handleSecretDelete}
onSecretUpdate={handleSecretUpdate}

@ -0,0 +1,85 @@
import { faCheck, faFileImport, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { Td, Tr } from "@app/components/v2";
import { TSecretImport, WorkspaceEnv } from "@app/hooks/api/types";
import { EnvFolderIcon } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretImportListView/SecretImportItem";
type Props = {
secretImport: { importPath: string; importEnv: WorkspaceEnv };
environments: { name: string; slug: string }[];
allSecretImports?: TSecretImport[];
};
export const SecretOverviewImportListView = ({
secretImport,
environments = [],
allSecretImports = []
}: Props) => {
const isSecretPresentInEnv = (envSlug: string) => {
return allSecretImports.some((item) => {
if (item.isReplication) {
if (
item.importPath === secretImport.importPath &&
item.importEnv.slug === secretImport.importEnv.slug
) {
const reservedItem = allSecretImports.find((element) =>
element.importPath.includes(`__reserve_replication_${item.id}`)
);
// If the reserved item exists, check if the envSlug matches
if (reservedItem) {
return reservedItem.environment === envSlug;
}
}
} else {
// If the item is not replication, check if the envSlug matches directly
return (
item.environment === envSlug &&
item.importPath === secretImport.importPath &&
item.importEnv.slug === secretImport.importEnv.slug
);
}
return false;
});
};
return (
<Tr className="group">
<Td className="sticky left-0 z-10 border-r border-mineshaft-600 bg-mineshaft-800 bg-clip-padding px-0 py-0 group-hover:bg-mineshaft-700">
<div className="group flex cursor-pointer">
<div className="flex w-11 items-center py-2 pl-5 text-green-700">
<FontAwesomeIcon icon={faFileImport} />
</div>
<div className="flex flex-grow items-center py-2 pl-4 pr-2">
<EnvFolderIcon
env={secretImport.importEnv.slug || ""}
secretPath={secretImport.importPath || ""}
/>
</div>
</div>
</Td>
{environments.map(({ slug }, i) => {
const isPresent = isSecretPresentInEnv(slug);
return (
<Td
key={`sec-overview-${slug}-${i + 1}-value`}
className={twMerge(
"px-0 py-0 group-hover:bg-mineshaft-700",
isPresent ? "text-green-600" : "text-red-600"
)}
>
<div className="h-full w-full border-r border-mineshaft-600 px-5 py-[0.85rem]">
<div className="flex justify-center">
<FontAwesomeIcon
// eslint-disable-next-line no-nested-ternary
icon={isSecretPresentInEnv(slug) ? faCheck : faXmark}
/>
</div>
</div>
</Td>
);
})}
</Tr>
);
};

@ -0,0 +1 @@
export { SecretOverviewImportListView } from "./SecretOverviewImportListView";

@ -162,7 +162,7 @@ function SecretRenameRow({ environments, getSecretByKey, secretKey, secretPath }
render={({ field, fieldState: { error } }) => (
<Input
autoComplete="off"
isReadOnly={isReadOnly || secrets.filter(Boolean).length === 0}
isReadOnly={isReadOnly}
autoCapitalization={currentWorkspace?.autoCapitalization}
variant="plain"
isDisabled={isOverriden}

@ -33,7 +33,6 @@ export default defineConfig({
// }
// }
},
base: "",
plugins: [
tsconfigPaths(),
nodePolyfills({