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

uc-cdis / fence / 19683980468

25 Nov 2025 09:02PM UTC coverage: 74.987% (+0.02%) from 74.969%
19683980468

push

github

web-flow
Update dependencies in fence after running deptry (#1302)

* Update dependencies in fence

* Remove Deprecated `local_settings.py`

* Delete references of `local_settings` and files that are linked to `local_settings.py`

* Update poetry lock

* Update poetry lock to resolve snyk errors

* Add a forced dependency on `idna`, to overcome a snyk dependency

* Replace `^` with `>=` and update gitignore

* Update `^` tp `>=` for all entries in pyproject.toml

* Update poetry lock

* Remove settings.py and test_settings.py completely

* Delete fence/settings.py

---------

Co-authored-by: Pauline Ribeyre <4224001+paulineribeyre@users.noreply.github.com>

8424 of 11234 relevant lines covered (74.99%)

0.75 hits per line

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

60.65
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.access_token_updater import TokenAndAuthUpdater
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,
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
    create_client,
61
    get_valid_expiration,
62
    generate_client_credentials,
63
    get_SQLAlchemyDriver,
64
)
65
from sqlalchemy.orm.attributes import flag_modified
1✔
66
from gen3authz.client.arborist.client import ArboristClient
1✔
67

68
logger = get_logger(__name__)
1✔
69

70

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

80

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

147

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

172

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

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

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

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

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

204

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

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

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

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

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

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

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

289

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

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

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

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

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

337

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

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

365

366
def get_default_init_syncer_inputs(authz_provider):
1✔
367
    DB = os.environ.get("FENCE_DB") or config.get("DB")
1✔
368

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

378
    storage_creds = config["STORAGE_CREDENTIALS"]
1✔
379

380
    return {
1✔
381
        "DB": DB,
382
        "arborist": arborist,
383
        "dbGaP": dbGaP,
384
        "STORAGE_CREDENTIALS": storage_creds,
385
    }
386

387

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

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

438
    project_mapping = None
1✔
439
    if projects:
1✔
440
        try:
×
441
            with open(projects, "r") as f:
×
442
                project_mapping = safe_load(f)
×
443
        except IOError:
×
444
            pass
×
445

446
    return UserSyncer(
1✔
447
        dbGaP,
448
        DB,
449
        project_mapping=project_mapping,
450
        storage_credentials=STORAGE_CREDENTIALS,
451
        is_sync_from_dbgap_server=is_sync_from_dbgap_server,
452
        sync_from_local_csv_dir=sync_from_local_csv_dir,
453
        sync_from_local_yaml_file=sync_from_local_yaml_file,
454
        arborist=arborist,
455
        folder=folder,
456
    )
457

458

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

486

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

513

514
def create_sample_data(DB, yaml_file_path):
1✔
515
    with open(yaml_file_path, "r") as f:
×
516
        data = safe_load(f)
×
517

518
    db = get_SQLAlchemyDriver(DB)
×
519
    with db.session as s:
×
520
        create_cloud_providers(s, data)
×
521
        create_projects(s, data)
×
522
        create_group(s, data)
×
523
        create_users_with_group(DB, s, data)
×
524

525

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

537

538
def create_projects(s, data):
1✔
539
    projects = data.get("projects", [])
1✔
540
    for project in projects:
1✔
541
        create_project(s, project)
1✔
542

543

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

587
    return project
1✔
588

589

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

637

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

652

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

661
        admin = data.get("admin", False)
×
662

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

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

690

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

700

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

709

710
def remove_expired_google_service_account_keys(db):
1✔
711
    cirrus_config.update(**config["CIRRUS_CFG"])
1✔
712

713
    db = get_SQLAlchemyDriver(db)
1✔
714
    with db.session as current_session:
1✔
715
        client_service_accounts = current_session.query(
1✔
716
            GoogleServiceAccount, Client
717
        ).filter(GoogleServiceAccount.client_id == Client.client_id)
718

719
        current_time = int(time.time())
1✔
720
        logger.info("Current time: {}\n".format(current_time))
1✔
721

722
        expired_sa_keys_for_users = current_session.query(
1✔
723
            GoogleServiceAccountKey
724
        ).filter(GoogleServiceAccountKey.expires <= current_time)
