provablyfine 0.1.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.
Files changed (180) hide show
  1. provablyfine/__init__.py +0 -0
  2. provablyfine/anet/__init__.py +5 -0
  3. provablyfine/anet/base.py +52 -0
  4. provablyfine/anet/conftest.py +137 -0
  5. provablyfine/anet/exceptions.py +2 -0
  6. provablyfine/anet/http.py +130 -0
  7. provablyfine/anet/mux.py +458 -0
  8. provablyfine/anet/socket.py +88 -0
  9. provablyfine/anet/sockets.py +57 -0
  10. provablyfine/anet/ssl.py +182 -0
  11. provablyfine/anet/stream.py +48 -0
  12. provablyfine/anet/test_http.py +232 -0
  13. provablyfine/anet/test_mux.py +403 -0
  14. provablyfine/anet/test_socket.py +152 -0
  15. provablyfine/anet/test_ssl.py +192 -0
  16. provablyfine/anet/test_stream.py +238 -0
  17. provablyfine/api/__init__.py +0 -0
  18. provablyfine/api/app.py +189 -0
  19. provablyfine/api/app_db.py +372 -0
  20. provablyfine/api/config.py +64 -0
  21. provablyfine/api/context.py +109 -0
  22. provablyfine/api/converters.py +586 -0
  23. provablyfine/api/crypto_policy.py +9 -0
  24. provablyfine/api/db.py +225 -0
  25. provablyfine/api/dependencies.py +36 -0
  26. provablyfine/api/endpoints/__init__.py +37 -0
  27. provablyfine/api/endpoints/audit_log.py +23 -0
  28. provablyfine/api/endpoints/auth.py +92 -0
  29. provablyfine/api/endpoints/auth_endpoint.py +158 -0
  30. provablyfine/api/endpoints/auth_http_sig.py +92 -0
  31. provablyfine/api/endpoints/auth_oauth2.py +258 -0
  32. provablyfine/api/endpoints/auth_oidc.py +180 -0
  33. provablyfine/api/endpoints/bastion.py +88 -0
  34. provablyfine/api/endpoints/boundary.py +151 -0
  35. provablyfine/api/endpoints/debug.py +21 -0
  36. provablyfine/api/endpoints/directory.py +29 -0
  37. provablyfine/api/endpoints/identity.py +321 -0
  38. provablyfine/api/endpoints/initialize.py +182 -0
  39. provablyfine/api/endpoints/public.py +30 -0
  40. provablyfine/api/endpoints/role.py +135 -0
  41. provablyfine/api/endpoints/ssh.py +265 -0
  42. provablyfine/api/endpoints/tag.py +65 -0
  43. provablyfine/api/endpoints/tenant.py +152 -0
  44. provablyfine/api/grant.py +503 -0
  45. provablyfine/api/middleware.py +43 -0
  46. provablyfine/api/model/__init__.py +27 -0
  47. provablyfine/api/model/audit_log.py +42 -0
  48. provablyfine/api/model/auth_config.py +77 -0
  49. provablyfine/api/model/bastion.py +146 -0
  50. provablyfine/api/model/boundary.py +110 -0
  51. provablyfine/api/model/denylist.py +21 -0
  52. provablyfine/api/model/grant.py +229 -0
  53. provablyfine/api/model/identity.py +131 -0
  54. provablyfine/api/model/identity_invitation_key.py +74 -0
  55. provablyfine/api/model/oidc_key.py +47 -0
  56. provablyfine/api/model/role.py +109 -0
  57. provablyfine/api/model/signing_key.py +38 -0
  58. provablyfine/api/model/utils.py +13 -0
  59. provablyfine/api/oauth2_providers.py +8 -0
  60. provablyfine/api/registry_db.py +63 -0
  61. provablyfine/api/responses.py +34 -0
  62. provablyfine/api/rotate.py +80 -0
  63. provablyfine/api/schemas/__init__.py +17 -0
  64. provablyfine/api/schemas/audit.py +20 -0
  65. provablyfine/api/schemas/auth.py +112 -0
  66. provablyfine/api/schemas/base.py +7 -0
  67. provablyfine/api/schemas/bastion.py +42 -0
  68. provablyfine/api/schemas/boundary.py +44 -0
  69. provablyfine/api/schemas/directory.py +33 -0
  70. provablyfine/api/schemas/grant.py +196 -0
  71. provablyfine/api/schemas/identity.py +99 -0
  72. provablyfine/api/schemas/jwk.py +34 -0
  73. provablyfine/api/schemas/problem.py +10 -0
  74. provablyfine/api/schemas/role.py +45 -0
  75. provablyfine/api/schemas/ssh.py +43 -0
  76. provablyfine/api/schemas/tag.py +30 -0
  77. provablyfine/api/schemas/tenant.py +28 -0
  78. provablyfine/api/server.py +65 -0
  79. provablyfine/api/signature.py +264 -0
  80. provablyfine/api/test_grant.py +579 -0
  81. provablyfine/base64url.py +20 -0
  82. provablyfine/bastion/__init__.py +3 -0
  83. provablyfine/bastion/app.py +179 -0
  84. provablyfine/bastion/atomic.py +28 -0
  85. provablyfine/bastion/control_app.py +68 -0
  86. provablyfine/bastion/exceptions.py +2 -0
  87. provablyfine/bastion/fdstore.py +82 -0
  88. provablyfine/bastion/http.py +204 -0
  89. provablyfine/bastion/relay.py +232 -0
  90. provablyfine/bastion/server.py +135 -0
  91. provablyfine/bastion/systemd.py +72 -0
  92. provablyfine/bastion/test_atomic.py +97 -0
  93. provablyfine/bastion/trusted_key.py +55 -0
  94. provablyfine/cli/__init__.py +0 -0
  95. provablyfine/cli/grant.py +38 -0
  96. provablyfine/cli/login.py +213 -0
  97. provablyfine/cli/pf/__init__.py +0 -0
  98. provablyfine/cli/pf/bastion_cli.py +274 -0
  99. provablyfine/cli/pf/dev_bastion_cli.py +55 -0
  100. provablyfine/cli/pf/main.py +147 -0
  101. provablyfine/cli/pf/openssh_cli.py +93 -0
  102. provablyfine/cli/pf/openssh_host_init.py +249 -0
  103. provablyfine/cli/pf/ssh_cli.py +200 -0
  104. provablyfine/cli/pfa/__init__.py +0 -0
  105. provablyfine/cli/pfa/audit_log_cli.py +56 -0
  106. provablyfine/cli/pfa/auth_cli.py +173 -0
  107. provablyfine/cli/pfa/bastion_cli.py +115 -0
  108. provablyfine/cli/pfa/boundary_cli.py +186 -0
  109. provablyfine/cli/pfa/grant_cli.py +278 -0
  110. provablyfine/cli/pfa/identity_cli.py +220 -0
  111. provablyfine/cli/pfa/main.py +149 -0
  112. provablyfine/cli/pfa/role_cli.py +200 -0
  113. provablyfine/cli/pfa/tag_cli.py +90 -0
  114. provablyfine/cli/pfa/tenant_cli.py +103 -0
  115. provablyfine/cli/yaml_utils.py +18 -0
  116. provablyfine/client/__init__.py +5 -0
  117. provablyfine/client/aio.py +311 -0
  118. provablyfine/client/configuration.py +31 -0
  119. provablyfine/client/exceptions.py +6 -0
  120. provablyfine/client/http_client.py +365 -0
  121. provablyfine/client/schemas.py +629 -0
  122. provablyfine/client/ssh_utils.py +30 -0
  123. provablyfine/client/sync.py +688 -0
  124. provablyfine/jwk.py +405 -0
  125. provablyfine/log.py +139 -0
  126. provablyfine/ssh/__init__.py +3 -0
  127. provablyfine/ssh/agent.py +143 -0
  128. provablyfine/ssh/buffer.py +94 -0
  129. provablyfine/ssh/cert.py +213 -0
  130. provablyfine/ssh/constants.py +7 -0
  131. provablyfine/ssh/exceptions.py +6 -0
  132. provablyfine/ssh/serde.py +143 -0
  133. provablyfine/ssh/test_buffer.py +47 -0
  134. provablyfine/ssh/test_serde.py +170 -0
  135. provablyfine/ssh/test_ssh_keygen.py +314 -0
  136. provablyfine/test_jwk.py +253 -0
  137. provablyfine/tui/__init__.py +0 -0
  138. provablyfine/tui/_utils.py +4 -0
  139. provablyfine/tui/app.py +74 -0
  140. provablyfine/tui/async_client.py +66 -0
  141. provablyfine/tui/audit_log_list.py +43 -0
  142. provablyfine/tui/auth_list.py +219 -0
  143. provablyfine/tui/auth_view.py +116 -0
  144. provablyfine/tui/auto_complete.py +75 -0
  145. provablyfine/tui/base.py +39 -0
  146. provablyfine/tui/bastion_list.py +134 -0
  147. provablyfine/tui/bastion_view.py +174 -0
  148. provablyfine/tui/boundary_list.py +118 -0
  149. provablyfine/tui/boundary_view.py +198 -0
  150. provablyfine/tui/checkbox_input.py +107 -0
  151. provablyfine/tui/clipboard.py +44 -0
  152. provablyfine/tui/grant_edit/__init__.py +24 -0
  153. provablyfine/tui/grant_edit/base.py +242 -0
  154. provablyfine/tui/grant_edit/boundary.py +77 -0
  155. provablyfine/tui/grant_edit/identity.py +128 -0
  156. provablyfine/tui/grant_edit/role.py +77 -0
  157. provablyfine/tui/grant_edit/screens.py +89 -0
  158. provablyfine/tui/grant_edit/ssh_command.py +53 -0
  159. provablyfine/tui/grant_edit/ssh_port_forward.py +43 -0
  160. provablyfine/tui/grant_edit/ssh_shell.py +57 -0
  161. provablyfine/tui/grant_edit/tag.py +66 -0
  162. provablyfine/tui/grant_edit/tenant.py +73 -0
  163. provablyfine/tui/grant_list.py +155 -0
  164. provablyfine/tui/header.py +72 -0
  165. provablyfine/tui/home.py +70 -0
  166. provablyfine/tui/identity_list.py +220 -0
  167. provablyfine/tui/identity_view.py +174 -0
  168. provablyfine/tui/member_list.py +49 -0
  169. provablyfine/tui/relogin.py +256 -0
  170. provablyfine/tui/role_list.py +117 -0
  171. provablyfine/tui/role_view.py +194 -0
  172. provablyfine/tui/setup.py +380 -0
  173. provablyfine/tui/tag_list.py +94 -0
  174. provablyfine/tui/tenant_list.py +101 -0
  175. provablyfine/tui/test_clipboard.py +30 -0
  176. provablyfine/tui/test_utils.py +17 -0
  177. provablyfine-0.1.0.dist-info/METADATA +133 -0
  178. provablyfine-0.1.0.dist-info/RECORD +180 -0
  179. provablyfine-0.1.0.dist-info/WHEEL +4 -0
  180. provablyfine-0.1.0.dist-info/entry_points.txt +5 -0
