Ir al contenido

API de acceso a proyectos

Cada proyecto almacena su propio modo de acceso — public o private — como una columna del propio proyecto; ya no se infiere a partir de los datos de membresía. public (el modo por defecto con el que empieza todo proyecto) significa que cualquier miembro activo de la organización puede alcanzarlo, salvo una exclusión individual revoked. private significa que solo los miembros con una fila granted — más el bypass de propietario/administrador de la organización — pueden alcanzarlo, y esa lista de acceso puede legítimamente estar vacía: un proyecto privado con cero miembros otorgados sigue siendo privado, no retrocede a público. Este recurso es dedicado: el estado de acceso nunca se agrega al objeto general del proyecto que devuelve GET /api/v1/projects/{projectId} — esa forma permanece sin cambios.

{projectId} es el código del proyecto (por ejemplo ACME) en todo lo siguiente.

El propietario de un proyecto se asigna al crearlo: el usuario que lo crea queda como propietario. Un proyecto creado con un token de API no tiene usuario a quien atribuirlo, así que nace sin asignar (ownerUserUlid es null) y necesita un PATCH explícito para tener propietario.

GET/api/v1/projects/{projectId}/access

200 con:

CampoNotas
modeEl modo de acceso "public" o "private" almacenado del proyecto
ownerUserUlidEl ULID del propietario del proyecto, o null cuando no está asignado (“Sin asignar”)
grantedCountTamaño de la lista de acceso. No es la señal del modo — un proyecto privado PUEDE reportar grantedCount: 0; nunca infieras el modo a partir de este número en ningún sentido.
{
"mode": "private",
"ownerUserUlid": "01J...OWNER",
"grantedCount": 2
}
PATCH/api/v1/projects/{projectId}/access

Acepta uno o ambos de:

CampoNotas
mode"public" o "private" — alterna el modo de acceso
ownerUserUlidULID del nuevo propietario, o null para dejarlo sin asignar

Se requiere al menos un campo; un cuerpo vacío devuelve 422 validation_failed.

Cambiar el estado de acceso es una pregunta distinta a la de leer un proyecto. Solo un propietario u administrador de la organización, o el propietario actual del proyecto, pueden invocar este endpoint con éxito — un miembro que solo tiene una fila granted (de lectura) en un proyecto privado no está automáticamente autorizado a alternar el modo, y recibe 403 forbidden.

Transferir la propiedad sigue la misma regla: quien llama debe ser propietario/administrador de la organización o el propietario actual. El nuevo propietario debe ser un miembro activo de la organización.

Cambiar un proyecto de público a privado crea exactamente dos filas granted: el propietario del proyecto y el usuario que realiza la acción — una sola fila si son la misma persona, y solo la del actor si el proyecto aún no tiene propietario. Cualquier otro miembro de la organización pierde el acceso en su siguiente solicitud (salvo que su propio rol de organización tenga bypass de acceso a nivel de proyecto — ver abajo).

Cambiar un proyecto de privado a público descarta toda la lista de acceso. Si el proyecto vuelve a hacerse privado más tarde, empieza desde cero y vuelve a sembrar solo al propietario y al actor — los accesos anteriores no se restauran.

Una vez que un proyecto es private, se pueden agregar o quitar miembros individuales de su lista de acceso sin cambiar nunca el modo del proyecto. Solo PATCH /access { mode } cambia el modo, en cualquier dirección.

GET/api/v1/projects/{projectId}/access/members

Paginado (?page=/?pageSize=, ambos opcionales). Devuelve 200 con cada miembro activo de la organización que tiene una fila granted para el proyecto o que lo alcanza mediante el bypass de rol owner/admin de la organización. En un proyecto público devuelve una página vacía (items: [], total: 0) — nunca responde 404.

{
"items": [
{
"userUlid": "01J...MEMBER",
"email": "ada@example.com",
"firstName": "Ada",
"lastName": "Lovelace",
"avatarKey": null,
"avatarVersion": 0,
"role": "member",
"positionTitle": "QA Lead"
}
],
"page": 1,
"pageSize": 50,
"total": 1
}

Cada elemento no lleva campo de procedencia de acceso: no puede indicar por qué aparece un miembro (un otorgamiento directo o el bypass de rol de organización). Combina esta respuesta con GET /access (grantedCount) y la lista de miembros de la organización para construir una vista de auditoría o cumplimiento que necesite esa distinción.

POST/api/v1/projects/{projectId}/access/members
{ "userUlid": "01J...MEMBER" }

Devuelve 201 con el miembro otorgado (misma forma que un elemento de la lista). Requiere el permiso de gestión de proyectos (que tienen owner/admin), o el propietario actual del proyecto — tener una fila granted por sí sola no autoriza a otorgar acceso a otros. Se rechaza con 409 conflict en un proyecto público, y con 404 not_found para un miembro que ya no está activo (se fue de la organización). Volver a otorgar acceso a un miembro previamente revocado sobrescribe esa exclusión voluntaria.

DELETE/api/v1/projects/{projectId}/access/members/{userUlid}

