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

uc-cdis / fence / 13726114786

07 Mar 2025 05:35PM UTC coverage: 75.427% (+0.2%) from 75.268%
13726114786

Pull #1209

github

AlbertSnows
move send_email
Pull Request #1209: move backoff settings as well as other functions out of utils

7855 of 10414 relevant lines covered (75.43%)

0.75 hits per line

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

86.68
fence/models.py
1
"""
2
Define sqlalchemy models.
3
The models here inherit from the `Base` in userdatamodel, so when the fence app
4
is initialized, the resulting db session includes everything from userdatamodel
5
and this file.
6
The `migrate` function in this file is called every init and can be used for
7
database migrations.
8
"""
9

10
from enum import Enum
1✔
11

12
from authlib.integrations.sqla_oauth2 import (
1✔
13
    OAuth2AuthorizationCodeMixin,
14
    OAuth2ClientMixin,
15
)
16

17
import time
1✔
18
import json
1✔
19
import bcrypt
1✔
20
from datetime import datetime, timedelta
1✔
21
import flask
1✔
22
from sqlalchemy import (
1✔
23
    Integer,
24
    BigInteger,
25
    String,
26
    Column,
27
    Boolean,
28
    Text,
29
    text,
30
    event,
31
)
32
from sqlalchemy.dialects.postgresql import ARRAY, JSONB
1✔
33
from sqlalchemy.orm import relationship, backref
1✔
34
from sqlalchemy import func
1✔
35
from sqlalchemy.schema import ForeignKey
1✔
36
from userdatamodel import Base
1✔
37
from userdatamodel.models import (
1✔
38
    AccessPrivilege,
39
    Application,
40
    AuthorizationProvider,
41
    Bucket,
42
    Certificate,
43
    CloudProvider,
44
    ComputeAccess,
45
    GoogleProxyGroup,
46
    Group,
47
    HMACKeyPair,
48
    HMACKeyPairArchive,
49
    IdentityProvider,
50
    Project,
51
    ProjectToBucket,
52
    S3Credential,
53
    StorageAccess,
54
    Tag,
55
    User,
56
    UserToBucket,
57
    UserToGroup,
58
)
59

60
from fence import logger, get_SQLAlchemyDriver
1✔
61
from fence.config import config, logger
1✔
62
from fence.errors import UserError
1✔
63
from fence.utils import generate_client_credentials
1✔
64

65

66
def query_for_user(session, username):
1✔
67
    return (
1✔
68
        session.query(User)
69
        .filter(func.lower(User.username) == username.lower())
70
        .first()
71
    )
72

73

74
def query_for_user_by_id(session, user_id):
1✔
75
    return session.query(User).filter(User.id == user_id).first()
1✔
76

77

78
def create_user(session, logger, username, email=None, idp_name=None):
1✔
79
    """
80
    Create a new user in the database.
81

82
    Args:
83
        session (sqlalchemy.orm.session.Session): database session
84
        logger (logging.Logger): logger
85
        username (str): username to save for the created user
86
        email (str): email to save for the created user
87
        idp_name (str): name of identity provider to link
88

89
    Return:
90
        userdatamodel.user.User: the created user
91
    """
92
    logger.info(
1✔
93
        f"Creating a new user with username: {username}, "
94
        f"email: {email}, and idp_name: {idp_name}"
95
    )
96

97
    user = User(username=username)
1✔
98
    if email:
1✔
99
        user.email = email
1✔
100
    if idp_name:
1✔
101
        idp = (
1✔
102
            session.query(IdentityProvider)
103
            .filter(IdentityProvider.name == idp_name)
104
            .first()
105
        )
106
        if not idp:
1✔
107
            idp = IdentityProvider(name=idp_name)
1✔
108
        user.identity_provider = idp
1✔
109

110
    session.add(user)
1✔
111
    session.commit()
1✔
112
    return user
1✔
113

114

115
def get_project_to_authz_mapping(session):
1✔
116
    """
117
    Get the mappings for Project.auth_id to authorization resource (Project.authz)
118
    from the database if a mapping exists. e.g. will only return if Project.authz is
119
    populated.
120

121
    Args:
122
        session (sqlalchemy.orm.session.Session): database session
123

124
    Returns:
125
        dict{str:str}: Mapping from Project.auth_id to Project.authz
126
    """
