Exemplo n.º 1
0
 def put(self):
     logger.debug("top of  PUT /tokens/keys")
     validator = RequestValidator(utils.spec)
     validated = validator.validate(FlaskOpenAPIRequest(request))
     if validated.errors:
         raise errors.ResourceError(msg=f'Invalid PUT data: {validated.errors}.')
     tenant_id = validated.body.tenant_id
     logger.debug(f"calling check_authz_private_keypair with tenant_id {tenant_id}")
     check_authz_private_keypair(tenant_id)
     logger.debug("returned from check_authz_private_keypair; updating keys...")
     private_key, public_key = generate_private_keypair_in_sk(tenant_id)
     # update the tenant definition with the new public key
     logger.debug(f"making request to update tenant {tenant_id} with new public key.")
     try:
         t.tenants.update_tenant(tenant_id=tenant_id, public_key=public_key)
     except Exception as e:
         logger.error(f"Got exception trying to update tenant with new public key. Tenants API"
                      f"and SK are now out of sync!! SHOULD BE LOOKED AT IMMEDIATELY. "
                      f"Exception: {e}")
         raise errors.ResourceError(msg=f'Unable to update tenant definition with new public key'
                                        f'Please contact system administrators.')
     logger.info(f"tenant {tenant_id} has been updated with the new public key.")
     # update token's tenant cache with this private key for signing:
     logger.debug("updating token cache...")
     for tenant in tenants.tenants:
         if tenant.tenant_id == tenant_id:
             tenant.private_key = private_key
     result = {'public_key': public_key}
     return utils.ok(result=result, msg="Tenant signing keys update successful.")
Exemplo n.º 2
0
 def post(self):
     logger.debug("top of POST /ldaps")
     validator = RequestValidator(utils.spec)
     result = validator.validate(FlaskOpenAPIRequest(request))
     if result.errors:
         raise errors.ResourceError(msg=f'Invalid POST data: {result.errors}.')
     validated_params = result.parameters
     validated_body = result.body
     logger.debug(f"validated_body: {dir(validated_body)}")
     ldap = LDAPConnection(ldap_id=validated_body.ldap_id,
                           url=validated_body.url,
                           port=validated_body.port,
                           use_ssl=validated_body.use_ssl,
                           user_dn=validated_body.user_dn,
                           bind_dn=validated_body.bind_dn,
                           bind_credential=validated_body.bind_credential,
                           account_type=validated_body.account_type,
                           create_time=datetime.datetime.utcnow(),
                           last_update_time = datetime.datetime.utcnow())
     db.session.add(ldap)
     try:
         db.session.commit()
     except (sqlalchemy.exc.SQLAlchemyError, sqlalchemy.exc.DBAPIError) as e:
         msg = utils.get_message_from_sql_exc(e)
         raise errors.ResourceError(f"Invalid POST data; {msg}")
     return utils.ok(result=ldap.serialize,
                     msg="LDAP object created successfully.")
Exemplo n.º 3
0
    def post(self):
        logger.debug("top of  POST /tokens")
        validator = RequestValidator(utils.spec)
        validated = validator.validate(FlaskOpenAPIRequest(request))
        if validated.errors:
            raise errors.ResourceError(msg=f'Invalid POST data: {validated.errors}.')
        validated_body = validated.body
        # this raises an exception if the claims are invalid -
        if hasattr(validated_body, 'claims'):
            check_extra_claims(request.json.get('claims'))
            # set it to the raw request's claims object which is an arbitrary python dictionary
            validated_body.claims = request.json.get('claims')
        try:
            token_data = TapisAccessToken.get_derived_values(validated_body)
        except Exception as e:
            logger.error(f"Got exception trying to compute get_derived_values() for validated body; e: {e}")
            raise errors.AuthenticationError("Unable to create token. Please contact system administrator.")
        access_token = TapisAccessToken(**token_data)
        try:
            access_token.sign_token()
        except Exception as e:
            logger.error(f"Got exception trying to sign token! Exception: {e}")
            raise errors.AuthenticationError("Unable to sign token. Please contact system administrator.")

        result = {'access_token': access_token.serialize}

        # refresh token --
        if hasattr(validated_body, 'generate_refresh_token') and validated_body.generate_refresh_token:
            if hasattr(validated_body, 'refresh_token_ttl'):
                token_data['refresh_token_ttl'] = validated_body.refresh_token_ttl
            refresh_token = TokensResource.get_refresh_from_access_token_data(token_data, access_token)
            result['refresh_token'] = refresh_token.serialize
        return utils.ok(result=result, msg="Token generation successful.")
Exemplo n.º 4
0
 def delete(self, email):
     logger.debug(f"top of DELETE /owners/{email}")
     owner = TenantOwner.query.filter_by(email=email).first()
     if not owner:
         raise errors.ResourceError(msg=f'No owner object found with email {email}.')
     db.session.delete(owner)
     db.session.commit()
     return utils.ok(result=None, msg=f'Owner object {owner} deleted successfully.')
Exemplo n.º 5
0
    def get(self, tenant_id):
        logger.debug(f"top of GET /tenants/{tenant_id}/history")
        tenant = Tenant.query.filter_by(tenant_id=tenant_id).first()
        if not tenant:
            raise errors.ResourceError(msg=f'No tenant found with tenant_id {tenant_id}.')

        tenant_history_list = TenantHistory.query.filter_by(tenant_id=tenant_id).all()
        return utils.ok(result=[t.serialize for t in tenant_history_list], msg='Tenant history retrieved successfully.')
Exemplo n.º 6
0
 def delete(self, site_id):
     logger.debug(f"top of DELETE /sites/{site_id}")
     site = Site.query.filter_by(site_id=site_id).first()
     if not site:
         logger.debug(f"Did not find a site with id {site_id}. Returning an error.")
         raise errors.ResourceError(msg=f'No site found with site_id {site_id}.')
     logger.debug("site found; issuing delete and commit.")
     db.session.delete(site)
     db.session.commit()
     return utils.ok(result=None, msg=f'Site {site_id} deleted successfully.')
