Skip to content

Commit 6199542

Browse files
authored
Merge pull request #3 from permitio/access-control
Access control
2 parents 4abd9fd + acd87d6 commit 6199542

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+11549
-275
lines changed

.github/workflows/pypi-release.yml

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
name: Release to PyPI
2+
3+
on:
4+
push:
5+
tags:
6+
- 'v*.*.*'
7+
8+
jobs:
9+
build-and-publish:
10+
runs-on: ubuntu-latest
11+
12+
steps:
13+
- name: Checkout code
14+
uses: actions/checkout@v4
15+
16+
- name: Set up Python
17+
uses: actions/setup-python@v5
18+
with:
19+
python-version: '3.12'
20+
21+
- name: Install build tools
22+
run: |
23+
python -m pip install --upgrade pip
24+
pip install build twine
25+
26+
- name: Build package
27+
run: |
28+
python -m build
29+
30+
- name: Publish to PyPI
31+
env:
32+
TWINE_USERNAME: __token__
33+
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
34+
run: |
35+
twine upload dist/*

.github/workflows/test-lint.yml

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: Test and Lint
2+
3+
on:
4+
pull_request:
5+
branches:
6+
- main
7+
8+
jobs:
9+
test-lint:
10+
runs-on: ubuntu-latest
11+
12+
steps:
13+
- name: Checkout code
14+
uses: actions/checkout@v4
15+
16+
- name: Set up Python
17+
uses: actions/setup-python@v5
18+
with:
19+
python-version: '3.12'
20+
21+
- name: Install dependencies
22+
run: |
23+
python -m pip install --upgrade pip
24+
pip install -r requirements.txt
25+
pip install pytest flake8
26+
27+
- name: Run linting
28+
run: |
29+
flake8 . --max-line-length=150
30+
31+
- name: Run tests
32+
run: |
33+
pytest

.pre-commit-config.yaml

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
repos:
2+
- repo: https://github.com/psf/black
3+
rev: 25.1.0
4+
hooks:
5+
- id: black
6+
language_version: python3
7+
- repo: https://github.com/pycqa/flake8
8+
rev: 7.1.2
9+
hooks:
10+
- id: flake8
11+
args: [--max-line-length=150]
12+
- repo: local
13+
hooks:
14+
- id: pytest
15+
name: Run Pytest
16+
entry: pytest
17+
language: system
18+
types: [python]
19+
pass_filenames: false
20+

components/__init__.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
from .data_protection import DataProtectionComponent
44
from .jwt_validator import JWTValidatorComponent
55
from .permissions_check import PermissionsCheckComponent
6-
from .message_formatter import MessageFormatterComponent
76

8-
__all__ = ["DataProtectionComponent", "JWTValidatorComponent", "PermissionsCheckComponent","MessageFormatterComponent"]
7+
__all__ = [
8+
"DataProtectionComponent",
9+
"JWTValidatorComponent",
10+
"PermissionsCheckComponent",
11+
]

components/data_protection.py

+3-7
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from langflow.template import Output
55
from permit import Permit
66

7+
78
class DataProtectionComponent(Component):
89
display_name = "Data Protection"
910
description = "Gets allowed resource IDs for a user."
@@ -51,13 +52,8 @@ async def validate_auth(self) -> Message:
5152
permit = Permit(token=self.api_key, pdp=self.pdp_url)
5253
permissions = await permit.get_user_permissions(self.user_id)
5354
allowed_ids = [
54-
p.resource_id
55-
for p in permissions
55+
p.resource_id
56+
for p in permissions
5657
if p.resource == self.resource_type and p.action == self.action
5758
]
5859
return Message(content=allowed_ids)
59-
60-
61-
62-
63-

components/jwt_validator.py

+34-7
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1+
import base64
2+
3+
import jwt
4+
import requests
5+
from jwt import PyJWK
6+
17
from langflow.custom import Component
28
from langflow.inputs import MessageTextInput
3-
from langflow.template import Output
49
from langflow.schema.message import Message
5-
import jwt
6-
import requests
10+
from langflow.template import Output
11+
712

813
class JWTValidatorComponent(Component):
914
display_name = "JWT Validator"
@@ -30,9 +35,31 @@ class JWTValidatorComponent(Component):
3035
]
3136

3237
def validate_auth(self) -> Message:
33-
response = requests.get(self.jwks_url)
38+
response = requests.get(self.jwks_url, timeout=10)
3439
jwks = response.json()
3540
headers = jwt.get_unverified_header(self.jwt_token)
36-
key = next(k for k in jwks["keys"] if k["kid"] == headers["kid"])
37-
payload = jwt.decode(self.jwt_token, key, algorithms=["RS256"])
38-
return Message(content=payload["sub"])
41+
42+
try:
43+
jwk = next(k for k in jwks["keys"] if k["kid"] == headers["kid"])
44+
if isinstance(jwk.get("e"), int):
45+
jwk["e"] = self._int_to_base64url(jwk["e"])
46+
if isinstance(jwk.get("n"), int):
47+
jwk["n"] = self._int_to_base64url(jwk["n"])
48+
49+
public_key = PyJWK(jwk).key
50+
payload = jwt.decode(self.jwt_token, public_key, algorithms=["RS256"])
51+
return Message(content=payload["sub"])
52+
except KeyError as e:
53+
error_message = f"Missing key in JWT or JWKS: {e!s}"
54+
raise KeyError(error_message) from e
55+
except jwt.ExpiredSignatureError:
56+
raise
57+
except jwt.PyJWTError as e:
58+
error_message = f"JWT validation failed: {e!s}"
59+
raise jwt.InvalidTokenError(error_message) from e
60+
61+
def _int_to_base64url(self, value: int) -> str:
62+
"""Convert an integer to a Base64URL-encoded string."""
63+
byte_length = (value.bit_length() + 7) // 8
64+
value_bytes = value.to_bytes(byte_length, byteorder="big")
65+
return base64.urlsafe_b64encode(value_bytes).rstrip(b"=").decode("ascii")

components/permissions_check.py

+24-10
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from langflow.schema.message import Message
55
from permit import Permit
66

7+
78
class PermissionsCheckComponent(Component):
89
display_name = "Permissions Check"
910
description = "Checks if a user is allowed an action on a resource, with separate outputs for allowed and denied."
@@ -54,19 +55,32 @@ class PermissionsCheckComponent(Component):
5455
Output(display_name="Denied", name="denied", method="denied_result"),
5556
]
5657

58+
def __init__(self, **kwargs):
59+
super().__init__(**kwargs)
60+
self._permission_result = False
61+
5762
async def validate_auth(self) -> bool:
5863
permit = Permit(token=self.api_key, pdp=self.pdp_url)
59-
context = {"tenant": self.tenant} if self.tenant else {}
60-
is_allowed = await permit.check(self.user_id, self.action, self.resource, context)
61-
self.status = is_allowed # Store the result for use in output methods
62-
return is_allowed
64+
context = (
65+
{"tenant": self.tenant} if hasattr(self, "tenant") and self.tenant else {}
66+
)
67+
68+
# Get the result from permit service
69+
self._permission_result = await permit.check(
70+
self.user_id, self.action, self.resource, context=context
71+
)
72+
return self._permission_result
6373

6474
def allowed_result(self) -> Message:
65-
if self.status: # True case
66-
return Message(content=f"Permission granted for {self.user_id} to {self.action} on {self.resource}")
67-
return Message(content="") # Empty message instead of None
75+
if self._permission_result:
76+
return Message(
77+
content=f"Permission granted for {self.user_id} to {self.action} on {self.resource}"
78+
)
79+
return Message(content="")
6880

6981
def denied_result(self) -> Message:
70-
if not self.status: # False case
71-
return Message(content=f"Permission denied for {self.user_id} to {self.action} on {self.resource}")
72-
return Message(content="") # Empty message instead of None
82+
if not self._permission_result:
83+
return Message(
84+
content=f"Permission denied for {self.user_id} to {self.action} on {self.resource}"
85+
)
86+
return Message(content="")

requirements.txt

+5-1
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,8 @@ python-jwt==4.1.0
33
cryptography==42.0.5
44
requests==2.32.3
55
pytest==8.3.4
6-
langflow==1.1.4.post1
6+
langflow==1.2.0
7+
blockbuster>=1.5.8,<1.6
8+
asgi-lifespan>=2.1.0
9+
10+

setup.py

+24-24
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,31 @@
11
from setuptools import setup, find_namespace_packages
22

33
setup(
4-
name='permit-components',
5-
version='0.1.0',
6-
packages=find_namespace_packages(include=['base.*', 'components.*']),
4+
name="permit-components",
5+
version="0.1.0",
6+
packages=find_namespace_packages(include=["base.*", "components.*"]),
77
install_requires=[
8-
'requests>=2.32.3',
9-
'python-jwt==4.1.0',
10-
'permit>=2.7.2,<3.0.0',
11-
'langflow==1.1.4.post1',
12-
'cryptography>=42.0.5',
8+
"requests>=2.32.3",
9+
"python-jwt==4.1.0",
10+
"permit>=2.7.2,<3.0.0",
11+
"langflow==1.1.4.post1",
12+
"cryptography>=42.0.5",
1313
],
14-
author='Ekekenta Clinton',
15-
author_email='[email protected]',
16-
description='Permit.io authentication components for Langflow',
17-
long_description=open('README.md', encoding='utf-8').read(),
18-
long_description_content_type='text/markdown',
19-
url='https://github.com/permitio/permit-components',
20-
license='MIT',
14+
author="Ekekenta Clinton",
15+
author_email="[email protected]",
16+
description="Permit.io authentication components for Langflow",
17+
long_description=open("README.md", encoding="utf-8").read(),
18+
long_description_content_type="text/markdown",
19+
url="https://github.com/permitio/permit-components",
20+
license="MIT",
2121
classifiers=[
22-
'Development Status :: 3 - Alpha',
23-
'Intended Audience :: Developers',
24-
'License :: OSI Approved :: MIT License',
25-
'Programming Language :: Python :: 3',
26-
'Programming Language :: Python :: 3.8',
27-
'Programming Language :: Python :: 3.9',
28-
'Programming Language :: Python :: 3.10',
22+
"Development Status :: 3 - Alpha",
23+
"Intended Audience :: Developers",
24+
"License :: OSI Approved :: MIT License",
25+
"Programming Language :: Python :: 3",
26+
"Programming Language :: Python :: 3.8",
27+
"Programming Language :: Python :: 3.9",
28+
"Programming Language :: Python :: 3.10",
2929
],
30-
python_requires='>=3.8',
31-
)
30+
python_requires=">=3.8",
31+
)

tests/api_keys.py

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import os.path
2+
3+
# we need to import tmpdir
4+
5+
6+
def get_required_env_var(var: str) -> str:
7+
"""Get the value of the specified environment variable.
8+
9+
Args:
10+
var (str): The environment variable to get.
11+
12+
Returns:
13+
str: The value of the environment variable.
14+
15+
Raises:
16+
ValueError: If the environment variable is not set.
17+
"""
18+
value = os.getenv(var)
19+
if not value:
20+
msg = f"Environment variable {var} is not set"
21+
raise ValueError(msg)
22+
return value
23+
24+
25+
def get_openai_api_key() -> str:
26+
return get_required_env_var("OPENAI_API_KEY")
27+
28+
29+
def get_astradb_application_token() -> str:
30+
return get_required_env_var("ASTRA_DB_APPLICATION_TOKEN")
31+
32+
33+
def get_astradb_api_endpoint() -> str:
34+
return get_required_env_var("ASTRA_DB_API_ENDPOINT")

0 commit comments

Comments
 (0)