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
57 changes: 34 additions & 23 deletions .github/actions/validator_pypi_publish/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,19 @@ name: Publish to Guardrails Hub
description: Re-Usable action to publish a Validator to Guardrails PyPi
inputs:
validator_id:
description: 'Validator ID ex. guardrails/detect_pii'
description: "Validator ID ex. guardrails/detect_pii"
required: true
guardrails_token:
description: 'Guardrails Token'
description: "Guardrails Token"
required: true
pypi_repository_url:
description: 'PyPi Repository URL'
description: "PyPi Repository URL"
required: false
default: 'https://pypi.guardrailsai.com'
default: "https://pypi.guardrailsai.com"
package_directory:
description: 'Package Directory "validator" or "some_parent_folder/package"'
required: false
default: 'validator'
default: "validator"

runs:
using: "composite"
Expand All @@ -36,7 +36,7 @@ runs:
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
python-version: "3.10"

- name: Install Twine & Build
shell: bash
Expand All @@ -46,36 +46,47 @@ runs:

- name: Create .pypirc
shell: bash
env:
PYPI_REPOSITORY_URL: ${{ inputs.pypi_repository_url }}
GUARDRAILS_TOKEN: ${{ inputs.guardrails_token }}
run: |
touch ~/.pypirc
echo "[distutils]" >> ~/.pypirc
echo "index-servers =" >> ~/.pypirc
echo " private-repository" >> ~/.pypirc
echo "" >> ~/.pypirc
echo "[private-repository]" >> ~/.pypirc
echo "repository = ${{ inputs.pypi_repository_url }}" >> ~/.pypirc
echo "username = __token__" >> ~/.pypirc
echo "password = ${{ inputs.guardrails_token }}" >> ~/.pypirc
{
echo "[distutils]"
echo "index-servers ="
echo " private-repository"
echo ""
echo "[private-repository]"
echo "repository = $PYPI_REPOSITORY_URL"
echo "username = __token__"
echo "password = $GUARDRAILS_TOKEN"
} > ~/.pypirc

