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

uc-cdis / fence / 23566471596

25 Mar 2026 10:04PM UTC coverage: 75.067% (+0.03%) from 75.033%
23566471596

Pull #1339

github

pieterlukasse
feat: add tests for new "reactivate user" endpoint
Pull Request #1339: feat: add new admin endpoint to allow for reactivation of soft-deleted users

8457 of 11266 relevant lines covered (75.07%)

0.75 hits per line

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

85.16
fence/resources/admin/admin_users.py
1
from cdislogging import get_logger
1✔
2
from gen3cirrus import GoogleCloudManager
1✔
3
from gen3cirrus.google_cloud.utils import get_proxy_group_name_for_user
1✔
4
from fence.config import config
1✔
5
from fence.errors import NotFound, UserError, UnavailableError
1✔
6
from fence.models import (
1✔
7
    GoogleProxyGroup,
8
    GoogleProxyGroupToGoogleBucketAccessGroup,
9
    GoogleServiceAccount,
10
    GoogleServiceAccountKey,
11
    User,
12
    UserGoogleAccount,
13
    UserGoogleAccountToProxyGroup,
14
    query_for_user,
15
    IdentityProvider,
16
    Tag,
17
)
18
from fence.resources import group as gp, project as pj, user as us, userdatamodel as udm
1✔
19
from flask import current_app as capp
1✔
20

21

22
__all__ = [
1✔
23
    "connect_user_to_project",
24
    "get_user_info",
25
    "get_all_users",
26
    "get_user_groups",
27
    "create_user",
28
    "update_user",
29
    "add_user_to_projects",
30
    "delete_user",
31
    "soft_delete_user",
32
    "reactivate_soft_deleted_user",
33
    "add_user_to_groups",
34
    "connect_user_to_group",
35
    "remove_user_from_groups",
36
    "disconnect_user_from_group",
37
    "remove_user_from_project",
38
]
39

40

41
logger = get_logger(__name__)
1✔
42

43

44
def connect_user_to_project(current_session, usr, project=None):
1✔
45
    """
46
    Create a user name for the specific project.
47
    Returns a dictionary.
48
    """
49
    datamodel_user = udm.create_user_by_username_project(current_session, usr, project)
1✔
50

51
    proj = datamodel_user["project"]
1✔
52
    priv = datamodel_user["privileges"]
1✔
53
    cloud_providers = udm.get_cloud_providers_from_project(current_session, proj.id)
1✔
54
    response = []
1✔
55
    for provider in cloud_providers:
1✔
56
        capp.storage_manager.get_or_create_user(provider.backend, usr)
×
57
        buckets = udm.get_buckets_by_project_cloud_provider(
×
58
            current_session, proj.id, provider.id
59
        )
60
        for bucket in buckets["buckets"]:
×
61
            try:
×
62
                capp.storage_manager.update_bucket_acl(
×
63
                    provider.backend, bucket, (usr, priv.privilege)
64
                )
65
                msg = "Success: user access" " created for a bucket in the project {0}"
×
66
                response.append(msg.format(proj.name))
×
67
            except Exception:
×
68
                msg = "Error user access not" " created for project {0} and bucket {2}"
×
69
                response.append(msg.format(proj.name, bucket["name"]))
×
70
    return response
1✔
71

72

73
def get_user_info(current_session, username):
1✔
74
    return us.get_user_info(current_session, username)
1✔
75

76

77
def get_all_users(current_session):
1✔
78
    users = udm.get_all_users(current_session)
1✔
79
    users_names = []
1✔
80
    for user in users:
1✔
81
        new_user = {}
1✔
82
        new_user["username"] = user.username
1✔
83
        if user.is_admin:
1✔
84
            new_user["role"] = "admin"
1✔
85
        else:
86
            new_user["role"] = "user"
1✔
87
        users_names.append(new_user)
1✔
88
    return {"users": users_names}
1✔
89

90

91
def get_user_groups(current_session, username):
1✔
92
    user_groups = us.get_user_groups(current_session, username)["groups"]
1✔
93
    user_groups_info = []
1✔
94
    for group in user_groups:
1✔
95
        user_groups_info.append(gp.get_group_info(current_session, group))
1✔
96
    return {"groups": user_groups_info}
1✔
97

98

99
def create_user(
1✔
100
    current_session,
101
    username,
102
    email,
103
    display_name=None,
104
    phone_number=None,
105
    idp_name=None,
106
    tags=None,
107
):
108
    """
109
    Create a user for all the projects or groups in the list.
110
    If the user already exists, to avoid unadvertedly changing it, we suggest update
111
    Returns a dictionary.
112
    """