127
    output = {}
1✔
128

129
    query_results = session.query(Project.auth_id, Project.authz)
1✔
130
    if query_results:
1✔
131
        output = {item.auth_id: item.authz for item in query_results if item.authz}
1✔
132

133
    return output
1✔
134

135

136
def get_client_expires_at(expires_in, grant_types):
1✔
137
    """
138
    Given an `expires_in` value (days from now), return an `expires_at` value (timestamp).
139

140
    expires_in (int/float/str): days until this client expires
141
    grant_types (str): list of the client's grants joined by "\n"
142
    """
143
    expires_at = None
1✔
144

145
    if expires_in:
1✔
146
        try:
1✔
147
            expires_in = float(expires_in)
1✔
148
            assert expires_in > 0
1✔
149
        except (ValueError, AssertionError):
1✔
150
            raise UserError(
1✔
151
                f"Requested expiry must be a positive integer; instead got: {expires_in}"
152
            )
153

154
        # for backwards compatibility, 0 means no expiration
155
        if expires_in != 0:
1✔
156
            # do not use `datetime.utcnow()` or the timestamp will be wrong,
157
            # `timestamp()` already converts to UTC
158
            expires_at = (datetime.now() + timedelta(days=expires_in)).timestamp()
1✔
159

160
    if "client_credentials" in grant_types:
1✔
161
        if not expires_in or expires_in <= 0 or expires_in > 366:
1✔
162
            logger.warning(
1✔
163
                "Credentials with the 'client_credentials' grant which will be used externally are required to expire within 12 months. Use the `--expires-in` parameter to add an expiration."
164
            )
165

166
    return expires_at
1✔
167

168

169
class ClientAuthType(Enum):
1✔
170
    """
171
    List the possible types of OAuth client authentication, which are
172
    - None (no authentication).
173
    - Basic (using basic HTTP authorization header to include the client ID & secret).
174
    - POST (the client ID & secret are included in the body of a POST request).
175
    These all have a corresponding string which identifies them to authlib.
176
    """
177

178
    none = "none"
1✔
179
    basic = "client_secret_basic"
1✔
180
    post = "client_secret_post"
1✔
181

182

183
class GrantType(Enum):
1✔
184
    """
185
    Enumerate the allowed grant types for the OAuth2 flow.
186
    """
187

188
    code = "authorization_code"
1✔
189
    refresh = "refresh_token"
1✔
190
    implicit = "implicit"
1✔
191
    client_credentials = "client_credentials"
1✔
192

193

194
class Client(Base, OAuth2ClientMixin):
1✔
195

196
    __tablename__ = "client"
1✔
197

198
    client_id = Column(String(48), primary_key=True, index=True)
1✔
199
    # this is hashed secret
200
    client_secret = Column(String(120), unique=True, index=True, nullable=True)
1✔
201

202
    # human readable name
203
    name = Column(String(40), nullable=False)
1✔
204

205
    # human readable description, not required
206
    description = Column(String(400))
1✔
207

208
    # required if you need to support client credential
209
    user_id = Column(Integer, ForeignKey(User.id, ondelete="CASCADE"))
1✔
210
    user = relationship(
1✔
211
        "User",
212
        backref=backref("clients", cascade="all, delete-orphan", passive_deletes=True),
213
    )
214

215
    # this is for internal microservices to skip user grant
216
    auto_approve = Column(Boolean, default=False)
1✔
217

218
    # public or confidential
219
    is_confidential = Column(Boolean, default=True)
1✔
220

221
    expires_at = Column(Integer, nullable=False, default=0)
1✔
222

223
    # Deprecated, keeping these around in case it is needed later
224
    _default_scopes = Column(Text)
1✔
225
    _scopes = ["compute", "storage", "user"]
1✔
226

227
    def __init__(self, client_id, expires_in=0, **kwargs):
1✔
228

229
        # New Json object for Authlib Oauth client
230
        if "_client_metadata" in kwargs:
1✔
231
            client_metadata = json.loads(kwargs.pop("_client_metadata"))
×
232
        else:
233
            client_metadata = {}
1✔
234

235
        if "allowed_scopes" in kwargs:
1✔
236
            allowed_scopes = kwargs.pop("allowed_scopes")