- name: Move CI Scripts to Validator
shell: bash
env:
PACKAGE_DIRECTORY: ${{ inputs.package_directory }}
run: |
mv shared-ci-scripts/.github/actions/validator_pypi_publish/*.py ./${{ inputs.package_directory }}
mv shared-ci-scripts/.github/actions/validator_pypi_publish/*.py ./$PACKAGE_DIRECTORY

- name: Rename Package
- name: Rename Package
shell: bash
env:
PACKAGE_DIRECTORY: ${{ inputs.package_directory }}
VALIDATOR_ID: ${{ inputs.validator_id }}
run: |
cd ${{ inputs.package_directory }}
CONCATANATED_NAME=$(python concat_name.py ${{ inputs.validator_id }})
cd $PACKAGE_DIRECTORY
CONCATANATED_NAME=$(python concat_name.py $VALIDATOR_ID)
NEW_PEP_PACKAGE_NAME=$(python package_name_normalization.py $CONCATANATED_NAME)
VALIDATOR_FOLDER_NAME=$(echo $NEW_PEP_PACKAGE_NAME | tr - _)
mv ./${{ inputs.package_directory }} ./$VALIDATOR_FOLDER_NAME
mv ./$PACKAGE_DIRECTORY ./$VALIDATOR_FOLDER_NAME
python add_build_prefix.py ./pyproject.toml $NEW_PEP_PACKAGE_NAME $VALIDATOR_FOLDER_NAME

- name: Build & Upload
shell: bash
env:
PACKAGE_DIRECTORY: ${{ inputs.package_directory }}
GUARDRAILS_TOKEN: ${{ inputs.guardrails_token }}
run: |
cd ${{ inputs.package_directory }}
cd $PACKAGE_DIRECTORY
python -m build
twine upload dist/* -u __token__ -p ${{ inputs.guardrails_token }} -r private-repository

twine upload dist/* -u __token__ -p $GUARDRAILS_TOKEN -r private-repository
15 changes: 3 additions & 12 deletions guardrails/cli/server/hub_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@

import requests
from guardrails_hub_types import Manifest
import jwt
from jwt import ExpiredSignatureError, DecodeError


from guardrails.settings import settings
from guardrails.classes.rc import RC
Expand Down Expand Up @@ -51,8 +48,6 @@ class HttpError(Exception):

def fetch(url: str, token: Optional[str], anonymousUserId: Optional[str]):
try:
# For Debugging
# headers = { "Authorization": f"Bearer {token}", "x-anonymous-user-id": anonymousUserId, "Cache-Control": "no-cache" } # noqa
headers = {
"Authorization": f"Bearer {token}",
"x-anonymous-user-id": anonymousUserId,
Expand Down Expand Up @@ -93,12 +88,8 @@ def get_jwt_token(rc: RC) -> Optional[str]:

# check for jwt expiration
if token:
try:
jwt.decode(token, options={"verify_signature": False, "verify_exp": True})
except ExpiredSignatureError:
raise ExpiredTokenError(TOKEN_EXPIRED_MESSAGE)
except DecodeError:
raise InvalidTokenError(TOKEN_INVALID_MESSAGE)
from guardrails.hub_token.utils import client_check_token_expiry
client_check_token_expiry(token)
return token


Expand Down Expand Up @@ -204,4 +195,4 @@ def post_validator_submit(package_name: str, content: str):
raise http_e
except Exception as e:
logger.error("An unexpected error occurred!", e)
sys.exit(1)
sys.exit(1)
12 changes: 3 additions & 9 deletions guardrails/hub_token/token.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import os
import jwt
from jwt import ExpiredSignatureError, DecodeError
from typing import Optional

from guardrails.classes.rc import RC
Expand Down Expand Up @@ -42,10 +40,6 @@ def get_jwt_token(rc: RC) -> Optional[str]:

# check for jwt expiration
if token:
try:
jwt.decode(token, options={"verify_signature": False, "verify_exp": True})
except ExpiredSignatureError:
raise ExpiredTokenError(TOKEN_EXPIRED_MESSAGE)
except DecodeError:
raise InvalidTokenError(TOKEN_INVALID_MESSAGE)
return token
from guardrails.hub_token.utils import client_check_token_expiry
client_check_token_expiry(token)
return token
30 changes: 30 additions & 0 deletions guardrails/hub_token/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import base64
import json
import time

from guardrails.hub_token.token import (
TOKEN_EXPIRED_MESSAGE,
TOKEN_INVALID_MESSAGE,
ExpiredTokenError,
InvalidTokenError,
)


def client_check_token_expiry(token: str) -> None:
"""Client-side check that token is not expired.

Does NOT validate the signature — the Guardrails Hub server validates
the signature on every request. This function exists only to fail
fast on locally-known expired tokens.
"""
try:
_header, payload_b64, _signature = token.split(".")
payload = json.loads(
base64.urlsafe_b64decode(payload_b64 + "===")
)
except (ValueError, json.JSONDecodeError) as exc:
raise InvalidTokenError(TOKEN_INVALID_MESSAGE) from exc

exp = payload.get("exp")
if exp is not None and exp < time.time():
raise ExpiredTokenError(TOKEN_EXPIRED_MESSAGE)
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ dependencies = [
"typing-extensions>=4.8.0,<5.0.0",
"python-dateutil>=2.8.2,<3.0.0",
"tiktoken>=0.5.1,<1.0.0",
"litellm>=1.37.14,<1.82.6",
"litellm>=1.37.14",
"pydash>=7.0.6,<9.0.0",
"langchain-core>=1.0.0,<2.0",
"requests>=2.31.0,<3.0.0",
Expand Down
49 changes: 49 additions & 0 deletions tests/unit_tests/hub/test_jwt_expiry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import time
import base64
import json
import pytest

from guardrails.hub_token.utils import client_check_token_expiry
from guardrails.hub_token.token import ExpiredTokenError, InvalidTokenError


def _make_token(exp_offset: int) -> str:
"""Helper to create a fake JWT-shaped token."""
payload = {"exp": int(time.time()) + exp_offset}
payload_b64 = (
base64.urlsafe_b64encode(json.dumps(payload).encode())
.rstrip(b"=")
.decode()
)
return f"header.{payload_b64}.signature"


def test_valid_token_passes():
"""A token expiring in the future should pass without raising."""
token = _make_token(exp_offset=3600) # 1 hour from now
client_check_token_expiry(token) # should not raise


def test_expired_token_raises():
"""A token expired in the past should raise ExpiredTokenError."""
token = _make_token(exp_offset=-3600) # 1 hour ago
with pytest.raises(ExpiredTokenError):
client_check_token_expiry(token)


def test_malformed_token_raises():
"""A token that is not valid JWT shape should raise InvalidTokenError."""
with pytest.raises(InvalidTokenError):
client_check_token_expiry("not.a.valid.jwt.token.at.all")


def test_token_without_exp_passes():
"""A token with no exp claim should pass (no expiry enforced)."""
payload = {"sub": "user123"} # no exp field
payload_b64 = (
base64.urlsafe_b64encode(json.dumps(payload).encode())
.rstrip(b"=")
.decode()
)
token = f"header.{payload_b64}.signature"
client_check_token_expiry(token) # should not raise