Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
3c80e07
Modernize Frauden UI
rcarvajalp May 9, 2026
488b2e5
Merge pull request #1 from rcarvajalp/codex/estilizar-interfaz-de-usu…
rcarvajalp May 9, 2026
8f2f915
Update hero subtitle
rcarvajalp May 10, 2026
b581721
Merge pull request #2 from rcarvajalp/codex/analyze-options-to-disabl…
rcarvajalp May 10, 2026
0fbda64
Grant delete where storage write is allowed
rcarvajalp May 11, 2026
512a08d
Merge pull request #3 from rcarvajalp/codex/locate-folder-or-bucket-c…
rcarvajalp May 11, 2026
23dca19
Use resource-defined S3 bucket names
rcarvajalp May 11, 2026
2b5b6eb
Merge pull request #4 from rcarvajalp/codex/locate-buckets-without-pr…
rcarvajalp May 11, 2026
b5f6ebe
Avoid physical bucket rename collision
rcarvajalp May 11, 2026
094eac4
Merge pull request #5 from rcarvajalp/codex/locate-buckets-without-pr…
rcarvajalp May 11, 2026
3ff587f
Remove reusable bucket name constants
rcarvajalp May 11, 2026
8a1d05b
Merge branch 'main' into codex/locate-buckets-without-prefixes-or-suf…
rcarvajalp May 11, 2026
b8225c9
Merge pull request #6 from rcarvajalp/codex/locate-buckets-without-pr…
rcarvajalp May 11, 2026
d3098d1
Revert "Merge pull request #5 from rcarvajalp/codex/locate-buckets-wi…
rcarvajalp May 12, 2026
47fef5c
Revert "Merge pull request #4 from rcarvajalp/codex/locate-buckets-wi…
rcarvajalp May 12, 2026
7fa26ae
quitar la palabra clave bucket del nombre del storage
rcarvajalp May 12, 2026
dd647df
Tighten hero title layout
rcarvajalp May 12, 2026
3190c6d
Merge pull request #7 from rcarvajalp/codex/create-pull-request
rcarvajalp May 12, 2026
4435e4d
Merge branch 'main' of https://github.com/rcarvajalp/frauden-amplify-…
rcarvajalp May 12, 2026
648c17a
Show storage bucket friendly names
rcarvajalp May 12, 2026
a7756f5
Merge branch 'main' into codex/create-pull-request-cwyxy7
rcarvajalp May 12, 2026
2156983
Merge pull request #8 from rcarvajalp/codex/create-pull-request-cwyxy7
rcarvajalp May 12, 2026
1db3610
Fix storage browser data table override
rcarvajalp May 12, 2026
b615b17
Merge pull request #9 from rcarvajalp/codex/fix-typescript-build-errors
rcarvajalp May 12, 2026
a3884c7
Fix storage browser friendly bucket labels
rcarvajalp May 12, 2026
6cfbb4f
Merge pull request #10 from rcarvajalp/codex/fix-folder-table-logic-e…
rcarvajalp May 12, 2026
b9ff508
Show real bucket names in storage browser
rcarvajalp May 12, 2026
e8a95aa
Merge pull request #11 from rcarvajalp/codex/revert-friendly-bucket-l…
rcarvajalp May 12, 2026
fa516b8
Hide bucket column in locations table
rcarvajalp May 12, 2026
dc02513
Merge pull request #12 from rcarvajalp/codex/ocultar-columna-nombre-b…
rcarvajalp May 12, 2026
bed39c1
Hide bucket column in folders table
rcarvajalp May 12, 2026
db7044c
Merge pull request #13 from rcarvajalp/codex/remove-bucket-name-colum…
rcarvajalp May 12, 2026
a2a9d2a
Add knowledge sync button
rcarvajalp May 12, 2026
2ad4f3c
Merge pull request #14 from rcarvajalp/codex/add-sync-knowledge-butto…
rcarvajalp May 12, 2026
7d66a4d
Add knowledge sync button tooltip
rcarvajalp May 17, 2026
174bf54
Merge pull request #15 from rcarvajalp/codex/remove-text-and-add-tool…
rcarvajalp May 17, 2026
6568376
Restrict file deletion to eliminadores group
rcarvajalp May 17, 2026
e3fc06d
Merge pull request #16 from rcarvajalp/codex/add-user-group-for-file-…
rcarvajalp May 17, 2026
ff5b06f
Update Amplify branch linker runtime to Node.js 24
rcarvajalp Jul 22, 2026
711516e
Update knowledge sync and storage access
rcarvajalp Jul 22, 2026
ba2cfee
Restrict private expediente storage to gexpedientes
rcarvajalp Jul 22, 2026
3c099eb
Move expediente access restrictions to bucket policy
rcarvajalp Jul 22, 2026
5bdbaab
Isolate expediente access policies in custom stack
rcarvajalp Jul 22, 2026
a1f7fd3
Stop recreating existing gexpedientes group
rcarvajalp Jul 22, 2026
756e523
Link existing gexpedientes group role
rcarvajalp Jul 22, 2026
022929b
Allow linking gexpedientes group role
rcarvajalp Jul 22, 2026
d671002
Configure expedientes bucket SSE-KMS
rcarvajalp Aug 17, 2026
eb5a857
Customize storage browser presentation and add tests
rcarvajalp Sep 9, 2026
74de9d8
Replace unsupported Storage Browser DataTable with compatible renderer
rcarvajalp Sep 9, 2026
40b501d
Enforce uploader access controls and session inactivity expiry
rcarvajalp Sep 9, 2026
5e25151
Merge pull request #17 from rcarvajalp/codex/permisos-changes-uploaders
rcarvajalp Sep 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 49 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@ This template equips you with a foundational React application integrated with A