113
    if not username:
1✔
114
        raise UserError(("Error: Please provide a username"))
1✔
115
    try:
1✔
116
        usr = us.get_user(current_session, username)
1✔
117
        logger.debug(f"User already exists for: {username}")
1✔
118
        raise UserError(
1✔
119
            (
120
                "Error: user already exist. If this is not a"
121
                " mistake, please, retry using update"
122
            )
123
        )
124
    except NotFound:
1✔
125
        logger.debug(f"User not found for: {username}. Checking again ignoring case...")
1✔
126
        user_list = [
1✔
127
            user["username"].upper() for user in get_all_users(current_session)["users"]
128
        ]
129
        if username.upper() in user_list:
1✔
130
            logger.debug(f"User already exists for: {username}")
×
131
            raise UserError(
×
132
                (
133
                    "Error: user with a name with the same combination/order "
134
                    "of characters already exists. Please remove this other user"
135
                    " or modify the new one. Contact us in case of doubt"
136
                )
137
            )
138
        logger.debug(f"User does not yet exist for: {username}. Creating a new one...")
1✔
139
        email_add = email
1✔
140
        usr = User(username=username, active=True, email=email_add)
1✔
141
        usr.display_name = display_name
1✔
142
        usr.phone_number = phone_number
1✔
143

144
        if idp_name:
1✔
145
            logger.debug(f"User {username} idp set to {idp_name}")
1✔
146
            idp = (
1✔
147
                current_session.query(IdentityProvider)
148
                .filter(IdentityProvider.name == idp_name)
149
                .first()
150
            )
151
            if not idp:
1✔
152
                idp = IdentityProvider(name=idp_name)
1✔
153
            usr.identity_provider = idp
1✔
154
        if tags:
1✔
155
            logger.debug(f"Setting {len(tags)} tags for user {username}...")
1✔
156
            for key, value in tags.items():
1✔
157
                tag = Tag(key=key, value=value)
1✔
158
                usr.tags.append(tag)
1✔
159

160
        logger.debug(f"Adding user {username}...")
1✔
161
        current_session.add(usr)
1✔
162
        current_session.commit()
1✔
163
        logger.debug(f"Success adding user {username}. Returning...")
1✔
164
        return us.get_user_info(current_session, username)
1✔
165

166

167
def update_user(current_session, username, role, email, new_name):
1✔
168
    usr = us.get_user(current_session, username)
1✔
169
    user_list = [
1✔
170
        user["username"].upper() for user in get_all_users(current_session)["users"]
171
    ]
172
    if (
1✔
173
        new_name
174
        and new_name.upper() in user_list
175
        and not username.upper() == new_name.upper()
176
    ):
177
        raise UserError(
1✔
178
            (
179
                "Error: user with a name with the same combination/order "
180
                "of characters already exists. Please remove this other user"
181
                " or modify the new one. Contact us in case of doubt"
182
            )
183
        )
184
    usr.email = email or usr.email
1✔
185
    if role:
1✔
186
        usr.is_admin = role == "admin"
1✔
187
    usr.username = new_name or usr.username
1✔
188
    return us.get_user_info(current_session, usr.username)
1✔
189

190

191
def add_user_to_projects(current_session, username, projects=None):
1✔
192
    if not projects:
×
193
        projects = []
×
194
    usr = us.get_user(current_session, username)
×
195
    responses = []
×
196
    for proj in projects:
×
197
        try:
×
198
            response = connect_user_to_project(current_session, usr, proj)
×
199
            responses.append(response)
×
200
        except Exception as e:
×
201
            current_session.rollback()
×
202
            raise e
×
203
    return {"result": responses}
×
204

205

206
def delete_google_service_accounts_and_keys(current_session, gcm, gpg_email):
1✔
207
    """
208
    Delete from both Google and Fence all Google service accounts and
209
    service account keys associated with one Google proxy group.
210
    """
211
    logger.debug("Deleting all associated service accounts...")
1✔
212

213
    # Referring to cirrus for list of SAs. You _could_ refer to fence db instead.
214
    service_account_emails = gcm.get_service_accounts_from_group(gpg_email)
1✔
215

216
    def raise_unavailable(sae):
1✔
217
        raise UnavailableError(
1✔
218
            "Error: Google unable to delete service account {}. Aborting".format(sae)
219
        )
220

221
    for sae in service_account_emails:
1✔
222
        # Upon deletion of a service account, Google will