Exemplo n.º 7
0
    def put(self):
        logger.debug("top of  PUT /tokens")
        validator = RequestValidator(utils.spec)
        validated = validator.validate(FlaskOpenAPIRequest(request))
        if validated.errors:
            raise errors.ResourceError(msg=f'Invalid PUT data: {validated.errors}.')
        refresh_token = validated.body.refresh_token
        logger.debug(f"type(refresh_token) = {type(refresh_token)}")
        try:
            refresh_token_data = auth.validate_token(refresh_token)
        except errors.AuthenticationError:
            raise errors.ResourceError(msg=f'Invalid PUT data: {request}.')

        # get the original access_token data from within the decoded refresh_token
        token_data = refresh_token_data['tapis/access_token']
        token_data.pop('tapis/token_type')
        token_data['exp'] = TapisAccessToken.compute_exp(token_data['ttl'])
        token_data['jti'] = str(uuid.uuid4())
        # create a dictionary of data that can be used to instantiate access and refresh tokens; the constructors
        # require variable names that do not include the Tapis prefix, so we need to remove that -
        new_token_data = { 'jti': token_data.pop('jti'),
                           'iss': token_data.pop('iss'),
                           'sub': token_data.pop('sub'),
                           'tenant_id': token_data.pop('tapis/tenant_id'),
                           'username': token_data.pop('tapis/username'),
                           'account_type': token_data.pop('tapis/account_type'),
                           'ttl': token_data.pop('ttl'),
                           'exp': token_data.pop('exp'),
                           'delegation': token_data.pop('tapis/delegation'),
                           'delegation_sub': token_data.pop('tapis/delegation_sub', None),
                           'extra_claims': token_data
                           }
        access_token = TapisAccessToken(**new_token_data)
        access_token.sign_token()

        # add the original refresh token's initial_ttl claim as the ttl for the new refresh token
        new_token_data['refresh_token_ttl'] = refresh_token_data['tapis/initial_ttl']
        refresh_token = TokensResource.get_refresh_from_access_token_data(new_token_data, access_token)
        result = {'access_token': access_token.serialize,
                  'refresh_token': refresh_token.serialize
                  }
        return utils.ok(result=result, msg="Token generation successful.")
Exemplo n.º 8
0
 def delete(self, ldap_id):
     logger.debug(f"top of DELETE /ldaps/{ldap_id}")
     ldap = LDAPConnection.query.filter_by(ldap_id=ldap_id).first()
     if not ldap:
         raise errors.ResourceError(msg=f'No LDAP object found with id {ldap_id}.')
     # ensure ldap is not references by existing tenant:
     tenants = Tenant.query.filter_by(service_ldap_connection_id=ldap_id).first()
     if tenants:
         logger.info("LDAP currently in use by tenants.")
         raise errors.ResourceError(msg='This LDAP is in use by existing tenants; delete the tenants firts.')
     tenants = Tenant.query.filter_by(user_ldap_connection_id=ldap_id).first()
     if tenants:
         logger.info("LDAP currently in use by tenants.")
         raise errors.ResourceError(msg='This LDAP is in use by existing tenants; delete the tenants firts.')
     try:
         db.session.delete(ldap)
         db.session.commit()
     except (sqlalchemy.exc.SQLAlchemyError, sqlalchemy.exc.DBAPIError) as e:
         msg = utils.get_message_from_sql_exc(e)
         raise errors.ResourceError(f"Invalid POST data; {msg}")
     return utils.ok(result=None, msg=f'LDAP object {ldap_id} deleted successfully.')
Exemplo n.º 9
0
 def get_token_from_callback(self, request):
     """
     For the cii tenant, get the token directly from the callback URL. This function is only called by
     the CII tenant; other tenants do standard OAuth and pass an authorization code which is exchanged
     for the token.
     :param request: The request made to the Tapis callback URL.
     :return:
     """
     if not self.ext_type == 'cii':
         msg = f'get_token_from_callback() called for a non-cii ext type; ext type: {self.ext_type}; ' \
               f'This function should only be called by cii tenants. ' \
               f'request: {request}.'
         logger.error(msg)
         raise errors.ResourceError(
             f"Program error; contact system administrators. "
             f"(Debug message: {msg})")
     # the CII OAuth server returns the access token in a URL query parameter, "token"
     self.access_token = request.args.get('token')
     if not self.access_token:
         msg = f"Did not get access token from CII callback. request args: {request.args}"
         raise errors.ResourceError()
     return self.access_token
Exemplo n.º 10
0
 def post(self):
     logger.debug(f"top of POST /owners")
     validator = RequestValidator(utils.spec)
     result = validator.validate(FlaskOpenAPIRequest(request))
     if result.errors:
         raise errors.ResourceError(msg=f'Invalid POST data: {result.errors}.')
     validated_params = result.parameters
     validated_body = result.body
     owner = TenantOwner(name=validated_body.name,
                         email=validated_body.email,
                         institution=validated_body.institution,
                         create_time=datetime.datetime.utcnow(),
                         last_update_time=datetime.datetime.utcnow()
                         )
     db.session.add(owner)
     try:
         db.session.commit()
     except (sqlalchemy.exc.SQLAlchemyError, sqlalchemy.exc.DBAPIError) as e:
         msg = utils.get_message_from_sql_exc(e)
         raise errors.ResourceError(f"Invalid POST data; {msg}")
     return utils.ok(result=owner.serialize,
                     msg="Owner object created successfully.")