## Features

- **Authentication**: Setup with Amazon Cognito for secure user authentication with email login.
- More info on how to setup and configuration option: https://docs.amplify.aws/react/build-a-backend/auth/set-up-auth/
- **Authentication**: Setup with Amazon Cognito for secure user authentication with email login.
- More info on how to setup and configuration option: https://docs.amplify.aws/react/build-a-backend/auth/set-up-auth/
- **Storage**: Configured with multiple S3 buckets and granular access controls. The sample is configured with
- Default storage bucket with public, admin, and private access paths
- Secondary storage bucket with separate backup paths.
- Default `frauden-bucket` storage bucket with `doctrina`, `medios`, `jurisprudencia`, and `legislacion` access paths for authenticated readers and admin read/write/delete access.
- Secondary `frauden-expedientes` storage bucket with owner-scoped `privado/{entity_id}` access paths. Read/write access is limited to users in the `gexpedientes` group for their own identity folder, with delete permissions for the `eliminadores` group.
- More info on how to setup : https://docs.amplify.aws/react/build-a-backend/storage/set-up-storage/#building-your-storage-backend
- **UI Components**: Pre-integrated Amplify UI React components including:
- Authenticator for sign-in/sign-up flows
- More info : https://ui.docs.amplify.aws/react/connected-components/authenticator
- More info : https://ui.docs.amplify.aws/react/connected-components/authenticator
- Storage Browser for S3 file management.
- More info : https://ui.docs.amplify.aws/react/connected-components/storage/storage-browser
- More info : https://ui.docs.amplify.aws/react/connected-components/storage/storage-browser

## Project Structure

