• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

uc-cdis / fence / 13727398361

07 Mar 2025 06:57PM UTC coverage: 75.427% (+0.2%) from 75.268%
13727398361

Pull #1209

github

web-flow
Merge branch 'master' into fix/move_backoff_settings_v2
Pull Request #1209: move backoff settings as well as other functions out of utils

7855 of 10414 relevant lines covered (75.43%)

0.75 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

60.5
fence/scripting/fence_create.py
1
from datetime import datetime, timedelta
1✔
2
import os
1✔
3
import os.path
1✔
4
import requests
1✔
5
import time
1✔
6
from yaml import safe_load
1✔
7
import json
1✔
8
import pprint
1✔
9
import asyncio
1✔
10

11
from alembic.config import main as alembic_main
1✔
12
from gen3cirrus import GoogleCloudManager
1✔
13
from gen3cirrus.google_cloud.errors import GoogleAuthError
1✔
14
from gen3cirrus.config import config as cirrus_config
1✔
15
from cdislogging import get_logger
1✔
16
from sqlalchemy import func
1✔
17
from userdatamodel.models import (
1✔
18
    AccessPrivilege,
19
    Bucket,
20
    CloudProvider,
21
    GoogleProxyGroup,
22
    Group,
23
    IdentityProvider,
24
    Project,
25
    StorageAccess,
26
    User,
27
    ProjectToBucket,
28
)
29
from sqlalchemy import and_
1✔
30

31
from fence.blueprints.link import (
1✔
32
    force_update_user_google_account_expiration,
33
    add_new_user_google_account,
34
)
35
from fence.errors import Unauthorized
1✔
36
from fence.jwt.token import (
1✔
37
    generate_signed_access_token,
38
    generate_signed_refresh_token,
39
    issued_and_expiration_times,
40
)
41
from fence.job.visa_update_cronjob import Visa_Token_Update
1✔
42
from fence.models import (
1✔
43
    Client,
44
    GoogleServiceAccount,
45
    GoogleServiceAccountKey,
46
    UserGoogleAccount,
47
    UserGoogleAccountToProxyGroup,
48
    GoogleBucketAccessGroup,
49
    GoogleProxyGroupToGoogleBucketAccessGroup,
50
    UserRefreshToken,
51
    ServiceAccountToGoogleBucketAccessGroup,
52
    query_for_user,
53
    GA4GHVisaV1,
54
    get_client_expires_at, create_client,
55
)
56
from fence.scripting.google_monitor import email_users_without_access, validation_check
1✔
57
from fence.config import config
1✔
58
from fence.sync.sync_users import UserSyncer
1✔
59
from fence.utils import (
1✔
60
    get_valid_expiration,
61
    generate_client_credentials,
62
)
63
from fence import get_SQLAlchemyDriver
1✔
64
from sqlalchemy.orm.attributes import flag_modified
1✔
65
from gen3authz.client.arborist.client import ArboristClient
1✔
66

67
logger = get_logger(__name__)
1✔
68

69

70
def list_client_action(db):
1✔
71
    try:
1✔
72
        driver = get_SQLAlchemyDriver(db)
1✔
73
        with driver.session as s:
1✔
74
            for row in s.query(Client).all():
1✔
75
                pprint.pprint(row.__dict__)
1✔
76
    except Exception as e:
×
77
        logger.error(str(e))
×
78

79

80
def modify_client_action(
1✔
81
    DB,
82
    client=None,
83
    delete_urls=False,
84
    urls=None,
85
    name=None,
86
    description=None,
87
    set_auto_approve=False,
88
    unset_auto_approve=False,
89
    arborist=None,
90
    policies=None,
91
    allowed_scopes=None,
92
    append=False,
93
    expires_in=None,
94
):
95
    driver = get_SQLAlchemyDriver(DB)
1✔
96
    with driver.session as s:
1✔
97
        client_name = client
1✔
98
        clients = s.query(Client).filter(Client.name == client_name).all()
1✔
99
        if not clients:
1✔
100
            raise Exception("client {} does not exist".format(client_name))
×
101
        for client in clients:
1✔
102
            metadata = client.client_metadata
1✔
103
            if urls:
1✔
104
                if append:
1✔
105
                    metadata["redirect_uris"] += urls
1✔
106
                    logger.info("Adding {} to urls".format(urls))
1✔
107
                else:
108
                    if isinstance(urls, list):
1✔
109
                        metadata["redirect_uris"] = urls
1✔
110
                    else:
111
                        metadata["redirect_uris"] = [urls]
×
112
                    logger.info("Changing urls to {}".format(urls))
1✔
113
            if delete_urls:
1✔
114
                metadata["redirect_uris"] = []
×
115
                logger.info("Deleting urls")
×
116
            if set_auto_approve:
1✔
117
                client.auto_approve = True
1✔
118
                logger.info("Auto approve set to True")
1✔
119
            if unset_auto_approve:
1✔
120
                client.auto_approve = False
×
121
                logger.info("Auto approve set to False")
×
122
            if name:
1✔
123
                client.name = name
1✔
124
                logger.info("Updating name to {}".format(name))
1✔
125
            if description:
1✔
126
                client.description = description
1✔
127
                logger.info("Updating description to {}".format(description))
1✔
128
            if allowed_scopes:
1✔
129
                if append:
1✔
130
                    new_scopes = client.scope.split() + allowed_scopes
1✔
131
                    metadata["scope"] = " ".join(new_scopes)
1✔
132
                    logger.info("Adding {} to allowed_scopes".format(allowed_scopes))
1✔
133
                else:
134
                    metadata["scope"] = " ".join(allowed_scopes)
1✔
135
                    logger.info("Updating allowed_scopes to {}".format(allowed_scopes))
1✔
136
            if expires_in:
1✔
137
                client.expires_at = get_client_expires_at(
1✔
138
                    expires_in=expires_in, grant_types=client.grant_types
139
                )
140
            # Call setter on Json object to persist changes if any
141
            client.set_client_metadata(metadata)
1✔
142
        s.commit()
1✔
143
        if arborist is not None and policies:
1✔
144
            arborist.update_client(client.client_id, policies)
1✔
145

146

147
def create_client_action(
1✔
148
    DB,
149
    username=None,
150
    client=None,
151
    urls=None,
152
    auto_approve=False,
153
    expires_in=None,
154
    **kwargs,
155
):
156
    print("\nSave these credentials! Fence will not save the unhashed client secret.")
1✔
157
    res = create_client(
1✔
158
        DB=DB,
159
        username=username,
160
        urls=urls,
161
        name=client,
162
        auto_approve=auto_approve,
163
        expires_in=expires_in,
164
        **kwargs,
165
    )
166
    print("client id, client secret:")
1✔
167
    # This should always be the last line of output and should remain in this format--
168
    # cloud-auto and gen3-qa use the output programmatically.
169
    print(res)
1✔
170

171

172
def delete_client_action(DB, client_name):
1✔
173
    try:
1✔
174
        cirrus_config.update(**config["CIRRUS_CFG"])
1✔
175
    except AttributeError:
×
176
        # no cirrus config, continue anyway. we don't have client service accounts
177
        # to delete
178
        pass
×
179

180
    try:
1✔
181
        driver = get_SQLAlchemyDriver(DB)
1✔
182
        with driver.session as current_session:
1✔
183
            if (
1✔
184
                not current_session.query(Client)
185
                .filter(Client.name == client_name)
186
                .first()
187
            ):
188
                raise Exception("client {} does not exist".format(client_name))
×
189

190
            clients = (
1✔
191
                current_session.query(Client).filter(Client.name == client_name).all()
192
            )
193

194
            for client in clients:
1✔
195
                _remove_client_service_accounts(current_session, client)
1✔
196
                current_session.delete(client)
1✔
197
            current_session.commit()
1✔
198

199
        logger.info("Client '{}' deleted".format(client_name))
1✔
200
    except Exception as e:
×
201
        logger.error(str(e))
×
202

203

204
def delete_expired_clients_action(DB, slack_webhook=None, warning_days=None):
1✔
205
    """
206
    Args:
207
        slack_webhook (str): Slack webhook to post warnings when clients expired or are about to expire
208
        warning_days (int): how many days before a client expires should we post a warning on
209
            Slack (default: 7)
210
    """
211
    try:
1✔
212
        cirrus_config.update(**config["CIRRUS_CFG"])
1✔
213
    except AttributeError:
×
214
        # no cirrus config, continue anyway. we don't have client service accounts
215
        # to delete
216
        pass
×
217

218
    def format_uris(uris):
1✔
219
        if not uris or uris == [None]:
1✔
220
            return uris
×
221
        return " ".join(uris)
1✔
222

223
    now = datetime.now().timestamp()
1✔
224
    driver = get_SQLAlchemyDriver(DB)