Exemplo n.º 11
0
    def post(self):
        logger.debug("top of POST /sites")
        validator = RequestValidator(utils.spec)
        result = validator.validate(FlaskOpenAPIRequest(request))
        logger.debug(f"just got result {result.parameters}")
        if result.errors:
            logger.debug(f"error in results!!!!!!!!")
            raise errors.ResourceError(msg=f'Invalid POST data: {result.errors}')
        validated_params = result.parameters
        logger.debug('got validated params')
        validated_body = result.body
        logger.debug(f'got validated body {dir(validated_body)}')
        try:
            # request is trying to create the primary site:
            if validated_body.primary:
                logger.debug('checks for primary site')
                primary_site = Site.query.filter_by(primary=True).first()
                if primary_site:
                    raise errors.ResourceError("Invalid site description: a primary site already exists.")
                if not validated_body.tenant_base_url_template:
                    raise errors.ResourceError("Invalid site description: tenant_base_url_template is required for primary site.")
                site = Site(site_id=validated_body.site_id,
                            primary=validated_body.primary,
                            base_url=validated_body.base_url,
                            tenant_base_url_template=validated_body.tenant_base_url_template,
                            site_admin_tenant_id=validated_body.site_admin_tenant_id,
                            services=validated_body.services,
                            create_time=datetime.datetime.utcnow(),
                            created_by=f'{g.username}@{g.tenant_id}',
                            last_updated_by=f'{g.username}@{g.tenant_id}',
                            last_update_time=datetime.datetime.utcnow())

            # request if for an associate site:
            else:
                logger.debug(f'checks for associate site.')
                if hasattr(validated_body, 'tenant_base_url_template') and validated_body.tenant_base_url_template:
                    raise errors.ResourceError("Invalid site description; "
                                               "the tenant_base_url_template property only applies to primary sites.")
                site = Site(site_id=validated_body.site_id,
                            primary=False,
                            base_url=validated_body.base_url,
                            site_admin_tenant_id=validated_body.site_admin_tenant_id,
                            services=validated_body.services,
                            create_time=datetime.datetime.utcnow(),
                            created_by=f'{g.username}@{g.tenant_id}',
                            last_updated_by=f'{g.username}@{g.tenant_id}',
                            last_update_time=datetime.datetime.utcnow())
            logger.info(f'creating site {validated_body.site_id}')
        except Exception as e:
            raise errors.ResourceError(f"Invalid POST data; {e}")
        db.session.add(site)
        try:
            db.session.commit()
        except (sqlalchemy.exc.SQLAlchemyError, sqlalchemy.exc.DBAPIError) as e:
            msg = utils.get_message_from_sql_exc(e)
            raise errors.ResourceError(f"Invalid POST data; {msg}")
        logger.info(f"site {validated_body.site_id} saved in db.")
        return utils.ok(result=site.serialize,
                        msg="Site object created successfully.")
Exemplo n.º 12
0
def check_if_primary(data):
    if data.primary and data.base_url is not None and data.tenant_base_url_template is not None:
        logger.debug('checking if primary')
        primary_site = Site.query.filter_by(primary=True).first()
        if primary_site:
            raise errors.ResourceError("A primary site already exists.")
        else:
            site = Site(site_id=data.site_id,
                        primary=data.primary,
                        base_url=data.base_url,
                        tenant_base_url_template=data.tenant_base_url_template,
                        site_admin_tenant_id=data.site_admintenant_id,
                        services=data.services)
    elif data.primary and data.base_url is None:
        logger.debug('checking if primary but no base url provided')
        raise errors.ResourceError(f"Invalid POST data")
    else:
        logger.debug(
            f'not primary, creating site {data.tenant_base_url_template}')
        site = Site(site_id=data.site_id,
                    primary=False,
                    tenant_base_url_template=data.tenant_base_url_template,
                    site_admin_tenant_id=data.site_admin_tenant_id,
                    services=data.services)
