def encode(self, private_key: str, validity_in_seconds: int, kid: str, pass_phrase: str = None) -> str: """ Generate Fabric Token by adding additional claims and signing with Fabric Cert :param private_key Private Key to sign the fabric_cm token :param validity_in_seconds Validity of the Token in seconds :param kid Public Key Id :param pass_phrase Pass Phrase for Private Key :return JWT String containing encoded Fabric Token """ if self.encoded: return self.token self._add_fabric_claims() code, token_or_exception = JWTManager.encode_and_sign_with_private_key(validity=validity_in_seconds, claims=self.claims, private_key_file_name=private_key, pass_phrase=pass_phrase, kid=kid, algorithm='RS256') if code != ValidateCode.VALID: LOG.error(f"Failed to encode the Fabric Token: {token_or_exception}") raise token_or_exception self.token = token_or_exception self.encoded = True return self.token
def version_get(): # noqa: E501 """version Version # noqa: E501 :rtype: Version """ received_counter.labels(HTTP_METHOD_GET, VERSION_URL).inc() try: version = '1.0.0' tag = '1.0.0' url = "https://api.github.com/repos/fabric-testbesd/CredentialManager/git/refs/tags/{}".format( tag) response = Version() response.version = version response.gitsha1 = 'Not Available' result = requests.get(url) if result.status_code == 200 and result.json() is not None: object_json = result.json().get("object", None) if object_json is not None: sha = object_json.get("sha", None) if sha is not None: response.gitsha1 = sha success_counter.labels(HTTP_METHOD_GET, VERSION_URL).inc() except Exception as ex: LOG.exception(ex) failure_counter.labels(HTTP_METHOD_GET, VERSION_URL).inc() return cors_response(status=INTERNAL_SERVER_ERROR, xerror=str(ex), body=str(ex)) return response
def tokens_refresh_post(body, project_name=None, scope=None): # noqa: E501 """Refresh FABRIC OAuth tokens for an user Request to refresh OAuth tokens for an user # noqa: E501 :param body: :type body: dict | bytes :param project_name: Project Name :type project_name: str :param scope: Scope for which token is requested :type scope: str :rtype: Success """ received_counter.labels(HTTP_METHOD_POST, TOKENS_REFRESH_URL).inc() if connexion.request.is_json: body = Request.from_dict(connexion.request.get_json()) # noqa: E501 try: ci_logon_id_token, refresh_token, cookie = authorize(connexion.request) credmgr = OAuthCredmgr() response = Success.from_dict( credmgr.refresh_token(refresh_token=body.refresh_token, project=project_name, scope=scope, cookie=cookie)) success_counter.labels(HTTP_METHOD_POST, TOKENS_REFRESH_URL).inc() return response except Exception as ex: LOG.exception(ex) failure_counter.labels(HTTP_METHOD_POST, TOKENS_REFRESH_URL).inc() msg = str(ex).replace("\n", "") return cors_response(status=INTERNAL_SERVER_ERROR, xerror=msg, body=msg)
def tokens_create_post(project_name=None, scope=None): # noqa: E501 """Generate Fabric OAuth tokens for an user Request to generate Fabric OAuth tokens for an user # noqa: E501 :param project_name: Project Name :type project_name: str :param scope: Scope for which token is requested :type scope: str :rtype: Success """ received_counter.labels(HTTP_METHOD_POST, TOKENS_CREATE_URL).inc() try: ci_logon_id_token, refresh_token, cookie = authorize(connexion.request) if ci_logon_id_token is None: return AUTHORIZATION_ERR, 401 credmgr = OAuthCredmgr() result = credmgr.create_token(ci_logon_id_token=ci_logon_id_token, refresh_token=refresh_token, project=project_name, scope=scope, cookie=cookie) response = Success.from_dict(result) LOG.debug(result) success_counter.labels(HTTP_METHOD_POST, TOKENS_CREATE_URL).inc() return response except Exception as ex: LOG.exception(ex) failure_counter.labels(HTTP_METHOD_POST, TOKENS_CREATE_URL).inc() return cors_response(status=INTERNAL_SERVER_ERROR, xerror=str(ex), body=str(ex))
def tokens_revoke_post(body): # noqa: E501 """Revoke a refresh token for an user Request to revoke a refresh token for an user # noqa: E501 :param body: :type body: dict | bytes :rtype: Success """ received_counter.labels(HTTP_METHOD_POST, TOKENS_REVOKE_URL).inc() if connexion.request.is_json: body = Request.from_dict(connexion.request.get_json()) # noqa: E501 try: credmgr = OAuthCredmgr() credmgr.revoke_token(refresh_token=body.refresh_token) success_counter.labels(HTTP_METHOD_POST, TOKENS_REVOKE_URL).inc() except Exception as ex: LOG.exception(ex) failure_counter.labels(HTTP_METHOD_POST, TOKENS_REVOKE_URL).inc() msg = str(ex).replace("\n", "") return cors_response(status=INTERNAL_SERVER_ERROR, xerror=msg, body=msg) return {}
def certs_get(): # noqa: E501 """Return Public Keys to verify signature of the tokens Json Web Keys # noqa: E501 :rtype: List[Jwk] """ received_counter.labels(HTTP_METHOD_GET, CERTS_URL).inc() try: response = Jwks.from_dict(fabric_jwks) LOG.debug(response) success_counter.labels(HTTP_METHOD_GET, CERTS_URL).inc() return response except Exception as ex: LOG.exception(ex) failure_counter.labels(HTTP_METHOD_GET, CERTS_URL).inc() return str(ex), 500
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 __init__(self, id_token, idp_claims: dict, project="all", scope="all", cookie: str = None): """ Constructor :param id_token: CI Logon Identity Token :param idp_claims: CI Logon Identity Claims :param project: Project for which token is requested :param scope: Scope for which token is requested :param cookie: Vouch Proxy Cookie :raises Exception in case of error """ if id_token is None or project is None or scope is None: raise TokenError("Missing required parameters id_token or project or scope") LOG.debug("id_token %s", id_token) self.id_token = id_token self.claims = idp_claims self.project = project self.scope = scope self.cookie = cookie self.encoded = False self.token = None self.unset = True
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 _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 _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_active_projects_and_roles_from_ldap(self, eppn: str, email: str): """ Return active projects for a user identified by eppn or email @params eppn: eppn @params email: user email @return list of active projects for user """ if eppn: ldap_search_filter = '(eduPersonPrincipalName=' + eppn + ')' else: ldap_search_filter = '(mail=' + email + ')' LOG.debug("ldap_host:%s", self.ldap_host) LOG.debug("ldap_user:%s", self.ldap_user) LOG.debug("ldap_password:%s", self.ldap_password) LOG.debug("ldap_search_base:%s", self.ldap_search_base) LOG.debug("ldap_search_filter:%s", ldap_search_filter) try: self.lock.acquire() conn = Connection(self.server, self.ldap_user, self.ldap_password, auto_bind=True) profile_found = conn.search(self.ldap_search_base, ldap_search_filter, attributes=[ 'isMemberOf', ]) if profile_found: attributes = conn.entries[0]['isMemberOf'] attributes = [attr for attr in attributes if 'active' in attr] else: attributes = None conn.unbind() finally: self.lock.release() LOG.debug(attributes) projects = None roles = None if attributes is not None: projects = {} roles = [] for a in attributes: m = re.match('CO:COU:(.+?):members:active', a) if m: found = m.group(1) if found not in self.project_ignore_list: if found in self.roles_list or "-po" in found or "-pm" in found: roles.append(found) else: projects[found] = [] LOG.debug("Projects: %s, Roles: %s", projects, roles) return roles, projects
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") if code != ValidateCode.VALID: LOG.error("Failed to encode JWK") raise jwk_public_key_rsa