223
        # automatically delete all key IDs associated with that
224
        # service account. So we skip doing that here.
225
        logger.debug(
1✔
226
            "Attempting to delete Google service account with email {} "
227
            "along with all associated service account keys...".format(sae)
228
        )
229
        try:
1✔
230
            r = gcm.delete_service_account(sae)
1✔
231
        except Exception as e:
×
232
            logger.exception(e)
×
233
            raise_unavailable(sae)
×
234

235
        if r != {}:
1✔
236
            logger.exception(r)
1✔
237
            raise_unavailable(sae)
1✔
238

239
        logger.info(
1✔
240
            "Google service account with email {} successfully removed "
241
            "from Google, along with all associated service account keys.".format(sae)
242
        )
243
        logger.debug(
1✔
244
            "Attempting to clear service account records from Fence database..."
245
        )
246
        sa = (
1✔
247
            current_session.query(GoogleServiceAccount)
248
            .filter(GoogleServiceAccount.email == sae)
249
            .first()
250
            # one_or_none() would be better, but is only in sqlalchemy 1.0.9
251
        )
252
        if sa:
1✔
253
            sa_keys = (
1✔
254
                current_session.query(GoogleServiceAccountKey)
255
                .filter(GoogleServiceAccountKey.service_account_id == sa.id)
256
                .all()
257
            )
258
            for sak in sa_keys:
1✔
259
                current_session.delete(sak)
1✔
260
            current_session.delete(sa)
1✔
261
            current_session.commit()
1✔
262
            logger.info(
1✔
263
                "Records for service account {} successfully cleared from Fence database.".format(
264
                    sae
265
                )
266
            )
267
        else:
268
            logger.info(
1✔
269
                "Records for service account {} NOT FOUND in Fence database. "
270
                "Continuing anyway.".format(sae)
271
            )
272

273

274
def delete_google_proxy_group(
1✔
275
    current_session, gcm, gpg_email, google_proxy_group_from_fence_db, user
276
):
277
    """
278
    Delete a Google proxy group from both Google and Fence.
279

280
    google_proxy_group_from_fence_db is the GPG row in Fence. If there is ever the case where
281
    the GPG exists in Google but is not in the Fence db, google_proxy_group_from_fence_db will be None
282
    but there will still be a GPG to delete from Google.
283

284
    user is the User row in Fence.
285
    """
286
    # Google will automatically remove
287
    # this proxy group from all GBAGs the proxy group is a member of.
288
    # So we skip doing that here.
289
    logger.debug(
1✔
290
        "Attempting to delete Google proxy group with email {}...".format(gpg_email)
291
    )
292

293
    def raise_unavailable(gpg_email):
1✔
294
        raise UnavailableError(
1✔
295
            "Error: Google unable to delete proxy group {}. Aborting".format(gpg_email)
296
        )
297

298
    try:
1✔
299
        r = gcm.delete_group(gpg_email)
1✔
300
    except Exception as e:
×
301
        logger.exception(e)
×
302
        raise_unavailable(gpg_email)
×
303

304
    if r != {}:
1✔
305
        logger.exception(r)
1✔
306
        raise_unavailable(gpg_email)
1✔
307

308
    logger.info(
1✔
309
        "Google proxy group with email {} successfully removed from Google.".format(
310
            gpg_email
311
        )
312
    )
313
    if google_proxy_group_from_fence_db:
1✔
314
        # (else it was google_proxy_group_from_google and there is nothing to delete in Fence db.)
315
        logger.debug("Attempting to clear proxy group records from Fence database...")
1✔
316
        logger.debug(
1✔
317
            "Deleting rows in {}...".format(
318
                GoogleProxyGroupToGoogleBucketAccessGroup.__tablename__
319
            )
320
        )
321
        gpg_to_gbag = (
1✔
322
            current_session.query(GoogleProxyGroupToGoogleBucketAccessGroup)
323
            .filter(
324
                GoogleProxyGroupToGoogleBucketAccessGroup.proxy_group_id
325
                == google_proxy_group_from_fence_db.id
326
            )
327
            .all()
328
        )
329
        for row in gpg_to_gbag:
1✔
330
            current_session.delete(row)
1✔
331
        logger.debug(
1✔
332
            "Deleting rows in {}...".format(UserGoogleAccountToProxyGroup.__tablename__)
333
        )
334
        uga_to_pg = (
1✔
335
            current_session.query(UserGoogleAccountToProxyGroup)
336
            .filter(
337
                UserGoogleAccountToProxyGroup.proxy_group_id
338
                == google_proxy_group_from_fence_db.id
339
            )
340
            .all()
341
        )