Exemplo n.º 13
0
def authentication():
    """
    Entry point for checking authentication for all requests to the authenticator.
    :return:
    """
    # The authenticator uses different authentication methods for different endpoints. For example, the service
    # APIs such as clients and profiles use pure JWT authentication, while the OAuth endpoints use Basic Authentication
    # with OAuth client credentials.
    logger.debug(f"base_url: {request.base_url}; url_rule: {request.url_rule}")
    if not hasattr(request, 'url_rule') or not hasattr(
            request.url_rule, 'rule') or not request.url_rule.rule:
        raise common_errors.ResourceError(
            "The endpoint and HTTP method combination "
            "are not available from this service.")

    # the metadata endpoint is publicly available
    if '/v3/oauth2/.well-known/' in request.url_rule.rule:
        logger.debug(
            ".well-known endpoint; request is allowed to be made unauthenticated."
        )
        auth.resolve_tenant_id_for_request()
        return True
    # only the authenticator's own service token and tenant admins for the tenant can retrieve or modify the tenant
    # config
    if '/v3/oauth2/admin' in request.url_rule.rule:
        logger.debug(
            "admin endpoint; checking for authentictor service token or tenant admin role..."
        )
        # admin endpoints always require tapis token auth
        auth.authentication()
        # we'll need to use the request's tenant_id, so make sure it is resolved now
        auth.resolve_tenant_id_for_request()
        # first, make sure this request is for a tenant served by this authenticator
        if g.request_tenant_id not in conf.tenants:
            raise common_errors.PermissionsError(
                f"The request is for a tenant ({g.request_tenant_id}) that is not "
                f"served by this authenticator.")
        # we only want to honor tokens from THIS authenticator; i.e., not some other authenticator. therefore, we need
        # to check that the tenant_id associated with the token (g.tenant_id) is the same as THIS authenticator's tenant
        # id;
        if g.username == conf.service_name and g.tenant_id == conf.service_tenant_id:
            logger.info(
                f"allowing admin request because username was {conf.service_name} "
                f"and tenant was {conf.service_tenant_id}")
            return True
        logger.debug(
            f"request token does not represent THIS authenticator: token username: {g.username};"
            f" request tenant: {g.tenant_id}. Now checking for tenant admin..."
        )
        # all other service accounts are not allowed to update authenticator
        if g.account_type == 'service':
            raise common_errors.PermissionsError(
                "Not authorized -- service accounts are not allowed to access the"
                "authenticator admin endpoints.")
        # sanity check -- the request tenant id should be the same as the token tenant id in the remaining cases because
        # they are all user tokens
        if not g.request_tenant_id == g.tenant_id:
            logger.error(
                f"program error -- g.request_tenant_id: {g.request_tenant_id} not equal to "
                f"g.tenant_id: {g.tenant_id} even though account type was user!"
            )
            raise common_errors.ServiceConfigError(
                f"Unexpected program error checking permissions. The tenant id of"
                f"the request ({g.request_tenant_id})  did not match the tenant id "
                f"of the access token ({g.tenant_id}). Please contact server "
                f"administrators.")
        # check SK for tenant admin --
        try:
            rsp = t.sk.isAdmin(tenant=g.tenant_id, user=g.username)
        except Exception as e:
            logger.error(
                f"Got exception trying to check tenant admin role for tenant: {g.tenant_id} "
                f"and user: {g.username}; exception: {e}")
            raise common_errors.PermissionsError(
                "Could not check tenant admin role with SK; this role is required for "
                "accessing the authenticator admin endpoints.")
        try:
            if rsp.isAuthorized:
                logger.info(
                    f"user {g.username} had tenant admin role for tenant {g.tenant_id}; allowing request."
                )
                return True
            else:
                logger.info(
                    f"user {g.username} DID NOT have tenant admin role for tenant {g.tenant_id}; "
                    f"NOT allowing request.")
                raise common_errors.PermissionsError(
                    "Permission denied -- Tenant admin role required for accessing "
                    "the authenticator admin endpoints.")
        except Exception as e:
            logger.error(
                f"got exception trying to check isAuthorized property from isAdmin() call to SK."
                f"username: {g.username}; tenant: {g.tenant_id}; rsp: {rsp}; e: {e}"
            )
            logger.info(
                f"user {g.username} DID NOT have tenant admin role for tenant {g.tenant_id}; "
                f"NOT allowing request.")
            raise common_errors.PermissionsError(
                "Permission denied -- Tenant admin role required for accessing the "
                "authenticator admin endpoints.")

    # no credentials required on the authorize, login and oa2 extenion pages
    if '/v3/oauth2/authorize' in request.url_rule.rule or '/v3/oauth2/login' in request.url_rule.rule \
            or '/oauth2/extensions' in request.url_rule.rule or 'v3/oauth2/mfa' in request.url_rule.rule \
            or '/v3/oauth2/device' in request.url_rule.rule:
        # always resolve the request tenant id based on the URL:
        logger.debug(
            "authorize, login or oa2 extension page. Resolving tenant_id")
        auth.resolve_tenant_id_for_request()
        try:
            logger.debug(f"request_tenant_id: {g.request_tenant_id}")
        except AttributeError:
            raise common_errors.BaseTapisError(
                "Unable to resolve tenant_id for request.")
        # make sure this request is for a tenant served by this authenticator
        if g.request_tenant_id not in conf.tenants:
            raise common_errors.PermissionsError(
                f"The request is for a tenant ({g.request_tenant_id}) that is not "
                f"served by this authenticator.")
        return True

    # the profiles endpoints always use standard Tapis Token auth -
    if '/v3/oauth2/profiles' in request.url_rule.rule or \
            '/v3/oauth2/userinfo' in request.url_rule.rule:
        auth.authentication()
        # always resolve the request tenant id based on the URL:
        auth.resolve_tenant_id_for_request()
        # make sure this request is for a tenant served by this authenticator
        if g.request_tenant_id not in conf.tenants:
            raise common_errors.PermissionsError(
                f"The request is for a tenant ({g.request_tenant_id}) that is not "
                f"served by this authenticator.")
        return True

    # the clients endpoints need to accept both standard Tapis Token auth and basic auth,
    if '/v3/oauth2/clients' in request.url_rule.rule:
        # first check for basic auth header:
        parts = get_basic_auth_parts()
        if parts:
            logger.debug("oauth2 clients page, with basic auth header.")
            # do basic auth against the ldap
            # always resolve the request tenant id based on the URL:
            auth.resolve_tenant_id_for_request()
            # make sure this request is for a tenant served by this authenticator
            if g.request_tenant_id not in conf.tenants:
                raise common_errors.PermissionsError(
                    f"The request is for a tenant ({g.request_tenant_id}) that is not "
                    f"served by this authenticator.")
            try:
                logger.debug(f"request_tenant_id: {g.request_tenant_id}")
            except AttributeError:
                raise common_errors.BaseTapisError(
                    "Unable to resolve tenant_id for request.")
            check_username_password(parts['tenant_id'], parts['username'],
                                    parts['password'])
            return True
        else:
            logger.debug("oauth2 clients page, no basic auth header.")
            # check for a Tapis token
            auth.authentication()
            # always resolve the request tenant id based on the URL:
            auth.resolve_tenant_id_for_request()
            try:
                logger.debug(f"request_tenant_id: {g.request_tenant_id}")
            except AttributeError:
                raise common_errors.BaseTapisError(
                    "Unable to resolve tenant_id for request.")
            return True

    if '/v3/oauth2/tokens' in request.url_rule.rule:
        logger.debug("oauth2 tokens URL")
        # the tokens endpoint uses basic auth with the client; logic handled in the controller. # however, it does
        # require the request tenant id:

        # first, check if an X-Tapis-Token header appears in the request. We do not honor JWT authentication for
        # generating new tokens, but we also don't want to fail for an expired token. So, we remove the token header
        # if it is present
        if 'X-Tapis-Token' in request.headers:
            logger.debug("Got an X-Tapis-Token header.")
            try:
                auth.add_headers()
                auth.validate_request_token()
            except:
                # we need to set the token claims because the resolve_tenant_id_for_request method depends on it:
                g.token_claims = {}
        # now, resolve the tenant_id
        try:
            auth.resolve_tenant_id_for_request()
        except:
            # we need to catch and swallow permissions errors having to do with an invalid JWT; if the JWT is invalid,
            # its claims (including its tenant claim) will be ignored, but then resolve_tenant_id_for_request() will
            # throw an error because the None tenant_id claim will not match the tenant_id of the URL.
            pass
        try:
            logger.debug(f"request_tenant_id: {g.request_tenant_id}")
        except AttributeError:
            raise common_errors.BaseTapisError(
                "Unable to resolve tenant_id for request.")
        # make sure this request is for a tenant served by this authenticator
        if g.request_tenant_id not in conf.tenants:
            raise common_errors.PermissionsError(
                f"The request is for a tenant ({g.request_tenant_id}) that is not "
                f"served by this authenticator.")
        return True

    if 'v3/oauth2/v2/token' in request.url_rule.rule:
        logger.debug("v2 token URL")
        # the v2/token endpoint takes a v3 token generated for a user and returns a v2 token for that user

        if 'X-Tapis-Token' in request.headers:
            logger.debug(
                f"Got an X-Tapis-Token header; {request.headers['X-Tapis-Token']}"
            )
            try:
                auth.authentication()
                auth.resolve_tenant_id_for_request()
            except Exception as e:
                g.token_claims = {}
                raise common_errors.BaseTapisError(
                    f"Unable to process access token; error: {e}")
        else:
            logger.debug("did not receive an X-Tapis-Token header.")
            raise common_errors.BaseTapisError(
                "Endpoint requires X-Tapis-Token.")

        return True

    if '/v3/oauth2/logout' in request.url_rule.rule \
        or '/v3/oauth2/login' in request.url_rule.rule \
        or '/v3/oauth2/tenant' in request.url_rule.rule \
        or '/v3/oauth2/webapp' in request.url_rule.rule \
        or '/v3/oauth2/portal-login' in request.url_rule.rule:
        # or '/v3/oauth2/webapp/callback' in request.url_rule.rule \
        # or '/v3/oauth2/webapp/token-display' in request.url_rule.rule \
        logger.debug("call is for some token webapp page.")
        auth.resolve_tenant_id_for_request()
        try:
            logger.debug(f"request_tenant_id: {g.request_tenant_id}")
        except AttributeError:
            raise common_errors.BaseTapisError(
                "Unable to resolve tenant_id for request.")
        #  make sure this tenant allows the token web app
        config = tenant_configs_cache.get_config(g.request_tenant_id)
        logger.debug(f"got tenant config: {config.serialize}")
        if not config.use_token_webapp:
            logger.info(
                f"tenant {g.request_tenant_id} not configured for the token webapp. Raising error"
            )
            raise common_errors.PermissionsError(
                "This tenant is not configured to use the Token Webapp.")

        return True
