Add Appwrite proxy functions and security rules

This commit is contained in:
Tiago Yamamoto 2025-12-11 18:06:11 -03:00
parent 7ca2a9d911
commit 37419a4492
10 changed files with 313 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
node_modules
npm-debug.log

30
SECURITY.md Normal file
View file

@ -0,0 +1,30 @@
# Segurança e Permissões do Appwrite
As coleções do projeto seguem uma política de Row-Level Security (RLS) para proteger os dados dos usuários e os registros de auditoria.
## Princípios gerais
- Cada documento pertence a um usuário autenticado, identificado pelo `userId` armazenado no documento.
- Operações de leitura e gravação são restritas ao proprietário do documento (role `member`).
- Logs de auditoria globais só podem ser consultados por usuários com a role `admin`.
## Regras por coleção
### `cloud_accounts`
- **Leitura**: apenas o usuário que criou o documento (role `member`).
- **Criação**: apenas usuários autenticados.
- **Atualização/Exclusão**: somente o proprietário do documento.
### `projects`
- **Leitura**: restrita ao proprietário do documento.
- **Criação**: usuários autenticados podem criar seus próprios registros.
- **Atualização/Exclusão**: apenas o proprietário do documento.
### `audit_logs`
- **Leitura**: exclusiva para usuários com role `admin` (para auditoria global).
- **Criação**: serviços e funções podem registrar ações em nome dos usuários; cada entrada mantém o `userId` responsável.
- **Atualização/Exclusão**: não permitidas para usuários finais; apenas processos administrativos podem gerenciar retenção.
## Considerações adicionais
- Tokens e chaves sensíveis (GitHub, Cloudflare) devem ser armazenados em `cloud_accounts` e nunca enviados ao frontend.
- Funções Cloud (como as de proxy) devem validar o `userId` associado ao documento antes de usar qualquer chave.
- Revogue ou rotacione chaves comprometidas removendo ou atualizando o documento correspondente na coleção.

View file

@ -0,0 +1,8 @@
{
"$schema": "https://appwrite.io/docs/schemas/functions.json",
"name": "check-cloudflare-status",
"entrypoint": "src/index.js",
"runtime": "node-20.0",
"commands": ["npm install"],
"ignore": ["node_modules", ".npm", "npm-debug.log", "build"]
}

View file

@ -0,0 +1,31 @@
{
"name": "check-cloudflare-status",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "check-cloudflare-status",
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"node-appwrite": "^14.0.0"
}
},
"node_modules/node-appwrite": {
"version": "14.2.0",
"resolved": "https://registry.npmjs.org/node-appwrite/-/node-appwrite-14.2.0.tgz",
"integrity": "sha512-sPPA+JzdBJRS+lM6azX85y3/6iyKQYlHcXCbjMuWLROh6IiU9EfXRW3XSUTa5HDoBrlo8ve+AnVA6BIjQfUs1g==",
"license": "BSD-3-Clause",
"dependencies": {
"node-fetch-native-with-agent": "1.7.2"
}
},
"node_modules/node-fetch-native-with-agent": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/node-fetch-native-with-agent/-/node-fetch-native-with-agent-1.7.2.tgz",
"integrity": "sha512-5MaOOCuJEvcckoz7/tjdx1M6OusOY6Xc5f459IaruGStWnKzlI1qpNgaAwmn4LmFYcsSlj+jBMk84wmmRxfk5g==",
"license": "MIT"
}
}
}

View file

@ -0,0 +1,10 @@
{
"name": "check-cloudflare-status",
"version": "1.0.0",
"type": "module",
"main": "src/index.js",
"license": "MIT",
"dependencies": {
"node-appwrite": "^14.0.0"
}
}

View file