1✔
225
    expired_messages = ["Some expired OIDC clients have been deleted!"]
1✔
226
    with driver.session as current_session:
1✔
227
        clients = (
1✔
228
            current_session.query(Client)
229
            # for backwards compatibility, 0 means no expiration
230
            .filter(Client.expires_at != 0)
231
            .filter(Client.expires_at <= now)
232
            .all()
233
        )
234

235
        for client in clients:
1✔
236
            expired_messages.append(
1✔
237
                f"Client '{client.name}' (ID '{client.client_id}') expired at {datetime.fromtimestamp(client.expires_at)} UTC. Redirect URIs: {format_uris(client.redirect_uris)})"
238
            )
239
            _remove_client_service_accounts(current_session, client)
1✔
240
            current_session.delete(client)
1✔
241
            current_session.commit()
1✔
242

243
        # get the clients that are expiring soon
244
        warning_days = float(warning_days) if warning_days else 7
1✔
245
        warning_days_in_secs = warning_days * 24 * 60 * 60  # days to seconds
1✔
246
        warning_expiry = (
1✔
247
            datetime.utcnow() + timedelta(seconds=warning_days_in_secs)
248
        ).timestamp()
249
        expiring_clients = (
1✔
250
            current_session.query(Client)
251
            .filter(Client.expires_at != 0)
252
            .filter(Client.expires_at <= warning_expiry)
253
            .all()
254
        )
255

256
    expiring_messages = ["Some OIDC clients are expiring soon!"]
1✔
257
    expiring_messages.extend(
1✔
258
        [
259
            f"Client '{client.name}' (ID '{client.client_id}') expires at {datetime.fromtimestamp(client.expires_at)} UTC. Redirect URIs: {format_uris(client.redirect_uris)}"
260
            for client in expiring_clients
261
        ]
262
    )
263

264
    for post_msgs, nothing_to_do_msg in (
1✔
265
        (expired_messages, "No expired clients to delete"),
266
        (expiring_messages, "No clients are close to expiring"),
267
    ):
268
        if len(post_msgs) > 1:
1✔
269
            for e in post_msgs:
1✔
270
                logger.info(e)
1✔
271
            if slack_webhook:  # post a warning on Slack
1✔
272
                logger.info("Posting to Slack...")
1✔
273
                payload = {
1✔
274
                    "attachments": [
275
                        {
276
                            "fallback": post_msgs[0],
277
                            "title": post_msgs[0],
278
                            "text": "\n- " + "\n- ".join(post_msgs[1:]),
279
                            "color": "#FF5F15",
280
                        }
281
                    ]
282
                }
283
                resp = requests.post(slack_webhook, json=payload)
1✔
284
                resp.raise_for_status()
1✔
285
        else:
286
            logger.info(nothing_to_do_msg)
×
287

288

289
def rotate_client_action(DB, client_name, expires_in=None):
1✔
290
    """
291
    Rorate a client's credentials (client ID and secret). The old credentials are
292
    NOT deactivated and must be deleted or expired separately. This allows for a
293
    rotation without downtime.
294

295
    Args:
296
        DB (str): database connection string
297
        client_name (str): name of the client to rotate credentials for
298
        expires_in (optional): number of days until this client expires (by default, no expiration)
299

300
    Returns:
301
        This functions does not return anything, but it prints the new set of credentials.
302
    """
303
    driver = get_SQLAlchemyDriver(DB)
1✔
304
    with driver.session as s:
1✔
305
        client = s.query(Client).filter(Client.name == client_name).first()
1✔
306
        if not client:
1✔
307
            raise Exception("client {} does not exist".format(client_name))
×
308

309
        # create a new row in the DB for the same client, with a new ID, secret and expiration
310
        client_id, client_secret, hashed_secret = generate_client_credentials(
1✔
311
            client.is_confidential
312
        )
313
        client = Client(
1✔
314
            client_id=client_id,
315
            client_secret=hashed_secret,
316
            expires_in=expires_in,
317
            # the rest is identical to the client being rotated
318
            user=client.user,
319
            redirect_uris=client.redirect_uris,
320
            allowed_scopes=client.scope,
321
            description=client.description,
322
            name=client.name,
323
            auto_approve=client.auto_approve,
324
            grant_types=client.grant_types,
325
            is_confidential=client.is_confidential,
326
            token_endpoint_auth_method=client.token_endpoint_auth_method,
327
        )
328
        s.add(client)
1✔
329
        s.commit()
1✔
330

331
    res = (client_id, client_secret)
1✔
332
    print(
1✔
333
        f"\nSave these credentials! Fence will not save the unhashed client secret.\nclient id, client secret:\n{res}"
334
    )
335

336

337
def _remove_client_service_accounts(db_session, client):
1✔
338
    client_service_accounts = (
1✔
339
        db_session.query(GoogleServiceAccount)
340
        .filter(GoogleServiceAccount.client_id == client.client_id)
341
        .all()
342
    )
343

344
    if client_service_accounts:
1✔
345
        with GoogleCloudManager() as g_mgr:
1✔
346
            for service_account in client_service_accounts:
1✔
347
                logger.info(
1✔
348
                    "Deleting client {}'s service account: {}".format(
349
                        client.name, service_account.email
350
                    )
351
                )
352
                response = g_mgr.delete_service_account(service_account.email)
1✔
353
                if not response.get("error"):
1✔
354
                    db_session.delete(service_account)
1✔
355
                    db_session.commit()
1✔
356
                else:
357
                    logger.error("ERROR - from Google: {}".format(response))
1✔
358
                    logger.error(
1✔
359
                        "ERROR - Could not delete client service account: {}".format(
360
                            service_account.email
361
                        )
362
                    )
363

364

365
def get_default_init_syncer_inputs(authz_provider):
1✔
366
    DB = os.environ.get("FENCE_DB") or config.get("DB")
1✔
367
    if DB is None:
1✔
368
        try:
×
369
            from fence.settings import DB
×
370
        except ImportError:
×
371
            pass
×
372

373
    arborist = ArboristClient(
1✔
374
        arborist_base_url=config["ARBORIST"],
375
        logger=get_logger("user_syncer.arborist_client"),
376
        authz_provider=authz_provider,
377
    )
378
    dbGaP = os.environ.get("dbGaP") or config.get("dbGaP")
1✔
379
    if not isinstance(dbGaP, list):
1✔
380
        dbGaP = [dbGaP]
×
381

382
    storage_creds = config["STORAGE_CREDENTIALS"]
1✔
383

384
    return {
1✔
385
        "DB": DB,
386
        "arborist": arborist,
387
        "dbGaP": dbGaP,
388
        "STORAGE_CREDENTIALS": storage_creds,
389
    }
390

391

392
def init_syncer(
1✔
393
    dbGaP,
394
    STORAGE_CREDENTIALS,
395
    DB,
396
    projects=None,
397
    is_sync_from_dbgap_server=False,
398
    sync_from_local_csv_dir=None,
399
    sync_from_local_yaml_file=None,
400
    arborist=None,
401
    folder=None,
402
):
403
    """
404
    sync ACL files from dbGap to auth db and storage backends
405
    imports from config is done here because dbGap is
406
    an optional requirement for fence so it might not be specified
407
    in config
408
    Args:
409
        projects: path to project_mapping yaml file which contains mapping
410
        from dbgap phsids to projects in fence database
411
    Returns:
412
        None
413
    Examples:
414
        the expected yaml structure sould look like:
415
        .. code-block:: yaml
416
            phs000178:
417
              - name: TCGA
418
                auth_id: phs000178
419
              - name: TCGA-PCAWG
420
                auth_id: TCGA-PCAWG
421
            phs000235:
422
              - name: CGCI
423
                auth_id: phs000235
424
    """
425
    try:
1✔
426
        cirrus_config.update(**config["CIRRUS_CFG"])
1✔
427
    except AttributeError:
×
428
        # no cirrus config, continue anyway. Google APIs will probably fail.
429
        # this is okay if users don't need access to Google buckets
430
        pass
×
431

432
    if projects is not None and not os.path.exists(projects):
1✔
433
        logger.error("====={} is not found!!!=======".format(projects))
×
434
        return
×
435
    if sync_from_local_csv_dir and not os.path.exists(sync_from_local_csv_dir):
1✔
436
        logger.error("====={} is not found!!!=======".format(sync_from_local_csv_dir))
×
437
        return
×
438
    if sync_from_local_yaml_file and not os.path.exists(sync_from_local_yaml_file):
1✔
439
        logger.error("====={} is not found!!!=======".format(sync_from_local_yaml_file))
×
440
        return
×
441

442
    project_mapping = None
1✔
443
    if projects:
1✔
444
        try:
×
445
            with open(projects, "r") as f:
×
446
                project_mapping = safe_load(f)
×
447
        except IOError:
×
448
            pass
×
449

450
    return UserSyncer(
1✔
451
        dbGaP,
452
        DB,
453
        project_mapping=project_mapping,
454
        storage_credentials=STORAGE_CREDENTIALS,
455
        is_sync_from_dbgap_server=is_sync_from_dbgap_server,
456
        sync_from_local_csv_dir=sync_from_local_csv_dir,
457
        sync_from_local_yaml_file=sync_from_local_yaml_file,
458
        arborist=arborist,
459
        folder=folder,
460
    )
461

462

463
def download_dbgap_files(
1✔
464
    # Note: need to keep all parameter to prevent download failure
465
    dbGaP,
466
    STORAGE_CREDENTIALS,
467
    DB,
468
    projects=None,
469
    is_sync_from_dbgap_server=False,
470
    sync_from_local_csv_dir=None,
471
    sync_from_local_yaml_file=None,
472
    arborist=None,
473
    folder=None,
474
):
475
    syncer = init_syncer(
×
476
        dbGaP,
477
        STORAGE_CREDENTIALS,
478
        DB,
479
        projects,
480
        is_sync_from_dbgap_server,
481
        sync_from_local_csv_dir,
482
        sync_from_local_yaml_file,
483
        arborist,
484
        folder,
485
    )
486
    if not syncer:
×
487
        exit(1)
×
488
    syncer.download()
×
489

490

491
def sync_users(
1✔
492
    dbGaP,
493
    STORAGE_CREDENTIALS,
494
    DB,
495
    projects=None,
496
    is_sync_from_dbgap_server=False,
497
    sync_from_local_csv_dir=None,
498
    sync_from_local_yaml_file=None,
499
    arborist=None,
500
    folder=None,
501
):
502
    syncer = init_syncer(
×
503
        dbGaP,
504
        STORAGE_CREDENTIALS,
505
        DB,
506
        projects,
507
        is_sync_from_dbgap_server,
508
        sync_from_local_csv_dir,
509
        sync_from_local_yaml_file,
510
        arborist,
511
        folder,
512
    )
513
    if not syncer:
×
514
        exit(1)
×
515
    syncer.sync()
×
516

517

518
def create_sample_data(DB, yaml_file_path):
1✔
519
    with open(yaml_file_path, "r") as f:
×
520
        data = safe_load(f)
×
521

522
    db = get_SQLAlchemyDriver(DB)
×
523
    with db.session as s:
×
524
        create_cloud_providers(s, data)
×
525
        create_projects(s, data)
×
526
        create_group(s, data)
×
527
        create_users_with_group(DB, s, data)
×
528

529

530
def create_group(s, data):
1✔
531
    groups = data.get("groups", {})
1✔
532
    for group_name, fields in groups.items():
1✔
533
        projects = fields.get("projects", [])
1✔
534
        group = s.query(Group).filter(Group.name == group_name).first()
1✔
535
        if not group:
1✔
536
            group = Group(name=group_name)
1✔
537
            s.add(group)
1✔
538
        for project_data in projects:
1✔
539
            grant_project_to_group_or_user(s, project_data, group)
1✔
540

541

542
def create_projects(s, data):
1✔
543
    projects = data.get("projects", [])
1✔
544
    for project in projects:
1✔
545
        create_project(s, project)
1✔
546

547

548
def create_project(s, project_data):
1✔
549
    auth_id = project_data["auth_id"]
1✔
550
    name = project_data.get("name", auth_id)
1✔
551
    project = s.query(Project).filter_by(name=name).first()
1✔
552
    if project is None:
1✔
553
        project = Project(name=name, auth_id=auth_id)
1✔
554
        s.add(project)
1✔
555
    if "storage_accesses" in project_data:
1✔
556
        sa_list = project_data.get("storage_accesses", [])
1✔
557
        for storage_access in sa_list:
1✔
558
            provider = storage_access["name"]
1✔
559
            buckets = storage_access.get("buckets", [])
1✔
560
            sa = (
1✔
561
                s.query(StorageAccess)
562
                .join(StorageAccess.provider, StorageAccess.project)
563
                .filter(Project.name == project.name)
564
                .filter(CloudProvider.name == provider)
565
                .first()
566
            )
567
            c_provider = s.query(CloudProvider).filter_by(name=provider).first()
1✔
568
            assert c_provider, "CloudProvider {} does not exist".format(provider)
1✔
569
            if not sa:
1✔
570
                sa = StorageAccess(provider=c_provider, project=project)
1✔
571
                s.add(sa)
1✔
572
                logger.info(
1✔
573
                    "created storage access for {} to {}".format(
574
                        project.name, c_provider.name
575
                    )
576
                )
577
            for bucket in buckets:
1✔
578
                b = (
1✔
579
                    s.query(Bucket)
580
                    .filter_by(name=bucket)
581
                    .join(Bucket.provider)
582
                    .filter(CloudProvider.name == provider)
583
                    .first()
584
                )
585
                if not b:
1✔
586
                    b = Bucket(name=bucket)
1✔
587
                    b.provider = c_provider
1✔
588
                    s.add(b)
1✔
589
                    logger.info("created bucket {} in db".format(bucket))
1✔
590

591
    return project
1✔
592

593

594
def grant_project_to_group_or_user(s, project_data, group=None, user=None):
1✔
595
    privilege = project_data.get("privilege", [])
1✔
596
    project = create_project(s, project_data)
1✔
597
    if group:
1✔
598
        ap = (
1✔
599
            s.query(AccessPrivilege)
600
            .join(AccessPrivilege.project)
601
            .join(AccessPrivilege.group)
602
            .filter(Project.name == project.name, Group.name == group.name)
603
            .first()
604
        )
605
        name = group.name
1✔
606
    elif user:
×
607
        ap = (
×
608
            s.query(AccessPrivilege)
609
            .join(AccessPrivilege.project)
610
            .join(AccessPrivilege.user)
611
            .filter(
612
                Project.name == project.name,
613
                func.lower(User.username) == func.lower(user.username),
614
            )
615
            .first()
616
        )
617
        name = user.username
×
618
    else:
619
        raise Exception("need to provide either a user or group")
×
620
    if not ap:
1✔
621
        if group:
1✔
622
            ap = AccessPrivilege(project=project, group=group, privilege=privilege)
1✔
623
        elif user:
×
624
            ap = AccessPrivilege(project=project, user=user, privilege=privilege)
×
625
        else:
626
            raise Exception("need to provide either a user or group")
×
627
        s.add(ap)
1✔
628
        logger.info(
1✔
629
            "created access privilege {} of project {} to {}".format(
630
                privilege, project.name, name
631
            )
632
        )
633
    else:
634
        ap.privilege = privilege
×
635
        logger.info(
×
636
            "updated access privilege {} of project {} to {}".format(
637
                privilege, project.name, name
638
            )
639
        )
640

641

642
def create_cloud_providers(s, data):
1✔
643
    cloud_data = data.get("cloud_providers", {})
×
644
    for name, fields in cloud_data.items():
×
645
        cloud_provider = (
×
646
            s.query(CloudProvider).filter(CloudProvider.name == name).first()
647
        )
648
        if not cloud_provider:
×
649
            cloud_provider = CloudProvider(
×
650
                name=name,
651
                backend=fields.get("backend", "cleversafe"),
652
                service=fields.get("service", "storage"),
653
            )
654
            s.add(cloud_provider)
×
655

656

657
def create_users_with_group(DB, s, data):
1✔
658
    providers = {}
×
659
    data_groups = data.get("groups", {})
×
660
    users = data.get("users", {})
×
661
    for username, data in users.items():
×
662
        is_existing_user = True
×
663
        user = query_for_user(session=s, username=username)
×
664

665
        admin = data.get("admin", False)
×
666

667
        if not user:
×
668
            is_existing_user = False
×
669
            provider_name = data.get("provider", "google")
×
670
            provider = providers.get(provider_name)
×
671
            if not provider:
×
672
                provider = (
×
673
                    s.query(IdentityProvider)
674
                    .filter(IdentityProvider.name == provider_name)
675
                    .first()
676
                )
677
                providers[provider_name] = provider
×
678
                if not provider:
×
679
                    raise Exception("provider {} not found".format(provider_name))
×
680

681
            user = User(username=username, idp_id=provider.id, is_admin=admin)
×
682
        user.is_admin = admin
×
683
        group_names = data.get("groups", [])
×
684
        for group_name in group_names:
×
685
            assign_group_to_user(s, user, group_name, data_groups[group_name])
×
686
        projects = data.get("projects", [])
×
687
        for project in projects:
×
688
            grant_project_to_group_or_user(s, project, user=user)
×
689
        if not is_existing_user:
×
690
            s.add(user)
×
691
        for client in data.get("clients", []):
×
692
            create_client_action(DB, username=username, **client)
×
693

694

695
def assign_group_to_user(s, user, group_name, group_data):
1✔
696
    group = s.query(Group).filter(Group.name == group_name).first()
×
697
    if not group:
×
698
        group = Group(name=group_name)
×
699
        s.add(group)
×
700
        user.groups.append(group)
×
701
    if group not in user.groups:
×
702
        user.groups.append(group)
×
703

704

705
def google_init(db):
1✔
706
    """
707
    DEPRECATED - Initial user proxy group / service account creation.
708
    No longer necessary as proxy groups and service accounts are lazily
709
    created.
710
    """
711
    pass
×
712

713

714
def remove_expired_google_service_account_keys(db):
1✔
715
    cirrus_config.update(**config["CIRRUS_CFG"])
1✔
716

717
    db = get_SQLAlchemyDriver(db)
1✔
718
    with db.session as current_session:
1✔
719
        client_service_accounts = current_session.query(
1✔
720
            GoogleServiceAccount, Client
721
        ).filter(GoogleServiceAccount.client_id == Client.client_id)
722

723
        current_time = int(time.time())
1✔
724
        logger.info("Current time: {}\n".format(current_time))
1✔
725

726
        expired_sa_keys_for_users = current_session.query(
1✔
727
            GoogleServiceAccountKey
728
        ).filter(GoogleServiceAccountKey.expires <= current_time)
729

730
        with GoogleCloudManager() as g_mgr:
1✔
731
            # handle service accounts with default max expiration
732
            for service_account, client in client_service_accounts:
1✔
733
                g_mgr.handle_expired_service_account_keys(service_account.email)
1✔
734

735
            # handle service accounts with custom expiration
736
            for expired_user_key in expired_sa_keys_for_users:
1✔
737
                logger.info("expired_user_key: {}\n".format(expired_user_key))
1✔
738
                sa = (
1✔
739
                    current_session.query(GoogleServiceAccount)
740
                    .filter(
741
                        GoogleServiceAccount.id == expired_user_key.service_account_id
742
                    )
743
                    .first()
744
                )
745

746
                response = g_mgr.delete_service_account_key(
1✔
747
                    account=sa.email, key_name=expired_user_key.key_id
748
                )
749
                response_error_code = response.get("error", {}).get("code")
1✔
750
                response_error_status = response.get("error", {}).get("status")
1✔
751
                logger.info("response_error_code: {}\n".format(response_error_code))
1✔
752
                logger.info("response_error_status: {}\n".format(response_error_status))
1✔
753

754
                if not response_error_code:
1✔
755
                    current_session.delete(expired_user_key)
1✔
756
                    logger.info(
1✔
757
                        "INFO: Removed expired service account key {} "
758
                        "for service account {} (owned by user with id {}).\n".format(
759
                            expired_user_key.key_id, sa.email, sa.user_id
760
                        )
761
                    )
762
                elif (
×
763
                    response_error_code == 404
764
                    or response_error_status == "FAILED_PRECONDITION"
765
                ):
766
                    logger.info(
×
767
                        "INFO: Service account key {} for service account {} "
768
                        "(owned by user with id {}) does not exist in Google. "
769
                        "Removing from database...\n".format(
770
                            expired_user_key.key_id, sa.email, sa.user_id
771
                        )
772
                    )
773
                    current_session.delete(expired_user_key)
×
774
                else:
775
                    logger.error(
×
776
                        "ERROR: Google returned an error when attempting to "
777
                        "remove service account key {} "
778
                        "for service account {} (owned by user with id {}). "
779
                        "Error:\n{}\n".format(
780
                            expired_user_key.key_id, sa.email, sa.user_id, response
781
                        )
782
                    )
783

784

785
def remove_expired_google_accounts_from_proxy_groups(db):
1✔
786
    cirrus_config.update(**config["CIRRUS_CFG"])
×
787

788
    db = get_SQLAlchemyDriver(db)
×
789
    with db.session as current_session:
×
790
        current_time = int(time.time())
×
791
        logger.info("Current time: {}".format(current_time))
×
792

793
        expired_accounts = current_session.query(UserGoogleAccountToProxyGroup).filter(
×
794
            UserGoogleAccountToProxyGroup.expires <= current_time
795
        )
796

797
        with GoogleCloudManager() as g_mgr:
×
798
            for expired_account_access in expired_accounts:
×
799
                g_account = (
×
800
                    current_session.query(UserGoogleAccount)
801
                    .filter(
802
                        UserGoogleAccount.id
803
                        == expired_account_access.user_google_account_id
804
                    )
805
                    .first()
806
                )
807
                try:
×
808
                    response = g_mgr.remove_member_from_group(
×
809
                        member_email=g_account.email,
810
                        group_id=expired_account_access.proxy_group_id,
811
                    )
812
                    response_error_code = response.get("error", {}).get("code")
×
813

814
                    if not response_error_code:
×
815
                        current_session.delete(expired_account_access)
×
816
                        logger.info(
×
817
                            "INFO: Removed {} from proxy group with id {}.\n".format(
818
                                g_account.email, expired_account_access.proxy_group_id
819
                            )
820
                        )
821
                    else:
822
                        logger.error(
×
823
                            "ERROR: Google returned an error when attempting to "
824
                            "remove member {} from proxy group {}. Error:\n{}\n".format(
825
                                g_account.email,
826
                                expired_account_access.proxy_group_id,
827
                                response,
828
                            )
829
                        )
830
                except Exception as exc:
×
831
                    logger.error(
×
832
                        "ERROR: Google returned an error when attempting to "
833
                        "remove member {} from proxy group {}. Error:\n{}\n".format(
834
                            g_account.email, expired_account_access.proxy_group_id, exc
835
                        )
836
                    )
837

838

839
def delete_users(DB, usernames):
1✔
840
    driver = get_SQLAlchemyDriver(DB)
1✔
841
    with driver.session as session:
1✔
842
        # NOTE that calling ``.delete()`` on the query itself will not follow
843
        # cascade deletion rules set up in any relationships.
844
        lowercase_usernames = [x.lower() for x in usernames]
1✔
845
        users_to_delete = (
1✔
846
            session.query(User)
847
            .filter(func.lower(User.username).in_(lowercase_usernames))
848
            .all()
849
        )
850
        for user in users_to_delete:
1✔
851
            session.delete(user)
1✔
852
        session.commit()
1✔
853

854

855
def cleanup_expired_ga4gh_information(DB):
1✔
856
    """
857
    Remove any expired passports/visas from the database if they're expired.
858

859
    IMPORTANT NOTE: This DOES NOT actually remove authorization, it assumes that the
860
                    same expiration was set and honored in the authorization system.
861
    """
862
    driver = get_SQLAlchemyDriver(DB)
1✔
863
    with driver.session as session:
1✔
864
        current_time = int(time.time())
1✔
865

866
        # Get expires field from db, if None default to NOT expired
867
        records_to_delete = (
1✔
868
            session.query(GA4GHVisaV1)
869
            .filter(
870
                and_(
871
                    GA4GHVisaV1.expires.isnot(None),
872
                    GA4GHVisaV1.expires < current_time,
873
                )
874
            )
875
            .all()
876
        )
877
        num_deleted_records = 0
1✔
878
        if records_to_delete:
1✔
879
            for record in records_to_delete:
1✔
880
                try:
1✔
881
                    session.delete(record)
1✔
882
                    session.commit()
1✔
883

884
                    num_deleted_records += 1
1✔
885
                except Exception as e:
×
886
                    logger.error(
×
887
                        "ERROR: Could not remove GA4GHVisaV1 with id={}. Detail {}".format(
888
                            record.id, e
889
                        )
890
                    )
891

892
        logger.info(
1✔
893
            f"Removed {num_deleted_records} expired GA4GHVisaV1 records from db."
894
        )
895

896

897
def delete_expired_google_access(DB):
1✔
898
    """
899
    Delete all expired Google data access (e.g. remove proxy groups from Google Bucket
900
    Access Groups if expired).
901
    """
902
    cirrus_config.update(**config["CIRRUS_CFG"])
1✔
903

904
    driver = get_SQLAlchemyDriver(DB)
1✔
905
    with driver.session as session:
1✔
906
        current_time = int(time.time())
1✔
907

908
        # Get expires field from db, if None default to NOT expired
909
        records_to_delete = (
1✔
910
            session.query(GoogleProxyGroupToGoogleBucketAccessGroup)
911
            .filter(
912
                and_(
913
                    GoogleProxyGroupToGoogleBucketAccessGroup.expires.isnot(None),
914
                    GoogleProxyGroupToGoogleBucketAccessGroup.expires < current_time,
915
                )
916
            )
917
            .all()
918
        )