Exemplo n.º 14
0
 def get(self, site_id):
     logger.debug(f"top of GET /sites/{site_id}")
     site = Site.query.filter_by(site_id=site_id).first()
     if not site:
         raise errors.ResourceError(msg=f'No site found with site_id {site_id}.')
     return utils.ok(result=site.serialize, msg='Site retrieved successfully.')
Exemplo n.º 15
0
    def get_user_from_token(self):
        """
        Determines the username for the user once an access token has been obtained.
        :return:
        """
        logger.debug("top of get_user_from_token")
        # todo -- each OAuth2 provider will have a different mechanism for determining the user's identity
        if self.ext_type == 'github' or self.ext_type == 'tacc_keycloak':
            if self.ext_type == 'github':
                user_info_url = 'https://api.github.com/user'
            if self.ext_type == 'tacc_keycloak':
                user_info_url = 'https://identity.tacc.cloud/auth/realms/tapis/protocol/openid-connect/userinfo'
            if self.ext_type == 'github':
                headers = {
                    'Authorization': f'token {self.access_token}',
                    'Accept': 'application/vnd.github.v3+json'
                }
            if self.ext_type == 'tacc_keycloak':
                headers = {
                    'Authorization': f'Bearer {self.access_token}',
                }
            try:
                rsp = requests.get(user_info_url, headers=headers)
            except Exception as e:
                logger.error(
                    f"Got exception from request to look up user's identity with {self.ext_type}. Debug data:"
                    f"exception: {e}")
                raise errors.ServiceConfigError(
                    "Error determining user identity. Contact server administrator."
                )
            if not rsp.status_code == 200:
                logger.error(
                    "Did not get 200 from request to look up user's identity with {self.ext_type}. Debug data:"
                    f"status code: {rsp.status_code};"
                    f"rsp content: {rsp.content}")
                raise errors.ServiceConfigError(
                    "Error determining user identity. Contact server administrator."
                )
            if self.ext_type == 'github':
                username = rsp.json().get('login')
                if not username:
                    logger.error(
                        f"username was none after processing the github response. Debug data:"
                        f"response: {rsp}")
                    raise errors.ServiceConfigError(
                        "Error determining user identity: username was empty. "
                        "Contact server administrator.")

                    self.username = f'{username}@github.com'
            elif self.ext_type == 'tacc_keycloak':
                logger.info(f"Response content from keycloak: {rsp.content}")
                try:
                    username = rsp.json().get('email')
                except Exception as e:
                    logger.error(
                        f"Got error trying to parse the username from the TACC Keycloak instance; error: {e}"
                    )
                    raise errors.ServiceConfigError(
                        "Error determining user identity: could not parse identity response. "
                        "Contact server administrator.")
                if not username:
                    logger.error(
                        f"Could not parse the username from the TACC Keycloak instance; username was empty"
                    )
                    raise errors.ServiceConfigError(
                        "Error determining user identity: username was empty. "
                        "Contact server administrator.")
                self.username = username
            logger.debug(
                f"Successfully determined user's identity: {self.username}")
            return self.username

        elif self.ext_type == 'cii':
            # the CII token is a JWT; we only need to decode it and get the username out of the payload.
            # todo -- we should verify the signature if that is working...
            logger.debug(f"CII jwt: {self.access_token}")
            try:
                claims = jwt.decode(self.access_token,
                                    self.jwt_decode_key,
                                    verify=self.check_jwt_signature,
                                    algorithms=["HS256"])
            except Exception as e:
                msg = f"got exception trying to decode the CII jwt; exception: {e}"
                logger.error(msg)
                raise errors.ResourceError(
                    f"Unable to decode third-party JWT. Contact system administrator."
                    f"(Debug message:{msg})")
            self.username = claims.get('username')
            if not self.username:
                msg = f"Did not get a username from the CII jwt; full claims: {claims}"
                logger.error(msg)
                raise errors.ResourceError(
                    f"Unable to determine username from third-party JWT. Contact system "
                    f"administrator."
                    f"(Debug message:{msg})")
            logger.debug(
                f"Successfully determined user's identity: {self.username}")
            return self.username
        # elif self.ext_type == 'google':
        #     ...
        else:
            logger.error(
                f"ERROR! OAuth2ProviderExtension.get_user_from_token not implemented for OAuth2 provider "
                f"extension {self.ext_type}.")
            raise errors.ServiceConfigError(
                f"Error determining user identity: extension type {self.ext_type} not "
                f"supported.")