1✔
237
            if isinstance(allowed_scopes, list):
1✔
238
                client_metadata["scope"] = " ".join(allowed_scopes)
1✔
239
            else:
240
                client_metadata["scope"] = allowed_scopes
1✔
241

242
        # redirect uri is now part of authlibs client_metadata
243
        if "redirect_uris" in kwargs:
1✔
244
            redirect_uris = kwargs.pop("redirect_uris")
1✔
245
            if isinstance(redirect_uris, list):
1✔
246
                # redirect_uris is now part of the metadata json object
247
                client_metadata["redirect_uris"] = redirect_uris
1✔
248
            elif redirect_uris:
1✔
249
                client_metadata["redirect_uris"] = [redirect_uris]
1✔
250
            else:
251
                client_metadata["redirect_uris"] = []
1✔
252

253
        # default grant types to allow for auth code flow and resfreshing
254
        grant_types = kwargs.pop("grant_types", None) or [
1✔
255
            GrantType.code.value,
256
            GrantType.refresh.value,
257
        ]
258
        # grant types is now part of authlibs client_metadata
259
        if isinstance(grant_types, list):
1✔
260
            client_metadata["grant_types"] = grant_types
1✔
261
        elif grant_types:
1✔
262
            # assume it's already in correct format and make it a list
263
            client_metadata["grant_types"] = [grant_types]
1✔
264
        else:
265
            client_metadata["grant_types"] = []
×
266

267
        supported_grant_types = [
1✔
268
            "authorization_code",
269
            "refresh_token",
270
            "implicit",
271
            "client_credentials",
272
        ]
273
        assert all(
1✔
274
            grant_type in supported_grant_types
275
            for grant_type in client_metadata["grant_types"]
276
        ), f"Grant types '{client_metadata['grant_types']}' are not in supported types {supported_grant_types}"
277

278
        if "authorization_code" in client_metadata["grant_types"]:
1✔
279
            assert kwargs.get("user") or kwargs.get(
1✔
280
                "user_id"
281
            ), "A username is required for the 'authorization_code' grant"
282
            assert client_metadata.get(
1✔
283
                "redirect_uris"
284
            ), "Redirect URL(s) are required for the 'authorization_code' grant"
285

286
        # response_types is now part of authlib's client_metadata
287
        response_types = kwargs.pop("response_types", None)
1✔
288
        if isinstance(response_types, list):
1✔
289
            client_metadata["response_types"] = "\n".join(response_types)
×
290
        elif response_types:
1✔
291
            # assume it's already in correct format
292
            client_metadata["response_types"] = [response_types]
1✔
293
        else:
294
            client_metadata["response_types"] = []
1✔
295

296
        if "token_endpoint_auth_method" in kwargs:
1✔
297
            client_metadata["token_endpoint_auth_method"] = kwargs.pop(
1✔
298
                "token_endpoint_auth_method"
299
            )
300

301
        # Do this if expires_in is specified or expires_at is not supplied
302
        if expires_in != 0 or ("expires_at" not in kwargs):
1✔
303
            expires_at = get_client_expires_at(
1✔
304
                expires_in=expires_in, grant_types=client_metadata["grant_types"]
305
            )
306
            if expires_at:
1✔
307
                kwargs["expires_at"] = expires_at
1✔
308

309
        if "client_id_issued_at" not in kwargs or kwargs["client_id_issued_at"] is None:
1✔
310
            kwargs["client_id_issued_at"] = int(time.time())
1✔
311

312
        kwargs["_client_metadata"] = json.dumps(client_metadata)
1✔
313

314
        super(Client, self).__init__(client_id=client_id, **kwargs)
1✔
315

316
    @property
1✔
317
    def allowed_scopes(self):
1✔
318
        return self.scope.split(" ")
1✔
319

320
    @property
1✔
321
    def client_type(self):
1✔
322
        """
323
        The client should be considered confidential either if it is actually
324
        marked confidential, *or* if the confidential setting was left empty.
325
        Only in the case where ``is_confidential`` is deliberately set to
326
        ``False`` should the client be considered public.
327
        """
328
        if self.is_confidential is False:
×
329
            return "public"
×
330
        return "confidential"
×
331

332
    @staticmethod
1✔
333
    def get_by_client_id(client_id):
