Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
42 changes: 42 additions & 0 deletions acceptance/specs/admin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { randomUUID } from 'node:crypto'
import { ListBucketsCommand } from '@aws-sdk/client-s3'
import { describeAcceptance, getAcceptanceConfig, requireConfigValue } from '../support/config'
import { AcceptanceHttpClient, createAdminClient } from '../support/http'
import { uniqueBucketName, uniqueObjectKey } from '../support/resources'
import { createAcceptanceS3Client } from '../support/s3'

interface TenantSummary {
Expand Down Expand Up @@ -71,6 +72,10 @@ interface MessageResponse {
message?: string
}

interface GenerateSignaturesResponse extends MessageResponse {
jobId?: string
}

interface TenantMigrationRunResponse {
migrated?: boolean
}
Expand Down Expand Up @@ -503,6 +508,43 @@ describeAcceptance(
}
}
})

it('covers object signature generation scheduling for a scoped object name', async () => {
const config = getAcceptanceConfig()
const client = createAdminClient()
const headers = {
apikey: requireConfigValue(config.adminApiKey, 'ACCEPTANCE_ADMIN_API_KEY'),
}
const tenantId = await resolveTenantId(client, headers)
const bucketName = uniqueBucketName('sig')
const objectKey = uniqueObjectKey('sig')
const expectScheduling = config.target === 'local' && process.env.PG_QUEUE_ENABLE === 'true'

const scheduled = await client.request<GenerateSignaturesResponse>(
'POST',
`/tenants/${tenantId}/storage/generate-signatures`,
{
body: {
bucketId: bucketName,
force: true,
objectNames: [objectKey],
},
expectedStatus: expectScheduling ? 200 : [200, 400],
headers,
}
)

if (scheduled.status === 200) {
expect(scheduled.json?.message).toBe('Object signature generation scheduled')
expect(typeof scheduled.json?.jobId).toBe('string')
expect(scheduled.json?.jobId?.length).toBeGreaterThan(0)
} else {
expect(expectScheduling).toBe(false)
expect(scheduled.json?.message).toMatch(
/^(Queue is not enabled|Tenant migrations must include add-objects-signature before generating signatures)$/
)
}
})
}
)

Expand Down
90 changes: 90 additions & 0 deletions migrations/tenant/0061-add-objects-signature.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
ALTER TABLE storage.objects
ADD COLUMN IF NOT EXISTS signature bytea;
Comment thread
ferhatelmas marked this conversation as resolved.
Comment thread
ferhatelmas marked this conversation as resolved.

Comment thread
ferhatelmas marked this conversation as resolved.
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'objects_signature_length'
AND conrelid = 'storage.objects'::regclass
) THEN
ALTER TABLE storage.objects
ADD CONSTRAINT objects_signature_length
CHECK (signature IS NULL OR octet_length(signature) = 32)
NOT VALID;
END IF;
END $$;

DROP TRIGGER IF EXISTS update_objects_updated_at ON storage.objects;

-- Keep this list in sync with all non-generated storage.objects columns except signature.
-- Generated columns such as path_tokens are intentionally omitted.
-- The explicit row comparison avoids converting every updated object row to jsonb.
CREATE TRIGGER update_objects_updated_at
BEFORE UPDATE ON storage.objects
FOR EACH ROW
WHEN (
ROW(
NEW.id,
NEW.bucket_id,
NEW.name,
NEW.owner,
NEW.created_at,
NEW.updated_at,
NEW.last_accessed_at,
NEW.metadata,
NEW.version,
NEW.owner_id,
NEW.user_metadata
)
IS DISTINCT FROM
ROW(
OLD.id,
OLD.bucket_id,
OLD.name,
OLD.owner,
OLD.created_at,
OLD.updated_at,
OLD.last_accessed_at,
OLD.metadata,
OLD.version,
OLD.owner_id,
OLD.user_metadata
)
Comment thread
ferhatelmas marked this conversation as resolved.
)
EXECUTE PROCEDURE update_updated_at_column();

CREATE OR REPLACE FUNCTION storage.enforce_objects_signature_client_writes()
RETURNS trigger
AS $$
DECLARE
anon_role text = COALESCE(current_setting('storage.anon_role', true), 'anon');
authenticated_role text = COALESCE(current_setting('storage.authenticated_role', true), 'authenticated');
effective_role text = COALESCE(NULLIF(current_setting('role', true), 'none'), current_user);
BEGIN
IF effective_role = anon_role OR effective_role = authenticated_role THEN
IF TG_OP = 'INSERT' AND NEW.signature IS NOT NULL THEN
RAISE EXCEPTION 'Only storage service roles can set object signatures'
USING ERRCODE = '42501';
END IF;

IF TG_OP = 'UPDATE'
AND NEW.signature IS NOT NULL
AND NEW.signature IS DISTINCT FROM OLD.signature
THEN
RAISE EXCEPTION 'Only storage service roles can set object signatures'
USING ERRCODE = '42501';
END IF;
Comment thread
ferhatelmas marked this conversation as resolved.
END IF;

RETURN NEW;
END;
$$ LANGUAGE plpgsql;

DROP TRIGGER IF EXISTS enforce_objects_signature_client_writes ON storage.objects;

CREATE TRIGGER enforce_objects_signature_client_writes
BEFORE INSERT OR UPDATE ON storage.objects
FOR EACH ROW
EXECUTE FUNCTION storage.enforce_objects_signature_client_writes();
6 changes: 6 additions & 0 deletions migrations/tenant/0062-add-objects-signature-index.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-- postgres-migrations disable-transaction

CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_objects_missing_signature_bucket_id_name
ON storage.objects (bucket_id, name)
INCLUDE (version)
WHERE signature IS NULL;
1 change: 1 addition & 0 deletions src/admin-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ const build = (opts: buildOpts = {}): FastifyInstance => {
app.register(routes.tenants, { prefix: 'tenants' })
app.register(routes.objects, { prefix: 'tenants' })
app.register(routes.jwks, { prefix: 'tenants' })
app.register(routes.signatureGeneration, { prefix: 'tenants' })
app.register(routes.migrations, { prefix: 'migrations' })
if (isRunningUnderWatt) {
app.register(routes.pprof, { prefix: 'debug/pprof' })
Expand Down
1 change: 1 addition & 0 deletions src/http/routes/admin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export { default as objects } from './objects'
export { default as pprof } from './pprof'
export { default as queue } from './queue'
export { default as s3Credentials } from './s3'
export { default as signatureGeneration } from './signature-generation'
export { default as tenants } from './tenants'
Loading