Compare commits

..

2 Commits

Author SHA1 Message Date
dependabot[bot]
cd2b301729 Bump sigstore/cosign-installer from 4.1.1 to 4.1.2
Bumps [sigstore/cosign-installer](https://github.com/sigstore/cosign-installer) from 4.1.1 to 4.1.2.
- [Release notes](https://github.com/sigstore/cosign-installer/releases)
- [Commits](cad07c2e89...6f9f177880)

---
updated-dependencies:
- dependency-name: sigstore/cosign-installer
  dependency-version: 4.1.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-11 01:38:47 +00:00
Owen Schwartz
e253195fdd Merge pull request #3035 from fosrl/dev
Add new log streaming and client endpoint to connection log
2026-05-08 17:18:16 -07:00
13 changed files with 83 additions and 160 deletions

View File

@@ -415,7 +415,7 @@ jobs:
- name: Install cosign
# cosign is used to sign container images using keyless (OIDC) signing
uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
- name: Sign (GHCR, keyless)
# Sign each GHCR image by digest using keyless (OIDC) signing via Sigstore/Rekor.

View File

@@ -23,7 +23,7 @@ jobs:
skopeo --version
- name: Install cosign
uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
- name: Input check
run: |

View File

@@ -523,12 +523,6 @@
"userMessageOrgRemove": "Once removed, this user will no longer have access to the organization. You can always re-invite them later, but they will need to accept the invitation again.",
"userRemoveOrgConfirm": "Confirm Remove User",
"userRemoveOrg": "Remove User from Organization",
"userQuestionOrgRemoveSelf": "Are you sure you want to remove yourself from this organization?",
"userMessageOrgRemoveSelf": "You will lose access immediately. An administrator can invite you again later, but you will need to accept a new invitation.",
"userRemoveOrgConfirmSelf": "Confirm Remove Myself",
"userRemoveOrgSelf": "Remove yourself from the organization",
"userRemoveOrgSelfWarning": "You will lose access to this organization immediately.",
"userRemoveOrgConfirmPhraseSelf": "REMOVE MYSELF FROM ORG",
"users": "Users",
"accessRoleMember": "Member",
"accessRoleOwner": "Owner",
@@ -537,11 +531,6 @@
"emailInvalid": "Invalid email address",
"inviteValidityDuration": "Please select a duration",
"accessRoleSelectPlease": "Please select a role",
"removeOwnAdminRoleConfirmTitle": "Remove your administrator access?",
"removeOwnAdminRoleConfirmDescription": "You will no longer have administrator permissions in this organization after saving. Another administrator can restore access if needed.",
"removeOwnAdminRoleConfirmButton": "Remove My Administrator Access",
"removeOwnAdminRoleConfirmPhrase": "REMOVE MY ADMIN ACCESS",
"ownerMustRetainAdminRole": "The organization owner must keep at least one administrator role.",
"usernameRequired": "Username is required",
"idpSelectPlease": "Please select an identity provider",
"idpGenericOidc": "Generic OAuth2/OIDC provider.",

View File

@@ -1227,11 +1227,7 @@ async function getDomainId(
return null;
}
// Pick the most specific (longest baseDomain) valid domain so that, e.g.,
// *.test.dev.example.com is assigned to *.dev.example.com rather than *.example.com.
const domainSelection = validDomains.sort(
(a, b) => b.domains.baseDomain.length - a.domains.baseDomain.length
)[0].domains;
const domainSelection = validDomains[0].domains;
const baseDomain = domainSelection.baseDomain;
// Wildcard full-domains are not allowed on namespace (provided/free) domains

View File

@@ -98,6 +98,15 @@ export async function addUserRole(
);
}
if (existingUser[0].isOwner) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Cannot change the role of the owner of the organization"
)
);
}
const roleExists = await db
.select()
.from(roles)

View File