919
        num_deleted_records = 0
1✔
920
        if records_to_delete:
1✔
921
            with GoogleCloudManager() as manager:
1✔
922
                for record in records_to_delete:
1✔
923
                    try:
1✔
924
                        member_email = record.proxy_group.email
1✔
925
                        access_group_email = record.access_group.email
1✔
926
                        manager.remove_member_from_group(
1✔
927
                            member_email, access_group_email
928
                        )
929
                        logger.info(
1✔
930
                            "Removed {} from {}, expired {}. Current time: {} ".format(
931
                                member_email,
932
                                access_group_email,
933
                                record.expires,
934
                                current_time,
935
                            )
936
                        )
937
                        session.delete(record)
1✔
938
                        session.commit()
1✔
939

940
                        num_deleted_records += 1
1✔
941
                    except Exception as e:
1✔
942
                        logger.error(
1✔
943
                            "ERROR: Could not remove Google group member {} from access group {}. Detail {}".format(
944
                                member_email, access_group_email, e
945
                            )
946
                        )
947

948
        logger.info(
1✔
949
            f"Removed {num_deleted_records} expired Google Access records from db and Google."
950
        )
951

952

953
def delete_expired_service_accounts(DB):
1✔
954
    """
955
    Delete all expired service accounts.
956
    """
957
    cirrus_config.update(**config["CIRRUS_CFG"])
1✔
958

959
    driver = get_SQLAlchemyDriver(DB)
1✔
960
    with driver.session as session:
1✔
961
        current_time = int(time.time())
1✔
962
        records_to_delete = (
1✔
963
            session.query(ServiceAccountToGoogleBucketAccessGroup)
964
            .filter(ServiceAccountToGoogleBucketAccessGroup.expires < current_time)
965
            .all()
966
        )
967
        if len(records_to_delete):
1✔
968
            with GoogleCloudManager() as manager:
1✔
969
                for record in records_to_delete:
1✔
970
                    try:
1✔
971
                        manager.remove_member_from_group(
1✔
972
                            record.service_account.email, record.access_group.email
973
                        )
974
                        session.delete(record)
1✔
975
                        logger.info(
1✔
976
                            "Removed expired service account: {}".format(
977
                                record.service_account.email
978
                            )
979
                        )
980
                    except Exception as e:
1✔
981
                        logger.error(
1✔
982
                            "ERROR: Could not delete service account {}. Details: {}".format(
983
                                record.service_account.email, e
984
                            )
985
                        )
986

987
                session.commit()
1✔
988

989

990
def verify_bucket_access_group(DB):
1✔
991
    """
992
    Go through all the google group members, remove them from Google group and Google
993
    user service account if they are not in Fence
994

995
    Args:
996
        DB(str): db connection string
997

998
    Returns:
999
        None
1000

1001
    """
1002
    cirrus_config.update(**config["CIRRUS_CFG"])
1✔
1003

1004
    driver = get_SQLAlchemyDriver(DB)
1✔
1005
    with driver.session as session:
1✔
1006
        access_groups = session.query(GoogleBucketAccessGroup).all()
1✔
1007
        with GoogleCloudManager() as manager:
1✔
1008
            for access_group in access_groups:
1✔
1009
                try:
1✔
1010
                    members = manager.get_group_members(access_group.email)
1✔
1011
                    logger.debug(f"google group members response: {members}")
1✔
1012
                except GoogleAuthError as e:
×
1013
                    logger.error("ERROR: Authentication error!!!. Detail {}".format(e))
×
1014
                    return
×
1015
                except Exception as e:
×
1016
                    logger.error(
×
1017
                        "ERROR: Could not list group members of {}. Detail {}".format(
1018
                            access_group.email, e
1019
                        )
1020
                    )
1021
                    return
×
1022

1023
                for member in members:
1✔
1024
                    if member.get("type") == "GROUP":
1✔
1025
                        _verify_google_group_member(session, access_group, member)
1✔
1026
                    elif member.get("type") == "USER":
1✔
1027
                        _verify_google_service_account_member(
1✔
1028
                            session, access_group, member
1029
                        )
1030

1031

1032
def _verify_google_group_member(session, access_group, member):
1✔
1033
    """
1034
    Delete if the member which is a google group is not in Fence.
1035

1036
    Args:
1037
        session(Session): db session
1038
        access_group(GoogleBucketAccessGroup): access group
1039
        member(dict): group member info
1040

1041
    Returns:
1042
        None
1043

1044
    """
1045
    account_emails = [
1✔
1046
        granted_group.proxy_group.email
1047
        for granted_group in (
1048
            session.query(GoogleProxyGroupToGoogleBucketAccessGroup)
1049
            .filter_by(access_group_id=access_group.id)
1050
            .all()
1051
        )
1052
    ]
1053

1054
    if not any([email for email in account_emails if email == member.get("email")]):
1✔
1055
        try:
1✔
1056
            with GoogleCloudManager() as manager:
1✔
1057
                manager.remove_member_from_group(
1✔
1058
                    member.get("email"), access_group.email
1059
                )
1060
                logger.info(
1✔
1061
                    "Removed {} from {}, not found in fence but found "
1062
                    "in Google Group.".format(member.get("email"), access_group.email)
1063
                )
1064
        except Exception as e:
×
1065
            logger.error(
×
1066
                "ERROR: Could not remove google group memeber {} from access group {}. Detail {}".format(
1067
                    member.get("email"), access_group.email, e
1068
                )
1069
            )
1070

1071

1072
def _verify_google_service_account_member(session, access_group, member):
1✔
1073
    """
1074
    Delete if the member which is a service account is not in Fence.
1075

1076
    Args:
1077
        session(session): db session
1078
        access_group(GoogleBucketAccessGroup): access group
1079
        members(dict): service account member info
1080

1081
    Returns:
1082
        None
1083

1084
    """
1085

1086
    account_emails = [
1✔
1087
        account.service_account.email
1088
        for account in (
1089
            session.query(ServiceAccountToGoogleBucketAccessGroup)
1090
            .filter_by(access_group_id=access_group.id)
1091
            .all()
1092
        )
1093
    ]
1094

1095
    if not any([email for email in account_emails if email == member.get("email")]):
1✔
1096
        try:
1✔
1097
            with GoogleCloudManager() as manager:
1✔
1098
                manager.remove_member_from_group(
1✔
1099
                    member.get("email"), access_group.email
1100
                )
1101
                logger.info(
1✔
1102
                    "Removed {} from {}, not found in fence but found "
1103
                    "in Google Group.".format(member.get("email"), access_group.email)
1104
                )
1105
        except Exception as e:
×
1106
            logger.error(
×
1107
                "ERROR: Could not remove service account memeber {} from access group {}. Detail {}".format(
1108
                    member.get("email"), access_group.email, e
1109
                )
1110
            )
1111

1112

1113
class JWTCreator(object):
1✔
1114
    required_kwargs = ["kid", "private_key", "username", "scopes"]
1✔
1115
    all_kwargs = required_kwargs + ["expires_in", "client_id"]
1✔
1116

1117
    default_expiration = 3600
1✔
1118

1119
    def __init__(self, db, base_url, **kwargs):
1✔
1120
        self.db = db
1✔
1121
        self.base_url = base_url
1✔
1122

1123
        # These get assigned values just below here, with setattr. Defined here
1124
        # so linters won't complain they're undefined.
1125
        self.kid = None
1✔
1126
        self.private_key = None
1✔
1127
        self.username = None
1✔
1128
        self.scopes = None
1✔
1129
        self.client_id = None
1✔
1130

1131
        for required_kwarg in self.required_kwargs:
1✔
1132
            if required_kwarg not in kwargs:
1✔
1133
                raise ValueError("missing required argument: " + required_kwarg)
×
1134

1135
        # Set attributes on this object from the kwargs.
1136
        for kwarg_name in self.all_kwargs:
1✔
1137
            setattr(self, kwarg_name, kwargs.get(kwarg_name))
1✔
1138

1139
        # If the scopes look like this:
1140
        #
1141
        #     'openid,fence,data'
1142
        #
1143
        # convert them to this:
1144
        #
1145
        #     ['openid', 'fence', 'data']
1146
        if isinstance(getattr(self, "scopes", ""), str):
1✔
1147
            self.scopes = [scope.strip() for scope in self.scopes.split(",")]
1✔
1148

1149
        self.expires_in = kwargs.get("expires_in") or self.default_expiration
1✔
1150

1151
    def create_access_token(self):
1✔
1152
        """
1153
        Create a new access token.
1154

1155
        Return:
1156
            JWTResult: result containing the encoded token and claims
1157
        """
1158
        driver = get_SQLAlchemyDriver(self.db)