File without changes
@@ -0,0 +1,5 @@
1
+ """Async socket library."""
2
+
3
+ from . import base, exceptions, http, mux, socket, sockets, ssl, stream
4
+
5
+ __all__ = ["base", "exceptions", "http", "mux", "socket", "sockets", "ssl", "stream"]
@@ -0,0 +1,52 @@
1
+ import abc
2
+ import enum
3
+ import socket as _socket
4
+ import typing
5
+
6
+
7
+ class Shut(enum.IntEnum):
8
+ RD = _socket.SHUT_RD
9
+ WR = _socket.SHUT_WR
10
+ RDWR = _socket.SHUT_RDWR
11
+
12
+
13
+ class Socket(abc.ABC):
14
+ @abc.abstractmethod
15
+ def fileno(self) -> int:
16
+ pass
17
+
18
+ @abc.abstractmethod
19
+ def detach(self) -> int:
20
+ pass
21
+
22
+ @abc.abstractmethod
23
+ async def send(self, data: bytes) -> int:
24
+ pass
25
+
26
+ @abc.abstractmethod
27
+ async def recv(self, n: int) -> bytes:
28
+ pass
29
+
30
+ @abc.abstractmethod
31
+ async def shutdown(self, flag: Shut) -> None:
32
+ pass
33
+
34
+ @abc.abstractmethod
35
+ def close(self) -> None:
36
+ pass
37
+
38
+ @abc.abstractmethod
39
+ def getsockname(self) -> typing.Any:
40
+ pass
41
+
42
+ @abc.abstractmethod
43
+ async def bind(self, address: typing.Any) -> None:
44
+ pass
45
+
46
+ @abc.abstractmethod
47
+ async def listen(self, n: int) -> None:
48
+ pass
49
+
50
+ @abc.abstractmethod
51
+ async def accept(self) -> tuple["Socket", typing.Any]:
52
+ pass
@@ -0,0 +1,137 @@
1
+ """Shared fixtures and helpers for anet tests."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import collections.abc
7
+ import datetime
8
+ import ipaddress
9
+ import os
10
+
11
+ import cryptography
12
+ import cryptography.hazmat.primitives.asymmetric.rsa as crypto_rsa
13
+ import cryptography.hazmat.primitives.hashes as crypto_hashes
14
+ import cryptography.hazmat.primitives.serialization as crypto_serialization
15
+ import cryptography.x509
16
+ import cryptography.x509.oid as crypto_oid
17
+ import pytest
18
+
19
+ from . import socket as anet_socket
20
+ from . import ssl as anet_ssl
21
+
22
+
23
+ def _create_server_context(
24
+ certfile: str | os.PathLike[str],
25
+ keyfile: str | os.PathLike[str],
26
+ ) -> anet_ssl.SSLContext:
27
+ """Create server-side SSL context from certificate and key files."""
28
+ ctx = anet_ssl.SSLContext(anet_ssl.ContextProtocol.SERVER)
29
+ ctx.load_cert_chain(str(certfile), str(keyfile))
30
+ return ctx
31
+
32
+
33
+ def _create_client_context(
34
+ cafile: str | os.PathLike[str],
35
+ check_hostname: bool = True,
36
+ ) -> anet_ssl.SSLContext:
37
+ """Create client-side SSL context with CA verification."""
38
+ ctx = anet_ssl.SSLContext(anet_ssl.ContextProtocol.CLIENT)
39
+ ctx.load_verify_locations(str(cafile))
40
+ ctx.check_hostname = check_hostname
41
+ ctx.verify_mode = anet_ssl.VerifyMode.REQUIRED
42
+ return ctx
43
+
44
+
45
+ @pytest.fixture(scope="session")
46
+ def ssl_contexts(tmp_path_factory: pytest.TempPathFactory) -> tuple[anet_ssl.SSLContext, anet_ssl.SSLContext]:
47
+ """Generate self-signed CA and server certificate; return (server_ctx, client_ctx)."""
48
+ tmpdir = tmp_path_factory.mktemp("ssl")
49
+
50
+ # Generate CA key and cert
51
+ ca_key = crypto_rsa.generate_private_key(public_exponent=65537, key_size=2048)
52
+ ca_subject = cryptography.x509.Name([cryptography.x509.NameAttribute(crypto_oid.NameOID.COMMON_NAME, "Test CA")])
53
+ now = datetime.datetime.now(datetime.UTC)
54
+ ca_cert = (
55
+ cryptography.x509.CertificateBuilder()
56
+ .subject_name(ca_subject)
57
+ .issuer_name(ca_subject)
58
+ .public_key(ca_key.public_key())
59
+ .serial_number(1)
60
+ .not_valid_before(now)
61
+ .not_valid_after(now + datetime.timedelta(days=365))
62
+ .add_extension(
63
+ cryptography.x509.BasicConstraints(ca=True, path_length=None),
64
+ critical=True,
65
+ )
66
+ .sign(ca_key, crypto_hashes.SHA256())
67
+ )
68
+
69
+ # Generate server key and cert
70
+ server_key = crypto_rsa.generate_private_key(public_exponent=65537, key_size=2048)
71
+ server_subject = cryptography.x509.Name(
72
+ [cryptography.x509.NameAttribute(crypto_oid.NameOID.COMMON_NAME, "127.0.0.1")]
73
+ )
74
+ server_cert = (
75
+ cryptography.x509.CertificateBuilder()
76
+ .subject_name(server_subject)
77
+ .issuer_name(ca_subject)
78
+ .public_key(server_key.public_key())
79
+ .serial_number(2)
80
+ .not_valid_before(now)
81
+ .not_valid_after(now + datetime.timedelta(days=365))
82
+ .add_extension(
83
+ cryptography.x509.SubjectAlternativeName([cryptography.x509.IPAddress(ipaddress.IPv4Address("127.0.0.1"))]),
84
+ critical=False,
85
+ )
86
+ .sign(ca_key, crypto_hashes.SHA256())
87
+ )
88
+
89
+ # Write to temp files
90
+ ca_cert_file = tmpdir / "ca.pem"
91
+ server_cert_file = tmpdir / "server.pem"
92
+ server_key_file = tmpdir / "server.key"
93
+
94
+ ca_cert_file.write_bytes(ca_cert.public_bytes(crypto_serialization.Encoding.PEM))
95
+ server_cert_file.write_bytes(server_cert.public_bytes(crypto_serialization.Encoding.PEM))
96
+ server_key_file.write_bytes(
97
+ server_key.private_bytes(
98
+ encoding=crypto_serialization.Encoding.PEM,
99
+ format=crypto_serialization.PrivateFormat.PKCS8,
100
+ encryption_algorithm=crypto_serialization.NoEncryption(),
101
+ )
102
+ )
103
+
104
+ # Create SSL contexts
105
+ server_ctx = _create_server_context(server_cert_file, server_key_file)
106
+ client_ctx = _create_client_context(ca_cert_file, check_hostname=False)
107
+
108
+ return server_ctx, client_ctx
109
+
110
+
111
+ @pytest.fixture
112
+ async def anet_socketpair() -> collections.abc.AsyncGenerator[tuple[anet_socket.Socket, anet_socket.Socket], None]:
113
+ """Create a connected AF_UNIX socketpair wrapped in anet.socket.Socket."""
114
+ a, b = anet_socket.socketpair(anet_socket.Family.UNIX, anet_socket.Type.STREAM)
115
+ yield a, b
116
+ a.close()
117
+ b.close()
118
+
119
+
120
+ @pytest.fixture
121
+ async def tls_socketpair(
122
+ anet_socketpair: tuple[anet_socket.Socket, anet_socket.Socket],
123
+ ssl_contexts: tuple[anet_ssl.SSLContext, anet_ssl.SSLContext],
124
+ ) -> collections.abc.AsyncGenerator[tuple[anet_ssl.Socket, anet_ssl.Socket], None]:
125
+ """Create a TLS-wrapped socketpair with completed handshake."""
126
+ raw_client, raw_server = anet_socketpair
127
+ server_ctx, client_ctx = ssl_contexts
128
+
129
+ ssl_server = await server_ctx.wrap_socket(raw_server, server_side=True)
130
+ ssl_client = await client_ctx.wrap_socket(raw_client, server_side=False, server_hostname=None)
131
+
132
+ # Handshake must be concurrent
133
+ await asyncio.gather(ssl_server.handshake(), ssl_client.handshake())
134
+
135
+ yield ssl_client, ssl_server
136
+ ssl_client.close()
137
+ ssl_server.close()
@@ -0,0 +1,2 @@
1
+ class Error(Exception):
2
+ pass
@@ -0,0 +1,130 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses
4
+ import logging
5
+
6
+ from . import base, exceptions, stream
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ @dataclasses.dataclass
12
+ class Message:
13
+ start_line: str
14
+ headers: dict[str, str]
15
+ body: bytes
16
+
17
+ @classmethod
18
+ async def deserialize(cls, sock: base.Socket) -> Message:
19
+ reader = stream.Reader(sock)
20
+ try:
21
+ start_line = (await reader.read_until(b"\r\n"))[:-2].decode("ascii")
22
+ except EOFError:
23
+ raise exceptions.Error("Unable to read start line before connection was closed")
24
+ headers: dict[str, str] = {}
25
+ while True:
26
+ try:
27
+ line = (await reader.read_until(b"\r\n"))[:-2].decode("ascii")
28
+ except EOFError:
29
+ raise exceptions.Error("Unable to last line before connection was closed")
30
+ if line == "":
31
+ break
32
+ colon = line.find(":")
33
+ if colon == -1:
34
+ logger.warning(f"Invalid header: {line}")
35
+ raise exceptions.Error("Invalid header")
36
+ key = line[:colon].lower()
37
+ value = line[colon + 1 :].lstrip()
38
+ headers[key] = value
39
+ if "content-length" in headers:
40
+ length = headers["content-length"].strip()
41
+ if not length.isdigit():
42
+ logger.warning(f"Invalid Content-Length: {length}")
43
+ raise exceptions.Error("Invalid Content-Length")
44
+ body = await reader.read(int(length))
45
+ else:
46
+ # We do not support chunk encoding
47
+ body = b""
48
+ return Message(start_line=start_line, headers=headers, body=body)
49
+
50
+ async def serialize(self, sock: base.Socket) -> None:
51
+ lines = [self.start_line]
52
+ for name, value in self.headers.items():
53
+ lines.append(f"{name}: {value}")
54
+ lines.extend(["", ""])
55
+ data = b"\r\n".join(line.encode("ascii") for line in lines) + self.body
56
+ await sock.send(data)
57
+
58
+
59
+ @dataclasses.dataclass
60
+ class Request:
61
+ method: str
62
+ resource_target: str
63
+ version: str
64
+ headers: dict[str, str]
65
+ body: bytes
66
+
67
+ @classmethod
68
+ async def deserialize(cls, sock: base.Socket) -> Request:
69
+ message = await Message.deserialize(sock)
70
+ start_line = message.start_line
71
+ space = start_line.find(" ")
72
+ if space == -1:
73
+ raise exceptions.Error(f"Invalid request start line: {message.start_line}")
74
+ method = start_line[:space]
75
+ start_line = start_line[space + 1 :]
76
+ space = start_line.find(" ")
77
+ if space == -1:
78
+ raise exceptions.Error(f"Invalid request start line: {message.start_line}")
79
+ resource_target = start_line[:space]
80
+ version = start_line[space + 1 :]
81
+ return Request(
82
+ method=method,
83
+ resource_target=resource_target,
84
+ version=version,
85
+ headers=message.headers,
86
+ body=message.body,
87
+ )
88
+
89
+ async def serialize(self, sock: base.Socket) -> None:
90
+ start = f"{self.method} {self.resource_target} {self.version}"
91
+ message = Message(start_line=start, headers=self.headers, body=self.body)
92
+ await message.serialize(sock)
93
+
94
+
95
+ @dataclasses.dataclass
96
+ class Response:
97
+ version: str
98
+ status_code: int
99
+ reason: str
100
+ headers: dict[str, str]
101
+ body: bytes
102
+
103
+ @classmethod
104
+ async def deserialize(cls, sock: base.Socket) -> Response:
105
+ message = await Message.deserialize(sock)
106
+ status_line = message.start_line
107
+ space1 = status_line.find(" ")
108
+ if space1 == -1:
109
+ raise exceptions.Error(f"Invalid status_line={status_line}")
110
+ version = status_line[:space1]
111
+ remainder = status_line[space1 + 1 :]
112
+ space2 = remainder.find(" ")
113
+ if space2 == -1:
114
+ raise exceptions.Error(f"Invalid status_line={status_line}")
115
+ status_code = remainder[:space2]
116
+ reason = remainder[space2 + 1 :]
117
+ if not status_code.isdigit():
118
+ raise exceptions.Error(f"Invalid status_code={status_code}")
119
+ return Response(
120
+ version=version,
121
+ status_code=int(status_code),
122
+ reason=reason,
123
+ headers=message.headers,
124
+ body=message.body,
125
+ )
126
+
127
+ async def serialize(self, sock: base.Socket) -> None:
128
+ start = f"{self.version} {self.status_code} {self.reason}"
129
+ message = Message(start_line=start, headers=self.headers, body=self.body)
130
+ await message.serialize(sock)