Skip to content

Commit

Permalink
user CRUD with keycloak
Browse files Browse the repository at this point in the history
Allow an admin to create, update and delete users in keycloak and gerrit
via a single command in the sfmanager CLI.

closes: TG-3545

Change-Id: I457b03f0316770021f65b62fe9bc2855875b1f42
  • Loading branch information
mhuin committed May 12, 2020
1 parent d17da90 commit 95c8403
Show file tree
Hide file tree
Showing 3 changed files with 203 additions and 30 deletions.
36 changes: 24 additions & 12 deletions sfmanager/sfauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,25 +34,33 @@ class IntrospectionNotAvailableError(Exception):
pass


def get_jwt(remote_gateway, username, password):
if 'keycloak' not in remote_gateway:
# assumption, might backfire
if not remote_gateway.startswith('https://'):
wk_root = 'https://' + remote_gateway
def get_jwt(remote_gateway, username, password, verify=True):
if (not remote_gateway.startswith('https://') and
not remote_gateway.startswith('http://')):
wk_root = 'https://' + remote_gateway
else:
wk_root = remote_gateway
wk_url = "%s/auth/realms/sf/.well-known/openid-configuration"
wk = requests.get(wk_url % wk_root, verify=True).json()
# TODO should be selectable
if username == "admin":
realm = "master"
client_id = "admin-cli"
else:
realm = "sf"
client_id = "managesf"
wk_url = ("%s/auth/realms/" +
realm +
"/.well-known/openid-configuration")
wk = requests.get(wk_url % wk_root, verify=verify).json()
token_endpoint = wk.get('token_endpoint')
if token_endpoint is None:
raise Exception('No Token Endpoint defined at %s' % (wk_url % wk_root))
data = {
'username': username,
'password': password,
'grant_type': 'password',
'client_id': 'managesf',
'client_id': client_id,
}
token_request = requests.post(token_endpoint, data, verify=True)
token_request = requests.post(token_endpoint, data=data, verify=verify)
if (int(token_request.status_code) >= 400 and
int(token_request.status_code) < 500):
raise Exception('Incorrect username/password combination')
Expand Down Expand Up @@ -128,7 +136,11 @@ def get_cauth_info(auth_server, verify=True):


def get_managesf_info(server, verify=True):
url = "%s/about/" % server
if not server.endswith('manage'):
_server = "%s/manage" % server
else:
_server = server
url = "%s/about/" % _server
return _get_service_info(url, verify)


Expand All @@ -139,7 +151,7 @@ def get_auth_params(server,
api_key=None,
use_ssl=True,
verify=True):
services = get_managesf_info(server)['service']['services']
services = get_managesf_info(server, verify)['service']['services']
params = {'cookies': None,
'headers': None}
if 'keycloak' in services:
Expand All @@ -150,7 +162,7 @@ def get_auth_params(server,
}
}
else:
extras = get_jwt(server, username, password)
extras = get_jwt(server, username, password, verify)
else:
cookie = get_cookie(server, username, password,
github_access_token, api_key, use_ssl,
Expand Down
195 changes: 178 additions & 17 deletions sfmanager/sfmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
import git
import requests
import sqlite3
import shlex
import subprocess
import sys
import time
try:
Expand Down Expand Up @@ -191,15 +193,11 @@ def load_rc_file(args):
"no rc file found" % args.env)


def fail_if_keycloak(func):
def fail_if_keycloak(args):
"""Actions that cannot be run without cauth."""
def wrapper_func(args, base_url):
services = sfauth.get_managesf_info(args.url)['service']['services']
if 'keycloak' in services:
die("This action is only available through the cauth service")
else:
return func(args, base_url)
return wrapper_func
services = sfauth.get_managesf_info(args.url)['service']['services']
if 'keycloak' in services:
die("This action is only available through the cauth service")


def default_arguments(parser):
Expand Down Expand Up @@ -232,6 +230,9 @@ def default_arguments(parser):
parser.add_argument('--debug', default=False, action='store_true',
help='enable debug messages in console, '
'disabled by default')
parser.add_argument('--gerrit-admin-key',
default="/root/.ssh/gerrit_admin",
help='path to gerrit admin ssh private key')


