def connect_project_to_group(current_session, grp, project=None): prj = pj.get_project(current_session, project) if not prj: raise UserError(("Project {0} doesn't exist".format(project))) return gp.connect_project_to_group(current_session, grp, prj)
def authorize(*args, **kwargs): """ OIDC Authorization Endpoint From the OIDC Specification: 3.1.1. Authorization Code Flow Steps The Authorization Code Flow goes through the following steps. - Client prepares an Authentication Request containing the desired request parameters. - Client sends the request to the Authorization Server. - Authorization Server Authenticates the End-User. - Authorization Server obtains End-User Consent/Authorization. - Authorization Server sends the End-User back to the Client with an Authorization Code. - Client requests a response using the Authorization Code at the Token Endpoint. - Client receives a response that contains an ID Token and Access Token in the response body. - Client validates the ID token and retrieves the End-User's Subject Identifier. Args: *args: additional arguments **kwargs: additional keyword arguments """ need_authentication = False try: user = get_current_user() except Unauthorized: need_authentication = True if need_authentication or not user: redirect_url = config.get("BASE_URL") + flask.request.full_path params = {"redirect": redirect_url} login_url = config.get("DEFAULT_LOGIN_URL") idp = flask.request.args.get("idp") if idp: if idp not in IDP_URL_MAP or idp not in config["OPENID_CONNECT"]: raise UserError("idp {} is not supported".format(idp)) idp_url = IDP_URL_MAP[idp] login_url = "{}/login/{}".format(config.get("BASE_URL"), idp_url) # handle valid extra params for fence multi-tenant and shib login fence_idp = flask.request.args.get("fence_idp") shib_idp = flask.request.args.get("shib_idp") if idp == "fence" and fence_idp: params["idp"] = fence_idp if fence_idp == "shibboleth": params["shib_idp"] = shib_idp elif idp == "shibboleth" and shib_idp: params["shib_idp"] = shib_idp login_url = add_params_to_uri(login_url, params) return flask.redirect(login_url) try: grant = server.validate_consent_request(end_user=user) except OAuth2Error as e: raise Unauthorized("Failed to authorize: {}".format(str(e))) client_id = grant.client.client_id with flask.current_app.db.session as session: client = session.query(Client).filter_by(client_id=client_id).first() # TODO: any way to get from grant? confirm = flask.request.form.get("confirm") or flask.request.args.get( "confirm") if client.auto_approve: confirm = "yes" if confirm is not None: response = _handle_consent_confirmation(user, confirm) # if it's a 302 for POST confirm, return 200 instead and include # redirect url in body because browser ajax POST doesn't follow # cross origin redirect if flask.request.method == "POST" and response.status_code == 302: return flask.jsonify({"redirect": response.headers["Location"]}) else: # no confirm param, so no confirmation has occured yet response = _authorize(user, grant, client) return response
def register_user(): """ - If config["REGISTER_USERS_ON"] is True, then unregistered users are redirected here after logging in. - Users may then register. - Users may access this endpoint directly. - At the moment, registration involves providing firstname, lastname, org, and (if user.email for the user is None) email. - If a user registers, add the new information to the user's additional_info column, and add the user to the Arborist group specified in config["REGISTERED_USERS_GROUP"]. The idea is that users can register in order to obtain certain permissions (where the permissions are defined by the group definition in the useryaml). The registration info is added as a dict under user.additional_info["registration_info"]; it is a separate blob in order to avoid namespace collision and make clear that the information was self-declared by the user during registration. Some basic validation is done on form inputs (non-empty fields, plausible emails), but actual verification (for example, checking organization info against some trusted authority's records) has been deemed out of scope. """ form = RegistrationForm() if flask.request.method == "GET": return flask.render_template( "register_user.html", user=flask.g.user, form=form, ) if not form.validate(): raise UserError("Form validation failed: {}".format(str(form.errors))) # Validation passed--don't check form data here. firstname = flask.request.form["firstname"] lastname = flask.request.form["lastname"] org = flask.request.form["organization"] email = flask.g.user.email or flask.request.form["email"] combined_info = {} if flask.g.user.additional_info is not None: combined_info.update(flask.g.user.additional_info) registration_info = { "registration_info": { "firstname": firstname, "lastname": lastname, "org": org, "email": email, } } combined_info.update(registration_info) flask.g.user.additional_info = combined_info current_session.add(flask.g.user) current_session.commit() with flask.current_app.arborist.context(): # make sure the user exists in Arborist flask.current_app.arborist.create_user(dict(name=flask.g.user.username)) flask.current_app.arborist.add_user_to_group( flask.g.user.username, config["REGISTERED_USERS_GROUP"], ) # Respect session redirect--important when redirected here from login flow if flask.session.get("redirect"): return flask.redirect(flask.session.get("redirect")) return flask.jsonify(registration_info)
def give_service_account_billing_access_if_necessary( sa_private_key, r_pays_project=None, default_billing_project=None): """ Give the Service Account (whose key is provided) the privilege to bill to the given project. If a project is not provided and there is a configured Google project to bill to, we will use that. Args: sa_private_key (dict): JSON key in Google Credentials File format: .. code-block:: JavaScript { "type": "service_account", "project_id": "project-id", "private_key_id": "some_number", "private_key": "-----BEGIN PRIVATE KEY-----\n.... =\n-----END PRIVATE KEY-----\n", "client_email": "<api-name>[email protected]", "client_id": "...", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://accounts.google.com/o/oauth2/token", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", "client_x509_cert_url": "https://www.googleapis.com/...<api-name>api%40project-id.iam.gserviceaccount.com" } r_pays_project (str, optional): The Google Project identifier to bill to default_billing_project (str, optional): the default The Google Project identifier to bill to if r_pays_project is None """ if not r_pays_project and not default_billing_project: sa_account_id = sa_private_key.get("client_email") raise UserError( "You did NOT provide a `userProject` for requester pays billing, " "so we could not create a custom role in that project to provide " "the necessary service account ({}) billing permission. " "Our main service account ({}) will need valid permissions in the " "project you supplied to create a custom role and change the project " "IAM policy. There is no configured default billing project so you must " "provide a `userProject` query parameter.".format( sa_account_id, config["CIRRUS_CFG"].get("GOOGLE_ADMIN_EMAIL"))) # use configured project if it exists and no user project was given is_default_billing = False if default_billing_project and not r_pays_project: r_pays_project = default_billing_project is_default_billing = True if r_pays_project: sa_account_id = sa_private_key.get("client_email") try: # attempt to create custom role that gives # the SA access to bill the project provided # NOTE: this may fail if our fence SA doesn't have the right permissions # to add this role and update the project policy with GoogleCloudManager( project_id=r_pays_project) as g_cloud_manager: g_cloud_manager.give_service_account_billing_access( sa_account_id, project_id=r_pays_project) except Exception as exc: logger.error( "Unable to create a custom role in Google Project {} to " "give Google service account {} rights to bill the project. Error: {}" .format(r_pays_project, sa_account_id, exc)) if is_default_billing: raise InternalError( "Fence has a configured Google Project for requester pays billing ({}), " "but could not create a custom role in that project to provide " "the necessary service account ({}) billing permission. It could be that " "the Fence admin service account ({}) does not have valid permissions in the " "project.".format( r_pays_project, sa_account_id, config["CIRRUS_CFG"].get("GOOGLE_ADMIN_EMAIL"), )) else: raise NotSupported( "You provided {} as a `userProject` for requester pays billing, " "but we could not create a custom role in that project to provide " "the necessary service account ({}) billing permission. It could be that " "our main service account ({}) does not have valid permissions in the " "project you supplied to create a custom role and change the project IAM policy." .format( r_pays_project, sa_account_id, config["CIRRUS_CFG"].get("GOOGLE_ADMIN_EMAIL"), )) logger.info( "Created a custom role in Google Project {} to " "give Google service account {} rights to bill the project.". format(r_pays_project, sa_account_id))
def upload_data_file(): """ Return a presigned URL for use with uploading a data file. See the documentation on the entire flow here for more info: https://github.com/uc-cdis/cdis-wiki/tree/master/dev/gen3/data_upload """ # make new record in indexd, with just the `uploader` field (and a GUID) params = flask.request.get_json() if not params: raise UserError("wrong Content-Type; expected application/json") if "file_name" not in params: raise UserError("missing required argument `file_name`") authorized = False authz_err_msg = "Auth error when attempting to get a presigned URL for upload. User must have '{}' access on '{}'." authz = params.get("authz") uploader = None if authz: # if requesting an authz field, using new authorization method which doesn't # rely on uploader field, so clear it out uploader = "" authorized = flask.current_app.arborist.auth_request( jwt=get_jwt(), service="fence", methods=["create", "write-storage"], resources=authz, ) if not authorized: logger.error( authz_err_msg.format("create' and 'write-storage", authz)) else: # no 'authz' was provided, so fall back on 'file_upload' logic authorized = flask.current_app.arborist.auth_request( jwt=get_jwt(), service="fence", methods=["file_upload"], resources=["/data_file"], ) if not authorized: logger.error(authz_err_msg.format("file_upload", "/data_file")) if not authorized: raise Forbidden( "You do not have access to upload data. You either need " "general file uploader permissions or create and write-storage permissions " "on the authz resources you specified (if you specified any).") blank_index = BlankIndex(file_name=params["file_name"], authz=params.get("authz"), uploader=uploader) default_expires_in = flask.current_app.config.get("MAX_PRESIGNED_URL_TTL", 3600) expires_in = get_valid_expiration( params.get("expires_in"), max_limit=default_expires_in, default=default_expires_in, ) protocol = params["protocol"] if "protocol" in params else None response = { "guid": blank_index.guid, "url": blank_index.make_signed_url(file_name=params["file_name"], protocol=protocol, expires_in=expires_in), } return flask.jsonify(response), 201
def delete_group(current_session, groupname): group = udm.get_group(current_session, groupname) if not group: raise UserError("Group doesn't exist") else: current_session.delete(group)
def get(self): """ Link a user's Google account after AuthN. This is Google's callback that occurs after oauth2 flow and does the actual linkage/creation in our db. This will redirect with `error` and `error_description` query params if any issues arise. Raises: UserError: No redirect provided """ provided_redirect = flask.session.get("redirect") code = flask.request.args.get("code") if not config.get("MOCK_GOOGLE_AUTH", False): google_response = flask.current_app.google_client.get_user_id(code) email = google_response.get("email") else: # if we're mocking google auth, mock response to include the email # from the provided access token try: token = validate_request({"user"}) email = get_user_from_claims(token).username except Exception as exc: logger.info( "Unable to parse Google email from token, using default mock value. " "Error: {}".format(exc)) email = flask.request.cookies.get( config.get("DEV_LOGIN_COOKIE_NAME"), "*****@*****.**") error = "" error_description = "" # get info from session and then clear it user_id = flask.session.get("user_id") proxy_group = flask.session.get("google_proxy_group_id") expires_in = flask.session.get("google_link_expires_in") _clear_google_link_info_from_session() if not email: error = "g_acnt_auth_failure" error_description = google_response else: error, error_description = get_errors_update_user_google_account_dry_run( user_id, email, proxy_group, _already_authed=True) if not error: exp = _force_update_user_google_account( user_id, email, proxy_group, _allow_new=True, requested_expires_in=expires_in, ) # TODO: perhaps this is problematic?? # keep linked email in session so when session refreshes access # token, we don't have to hit db to see if user has linked acnt # NOTE: This only saves us from a db hit if they maintain their # session flask.session["linked_google_email"] = email # if we have a redirect, follow it and add any errors if provided_redirect: if error: redirect_with_params = append_query_params( provided_redirect, error=error, error_description=error_description) else: redirect_with_params = append_query_params(provided_redirect, linked_email=email, exp=exp) return flask.redirect(redirect_with_params) else: # we don't have a redirect, so the endpoint was probably hit # without the actual auth flow. Raise with error info if error: raise UserError({error: error_description}) else: raise UserError({"error": "No redirect provided."})
def _link_google_account(): provided_redirect = flask.request.args.get("redirect") if not provided_redirect: raise UserError({"error": "No redirect provided."}) user_id = current_token["sub"] google_email = get_users_linked_google_email(user_id) proxy_group = get_or_create_proxy_group_id() # Set session flag to signify that we're linking and not logging in # Save info needed for linking in session since we need to AuthN first flask.session["google_link"] = True flask.session["user_id"] = user_id flask.session["google_proxy_group_id"] = proxy_group flask.session["linked_google_email"] = google_email if not google_email: # save off provided redirect in session and initiate Google AuthN flask.session["redirect"] = provided_redirect # requested time (in seconds) during which the link will be valid requested_expires_in = get_valid_expiration_from_request() if requested_expires_in: flask.session["google_link_expires_in"] = requested_expires_in # if we're mocking Google login, skip to callback if config.get("MOCK_GOOGLE_AUTH", False): flask.redirect_url = (config["BASE_URL"].strip("/") + "/link/google/callback?code=abc") response = flask.redirect(flask.redirect_url) # pass-through the authorization header. The user's username # MUST be a Google email for MOCK_GOOGLE_AUTH to actually link that # email correctly response.headers["Authorization"] = flask.request.headers.get( "Authorization") return response flask.redirect_url = flask.current_app.google_client.get_auth_url() # Tell Google to let user select an account flask.redirect_url = append_query_params(flask.redirect_url, prompt="select_account") else: # double check that the token isn't stale by hitting db linked_email_in_db = get_linked_google_account_email(user_id) if linked_email_in_db: # skip Google AuthN, already linked, error redirect_with_errors = append_query_params( provided_redirect, error="g_acnt_link_error", error_description= "User already has a linked Google account.", ) flask.redirect_url = redirect_with_errors _clear_google_link_info_from_session() else: # TODO can we handle this error? redirect_with_errors = append_query_params( provided_redirect, error="g_acnt_link_error", error_description="Stale access token, please refresh.", ) flask.redirect_url = redirect_with_errors _clear_google_link_info_from_session() return flask.redirect(flask.redirect_url)