1✔
334
        with flask.current_app.db.session as session:
×
335
            return session.query(Client).filter_by(client_id=client_id).first()
×
336

337
    def check_client_type(self, client_type):
1✔
338
        return (client_type == "confidential" and self.is_confidential) or (
×
339
            client_type == "public" and not self.is_confidential
340
        )
341

342
    def check_client_secret(self, client_secret):
1✔
343
        check_hash = bcrypt.hashpw(
1✔
344
            client_secret.encode("utf-8"), self.client_secret.encode("utf-8")
345
        ).decode("utf-8")
346
        return check_hash == self.client_secret
1✔
347

348
    def check_requested_scopes(self, scopes):
1✔
349
        if "openid" not in scopes:
1✔
350
            logger.error(f"Invalid scopes: 'openid' not in requested scopes ({scopes})")
×
351
            return False
×
352
        return set(self.allowed_scopes).issuperset(scopes)
1✔
353

354
    # Replaces Authlib method. Our logic does not actually look at token_auth_endpoint value
355
    def check_endpoint_auth_method(self, method, endpoint):
1✔
356
        """
357
        Only basic auth is supported. If anything else gets added, change this
358
        """
359
        if endpoint == "token":
1✔
360
            protected_types = [ClientAuthType.basic.value, ClientAuthType.post.value]
1✔
361
            return (self.is_confidential and method in protected_types) or (
1✔
362
                not self.is_confidential and method == ClientAuthType.none.value
363
            )
364

365
        return True
1✔
366

367
    def check_response_type(self, response_type):
1✔
368
        allowed_response_types = []
1✔
369
        if "authorization_code" in self.grant_types:
1✔
370
            allowed_response_types.append("code")
1✔
371
        if "implicit" in self.grant_types:
1✔
372
            allowed_response_types.append("id_token")
×
373
            allowed_response_types.append("id_token token")
×
374
        return response_type in allowed_response_types
1✔
375

376

377
class AuthorizationCode(Base, OAuth2AuthorizationCodeMixin):
1✔
378

379
    __tablename__ = "authorization_code"
1✔
380

381
    id = Column(Integer, primary_key=True)
1✔
382

383
    user_id = Column(Integer, ForeignKey("User.id", ondelete="CASCADE"))
1✔
384
    user = relationship(
1✔
385
        "User",
386
        backref=backref(
387
            "authorization_codes", cascade="all, delete-orphan", passive_deletes=True
388
        ),
389
    )
390

391
    nonce = Column(String, nullable=True)
1✔
392

393
    refresh_token_expires_in = Column(Integer, nullable=True)
1✔
394

395
    _scope = Column(Text, default="")
1✔
396

397
    def __init__(self, **kwargs):
1✔
398
        if "scope" in kwargs:
1✔
399
            scope = kwargs.pop("scope")
1✔
400
            if isinstance(scope, list):
1✔
401
                kwargs["_scope"] = " ".join(scope)
×
402
            else:
403
                kwargs["_scope"] = scope
1✔
404
        super(AuthorizationCode, self).__init__(**kwargs)
1✔
405

406
    @property
1✔
407
    def scope(self):
1✔
408
        return self._scope.split(" ")
1✔
409

410

411
class UserRefreshToken(Base):
1✔
412
    __tablename__ = "user_refresh_token"
1✔
413

414
    jti = Column(String, primary_key=True)
1✔
415
    userid = Column(Integer)
1✔
416
    expires = Column(BigInteger)
1✔
417

418
    def delete(self):
1✔
419
        with flask.current_app.db.session as session:
×
420
            session.delete(self)
×
421
            session.commit()
×
422

423

424
class GoogleServiceAccount(Base):
1✔
425
    __tablename__ = "google_service_account"
1✔
426

427
    id = Column(Integer, primary_key=True)
1✔
428

429
    # The uniqueId google provides to resources is ONLY unique within
430
    # the given project, so we shouldn't rely on that for a primary key (in
431
    # case we're ever juggling mult. projects)
432
    google_unique_id = Column(String, unique=False, nullable=False)
1✔
433

434
    client_id = Column(String(40))
1✔
435

436
    user_id = Column(Integer, ForeignKey(User.id, ondelete="CASCADE"), nullable=False)