@ -0,0 +1,107 @@
import { Client, Databases } from 'node-appwrite';
const APPWRITE_ENDPOINT = process.env.APPWRITE_ENDPOINT || process.env.APPWRITE_FUNCTION_ENDPOINT;
const APPWRITE_PROJECT_ID = process.env.APPWRITE_PROJECT_ID || process.env.APPWRITE_FUNCTION_PROJECT_ID;
const APPWRITE_API_KEY = process.env.APPWRITE_API_KEY;
const DATABASE_ID = process.env.APPWRITE_DATABASE_ID;
const cfHeaders = (token) => ({
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json'
});
async function fetchZones(token, log) {
const response = await fetch('https://api.cloudflare.com/client/v4/zones', {
headers: cfHeaders(token)
});
if (!response.ok) {
const body = await response.text();
log(`Cloudflare zones error: ${body}`);
throw new Error('Failed to fetch Cloudflare zones');
}
const { result } = await response.json();
return result.map((zone) => ({
id: zone.id,
name: zone.name,
status: zone.status,
paused: zone.paused
}));
}
async function fetchWorkers(token, accountId, log) {
if (!accountId) return [];
const response = await fetch(`https://api.cloudflare.com/client/v4/accounts/${accountId}/workers/scripts`, {
headers: cfHeaders(token)
});
if (!response.ok) {
const body = await response.text();
log(`Cloudflare workers error: ${body}`);
throw new Error('Failed to fetch Cloudflare workers');
}
const { result } = await response.json();
return result.map((worker) => ({
name: worker.name,
modifiedOn: worker.modified_on,
active: worker.created_on !== undefined
}));
}
export default async function ({ req, res, log, error }) {
try {
if (!APPWRITE_ENDPOINT || !APPWRITE_PROJECT_ID || !APPWRITE_API_KEY || !DATABASE_ID) {
return res.json({ error: 'Missing Appwrite environment configuration.' }, 500);
}
const payload = req.body ? JSON.parse(req.body) : {};
const accountId = payload.accountId;
const cloudflareAccountId = payload.cloudflareAccountId;
const requesterId =
(req.headers && (req.headers['x-appwrite-user-id'] || req.headers['x-appwrite-userid'])) ||
process.env.APPWRITE_FUNCTION_USER_ID ||
payload.userId;
if (!accountId) {
return res.json({ error: 'accountId is required in the request body.' }, 400);
}
const client = new Client()
.setEndpoint(APPWRITE_ENDPOINT)
.setProject(APPWRITE_PROJECT_ID)
.setKey(APPWRITE_API_KEY);
const databases = new Databases(client);
const account = await databases.getDocument(DATABASE_ID, 'cloud_accounts', accountId);
if (!account || account.provider !== 'cloudflare') {
return res.json({ error: 'Cloud account not found or not a Cloudflare credential.' }, 404);
}
if (account.userId && requesterId && account.userId !== requesterId) {
return res.json({ error: 'You are not allowed to use this credential.' }, 403);
}
const token = account.apiKey;
if (!token) {
return res.json({ error: 'Cloudflare token is missing for this account.' }, 400);
}
const [zones, workers] = await Promise.all([
fetchZones(token, log),
fetchWorkers(token, cloudflareAccountId, log)
]);
return res.json({
zones,
workers,
message: workers.length ? 'Zones and Workers status fetched successfully.' : 'Zones status fetched successfully.'
});
} catch (err) {
error(err.message);
return res.json({ error: 'Unexpected error while checking Cloudflare status.' }, 500);
}
}

View file

@ -0,0 +1,8 @@
{
"$schema": "https://appwrite.io/docs/schemas/functions.json",
"name": "sync-github",
"entrypoint": "src/index.js",
"runtime": "node-20.0",
"commands": ["npm install"],
"ignore": ["node_modules", ".npm", "npm-debug.log", "build"]
}

View file