1✔
1159
        with driver.session as current_session:
1✔
1160
            user = query_for_user(session=current_session, username=self.username)
1✔
1161

1162
            if not user:
1✔
1163
                raise EnvironmentError(
1✔
1164
                    "no user found with given username: " + self.username
1165
                )
1166
            return generate_signed_access_token(
1✔
1167
                self.kid,
1168
                self.private_key,
1169
                self.expires_in,
1170
                self.scopes,
1171
                user=user,
1172
                iss=self.base_url,
1173
            )
1174

1175
    def create_refresh_token(self):
1✔
1176
        """
1177
        Create a new refresh token and add its entry to the database.
1178

1179
        Return:
1180
            JWTResult: the refresh token result
1181
        """
1182
        driver = get_SQLAlchemyDriver(self.db)
1✔
1183
        with driver.session as current_session:
1✔
1184
            user = query_for_user(session=current_session, username=self.username)
1✔
1185

1186
            if not user:
1✔
1187
                raise EnvironmentError(
1✔
1188
                    "no user found with given username: " + self.username
1189
                )
1190
            if not self.client_id:
1✔
1191
                raise EnvironmentError(
×
1192
                    "no client id is provided. Required for creating refresh token"
1193
                )
1194
            jwt_result = generate_signed_refresh_token(
1✔
1195
                self.kid,
1196
                self.private_key,
1197
                user,
1198
                self.expires_in,
1199
                self.scopes,
1200
                iss=self.base_url,
1201
                client_id=self.client_id,
1202
            )
1203

1204
            current_session.add(
1✔
1205
                UserRefreshToken(
1206
                    jti=jwt_result.claims["jti"],
1207
                    userid=user.id,
1208
                    expires=jwt_result.claims["exp"],
1209
                )
1210
            )
1211

1212
            return jwt_result
1✔
1213

1214

1215
def link_bucket_to_project(db, bucket_id, bucket_provider, project_auth_id):
1✔
1216
    """
1217
    Associate a bucket to a specific project (with provided auth_id).
1218

1219
    Args:
1220
        db (TYPE): database
1221
        bucket_id (str): bucket db id or unique name
1222
            WARNING: name uniqueness is only required for Google so it's not
1223
                     a requirement of the db table. You will get an error if
1224
                     there are multiple buckets with the given name. In that
1225
                     case, you'll have to use the bucket's id.
1226
        bucket_provider (str): CloudProvider.name for the bucket
1227
        project_auth_id (str): Project.auth_id to link to bucket
1228
    """
1229
    driver = get_SQLAlchemyDriver(db)
×
1230
    with driver.session as current_session:
×
1231
        cloud_provider = (
×
1232
            current_session.query(CloudProvider).filter_by(name=bucket_provider).first()
1233
        )
1234
        if not cloud_provider:
×
1235
            raise NameError(
×
1236
                'No bucket with provider "{}" exists.'.format(bucket_provider)
1237
            )
1238

1239
        # first try by searching using id
1240
        try:
×
1241
            bucket_id = int(bucket_id)
×
1242
            bucket_db_entry = (
×
1243
                current_session.query(Bucket).filter_by(
1244
                    id=bucket_id, provider_id=cloud_provider.id
1245
                )
1246
            ).first()
1247
        except ValueError:
×
1248
            # invalid id, must be int
1249
            bucket_db_entry = None
×
1250

1251
        # nothing found? try searching for single bucket with name bucket_id
1252
        if not bucket_db_entry:
×
1253
            buckets_by_name = current_session.query(Bucket).filter_by(
×
1254
                name=bucket_id, provider_id=cloud_provider.id
1255
            )
1256
            # don't get a bucket if the name isn't unique. NOTE: for Google,
1257
            # these have to be globally unique so they'll be unique here.
1258
            buckets_with_name = buckets_by_name.count()
×
1259
            if buckets_with_name == 1:
×
1260
                bucket_db_entry = buckets_by_name[0]
×
1261
            elif buckets_with_name > 1:
×
1262
                raise NameError(
×
1263
                    'No bucket with id "{bucket_id}" exists. Tried buckets '
1264
                    'with name "{bucket_id}", but this returned multiple '
1265
                    "buckets. Please specify the id from the db and not just "
1266
                    "the name.".format(bucket_id=bucket_id)
1267
                )
1268
            else:
1269
                # keep bucket_db_entry as None
1270
                pass
1271

1272
        if not bucket_db_entry:
×
1273
            raise NameError('No bucket with id or name "{}" exists.'.format(bucket_id))
×
1274

1275
        project_db_entry = (
×
1276
            current_session.query(Project).filter_by(auth_id=project_auth_id).first()
1277
        )
1278
        if not project_db_entry:
×
1279
            logger.warning(
×
1280
                'WARNING: No project with auth_id "{}" exists. Creating...'.format(
1281
                    project_auth_id
1282
                )
1283
            )
1284
            project_db_entry = Project(name=project_auth_id, auth_id=project_auth_id)
×
1285
            current_session.add(project_db_entry)
×
1286
            current_session.commit()
×
1287

1288
        # Add StorageAccess if it doesn't exist for the project
1289
        storage_access = (
×
1290
            current_session.query(StorageAccess)
1291
            .filter_by(project_id=project_db_entry.id, provider_id=cloud_provider.id)
1292
            .first()
1293
        )
1294
        if not storage_access:
×
1295
            storage_access = StorageAccess(
×
1296
                project_id=project_db_entry.id, provider_id=cloud_provider.id
1297
            )
1298
            current_session.add(storage_access)
×
1299
            current_session.commit()
×
1300

1301
        project_linkage = (
×
1302
            current_session.query(ProjectToBucket)
1303
            .filter_by(project_id=project_db_entry.id, bucket_id=bucket_db_entry.id)
1304
            .first()
1305
        )
1306
        if not project_linkage:
×
1307
            project_linkage = ProjectToBucket(
×
1308
                project_id=project_db_entry.id,
1309
                bucket_id=bucket_db_entry.id,
1310
                privilege=["owner"],  # TODO What should this be???
1311
            )
1312
            current_session.add(project_linkage)
×
1313
            current_session.commit()
×
1314

1315

1316
def create_or_update_google_bucket(
1✔
1317
    db,
1318
    name,
1319
    storage_class=None,
1320
    public=None,
1321
    requester_pays=False,
1322
    google_project_id=None,
1323
    project_auth_id=None,
1324
    access_logs_bucket=None,
1325
    allowed_privileges=None,
1326
):
1327
    """
1328
    Create a Google bucket and populate database with necessary information.
1329

1330
    If the bucket is not public, this will also create a Google Bucket Access
1331
    Group(s) to control access to the new bucket. In order to give access
1332
    to a new user, simply add them to the Google Bucket Access Group.
1333

1334
    NOTE: At the moment, a different Google Bucket Access Group is created
1335
          for each different privilege in allowed_privileges (which defaults
1336
          to ['read', 'write']). So there will be separate Google Groups for
1337
          each access level.
1338

1339
    Args:
1340
        db (TYPE): database
1341
        name (str): name for the bucket, must be globally unique throughout Google
1342
        storage_class (str): enum, one of the cirrus's GOOGLE_STORAGE_CLASSES
1343
        public (bool or None, optional): whether or not the bucket should be public.
1344
            None means leave IAM on the bucket unchanged.
1345
        requester_pays (bool, optional): Whether or not to enable requester_pays
1346
            on the bucket
1347
        google_project_id (str, optional): Google project this bucket should be
1348
            associated with
1349
        project_auth_id (str, optional): a Project.auth_id to associate this
1350
            bucket with. The project must exist in the db already.
1351
        access_logs_bucket (str, optional): Enables logging. Must provide a
1352
            Google bucket name which will store the access logs
1353
        allowed_privileges (List(str), optional): privileges to allow on
1354
            the bucket. Defaults to ['read', 'write']. Also allows:
1355
            ['admin'] for all permission on the bucket including delete,
1356
            ['read'] for viewing access,
1357
            ['write'] for creation rights but not viewing access
1358
    """
1359
    cirrus_config.update(**config["CIRRUS_CFG"])
×
1360

1361
    google_project_id = google_project_id or cirrus_config.GOOGLE_PROJECT_ID
×
1362

1363
    # determine project where buckets are located
1364
    # default to same project, try to get storage creds project from key file
1365
    storage_creds_project_id = _get_storage_project_id() or google_project_id
×
1366

1367
    # default to read access
1368
    allowed_privileges = allowed_privileges or ["read", "write"]
×
1369

1370
    driver = get_SQLAlchemyDriver(db)
×
1371
    with driver.session as current_session:
×
1372
        # use storage creds to create bucket
1373
        # (default creds don't have permission)