1✔
437
    user = relationship(
1✔
438
        "User",
439
        backref=backref(
440
            "google_service_accounts",
441
            cascade="all, delete-orphan",
442
            passive_deletes=True,
443
        ),
444
    )
445

446
    google_project_id = Column(String, nullable=False)
1✔
447

448
    email = Column(String, unique=True, nullable=False)
1✔
449

450
    def delete(self):
1✔
451
        with flask.current_app.db.session as session:
×
452
            session.delete(self)
×
453
            session.commit()
×
454
            return self
×
455

456

457
class UserGoogleAccount(Base):
1✔
458
    __tablename__ = "user_google_account"
1✔
459

460
    id = Column(Integer, primary_key=True)
1✔
461

462
    email = Column(String, unique=True, nullable=False)
1✔
463

464
    user_id = Column(Integer, ForeignKey(User.id, ondelete="CASCADE"), nullable=False)
1✔
465
    user = relationship(
1✔
466
        "User",
467
        backref=backref(
468
            "user_google_accounts", cascade="all, delete-orphan", passive_deletes=True
469
        ),
470
    )
471

472
    def delete(self):
1✔
473
        with flask.current_app.db.session as session:
×
474
            session.delete(self)
×
475
            session.commit()
×
476
            return self
×
477

478

479
class UserGoogleAccountToProxyGroup(Base):
1✔
480
    __tablename__ = "user_google_account_to_proxy_group"
1✔
481

482
    user_google_account_id = Column(
1✔
483
        Integer,
484
        ForeignKey(UserGoogleAccount.id, ondelete="CASCADE"),
485
        nullable=False,
486
        primary_key=True,
487
    )
488
    user_google_account = relationship(
1✔
489
        "UserGoogleAccount",
490
        backref=backref(
491
            "user_google_account_to_proxy_group",
492
            cascade="all, delete-orphan",
493
            passive_deletes=True,
494
        ),
495
    )
496

497
    proxy_group_id = Column(
1✔
498
        String,
499
        ForeignKey(GoogleProxyGroup.id, ondelete="CASCADE"),
500
        nullable=False,
501
        primary_key=True,
502
    )
503
    google_proxy_group = relationship(
1✔
504
        "GoogleProxyGroup",
505
        backref=backref(
506
            "user_google_account_to_proxy_group",
507
            cascade="all, delete-orphan",
508
            passive_deletes=True,
509
        ),
510
    )
511

512
    expires = Column(BigInteger)
1✔
513

514
    def delete(self):
1✔
515
        with flask.current_app.db.session as session:
×
516
            session.delete(self)
×
517
            session.commit()
×
518
            return self
×
519

520

521
class GoogleServiceAccountKey(Base):
1✔
522
    __tablename__ = "google_service_account_key"
1✔
523

524
    id = Column(Integer, primary_key=True)
1✔
525

526
    key_id = Column(String, nullable=False)
1✔
527

528
    service_account_id = Column(
1✔
529
        Integer, ForeignKey(GoogleServiceAccount.id, ondelete="CASCADE"), nullable=False
530
    )
531
    google_service_account = relationship(
1✔
532
        "GoogleServiceAccount",
533
        backref=backref(
534
            "google_service_account_keys",
535
            cascade="all, delete-orphan",
536
            passive_deletes=True,
537
        ),
538
    )
539

540
    expires = Column(BigInteger)
1✔
541

542
    private_key = Column(String)
1✔
543

544
    def delete(self):
1✔
545
        with flask.current_app.db.session as session:
×
546
            session.delete(self)
×
547
            session.commit()
×
548
            return self
×
549

550

551
class GoogleBucketAccessGroup(Base):
1✔
552
    __tablename__ = "google_bucket_access_group"
1✔
553
    id = Column(Integer, primary_key=True)
1✔
554

555
    bucket_id = Column(
1✔
556
        Integer, ForeignKey(Bucket.id, ondelete="CASCADE"), nullable=False
557
    )
558
    bucket = relationship(
1✔
559
        "Bucket",
560
        backref=backref(
561
            "google_bucket_access_groups",
562
            cascade="all, delete-orphan",
563
            passive_deletes=True,
564
        ),
565
    )
566

567
    email = Column(String, nullable=False)
1✔
568