@@ -98,11 +98,11 @@ export async function removeUserRole(
);
}
if (existingUser.isOwner && role.isAdmin === true) {
if (existingUser.isOwner) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Cannot remove the administrator role from the organization owner"
"Cannot change the roles of the owner of the organization"
)
);
}

View File

@@ -87,8 +87,17 @@ export async function setUserOrgRoles(
);
}
if (existingUser.isOwner) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Cannot change the roles of the owner of the organization"
)
);
}
const orgRoles = await db
.select({ roleId: roles.roleId, isAdmin: roles.isAdmin })
.select({ roleId: roles.roleId })
.from(roles)
.where(
and(
@@ -106,18 +115,6 @@ export async function setUserOrgRoles(
);
}
if (existingUser.isOwner) {
const hasAdminRole = orgRoles.some((r) => r.isAdmin === true);
if (!hasAdminRole) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"The organization owner must retain an administrator role"
)
);
}
}
let orgClientsToRebuild: Client[] = [];
await db.transaction(async (trx) => {
await trx

View File

@@ -88,11 +88,11 @@ export async function addUserRoleLegacy(
);
}
if (existingUser.isOwner && role.isAdmin !== true) {
if (existingUser.isOwner) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"The organization owner must retain an administrator role"
"Cannot change the role of the owner of the organization"
)
);
}

View File

@@ -47,7 +47,10 @@ export async function queryUser(orgId: string, userId: string) {
.from(userOrgRoles)
.leftJoin(roles, eq(userOrgRoles.roleId, roles.roleId))
.where(
and(eq(userOrgRoles.userId, userId), eq(userOrgRoles.orgId, orgId))
and(
eq(userOrgRoles.userId, userId),
eq(userOrgRoles.orgId, orgId)
)
);
const isAdmin = roleRows.some((r) => r.isAdmin);
@@ -58,8 +61,7 @@ export async function queryUser(orgId: string, userId: string) {
roleIds: roleRows.map((r) => r.roleId),
roles: roleRows.map((r) => ({
roleId: r.roleId,
name: r.roleName ?? "",
isAdmin: r.isAdmin === true
name: r.roleName ?? ""
}))
};
}

View File