def user_management_command(parser):
Expand All @@ -243,7 +244,7 @@ def user_management_command(parser):
cump.add_argument('--username', '-u', nargs='?', metavar='username',
required=True, help='A unique username/login')
cump.add_argument('--password', '-p', nargs='?', metavar='password',
required=True,
required=False,
help='The user password, can be provided interactively'
' if this option is empty')
cump.add_argument('--email', '-e', nargs='?', metavar='email',
Expand Down Expand Up @@ -285,6 +286,11 @@ def sf_user_management_command(parser):
required=True, help="The user's full name")
create.add_argument('--email', '-e', nargs='?', metavar='email',
required=True, help="The user's email")
create.add_argument('--ssh-key', '-s', nargs='?',
metavar='/path/to/pubkey',
required=False, help="The user's ssh public key file")
create.add_argument('--password', '-p', nargs='?', metavar='password',
required=False, help="the user's password")
sfu_sub.add_parser('list', help='list all registered users')
delete = sfu_sub.add_parser('delete', help='de-register a user from SF')
delete.add_argument('--username', '-u', nargs='?', metavar='username',
Expand Down Expand Up @@ -440,7 +446,6 @@ def build_url(*args):
return '/'.join(s.strip('/') for s in args) + '/'


@fail_if_keycloak
def apikey_action(args, base_url):
url = base_url + '/apikey'
if args.command != 'apikey':
Expand All @@ -449,6 +454,8 @@ def apikey_action(args, base_url):
if args.subcommand not in ['create', 'delete', 'get']:
return False

fail_if_keycloak(args)

if args.subcommand == 'get':
resp = request('get', url)
return response(resp)
Expand Down Expand Up @@ -559,12 +566,119 @@ def github_action(args, base_url):
return False


@fail_if_keycloak
def user_management_action(args, base_url):
if args.command != 'user':
return False
if args.subcommand not in ['create', 'update', 'delete']:
return False
services = sfauth.get_managesf_info(
args.url,
not args.insecure)['service']['services']
if 'keycloak' in services:
return keycloak_user_management_action(args, base_url)
else:
return cauth_user_management_action(args, base_url)


def keycloak_user_management_action(args, base_url):
base = args.url.rstrip('/')
url = base + '/auth/admin/realms/sf/users'
if args.subcommand == 'create':
password = None
if args.password is None:
if not getattr(args, 'email'):
die("email required if no password is provided.")
print("An email will be sent to %s "
"to set the user's password." % args.email)
elif args.password:
password = args.password
userInfo = {"username": args.username,
"enabled": True}
if getattr(args, 'email'):
userInfo['email'] = args.email
if getattr(args, 'fullname'):
userInfo['firstName'] = args.fullname[0]
if len(args.fullname) > 1:
userInfo['lastName'] = args.fullname[1]
if password is not None:
userInfo['credentials'] = [{"type": "password",
"value": password}]
resp = request('post', url, json=userInfo)
if resp.ok:
if getattr(args, 'ssh_key') or (password is None):
kc_user = request('get',
url + ('?username=%s' % args.username))
userInfo = kc_user.json()[0]
if getattr(args, 'ssh_key'):
with open(args.ssh_key, 'r') as f:
userInfo['attributes'] = {"publicKey": [f.read()]}
resp = request('put', url + "/" + userInfo["id"],
json=userInfo)
# send an email to the user to complete registration
if password is None:
action_url = url + "/%s/execute-actions-email" % userInfo['id']
action_resp = request('put', action_url,
json=["UPDATE_PASSWORD",
"UPDATE_PROFILE"])
if not action_resp.ok:
print(action_resp.text)
print("Provisioning services ...")
else:
die('Could not create user "%s": "%s"' % (args.username,
resp.text))
elif args.subcommand == 'delete':
resp = request('get',
url + ('?username=%s' % args.username))
if resp.ok:
kc_users = resp.json()
if len(kc_users) != 1:
die('%i user(s) found as username "%s"' % (len(kc_users),
args.username))
user_id = kc_users[0]['id']
del_resp = request('delete', url + "/" + user_id)
if del_resp.ok:
print("Deleting in services ...")
else:
die('Error deleting user "%s": %s' % (args.username,
del_resp.text))
else:
die("Error during user lookup: %s" % resp.text)
else:
resp = request('get',
url + ('?username=%s' % args.username))
if resp.ok:
kc_users = resp.json()
if len(kc_users) != 1:
die('%i user(s) found as username "%s"' % (len(kc_users),
args.username))
userInfo = kc_users[0]
user_id = userInfo['id']
if getattr(args, 'email'):
userInfo['email'] = args.email
if getattr(args, 'fullname'):
userInfo['firstName'] = args.fullname[0]
if len(args.fullname) > 1:
userInfo['lastName'] = args.fullname[1]
password = getattr(args, 'password')
if password is not None:
userInfo['credentials'] = [{"type": "password",
"value": password}]
if getattr(args, 'ssh_key'):
with open(args.ssh_key, 'r') as f:
userInfo['attributes'] = {"publicKey": [f.read()]}
update_resp = request('put', url + "/" + user_id, json=userInfo)
if update_resp.ok:
print("Updating in services ...")
else:
die('Updating user "%s" failed: %s' % (args.username,
update_resp.text))
else:
die("Error during user lookup: %s" % resp.text)
keycloak_services_users_management_action(args, base_url)
return response(resp)


def cauth_user_management_action(args, base_url):
url = build_url(base_url, 'user', args.username)
if args.subcommand in ['create', 'update']:
password = None
Expand Down Expand Up @@ -641,12 +755,7 @@ def project_action(args, base_url):
return True


@fail_if_keycloak
def services_users_management_action(args, base_url):
if args.command != 'sf_user':
return False
if args.subcommand not in ['create', 'list', 'delete']:
return False
def cauth_services_users_management_action(args, base_url):
url = build_url(base_url, 'services_users')
if args.subcommand in ['create', 'delete']:
info = {}
Expand Down Expand Up @@ -676,6 +785,58 @@ def services_users_management_action(args, base_url):
return response(resp)


def keycloak_services_users_management_action(args, base_url):
print("Handling user in gerrit...")
cmd = "ssh admin@gerrit -p 29418 -i %s gerrit " % args.gerrit_admin_key
if args.subcommand in ['delete', 'update']:
cmd += "set-account "
if args.subcommand == 'delete':
# you can't really delete a user in gerrit
cmd += "--inactive "
else:
cmd += "create-account "
if args.subcommand in ['create', 'update']:
if getattr(args, 'fullname'):
cmd += " --full-name \"'%s'\"" % ' '.join(args.fullname)
if getattr(args, 'email'):
if args.subcommand == 'update':
cmd += " --add-email %s" % args.email
else:
cmd += " --email %s" % args.email
if getattr(args, 'password'):
cmd += " --http-password %s" % args.password
if getattr(args, 'username'):
cmd += " %s" % args.username
try:
env = os.environ.copy()
env['LC_ALL'] = 'en_US.UTF-8'
output = subprocess.check_output(
shlex.split(cmd), stderr=subprocess.STDOUT,
env=env).decode('utf-8')
except subprocess.CalledProcessError as err:
if err.output:
die('Command "%s" failed with the following '
'error message: "%s"' % (cmd, err.output))
else:
die("Error with user management in gerrit: %s" % err)
print(output)
return True


def services_users_management_action(args, base_url):
if args.command != 'sf_user':
return False
if args.subcommand not in ['create', 'list', 'delete']:
return False
services = sfauth.get_managesf_info(
args.url,
not args.insecure)['service']['services']
if 'keycloak' in services:
return keycloak_services_users_management_action(args, base_url)
else:
return cauth_services_users_management_action(args, base_url)


def main():
parser = argparse.ArgumentParser(
description="Software Factory CLI")
Expand Down
2 changes: 1 addition & 1 deletion sfmanager/tests/test_sfauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ def test_get_managesf_info(self):
'version': 'x.y.z',
'services': ['gerrit', ]}})
i = sfauth.get_managesf_info('https://auth.tests.dom')
g.assert_called_with('https://auth.tests.dom/about/',
g.assert_called_with('https://auth.tests.dom/manage/about/',
allow_redirects=False,
verify=True)
self.assertEqual('managesf',
Expand Down

0 comments on commit 95c8403

Please sign in to comment.