569
    # specify what kind of storage access this group has e.g. ['read-storage']
570
    privileges = Column(ARRAY(String))
1✔
571

572
    def delete(self):
1✔
573
        with flask.current_app.db.session as session:
×
574
            session.delete(self)
×
575
            session.commit()
×
576
            return self
×
577

578

579
class GoogleProxyGroupToGoogleBucketAccessGroup(Base):
1✔
580
    __tablename__ = "google_proxy_group_to_google_bucket_access_group"
1✔
581
    id = Column(Integer, primary_key=True)
1✔
582

583
    proxy_group_id = Column(
1✔
584
        String, ForeignKey(GoogleProxyGroup.id, ondelete="CASCADE"), nullable=False
585
    )
586
    proxy_group = relationship(
1✔
587
        "GoogleProxyGroup",
588
        backref=backref(
589
            "google_proxy_group_to_google_bucket_access_group",
590
            cascade="all, delete-orphan",
591
            passive_deletes=True,
592
        ),
593
    )
594

595
    access_group_id = Column(
1✔
596
        Integer,
597
        ForeignKey(GoogleBucketAccessGroup.id, ondelete="CASCADE"),
598
        nullable=False,
599
    )
600
    access_group = relationship(
1✔
601
        "GoogleBucketAccessGroup",
602
        backref=backref(
603
            "google_proxy_group_to_google_bucket_access_group",
604
            cascade="all, delete-orphan",
605
            passive_deletes=True,
606
        ),
607
    )
608

609
    expires = Column(BigInteger)
1✔
610

611

612
class UserServiceAccount(Base):
1✔
613
    __tablename__ = "user_service_account"
1✔
614
    id = Column(Integer, primary_key=True)
1✔
615

616
    # The uniqueId google provides to resources is ONLY unique within
617
    # the given project, so we shouldn't rely on that for a primary key (in
618
    # case we're ever juggling mult. projects)
619
    google_unique_id = Column(String, nullable=False)
1✔
620

621
    email = Column(String, nullable=False)
1✔
622

623
    google_project_id = Column(String, nullable=False)
1✔
624

625

626
class ServiceAccountAccessPrivilege(Base):
1✔
627
    __tablename__ = "service_account_access_privilege"
1✔
628

629
    id = Column(Integer, primary_key=True)
1✔
630

631
    project_id = Column(
1✔
632
        Integer, ForeignKey(Project.id, ondelete="CASCADE"), nullable=False
633
    )
634
    project = relationship(
1✔
635
        "Project",
636
        backref=backref(
637
            "sa_access_privileges", cascade="all, delete-orphan", passive_deletes=True
638
        ),
639
    )
640

641
    service_account_id = Column(
1✔
642
        Integer, ForeignKey(UserServiceAccount.id, ondelete="CASCADE"), nullable=False
643
    )
644
    service_account = relationship(
1✔
645
        "UserServiceAccount",
646
        backref=backref(
647
            "access_privileges", cascade="all, delete-orphan", passive_deletes=True
648
        ),
649
    )
650

651

652
class ServiceAccountToGoogleBucketAccessGroup(Base):
1✔
653
    __tablename__ = "service_account_to_google_bucket_access_group"
1✔
654
    id = Column(Integer, primary_key=True)
1✔
655

656
    service_account_id = Column(
1✔
657
        Integer, ForeignKey(UserServiceAccount.id, ondelete="CASCADE"), nullable=False
658
    )
659
    service_account = relationship(
1✔
660
        "UserServiceAccount",
661
        backref=backref(
662
            "to_access_groups", cascade="all, delete-orphan", passive_deletes=True
663
        ),
664
    )
665

666
    expires = Column(BigInteger)
1✔
667

668
    access_group_id = Column(
1✔
669
        Integer,
670
        ForeignKey(GoogleBucketAccessGroup.id, ondelete="CASCADE"),
671
        nullable=False,
672
    )
673

674
    access_group = relationship(
1✔
675
        "GoogleBucketAccessGroup",
676
        backref=backref(
677
            "to_access_groups", cascade="all, delete-orphan", passive_deletes=True
678
        ),
679
    )
680

681

682
class AssumeRoleCacheAWS(Base):
1✔
683
    __tablename__ = "assume_role_cache"
1✔
684

