Skip to content

Commit

Permalink
Merge branch 'master' into docs/alembic
Browse files Browse the repository at this point in the history
  • Loading branch information
AlbertSnows authored Jul 26, 2024
2 parents 9469559 + 7b0aa60 commit d46ed58
Show file tree
Hide file tree
Showing 18 changed files with 864 additions and 141 deletions.
6 changes: 3 additions & 3 deletions .secrets.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -268,14 +268,14 @@
"filename": "tests/conftest.py",
"hashed_secret": "1348b145fa1a555461c1b790a2f66614781091e9",
"is_verified": false,
"line_number": 1561
"line_number": 1569
},
{
"type": "Base64 High Entropy String",
"filename": "tests/conftest.py",
"hashed_secret": "227dea087477346785aefd575f91dd13ab86c108",
"is_verified": false,
"line_number": 1583
"line_number": 1593
}
],
"tests/credentials/google/test_credentials.py": [
Expand Down Expand Up @@ -422,5 +422,5 @@
}
]
},
"generated_at": "2024-03-16T00:09:27Z"
"generated_at": "2024-07-25T17:19:58Z"
}
6 changes: 4 additions & 2 deletions clear_prometheus_multiproc
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
set -ex

rm -Rf $1
mkdir $1
mkdir -p $1
chmod 755 $1
chown 100:101 $1
if id -u nginx &>/dev/null; then
chown $(id -u nginx):$(id -g nginx) $1
fi
64 changes: 20 additions & 44 deletions fence/__init__.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
from collections import OrderedDict
import os
import tempfile
from urllib.parse import urljoin
import flask
from flask_cors import CORS
from sqlalchemy.orm import scoped_session
from flask import current_app
from werkzeug.local import LocalProxy

from authutils.oauth2.client import OAuthClient
from cdislogging import get_logger
from gen3authz.client.arborist.client import ArboristClient
from flask_wtf.csrf import validate_csrf
from werkzeug.middleware.dispatcher import DispatcherMiddleware
from azure.storage.blob import BlobServiceClient
from azure.core.exceptions import ResourceNotFoundError
from urllib.parse import urlparse
from cdislogging import get_logger
import flask
from flask_cors import CORS
from flask_wtf.csrf import validate_csrf
from gen3authz.client.arborist.client import ArboristClient
from sqlalchemy.orm import scoped_session


# Can't read config yet. Just set to debug for now, else no handlers.
# Later, in app_config(), will actually set level based on config
Expand All @@ -31,6 +27,7 @@
)

from fence.auth import logout, build_redirect_url
from fence.metrics import metrics
from fence.blueprints.data.indexd import S3IndexedFileLocation
from fence.blueprints.login.utils import allowed_login_redirects, domain
from fence.errors import UserError
Expand Down Expand Up @@ -67,11 +64,6 @@
import fence.blueprints.ga4gh


# for some reason the temp dir does not get created properly if we move
# this statement to `_setup_prometheus()`
PROMETHEUS_TMP_COUNTER_DIR = tempfile.TemporaryDirectory()


app = flask.Flask(__name__)
CORS(app=app, headers=["content-type", "accept"], expose_headers="*")

Expand Down Expand Up @@ -102,6 +94,9 @@ def app_init(
app_sessions(app)
app_register_blueprints(app)
server.init_app(app, query_client=query_client)
logger.info(
f"Prometheus metrics are{'' if config['ENABLE_PROMETHEUS_METRICS'] else ' NOT'} enabled."
)


def app_sessions(app):
Expand Down Expand Up @@ -206,6 +201,15 @@ def public_keys():
{"keys": [(keypair.kid, keypair.public_key) for keypair in app.keypairs]}
)

@app.route("/metrics")
def metrics_endpoint():
"""
/!\ There is no authz control on this endpoint!
In cloud-automation setups, access to this endpoint is blocked at the revproxy level.
"""
data, content_type = metrics.get_latest_metrics()
return flask.Response(data, content_type=content_type)


def _check_azure_storage(app):
"""
Expand Down Expand Up @@ -365,13 +369,6 @@ def app_config(
_setup_data_endpoint_and_boto(app)
_load_keys(app, root_dir)

app.prometheus_counters = {}
if config["ENABLE_PROMETHEUS_METRICS"]:
logger.info("Enabling Prometheus metrics...")
_setup_prometheus(app)
else:
logger.info("Prometheus metrics are NOT enabled.")

app.storage_manager = StorageManager(config["STORAGE_CREDENTIALS"], logger=logger)

app.debug = config["DEBUG"]
Expand Down Expand Up @@ -495,27 +492,6 @@ def _setup_audit_service_client(app):
)


def _setup_prometheus(app):
# This environment variable MUST be declared before importing the
# prometheus modules (or unit tests fail)
# More details on this awkwardness: https://github.com/prometheus/client_python/issues/250
os.environ["prometheus_multiproc_dir"] = PROMETHEUS_TMP_COUNTER_DIR.name

from prometheus_client import (
CollectorRegistry,
multiprocess,
make_wsgi_app,
)

app.prometheus_registry = CollectorRegistry()
multiprocess.MultiProcessCollector(app.prometheus_registry)

# Add prometheus wsgi middleware to route /metrics requests
app.wsgi_app = DispatcherMiddleware(
app.wsgi_app, {"/metrics": make_wsgi_app(registry=app.prometheus_registry)}
)


@app.errorhandler(Exception)
def handle_error(error):
"""
Expand Down
52 changes: 39 additions & 13 deletions fence/blueprints/data/indexd.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
from fence.resources.ga4gh.passports import sync_gen3_users_authz_from_ga4gh_passports
from fence.resources.audit.utils import enable_audit_logging
from fence.utils import get_valid_expiration_from_request
from fence.metrics import metrics