Exemplo n.º 16
0
 def delete(self, tenant_id):
     logger.debug(f"top of DELETE /tenants/{tenant_id}")
     # updated jfs 5/2021 -- removed delete functionality, as deleting tenants can cause issues with
     # historical data that reference the tenant_id.
     raise errors.ResourceError(msg=f'Deleting tenants is not supported; '
                                    f'update the tenant status to inactive instead.')
Exemplo n.º 17
0
    def put(self, tenant_id):
        logger.debug(f"top of PUT /tenants/{tenant_id}")
        tenant = Tenant.query.filter_by(tenant_id=tenant_id).first()
        if not tenant:
            raise errors.ResourceError(msg=f'No tenant found with tenant_id {tenant_id}.')
        # additional authorization checks on update based on the tenant_id of the request:
        check_authz_tenant_update(tenant_id)
        validator = RequestValidator(utils.spec)
        result = validator.validate(FlaskOpenAPIRequest(request))
        if result.errors:
            logger.debug(f"openapi_core validation failed. errors: {result.errors}")
            raise errors.ResourceError(msg=f'Invalid PUT data: {result.errors}.')
        validated_body = result.body
        logger.debug(f"initial openapi_core validation passed. validated_body: {dir(validated_body)}")
        # TODO --
        # ------------------------- This DOES NOT WORK ------------------------------------
        # the validated_body ONLY contains fields in the OAI spec; need to change this to look at the
        # request body itself
        if not getattr(validated_body, 'site_id', tenant.site_id) == tenant.site_id:
            raise errors.ResourceError(msg=f'Invalid PUT data: cannot change site_id.')
        if not getattr(validated_body, 'tenant_id', tenant.tenant_id) == tenant.tenant_id:
            raise errors.ResourceError(msg=f'Invalid PUT data: cannot change tenant_id.')
        if not getattr(validated_body, 'base_url', tenant.base_url) == tenant.base_url:
            raise errors.ResourceError(msg=f'Invalid PUT data: cannot change base_url.')
        # ------------------------------------------------------------------------------------

        # validate the existence of the ldap and owner objects:
        if getattr(validated_body, 'owner', None):
            owner = TenantOwner.query.filter_by(email=validated_body.owner).first()
            if not owner:
                raise errors.ResourceError(msg=f'Invalid tenant description. Owner {validated_body.owner} not found.')
            logger.debug("owner was valid.")
        if getattr(validated_body, 'user_ldap_connection_id', None):
            ldap = LDAPConnection.query.filter_by(ldap_id=validated_body.user_ldap_connection_id).first()
            if not ldap:
                raise errors.ResourceError(msg=f'Invalid tenant description. '
                                               f'LDAP {validated_body.user_ldap_connection_id} not found.')
        if getattr(validated_body, 'service_ldap_connection_id', None) and \
                not validated_body.service_ldap_connection_id == getattr(validated_body, 'user_ldap_connection_id', None):
            ldap = LDAPConnection.query.filter_by(ldap_id=validated_body.service_ldap_connection_id).first()
            if not ldap:
                raise errors.ResourceError(msg=f'Invalid tenant description. '
                                               f'LDAP {validated_body.service_ldap_connection_id} not found.')

        # overlay the tenant_current with the updates specified in the request body.
        changes_dict = {}
        # security_kernel
        new_security_kernel = getattr(validated_body, 'security_kernel', None)
        if new_security_kernel and not new_security_kernel == tenant.security_kernel:
            changes_dict['security_kernel'] = {'prev': tenant.security_kernel, 'new': new_security_kernel}
            tenant.security_kernel = new_security_kernel
        # token_service
        new_tokens_service = getattr(validated_body, 'token_service', None)
        if new_tokens_service and not new_tokens_service == tenant.token_service:
            changes_dict['tokens_service'] = {'prev': tenant.token_service, 'new': new_tokens_service}
            tenant.token_service = new_tokens_service
        # authenticator
        new_authenticator = getattr(validated_body, 'authenticator', None)
        if new_authenticator and not new_authenticator == tenant.authenticator:
            changes_dict['authenticator'] = {'prev': tenant.authenticator, 'new': new_authenticator}
            tenant.authenticator = new_authenticator
        # admin_user
        new_admin_user = getattr(validated_body, 'admin_user', None)
        if new_admin_user and not new_admin_user == tenant.admin_user:
            changes_dict['admin_user'] = {'prev': tenant.admin_user, 'new': new_admin_user}
            tenant.admin_user = new_admin_user
        # token_gen_services
        new_token_gen_services = getattr(validated_body, 'token_gen_services', None)
        if new_token_gen_services and not new_token_gen_services == tenant.token_gen_services:
            changes_dict['token_gen_services'] = {'prev': tenant.token_gen_services, 'new': new_token_gen_services}
            tenant.token_gen_services = new_token_gen_services
        # service_ldap_connection_id
        new_service_ldap_connection_id = getattr(validated_body, 'service_ldap_connection_id', None)
        if new_service_ldap_connection_id and not new_service_ldap_connection_id == tenant.service_ldap_connection_id:
            changes_dict['service_ldap_connection_id'] = {'prev': tenant.service_ldap_connection_id,
                                                          'new': new_service_ldap_connection_id}
            tenant.service_ldap_connection_id = new_service_ldap_connection_id
        # user_ldap_connection_id
        new_user_ldap_connection_id = getattr(validated_body, 'user_ldap_connection_id', None)
        if new_user_ldap_connection_id and not new_user_ldap_connection_id == tenant.user_ldap_connection_id:
            changes_dict['user_ldap_connection_id'] = {'prev': tenant.user_ldap_connection_id,
                                                          'new': new_user_ldap_connection_id}
            tenant.user_ldap_connection_id = new_user_ldap_connection_id
        # public_key
        new_public_key = getattr(validated_body, 'public_key', None)
        if new_public_key and not new_public_key == tenant.public_key:
            changes_dict['public_key'] = {'prev': tenant.public_key, 'new': new_public_key}
            tenant.public_key = new_public_key
        # status
        new_status = getattr(validated_body, 'status', None)
        if new_status and not new_status == tenant.status:
            changes_dict['status'] = {'prev': tenant.status.serialize, 'new': new_status.upper()}
            tenant.status = new_status
        # description
        new_description = getattr(validated_body, 'description', None)
        if new_description and not new_description == tenant.description:
            changes_dict['description'] = {'prev': tenant.description, 'new': new_description}
            tenant.description = new_description
        # owner
        new_owner = getattr(validated_body, 'owner', None)
        if new_owner and not new_owner == tenant.owner:
            changes_dict['owner'] = {'prev': tenant.owner, 'new': new_owner}
            tenant.owner = new_owner
        # last_update_time and last_updated_by
        update_time = datetime.datetime.utcnow()
        updated_by = f'{g.username}@{g.tenant_id}'
        tenant.last_update_time = update_time
        tenant.last_updated_by = updated_by
        # create the history record
        tenant_history = TenantHistory(
            tenant_id=tenant.tenant_id,
            update_time=update_time,
            updated_by=updated_by,
            updates_as_json=json.dumps(changes_dict)
        )
        db.session.add(tenant_history)
        try:
            db.session.commit()
            logger.info(f"update to tenant committed to db. tenant object: {tenant}")
        except (sqlalchemy.exc.SQLAlchemyError, sqlalchemy.exc.DBAPIError) as e:
            logger.debug(f"got exception trying to commit updated tenant object to db. Exception: {e}")
            msg = utils.get_message_from_sql_exc(e)
            logger.debug(f"returning msg: {msg}")
            raise errors.ResourceError(f"Invalid PUT data; {msg}")
        logger.debug("returning serialized tenant object.")
        return utils.ok(result=tenant.serialize, msg="Tenant updated successfully.")