1374
        bucket_db_entry = _create_or_update_google_bucket_and_db(
×
1375
            db_session=current_session,
1376
            name=name,
1377
            storage_class=storage_class,
1378
            requester_pays=requester_pays,
1379
            storage_creds_project_id=storage_creds_project_id,
1380
            public=public,
1381
            project_auth_id=project_auth_id,
1382
            access_logs_bucket=access_logs_bucket,
1383
        )
1384

1385
        if public is not None and not public:
×
1386
            for privilege in allowed_privileges:
×
1387
                _setup_google_bucket_access_group(
×
1388
                    db_session=current_session,
1389
                    google_bucket_name=name,
1390
                    bucket_db_id=bucket_db_entry.id,
1391
                    google_project_id=google_project_id,
1392
                    storage_creds_project_id=storage_creds_project_id,
1393
                    privileges=[privilege],
1394
                )
1395

1396

1397
def create_google_logging_bucket(name, storage_class=None, google_project_id=None):
1✔
1398
    cirrus_config.update(**config["CIRRUS_CFG"])
×
1399

1400
    # determine project where buckets are located if not provided, default
1401
    # to configured project if checking creds doesn't work
1402
    storage_creds_project_id = (
×
1403
        google_project_id
1404
        or _get_storage_project_id()
1405
        or cirrus_config.GOOGLE_PROJECT_ID
1406
    )
1407

1408
    manager = GoogleCloudManager(
×
1409
        storage_creds_project_id, creds=cirrus_config.configs["GOOGLE_STORAGE_CREDS"]
1410
    )
1411
    with manager as g_mgr:
×
1412
        g_mgr.create_or_update_bucket(
×
1413
            name,
1414
            storage_class=storage_class,
1415
            public=False,
1416
            requester_pays=False,
1417
            for_logging=True,
1418
        )
1419

1420
        logger.info(
×
1421
            "Successfully created Google Bucket {} "
1422
            "to store Access Logs.".format(name)
1423
        )
1424

1425

1426
def _get_storage_project_id():
1✔
1427
    """
1428
    Determine project where buckets are located.
1429
    Try to get storage creds project from key file
1430
    """
1431
    storage_creds_project_id = None
×
1432
    storage_creds_file = cirrus_config.configs["GOOGLE_STORAGE_CREDS"]
×
1433
    if os.path.exists(storage_creds_file):
×
1434
        with open(storage_creds_file) as creds_file:
×
1435
            storage_creds_project_id = json.load(creds_file).get("project_id")
×
1436
    return storage_creds_project_id
×
1437

1438

1439
def _create_or_update_google_bucket_and_db(
1✔
1440
    db_session,
1441
    name,
1442
    storage_class,
1443
    public,
1444
    requester_pays,
1445
    storage_creds_project_id,
1446
    project_auth_id,
1447
    access_logs_bucket,
1448
):
1449
    """
1450
    Handles creates the Google bucket and adding necessary db entry
1451
    """
1452
    manager = GoogleCloudManager(
×
1453
        storage_creds_project_id, creds=cirrus_config.configs["GOOGLE_STORAGE_CREDS"]
1454
    )
1455
    with manager as g_mgr:
×
1456
        g_mgr.create_or_update_bucket(
×
1457
            name,
1458
            storage_class=storage_class,
1459
            public=public,
1460
            requester_pays=requester_pays,
1461
            access_logs_bucket=access_logs_bucket,
1462
        )
1463

1464
        # add bucket to db
1465
        google_cloud_provider = _get_or_create_google_provider(db_session)
×
1466

1467
        bucket_db_entry = (
×
1468
            db_session.query(Bucket)
1469
            .filter_by(name=name, provider_id=google_cloud_provider.id)
1470
            .first()
1471
        )
1472
        if not bucket_db_entry:
×
1473
            bucket_db_entry = Bucket(name=name, provider_id=google_cloud_provider.id)
×
1474
            db_session.add(bucket_db_entry)
×
1475
            db_session.commit()
×
1476

1477
        logger.info("Successfully updated Google Bucket {}.".format(name))
×
1478

1479
        # optionally link this new bucket to an existing project
1480
        if project_auth_id:
×
1481
            project_db_entry = (
×
1482
                db_session.query(Project).filter_by(auth_id=project_auth_id).first()
1483
            )
1484
            if not project_db_entry:
×
1485
                logger.info(
×
1486
                    "No project with auth_id {} found. No linking "
1487
                    "occured.".format(project_auth_id)
1488
                )
1489
            else:
1490
                project_linkage = (
×
1491
                    db_session.query(ProjectToBucket)
1492
                    .filter_by(
1493
                        project_id=project_db_entry.id, bucket_id=bucket_db_entry.id
1494
                    )
1495
                    .first()
1496
                )
1497
                if not project_linkage:
×
1498
                    project_linkage = ProjectToBucket(
×
1499
                        project_id=project_db_entry.id,
1500
                        bucket_id=bucket_db_entry.id,
1501
                        privilege=["owner"],  # TODO What should this be???
1502
                    )
1503
                    db_session.add(project_linkage)
×
1504
                    db_session.commit()
×
1505
                logger.info(
×
1506
                    "Successfully linked project with auth_id {} "
1507
                    "to the bucket.".format(project_auth_id)
1508
                )
1509

1510
                # Add StorageAccess if it doesn't exist for the project
1511
                storage_access = (
×
1512
                    db_session.query(StorageAccess)
1513
                    .filter_by(
1514
                        project_id=project_db_entry.id,
1515
                        provider_id=google_cloud_provider.id,
1516
                    )
1517
                    .first()
1518
                )
1519
                if not storage_access:
×
1520
                    storage_access = StorageAccess(
×
1521
                        project_id=project_db_entry.id,
1522
                        provider_id=google_cloud_provider.id,
1523
                    )
1524
                    db_session.add(storage_access)
×
1525
                    db_session.commit()
×
1526

1527
    return bucket_db_entry
×
1528

1529

1530
def _setup_google_bucket_access_group(
1✔
1531
    db_session,
1532
    google_bucket_name,
1533
    bucket_db_id,
1534
    google_project_id,
1535
    storage_creds_project_id,
1536
    privileges,
1537
):
1538
    access_group = _create_google_bucket_access_group(
×
1539
        db_session, google_bucket_name, bucket_db_id, google_project_id, privileges
1540
    )
1541
    # use storage creds to update bucket iam
1542
    storage_manager = GoogleCloudManager(
×
1543
        storage_creds_project_id, creds=cirrus_config.configs["GOOGLE_STORAGE_CREDS"]
1544
    )
1545
    with storage_manager as g_mgr:
×
1546
        g_mgr.give_group_access_to_bucket(
×
1547
            access_group.email, google_bucket_name, access=privileges
1548
        )
1549

1550
    logger.info(
×
1551
        "Successfully set up Google Bucket Access Group {} "
1552
        "for Google Bucket {}.".format(access_group.email, google_bucket_name)
1553
    )
1554

1555
    return access_group
×
1556

1557

1558
def _create_google_bucket_access_group(
1✔
1559
    db_session, google_bucket_name, bucket_db_id, google_project_id, privileges
1560
):
1561
    access_group = None
1✔
1562
    prefix = config.get("GOOGLE_GROUP_PREFIX", "")
1✔
1563
    # use default creds for creating group and iam policies
1564
    with GoogleCloudManager(google_project_id) as g_mgr:
1✔
1565
        # create bucket access group
1566
        result = g_mgr.create_group(
1✔
1567
            name=prefix
1568
            + "_"
1569
            + google_bucket_name
1570
            + "_"
1571
            + "_".join(privileges)
1572
            + "_gbag"
1573
        )
1574
        group_email = result["email"]
1✔
1575

1576
        # add bucket group to db
1577
        access_group = (
1✔
1578
            db_session.query(GoogleBucketAccessGroup)
1579
            .filter_by(bucket_id=bucket_db_id, email=group_email)
1580
            .first()
1581
        )
1582
        if not access_group:
1✔
1583
            access_group = GoogleBucketAccessGroup(
1✔
1584
                bucket_id=bucket_db_id, email=group_email, privileges=privileges
1585
            )
1586
            db_session.add(access_group)
1✔
1587
            db_session.commit()
1✔
1588

1589
    return access_group
1✔
1590

1591

1592
def _get_or_create_google_provider(db_session):
1✔
1593
    google_cloud_provider = (
1✔
1594
        db_session.query(CloudProvider).filter_by(name="google").first()
1595
    )
1596
    if not google_cloud_provider:
1✔
1597
        google_cloud_provider = CloudProvider(
1✔
1598
            name="google", description="Google Cloud Platform", service="general"
1599
        )
1600
        db_session.add(google_cloud_provider)
1✔
1601
        db_session.commit()
1✔
1602
    return google_cloud_provider
