From: Tomek Mrugalski Date: Thu, 6 Feb 2014 19:40:02 +0000 (+0100) Subject: [3274] Changes after review: X-Git-Tag: bind10-1.2.0beta1-release~57^2~1^2~4 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=f2c1e775f7b2281655da68327fcae2be3a5fd2ca;p=thirdparty%2Fkea.git [3274] Changes after review: - Copyright years updated - Minor corrections in BINDX Guide - dhcp4.spec relay structure description updated. - new unit-test for DHCPv4 client classification added - new unit-test for DHCPv6 client classification added - fixed Doxygen errors - comments for CfgMgr::getSubnet{4,6} updated --- diff --git a/doc/guide/bind10-guide.xml b/doc/guide/bind10-guide.xml index e63512f6a7..6703f1e5e5 100644 --- a/doc/guide/bind10-guide.xml +++ b/doc/guide/bind10-guide.xml @@ -3525,7 +3525,7 @@ then change those defaults with config set Resolver/forward_addresses[0]/address protocols. BIND 10 offers two server implementations, one for DHCPv4 and one for DHCPv6. The DHCP part of the BIND 10 project is codenamed Kea. The DHCPv4 component is colloquially referred to as Kea4 and its - DHCPv6 is called Kea6. + DHCPv6 counterpart is called Kea6. This chapter covers those parts of BIND 10 that are common to both servers. DHCPv4-specific details are covered in , while those details specific to DHCPv6 are described in @@ -4416,7 +4416,7 @@ Dhcp4/subnet4 [] list (default) It is envisaged that the client classification will be used for changing - behavior of almost any part of the DHCP engine processing, including assigning + behavior of almost any part of the DHCP message processing, including assigning leases from different pools, assigning different option (or different values of the same options) etc. For now, there are only two mechanisms that are taking advantage of client classification: specific processing for cable modems and @@ -4435,7 +4435,7 @@ Dhcp4/subnet4 [] list (default) Kea can be instructed to limit access to given subnets based on class information. This is particularly useful for cases where two types of devices share the same link and are expected to be served from two different subnets. The - primary use case for such a scenario are cable networks. There are two + primary use case for such a scenario is cable networks. There are two classes of devices: cable modem itself, which should be handled a lease from subnet A and all other devices behind modems that should get a lease from subnet B. That segregation is essential to prevent overly curious @@ -4448,7 +4448,7 @@ Dhcp4/subnet4 [] list (default)
Limiting access to IPv4 subnet to certain classes - In certain cases it beneficial to restrict access to certains subnets + In certain cases it beneficial to restrict access to certain subnets only to clients that belong to a given subnet. For details on client classes, see . This is an extension of a previous example from . diff --git a/src/bin/dhcp4/config_parser.cc b/src/bin/dhcp4/config_parser.cc index 7dd19bf23f..5809739b71 100644 --- a/src/bin/dhcp4/config_parser.cc +++ b/src/bin/dhcp4/config_parser.cc @@ -1,4 +1,4 @@ -// Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC") +// Copyright (C) 2012-2014 Internet Systems Consortium, Inc. ("ISC") // // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted, provided that the above diff --git a/src/bin/dhcp4/dhcp4.dox b/src/bin/dhcp4/dhcp4.dox index 5182f73794..22603f31dd 100644 --- a/src/bin/dhcp4/dhcp4.dox +++ b/src/bin/dhcp4/dhcp4.dox @@ -1,4 +1,4 @@ -// Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC") +// Copyright (C) 2012-2014 Internet Systems Consortium, Inc. ("ISC") // // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted, provided that the above diff --git a/src/bin/dhcp4/dhcp4.spec b/src/bin/dhcp4/dhcp4.spec index 0777b8ee1e..0b6716103e 100644 --- a/src/bin/dhcp4/dhcp4.spec +++ b/src/bin/dhcp4/dhcp4.spec @@ -261,14 +261,14 @@ "item_type": "map", "item_optional": false, "item_default": {}, - "item_description" : "Structure holding optional relay information.", + "item_description" : "Structure holding relay information.", "map_item_spec": [ { "item_name": "ip-address", "item_type": "string", "item_optional": false, "item_default": "0.0.0.0", - "item_description" : "IPv4 address of the relay (optional)." + "item_description" : "IPv4 address of the relay (defaults to 0.0.0.0 if not specified)." } ] }, diff --git a/src/bin/dhcp4/tests/dhcp4_srv_unittest.cc b/src/bin/dhcp4/tests/dhcp4_srv_unittest.cc index e930ba0316..310b939f54 100644 --- a/src/bin/dhcp4/tests/dhcp4_srv_unittest.cc +++ b/src/bin/dhcp4/tests/dhcp4_srv_unittest.cc @@ -3201,4 +3201,68 @@ TEST_F(Dhcpv4SrvTest, clientClassification) { EXPECT_FALSE(dis2->inClass("docsis3.0")); } +// Checks if the client-class field is indeed used for subnet selection. +// Note that packet classification is already checked in Dhcpv4SrvTest +// .clientClassification above. +TEST_F(Dhcpv4SrvTest, clientClassify2) { + + NakedDhcpv4Srv srv(0); + + ConstElementPtr status; + + // This test configures 2 subnets. We actually only need the + // first one, but since there's still this ugly hack that picks + // the pool if there is only one, we must use more than one + // subnet. That ugly hack will be removed in #3242, currently + // under review. + + // The second subnet does not play any role here. The client's + // IP address belongs to the first subnet, so only that first + // subnet it being tested. + string config = "{ \"interfaces\": [ \"*\" ]," + "\"rebind-timer\": 2000, " + "\"renew-timer\": 1000, " + "\"subnet4\": [ " + "{ \"pool\": [ \"192.0.2.1 - 192.0.2.100\" ]," + " \"client-class\": \"foo\", " + " \"subnet\": \"192.0.2.0/24\" }, " + "{ \"pool\": [ \"192.0.3.1 - 192.0.3.100\" ]," + " \"client-class\": \"xyzzy\", " + " \"subnet\": \"192.0.3.0/24\" } " + "]," + "\"valid-lifetime\": 4000 }"; + + ElementPtr json = Element::fromJSON(config); + + EXPECT_NO_THROW(status = configureDhcp4Server(srv, json)); + + // check if returned status is OK + ASSERT_TRUE(status); + comment_ = config::parseAnswer(rcode_, status); + ASSERT_EQ(0, rcode_); + + Pkt4Ptr dis = Pkt4Ptr(new Pkt4(DHCPDISCOVER, 1234)); + dis->setRemoteAddr(IOAddress("192.0.2.1")); + dis->setIface("eth0"); + OptionPtr clientid = generateClientId(); + dis->addOption(clientid); + + // This discover does not belong to foo class, so it will not + // be serviced + EXPECT_FALSE(srv.selectSubnet(dis)); + + // Let's add the packet to bar class and try again. + dis->addClass("bar"); + + // Still not supported, because it belongs to wrong class. + EXPECT_FALSE(srv.selectSubnet(dis)); + + // Let's add it to maching class. + dis->addClass("foo"); + + // This time it should work + EXPECT_TRUE(srv.selectSubnet(dis)); +} + + }; // end of anonymous namespace diff --git a/src/bin/dhcp4/tests/dhcp4_test_utils.h b/src/bin/dhcp4/tests/dhcp4_test_utils.h index 8a2211789e..355d8eaee5 100644 --- a/src/bin/dhcp4/tests/dhcp4_test_utils.h +++ b/src/bin/dhcp4/tests/dhcp4_test_utils.h @@ -436,6 +436,7 @@ public: std::list fake_sent_; + /// Let's make those methods public, so they can be accessed easily in tests. using Dhcpv4Srv::adjustIfaceData; using Dhcpv4Srv::appendServerID; using Dhcpv4Srv::processDiscover; @@ -452,6 +453,7 @@ public: using Dhcpv4Srv::unpackOptions; using Dhcpv4Srv::name_change_reqs_; using Dhcpv4Srv::classifyPacket; + using Dhcpv4Srv::selectSubnet; }; }; // end of isc::dhcp::test namespace diff --git a/src/bin/dhcp6/dhcp6.dox b/src/bin/dhcp6/dhcp6.dox index 3eb6994872..81f52c653d 100644 --- a/src/bin/dhcp6/dhcp6.dox +++ b/src/bin/dhcp6/dhcp6.dox @@ -32,7 +32,7 @@ @section dhcpv6Session BIND10 message queue integration - DHCPv4 server component is now integrated with BIND10 message queue. + DHCPv6 server component is now integrated with BIND10 message queue. It follows the same principle as DHCPv4. See \ref dhcpv4Session for details. @@ -218,8 +218,8 @@ Currently there is no class behaviour coded in DHCPv6, hence no v6 equivalent of it will be conducted in an external hooks library. It is possible to define class restrictions in subnet, so a given subnet is only -accessible to clients that belong to a given class. That is implemented as isc::dhcp::Pkt4::classes_ -being passed in isc::dhcp::Dhcpv4Srv::selectSubnet() to isc::dhcp::CfgMgr::getSubnet4(). +accessible to clients that belong to a given class. That is implemented as isc::dhcp::Pkt6::classes_ +being passed in isc::dhcp::Dhcpv6Srv::selectSubnet() to isc::dhcp::CfgMgr::getSubnet6(). Currently this capability is usable, but the number of scenarios it supports is limited. diff --git a/src/bin/dhcp6/dhcp6.spec b/src/bin/dhcp6/dhcp6.spec index 7978de6eb3..540d48afa2 100644 --- a/src/bin/dhcp6/dhcp6.spec +++ b/src/bin/dhcp6/dhcp6.spec @@ -266,14 +266,14 @@ "item_type": "map", "item_optional": false, "item_default": {}, - "item_description" : "Structure holding optional relay information.", + "item_description" : "Structure holding relay information.", "map_item_spec": [ { "item_name": "ip-address", "item_type": "string", "item_optional": false, "item_default": "::", - "item_description" : "IPv6 address of the relay (optional)." + "item_description" : "IPv6 address of the relay (defaults to :: if not specified)." } ] }, diff --git a/src/bin/dhcp6/tests/dhcp6_srv_unittest.cc b/src/bin/dhcp6/tests/dhcp6_srv_unittest.cc index b26a7a301c..6b199055e1 100644 --- a/src/bin/dhcp6/tests/dhcp6_srv_unittest.cc +++ b/src/bin/dhcp6/tests/dhcp6_srv_unittest.cc @@ -1738,6 +1738,72 @@ TEST_F(Dhcpv6SrvTest, clientClassification) { EXPECT_FALSE(sol2->inClass("docsis3.0")); } +// Checks if the client-class field is indeed used for subnet selection. +// Note that packet classification is already checked in Dhcpv6SrvTest +// .clientClassification above. +TEST_F(Dhcpv6SrvTest, clientClassify2) { + + NakedDhcpv6Srv srv(0); + + ConstElementPtr status; + + // This test configures 2 subnets. We actually only need the + // first one, but since there's still this ugly hack that picks + // the pool if there is only one, we must use more than one + // subnet. That ugly hack will be removed in #3242, currently + // under review. + + // The second subnet does not play any role here. The client's + // IP address belongs to the first subnet, so only that first + // subnet it being tested. + string config = "{ \"interfaces\": [ \"*\" ]," + "\"preferred-lifetime\": 3000," + "\"rebind-timer\": 2000, " + "\"renew-timer\": 1000, " + "\"subnet6\": [ " + " { \"pool\": [ \"2001:db8:1::/64\" ]," + " \"subnet\": \"2001:db8:1::/48\", " + " \"client-class\": \"foo\" " + " }, " + " { \"pool\": [ \"2001:db8:2::/64\" ]," + " \"subnet\": \"2001:db8:2::/48\", " + " \"client-class\": \"xyzzy\" " + " } " + "]," + "\"valid-lifetime\": 4000 }"; + + ElementPtr json = Element::fromJSON(config); + + EXPECT_NO_THROW(status = configureDhcp6Server(srv, json)); + + // check if returned status is OK + ASSERT_TRUE(status); + comment_ = config::parseAnswer(rcode_, status); + ASSERT_EQ(0, rcode_); + + Pkt6Ptr sol = Pkt6Ptr(new Pkt6(DHCPV6_SOLICIT, 1234)); + sol->setRemoteAddr(IOAddress("2001:db8:1::3")); + sol->addOption(generateIA(D6O_IA_NA, 234, 1500, 3000)); + OptionPtr clientid = generateClientId(); + sol->addOption(clientid); + + // This discover does not belong to foo class, so it will not + // be serviced + EXPECT_FALSE(srv.selectSubnet(sol)); + + // Let's add the packet to bar class and try again. + sol->addClass("bar"); + + // Still not supported, because it belongs to wrong class. + EXPECT_FALSE(srv.selectSubnet(sol)); + + // Let's add it to maching class. + sol->addClass("foo"); + + // This time it should work + EXPECT_TRUE(srv.selectSubnet(sol)); +} + /// @todo: Add more negative tests for processX(), e.g. extend sanityCheck() test /// to call processX() methods. diff --git a/src/lib/dhcp/classify.h b/src/lib/dhcp/classify.h index 75945c52ab..47db9421a5 100644 --- a/src/lib/dhcp/classify.h +++ b/src/lib/dhcp/classify.h @@ -29,9 +29,10 @@ /// classification is expected to grow significantly. /// /// @todo This file should be moved to dhcpsrv eventually as the classification -/// is server side concept. That is not possible yet, as the Pkt4 and Pkt6 -/// classes have server-side implementation, even though they reside in the -/// dhcp directory. +/// is server side concept. Client has no notion of classifying incoming server +/// messages as it usually talks to only one server. That move is not possible +/// yet, as the Pkt4 and Pkt6 classes have server-side implementation, even +/// though they reside in the dhcp directory. namespace isc { @@ -40,23 +41,25 @@ namespace dhcp { /// Definition of a single class. typedef std::string ClientClass; - /// Container for storing client classes + /// @brief Container for storing client classes /// /// Depending on how you look at it, this is either a little more than just /// a set of strings or a client classifier that performs access control. /// For now, it is a simple access list that may contain zero or more /// class names. It is expected to grow in complexity once support for /// client classes becomes more feature rich. + /// + /// Note: This class is derived from std::set which may not have Doxygen + /// documentation. See http://www.cplusplus.com/reference/set/set/. class ClientClasses : public std::set { public: - /// @brief returns if class X belongs to the defined classes + /// @brief returns if class x belongs to the defined classes /// /// @param x client class to be checked - /// @return true if X belongs to the classes + /// @return true if x belongs to the classes bool contains(const ClientClass& x) const { - const_iterator it = find(x); - return (it != end()); + return (find(x) != end()); } }; diff --git a/src/lib/dhcp/pkt4.cc b/src/lib/dhcp/pkt4.cc index b641a03a40..01c598a544 100644 --- a/src/lib/dhcp/pkt4.cc +++ b/src/lib/dhcp/pkt4.cc @@ -482,12 +482,12 @@ Pkt4::isRelayed() const { "hops != 0)"); } -bool Pkt4::inClass(const std::string& client_class) { +bool Pkt4::inClass(const isc::dhcp::ClientClass& client_class) { return (classes_.find(client_class) != classes_.end()); } void -Pkt4::addClass(const std::string& client_class) { +Pkt4::addClass(const isc::dhcp::ClientClass& client_class) { if (classes_.find(client_class) == classes_.end()) { classes_.insert(client_class); } diff --git a/src/lib/dhcp/pkt4.h b/src/lib/dhcp/pkt4.h index 510ee8e221..d5817b89f2 100644 --- a/src/lib/dhcp/pkt4.h +++ b/src/lib/dhcp/pkt4.h @@ -1,4 +1,4 @@ -// Copyright (C) 2011-2013 Internet Systems Consortium, Inc. ("ISC") +// Copyright (C) 2011-2014 Internet Systems Consortium, Inc. ("ISC") // // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted, provided that the above @@ -556,7 +556,7 @@ public: /// /// @param client_class name of the class /// @return true if belongs - bool inClass(const std::string& client_class); + bool inClass(const isc::dhcp::ClientClass& client_class); /// @brief Adds packet to a specified class /// @@ -573,7 +573,7 @@ public: /// so I decided to stick with addClass(). /// /// @param client_class name of the class to be added - void addClass(const std::string& client_class); + void addClass(const isc::dhcp::ClientClass& client_class); /// @brief Classes this packet belongs to. /// diff --git a/src/lib/dhcp/pkt6.h b/src/lib/dhcp/pkt6.h index 8e5147466f..0e88e5642a 100644 --- a/src/lib/dhcp/pkt6.h +++ b/src/lib/dhcp/pkt6.h @@ -1,4 +1,4 @@ -// Copyright (C) 2011-2013 Internet Systems Consortium, Inc. ("ISC") +// Copyright (C) 2011-2014 Internet Systems Consortium, Inc. ("ISC") // // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted, provided that the above diff --git a/src/lib/dhcpsrv/cfgmgr.cc b/src/lib/dhcpsrv/cfgmgr.cc index 3d2e87b6ce..2494ff946c 100644 --- a/src/lib/dhcpsrv/cfgmgr.cc +++ b/src/lib/dhcpsrv/cfgmgr.cc @@ -1,4 +1,4 @@ -// Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC") +// Copyright (C) 2012-2014 Internet Systems Consortium, Inc. ("ISC") // // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted, provided that the above diff --git a/src/lib/dhcpsrv/cfgmgr.h b/src/lib/dhcpsrv/cfgmgr.h index 1f548dc786..6b65f929ed 100644 --- a/src/lib/dhcpsrv/cfgmgr.h +++ b/src/lib/dhcpsrv/cfgmgr.h @@ -1,4 +1,4 @@ -// Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC") +// Copyright (C) 2012-2014 Internet Systems Consortium, Inc. ("ISC") // // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted, provided that the above @@ -166,7 +166,11 @@ public: /// b) our global address on the interface the message was received on /// (for directly connected clients) /// + /// If there are any classes specified in a subnet, that subnet + /// will be selected only if the client belongs to appropriate class. + /// /// @param hint an address that belongs to a searched subnet + /// @param classes classes the client belongs to /// /// @return a subnet object (or NULL if no suitable match was fount) Subnet6Ptr getSubnet6(const isc::asiolink::IOAddress& hint, @@ -177,7 +181,13 @@ public: /// Finds a matching local subnet, based on interface name. This /// is used for selecting subnets that were explicitly marked by the /// user as reachable over specified network interface. + /// + /// If there are any classes specified in a subnet, that subnet + /// will be selected only if the client belongs to appropriate class. + /// /// @param iface_name interface name + /// @param classes classes the client belongs to + /// /// @return a subnet object (or NULL if no suitable match was fount) Subnet6Ptr getSubnet6(const std::string& iface_name, const isc::dhcp::ClientClasses& classes); @@ -186,7 +196,11 @@ public: /// /// Another possibility to find a subnet is based on interface-id. /// + /// If there are any classes specified in a subnet, that subnet + /// will be selected only if the client belongs to appropriate class. + /// /// @param interface_id content of interface-id option returned by a relay + /// @param classes classes the client belongs to /// /// @return a subnet object Subnet6Ptr getSubnet6(OptionPtr interface_id, @@ -245,7 +259,11 @@ public: /// b) our global address on the interface the message was received on /// (for directly connected clients) /// + /// If there are any classes specified in a subnet, that subnet + /// will be selected only if the client belongs to appropriate class. + /// /// @param hint an address that belongs to a searched subnet + /// @param classes classes the client belongs to /// /// @return a subnet object Subnet4Ptr getSubnet4(const isc::asiolink::IOAddress& hint, @@ -346,7 +364,7 @@ public: /// pointer. void setD2ClientConfig(D2ClientConfigPtr& new_config); - /// @param Convenience method for checking if DHCP-DDNS updates are enabled. + /// @brief Convenience method for checking if DHCP-DDNS updates are enabled. /// /// @return True if the D2 configuration is enabled. bool ddnsEnabled(); diff --git a/src/lib/dhcpsrv/dhcp_parsers.cc b/src/lib/dhcpsrv/dhcp_parsers.cc index 3eefd15152..1cf41dfe2f 100644 --- a/src/lib/dhcpsrv/dhcp_parsers.cc +++ b/src/lib/dhcpsrv/dhcp_parsers.cc @@ -811,7 +811,7 @@ OptionDefListParser::commit() { } } -//****************************** PoolParser ******************************** +//****************************** RelayInfoParser ******************************** RelayInfoParser::RelayInfoParser(const std::string&, const isc::dhcp::Subnet::RelayInfoPtr& relay_info, const asiolink::IOAddress& default_addr) diff --git a/src/lib/dhcpsrv/dhcp_parsers.h b/src/lib/dhcpsrv/dhcp_parsers.h index 32abc9afc3..0e4f049400 100644 --- a/src/lib/dhcpsrv/dhcp_parsers.h +++ b/src/lib/dhcpsrv/dhcp_parsers.h @@ -768,6 +768,7 @@ public: /// @param dummy first argument is ignored, all Parser constructors /// accept string as first argument. /// @param relay_info is the storage in which to store the parsed + /// @param default_addr 0.0.0.0 for IPv4 or :: for IPv6 /// relay information upon "commit". RelayInfoParser(const std::string& dummy, const isc::dhcp::Subnet::RelayInfoPtr& relay_info, @@ -803,7 +804,6 @@ public: /// @brief constructor /// - /// @param unusued /// @param global_context /// @param default_addr default IP address (0.0.0.0 for IPv4, :: for IPv6) SubnetConfigParser(const std::string&, ParserContextPtr global_context, diff --git a/src/lib/dhcpsrv/subnet.cc b/src/lib/dhcpsrv/subnet.cc index f5e9a7c458..5a85445dd4 100644 --- a/src/lib/dhcpsrv/subnet.cc +++ b/src/lib/dhcpsrv/subnet.cc @@ -1,4 +1,4 @@ -// Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC") +// Copyright (C) 2012-2014 Internet Systems Consortium, Inc. ("ISC") // // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted, provided that the above diff --git a/src/lib/dhcpsrv/subnet.h b/src/lib/dhcpsrv/subnet.h index fc59e9a7e3..316556b580 100644 --- a/src/lib/dhcpsrv/subnet.h +++ b/src/lib/dhcpsrv/subnet.h @@ -1,4 +1,4 @@ -// Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC") +// Copyright (C) 2012-2014 Internet Systems Consortium, Inc. ("ISC") // // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted, provided that the above @@ -412,7 +412,7 @@ public: /// /// Setting this parameter is not needed in most deployments. /// - /// @params relay IP address of the relay + /// @param relay IP address of the relay void setRelay(const isc::dhcp::Subnet::RelayInfo& relay); /// @brief Relay information @@ -482,8 +482,9 @@ protected: /// @brief keeps the subnet-id value /// - /// It is inreased every time a new Subnet object is created. - /// It is reset (@ref resetSubnetId) every time reconfiguration occurs. + /// It is inreased every time a new Subnet object is created. It is reset + /// (@ref resetSubnetID) every time reconfiguration + /// occurs. /// /// Static value initialized in subnet.cc. static SubnetID static_id_; diff --git a/src/lib/dhcpsrv/tests/subnet_unittest.cc b/src/lib/dhcpsrv/tests/subnet_unittest.cc index 8e81cc15e0..9e86579646 100644 --- a/src/lib/dhcpsrv/tests/subnet_unittest.cc +++ b/src/lib/dhcpsrv/tests/subnet_unittest.cc @@ -1,4 +1,4 @@ -// Copyright (C) 2012-2013 Internet Systems Consortium, Inc. ("ISC") +// Copyright (C) 2012-2014 Internet Systems Consortium, Inc. ("ISC") // // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted, provided that the above