342
        for row in uga_to_pg:
1✔
343
            current_session.delete(row)
1✔
344
        logger.debug("Deleting rows in {}...".format(UserGoogleAccount.__tablename__))
1✔
345
        uga = (
1✔
346
            current_session.query(UserGoogleAccount)
347
            .filter(UserGoogleAccount.user_id == user.id)
348
            .all()
349
        )
350
        for row in uga:
1✔
351
            current_session.delete(row)
1✔
352
        logger.debug("Deleting row in {}...".format(GoogleProxyGroup.__tablename__))
1✔
353
        current_session.delete(google_proxy_group_from_fence_db)
1✔
354
        current_session.commit()
1✔
355
        logger.info(
1✔
356
            "Records for Google proxy group {} successfully cleared from Fence "
357
            "database, along with associated user Google accounts.".format(gpg_email)
358
        )
359
        logger.info("Done with Google deletions.")
1✔
360

361

362
def soft_delete_user(current_session, username):
1✔
363
    """
364
    Soft-remove the user by marking it as active=False.
365
    """
366
    logger.debug(f"Soft-delete user '{username}'")
1✔
367
    usr = us.get_user(current_session, username)
1✔
368
    usr.active = False
1✔
369
    current_session.commit()
1✔
370
    return us.get_user_info(current_session, usr.username)
1✔
371

372

373
def reactivate_soft_deleted_user(current_session, username):
1✔
374
    """
375
    Reverts the soft-remove done by soft_delete_user above
376
    by marking user as active=True.
377
    """
378
    usr = us.get_user(current_session, username)
1✔
379
    if (usr.active):
1✔
380
        raise UserError(("Error: user is already active"))
1✔
381

382
    logger.debug(f"Reactivate soft-deleted user '{username}'")
1✔
383
    usr.active = True
1✔
384
    current_session.commit()
1✔
385
    return us.get_user_info(current_session, usr.username)
1✔
386

387

388
def delete_user(current_session, username):
1✔
389
    """
390
    Remove a user from both the userdatamodel
391
    and the associated storage for that project/bucket.
392
    Returns a dictionary.
393

394
    The Fence db may not always be in perfect sync with Google.  We err on the
395
    side of safety (we prioritise making sure the user is really cleared out of
396
    Google to prevent unauthorized data access issues; we prefer cirrus/Google
397
    over the Fence db as the source of truth.) So, if the Fence-Google sync
398
    situation changes, do edit this code accordingly.
399
    """
400

401
    logger.debug("Beginning delete user.")
1✔
402

403
    with GoogleCloudManager() as gcm:
1✔
404
        # Delete user's service accounts, SA keys, user proxy group from Google.
405
        # Noop if Google not in use.
406

407
        user = query_for_user(session=current_session, username=username)
1✔
408
        if not user:
1✔
409
            raise NotFound("user name {} not found".format(username))
×
410

411
        logger.debug("Found user in Fence db: {}".format(user))
1✔
412

413
        # First: Find this user's proxy group.
414
        google_proxy_group_from_fence_db = (
1✔
415
            current_session.query(GoogleProxyGroup)
416
            .filter(GoogleProxyGroup.id == user.google_proxy_group_id)
417
            .first()
418
            # one_or_none() would be better, but is only in sqlalchemy 1.0.9
419
        )
420

421
        if google_proxy_group_from_fence_db:
1✔
422
            gpg_email = google_proxy_group_from_fence_db.email
1✔
423
            logger.debug("Found Google proxy group in Fence db: {}".format(gpg_email))
1✔
424
        else:
425
            # Construct the proxy group name that would have been used
426
            # and check if it exists in cirrus, in case Fence db just
427
            # didn't know about it.
428
            logger.debug(
1✔
429
                "Could not find Google proxy group for this user in Fence db. Checking gen3cirrus..."
430
            )
431
            pgname = get_proxy_group_name_for_user(
1✔
432
                user.id, user.username, prefix=config["GOOGLE_GROUP_PREFIX"]
433
            )
434
            google_proxy_group_from_google = gcm.get_group(pgname)
1✔
435
            gpg_email = (
1✔
436
                google_proxy_group_from_google.get("email")
437
                if google_proxy_group_from_google
438
                else None
439
            )
440

441
        if not gpg_email:
1✔
442
            logger.info(
1✔
443
                "Could not find Google proxy group for user in Fence db or in gen3cirrus. "
444
                "Assuming Google not in use as IdP. Proceeding with Fence deletes."
445
            )
