Skip to content

Commit dddf915

Browse files
authored
v0.16.22 (#122)
api.py: * enabled namespace, name, and snapshot ID filtering for method repository listings * new command whoami returns FireCloud user id fiss.py: * updated listing calls (method_list, config_list) to enable new filters * calls to listings updated to use filters when appropriate * new command set_config_acl for setting ACLs on configs in the methods repo * added capability to set method and config ACLs for groups * meth_list and meth_exists commands updated to use argument method instead of name to be consistent with the rest of the CLI. * config_list updated to work on methods repo when user has a default workspace defined in their ~/.fissconfig setup.py: * google-auth updated to use current release __init__.py: * Added warning filter to silence application default warning Makefile: * publish updated to use twine
1 parent 590627c commit dddf915

9 files changed

+175
-61
lines changed

Makefile

+4-3
Original file line numberDiff line numberDiff line change
@@ -78,14 +78,15 @@ reinstall:
7878
uninstall:
7979
$(PIP) uninstall -y firecloud
8080

81-
publish:
82-
$(PYTHON) setup.py sdist upload && \
81+
publish: clean
82+
$(PYTHON) setup.py sdist && \
83+
twine upload dist/* && \
8384
rm -rf build dist *.egg-info
8485

8586
image:
8687
docker build -t broadgdac/fiss .
8788

8889
clean:
89-
rm -rf build dist *.egg-info *~ */*~ *.pyc */*.pyc
90+
rm -rf build dist .eggs *.egg-info *~ */*~ *.pyc */*.pyc
9091

9192
.PHONY: help test test_cli test_one install release publish clean lintify

changelog.txt

+7
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@ Change Log for FISSFC: the (Fi)recloud (S)ervice (S)elector
33
=======================================================================
44
Terms used below: HL = high level interface, LL = low level interface
55

6+
v0.16.22 - LL: enabled namespace, name, and snapshot ID filtering for
7+
repository listings, new command whoami returns user id; HL: updated
8+
listing calls to enable new filters, added capability to set method
9+
ACLs for groups, new command set_config_acl for setting ACLs on
10+
configs in the methods repo; google-auth updated to use current
11+
release.
12+
613
v0.16.21 - api.py will attempt to update credentials on a refresh error; typo
714
fixes to several HL command descriptions; setup.py now requires a
815
minimum version of setuptools (40.3.0); added Groups API to LL.

firecloud/__about__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
# Package version
2-
__version__ = "0.16.21"
2+
__version__ = "0.16.22"

firecloud/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import os
2+
import warnings
3+
warnings.filterwarnings('ignore', 'Your application has authenticated using end user credentials from Google Cloud SDK. We recommend that most server applications use service accounts instead. If your application continues to use end user credentials from Cloud SDK, you might receive a "quota exceeded" or "API not enabled" error. For more information about service accounts, see https://cloud.google.com/docs/authentication/')
4+
25

36
# Based on https://stackoverflow.com/a/379535
47
def which(program):

firecloud/api.py

+45-20
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020

2121
import google.auth
2222
from google.auth.exceptions import DefaultCredentialsError, RefreshError
23-
from google.auth.transport.requests import AuthorizedSession
23+
from google.auth.transport.requests import AuthorizedSession, Request
24+
from google.oauth2 import id_token
2425

2526
from firecloud.errors import FireCloudServerError
2627
from firecloud.fccore import __fcconfig as fcconfig
@@ -31,27 +32,26 @@
3132

3233
# Set Global Authorized Session
3334
__SESSION = None
35+
__USER_ID = None
3436

3537
# Suppress warnings about project ID
3638
logging.getLogger('google.auth').setLevel(logging.ERROR)
3739

3840
#################################################
3941
# Utilities
4042
#################################################
41-
def _fiss_agent_header(headers=None):
42-
""" Return request headers for fiss.
43-
Inserts FISS as the User-Agent.
44-
Initializes __SESSION if it hasn't been set.
45-
46-
Args:
47-
headers (dict): Include additional headers as key-value pairs
48-
49-
"""
43+
def _set_session():
44+
""" Sets global __SESSION and __USER_ID if they haven't been set """
5045
global __SESSION
46+
global __USER_ID
47+
5148
if __SESSION is None:
5249
try:
5350
__SESSION = AuthorizedSession(google.auth.default(['https://www.googleapis.com/auth/userinfo.profile',
5451
'https://www.googleapis.com/auth/userinfo.email'])[0])
52+
health()
53+
__USER_ID = id_token.verify_oauth2_token(__SESSION.credentials.id_token,
54+
Request(session=__SESSION))['email']
5555
except (DefaultCredentialsError, RefreshError) as gae:
5656
if os.getenv('SERVER_SOFTWARE', '').startswith('Google App Engine/'):
5757
raise
@@ -70,6 +70,17 @@ def _fiss_agent_header(headers=None):
7070
cpe.returncode)
7171
raise gae
7272

73+
def _fiss_agent_header(headers=None):
74+
""" Return request headers for fiss.
75+
Inserts FISS as the User-Agent.
76+
Initializes __SESSION if it hasn't been set.
77+
78+
Args:
79+
headers (dict): Include additional headers as key-value pairs
80+
81+
"""
82+
_set_session()
83+
7384
fiss_headers = {"User-Agent" : FISS_USER_AGENT}
7485
if headers is not None:
7586
fiss_headers.update(headers)
@@ -130,6 +141,11 @@ def _check_response_code(response, codes):
130141
if response.status_code not in codes:
131142
raise FireCloudServerError(response.status_code, response.content)
132143

144+
def whoami():
145+
""" Return __USER_ID """
146+
_set_session()
147+
return __USER_ID
148+
133149
##############################################################
134150
# 1. Orchestration API calls, see https://api.firecloud.org/
135151
##############################################################
@@ -465,7 +481,7 @@ def update_entity(namespace, workspace, etype, ename, updates):
465481
### 1.2 Method Configurations
466482
###############################
467483

468-
def list_workspace_configs(namespace, workspace):
484+
def list_workspace_configs(namespace, workspace, allRepos=False):
469485
"""List method configurations in workspace.
470486
471487
Args:
@@ -477,7 +493,7 @@ def list_workspace_configs(namespace, workspace):
477493
DUPLICATE: https://api.firecloud.org/#!/Workspaces/listWorkspaceMethodConfigs
478494
"""
479495
uri = "workspaces/{0}/{1}/methodconfigs".format(namespace, workspace)
480-
return __get(uri)
496+
return __get(uri, params={'allRepos': allRepos})
481497

482498
def create_workspace_config(namespace, workspace, body):
483499
"""Create method configuration in workspace.
@@ -678,32 +694,41 @@ def copy_config_to_repo(namespace, workspace, from_cnamespace,
678694
### 1.3 Method Repository
679695
###########################
680696

681-
def list_repository_methods(name=None):
682-
"""List methods in the methods repository.
697+
def list_repository_methods(namespace=None, name=None, snapshotId=None):
698+
"""List method(s) in the methods repository.
699+
700+
Args:
701+
namespace (str): Method Repository namespace
702+
name (str): method name
703+
snapshotId (int): method snapshot ID
683704
684705
Swagger:
685706
https://api.firecloud.org/#!/Method_Repository/listMethodRepositoryMethods
686707
"""
687-
params = dict()
688-
if name:
689-
params['name'] = name
708+
params = {k:v for (k,v) in locals().items() if v is not None}
690709
return __get("methods", params=params)
691710

692-
def list_repository_configs():
711+
def list_repository_configs(namespace=None, name=None, snapshotId=None):
693712
"""List configurations in the methods repository.
694713
714+
Args:
715+
namespace (str): Method Repository namespace
716+
name (str): config name
717+
snapshotId (int): config snapshot ID
718+
695719
Swagger:
696720
https://api.firecloud.org/#!/Method_Repository/listMethodRepositoryConfigurations
697721
"""
698-
return __get("configurations")
722+
params = {k:v for (k,v) in locals().items() if v is not None}
723+
return __get("configurations", params=params)
699724

700725
def get_config_template(namespace, method, version):
701726
"""Get the configuration template for a method.
702727
703728
The method should exist in the methods repository.
704729
705730
Args:
706-
namespace (str): Methods namespace
731+
namespace (str): Method's namespace
707732
method (str): method name
708733
version (int): snapshot_id of the method
709734

firecloud/fiss.py

+104-25
Original file line numberDiff line numberDiff line change
@@ -438,32 +438,72 @@ def meth_acl(args):
438438
@fiss_cmd
439439
def meth_set_acl(args):
440440
""" Assign an ACL role to a list of users for a workflow. """
441-
acl_updates = [{"user": user, "role": args.role} for user in args.users]
441+
acl_updates = [{"user": user, "role": args.role} \
442+
for user in set(expand_fc_groups(args.users)) \
443+
if user != fapi.whoami()]
442444

443445
id = args.snapshot_id
444446
if not id:
445447
# get the latest snapshot_id for this method from the methods repo
446-
r = fapi.list_repository_methods()
448+
r = fapi.list_repository_methods(namespace=args.namespace,
449+
name=args.method)
447450
fapi._check_response_code(r, 200)
448-
versions = [m for m in r.json()
449-
if m['name'] == args.method and m['namespace'] == args.namespace]
451+
versions = r.json()
450452
if len(versions) == 0:
451453
if fcconfig.verbosity:
452-
eprint("method {0}/{1} not found".format(args.namespace, args.method))
454+
eprint("method {0}/{1} not found".format(args.namespace,
455+
args.method))
453456
return 1
454457
latest = sorted(versions, key=lambda m: m['snapshotId'])[-1]
455458
id = latest['snapshotId']
456459

457-
r = fapi.update_repository_method_acl(args.namespace, args.method, id, acl_updates)
460+
r = fapi.update_repository_method_acl(args.namespace, args.method, id,
461+
acl_updates)
458462
fapi._check_response_code(r, 200)
459463
if fcconfig.verbosity:
460-
print("Updated ACL for {0}/{1}:{2}".format(args.namespace, args.method, id))
464+
print("Updated ACL for {0}/{1}:{2}".format(args.namespace, args.method,
465+
id))
461466
return 0
462467

468+
def expand_fc_groups(users):
469+
""" If user is a firecloud group, return all members of the group.
470+
Caveat is that only group admins may do this.
471+
"""
472+
groups = None
473+
for user in users:
474+
fcgroup = None
475+
if '@' not in user:
476+
fcgroup = user
477+
elif user.lower().endswith('@firecloud.org'):
478+
if groups is None:
479+
r = fapi.get_groups()
480+
fapi._check_response_code(r, 200)
481+
groups = {group['groupEmail'].lower():group['groupName'] \
482+
for group in r.json() if group['role'] == 'Admin'}
483+
if user.lower() not in groups:
484+
if fcconfig.verbosity:
485+
eprint("You do not have access to the members of {}".format(user))
486+
yield user
487+
continue
488+
else:
489+
fcgroup = groups[user.lower()]
490+
else:
491+
yield user
492+
continue
493+
r = fapi.get_group(fcgroup)
494+
fapi._check_response_code(r, 200)
495+
fcgroup_data = r.json()
496+
for admin in fcgroup_data['adminsEmails']:
497+
yield admin
498+
for member in fcgroup_data['membersEmails']:
499+
yield member
500+
463501
@fiss_cmd
464502
def meth_list(args):
465503
""" List workflows in the methods repository """
466-
r = fapi.list_repository_methods(name=args.name)
504+
r = fapi.list_repository_methods(namespace=args.namespace,
505+
name=args.method,
506+
snapshotId=args.snapshot_id)
467507
fapi._check_response_code(r, 200)
468508

469509
# Parse the JSON for the workspace + namespace
@@ -481,6 +521,8 @@ def meth_list(args):
481521
@fiss_cmd
482522
def meth_exists(args):
483523
'''Determine whether a given workflow is present in methods repo'''
524+
args.namespace = None
525+
args.snapshot_id = None
484526
return len(meth_list(args)) != 0
485527

486528
@fiss_cmd
@@ -519,7 +561,7 @@ def config_stop(args):
519561

520562
@fiss_cmd
521563
def config_list(args):
522-
""" List configurations in the methods repository or a workspace. """
564+
""" List configuration(s) in the methods repository or a workspace. """
523565
verbose = fcconfig.verbosity
524566
if args.workspace:
525567
if verbose:
@@ -532,7 +574,9 @@ def config_list(args):
532574
else:
533575
if verbose:
534576
print("Retrieving method configs from method repository")
535-
r = fapi.list_repository_configs()
577+
r = fapi.list_repository_configs(namespace=args.namespace,
578+
name=args.config,
579+
snapshotId=args.snapshot_id)
536580
fapi._check_response_code(r, 200)
537581

538582
# Parse the JSON for the workspace + namespace
@@ -563,6 +607,36 @@ def config_acl(args):
563607
acls = sorted(r.json(), key=lambda k: k['user'])
564608
return map(lambda acl: '{0}\t{1}'.format(acl['user'], acl['role']), acls)
565609

610+
@fiss_cmd
611+
def config_set_acl(args):
612+
""" Assign an ACL role to a list of users for a config. """
613+
acl_updates = [{"user": user, "role": args.role} \
614+
for user in set(expand_fc_groups(args.users)) \
615+
if user != fapi.whoami()]
616+
617+
id = args.snapshot_id
618+
if not id:
619+
# get the latest snapshot_id for this method from the methods repo
620+
r = fapi.list_repository_configs(namespace=args.namespace,
621+
name=args.config)
622+
fapi._check_response_code(r, 200)
623+
versions = r.json()
624+
if len(versions) == 0:
625+
if fcconfig.verbosity:
626+
eprint("Configuration {0}/{1} not found".format(args.namespace,
627+
args.config))
628+
return 1
629+
latest = sorted(versions, key=lambda c: c['snapshotId'])[-1]
630+
id = latest['snapshotId']
631+
632+
r = fapi.update_repository_config_acl(args.namespace, args.config, id,
633+
acl_updates)
634+
fapi._check_response_code(r, 200)
635+
if fcconfig.verbosity:
636+
print("Updated ACL for {0}/{1}:{2}".format(args.namespace, args.config,
637+
id))
638+
return 0
639+
566640
@fiss_cmd
567641
def config_get(args):
568642
""" Retrieve a method config from a workspace, send stdout """
@@ -2093,22 +2167,31 @@ def main(argv=None):
20932167
# List available methods
20942168
subp = subparsers.add_parser('meth_list',
20952169
description='List available workflows')
2096-
subp.add_argument('-n', '--name', default=None,required=False,
2170+
subp.add_argument('-m', '--method', default=None,
2171+
help='name of single workflow to search for (optional)')
2172+
subp.add_argument('-n', '--namespace', default=None,
20972173
help='name of single workflow to search for (optional)')
2174+
subp.add_argument('-i', '--snapshot-id', default=None,
2175+
help="Snapshot ID (version) of method/config")
20982176
subp.set_defaults(func=meth_list)
20992177

21002178
subp = subparsers.add_parser('meth_exists',
21012179
description='Determine if named workflow exists in method repository')
2102-
subp.add_argument('name', help='name of method to search for In repository')
2180+
subp.add_argument('method', help='name of method to search for in repository')
21032181
subp.set_defaults(func=meth_exists)
21042182

21052183
# Configuration: list
21062184
subp = subparsers.add_parser(
21072185
'config_list', description='List available configurations')
2108-
subp.add_argument('-w', '--workspace', help='Workspace name',
2109-
default=fcconfig.workspace, required=False)
2186+
subp.add_argument('-w', '--workspace', help='Workspace name')
21102187
subp.add_argument('-p', '--project', default=fcconfig.project,
2111-
help=proj_help, required=proj_required)
2188+
help=proj_help)
2189+
subp.add_argument('-c', '--config', default=None,
2190+
help='name of single workflow to search for (optional)')
2191+
subp.add_argument('-n', '--namespace', default=None,
2192+
help='name of single workflow to search for (optional)')
2193+
subp.add_argument('-i', '--snapshot-id', default=None,
2194+
help="Snapshot ID (version) of config (optional)")
21122195
subp.set_defaults(func=config_list)
21132196

21142197
# Configuration: delete
@@ -2200,16 +2283,12 @@ def main(argv=None):
22002283
# pushed into a separate function and/or auto-generated
22012284

22022285
# Set ACL
2203-
# cacl_parser = subparsers.add_parser('config_set_acl',
2204-
# description='Set roles for config')
2205-
# cacl_parser.add_argument('namespace', help='Method namespace')
2206-
# cacl_parser.add_argument('name', help='Config name')
2207-
# cacl_parser.add_argument('snapshot_id', help='Snapshot ID')
2208-
# cacl_parser.add_argument('role', help='ACL role',
2209-
# choices=['OWNER', 'READER', 'WRITER', 'NO ACCESS'])
2210-
# cacl_parser.add_argument('users', metavar='user', help='Firecloud username',
2211-
# nargs='+')
2212-
# cacl_parser.set_defaults(func=meth_set_acl)
2286+
subp = subparsers.add_parser('config_set_acl', description='Assign an ' +
2287+
'ACL role to a list of users for a config',
2288+
parents=[conf_parent, acl_parent])
2289+
subp.add_argument('-i', '--snapshot-id',
2290+
help="Snapshot ID (version) of method/config")
2291+
subp.set_defaults(func=config_set_acl)
22132292

22142293
# Status
22152294
subp = subparsers.add_parser('health',

0 commit comments

Comments
 (0)