Devuelve 204 si tiene éxito. Misma regla de autorización que otorgar acceso. Se rechaza con 409 conflict en un proyecto público. Revocar la última fila granted restante ahora tiene éxito: el proyecto permanece private con la lista de acceso vacía — no se publica. Publicar un proyecto conserva exactamente un punto de entrada, PATCH /access { mode: "public" }. Revocar nunca escribe una fila revoked; elimina la fila granted directamente.

Un grupo de usuarios también puede asignarse a la lista de acceso de un proyecto, otorgando acceso a todos los miembros actuales y futuros del grupo en una sola operación — un mecanismo separado y de grano más grueso, junto a la lista de acceso individual de arriba.

GET/api/v1/projects/{projectId}/access/groups

Basado en páginas (?page=/?pageSize=, ambos opcionales). Refleja el plano, la protección y la forma de los elementos de /access/members. A diferencia de la subcolección de miembros, este endpoint de listado nunca da 404 en un proyecto público — una asignación dormida es un estado válido y visible en cualquier modo del proyecto.

{
"items": [{ "ulid": "01J...GROUP", "name": "QA Squad", "description": null, "userCount": 4 }],
"page": 1,
"pageSize": 50,
"total": 1
}
POST/api/v1/projects/{projectId}/access/groups
{ "userGroupUlid": "01J...GROUP" }

Devuelve 201 con el grupo asignado (misma forma que un elemento del listado). Misma regla de autorización que la familia individual: owner/admin de la organización, o el propietario actual del proyecto. A diferencia de otorgar acceso a un miembro, esto tiene éxito en un proyecto público — access_mode es un dato almacenado, nunca inferido de una asignación, así que el otorgamiento queda dormido hasta que el proyecto se privatiza y sobrevive a cualquier cambio de modo posterior en cualquier dirección. Volver a asignar un grupo ya asignado es una operación idempotente sin efecto (201 de nuevo, sin duplicar la fila).

DELETE/api/v1/projects/{projectId}/access/groups/{userGroupUlid}

Devuelve 204 si tiene éxito. Misma regla de autorización que asignar. Nunca 409, ni siquiera para el último grupo restante — desasignar el último grupo no vuelve a publicar el proyecto; publicar conserva exactamente un punto de entrada, PATCH /access { mode: "public" }. Devuelve 404 not_found para un ULID de grupo desconocido, un grupo de otra organización, o un grupo que nunca fue asignado.

GET /api/v1/groups/{userGroupUlid}/projects lista los proyectos a los que está asignado un grupo dado — el reflejo centrado en el grupo del listado de arriba. Está abierto a lectura para cualquier miembro de la organización (como los demás GET de grupos de usuarios), así que, a diferencia del projectCount del propio grupo, filtra cualquier proyecto que el miembro que llama no pueda descubrir de otra forma: un proyecto privado al que el grupo está asignado se omite silenciosamente para ese llamante, mientras que un owner/admin de la organización ve todas las asignaciones. Ver Listar proyectos asignados para la forma completa.

No existe un estado revoked a nivel de grupo. La fila revoked propia de la lista de acceso individual de un miembro — establecida al eliminarlo o mediante una revocación explícita — siempre prevalece sobre un otorgamiento por grupo al que ese miembro pudiera acceder de otra forma: un grupo otorga acceso, nunca anula el veto individual.

Bypass de propietario/administrador de la organización

Sección titulada «Bypass de propietario/administrador de la organización»

Quien llama con rol de organización owner o admin siempre alcanza todos los proyectos de esa organización, sean privados o no, y siempre está autorizado a alternar el modo o transferir la propiedad.

La aplicación sigue a los datos, no a la forma de la URL

Sección titulada «La aplicación sigue a los datos, no a la forma de la URL»

El control de acceso no se limita a la familia de rutas /projects/{projectId}/*. Alcanzar el contenido de un proyecto de forma transitiva a través de un ULID de entidad — un caso de prueba, una ejecución, un defecto, un plan, un hito, un entorno, una configuración o un comentario — produce la misma decisión de acceso que dirigirse al proyecto directamente. Un miembro sin acceso otorgado en un proyecto privado recibe 403 por cualquiera de las dos vías; un miembro con acceso otorgado, y un owner/admin de la organización, alcanzan ambas de la misma forma.

Ventana de terminal
curl -sS -X PATCH \
-H "Authorization: Bearer $PROBARA_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"mode":"private"}' \
"https://app.probara.net/api/v1/projects/ACME/access"

Son huecos intencionales y documentados, no descuidos:

  • Los tokens de API no se evalúan contra la lista de acceso. Una solicitud autenticada con un token Bearer no lleva identidad por usuario para este propósito y sigue alcanzando todos los proyectos de su organización, sean privados o no — “privado” aplica solo a quienes llaman como humanos (sesión/OAuth).
  • La interfaz web cubre el modo, el propietario y los miembros individuales. La pestaña Control de acceso en la configuración del proyecto muestra el modo almacenado, permite a quien esté autorizado alternar público/privado, transferir la propiedad, y otorgar/revocar miembros individuales desde la lista de miembros individuales. Los grupos todavía no están disponibles — la pestaña Grupos es un diseño terminado sin funcionalidad; el acceso por grupos queda diferido a una versión futura.