lldap-py 0.1.0__py3-none-any.whl → 0.2.0__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.
- lldap/__init__.py +82 -76
- lldap/client.py +228 -213
- lldap/config.py +48 -46
- lldap/exceptions.py +31 -31
- lldap/groups.py +159 -159
- lldap/models.py +27 -27
- lldap/users.py +230 -204
- {lldap_py-0.1.0.dist-info → lldap_py-0.2.0.dist-info}/METADATA +109 -61
- lldap_py-0.2.0.dist-info/RECORD +12 -0
- {lldap_py-0.1.0.dist-info → lldap_py-0.2.0.dist-info}/WHEEL +1 -1
- {lldap_py-0.1.0.dist-info → lldap_py-0.2.0.dist-info}/licenses/LICENSE +21 -21
- lldap_py-0.1.0.dist-info/RECORD +0 -12
- {lldap_py-0.1.0.dist-info → lldap_py-0.2.0.dist-info}/top_level.txt +0 -0
lldap/__init__.py
CHANGED
|
@@ -1,76 +1,82 @@
|
|
|
1
|
-
"""LLDAP-py - Simple Python interface for managing LLDAP servers."""
|
|
2
|
-
|
|
3
|
-
__version__ = "0.1.0"
|
|
4
|
-
|
|
5
|
-
from .config import Config
|
|
6
|
-
from .client import LLDAPClient
|
|
7
|
-
from .users import UserManager
|
|
8
|
-
from .groups import GroupManager
|
|
9
|
-
from .exceptions import LLDAPError, AuthenticationError, ConnectionError, GraphQLError, ValidationError
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class LLDAPManager(UserManager, GroupManager):
|
|
13
|
-
"""Simplified LLDAP manager combining user and group management.
|
|
14
|
-
|
|
15
|
-
This is the main interface for interacting with LLDAP servers.
|
|
16
|
-
Pass connection values in the constructor and use methods for operations.
|
|
17
|
-
"""
|
|
18
|
-
|
|
19
|
-
def __init__(
|
|
20
|
-
self,
|
|
21
|
-
http_url: str, # (http(s)://<host>:<port>)
|
|
22
|
-
username: str = None,
|
|
23
|
-
password: str = None,
|
|
24
|
-
token: str = None,
|
|
25
|
-
refresh_token: str = None,
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
"
|
|
74
|
-
"
|
|
75
|
-
"
|
|
76
|
-
|
|
1
|
+
"""LLDAP-py - Simple Python interface for managing LLDAP servers."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
|
|
5
|
+
from .config import Config
|
|
6
|
+
from .client import LLDAPClient
|
|
7
|
+
from .users import UserManager
|
|
8
|
+
from .groups import GroupManager
|
|
9
|
+
from .exceptions import LLDAPError, AuthenticationError, ConnectionError, GraphQLError, ValidationError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LLDAPManager(UserManager, GroupManager):
|
|
13
|
+
"""Simplified LLDAP manager combining user and group management.
|
|
14
|
+
|
|
15
|
+
This is the main interface for interacting with LLDAP servers.
|
|
16
|
+
Pass connection values in the constructor and use methods for operations.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
http_url: str, # (http(s)://<host>:<port>)
|
|
22
|
+
username: str = None,
|
|
23
|
+
password: str = None,
|
|
24
|
+
token: str = None,
|
|
25
|
+
refresh_token: str = None,
|
|
26
|
+
base_dn: str = None,
|
|
27
|
+
ldap_server: str = None,
|
|
28
|
+
verify_ssl: bool = True
|
|
29
|
+
):
|
|
30
|
+
"""Initialize LLDAP Manager with connection details.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
http_url: HTTP URL of LLDAP server (e.g., "http://localhost:17170")
|
|
34
|
+
username: Admin username (default: "admin")
|
|
35
|
+
password: Admin password
|
|
36
|
+
token: Authentication token (if already authenticated)
|
|
37
|
+
refresh_token: Refresh token for token renewal
|
|
38
|
+
verify_ssl: Whether to verify SSL certificates (default: True)
|
|
39
|
+
Raises:
|
|
40
|
+
AuthenticationError: If connection/authentication fails
|
|
41
|
+
"""
|
|
42
|
+
self.config = Config(
|
|
43
|
+
http_url=http_url,
|
|
44
|
+
username=username,
|
|
45
|
+
password=password,
|
|
46
|
+
token=token,
|
|
47
|
+
refresh_token=refresh_token,
|
|
48
|
+
base_dn=base_dn,
|
|
49
|
+
ldap_server=ldap_server,
|
|
50
|
+
verify_ssl=verify_ssl,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
self.config.validate()
|
|
55
|
+
except LLDAPError:
|
|
56
|
+
raise
|
|
57
|
+
|
|
58
|
+
self.client = LLDAPClient(self.config)
|
|
59
|
+
|
|
60
|
+
# Authenticate on initialization
|
|
61
|
+
try:
|
|
62
|
+
self.client.authenticate()
|
|
63
|
+
except (AuthenticationError, ConnectionError):
|
|
64
|
+
raise
|
|
65
|
+
|
|
66
|
+
def close(self):
|
|
67
|
+
"""Close the session."""
|
|
68
|
+
if hasattr(self.client, 'session'):
|
|
69
|
+
self.client.session.close()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
__all__ = [
|
|
73
|
+
"LLDAPManager",
|
|
74
|
+
"LLDAPClient",
|
|
75
|
+
"LLDAPError",
|
|
76
|
+
"UserManager",
|
|
77
|
+
"GroupManager",
|
|
78
|
+
"AuthenticationError",
|
|
79
|
+
"ConnectionError",
|
|
80
|
+
"ValidationError",
|
|
81
|
+
"GraphQLError",
|
|
82
|
+
]
|
lldap/client.py
CHANGED
|
@@ -1,213 +1,228 @@
|
|
|
1
|
-
import json
|
|
2
|
-
from typing import Optional, Dict, Any, Tuple
|
|
3
|
-
import requests
|
|
4
|
-
from .config import Config
|
|
5
|
-
from .exceptions import (
|
|
6
|
-
AuthenticationError,
|
|
7
|
-
ConnectionError,
|
|
8
|
-
GraphQLError,
|
|
9
|
-
)
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class LLDAPClient:
|
|
13
|
-
"""Client for interacting with LLDAP server via GraphQL API."""
|
|
14
|
-
|
|
15
|
-
def __init__(self, config: Config):
|
|
16
|
-
"""Initialize LLDAP client.
|
|
17
|
-
|
|
18
|
-
Args:
|
|
19
|
-
config: Configuration object
|
|
20
|
-
"""
|
|
21
|
-
self.config = config
|
|
22
|
-
self.session = requests.Session()
|
|
23
|
-
self._authenticated = False
|
|
24
|
-
|
|
25
|
-
def authenticate(self) -> Tuple[str, str]:
|
|
26
|
-
"""Authenticate and get tokens.
|
|
27
|
-
|
|
28
|
-
Returns:
|
|
29
|
-
Tuple of (token, refresh_token)
|
|
30
|
-
|
|
31
|
-
Raises:
|
|
32
|
-
AuthenticationError: If authentication fails
|
|
33
|
-
ConnectionError: If connection fails
|
|
34
|
-
"""
|
|
35
|
-
if self.config.token:
|
|
36
|
-
# Already have a token
|
|
37
|
-
return self.config.token, self.config.refresh_token or ""
|
|
38
|
-
|
|
39
|
-
if self.config.refresh_token:
|
|
40
|
-
# Use refresh token to get new token
|
|
41
|
-
token = self.refresh_token(self.config.refresh_token)
|
|
42
|
-
self.config.token = token
|
|
43
|
-
return token, self.config.refresh_token
|
|
44
|
-
|
|
45
|
-
# Use username and password
|
|
46
|
-
url = self.config.get_endpoint_url("auth")
|
|
47
|
-
payload = {
|
|
48
|
-
"username": self.config.username,
|
|
49
|
-
"password": self.config.password,
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
try:
|
|
53
|
-
response = self.session.post(
|
|
54
|
-
url,
|
|
55
|
-
json=payload,
|
|
56
|
-
headers={"Content-Type": "application/json"},
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
self.config.
|
|
71
|
-
self.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
return
|
|
1
|
+
import json
|
|
2
|
+
from typing import Optional, Dict, Any, Tuple
|
|
3
|
+
import requests
|
|
4
|
+
from .config import Config
|
|
5
|
+
from .exceptions import (
|
|
6
|
+
AuthenticationError,
|
|
7
|
+
ConnectionError,
|
|
8
|
+
GraphQLError,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LLDAPClient:
|
|
13
|
+
"""Client for interacting with LLDAP server via GraphQL API."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, config: Config):
|
|
16
|
+
"""Initialize LLDAP client.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
config: Configuration object
|
|
20
|
+
"""
|
|
21
|
+
self.config = config
|
|
22
|
+
self.session = requests.Session()
|
|
23
|
+
self._authenticated = False
|
|
24
|
+
|
|
25
|
+
def authenticate(self) -> Tuple[str, str]:
|
|
26
|
+
"""Authenticate and get tokens.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Tuple of (token, refresh_token)
|
|
30
|
+
|
|
31
|
+
Raises:
|
|
32
|
+
AuthenticationError: If authentication fails
|
|
33
|
+
ConnectionError: If connection fails
|
|
34
|
+
"""
|
|
35
|
+
if self.config.token:
|
|
36
|
+
# Already have a token
|
|
37
|
+
return self.config.token, self.config.refresh_token or ""
|
|
38
|
+
|
|
39
|
+
if self.config.refresh_token:
|
|
40
|
+
# Use refresh token to get new token
|
|
41
|
+
token = self.refresh_token(self.config.refresh_token)
|
|
42
|
+
self.config.token = token
|
|
43
|
+
return token, self.config.refresh_token
|
|
44
|
+
|
|
45
|
+
# Use username and password
|
|
46
|
+
url = self.config.get_endpoint_url("auth")
|
|
47
|
+
payload = {
|
|
48
|
+
"username": self.config.username,
|
|
49
|
+
"password": self.config.password,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
response = self.session.post(
|
|
54
|
+
url,
|
|
55
|
+
json=payload,
|
|
56
|
+
headers={"Content-Type": "application/json"},
|
|
57
|
+
verify=self.config.verify_ssl,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if response.status_code != 200:
|
|
61
|
+
raise AuthenticationError(f"Authentication failed: {response.text}")
|
|
62
|
+
|
|
63
|
+
data = response.json()
|
|
64
|
+
token = data.get("token")
|
|
65
|
+
refresh_token = data.get("refreshToken")
|
|
66
|
+
|
|
67
|
+
if not token:
|
|
68
|
+
raise AuthenticationError("No token in response")
|
|
69
|
+
|
|
70
|
+
self.config.token = token
|
|
71
|
+
self.config.refresh_token = refresh_token
|
|
72
|
+
self._authenticated = True
|
|
73
|
+
|
|
74
|
+
return token, refresh_token
|
|
75
|
+
|
|
76
|
+
except requests.RequestException as e:
|
|
77
|
+
raise ConnectionError(f"Connection error: {e}")
|
|
78
|
+
|
|
79
|
+
def refresh_token(self, refresh_token: str) -> str:
|
|
80
|
+
"""Get a new token using refresh token.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
refresh_token: Refresh token
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
New authentication token
|
|
87
|
+
|
|
88
|
+
Raises:
|
|
89
|
+
AuthenticationError: If token refresh fails
|
|
90
|
+
ConnectionError: If connection fails
|
|
91
|
+
"""
|
|
92
|
+
url = self.config.get_endpoint_url("refresh")
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
response = self.session.get(
|
|
96
|
+
url,
|
|
97
|
+
cookies={"refresh_token": refresh_token},
|
|
98
|
+
verify=self.config.verify_ssl,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
if response.status_code != 200:
|
|
102
|
+
raise AuthenticationError(f"Token refresh failed: {response.text}")
|
|
103
|
+
|
|
104
|
+
data = response.json()
|
|
105
|
+
token = data.get("token")
|
|
106
|
+
|
|
107
|
+
if not token:
|
|
108
|
+
raise AuthenticationError("No token in refresh response")
|
|
109
|
+
|
|
110
|
+
return token
|
|
111
|
+
|
|
112
|
+
except requests.RequestException as e:
|
|
113
|
+
raise ConnectionError(f"Connection error: {e}")
|
|
114
|
+
|
|
115
|
+
def logout(self) -> bool:
|
|
116
|
+
"""Logout and invalidate refresh token.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
True if successful
|
|
120
|
+
|
|
121
|
+
Raises:
|
|
122
|
+
ConnectionError: If connection fails
|
|
123
|
+
"""
|
|
124
|
+
if not self.config.refresh_token:
|
|
125
|
+
return False
|
|
126
|
+
|
|
127
|
+
url = self.config.get_endpoint_url("logout")
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
response = self.session.get(
|
|
131
|
+
url,
|
|
132
|
+
cookies={"refresh_token": self.config.refresh_token},
|
|
133
|
+
verify=self.config.verify_ssl,
|
|
134
|
+
)
|
|
135
|
+
return response.status_code == 200
|
|
136
|
+
|
|
137
|
+
except requests.RequestException as e:
|
|
138
|
+
raise ConnectionError(f"Connection error: {e}")
|
|
139
|
+
|
|
140
|
+
def query(
|
|
141
|
+
self,
|
|
142
|
+
query: str,
|
|
143
|
+
variables: Optional[Dict[str, Any]] = None,
|
|
144
|
+
) -> Dict[str, Any]:
|
|
145
|
+
"""Execute a GraphQL query.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
query: GraphQL query string
|
|
149
|
+
variables: Query variables
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
GraphQL response data
|
|
153
|
+
|
|
154
|
+
Raises:
|
|
155
|
+
GraphQLError: If query returns errors
|
|
156
|
+
ConnectionError: If connection fails
|
|
157
|
+
"""
|
|
158
|
+
# Ensure we're authenticated
|
|
159
|
+
if not self.config.token:
|
|
160
|
+
self.authenticate()
|
|
161
|
+
|
|
162
|
+
url = self.config.get_endpoint_url("graphql")
|
|
163
|
+
|
|
164
|
+
payload = {
|
|
165
|
+
"query": query,
|
|
166
|
+
"variables": variables or {},
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
headers = {
|
|
170
|
+
"Content-Type": "application/json",
|
|
171
|
+
"Authorization": f"Bearer {self.config.token}",
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
data = json.dumps(payload)
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
response = self.session.post(url, data=data, headers=headers, verify=self.config.verify_ssl)
|
|
178
|
+
|
|
179
|
+
# Check for HTTP errors
|
|
180
|
+
if response.status_code == 401:
|
|
181
|
+
raise AuthenticationError(
|
|
182
|
+
"Authentication failed. Token may be expired. Try logging in again."
|
|
183
|
+
)
|
|
184
|
+
elif response.status_code != 200:
|
|
185
|
+
raise ConnectionError(
|
|
186
|
+
f"HTTP {response.status_code}: {response.text}"
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
result = response.json()
|
|
190
|
+
|
|
191
|
+
# Check for GraphQL errors
|
|
192
|
+
if "errors" in result:
|
|
193
|
+
error_messages = [err.get("message", str(err)) for err in result["errors"]]
|
|
194
|
+
raise GraphQLError("; ".join(error_messages))
|
|
195
|
+
|
|
196
|
+
return result
|
|
197
|
+
|
|
198
|
+
except requests.RequestException as e:
|
|
199
|
+
raise ConnectionError(f"Connection error: {e}")
|
|
200
|
+
|
|
201
|
+
def ensure_authenticated(self) -> None:
|
|
202
|
+
"""Ensure client is authenticated, authenticate if not."""
|
|
203
|
+
if not self.config.token:
|
|
204
|
+
self.authenticate()
|
|
205
|
+
|
|
206
|
+
def create_bind_dn(self, user_id: str) -> str:
|
|
207
|
+
"""Create bind DN for a user.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
user_id: User ID """
|
|
211
|
+
if self.config.base_dn is None:
|
|
212
|
+
raise ValueError("base_dn is not configured")
|
|
213
|
+
return f"uid={user_id},ou=people,{self.config.base_dn}"
|
|
214
|
+
|
|
215
|
+
def ensure_ldap_connection(self) -> bool:
|
|
216
|
+
from ldap3 import Server, Connection, ALL, MODIFY_REPLACE
|
|
217
|
+
if (self.config.ldap_server is not None) and (self.config.base_dn is not None):
|
|
218
|
+
conn = None
|
|
219
|
+
try:
|
|
220
|
+
login_dn = self.create_bind_dn(self.config.username)
|
|
221
|
+
server = Server(self.config.ldap_server, get_info=ALL)
|
|
222
|
+
self.conn = Connection(server, login_dn, self.config.password, auto_bind=True)
|
|
223
|
+
except Exception:
|
|
224
|
+
return False
|
|
225
|
+
return True
|
|
226
|
+
return False
|
|
227
|
+
|
|
228
|
+
|