nasiko-cli 2.0.0__tar.gz
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.
- nasiko_cli-2.0.0/PKG-INFO +33 -0
- nasiko_cli-2.0.0/auth/__init__.py +1 -0
- nasiko_cli-2.0.0/auth/auth_commands.py +218 -0
- nasiko_cli-2.0.0/auth/auth_manager.py +381 -0
- nasiko_cli-2.0.0/commands/__init__.py +1 -0
- nasiko_cli-2.0.0/commands/access.py +299 -0
- nasiko_cli-2.0.0/commands/chat_history.py +198 -0
- nasiko_cli-2.0.0/commands/chat_send.py +173 -0
- nasiko_cli-2.0.0/commands/github.py +383 -0
- nasiko_cli-2.0.0/commands/n8n.py +251 -0
- nasiko_cli-2.0.0/commands/observability.py +974 -0
- nasiko_cli-2.0.0/commands/registry.py +437 -0
- nasiko_cli-2.0.0/commands/search.py +127 -0
- nasiko_cli-2.0.0/commands/upload_agent.py +270 -0
- nasiko_cli-2.0.0/commands/user_management.py +327 -0
- nasiko_cli-2.0.0/core/__init__.py +1 -0
- nasiko_cli-2.0.0/core/api_client.py +374 -0
- nasiko_cli-2.0.0/core/settings.py +94 -0
- nasiko_cli-2.0.0/groups/__init__.py +1 -0
- nasiko_cli-2.0.0/groups/access_group.py +77 -0
- nasiko_cli-2.0.0/groups/agent_group.py +109 -0
- nasiko_cli-2.0.0/groups/chat_group.py +90 -0
- nasiko_cli-2.0.0/groups/github_group.py +57 -0
- nasiko_cli-2.0.0/groups/images_group.py +478 -0
- nasiko_cli-2.0.0/groups/local_group.py +610 -0
- nasiko_cli-2.0.0/groups/n8n_group.py +94 -0
- nasiko_cli-2.0.0/groups/observability_group.py +81 -0
- nasiko_cli-2.0.0/groups/search_group.py +34 -0
- nasiko_cli-2.0.0/groups/user_group.py +89 -0
- nasiko_cli-2.0.0/k8s/__init__.py +1 -0
- nasiko_cli-2.0.0/k8s/agent-rbac.yaml +57 -0
- nasiko_cli-2.0.0/k8s/charts/nasiko-platform/Chart.yaml +0 -0
- nasiko_cli-2.0.0/k8s/charts/nasiko-platform/environments/dev.yaml +0 -0
- nasiko_cli-2.0.0/k8s/charts/nasiko-platform/environments/prod.yaml +0 -0
- nasiko_cli-2.0.0/k8s/charts/nasiko-platform/environments/staging.yaml +0 -0
- nasiko_cli-2.0.0/k8s/charts/nasiko-platform/templates/configmaps/app-config.yaml +0 -0
- nasiko_cli-2.0.0/k8s/charts/nasiko-platform/templates/infrastructure/buildkit/deployment.yaml +79 -0
- nasiko_cli-2.0.0/k8s/charts/nasiko-platform/templates/infrastructure/buildkit/namespace.yaml +6 -0
- nasiko_cli-2.0.0/k8s/charts/nasiko-platform/templates/infrastructure/buildkit/pvc.yaml +13 -0
- nasiko_cli-2.0.0/k8s/charts/nasiko-platform/templates/infrastructure/buildkit/regcred-secret.yaml +15 -0
- nasiko_cli-2.0.0/k8s/charts/nasiko-platform/templates/infrastructure/buildkit/service.yaml +12 -0
- nasiko_cli-2.0.0/k8s/charts/nasiko-platform/templates/infrastructure/buildkit/serviceaccount.yaml +5 -0
- nasiko_cli-2.0.0/k8s/charts/nasiko-platform/templates/infrastructure/mongodb.yaml +31 -0
- nasiko_cli-2.0.0/k8s/charts/nasiko-platform/templates/infrastructure/ollama.yaml +56 -0
- nasiko_cli-2.0.0/k8s/charts/nasiko-platform/templates/infrastructure/phoenix.yaml +146 -0
- nasiko_cli-2.0.0/k8s/charts/nasiko-platform/templates/infrastructure/postgresql.yaml +32 -0
- nasiko_cli-2.0.0/k8s/charts/nasiko-platform/templates/infrastructure/redis.yaml +27 -0
- nasiko_cli-2.0.0/k8s/charts/nasiko-platform/templates/initialization/superuser-init.yaml +154 -0
- nasiko_cli-2.0.0/k8s/charts/nasiko-platform/templates/namespace.yaml +14 -0
- nasiko_cli-2.0.0/k8s/charts/nasiko-platform/templates/networking/ingress.yaml +0 -0
- nasiko_cli-2.0.0/k8s/charts/nasiko-platform/templates/networking/networkpolicies.yaml +0 -0
- nasiko_cli-2.0.0/k8s/charts/nasiko-platform/templates/rbac/clusterrole.yaml +33 -0
- nasiko_cli-2.0.0/k8s/charts/nasiko-platform/templates/rbac/clusterrolebinding.yaml +12 -0
- nasiko_cli-2.0.0/k8s/charts/nasiko-platform/templates/rbac/serviceaccount.yaml +5 -0
- nasiko_cli-2.0.0/k8s/charts/nasiko-platform/templates/secrets/registry-secret.yaml +0 -0
- nasiko_cli-2.0.0/k8s/charts/nasiko-platform/templates/services/agent-gateway/deployment.yaml +136 -0
- nasiko_cli-2.0.0/k8s/charts/nasiko-platform/templates/services/agent-gateway/kong-migrations.yaml +29 -0
- nasiko_cli-2.0.0/k8s/charts/nasiko-platform/templates/services/agent-gateway/kong-plugins-config.yaml +349 -0
- nasiko_cli-2.0.0/k8s/charts/nasiko-platform/templates/services/agent-gateway/service-registry-deployment.yaml +56 -0
- nasiko_cli-2.0.0/k8s/charts/nasiko-platform/templates/services/auth-service/deployment.yaml +47 -0
- nasiko_cli-2.0.0/k8s/charts/nasiko-platform/templates/services/n8n/deployment.yaml +84 -0
- nasiko_cli-2.0.0/k8s/charts/nasiko-platform/templates/services/n8n/pvc.yaml +14 -0
- nasiko_cli-2.0.0/k8s/charts/nasiko-platform/templates/services/n8n/service.yaml +16 -0
- nasiko_cli-2.0.0/k8s/charts/nasiko-platform/templates/services/nasiko-backend/deployment.yaml +67 -0
- nasiko_cli-2.0.0/k8s/charts/nasiko-platform/templates/services/nasiko-k8s-build-worker/deployment.yaml +101 -0
- nasiko_cli-2.0.0/k8s/charts/nasiko-platform/templates/services/nasiko-router/deployment.yaml +62 -0
- nasiko_cli-2.0.0/k8s/charts/nasiko-platform/templates/services/nasiko-web/deployment.yaml +49 -0
- nasiko_cli-2.0.0/k8s/charts/nasiko-platform/values.yaml +0 -0
- nasiko_cli-2.0.0/k8s/dashboard-admin.yaml +18 -0
- nasiko_cli-2.0.0/k8s/kube-dashboard.yaml +307 -0
- nasiko_cli-2.0.0/k8s/utils.py +150 -0
- nasiko_cli-2.0.0/main.py +283 -0
- nasiko_cli-2.0.0/nasiko_cli.egg-info/PKG-INFO +33 -0
- nasiko_cli-2.0.0/nasiko_cli.egg-info/SOURCES.txt +101 -0
- nasiko_cli-2.0.0/nasiko_cli.egg-info/dependency_links.txt +1 -0
- nasiko_cli-2.0.0/nasiko_cli.egg-info/entry_points.txt +2 -0
- nasiko_cli-2.0.0/nasiko_cli.egg-info/requires.txt +16 -0
- nasiko_cli-2.0.0/nasiko_cli.egg-info/top_level.txt +8 -0
- nasiko_cli-2.0.0/pyproject.toml +78 -0
- nasiko_cli-2.0.0/setup/__init__.py +0 -0
- nasiko_cli-2.0.0/setup/app_setup.py +1194 -0
- nasiko_cli-2.0.0/setup/buildkit_setup.py +342 -0
- nasiko_cli-2.0.0/setup/config.py +611 -0
- nasiko_cli-2.0.0/setup/container_registry_setup.py +626 -0
- nasiko_cli-2.0.0/setup/harbor_setup.py +332 -0
- nasiko_cli-2.0.0/setup/k8s_setup.py +672 -0
- nasiko_cli-2.0.0/setup/setup.py +2149 -0
- nasiko_cli-2.0.0/setup/terraform/__init__.py +2 -0
- nasiko_cli-2.0.0/setup/terraform/aws/__init__.py +1 -0
- nasiko_cli-2.0.0/setup/terraform/aws/main.tf +225 -0
- nasiko_cli-2.0.0/setup/terraform/aws/outputs.tf +14 -0
- nasiko_cli-2.0.0/setup/terraform/aws/variables.tf +23 -0
- nasiko_cli-2.0.0/setup/terraform/aws/versions.tf +10 -0
- nasiko_cli-2.0.0/setup/terraform/digitalocean/__init__.py +1 -0
- nasiko_cli-2.0.0/setup/terraform/digitalocean/doks.tf +47 -0
- nasiko_cli-2.0.0/setup/terraform/digitalocean/outputs.tf +59 -0
- nasiko_cli-2.0.0/setup/terraform/digitalocean/provider.tf +13 -0
- nasiko_cli-2.0.0/setup/terraform/digitalocean/variables.tf +65 -0
- nasiko_cli-2.0.0/setup/terraform_state.py +325 -0
- nasiko_cli-2.0.0/setup/utils.py +535 -0
- nasiko_cli-2.0.0/setup.cfg +4 -0
- nasiko_cli-2.0.0/utils/__init__.py +1 -0
- nasiko_cli-2.0.0/utils/utils.py +3 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nasiko-cli
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Command-line interface for Nasiko agent management platform
|
|
5
|
+
Author: Nasiko Team
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/arithmic/nasiko
|
|
8
|
+
Project-URL: Repository, https://github.com/arithmic/nasiko
|
|
9
|
+
Project-URL: Issues, https://github.com/arithmic/nasiko/issues
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Requires-Dist: typer>=0.9.0
|
|
20
|
+
Requires-Dist: requests>=2.28.0
|
|
21
|
+
Requires-Dist: rich>=13.0.0
|
|
22
|
+
Requires-Dist: kubernetes>=34.1.0
|
|
23
|
+
Requires-Dist: docker>=7.1.0
|
|
24
|
+
Requires-Dist: keyring>=24.0.0
|
|
25
|
+
Requires-Dist: cryptography>=41.0.0
|
|
26
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
30
|
+
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
31
|
+
Requires-Dist: isort>=5.0.0; extra == "dev"
|
|
32
|
+
Requires-Dist: flake8>=6.0.0; extra == "dev"
|
|
33
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Auth module for Nasiko CLI
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Authentication commands for Nasiko CLI.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from typing import Optional
|
|
7
|
+
import sys
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
11
|
+
|
|
12
|
+
from auth.auth_manager import get_auth_manager
|
|
13
|
+
from core.api_client import get_api_client
|
|
14
|
+
|
|
15
|
+
# Create auth command group
|
|
16
|
+
auth_app = typer.Typer(help="Authentication commands")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@auth_app.command("login")
|
|
20
|
+
def login_command(
|
|
21
|
+
access_key: Optional[str] = typer.Option(
|
|
22
|
+
None, "--access-key", "-k", help="Your access key"
|
|
23
|
+
),
|
|
24
|
+
access_secret: Optional[str] = typer.Option(
|
|
25
|
+
None, "--access-secret", "-s", help="Your access secret"
|
|
26
|
+
),
|
|
27
|
+
save_credentials: bool = typer.Option(
|
|
28
|
+
True, "--save-credentials/--no-save", help="Save credentials for auto-renewal"
|
|
29
|
+
),
|
|
30
|
+
api_url: Optional[str] = typer.Option(
|
|
31
|
+
None, "--api-url", help="API base URL (optional)"
|
|
32
|
+
),
|
|
33
|
+
):
|
|
34
|
+
"""Login to Nasiko with access key and secret."""
|
|
35
|
+
|
|
36
|
+
# Get credentials interactively if not provided
|
|
37
|
+
if not access_key:
|
|
38
|
+
access_key = typer.prompt("Access Key")
|
|
39
|
+
|
|
40
|
+
if not access_secret:
|
|
41
|
+
access_secret = typer.prompt("Access Secret", hide_input=True)
|
|
42
|
+
|
|
43
|
+
# Validate inputs
|
|
44
|
+
if not access_key or not access_secret:
|
|
45
|
+
typer.echo("❌ Access key and secret are required")
|
|
46
|
+
raise typer.Exit(1)
|
|
47
|
+
|
|
48
|
+
if not access_key.startswith("NASK_"):
|
|
49
|
+
typer.echo("❌ Invalid access key format (should start with NASK_)")
|
|
50
|
+
raise typer.Exit(1)
|
|
51
|
+
|
|
52
|
+
# Login with auth manager
|
|
53
|
+
auth_manager = get_auth_manager()
|
|
54
|
+
if api_url:
|
|
55
|
+
auth_manager.base_url = api_url
|
|
56
|
+
|
|
57
|
+
typer.echo("🔐 Authenticating...")
|
|
58
|
+
|
|
59
|
+
if auth_manager.login(access_key, access_secret, save_credentials):
|
|
60
|
+
# Get user info if available
|
|
61
|
+
user_info = auth_manager.get_user_info()
|
|
62
|
+
if user_info:
|
|
63
|
+
username = user_info.get("username", "Unknown")
|
|
64
|
+
is_super = user_info.get("is_super_user", False)
|
|
65
|
+
role = "Super User" if is_super else "User"
|
|
66
|
+
typer.echo(f"👋 Welcome back, {username} ({role})")
|
|
67
|
+
|
|
68
|
+
typer.echo("\n🚀 You can now use authenticated commands:")
|
|
69
|
+
typer.echo(" • nasiko registry-list")
|
|
70
|
+
typer.echo(" • nasiko upload-zip <file.zip>")
|
|
71
|
+
typer.echo(" • nasiko status")
|
|
72
|
+
typer.echo(" • nasiko traces <agent-name>")
|
|
73
|
+
else:
|
|
74
|
+
raise typer.Exit(1)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@auth_app.command("logout")
|
|
78
|
+
def logout_command(
|
|
79
|
+
clear_all: bool = typer.Option(
|
|
80
|
+
False, "--clear-all", help="Clear all stored credentials"
|
|
81
|
+
)
|
|
82
|
+
):
|
|
83
|
+
"""Logout from Nasiko."""
|
|
84
|
+
|
|
85
|
+
auth_manager = get_auth_manager()
|
|
86
|
+
|
|
87
|
+
if not auth_manager.is_logged_in():
|
|
88
|
+
typer.echo("ℹ️ You are not logged in")
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
if auth_manager.logout(clear_credentials=clear_all):
|
|
92
|
+
if clear_all:
|
|
93
|
+
typer.echo("🗑️ All authentication data cleared")
|
|
94
|
+
typer.echo("👋 See you next time!")
|
|
95
|
+
else:
|
|
96
|
+
typer.echo("⚠️ Logout may not have completed successfully")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@auth_app.command("status")
|
|
100
|
+
def status_command():
|
|
101
|
+
"""Check authentication status."""
|
|
102
|
+
|
|
103
|
+
auth_manager = get_auth_manager()
|
|
104
|
+
|
|
105
|
+
if auth_manager.is_logged_in():
|
|
106
|
+
typer.echo("✅ Logged in")
|
|
107
|
+
|
|
108
|
+
# Try to get user info
|
|
109
|
+
user_info = auth_manager.get_user_info()
|
|
110
|
+
if user_info:
|
|
111
|
+
typer.echo(f" User: {user_info.get('username', 'Unknown')}")
|
|
112
|
+
typer.echo(
|
|
113
|
+
f" Role: {'Super User' if user_info.get('is_super_user') else 'User'}"
|
|
114
|
+
)
|
|
115
|
+
typer.echo(f" Email: {user_info.get('email', 'Not available')}")
|
|
116
|
+
|
|
117
|
+
if user_info.get("last_login"):
|
|
118
|
+
typer.echo(f" Last login: {user_info['last_login']}")
|
|
119
|
+
|
|
120
|
+
# Test API connectivity
|
|
121
|
+
try:
|
|
122
|
+
client = get_api_client()
|
|
123
|
+
response = client.get("healthcheck", require_auth=False)
|
|
124
|
+
if response.status_code == 200:
|
|
125
|
+
typer.echo(" API: ✅ Connected")
|
|
126
|
+
else:
|
|
127
|
+
typer.echo(" API: ⚠️ Connection issues")
|
|
128
|
+
except:
|
|
129
|
+
typer.echo(" API: ❌ Cannot connect")
|
|
130
|
+
|
|
131
|
+
else:
|
|
132
|
+
typer.echo("❌ Not logged in")
|
|
133
|
+
typer.echo("\n💡 To login:")
|
|
134
|
+
typer.echo(" nasiko login")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@auth_app.command("whoami")
|
|
138
|
+
def whoami_command():
|
|
139
|
+
"""Show current user information."""
|
|
140
|
+
|
|
141
|
+
auth_manager = get_auth_manager()
|
|
142
|
+
|
|
143
|
+
if not auth_manager.is_logged_in():
|
|
144
|
+
typer.echo("❌ Not logged in")
|
|
145
|
+
typer.echo("💡 Use: nasiko login")
|
|
146
|
+
raise typer.Exit(1)
|
|
147
|
+
|
|
148
|
+
user_info = auth_manager.get_user_info()
|
|
149
|
+
|
|
150
|
+
if not user_info:
|
|
151
|
+
typer.echo("❌ Could not retrieve user information")
|
|
152
|
+
typer.echo("💡 Try: nasiko login")
|
|
153
|
+
raise typer.Exit(1)
|
|
154
|
+
|
|
155
|
+
# Display user information
|
|
156
|
+
typer.echo("👤 User Information:")
|
|
157
|
+
typer.echo(f" Username: {user_info.get('username', 'Unknown')}")
|
|
158
|
+
typer.echo(f" Email: {user_info.get('email', 'Not available')}")
|
|
159
|
+
typer.echo(f" Role: {'Super User' if user_info.get('is_super_user') else 'User'}")
|
|
160
|
+
typer.echo(f" Active: {'Yes' if user_info.get('is_active') else 'No'}")
|
|
161
|
+
|
|
162
|
+
if user_info.get("created_at"):
|
|
163
|
+
typer.echo(f" Created: {user_info['created_at']}")
|
|
164
|
+
|
|
165
|
+
if user_info.get("last_login"):
|
|
166
|
+
typer.echo(f" Last login: {user_info['last_login']}")
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# Standalone commands for backward compatibility
|
|
170
|
+
def login_standalone(
|
|
171
|
+
access_key: str = None,
|
|
172
|
+
access_secret: str = None,
|
|
173
|
+
save_credentials: bool = True,
|
|
174
|
+
api_url: str = None,
|
|
175
|
+
):
|
|
176
|
+
"""Standalone login function for backward compatibility"""
|
|
177
|
+
# Call the actual login_command with proper parameters
|
|
178
|
+
return _do_login(access_key, access_secret, save_credentials, api_url)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _do_login(
|
|
182
|
+
access_key: Optional[str],
|
|
183
|
+
access_secret: Optional[str],
|
|
184
|
+
save_credentials: bool = True,
|
|
185
|
+
api_url: Optional[str] = None,
|
|
186
|
+
):
|
|
187
|
+
"""Internal login function that does the actual work"""
|
|
188
|
+
# Validate inputs
|
|
189
|
+
if not access_key or not access_secret:
|
|
190
|
+
typer.echo("❌ Access key and secret are required")
|
|
191
|
+
raise typer.Exit(1)
|
|
192
|
+
|
|
193
|
+
if not access_key.startswith("NASK_"):
|
|
194
|
+
typer.echo("❌ Invalid access key format (should start with NASK_)")
|
|
195
|
+
raise typer.Exit(1)
|
|
196
|
+
|
|
197
|
+
# Login with auth manager
|
|
198
|
+
auth_manager = get_auth_manager()
|
|
199
|
+
if api_url:
|
|
200
|
+
auth_manager.auth_url = api_url
|
|
201
|
+
|
|
202
|
+
typer.echo("🔐 Authenticating...")
|
|
203
|
+
|
|
204
|
+
if auth_manager.login(access_key, access_secret, save_credentials):
|
|
205
|
+
# Get user info if available
|
|
206
|
+
user_info = auth_manager.get_user_info()
|
|
207
|
+
if user_info:
|
|
208
|
+
username = user_info.get("username", "Unknown")
|
|
209
|
+
is_super = user_info.get("is_super_user", False)
|
|
210
|
+
role = "Super User" if is_super else "User"
|
|
211
|
+
typer.echo(f"👋 Welcome back, {username} ({role})")
|
|
212
|
+
else:
|
|
213
|
+
raise typer.Exit(1)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# Export for use in main CLI
|
|
217
|
+
if __name__ == "__main__":
|
|
218
|
+
auth_app()
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Secure Authentication Manager for Nasiko CLI.
|
|
3
|
+
Provides secure token storage using OS keyring with fallback to encrypted files.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
from typing import Optional, Dict, Any
|
|
8
|
+
import requests
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
# Try to import keyring for secure storage
|
|
12
|
+
try:
|
|
13
|
+
import keyring
|
|
14
|
+
|
|
15
|
+
KEYRING_AVAILABLE = True
|
|
16
|
+
except ImportError:
|
|
17
|
+
KEYRING_AVAILABLE = False
|
|
18
|
+
typer.echo(
|
|
19
|
+
"⚠️ Warning: keyring not available. Falling back to file-based storage."
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
# Fallback imports for encrypted storage
|
|
23
|
+
try:
|
|
24
|
+
from cryptography.fernet import Fernet
|
|
25
|
+
import base64
|
|
26
|
+
import hashlib
|
|
27
|
+
|
|
28
|
+
CRYPTO_AVAILABLE = True
|
|
29
|
+
except ImportError:
|
|
30
|
+
CRYPTO_AVAILABLE = False
|
|
31
|
+
|
|
32
|
+
import sys
|
|
33
|
+
import os
|
|
34
|
+
|
|
35
|
+
# Add CLI directory to path for imports
|
|
36
|
+
cli_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
37
|
+
if cli_dir not in sys.path:
|
|
38
|
+
sys.path.insert(0, cli_dir)
|
|
39
|
+
|
|
40
|
+
from core.settings import CONFIG_DIR
|
|
41
|
+
from setup.config import get_cluster_api_url
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class AuthManager:
|
|
45
|
+
"""Secure authentication manager for Nasiko CLI"""
|
|
46
|
+
|
|
47
|
+
SERVICE_NAME = "nasiko-cli"
|
|
48
|
+
TOKEN_KEY = "jwt_token"
|
|
49
|
+
CREDS_KEY = "user_creds"
|
|
50
|
+
|
|
51
|
+
def __init__(self, base_url: str = None, cluster_name: str = None):
|
|
52
|
+
self.config_dir = CONFIG_DIR
|
|
53
|
+
self.config_dir.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
|
|
55
|
+
# Determine Base URL
|
|
56
|
+
if base_url:
|
|
57
|
+
self.base_url = base_url.rstrip("/")
|
|
58
|
+
elif cluster_name:
|
|
59
|
+
# Look up URL for specific cluster
|
|
60
|
+
url = get_cluster_api_url(cluster_name)
|
|
61
|
+
if url:
|
|
62
|
+
self.base_url = url.rstrip("/")
|
|
63
|
+
else:
|
|
64
|
+
# Fallback to localhost if cluster unknown (shouldn't happen if validated before)
|
|
65
|
+
self.base_url = "http://localhost:8000"
|
|
66
|
+
else:
|
|
67
|
+
# Fallback 1: NASIKO_API_URL env var
|
|
68
|
+
env_url = os.getenv("NASIKO_API_URL")
|
|
69
|
+
if env_url:
|
|
70
|
+
self.base_url = env_url.rstrip("/")
|
|
71
|
+
else:
|
|
72
|
+
# Check for NASIKO_CLUSTER_NAME env var
|
|
73
|
+
default_cluster = os.environ.get("NASIKO_CLUSTER_NAME")
|
|
74
|
+
if default_cluster:
|
|
75
|
+
url = get_cluster_api_url(default_cluster)
|
|
76
|
+
if url:
|
|
77
|
+
self.base_url = url.rstrip("/")
|
|
78
|
+
else:
|
|
79
|
+
self.base_url = "http://localhost:8000"
|
|
80
|
+
else:
|
|
81
|
+
# Fallback 2: Default to localhost
|
|
82
|
+
self.base_url = "http://localhost:8000"
|
|
83
|
+
|
|
84
|
+
self.auth_url = self.base_url # Auth service is at base URL
|
|
85
|
+
|
|
86
|
+
# File-based fallback paths
|
|
87
|
+
self.token_file = self.config_dir / "token.enc"
|
|
88
|
+
self.creds_file = self.config_dir / "credentials.enc"
|
|
89
|
+
|
|
90
|
+
def _get_encryption_key(self) -> bytes:
|
|
91
|
+
"""Generate encryption key from system/user information"""
|
|
92
|
+
if not CRYPTO_AVAILABLE:
|
|
93
|
+
raise RuntimeError("Cryptography not available for secure storage")
|
|
94
|
+
|
|
95
|
+
# Use a combination of username and machine info for key derivation
|
|
96
|
+
import getpass
|
|
97
|
+
import platform
|
|
98
|
+
|
|
99
|
+
user_info = f"{getpass.getuser()}:{platform.node()}:{platform.system()}"
|
|
100
|
+
key = hashlib.pbkdf2_hmac(
|
|
101
|
+
"sha256", user_info.encode(), b"nasiko-cli-salt", 100000
|
|
102
|
+
)
|
|
103
|
+
return base64.urlsafe_b64encode(key)
|
|
104
|
+
|
|
105
|
+
def _encrypt_data(self, data: str) -> bytes:
|
|
106
|
+
"""Encrypt data using system-derived key"""
|
|
107
|
+
if not CRYPTO_AVAILABLE:
|
|
108
|
+
return data.encode()
|
|
109
|
+
|
|
110
|
+
key = self._get_encryption_key()
|
|
111
|
+
f = Fernet(key)
|
|
112
|
+
return f.encrypt(data.encode())
|
|
113
|
+
|
|
114
|
+
def _decrypt_data(self, encrypted_data: bytes) -> str:
|
|
115
|
+
"""Decrypt data using system-derived key"""
|
|
116
|
+
if not CRYPTO_AVAILABLE:
|
|
117
|
+
return encrypted_data.decode()
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
key = self._get_encryption_key()
|
|
121
|
+
f = Fernet(key)
|
|
122
|
+
return f.decrypt(encrypted_data).decode()
|
|
123
|
+
except Exception:
|
|
124
|
+
raise ValueError("Failed to decrypt data - key may have changed")
|
|
125
|
+
|
|
126
|
+
def _store_secure(self, key: str, value: str) -> bool:
|
|
127
|
+
"""Store value securely using best available method"""
|
|
128
|
+
if KEYRING_AVAILABLE:
|
|
129
|
+
try:
|
|
130
|
+
keyring.set_password(self.SERVICE_NAME, key, value)
|
|
131
|
+
return True
|
|
132
|
+
except Exception as e:
|
|
133
|
+
typer.echo(f"⚠️ Keyring storage failed: {e}")
|
|
134
|
+
|
|
135
|
+
# Fallback to encrypted file storage
|
|
136
|
+
try:
|
|
137
|
+
if key == self.TOKEN_KEY:
|
|
138
|
+
file_path = self.token_file
|
|
139
|
+
else:
|
|
140
|
+
file_path = self.creds_file
|
|
141
|
+
|
|
142
|
+
encrypted_data = self._encrypt_data(value)
|
|
143
|
+
file_path.write_bytes(encrypted_data)
|
|
144
|
+
file_path.chmod(0o600) # Read-write for owner only
|
|
145
|
+
return True
|
|
146
|
+
except Exception as e:
|
|
147
|
+
typer.echo(f"❌ Failed to store {key} securely: {e}")
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
def _retrieve_secure(self, key: str) -> Optional[str]:
|
|
151
|
+
"""Retrieve value securely using best available method"""
|
|
152
|
+
if KEYRING_AVAILABLE:
|
|
153
|
+
try:
|
|
154
|
+
value = keyring.get_password(self.SERVICE_NAME, key)
|
|
155
|
+
if value:
|
|
156
|
+
return value
|
|
157
|
+
except Exception as e:
|
|
158
|
+
typer.echo(f"⚠️ Keyring retrieval failed: {e}")
|
|
159
|
+
|
|
160
|
+
# Fallback to encrypted file storage
|
|
161
|
+
try:
|
|
162
|
+
if key == self.TOKEN_KEY:
|
|
163
|
+
file_path = self.token_file
|
|
164
|
+
else:
|
|
165
|
+
file_path = self.creds_file
|
|
166
|
+
|
|
167
|
+
if file_path.exists():
|
|
168
|
+
encrypted_data = file_path.read_bytes()
|
|
169
|
+
return self._decrypt_data(encrypted_data)
|
|
170
|
+
except Exception as e:
|
|
171
|
+
typer.echo(f"⚠️ Failed to retrieve {key}: {e}")
|
|
172
|
+
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
def _delete_secure(self, key: str) -> bool:
|
|
176
|
+
"""Delete stored value securely"""
|
|
177
|
+
success = True
|
|
178
|
+
|
|
179
|
+
if KEYRING_AVAILABLE:
|
|
180
|
+
try:
|
|
181
|
+
keyring.delete_password(self.SERVICE_NAME, key)
|
|
182
|
+
except Exception:
|
|
183
|
+
pass # May not exist
|
|
184
|
+
|
|
185
|
+
# Also remove file-based storage
|
|
186
|
+
try:
|
|
187
|
+
if key == self.TOKEN_KEY:
|
|
188
|
+
file_path = self.token_file
|
|
189
|
+
else:
|
|
190
|
+
file_path = self.creds_file
|
|
191
|
+
|
|
192
|
+
if file_path.exists():
|
|
193
|
+
file_path.unlink()
|
|
194
|
+
except Exception:
|
|
195
|
+
success = False
|
|
196
|
+
|
|
197
|
+
return success
|
|
198
|
+
|
|
199
|
+
def login(
|
|
200
|
+
self, access_key: str, access_secret: str, save_credentials: bool = True
|
|
201
|
+
) -> bool:
|
|
202
|
+
"""Login and store JWT token securely"""
|
|
203
|
+
try:
|
|
204
|
+
# Make login request
|
|
205
|
+
base_url_str = str(self.base_url) # Ensure it's a string
|
|
206
|
+
login_url = f"{self.auth_url}/auth/users/login"
|
|
207
|
+
response = requests.post(
|
|
208
|
+
login_url,
|
|
209
|
+
json={"access_key": access_key, "access_secret": access_secret},
|
|
210
|
+
timeout=30,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
if response.status_code == 200:
|
|
214
|
+
token_data = response.json()
|
|
215
|
+
jwt_token = token_data["token"]
|
|
216
|
+
|
|
217
|
+
# Store JWT token securely
|
|
218
|
+
if self._store_secure(self.TOKEN_KEY, jwt_token):
|
|
219
|
+
typer.echo("✅ Login successful! Token stored securely.")
|
|
220
|
+
|
|
221
|
+
# Optionally store credentials for auto-renewal
|
|
222
|
+
if save_credentials:
|
|
223
|
+
creds = json.dumps(
|
|
224
|
+
{"access_key": access_key, "access_secret": access_secret}
|
|
225
|
+
)
|
|
226
|
+
self._store_secure(self.CREDS_KEY, creds)
|
|
227
|
+
|
|
228
|
+
return True
|
|
229
|
+
else:
|
|
230
|
+
typer.echo("❌ Login succeeded but failed to store token securely")
|
|
231
|
+
return False
|
|
232
|
+
else:
|
|
233
|
+
error_detail = (
|
|
234
|
+
response.json().get("detail", "Unknown error")
|
|
235
|
+
if response.content
|
|
236
|
+
else "Connection failed"
|
|
237
|
+
)
|
|
238
|
+
typer.echo(f"❌ Login failed: {error_detail}")
|
|
239
|
+
return False
|
|
240
|
+
|
|
241
|
+
except requests.exceptions.RequestException as e:
|
|
242
|
+
typer.echo(f"❌ Connection error: {e}")
|
|
243
|
+
return False
|
|
244
|
+
except Exception as e:
|
|
245
|
+
typer.echo(f"❌ Login error: {e}")
|
|
246
|
+
return False
|
|
247
|
+
|
|
248
|
+
def get_auth_headers(self) -> Optional[Dict[str, str]]:
|
|
249
|
+
"""Get authentication headers for API calls"""
|
|
250
|
+
token = self._retrieve_secure(self.TOKEN_KEY)
|
|
251
|
+
if token:
|
|
252
|
+
return {"Authorization": f"Bearer {token}"}
|
|
253
|
+
return None
|
|
254
|
+
|
|
255
|
+
def is_logged_in(self) -> bool:
|
|
256
|
+
"""Check if user is logged in"""
|
|
257
|
+
return self.get_auth_headers() is not None
|
|
258
|
+
|
|
259
|
+
def logout(self, clear_credentials: bool = False) -> bool:
|
|
260
|
+
"""Logout and remove stored token"""
|
|
261
|
+
try:
|
|
262
|
+
success = self._delete_secure(self.TOKEN_KEY)
|
|
263
|
+
|
|
264
|
+
if clear_credentials:
|
|
265
|
+
self._delete_secure(self.CREDS_KEY)
|
|
266
|
+
|
|
267
|
+
if success:
|
|
268
|
+
typer.echo("✅ Logged out successfully!")
|
|
269
|
+
else:
|
|
270
|
+
typer.echo("⚠️ Logout completed (some data may not have been cleared)")
|
|
271
|
+
return True
|
|
272
|
+
except Exception as e:
|
|
273
|
+
typer.echo(f"❌ Logout error: {e}")
|
|
274
|
+
return False
|
|
275
|
+
|
|
276
|
+
def refresh_token_if_needed(self) -> bool:
|
|
277
|
+
"""Check token validity and refresh if needed"""
|
|
278
|
+
headers = self.get_auth_headers()
|
|
279
|
+
if not headers:
|
|
280
|
+
return False
|
|
281
|
+
|
|
282
|
+
try:
|
|
283
|
+
# Test token with a lightweight API call
|
|
284
|
+
base_url_str = str(self.base_url) # Ensure it's a string
|
|
285
|
+
health_url = f"{base_url_str}/api/v1/healthcheck"
|
|
286
|
+
response = requests.get(health_url, headers=headers, timeout=10)
|
|
287
|
+
|
|
288
|
+
if response.status_code == 401:
|
|
289
|
+
# Try to auto-renew with stored credentials
|
|
290
|
+
return self._auto_renew_token()
|
|
291
|
+
|
|
292
|
+
return response.status_code == 200
|
|
293
|
+
|
|
294
|
+
except Exception:
|
|
295
|
+
return self._auto_renew_token()
|
|
296
|
+
|
|
297
|
+
def _auto_renew_token(self) -> bool:
|
|
298
|
+
"""Attempt to automatically renew token using stored credentials"""
|
|
299
|
+
try:
|
|
300
|
+
creds_data = self._retrieve_secure(self.CREDS_KEY)
|
|
301
|
+
if not creds_data:
|
|
302
|
+
return False
|
|
303
|
+
|
|
304
|
+
creds = json.loads(creds_data)
|
|
305
|
+
typer.echo("🔄 Token expired, attempting auto-renewal...")
|
|
306
|
+
|
|
307
|
+
# Clear old token and re-login
|
|
308
|
+
self._delete_secure(self.TOKEN_KEY)
|
|
309
|
+
return self.login(
|
|
310
|
+
creds["access_key"], creds["access_secret"], save_credentials=False
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
except Exception:
|
|
314
|
+
typer.echo("❌ Auto-renewal failed. Please login again.")
|
|
315
|
+
return False
|
|
316
|
+
|
|
317
|
+
def get_user_info(self) -> Optional[Dict[str, Any]]:
|
|
318
|
+
"""Get current user information"""
|
|
319
|
+
headers = self.get_auth_headers()
|
|
320
|
+
if not headers:
|
|
321
|
+
return None
|
|
322
|
+
|
|
323
|
+
try:
|
|
324
|
+
user_url = f"{self.auth_url}/auth/user/"
|
|
325
|
+
response = requests.get(user_url, headers=headers, timeout=10)
|
|
326
|
+
|
|
327
|
+
if response.status_code == 200:
|
|
328
|
+
return response.json()
|
|
329
|
+
return None
|
|
330
|
+
|
|
331
|
+
except Exception:
|
|
332
|
+
return None
|
|
333
|
+
|
|
334
|
+
def clear_all_data(self) -> bool:
|
|
335
|
+
"""Clear all stored authentication data"""
|
|
336
|
+
try:
|
|
337
|
+
self._delete_secure(self.TOKEN_KEY)
|
|
338
|
+
self._delete_secure(self.CREDS_KEY)
|
|
339
|
+
|
|
340
|
+
# Also remove any legacy token files
|
|
341
|
+
legacy_files = [
|
|
342
|
+
self.config_dir / "token",
|
|
343
|
+
self.config_dir / "token.json",
|
|
344
|
+
]
|
|
345
|
+
|
|
346
|
+
for file_path in legacy_files:
|
|
347
|
+
if file_path.exists():
|
|
348
|
+
file_path.unlink()
|
|
349
|
+
|
|
350
|
+
typer.echo("✅ All authentication data cleared!")
|
|
351
|
+
return True
|
|
352
|
+
|
|
353
|
+
except Exception as e:
|
|
354
|
+
typer.echo(f"❌ Error clearing data: {e}")
|
|
355
|
+
return False
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
# Global auth manager instances cache
|
|
359
|
+
_auth_managers: Dict[str, AuthManager] = {}
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def get_auth_manager(base_url: str = None, cluster_name: str = None) -> AuthManager:
|
|
363
|
+
"""
|
|
364
|
+
Get an auth manager instance, creating one if needed.
|
|
365
|
+
Instances are cached by cluster_name to ensure singleton behavior per cluster.
|
|
366
|
+
"""
|
|
367
|
+
global _auth_managers
|
|
368
|
+
|
|
369
|
+
# Determine cache key
|
|
370
|
+
if cluster_name:
|
|
371
|
+
key = cluster_name
|
|
372
|
+
elif base_url:
|
|
373
|
+
key = base_url
|
|
374
|
+
else:
|
|
375
|
+
# Check env var for default cluster
|
|
376
|
+
key = os.environ.get("NASIKO_CLUSTER_NAME", "default")
|
|
377
|
+
|
|
378
|
+
if key not in _auth_managers:
|
|
379
|
+
_auth_managers[key] = AuthManager(base_url=base_url, cluster_name=cluster_name)
|
|
380
|
+
|
|
381
|
+
return _auth_managers[key]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Commands module for Nasiko CLI
|