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

uc-cdis / fence / 12324526000

20 Nov 2024 06:58PM UTC coverage: 75.24% (-0.004%) from 75.244%
12324526000

push

github

web-flow
Merge pull request #1202 from uc-cdis/feat/remove_role_from_admin_endpoints

Feat: remove role from POST /admin/user endpoint

7852 of 10436 relevant lines covered (75.24%)

0.75 hits per line

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

84.68
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
    "add_user_to_groups",
33
    "connect_user_to_group",
34
    "remove_user_from_groups",
35
    "disconnect_user_from_group",
36
    "remove_user_from_project",
37
]
38

39

40
logger = get_logger(__name__)
1✔
41

42

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

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

71

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

75

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

89

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

97

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

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

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

165

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

189

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

204

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

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

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

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

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

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

272

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

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

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

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

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

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

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

360

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

371

372
def delete_user(current_session, username):
1✔
373
    """
374
    Remove a user from both the userdatamodel
375
    and the associated storage for that project/bucket.
376
    Returns a dictionary.
377

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

385
    logger.debug("Beginning delete user.")
1✔
386

387
    with GoogleCloudManager() as gcm:
1✔
388
        # Delete user's service accounts, SA keys, user proxy group from Google.
389
        # Noop if Google not in use.
390

391
        user = query_for_user(session=current_session, username=username)
1✔
392
        if not user:
1✔
393
            raise NotFound("user name {} not found".format(username))
×
394

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

397
        # First: Find this user's proxy group.
398
        google_proxy_group_from_fence_db = (
1✔
399
            current_session.query(GoogleProxyGroup)
400
            .filter(GoogleProxyGroup.id == user.google_proxy_group_id)
401
            .first()
402
            # one_or_none() would be better, but is only in sqlalchemy 1.0.9
403
        )
404

405
        if google_proxy_group_from_fence_db:
1✔
406
            gpg_email = google_proxy_group_from_fence_db.email
1✔
407
            logger.debug("Found Google proxy group in Fence db: {}".format(gpg_email))
1✔
408
        else:
409
            # Construct the proxy group name that would have been used
410
            # and check if it exists in cirrus, in case Fence db just
411
            # didn't know about it.
412
            logger.debug(
1✔
413
                "Could not find Google proxy group for this user in Fence db. Checking gen3cirrus..."
414
            )
415
            pgname = get_proxy_group_name_for_user(
1✔
416
                user.id, user.username, prefix=config["GOOGLE_GROUP_PREFIX"]
417
            )
418
            google_proxy_group_from_google = gcm.get_group(pgname)
1✔
419
            gpg_email = (
1✔
420
                google_proxy_group_from_google.get("email")
421
                if google_proxy_group_from_google
422
                else None
423
            )
424

425
        if not gpg_email:
1✔
426
            logger.info(
1✔
427
                "Could not find Google proxy group for user in Fence db or in gen3cirrus. "
428
                "Assuming Google not in use as IdP. Proceeding with Fence deletes."
429
            )
430
        else:
431
            logger.debug(
1✔
432
                "Found Google proxy group email of user to delete: {}."
433
                "Proceeding with Google deletions.".format(gpg_email)
434
            )
435
            # Note: Fence db deletes here are interleaved with Google deletes.
436
            # This is so that if (for example) Google succeeds in deleting one SA
437
            # and then fails on the next, and the deletion process aborts, there
438
            # will not remain a record in Fence of the first, now-nonexistent SA.
439

440
            delete_google_service_accounts_and_keys(current_session, gcm, gpg_email)
1✔
441
            delete_google_proxy_group(
1✔
442
                current_session, gcm, gpg_email, google_proxy_group_from_fence_db, user
443
            )
444

445
    logger.debug("Deleting all user data from Fence database...")
1✔
446
    current_session.delete(user)
1✔
447
    current_session.commit()
1✔
448
    logger.info("Deleted all user data from Fence database. Returning.")
1✔
449

450
    return {"result": "success"}
1✔
451

452

453
def add_user_to_groups(current_session, username, groups=None):
1✔
454
    if not groups:
1✔
455
        groups = []
×
456
    usr = us.get_user(current_session, username)
1✔
457
    responses = []
1✔
458
    for groupname in groups:
1✔
459
        try:
1✔
460
            response = connect_user_to_group(current_session, usr, groupname)
1✔
461
            responses.append(response)
1✔
462
        except Exception as e:
×
463
            current_session.rollback()
×
464
            raise e
×
465
    return {"result": responses}
1✔
466

467

468
def connect_user_to_group(current_session, usr, groupname=None):
1✔
469
    grp = gp.get_group(current_session, groupname)
1✔
470
    if not grp:
1✔
471
        raise UserError(("Group {0} doesn't exist".format(groupname)))
×
472
    else:
473
        responses = []
1✔
474
        responses.append(gp.connect_user_to_group(current_session, usr, grp))
1✔
475
        projects = gp.get_group_projects(current_session, groupname)
1✔
476
        projects_data = [
1✔
477
            pj.get_project(current_session, project).auth_id for project in projects
478
        ]
479
        projects_list = [
1✔
480
            {"auth_id": auth_id, "privilege": ["read"]} for auth_id in projects_data
481
        ]
482
        for project in projects_list:
1✔
483
            connect_user_to_project(current_session, usr, project)
1✔
484
        return responses
1✔
485

486

487
def remove_user_from_groups(current_session, username, groups=None):
1✔
488
    if not groups:
1✔
489
        groups = []
×
490
    usr = us.get_user(current_session, username)
1✔
491
    user_groups = us.get_user_groups(current_session, username)["groups"]
1✔
492
    groups_to_keep = [x for x in user_groups if x not in groups]
1✔
493

494
    projects_to_keep = {
1✔
495
        item
496
        for sublist in [
497
            gp.get_group_projects(current_session, x) for x in groups_to_keep
498
        ]
499
        for item in sublist
500
    }
501

502
    projects_to_remove = {
1✔
503
        item
504
        for sublist in [gp.get_group_projects(current_session, x) for x in groups]
505
        for item in sublist
506
        if item not in projects_to_keep
507
    }
508

509
    responses = []
1✔
510
    for groupname in groups:
1✔
511
        try:
1✔
512
            response = disconnect_user_from_group(current_session, usr, groupname)
1✔
513
            responses.append(response)
1✔
514
        except Exception as e:
1✔
515
            current_session.rollback()
1✔
516
            raise e
1✔
517
    for project in projects_to_remove:
1✔
518
        remove_user_from_project(current_session, usr, project)
1✔
519
    return {"result": responses}
1✔
520

521

522
def disconnect_user_from_group(current_session, usr, groupname):
1✔
523
    grp = gp.get_group(current_session, groupname)
1✔
524
    if not grp:
1✔
525
        return {"warning": ("Group {0} doesn't exist".format(groupname))}
×
526

527
    response = gp.remove_user_from_group(current_session, usr, grp)
1✔
528
    projects = gp.get_group_projects(current_session, groupname)
1✔
529
    projects_data = [
1✔
530
        pj.get_project(current_session, project).auth_id for project in projects
531
    ]
532
    return response
1✔
533

534

535
def remove_user_from_project(current_session, usr, project_name):
1✔
536
    proj = pj.get_project(current_session, project_name)
1✔
537
    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