pylantir 0.1.3__py3-none-any.whl → 0.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,188 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """
4
+ Author: Milton Camacho
5
+ Date: 2025-11-18
6
+ Database setup for authentication system.
7
+ Manages separate users database connection and session handling.
8
+ """
9
+
10
+ import os
11
+ import logging
12
+ from typing import Optional
13
+ from sqlalchemy import create_engine
14
+ from sqlalchemy.orm import sessionmaker, Session
15
+ from sqlalchemy.exc import SQLAlchemyError
16
+ from pathlib import Path
17
+
18
+ lgr = logging.getLogger(__name__)
19
+
20
+
21
+ class AuthDatabaseError(Exception):
22
+ """Raised when authentication database operations fail."""
23
+ pass
24
+
25
+
26
+ def get_auth_database_url(users_db_path: Optional[str] = None) -> str:
27
+ """
28
+ Get authentication database URL from configuration, environment, or default location.
29
+
30
+ Args:
31
+ users_db_path: Optional path to users database from configuration
32
+
33
+ Returns:
34
+ str: SQLite database URL for authentication
35
+ """
36
+ if users_db_path:
37
+ # Use explicitly provided users database path from configuration
38
+ auth_db_path = os.path.expanduser(users_db_path)
39
+ lgr.info(f"Using configured users database path: {auth_db_path}")
40
+ else:
41
+ # Fallback to environment variable or default location
42
+ auth_db_path_env = os.getenv("USERS_DB_PATH")
43
+
44
+ if auth_db_path_env:
45
+ auth_db_path = os.path.expanduser(auth_db_path_env)
46
+ lgr.info(f"Using environment users database path: {auth_db_path}")
47
+ else:
48
+ # Default: users.db in same directory as main database
49
+ main_db_path = os.getenv("DB_PATH", "~/Desktop/worklist.db")
50
+ main_db_path = os.path.expanduser(main_db_path)
51
+
52
+ db_dir = Path(main_db_path).parent
53
+ auth_db_path = db_dir / "users.db"
54
+ lgr.info(f"Using default users database path: {auth_db_path}")
55
+
56
+ return f"sqlite:///{auth_db_path}"
57
+
58
+
59
+ # Create authentication database engine
60
+ auth_engine = None
61
+ AuthSessionLocal = None
62
+
63
+
64
+ def init_auth_database(users_db_path: Optional[str] = None) -> None:
65
+ """
66
+ Initialize authentication database engine and session factory.
67
+
68
+ Args:
69
+ users_db_path: Optional path to users database from configuration
70
+ """
71
+ global auth_engine, AuthSessionLocal
72
+
73
+ try:
74
+ database_url = get_auth_database_url(users_db_path)
75
+ lgr.info(f"Initializing authentication database: {database_url}")
76
+
77
+ auth_engine = create_engine(
78
+ database_url,
79
+ connect_args={"check_same_thread": False}, # SQLite specific
80
+ echo=os.getenv("DB_ECHO", "False").lower() == "true"
81
+ )
82
+
83
+ AuthSessionLocal = sessionmaker(
84
+ autocommit=False,
85
+ autoflush=False,
86
+ bind=auth_engine
87
+ )
88
+
89
+ # Create all tables
90
+ from .auth_models import AuthBase
91
+ AuthBase.metadata.create_all(bind=auth_engine)
92
+
93
+ lgr.info("Authentication database initialized successfully")
94
+
95
+ except Exception as e:
96
+ lgr.error(f"Failed to initialize authentication database: {e}")
97
+ raise AuthDatabaseError(f"Database initialization failed: {e}")
98
+
99
+
100
+ def get_auth_db() -> Session:
101
+ """
102
+ Get authentication database session.
103
+
104
+ Returns:
105
+ Session: SQLAlchemy session for authentication database
106
+
107
+ Raises:
108
+ AuthDatabaseError: If database is not initialized
109
+ """
110
+ if AuthSessionLocal is None:
111
+ init_auth_database()
112
+
113
+ if AuthSessionLocal is None:
114
+ raise AuthDatabaseError("Authentication database not initialized")
115
+
116
+ db = AuthSessionLocal()
117
+ try:
118
+ yield db
119
+ except SQLAlchemyError as e:
120
+ lgr.error(f"Database session error: {e}")
121
+ db.rollback()
122
+ raise AuthDatabaseError(f"Database operation failed: {e}")
123
+ finally:
124
+ db.close()
125
+
126
+
127
+ def create_initial_admin_user(users_db_path: Optional[str] = None) -> None:
128
+ """
129
+ Create initial admin user if no users exist in database.
130
+
131
+ Args:
132
+ users_db_path: Optional path to users database from configuration
133
+ """
134
+ try:
135
+ # Initialize database with correct path if not already done
136
+ if AuthSessionLocal is None:
137
+ init_auth_database(users_db_path)
138
+
139
+ db = next(get_auth_db())
140
+
141
+ from .auth_utils import create_admin_user
142
+
143
+ admin_user = create_admin_user(
144
+ db=db,
145
+ username="admin",
146
+ password="admin123", # Should be changed immediately
147
+ email="admin@localhost",
148
+ full_name="System Administrator"
149
+ )
150
+
151
+ if admin_user:
152
+ lgr.warning("Created default admin user with password 'admin123'. Please change this immediately!")
153
+ lgr.info("Use 'pylantir admin-password' command to change the admin password")
154
+
155
+ except Exception as e:
156
+ lgr.error(f"Failed to create initial admin user: {e}")
157
+
158
+
159
+ def backup_auth_database(backup_path: Optional[str] = None) -> bool:
160
+ """
161
+ Create backup of authentication database.
162
+
163
+ Args:
164
+ backup_path: Optional custom backup path
165
+
166
+ Returns:
167
+ bool: True if backup successful
168
+ """
169
+ try:
170
+ database_url = get_auth_database_url()
171
+ source_path = database_url.replace("sqlite:///", "")
172
+
173
+ if backup_path is None:
174
+ # Create backup in same directory with timestamp
175
+ from datetime import datetime
176
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
177
+ backup_path = f"{source_path}.backup_{timestamp}"
178
+
179
+ # Copy database file
180
+ import shutil
181
+ shutil.copy2(source_path, backup_path)
182
+
183
+ lgr.info(f"Authentication database backed up to: {backup_path}")
184
+ return True
185
+
186
+ except Exception as e:
187
+ lgr.error(f"Failed to backup authentication database: {e}")
188
+ return False
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """
4
+ Author: Milton Camacho
5
+ Date: 2025-11-18
6
+ This script provides the SQLAlchemy models for user authentication and authorization.
7
+
8
+ User roles:
9
+ - admin: Full access to users and worklist data (CRUD operations)
10
+ - write: Read and write access to worklist data only
11
+ - read: Read-only access to worklist data only
12
+ """
13
+
14
+ from sqlalchemy.orm import declarative_base
15
+ from sqlalchemy import Column, Integer, String, DateTime, Boolean, Enum
16
+ from datetime import datetime
17
+ import logging
18
+ import enum
19
+
20
+ AuthBase = declarative_base()
21
+
22
+ lgr = logging.getLogger(__name__)
23
+
24
+
25
+ class UserRole(enum.Enum):
26
+ """User role enumeration for access control."""
27
+ ADMIN = "admin"
28
+ WRITE = "write"
29
+ READ = "read"
30
+
31
+
32
+ class User(AuthBase):
33
+ """User model for API authentication and authorization."""
34
+
35
+ __tablename__ = 'users'
36
+
37
+ id = Column(Integer, primary_key=True, autoincrement=True)
38
+ username = Column(String(50), unique=True, nullable=False, index=True)
39
+ email = Column(String(100), unique=True, nullable=True)
40
+ full_name = Column(String(100), nullable=True)
41
+ hashed_password = Column(String(255), nullable=False)
42
+ role = Column(Enum(UserRole), nullable=False, default=UserRole.READ)
43
+ is_active = Column(Boolean, default=True)
44
+ created_at = Column(DateTime, default=datetime.utcnow)
45
+ last_login = Column(DateTime, nullable=True)
46
+ created_by = Column(Integer, nullable=True) # ID of user who created this account
47
+
48
+ def __repr__(self):
49
+ return f"<User(id={self.id}, username={self.username}, role={self.role.value}, active={self.is_active})>"
50
+
51
+ def has_permission(self, action: str, resource: str = "worklist") -> bool:
52
+ """
53
+ Check if user has permission for specific action on resource.
54
+
55
+ Args:
56
+ action: Action type ('read', 'write', 'delete', 'create')
57
+ resource: Resource type ('worklist', 'users')
58
+
59
+ Returns:
60
+ bool: True if user has permission
61
+ """
62
+ if not self.is_active:
63
+ return False
64
+
65
+ # Admin has all permissions
66
+ if self.role == UserRole.ADMIN:
67
+ return True
68
+
69
+ # Non-admin users cannot manage other users
70
+ if resource == "users":
71
+ return False
72
+
73
+ # Worklist permissions based on role
74
+ if resource == "worklist":
75
+ if action == "read":
76
+ return self.role in [UserRole.READ, UserRole.WRITE, UserRole.ADMIN]
77
+ elif action in ["write", "create", "update", "delete"]:
78
+ return self.role in [UserRole.WRITE, UserRole.ADMIN]
79
+
80
+ return False
pylantir/auth_utils.py ADDED
@@ -0,0 +1,210 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """
4
+ Author: Milton Camacho
5
+ Date: 2025-11-18
6
+ Authentication utilities for password hashing, JWT token generation,
7
+ and user authentication functions.
8
+ """
9
+
10
+ import os
11
+ import logging
12
+ from datetime import datetime, timedelta
13
+ from typing import Optional, Dict, Any
14
+ from passlib.context import CryptContext
15
+ from jose import JWTError, jwt
16
+ from sqlalchemy.orm import Session
17
+
18
+ lgr = logging.getLogger(__name__)
19
+
20
+ # Password hashing context
21
+ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
22
+
23
+ # JWT configuration
24
+ SECRET_KEY = os.getenv("JWT_SECRET_KEY", "your-secret-key-change-in-production")
25
+ ALGORITHM = "HS256"
26
+ ACCESS_TOKEN_EXPIRE_MINUTES = 30
27
+
28
+
29
+ class AuthenticationError(Exception):
30
+ """Raised when authentication fails."""
31
+ pass
32
+
33
+
34
+ class AuthorizationError(Exception):
35
+ """Raised when user lacks required permissions."""
36
+ pass
37
+
38
+
39
+ def verify_password(plain_password: str, hashed_password: str) -> bool:
40
+ """
41
+ Verify a plain password against a hashed password.
42
+
43
+ Args:
44
+ plain_password: Plain text password
45
+ hashed_password: Hashed password from database
46
+
47
+ Returns:
48
+ bool: True if password matches
49
+ """
50
+ try:
51
+ return pwd_context.verify(plain_password, hashed_password)
52
+ except Exception as e:
53
+ lgr.error(f"Password verification error: {e}")
54
+ return False
55
+
56
+
57
+ def get_password_hash(password: str) -> str:
58
+ """
59
+ Hash a plain password using bcrypt.
60
+
61
+ Args:
62
+ password: Plain text password
63
+
64
+ Returns:
65
+ str: Hashed password
66
+ """
67
+ try:
68
+ return pwd_context.hash(password)
69
+ except Exception as e:
70
+ lgr.error(f"Password hashing error: {e}")
71
+ raise AuthenticationError("Failed to hash password")
72
+
73
+
74
+ def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str:
75
+ """
76
+ Create a JWT access token.
77
+
78
+ Args:
79
+ data: Data to encode in token (typically user info)
80
+ expires_delta: Token expiration time
81
+
82
+ Returns:
83
+ str: JWT token
84
+ """
85
+ to_encode = data.copy()
86
+
87
+ if expires_delta:
88
+ expire = datetime.utcnow() + expires_delta
89
+ else:
90
+ expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
91
+
92
+ to_encode.update({"exp": expire})
93
+
94
+ try:
95
+ encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
96
+ return encoded_jwt
97
+ except Exception as e:
98
+ lgr.error(f"Token creation error: {e}")
99
+ raise AuthenticationError("Failed to create access token")
100
+
101
+
102
+ def verify_token(token: str) -> Optional[Dict[str, Any]]:
103
+ """
104
+ Verify and decode a JWT token.
105
+
106
+ Args:
107
+ token: JWT token string
108
+
109
+ Returns:
110
+ Dict containing token payload or None if invalid
111
+ """
112
+ try:
113
+ payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
114
+ return payload
115
+ except JWTError as e:
116
+ lgr.warning(f"Token verification failed: {e}")
117
+ return None
118
+
119
+
120
+ def authenticate_user(db: Session, username: str, password: str):
121
+ """
122
+ Authenticate user with username and password.
123
+
124
+ Args:
125
+ db: Database session
126
+ username: Username
127
+ password: Plain text password
128
+
129
+ Returns:
130
+ User object if authentication successful, None otherwise
131
+ """
132
+ from .auth_models import User
133
+
134
+ try:
135
+ user = db.query(User).filter(User.username == username).first()
136
+
137
+ if not user:
138
+ lgr.warning(f"Authentication attempt for non-existent user: {username}")
139
+ return None
140
+
141
+ if not user.is_active:
142
+ lgr.warning(f"Authentication attempt for inactive user: {username}")
143
+ return None
144
+
145
+ if not verify_password(password, user.hashed_password):
146
+ lgr.warning(f"Failed password authentication for user: {username}")
147
+ return None
148
+
149
+ # Update last login time
150
+ user.last_login = datetime.utcnow()
151
+ db.commit()
152
+
153
+ lgr.info(f"Successful authentication for user: {username}")
154
+ return user
155
+
156
+ except Exception as e:
157
+ lgr.error(f"Authentication error for user {username}: {e}")
158
+ db.rollback()
159
+ return None
160
+
161
+
162
+ def create_admin_user(db: Session, username: str = "admin", password: str = "admin123",
163
+ email: str = "admin@localhost", full_name: str = "System Administrator"):
164
+ """
165
+ Create initial admin user if no users exist.
166
+
167
+ Args:
168
+ db: Database session
169
+ username: Admin username
170
+ password: Admin password
171
+ email: Admin email
172
+ full_name: Admin full name
173
+
174
+ Returns:
175
+ User object or None if creation fails
176
+ """
177
+ from .auth_models import User, UserRole
178
+
179
+ try:
180
+ # Check if any users exist
181
+ user_count = db.query(User).count()
182
+
183
+ if user_count > 0:
184
+ lgr.info("Users already exist, skipping admin user creation")
185
+ return None
186
+
187
+ # Create admin user
188
+ hashed_password = get_password_hash(password)
189
+
190
+ admin_user = User(
191
+ username=username,
192
+ email=email,
193
+ full_name=full_name,
194
+ hashed_password=hashed_password,
195
+ role=UserRole.ADMIN,
196
+ is_active=True,
197
+ created_at=datetime.utcnow()
198
+ )
199
+
200
+ db.add(admin_user)
201
+ db.commit()
202
+ db.refresh(admin_user)
203
+
204
+ lgr.info(f"Created initial admin user: {username}")
205
+ return admin_user
206
+
207
+ except Exception as e:
208
+ lgr.error(f"Failed to create admin user: {e}")
209
+ db.rollback()
210
+ return None