bizteamai-smcp-biz 1.13.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.
smcp/cli/approve.py ADDED
@@ -0,0 +1,261 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Action approval tool for SMCP.
4
+
5
+ Allows administrators to approve or reject queued destructive actions.
6
+ """
7
+
8
+ import argparse
9
+ import json
10
+ import sys
11
+ import time
12
+ from pathlib import Path
13
+ from typing import Dict, List, Optional
14
+
15
+ try:
16
+ import yaml
17
+ YAML_AVAILABLE = True
18
+ except ImportError:
19
+ YAML_AVAILABLE = False
20
+
21
+ from ..confirm import ActionQueue, format_action_summary
22
+
23
+
24
+ def load_config(config_path: Optional[str] = None) -> Dict:
25
+ """Load configuration from file or environment."""
26
+ config = {}
27
+
28
+ # Try to load from config file
29
+ if config_path:
30
+ config_file = Path(config_path)
31
+ if config_file.exists():
32
+ if config_file.suffix.lower() in ['.yaml', '.yml'] and YAML_AVAILABLE:
33
+ with open(config_file, 'r') as f:
34
+ config = yaml.safe_load(f) or {}
35
+ elif config_file.suffix.lower() == '.json':
36
+ with open(config_file, 'r') as f:
37
+ config = json.load(f)
38
+
39
+ # Override with environment variables
40
+ import os
41
+ env_config = {
42
+ "QUEUE_FILE": os.getenv("SMCP_QUEUE_FILE"),
43
+ "LOG_PATH": os.getenv("SMCP_LOG_PATH"),
44
+ }
45
+
46
+ # Add non-None environment variables
47
+ for key, value in env_config.items():
48
+ if value is not None:
49
+ config[key] = value
50
+
51
+ return config
52
+
53
+
54
+ def list_pending_actions(queue: ActionQueue) -> List[Dict]:
55
+ """List all pending actions."""
56
+ return queue.get_pending_actions()
57
+
58
+
59
+ def display_action_details(action: Dict) -> None:
60
+ """Display detailed information about an action."""
61
+ print("=" * 60)
62
+ print(format_action_summary(action))
63
+ print("=" * 60)
64
+
65
+
66
+ def approve_action(queue: ActionQueue, action_id: str) -> bool:
67
+ """Approve an action."""
68
+ action = queue.approve_action(action_id)
69
+ if action:
70
+ print(f"✓ Action {action_id} approved")
71
+ return True
72
+ else:
73
+ print(f"✗ Action {action_id} not found or already processed")
74
+ return False
75
+
76
+
77
+ def reject_action(queue: ActionQueue, action_id: str) -> bool:
78
+ """Reject an action."""
79
+ if queue.reject_action(action_id):
80
+ print(f"✗ Action {action_id} rejected")
81
+ return True
82
+ else:
83
+ print(f"✗ Action {action_id} not found or already processed")
84
+ return False
85
+
86
+
87
+ def interactive_approval(queue: ActionQueue) -> None:
88
+ """Interactive approval mode."""
89
+ print("Interactive approval mode. Type 'help' for commands.")
90
+
91
+ while True:
92
+ try:
93
+ command = input("\nsmcp-approve> ").strip().lower()
94
+
95
+ if command in ['quit', 'exit', 'q']:
96
+ break
97
+ elif command == 'help':
98
+ print("Commands:")
99
+ print(" list - List all pending actions")
100
+ print(" show <action-id> - Show action details")
101
+ print(" approve <action-id> - Approve an action")
102
+ print(" reject <action-id> - Reject an action")
103
+ print(" cleanup [hours] - Clean up old actions")
104
+ print(" quit/exit/q - Exit interactive mode")
105
+ elif command == 'list':
106
+ actions = list_pending_actions(queue)
107
+ if not actions:
108
+ print("No pending actions")
109
+ else:
110
+ print(f"\n{len(actions)} pending action(s):")
111
+ for action in actions:
112
+ timestamp = time.strftime("%H:%M:%S", time.localtime(action["timestamp"]))
113
+ print(f" {action['id'][:8]}... - {action['function_name']} ({timestamp})")
114
+ elif command.startswith('show '):
115
+ action_id = command[5:].strip()
116
+ actions = list_pending_actions(queue)
117
+ action = next((a for a in actions if a['id'].startswith(action_id)), None)
118
+ if action:
119
+ display_action_details(action)
120
+ else:
121
+ print(f"Action not found: {action_id}")
122
+ elif command.startswith('approve '):
123
+ action_id = command[8:].strip()
124
+ # Find full action ID from partial
125
+ actions = list_pending_actions(queue)
126
+ action = next((a for a in actions if a['id'].startswith(action_id)), None)
127
+ if action:
128
+ approve_action(queue, action['id'])
129
+ else:
130
+ print(f"Action not found: {action_id}")
131
+ elif command.startswith('reject '):
132
+ action_id = command[7:].strip()
133
+ # Find full action ID from partial
134
+ actions = list_pending_actions(queue)
135
+ action = next((a for a in actions if a['id'].startswith(action_id)), None)
136
+ if action:
137
+ reject_action(queue, action['id'])
138
+ else:
139
+ print(f"Action not found: {action_id}")
140
+ elif command.startswith('cleanup'):
141
+ parts = command.split()
142
+ hours = int(parts[1]) if len(parts) > 1 else 24
143
+ removed = queue.cleanup_old_actions(hours)
144
+ print(f"Removed {removed} old action(s)")
145
+ else:
146
+ print(f"Unknown command: {command}. Type 'help' for available commands.")
147
+
148
+ except (KeyboardInterrupt, EOFError):
149
+ print("\nExiting...")
150
+ break
151
+ except Exception as e:
152
+ print(f"Error: {e}")
153
+
154
+
155
+ def main() -> None:
156
+ """Main entry point for the approve CLI tool."""
157
+ parser = argparse.ArgumentParser(
158
+ description="Approve or reject queued SMCP actions"
159
+ )
160
+ parser.add_argument(
161
+ "action_id",
162
+ nargs="?",
163
+ help="Action ID to approve (if not provided, enters interactive mode)"
164
+ )
165
+ parser.add_argument(
166
+ "--reject", "-r",
167
+ action="store_true",
168
+ help="Reject the action instead of approving it"
169
+ )
170
+ parser.add_argument(
171
+ "--list", "-l",
172
+ action="store_true",
173
+ help="List all pending actions and exit"
174
+ )
175
+ parser.add_argument(
176
+ "--config", "-c",
177
+ help="Path to configuration file"
178
+ )
179
+ parser.add_argument(
180
+ "--queue-file",
181
+ help="Path to the action queue file (overrides config)"
182
+ )
183
+ parser.add_argument(
184
+ "--cleanup",
185
+ type=int,
186
+ metavar="HOURS",
187
+ help="Clean up actions older than specified hours"
188
+ )
189
+ parser.add_argument(
190
+ "--interactive", "-i",
191
+ action="store_true",
192
+ help="Enter interactive approval mode"
193
+ )
194
+
195
+ args = parser.parse_args()
196
+
197
+ # Load configuration
198
+ config = load_config(args.config)
199
+
200
+ # Override queue file if specified
201
+ if args.queue_file:
202
+ config["QUEUE_FILE"] = args.queue_file
203
+
204
+ # Initialize action queue
205
+ queue_file = config.get("QUEUE_FILE", "/tmp/smcp_queue.json")
206
+ queue = ActionQueue(queue_file)
207
+
208
+ try:
209
+ # Handle cleanup
210
+ if args.cleanup:
211
+ removed = queue.cleanup_old_actions(args.cleanup)
212
+ print(f"Removed {removed} old action(s)")
213
+ return
214
+
215
+ # Handle list command
216
+ if args.list:
217
+ actions = list_pending_actions(queue)
218
+ if not actions:
219
+ print("No pending actions")
220
+ else:
221
+ print(f"{len(actions)} pending action(s):")
222
+ for action in actions:
223
+ timestamp = time.strftime("%Y-%m-%d %H:%M:%S",
224
+ time.localtime(action["timestamp"]))
225
+ print(f" {action['id']} - {action['function_name']} ({timestamp})")
226
+ return
227
+
228
+ # Handle interactive mode
229
+ if args.interactive or not args.action_id:
230
+ interactive_approval(queue)
231
+ return
232
+
233
+ # Handle specific action approval/rejection
234
+ if args.action_id:
235
+ # Find action (support partial IDs)
236
+ actions = list_pending_actions(queue)
237
+ action = next((a for a in actions if a['id'].startswith(args.action_id)), None)
238
+
239
+ if not action:
240
+ print(f"Action not found: {args.action_id}")
241
+ sys.exit(1)
242
+
243
+ action_id = action['id']
244
+
245
+ # Show action details
246
+ display_action_details(action)
247
+
248
+ if args.reject:
249
+ success = reject_action(queue, action_id)
250
+ else:
251
+ success = approve_action(queue, action_id)
252
+
253
+ sys.exit(0 if success else 1)
254
+
255
+ except Exception as e:
256
+ print(f"Error: {e}")
257
+ sys.exit(1)
258
+
259
+
260
+ if __name__ == "__main__":
261
+ main()
smcp/cli/gen_key.py ADDED
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ License key generation utility for SMCP Business Edition.
4
+ """
5
+ import sys
6
+ import hmac
7
+ import hashlib
8
+ import secrets
9
+ import argparse
10
+ from datetime import datetime, timedelta, timezone
11
+
12
+ def generate_license_key(customer_id: str, cores: int, days: int, secret: str) -> str:
13
+ """Generate a new license key."""
14
+ # Generate expiry date
15
+ expiry_date = datetime.now(timezone.utc) + timedelta(days=days)
16
+ expiry_str = expiry_date.strftime('%Y%m%d')
17
+
18
+ # Generate random nonce
19
+ nonce = secrets.token_hex(8)
20
+
21
+ # Create payload for signing
22
+ payload = f"BZT.{customer_id}.{cores}.{expiry_str}.{nonce}"
23
+
24
+ # Generate HMAC signature
25
+ signature = hmac.new(
26
+ secret.encode('utf-8'),
27
+ payload.encode('utf-8'),
28
+ hashlib.sha256
29
+ ).hexdigest()
30
+
31
+ return f"{payload}.{signature}"
32
+
33
+ def main():
34
+ parser = argparse.ArgumentParser(description='Generate SMCP Business Edition license keys')
35
+ parser.add_argument('--customer', '-c', required=True, help='Customer ID')
36
+ parser.add_argument('--cores', '-n', type=int, required=True, help='Number of cores')
37
+ parser.add_argument('--days', '-d', type=int, required=True, help='Validity period in days')
38
+ parser.add_argument('--secret', '-s', help='Server secret (default: use BIZTEAM_SERVER_SECRET env var)')
39
+ parser.add_argument('--output', '-o', help='Output file (default: stdout)')
40
+
41
+ args = parser.parse_args()
42
+
43
+ # Get server secret
44
+ secret = args.secret
45
+ if not secret:
46
+ import os
47
+ secret = os.getenv('BIZTEAM_SERVER_SECRET')
48
+ if not secret:
49
+ print("Error: Server secret required. Use --secret or set BIZTEAM_SERVER_SECRET", file=sys.stderr)
50
+ sys.exit(1)
51
+
52
+ # Generate key
53
+ try:
54
+ license_key = generate_license_key(args.customer, args.cores, args.days, secret)
55
+
56
+ # Output
57
+ if args.output:
58
+ with open(args.output, 'w') as f:
59
+ f.write(license_key + '\n')
60
+ print(f"License key written to {args.output}")
61
+ else:
62
+ print(license_key)
63
+
64
+ # Log for audit (in production, this would go to a database)
65
+ key_hash = hashlib.sha256(license_key.encode()).hexdigest()
66
+ print(f"# Audit: Customer={args.customer}, Cores={args.cores}, Days={args.days}, Hash={key_hash}", file=sys.stderr)
67
+
68
+ except Exception as e:
69
+ print(f"Error generating license key: {e}", file=sys.stderr)
70
+ sys.exit(1)
71
+
72
+ if __name__ == '__main__':
73
+ main()
smcp/cli/mkcert.py ADDED
@@ -0,0 +1,327 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Certificate generation tool for SMCP.
4
+
5
+ Creates a private CA and generates server/client certificates for mutual TLS.
6
+ """
7
+
8
+ import argparse
9
+ import os
10
+ import sys
11
+ from pathlib import Path
12
+ from typing import Optional
13
+
14
+ try:
15
+ from cryptography import x509
16
+ from cryptography.x509.oid import NameOID, ExtendedKeyUsageOID
17
+ from cryptography.hazmat.primitives import hashes, serialization
18
+ from cryptography.hazmat.primitives.asymmetric import rsa
19
+ from datetime import datetime, timedelta
20
+ CRYPTOGRAPHY_AVAILABLE = True
21
+ except ImportError:
22
+ CRYPTOGRAPHY_AVAILABLE = False
23
+
24
+
25
+ def generate_private_key() -> "rsa.RSAPrivateKey":
26
+ """Generate a new RSA private key."""
27
+ return rsa.generate_private_key(
28
+ public_exponent=65537,
29
+ key_size=2048,
30
+ )
31
+
32
+
33
+ def create_ca_certificate(
34
+ private_key: "rsa.RSAPrivateKey",
35
+ ca_name: str,
36
+ days: int = 365
37
+ ) -> "x509.Certificate":
38
+ """Create a self-signed CA certificate."""
39
+ subject = issuer = x509.Name([
40
+ x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
41
+ x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "CA"),
42
+ x509.NameAttribute(NameOID.LOCALITY_NAME, "San Francisco"),
43
+ x509.NameAttribute(NameOID.ORGANIZATION_NAME, "SMCP"),
44
+ x509.NameAttribute(NameOID.COMMON_NAME, ca_name),
45
+ ])
46
+
47
+ cert = x509.CertificateBuilder().subject_name(
48
+ subject
49
+ ).issuer_name(
50
+ issuer
51
+ ).public_key(
52
+ private_key.public_key()
53
+ ).serial_number(
54
+ x509.random_serial_number()
55
+ ).not_valid_before(
56
+ datetime.utcnow()
57
+ ).not_valid_after(
58
+ datetime.utcnow() + timedelta(days=days)
59
+ ).add_extension(
60
+ x509.SubjectAlternativeName([
61
+ x509.DNSName(ca_name),
62
+ ]),
63
+ critical=False,
64
+ ).add_extension(
65
+ x509.BasicConstraints(ca=True, path_length=None),
66
+ critical=True,
67
+ ).add_extension(
68
+ x509.KeyUsage(
69
+ digital_signature=True,
70
+ content_commitment=False,
71
+ key_encipherment=False,
72
+ data_encipherment=False,
73
+ key_agreement=False,
74
+ key_cert_sign=True,
75
+ crl_sign=True,
76
+ encipher_only=False,
77
+ decipher_only=False,
78
+ ),
79
+ critical=True,
80
+ ).sign(private_key, hashes.SHA256())
81
+
82
+ return cert
83
+
84
+
85
+ def create_server_certificate(
86
+ private_key: "rsa.RSAPrivateKey",
87
+ ca_cert: "x509.Certificate",
88
+ ca_private_key: "rsa.RSAPrivateKey",
89
+ server_name: str,
90
+ days: int = 365
91
+ ) -> "x509.Certificate":
92
+ """Create a server certificate signed by the CA."""
93
+ subject = x509.Name([
94
+ x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
95
+ x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "CA"),
96
+ x509.NameAttribute(NameOID.LOCALITY_NAME, "San Francisco"),
97
+ x509.NameAttribute(NameOID.ORGANIZATION_NAME, "SMCP"),
98
+ x509.NameAttribute(NameOID.COMMON_NAME, server_name),
99
+ ])
100
+
101
+ cert = x509.CertificateBuilder().subject_name(
102
+ subject
103
+ ).issuer_name(
104
+ ca_cert.subject
105
+ ).public_key(
106
+ private_key.public_key()
107
+ ).serial_number(
108
+ x509.random_serial_number()
109
+ ).not_valid_before(
110
+ datetime.utcnow()
111
+ ).not_valid_after(
112
+ datetime.utcnow() + timedelta(days=days)
113
+ ).add_extension(
114
+ x509.SubjectAlternativeName([
115
+ x509.DNSName(server_name),
116
+ x509.DNSName("localhost"),
117
+ x509.IPAddress("127.0.0.1"),
118
+ ]),
119
+ critical=False,
120
+ ).add_extension(
121
+ x509.BasicConstraints(ca=False, path_length=None),
122
+ critical=True,
123
+ ).add_extension(
124
+ x509.KeyUsage(
125
+ digital_signature=True,
126
+ content_commitment=False,
127
+ key_encipherment=True,
128
+ data_encipherment=False,
129
+ key_agreement=False,
130
+ key_cert_sign=False,
131
+ crl_sign=False,
132
+ encipher_only=False,
133
+ decipher_only=False,
134
+ ),
135
+ critical=True,
136
+ ).add_extension(
137
+ x509.ExtendedKeyUsage([
138
+ ExtendedKeyUsageOID.SERVER_AUTH,
139
+ ]),
140
+ critical=True,
141
+ ).sign(ca_private_key, hashes.SHA256())
142
+
143
+ return cert
144
+
145
+
146
+ def create_client_certificate(
147
+ private_key: "rsa.RSAPrivateKey",
148
+ ca_cert: "x509.Certificate",
149
+ ca_private_key: "rsa.RSAPrivateKey",
150
+ client_name: str,
151
+ days: int = 365
152
+ ) -> "x509.Certificate":
153
+ """Create a client certificate signed by the CA."""
154
+ subject = x509.Name([
155
+ x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
156
+ x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "CA"),
157
+ x509.NameAttribute(NameOID.LOCALITY_NAME, "San Francisco"),
158
+ x509.NameAttribute(NameOID.ORGANIZATION_NAME, "SMCP"),
159
+ x509.NameAttribute(NameOID.COMMON_NAME, client_name),
160
+ ])
161
+
162
+ cert = x509.CertificateBuilder().subject_name(
163
+ subject
164
+ ).issuer_name(
165
+ ca_cert.subject
166
+ ).public_key(
167
+ private_key.public_key()
168
+ ).serial_number(
169
+ x509.random_serial_number()
170
+ ).not_valid_before(
171
+ datetime.utcnow()
172
+ ).not_valid_after(
173
+ datetime.utcnow() + timedelta(days=days)
174
+ ).add_extension(
175
+ x509.BasicConstraints(ca=False, path_length=None),
176
+ critical=True,
177
+ ).add_extension(
178
+ x509.KeyUsage(
179
+ digital_signature=True,
180
+ content_commitment=False,
181
+ key_encipherment=True,
182
+ data_encipherment=False,
183
+ key_agreement=False,
184
+ key_cert_sign=False,
185
+ crl_sign=False,
186
+ encipher_only=False,
187
+ decipher_only=False,
188
+ ),
189
+ critical=True,
190
+ ).add_extension(
191
+ x509.ExtendedKeyUsage([
192
+ ExtendedKeyUsageOID.CLIENT_AUTH,
193
+ ]),
194
+ critical=True,
195
+ ).sign(ca_private_key, hashes.SHA256())
196
+
197
+ return cert
198
+
199
+
200
+ def save_private_key(private_key: "rsa.RSAPrivateKey", path: Path) -> None:
201
+ """Save a private key to a PEM file."""
202
+ with open(path, "wb") as f:
203
+ f.write(private_key.private_bytes(
204
+ encoding=serialization.Encoding.PEM,
205
+ format=serialization.PrivateFormat.PKCS8,
206
+ encryption_algorithm=serialization.NoEncryption()
207
+ ))
208
+ os.chmod(path, 0o600) # Make private key readable only by owner
209
+
210
+
211
+ def save_certificate(cert: "x509.Certificate", path: Path) -> None:
212
+ """Save a certificate to a PEM file."""
213
+ with open(path, "wb") as f:
214
+ f.write(cert.public_bytes(serialization.Encoding.PEM))
215
+
216
+
217
+ def generate_certificates(
218
+ output_dir: Path,
219
+ ca_name: str,
220
+ server_name: str,
221
+ client_name: Optional[str] = None,
222
+ days: int = 365
223
+ ) -> None:
224
+ """Generate a complete certificate set."""
225
+ output_dir.mkdir(parents=True, exist_ok=True)
226
+
227
+ print(f"Generating certificates in {output_dir}")
228
+
229
+ # Generate CA
230
+ print("1. Generating CA private key...")
231
+ ca_private_key = generate_private_key()
232
+ save_private_key(ca_private_key, output_dir / "ca-key.pem")
233
+
234
+ print("2. Creating CA certificate...")
235
+ ca_cert = create_ca_certificate(ca_private_key, ca_name, days)
236
+ save_certificate(ca_cert, output_dir / "ca.pem")
237
+
238
+ # Generate server certificate
239
+ print("3. Generating server private key...")
240
+ server_private_key = generate_private_key()
241
+ save_private_key(server_private_key, output_dir / "server-key.pem")
242
+
243
+ print("4. Creating server certificate...")
244
+ server_cert = create_server_certificate(
245
+ server_private_key, ca_cert, ca_private_key, server_name, days
246
+ )
247
+ save_certificate(server_cert, output_dir / "server.pem")
248
+
249
+ # Generate client certificate if requested
250
+ if client_name:
251
+ print("5. Generating client private key...")
252
+ client_private_key = generate_private_key()
253
+ save_private_key(client_private_key, output_dir / "client-key.pem")
254
+
255
+ print("6. Creating client certificate...")
256
+ client_cert = create_client_certificate(
257
+ client_private_key, ca_cert, ca_private_key, client_name, days
258
+ )
259
+ save_certificate(client_cert, output_dir / "client.pem")
260
+
261
+ print("\nCertificate generation complete!")
262
+ print(f"Files created in {output_dir}:")
263
+ print(" ca.pem - CA certificate")
264
+ print(" ca-key.pem - CA private key")
265
+ print(" server.pem - Server certificate")
266
+ print(" server-key.pem - Server private key")
267
+ if client_name:
268
+ print(" client.pem - Client certificate")
269
+ print(" client-key.pem - Client private key")
270
+
271
+ print(f"\nCertificates valid for {days} days")
272
+
273
+
274
+ def main() -> None:
275
+ """Main entry point for the mkcert CLI tool."""
276
+ if not CRYPTOGRAPHY_AVAILABLE:
277
+ print("Error: cryptography package is required for certificate generation")
278
+ print("Install it with: pip install cryptography")
279
+ sys.exit(1)
280
+
281
+ parser = argparse.ArgumentParser(
282
+ description="Generate certificates for SMCP mutual TLS"
283
+ )
284
+ parser.add_argument(
285
+ "--output-dir", "-o",
286
+ type=Path,
287
+ default=Path("./certs"),
288
+ help="Output directory for certificates (default: ./certs)"
289
+ )
290
+ parser.add_argument(
291
+ "--ca-name",
292
+ default="SMCP-CA",
293
+ help="Name for the Certificate Authority (default: SMCP-CA)"
294
+ )
295
+ parser.add_argument(
296
+ "--server-name",
297
+ default="smcp-server",
298
+ help="Server name for certificate (default: smcp-server)"
299
+ )
300
+ parser.add_argument(
301
+ "--client-name",
302
+ help="Client name for certificate (optional)"
303
+ )
304
+ parser.add_argument(
305
+ "--days",
306
+ type=int,
307
+ default=365,
308
+ help="Certificate validity period in days (default: 365)"
309
+ )
310
+
311
+ args = parser.parse_args()
312
+
313
+ try:
314
+ generate_certificates(
315
+ output_dir=args.output_dir,
316
+ ca_name=args.ca_name,
317
+ server_name=args.server_name,
318
+ client_name=args.client_name,
319
+ days=args.days
320
+ )
321
+ except Exception as e:
322
+ print(f"Error generating certificates: {e}")
323
+ sys.exit(1)
324
+
325
+
326
+ if __name__ == "__main__":
327
+ main()