Skip to content

run_code as a user instead of root #94

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 14 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
7 changes: 7 additions & 0 deletions .changeset/five-jobs-cry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@e2b/code-interpreter-template': patch
'@e2b/code-interpreter-python': patch
'@e2b/code-interpreter': patch
---

adds the ability to set the user for new contexts
7 changes: 7 additions & 0 deletions js/src/sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ export interface CreateCodeContextOpts {
* @default python
*/
language?: string,
/**
* User for the context.
*
* @default root
*/
user?: "root" | "user",
/**
* Timeout for the request in **milliseconds**.
*
Expand Down Expand Up @@ -269,6 +275,7 @@ export class Sandbox extends BaseSandbox {
body: JSON.stringify({
language: opts?.language,
cwd: opts?.cwd,
user: opts?.user,
}),
keepalive: true,
signal: this.connectionConfig.getSignal(opts?.requestTimeoutMs),
Expand Down
4 changes: 4 additions & 0 deletions python/e2b_code_interpreter/code_interpreter_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,13 +231,15 @@ async def create_code_context(
self,
cwd: Optional[str] = None,
language: Optional[str] = None,
user: Optional[Literal["root", "user"]] = None,
request_timeout: Optional[float] = None,
) -> Context:
"""
Creates a new context to run code in.

:param cwd: Set the current working directory for the context, defaults to `/home/user`
:param language: Language of the context. If not specified, defaults to Python
:param user: User of the context. If not specified, defaults to `root`
:param request_timeout: Timeout for the request in **milliseconds**

:return: Context object
Expand All @@ -249,6 +251,8 @@ async def create_code_context(
data["language"] = language
if cwd:
data["cwd"] = cwd
if user:
data["user"] = user

try:
response = await self._client.post(
Expand Down
23 changes: 14 additions & 9 deletions template/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
FROM python:3.10.14

ENV HOME=/home/user

RUN DEBIAN_FRONTEND=noninteractive apt-get update && apt-get install -y --no-install-recommends \
build-essential curl git util-linux jq sudo fonts-noto-cjk

# Install Node.js 20.x from NodeSource
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
apt-get install -y nodejs

RUN mkdir -p $HOME/.jupyter $HOME/.ipython $HOME/.server

ENV PIP_DEFAULT_TIMEOUT=100 \
PIP_DISABLE_PIP_VERSION_CHECK=1 \
PIP_NO_CACHE_DIR=1 \
JUPYTER_CONFIG_PATH="/root/.jupyter" \
IPYTHON_CONFIG_PATH="/root/.ipython" \
SERVER_PATH="/root/.server" \
JUPYTER_CONFIG_PATH="$HOME/.jupyter" \
IPYTHON_CONFIG_PATH="$HOME/.ipython" \
SERVER_PATH="$HOME/.server" \
R_VERSION=4.4.2

ENV R_HOME=/opt/R/${R_VERSION} \
JAVA_HOME=/opt/java/openjdk

# Install Jupyter
COPY ./requirements.txt requirements.txt
RUN pip install --no-cache-dir -r requirements.txt && ipython kernel install --name "python3" --user
RUN pip install --no-cache-dir -r requirements.txt && ipython kernel install --name "python3"

# R Kernel
RUN curl -O https://cdn.rstudio.com/r/debian-12/pkgs/r-${R_VERSION}_1_amd64.deb && sudo apt-get update && sudo apt-get install -y ./r-${R_VERSION}_1_amd64.deb && ln -s ${R_HOME}/bin/R /usr/bin/R
Expand All @@ -38,8 +42,11 @@ COPY .ts.swcrc $SERVER_PATH/.ts.swcrc
# Deno Kernel
COPY --from=denoland/deno:bin-2.0.4 /deno /usr/bin/deno
RUN chmod +x /usr/bin/deno
RUN deno jupyter --unstable --install
COPY ./deno.json /root/.local/share/jupyter/kernels/deno/kernel.json
RUN deno jupyter --unstable --install && \
mkdir -p /usr/local/share/jupyter/kernels/deno && \
mv $HOME/.local/share/jupyter/kernels/deno/* /usr/local/share/jupyter/kernels/deno/ && \
rmdir $HOME/.local/share/jupyter/kernels/deno
COPY ./deno.json /usr/local/share/jupyter/kernels/deno/kernel.json

# Bash Kernel
RUN pip install bash_kernel
Expand All @@ -49,13 +56,12 @@ RUN python -m bash_kernel.install
RUN python -m venv $SERVER_PATH/.venv

# Copy server and its requirements
RUN mkdir -p $SERVER_PATH/
COPY ./server/requirements.txt $SERVER_PATH
RUN $SERVER_PATH/.venv/bin/pip install --no-cache-dir -r $SERVER_PATH/requirements.txt
COPY ./server $SERVER_PATH

# Copy matplotlibrc
COPY matplotlibrc /root/.config/matplotlib/.matplotlibrc
COPY matplotlibrc $HOME/.config/matplotlib/matplotlibrc

# Copy Jupyter configuration
COPY ./start-up.sh $JUPYTER_CONFIG_PATH/
Expand All @@ -69,7 +75,6 @@ COPY ipython_kernel_config.py $IPYTHON_CONFIG_PATH/profile_default/
RUN mkdir -p $IPYTHON_CONFIG_PATH/profile_default/startup
COPY startup_scripts/* $IPYTHON_CONFIG_PATH/profile_default/startup


COPY --from=eclipse-temurin:11-jdk $JAVA_HOME $JAVA_HOME
RUN ln -s ${JAVA_HOME}/bin/java /usr/bin/java

Expand Down
5 changes: 2 additions & 3 deletions template/e2b.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,9 @@
# JS SDK
# import { Sandbox } from 'e2b'
# const sandbox = await Sandbox.create('code-interpreter-v1')

team_id = "460355b3-4f64-48f9-9a16-4442817f79f5"
memory_mb = 1_024
start_cmd = "/root/.jupyter/start-up.sh"
start_cmd = "sudo -u user /home/user/.jupyter/start-up.sh"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one thing: this is a breaking change, should I put a script in /root/.jupyter/start-up.sh that just calls the actual start-up.sh with sudo -u user?

dockerfile = "e2b.Dockerfile"
template_name = "code-interpreter-v1"
template_id = "nlhz8vlwyupq845jsdg9"
template_id = "nlhz8vlwyupq845jsdg9"
1 change: 1 addition & 0 deletions template/server/api/models/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ class Context(BaseModel):
id: StrictStr = Field(description="Context ID")
language: StrictStr = Field(description="Language of the context")
cwd: StrictStr = Field(description="Current working directory of the context")
user: StrictStr = Field(description="User of the context")

def __hash__(self):
return hash(self.id)
4 changes: 4 additions & 0 deletions template/server/api/models/create_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@


class CreateContext(BaseModel):
user: Optional[StrictStr] = Field(
default="root",
description="User to run the context",
)
cwd: Optional[StrictStr] = Field(
default="/home/user",
description="Current working directory",
Expand Down
23 changes: 13 additions & 10 deletions template/server/contexts.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,6 @@

logger = logging.Logger(__name__)

def get_kernel_for_language(language: str) -> str:
if language == "typescript":
return "javascript"

return language

def normalize_language(language: Optional[str]) -> str:
if not language:
return "python"
Expand All @@ -32,14 +26,23 @@ def normalize_language(language: Optional[str]) -> str:
return language


async def create_context(client, websockets: dict, language: str, cwd: str) -> Context:
def get_kernel_name(language: str, user: str) -> str:
if language == "typescript":
language = "javascript"

if user == "root":
return language+"_root"
return language


async def create_context(client, websockets: dict, language: str, cwd: str, user: str) -> Context:
data = {
"path": str(uuid.uuid4()),
"kernel": {"name": get_kernel_for_language(language)},
"kernel": {"name": get_kernel_name(language, user)}, # replace with root kernel when user is root
"type": "notebook",
"name": str(uuid.uuid4()),
}
logger.debug(f"Creating new {language} context")
logger.debug(f"Creating new {language} context for user {user}")

response = await client.post(f"{JUPYTER_BASE_URL}/api/sessions", json=data)

Expand Down Expand Up @@ -67,4 +70,4 @@ async def create_context(client, websockets: dict, language: str, cwd: str) -> C
status_code=500,
)

return Context(language=language, id=context_id, cwd=cwd)
return Context(language=language, id=context_id, cwd=cwd, user=user)
7 changes: 4 additions & 3 deletions template/server/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ async def lifespan(app: FastAPI):
global client
client = httpx.AsyncClient()

with open("/root/.jupyter/kernel_id") as file:
with open("/home/user/.jupyter/kernel_id") as file:
default_context_id = file.read().strip()

default_ws = ContextWebSocket(
Expand Down Expand Up @@ -91,7 +91,7 @@ async def post_execute(request: ExecutionRequest):
if not context_id:
try:
context = await create_context(
client, websockets, language, "/home/user"
client, websockets, language, "/home/user", "root"
)
except Exception as e:
return PlainTextResponse(str(e), status_code=500)
Expand Down Expand Up @@ -127,9 +127,10 @@ async def post_contexts(request: CreateContext) -> Context:

language = normalize_language(request.language)
cwd = request.cwd or "/home/user"
user = request.user or "root"

try:
return await create_context(client, websockets, language, cwd)
return await create_context(client, websockets, language, cwd, user)
except Exception as e:
return PlainTextResponse(str(e), status_code=500)

Expand Down
39 changes: 32 additions & 7 deletions template/start-up.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
#!/bin/bash

function create_root_kernels() {
# Get all installed kernels
kernels=$(jupyter kernelspec list --json | jq -r '.kernelspecs | keys[]')

for kernel in $kernels; do
# Get the kernel directory
kernel_dir=$(jupyter kernelspec list --json | jq -r ".kernelspecs[\"$kernel\"].resource_dir")

# Create directory for root kernel if it doesn't exist
root_kernel_dir="/usr/local/share/jupyter/kernels/${kernel}_root"
sudo mkdir -p "$root_kernel_dir"

# Copy all files from original kernel first
sudo cp -r "$kernel_dir"/* "$root_kernel_dir/" 2>/dev/null || true

# Create and write the modified kernel.json
cat "$kernel_dir/kernel.json" | jq '.argv = ["sudo"] + .argv | .display_name = .display_name + " (root)"' | sudo tee "$root_kernel_dir/kernel.json" > /dev/null

echo "Created root version of kernel: ${kernel}_root"
done
}

function start_jupyter_server() {
counter=0
response=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:8888/api/status")
Expand All @@ -13,22 +35,25 @@ function start_jupyter_server() {
response=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:8888/api/status")
done

response=$(curl -s -X POST "localhost:8888/api/sessions" -H "Content-Type: application/json" -d '{"path": "/home/user", "kernel": {"name": "python3"}, "type": "notebook", "name": "default"}')
response=$(curl -s -X POST "localhost:8888/api/sessions" -H "Content-Type: application/json" -d '{"path": "'$HOME'", "kernel": {"name": "python3_root"}, "type": "notebook", "name": "default"}')
status=$(echo "${response}" | jq -r '.kernel.execution_state')
if [[ ${status} != "starting" ]]; then
echo "Error creating kernel: ${response} ${status}"
exit 1
fi

sudo mkdir -p /root/.jupyter
mkdir -p $HOME/.jupyter
kernel_id=$(echo "${response}" | jq -r '.kernel.id')
sudo echo "${kernel_id}" | sudo tee /root/.jupyter/kernel_id >/dev/null
sudo echo "${response}" | sudo tee /root/.jupyter/.session_info >/dev/null
echo "${kernel_id}" > $HOME/.jupyter/kernel_id
echo "${response}" > $HOME/.jupyter/.session_info

cd /root/.server/
/root/.server/.venv/bin/uvicorn main:app --host 0.0.0.0 --port 49999 --workers 1 --no-access-log --no-use-colors --timeout-keep-alive 640
cd $HOME/.server/
$HOME/.server/.venv/bin/uvicorn main:app --host 0.0.0.0 --port 49999 --workers 1 --no-access-log --no-use-colors --timeout-keep-alive 640
}

echo "Creating root versions of kernels..."
create_root_kernels

echo "Starting Code Interpreter server..."
start_jupyter_server &
MATPLOTLIBRC=/root/.config/matplotlib/.matplotlibrc jupyter server --IdentityProvider.token="" >/dev/null 2>&1
MATPLOTLIBRC=$HOME/.config/matplotlib/.matplotlibrc jupyter server --IdentityProvider.token=""
33 changes: 25 additions & 8 deletions template/test.Dockerfile
Original file line number Diff line number Diff line change
@@ -1,26 +1,34 @@
FROM python:3.10.14

ENV HOME=/home/user

ENV JAVA_HOME=/opt/java/openjdk
COPY --from=eclipse-temurin:11-jdk $JAVA_HOME $JAVA_HOME
ENV PATH="${JAVA_HOME}/bin:${PATH}"

RUN DEBIAN_FRONTEND=noninteractive apt-get update && apt-get install -y --no-install-recommends \
build-essential curl git util-linux jq sudo fonts-noto-cjk

# Create new user with root privileges while keeping root user
RUN useradd -m -s /bin/bash user && \
echo 'user ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers && \
echo 'user:password' | chpasswd && \
usermod -aG sudo user

# Install Node.js 20.x from NodeSource
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
apt-get install -y nodejs

ENV PIP_DEFAULT_TIMEOUT=100 \
PIP_DISABLE_PIP_VERSION_CHECK=1 \
PIP_NO_CACHE_DIR=1 \
JUPYTER_CONFIG_PATH="/root/.jupyter" \
IPYTHON_CONFIG_PATH="/root/.ipython" \
SERVER_PATH="/root/.server"
JUPYTER_CONFIG_PATH="$HOME/.jupyter" \
IPYTHON_CONFIG_PATH="$HOME/.ipython" \
SERVER_PATH="$HOME/.server"

# Install Jupyter
COPY ./template/requirements.txt requirements.txt
RUN pip install --no-cache-dir -r requirements.txt && ipython kernel install --name "python3" --user
RUN pip install --no-cache-dir -r requirements.txt && ipython kernel install --name "python3"

# Javascript Kernel
RUN npm install -g --unsafe-perm ijavascript
Expand All @@ -33,8 +41,12 @@ COPY ./template/.ts.swcrc $SERVER_PATH/.ts.swcrc
# Deno Kernel
COPY --from=denoland/deno:bin-2.0.4 /deno /usr/bin/deno
RUN chmod +x /usr/bin/deno
RUN deno jupyter --unstable --install
COPY ./template/deno.json /root/.local/share/jupyter/kernels/deno/kernel.json
RUN deno jupyter --unstable --install && \
mkdir -p /usr/local/share/jupyter/kernels/deno && \
mv $HOME/.local/share/jupyter/kernels/deno/* /usr/local/share/jupyter/kernels/deno/ && \
rmdir $HOME/.local/share/jupyter/kernels/deno

COPY ./template/deno.json /usr/local/share/jupyter/kernels/deno/kernel.json

# Create separate virtual environment for server
RUN python -m venv $SERVER_PATH/.venv
Expand All @@ -46,7 +58,7 @@ RUN $SERVER_PATH/.venv/bin/pip install --no-cache-dir -r $SERVER_PATH/requiremen
COPY ./template/server $SERVER_PATH

# Copy matplotlibrc
COPY ./template/matplotlibrc /root/.config/matplotlib/matplotlibrc
COPY ./template/matplotlibrc $HOME/.config/matplotlib/matplotlibrc

# Copy Jupyter configuration
COPY ./template/start-up.sh $JUPYTER_CONFIG_PATH/
Expand All @@ -61,7 +73,12 @@ RUN mkdir -p $IPYTHON_CONFIG_PATH/profile_default/startup
COPY ./template/startup_scripts/* $IPYTHON_CONFIG_PATH/profile_default/startup

# Setup entrypoint for local development
WORKDIR /home/user
WORKDIR $HOME
COPY ./chart_data_extractor ./chart_data_extractor
RUN pip install -e ./chart_data_extractor

# Change ownership of all files to user
RUN chown -R user:user $HOME

USER user
Comment on lines +79 to +83
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are you doing it as last thing? If you would set the user as a first thing you probably don't need change the ownership

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the problem here is: "user" does not exist in python image
and we cannot add user before we have "sudo" dependency installed

so this is why it's the last

ENTRYPOINT $JUPYTER_CONFIG_PATH/start-up.sh
Loading