@@ -1,6 +1,5 @@
"use client";
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
import IdpTypeBadge from "@app/components/IdpTypeBadge";
import OrgRolesTagField from "@app/components/OrgRolesTagField";
import {
@@ -26,7 +25,6 @@ import { useEnvContext } from "@app/hooks/useEnvContext";
import { userOrgUserContext } from "@app/hooks/useOrgUserContext";
import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { toast } from "@app/hooks/useToast";
import { useUserContext } from "@app/hooks/useUserContext";
import { createApiClient, formatAxiosError } from "@app/lib/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { build } from "@server/build";
@@ -34,7 +32,7 @@ import { tierMatrix } from "@server/lib/billing/tierMatrix";
import { UserType } from "@server/types/UserTypes";
import { useTranslations } from "next-intl";
import { useParams } from "next/navigation";
import { useEffect, useState } from "react";
import { useActionState, useEffect } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
@@ -44,15 +42,13 @@ const accessControlsFormSchema = z.object({
roles: z.array(
z.object({
id: z.string(),
text: z.string(),
isAdmin: z.boolean().optional()
text: z.string()
})
)
});
export default function AccessControlsPage() {
const { orgUser: user, updateOrgUser } = userOrgUserContext();
const { user: sessionUser } = useUserContext();
const { env } = useEnvContext();
const api = createApiClient({ env });
@@ -76,8 +72,7 @@ export default function AccessControlsPage() {
autoProvisioned: user.autoProvisioned || false,
roles: (user.roles ?? []).map((r) => ({
id: r.roleId.toString(),
text: r.name,
isAdmin: r.isAdmin === true
text: r.name
}))
}
});
@@ -89,8 +84,7 @@ export default function AccessControlsPage() {
"roles",
(user.roles ?? []).map((r) => ({
id: r.roleId.toString(),
text: r.name,
isAdmin: r.isAdmin === true
text: r.name
}))
);
form.setValue("autoProvisioned", user.autoProvisioned || false);
@@ -101,11 +95,11 @@ export default function AccessControlsPage() {
? t("singleRolePerUserPlanNotice")
: t("singleRolePerUserEditionNotice");
const [isSaving, setIsSaving] = useState(false);
const [confirmRemoveOwnAdminOpen, setConfirmRemoveOwnAdminOpen] =
useState(false);
const [, action, isSubmitting] = useActionState(onSubmit, null);
async function onSubmit() {
const isValid = await form.trigger();
if (!isValid) return;
async function executeSave() {
const values = form.getValues();
if (values.roles.length === 0) {
@@ -117,7 +111,6 @@ export default function AccessControlsPage() {
return;
}
setIsSaving(true);
try {
const roleIds = values.roles.map((r) => parseInt(r.id, 10));
const updateRoleRequest = supportsMultipleRolesPerUser
@@ -137,8 +130,7 @@ export default function AccessControlsPage() {
roleIds,
roles: values.roles.map((r) => ({
roleId: parseInt(r.id, 10),
name: r.text,
isAdmin: r.isAdmin === true
name: r.text
})),
autoProvisioned: values.autoProvisioned
});
@@ -157,61 +149,11 @@ export default function AccessControlsPage() {
t("accessRoleErrorAddDescription")
)
});
} finally {
setIsSaving(false);
}
}
async function handleAccessControlsSubmit(e: React.FormEvent) {
e.preventDefault();
const isValid = await form.trigger();
if (!isValid) return;
const values = form.getValues();
if (values.roles.length === 0) {
toast({
variant: "destructive",
title: t("accessRoleErrorAdd"),
description: t("accessRoleSelectPlease")
});
return;
}
const willHaveAdminRole = values.roles.some(
(r) => r.isAdmin === true
);
const isRemovingOwnAdmin =
sessionUser.userId === user.userId &&
user.isAdmin &&
!willHaveAdminRole;
if (isRemovingOwnAdmin) {
setConfirmRemoveOwnAdminOpen(true);
return;
}
await executeSave();
}
return (
<SettingsContainer>
<ConfirmDeleteDialog
open={confirmRemoveOwnAdminOpen}
setOpen={setConfirmRemoveOwnAdminOpen}
title={t("removeOwnAdminRoleConfirmTitle")}
dialog={
<div className="space-y-2">
<p>{t("removeOwnAdminRoleConfirmDescription")}</p>
</div>
}
buttonText={t("removeOwnAdminRoleConfirmButton")}
string={t("removeOwnAdminRoleConfirmPhrase")}
onConfirm={executeSave}
/>
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
@@ -226,7 +168,7 @@ export default function AccessControlsPage() {
<SettingsSectionForm>
<Form {...form}>
<form
onSubmit={(e) => void handleAccessControlsSubmit(e)}
action={action}
className="space-y-4"
id="access-controls-form"
>
@@ -295,8 +237,8 @@ export default function AccessControlsPage() {
<SettingsSectionFooter>
<Button
type="submit"
loading={isSaving}
disabled={isSaving}
loading={isSubmitting}
disabled={isSubmitting}
form="access-controls-form"
>
{t("accessControlsSubmit")}

View File

@@ -99,14 +99,6 @@ export default function UsersTable({
];
}, [searchParams.toString()]);
const isRemovingSelf = useMemo(() => {
if (!selectedUser || !user) return false;
return (
`${selectedUser.username}-${selectedUser.idpId}` ===
`${user.username}-${user.idpId}`
);
}, [selectedUser, user]);
function handleFilterChange(
column: string,
value: string | undefined | null
@@ -231,7 +223,10 @@ export default function UsersTable({
header: () => <span className="p-3"></span>,
cell: ({ row }) => {
const userRow = row.original;
const canRemoveFromOrg = !userRow.isOwner;
const isCurrentUser =
`${userRow.username}-${userRow.idpId}` ===
`${user?.username}-${user?.idpId}`;
const isDisabled = userRow.isOwner || isCurrentUser;
return (
<div className="flex items-center justify-end">
<div>
@@ -240,6 +235,7 @@ export default function UsersTable({
<Button
variant="ghost"
className="h-8 w-8 p-0"
disabled={isDisabled}
>
<span className="sr-only">
{t("openMenu")}
@@ -251,12 +247,16 @@ export default function UsersTable({
<Link
href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`}
className="block w-full"
aria-disabled={isDisabled}
onClick={(e) =>
isDisabled && e.preventDefault()
}
>
<DropdownMenuItem>
<DropdownMenuItem disabled={isDisabled}>
{t("accessUserManage")}
</DropdownMenuItem>
</Link>
{canRemoveFromOrg && (
{!isDisabled && (
<DropdownMenuItem
onClick={() => {
setIsDeleteModalOpen(true);
@@ -271,14 +271,25 @@ export default function UsersTable({
</DropdownMenuContent>
</DropdownMenu>
</div>
<Link
href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`}
>
<Button variant={"outline"} className="ml-2">
{isDisabled ? (
<Button
variant={"outline"}
className="ml-2"
disabled
>
{t("manage")}
<ArrowRight className="ml-2 w-4 h-4" />
</Button>
</Link>
) : (
<Link
href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`}
>
<Button variant={"outline"} className="ml-2">
{t("manage")}
<ArrowRight className="ml-2 w-4 h-4" />
</Button>
</Link>
)}
</div>
);
}
@@ -348,45 +359,22 @@ export default function UsersTable({
}}
dialog={
<div className="space-y-2">
<p>
{t(
isRemovingSelf
? "userQuestionOrgRemoveSelf"
: "userQuestionOrgRemove"
)}
</p>
<p>
{t(
isRemovingSelf
? "userMessageOrgRemoveSelf"
: "userMessageOrgRemove"
)}
</p>
<p>{t("userQuestionOrgRemove")}</p>
<p>{t("userMessageOrgRemove")}</p>
</div>
}
buttonText={t(
isRemovingSelf
? "userRemoveOrgConfirmSelf"
: "userRemoveOrgConfirm"
)}
warningText={
isRemovingSelf ? t("userRemoveOrgSelfWarning") : undefined
}
buttonText={t("userRemoveOrgConfirm")}
onConfirm={async () => startTransition(removeUser)}
string={
isRemovingSelf
? t("userRemoveOrgConfirmPhraseSelf")
: selectedUser
? getUserDisplayName({
email: selectedUser.email,
name: selectedUser.name,
username: selectedUser.username
})
: ""
selectedUser
? getUserDisplayName({
email: selectedUser.email,
name: selectedUser.name,
username: selectedUser.username
})
: ""
}
title={t(
isRemovingSelf ? "userRemoveOrgSelf" : "userRemoveOrg"
)}
title={t("userRemoveOrg")}
/>
<ControlledDataTable

View File

@@ -11,7 +11,7 @@ import { cn } from "@app/lib/cn";
import { CheckIcon } from "lucide-react";
import { useTranslations } from "next-intl";
export type TagValue = { text: string; id: string; isAdmin?: boolean };
export type TagValue = { text: string; id: string };
export type MultiSelectTagsProps<T extends TagValue> = {
emptyPlaceholder?: string;

View File

@@ -6,7 +6,7 @@ import { useDebounce } from "use-debounce";
import { useTranslations } from "next-intl";
import { MultiSelectTagInput } from "./multi-select/multi-select-tag-input";
export type SelectedRole = { id: string; text: string; isAdmin?: boolean };
export type SelectedRole = { id: string; text: string };
export type RolesSelectorProps = {
orgId: string;