Exemplo n.º 18
0
 def get(self, tenant_id):
     logger.debug(f"top of GET /tenants/{tenant_id}")
     tenant = Tenant.query.filter_by(tenant_id=tenant_id).first()
     if not tenant:
         raise errors.ResourceError(msg=f'No tenant found with tenant_id {tenant_id}.')
     return utils.ok(result=tenant.serialize, msg='Tenant retrieved successfully.')
Exemplo n.º 19
0
    def post(self):
        logger.debug(f"top of POST /tenants")
        validator = RequestValidator(utils.spec)
        result = validator.validate(FlaskOpenAPIRequest(request))
        if result.errors:
            logger.debug(f"openapi_core validation failed. errors: {result.errors}")
            raise errors.ResourceError(msg=f'Invalid POST data: {result.errors}.')

        validated_body = result.body
        logger.debug(f"initial openapi_core validation passed. validated_body: {dir(validated_body)}")

        # check reserved words "owners" and "ldaps" -- these cannot be tenant id's:
        try:
            if validated_body.tenant_id.lower() == 'owners':
                raise errors.ResourceError("Invalid tenant_id; 'owners' is a reserved keyword.")
            if validated_body.tenant_id.lower() == 'ldaps':
                raise errors.ResourceError("Invalid tenant_id; 'ldaps' is a reserved keyword.")
            if validated_body.tenant_id.lower() == 'ready':
                raise errors.ResourceError("Invalid tenant_id; 'ready' is a reserved keyword.")
            if validated_body.tenant_id.lower() == 'hello':
                raise errors.ResourceError("Invalid tenant_id; 'hello' is a reserved keyword.")
        except Exception as e:
            msg = f"Could not check tenant description for reserved words; Errors: {e}"
            logger.error(msg)
            raise errors.ResourceError(msg)
        logger.debug("got past the reserved words check.")
        # validate the existence of the site object:
        try:
            site_id = validated_body.site_id
            site = Site.query.filter_by(site_id=site_id).first()
        except Exception as e:
            logger.error(f"Got exception trying to retrieve site; e: {e}")
            raise errors.ResourceError(msg='Invalid tenant description; could not verify site_id.')
        if not site:
            raise errors.ResourceError(msg=f'Invalid tenant description. site {validated_body.site_id} not found.')
        logger.debug(f"site_id {site_id} is ok.")
        # validate the existence of the ldap and owner objects:
        owner = TenantOwner.query.filter_by(email=validated_body.owner).first()
        if not owner:
            raise errors.ResourceError(msg=f'Invalid tenant description. Owner {validated_body.owner} not found.')
        logger.debug("owner was valid.")

        # ldap objects are optional:
        if getattr(validated_body, 'user_ldap_connection_id', None):
            ldap = LDAPConnection.query.filter_by(ldap_id=validated_body.user_ldap_connection_id).first()
            if not ldap:
                raise errors.ResourceError(msg=f'Invalid tenant description. '
                                               f'LDAP {validated_body.user_ldap_connection_id} not found.')
        if getattr(validated_body, 'service_ldap_connection_id', None) and \
                not validated_body.service_ldap_connection_id == getattr(validated_body, 'user_ldap_connection_id', None):
            ldap = LDAPConnection.query.filter_by(ldap_id=validated_body.service_ldap_connection_id).first()
            if not ldap:
                raise errors.ResourceError(msg=f'Invalid tenant description. '
                                               f'LDAP {validated_body.service_ldap_connection_id} not found.')

        logger.debug("ldap was valid; creating tenant record..")
        # create the tenant record --
        tenant = Tenant(tenant_id=validated_body.tenant_id,
                        base_url=validated_body.base_url,
                        site_id=validated_body.site_id,
                        status=validated_body.status,
                        public_key=getattr(validated_body, 'public_key', None),
                        token_service=validated_body.token_service,
                        security_kernel=validated_body.security_kernel,
                        authenticator=validated_body.authenticator,
                        owner=validated_body.owner,
                        admin_user=validated_body.admin_user,
                        token_gen_services=validated_body.token_gen_services,
                        service_ldap_connection_id=getattr(validated_body, 'service_ldap_connection_id', None),
                        user_ldap_connection_id=getattr(validated_body, 'user_ldap_connection_id', None),
                        description=getattr(validated_body, 'description', None),
                        create_time=datetime.datetime.utcnow(),
                        created_by=f'{g.username}@{g.tenant_id}',
                        last_updated_by=f'{g.username}@{g.tenant_id}',
                        last_update_time=datetime.datetime.utcnow())
        db.session.add(tenant)
        try:
            db.session.commit()
            logger.info(f"new tenant committed to db. tenant object: {tenant}")
        except (sqlalchemy.exc.SQLAlchemyError, sqlalchemy.exc.DBAPIError) as e:
            logger.debug(f"got exception trying to commit new tenant object to db. Exception: {e}")
            msg = utils.get_message_from_sql_exc(e)
            logger.debug(f"returning msg: {msg}")
            raise errors.ResourceError(f"Invalid POST data; {msg}")
        logger.debug("returning serialized tenant object.")
        return utils.ok(result=tenant.serialize, msg="Tenant created successfully.")
