Beispiel #1
0
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)
Beispiel #2
0
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
Beispiel #3
0
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)
Beispiel #4
0
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))
Beispiel #5
0
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
Beispiel #6
0
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)
Beispiel #7
0
    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."})
Beispiel #8
0
    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)