{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# El día que subes una clave sin querer\n",
    "\n",
    "Qué hacer primero, qué hacer después y qué no sirve de nada\n",
    "\n",
    "Cuaderno de soluciones del capítulo 28 de **Git desde cero**, de Miss Yera.\n",
    "\n",
    "Corre de arriba abajo. Si lo abres en Google Colab no necesitas instalar nada.\n",
    "\n",
    "Capítulo completo: https://missyera.com/guias/git-desde-cero/subiste-una-clave/\n",
    "\n",
    "Este es el cuaderno de **soluciones**. Trae el código de cada ejercicio, la\n",
    "explicación de la trampa y la respuesta del quiz. Si vienes del cuaderno de\n",
    "práctica sin haberlo intentado, vuelve 🙂"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Antes de empezar\n",
    "\n",
    "Este capítulo son comandos de terminal, no Python. La celda de abajo baja el\n",
    "ayudante que los ejecuta y que **recuerda en qué carpeta quedaste**, que es lo\n",
    "que hace falta para que un `cd` de una celda siga valiendo en la siguiente.\n",
    "\n",
    "A partir de ahí, cada celda de comandos empieza por `%%consola`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import urllib.request\n",
    "\n",
    "# El ayudante de los cuadernos. Trae la corrección de los ejercicios y, en los\n",
    "# capítulos de consola, la celda mágica que ejecuta los comandos. Se baja en\n",
    "# vez de venir pegado aquí para que siempre sea el último.\n",
    "urllib.request.urlretrieve(\n",
    "    \"https://missyera.com/static/cuadernos/revisa.py\", \"revisa.py\")\n",
    "import revisa\n",
    "revisa.carga({}, lenguaje=\"bash\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Este capítulo lo escribo con cariño porque le pasa a todo el mundo, incluida\n",
    "gente con veinte años de oficio. Un `git add .` se llevó el\n",
    "`.env`, y ahora las claves de la base de ventas están en\n",
    "internet 😬\n",
    "\n",
    "Lo importante es el orden. Casi todo el mundo lo hace al revés."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El orden correcto"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "| Paso | Qué | Por qué |\n",
    "|---|---|---|\n",
    "| 1 | Rotar la clave en el proveedor | Es lo único que la vuelve inútil |\n",
    "| 2 | Poner el repositorio en privado, si era público | Corta el goteo mientras limpias |\n",
    "| 3 | Sacar el archivo y agregarlo al `.gitignore` | Para que no vuelva a entrar |\n",
    "| 4 | Limpiar la historia, si hace falta | Es lo más caro y lo menos urgente |"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Fíjate en que limpiar la historia es **el último** paso y no el\n",
    "primero. Es el que más tiempo cuesta y el que menos resuelve 🧯"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Por qué borrar el archivo no basta"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%consola\n",
    "mkdir tienda\n",
    "cd tienda\n",
    "git init -q\n",
    "printf 'ciudad,monto\\nLima,1200\\n' > ventas.csv\n",
    "printf 'API_KEY=clave-de-ejemplo-1234\\n' > .env\n",
    "git add .\n",
    "git commit -q -m \"Conexion a la base de ventas\"\n",
    "git rm -q .env\n",
    "git commit -q -m \"Se quita el archivo de configuracion\"\n",
    "ls -a\n",
    "git log --oneline"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El archivo ya no está en la carpeta. Pero:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%consola\n",
    "git show HEAD~1:.env\n",
    "git log --all --oneline -- .env"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahí está la clave, entera, sacada de la historia con un comando. Cualquiera\n",
    "con acceso al repositorio puede hacer eso, y en GitHub además quedó en la vista\n",
    "de commits 🔍"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Que no vuelva a entrar"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%consola\n",
    "printf 'API_KEY=otra-clave-de-ejemplo\\n' > .env\n",
    "printf '.env\\n*.key\\n' > .gitignore\n",
    "git add .gitignore\n",
    "git commit -q -m \"El archivo de configuracion no entra al repositorio\"\n",
    "git status --short\n",
    "git check-ignore -v .env"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ese `check-ignore` te dice qué línea del\n",
    "`.gitignore` está tapando el archivo, que es lo que hay que\n",
    "comprobar y no suponer. Todo esto está en 9 🚫"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Y lo que sí va al repositorio"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La costumbre que resuelve esto para siempre: un archivo de ejemplo,\n",
    "**sin valores**, que sí se sube y le dice a quien clone qué tiene\n",
    "que rellenar."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%consola\n",
    "printf 'API_KEY=\\nBASE_URL=\\n' > .env.ejemplo\n",
    "git add .env.ejemplo\n",
    "git commit -q -m \"Ejemplo de configuracion, sin valores\"\n",
    "cat .env.ejemplo\n",
    "git ls-files"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El repositorio lleva el molde y nunca los valores. Es lo que hace todo\n",
    "proyecto serio 📄"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Limpiar la historia, si hace falta"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Existe, se hace con la herramienta git filter-repo, y hay que saber tres cosas antes:\n",
    "\n",
    "- Reescribe todos los commits, o sea que cambian todos los hashes, con lo que\n",
    "vimos en 27.\n",
    "\n",
    "- Todo el que tenga una copia tiene que clonar de nuevo.\n",
    "\n",
    "- Los pull requests viejos de GitHub pueden seguir mostrando el contenido, y\n",
    "hay que pedirle soporte a GitHub que los purgue.\n",
    "\n",
    "O sea que es caro. Y con la clave ya rotada, casi nunca es urgente: lo que\n",
    "queda en la historia es una clave que no abre nada 🔒"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Buscar antes de subir"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%consola\n",
    "grep -rn \"API_KEY\\|PASSWORD\\|SECRET\" . --exclude-dir=.git --exclude=\".env\" || echo \"limpio\"\n",
    "git ls-files"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ese grep, corrido antes del primer push de un proyecto, es de las cosas que\n",
    "más disgustos ahorran. Y GitHub tiene lo suyo: el *secret scanning*, que\n",
    "avisa solo cuando detecta una clave de un proveedor conocido 🔔"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### La trampa\n",
    "\n",
    "Subiste sin querer el archivo con la clave de la API. Lo borras, haces commit y respiras.\n",
    "\n",
    "```\n",
    "$ git rm .env\n",
    "$ git commit -m \"Se quita el archivo de configuracion\"\n",
    "$ git push\n",
    "\n",
    "$ git log --all --oneline -- .env\n",
    "8f2a91c Conexion a la base de ventas\n",
    "```\n",
    "\n",
    "**Qué está mal**\n",
    "\n",
    "El archivo se fue del presente y sigue en la historia 🔍\n",
    "\n",
    "Borrar algo en Git es un cambio más: el commit de ayer sigue teniendo el archivo entero, y cualquiera puede sacarlo con un `git show`. En GitHub, además, quedó en la vista de commits.\n",
    "\n",
    "Y aunque limpiaras la historia entera, sigue habiendo algo que no controlas: **los robots que rastrean GitHub buscando claves ya la copiaron**, casi siempre en cuestión de minutos.\n",
    "\n",
    "Por eso el primer paso nunca es limpiar el repositorio. Es **rotar la clave** en el proveedor, que la vuelve inútil. Limpiar viene después y con calma."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Comprueba que se entendió"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Comprueba que lo tienes\n",
    "\n",
    "Te das cuenta de que subiste el `.env` con la clave de la API. ¿Qué haces primero?\n",
    "\n",
    "a) Rotar la clave en el proveedor\n",
    "\n",
    "b) Borrar el archivo y hacer push\n",
    "\n",
    "c) Borrar el repositorio de GitHub\n",
    "\n",
    "d) Limpiar la historia con filter-repo\n",
    "\n",
    "---\n",
    "\n",
    "**La correcta es la a.**\n",
    "\n",
    "*b)* La clave sigue entera en el commit anterior, así que no cambia nada.\n",
    "\n",
    "*c)* Corta el acceso público, pero la clave ya pudo copiarse y sigue siendo válida.\n",
    "\n",
    "*d)* Es el paso más caro y no sirve de nada si la clave sigue funcionando para quien ya la copió.\n",
    "\n",
    "Rotar primero. Todo lo demás es limpieza 🔑"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejercicios"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. Comete el error a propósito\n",
    "\n",
    "Un `git add .` que se lleva el archivo de\n",
    "configuración."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%consola\n",
    "cd ..\n",
    "mkdir bodega\n",
    "cd bodega\n",
    "git init -q\n",
    "printf 'bodega,ciudad\\nBodega Inti,Cusco\\n' > bodegas.csv\n",
    "printf 'API_KEY=clave-de-ejemplo-abcd\\n' > .env\n",
    "git add .\n",
    "git commit -q -m \"Conexion al catalogo de bodegas\"\n",
    "git ls-files"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    ".env\n",
    "bodegas.csv\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahí está el `.env` dentro del repositorio."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. Comprueba que borrarlo no basta\n",
    "\n",
    "Quítalo y sácalo igual de la historia."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%consola\n",
    "git rm -q .env\n",
    "git commit -q -m \"Se quita el archivo de configuracion\"\n",
    "ls -a\n",
    "git show HEAD~1:.env"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    ".\n",
    "..\n",
    ".git\n",
    "bodegas.csv\n",
    "API_KEY=clave-de-ejemplo-abcd\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Fuera de la carpeta y dentro de la historia. Por eso se rota primero 🔑"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. Ponle el gitignore\n",
    "\n",
    "Que no pueda volver a entrar."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%consola\n",
    "printf 'API_KEY=otra-clave\\n' > .env\n",
    "printf '.env\\n' > .gitignore\n",
    "git add .gitignore\n",
    "git commit -q -m \"El .env no entra al repositorio\"\n",
    "git status --short\n",
    "git check-ignore -v .env"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    ".gitignore:1:.env\t.env\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Comprobado, no supuesto."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4. Sube el molde sin valores\n",
    "\n",
    "El archivo de ejemplo que sí va al repositorio."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%consola\n",
    "printf 'API_KEY=\\nBASE_URL=\\n' > .env.ejemplo\n",
    "git add .env.ejemplo\n",
    "git commit -q -m \"Ejemplo de configuracion, sin valores\"\n",
    "cat .env.ejemplo\n",
    "git ls-files"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "API_KEY=\n",
    "BASE_URL=\n",
    ".env.ejemplo\n",
    ".gitignore\n",
    "bodegas.csv\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Quien clone sabe qué rellenar y no recibe ninguna clave 📄"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5. Busca secretos antes de subir\n",
    "\n",
    "La revisión de cinco segundos antes del primer push."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%consola\n",
    "grep -rn \"API_KEY\\|PASSWORD\" . --exclude-dir=.git --exclude=\".env\" || echo \"limpio\""
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "./.env.ejemplo:1:API_KEY=\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Solo aparece el molde vacío, que es justo lo que debe aparecer."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 6. Revisa toda la historia, no solo el presente\n",
    "\n",
    "Busca el archivo en cualquier commit del repositorio."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%consola\n",
    "git log --all --oneline -- .env"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "d5b0f0c Se quita el archivo de configuracion\n",
    "ce26339 Conexion al catalogo de bodegas\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Dos commits lo tocaron: el que lo metió y el que lo sacó. Los dos lo\n",
    "contienen 🔍"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 7. Pide un archivo que ya no está en el último commit\n",
    "\n",
    "Comprueba qué contesta Git cuando pides el\n",
    "`.env` de ahora."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%consola\n",
    "git show HEAD:.env"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "fatal: path '.env' exists on disk, but not in 'HEAD'\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En el presente no está, y ese es justo el error que hace creer que el\n",
    "problema se resolvió 🙈"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que te llevas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Rotar primero. Borrar el archivo no lo saca de la historia, y la\n",
    "historia es lo de menos si la clave ya no abre nada.**"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "Ese era el capítulo 28 de **Git desde cero**. El texto completo, con las salidas de cada bloque, está en https://missyera.com/guias/git-desde-cero/subiste-una-clave/\n",
    "\n",
    "Que tengas lindo día! 🌸"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.11"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