725

726
        with GoogleCloudManager() as g_mgr:
1✔
727
            # handle service accounts with default max expiration
728
            for service_account, client in client_service_accounts:
1✔
729
                g_mgr.handle_expired_service_account_keys(service_account.email)
1✔
730

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

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

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

780

781
def remove_expired_google_accounts_from_proxy_groups(db):
1✔
782
    cirrus_config.update(**config["CIRRUS_CFG"])
×
783

784
    db = get_SQLAlchemyDriver(db)
×
785
    with db.session as current_session:
×
786
        current_time = int(time.time())
×
787
        logger.info("Current time: {}".format(current_time))
×
788

789
        expired_accounts = current_session.query(UserGoogleAccountToProxyGroup).filter(
×
790
            UserGoogleAccountToProxyGroup.expires <= current_time
791
        )
792

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

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

834

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

850

851
def cleanup_expired_ga4gh_information(DB):
1✔
852
    """
853
    Remove any expired passports/visas from the database if they're expired.
854

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

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

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

888
        logger.info(
1✔
889
            f"Removed {num_deleted_records} expired GA4GHVisaV1 records from db."
890
        )
891

892

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

900
    driver = get_SQLAlchemyDriver(DB)
1✔
901
    with driver.session as session:
1✔
902
        current_time = int(time.time())
1✔
903

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

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

944
        logger.info(
1✔
945
            f"Removed {num_deleted_records} expired Google Access records from db and Google."
946
        )
947

948

949
def delete_expired_service_accounts(DB):
1✔
950
    """
951
    Delete all expired service accounts.
952
    """
953
    cirrus_config.update(**config["CIRRUS_CFG"])
1✔
954

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

983
                session.commit()
1✔
984

985

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

991
    Args:
992
        DB(str): db connection string
993

994
    Returns:
995
        None
996

997
    """
998
    cirrus_config.update(**config["CIRRUS_CFG"])
1✔
999

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

1019
                for member in members:
1✔
1020
                    if member.get("type") == "GROUP":
1✔
1021
                        _verify_google_group_member(session, access_group, member)
1✔
1022
                    elif member.get("type") == "USER":
1✔
1023
                        _verify_google_service_account_member(
1✔
1024
                            session, access_group, member
1025
                        )
1026

1027

1028
def _verify_google_group_member(session, access_group, member):
1✔
1029
    """
1030
    Delete if the member which is a google group is not in Fence.
1031

1032
    Args:
1033
        session(Session): db session
1034
        access_group(GoogleBucketAccessGroup): access group
1035
        member(dict): group member info
1036

1037
    Returns:
1038
        None
1039

1040
    """
1041
    account_emails = [
1✔
1042
        granted_group.proxy_group.email
1043
        for granted_group in (
1044
            session.query(GoogleProxyGroupToGoogleBucketAccessGroup)
1045
            .filter_by(access_group_id=access_group.id)
1046
            .all()
1047
        )
1048
    ]
1049

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

1067

1068
def _verify_google_service_account_member(session, access_group, member):
1✔
1069
    """
1070
    Delete if the member which is a service account is not in Fence.
1071

1072
    Args:
1073
        session(session): db session
1074
        access_group(GoogleBucketAccessGroup): access group
1075
        members(dict): service account member info
1076

1077
    Returns:
1078
        None
1079

1080
    """
1081

1082
    account_emails = [
1✔
1083
        account.service_account.email
1084
        for account in (
1085
            session.query(ServiceAccountToGoogleBucketAccessGroup)
1086
            .filter_by(access_group_id=access_group.id)
1087
            .all()
1088
        )
1089
    ]
1090

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

1108

1109
class JWTCreator(object):
1✔
1110
    required_kwargs = ["kid", "private_key", "username", "scopes"]
1✔
1111
    all_kwargs = required_kwargs + ["expires_in", "client_id"]
1✔
1112

1113
    default_expiration = 3600
1✔
1114

1115
    def __init__(self, db, base_url, **kwargs):
1✔
1116
        self.db = db
1✔
1117
        self.base_url = base_url
1✔
1118

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

1127
        for required_kwarg in self.required_kwargs:
1✔
1128
            if required_kwarg not in kwargs:
1✔
1129
                raise ValueError("missing required argument: " + required_kwarg)
×
1130

1131
        # Set attributes on this object from the kwargs.
1132
        for kwarg_name in self.all_kwargs:
1✔
1133
            setattr(self, kwarg_name, kwargs.get(kwarg_name))
1✔
1134

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

1145
        self.expires_in = kwargs.get("expires_in") or self.default_expiration
1✔
1146

1147
    def create_access_token(self):
1✔
1148
        """
1149
        Create a new access token.
1150

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

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

1171
    def create_refresh_token(self):
1✔
1172
        """
1173
        Create a new refresh token and add its entry to the database.
1174

1175
        Return:
1176
            JWTResult: the refresh token result
1177
        """
1178
        driver = get_SQLAlchemyDriver(self.db)
1✔
1179
        with driver.session as current_session:
1✔
1180
            user = query_for_user(session=current_session, username=self.username)
1✔
1181

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

1200
            current_session.add(
1✔
1201
                UserRefreshToken(
1202
                    jti=jwt_result.claims["jti"],
1203
                    userid=user.id,
1204
                    expires=jwt_result.claims["exp"],
1205
                )
1206
            )
1207

1208
            return jwt_result
1✔
1209

1210

1211
def link_bucket_to_project(db, bucket_id, bucket_provider, project_auth_id):
1✔
1212
    """
1213
    Associate a bucket to a specific project (with provided auth_id).
1214

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

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

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

1268
        if not bucket_db_entry:
×
1269
            raise NameError('No bucket with id or name "{}" exists.'.format(bucket_id))
×
1270

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

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

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

1311

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

1326
    If the bucket is not public, this will also create a Google Bucket Access
1327
    Group(s) to control access to the new bucket. In order to give access
1328
    to a new user, simply add them to the Google Bucket Access Group.
1329

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

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

1357
    google_project_id = google_project_id or cirrus_config.GOOGLE_PROJECT_ID
×
1358

1359
    # determine project where buckets are located
1360
    # default to same project, try to get storage creds project from key file
1361
    storage_creds_project_id = _get_storage_project_id() or google_project_id
×
1362

1363
    # default to read access
1364
    allowed_privileges = allowed_privileges or ["read", "write"]
×
1365

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

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

1392

1393
def create_google_logging_bucket(name, storage_class=None, google_project_id=None):
1✔
1394
    cirrus_config.update(**config["CIRRUS_CFG"])
×
1395

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

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

1416
        logger.info(
×
1417
            "Successfully created Google Bucket {} "
1418
            "to store Access Logs.".format(name)
1419
        )
1420

1421

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

1434

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

1460
        # add bucket to db
1461
        google_cloud_provider = _get_or_create_google_provider(db_session)
×
1462

1463
        bucket_db_entry = (
×
1464
            db_session.query(Bucket)
1465
            .filter_by(name=name, provider_id=google_cloud_provider.id)
1466
            .first()
1467
        )
1468
        if not bucket_db_entry:
×
1469
            bucket_db_entry = Bucket(name=name, provider_id=google_cloud_provider.id)
×
1470
            db_session.add(bucket_db_entry)
×
1471
            db_session.commit()
×
1472

1473
        logger.info("Successfully updated Google Bucket {}.".format(name))
×
1474

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

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

1523
    return bucket_db_entry
×
1524

1525

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

1546
    logger.info(
×
1547
        "Successfully set up Google Bucket Access Group {} "
1548
        "for Google Bucket {}.".format(access_group.email, google_bucket_name)
1549
    )
1550

1551
    return access_group
×
1552

1553

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

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

1585
    return access_group
1✔
1586

1587

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

1600

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

1610
    cirrus_config.update(**config["CIRRUS_CFG"])
1✔
1611

1612
    google_project_id = cirrus_config.GOOGLE_PROJECT_ID
1✔
1613

1614
    db = get_SQLAlchemyDriver(db)
1✔
1615
    with db.session as current_session:
1✔
1616
        google_cloud_provider = _get_or_create_google_provider(current_session)
1✔
1617

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

1640
            access_group = access_group[0]
×
1641

1642
            email = access_group.email
×
1643

