def verify_resource_policy(policy: str): try: resource = json.loads(policy)['Statement'][0]['Resource'] if isinstance(resource, str): resource = [resource] iam.simulate_custom_policy( PolicyInputList=[ json.dumps({ "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Action": ["fake:Fake"], "Resource": "arn:hca:fus:*:*:resource/fake/1234", }] }), ], ActionNames=["fake:action"], ResourceArns=resource, ResourcePolicy=policy, CallerArn='arn:aws:iam::634134578715:user/anyone') except iam.exceptions.InvalidInputException: raise FusilladeHTTPException(title="Bad Request", detail="Invalid resource policy format.")
def update_policy(self, policy_name: str, policy: dict, policy_type: str): self.check_actions(policy) params = [ UpdateObjectParams( 'POLICY', 'policy_document', ValueTypes.BinaryValue, self.cd.format_policy(policy), UpdateActions.CREATE_OR_UPDATE, ) ] try: verify_policy(policy, policy_type) self.cd.update_object_attribute( self.get_policy_reference(policy_name), params, self.cd.node_schema) except cd_client.exceptions.LimitExceededException as ex: raise FusilladeHTTPException(ex) except cd_client.exceptions.ResourceNotFoundException: raise FusilladeNotFoundException( f"{self.get_policy_path(policy_name)} does not exist.") else: logger.info( dict(message="Policy updated", object=dict(type=self.object_type, path_name=self._path_name), policy=dict(link_name=policy_name, policy_type=policy_type)))
def _modify_groups(cloud_node, request): action = request.args['action'] resp = { 'groups': request.json['groups'], 'action': action, f'{cloud_node.object_type}_id': cloud_node.name } try: Group.exists(request.json['groups']) if action == 'add': cloud_node.add_groups(request.json['groups']) elif action == 'remove': cloud_node.remove_groups(request.json['groups']) except cd_client.exceptions.BatchWriteException as ex: resp['msg'] = ex.response['Error']['Message'] code = 304 except FusilladeLimitException as ex: raise FusilladeHTTPException(detail=ex.reason, status=requests.codes.conflict, title="Conflict") else: resp[ 'msg'] = f"{cloud_node.object_type}'s groups successfully modified." code = 200 return resp, code
def get_policy(self, policy_type: str = 'IAMPolicy'): """ Policy statements follow AWS IAM Policy Grammer. See for grammar details https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_grammar.html """ if policy_type in self.allowed_policy_types: # check if this policy type is allowed if not self.attached_policies.get(policy_type): # check if we already have the policy policy_ref = get_obj_type_path('policy') + self.get_policy_name(policy_type) try: resp = self.cd.get_object_attributes( policy_ref, 'POLICY', ['policy_document', 'policy_type'], self.cd.node_schema ) attrs = dict([(attr['Key']['Name'], attr['Value'].popitem()[1]) for attr in resp['Attributes']]) if attrs['policy_type'] != policy_type: logger.warning({'message': "Retrieved policy_type does not match requested policy_type.", 'expected': policy_type, 'received': attrs['policy_type'] }) self.attached_policies[policy_type] = json.loads(attrs['policy_document'].decode("utf-8")) except cd_client.exceptions.ResourceNotFoundException: pass return self.attached_policies.get(policy_type, {}) else: FusilladeHTTPException( title='Bad Request', detail=f"{self.object_type} cannot have policy type {policy_type}." f" Allowed types are: {self.allowed_policy_types}")
def _set_policy(self, statement: Dict[str, Any], policy_type: str = 'IAMPolicy'): verify_policy(statement, policy_type) params = [ UpdateObjectParams('POLICY', 'policy_document', ValueTypes.BinaryValue, self.cd.format_policy(statement), UpdateActions.CREATE_OR_UPDATE, ) ] try: try: self.cd.update_object_attribute(get_obj_type_path('policy') + self.get_policy_name(policy_type), params, self.cd.node_schema) except cd_client.exceptions.ResourceNotFoundException: self.create_policy(statement, policy_type, type=self.object_type, name=self.name) except cd_client.exceptions.LimitExceededException as ex: raise FusilladeHTTPException(ex) else: logger.info(dict(message="Policy updated", object=dict( type=self.object_type, path_name=self._path_name ), policy=dict( link_name=self.get_policy_name(policy_type), policy_type=policy_type) )) self.attached_policies[policy_type] = None
def get_public_key(issuer: str, kid: str) -> bytearray: """ Fetches the public keys from an OIDC Identity provider to verify the JWT. If the key is not found in the public key cache, the cache is cleared and a retry is performed. :param issuer: the openid provider's domain. :param kid: the key identifier for verifying the JWT :return: A Public Key """ public_keys = get_public_keys(issuer) try: return public_keys[kid] except KeyError: logger.error({ "message": "Failed to fetched public key from openid provider.", "public_keys": public_keys, "issuer": issuer, "kid": kid }) logger.debug({"message": "Clearing public key cache."}) get_public_keys.cache_clear() public_keys = get_public_keys(issuer) try: return public_keys[kid] except KeyError: raise FusilladeHTTPException( 401, 'Unauthorized', f"Unable to verify JWT. KID:{kid} does not exists for issuer:{issuer}." )
def get_public_keys(issuer: str) -> typing.Dict[str, bytearray]: """ Fetches the public keys from an OIDC Identity provider to verify the JWT and caching for later use. :param issuer: the openid provider's domain. :param kid: the key identifier for verifying the JWT :return: A Public Keys """ resp = session.get(get_jwks_uri(issuer)) try: resp.raise_for_status() except requests.exceptions.HTTPError: logger.error({ "message": f"Get {get_jwks_uri(issuer)} Failed", "text": resp.text, "status_code": resp.status_code, }) raise FusilladeHTTPException( 503, 'Service Unavailable', "Failed to fetched public key from openid provider.") else: logger.info({ "message": f"Get {get_jwks_uri(issuer)} Succeeded", "response": resp.json(), "status_code": resp.status_code }) return { key["kid"]: rsa.RSAPublicNumbers( e=int.from_bytes(base64.urlsafe_b64decode(key["e"] + "==="), byteorder="big"), n=int.from_bytes( base64.urlsafe_b64decode(key["n"] + "==="), byteorder="big")).public_key(backend=default_backend()) for key in resp.json()["keys"] }
def create(cls, name: str, statement: Optional[Dict[str, Any]] = None, creator=None, **kwargs) -> Type['CloudNode']: ops = [] new_node = cls(name) _creator = creator if creator else "fusillade" ops.append(new_node.cd.batch_create_object( get_obj_type_path(cls.object_type), new_node.hash_name(name), new_node._facet, new_node.cd.get_object_attribute_list(facet=new_node._facet, name=name, created_by=_creator, **kwargs) )) if creator: ops.append(User(name=creator).batch_add_ownership(new_node)) if not statement and not getattr(cls, '_default_policy_path', None): pass else: if not statement: statement = get_json_file(cls._default_policy_path) ops.extend(new_node.create_policy(statement, run=False, type=new_node.object_type, name=new_node.name)) try: new_node.cd.batch_write(ops) except cd_client.exceptions.BatchWriteException as ex: if 'LinkNameAlreadyInUseException' in ex.response['Error']['Message']: raise FusilladeHTTPException( status=409, title="Conflict", detail=f"The {cls.object_type} named {name} already exists. " f"{cls.object_type} was not modified.") else: raise FusilladeHTTPException(ex) else: new_node.cd.get_object_information(new_node.object_ref, ConsistencyLevel=ConsistencyLevel.SERIALIZABLE.name) logger.info(dict(message=f"{new_node.object_type} created by {_creator}", object=dict(type=new_node.object_type, path_name=new_node._path_name))) logger.info(dict(message="Policy updated", object=dict( type=new_node.object_type, path_name=new_node._path_name ), policy=dict( link_name=new_node.get_policy_name('IAMPolicy'), policy_type='IAMPolicy') )) return new_node
def verify_iam_policy(policy: str): try: iam.simulate_custom_policy( PolicyInputList=[policy], ActionNames=["fake:action"], ResourceArns=["arn:aws:iam::123456789012:user/Bob"]) except iam.exceptions.InvalidInputException: raise FusilladeHTTPException(title="Bad Request", detail="Invalid iam policy format.")
def verify_jwt(token: str) -> typing.Optional[typing.Mapping]: """ Verify the JWT from the request. This is function is referenced in fusillade-api.yml securitySchemes.BearerAuth.x-bearerInfoFunc. It's used by connexion to authorize api endpoints that use BearAuth securitySchema. :param token: the Authorization header in the request. :return: Decoded and verified token. """ try: unverified_token = jwt.decode(token, verify=False) token_header = jwt.get_unverified_header(token) except jwt.DecodeError: logger.debug({"msg": "Failed to decode token."}, exc_info=True) raise FusilladeHTTPException(401, 'Unauthorized', 'Failed to decode token.') issuer = unverified_token['iss'] public_key = get_public_key(issuer, token_header["kid"]) try: verified_tok = jwt.decode( token, key=public_key, issuer=issuer, audience=Config.get_audience(), algorithms=allowed_algorithms, ) logger.debug({"message": "Token Validated"}) except jwt.PyJWTError as ex: # type: ignore logger.debug({"message": "Failed to validate token."}, exc_info=True) raise FusilladeHTTPException(401, 'Unauthorized', 'Authorization token is invalid') from ex tokeninfo_endpoint = [ i for i in verified_tok['aud'] if i.endswith('userinfo') or i.endswith('tokeninfo') ] if tokeninfo_endpoint: # Use the OIDC tokeninfo endpoint to get info about the user. return get_tokeninfo(tokeninfo_endpoint[0], token) else: # If No OIDC tokeninfo endpoint is present then this is a google service account and there is no info to # retrieve return verified_tok
def create(cls, resource_type: str, name: str, owner: str = None, **kwargs) -> 'ResourceId': ops = [] new_node = cls(resource_type, name=name) _owner = owner if owner else "fusillade" ops.append( new_node.cd.batch_create_object( f'{new_node.resource_type.object_ref}/id', new_node.name, new_node._facet, new_node.cd.get_object_attribute_list(facet=new_node._facet, name=name, created_by=_owner, **kwargs))) if owner: ops.append(User(name=owner).batch_add_ownership(new_node)) try: new_node.cd.batch_write(ops) except cd_client.exceptions.BatchWriteException as ex: if 'LinkNameAlreadyInUseException' in ex.response['Error'][ 'Message']: raise FusilladeHTTPException( status=409, title="Conflict", detail=f"The {cls.object_type} named {name} already exists. " f"{cls.object_type} was not modified.") else: raise FusilladeHTTPException(ex) else: new_node.cd.get_object_information( new_node.object_ref, ConsistencyLevel=ConsistencyLevel.SERIALIZABLE.name) logger.info( dict(message=f"{new_node.object_type} created", creator=_owner, object=dict(type=new_node.object_type, path_name=new_node._path_name))) return new_node
def serve_openid_config(): """ Part of OIDC """ auth_host = request.headers['host'] if auth_host != os.environ["API_DOMAIN_NAME"]: raise FusilladeHTTPException( status=400, title="Bad Request", detail= f"host: {auth_host}, is not supported. host must be {os.environ['API_DOMAIN_NAME']}." ) openid_config = get_openid_config(Config.get_openid_provider()).copy() openid_config.update(**proxied_endpoints) return ConnexionResponse(body=openid_config, status_code=requests.codes.ok)
def create(cls, name: str, actions: List[str], owner_policy: Dict[str, Any] = None, creator: str = None, **kwargs) -> 'ResourceType': """ Create a new resource type in cloud directory. :param name: The name of the new resource type. :param owner_policy: an IAM policy that determines what a resource owner can do to a resource. :param creator: The user who initiated the creation of the resource type. :param actions: The actions that can be performed on this resource type :param kwargs: additional attributes describing the resource type. :return: """ ops = [] new_node = cls(name) _creator = creator if creator else "fusillade" # Create the node /resource/{resource_type} ops.append( new_node.cd.batch_create_object( get_obj_type_path(cls.object_type), name, new_node._facet, new_node.cd.get_object_attribute_list( facet=new_node._facet, name=name, created_by=_creator, actions=' '.join(actions), # TODO actions should also have descriptions **kwargs), batch_reference='type_node')) # Create the node /resource/{resource_type}/id ops.append( new_node.cd.batch_create_object( '#type_node', 'id', 'NodeFacet', new_node.cd.get_object_attribute_list(facet='NodeFacet', name='id', created_by=_creator))) # Create the node /resource/{resource_type}/policy ops.append( new_node.cd.batch_create_object( '#type_node', 'policy', 'NodeFacet', new_node.cd.get_object_attribute_list(facet='NodeFacet', name='policy', created_by=_creator), batch_reference='policy_node')) # link owner policy to resource type if not owner_policy and not getattr(cls, '_default_policy_path', None): raise FusilladeHTTPException('Must provide owner policy.') else: if owner_policy: pass elif getattr(cls, '_default_policy_path'): with open(cls._default_policy_path, 'r') as fp: owner_policy = json.load(fp) ops.extend( new_node.create_policy('Owner', owner_policy, parent_path='#policy_node', run=False)) # Execute batch request try: new_node.cd.batch_write(ops) except cd_client.exceptions.BatchWriteException as ex: if 'LinkNameAlreadyInUseException' in ex.response['Error'][ 'Message']: raise FusilladeHTTPException( status=409, title="Conflict", detail=f"The {cls.object_type} named {name} already exists. " f"{cls.object_type} was not modified.") else: raise FusilladeHTTPException(ex) else: new_node.cd.get_object_information( new_node.object_ref, ConsistencyLevel=ConsistencyLevel.SERIALIZABLE.name) logger.info( dict(message=f"{new_node.object_type} created by {_creator}", object=dict(type=new_node.object_type, path_name=new_node._path_name))) logger.info( dict(message="Policy updated", object=dict(type=new_node.object_type, path_name=new_node._path_name), policy=dict(link_name='Owner', policy_type='IAMPolicy'))) return new_node