Skip to content

Commit

Permalink
feat: support SSO (#86)
Browse files Browse the repository at this point in the history
* feat: support plugins

* fix: add test plugin

* fix: ServiceRegister -> register_service

* doc: plugin

* feat: auth

* Update add.py

* fix: login

* feat: oauth

* fix: oauth

* Update oauth.py

* fix: auth login

* feat: logout

* Update configure.py

* fix: py3 compatibility

* Update login.py

* Update login.py

* Update login.py
  • Loading branch information
sesky4 authored Sep 12, 2024
1 parent be2ffff commit ee989f5
Show file tree
Hide file tree
Showing 13 changed files with 652 additions and 12 deletions.
8 changes: 7 additions & 1 deletion tccli/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import tccli.services as Services
import tccli.options_define as Options_define
from collections import OrderedDict

from tccli import oauth
from tccli.utils import Utils
from tccli.argument import CLIArgument, CustomArgument, ListArgument, BooleanArgument
from tccli.exceptions import UnknownArgumentError
Expand Down Expand Up @@ -176,12 +178,15 @@ def _build_command_map(self):
service_model = self._get_service_model()
for action in service_model["actions"]:
action_model = service_model["actions"][action]
action_caller = action_model.get("action_caller", None)
if not action_caller:
action_caller = Services.action_caller(self._service_name)()[action]
command_map[action] = ActionCommand(
service_name=self._service_name,
version=self._version,
action_name=action,
action_model=action_model,
action_caller=Services.action_caller(self._service_name)()[action],
action_caller=action_caller,
)
return command_map

Expand Down Expand Up @@ -286,6 +291,7 @@ def __call__(self, args, parsed_globals):
action_parameters = self.cli_unfold_argument.build_action_parameters(parsed_args)
else:
action_parameters = self._build_action_parameters(parsed_args, self.argument_map)
oauth.maybe_refresh_credential(parsed_globals.profile if parsed_globals.profile else "default")
return self._action_caller(action_parameters, vars(parsed_globals))

def create_help_command(self):
Expand Down
14 changes: 11 additions & 3 deletions tccli/configure.py
Original file line number Diff line number Diff line change
Expand Up @@ -416,16 +416,24 @@ def _run_main(self, parsed_args, parsed_globals):

def init_configures(self):
config = {}
if not self._profile_existed("default.configure")[0]:
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--profile", type=str)
args, _ = parser.parse_known_args()
profile = args.profile or "default"
profile_file = "%s.configure" % profile

if not self._profile_existed(profile_file)[0]:
config = {
"region": "ap-guangzhou",
"output": "json",
"arrayCount": 10,
"warning": "off"
}
self._init_configure("default.configure", config)
self._init_configure(profile_file, config)

for profile_name in os.listdir(self.cli_path):
if profile_name == "default.configure":
if profile_name == profile_file:
continue
if profile_name.endswith(".configure"):
self._init_configure(profile_name, {})
Expand Down
51 changes: 43 additions & 8 deletions tccli/loaders.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from tccli import __version__
from tccli.services import SERVICE_VERSIONS
from collections import OrderedDict
import tccli.plugin as plugin

BASE_TYPE = ["int64", "uint64", "string", "float", "bool", "date", "datetime", "datetime_iso", "binary"]
CLI_BASE_TYPE = ["Integer", "String", "Float", "Timestamp", "Boolean", "Binary"]
Expand Down Expand Up @@ -175,7 +176,15 @@ def _version_transform(self, version):
return version[1:5] + "-" + version[5:7] + "-" + version[7:9]

def get_available_services(self):
return SERVICE_VERSIONS
services = copy.deepcopy(SERVICE_VERSIONS)
for name, vers in plugin.import_plugins().items():
if name not in services:
services[name] = []
for ver, spec in vers.items():
api_ver = spec["metadata"]["apiVersion"]
if api_ver not in services[name]:
services[name].append(api_ver)
return services

def get_service_default_version(self, service):
args = sys.argv[1:]
Expand All @@ -194,15 +203,41 @@ def get_service_model(self, service, version):
services_path = self.get_services_path()
version = "v" + version.replace('-', '')
apis_path = os.path.join(services_path, service, version, "api.json")
if not os.path.exists(apis_path):
model = {
"metadata": {},
"actions": {},
"objects": {},
}
if os.path.exists(apis_path):
if six.PY2:
with open(apis_path, 'r') as f:
model = json.load(f)
else:
with open(apis_path, 'r', encoding='utf-8') as f:
model = json.load(f)

# merge plugins
for plugin_name, vers in plugin.import_plugins().items():

if plugin_name != service:
continue

for ver, spec in vers.items():

# 2017-03-12 -> v20170312
compact_ver = 'v' + ver.replace('-', '')

if compact_ver != version:
continue

model["metadata"].update(spec["metadata"])
model["actions"].update(spec["actions"])
model["objects"].update(spec["objects"])

if not model:
raise Exception("Not find service:%s version:%s model" % (service, version))

if six.PY2:
with open(apis_path, 'r') as f:
return json.load(f)
else:
with open(apis_path, 'r', encoding='utf-8') as f:
return json.load(f)
return model

def get_service_description(self, service, version):
service_model = self.get_service_model(service, version)
Expand Down
115 changes: 115 additions & 0 deletions tccli/oauth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import json
import os
import time

import requests
import uuid

_API_ENDPOINT = "https://cli.cloud.tencent.com"
_CRED_REFRESH_SAFE_DUR = 60 * 5
_ACCESS_REFRESH_SAFE_DUR = 60 * 5


def maybe_refresh_credential(profile):
cred_path = cred_path_of_profile(profile)
try:
with open(cred_path, "r") as cred_file:
cred = json.load(cred_file)
except IOError:
# file not found, don't check
return

if cred.get("type") != "oauth":
return

try:
now = time.time()

expires_at = cred["expiresAt"]
if expires_at - now > _CRED_REFRESH_SAFE_DUR:
return

token_info = cred["oauth"]
site = token_info["site"]
access_expires = token_info["expiresAt"]
if access_expires - now < _ACCESS_REFRESH_SAFE_DUR:
refresh_token = token_info["refreshToken"]
open_id = token_info["openId"]
new_token = refresh_user_token(refresh_token, open_id, site)
token_info.update(new_token)

access_token = token_info["accessToken"]
new_cred = get_temp_cred(access_token, site)
save_credential(token_info, new_cred, profile)

except KeyError as e:
print("failed to refresh credential, your credential file(%s) is corrupted, %s" % (cred_path, e))

except Exception as e:
print("failed to refresh credential, %s" % e)


def refresh_user_token(ref_token, open_id, site):
api_endpoint = _API_ENDPOINT + "/refresh_user_token"
body = {
"TraceId": str(uuid.uuid4()),
"RefreshToken": ref_token,
"OpenId": open_id,
"Site": site,
}
http_response = requests.post(api_endpoint, json=body, verify=False)
resp = http_response.json()

if "Error" in resp:
raise ValueError("refresh_user_token: %s" % json.dumps(resp))

return {
"accessToken": resp["AccessToken"],
"expiresAt": resp["ExpiresAt"],
}


def get_temp_cred(access_token, site):
api_endpoint = _API_ENDPOINT + "/get_temp_cred"
body = {
"TraceId": str(uuid.uuid4()),
"AccessToken": access_token,
"Site": site,
}
http_response = requests.post(api_endpoint, json=body, verify=False)
resp = http_response.json()

if "Error" in resp:
raise ValueError("get_temp_key: %s" % json.dumps(resp))

return {
"secretId": resp["SecretId"],
"secretKey": resp["SecretKey"],
"token": resp["Token"],
"expiresAt": resp["ExpiresAt"],
}


def cred_path_of_profile(profile):
return os.path.join(os.path.expanduser("~"), ".tccli", profile + ".credential")


def save_credential(token, new_cred, profile):
cred_path = cred_path_of_profile(profile)

cred = {
"type": "oauth",
"secretId": new_cred["secretId"],
"secretKey": new_cred["secretKey"],
"token": new_cred["token"],
"expiresAt": new_cred["expiresAt"],
"oauth": {
"openId": token["openId"],
"accessToken": token["accessToken"],
"expiresAt": token["expiresAt"],
"refreshToken": token["refreshToken"],
"site": token["site"],
},
}
with open(cred_path, "w") as cred_file:
json.dump(cred, cred_file, indent=4)
30 changes: 30 additions & 0 deletions tccli/plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import importlib
import logging
import pkgutil

import tccli.plugins as plugins

_plugins = {}
_imported = False


def import_plugins():
global _imported

if not _imported:
_reimport_plugins()
_imported = True

return _plugins


def _reimport_plugins():
for _, name, _ in pkgutil.iter_modules(plugins.__path__, plugins.__name__ + "."):
mod = importlib.import_module(name)
register_service = getattr(mod, "register_service", None)
if not register_service:
logging.warning("invalid module %s" % name)
continue
register_service(_plugins)

return _plugins
Empty file added tccli/plugins/__init__.py
Empty file.
59 changes: 59 additions & 0 deletions tccli/plugins/auth/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# encoding: utf-8
from tccli.plugins.auth.login import login_command_entrypoint
from tccli.plugins.auth.logout import logout_command_entrypoint

service_name = "auth"
service_version = "2024-08-20"

_spec = {
"metadata": {
"serviceShortName": service_name,
"apiVersion": service_version,
"description": "auth related commands",
},
"actions": {
"login": {
"name": "登陆",
"document": "login through sso",
"input": "loginRequest",
"output": "loginResponse",
"action_caller": login_command_entrypoint,
},
"logout": {
"name": "登出",
"document": "remove local credential file",
"input": "logoutRequest",
"output": "logoutResponse",
"action_caller": logout_command_entrypoint,
},
},
"objects": {
"loginRequest": {
"members": [
{
"name": "browser",
"member": "string",
"type": "string",
"required": False,
"document": "use browser=no to indicate no browser login mode",
},
],
},
"loginResponse": {
"members": [],
},
"logoutRequest": {
"members": [],
},
"logoutResponse": {
"members": [],
},
},
"version": "1.0",
}


def register_service(specs):
specs[service_name] = {
service_version: _spec,
}
Loading

0 comments on commit ee989f5

Please sign in to comment.