446
        else:
447
            logger.debug(
1✔
448
                "Found Google proxy group email of user to delete: {}."
449
                "Proceeding with Google deletions.".format(gpg_email)
450
            )
451
            # Note: Fence db deletes here are interleaved with Google deletes.
452
            # This is so that if (for example) Google succeeds in deleting one SA
453
            # and then fails on the next, and the deletion process aborts, there
454
            # will not remain a record in Fence of the first, now-nonexistent SA.
455

456
            delete_google_service_accounts_and_keys(current_session, gcm, gpg_email)
1✔
457
            delete_google_proxy_group(
1✔
458
                current_session, gcm, gpg_email, google_proxy_group_from_fence_db, user
459
            )
460

461
    logger.debug("Deleting all user data from Fence database...")
1✔
462
    current_session.delete(user)
1✔
463
    current_session.commit()
1✔
464
    logger.info("Deleted all user data from Fence database. Returning.")
1✔
465

466
    return {"result": "success"}
1✔
467

468

469
def add_user_to_groups(current_session, username, groups=None):
1✔
470
    if not groups:
1✔
471
        groups = []
×
472
    usr = us.get_user(current_session, username)
1✔
473
    responses = []
1✔
474
    for groupname in groups:
1✔
475
        try:
1✔
476
            response = connect_user_to_group(current_session, usr, groupname)
1✔
477
            responses.append(response)
1✔
478
        except Exception as e:
×
479
            current_session.rollback()
×
480
            raise e
×
481
    return {"result": responses}
1✔
482

483

484
def connect_user_to_group(current_session, usr, groupname=None):
1✔
485
    grp = gp.get_group(current_session, groupname)
1✔
486
    if not grp:
1✔
487
        raise UserError(("Group {0} doesn't exist".format(groupname)))
×
488
    else:
489
        responses = []
1✔
490
        responses.append(gp.connect_user_to_group(current_session, usr, grp))
1✔
491
        projects = gp.get_group_projects(current_session, groupname)
1✔
492
        projects_data = [
1✔
493
            pj.get_project(current_session, project).auth_id for project in projects
494
        ]
495
        projects_list = [
1✔
496
            {"auth_id": auth_id, "privilege": ["read"]} for auth_id in projects_data
497
        ]
498
        for project in projects_list:
1✔
499
            connect_user_to_project(current_session, usr, project)
1✔
500
        return responses
1✔
501

502

503
def remove_user_from_groups(current_session, username, groups=None):
1✔
504
    if not groups:
1✔
505
        groups = []
×
506
    usr = us.get_user(current_session, username)
1✔
507
    user_groups = us.get_user_groups(current_session, username)["groups"]
1✔
508
    groups_to_keep = [x for x in user_groups if x not in groups]
1✔
509

510
    projects_to_keep = {
1✔
511
        item
512
        for sublist in [
513
            gp.get_group_projects(current_session, x) for x in groups_to_keep
514
        ]
515
        for item in sublist
516
    }
517

518
    projects_to_remove = {
1✔
519
        item
520
        for sublist in [gp.get_group_projects(current_session, x) for x in groups]
521
        for item in sublist
522
        if item not in projects_to_keep
523
    }
524

525
    responses = []
1✔
526
    for groupname in groups:
1✔
527
        try:
1✔
528
            response = disconnect_user_from_group(current_session, usr, groupname)
1✔
529
            responses.append(response)
1✔
530
        except Exception as e:
1✔
531
            current_session.rollback()
1✔
532
            raise e
1✔
533
    for project in projects_to_remove:
1✔
534
        remove_user_from_project(current_session, usr, project)
1✔
535
    return {"result": responses}
1✔
536

537

538
def disconnect_user_from_group(current_session, usr, groupname):
1✔
539
    grp = gp.get_group(current_session, groupname)
1✔
540
    if not grp:
1✔
541
        return {"warning": ("Group {0} doesn't exist".format(groupname))}
×
542

543
    response = gp.remove_user_from_group(current_session, usr, grp)
1✔
544
    projects = gp.get_group_projects(current_session, groupname)
1✔
545
    projects_data = [
1✔
546
        pj.get_project(current_session, project).auth_id for project in projects
547
    ]
548
    return response
1✔
549

550

551
def remove_user_from_project(current_session, usr, project_name):
1✔
552
    proj = pj.get_project(current_session, project_name)
1✔
553
    us.remove_user_from_project(current_session, usr, proj)
1✔
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