def revoke_token(self, refresh_token: str): """ Revoke a refresh token @returns dictionary containing status of the operation @raises Exception in case of error """ if OAuth2Session is None or refresh_token is None: raise ImportError( "No module named OAuth2Session or revoke_token not provided") provider = CONFIG_OBJ.get_oauth_provider() providers = CONFIG_OBJ.get_providers() auth = providers[provider][self.CLIENT_ID] + ":" + providers[provider][ self.CLIENT_SECRET] encoded_auth = base64.b64encode(bytes(auth, self.UTF_8)) headers = { 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': 'Basic ' + str(encoded_auth, self.UTF_8) } data = f"token={refresh_token}&token_type_hint=refresh_token" response = requests.post(providers[provider][self.REVOKE_URI], headers=headers, data=data) self.log.debug("Response Status=%d", response.status_code) self.log.debug("Response Reason=%s", response.reason) self.log.debug("Response content=%s", response.content) self.log.debug(str(response.content, self.UTF_8)) if response.status_code != 200: raise OAuthCredMgrError("Refresh token could not be revoked!")
def _add_fabric_claims(self): """ Set the claims for the Token by adding membership, project and scope """ sub = self.claims.get("sub") url = CONFIG_OBJ.get_pr_url() roles = None projects = None if CONFIG_OBJ.is_project_registry_enabled(): project_registry = ProjectRegistry(api_server=url, cookie=self._get_vouch_cookie(), cookie_name=CONFIG_OBJ.get_vouch_cookie_name(), cookie_domain=CONFIG_OBJ.get_vouch_cookie_domain_name()) roles, projects = project_registry.get_projects_and_roles(sub) else: email = self.claims.get("email") roles, projects = CmLdapMgrSingleton.get().get_active_projects_and_roles_from_ldap(eppn=None, email=email) LOG.debug("Projects: %s, Roles: %s", projects, roles) projects_to_be_removed = [] for project in projects.keys(): LOG.debug("Processing %s", project) if self.project != "all" and self.project not in project: projects_to_be_removed.append(project) for x in projects_to_be_removed: projects.pop(x) if len(projects) < 1: raise TokenError("User is not a member of any of the project") self.claims['projects'] = projects self.claims["roles"] = roles self.claims["scope"] = self.scope LOG.debug("Claims %s", self.claims) self.unset = False
def authorize(request): ci_logon_id_token = request.headers.get(VOUCH_ID_TOKEN, None) refresh_token = request.headers.get(VOUCH_REFRESH_TOKEN, None) cookie_name = CONFIG_OBJ.get_vouch_cookie_name() cookie = request.cookies.get(cookie_name) return ci_logon_id_token, refresh_token, cookie
def get_projects_and_roles(self, sub: str): """ Determine Role from Project Registry :param sub: OIDC claim sub :param returns the roles and project with tags :returns a tuple containing user specific roles and project tags """ if self.api_server is None or self.cookie is None: raise ProjectRegistryError(f"Project Registry URL: {self.api_server} or " "Cookie: {self.cookie} not available") # Create Session s = requests.Session() # Set the Cookie cookie_obj = requests.cookies.create_cookie( name=self.cookie_name, value=self.cookie ) s.cookies.set_cookie(cookie_obj) LOG.debug(f"Using vouch cookie: {s.cookies}") # Set the headers headers = { 'Accept': 'application/json', 'Content-Type': "application/json" } s.headers.update(headers) # Get User by OIDC SUB Claim url = self.api_server + "/people/oidc_claim_sub?oidc_claim_sub={}".format(sub) ssl_verify = CONFIG_OBJ.is_pr_ssl_verify() response = s.get(url, verify=ssl_verify) if response.status_code != 200: raise ProjectRegistryError(f"Project Registry error occurred " f"status_code: {response.status_code} message: {response.content}") LOG.debug(f"Response : {response.json()}") roles = response.json().get('roles', None) projects = response.json().get('projects', None) # Get Per Project Tags project_tags = {} for p in projects: project_name = p.get('name', None) project_uuid = p.get('uuid', None) LOG.debug(f"Getting tags for Project: {project_name}") url = self.api_server + "/projects/{}".format(project_uuid) response = s.get(url, verify=ssl_verify) if response.status_code != 200: raise ProjectRegistryError(f"Project Registry error occurred " f"status_code: {response.status_code} message: {response.content}") project_tags[project_name] = response.json().get('tags', None) return roles, project_tags
def _generate_fabric_token(self, ci_logon_id_token: str, project: str, scope: str, cookie: str = None): self.log.debug("CILogon Token: %s", ci_logon_id_token) # validate the token if jwt_validator is not None: LOG.info("Validating CI Logon token") code, claims_or_exception = jwt_validator.validate_jwt( token=ci_logon_id_token) if code is not ValidateCode.VALID: LOG.error( f"Unable to validate provided token: {code}/{claims_or_exception}" ) raise claims_or_exception fabric_token_encoder = FabricTokenEncoder( id_token=ci_logon_id_token, idp_claims=claims_or_exception, project=project, scope=scope, cookie=cookie) validity = CONFIG_OBJ.get_token_life_time() private_key = CONFIG_OBJ.get_jwt_private_key() pass_phrase = CONFIG_OBJ.get_jwt_private_key_pass_phrase() kid = CONFIG_OBJ.get_jwt_public_key_kid() return fabric_token_encoder.encode(private_key=private_key, validity_in_seconds=validity, kid=kid, pass_phrase=pass_phrase) else: LOG.warning( "JWT Token validator not initialized, skipping validation") return None
def _get_vouch_cookie(self) -> str: vouch_cookie_enabled = CONFIG_OBJ.is_vouch_cookie_enabled() if not vouch_cookie_enabled or self.cookie is not None: return self.cookie vouch_secret = CONFIG_OBJ.get_vouch_secret() vouch_compression = CONFIG_OBJ.is_vouch_cookie_compressed() vouch_claims = CONFIG_OBJ.get_vouch_custom_claims() vouch_cookie_lifetime = CONFIG_OBJ.get_vouch_cookie_lifetime() vouch_helper = VouchEncoder(secret=vouch_secret, compression=vouch_compression) custom_claims = [] for c in vouch_claims: c_type = c.strip().upper() if c_type == CustomClaimsType.OPENID.name: custom_claims.append(CustomClaimsType.OPENID) if c_type == CustomClaimsType.EMAIL.name: custom_claims.append(CustomClaimsType.EMAIL) if c_type == CustomClaimsType.PROFILE.name: custom_claims.append(CustomClaimsType.PROFILE) if c_type == CustomClaimsType.CILOGON_USER_INFO.name: custom_claims.append(CustomClaimsType.CILOGON_USER_INFO) p_tokens = PTokens(id_token=self.id_token, idp_claims=self.claims) code, cookie_or_exception = vouch_helper.encode(custom_claims_type=custom_claims, p_tokens=p_tokens, validity_in_seconds=vouch_cookie_lifetime) if code != ValidateCode.VALID: LOG.error(f"Failed to encode the Vouch Cookie: {cookie_or_exception}") raise cookie_or_exception return cookie_or_exception
def main(): """ Main Entry Point """ log = LOG try: app = connexion.App(__name__, specification_dir='swagger/') app.app.json_encoder = encoder.JSONEncoder app.add_api('swagger.yaml', arguments={'title': 'Fabric Credential Manager API'}, pythonic_params=True) port = CONFIG_OBJ.get_rest_port() # prometheus server prometheus_port = CONFIG_OBJ.get_prometheus_port() prometheus_client.start_http_server(prometheus_port) # Start up the server to expose the metrics. waitress.serve(app, port=port) except Exception as ex: log.error("Exception occurred while starting Flask app") log.error(ex) raise ex
def __init__(self): self.lock = threading.Lock() self.ldap_host = CONFIG_OBJ.get_ldap_host() self.ldap_user = CONFIG_OBJ.get_ldap_user() self.ldap_password = CONFIG_OBJ.get_ldap_pwd() self.ldap_search_base = CONFIG_OBJ.get_ldap_search_base() self.project_ignore_list = CONFIG_OBJ.get_project_ignore_list() self.roles_list = CONFIG_OBJ.get_roles() self.server = Server(host=self.ldap_host, use_ssl=True, get_info=ALL)
def get_logger(): """ Detects the path and level for the log file from the credmgr CONFIG_OBJ and sets up a logger. Instead of detecting the path and/or level from the credmgr CONFIG_OBJ, a custom path and/or level for the log file can be passed as optional arguments. :param log_path: Path to custom log file :param log_level: Custom log level :return: logging.Logger object """ # Get the log path log_path = CONFIG_OBJ.get_logger_dir() + '/' + CONFIG_OBJ.get_logger_file() if log_path is None: raise RuntimeError( 'The log file path must be specified in CONFIG_OBJ or passed as an argument' ) # Get the log level log_level = CONFIG_OBJ.get_logger_level() if log_level is None: log_level = logging.INFO logger = CONFIG_OBJ.get_logger_name() # Set up the root logger log = logging.getLogger(logger) log.setLevel(log_level) log_format = '%(asctime)s - %(name)s - {%(filename)s:%(lineno)d} - %(levelname)s - %(message)s' os.makedirs(os.path.dirname(log_path), exist_ok=True) file_handler = RotatingFileHandler( log_path, backupCount=CONFIG_OBJ.get_logger_retain(), maxBytes=CONFIG_OBJ.get_logger_size()) logging.basicConfig(handlers=[file_handler], format=log_format) return log, file_handler
from fss_utils.jwt_validate import JWTValidator import prometheus_client from fabric_cm.credmgr.config import CONFIG_OBJ from fabric_cm.credmgr.logging import LOG received_counter = prometheus_client.Counter('Requests_Received', 'HTTP Requests', ['method', 'endpoint']) success_counter = prometheus_client.Counter('Requests_Success', 'HTTP Success', ['method', 'endpoint']) failure_counter = prometheus_client.Counter('Requests_Failed', 'HTTP Failures', ['method', 'endpoint']) # initialize CI Logon Token Validation CILOGON_CERTS = CONFIG_OBJ.get_oauth_jwks_url() CILOGON_KEY_REFRESH = CONFIG_OBJ.get_oauth_key_refresh() LOG.info(f'Initializing JWT Validator to use {CILOGON_CERTS} endpoint, ' f'refreshing keys every {CILOGON_KEY_REFRESH} HH:MM:SS') jwt_validator = JWTValidator(url=CILOGON_CERTS, refresh_period=timedelta( hours=CILOGON_KEY_REFRESH.hour, minutes=CILOGON_KEY_REFRESH.minute, seconds=CILOGON_KEY_REFRESH.second), audience=CONFIG_OBJ.get_oauth_client_id()) kid = CONFIG_OBJ.get_jwt_public_key_kid() public_key = CONFIG_OBJ.get_jwt_public_key() code, jwk_public_key_rsa = JWTManager.encode_jwk(key_file_name=public_key, kid=kid, alg="RS256")
def validate_scope(scope: str): allowed_scopes = CONFIG_OBJ.get_allowed_scopes() if scope not in allowed_scopes: raise OAuthCredMgrError( f"Scope {scope} is not allowed! Allowed scope values: {allowed_scopes}" )
def refresh_token(self, refresh_token: str, project: str, scope: str, cookie: str = None) -> dict: """ Refreshes a token from CILogon and generates Fabric token using project and scope saved in Database @param project: Project for which token is requested, by default it is set to 'all' @param scope: Scope of the requested token, by default it is set to 'all' @param refresh_token: Refresh Token @param cookie: Vouch Proxy Cookie @returns dict containing id_token and refresh_token @raises Exception in case of error """ self.validate_scope(scope=scope) if OAuth2Session is None or refresh_token is None: raise ImportError( "No module named OAuth2Session or refresh_token not provided") provider = CONFIG_OBJ.get_oauth_provider() providers = CONFIG_OBJ.get_providers() refresh_token_dict = {self.REFRESH_TOKEN: refresh_token} self.log.debug(f"Incoming refresh_token: {refresh_token}") # refresh the token (provides both new refresh and access tokens) oauth_client = OAuth2Session(providers[provider][self.CLIENT_ID], token=refresh_token_dict) new_token = oauth_client.refresh_token( providers[provider][self.TOKEN_URI], client_id=providers[provider][self.CLIENT_ID], client_secret=providers[provider][self.CLIENT_SECRET]) new_refresh_token = None try: new_refresh_token = new_token.pop(self.REFRESH_TOKEN) id_token = new_token.pop(self.ID_TOKEN) except KeyError: self.log.error("No refresh or id token returned") raise OAuthCredMgrError("No refresh or id token returned") self.log.debug(f"new_refresh_token: {new_refresh_token}") try: id_token = self._generate_fabric_token(ci_logon_id_token=id_token, project=project, scope=scope, cookie=cookie) result = { self.ID_TOKEN: id_token, self.REFRESH_TOKEN: new_refresh_token } return result except Exception as e: self.log.error( f"Exception error while generating Fabric Token: {e}") self.log.error( f"Failed generating the token but still returning refresh token" ) exception_string = str(e) if exception_string.__contains__( "could not be associated with a pending flow"): exception_string = "Specified refresh token is expired and can not be found in the database." error_string = f"error: {exception_string}, {self.REFRESH_TOKEN}: {new_refresh_token}" raise OAuthCredMgrError(error_string)