from . import multipart_upload
from ...models import AssumeRoleCacheAWS, query_for_user, query_for_user_by_id
Expand Down Expand Up @@ -77,6 +78,7 @@ def get_signed_url_for_file(
ga4gh_passports=None,
db_session=None,
bucket=None,
drs="False",
):
requested_protocol = requested_protocol or flask.request.args.get("protocol", None)
r_pays_project = flask.request.args.get("userProject", None)
Expand Down Expand Up @@ -164,12 +166,33 @@ def get_signed_url_for_file(
user_sub=flask.g.audit_data.get("sub", ""),
client_id=_get_client_id(),
requested_protocol=requested_protocol,
action=action,
drs=drs,
)

return {"url": signed_url}


def _log_signed_url_data_info(indexed_file, user_sub, client_id, requested_protocol):
def get_bucket_from_urls(urls, protocol):
"""
Return the bucket name from the first of the provided URLs that starts with the given protocol (usually `gs`, `s3`, `az`...)
"""
bucket = ""
for url in urls:
if "://" in url:
# Extract the protocol and the rest of the URL
bucket_protocol, rest_of_url = url.split("://", 1)

if bucket_protocol == protocol:
# Extract bucket name
bucket = f"{bucket_protocol}://{rest_of_url.split('/')[0]}"
break
return bucket


def _log_signed_url_data_info(
indexed_file, user_sub, client_id, requested_protocol, action, drs="False"
):
size_in_kibibytes = (indexed_file.index_document.get("size") or 0) / 1024
acl = indexed_file.index_document.get("acl")
authz = indexed_file.index_document.get("authz")
Expand All @@ -180,23 +203,25 @@ def _log_signed_url_data_info(indexed_file, user_sub, client_id, requested_proto
protocol = indexed_file.indexed_file_locations[0].protocol

# figure out which bucket was used based on the protocol
bucket = ""
for url in indexed_file.index_document.get("urls", []):
bucket_name = None
if "://" in url:
# Extract the protocol and the rest of the URL
bucket_protocol, rest_of_url = url.split("://", 1)

if bucket_protocol == protocol:
# Extract bucket name
bucket = f"{bucket_protocol}://{rest_of_url.split('/')[0]}"
break
bucket = get_bucket_from_urls(indexed_file.index_document.get("urls", []), protocol)

logger.info(
f"Signed URL Generated. size_in_kibibytes={size_in_kibibytes} "
f"Signed URL Generated. action={action} size_in_kibibytes={size_in_kibibytes} "
f"acl={acl} authz={authz} bucket={bucket} user_sub={user_sub} client_id={client_id}"
)

metrics.add_signed_url_event(
action,
protocol,
acl,
authz,
bucket,
user_sub,
client_id,
drs,
size_in_kibibytes,
)


def _get_client_id():
client_id = "Unknown Client"
Expand All @@ -208,6 +233,7 @@ def _get_client_id():

return client_id


def prepare_presigned_url_audit_log(protocol, indexed_file):
"""
Store in `flask.g.audit_data` the data needed to record an audit log.
Expand Down
2 changes: 2 additions & 0 deletions fence/blueprints/ga4gh.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,7 @@ def get_ga4gh_signed_url(object_id, access_id):
object_id,
requested_protocol=access_id,
ga4gh_passports=ga4gh_passports,
drs="True",
)

return flask.jsonify(result)
9 changes: 9 additions & 0 deletions fence/blueprints/login/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from fence.blueprints.login.redirect import validate_redirect
from fence.config import config
from fence.errors import UserError
from fence.metrics import metrics

logger = get_logger(__name__)

Expand Down Expand Up @@ -133,6 +134,14 @@ def get(self):

def post_login(self, user=None, token_result=None, **kwargs):
prepare_login_log(self.idp_name)
metrics.add_login_event(
user_sub=flask.g.user.id,
idp=self.idp_name,
fence_idp=flask.session.get("fence_idp"),
shib_idp=flask.session.get("shib_idp"),
client_id=flask.session.get("client_id"),
)

if token_result:
username = token_result.get(self.username_field)
if self.is_mfa_enabled:
Expand Down
1 change: 1 addition & 0 deletions fence/blueprints/login/google.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@ def get(self):
config.get("BASE_URL", "")
+ "/link/google/callback?code={}".format(flask.request.args.get("code"))
)

return super(GoogleCallback, self).get()
1 change: 1 addition & 0 deletions fence/config-default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ MOCK_STORAGE: true
# WARNING: ONLY set to true when fence will be deployed in such a way that it will
# ONLY receive traffic from internal clients and can safely use HTTP.
AUTHLIB_INSECURE_TRANSPORT: true

# enable Prometheus Metrics for observability purposes
#
# WARNING: Any counters, gauges, histograms, etc. should be carefully
Expand Down
Loading

0 comments on commit d46ed58

Please sign in to comment.