Expand All @@ -33,22 +33,65 @@ This template equips you with a foundational React application integrated with A
└── package.json # Project dependencies
```

## Permisos de almacenamiento

Las precedencias de los grupos de Cognito son únicas y se evalúan en este orden:
`uploaders` (0), `admin` (1), `eliminadores` (2) y `gexpedientes` (3). Por ello,
un usuario que pertenezca a `uploaders` y a cualquier otro de esos grupos conserva
el perfil restrictivo de `uploaders`.

| Perfil efectivo | Almacenamiento principal (`doctrina/`, `medios/`, `jurisprudencia/`, `legislacion/`) | `frauden-expedientes` | Acciones visibles |
| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `uploaders` | Listar y subir/reemplazar (`ListBucket`, `PutObject`). Denegación explícita de obtener versiones/objetos y eliminar versiones/objetos. | Sin acceso | Solo `Subir`; sin selección, descarga, copia, eliminación ni creación dedicada de carpetas |
| `admin` | Lectura y escritura según las reglas de Amplify existentes | Sin cambios | Sin cambios |
| `eliminadores` | Eliminación según las reglas de Amplify existentes | Eliminación según las reglas de Amplify existentes | Sin cambios |
| `gexpedientes` | Sin cambios | Lectura/escritura de la carpeta privada de su identidad | Sin cambios |
| Autenticado sin esos grupos | Lectura según las reglas de Amplify existentes | Sin acceso por la denegación del rol autenticado | Sin cambios |

`Sincronizar conocimiento` permanece disponible para `uploaders`; su endpoint,
token y payload no cambian. El grupo `uploaders` debe existir previamente en el
User Pool. Amplify genera su rol y salidas de cliente, omite únicamente la creación
del grupo y usa `UpdateGroup` para asociar el grupo existente con el rol generado.

## Vencimiento por inactividad

Toda sesión de esta aplicación web vence tras 60 minutos efectivos sin actividad.
Se consideran actividad el teclado, puntero, toque y desplazamiento, además de las
listas/búsquedas, cargas, copias, eliminaciones, creación de carpetas, preparación
de descargas y solicitudes de sincronización. La renovación automática de tokens
no cuenta como actividad.

Mientras exista una operación activa el cierre se aplaza. Al finalizar la última
operación comienza un nuevo período completo de 60 minutos. La última actividad y
los marcadores de operación se comparten entre pestañas mediante `localStorage` y
`BroadcastChannel`; los marcadores tienen una concesión renovable y caducan si una
pestaña desaparece abruptamente. Al volver a una pestaña se comprueba el vencimiento
antes de aceptar nueva actividad.

El vencimiento ejecuta un cierre local de Cognito una sola vez por pestaña, vuelve
al Authenticator y muestra el motivo. No hay aviso previo ni cierre global de otros
dispositivos. Esta es una garantía de la aplicación web, no un control centralizado
sobre tokens utilizados fuera de ella.

## Getting Started

### Installation

1. Clone this repository

```bash
git clone <repository-url>
cd sample-amplify-storage-browser
```

2. Install dependencies

```bash
npm install
```

3. Initialize and deploy the Amplify backend

```bash
npx ampx sandbox
```
Expand All @@ -74,5 +117,4 @@ See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more inform

This library is licensed under the MIT-0 License. See the LICENSE file.


_These sample applications are provided as a reference to help get started easily and are not supported by AWS Support._
4 changes: 3 additions & 1 deletion amplify/auth/resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ export const auth = defineAuth({
loginWith: {
email: true,
},
groups: ['admin']
// Array order defines Cognito group precedence. `uploaders` must win for
// users that also belong to a more privileged group.
groups: ['uploaders', 'admin', 'eliminadores'],
});
224 changes: 219 additions & 5 deletions amplify/backend.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,227 @@
import { defineBackend } from '@aws-amplify/backend';
import { Aspects, CfnCondition, Fn, IAspect, Stack } from 'aws-cdk-lib';
import {
AwsCustomResource,
AwsCustomResourcePolicy,
PhysicalResourceId,
} from 'aws-cdk-lib/custom-resources';
import { Effect, FederatedPrincipal, Policy, PolicyStatement, Role } from 'aws-cdk-lib/aws-iam';
import { Key } from 'aws-cdk-lib/aws-kms';
import { CfnFunction } from 'aws-cdk-lib/aws-lambda';
import { CfnBucket } from 'aws-cdk-lib/aws-s3';
import { IConstruct } from 'constructs';
import { auth } from './auth/resource';
import { storage, secondaryStorage } from './storage/resource';

import { secondaryStorage, storage } from './storage/resource';

/**
* @see https://docs.amplify.aws/react/build-a-backend/ to add storage, functions, and more
*/
defineBackend({
const backend = defineBackend({
auth,
storage,
secondaryStorage
storage,
secondaryStorage,
});

// Do not override cfnBucket.bucketName here. S3 bucket names are globally unique,
// and Amplify's generated physical names avoid collisions during deployments.

class GeneratedNodeRuntime implements IAspect {
visit(node: IConstruct) {
if (
node instanceof CfnFunction &&
(node.runtime === 'nodejs18.x' || node.node.path.includes('AmplifyBranchLinker'))
) {
node.runtime = 'nodejs24.x';
}
}
}

Aspects.of(backend.stack).add(new GeneratedNodeRuntime());

const expedientesBucket = backend.secondaryStorage.resources.bucket;
const expedientesKmsKeyArn =
'arn:aws:kms:us-east-1:953290282809:key/e0b3229c-c603-4e8f-9157-8abbed28943f';
const expedientesCfnBucket = expedientesBucket.node.defaultChild as CfnBucket;

expedientesCfnBucket.bucketEncryption = {
serverSideEncryptionConfiguration: [
{
serverSideEncryptionByDefault: {
sseAlgorithm: 'aws:kms',
kmsMasterKeyId: expedientesKmsKeyArn,
},
},
],
};

const expedientesAccessStack = backend.createStack('expedientesAccess');
const primaryBucket = backend.storage.resources.bucket;
const expedientesKmsKey = Key.fromKeyArn(
expedientesAccessStack,
'ExpedientesKmsKey',
expedientesKmsKeyArn
);
const gexpedientesGroupName = 'gexpedientes';
const uploadersGroupName = 'uploaders';
const privateIdentityPrefix = 'privado/${cognito-identity.amazonaws.com:sub}';
const uploadersGroup = backend.auth.resources.groups[uploadersGroupName];

if (!uploadersGroup) {
throw new Error('Amplify did not generate the expected uploaders group resources.');
}

// `uploaders` already exists in the deployed User Pool. Keep Amplify's generated
// IAM role and client outputs, but make only the CfnUserPoolGroup resource a
// no-op so CloudFormation never attempts to recreate the existing group.
const skipExistingUploadersGroup = new CfnCondition(
Stack.of(uploadersGroup.cfnUserGroup),
'SkipExistingUploadersGroupCreation',
{ expression: Fn.conditionEquals('existing', 'create') }
);
uploadersGroup.cfnUserGroup.cfnOptions.condition = skipExistingUploadersGroup;

const linkUploadersGroupRoleCall = {
service: 'cognito-identity-provider',
action: 'UpdateGroup',
parameters: {
GroupName: uploadersGroupName,
UserPoolId: backend.auth.resources.userPool.userPoolId,
RoleArn: uploadersGroup.role.roleArn,
Precedence: 0,
},
physicalResourceId: PhysicalResourceId.of('uploaders-group-role-link'),
};

new AwsCustomResource(expedientesAccessStack, 'LinkUploadersGroupRole', {
onCreate: linkUploadersGroupRoleCall,
onUpdate: linkUploadersGroupRoleCall,
installLatestAwsSdk: false,
policy: AwsCustomResourcePolicy.fromStatements([
new PolicyStatement({
effect: Effect.ALLOW,
actions: ['cognito-idp:UpdateGroup'],
resources: [backend.auth.resources.userPool.userPoolArn],
}),
new PolicyStatement({
effect: Effect.ALLOW,
actions: ['iam:PassRole'],
resources: [uploadersGroup.role.roleArn],
}),
]),
});

new Policy(expedientesAccessStack, 'UploadersObjectReadDeleteDeny', {
roles: [uploadersGroup.role],
statements: [
new PolicyStatement({
effect: Effect.DENY,
actions: ['s3:GetObject', 's3:GetObjectVersion', 's3:DeleteObject', 's3:DeleteObjectVersion'],
resources: [
`${primaryBucket.bucketArn}/doctrina/*`,
`${primaryBucket.bucketArn}/medios/*`,
`${primaryBucket.bucketArn}/jurisprudencia/*`,
`${primaryBucket.bucketArn}/legislacion/*`,
],
}),
],
});

// Storage group rules replace {entity_id} with a wildcard. Keep the owner-scoped
// storage output and enforce the gexpedientes gate from a separate stack. The
// Cognito group already exists in the deployed User Pool, so link it to this
// role instead of asking CloudFormation to create the group again.
const gexpedientesRole = new Role(expedientesAccessStack, 'GExpedientesGroupRole', {
assumedBy: new FederatedPrincipal(
'cognito-identity.amazonaws.com',
{
StringEquals: {
'cognito-identity.amazonaws.com:aud': backend.auth.resources.identityPoolId,
},
'ForAnyValue:StringLike': {
'cognito-identity.amazonaws.com:amr': 'authenticated',
},
},
'sts:AssumeRoleWithWebIdentity'
),
});

expedientesKmsKey.grantEncryptDecrypt(gexpedientesRole);

const linkGExpedientesGroupRoleCall = {
service: 'cognito-identity-provider',
action: 'UpdateGroup',
parameters: {
GroupName: gexpedientesGroupName,
UserPoolId: backend.auth.resources.userPool.userPoolId,
RoleArn: gexpedientesRole.roleArn,
Precedence: 3,
},
physicalResourceId: PhysicalResourceId.of('gexpedientes-group-role-link'),
};

new AwsCustomResource(expedientesAccessStack, 'LinkGExpedientesGroupRole', {
onCreate: linkGExpedientesGroupRoleCall,
onUpdate: linkGExpedientesGroupRoleCall,
installLatestAwsSdk: false,
policy: AwsCustomResourcePolicy.fromStatements([
new PolicyStatement({
effect: Effect.ALLOW,
actions: ['cognito-idp:UpdateGroup'],
resources: [backend.auth.resources.userPool.userPoolArn],
}),
new PolicyStatement({
effect: Effect.ALLOW,
actions: ['iam:PassRole'],
resources: [gexpedientesRole.roleArn],
}),
]),
});

new Policy(expedientesAccessStack, 'GExpedientesPrivateFolderAccess', {
roles: [gexpedientesRole],
statements: [
new PolicyStatement({
effect: Effect.ALLOW,
actions: ['s3:GetObject', 's3:PutObject'],
resources: [`${expedientesBucket.bucketArn}/${privateIdentityPrefix}/*`],
}),
new PolicyStatement({
effect: Effect.ALLOW,
actions: ['s3:ListBucket'],
resources: [expedientesBucket.bucketArn],
conditions: {
StringLike: {
's3:prefix': [`${privateIdentityPrefix}/*`, `${privateIdentityPrefix}/`],
},
},
}),
],
});

new Policy(expedientesAccessStack, 'AuthenticatedPrivateFolderDeny', {
roles: [backend.auth.resources.authenticatedUserIamRole],
statements: [
new PolicyStatement({
effect: Effect.DENY,
actions: ['s3:GetObject', 's3:PutObject'],
resources: [`${expedientesBucket.bucketArn}/privado/*`],
}),
new PolicyStatement({
effect: Effect.DENY,
actions: ['s3:ListBucket'],
resources: [expedientesBucket.bucketArn],
conditions: {
StringLike: {
's3:prefix': ['privado/*', 'privado/'],
},
},
}),
],
});

const { cfnUserPool } = backend.auth.resources.cfnResources;

cfnUserPool.adminCreateUserConfig = {
...cfnUserPool.adminCreateUserConfig,
allowAdminCreateUserOnly: true,
};
Loading