1644
            logger.warning(
×
1645
                f"bucket already exists with name: {name}, using existing group email: {email}"
1646
            )
1647

1648
            return email
×
1649

1650
        bucket_db_entry = Bucket(name=name, provider_id=google_cloud_provider.id)
1✔
1651
        current_session.add(bucket_db_entry)
1✔
1652
        current_session.commit()
1✔
1653
        privileges = ["read"]
1✔
1654

1655
        access_group = _create_google_bucket_access_group(
1✔
1656
            current_session,
1657
            name,
1658
            bucket_db_entry.id,
1659
            google_project_id,
1660
            privileges,
1661
        )
1662

1663
    logger.info("bucket access group email: {}".format(access_group.email))
1✔
1664
    return access_group.email
1✔
1665

1666

1667
def verify_user_registration(DB):
1✔
1668
    """
1669
    Validate user registration
1670
    """
1671
    cirrus_config.update(**config["CIRRUS_CFG"])
×
1672

1673
    validation_check(DB)
×
1674

1675

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

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

1691
    Args:
1692
        DB
1693
        username (str): Username to link with
1694
        google_email (str): Google email to link to
1695

1696
    Raises:
1697
        NotFound: Linked Google account not found
1698
        Unauthorized: Couldn't determine user
1699

1700
    Returns:
1701
        Expiration time of the newly updated google account's access
1702
    """
1703
    cirrus_config.update(**config["CIRRUS_CFG"])
×
1704

1705
    db = get_SQLAlchemyDriver(DB)
×
1706
    with db.session as session:
×
1707
        user_account = query_for_user(session=session, username=username)
×
1708

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

1718
        user_google_account = (
×
1719
            session.query(UserGoogleAccount)
1720
            .filter(UserGoogleAccount.email == google_email)
1721
            .first()
1722
        )
1723
        if not user_google_account:
×
1724
            user_google_account = add_new_user_google_account(
×
1725
                user_id, google_email, session
1726
            )
1727

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

1740
        force_update_user_google_account_expiration(
×
1741
            user_google_account, proxy_group_id, google_email, expiration, session
1742
        )
1743

1744
        session.commit()
×
1745

1746
        return expiration
×
1747

1748

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

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

1762

1763
def migrate_database():
1✔
1764
    alembic_main(["--raiseerr", "upgrade", "head"])
×
1765
    logger.info("Done.")
×
1766

1767

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

1774
    db (string): database instance
1775
    """
1776
    driver = get_SQLAlchemyDriver(db)
×
1777

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

1793
        print("GoogleBucketAccessGroup.email, Bucket.name, Project.auth_id")
×
1794
        for item in google_authz:
×
1795
            print(", ".join(item[:-1]))
×
1796

1797
        return google_authz
×
1798

1799

1800
def access_token_polling_job(
1✔
1801
    db, chunk_size=None, concurrency=None, thread_pool_size=None, buffer_size=None
1802
):
1803
    """
1804
    Update visas and refresh tokens for users with valid visas and refresh tokens
1805

1806
    db (string): database instance
1807
    chunk_size (int): size of chunk of users we want to take from each iteration
1808
    concurrency (int): number of concurrent users going through the visa update flow
1809
    thread_pool_size (int): number of Docker container CPU used for jwt verifcation
1810
    buffer_size (int): max size of queue
1811
    """
1812
    # Instantiating a new client here because the existing
1813
    # client uses authz_provider
1814
    arborist = ArboristClient(
×
1815
        arborist_base_url=config["ARBORIST"],
1816
        logger=get_logger("user_syncer.arborist_client"),
1817
    )
1818
    driver = get_SQLAlchemyDriver(db)
×
1819
    job = TokenAndAuthUpdater(
×
1820
        chunk_size=int(chunk_size) if chunk_size else None,
1821
        concurrency=int(concurrency) if concurrency else None,
1822
        thread_pool_size=int(thread_pool_size) if thread_pool_size else None,
1823
        buffer_size=int(buffer_size) if buffer_size else None,
1824
        arborist=arborist,
1825
    )
1826
    with driver.session as db_session:
×
1827
        loop = asyncio.get_event_loop()
×
1828
        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

© 2026 Coveralls, Inc