-
Notifications
You must be signed in to change notification settings - Fork 48
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
PXP-10541 Client credentials rotation #1068
Changes from all commits
d4dd8c1
d3fae29
5614cb7
dedcee4
94807c5
03000a9
dda4a8b
8a211e8
14c84fe
3fcde12
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -57,7 +57,7 @@ | |
from fence.scripting.google_monitor import email_users_without_access, validation_check | ||
from fence.config import config | ||
from fence.sync.sync_users import UserSyncer | ||
from fence.utils import create_client, get_valid_expiration | ||
from fence.utils import create_client, get_valid_expiration, generate_client_credentials | ||
|
||
from gen3authz.client.arborist.client import ArboristClient | ||
|
||
|
@@ -92,43 +92,44 @@ def modify_client_action( | |
driver = SQLAlchemyDriver(DB) | ||
with driver.session as s: | ||
client_name = client | ||
client = s.query(Client).filter(Client.name == client_name).first() | ||
if not client: | ||
clients = s.query(Client).filter(Client.name == client_name).all() | ||
if not clients: | ||
raise Exception("client {} does not exist".format(client_name)) | ||
if urls: | ||
if append: | ||
client.redirect_uris += urls | ||
logger.info("Adding {} to urls".format(urls)) | ||
else: | ||
client.redirect_uris = urls | ||
logger.info("Changing urls to {}".format(urls)) | ||
if delete_urls: | ||
client.redirect_uris = [] | ||
logger.info("Deleting urls") | ||
if set_auto_approve: | ||
client.auto_approve = True | ||
logger.info("Auto approve set to True") | ||
if unset_auto_approve: | ||
client.auto_approve = False | ||
logger.info("Auto approve set to False") | ||
if name: | ||
client.name = name | ||
logger.info("Updating name to {}".format(name)) | ||
if description: | ||
client.description = description | ||
logger.info("Updating description to {}".format(description)) | ||
if allowed_scopes: | ||
if append: | ||
new_scopes = client._allowed_scopes.split() + allowed_scopes | ||
client._allowed_scopes = " ".join(new_scopes) | ||
logger.info("Adding {} to allowed_scopes".format(allowed_scopes)) | ||
else: | ||
client._allowed_scopes = " ".join(allowed_scopes) | ||
logger.info("Updating allowed_scopes to {}".format(allowed_scopes)) | ||
if expires_in: | ||
client.expires_at = get_client_expires_at( | ||
expires_in=expires_in, grant_types=client.grant_type | ||
) | ||
for client in clients: | ||
if urls: | ||
if append: | ||
client.redirect_uris += urls | ||
logger.info("Adding {} to urls".format(urls)) | ||
else: | ||
client.redirect_uris = urls | ||
logger.info("Changing urls to {}".format(urls)) | ||
if delete_urls: | ||
client.redirect_uris = [] | ||
logger.info("Deleting urls") | ||
if set_auto_approve: | ||
client.auto_approve = True | ||
logger.info("Auto approve set to True") | ||
if unset_auto_approve: | ||
client.auto_approve = False | ||
logger.info("Auto approve set to False") | ||
if name: | ||
client.name = name | ||
logger.info("Updating name to {}".format(name)) | ||
if description: | ||
client.description = description | ||
logger.info("Updating description to {}".format(description)) | ||
if allowed_scopes: | ||
if append: | ||
new_scopes = client._allowed_scopes.split() + allowed_scopes | ||
client._allowed_scopes = " ".join(new_scopes) | ||
logger.info("Adding {} to allowed_scopes".format(allowed_scopes)) | ||
else: | ||
client._allowed_scopes = " ".join(allowed_scopes) | ||
logger.info("Updating allowed_scopes to {}".format(allowed_scopes)) | ||
if expires_in: | ||
client.expires_at = get_client_expires_at( | ||
expires_in=expires_in, grant_types=client.grant_type | ||
) | ||
s.commit() | ||
if arborist is not None and policies: | ||
arborist.update_client(client.client_id, policies) | ||
|
@@ -276,6 +277,54 @@ def split_uris(uris): | |
logger.info(nothing_to_do_msg) | ||
|
||
|
||
def rotate_client_action(DB, client_name, expires_in=None): | ||
""" | ||
Rorate a client's credentials (client ID and secret). The old credentials are | ||
NOT deactivated and must be deleted or expired separately. This allows for a | ||
rotation without downtime. | ||
|
||
Args: | ||
DB (str): database connection string | ||
client_name (str): name of the client to rotate credentials for | ||
expires_in (optional): number of days until this client expires (by default, no expiration) | ||
|
||
Returns: | ||
This functions does not return anything, but it prints the new set of credentials. | ||
""" | ||
driver = SQLAlchemyDriver(DB) | ||
with driver.session as s: | ||
client = s.query(Client).filter(Client.name == client_name).first() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was wondering if we need to be more particular than .first() (vs getting the one with the latest expiration or something), but since everything is being copied every time to the new client, I think it's fine. |
||
if not client: | ||
raise Exception("client {} does not exist".format(client_name)) | ||
|
||
# create a new row in the DB for the same client, with a new ID, secret and expiration | ||
client_id, client_secret, hashed_secret = generate_client_credentials( | ||
client.is_confidential | ||
) | ||
client = Client( | ||
client_id=client_id, | ||
client_secret=hashed_secret, | ||
expires_in=expires_in, | ||
# the rest is identical to the client being rotated | ||
user=client.user, | ||
redirect_uris=client.redirect_uris, | ||
_allowed_scopes=client._allowed_scopes, | ||
description=client.description, | ||
name=client.name, | ||
auto_approve=client.auto_approve, | ||
grant_types=client.grant_types, | ||
is_confidential=client.is_confidential, | ||
token_endpoint_auth_method=client.token_endpoint_auth_method, | ||
) | ||
s.add(client) | ||
s.commit() | ||
|
||
res = (client_id, client_secret) | ||
print( | ||
f"\nSave these credentials! Fence will not save the unhashed client secret.\nclient id, client secret:\n{res}" | ||
) | ||
|
||
|
||
def _remove_client_service_accounts(db_session, client): | ||
client_service_accounts = ( | ||
db_session.query(GoogleServiceAccount) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -34,6 +34,29 @@ def json_res(data): | |
return flask.Response(json.dumps(data), mimetype="application/json") | ||
|
||
|
||
def generate_client_credentials(confidential): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. docstring pls |
||
""" | ||
Generate a new client ID. If the client is confidential, also generate a new client secret. | ||
The unhashed secret should be returned to the user and the hashed secret should be stored | ||
in the database for later use. | ||
|
||
Args: | ||
confidential (bool): true if the client is confidential, false if it is public | ||
|
||
Returns: | ||
tuple: (client ID, unhashed client secret or None, hashed client secret or None) | ||
""" | ||
client_id = random_str(40) | ||
client_secret = None | ||
hashed_secret = None | ||
if confidential: | ||
client_secret = random_str(55) | ||
hashed_secret = bcrypt.hashpw( | ||
client_secret.encode("utf-8"), bcrypt.gensalt() | ||
).decode("utf-8") | ||
return client_id, client_secret, hashed_secret | ||
|
||
|
||
def create_client( | ||
DB, | ||
username=None, | ||
|
@@ -49,17 +72,10 @@ def create_client( | |
allowed_scopes=None, | ||
expires_in=None, | ||
): | ||
client_id = random_str(40) | ||
client_id, client_secret, hashed_secret = generate_client_credentials(confidential) | ||
if arborist is not None: | ||
arborist.create_client(client_id, policies) | ||
driver = SQLAlchemyDriver(DB) | ||
client_secret = None | ||
hashed_secret = None | ||
if confidential: | ||
client_secret = random_str(55) | ||
hashed_secret = bcrypt.hashpw( | ||
client_secret.encode("utf-8"), bcrypt.gensalt() | ||
).decode("utf-8") | ||
auth_method = "client_secret_basic" if confidential else "none" | ||
|
||
allowed_scopes = allowed_scopes or config["CLIENT_ALLOWED_SCOPES"] | ||
|
@@ -86,6 +102,7 @@ def create_client( | |
if arborist is not None: | ||
arborist.delete_client(client_id) | ||
raise Exception("client {} already exists".format(name)) | ||
|
||
client = Client( | ||
client_id=client_id, | ||
client_secret=hashed_secret, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
docstring pls