685
    arn = Column(String(), primary_key=True)
1✔
686
    expires_at = Column(Integer())
1✔
687
    aws_access_key_id = Column(String())
1✔
688
    aws_secret_access_key = Column(String())
1✔
689
    aws_session_token = Column(String())
1✔
690

691

692
class AssumeRoleCacheGCP(Base):
1✔
693
    __tablename__ = "gcp_assume_role_cache"
1✔
694

695
    gcp_proxy_group_id = Column(String(), primary_key=True)
1✔
696
    expires_at = Column(Integer())
1✔
697
    gcp_private_key = Column(String())
1✔
698
    gcp_key_db_entry = Column(String())
1✔
699

700

701
class GA4GHPassportCache(Base):
1✔
702
    __tablename__ = "ga4gh_passport_cache"
1✔
703

704
    passport_hash = Column(String(64), primary_key=True)
1✔
705
    expires_at = Column(BigInteger, nullable=False)
1✔
706
    user_ids = Column(ARRAY(String(255)), nullable=False)
1✔
707

708

709
class GA4GHVisaV1(Base):
1✔
710

711
    __tablename__ = "ga4gh_visa_v1"
1✔
712

713
    # As Fence will consume visas from many visa issuers, will not use jti as pkey
714
    id = Column(BigInteger, primary_key=True)
1✔
715

716
    user_id = Column(Integer, ForeignKey(User.id, ondelete="CASCADE"), nullable=False)
1✔
717
    user = relationship(
1✔
718
        "User",
719
        backref=backref(
720
            "ga4gh_visas_v1", cascade="all, delete-orphan", passive_deletes=True
721
        ),
722
    )
723
    ga4gh_visa = Column(Text, nullable=False)  # In encoded form
1✔
724
    source = Column(String, nullable=False)
1✔
725
    type = Column(String, nullable=False)
1✔
726
    asserted = Column(BigInteger, nullable=False)
1✔
727
    expires = Column(BigInteger, nullable=False)
1✔
728

729

730
class UpstreamRefreshToken(Base):
1✔
731
    # General table to store any refresh_token sent from any oidc client
732

733
    __tablename__ = "upstream_refresh_token"
1✔
734

735
    id = Column(BigInteger, primary_key=True)
1✔
736

737
    user_id = Column(Integer, ForeignKey(User.id, ondelete="CASCADE"), nullable=False)
1✔
738
    user = relationship(
1✔
739
        "User",
740
        backref=backref(
741
            "upstream_refresh_tokens",
742
            cascade="all, delete-orphan",
743
            passive_deletes=True,
744
        ),
745
    )
746
    refresh_token = Column(Text, nullable=False)
1✔
747
    expires = Column(BigInteger, nullable=False)
1✔
748

749

750
class IssSubPairToUser(Base):
1✔
751
    # issuer & sub pair mapping to Gen3 User sub
752

753
    __tablename__ = "iss_sub_pair_to_user"
1✔
754

755
    iss = Column(String(), primary_key=True)
1✔
756
    sub = Column(String(), primary_key=True)
1✔
757

758
    fk_to_User = Column(
1✔
759
        Integer, ForeignKey(User.id, ondelete="CASCADE"), nullable=False
760
    )  #  foreign key for User table
761
    user = relationship(
1✔
762
        "User",
763
        backref=backref(
764
            "iss_sub_pairs",
765
            cascade="all, delete-orphan",
766
            passive_deletes=True,
767
        ),
768
    )
769

770
    # dump whatever idp provides in here
771
    extra_info = Column(JSONB(), server_default=text("'{}'"))
1✔
772

773
    def _get_issuer_to_idp():
1✔
774
        possibly_matching_idps = [IdentityProvider.ras]
1✔
775
        issuer_to_idp = {}
1✔
776

777
        oidc = config.get("OPENID_CONNECT", {})
1✔
778
        for idp in possibly_matching_idps:
1✔
779
            discovery_url = oidc.get(idp, {}).get("discovery_url")
1✔
780
            if discovery_url:
1✔
781
                for allowed_issuer in config["GA4GH_VISA_ISSUER_ALLOWLIST"]:
1✔
782
                    if discovery_url.startswith(allowed_issuer):
1✔
783
                        issuer_to_idp[allowed_issuer] = idp