Exemplo n.º 20
0
    def __init__(self, tenant_id, is_local_development=False):
        # the tenant id for this OAuth2 provider
        self.tenant_id = tenant_id
        # the custom tenant config for this tenant
        self.tenant_config = tenant_configs_cache.get_config(
            tenant_id=tenant_id)
        # the type of OAuth2 provider, such as github
        self.ext_type = tenant_configs_cache.get_custom_oa2_extension_type(
            tenant_id)
        # the actual custom_idp_configuration object, as a python dictionary
        self.custom_idp_config_dict = json.loads(
            self.tenant_config.custom_idp_configuration)
        # whether or not this authenticator is running in local development mode (i.e., on localhost)
        self.is_local_development = is_local_development
        # validate that this tenant should be using the OAuth2 extension module.
        if not self.ext_type:
            raise errors.ResourceError(
                f"Tenant {tenant_id} not configured for a custom OAuth2 extension."
            )
        tenant_base_url = t.tenant_cache.get_tenant_config(tenant_id).base_url
        if self.is_local_development:
            self.callback_url = f'http://localhost:5000/v3/oauth2/extensions/oa2/callback'
        else:
            self.callback_url = f'{tenant_base_url}/v3/oauth2/extensions/oa2/callback'
        # These attributes get computed later, as a result of the OAuth flow ----------
        self.authorization_code = None
        self.access_token = None
        self.username = None

        # Custom configs for each provider ---------------
        if self.ext_type == 'github':
            # github calls the client_key the "client_secret"
            self.client_id = self.custom_idp_config_dict.get('github').get(
                'client_id')
            self.client_key = self.custom_idp_config_dict.get('github').get(
                'client_secret')
            # initial redirect URL; used to start the oauth flow and log in the user
            self.identity_redirect_url = 'https://github.com/login/oauth/authorize'
            # URL to use to exchange the code for an qccess token
            self.oauth2_token_url = 'https://github.com/login/oauth/access_token'
        elif self.ext_type == 'cii':
            # we configure the CII redirect URL directly in the config because there are different CII environments.
            self.identity_redirect_url = self.custom_idp_config_dict.get(
                'cii').get('login_url')
            if not self.identity_redirect_url:
                raise errors.ServiceConfigError(
                    f"Missing required cii config, identity_redirect_url. "
                    f"Config: {self.custom_idp_config_dict}")
            self.jwt_decode_key = self.custom_idp_config_dict.get('cii').get(
                'jwt_decode_key')
            if not self.jwt_decode_key:
                raise errors.ServiceConfigError(
                    f"Missing required cii config, jwt_decode_key. "
                    f"Config: {self.custom_idp_config_dict}")
            self.check_jwt_signature = self.custom_idp_config_dict.get(
                'check_jwt_signature')
            # note that CII does not implement standard OAuth2; they do not require a client id and key and they do not
            # create an authorization code to be exchanged for a token.
            self.client_id = 'not_used'
            self.client_key = 'not_used'
        elif self.ext_type == 'tacc_keycloak':
            # keycloak utilizes a client id and secret like github
            self.client_id = self.custom_idp_config_dict.get(
                'tacc_keycloak').get('client_id')
            self.client_key = self.custom_idp_config_dict.get(
                'tacc_keycloak').get('client_secret')
            # initial redirect URL; used to start the oauth flow and log in the user
            self.identity_redirect_url = 'https://identity.tacc.cloud/auth/realms/tapis/protocol/openid-connect/auth'
            # URL to use to exchange the code for an qccess token
            self.oauth2_token_url = 'https://identity.tacc.cloud/auth/realms/tapis/protocol/openid-connect/token'

        # NOTE: each provider type must implement this check
        # elif self.ext_type == 'google'
        #     ...
        else:
            logger.error(
                f"ERROR! OAuth2ProviderExtension constructor not implemented for OAuth2 provider "
                f"extension {self.ext_type}.")
            raise errors.ServiceConfigError(
                f"Error processing callback URL: extension type {self.ext_type} not "
                f"supported.")
Exemplo n.º 21
0
 def get(self, email):
     logger.debug(f"top of GET /owners/{email}")
     owner = TenantOwner.query.filter_by(email=email).first()
     if not owner:
         raise errors.ResourceError(msg=f'No owner object found with email {email}.')
     return utils.ok(result=owner.serialize, msg='Owner object retrieved successfully.')
Exemplo n.º 22
0
 def get(self, ldap_id):
     logger.debug(f"top of GET /ldaps/{ldap_id}")
     ldap = LDAPConnection.query.filter_by(ldap_id=ldap_id).first()
     if not ldap:
         raise errors.ResourceError(msg=f'No LDAP object found with id {ldap_id}.')
     return utils.ok(result=ldap.serialize, msg='LDAP object retrieved successfully.')