@ -0,0 +1,31 @@
{
"name": "sync-github",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sync-github",
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"node-appwrite": "^14.0.0"
}
},
"node_modules/node-appwrite": {
"version": "14.2.0",
"resolved": "https://registry.npmjs.org/node-appwrite/-/node-appwrite-14.2.0.tgz",
"integrity": "sha512-sPPA+JzdBJRS+lM6azX85y3/6iyKQYlHcXCbjMuWLROh6IiU9EfXRW3XSUTa5HDoBrlo8ve+AnVA6BIjQfUs1g==",
"license": "BSD-3-Clause",
"dependencies": {
"node-fetch-native-with-agent": "1.7.2"
}
},
"node_modules/node-fetch-native-with-agent": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/node-fetch-native-with-agent/-/node-fetch-native-with-agent-1.7.2.tgz",
"integrity": "sha512-5MaOOCuJEvcckoz7/tjdx1M6OusOY6Xc5f459IaruGStWnKzlI1qpNgaAwmn4LmFYcsSlj+jBMk84wmmRxfk5g==",
"license": "MIT"
}
}
}

View file

@ -0,0 +1,10 @@
{
"name": "sync-github",
"version": "1.0.0",
"type": "module",
"main": "src/index.js",
"license": "MIT",
"dependencies": {
"node-appwrite": "^14.0.0"
}
}

View file

@ -0,0 +1,76 @@
import { Client, Databases } from 'node-appwrite';
const APPWRITE_ENDPOINT = process.env.APPWRITE_ENDPOINT || process.env.APPWRITE_FUNCTION_ENDPOINT;
const APPWRITE_PROJECT_ID = process.env.APPWRITE_PROJECT_ID || process.env.APPWRITE_FUNCTION_PROJECT_ID;
const APPWRITE_API_KEY = process.env.APPWRITE_API_KEY;
const DATABASE_ID = process.env.APPWRITE_DATABASE_ID;
const githubHeaders = (token) => ({
Authorization: `Bearer ${token}`,
Accept: 'application/vnd.github+json',
'User-Agent': 'appwrite-sync-github'
});
export default async function ({ req, res, log, error }) {
try {
if (!APPWRITE_ENDPOINT || !APPWRITE_PROJECT_ID || !APPWRITE_API_KEY || !DATABASE_ID) {
return res.json({ error: 'Missing Appwrite environment configuration.' }, 500);
}
const payload = req.body ? JSON.parse(req.body) : {};
const accountId = payload.accountId;
const requesterId =
(req.headers && (req.headers['x-appwrite-user-id'] || req.headers['x-appwrite-userid'])) ||
process.env.APPWRITE_FUNCTION_USER_ID ||
payload.userId;
if (!accountId) {
return res.json({ error: 'accountId is required in the request body.' }, 400);
}
const client = new Client()
.setEndpoint(APPWRITE_ENDPOINT)
.setProject(APPWRITE_PROJECT_ID)
.setKey(APPWRITE_API_KEY);
const databases = new Databases(client);
const account = await databases.getDocument(DATABASE_ID, 'cloud_accounts', accountId);
if (!account || account.provider !== 'github') {
return res.json({ error: 'Cloud account not found or not a GitHub credential.' }, 404);
}
if (account.userId && requesterId && account.userId !== requesterId) {
return res.json({ error: 'You are not allowed to use this credential.' }, 403);
}
const token = account.apiKey;
if (!token) {
return res.json({ error: 'GitHub token is missing for this account.' }, 400);
}
const githubResponse = await fetch('https://api.github.com/user/repos?per_page=100', {
headers: githubHeaders(token)
});
if (!githubResponse.ok) {
const body = await githubResponse.text();
log(`GitHub API error: ${body}`);
return res.json({ error: 'Failed to fetch repositories from GitHub.' }, githubResponse.status);
}
const repositories = await githubResponse.json();
const simplified = repositories.map((repo) => ({
id: repo.id,
name: repo.name,
fullName: repo.full_name,
private: repo.private,
url: repo.html_url,
defaultBranch: repo.default_branch
}));
return res.json({ repositories: simplified }, 200);
} catch (err) {
error(err.message);
return res.json({ error: 'Unexpected error while syncing with GitHub.' }, 500);
}
}