1✔
784
                        break
1✔
785

786
        return issuer_to_idp
1✔
787

788
    ISSUER_TO_IDP = _get_issuer_to_idp()
1✔
789

790
    # no longer need function since results stored in var
791
    del _get_issuer_to_idp
1✔
792

793

794
@event.listens_for(IssSubPairToUser.__table__, "after_create")
1✔
795
def populate_iss_sub_pair_to_user_table(target, connection, **kw):
1✔
796
    """
797
    Populate iss_sub_pair_to_user table using User table's id_from_idp
798
    column.
799
    """
800
    for issuer, idp_name in IssSubPairToUser.ISSUER_TO_IDP.items():
×
801
        logger.info(
×
802
            'Attempting to populate iss_sub_pair_to_user table for users with "{}" idp and "{}" issuer'.format(
803
                idp_name, issuer
804
            )
805
        )
806
        transaction = connection.begin()
×
807
        try:
×
808
            connection.execute(
×
809
                text(
810
                    """
811
                    WITH identity_provider_id AS (SELECT id FROM identity_provider WHERE name=:idp_name)
812
                    INSERT INTO iss_sub_pair_to_user (iss, sub, "fk_to_User", extra_info)
813
                    SELECT :iss, id_from_idp, id, additional_info
814
                    FROM "User"
815
                    WHERE idp_id IN (SELECT * FROM identity_provider_id) AND id_from_idp IS NOT NULL;
816
                    """
817
                ),
818
                idp_name=idp_name,
819
                iss=issuer,
820
            )
821
        except Exception as e:
×
822
            transaction.rollback()
×
823
            logger.warning(
×
824
                "Could not populate iss_sub_pair_to_user table: {}".format(e)
825
            )
826
        else:
827
            transaction.commit()
×
828
            logger.info("Population was successful")
×
829

830

831
def create_client(
1✔
832
    DB,
833
    username=None,
834
    urls=[],
835
    name="",
836
    description="",
837
    auto_approve=False,
838
    is_admin=False,
839
    grant_types=None,
840
    confidential=True,
841
    arborist=None,
842
    policies=None,
843
    allowed_scopes=None,
844
    expires_in=None,
845
):
846
    client_id, client_secret, hashed_secret = generate_client_credentials(confidential)
1✔
847
    if arborist is not None:
1✔
848
        arborist.create_client(client_id, policies)
×
849
    driver = get_SQLAlchemyDriver(DB)
1✔
850
    auth_method = "client_secret_basic" if confidential else "none"
1✔
851

852
    allowed_scopes = allowed_scopes or config["CLIENT_ALLOWED_SCOPES"]
1✔
853
    if not set(allowed_scopes).issubset(set(config["CLIENT_ALLOWED_SCOPES"])):
1✔
854
        raise ValueError(
1✔
855
            "Each allowed scope must be one of: {}".format(
856
                config["CLIENT_ALLOWED_SCOPES"]
857
            )
858
        )
859

860
    if "openid" not in allowed_scopes:
1✔
861
        allowed_scopes.append("openid")
1✔
862
        logger.warning('Adding required "openid" scope to list of allowed scopes.')
1✔
863

864
    with driver.session as s:
1✔
865
        user = None
1✔
866
        if username:
1✔
867
            user = query_for_user(session=s, username=username)
1✔
868
            if not user:
1✔
869
                user = User(username=username, is_admin=is_admin)
1✔
870
                s.add(user)
1✔
871

872
        if s.query(Client).filter(Client.name == name).first():
1✔
873
            if arborist is not None:
1✔
874
                arborist.delete_client(client_id)
×
875
            raise Exception("client {} already exists".format(name))
1✔
876

877
        client = Client(
1✔
878
            client_id=client_id,
879
            client_secret=hashed_secret,
880
            user=user,
881
            redirect_uris=urls,
882
            allowed_scopes=" ".join(allowed_scopes),
883
            description=description,
884
            name=name,
885
            auto_approve=auto_approve,
886
            grant_types=grant_types,
887
            is_confidential=confidential,
888
            token_endpoint_auth_method=auth_method,
889
            expires_in=expires_in,
890
        )
891
        s.add(client)
1✔
892
        s.commit()
1✔
893

894
    return client_id, client_secret
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