1✔
1603

1604

1605
def link_external_bucket(db, name):
1✔
1606
    """
1607
    Link with bucket owned by an external party. This will create the bucket
1608
    in fence database and create a google group to access the bucket in both
1609
    Google and fence database.
1610
    The external party will need to add the google group read access to bucket
1611
    afterwards.
1612
    """
1613

1614
    cirrus_config.update(**config["CIRRUS_CFG"])
1✔
1615

1616
    google_project_id = cirrus_config.GOOGLE_PROJECT_ID
1✔
1617

1618
    db = get_SQLAlchemyDriver(db)
1✔
1619
    with db.session as current_session:
1✔
1620
        google_cloud_provider = _get_or_create_google_provider(current_session)
1✔
1621

1622
        # search for existing bucket based on name, try to use existing group email
1623
        existing_bucket = current_session.query(Bucket).filter_by(name=name).first()
1✔
1624
        if existing_bucket:
1✔
1625
            access_group = (
×
1626
                current_session.query(GoogleBucketAccessGroup)
1627
                .filter(GoogleBucketAccessGroup.privileges.any("read"))
1628
                .filter_by(bucket_id=existing_bucket.id)
1629
                .all()
1630
            )
1631
            if len(access_group) > 1:
×
1632
                raise Exception(
×
1633
                    f"Existing bucket {name} has more than 1 associated "
1634
                    "Google Bucket Access Group with privilege of 'read'. "
1635
                    "This is not expected and we cannot continue linking."
1636
                )
1637
            elif len(access_group) == 0:
×
1638
                raise Exception(
×
1639
                    f"Existing bucket {name} has no associated "
1640
                    "Google Bucket Access Group with privilege of 'read'. "
1641
                    "This is not expected and we cannot continue linking."
1642
                )
1643

1644
            access_group = access_group[0]
×
1645

1646
            email = access_group.email
×
1647

1648
            logger.warning(
×
1649
                f"bucket already exists with name: {name}, using existing group email: {email}"
1650
            )
1651

1652
            return email
×
1653

1654
        bucket_db_entry = Bucket(name=name, provider_id=google_cloud_provider.id)
1✔
1655
        current_session.add(bucket_db_entry)
1✔
1656
        current_session.commit()
1✔
1657
        privileges = ["read"]
1✔
1658

1659
        access_group = _create_google_bucket_access_group(
1✔
1660
            current_session,
1661
            name,
1662
            bucket_db_entry.id,
1663
            google_project_id,
1664
            privileges,
1665
        )
1666

1667
    logger.info("bucket access group email: {}".format(access_group.email))
1✔
1668
    return access_group.email
1✔
1669

1670

1671
def verify_user_registration(DB):
1✔
1672
    """
1673
    Validate user registration
1674
    """
1675
    cirrus_config.update(**config["CIRRUS_CFG"])
×
1676

1677
    validation_check(DB)
×
1678

1679

1680
def force_update_google_link(DB, username, google_email, expires_in=None):
1✔
1681
    """
1682
    WARNING: This function circumvents Google Auth flow, and should only be
1683
    used for internal testing!
1684
    WARNING: This function assumes that a user already has a proxy group!
1685

1686
    Adds user's google account to proxy group and/or updates expiration for
1687
    that google account's access.
1688
    WARNING: This assumes that provided arguments represent valid information.
1689
             This BLINDLY adds without verification. Do verification
1690
             before this.
1691
    Specifically, this ASSUMES that the proxy group provided belongs to the
1692
    given user and that the user has ALREADY authenticated to prove the
1693
    provided google_email is also their's.
1694

1695
    Args:
1696
        DB
1697
        username (str): Username to link with
1698
        google_email (str): Google email to link to
1699

1700
    Raises:
1701
        NotFound: Linked Google account not found
1702
        Unauthorized: Couldn't determine user
1703

1704
    Returns:
1705
        Expiration time of the newly updated google account's access
1706
    """
1707
    cirrus_config.update(**config["CIRRUS_CFG"])
×
1708

1709
    db = get_SQLAlchemyDriver(DB)
×
1710
    with db.session as session:
×
1711
        user_account = query_for_user(session=session, username=username)
×
1712

1713
        if user_account:
×
1714
            user_id = user_account.id
×
1715
            proxy_group_id = user_account.google_proxy_group_id
×
1716
        else:
1717
            raise Unauthorized(
×
1718
                "Could not determine authed user "
1719
                "from session. Unable to link Google account."
1720
            )
1721

1722
        user_google_account = (
×
1723
            session.query(UserGoogleAccount)
1724
            .filter(UserGoogleAccount.email == google_email)
1725
            .first()
1726
        )
1727
        if not user_google_account:
×
1728
            user_google_account = add_new_user_google_account(
×
1729
                user_id, google_email, session
1730
            )
1731

1732
        # time until the SA will lose bucket access
1733
        # by default: use configured time or 7 days
1734
        default_expires_in = config.get(
×
1735
            "GOOGLE_USER_SERVICE_ACCOUNT_ACCESS_EXPIRES_IN", 604800
1736
        )
1737
        # use expires_in from arg if it was provided and it was not greater than the default
1738
        expires_in = get_valid_expiration(
×
1739
            expires_in, max_limit=default_expires_in, default=default_expires_in
1740
        )
1741
        # convert expires_in to timestamp
1742
        expiration = int(time.time() + expires_in)
×
1743

1744
        force_update_user_google_account_expiration(
×
1745
            user_google_account, proxy_group_id, google_email, expiration, session
1746
        )
1747

1748
        session.commit()
×
1749

1750
        return expiration
×
1751

1752

1753
def notify_problem_users(db, emails, auth_ids, check_linking, google_project_id):
1✔
1754
    """
1755
    Builds a list of users (from provided list of emails) who do not
1756
    have access to any subset of provided auth_ids. Send email to users
1757
    informing them to get access to needed projects.
1758

1759
    db (string): database instance
1760
    emails (list(string)): list of emails to check for access
1761
    auth_ids (list(string)): list of project auth_ids to check that emails have access
1762
    check_linking (bool): flag for if emails should be checked for linked google email
1763
    """
1764
    email_users_without_access(db, auth_ids, emails, check_linking, google_project_id)
×
1765

1766

1767
def migrate_database():
1✔
1768
    alembic_main(["--raiseerr", "upgrade", "head"])
×
1769
    logger.info("Done.")
×
1770

1771

1772
def google_list_authz_groups(db):
1✔
1773
    """
1774
    Builds a list of Google authorization information which includes
1775
    the Google Bucket Access Group emails, Bucket(s) they're associated with, and
1776
    underlying Fence Project auth_id that provides access to that Bucket/Group
1777

1778
    db (string): database instance
1779
    """
1780
    driver = get_SQLAlchemyDriver(db)
×
1781

1782
    with driver.session as db_session:
×
1783
        google_authz = (
×
1784
            db_session.query(
1785
                GoogleBucketAccessGroup.email,
1786
                Bucket.name,
1787
                Project.auth_id,
1788
                ProjectToBucket,
1789
            )
1790
            .join(Project, ProjectToBucket.project_id == Project.id)
1791
            .join(Bucket, ProjectToBucket.bucket_id == Bucket.id)
1792
            .join(
1793
                GoogleBucketAccessGroup, GoogleBucketAccessGroup.bucket_id == Bucket.id
1794
            )
1795
        ).all()
1796

1797
        print("GoogleBucketAccessGroup.email, Bucket.name, Project.auth_id")
×
1798
        for item in google_authz:
×
1799
            print(", ".join(item[:-1]))
×
1800

1801
        return google_authz
×
1802

1803

1804
def access_token_polling_job(
1✔
1805
    db, chunk_size=None, concurrency=None, thread_pool_size=None, buffer_size=None
1806
):
1807
    """
1808
    Update visas and refresh tokens for users with valid visas and refresh tokens
1809

1810
    db (string): database instance
1811
    chunk_size (int): size of chunk of users we want to take from each iteration
1812
    concurrency (int): number of concurrent users going through the visa update flow
1813
    thread_pool_size (int): number of Docker container CPU used for jwt verifcation
1814
    buffer_size (int): max size of queue
1815
    """
1816
    driver = get_SQLAlchemyDriver(db)
×
1817
    job = Visa_Token_Update(
×
1818
        chunk_size=int(chunk_size) if chunk_size else None,
1819
        concurrency=int(concurrency) if concurrency else None,
1820
        thread_pool_size=int(thread_pool_size) if thread_pool_size else None,
1821
        buffer_size=int(buffer_size) if buffer_size else None,
1822
    )
1823
    with driver.session as db_session:
×
1824
        loop = asyncio.get_event_loop()
×
1825
        loop.run_until_complete(job.update_tokens(db_session))
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc