diff --git a/check_hadoop_jakarta_impact.sh b/check_hadoop_jakarta_impact.sh new file mode 100644 index 000000000000..53506e6aace9 --- /dev/null +++ b/check_hadoop_jakarta_impact.sh @@ -0,0 +1,399 @@ +#!/usr/bin/env sh +# Find import lines that end with any class name from the hardcoded Jetty 12 migration change list. +# Usage: ./check_hadoop_jakarta_impact.sh /path/to/hbase + +# TODO: Drop before merge + +set -eu + +if [ $# -ne 1 ]; then + echo "Usage: $0 /path/to/hbase" >&2 + exit 1 +fi + +HBASE_ROOT="$1" +if [ ! -d "$HBASE_ROOT" ]; then + echo "Error: '$HBASE_ROOT' is not a directory." >&2 + exit 2 +fi + +# --- Hardcoded change list (exactly the list you provided) --- +CHANGED_FILES_LIST=$(cat <<'EOF' +--- + LICENSE-binary | 66 +++++++------- + .../ensure-jars-have-correct-contents.sh | 1 + + .../ensure-jars-have-correct-contents.sh | 1 + + .../hadoop-client-minicluster/pom.xml | 42 ++++++++- + .../hadoop/fs/obs/OBSBlockOutputStream.java | 2 +- + .../apache/hadoop/fs/obs/OBSInputStream.java | 2 +- + .../examples/RequestLoggerFilter.java | 41 +++------ + .../authentication/examples/WhoServlet.java | 8 +- + hadoop-common-project/hadoop-auth/pom.xml | 4 +- + .../AltKerberosAuthenticationHandler.java | 6 +- + .../server/AuthenticationFilter.java | 22 ++--- + .../server/AuthenticationHandler.java | 6 +- + .../server/AuthenticationToken.java | 2 +- + .../JWTRedirectAuthenticationHandler.java | 8 +- + .../server/KerberosAuthenticationHandler.java | 6 +- + .../server/LdapAuthenticationHandler.java | 6 +- + .../MultiSchemeAuthenticationHandler.java | 6 +- + .../server/PseudoAuthenticationHandler.java | 6 +- + .../authentication/util/CertificateUtil.java | 2 +- + .../util/FileSignerSecretProvider.java | 2 +- + .../util/RolloverSignerSecretProvider.java | 2 +- + .../util/SignerSecretProvider.java | 2 +- + .../util/ZKSignerSecretProvider.java | 2 +- + hadoop-common-project/hadoop-common/pom.xml | 8 +- + .../org/apache/hadoop/conf/ConfServlet.java | 10 +-- + .../hadoop/conf/ReconfigurationServlet.java | 8 +- + .../hadoop/http/AdminAuthorizedServlet.java | 8 +- + .../org/apache/hadoop/http/HttpServer2.java | 88 ++++++++++--------- + .../hadoop/http/HttpServer2Metrics.java | 56 ------------ + .../apache/hadoop/http/IsActiveServlet.java | 6 +- + .../org/apache/hadoop/http/NoCacheFilter.java | 14 +-- + .../hadoop/http/ProfileOutputServlet.java | 8 +- + .../apache/hadoop/http/ProfileServlet.java | 6 +- + .../hadoop/http/ProfilerDisabledServlet.java | 6 +- + .../apache/hadoop/http/PrometheusServlet.java | 8 +- + .../org/apache/hadoop/http/WebServlet.java | 8 +- + .../hadoop/http/lib/StaticUserWebFilter.java | 16 ++-- + .../org/apache/hadoop/jmx/JMXJsonServlet.java | 8 +- + .../java/org/apache/hadoop/log/LogLevel.java | 8 +- + .../server/ProxyUserAuthenticationFilter.java | 12 +-- + .../security/http/CrossOriginFilter.java | 16 ++-- + .../http/RestCsrfPreventionFilter.java | 18 ++-- + .../security/http/XFrameOptionsFilter.java | 16 ++-- + .../DelegationTokenAuthenticationFilter.java | 14 +-- + .../DelegationTokenAuthenticationHandler.java | 8 +- + ...eDelegationTokenAuthenticationHandler.java | 6 +- + .../token/delegation/web/ServletUtils.java | 2 +- + .../hadoop/util/HttpExceptionUtils.java | 6 +- + .../org/apache/hadoop/util/ServletUtil.java | 4 +- + hadoop-common-project/hadoop-kms/pom.xml | 4 +- + .../hadoop/crypto/key/kms/server/KMS.java | 26 +++--- + .../kms/server/KMSAuthenticationFilter.java | 41 +++------ + .../key/kms/server/KMSExceptionsProvider.java | 6 +- + .../crypto/key/kms/server/KMSJSONReader.java | 12 +-- + .../crypto/key/kms/server/KMSJSONWriter.java | 12 +-- + .../crypto/key/kms/server/KMSMDCFilter.java | 14 +-- + .../crypto/key/kms/server/KMSWebApp.java | 4 +- + .../hadoop/hdfs/web/WebHdfsFileSystem.java | 4 +- + .../hdfs/web/resources/HttpOpParam.java | 2 +- + .../hadoop-hdfs-httpfs/pom.xml | 4 +- + .../hadoop/fs/http/client/HttpFSUtils.java | 2 +- + .../server/CheckUploadContentTypeFilter.java | 16 ++-- + .../server/HttpFSAuthenticationFilter.java | 4 +- + .../http/server/HttpFSExceptionProvider.java | 4 +- + .../hadoop/fs/http/server/HttpFSServer.java | 30 +++---- + .../lib/servlet/FileSystemReleaseFilter.java | 12 +-- + .../hadoop/lib/servlet/HostnameFilter.java | 12 +-- + .../apache/hadoop/lib/servlet/MDCFilter.java | 14 +-- + .../hadoop/lib/servlet/ServerWebApp.java | 4 +- + .../hadoop/lib/wsrs/ExceptionProvider.java | 4 +- + .../hadoop/lib/wsrs/InputStreamEntity.java | 2 +- + .../hadoop/lib/wsrs/JSONMapProvider.java | 12 +-- + .../apache/hadoop/lib/wsrs/JSONProvider.java | 12 +-- + .../hadoop/lib/wsrs/ParametersProvider.java | 2 +- + .../metrics/NamenodeBeanMetrics.java | 2 +- + .../server/federation/metrics/RBFMetrics.java | 12 +-- + .../federation/router/ConnectionManager.java | 2 +- + .../federation/router/ConnectionPool.java | 2 +- + .../router/IsRouterActiveServlet.java | 2 +- + .../federation/router/RouterFsckServlet.java | 6 +- + .../federation/router/RouterHttpServer.java | 2 +- + .../router/RouterNetworkTopologyServlet.java | 6 +- + .../federation/router/RouterRpcClient.java | 6 +- + .../router/RouterWebHdfsMethods.java | 10 +-- + .../server/GetJournalEditServlet.java | 8 +- + .../hdfs/qjournal/server/JournalNode.java | 2 +- + .../server/JournalNodeHttpServer.java | 2 +- + .../server/aliasmap/InMemoryAliasMap.java | 4 +- + .../HostRestrictingAuthorizationFilter.java | 16 ++-- + .../hadoop/hdfs/server/common/JspHelper.java | 4 +- + .../hdfs/server/datanode/BlockScanner.java | 6 +- + .../hadoop/hdfs/server/datanode/DataNode.java | 8 +- + .../datanode/web/DatanodeHttpServer.java | 4 +- + ...RestrictingAuthorizationFilterHandler.java | 4 +- + .../web/RestCsrfPreventionFilterHandler.java | 2 +- + .../hdfs/server/namenode/DfsServlet.java | 4 +- + .../hdfs/server/namenode/FSNamesystem.java | 22 ++--- + .../hdfs/server/namenode/FsckServlet.java | 6 +- + .../hdfs/server/namenode/ImageServlet.java | 12 +-- + .../hdfs/server/namenode/NNStorage.java | 2 +- + .../server/namenode/NameNodeHttpServer.java | 2 +- + .../namenode/NetworkTopologyServlet.java | 8 +- + .../namenode/StartupProgressServlet.java | 4 +- + .../hdfs/server/namenode/TransferFsImage.java | 4 +- + .../web/resources/NamenodeWebHdfsMethods.java | 40 ++++----- + .../OfflineImageReconstructor.java | 2 +- + .../apache/hadoop/hdfs/web/AuthFilter.java | 14 +-- + .../apache/hadoop/hdfs/web/ParamFilter.java | 16 ++-- + .../hdfs/web/resources/ExceptionHandler.java | 12 +-- + .../hdfs/web/resources/UserProvider.java | 8 +- + .../jobhistory/JobHistoryEventHandler.java | 2 +- + .../mapreduce/v2/app/JobEndNotifier.java | 32 +++---- + .../mapreduce/v2/app/webapp/AMWebApp.java | 2 +- + .../v2/app/webapp/AMWebServices.java | 34 +++---- + .../hadoop/mapreduce/v2/app/webapp/App.java | 2 +- + .../v2/app/webapp/AppController.java | 2 +- + .../v2/app/webapp/JAXBContextResolver.java | 8 +- + .../v2/app/webapp/dao/AMAttemptInfo.java | 6 +- + .../v2/app/webapp/dao/AMAttemptsInfo.java | 8 +- + .../mapreduce/v2/app/webapp/dao/AppInfo.java | 6 +- + .../app/webapp/dao/BlacklistedNodesInfo.java | 6 +- + .../v2/app/webapp/dao/ConfEntryInfo.java | 6 +- + .../mapreduce/v2/app/webapp/dao/ConfInfo.java | 6 +- + .../v2/app/webapp/dao/CounterGroupInfo.java | 8 +- + .../v2/app/webapp/dao/CounterInfo.java | 6 +- + .../v2/app/webapp/dao/JobCounterInfo.java | 8 +- + .../mapreduce/v2/app/webapp/dao/JobInfo.java | 8 +- + .../webapp/dao/JobTaskAttemptCounterInfo.java | 8 +- + .../app/webapp/dao/JobTaskAttemptState.java | 6 +- + .../v2/app/webapp/dao/JobTaskCounterInfo.java | 8 +- + .../mapreduce/v2/app/webapp/dao/JobsInfo.java | 6 +- + .../v2/app/webapp/dao/MapTaskAttemptInfo.java | 2 +- + .../app/webapp/dao/ReduceTaskAttemptInfo.java | 2 +- + .../v2/app/webapp/dao/TaskAttemptInfo.java | 10 +-- + .../v2/app/webapp/dao/TaskAttemptsInfo.java | 4 +- + .../app/webapp/dao/TaskCounterGroupInfo.java | 6 +- + .../v2/app/webapp/dao/TaskCounterInfo.java | 6 +- + .../mapreduce/v2/app/webapp/dao/TaskInfo.java | 8 +- + .../v2/app/webapp/dao/TasksInfo.java | 6 +- + .../ContainerLogsInfoMessageBodyReader.java | 12 +-- + .../RemoteLogPathsMessageBodyReader.java | 12 +-- + hadoop-project/pom.xml | 74 ++++++---------- + .../fs/azure/AzureNativeFileSystemStore.java | 2 +- + .../hadoop/fs/azure/MockStorageInterface.java | 4 +- + .../service/ResourceEstimatorServer.java | 2 +- + .../service/ResourceEstimatorService.java | 16 ++-- + .../api/records/timeline/TimelineAbout.java | 8 +- + .../TimelineDelegationTokenResponse.java | 8 +- + .../api/records/timeline/TimelineDomain.java | 8 +- + .../api/records/timeline/TimelineDomains.java | 8 +- + .../records/timeline/TimelineEntities.java | 8 +- + .../api/records/timeline/TimelineEntity.java | 8 +- + .../api/records/timeline/TimelineEvent.java | 8 +- + .../api/records/timeline/TimelineEvents.java | 8 +- + .../api/records/timeline/TimelineHealth.java | 8 +- + .../records/timeline/TimelinePutResponse.java | 8 +- + .../timeline/reader/TimelineDomainReader.java | 12 +-- + .../reader/TimelineEntitiesReader.java | 12 +-- + .../reader/TimelinePutResponseReader.java | 12 +-- + .../timeline/writer/TimelineDomainWriter.java | 12 +-- + .../writer/TimelineDomainsWriter.java | 12 +-- + .../writer/TimelineEntitiesWriter.java | 12 +-- + .../timeline/writer/TimelineEntityWriter.java | 12 +-- + .../timeline/writer/TimelineEventsWriter.java | 12 +-- + .../writer/TimelinePutResponseWriter.java | 12 +-- + .../timelineservice/FlowActivityEntity.java | 2 +- + .../timelineservice/FlowRunEntity.java | 2 +- + .../timelineservice/TimelineDomain.java | 8 +- + .../timelineservice/TimelineEntities.java | 8 +- + .../timelineservice/TimelineEntity.java | 8 +- + .../timelineservice/TimelineEvent.java | 8 +- + .../timelineservice/TimelineMetric.java | 8 +- + .../TimelineWriteResponse.java | 8 +- + .../reader/TimelineDomainReader.java | 12 +-- + .../reader/TimelineEntitiesReader.java | 12 +-- + .../reader/TimelineEntityReader.java | 12 +-- + .../writer/TimelineDomainWriter.java | 12 +-- + .../writer/TimelineEntitiesWriter.java | 12 +-- + .../writer/TimelineEntitySetWriter.java | 12 +-- + .../writer/TimelineEntityWriter.java | 12 +-- + .../writer/TimelineHealthWriter.java | 12 +-- + .../pom.xml | 16 +++- + .../appcatalog/application/AppCatalog.java | 4 +- + .../application/AppCatalogInitializer.java | 5 +- + .../application/YarnServiceClient.java | 6 +- + .../controller/AppDetailsController.java | 20 ++--- + .../controller/AppListController.java | 20 ++--- + .../controller/AppStoreController.java | 18 ++-- + .../hadoop-yarn-services-api/pom.xml | 8 +- + .../yarn/service/client/ApiServiceClient.java | 16 ++-- + .../hadoop/yarn/service/webapp/ApiServer.java | 12 +-- + .../yarn/service/webapp/ApiServerWebApp.java | 4 +- + .../hadoop/yarn/service/ServiceScheduler.java | 8 +- + .../yarn/service/conf/RestApiConstants.java | 2 +- + .../hadoop/yarn/service/utils/HttpUtil.java | 10 +-- + .../hadoop-yarn/hadoop-yarn-client/pom.xml | 7 +- + .../client/api/ContainerShellWebSocket.java | 45 +++++----- + .../yarn/client/api/impl/YarnClientImpl.java | 3 +- + .../hadoop/yarn/client/cli/LogsCLI.java | 14 +-- + .../hadoop/yarn/client/cli/SchedConfCLI.java | 20 ++--- + .../hadoop-yarn/hadoop-yarn-common/pom.xml | 4 +- + .../client/api/impl/DirectTimelineWriter.java | 2 +- + .../api/impl/FileSystemTimelineWriter.java | 2 +- + .../client/api/impl/TimelineClientImpl.java | 2 +- + .../client/api/impl/TimelineConnector.java | 6 +- + .../api/impl/TimelineReaderClientImpl.java | 16 ++-- + .../client/api/impl/TimelineV2ClientImpl.java | 14 +-- + .../yarn/client/api/impl/TimelineWriter.java | 10 +-- + .../yarn/logaggregation/LogToolUtils.java | 8 +- + .../yarn/webapp/BadRequestException.java | 4 +- + .../hadoop/yarn/webapp/ConflictException.java | 4 +- + .../apache/hadoop/yarn/webapp/Controller.java | 6 +- + .../yarn/webapp/DefaultWrapperServlet.java | 12 +-- + .../apache/hadoop/yarn/webapp/Dispatcher.java | 10 +-- + .../yarn/webapp/ForbiddenException.java | 4 +- + .../yarn/webapp/GenericExceptionHandler.java | 22 ++--- + .../hadoop/yarn/webapp/NotFoundException.java | 4 +- + .../yarn/webapp/RemoteExceptionData.java | 6 +- + .../org/apache/hadoop/yarn/webapp/View.java | 8 +- + .../org/apache/hadoop/yarn/webapp/WebApp.java | 2 +- + .../apache/hadoop/yarn/webapp/WebApps.java | 4 +- + .../webapp/YarnJacksonJaxbJsonProvider.java | 13 +-- + .../hadoop/yarn/webapp/dao/ConfInfo.java | 6 +- + .../yarn/webapp/dao/QueueConfigInfo.java | 8 +- + .../yarn/webapp/dao/SchedConfUpdateInfo.java | 10 +-- + .../hadoop/yarn/webapp/util/WebAppUtils.java | 4 +- + .../yarn/webapp/util/WebServiceClient.java | 4 +- + .../yarn/webapp/util/YarnWebServiceUtils.java | 10 +-- + .../hadoop-yarn/hadoop-yarn-csi/pom.xml | 4 + + .../ApplicationHistoryServer.java | 6 +- + .../webapp/AHSWebApp.java | 2 +- + .../webapp/AHSWebServices.java | 28 +++--- + .../webapp/ContextFactory.java | 4 +- + .../webapp/JAXBContextResolver.java | 8 +- + .../timeline/webapp/TimelineWebServices.java | 32 +++---- + .../reader/ContainerLogsInfoListReader.java | 12 +-- + .../timeline/reader/TimelineAboutReader.java | 12 +-- + .../timeline/reader/TimelineDomainReader.java | 12 +-- + .../reader/TimelineDomainsReader.java | 12 +-- + .../reader/TimelineEntitiesReader.java | 12 +-- + .../timeline/reader/TimelineEntityReader.java | 12 +-- + .../timeline/reader/TimelineEventsReader.java | 12 +-- + .../reader/TimelinePutResponseReader.java | 12 +-- + .../security/http/RMAuthenticationFilter.java | 14 +-- + .../TimelineAuthenticationFilter.java | 4 +- + .../yarn/server/webapp/AppInfoProvider.java | 2 +- + .../hadoop/yarn/server/webapp/LogServlet.java | 14 +-- + .../yarn/server/webapp/LogWebService.java | 32 +++---- + .../server/webapp/LogWebServiceUtils.java | 8 +- + .../yarn/server/webapp/WebServices.java | 6 +- + .../globalpolicygenerator/GPGUtils.java | 12 +-- + .../webapp/GPGWebServices.java | 14 +-- + .../hadoop-yarn-server-nodemanager/pom.xml | 24 ++++- + .../amrmproxy/FederationInterceptor.java | 3 +- + .../webapp/ContainerShellWebSocket.java | 27 +++--- + .../ContainerShellWebSocketServlet.java | 10 +-- + .../webapp/JAXBContextResolver.java | 8 +- + .../nodemanager/webapp/NMWebAppFilter.java | 16 ++-- + .../nodemanager/webapp/NMWebServices.java | 64 +++++++------- + .../nodemanager/webapp/TerminalServlet.java | 10 +-- + .../server/nodemanager/webapp/WebServer.java | 2 +- + .../yarn/server/resourcemanager/RMNMInfo.java | 2 +- + .../resourcemanager/ResourceManager.java | 4 +- + .../webapp/FairSchedulerAppsBlock.java | 2 +- + .../webapp/JAXBContextResolver.java | 12 +-- + .../resourcemanager/webapp/RMWebApp.java | 2 +- + .../webapp/RMWebAppFilter.java | 16 ++-- + .../resourcemanager/webapp/RMWebAppUtil.java | 2 +- + .../webapp/RMWebServiceProtocol.java | 4 +- + .../resourcemanager/webapp/RMWebServices.java | 44 +++++----- + .../yarn/server/resourcemanager/MockNM.java | 6 +- + ...MWebServicesCapacitySchedDefaultLabel.java | 10 +-- + ...WebServicesCapacitySchedDynamicConfig.java | 6 +- + ...apacitySchedDynamicConfigAbsoluteMode.java | 6 +- + ...sCapacitySchedDynamicConfigWeightMode.java | 6 +- + ...pacitySchedDynamicConfigWeightModeDQC.java | 6 +- + ...vicesCapacitySchedLegacyQueueCreation.java | 10 +-- + ...ySchedLegacyQueueCreationAbsoluteMode.java | 10 +-- + ...rvicesCapacitySchedulerConfigMutation.java | 12 +-- + ...WebServicesCapacitySchedulerMixedMode.java | 6 +- + ...hedulerMixedModeAbsoluteAndPercentage.java | 6 +- + ...xedModeAbsoluteAndPercentageAndWeight.java | 6 +- + ...eAbsoluteAndPercentageAndWeightVector.java | 6 +- + ...rMixedModeAbsoluteAndPercentageVector.java | 6 +- + ...tySchedulerMixedModeAbsoluteAndWeight.java | 6 +- + ...dulerMixedModeAbsoluteAndWeightVector.java | 6 +- + ...SchedulerMixedModePercentageAndWeight.java | 6 +- + ...lerMixedModePercentageAndWeightVector.java | 6 +- + ...estRMWebServicesConfigurationMutation.java | 16 ++-- + ...vicesFairSchedulerCustomResourceTypes.java | 12 +-- + .../webapp/helper/BufferedClientResponse.java | 4 +- + .../webapp/reader/AppStateReader.java | 14 +-- + ...pplicationSubmissionContextInfoReader.java | 14 +-- + .../reader/LabelsToNodesInfoReader.java | 14 +-- + .../webapp/reader/NodeLabelsInfoReader.java | 14 +-- + .../webapp/reader/NodeToLabelsInfoReader.java | 14 +-- + .../reader/ResourceOptionInfoReader.java | 14 +-- + ...pplicationSubmissionContextInfoWriter.java | 16 ++-- + .../writer/ResourceOptionInfoWriter.java | 16 ++-- + .../writer/SchedConfUpdateInfoWriter.java | 16 ++-- + .../hadoop/yarn/server/router/Router.java | 4 +- + .../yarn/server/router/webapp/AppsBlock.java | 2 +- + .../webapp/DefaultRequestInterceptorREST.java | 8 +- + .../webapp/FederationInterceptorREST.java | 12 +-- + .../router/webapp/MetricsOverviewTable.java | 2 +- + .../server/router/webapp/NodeLabelsBlock.java | 2 +- + .../yarn/server/router/webapp/NodesBlock.java | 2 +- + .../router/webapp/RESTRequestInterceptor.java | 4 +- + .../server/router/webapp/RouterBlock.java | 2 +- + .../server/router/webapp/RouterWebApp.java | 2 +- + .../router/webapp/RouterWebServiceUtil.java | 22 ++--- + .../router/webapp/RouterWebServices.java | 38 ++++---- + .../webapp/cache/RouterAppInfoCacheKey.java | 2 +- + .../MockDefaultRequestInterceptorREST.java | 12 +-- + .../webapp/MockRESTRequestInterceptor.java | 8 +- + .../PassThroughRESTRequestInterceptor.java | 6 +---- + .../pom.xml | 8 +- + ...TimelineReaderWebServicesHBaseStorage.java | 8 +- + .../TimelineCollectorWebService.java | 36 ++++---- + .../reader/TimelineReaderWebServices.java | 28 +++--- + .../TimelineReaderWebServicesUtils.java | 2 +- + ...ineReaderWhitelistAuthorizationFilter.java | 16 ++-- + ...ineReaderWhitelistAuthorizationFilter.java | 10 +-- + .../reader/TimelineAboutReader.java | 12 +-- + .../reader/TimelineEntityReader.java | 12 +-- + .../reader/TimelineEntitySetReader.java | 12 +-- + .../reader/TimelineHealthReader.java | 12 +-- + .../yarn/server/webproxy/ProxyUtils.java | 8 +- + .../server/webproxy/WebAppProxyServlet.java | 16 ++-- + .../server/webproxy/amfilter/AmIpFilter.java | 18 ++-- + .../amfilter/AmIpServletRequestWrapper.java | 4 +- +EOF +) + +# Create a temp file with unique class names extracted from the list +CLASSES_FILE="$(mktemp 2>/dev/null || echo /tmp/jetty12_classes.$$)" +echo "$CHANGED_FILES_LIST" \ + | grep -Eo '[^[:space:]]+\.java' \ + | awk '{ + n=$0; sub(/^.*\//,"",n); sub(/\.java$/,"",n); print n + }' \ + | sort -u > "$CLASSES_FILE" + +echo "Scanning HBase imports for $(wc -l < "$CLASSES_FILE" | tr -d ' ') changed classes..." + +FOUND=0 + +# Portable search: find all *.java and grep them with regex +# Pattern logic: +# ^[[:space:]]*import[[:space:]]+(static[[:space:]]+)? -> import or import static +# [^;]* -> up to class token +# [^A-Za-z0-9_$]ClassName[[:space:]]*; -> ensure ClassName is a full token before ';' +# Note: We avoid GNU grep --include by using find | xargs. +find "$HBASE_ROOT" -type f -name '*.java' -print0 | while IFS= read -r -d '' file; do + : +done + +# Iterate over each class and search +while IFS= read -r cls; do + # Build ERE safely: prepend a non-identifier char before class name to approximate word boundary in imports. + pattern="^[[:space:]]*import[[:space:]]+(static[[:space:]]+)?[^;]*[^A-Za-z0-9_\$]${cls}[[:space:]]*;" + # Use find+xargs for portability; suppress errors for long lines/binary files. + hits=$(find "$HBASE_ROOT" -type f -name '*.java' -print0 \ + | xargs -0 grep -nEIH "$pattern" 2>/dev/null || true) + if [ -n "$hits" ]; then + echo "=== $cls ===" + echo "$hits" + FOUND=1 + fi +done < "$CLASSES_FILE" + +if [ "$FOUND" -eq 0 ]; then + echo "No matching imports found." +fi + +# Cleanup temp file +rm -f "$CLASSES_FILE" diff --git a/hbase-auth-filters/pom.xml b/hbase-auth-filters/pom.xml new file mode 100644 index 000000000000..581533766a72 --- /dev/null +++ b/hbase-auth-filters/pom.xml @@ -0,0 +1,185 @@ + + + + 4.0.0 + + org.apache.hbase + hbase-build-configuration + ${revision} + ../hbase-build-configuration + + hbase-auth-filters + Apache HBase - Auth Filters + Auth Filters for HBase + + + + org.apache.hbase + hbase-annotations + provided + + + org.apache.hbase + hbase-logging + test + + + org.apache.hbase + hbase-common + + + org.apache.hbase + hbase-common + test-jar + test + + + org.slf4j + jcl-over-slf4j + test + + + org.slf4j + jul-to-slf4j + test + + + org.apache.logging.log4j + log4j-api + test + + + org.apache.logging.log4j + log4j-core + test + + + org.apache.logging.log4j + log4j-slf4j-impl + test + + + org.apache.hadoop + hadoop-auth + ${hadoop-three.version} + + + org.mockito + mockito-core + test + + + org.apache.hbase.thirdparty + hbase-shaded-jetty-12-plus-core + + + org.apache.hbase.thirdparty + hbase-shaded-miscellaneous + + + org.apache.hbase.thirdparty + hbase-shaded-jetty-12-plus-ee8 + + + org.slf4j + slf4j-api + compile + + + commons-codec + commons-codec + compile + + + org.apache.hadoop + hadoop-minikdc + test + ${hadoop-three.version} + + + org.apache.zookeeper + zookeeper + + + io.dropwizard.metrics + metrics-core + + + org.xerial.snappy + snappy-java + provided + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.junit.jupiter + junit-jupiter-params + test + + + org.junit.vintage + junit-vintage-engine + test + + + + + + + + maven-assembly-plugin + + true + + + + + org.apache.maven.plugins + maven-source-plugin + + + + jar + test-jar + + package + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + true + + + + net.revelc.code + warbucks-maven-plugin + + + + diff --git a/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/AltKerberosAuthenticationHandler.java b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/AltKerberosAuthenticationHandler.java new file mode 100644 index 000000000000..33b13c750b8b --- /dev/null +++ b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/AltKerberosAuthenticationHandler.java @@ -0,0 +1,151 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. See accompanying LICENSE file. + */ +package org.apache.hadoop.hbase.security.authentication.server; + +import java.io.IOException; +import java.util.Locale; +import java.util.Properties; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.apache.hadoop.security.authentication.client.AuthenticationException; +import org.apache.yetus.audience.InterfaceAudience; + + /** + * The {@link AltKerberosAuthenticationHandler} behaves exactly the same way as + * the {@link KerberosAuthenticationHandler}, except that it allows for an + * alternative form of authentication for browsers while still using Kerberos + * for Java access. This is an abstract class that should be subclassed + * to allow a developer to implement their own custom authentication for browser + * access. The alternateAuthenticate method will be called whenever a request + * comes from a browser. + */ +@InterfaceAudience.Private +public abstract class AltKerberosAuthenticationHandler + extends KerberosAuthenticationHandler { + + /** + * Constant that identifies the authentication mechanism. + */ + public static final String TYPE = "alt-kerberos"; + + /** + * Constant for the configuration property that indicates which user agents + * are not considered browsers (comma separated) + */ + public static final String NON_BROWSER_USER_AGENTS = + TYPE + ".non-browser.user-agents"; + private static final String NON_BROWSER_USER_AGENTS_DEFAULT = + "java,curl,wget,perl"; + + private String[] nonBrowserUserAgents; + + /** + * Returns the authentication type of the authentication handler, + * 'alt-kerberos'. + * + * @return the authentication type of the authentication handler, + * 'alt-kerberos'. + */ + @Override + public String getType() { + return TYPE; + } + + @Override + public void init(Properties config) throws ServletException { + super.init(config); + + nonBrowserUserAgents = config.getProperty( + NON_BROWSER_USER_AGENTS, NON_BROWSER_USER_AGENTS_DEFAULT) + .split("\\W*,\\W*"); + for (int i = 0; i < nonBrowserUserAgents.length; i++) { + nonBrowserUserAgents[i] = + nonBrowserUserAgents[i].toLowerCase(Locale.ENGLISH); + } + } + + /** + * It enforces the the Kerberos SPNEGO authentication sequence returning an + * {@link AuthenticationToken} only after the Kerberos SPNEGO sequence has + * completed successfully (in the case of Java access) and only after the + * custom authentication implemented by the subclass in alternateAuthenticate + * has completed successfully (in the case of browser access). + * + * @param request the HTTP client request. + * @param response the HTTP client response. + * + * @return an authentication token if the request is authorized or null + * + * @throws IOException thrown if an IO error occurred + * @throws AuthenticationException thrown if an authentication error occurred + */ + @Override + public AuthenticationToken authenticate(HttpServletRequest request, + HttpServletResponse response) + throws IOException, AuthenticationException { + AuthenticationToken token; + if (isBrowser(request.getHeader("User-Agent"))) { + token = alternateAuthenticate(request, response); + } + else { + token = super.authenticate(request, response); + } + return token; + } + + /** + * This method parses the User-Agent String and returns whether or not it + * refers to a browser. If its not a browser, then Kerberos authentication + * will be used; if it is a browser, alternateAuthenticate from the subclass + * will be used. + *

+ * A User-Agent String is considered to be a browser if it does not contain + * any of the values from alt-kerberos.non-browser.user-agents; the default + * behavior is to consider everything a browser unless it contains one of: + * "java", "curl", "wget", or "perl". Subclasses can optionally override + * this method to use different behavior. + * + * @param userAgent The User-Agent String, or null if there isn't one + * @return true if the User-Agent String refers to a browser, false if not + */ + protected boolean isBrowser(String userAgent) { + if (userAgent == null) { + return false; + } + userAgent = userAgent.toLowerCase(Locale.ENGLISH); + boolean isBrowser = true; + for (String nonBrowserUserAgent : nonBrowserUserAgents) { + if (userAgent.contains(nonBrowserUserAgent)) { + isBrowser = false; + break; + } + } + return isBrowser; + } + + /** + * Subclasses should implement this method to provide the custom + * authentication to be used for browsers. + * + * @param request the HTTP client request. + * @param response the HTTP client response. + * @return an authentication token if the request is authorized, or null + * @throws IOException thrown if an IO error occurs + * @throws AuthenticationException thrown if an authentication error occurs + */ + public abstract AuthenticationToken alternateAuthenticate( + HttpServletRequest request, HttpServletResponse response) + throws IOException, AuthenticationException; +} diff --git a/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/AuthenticationFilter.java b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/AuthenticationFilter.java new file mode 100644 index 000000000000..d95a6652decf --- /dev/null +++ b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/AuthenticationFilter.java @@ -0,0 +1,712 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. See accompanying LICENSE file. + */ +package org.apache.hadoop.hbase.security.authentication.server; + +import org.apache.yetus.audience.InterfaceAudience; +import org.apache.yetus.audience.InterfaceStability; +import org.apache.hadoop.security.authentication.client.AuthenticatedURL; +import org.apache.hadoop.security.authentication.client.AuthenticationException; +import org.apache.hadoop.security.authentication.client.KerberosAuthenticator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import javax.servlet.http.HttpServletResponse; + +import java.lang.reflect.InvocationTargetException; +import java.io.IOException; +import java.security.Principal; +import java.text.SimpleDateFormat; +import java.util.*; + +import org.apache.hadoop.hbase.security.authentication.util.Signer; +import org.apache.hadoop.hbase.security.authentication.util.SignerException; +import org.apache.hadoop.hbase.security.authentication.util.SignerSecretProvider; +import org.apache.hadoop.hbase.security.authentication.util.FileSignerSecretProvider; +import org.apache.hadoop.hbase.security.authentication.util.RandomSignerSecretProvider; +import org.apache.hadoop.hbase.security.authentication.util.ZKSignerSecretProvider; +import org.apache.yetus.audience.InterfaceAudience; + +/** + * The {@link AuthenticationFilter} enables protecting web application + * resources with different (pluggable) + * authentication mechanisms and signer secret providers. + *

+ * Additional authentication mechanisms are supported via the {@link AuthenticationHandler} interface. + *

+ * This filter delegates to the configured authentication handler for authentication and once it obtains an + * {@link AuthenticationToken} from it, sets a signed HTTP cookie with the token. For client requests + * that provide the signed HTTP cookie, it verifies the validity of the cookie, extracts the user information + * and lets the request proceed to the target resource. + *

+ * The rest of the configuration properties are specific to the {@link AuthenticationHandler} implementation and the + * {@link AuthenticationFilter} will take all the properties that start with the prefix #PREFIX#, it will remove + * the prefix from it and it will pass them to the the authentication handler for initialization. Properties that do + * not start with the prefix will not be passed to the authentication handler initialization. + *

+ * Details of the configurations are listed on Configuration Page + *

+ * The "zookeeper" implementation has additional configuration properties that + * must be specified; see {@link ZKSignerSecretProvider} for details. + */ + +@InterfaceAudience.Private +@InterfaceStability.Unstable +public class AuthenticationFilter implements Filter { + + private static Logger LOG = LoggerFactory.getLogger(AuthenticationFilter.class); + + /** + * Constant for the property that specifies the configuration prefix. + */ + public static final String CONFIG_PREFIX = "config.prefix"; + + /** + * Constant for the property that specifies the authentication handler to use. + */ + public static final String AUTH_TYPE = "type"; + + /** + * Constant for the property that specifies the secret to use for signing the HTTP Cookies. + */ + public static final String SIGNATURE_SECRET = "signature.secret"; + + public static final String SIGNATURE_SECRET_FILE = SIGNATURE_SECRET + ".file"; + + /** + * Constant for the configuration property + * that indicates the max inactive interval of the generated token. + */ + public static final String + AUTH_TOKEN_MAX_INACTIVE_INTERVAL = "token.max-inactive-interval"; + + /** + * Constant for the configuration property that indicates the validity of the generated token. + */ + public static final String AUTH_TOKEN_VALIDITY = "token.validity"; + + /** + * Constant for the configuration property that indicates the domain to use in the HTTP cookie. + */ + public static final String COOKIE_DOMAIN = "cookie.domain"; + + /** + * Constant for the configuration property that indicates the path to use in the HTTP cookie. + */ + public static final String COOKIE_PATH = "cookie.path"; + + /** + * Constant for the configuration property + * that indicates the persistence of the HTTP cookie. + */ + public static final String COOKIE_PERSISTENT = "cookie.persistent"; + + /** + * Constant for the configuration property that indicates the name of the + * SignerSecretProvider class to use. + * Possible values are: "file", "random", "zookeeper", or a classname. + * If not specified, the "file" implementation will be used with + * SIGNATURE_SECRET_FILE; and if that's not specified, the "random" + * implementation will be used. + */ + public static final String SIGNER_SECRET_PROVIDER = + "signer.secret.provider"; + + /** + * Constant for the ServletContext attribute that can be used for providing a + * custom implementation of the SignerSecretProvider. Note that the class + * should already be initialized. If not specified, SIGNER_SECRET_PROVIDER + * will be used. + */ + public static final String SIGNER_SECRET_PROVIDER_ATTRIBUTE = + "signer.secret.provider.object"; + + private Properties config; + private Signer signer; + private SignerSecretProvider secretProvider; + private AuthenticationHandler authHandler; + private long maxInactiveInterval; + private long validity; + private String cookieDomain; + private String cookiePath; + private boolean isCookiePersistent; + private boolean destroySecretProvider; + + /** + *

Initializes the authentication filter and signer secret provider.

+ * It instantiates and initializes the specified {@link + * AuthenticationHandler}. + * + * @param filterConfig filter configuration. + * + * @throws ServletException thrown if the filter or the authentication handler could not be initialized properly. + */ + @Override + public void init(FilterConfig filterConfig) throws ServletException { + String configPrefix = filterConfig.getInitParameter(CONFIG_PREFIX); + configPrefix = (configPrefix != null) ? configPrefix + "." : ""; + config = getConfiguration(configPrefix, filterConfig); + String authHandlerName = config.getProperty(AUTH_TYPE, null); + String authHandlerClassName; + if (authHandlerName == null) { + throw new ServletException("Authentication type must be specified: " + + PseudoAuthenticationHandler.TYPE + "|" + + KerberosAuthenticationHandler.TYPE + "|"); + } + authHandlerClassName = + AuthenticationHandlerUtil + .getAuthenticationHandlerClassName(authHandlerName); + maxInactiveInterval = Long.parseLong(config.getProperty( + AUTH_TOKEN_MAX_INACTIVE_INTERVAL, "-1")); // By default, disable. + if (maxInactiveInterval > 0) { + maxInactiveInterval *= 1000; + } + validity = Long.parseLong(config.getProperty(AUTH_TOKEN_VALIDITY, "36000")) + * 1000; //10 hours + initializeSecretProvider(filterConfig); + + initializeAuthHandler(authHandlerClassName, filterConfig); + + cookieDomain = config.getProperty(COOKIE_DOMAIN, null); + cookiePath = config.getProperty(COOKIE_PATH, null); + isCookiePersistent = Boolean.parseBoolean( + config.getProperty(COOKIE_PERSISTENT, "false")); + + } + + protected void initializeAuthHandler(String authHandlerClassName, FilterConfig filterConfig) + throws ServletException { + try { + Class klass = Thread.currentThread().getContextClassLoader().loadClass(authHandlerClassName); + authHandler = (AuthenticationHandler) klass.getDeclaredConstructor().newInstance(); + authHandler.init(config); + } catch (ClassNotFoundException | InstantiationException | + IllegalAccessException | NoSuchMethodException | InvocationTargetException ex) { + throw new ServletException(ex); + } + } + + protected void initializeSecretProvider(FilterConfig filterConfig) + throws ServletException { + secretProvider = (SignerSecretProvider) filterConfig.getServletContext(). + getAttribute(SIGNER_SECRET_PROVIDER_ATTRIBUTE); + if (secretProvider == null) { + // As tomcat cannot specify the provider object in the configuration. + // It'll go into this path + try { + secretProvider = constructSecretProvider( + filterConfig.getServletContext(), + config, false); + destroySecretProvider = true; + } catch (Exception ex) { + throw new ServletException(ex); + } + } + signer = new Signer(secretProvider); + } + + public static SignerSecretProvider constructSecretProvider( + ServletContext ctx, Properties config, + boolean disallowFallbackToRandomSecretProvider) throws Exception { + String name = config.getProperty(SIGNER_SECRET_PROVIDER, "file"); + long validity = Long.parseLong(config.getProperty(AUTH_TOKEN_VALIDITY, + "36000")) * 1000; + + if (!disallowFallbackToRandomSecretProvider + && "file".equals(name) + && config.getProperty(SIGNATURE_SECRET_FILE) == null) { + name = "random"; + } + + SignerSecretProvider provider; + if ("file".equals(name)) { + provider = new FileSignerSecretProvider(); + try { + provider.init(config, ctx, validity); + } catch (Exception e) { + if (!disallowFallbackToRandomSecretProvider) { + LOG.warn("Unable to initialize FileSignerSecretProvider, " + + "falling back to use random secrets. Reason: " + e.getMessage()); + provider = new RandomSignerSecretProvider(); + provider.init(config, ctx, validity); + } else { + throw e; + } + } + } else if ("random".equals(name)) { + provider = new RandomSignerSecretProvider(); + provider.init(config, ctx, validity); + } else if ("zookeeper".equals(name)) { + provider = new ZKSignerSecretProvider(); + provider.init(config, ctx, validity); + } else { + provider = (SignerSecretProvider) Thread.currentThread(). + getContextClassLoader().loadClass(name).getDeclaredConstructor().newInstance(); + provider.init(config, ctx, validity); + } + return provider; + } + + /** + * Returns the configuration properties of the {@link AuthenticationFilter} + * without the prefix. The returned properties are the same that the + * {@link #getConfiguration(String, FilterConfig)} method returned. + * + * @return the configuration properties. + */ + protected Properties getConfiguration() { + return config; + } + + /** + * Returns the authentication handler being used. + * + * @return the authentication handler being used. + */ + protected AuthenticationHandler getAuthenticationHandler() { + return authHandler; + } + + /** + * Returns if a random secret is being used. + * + * @return if a random secret is being used. + */ + protected boolean isRandomSecret() { + return secretProvider.getClass() == RandomSignerSecretProvider.class; + } + + /** + * Returns if a custom implementation of a SignerSecretProvider is being used. + * + * @return if a custom implementation of a SignerSecretProvider is being used. + */ + protected boolean isCustomSignerSecretProvider() { + Class clazz = secretProvider.getClass(); + return clazz != FileSignerSecretProvider.class && clazz != + RandomSignerSecretProvider.class && clazz != ZKSignerSecretProvider + .class; + } + + /** + * Returns the max inactive interval time of the generated tokens. + * + * @return the max inactive interval time of the generated tokens in seconds. + */ + protected long getMaxInactiveInterval() { + return maxInactiveInterval / 1000; + } + + /** + * Returns the validity time of the generated tokens. + * + * @return the validity time of the generated tokens, in seconds. + */ + protected long getValidity() { + return validity / 1000; + } + + /** + * Returns the cookie domain to use for the HTTP cookie. + * + * @return the cookie domain to use for the HTTP cookie. + */ + protected String getCookieDomain() { + return cookieDomain; + } + + /** + * Returns the cookie path to use for the HTTP cookie. + * + * @return the cookie path to use for the HTTP cookie. + */ + protected String getCookiePath() { + return cookiePath; + } + + /** + * Returns the cookie persistence to use for the HTTP cookie. + * + * @return the cookie persistence to use for the HTTP cookie. + */ + protected boolean isCookiePersistent() { + return isCookiePersistent; + } + + /** + * Destroys the filter. + *

+ * It invokes the {@link AuthenticationHandler#destroy()} method to release any resources it may hold. + */ + @Override + public void destroy() { + if (authHandler != null) { + authHandler.destroy(); + authHandler = null; + } + if (secretProvider != null && destroySecretProvider) { + secretProvider.destroy(); + secretProvider = null; + } + } + + /** + * Returns the filtered configuration (only properties starting with the specified prefix). The property keys + * are also trimmed from the prefix. The returned {@link Properties} object is used to initialized the + * {@link AuthenticationHandler}. + *

+ * This method can be overriden by subclasses to obtain the configuration from other configuration source than + * the web.xml file. + * + * @param configPrefix configuration prefix to use for extracting configuration properties. + * @param filterConfig filter configuration object + * + * @return the configuration to be used with the {@link AuthenticationHandler} instance. + * + * @throws ServletException thrown if the configuration could not be created. + */ + protected Properties getConfiguration(String configPrefix, FilterConfig filterConfig) throws ServletException { + Properties props = new Properties(); + Enumeration names = filterConfig.getInitParameterNames(); + while (names.hasMoreElements()) { + String name = (String) names.nextElement(); + if (name.startsWith(configPrefix)) { + String value = filterConfig.getInitParameter(name); + props.put(name.substring(configPrefix.length()), value); + } + } + return props; + } + + /** + * Returns the full URL of the request including the query string. + *

+ * Used as a convenience method for logging purposes. + * + * @param request the request object. + * + * @return the full URL of the request including the query string. + */ + protected String getRequestURL(HttpServletRequest request) { + StringBuffer sb = request.getRequestURL(); + if (request.getQueryString() != null) { + sb.append("?").append(request.getQueryString()); + } + return sb.toString(); + } + + /** + * Returns the {@link AuthenticationToken} for the request. + *

+ * It looks at the received HTTP cookies and extracts the value of the {@link AuthenticatedURL#AUTH_COOKIE} + * if present. It verifies the signature and if correct it creates the {@link AuthenticationToken} and returns + * it. + *

+ * If this method returns null the filter will invoke the configured {@link AuthenticationHandler} + * to perform user authentication. + * + * @param request request object. + * + * @return the Authentication token if the request is authenticated, null otherwise. + * + * @throws IOException thrown if an IO error occurred. + * @throws AuthenticationException thrown if the token is invalid or if it has expired. + */ + protected AuthenticationToken getToken(HttpServletRequest request) throws IOException, AuthenticationException { + AuthenticationToken token = null; + String tokenStr = null; + Cookie[] cookies = request.getCookies(); + if (cookies != null) { + for (Cookie cookie : cookies) { + if (cookie.getName().equals(AuthenticatedURL.AUTH_COOKIE)) { + tokenStr = cookie.getValue(); + if (tokenStr.isEmpty()) { + throw new AuthenticationException("Unauthorized access"); + } + try { + tokenStr = signer.verifyAndExtract(tokenStr); + } catch (SignerException ex) { + throw new AuthenticationException(ex); + } + break; + } + } + } + if (tokenStr != null) { + token = AuthenticationToken.parse(tokenStr); + boolean match = verifyTokenType(getAuthenticationHandler(), token); + if (!match) { + throw new AuthenticationException("Invalid AuthenticationToken type"); + } + if (token.isExpired()) { + throw new AuthenticationException("AuthenticationToken expired"); + } + } + return token; + } + + /** + * This method verifies if the specified token type matches one of the the + * token types supported by a specified {@link AuthenticationHandler}. This + * method is specifically designed to work with + * {@link CompositeAuthenticationHandler} implementation which supports + * multiple authentication schemes while the {@link AuthenticationHandler} + * interface supports a single type via + * {@linkplain AuthenticationHandler#getType()} method. + * + * @param handler The authentication handler whose supported token types + * should be used for verification. + * @param token The token whose type needs to be verified. + * @return true If the token type matches one of the supported token types + * false Otherwise + */ + protected boolean verifyTokenType(AuthenticationHandler handler, + AuthenticationToken token) { + if(!(handler instanceof CompositeAuthenticationHandler)) { + return handler.getType().equals(token.getType()); + } + boolean match = false; + Collection tokenTypes = + ((CompositeAuthenticationHandler) handler).getTokenTypes(); + for (String tokenType : tokenTypes) { + if (tokenType.equals(token.getType())) { + match = true; + break; + } + } + return match; + } + + /** + * If the request has a valid authentication token it allows the request to continue to the target resource, + * otherwise it triggers an authentication sequence using the configured {@link AuthenticationHandler}. + * + * @param request the request object. + * @param response the response object. + * @param filterChain the filter chain object. + * + * @throws IOException thrown if an IO error occurred. + * @throws ServletException thrown if a processing error occurred. + */ + @Override + public void doFilter(ServletRequest request, + ServletResponse response, + FilterChain filterChain) + throws IOException, ServletException { + boolean unauthorizedResponse = true; + int errCode = HttpServletResponse.SC_UNAUTHORIZED; + AuthenticationException authenticationEx = null; + HttpServletRequest httpRequest = (HttpServletRequest) request; + HttpServletResponse httpResponse = (HttpServletResponse) response; + boolean isHttps = "https".equals(httpRequest.getScheme()); + try { + boolean newToken = false; + AuthenticationToken token; + try { + token = getToken(httpRequest); + if (LOG.isDebugEnabled()) { + LOG.debug("Got token {} from httpRequest {}", token, + getRequestURL(httpRequest)); + } + } + catch (AuthenticationException ex) { + LOG.warn("AuthenticationToken ignored: " + ex.getMessage()); + // will be sent back in a 401 unless filter authenticates + authenticationEx = ex; + token = null; + } + if (authHandler.managementOperation(token, httpRequest, httpResponse)) { + if (token == null) { + if (LOG.isDebugEnabled()) { + LOG.debug("Request [{}] triggering authentication. handler: {}", + getRequestURL(httpRequest), authHandler.getClass()); + } + token = authHandler.authenticate(httpRequest, httpResponse); + if (token != null && token != AuthenticationToken.ANONYMOUS) { + if (token.getMaxInactives() > 0) { + token.setMaxInactives(System.currentTimeMillis() + + getMaxInactiveInterval() * 1000); + } + if (token.getExpires() != 0) { + token.setExpires(System.currentTimeMillis() + + getValidity() * 1000); + } + } + newToken = true; + } + if (token != null) { + unauthorizedResponse = false; + if (LOG.isDebugEnabled()) { + LOG.debug("Request [{}] user [{}] authenticated", + getRequestURL(httpRequest), token.getUserName()); + } + final AuthenticationToken authToken = token; + httpRequest = new HttpServletRequestWrapper(httpRequest) { + + @Override + public String getAuthType() { + return authToken.getType(); + } + + @Override + public String getRemoteUser() { + return authToken.getUserName(); + } + + @Override + public Principal getUserPrincipal() { + return (authToken != AuthenticationToken.ANONYMOUS) ? + authToken : null; + } + }; + + // If cookie persistence is configured to false, + // it means the cookie will be a session cookie. + // If the token is an old one, renew the its maxInactiveInterval. + if (!newToken && !isCookiePersistent() + && getMaxInactiveInterval() > 0) { + token.setMaxInactives(System.currentTimeMillis() + + getMaxInactiveInterval() * 1000); + token.setExpires(token.getExpires()); + newToken = true; + } + if (newToken && !token.isExpired() + && token != AuthenticationToken.ANONYMOUS) { + String signedToken = signer.sign(token.toString()); + createAuthCookie(httpResponse, signedToken, getCookieDomain(), + getCookiePath(), token.getExpires(), + isCookiePersistent(), isHttps); + } + doFilter(filterChain, httpRequest, httpResponse); + } + } else { + if (LOG.isDebugEnabled()) { + LOG.debug("managementOperation returned false for request {}." + + " token: {}", getRequestURL(httpRequest), token); + } + unauthorizedResponse = false; + } + } catch (AuthenticationException ex) { + // exception from the filter itself is fatal + errCode = HttpServletResponse.SC_FORBIDDEN; + authenticationEx = ex; + if (LOG.isDebugEnabled()) { + LOG.debug("Authentication exception: " + ex.getMessage(), ex); + } else { + LOG.warn("Authentication exception: " + ex.getMessage()); + } + } + if (unauthorizedResponse) { + if (!httpResponse.isCommitted()) { + createAuthCookie(httpResponse, "", getCookieDomain(), + getCookiePath(), 0, isCookiePersistent(), isHttps); + // If response code is 401. Then WWW-Authenticate Header should be + // present.. reset to 403 if not found.. + if ((errCode == HttpServletResponse.SC_UNAUTHORIZED) + && (!httpResponse.containsHeader( + KerberosAuthenticator.WWW_AUTHENTICATE) + && !httpResponse.containsHeader( + KerberosAuthenticator.WWW_AUTHENTICATE.toLowerCase()))) { + errCode = HttpServletResponse.SC_FORBIDDEN; + } + // After Jetty 9.4.21, sendError() no longer allows a custom message. + // use setStatus() to set a custom message. + String reason; + if (authenticationEx == null) { + reason = "Authentication required"; + } else { + reason = authenticationEx.getMessage(); + } + + httpResponse.setStatus(errCode, reason); + httpResponse.sendError(errCode, reason); + } + } + } + + /** + * Delegates call to the servlet filter chain. Sub-classes my override this + * method to perform pre and post tasks. + * + * @param filterChain the filter chain object. + * @param request the request object. + * @param response the response object. + * + * @throws IOException thrown if an IO error occurred. + * @throws ServletException thrown if a processing error occurred. + */ + protected void doFilter(FilterChain filterChain, HttpServletRequest request, + HttpServletResponse response) throws IOException, ServletException { + filterChain.doFilter(request, response); + } + + /** + * Creates the Hadoop authentication HTTP cookie. + * + * @param resp the response object. + * @param token authentication token for the cookie. + * @param domain the cookie domain. + * @param path the cookie path. + * @param expires UNIX timestamp that indicates the expire date of the + * cookie. It has no effect if its value < 0. + * @param isSecure is the cookie secure? + * @param isCookiePersistent whether the cookie is persistent or not. + * + * XXX the following code duplicate some logic in Jetty / Servlet API, + * because of the fact that Hadoop is stuck at servlet 2.5 and jetty 6 + * right now. + */ + public static void createAuthCookie(HttpServletResponse resp, String token, + String domain, String path, long expires, + boolean isCookiePersistent, + boolean isSecure) { + StringBuilder sb = new StringBuilder(AuthenticatedURL.AUTH_COOKIE) + .append("="); + if (token != null && token.length() > 0) { + sb.append("\"").append(token).append("\""); + } + + if (path != null) { + sb.append("; Path=").append(path); + } + + if (domain != null) { + sb.append("; Domain=").append(domain); + } + + if (expires >= 0 && isCookiePersistent) { + Date date = new Date(expires); + SimpleDateFormat df = new SimpleDateFormat("EEE, " + + "dd-MMM-yyyy HH:mm:ss zzz", Locale.US); + df.setTimeZone(TimeZone.getTimeZone("GMT")); + sb.append("; Expires=").append(df.format(date)); + } + + if (isSecure) { + sb.append("; Secure"); + } + + sb.append("; HttpOnly"); + resp.addHeader("Set-Cookie", sb.toString()); + } +} diff --git a/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/AuthenticationHandler.java b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/AuthenticationHandler.java new file mode 100644 index 000000000000..ca0edac772f0 --- /dev/null +++ b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/AuthenticationHandler.java @@ -0,0 +1,119 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. See accompanying LICENSE file. + */ +package org.apache.hadoop.hbase.security.authentication.server; + +import org.apache.hadoop.security.authentication.client.AuthenticationException; +import org.apache.yetus.audience.InterfaceAudience; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.Properties; + +/** + * Interface for server authentication mechanisms. + * The {@link AuthenticationFilter} manages the lifecycle of the authentication handler. + * Implementations must be thread-safe as one instance is initialized and used for all requests. + */ +@InterfaceAudience.Private +public interface AuthenticationHandler { + + String WWW_AUTHENTICATE = HttpConstants.WWW_AUTHENTICATE_HEADER; + + /** + * Returns the authentication type of the authentication handler. + * This should be a name that uniquely identifies the authentication type. + * For example 'simple' or 'kerberos'. + * + * @return the authentication type of the authentication handler. + */ + public String getType(); + + /** + * Initializes the authentication handler instance. + *

+ * This method is invoked by the {@link AuthenticationFilter#init} method. + * + * @param config configuration properties to initialize the handler. + * + * @throws ServletException thrown if the handler could not be initialized. + */ + public void init(Properties config) throws ServletException; + + /** + * Destroys the authentication handler instance. + *

+ * This method is invoked by the {@link AuthenticationFilter#destroy} method. + */ + public void destroy(); + + /** + * Performs an authentication management operation. + *

+ * This is useful for handling operations like get/renew/cancel + * delegation tokens which are being handled as operations of the + * service end-point. + *

+ * If the method returns TRUE the request will continue normal + * processing, this means the method has not produced any HTTP response. + *

+ * If the method returns FALSE the request will end, this means + * the method has produced the corresponding HTTP response. + * + * @param token the authentication token if any, otherwise NULL. + * @param request the HTTP client request. + * @param response the HTTP client response. + * @return TRUE if the request should be processed as a regular + * request, + * FALSE otherwise. + * + * @throws IOException thrown if an IO error occurred. + * @throws AuthenticationException thrown if an Authentication error occurred. + */ + public boolean managementOperation(AuthenticationToken token, + HttpServletRequest request, + HttpServletResponse response) + throws IOException, AuthenticationException; + + /** + * Performs an authentication step for the given HTTP client request. + *

+ * This method is invoked by the {@link AuthenticationFilter} only if the HTTP client request is + * not yet authenticated. + *

+ * Depending upon the authentication mechanism being implemented, a particular HTTP client may + * end up making a sequence of invocations before authentication is successfully established (this is + * the case of Kerberos SPNEGO). + *

+ * This method must return an {@link AuthenticationToken} only if the the HTTP client request has + * been successfully and fully authenticated. + *

+ * If the HTTP client request has not been completely authenticated, this method must take over + * the corresponding HTTP response and it must return null. + * + * @param request the HTTP client request. + * @param response the HTTP client response. + * + * @return an {@link AuthenticationToken} if the HTTP client request has been authenticated, + * null otherwise (in this case it must take care of the response). + * + * @throws IOException thrown if an IO error occurred. + * @throws AuthenticationException thrown if an Authentication error occurred. + */ + public AuthenticationToken authenticate(HttpServletRequest request, HttpServletResponse response) + throws IOException, AuthenticationException; + +} diff --git a/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/AuthenticationHandlerUtil.java b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/AuthenticationHandlerUtil.java new file mode 100644 index 000000000000..b234e2663972 --- /dev/null +++ b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/AuthenticationHandlerUtil.java @@ -0,0 +1,114 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. See accompanying LICENSE file. + */ + +package org.apache.hadoop.hbase.security.authentication.server; + +import org.apache.yetus.audience.InterfaceAudience; + +import static org.apache.hadoop.hbase.security.authentication.server.HttpConstants.NEGOTIATE; +import static org.apache.hadoop.hbase.security.authentication.server.HttpConstants.BASIC; +import static org.apache.hadoop.hbase.security.authentication.server.HttpConstants.DIGEST; + +import java.util.Locale; + +/** + * This is a utility class designed to provide functionality related to + * {@link AuthenticationHandler}. + */ +@InterfaceAudience.Private +public final class AuthenticationHandlerUtil { + + /** + * This class should only contain the static utility methods. Hence it is not + * intended to be instantiated. + */ + private AuthenticationHandlerUtil() { + } + + /** + * This method provides an instance of {@link AuthenticationHandler} based on + * specified authHandlerName. + * + * @param authHandler The short-name (or fully qualified class name) of the + * authentication handler. + * @return an instance of AuthenticationHandler implementation. + */ + public static String getAuthenticationHandlerClassName(String authHandler) { + if (authHandler == null) { + throw new NullPointerException(); + } + String handlerName = authHandler.toLowerCase(Locale.ENGLISH); + + String authHandlerClassName = null; + + if (handlerName.equals(PseudoAuthenticationHandler.TYPE)) { + authHandlerClassName = PseudoAuthenticationHandler.class.getName(); + } else if (handlerName.equals(KerberosAuthenticationHandler.TYPE)) { + authHandlerClassName = KerberosAuthenticationHandler.class.getName(); + } else if (handlerName.equals(LdapAuthenticationHandler.TYPE)) { + authHandlerClassName = LdapAuthenticationHandler.class.getName(); + } else if (handlerName.equals(MultiSchemeAuthenticationHandler.TYPE)) { + authHandlerClassName = MultiSchemeAuthenticationHandler.class.getName(); + } else { + authHandlerClassName = authHandler; + } + + return authHandlerClassName; + } + + /** + * This method checks if the specified HTTP authentication scheme + * value is valid. + * + * @param scheme HTTP authentication scheme to be checked + * @return Canonical representation of HTTP authentication scheme + * @throws IllegalArgumentException In case the specified value is not a valid + * HTTP authentication scheme. + */ + public static String checkAuthScheme(String scheme) { + if (BASIC.equalsIgnoreCase(scheme)) { + return BASIC; + } else if (NEGOTIATE.equalsIgnoreCase(scheme)) { + return NEGOTIATE; + } else if (DIGEST.equalsIgnoreCase(scheme)) { + return DIGEST; + } + throw new IllegalArgumentException(String.format( + "Unsupported HTTP authentication scheme %s ." + + " Supported schemes are [%s, %s, %s]", scheme, BASIC, NEGOTIATE, + DIGEST)); + } + + /** + * This method checks if the specified authToken belongs to the + * specified HTTP authentication scheme. + * + * @param scheme HTTP authentication scheme to be checked + * @param auth Authentication header value which is to be compared with the + * authentication scheme. + * @return true If the authentication header value corresponds to the + * specified authentication scheme false Otherwise. + */ + public static boolean matchAuthScheme(String scheme, String auth) { + if (scheme == null) { + throw new NullPointerException(); + } + scheme = scheme.trim(); + if (auth == null) { + throw new NullPointerException(); + } + auth = auth.trim(); + return auth.regionMatches(true, 0, scheme, 0, scheme.length()); + } +} diff --git a/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/AuthenticationToken.java b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/AuthenticationToken.java new file mode 100644 index 000000000000..176887da09cd --- /dev/null +++ b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/AuthenticationToken.java @@ -0,0 +1,109 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. See accompanying LICENSE file. + */ +package org.apache.hadoop.hbase.security.authentication.server; + +import org.apache.hadoop.security.authentication.client.AuthenticationException; +import org.apache.hadoop.security.authentication.util.AuthToken; +import org.apache.yetus.audience.InterfaceAudience; + +import java.security.Principal; + +import javax.servlet.http.HttpServletRequest; + +/** + * The {@link AuthenticationToken} contains information about an authenticated + * HTTP client and doubles as the {@link Principal} to be returned by + * authenticated {@link HttpServletRequest}s + *

+ * The token can be serialized/deserialized to and from a string as it is sent + * and received in HTTP client responses and requests as a HTTP cookie (this is + * done by the {@link AuthenticationFilter}). + */ +@InterfaceAudience.Private +public class AuthenticationToken extends AuthToken { + + /** + * Constant that identifies an anonymous request. + */ + public static final AuthenticationToken ANONYMOUS = new AuthenticationToken(); + + private AuthenticationToken() { + super(); + } + + private AuthenticationToken(AuthToken token) { + super(token.getUserName(), token.getName(), token.getType()); + setMaxInactives(token.getMaxInactives()); + setExpires(token.getExpires()); + } + + /** + * Creates an authentication token. + * + * @param userName user name. + * @param principal principal (commonly matches the user name, with Kerberos is the full/long principal + * name while the userName is the short name). + * @param type the authentication mechanism name. + * (System.currentTimeMillis() + validityPeriod). + */ + public AuthenticationToken(String userName, String principal, String type) { + super(userName, principal, type); + } + + /** + * Sets the max inactive time of the token. + * + * @param maxInactives inactive time of the token in milliseconds + * since the epoch. + */ + public void setMaxInactives(long maxInactives) { + if (this != AuthenticationToken.ANONYMOUS) { + super.setMaxInactives(maxInactives); + } + } + + /** + * Sets the expiration of the token. + * + * @param expires expiration time of the token in milliseconds since the epoch. + */ + public void setExpires(long expires) { + if (this != AuthenticationToken.ANONYMOUS) { + super.setExpires(expires); + } + } + + /** + * Returns true if the token has expired. + * + * @return true if the token has expired. + */ + public boolean isExpired() { + return super.isExpired(); + } + + /** + * Parses a string into an authentication token. + * + * @param tokenStr string representation of a token. + * + * @return the parsed authentication token. + * + * @throws AuthenticationException thrown if the string representation could not be parsed into + * an authentication token. + */ + public static AuthenticationToken parse(String tokenStr) throws AuthenticationException { + return new AuthenticationToken(AuthToken.parse(tokenStr)); + } +} diff --git a/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/CompositeAuthenticationHandler.java b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/CompositeAuthenticationHandler.java new file mode 100644 index 000000000000..00f7444f2914 --- /dev/null +++ b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/CompositeAuthenticationHandler.java @@ -0,0 +1,32 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. See accompanying LICENSE file. + */ +package org.apache.hadoop.hbase.security.authentication.server; + +import org.apache.yetus.audience.InterfaceAudience; +import java.util.Collection; + +/** + * Interface to support multiple authentication mechanisms simultaneously. + * + */ +@InterfaceAudience.Private +public interface CompositeAuthenticationHandler extends AuthenticationHandler { + /** + * This method returns the token types supported by this authentication + * handler. + * + * @return the token types supported by this authentication handler. + */ + Collection getTokenTypes(); +} diff --git a/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/HttpConstants.java b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/HttpConstants.java new file mode 100644 index 000000000000..516186cd7d58 --- /dev/null +++ b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/HttpConstants.java @@ -0,0 +1,58 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. See accompanying LICENSE file. + */ +package org.apache.hadoop.hbase.security.authentication.server; + +import org.apache.yetus.audience.InterfaceAudience; + +/** + * This class defines constants used for HTTP protocol entities (such as + * headers, methods and their values). + */ +@InterfaceAudience.Private +public final class HttpConstants { + + /** + * This class defines the HTTP protocol constants. Hence it is not intended + * to be instantiated. + */ + private HttpConstants() { + } + + /** + * HTTP header used by the server endpoint during an authentication sequence. + */ + public static final String WWW_AUTHENTICATE_HEADER = "WWW-Authenticate"; + + /** + * HTTP header used by the client endpoint during an authentication sequence. + */ + public static final String AUTHORIZATION_HEADER = "Authorization"; + + /** + * HTTP header prefix used by the SPNEGO client/server endpoints during an + * authentication sequence. + */ + public static final String NEGOTIATE = "Negotiate"; + + /** + * HTTP header prefix used during the Basic authentication sequence. + */ + public static final String BASIC = "Basic"; + + /** + * HTTP header prefix used during the Basic authentication sequence. + */ + public static final String DIGEST = "Digest"; + +} diff --git a/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/JWTRedirectAuthenticationHandler.java b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/JWTRedirectAuthenticationHandler.java new file mode 100644 index 000000000000..550ecbc1d085 --- /dev/null +++ b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/JWTRedirectAuthenticationHandler.java @@ -0,0 +1,357 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. See accompanying LICENSE file. + */ +package org.apache.hadoop.hbase.security.authentication.server; + +import java.io.IOException; + +import javax.servlet.http.Cookie; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Properties; +import java.text.ParseException; + +import java.security.interfaces.RSAPublicKey; + +import org.apache.hadoop.security.authentication.client.AuthenticationException; +import org.apache.hadoop.hbase.security.authentication.util.CertificateUtil; +import org.apache.yetus.audience.InterfaceAudience; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.nimbusds.jwt.SignedJWT; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSObject; +import com.nimbusds.jose.JWSVerifier; +import com.nimbusds.jose.crypto.RSASSAVerifier; + +/** + * The {@link JWTRedirectAuthenticationHandler} extends + * AltKerberosAuthenticationHandler to add WebSSO behavior for UIs. The expected + * SSO token is a JsonWebToken (JWT). The supported algorithm is RS256 which + * uses PKI between the token issuer and consumer. The flow requires a redirect + * to a configured authentication server URL and a subsequent request with the + * expected JWT token. This token is cryptographically verified and validated. + * The user identity is then extracted from the token and used to create an + * AuthenticationToken - as expected by the AuthenticationFilter. + * + *

+ * The supported configuration properties are: + *

+ * + */ +@InterfaceAudience.Private +public class JWTRedirectAuthenticationHandler extends + AltKerberosAuthenticationHandler { + private static Logger LOG = LoggerFactory + .getLogger(JWTRedirectAuthenticationHandler.class); + + public static final String AUTHENTICATION_PROVIDER_URL = + "authentication.provider.url"; + public static final String PUBLIC_KEY_PEM = "public.key.pem"; + public static final String EXPECTED_JWT_AUDIENCES = "expected.jwt.audiences"; + public static final String JWT_COOKIE_NAME = "jwt.cookie.name"; + private static final String ORIGINAL_URL_QUERY_PARAM = "originalUrl="; + private String authenticationProviderUrl = null; + private RSAPublicKey publicKey = null; + private List audiences = null; + private String cookieName = "hadoop-jwt"; + + /** + * Primarily for testing, this provides a way to set the publicKey for + * signature verification without needing to get a PEM encoded value. + * + * @param pk publicKey for the token signtature verification + */ + public void setPublicKey(RSAPublicKey pk) { + publicKey = pk; + } + + /** + * Initializes the authentication handler instance. + *

+ * This method is invoked by the {@link AuthenticationFilter#init} method. + *

+ * @param config + * configuration properties to initialize the handler. + * + * @throws ServletException + * thrown if the handler could not be initialized. + */ + @Override + public void init(Properties config) throws ServletException { + super.init(config); + // setup the URL to redirect to for authentication + authenticationProviderUrl = config + .getProperty(AUTHENTICATION_PROVIDER_URL); + if (authenticationProviderUrl == null) { + throw new ServletException( + "Authentication provider URL must not be null - configure: " + + AUTHENTICATION_PROVIDER_URL); + } + + // setup the public key of the token issuer for verification + if (publicKey == null) { + String pemPublicKey = config.getProperty(PUBLIC_KEY_PEM); + if (pemPublicKey == null) { + throw new ServletException( + "Public key for signature validation must be provisioned."); + } + publicKey = CertificateUtil.parseRSAPublicKey(pemPublicKey); + } + // setup the list of valid audiences for token validation + String auds = config.getProperty(EXPECTED_JWT_AUDIENCES); + if (auds != null) { + // parse into the list + String[] audArray = auds.split(","); + audiences = new ArrayList(); + for (String a : audArray) { + audiences.add(a); + } + } + + // setup custom cookie name if configured + String customCookieName = config.getProperty(JWT_COOKIE_NAME); + if (customCookieName != null) { + cookieName = customCookieName; + } + } + + @Override + public AuthenticationToken alternateAuthenticate(HttpServletRequest request, + HttpServletResponse response) throws IOException, + AuthenticationException { + AuthenticationToken token = null; + + String serializedJWT = null; + HttpServletRequest req = (HttpServletRequest) request; + serializedJWT = getJWTFromCookie(req); + if (serializedJWT == null) { + String loginURL = constructLoginURL(request); + LOG.info("sending redirect to: " + loginURL); + ((HttpServletResponse) response).sendRedirect(loginURL); + } else { + String userName = null; + SignedJWT jwtToken = null; + boolean valid = false; + try { + jwtToken = SignedJWT.parse(serializedJWT); + valid = validateToken(jwtToken); + if (valid) { + userName = jwtToken.getJWTClaimsSet().getSubject(); + LOG.info("USERNAME: " + userName); + } else { + LOG.warn("jwtToken failed validation: " + jwtToken.serialize()); + } + } catch(ParseException pe) { + // unable to parse the token let's try and get another one + LOG.warn("Unable to parse the JWT token", pe); + } + if (valid) { + LOG.debug("Issuing AuthenticationToken for user."); + token = new AuthenticationToken(userName, userName, getType()); + } else { + String loginURL = constructLoginURL(request); + LOG.info("token validation failed - sending redirect to: " + loginURL); + ((HttpServletResponse) response).sendRedirect(loginURL); + } + } + return token; + } + + /** + * Encapsulate the acquisition of the JWT token from HTTP cookies within the + * request. + * + * @param req servlet request to get the JWT token from + * @return serialized JWT token + */ + protected String getJWTFromCookie(HttpServletRequest req) { + String serializedJWT = null; + Cookie[] cookies = req.getCookies(); + if (cookies != null) { + for (Cookie cookie : cookies) { + if (cookieName.equals(cookie.getName())) { + LOG.info(cookieName + + " cookie has been found and is being processed"); + serializedJWT = cookie.getValue(); + break; + } + } + } + return serializedJWT; + } + + /** + * Create the URL to be used for authentication of the user in the absence of + * a JWT token within the incoming request. + * + * @param request for getting the original request URL + * @return url to use as login url for redirect + */ + String constructLoginURL(HttpServletRequest request) { + String delimiter = "?"; + if (authenticationProviderUrl.contains("?")) { + delimiter = "&"; + } + String loginURL = authenticationProviderUrl + delimiter + + ORIGINAL_URL_QUERY_PARAM + + request.getRequestURL().toString() + getOriginalQueryString(request); + return loginURL; + } + + private String getOriginalQueryString(HttpServletRequest request) { + String originalQueryString = request.getQueryString(); + return (originalQueryString == null) ? "" : "?" + originalQueryString; + } + + /** + * This method provides a single method for validating the JWT for use in + * request processing. It provides for the override of specific aspects of + * this implementation through submethods used within but also allows for the + * override of the entire token validation algorithm. + * + * @param jwtToken the token to validate + * @return true if valid + */ + protected boolean validateToken(SignedJWT jwtToken) { + boolean sigValid = validateSignature(jwtToken); + if (!sigValid) { + LOG.warn("Signature could not be verified"); + } + boolean audValid = validateAudiences(jwtToken); + if (!audValid) { + LOG.warn("Audience validation failed."); + } + boolean expValid = validateExpiration(jwtToken); + if (!expValid) { + LOG.info("Expiration validation failed."); + } + + return sigValid && audValid && expValid; + } + + /** + * Verify the signature of the JWT token in this method. This method depends + * on the public key that was established during init based upon the + * provisioned public key. Override this method in subclasses in order to + * customize the signature verification behavior. + * + * @param jwtToken the token that contains the signature to be validated + * @return valid true if signature verifies successfully; false otherwise + */ + protected boolean validateSignature(SignedJWT jwtToken) { + boolean valid = false; + if (JWSObject.State.SIGNED == jwtToken.getState()) { + LOG.debug("JWT token is in a SIGNED state"); + if (jwtToken.getSignature() != null) { + LOG.debug("JWT token signature is not null"); + try { + JWSVerifier verifier = new RSASSAVerifier(publicKey); + if (jwtToken.verify(verifier)) { + valid = true; + LOG.debug("JWT token has been successfully verified"); + } else { + LOG.warn("JWT signature verification failed."); + } + } catch (JOSEException je) { + LOG.warn("Error while validating signature", je); + } + } + } + return valid; + } + + /** + * Validate whether any of the accepted audience claims is present in the + * issued token claims list for audience. Override this method in subclasses + * in order to customize the audience validation behavior. + * + * @param jwtToken + * the JWT token where the allowed audiences will be found + * @return true if an expected audience is present, otherwise false + */ + protected boolean validateAudiences(SignedJWT jwtToken) { + boolean valid = false; + try { + List tokenAudienceList = jwtToken.getJWTClaimsSet() + .getAudience(); + // if there were no expected audiences configured then just + // consider any audience acceptable + if (audiences == null) { + valid = true; + } else { + // if any of the configured audiences is found then consider it + // acceptable + boolean found = false; + for (String aud : tokenAudienceList) { + if (audiences.contains(aud)) { + LOG.debug("JWT token audience has been successfully validated"); + valid = true; + break; + } + } + if (!valid) { + LOG.warn("JWT audience validation failed."); + } + } + } catch (ParseException pe) { + LOG.warn("Unable to parse the JWT token.", pe); + } + return valid; + } + + /** + * Validate that the expiration time of the JWT token has not been violated. + * If it has then throw an AuthenticationException. Override this method in + * subclasses in order to customize the expiration validation behavior. + * + * @param jwtToken the token that contains the expiration date to validate + * @return valid true if the token has not expired; false otherwise + */ + protected boolean validateExpiration(SignedJWT jwtToken) { + boolean valid = false; + try { + Date expires = jwtToken.getJWTClaimsSet().getExpirationTime(); + if (expires == null || new Date().before(expires)) { + LOG.debug("JWT token expiration date has been " + + "successfully validated"); + valid = true; + } else { + LOG.warn("JWT expiration date validation failed."); + } + } catch (ParseException pe) { + LOG.warn("JWT expiration date validation failed.", pe); + } + return valid; + } +} diff --git a/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/KerberosAuthenticationHandler.java b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/KerberosAuthenticationHandler.java new file mode 100644 index 000000000000..a9e8b168456d --- /dev/null +++ b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/KerberosAuthenticationHandler.java @@ -0,0 +1,405 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. See accompanying LICENSE file. + */ +package org.apache.hadoop.hbase.security.authentication.server; + +import org.apache.hadoop.security.authentication.client.AuthenticationException; +import org.apache.hadoop.security.authentication.client.KerberosAuthenticator; +import org.apache.commons.codec.binary.Base64; +import org.apache.hadoop.security.authentication.util.KerberosName; +import org.apache.hadoop.security.authentication.util.KerberosUtil; +import org.apache.yetus.audience.InterfaceAudience; +import org.ietf.jgss.GSSException; +import org.ietf.jgss.GSSContext; +import org.ietf.jgss.GSSCredential; +import org.ietf.jgss.GSSManager; +import org.ietf.jgss.Oid; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.security.auth.Subject; +import javax.security.auth.kerberos.KerberosPrincipal; +import javax.security.auth.kerberos.KeyTab; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import java.io.File; +import java.io.IOException; +import java.security.Principal; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.Collection; +import java.util.HashSet; +import java.util.Properties; +import java.util.Set; +import java.util.regex.Pattern; + +/** + * The {@link KerberosAuthenticationHandler} implements the Kerberos SPNEGO + * authentication mechanism for HTTP. + *

+ * The supported configuration properties are: + *

    + *
  • kerberos.principal: the Kerberos principal to used by the server. As + * stated by the Kerberos SPNEGO specification, it should be + * HTTP/${HOSTNAME}@{REALM}. The realm can be omitted from the + * principal as the JDK GSS libraries will use the realm name of the configured + * default realm. + * It does not have a default value.
  • + *
  • kerberos.keytab: the keytab file containing the credentials for the + * Kerberos principal. + * It does not have a default value.
  • + *
  • kerberos.name.rules: kerberos names rules to resolve principal names, see + * {@link KerberosName#setRules(String)}
  • + *
+ */ +@InterfaceAudience.Private +public class KerberosAuthenticationHandler implements AuthenticationHandler { + public static final Logger LOG = LoggerFactory.getLogger( + KerberosAuthenticationHandler.class); + + /** + * Constant that identifies the authentication mechanism. + */ + public static final String TYPE = "kerberos"; + + /** + * Constant for the configuration property that indicates the kerberos + * principal. + */ + public static final String PRINCIPAL = TYPE + ".principal"; + + /** + * Constant for the configuration property that indicates the keytab + * file path. + */ + public static final String KEYTAB = TYPE + ".keytab"; + + /** + * Constant for the configuration property that indicates the Kerberos name + * rules for the Kerberos principals. + */ + public static final String NAME_RULES = TYPE + ".name.rules"; + + /** + * Constant for the configuration property that indicates how auth_to_local + * rules are evaluated. + */ + public static final String RULE_MECHANISM = TYPE + ".name.rules.mechanism"; + + /** + * Constant for the list of endpoints that skips Kerberos authentication. + */ + static final String ENDPOINT_WHITELIST = TYPE + ".endpoint.whitelist"; + private static final Pattern ENDPOINT_PATTERN = Pattern.compile("^/[\\w]+"); + + private String type; + private String keytab; + private GSSManager gssManager; + private Subject serverSubject = new Subject(); + private final Collection whitelist = new HashSet<>(); + + /** + * Creates a Kerberos SPNEGO authentication handler with the default + * auth-token type, kerberos. + */ + public KerberosAuthenticationHandler() { + this(TYPE); + } + + /** + * Creates a Kerberos SPNEGO authentication handler with a custom auth-token + * type. + * + * @param type auth-token type. + */ + public KerberosAuthenticationHandler(String type) { + this.type = type; + } + + /** + * Initializes the authentication handler instance. + *

+ * It creates a Kerberos context using the principal and keytab specified in + * the configuration. + *

+ * This method is invoked by the {@link AuthenticationFilter#init} method. + * + * @param config configuration properties to initialize the handler. + * + * @throws ServletException thrown if the handler could not be initialized. + */ + @Override + public void init(Properties config) throws ServletException { + try { + String principal = config.getProperty(PRINCIPAL); + if (principal == null || principal.trim().length() == 0) { + throw new ServletException("Principal not defined in configuration"); + } + keytab = config.getProperty(KEYTAB, keytab); + if (keytab == null || keytab.trim().length() == 0) { + throw new ServletException("Keytab not defined in configuration"); + } + File keytabFile = new File(keytab); + if (!keytabFile.exists()) { + throw new ServletException("Keytab does not exist: " + keytab); + } + + // use all SPNEGO principals in the keytab if a principal isn't + // specifically configured + final String[] spnegoPrincipals; + if (principal.equals("*")) { + spnegoPrincipals = KerberosUtil.getPrincipalNames( + keytab, Pattern.compile("HTTP/.*")); + if (spnegoPrincipals.length == 0) { + throw new ServletException("Principals do not exist in the keytab"); + } + } else { + spnegoPrincipals = new String[]{principal}; + } + KeyTab keytabInstance = KeyTab.getInstance(keytabFile); + serverSubject.getPrivateCredentials().add(keytabInstance); + for (String spnegoPrincipal : spnegoPrincipals) { + Principal krbPrincipal = new KerberosPrincipal(spnegoPrincipal); + LOG.info("Using keytab {}, for principal {}", + keytab, krbPrincipal); + serverSubject.getPrincipals().add(krbPrincipal); + } + String nameRules = config.getProperty(NAME_RULES, null); + if (nameRules != null) { + KerberosName.setRules(nameRules); + } + String ruleMechanism = config.getProperty(RULE_MECHANISM, null); + if (ruleMechanism != null) { + KerberosName.setRuleMechanism(ruleMechanism); + } + + final String whitelistStr = config.getProperty(ENDPOINT_WHITELIST, null); + if (whitelistStr != null) { + final String[] strs = whitelistStr.trim().split("\\s*[,\n]\\s*"); + for (String s: strs) { + if (s.isEmpty()) continue; + if (ENDPOINT_PATTERN.matcher(s).matches()) { + whitelist.add(s); + } else { + throw new ServletException( + "The element of the whitelist: " + s + " must start with '/'" + + " and must not contain special characters afterwards"); + } + } + } + + try { + gssManager = Subject.doAs(serverSubject, + new PrivilegedExceptionAction() { + @Override + public GSSManager run() throws Exception { + return GSSManager.getInstance(); + } + }); + } catch (PrivilegedActionException ex) { + throw ex.getException(); + } + } catch (Exception ex) { + throw new ServletException(ex); + } + } + + /** + * Releases any resources initialized by the authentication handler. + *

+ * It destroys the Kerberos context. + */ + @Override + public void destroy() { + keytab = null; + serverSubject = null; + } + + /** + * Returns the authentication type of the authentication handler, 'kerberos'. + *

+ * + * @return the authentication type of the authentication handler, 'kerberos'. + */ + @Override + public String getType() { + return type; + } + + /** + * Returns the Kerberos principals used by the authentication handler. + * + * @return the Kerberos principals used by the authentication handler. + */ + protected Set getPrincipals() { + return serverSubject.getPrincipals(KerberosPrincipal.class); + } + + /** + * Returns the keytab used by the authentication handler. + * + * @return the keytab used by the authentication handler. + */ + protected String getKeytab() { + return keytab; + } + + /** + * This is an empty implementation, it always returns TRUE. + * + * + * + * @param token the authentication token if any, otherwise NULL. + * @param request the HTTP client request. + * @param response the HTTP client response. + * + * @return TRUE + * @throws IOException it is never thrown. + * @throws AuthenticationException it is never thrown. + */ + @Override + public boolean managementOperation(AuthenticationToken token, + HttpServletRequest request, + HttpServletResponse response) + throws IOException, AuthenticationException { + return true; + } + + /** + * It enforces the the Kerberos SPNEGO authentication sequence returning an + * {@link AuthenticationToken} only after the Kerberos SPNEGO sequence has + * completed successfully. + * + * @param request the HTTP client request. + * @param response the HTTP client response. + * + * @return an authentication token if the Kerberos SPNEGO sequence is complete + * and valid, null if it is in progress (in this case the handler + * handles the response to the client). + * + * @throws IOException thrown if an IO error occurred. + * @throws AuthenticationException thrown if Kerberos SPNEGO sequence failed. + */ + @Override + public AuthenticationToken authenticate(HttpServletRequest request, + final HttpServletResponse response) + throws IOException, AuthenticationException { + + // If the request servlet path is in the whitelist, + // skip Kerberos authentication and return anonymous token. + final String path = request.getServletPath(); + for(final String endpoint: whitelist) { + if (endpoint.equals(path)) { + return AuthenticationToken.ANONYMOUS; + } + } + + AuthenticationToken token = null; + String authorization = request.getHeader( + KerberosAuthenticator.AUTHORIZATION); + + if (authorization == null + || !authorization.startsWith(KerberosAuthenticator.NEGOTIATE)) { + response.setHeader(WWW_AUTHENTICATE, KerberosAuthenticator.NEGOTIATE); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + if (authorization == null) { + LOG.trace("SPNEGO starting for url: {}", request.getRequestURL()); + } else { + LOG.warn("'" + KerberosAuthenticator.AUTHORIZATION + + "' does not start with '" + + KerberosAuthenticator.NEGOTIATE + "' : {}", authorization); + } + } else { + authorization = authorization.substring( + KerberosAuthenticator.NEGOTIATE.length()).trim(); + final Base64 base64 = new Base64(0); + final byte[] clientToken = base64.decode(authorization); + try { + final String serverPrincipal = + KerberosUtil.getTokenServerName(clientToken); + if (!serverPrincipal.startsWith("HTTP/")) { + throw new IllegalArgumentException( + "Invalid server principal " + serverPrincipal + + "decoded from client request"); + } + token = Subject.doAs(serverSubject, + new PrivilegedExceptionAction() { + @Override + public AuthenticationToken run() throws Exception { + return runWithPrincipal(serverPrincipal, clientToken, + base64, response); + } + }); + } catch (PrivilegedActionException ex) { + if (ex.getException() instanceof IOException) { + throw (IOException) ex.getException(); + } else { + throw new AuthenticationException(ex.getException()); + } + } catch (Exception ex) { + throw new AuthenticationException(ex); + } + } + return token; + } + + private AuthenticationToken runWithPrincipal(String serverPrincipal, + byte[] clientToken, Base64 base64, HttpServletResponse response) throws + IOException, GSSException { + GSSContext gssContext = null; + GSSCredential gssCreds = null; + AuthenticationToken token = null; + try { + LOG.trace("SPNEGO initiated with server principal [{}]", serverPrincipal); + gssCreds = this.gssManager.createCredential( + this.gssManager.createName(serverPrincipal, + KerberosUtil.NT_GSS_KRB5_PRINCIPAL_OID), + GSSCredential.INDEFINITE_LIFETIME, + new Oid[]{ + KerberosUtil.GSS_SPNEGO_MECH_OID, + KerberosUtil.GSS_KRB5_MECH_OID }, + GSSCredential.ACCEPT_ONLY); + gssContext = this.gssManager.createContext(gssCreds); + byte[] serverToken = gssContext.acceptSecContext(clientToken, 0, + clientToken.length); + if (serverToken != null && serverToken.length > 0) { + String authenticate = base64.encodeToString(serverToken); + response.setHeader(KerberosAuthenticator.WWW_AUTHENTICATE, + KerberosAuthenticator.NEGOTIATE + " " + + authenticate); + } + if (!gssContext.isEstablished()) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + LOG.trace("SPNEGO in progress"); + } else { + String clientPrincipal = gssContext.getSrcName().toString(); + KerberosName kerberosName = new KerberosName(clientPrincipal); + String userName = kerberosName.getShortName(); + token = new AuthenticationToken(userName, clientPrincipal, getType()); + response.setStatus(HttpServletResponse.SC_OK); + LOG.trace("SPNEGO completed for client principal [{}]", + clientPrincipal); + } + } finally { + if (gssContext != null) { + gssContext.dispose(); + } + if (gssCreds != null) { + gssCreds.dispose(); + } + } + return token; + } +} diff --git a/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/LdapAuthenticationHandler.java b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/LdapAuthenticationHandler.java new file mode 100644 index 000000000000..dff8b5a09a2a --- /dev/null +++ b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/LdapAuthenticationHandler.java @@ -0,0 +1,338 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. See accompanying LICENSE file. + */ +package org.apache.hadoop.hbase.security.authentication.server; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Hashtable; +import java.util.Properties; + +import javax.naming.Context; +import javax.naming.NamingException; +import javax.naming.directory.InitialDirContext; +import javax.naming.ldap.InitialLdapContext; +import javax.naming.ldap.LdapContext; +import javax.naming.ldap.StartTlsRequest; +import javax.naming.ldap.StartTlsResponse; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLSession; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.codec.binary.Base64; +import org.apache.yetus.audience.InterfaceAudience; +import org.apache.yetus.audience.InterfaceStability; +import org.apache.hadoop.security.authentication.client.AuthenticationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link LdapAuthenticationHandler} implements the BASIC authentication + * mechanism for HTTP using LDAP back-end. + * + * The supported configuration properties are: + *

    + *
  • ldap.providerurl: The url of the LDAP server. It does not have a default + * value.
  • + *
  • ldap.basedn: the base distinguished name (DN) to be used with the LDAP + * server. This value is appended to the provided user id for authentication + * purpose. It does not have a default value.
  • + *
  • ldap.binddomain: the LDAP bind domain value to be used with the LDAP + * server. This property is optional and useful only in case of Active + * Directory server. + *
  • ldap.enablestarttls: A boolean value used to define if the LDAP server + * supports 'StartTLS' extension.
  • + *
+ */ +@InterfaceAudience.Private +@InterfaceStability.Evolving +public class LdapAuthenticationHandler implements AuthenticationHandler { + private static Logger logger = LoggerFactory + .getLogger(LdapAuthenticationHandler.class); + + /** + * Constant that identifies the authentication mechanism. + */ + public static final String TYPE = "ldap"; + + /** + * Constant that identifies the authentication mechanism to be used with the + * LDAP server. + */ + public static final String SECURITY_AUTHENTICATION = "simple"; + + /** + * Constant for the configuration property that indicates the url of the LDAP + * server. + */ + public static final String PROVIDER_URL = TYPE + ".providerurl"; + + /** + * Constant for the configuration property that indicates the base + * distinguished name (DN) to be used with the LDAP server. This value is + * appended to the provided user id for authentication purpose. + */ + public static final String BASE_DN = TYPE + ".basedn"; + + /** + * Constant for the configuration property that indicates the LDAP bind + * domain value to be used with the LDAP server. + */ + public static final String LDAP_BIND_DOMAIN = TYPE + ".binddomain"; + + /** + * Constant for the configuration property that indicates whether + * the LDAP server supports 'StartTLS' extension. + */ + public static final String ENABLE_START_TLS = TYPE + ".enablestarttls"; + + private String ldapDomain; + private String baseDN; + private String providerUrl; + private Boolean enableStartTls; + private Boolean disableHostNameVerification; + + /** + * Configure StartTLS LDAP extension for this handler. + * + * @param enableStartTls true If the StartTLS LDAP extension is to be enabled + * false otherwise + */ + public void setEnableStartTls(Boolean enableStartTls) { + this.enableStartTls = enableStartTls; + } + + /** + * Configure the Host name verification for this handler. This method is + * introduced only for unit testing and should never be used in production. + * + * @param disableHostNameVerification true to disable host-name verification + * false otherwise + */ + public void setDisableHostNameVerification( + Boolean disableHostNameVerification) { + this.disableHostNameVerification = disableHostNameVerification; + } + + @Override + public String getType() { + return TYPE; + } + + @Override + public void init(Properties config) throws ServletException { + this.baseDN = config.getProperty(BASE_DN); + this.providerUrl = config.getProperty(PROVIDER_URL); + this.ldapDomain = config.getProperty(LDAP_BIND_DOMAIN); + this.enableStartTls = + Boolean.valueOf(config.getProperty(ENABLE_START_TLS, "false")); + + if (this.providerUrl == null) { + throw new NullPointerException("The LDAP URI can not be null"); + } + if (!((this.baseDN == null) + ^ (this.ldapDomain == null))) { + throw new IllegalArgumentException( + "Either LDAP base DN or LDAP domain value needs to be specified"); + } + if (this.enableStartTls) { + String tmp = this.providerUrl.toLowerCase(); + if (tmp.startsWith("ldaps")) { + throw new IllegalArgumentException( + "Can not use ldaps and StartTLS option at the same time"); + } + } + } + + @Override + public void destroy() { + } + + @Override + public boolean managementOperation(AuthenticationToken token, + HttpServletRequest request, HttpServletResponse response) + throws IOException, AuthenticationException { + return true; + } + + @Override + public AuthenticationToken authenticate(HttpServletRequest request, + HttpServletResponse response) + throws IOException, AuthenticationException { + AuthenticationToken token = null; + String authorization = + request.getHeader(HttpConstants.AUTHORIZATION_HEADER); + + if (authorization == null + || !AuthenticationHandlerUtil.matchAuthScheme(HttpConstants.BASIC, + authorization)) { + response.setHeader(WWW_AUTHENTICATE, HttpConstants.BASIC); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + if (authorization == null) { + logger.trace("Basic auth starting"); + } else { + logger.warn("'" + HttpConstants.AUTHORIZATION_HEADER + + "' does not start with '" + HttpConstants.BASIC + "' : {}", + authorization); + } + } else { + authorization = + authorization.substring(HttpConstants.BASIC.length()).trim(); + final Base64 base64 = new Base64(0); + // As per RFC7617, UTF-8 charset should be used for decoding. + String[] credentials = new String(base64.decode(authorization), + StandardCharsets.UTF_8).split(":", 2); + if (credentials.length == 2) { + token = authenticateUser(credentials[0], credentials[1]); + response.setStatus(HttpServletResponse.SC_OK); + } + } + return token; + } + + private AuthenticationToken authenticateUser(String userName, + String password) throws AuthenticationException { + if (userName == null || userName.isEmpty()) { + throw new AuthenticationException("Error validating LDAP user:" + + " a null or blank username has been provided"); + } + + // If the domain is available in the config, then append it unless domain + // is already part of the username. LDAP providers like Active Directory + // use a fully qualified user name like foo@bar.com. + if (!hasDomain(userName) && ldapDomain != null) { + userName = userName + "@" + ldapDomain; + } + + if (password == null || password.isEmpty() || + password.getBytes(StandardCharsets.UTF_8)[0] == 0) { + throw new AuthenticationException("Error validating LDAP user:" + + " a null or blank password has been provided"); + } + + // setup the security principal + String bindDN; + if (baseDN == null) { + bindDN = userName; + } else { + bindDN = "uid=" + userName + "," + baseDN; + } + + if (this.enableStartTls) { + authenticateWithTlsExtension(bindDN, password); + } else { + authenticateWithoutTlsExtension(bindDN, password); + } + + return new AuthenticationToken(userName, userName, TYPE); + } + + private void authenticateWithTlsExtension(String userDN, String password) + throws AuthenticationException { + LdapContext ctx = null; + Hashtable env = new Hashtable(); + env.put(Context.INITIAL_CONTEXT_FACTORY, + "com.sun.jndi.ldap.LdapCtxFactory"); + env.put(Context.PROVIDER_URL, providerUrl); + + try { + // Create initial context + ctx = new InitialLdapContext(env, null); + // Establish TLS session + StartTlsResponse tls = + (StartTlsResponse) ctx.extendedOperation(new StartTlsRequest()); + + if (disableHostNameVerification) { + tls.setHostnameVerifier(new HostnameVerifier() { + @Override + public boolean verify(String hostname, SSLSession session) { + return true; + } + }); + } + + tls.negotiate(); + + // Initialize security credentials & perform read operation for + // verification. + ctx.addToEnvironment(Context.SECURITY_AUTHENTICATION, + SECURITY_AUTHENTICATION); + ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, userDN); + ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, password); + ctx.lookup(userDN); + logger.debug("Authentication successful for {}", userDN); + + } catch (NamingException | IOException ex) { + throw new AuthenticationException("Error validating LDAP user", ex); + } finally { + if (ctx != null) { + try { + ctx.close(); + } catch (NamingException e) { /* Ignore. */ + } + } + } + } + + private void authenticateWithoutTlsExtension(String userDN, String password) + throws AuthenticationException { + Hashtable env = new Hashtable(); + env.put(Context.INITIAL_CONTEXT_FACTORY, + "com.sun.jndi.ldap.LdapCtxFactory"); + env.put(Context.PROVIDER_URL, providerUrl); + env.put(Context.SECURITY_AUTHENTICATION, SECURITY_AUTHENTICATION); + env.put(Context.SECURITY_PRINCIPAL, userDN); + env.put(Context.SECURITY_CREDENTIALS, password); + + try { + // Create initial context + Context ctx = new InitialDirContext(env); + ctx.close(); + logger.debug("Authentication successful for {}", userDN); + + } catch (NamingException e) { + throw new AuthenticationException("Error validating LDAP user", e); + } + } + + private static boolean hasDomain(String userName) { + return (indexOfDomainMatch(userName) > 0); + } + + /* + * Get the index separating the user name from domain name (the user's name + * up to the first '/' or '@'). + * + * @param userName full user name. + * + * @return index of domain match or -1 if not found + */ + private static int indexOfDomainMatch(String userName) { + if (userName == null) { + return -1; + } + + int idx = userName.indexOf('/'); + int idx2 = userName.indexOf('@'); + int endIdx = Math.min(idx, idx2); // Use the earlier match. + // Unless at least one of '/' or '@' was not found, in + // which case, user the latter match. + if (endIdx == -1) { + endIdx = Math.max(idx, idx2); + } + return endIdx; + } + +} diff --git a/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/MultiSchemeAuthenticationHandler.java b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/MultiSchemeAuthenticationHandler.java new file mode 100644 index 000000000000..1ef996d82fc2 --- /dev/null +++ b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/MultiSchemeAuthenticationHandler.java @@ -0,0 +1,214 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. See accompanying LICENSE file. + */ +package org.apache.hadoop.hbase.security.authentication.server; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Properties; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.yetus.audience.InterfaceAudience; +import org.apache.yetus.audience.InterfaceStability; +import org.apache.hadoop.security.authentication.client.AuthenticationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.hbase.thirdparty.com.google.common.base.Splitter; + +/** + * The {@link MultiSchemeAuthenticationHandler} supports configuring multiple + * authentication mechanisms simultaneously. e.g. server can support multiple + * authentication mechanisms such as Kerberos (SPENGO) and LDAP. During the + * authentication phase, server will specify all possible authentication schemes + * and let client choose the appropriate scheme. Please refer to RFC-2616 and + * HADOOP-12082 for more details. + *

+ * The supported configuration properties are: + *

    + *
  • multi-scheme-auth-handler.schemes: A comma separated list of HTTP + * authentication mechanisms supported by this handler. It does not have a + * default value. e.g. multi-scheme-auth-handler.schemes=basic,negotiate + *
  • multi-scheme-auth-handler.schemes.${scheme-name}.handler: The + * authentication handler implementation to be used for the specified + * authentication scheme. It does not have a default value. e.g. + * multi-scheme-auth-handler.schemes.negotiate.handler=kerberos + *
+ * + * It expected that for every authentication scheme specified in + * multi-scheme-auth-handler.schemes property, a handler needs to be configured. + * Note that while scheme values in 'multi-scheme-auth-handler.schemes' property + * are case-insensitive, the scheme value in the handler configuration property + * name must be lower case. i.e. property name such as + * multi-scheme-auth-handler.schemes.Negotiate.handler is invalid. + */ +@InterfaceAudience.Private +@InterfaceStability.Evolving +public class MultiSchemeAuthenticationHandler implements + CompositeAuthenticationHandler { + private static Logger logger = LoggerFactory + .getLogger(MultiSchemeAuthenticationHandler.class); + public static final String SCHEMES_PROPERTY = + "multi-scheme-auth-handler.schemes"; + public static final String AUTH_HANDLER_PROPERTY = + "multi-scheme-auth-handler.schemes.%s.handler"; + private static final Splitter STR_SPLITTER = Splitter.on(',').trimResults() + .omitEmptyStrings(); + + private final Map schemeToAuthHandlerMapping = + new HashMap<>(); + private final Collection types = new HashSet<>(); + private final String authType; + + /** + * Constant that identifies the authentication mechanism. + */ + public static final String TYPE = "multi-scheme"; + + public MultiSchemeAuthenticationHandler() { + this(TYPE); + } + + public MultiSchemeAuthenticationHandler(String authType) { + this.authType = authType; + } + + @Override + public String getType() { + return authType; + } + + /** + * This method returns the token types supported by this authentication + * handler. + * + * @return the token types supported by this authentication handler. + */ + @Override + public Collection getTokenTypes() { + return types; + } + + @Override + public void init(Properties config) throws ServletException { + // Useful for debugging purpose. + for (Map.Entry prop : config.entrySet()) { + logger.info("{} : {}", prop.getKey(), prop.getValue()); + } + + this.types.clear(); + if (config.getProperty(SCHEMES_PROPERTY) == null) { + throw new NullPointerException(SCHEMES_PROPERTY + " system property is not specified."); + } + String schemesProperty = config.getProperty(SCHEMES_PROPERTY); + for (String scheme : STR_SPLITTER.split(schemesProperty)) { + scheme = AuthenticationHandlerUtil.checkAuthScheme(scheme); + if (schemeToAuthHandlerMapping.containsKey(scheme)) { + throw new IllegalArgumentException("Handler is already specified for " + + scheme + " authentication scheme."); + } + + String authHandlerPropName = + String.format(AUTH_HANDLER_PROPERTY, scheme).toLowerCase(); + String authHandlerName = config.getProperty(authHandlerPropName); + if (authHandlerName == null) { + throw new NullPointerException( + "No auth handler configured for scheme " + scheme); + } + + String authHandlerClassName = + AuthenticationHandlerUtil + .getAuthenticationHandlerClassName(authHandlerName); + AuthenticationHandler handler = + initializeAuthHandler(authHandlerClassName, config); + schemeToAuthHandlerMapping.put(scheme, handler); + types.add(handler.getType()); + } + logger.info("Successfully initialized MultiSchemeAuthenticationHandler"); + } + + protected AuthenticationHandler initializeAuthHandler( + String authHandlerClassName, Properties config) throws ServletException { + try { + if (authHandlerClassName == null) { + throw new NullPointerException(); + } + logger.debug("Initializing Authentication handler of type " + + authHandlerClassName); + Class klass = + Thread.currentThread().getContextClassLoader() + .loadClass(authHandlerClassName); + AuthenticationHandler authHandler = + (AuthenticationHandler) klass.getDeclaredConstructor().newInstance(); + authHandler.init(config); + logger.info("Successfully initialized Authentication handler of type " + + authHandlerClassName); + return authHandler; + } catch (ClassNotFoundException | InstantiationException + | IllegalAccessException | NoSuchMethodException | InvocationTargetException ex) { + logger.error("Failed to initialize authentication handler " + + authHandlerClassName, ex); + throw new ServletException(ex); + } + } + + @Override + public void destroy() { + for (AuthenticationHandler handler : schemeToAuthHandlerMapping.values()) { + handler.destroy(); + } + } + + @Override + public boolean managementOperation(AuthenticationToken token, + HttpServletRequest request, HttpServletResponse response) + throws IOException, AuthenticationException { + return true; + } + + @Override + public AuthenticationToken authenticate(HttpServletRequest request, + HttpServletResponse response) + throws IOException, AuthenticationException { + String authorization = + request.getHeader(HttpConstants.AUTHORIZATION_HEADER); + if (authorization != null) { + for (Map.Entry entry : + schemeToAuthHandlerMapping.entrySet()) { + if (AuthenticationHandlerUtil.matchAuthScheme( + entry.getKey(), authorization)) { + AuthenticationToken token = + entry.getValue().authenticate(request, response); + logger.trace("Token generated with type {}", token.getType()); + return token; + } + } + } + + // Handle the case when (authorization == null) or an invalid authorization + // header (e.g. a header value without the scheme name). + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + for (String scheme : schemeToAuthHandlerMapping.keySet()) { + response.addHeader(HttpConstants.WWW_AUTHENTICATE_HEADER, scheme); + } + + return null; + } +} diff --git a/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/PseudoAuthenticationHandler.java b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/PseudoAuthenticationHandler.java new file mode 100644 index 000000000000..c927aa122c4e --- /dev/null +++ b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/server/PseudoAuthenticationHandler.java @@ -0,0 +1,200 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. See accompanying LICENSE file. + */ +package org.apache.hadoop.hbase.security.authentication.server; + +import org.apache.hadoop.security.authentication.client.AuthenticationException; +import org.apache.hadoop.security.authentication.client.PseudoAuthenticator; +import org.apache.http.client.utils.URLEncodedUtils; +import org.apache.http.NameValuePair; +import org.apache.yetus.audience.InterfaceAudience; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Properties; + +/** + * The PseudoAuthenticationHandler provides a pseudo authentication mechanism that accepts + * the user name specified as a query string parameter. + *

+ * This mimics the model of Hadoop Simple authentication which trust the 'user.name' property provided in + * the configuration object. + *

+ * This handler can be configured to support anonymous users. + *

+ * The only supported configuration property is: + *

    + *
  • simple.anonymous.allowed: true|false, default value is false
  • + *
+ */ +@InterfaceAudience.Private +public class PseudoAuthenticationHandler implements AuthenticationHandler { + + /** + * Constant that identifies the authentication mechanism. + */ + public static final String TYPE = "simple"; + + /** + * Constant for the configuration property that indicates if anonymous users are allowed. + */ + public static final String ANONYMOUS_ALLOWED = TYPE + ".anonymous.allowed"; + + private static final String PSEUDO_AUTH = "PseudoAuth"; + + private boolean acceptAnonymous; + private String type; + + /** + * Creates a Hadoop pseudo authentication handler with the default auth-token + * type, simple. + */ + public PseudoAuthenticationHandler() { + this(TYPE); + } + + /** + * Creates a Hadoop pseudo authentication handler with a custom auth-token + * type. + * + * @param type auth-token type. + */ + public PseudoAuthenticationHandler(String type) { + this.type = type; + } + + /** + * Initializes the authentication handler instance. + *

+ * This method is invoked by the {@link AuthenticationFilter#init} method. + * + * @param config configuration properties to initialize the handler. + * + * @throws ServletException thrown if the handler could not be initialized. + */ + @Override + public void init(Properties config) throws ServletException { + acceptAnonymous = Boolean.parseBoolean(config.getProperty(ANONYMOUS_ALLOWED, "false")); + } + + /** + * Returns if the handler is configured to support anonymous users. + * + * @return if the handler is configured to support anonymous users. + */ + protected boolean getAcceptAnonymous() { + return acceptAnonymous; + } + + /** + * Releases any resources initialized by the authentication handler. + *

+ * This implementation does a NOP. + */ + @Override + public void destroy() { + } + + /** + * Returns the authentication type of the authentication handler, 'simple'. + * + * @return the authentication type of the authentication handler, 'simple'. + */ + @Override + public String getType() { + return type; + } + + /** + * This is an empty implementation, it always returns TRUE. + * + * + * + * @param token the authentication token if any, otherwise NULL. + * @param request the HTTP client request. + * @param response the HTTP client response. + * + * @return TRUE + * @throws IOException it is never thrown. + * @throws AuthenticationException it is never thrown. + */ + @Override + public boolean managementOperation(AuthenticationToken token, + HttpServletRequest request, + HttpServletResponse response) + throws IOException, AuthenticationException { + return true; + } + + private String getUserName(HttpServletRequest request) { + String queryString = request.getQueryString(); + if(queryString == null || queryString.length() == 0) { + return null; + } + List list = URLEncodedUtils.parse(queryString, StandardCharsets.UTF_8); + if (list != null) { + for (NameValuePair nv : list) { + if (PseudoAuthenticator.USER_NAME.equals(nv.getName())) { + return nv.getValue(); + } + } + } + return null; + } + + /** + * Authenticates an HTTP client request. + *

+ * It extracts the {@link PseudoAuthenticator#USER_NAME} parameter from the query string and creates + * an {@link AuthenticationToken} with it. + *

+ * If the HTTP client request does not contain the {@link PseudoAuthenticator#USER_NAME} parameter and + * the handler is configured to allow anonymous users it returns the {@link AuthenticationToken#ANONYMOUS} + * token. + *

+ * If the HTTP client request does not contain the {@link PseudoAuthenticator#USER_NAME} parameter and + * the handler is configured to disallow anonymous users it throws an {@link AuthenticationException}. + * + * @param request the HTTP client request. + * @param response the HTTP client response. + * + * @return an authentication token if the HTTP client request is accepted and credentials are valid. + * + * @throws IOException thrown if an IO error occurred. + * @throws AuthenticationException thrown if HTTP client request was not accepted as an authentication request. + */ + @Override + public AuthenticationToken authenticate(HttpServletRequest request, HttpServletResponse response) + throws IOException, AuthenticationException { + AuthenticationToken token; + String userName = getUserName(request); + if (userName == null) { + if (getAcceptAnonymous()) { + token = AuthenticationToken.ANONYMOUS; + } else { + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + response.setHeader(WWW_AUTHENTICATE, PSEUDO_AUTH); + token = null; + } + } else { + token = new AuthenticationToken(userName, userName, getType()); + } + return token; + } + +} diff --git a/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/CertificateUtil.java b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/CertificateUtil.java new file mode 100644 index 000000000000..c7337f69cb24 --- /dev/null +++ b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/CertificateUtil.java @@ -0,0 +1,66 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.security.authentication.util; + +import org.apache.yetus.audience.InterfaceAudience; +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.security.PublicKey; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.interfaces.RSAPublicKey; + +import javax.servlet.ServletException; + +@InterfaceAudience.Private +public class CertificateUtil { + private static final String PEM_HEADER = "-----BEGIN CERTIFICATE-----\n"; + private static final String PEM_FOOTER = "\n-----END CERTIFICATE-----"; + + /** + * Gets an RSAPublicKey from the provided PEM encoding. + * + * @param pem + * - the pem encoding from config without the header and footer + * @return RSAPublicKey the RSA public key + * @throws ServletException thrown if a processing error occurred + */ + public static RSAPublicKey parseRSAPublicKey(String pem) throws ServletException { + String fullPem = PEM_HEADER + pem + PEM_FOOTER; + PublicKey key = null; + try { + CertificateFactory fact = CertificateFactory.getInstance("X.509"); + ByteArrayInputStream is = new ByteArrayInputStream( + fullPem.getBytes(StandardCharsets.UTF_8)); + + X509Certificate cer = (X509Certificate) fact.generateCertificate(is); + key = cer.getPublicKey(); + } catch (CertificateException ce) { + String message = null; + if (pem.startsWith(PEM_HEADER)) { + message = "CertificateException - be sure not to include PEM header " + + "and footer in the PEM configuration element."; + } else { + message = "CertificateException - PEM may be corrupt"; + } + throw new ServletException(message, ce); + } + return (RSAPublicKey) key; + } +} diff --git a/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/FileSignerSecretProvider.java b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/FileSignerSecretProvider.java new file mode 100644 index 000000000000..3c79ea10f514 --- /dev/null +++ b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/FileSignerSecretProvider.java @@ -0,0 +1,79 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. See accompanying LICENSE file. + */ +package org.apache.hadoop.hbase.security.authentication.util; + +import org.apache.yetus.audience.InterfaceAudience; +import org.apache.yetus.audience.InterfaceStability; +import org.apache.hadoop.hbase.security.authentication.server.AuthenticationFilter; + +import javax.servlet.ServletContext; +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Properties; + +/** + * A SignerSecretProvider that simply loads a secret from a specified file. + */ +@InterfaceStability.Unstable +@InterfaceAudience.Private +public class FileSignerSecretProvider extends SignerSecretProvider { + + private byte[] secret; + private byte[][] secrets; + + public FileSignerSecretProvider() {} + + @Override + public void init(Properties config, ServletContext servletContext, + long tokenValidity) throws Exception { + + String signatureSecretFile = config.getProperty( + AuthenticationFilter.SIGNATURE_SECRET_FILE, null); + + if (signatureSecretFile != null) { + try (Reader reader = new InputStreamReader(Files.newInputStream( + Paths.get(signatureSecretFile)), StandardCharsets.UTF_8)) { + StringBuilder sb = new StringBuilder(); + int c = reader.read(); + while (c > -1) { + sb.append((char) c); + c = reader.read(); + } + + secret = sb.toString().getBytes(StandardCharsets.UTF_8); + if (secret.length == 0) { + throw new RuntimeException("No secret in signature secret file: " + + signatureSecretFile); + } + } catch (IOException ex) { + throw new RuntimeException("Could not read signature secret file: " + + signatureSecretFile); + } + } + + secrets = new byte[][]{secret}; + } + + @Override + public byte[] getCurrentSecret() { + return secret; + } + + @Override + public byte[][] getAllSecrets() { + return secrets; + } +} diff --git a/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/JaasConfiguration.java b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/JaasConfiguration.java new file mode 100644 index 000000000000..1505e56880f2 --- /dev/null +++ b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/JaasConfiguration.java @@ -0,0 +1,78 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. See accompanying LICENSE file. + */ +package org.apache.hadoop.hbase.security.authentication.util; + +import org.apache.yetus.audience.InterfaceAudience; +import java.util.HashMap; +import java.util.Map; +import javax.security.auth.login.AppConfigurationEntry; +import javax.security.auth.login.Configuration; + +/** + * Creates a programmatic version of a jaas.conf file. This can be used + * instead of writing a jaas.conf file and setting the system property, + * "java.security.auth.login.config", to point to that file. It is meant to be + * used for connecting to ZooKeeper. + */ +@InterfaceAudience.Private +public class JaasConfiguration extends Configuration { + + private final javax.security.auth.login.Configuration baseConfig = + javax.security.auth.login.Configuration.getConfiguration(); + private final AppConfigurationEntry[] entry; + private final String entryName; + + /** + * Add an entry to the jaas configuration with the passed in name, + * principal, and keytab. The other necessary options will be set for you. + * + * @param entryName The name of the entry (e.g. "Client") + * @param principal The principal of the user + * @param keytab The location of the keytab + */ + public JaasConfiguration(String entryName, String principal, String keytab) { + this.entryName = entryName; + Map options = new HashMap<>(); + options.put("keyTab", keytab); + options.put("principal", principal); + options.put("useKeyTab", "true"); + options.put("storeKey", "true"); + options.put("useTicketCache", "false"); + options.put("refreshKrb5Config", "true"); + String jaasEnvVar = System.getenv("HADOOP_JAAS_DEBUG"); + if ("true".equalsIgnoreCase(jaasEnvVar)) { + options.put("debug", "true"); + } + entry = new AppConfigurationEntry[]{ + new AppConfigurationEntry(getKrb5LoginModuleName(), + AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, + options)}; + } + + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(String name) { + return (entryName.equals(name)) ? entry : ((baseConfig != null) + ? baseConfig.getAppConfigurationEntry(name) : null); + } + + private String getKrb5LoginModuleName() { + String krb5LoginModuleName; + if (System.getProperty("java.vendor").contains("IBM")) { + krb5LoginModuleName = "com.ibm.security.auth.module.Krb5LoginModule"; + } else { + krb5LoginModuleName = "com.sun.security.auth.module.Krb5LoginModule"; + } + return krb5LoginModuleName; + } +} diff --git a/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/RandomSignerSecretProvider.java b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/RandomSignerSecretProvider.java new file mode 100644 index 000000000000..39da6f333d4a --- /dev/null +++ b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/RandomSignerSecretProvider.java @@ -0,0 +1,53 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. See accompanying LICENSE file. + */ +package org.apache.hadoop.hbase.security.authentication.util; + +import java.security.SecureRandom; +import java.util.Random; + +import org.apache.yetus.audience.InterfaceAudience; +import org.apache.yetus.audience.InterfaceStability; + +/** + * A SignerSecretProvider that uses a random number as its secret. It rolls + * the secret at a regular interval. + */ +@InterfaceStability.Unstable +@InterfaceAudience.Private +public class RandomSignerSecretProvider extends RolloverSignerSecretProvider { + + private final Random rand; + + public RandomSignerSecretProvider() { + super(); + rand = new SecureRandom(); + } + + /** + * This constructor lets you set the seed of the Random Number Generator and + * is meant for testing. + * @param seed the seed for the random number generator + */ + public RandomSignerSecretProvider(long seed) { + super(); + rand = new Random(seed); + } + + @Override + protected byte[] generateNewSecret() { + byte[] secret = new byte[32]; // 32 bytes = 256 bits + rand.nextBytes(secret); + return secret; + } +} diff --git a/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/RolloverSignerSecretProvider.java b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/RolloverSignerSecretProvider.java new file mode 100644 index 000000000000..468b0bc8e263 --- /dev/null +++ b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/RolloverSignerSecretProvider.java @@ -0,0 +1,142 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. See accompanying LICENSE file. + */ +package org.apache.hadoop.hbase.security.authentication.util; + +import java.util.Properties; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import javax.servlet.ServletContext; +import org.apache.yetus.audience.InterfaceAudience; +import org.apache.yetus.audience.InterfaceStability; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * An abstract SignerSecretProvider that can be use used as the base for a + * rolling secret. The secret will roll over at the same interval as the token + * validity, so there are only ever a maximum of two valid secrets at any + * given time. This class handles storing and returning the secrets, as well + * as the rolling over. At a minimum, subclasses simply need to implement the + * generateNewSecret() method. More advanced implementations can override + * other methods to provide more advanced behavior, but should be careful when + * doing so. + */ +@InterfaceStability.Unstable +@InterfaceAudience.Private +public abstract class RolloverSignerSecretProvider + extends SignerSecretProvider { + + static Logger LOG = LoggerFactory.getLogger( + RolloverSignerSecretProvider.class); + /** + * Stores the currently valid secrets. The current secret is the 0th element + * in the array. + */ + private volatile byte[][] secrets; + private ScheduledExecutorService scheduler; + private boolean schedulerRunning; + private boolean isDestroyed; + + public RolloverSignerSecretProvider() { + schedulerRunning = false; + isDestroyed = false; + } + + /** + * Initialize the SignerSecretProvider. It initializes the current secret + * and starts the scheduler for the rollover to run at an interval of + * tokenValidity. + * @param config configuration properties + * @param servletContext servlet context + * @param tokenValidity The amount of time a token is valid for + * @throws Exception thrown if an error occurred + */ + @Override + public void init(Properties config, ServletContext servletContext, + long tokenValidity) throws Exception { + initSecrets(generateNewSecret(), null); + startScheduler(tokenValidity, tokenValidity); + } + + /** + * Initializes the secrets array. This should typically be called only once, + * during init but some implementations may wish to call it other times. + * previousSecret can be null if there isn't a previous secret, but + * currentSecret should never be null. + * @param currentSecret The current secret + * @param previousSecret The previous secret + */ + protected void initSecrets(byte[] currentSecret, byte[] previousSecret) { + secrets = new byte[][]{currentSecret, previousSecret}; + } + + /** + * Starts the scheduler for the rollover to run at an interval. + * @param initialDelay The initial delay in the rollover in milliseconds + * @param period The interval for the rollover in milliseconds + */ + protected synchronized void startScheduler(long initialDelay, long period) { + if (!schedulerRunning) { + schedulerRunning = true; + scheduler = Executors.newSingleThreadScheduledExecutor(); + scheduler.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + rollSecret(); + } + }, initialDelay, period, TimeUnit.MILLISECONDS); + } + } + + @Override + public synchronized void destroy() { + if (!isDestroyed) { + isDestroyed = true; + if (scheduler != null) { + scheduler.shutdown(); + } + schedulerRunning = false; + super.destroy(); + } + } + + /** + * Rolls the secret. It is called automatically at the rollover interval. + */ + protected synchronized void rollSecret() { + if (!isDestroyed) { + LOG.debug("rolling secret"); + byte[] newSecret = generateNewSecret(); + secrets = new byte[][]{newSecret, secrets[0]}; + } + } + + /** + * Subclasses should implement this to return a new secret. It will be called + * automatically at the secret rollover interval. It should never return null. + * @return a new secret + */ + protected abstract byte[] generateNewSecret(); + + @Override + public byte[] getCurrentSecret() { + return secrets[0]; + } + + @Override + public byte[][] getAllSecrets() { + return secrets; + } +} diff --git a/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/Signer.java b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/Signer.java new file mode 100644 index 000000000000..7b0cd122df3d --- /dev/null +++ b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/Signer.java @@ -0,0 +1,125 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. See accompanying LICENSE file. + */ +package org.apache.hadoop.hbase.security.authentication.util; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.codec.binary.StringUtils; +import org.apache.yetus.audience.InterfaceAudience; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * Signs strings and verifies signed strings using a SHA digest. + */ +@InterfaceAudience.Private +public class Signer { + private static final String SIGNATURE = "&s="; + private static final String SIGNING_ALGORITHM = "HmacSHA256"; + + private SignerSecretProvider secretProvider; + + /** + * Creates a Signer instance using the specified SignerSecretProvider. The + * SignerSecretProvider should already be initialized. + * + * @param secretProvider The SignerSecretProvider to use + */ + public Signer(SignerSecretProvider secretProvider) { + if (secretProvider == null) { + throw new IllegalArgumentException("secretProvider cannot be NULL"); + } + this.secretProvider = secretProvider; + } + + /** + * Returns a signed string. + * + * @param str string to sign. + * + * @return the signed string. + */ + public synchronized String sign(String str) { + if (str == null || str.length() == 0) { + throw new IllegalArgumentException("NULL or empty string to sign"); + } + byte[] secret = secretProvider.getCurrentSecret(); + String signature = computeSignature(secret, str); + return str + SIGNATURE + signature; + } + + /** + * Verifies a signed string and extracts the original string. + * + * @param signedStr the signed string to verify and extract. + * + * @return the extracted original string. + * + * @throws SignerException thrown if the given string is not a signed string or if the signature is invalid. + */ + public String verifyAndExtract(String signedStr) throws SignerException { + int index = signedStr.lastIndexOf(SIGNATURE); + if (index == -1) { + throw new SignerException("Invalid signed text: " + signedStr); + } + String originalSignature = signedStr.substring(index + SIGNATURE.length()); + String rawValue = signedStr.substring(0, index); + checkSignatures(rawValue, originalSignature); + return rawValue; + } + + /** + * Returns then signature of a string. + * + * @param secret The secret to use + * @param str string to sign. + * + * @return the signature for the string. + */ + protected String computeSignature(byte[] secret, String str) { + try { + SecretKeySpec key = new SecretKeySpec((secret), SIGNING_ALGORITHM); + Mac mac = Mac.getInstance(SIGNING_ALGORITHM); + mac.init(key); + byte[] sig = mac.doFinal(StringUtils.getBytesUtf8(str)); + return new Base64(0).encodeToString(sig); + } catch (NoSuchAlgorithmException | InvalidKeyException ex) { + throw new RuntimeException("It should not happen, " + ex.getMessage(), ex); + } + } + + protected void checkSignatures(String rawValue, String originalSignature) + throws SignerException { + byte[] orginalSignatureBytes = StringUtils.getBytesUtf8(originalSignature); + boolean isValid = false; + byte[][] secrets = secretProvider.getAllSecrets(); + for (int i = 0; i < secrets.length; i++) { + byte[] secret = secrets[i]; + if (secret != null) { + String currentSignature = computeSignature(secret, rawValue); + if (MessageDigest.isEqual(orginalSignatureBytes, + StringUtils.getBytesUtf8(currentSignature))) { + isValid = true; + break; + } + } + } + if (!isValid) { + throw new SignerException("Invalid signature"); + } + } +} diff --git a/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/SignerException.java b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/SignerException.java new file mode 100644 index 000000000000..7865eda06ab1 --- /dev/null +++ b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/SignerException.java @@ -0,0 +1,34 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. See accompanying LICENSE file. + */ +package org.apache.hadoop.hbase.security.authentication.util; + +import org.apache.yetus.audience.InterfaceAudience; + +/** + * Exception thrown by {@link Signer} when a string signature is invalid. + */ +@InterfaceAudience.Private +public class SignerException extends Exception { + + static final long serialVersionUID = 0; + + /** + * Creates an exception instance. + * + * @param msg message for the exception. + */ + public SignerException(String msg) { + super(msg); + } +} diff --git a/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/SignerSecretProvider.java b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/SignerSecretProvider.java new file mode 100644 index 000000000000..0fcccb685231 --- /dev/null +++ b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/SignerSecretProvider.java @@ -0,0 +1,63 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. See accompanying LICENSE file. + */ +package org.apache.hadoop.hbase.security.authentication.util; + +import java.util.Properties; +import javax.servlet.ServletContext; +import org.apache.yetus.audience.InterfaceAudience; +import org.apache.yetus.audience.InterfaceStability; + +/** + * The SignerSecretProvider is an abstract way to provide a secret to be used + * by the Signer so that we can have different implementations that potentially + * do more complicated things in the backend. + * See the RolloverSignerSecretProvider class for an implementation that + * supports rolling over the secret at a regular interval. + */ +@InterfaceStability.Unstable +@InterfaceAudience.Private +public abstract class SignerSecretProvider { + + /** + * Initialize the SignerSecretProvider + * @param config configuration properties + * @param servletContext servlet context + * @param tokenValidity The amount of time a token is valid for + * @throws Exception thrown if an error occurred + */ + public abstract void init(Properties config, ServletContext servletContext, + long tokenValidity) throws Exception; + /** + * Will be called on shutdown; subclasses should perform any cleanup here. + */ + public void destroy() {} + + /** + * Returns the current secret to be used by the Signer for signing new + * cookies. This should never return null. + *

+ * Callers should be careful not to modify the returned value. + * @return the current secret + */ + public abstract byte[] getCurrentSecret(); + + /** + * Returns all secrets that a cookie could have been signed with and are still + * valid; this should include the secret returned by getCurrentSecret(). + *

+ * Callers should be careful not to modify the returned value. + * @return the secrets + */ + public abstract byte[][] getAllSecrets(); +} diff --git a/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/ZKSignerSecretProvider.java b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/ZKSignerSecretProvider.java new file mode 100644 index 000000000000..591b41d89dde --- /dev/null +++ b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/ZKSignerSecretProvider.java @@ -0,0 +1,379 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. See accompanying LICENSE file. + */ +package org.apache.hadoop.hbase.security.authentication.util; + +import java.nio.ByteBuffer; +import java.security.SecureRandom; +import java.util.Properties; +import java.util.Random; +import javax.servlet.ServletContext; +import org.apache.curator.framework.CuratorFramework; +import org.apache.yetus.audience.InterfaceAudience; +import org.apache.yetus.audience.InterfaceStability; +import org.apache.zookeeper.KeeperException; +import org.apache.zookeeper.data.Stat; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.hadoop.hbase.security.authentication.util.ZookeeperClient; + +/** + * A SignerSecretProvider that synchronizes a rolling random secret between + * multiple servers using ZooKeeper. + *

+ * It works by storing the secrets and next rollover time in a ZooKeeper znode. + * All ZKSignerSecretProviders looking at that znode will use those + * secrets and next rollover time to ensure they are synchronized. There is no + * "leader" -- any of the ZKSignerSecretProviders can choose the next secret; + * which one is indeterminate. Kerberos-based ACLs can also be enforced to + * prevent a malicious third-party from getting or setting the secrets. It uses + * its own CuratorFramework client for talking to ZooKeeper. If you want to use + * your own Curator client, you can pass it to ZKSignerSecretProvider; see + * {@link org.apache.hadoop.hbase.security.authentication.server.AuthenticationFilter} + * for more details. + *

+ * Details of the configurations are listed on Configuration Page + */ +@InterfaceStability.Unstable +@InterfaceAudience.Private +public class ZKSignerSecretProvider extends RolloverSignerSecretProvider { + + private static final String CONFIG_PREFIX = + "signer.secret.provider.zookeeper."; + + /** + * Constant for the property that specifies the ZooKeeper connection string. + */ + public static final String ZOOKEEPER_CONNECTION_STRING = + CONFIG_PREFIX + "connection.string"; + + /** + * Constant for the property that specifies the ZooKeeper path. + */ + public static final String ZOOKEEPER_PATH = CONFIG_PREFIX + "path"; + + /** + * Constant for the property that specifies the auth type to use. Supported + * values are "none" and "sasl". The default value is "none". + */ + public static final String ZOOKEEPER_AUTH_TYPE = CONFIG_PREFIX + "auth.type"; + + /** + * Constant for the property that specifies the Kerberos keytab file. + */ + public static final String ZOOKEEPER_KERBEROS_KEYTAB = + CONFIG_PREFIX + "kerberos.keytab"; + + /** + * Constant for the property that specifies the Kerberos principal. + */ + public static final String ZOOKEEPER_KERBEROS_PRINCIPAL = + CONFIG_PREFIX + "kerberos.principal"; + + public static final String ZOOKEEPER_SSL_ENABLED = CONFIG_PREFIX + "ssl.enabled"; + public static final String ZOOKEEPER_SSL_KEYSTORE_LOCATION = + CONFIG_PREFIX + "ssl.keystore.location"; + public static final String ZOOKEEPER_SSL_KEYSTORE_PASSWORD = + CONFIG_PREFIX + "ssl.keystore.password"; + public static final String ZOOKEEPER_SSL_TRUSTSTORE_LOCATION = + CONFIG_PREFIX + "ssl.truststore.location"; + public static final String ZOOKEEPER_SSL_TRUSTSTORE_PASSWORD = + CONFIG_PREFIX + "ssl.truststore.password"; + + /** + * Constant for the property that specifies whether or not the Curator client + * should disconnect from ZooKeeper on shutdown. The default is "true". Only + * set this to "false" if a custom Curator client is being provided and the + * disconnection is being handled elsewhere. + */ + public static final String DISCONNECT_FROM_ZOOKEEPER_ON_SHUTDOWN = + CONFIG_PREFIX + "disconnect.on.shutdown"; + + /** + * Constant for the ServletContext attribute that can be used for providing a + * custom CuratorFramework client. If set ZKSignerSecretProvider will use this + * Curator client instead of creating a new one. The providing class is + * responsible for creating and configuring the Curator client (including + * security and ACLs) in this case. + */ + public static final String + ZOOKEEPER_SIGNER_SECRET_PROVIDER_CURATOR_CLIENT_ATTRIBUTE = + CONFIG_PREFIX + "curator.client"; + + private static final String JAAS_LOGIN_ENTRY_NAME = + "ZKSignerSecretProviderClient"; + + private static Logger LOG = LoggerFactory.getLogger( + ZKSignerSecretProvider.class); + private String path; + /** + * Stores the next secret that will be used after the current one rolls over. + * We do this to help with rollover performance by actually deciding the next + * secret at the previous rollover. This allows us to switch to the next + * secret very quickly. Afterwards, we have plenty of time to decide on the + * next secret. + */ + private volatile byte[] nextSecret; + private final Random rand; + /** + * Stores the current version of the znode. + */ + private int zkVersion; + /** + * Stores the next date that the rollover will occur. This is only used + * for allowing new servers joining later to synchronize their rollover + * with everyone else. + */ + private long nextRolloverDate; + private long tokenValidity; + private CuratorFramework client; + private boolean shouldDisconnect; + private static int INT_BYTES = Integer.SIZE / Byte.SIZE; + private static int LONG_BYTES = Long.SIZE / Byte.SIZE; + private static int DATA_VERSION = 0; + + public ZKSignerSecretProvider() { + super(); + rand = new SecureRandom(); + } + + /** + * This constructor lets you set the seed of the Random Number Generator and + * is meant for testing. + * @param seed the seed for the random number generator + */ + public ZKSignerSecretProvider(long seed) { + super(); + rand = new Random(seed); + } + + @Override + public void init(Properties config, ServletContext servletContext, + long tokenValidity) throws Exception { + Object curatorClientObj = servletContext.getAttribute( + ZOOKEEPER_SIGNER_SECRET_PROVIDER_CURATOR_CLIENT_ATTRIBUTE); + if (curatorClientObj != null + && curatorClientObj instanceof CuratorFramework) { + client = (CuratorFramework) curatorClientObj; + } else { + client = createCuratorClient(config); + servletContext.setAttribute( + ZOOKEEPER_SIGNER_SECRET_PROVIDER_CURATOR_CLIENT_ATTRIBUTE, client); + } + this.tokenValidity = tokenValidity; + shouldDisconnect = Boolean.parseBoolean( + config.getProperty(DISCONNECT_FROM_ZOOKEEPER_ON_SHUTDOWN, "true")); + path = config.getProperty(ZOOKEEPER_PATH); + if (path == null) { + throw new IllegalArgumentException(ZOOKEEPER_PATH + + " must be specified"); + } + try { + nextRolloverDate = System.currentTimeMillis() + tokenValidity; + // everyone tries to do this, only one will succeed and only when the + // znode doesn't already exist. Everyone else will synchronize on the + // data from the znode + client.create().creatingParentsIfNeeded() + .forPath(path, generateZKData(generateRandomSecret(), + generateRandomSecret(), null)); + zkVersion = 0; + LOG.info("Creating secret znode"); + } catch (KeeperException.NodeExistsException nee) { + LOG.info("The secret znode already exists, retrieving data"); + } + // Synchronize on the data from the znode + // passing true tells it to parse out all the data for initing + pullFromZK(true); + long initialDelay = nextRolloverDate - System.currentTimeMillis(); + // If it's in the past, try to find the next interval that we should + // be using + if (initialDelay < 1l) { + int i = 1; + while (initialDelay < 1l) { + initialDelay = nextRolloverDate + tokenValidity * i + - System.currentTimeMillis(); + i++; + } + } + super.startScheduler(initialDelay, tokenValidity); + } + + /** + * Disconnects from ZooKeeper unless told not to. + */ + @Override + public void destroy() { + if (shouldDisconnect && client != null) { + client.close(); + } + super.destroy(); + } + + @Override + protected synchronized void rollSecret() { + super.rollSecret(); + // Try to push the information to ZooKeeper with a potential next secret. + nextRolloverDate += tokenValidity; + byte[][] secrets = super.getAllSecrets(); + pushToZK(generateRandomSecret(), secrets[0], secrets[1]); + // Pull info from ZooKeeper to get the decided next secret + // passing false tells it that we don't care about most of the data + pullFromZK(false); + } + + @Override + protected byte[] generateNewSecret() { + // We simply return nextSecret because it's already been decided on + return nextSecret; + } + + /** + * Pushes proposed data to ZooKeeper. If a different server pushes its data + * first, it gives up. + * @param newSecret The new secret to use + * @param currentSecret The current secret + * @param previousSecret The previous secret + */ + private synchronized void pushToZK(byte[] newSecret, byte[] currentSecret, + byte[] previousSecret) { + byte[] bytes = generateZKData(newSecret, currentSecret, previousSecret); + try { + client.setData().withVersion(zkVersion).forPath(path, bytes); + } catch (KeeperException.BadVersionException bve) { + LOG.debug("Unable to push to znode; another server already did it"); + } catch (Exception ex) { + LOG.error("An unexpected exception occurred pushing data to ZooKeeper", + ex); + } + } + + /** + * Serialize the data to attempt to push into ZooKeeper. The format is this: + *

+ * [DATA_VERSION, newSecretLength, newSecret, currentSecretLength, currentSecret, previousSecretLength, previousSecret, nextRolloverDate] + *

+ * Only previousSecret can be null, in which case the format looks like this: + *

+ * [DATA_VERSION, newSecretLength, newSecret, currentSecretLength, currentSecret, 0, nextRolloverDate] + *

+ * @param newSecret The new secret to use + * @param currentSecret The current secret + * @param previousSecret The previous secret + * @return The serialized data for ZooKeeper + */ + private synchronized byte[] generateZKData(byte[] newSecret, + byte[] currentSecret, byte[] previousSecret) { + int newSecretLength = newSecret.length; + int currentSecretLength = currentSecret.length; + int previousSecretLength = 0; + if (previousSecret != null) { + previousSecretLength = previousSecret.length; + } + ByteBuffer bb = ByteBuffer.allocate(INT_BYTES + INT_BYTES + newSecretLength + + INT_BYTES + currentSecretLength + INT_BYTES + previousSecretLength + + LONG_BYTES); + bb.putInt(DATA_VERSION); + bb.putInt(newSecretLength); + bb.put(newSecret); + bb.putInt(currentSecretLength); + bb.put(currentSecret); + bb.putInt(previousSecretLength); + if (previousSecretLength > 0) { + bb.put(previousSecret); + } + bb.putLong(nextRolloverDate); + return bb.array(); + } + + /** + * Pulls data from ZooKeeper. If isInit is false, it will only parse the + * next secret and version. If isInit is true, it will also parse the current + * and previous secrets, and the next rollover date; it will also init the + * secrets. Hence, isInit should only be true on startup. + * @param isInit see description above + */ + private synchronized void pullFromZK(boolean isInit) { + try { + Stat stat = new Stat(); + byte[] bytes = client.getData().storingStatIn(stat).forPath(path); + ByteBuffer bb = ByteBuffer.wrap(bytes); + int dataVersion = bb.getInt(); + if (dataVersion > DATA_VERSION) { + throw new IllegalStateException("Cannot load data from ZooKeeper; it" + + "was written with a newer version"); + } + int nextSecretLength = bb.getInt(); + byte[] nextSecret = new byte[nextSecretLength]; + bb.get(nextSecret); + this.nextSecret = nextSecret; + zkVersion = stat.getVersion(); + if (isInit) { + int currentSecretLength = bb.getInt(); + byte[] currentSecret = new byte[currentSecretLength]; + bb.get(currentSecret); + int previousSecretLength = bb.getInt(); + byte[] previousSecret = null; + if (previousSecretLength > 0) { + previousSecret = new byte[previousSecretLength]; + bb.get(previousSecret); + } + super.initSecrets(currentSecret, previousSecret); + nextRolloverDate = bb.getLong(); + } + } catch (Exception ex) { + LOG.error("An unexpected exception occurred while pulling data from" + + "ZooKeeper", ex); + } + } + + protected byte[] generateRandomSecret() { + byte[] secret = new byte[32]; // 32 bytes = 256 bits + rand.nextBytes(secret); + return secret; + } + + /** + * This method creates the Curator client and connects to ZooKeeper. + * @param config configuration properties + * @return A Curator client + */ + protected CuratorFramework createCuratorClient(Properties config) { + String connectionString = config.getProperty(ZOOKEEPER_CONNECTION_STRING, "localhost:2181"); + String authType = config.getProperty(ZOOKEEPER_AUTH_TYPE, "none"); + String keytab = config.getProperty(ZOOKEEPER_KERBEROS_KEYTAB, "").trim(); + String principal = config.getProperty(ZOOKEEPER_KERBEROS_PRINCIPAL, "").trim(); + + boolean sslEnabled = Boolean.parseBoolean(config.getProperty(ZOOKEEPER_SSL_ENABLED, "false")); + String keystoreLocation = config.getProperty(ZOOKEEPER_SSL_KEYSTORE_LOCATION, ""); + String keystorePassword = config.getProperty(ZOOKEEPER_SSL_KEYSTORE_PASSWORD, ""); + String truststoreLocation = config.getProperty(ZOOKEEPER_SSL_TRUSTSTORE_LOCATION, ""); + String truststorePassword = config.getProperty(ZOOKEEPER_SSL_TRUSTSTORE_PASSWORD, ""); + + CuratorFramework zkClient = + ZookeeperClient.configure() + .withConnectionString(connectionString) + .withAuthType(authType) + .withKeytab(keytab) + .withPrincipal(principal) + .withJaasLoginEntryName(JAAS_LOGIN_ENTRY_NAME) + .enableSSL(sslEnabled) + .withKeystore(keystoreLocation) + .withKeystorePassword(keystorePassword) + .withTruststore(truststoreLocation) + .withTruststorePassword(truststorePassword) + .create(); + zkClient.start(); + return zkClient; + } +} diff --git a/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/ZookeeperClient.java b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/ZookeeperClient.java new file mode 100644 index 000000000000..5343900d6293 --- /dev/null +++ b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/security/authentication/util/ZookeeperClient.java @@ -0,0 +1,320 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. See accompanying LICENSE file. + */ + +package org.apache.hadoop.hbase.security.authentication.util; + +import org.apache.curator.RetryPolicy; +import org.apache.curator.framework.CuratorFramework; +import org.apache.curator.framework.CuratorFrameworkFactory; +import org.apache.curator.framework.api.ACLProvider; +import org.apache.curator.framework.imps.DefaultACLProvider; +import org.apache.curator.retry.ExponentialBackoffRetry; +import org.apache.curator.utils.ConfigurableZookeeperFactory; +import org.apache.curator.utils.ZookeeperFactory; +import org.apache.yetus.audience.InterfaceAudience; +import org.apache.zookeeper.ZooDefs; +import org.apache.zookeeper.client.ZKClientConfig; +import org.apache.zookeeper.common.ClientX509Util; +import org.apache.zookeeper.data.ACL; +import org.apache.zookeeper.data.Id; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.security.auth.login.Configuration; +import java.util.Collections; +import java.util.List; + +import org.apache.hadoop.hbase.security.authentication.util.JaasConfiguration; + + +/** + * Utility class to create a CuratorFramework object that can be used to connect to Zookeeper + * based on configuration values that can be supplied from different configuration properties. + * It is used from ZKDelegationTokenSecretManager in hadoop-common, and from + * {@link ZKSignerSecretProvider}. + * + * The class implements a fluid API to set up all the different properties. A very basic setup + * would seem like: + *

+ *   ZookeeperClient.configure()
+ *     .withConnectionString(<connectionString>)
+ *     .create();
+ * 
+ * + * Mandatory parameters to be set: + *
    + *
  • connectionString: A Zookeeper connection string.
  • + *
  • if authentication type is set to 'sasl': + *
      + *
    • keytab: the location of the keytab to be used for Kerberos authentication
    • + *
    • principal: the Kerberos principal to be used from the supplied Kerberos keytab file.
    • + *
    • jaasLoginEntryName: the login entry name in the JAAS configuration that is created for + * the KerberosLoginModule to be used by the Zookeeper client code.
    • + *
    + *
  • + *
  • if SSL is enabled: + *
      + *
    • the location of the Truststore file to be used
    • + *
    • the location of the Keystore file to be used
    • + *
    • if the Truststore is protected by a password, then the password of the Truststore
    • + *
    • if the Keystore is protected by a password, then the password if the Keystore
    • + *
    + *
  • + *
+ * + * When using 'sasl' authentication type, the JAAS configuration to be used by the Zookeeper client + * withing CuratorFramework is set to use the supplied keytab and principal for Kerberos login, + * moreover an ACL provider is set to provide a default ACL that requires SASL auth and the same + * principal to have access to the used paths. + * + * When using SSL/TLS, the Zookeeper client will set to use the secure channel towards Zookeeper, + * with the specified Keystore and Truststore. + * + * Default values: + *
    + *
  • authentication type: 'none'
  • + *
  • sessionTimeout: either the system property curator-default-session-timeout, or 60 + * seconds
  • + *
  • connectionTimeout: either the system property curator-default-connection-timeout, or 15 + * seconds
  • + *
  • retryPolicy: an ExponentialBackoffRetry, with a starting interval of 1 seconds and 3 + * retries
  • + *
  • zkFactory: a ConfigurableZookeeperFactory instance, to allow SSL setup via + * ZKClientConfig
  • + *
+ * + * @see ZKSignerSecretProvider + */ +@InterfaceAudience.Private +public class ZookeeperClient { + + private static final Logger LOG = LoggerFactory.getLogger(ZookeeperClient.class); + + private String connectionString; + private String namespace; + + private String authenticationType = "none"; + private String keytab; + private String principal; + private String jaasLoginEntryName; + + private int sessionTimeout = + Integer.getInteger("curator-default-session-timeout", 60 * 1000); + private int connectionTimeout = + Integer.getInteger("curator-default-connection-timeout", 15 * 1000); + + private RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3); + + private ZookeeperFactory zkFactory = new ConfigurableZookeeperFactory(); + + private boolean isSSLEnabled; + private String keystoreLocation; + private String keystorePassword; + private String truststoreLocation; + private String truststorePassword; + + public static ZookeeperClient configure() { + return new ZookeeperClient(); + } + + public ZookeeperClient withConnectionString(String conn) { + connectionString = conn; + return this; + } + + public ZookeeperClient withNamespace(String ns) { + this.namespace = ns; + return this; + } + + public ZookeeperClient withAuthType(String authType) { + this.authenticationType = authType; + return this; + } + + public ZookeeperClient withKeytab(String keytabPath) { + this.keytab = keytabPath; + return this; + } + + public ZookeeperClient withPrincipal(String princ) { + this.principal = princ; + return this; + } + + public ZookeeperClient withJaasLoginEntryName(String entryName) { + this.jaasLoginEntryName = entryName; + return this; + } + + public ZookeeperClient withSessionTimeout(int timeoutMS) { + this.sessionTimeout = timeoutMS; + return this; + } + + public ZookeeperClient withConnectionTimeout(int timeoutMS) { + this.connectionTimeout = timeoutMS; + return this; + } + + public ZookeeperClient withRetryPolicy(RetryPolicy policy) { + this.retryPolicy = policy; + return this; + } + + public ZookeeperClient withZookeeperFactory(ZookeeperFactory factory) { + this.zkFactory = factory; + return this; + } + + public ZookeeperClient enableSSL(boolean enable) { + this.isSSLEnabled = enable; + return this; + } + + public ZookeeperClient withKeystore(String keystorePath) { + this.keystoreLocation = keystorePath; + return this; + } + + public ZookeeperClient withKeystorePassword(String keystorePass) { + this.keystorePassword = keystorePass; + return this; + } + + public ZookeeperClient withTruststore(String truststorePath) { + this.truststoreLocation = truststorePath; + return this; + } + + public ZookeeperClient withTruststorePassword(String truststorePass) { + this.truststorePassword = truststorePass; + return this; + } + + public CuratorFramework create() { + checkNotNull(connectionString, "Zookeeper connection string cannot be null!"); + checkNotNull(retryPolicy, "Zookeeper connection retry policy cannot be null!"); + + return createFrameworkFactoryBuilder() + .connectString(connectionString) + .zookeeperFactory(zkFactory) + .namespace(namespace) + .sessionTimeoutMs(sessionTimeout) + .connectionTimeoutMs(connectionTimeout) + .retryPolicy(retryPolicy) + .aclProvider(aclProvider()) + .zkClientConfig(zkClientConfig()) + .build(); + } + + CuratorFrameworkFactory.Builder createFrameworkFactoryBuilder() { + return CuratorFrameworkFactory.builder(); + } + + private ACLProvider aclProvider() { + // AuthType has to be explicitly set to 'none' or 'sasl' + checkNotNull(authenticationType, "Zookeeper authType cannot be null!"); + checkArgument(authenticationType.equals("sasl") || authenticationType.equals("none"), + "Zookeeper authType must be one of [none, sasl]!"); + + ACLProvider aclProvider; + if (authenticationType.equals("sasl")) { + LOG.info("Connecting to ZooKeeper with SASL/Kerberos and using 'sasl' ACLs."); + + checkArgument(!isEmpty(keytab), "Zookeeper client's Kerberos Keytab must be specified!"); + checkArgument(!isEmpty(principal), + "Zookeeper client's Kerberos Principal must be specified!"); + checkArgument(!isEmpty(jaasLoginEntryName), "JAAS Login Entry name must be specified!"); + + JaasConfiguration jConf = new JaasConfiguration(jaasLoginEntryName, principal, keytab); + Configuration.setConfiguration(jConf); + System.setProperty(ZKClientConfig.LOGIN_CONTEXT_NAME_KEY, jaasLoginEntryName); + System.setProperty("zookeeper.authProvider.1", + "org.apache.zookeeper.server.auth.SASLAuthenticationProvider"); + aclProvider = new SASLOwnerACLProvider(principal.split("[/@]")[0]); + } else { // "none" + LOG.info("Connecting to ZooKeeper without authentication."); + aclProvider = new DefaultACLProvider(); // open to everyone + } + return aclProvider; + } + + private ZKClientConfig zkClientConfig() { + ZKClientConfig zkClientConfig = new ZKClientConfig(); + if (isSSLEnabled){ + LOG.info("Zookeeper client will use SSL connection. (keystore = {}; truststore = {};)", + keystoreLocation, truststoreLocation); + checkArgument(!isEmpty(keystoreLocation), + "The keystore location parameter is empty for the ZooKeeper client connection."); + checkArgument(!isEmpty(truststoreLocation), + "The truststore location parameter is empty for the ZooKeeper client connection."); + + try (ClientX509Util sslOpts = new ClientX509Util()) { + zkClientConfig.setProperty(ZKClientConfig.SECURE_CLIENT, "true"); + zkClientConfig.setProperty(ZKClientConfig.ZOOKEEPER_CLIENT_CNXN_SOCKET, + "org.apache.zookeeper.ClientCnxnSocketNetty"); + zkClientConfig.setProperty(sslOpts.getSslKeystoreLocationProperty(), keystoreLocation); + zkClientConfig.setProperty(sslOpts.getSslKeystorePasswdProperty(), keystorePassword); + zkClientConfig.setProperty(sslOpts.getSslTruststoreLocationProperty(), truststoreLocation); + zkClientConfig.setProperty(sslOpts.getSslTruststorePasswdProperty(), truststorePassword); + } + } else { + LOG.info("Zookeeper client will use Plain connection."); + } + return zkClientConfig; + } + + /** + * Simple implementation of an {@link ACLProvider} that simply returns an ACL + * that gives all permissions only to a single principal. + */ + static final class SASLOwnerACLProvider implements ACLProvider { + + private final List saslACL; + + private SASLOwnerACLProvider(String principal) { + this.saslACL = Collections.singletonList( + new ACL(ZooDefs.Perms.ALL, new Id("sasl", principal))); + } + + @Override + public List getDefaultAcl() { + return saslACL; + } + + @Override + public List getAclForPath(String path) { + return saslACL; + } + } + + private boolean isEmpty(String str) { + return str == null || str.length() == 0; + } + + //Preconditions allowed to be imported from hadoop-common, but that results + // in a circular dependency + private void checkNotNull(Object reference, String errorMessage) { + if (reference == null) { + throw new NullPointerException(errorMessage); + } + } + + private void checkArgument(boolean expression, String errorMessage) { + if (!expression) { + throw new IllegalArgumentException(errorMessage); + } + } +} diff --git a/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/util/HttpExceptionUtils.java b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/util/HttpExceptionUtils.java new file mode 100644 index 000000000000..d0725106ac9e --- /dev/null +++ b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/util/HttpExceptionUtils.java @@ -0,0 +1,88 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.util; + +import org.apache.hadoop.util.JsonSerialization; +import org.apache.yetus.audience.InterfaceAudience; +import org.apache.yetus.audience.InterfaceStability; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.Writer; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * HTTP utility class to help propagate server side exception to the client + * over HTTP as a JSON payload. + *

+ * It creates HTTP Servlet and JAX-RPC error responses including details of the + * exception that allows a client to recreate the remote exception. + *

+ * It parses HTTP client connections and recreates the exception. + */ +@InterfaceAudience.Private +@InterfaceStability.Unstable +public class HttpExceptionUtils { + + public static final String ERROR_JSON = "RemoteException"; + public static final String ERROR_EXCEPTION_JSON = "exception"; + public static final String ERROR_CLASSNAME_JSON = "javaClassName"; + public static final String ERROR_MESSAGE_JSON = "message"; + + private static final String APPLICATION_JSON_MIME = "application/json"; + + private static final String ENTER = System.getProperty("line.separator"); + + /** + * Creates a HTTP servlet response serializing the exception in it as JSON. + * + * @param response the servlet response + * @param status the error code to set in the response + * @param ex the exception to serialize in the response + * @throws IOException thrown if there was an error while creating the + * response + */ + public static void createServletExceptionResponse( + HttpServletResponse response, int status, Throwable ex) + throws IOException { + response.setStatus(status); + response.setContentType(APPLICATION_JSON_MIME); + Map json = new LinkedHashMap(); + json.put(ERROR_MESSAGE_JSON, getOneLineMessage(ex)); + json.put(ERROR_EXCEPTION_JSON, ex.getClass().getSimpleName()); + json.put(ERROR_CLASSNAME_JSON, ex.getClass().getName()); + Map jsonResponse = + Collections.singletonMap(ERROR_JSON, json); + Writer writer = response.getWriter(); + JsonSerialization.writer().writeValue(writer, jsonResponse); + writer.flush(); + } + + private static String getOneLineMessage(Throwable exception) { + String message = exception.getMessage(); + if (message != null) { + int i = message.indexOf(ENTER); + if (i > -1) { + message = message.substring(0, i); + } + } + return message; + } +} diff --git a/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/util/ServletUtil.java b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/util/ServletUtil.java new file mode 100644 index 000000000000..73e0abf4b736 --- /dev/null +++ b/hbase-auth-filters/src/main/java/org/apache/hadoop/hbase/util/ServletUtil.java @@ -0,0 +1,112 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.util; + +import java.io.*; +import java.util.Calendar; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; + +import org.apache.hbase.thirdparty.com.google.common.base.Preconditions; +import org.apache.yetus.audience.InterfaceAudience; +import org.apache.yetus.audience.InterfaceStability; + +@InterfaceAudience.Private +@InterfaceStability.Unstable +public class ServletUtil { + /** + * Initial HTML header. + * + * @param response response. + * @param title title. + * @throws IOException raised on errors performing I/O. + * @return PrintWriter. + */ + public static PrintWriter initHTML(ServletResponse response, String title + ) throws IOException { + response.setContentType("text/html"); + PrintWriter out = response.getWriter(); + out.println("\n" + + "\n" + + "" + title + "\n" + + "\n" + + "

" + title + "

\n"); + return out; + } + + /** + * Get a parameter from a ServletRequest. + * Return null if the parameter contains only white spaces. + * + * @param request request. + * @param name name. + * @return get a parameter from a ServletRequest. + */ + public static String getParameter(ServletRequest request, String name) { + String s = request.getParameter(name); + if (s == null) { + return null; + } + s = s.trim(); + return s.length() == 0? null: s; + } + + /** + * parseLongParam. + * + * @param request request. + * @param param param. + * @return a long value as passed in the given parameter, throwing + * an exception if it is not present or if it is not a valid number. + * @throws IOException raised on errors performing I/O. + */ + public static long parseLongParam(ServletRequest request, String param) + throws IOException { + String paramStr = request.getParameter(param); + if (paramStr == null) { + throw new IOException("Invalid request has no " + param + " parameter"); + } + + return Long.parseLong(paramStr); + } + + public static final String HTML_TAIL = "
\n" + + "Hadoop, " + + Calendar.getInstance().get(Calendar.YEAR) + ".\n" + + ""; + + /** + * HTML footer to be added in the jsps. + * @return the HTML footer. + */ + public static String htmlFooter() { + return HTML_TAIL; + } + + /** + * Parse the path component from the given request and return w/o decoding. + * @param request Http request to parse + * @param servletName the name of servlet that precedes the path + * @return path component, null if the default charset is not supported + */ + public static String getRawPath(final HttpServletRequest request, String servletName) { + Preconditions.checkArgument(request.getRequestURI().startsWith(servletName+"/")); + return request.getRequestURI().substring(servletName.length()); + } +} diff --git a/hbase-auth-filters/src/test/java/org/apache/hadoop/hbase/security/authentication/KerberosTestUtils.java b/hbase-auth-filters/src/test/java/org/apache/hadoop/hbase/security/authentication/KerberosTestUtils.java new file mode 100644 index 000000000000..2ff1feb84a73 --- /dev/null +++ b/hbase-auth-filters/src/test/java/org/apache/hadoop/hbase/security/authentication/KerberosTestUtils.java @@ -0,0 +1,140 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. See accompanying LICENSE file. + */ +package org.apache.hadoop.hbase.security.authentication; + +import javax.security.auth.Subject; +import javax.security.auth.kerberos.KerberosPrincipal; +import javax.security.auth.login.AppConfigurationEntry; +import javax.security.auth.login.Configuration; +import javax.security.auth.login.LoginContext; + +import org.apache.hadoop.security.authentication.util.KerberosUtil; + +import java.io.File; +import java.security.Principal; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.UUID; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Callable; + +import static org.apache.hadoop.util.PlatformName.IBM_JAVA; + +/** + * Test helper class for Java Kerberos setup. + */ +public class KerberosTestUtils { + private static String keytabFile = new File(System.getProperty("test.dir", "target"), + UUID.randomUUID().toString()).getAbsolutePath(); + + public static String getRealm() { + return "EXAMPLE.COM"; + } + + public static String getClientPrincipal() { + return "client@EXAMPLE.COM"; + } + + public static String getServerPrincipal() { + return "HTTP/localhost@EXAMPLE.COM"; + } + + public static String getKeytabFile() { + return keytabFile; + } + + private static class KerberosConfiguration extends Configuration { + private String principal; + + public KerberosConfiguration(String principal) { + this.principal = principal; + } + + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(String name) { + Map options = new HashMap(); + if (IBM_JAVA) { + options.put("useKeytab", KerberosTestUtils.getKeytabFile().startsWith("file://") ? + KerberosTestUtils.getKeytabFile() : "file://" + KerberosTestUtils.getKeytabFile()); + options.put("principal", principal); + options.put("refreshKrb5Config", "true"); + options.put("credsType", "both"); + } else { + options.put("keyTab", KerberosTestUtils.getKeytabFile()); + options.put("principal", principal); + options.put("useKeyTab", "true"); + options.put("storeKey", "true"); + options.put("doNotPrompt", "true"); + options.put("useTicketCache", "true"); + options.put("renewTGT", "true"); + options.put("refreshKrb5Config", "true"); + options.put("isInitiator", "true"); + } + String ticketCache = System.getenv("KRB5CCNAME"); + if (ticketCache != null) { + if (IBM_JAVA) { + // IBM JAVA only respect system property and not env variable + // The first value searched when "useDefaultCcache" is used. + System.setProperty("KRB5CCNAME", ticketCache); + options.put("useDefaultCcache", "true"); + options.put("renewTGT", "true"); + } else { + options.put("ticketCache", ticketCache); + } + } + options.put("debug", "true"); + + return new AppConfigurationEntry[]{ + new AppConfigurationEntry(KerberosUtil.getKrb5LoginModuleName(), + AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, + options),}; + } + } + + public static T doAs(String principal, final Callable callable) throws Exception { + LoginContext loginContext = null; + try { + Set principals = new HashSet<>(); + principals.add(new KerberosPrincipal(KerberosTestUtils.getClientPrincipal())); + Subject subject = new Subject(false, principals, new HashSet<>(), new HashSet<>()); + loginContext = new LoginContext("", subject, null, new KerberosConfiguration(principal)); + loginContext.login(); + subject = loginContext.getSubject(); + return Subject.doAs(subject, new PrivilegedExceptionAction() { + @Override + public T run() throws Exception { + return callable.call(); + } + }); + } catch (PrivilegedActionException ex) { + throw ex.getException(); + } finally { + if (loginContext != null) { + loginContext.logout(); + } + } + } + + public static T doAsClient(Callable callable) throws Exception { + return doAs(getClientPrincipal(), callable); + } + + public static T doAsServer(Callable callable) throws Exception { + return doAs(getServerPrincipal(), callable); + } + +} diff --git a/hbase-auth-filters/src/test/java/org/apache/hadoop/hbase/security/authentication/client/AuthenticatorTestCase.java b/hbase-auth-filters/src/test/java/org/apache/hadoop/hbase/security/authentication/client/AuthenticatorTestCase.java new file mode 100644 index 000000000000..1f69af369fbb --- /dev/null +++ b/hbase-auth-filters/src/test/java/org/apache/hadoop/hbase/security/authentication/client/AuthenticatorTestCase.java @@ -0,0 +1,275 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. See accompanying LICENSE file. + */ +package org.apache.hadoop.hbase.security.authentication.client; + +import org.apache.hadoop.hbase.security.authentication.server.AuthenticationFilter; +import org.apache.http.HttpResponse; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.Credentials; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.entity.InputStreamEntity; +import org.apache.http.impl.auth.SPNegoScheme; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.util.EntityUtils; +import org.apache.hbase.thirdparty.org.eclipse.jetty.server.Connector; +import org.apache.hbase.thirdparty.org.eclipse.jetty.server.Server; +import org.apache.hbase.thirdparty.org.eclipse.jetty.server.ServerConnector; +import org.apache.hbase.thirdparty.org.eclipse.jetty.ee8.servlet.FilterHolder; +import org.apache.hbase.thirdparty.org.eclipse.jetty.ee8.servlet.ServletContextHandler; +import org.apache.hbase.thirdparty.org.eclipse.jetty.ee8.servlet.ServletHolder; + +import javax.servlet.DispatcherType; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.InputStreamReader; +import java.io.Writer; +import java.net.HttpURLConnection; +import java.net.ServerSocket; +import java.net.URL; +import java.security.Principal; +import java.util.EnumSet; +import java.util.Properties; + +import org.junit.Test; +import static org.junit.Assert.*; +import org.junit.ClassRule; +import org.junit.experimental.categories.Category; +import org.apache.hadoop.hbase.testclassification.MiscTests; +import org.apache.hadoop.hbase.testclassification.SmallTests; +import org.apache.hadoop.hbase.HBaseClassTestRule; +import org.apache.hadoop.security.authentication.client.AuthenticatedURL; +import org.apache.hadoop.security.authentication.client.Authenticator; +import org.apache.hadoop.security.authentication.client.ConnectionConfigurator; + +public class AuthenticatorTestCase { + private Server server; + private String host = null; + private int port = -1; + ServletContextHandler context; + + private static Properties authenticatorConfig; + + public AuthenticatorTestCase() {} + + protected static void setAuthenticationHandlerConfig(Properties config) { + authenticatorConfig = config; + } + + public static class TestFilter extends AuthenticationFilter { + + @Override + protected Properties getConfiguration(String configPrefix, FilterConfig filterConfig) throws ServletException { + return authenticatorConfig; + } + } + + @SuppressWarnings("serial") + public static class TestServlet extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.setStatus(HttpServletResponse.SC_OK); + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + InputStream is = req.getInputStream(); + OutputStream os = resp.getOutputStream(); + int c = is.read(); + while (c > -1) { + os.write(c); + c = is.read(); + } + is.close(); + os.close(); + resp.setStatus(HttpServletResponse.SC_OK); + } + } + + protected int getLocalPort() throws Exception { + ServerSocket ss = new ServerSocket(0); + int ret = ss.getLocalPort(); + ss.close(); + return ret; + } + + protected void start() throws Exception { + startJetty(); + } + + protected void startJetty() throws Exception { + server = new Server(); + context = new ServletContextHandler(); + context.setContextPath("/foo"); + server.setHandler(context); + context.addFilter(new FilterHolder(TestFilter.class), "/*", + EnumSet.of(DispatcherType.REQUEST)); + context.addServlet(new ServletHolder(TestServlet.class), "/bar"); + host = "localhost"; + port = getLocalPort(); + ServerConnector connector = new ServerConnector(server); + connector.setHost(host); + connector.setPort(port); + server.setConnectors(new Connector[] {connector}); + server.start(); + System.out.println("Running embedded servlet container at: http://" + host + ":" + port); + } + + protected void stop() throws Exception { + stopJetty(); + } + + protected void stopJetty() throws Exception { + try { + server.stop(); + } catch (Exception e) { + } + + try { + server.destroy(); + } catch (Exception e) { + } + } + + protected String getBaseURL() { + return "http://" + host + ":" + port + "/foo/bar"; + } + + private static class TestConnectionConfigurator + implements ConnectionConfigurator { + boolean invoked; + + @Override + public HttpURLConnection configure(HttpURLConnection conn) + throws IOException { + invoked = true; + return conn; + } + } + + private String POST = "test"; + + protected void _testAuthentication(Authenticator authenticator, boolean doPost) throws Exception { + start(); + try { + URL url = new URL(getBaseURL()); + AuthenticatedURL.Token token = new AuthenticatedURL.Token(); + assertFalse(token.isSet()); + TestConnectionConfigurator connConf = new TestConnectionConfigurator(); + AuthenticatedURL aUrl = new AuthenticatedURL(authenticator, connConf); + HttpURLConnection conn = aUrl.openConnection(url, token); + assertTrue(connConf.invoked); + String tokenStr = token.toString(); + if (doPost) { + conn.setRequestMethod("POST"); + conn.setDoOutput(true); + } + conn.connect(); + if (doPost) { + Writer writer = new OutputStreamWriter(conn.getOutputStream()); + writer.write(POST); + writer.close(); + } + assertEquals(HttpURLConnection.HTTP_OK, conn.getResponseCode()); + if (doPost) { + BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream())); + String echo = reader.readLine(); + assertEquals(POST, echo); + assertNull(reader.readLine()); + } + aUrl = new AuthenticatedURL(); + conn = aUrl.openConnection(url, token); + conn.connect(); + assertEquals(HttpURLConnection.HTTP_OK, conn.getResponseCode()); + assertEquals(tokenStr, token.toString()); + } finally { + stop(); + } + } + + private HttpClient getHttpClient() { + HttpClientBuilder builder = HttpClientBuilder.create(); + // Register auth schema + builder.setDefaultAuthSchemeRegistry( + s-> httpContext -> new SPNegoScheme(true, true) + ); + + Credentials useJaasCreds = new Credentials() { + public String getPassword() { + return null; + } + public Principal getUserPrincipal() { + return null; + } + }; + + CredentialsProvider jaasCredentialProvider + = new BasicCredentialsProvider(); + jaasCredentialProvider.setCredentials(AuthScope.ANY, useJaasCreds); + // Set credential provider + builder.setDefaultCredentialsProvider(jaasCredentialProvider); + + return builder.build(); + } + + private void doHttpClientRequest(HttpClient httpClient, HttpUriRequest request) throws Exception { + HttpResponse response = null; + try { + response = httpClient.execute(request); + final int httpStatus = response.getStatusLine().getStatusCode(); + assertEquals(HttpURLConnection.HTTP_OK, httpStatus); + } finally { + if (response != null) EntityUtils.consumeQuietly(response.getEntity()); + } + } + + protected void _testAuthenticationHttpClient(Authenticator authenticator, boolean doPost) throws Exception { + start(); + try { + HttpClient httpClient = getHttpClient(); + doHttpClientRequest(httpClient, new HttpGet(getBaseURL())); + + // Always do a GET before POST to trigger the SPNego negotiation + if (doPost) { + HttpPost post = new HttpPost(getBaseURL()); + byte [] postBytes = POST.getBytes(); + ByteArrayInputStream bis = new ByteArrayInputStream(postBytes); + InputStreamEntity entity = new InputStreamEntity(bis, postBytes.length); + + // Important that the entity is not repeatable -- this means if + // we have to renegotiate (e.g. b/c the cookie wasn't handled properly) + // the test will fail. + assertFalse(entity.isRepeatable()); + post.setEntity(entity); + doHttpClientRequest(httpClient, post); + } + } finally { + stop(); + } + } +} diff --git a/hbase-auth-filters/src/test/java/org/apache/hadoop/hbase/security/authentication/client/TestKerberosAuthenticator.java b/hbase-auth-filters/src/test/java/org/apache/hadoop/hbase/security/authentication/client/TestKerberosAuthenticator.java new file mode 100644 index 000000000000..8dd607318a3a --- /dev/null +++ b/hbase-auth-filters/src/test/java/org/apache/hadoop/hbase/security/authentication/client/TestKerberosAuthenticator.java @@ -0,0 +1,320 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. See accompanying LICENSE file. + */ +package org.apache.hadoop.hbase.security.authentication.client; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.apache.hadoop.hbase.security.authentication.server.MultiSchemeAuthenticationHandler.SCHEMES_PROPERTY; +import static org.apache.hadoop.hbase.security.authentication.server.MultiSchemeAuthenticationHandler.AUTH_HANDLER_PROPERTY; +import static org.apache.hadoop.hbase.security.authentication.server.AuthenticationFilter.AUTH_TYPE; +import static org.apache.hadoop.hbase.security.authentication.server.KerberosAuthenticationHandler.PRINCIPAL; +import static org.apache.hadoop.hbase.security.authentication.server.KerberosAuthenticationHandler.KEYTAB; +import static org.apache.hadoop.hbase.security.authentication.server.KerberosAuthenticationHandler.NAME_RULES; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.charset.CharacterCodingException; +import javax.security.sasl.AuthenticationException; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.hadoop.minikdc.KerberosSecurityTestcase; +import org.apache.hadoop.security.authentication.client.KerberosAuthenticator; +import org.apache.hadoop.hbase.security.authentication.KerberosTestUtils; +import org.apache.hadoop.hbase.security.authentication.server.AuthenticationFilter; +import org.apache.hadoop.hbase.security.authentication.server.MultiSchemeAuthenticationHandler; +import org.apache.hadoop.hbase.security.authentication.server.PseudoAuthenticationHandler; +import org.apache.hadoop.hbase.security.authentication.server.KerberosAuthenticationHandler; + +import java.io.File; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Arrays; +import java.util.Properties; +import java.util.concurrent.Callable; + +import org.junit.Test; +import static org.junit.Assert.*; +import org.junit.ClassRule; +import org.junit.experimental.categories.Category; +import org.apache.hadoop.hbase.testclassification.MiscTests; +import org.apache.hadoop.hbase.testclassification.SmallTests; +import org.apache.hadoop.hbase.HBaseClassTestRule; +import org.junit.Before; + +/** + * Test class for {@link KerberosAuthenticator}. + */ +@Category({ MiscTests.class, SmallTests.class }) +public class TestKerberosAuthenticator extends KerberosSecurityTestcase { + @ClassRule + public static final HBaseClassTestRule CLASS_RULE = + HBaseClassTestRule.forClass(TestKerberosAuthenticator.class); + + + public TestKerberosAuthenticator() { + } + + @Before + public void setup() throws Exception { + // create keytab + File keytabFile = new File(KerberosTestUtils.getKeytabFile()); + String clientPrincipal = KerberosTestUtils.getClientPrincipal(); + String serverPrincipal = KerberosTestUtils.getServerPrincipal(); + clientPrincipal = clientPrincipal.substring(0, clientPrincipal.lastIndexOf("@")); + serverPrincipal = serverPrincipal.substring(0, serverPrincipal.lastIndexOf("@")); + getKdc().createPrincipal(keytabFile, clientPrincipal, serverPrincipal); + } + + private Properties getAuthenticationHandlerConfiguration() { + Properties props = new Properties(); + props.setProperty(AuthenticationFilter.AUTH_TYPE, "kerberos"); + props.setProperty(KerberosAuthenticationHandler.PRINCIPAL, KerberosTestUtils.getServerPrincipal()); + props.setProperty(KerberosAuthenticationHandler.KEYTAB, KerberosTestUtils.getKeytabFile()); + props.setProperty(KerberosAuthenticationHandler.NAME_RULES, + "RULE:[1:$1@$0](.*@" + KerberosTestUtils.getRealm()+")s/@.*//\n"); + props.setProperty(KerberosAuthenticationHandler.RULE_MECHANISM, "hadoop"); + return props; + } + + private Properties getMultiAuthHandlerConfiguration() { + Properties props = new Properties(); + props.setProperty(AUTH_TYPE, MultiSchemeAuthenticationHandler.TYPE); + props.setProperty(SCHEMES_PROPERTY, "negotiate"); + props.setProperty(String.format(AUTH_HANDLER_PROPERTY, "negotiate"), + "kerberos"); + props.setProperty(PRINCIPAL, KerberosTestUtils.getServerPrincipal()); + props.setProperty(KEYTAB, KerberosTestUtils.getKeytabFile()); + props.setProperty(NAME_RULES, + "RULE:[1:$1@$0](.*@" + KerberosTestUtils.getRealm() + ")s/@.*//\n"); + return props; + } + + @Test(timeout = 60000) + public void testFallbacktoPseudoAuthenticator() throws Exception { + AuthenticatorTestCase auth = new AuthenticatorTestCase(); + Properties props = new Properties(); + props.setProperty(AuthenticationFilter.AUTH_TYPE, "simple"); + props.setProperty(PseudoAuthenticationHandler.ANONYMOUS_ALLOWED, "false"); + AuthenticatorTestCase.setAuthenticationHandlerConfig(props); + auth._testAuthentication(new KerberosAuthenticator(), false); + } + + @Test(timeout = 60000) + public void testFallbacktoPseudoAuthenticatorAnonymous() throws Exception { + AuthenticatorTestCase auth = new AuthenticatorTestCase(); + Properties props = new Properties(); + props.setProperty(AuthenticationFilter.AUTH_TYPE, "simple"); + props.setProperty(PseudoAuthenticationHandler.ANONYMOUS_ALLOWED, "true"); + AuthenticatorTestCase.setAuthenticationHandlerConfig(props); + auth._testAuthentication(new KerberosAuthenticator(), false); + } + + @Test(timeout = 60000) + public void testNotAuthenticated() throws Exception { + AuthenticatorTestCase auth = new AuthenticatorTestCase(); + AuthenticatorTestCase.setAuthenticationHandlerConfig(getAuthenticationHandlerConfiguration()); + auth.start(); + try { + URL url = new URL(auth.getBaseURL()); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.connect(); + assertEquals(HttpURLConnection.HTTP_UNAUTHORIZED, conn.getResponseCode()); + assertTrue(conn.getHeaderField(KerberosAuthenticator.WWW_AUTHENTICATE) != null); + } finally { + auth.stop(); + } + } + + @Test(timeout = 60000) + public void testAuthentication() throws Exception { + final AuthenticatorTestCase auth = new AuthenticatorTestCase(); + AuthenticatorTestCase.setAuthenticationHandlerConfig( + getAuthenticationHandlerConfiguration()); + KerberosTestUtils.doAsClient(new Callable() { + @Override + public Void call() throws Exception { + auth._testAuthentication(new KerberosAuthenticator(), false); + return null; + } + }); + } + + @Test(timeout = 60000) + public void testAuthenticationPost() throws Exception { + final AuthenticatorTestCase auth = new AuthenticatorTestCase(); + AuthenticatorTestCase.setAuthenticationHandlerConfig( + getAuthenticationHandlerConfiguration()); + KerberosTestUtils.doAsClient(new Callable() { + @Override + public Void call() throws Exception { + auth._testAuthentication(new KerberosAuthenticator(), true); + return null; + } + }); + } + + @Test(timeout = 60000) + public void testAuthenticationHttpClient() throws Exception { + final AuthenticatorTestCase auth = new AuthenticatorTestCase(); + AuthenticatorTestCase.setAuthenticationHandlerConfig( + getAuthenticationHandlerConfiguration()); + KerberosTestUtils.doAsClient(new Callable() { + @Override + public Void call() throws Exception { + auth._testAuthenticationHttpClient(new KerberosAuthenticator(), false); + return null; + } + }); + } + + @Test(timeout = 60000) + public void testAuthenticationHttpClientPost() throws Exception { + final AuthenticatorTestCase auth = new AuthenticatorTestCase(); + AuthenticatorTestCase.setAuthenticationHandlerConfig( + getAuthenticationHandlerConfiguration()); + KerberosTestUtils.doAsClient(new Callable() { + @Override + public Void call() throws Exception { + auth._testAuthenticationHttpClient(new KerberosAuthenticator(), true); + return null; + } + }); + } + + @Test(timeout = 60000) + public void testNotAuthenticatedWithMultiAuthHandler() throws Exception { + AuthenticatorTestCase auth = new AuthenticatorTestCase(); + AuthenticatorTestCase + .setAuthenticationHandlerConfig(getMultiAuthHandlerConfiguration()); + auth.start(); + try { + URL url = new URL(auth.getBaseURL()); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.connect(); + assertEquals(HttpURLConnection.HTTP_UNAUTHORIZED, + conn.getResponseCode()); + assertTrue(conn + .getHeaderField(KerberosAuthenticator.WWW_AUTHENTICATE) != null); + } finally { + auth.stop(); + } + } + + @Test(timeout = 60000) + public void testAuthenticationWithMultiAuthHandler() throws Exception { + final AuthenticatorTestCase auth = new AuthenticatorTestCase(); + AuthenticatorTestCase + .setAuthenticationHandlerConfig(getMultiAuthHandlerConfiguration()); + KerberosTestUtils.doAsClient(new Callable() { + @Override + public Void call() throws Exception { + auth._testAuthentication(new KerberosAuthenticator(), false); + return null; + } + }); + } + + @Test(timeout = 60000) + public void testAuthenticationHttpClientPostWithMultiAuthHandler() + throws Exception { + final AuthenticatorTestCase auth = new AuthenticatorTestCase(); + AuthenticatorTestCase + .setAuthenticationHandlerConfig(getMultiAuthHandlerConfiguration()); + KerberosTestUtils.doAsClient(new Callable() { + @Override + public Void call() throws Exception { + auth._testAuthenticationHttpClient(new KerberosAuthenticator(), true); + return null; + } + }); + } + + @Test(timeout = 60000) + public void testNegotiate() throws NoSuchMethodException, InvocationTargetException, + IllegalAccessException, IOException { + KerberosAuthenticator kerberosAuthenticator = new KerberosAuthenticator(); + + HttpURLConnection conn = mock(HttpURLConnection.class); + when(conn.getHeaderField(KerberosAuthenticator.WWW_AUTHENTICATE)). + thenReturn(KerberosAuthenticator.NEGOTIATE); + when(conn.getResponseCode()).thenReturn(HttpURLConnection.HTTP_UNAUTHORIZED); + + Method method = KerberosAuthenticator.class.getDeclaredMethod("isNegotiate", + HttpURLConnection.class); + method.setAccessible(true); + + assertTrue((boolean)method.invoke(kerberosAuthenticator, conn)); + } + + @Test(timeout = 60000) + public void testNegotiateLowerCase() throws NoSuchMethodException, InvocationTargetException, + IllegalAccessException, IOException { + KerberosAuthenticator kerberosAuthenticator = new KerberosAuthenticator(); + + HttpURLConnection conn = mock(HttpURLConnection.class); + when(conn.getHeaderField("www-authenticate")) + .thenReturn(KerberosAuthenticator.NEGOTIATE); + when(conn.getResponseCode()).thenReturn(HttpURLConnection.HTTP_UNAUTHORIZED); + + Method method = KerberosAuthenticator.class.getDeclaredMethod("isNegotiate", + HttpURLConnection.class); + method.setAccessible(true); + + assertTrue((boolean)method.invoke(kerberosAuthenticator, conn)); + } + + @Test(timeout = 60000) + public void testReadToken() throws NoSuchMethodException, IOException, IllegalAccessException, + InvocationTargetException { + KerberosAuthenticator kerberosAuthenticator = new KerberosAuthenticator(); + FieldUtils.writeField(kerberosAuthenticator, "base64", new Base64(), true); + + Base64 base64 = new Base64(); + + HttpURLConnection conn = mock(HttpURLConnection.class); + when(conn.getResponseCode()).thenReturn(HttpURLConnection.HTTP_UNAUTHORIZED); + when(conn.getHeaderField(KerberosAuthenticator.WWW_AUTHENTICATE)) + .thenReturn(KerberosAuthenticator.NEGOTIATE + " " + + Arrays.toString(base64.encode("foobar".getBytes()))); + + Method method = KerberosAuthenticator.class.getDeclaredMethod("readToken", + HttpURLConnection.class); + method.setAccessible(true); + + method.invoke(kerberosAuthenticator, conn); // expecting this not to throw an exception + } + + @Test(timeout = 60000) + public void testReadTokenLowerCase() throws NoSuchMethodException, IOException, + IllegalAccessException, InvocationTargetException { + KerberosAuthenticator kerberosAuthenticator = new KerberosAuthenticator(); + FieldUtils.writeField(kerberosAuthenticator, "base64", new Base64(), true); + + Base64 base64 = new Base64(); + + HttpURLConnection conn = mock(HttpURLConnection.class); + when(conn.getResponseCode()).thenReturn(HttpURLConnection.HTTP_UNAUTHORIZED); + when(conn.getHeaderField("www-authenticate")) + .thenReturn(KerberosAuthenticator.NEGOTIATE + + Arrays.toString(base64.encode("foobar".getBytes()))); + + Method method = KerberosAuthenticator.class.getDeclaredMethod("readToken", + HttpURLConnection.class); + method.setAccessible(true); + + method.invoke(kerberosAuthenticator, conn); // expecting this not to throw an exception + } +} diff --git a/hbase-auth-filters/src/test/java/org/apache/hadoop/hbase/security/authentication/client/TestPseudoAuthenticator.java b/hbase-auth-filters/src/test/java/org/apache/hadoop/hbase/security/authentication/client/TestPseudoAuthenticator.java new file mode 100644 index 000000000000..1ab04e65b707 --- /dev/null +++ b/hbase-auth-filters/src/test/java/org/apache/hadoop/hbase/security/authentication/client/TestPseudoAuthenticator.java @@ -0,0 +1,120 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. See accompanying LICENSE file. + */ +package org.apache.hadoop.hbase.security.authentication.client; + +import org.apache.hadoop.security.authentication.client.PseudoAuthenticator; +import org.apache.hadoop.hbase.security.authentication.server.AuthenticationFilter; +import org.apache.hadoop.hbase.security.authentication.server.PseudoAuthenticationHandler; + +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Properties; + +import org.junit.Test; +import static org.junit.Assert.*; +import org.junit.ClassRule; +import org.junit.experimental.categories.Category; +import org.apache.hadoop.hbase.testclassification.MiscTests; +import org.apache.hadoop.hbase.testclassification.SmallTests; +import org.apache.hadoop.hbase.HBaseClassTestRule; + +@Category({ MiscTests.class, SmallTests.class }) +public class TestPseudoAuthenticator { + @ClassRule + public static final HBaseClassTestRule CLASS_RULE = + HBaseClassTestRule.forClass(TestPseudoAuthenticator.class); + + private Properties getAuthenticationHandlerConfiguration(boolean anonymousAllowed) { + Properties props = new Properties(); + props.setProperty(AuthenticationFilter.AUTH_TYPE, "simple"); + props.setProperty(PseudoAuthenticationHandler.ANONYMOUS_ALLOWED, Boolean.toString(anonymousAllowed)); + return props; + } + + @Test + public void testGetUserName() throws Exception { + PseudoAuthenticator authenticator = new PseudoAuthenticator(); + // TODO getUserName() has protected access + // assertEquals(System.getProperty("user.name"), authenticator.getUserName()); + } + + @Test + public void testAnonymousAllowed() throws Exception { + AuthenticatorTestCase auth = new AuthenticatorTestCase(); + AuthenticatorTestCase.setAuthenticationHandlerConfig( + getAuthenticationHandlerConfiguration(true)); + auth.start(); + try { + URL url = new URL(auth.getBaseURL()); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.connect(); + assertEquals(HttpURLConnection.HTTP_OK, conn.getResponseCode()); + } finally { + auth.stop(); + } + } + + @Test + public void testAnonymousDisallowed() throws Exception { + AuthenticatorTestCase auth = new AuthenticatorTestCase(); + AuthenticatorTestCase.setAuthenticationHandlerConfig( + getAuthenticationHandlerConfiguration(false)); + auth.start(); + try { + URL url = new URL(auth.getBaseURL()); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.connect(); + assertEquals(HttpURLConnection.HTTP_UNAUTHORIZED, conn.getResponseCode()); + assertTrue(conn.getHeaderFields().containsKey("WWW-Authenticate")); + //assertEquals("Authentication required", conn.getResponseMessage()); + // TODO: Was support to be "Authentication required" + assertEquals("Unauthorized", conn.getResponseMessage()); + } finally { + auth.stop(); + } + } + + @Test + public void testAuthenticationAnonymousAllowed() throws Exception { + AuthenticatorTestCase auth = new AuthenticatorTestCase(); + AuthenticatorTestCase.setAuthenticationHandlerConfig( + getAuthenticationHandlerConfiguration(true)); + auth._testAuthentication(new PseudoAuthenticator(), false); + } + + @Test + public void testAuthenticationAnonymousDisallowed() throws Exception { + AuthenticatorTestCase auth = new AuthenticatorTestCase(); + AuthenticatorTestCase.setAuthenticationHandlerConfig( + getAuthenticationHandlerConfiguration(false)); + auth._testAuthentication(new PseudoAuthenticator(), false); + } + + @Test + public void testAuthenticationAnonymousAllowedWithPost() throws Exception { + AuthenticatorTestCase auth = new AuthenticatorTestCase(); + AuthenticatorTestCase.setAuthenticationHandlerConfig( + getAuthenticationHandlerConfiguration(true)); + auth._testAuthentication(new PseudoAuthenticator(), true); + } + + @Test + public void testAuthenticationAnonymousDisallowedWithPost() throws Exception { + AuthenticatorTestCase auth = new AuthenticatorTestCase(); + AuthenticatorTestCase.setAuthenticationHandlerConfig( + getAuthenticationHandlerConfiguration(false)); + auth._testAuthentication(new PseudoAuthenticator(), true); + } + +} diff --git a/hbase-auth-filters/src/test/java/org/apache/hadoop/hbase/security/authentication/server/TestAuthenticationFilter.java b/hbase-auth-filters/src/test/java/org/apache/hadoop/hbase/security/authentication/server/TestAuthenticationFilter.java new file mode 100644 index 000000000000..7ba0ea1c4672 --- /dev/null +++ b/hbase-auth-filters/src/test/java/org/apache/hadoop/hbase/security/authentication/server/TestAuthenticationFilter.java @@ -0,0 +1,1332 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. See accompanying LICENSE file. + */ +package org.apache.hadoop.hbase.security.authentication.server; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.Writer; +import java.net.HttpCookie; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Vector; + +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.apache.hadoop.security.authentication.client.AuthenticatedURL; +import org.apache.hadoop.security.authentication.client.AuthenticationException; +import org.apache.hadoop.hbase.security.authentication.server.AuthenticationToken; +import org.apache.hadoop.hbase.security.authentication.server.AuthenticationFilter; +import org.apache.hadoop.hbase.security.authentication.util.Signer; +import org.apache.hadoop.hbase.security.authentication.util.SignerSecretProvider; +import org.apache.hadoop.hbase.security.authentication.util.StringSignerSecretProviderCreator; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.reset; + +import org.junit.Test; +import static org.junit.Assert.*; +import org.junit.ClassRule; +import org.junit.experimental.categories.Category; +import org.apache.hadoop.hbase.testclassification.MiscTests; +import org.apache.hadoop.hbase.testclassification.SmallTests; +import org.apache.hadoop.hbase.HBaseClassTestRule; + +@Category({ MiscTests.class, SmallTests.class }) +public class TestAuthenticationFilter { + @ClassRule + public static final HBaseClassTestRule CLASS_RULE = + HBaseClassTestRule.forClass(TestAuthenticationFilter.class); + + private static final long TOKEN_VALIDITY_SEC = 1000; + private static final long TOKEN_MAX_INACTIVE_INTERVAL = 1000; + + @Test + public void testGetConfiguration() throws Exception { + AuthenticationFilter filter = new AuthenticationFilter(); + FilterConfig config = mock(FilterConfig.class); + when(config.getInitParameter(AuthenticationFilter.CONFIG_PREFIX)).thenReturn(""); + when(config.getInitParameter("a")).thenReturn("A"); + when(config.getInitParameterNames()).thenReturn( + new Vector(Arrays.asList("a")).elements()); + Properties props = filter.getConfiguration("", config); + assertEquals("A", props.getProperty("a")); + + config = mock(FilterConfig.class); + when(config.getInitParameter(AuthenticationFilter.CONFIG_PREFIX)).thenReturn("foo"); + when(config.getInitParameter("foo.a")).thenReturn("A"); + when(config.getInitParameterNames()).thenReturn( + new Vector(Arrays.asList("foo.a")).elements()); + props = filter.getConfiguration("foo.", config); + assertEquals("A", props.getProperty("a")); + } + + @Test + public void testInitEmpty() throws Exception { + AuthenticationFilter filter = new AuthenticationFilter(); + try { + FilterConfig config = mock(FilterConfig.class); + when(config.getInitParameterNames()).thenReturn(new Vector().elements()); + filter.init(config); + fail(); + } catch (ServletException ex) { + // Expected + assertEquals("Authentication type must be specified: simple|kerberos|", + ex.getMessage()); + } catch (Exception ex) { + fail(); + } finally { + filter.destroy(); + } + } + + public static class DummyAuthenticationHandler implements AuthenticationHandler { + public static boolean init; + public static boolean managementOperationReturn; + public static boolean destroy; + public static boolean expired; + + public static final String TYPE = "dummy"; + + public static void reset() { + init = false; + destroy = false; + } + + @Override + public void init(Properties config) throws ServletException { + init = true; + managementOperationReturn = + config.getProperty("management.operation.return", "true").equals("true"); + expired = config.getProperty("expired.token", "false").equals("true"); + } + + @Override + public boolean managementOperation(AuthenticationToken token, + HttpServletRequest request, + HttpServletResponse response) + throws IOException, AuthenticationException { + if (!managementOperationReturn) { + response.setStatus(HttpServletResponse.SC_ACCEPTED); + } + return managementOperationReturn; + } + + @Override + public void destroy() { + destroy = true; + } + + @Override + public String getType() { + return TYPE; + } + + @Override + public AuthenticationToken authenticate(HttpServletRequest request, HttpServletResponse response) + throws IOException, AuthenticationException { + AuthenticationToken token = null; + String param = request.getParameter("authenticated"); + if (param != null && param.equals("true")) { + token = new AuthenticationToken("u", "p", "t"); + token.setExpires((expired) ? 0 : System.currentTimeMillis() + TOKEN_VALIDITY_SEC); + } else { + if (request.getHeader("WWW-Authenticate") == null) { + response.setHeader("WWW-Authenticate", "dummyauth"); + } else { + throw new AuthenticationException("AUTH FAILED"); + } + } + return token; + } + } + + @Test + public void testFallbackToRandomSecretProvider() throws Exception { + // minimal configuration & simple auth handler (Pseudo) + AuthenticationFilter filter = new AuthenticationFilter(); + try { + FilterConfig config = mock(FilterConfig.class); + when(config.getInitParameter(AuthenticationFilter.AUTH_TYPE)).thenReturn("simple"); + when(config.getInitParameter( + AuthenticationFilter.AUTH_TOKEN_VALIDITY)).thenReturn( + (new Long(TOKEN_VALIDITY_SEC)).toString()); + when(config.getInitParameterNames()).thenReturn( + new Vector<>(Arrays.asList(AuthenticationFilter.AUTH_TYPE, + AuthenticationFilter.AUTH_TOKEN_VALIDITY)).elements()); + ServletContext context = mock(ServletContext.class); + when(context.getAttribute(AuthenticationFilter.SIGNER_SECRET_PROVIDER_ATTRIBUTE)) + .thenReturn(null); + when(config.getServletContext()).thenReturn(context); + filter.init(config); + assertEquals(PseudoAuthenticationHandler.class, filter.getAuthenticationHandler().getClass()); + assertTrue(filter.isRandomSecret()); + assertFalse(filter.isCustomSignerSecretProvider()); + assertNull(filter.getCookieDomain()); + assertNull(filter.getCookiePath()); + assertEquals(TOKEN_VALIDITY_SEC, filter.getValidity()); + } finally { + filter.destroy(); + } + } + @Test + public void testInit() throws Exception { + // custom secret as inline + AuthenticationFilter filter = new AuthenticationFilter(); + try { + FilterConfig config = mock(FilterConfig.class); + when(config.getInitParameter(AuthenticationFilter.AUTH_TYPE)).thenReturn("simple"); + when(config.getInitParameterNames()).thenReturn( + new Vector<>(Arrays.asList(AuthenticationFilter.AUTH_TYPE)) + .elements()); + ServletContext context = mock(ServletContext.class); + when(context.getAttribute( + AuthenticationFilter.SIGNER_SECRET_PROVIDER_ATTRIBUTE)).thenReturn( + new SignerSecretProvider() { + @Override + public void init(Properties config, ServletContext servletContext, + long tokenValidity) { + } + @Override + public byte[] getCurrentSecret() { + return null; + } + @Override + public byte[][] getAllSecrets() { + return null; + } + }); + when(config.getServletContext()).thenReturn(context); + filter.init(config); + assertFalse(filter.isRandomSecret()); + assertTrue(filter.isCustomSignerSecretProvider()); + } finally { + filter.destroy(); + } + + // custom secret by file + File testDir = new File(System.getProperty("test.build.data", + "target/test-dir")); + testDir.mkdirs(); + String secretValue = "hadoop"; + File secretFile = new File(testDir, "http-secret.txt"); + Writer writer = new FileWriter(secretFile); + writer.write(secretValue); + writer.close(); + + filter = new AuthenticationFilter(); + try { + FilterConfig config = mock(FilterConfig.class); + when(config.getInitParameter( + AuthenticationFilter.AUTH_TYPE)).thenReturn("simple"); + when(config.getInitParameter( + AuthenticationFilter.SIGNATURE_SECRET_FILE)) + .thenReturn(secretFile.getAbsolutePath()); + when(config.getInitParameterNames()).thenReturn( + new Vector(Arrays.asList(AuthenticationFilter.AUTH_TYPE, + AuthenticationFilter.SIGNATURE_SECRET_FILE)).elements()); + ServletContext context = mock(ServletContext.class); + when(context.getAttribute( + AuthenticationFilter.SIGNER_SECRET_PROVIDER_ATTRIBUTE)) + .thenReturn(null); + when(config.getServletContext()).thenReturn(context); + filter.init(config); + assertFalse(filter.isRandomSecret()); + assertFalse(filter.isCustomSignerSecretProvider()); + } finally { + filter.destroy(); + } + + // custom cookie domain and cookie path + filter = new AuthenticationFilter(); + try { + FilterConfig config = mock(FilterConfig.class); + when(config.getInitParameter(AuthenticationFilter.AUTH_TYPE)).thenReturn("simple"); + when(config.getInitParameter(AuthenticationFilter.COOKIE_DOMAIN)).thenReturn(".foo.com"); + when(config.getInitParameter(AuthenticationFilter.COOKIE_PATH)).thenReturn("/bar"); + when(config.getInitParameterNames()).thenReturn( + new Vector(Arrays.asList(AuthenticationFilter.AUTH_TYPE, + AuthenticationFilter.COOKIE_DOMAIN, + AuthenticationFilter.COOKIE_PATH)).elements()); + getMockedServletContextWithStringSigner(config); + filter.init(config); + assertEquals(".foo.com", filter.getCookieDomain()); + assertEquals("/bar", filter.getCookiePath()); + } finally { + filter.destroy(); + } + + // authentication handler lifecycle, and custom impl + DummyAuthenticationHandler.reset(); + filter = new AuthenticationFilter(); + try { + FilterConfig config = mock(FilterConfig.class); + when(config.getInitParameter("management.operation.return")). + thenReturn("true"); + when(config.getInitParameter(AuthenticationFilter.AUTH_TYPE)).thenReturn( + DummyAuthenticationHandler.class.getName()); + when(config.getInitParameterNames()).thenReturn( + new Vector( + Arrays.asList(AuthenticationFilter.AUTH_TYPE, + "management.operation.return")).elements()); + getMockedServletContextWithStringSigner(config); + filter.init(config); + assertTrue(DummyAuthenticationHandler.init); + } finally { + filter.destroy(); + assertTrue(DummyAuthenticationHandler.destroy); + } + + // kerberos auth handler + filter = new AuthenticationFilter(); + try { + FilterConfig config = mock(FilterConfig.class); + ServletContext sc = mock(ServletContext.class); + when(config.getServletContext()).thenReturn(sc); + when(config.getInitParameter(AuthenticationFilter.AUTH_TYPE)).thenReturn("kerberos"); + when(config.getInitParameterNames()).thenReturn( + new Vector(Arrays.asList(AuthenticationFilter.AUTH_TYPE)).elements()); + filter.init(config); + } catch (ServletException ex) { + // Expected + } finally { + assertEquals(KerberosAuthenticationHandler.class, + filter.getAuthenticationHandler().getClass()); + filter.destroy(); + } + } + + @Test + public void testEmptySecretFileFallbacksToRandomSecret() throws Exception { + AuthenticationFilter filter = new AuthenticationFilter(); + try { + FilterConfig config = mock(FilterConfig.class); + when(config.getInitParameter( + AuthenticationFilter.AUTH_TYPE)).thenReturn("simple"); + File secretFile = File.createTempFile("test_empty_secret", ".txt"); + secretFile.deleteOnExit(); + assertTrue(secretFile.exists()); + when(config.getInitParameter( + AuthenticationFilter.SIGNATURE_SECRET_FILE)) + .thenReturn(secretFile.getAbsolutePath()); + when(config.getInitParameterNames()).thenReturn( + new Vector<>(Arrays.asList(AuthenticationFilter.AUTH_TYPE, + AuthenticationFilter.SIGNATURE_SECRET_FILE)).elements()); + ServletContext context = mock(ServletContext.class); + when(context.getAttribute( + AuthenticationFilter.SIGNER_SECRET_PROVIDER_ATTRIBUTE)) + .thenReturn(null); + when(config.getServletContext()).thenReturn(context); + filter.init(config); + assertTrue(filter.isRandomSecret()); + } finally { + filter.destroy(); + } + } + + @Test + public void testInitCaseSensitivity() throws Exception { + // minimal configuration & simple auth handler (Pseudo) + AuthenticationFilter filter = new AuthenticationFilter(); + try { + FilterConfig config = mock(FilterConfig.class); + when(config.getInitParameter(AuthenticationFilter.AUTH_TYPE)).thenReturn("SimPle"); + when(config.getInitParameter(AuthenticationFilter.AUTH_TOKEN_VALIDITY)).thenReturn( + (new Long(TOKEN_VALIDITY_SEC)).toString()); + when(config.getInitParameterNames()).thenReturn( + new Vector(Arrays.asList(AuthenticationFilter.AUTH_TYPE, + AuthenticationFilter.AUTH_TOKEN_VALIDITY)).elements()); + getMockedServletContextWithStringSigner(config); + + filter.init(config); + assertEquals(PseudoAuthenticationHandler.class, + filter.getAuthenticationHandler().getClass()); + } finally { + filter.destroy(); + } + } + + @Test + public void testGetRequestURL() throws Exception { + AuthenticationFilter filter = new AuthenticationFilter(); + try { + FilterConfig config = mock(FilterConfig.class); + when(config.getInitParameter("management.operation.return")). + thenReturn("true"); + when(config.getInitParameter(AuthenticationFilter.AUTH_TYPE)).thenReturn( + DummyAuthenticationHandler.class.getName()); + when(config.getInitParameterNames()).thenReturn( + new Vector( + Arrays.asList(AuthenticationFilter.AUTH_TYPE, + "management.operation.return")).elements()); + getMockedServletContextWithStringSigner(config); + filter.init(config); + + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getRequestURL()).thenReturn(new StringBuffer("http://foo:8080/bar")); + when(request.getQueryString()).thenReturn("a=A&b=B"); + + assertEquals("http://foo:8080/bar?a=A&b=B", filter.getRequestURL(request)); + } finally { + filter.destroy(); + } + } + + @Test + public void testGetToken() throws Exception { + AuthenticationFilter filter = new AuthenticationFilter(); + + try { + FilterConfig config = mock(FilterConfig.class); + when(config.getInitParameter("management.operation.return")). + thenReturn("true"); + when(config.getInitParameter(AuthenticationFilter.AUTH_TYPE)).thenReturn( + DummyAuthenticationHandler.class.getName()); + when(config.getInitParameter(AuthenticationFilter.SIGNATURE_SECRET)).thenReturn("secret"); + when(config.getInitParameterNames()).thenReturn( + new Vector( + Arrays.asList(AuthenticationFilter.AUTH_TYPE, + AuthenticationFilter.SIGNATURE_SECRET, + "management.operation.return")).elements()); + SignerSecretProvider secretProvider = + getMockedServletContextWithStringSigner(config); + filter.init(config); + + AuthenticationToken token = new AuthenticationToken("u", "p", DummyAuthenticationHandler.TYPE); + token.setExpires(System.currentTimeMillis() + TOKEN_VALIDITY_SEC); + + Signer signer = new Signer(secretProvider); + String tokenSigned = signer.sign(token.toString()); + + Cookie cookie = new Cookie(AuthenticatedURL.AUTH_COOKIE, tokenSigned); + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getCookies()).thenReturn(new Cookie[]{cookie}); + + AuthenticationToken newToken = filter.getToken(request); + + assertEquals(token.toString(), newToken.toString()); + } finally { + filter.destroy(); + } + } + + @Test + public void testGetTokenExpired() throws Exception { + AuthenticationFilter filter = new AuthenticationFilter(); + try { + FilterConfig config = mock(FilterConfig.class); + when(config.getInitParameter("management.operation.return")).thenReturn("true"); + when(config.getInitParameter(AuthenticationFilter.AUTH_TYPE)).thenReturn( + DummyAuthenticationHandler.class.getName()); + when(config.getInitParameter(AuthenticationFilter.SIGNATURE_SECRET)).thenReturn("secret"); + when(config.getInitParameterNames()).thenReturn( + new Vector( + Arrays.asList(AuthenticationFilter.AUTH_TYPE, + AuthenticationFilter.SIGNATURE_SECRET, + "management.operation.return")).elements()); + getMockedServletContextWithStringSigner(config); + filter.init(config); + + AuthenticationToken token = + new AuthenticationToken("u", "p", DummyAuthenticationHandler.TYPE); + token.setExpires(System.currentTimeMillis() - TOKEN_VALIDITY_SEC); + SignerSecretProvider secretProvider = + StringSignerSecretProviderCreator.newStringSignerSecretProvider(); + Properties secretProviderProps = new Properties(); + secretProviderProps.setProperty( + AuthenticationFilter.SIGNATURE_SECRET, "secret"); + secretProvider.init(secretProviderProps, null, TOKEN_VALIDITY_SEC); + Signer signer = new Signer(secretProvider); + String tokenSigned = signer.sign(token.toString()); + + Cookie cookie = new Cookie(AuthenticatedURL.AUTH_COOKIE, tokenSigned); + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getCookies()).thenReturn(new Cookie[]{cookie}); + + boolean failed = false; + try { + filter.getToken(request); + } catch (AuthenticationException ex) { + assertEquals("AuthenticationToken expired", ex.getMessage()); + failed = true; + } finally { + assertTrue(failed); + } + } finally { + filter.destroy(); + } + } + + @Test + public void testGetTokenInvalidType() throws Exception { + AuthenticationFilter filter = new AuthenticationFilter(); + try { + FilterConfig config = mock(FilterConfig.class); + when(config.getInitParameter("management.operation.return")). + thenReturn("true"); + when(config.getInitParameter(AuthenticationFilter.AUTH_TYPE)).thenReturn( + DummyAuthenticationHandler.class.getName()); + when(config.getInitParameter(AuthenticationFilter.SIGNATURE_SECRET)).thenReturn("secret"); + when(config.getInitParameterNames()).thenReturn( + new Vector( + Arrays.asList(AuthenticationFilter.AUTH_TYPE, + AuthenticationFilter.SIGNATURE_SECRET, + "management.operation.return")).elements()); + getMockedServletContextWithStringSigner(config); + filter.init(config); + + AuthenticationToken token = new AuthenticationToken("u", "p", "invalidtype"); + token.setExpires(System.currentTimeMillis() + TOKEN_VALIDITY_SEC); + SignerSecretProvider secretProvider = + StringSignerSecretProviderCreator.newStringSignerSecretProvider(); + Properties secretProviderProps = new Properties(); + secretProviderProps.setProperty( + AuthenticationFilter.SIGNATURE_SECRET, "secret"); + secretProvider.init(secretProviderProps, null, TOKEN_VALIDITY_SEC); + Signer signer = new Signer(secretProvider); + String tokenSigned = signer.sign(token.toString()); + + Cookie cookie = new Cookie(AuthenticatedURL.AUTH_COOKIE, tokenSigned); + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getCookies()).thenReturn(new Cookie[]{cookie}); + + boolean failed = false; + try { + filter.getToken(request); + } catch (AuthenticationException ex) { + assertEquals("Invalid AuthenticationToken type", ex.getMessage()); + failed = true; + } finally { + assertTrue(failed); + } + } finally { + filter.destroy(); + } + } + + private static SignerSecretProvider getMockedServletContextWithStringSigner( + FilterConfig config) throws Exception { + Properties secretProviderProps = new Properties(); + secretProviderProps.setProperty(AuthenticationFilter.SIGNATURE_SECRET, + "secret"); + SignerSecretProvider secretProvider = + StringSignerSecretProviderCreator.newStringSignerSecretProvider(); + secretProvider.init(secretProviderProps, null, TOKEN_VALIDITY_SEC); + + ServletContext context = mock(ServletContext.class); + when(context.getAttribute( + AuthenticationFilter.SIGNER_SECRET_PROVIDER_ATTRIBUTE)) + .thenReturn(secretProvider); + when(config.getServletContext()).thenReturn(context); + return secretProvider; + } + + @Test + public void testDoFilterNotAuthenticated() throws Exception { + AuthenticationFilter filter = new AuthenticationFilter(); + try { + FilterConfig config = mock(FilterConfig.class); + when(config.getInitParameter("management.operation.return")). + thenReturn("true"); + when(config.getInitParameter(AuthenticationFilter.AUTH_TYPE)).thenReturn( + DummyAuthenticationHandler.class.getName()); + when(config.getInitParameterNames()).thenReturn( + new Vector( + Arrays.asList(AuthenticationFilter.AUTH_TYPE, + "management.operation.return")).elements()); + getMockedServletContextWithStringSigner(config); + filter.init(config); + + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getRequestURL()).thenReturn(new StringBuffer("http://foo:8080/bar")); + + HttpServletResponse response = mock(HttpServletResponse.class); + + FilterChain chain = mock(FilterChain.class); + + doAnswer( + new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + fail(); + return null; + } + } + ).when(chain).doFilter(any(), any()); + + when(response.containsHeader("WWW-Authenticate")).thenReturn(true); + filter.doFilter(request, response, chain); + + verify(response).sendError( + HttpServletResponse.SC_UNAUTHORIZED, "Authentication required"); + } finally { + filter.destroy(); + } + } + + @Test + public void testDoFilterNotAuthenticatedLowerCase() throws Exception { + AuthenticationFilter filter = new AuthenticationFilter(); + try { + FilterConfig config = mock(FilterConfig.class); + when(config.getInitParameter("management.operation.return")). + thenReturn("true"); + when(config.getInitParameter(AuthenticationFilter.AUTH_TYPE)).thenReturn( + DummyAuthenticationHandler.class.getName()); + when(config.getInitParameterNames()).thenReturn( + new Vector<>( + Arrays.asList(AuthenticationFilter.AUTH_TYPE, + "management.operation.return")).elements()); + getMockedServletContextWithStringSigner(config); + filter.init(config); + + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getRequestURL()).thenReturn(new StringBuffer("http://foo:8080/bar")); + + HttpServletResponse response = mock(HttpServletResponse.class); + + FilterChain chain = mock(FilterChain.class); + + doAnswer((Answer) invocation -> { + fail(); + return null; + }).when(chain).doFilter(any(), any()); + + when(response.containsHeader("www-authenticate")).thenReturn(true); + filter.doFilter(request, response, chain); + + verify(response).sendError( + HttpServletResponse.SC_UNAUTHORIZED, "Authentication required"); + } finally { + filter.destroy(); + } + } + + private void _testDoFilterAuthentication(boolean withDomainPath, + boolean invalidToken, + boolean expired) throws Exception { + AuthenticationFilter filter = new AuthenticationFilter(); + FilterConfig config = mock(FilterConfig.class); + when(config.getInitParameter("management.operation.return")). + thenReturn("true"); + when(config.getInitParameter("expired.token")). + thenReturn(Boolean.toString(expired)); + when(config.getInitParameter(AuthenticationFilter.AUTH_TYPE)) + .thenReturn(DummyAuthenticationHandler.class.getName()); + when(config.getInitParameter(AuthenticationFilter + .AUTH_TOKEN_VALIDITY)).thenReturn(new Long(TOKEN_VALIDITY_SEC).toString()); + when(config.getInitParameter(AuthenticationFilter + .SIGNATURE_SECRET)).thenReturn("secret"); + when(config.getInitParameterNames()).thenReturn(new + Vector(Arrays.asList(AuthenticationFilter.AUTH_TYPE, + AuthenticationFilter.AUTH_TOKEN_VALIDITY, + AuthenticationFilter.SIGNATURE_SECRET, "management.operation" + + ".return", "expired.token")).elements()); + getMockedServletContextWithStringSigner(config); + + if (withDomainPath) { + when(config.getInitParameter(AuthenticationFilter + .COOKIE_DOMAIN)).thenReturn(".foo.com"); + when(config.getInitParameter(AuthenticationFilter.COOKIE_PATH)) + .thenReturn("/bar"); + when(config.getInitParameterNames()).thenReturn(new + Vector(Arrays.asList(AuthenticationFilter.AUTH_TYPE, + AuthenticationFilter.AUTH_TOKEN_VALIDITY, + AuthenticationFilter.SIGNATURE_SECRET, + AuthenticationFilter.COOKIE_DOMAIN, AuthenticationFilter + .COOKIE_PATH, "management.operation.return")).elements()); + } + + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getParameter("authenticated")).thenReturn("true"); + when(request.getRequestURL()).thenReturn(new StringBuffer + ("http://foo:8080/bar")); + when(request.getQueryString()).thenReturn("authenticated=true"); + + if (invalidToken) { + when(request.getCookies()).thenReturn(new Cookie[]{new Cookie + (AuthenticatedURL.AUTH_COOKIE, "foo")}); + } + + HttpServletResponse response = mock(HttpServletResponse.class); + FilterChain chain = mock(FilterChain.class); + + final Map cookieMap = new HashMap(); + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + String cookieHeader = (String)invocation.getArguments()[1]; + parseCookieMap(cookieHeader, cookieMap); + return null; + } + }).when(response).addHeader(eq("Set-Cookie"), anyString()); + + try { + filter.init(config); + filter.doFilter(request, response, chain); + + if (expired) { + verify(response, never()).addHeader(eq("Set-Cookie"), anyString()); + } else { + String v = cookieMap.get(AuthenticatedURL.AUTH_COOKIE); + assertNotNull(v, "cookie missing"); + assertTrue(v.contains("u=") && v.contains("p=") && v.contains + ("t=") && v.contains("e=") && v.contains("s=")); + verify(chain).doFilter(any(ServletRequest.class), + any(ServletResponse.class)); + + SignerSecretProvider secretProvider = + StringSignerSecretProviderCreator.newStringSignerSecretProvider(); + Properties secretProviderProps = new Properties(); + secretProviderProps.setProperty( + AuthenticationFilter.SIGNATURE_SECRET, "secret"); + secretProvider.init(secretProviderProps, null, TOKEN_VALIDITY_SEC); + Signer signer = new Signer(secretProvider); + String value = signer.verifyAndExtract(v); + AuthenticationToken token = AuthenticationToken.parse(value); + assertNotEquals(0L, token.getExpires()); + + if (withDomainPath) { + assertEquals(".foo.com", cookieMap.get("Domain")); + assertEquals("/bar", cookieMap.get("Path")); + } else { + assertFalse(cookieMap.containsKey("Domain")); + assertFalse(cookieMap.containsKey("Path")); + } + } + } finally { + filter.destroy(); + } + } + + private static void parseCookieMap(String cookieHeader, Map cookieMap) { + List cookies = HttpCookie.parse(cookieHeader); + for (HttpCookie cookie : cookies) { + if (AuthenticatedURL.AUTH_COOKIE.equals(cookie.getName())) { + cookieMap.put(cookie.getName(), cookie.getValue()); + if (cookie.getPath() != null) { + cookieMap.put("Path", cookie.getPath()); + } + if (cookie.getDomain() != null) { + cookieMap.put("Domain", cookie.getDomain()); + } + } + } + } + + @Test + public void testDoFilterAuthentication() throws Exception { + _testDoFilterAuthentication(false, false, false); + } + + @Test + public void testDoFilterAuthenticationImmediateExpiration() throws Exception { + _testDoFilterAuthentication(false, false, true); + } + + @Test + public void testDoFilterAuthenticationWithInvalidToken() throws Exception { + _testDoFilterAuthentication(false, true, false); + } + + @Test + public void testDoFilterAuthenticationWithDomainPath() throws Exception { + _testDoFilterAuthentication(true, false, false); + } + + @Test + public void testDoFilterAuthenticated() throws Exception { + AuthenticationFilter filter = new AuthenticationFilter(); + try { + FilterConfig config = mock(FilterConfig.class); + when(config.getInitParameter("management.operation.return")). + thenReturn("true"); + when(config.getInitParameter(AuthenticationFilter.AUTH_TYPE)).thenReturn( + DummyAuthenticationHandler.class.getName()); + when(config.getInitParameterNames()).thenReturn( + new Vector( + Arrays.asList(AuthenticationFilter.AUTH_TYPE, + "management.operation.return")).elements()); + getMockedServletContextWithStringSigner(config); + filter.init(config); + + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getRequestURL()).thenReturn(new StringBuffer("http://foo:8080/bar")); + + AuthenticationToken token = new AuthenticationToken("u", "p", "t"); + token.setExpires(System.currentTimeMillis() + TOKEN_VALIDITY_SEC); + SignerSecretProvider secretProvider = + StringSignerSecretProviderCreator.newStringSignerSecretProvider(); + Properties secretProviderProps = new Properties(); + secretProviderProps.setProperty( + AuthenticationFilter.SIGNATURE_SECRET, "secret"); + secretProvider.init(secretProviderProps, null, TOKEN_VALIDITY_SEC); + Signer signer = new Signer(secretProvider); + String tokenSigned = signer.sign(token.toString()); + + Cookie cookie = new Cookie(AuthenticatedURL.AUTH_COOKIE, tokenSigned); + when(request.getCookies()).thenReturn(new Cookie[]{cookie}); + + HttpServletResponse response = mock(HttpServletResponse.class); + + FilterChain chain = mock(FilterChain.class); + + doAnswer( + new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + Object[] args = invocation.getArguments(); + HttpServletRequest request = (HttpServletRequest) args[0]; + assertEquals("u", request.getRemoteUser()); + assertEquals("p", request.getUserPrincipal().getName()); + return null; + } + } + ).when(chain).doFilter(any(), any()); + + filter.doFilter(request, response, chain); + + } finally { + filter.destroy(); + } + } + + @Test + public void testDoFilterAuthenticationFailure() throws Exception { + AuthenticationFilter filter = new AuthenticationFilter(); + try { + FilterConfig config = mock(FilterConfig.class); + when(config.getInitParameter("management.operation.return")). + thenReturn("true"); + when(config.getInitParameter(AuthenticationFilter.AUTH_TYPE)).thenReturn( + DummyAuthenticationHandler.class.getName()); + when(config.getInitParameterNames()).thenReturn( + new Vector( + Arrays.asList(AuthenticationFilter.AUTH_TYPE, + "management.operation.return")).elements()); + getMockedServletContextWithStringSigner(config); + filter.init(config); + + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getRequestURL()).thenReturn(new StringBuffer("http://foo:8080/bar")); + when(request.getCookies()).thenReturn(new Cookie[]{}); + when(request.getHeader("WWW-Authenticate")).thenReturn("dummyauth"); + HttpServletResponse response = mock(HttpServletResponse.class); + + FilterChain chain = mock(FilterChain.class); + + final Map cookieMap = new HashMap(); + doAnswer( + new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + Object[] args = invocation.getArguments(); + parseCookieMap((String) args[1], cookieMap); + return null; + } + } + ).when(response).addHeader(eq("Set-Cookie"), anyString()); + + doAnswer( + new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + fail("shouldn't get here"); + return null; + } + } + ).when(chain).doFilter(any(), any()); + + filter.doFilter(request, response, chain); + + verify(response).sendError( + HttpServletResponse.SC_FORBIDDEN, "AUTH FAILED"); + verify(response, never()).setHeader(eq("WWW-Authenticate"), anyString()); + + String value = cookieMap.get(AuthenticatedURL.AUTH_COOKIE); + assertNotNull(value, "cookie missing"); + assertEquals("", value); + } finally { + filter.destroy(); + } + } + + @Test + public void testDoFilterAuthenticatedExpired() throws Exception { + String secret = "secret"; + AuthenticationFilter filter = new AuthenticationFilter(); + try { + FilterConfig config = mock(FilterConfig.class); + when(config.getInitParameter("management.operation.return")). + thenReturn("true"); + when(config.getInitParameter(AuthenticationFilter.AUTH_TYPE)).thenReturn( + DummyAuthenticationHandler.class.getName()); + when(config.getInitParameter(AuthenticationFilter.SIGNATURE_SECRET)).thenReturn( + secret); + when(config.getInitParameterNames()).thenReturn( + new Vector( + Arrays.asList(AuthenticationFilter.AUTH_TYPE, + AuthenticationFilter.SIGNATURE_SECRET, + "management.operation.return")).elements()); + getMockedServletContextWithStringSigner(config); + filter.init(config); + + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getRequestURL()).thenReturn(new StringBuffer("http://foo:8080/bar")); + + AuthenticationToken token = new AuthenticationToken("u", "p", DummyAuthenticationHandler.TYPE); + token.setExpires(System.currentTimeMillis() - TOKEN_VALIDITY_SEC); + SignerSecretProvider secretProvider = + StringSignerSecretProviderCreator.newStringSignerSecretProvider(); + Properties secretProviderProps = new Properties(); + secretProviderProps.setProperty( + AuthenticationFilter.SIGNATURE_SECRET, secret); + secretProvider.init(secretProviderProps, null, TOKEN_VALIDITY_SEC); + Signer signer = new Signer(secretProvider); + String tokenSigned = signer.sign(token.toString()); + + Cookie cookie = new Cookie(AuthenticatedURL.AUTH_COOKIE, tokenSigned); + when(request.getCookies()).thenReturn(new Cookie[]{cookie}); + + HttpServletResponse response = mock(HttpServletResponse.class); + when(response.containsHeader("WWW-Authenticate")).thenReturn(true); + FilterChain chain = mock(FilterChain.class); + + verifyUnauthorized(filter, request, response, chain); + } finally { + filter.destroy(); + } + } + + @Test + public void + testDoFilterAuthenticationAuthorized() throws Exception { + // Both expired period and MaxInActiveInterval are not reached. + long maxInactives = System.currentTimeMillis() + + TOKEN_MAX_INACTIVE_INTERVAL; + long expires = System.currentTimeMillis() + TOKEN_VALIDITY_SEC; + boolean authorized = true; + _testDoFilterAuthenticationMaxInactiveInterval(maxInactives, + expires, + authorized); + } + + @Test + public void + testDoFilterAuthenticationUnauthorizedExpired() throws Exception { + // Expired period is reached, MaxInActiveInterval is not reached. + long maxInactives = System.currentTimeMillis() + + TOKEN_MAX_INACTIVE_INTERVAL; + long expires = System.currentTimeMillis() - TOKEN_VALIDITY_SEC; + boolean authorized = false; + _testDoFilterAuthenticationMaxInactiveInterval(maxInactives, + expires, + authorized); + } + + @Test + public void + testDoFilterAuthenticationUnauthorizedInactived() throws Exception { + // Expired period is not reached, MaxInActiveInterval is reached. + long maxInactives = System.currentTimeMillis() + - TOKEN_MAX_INACTIVE_INTERVAL; + long expires = System.currentTimeMillis() + TOKEN_VALIDITY_SEC; + boolean authorized = false; + _testDoFilterAuthenticationMaxInactiveInterval(maxInactives, + expires, + authorized); + } + + @Test + public void + testDoFilterAuthenticationUnauthorizedInactivedExpired() + throws Exception { + // Both expired period and MaxInActiveInterval is reached. + long maxInactives = System.currentTimeMillis() + - TOKEN_MAX_INACTIVE_INTERVAL; + long expires = System.currentTimeMillis() - TOKEN_VALIDITY_SEC; + boolean authorized = false; + _testDoFilterAuthenticationMaxInactiveInterval(maxInactives, + expires, + authorized); + } + + @Test + public void testTokenWithValidActivityInterval() throws Exception { + // Provide token containing valid maxInactive value. + // The token is active. + // The server has maxInactiveInterval configured to -1.(disabled) + // The server shall authorize the access, but should not drop a new cookie + long maxInactives = System.currentTimeMillis() + + TOKEN_MAX_INACTIVE_INTERVAL; + long expires = System.currentTimeMillis() + TOKEN_VALIDITY_SEC; + _testDoFilterAuthenticationMaxInactiveInterval( + maxInactives, + -1, + expires, + true, //authorized + false //newCookie + ); + // Provide token containing valid maxInactive value. + // The token is active. + // The server has maxInactiveInterval configured to value + // greater than 0.(enabled) + // The server shall authorize the access and drop a new cookie + // with renewed activity interval + maxInactives = System.currentTimeMillis() + + TOKEN_MAX_INACTIVE_INTERVAL; + expires = System.currentTimeMillis() + TOKEN_VALIDITY_SEC; + _testDoFilterAuthenticationMaxInactiveInterval( + maxInactives, + TOKEN_MAX_INACTIVE_INTERVAL, + expires, + true, //authorized + true //newCookie + ); + } + + @Test + public void testTokenWithExpiredActivityIntervaln() throws Exception { + // Provide token containing invalid maxInactive value. + // The token is inactive. + // The server has maxInactiveInterval configured to -1.(disabled) + // The server should deny access and expire the token. + long maxInactives = System.currentTimeMillis() + - TOKEN_MAX_INACTIVE_INTERVAL; + long expires = System.currentTimeMillis() + TOKEN_VALIDITY_SEC; + _testDoFilterAuthenticationMaxInactiveInterval( + maxInactives, + -1, + expires, + false, //authorized + false //newCookie + ); + // Provide token containing invalid maxInactive value. + // The token is inactive. + // The server has maxInactiveInterval configured to value + // greater than 0.(enabled) + // The server should deny access and expire the token. + maxInactives = System.currentTimeMillis() + + TOKEN_MAX_INACTIVE_INTERVAL; + expires = System.currentTimeMillis() + TOKEN_VALIDITY_SEC; + _testDoFilterAuthenticationMaxInactiveInterval( + maxInactives, + -1, + expires, + true, //authorized + false //newCookie + ); + } + + @Test + public void testTokenWithNoActivityIntervals() + throws Exception { + // Provide token which does not contain maxInactive value. + // The server has maxInactiveInterval configured to -1. + // The server shall authorize the access, but should not drop a new cookie + long expires = System.currentTimeMillis() + TOKEN_VALIDITY_SEC; + _testDoFilterAuthenticationMaxInactiveInterval( + -1, + -1, + expires, + true, //authorized + false //newCookie + ); + // Provide token which does not contain maxInactive value. + // The server has maxInactiveInterval to some value + // The server shall authorize the access and drop a new cookie + // with renewed activity interval + expires = System.currentTimeMillis() + TOKEN_VALIDITY_SEC; + _testDoFilterAuthenticationMaxInactiveInterval( + -1, + TOKEN_MAX_INACTIVE_INTERVAL, + expires, + true, //authorized + true //newCookie + ); + } + + private void + _testDoFilterAuthenticationMaxInactiveInterval(long maxInactivesInToken, + long expires, + boolean authorized) + throws Exception { + _testDoFilterAuthenticationMaxInactiveInterval(maxInactivesInToken, + TOKEN_MAX_INACTIVE_INTERVAL, expires, authorized, true); + } + + private void + _testDoFilterAuthenticationMaxInactiveInterval(long maxInactivesInToken, + long maxInactivesOnServer, + long expires, + boolean authorized, + boolean newCookie) + throws Exception { + String secret = "secret"; + AuthenticationFilter filter = new AuthenticationFilter(); + try { + FilterConfig config = mock(FilterConfig.class); + when(config.getInitParameter("management.operation.return")). + thenReturn("true"); + when(config.getInitParameter( + AuthenticationFilter.AUTH_TYPE)).thenReturn( + DummyAuthenticationHandler.class.getName()); + when(config.getInitParameter( + AuthenticationFilter.SIGNATURE_SECRET)).thenReturn(secret); + when(config.getInitParameter( + AuthenticationFilter.AUTH_TOKEN_MAX_INACTIVE_INTERVAL)).thenReturn( + Long.toString(maxInactivesOnServer)); + when(config.getInitParameterNames()).thenReturn( + new Vector( + Arrays.asList(AuthenticationFilter.AUTH_TYPE, + AuthenticationFilter.SIGNATURE_SECRET, + AuthenticationFilter.AUTH_TOKEN_MAX_INACTIVE_INTERVAL, + "management.operation.return")).elements()); + getMockedServletContextWithStringSigner(config); + filter.init(config); + + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getRequestURL()).thenReturn( + new StringBuffer("http://foo:8080/bar")); + + AuthenticationToken token = new AuthenticationToken("u", "p", + DummyAuthenticationHandler.TYPE); + token.setMaxInactives(maxInactivesInToken); + token.setExpires(expires); + + SignerSecretProvider secretProvider = + StringSignerSecretProviderCreator.newStringSignerSecretProvider(); + Properties secretProviderProps = new Properties(); + secretProviderProps.setProperty( + AuthenticationFilter.SIGNATURE_SECRET, secret); + secretProvider.init(secretProviderProps, null, TOKEN_VALIDITY_SEC); + Signer signer = new Signer(secretProvider); + String tokenSigned = signer.sign(token.toString()); + + Cookie cookie = new Cookie(AuthenticatedURL.AUTH_COOKIE, tokenSigned); + when(request.getCookies()).thenReturn(new Cookie[]{cookie}); + HttpServletResponse response = mock(HttpServletResponse.class); + when(response.containsHeader("WWW-Authenticate")) + .thenReturn(true); + FilterChain chain = mock(FilterChain.class); + + if (authorized) { + verifyAuthorized(filter, request, response, chain, newCookie); + } else { + verifyUnauthorized(filter, request, response, chain); + } + } finally { + filter.destroy(); + } + } + + private static void verifyAuthorized(AuthenticationFilter filter, + HttpServletRequest request, + HttpServletResponse response, + FilterChain chain, + boolean newCookie) throws + Exception { + final Map cookieMap = new HashMap<>(); + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + String cookieHeader = (String) invocation.getArguments()[1]; + parseCookieMap(cookieHeader, cookieMap); + return null; + } + }).when(response).addHeader(eq("Set-Cookie"), anyString()); + + filter.doFilter(request, response, chain); + + if (newCookie) { + // a new cookie should be dropped when maxInactiveInterval is enabled + String v = cookieMap.get(AuthenticatedURL.AUTH_COOKIE); + assertNotNull(v, "cookie missing"); + assertTrue(v.contains("u=") && v.contains("p=") && v.contains + ("t=") && v.contains("i=") && v.contains("e=") + && v.contains("s=")); + verify(chain).doFilter(any(ServletRequest.class), + any(ServletResponse.class)); + + SignerSecretProvider secretProvider = + StringSignerSecretProviderCreator.newStringSignerSecretProvider(); + Properties secretProviderProps = new Properties(); + secretProviderProps.setProperty( + AuthenticationFilter.SIGNATURE_SECRET, "secret"); + secretProvider.init(secretProviderProps, null, TOKEN_VALIDITY_SEC); + Signer signer = new Signer(secretProvider); + String value = signer.verifyAndExtract(v); + AuthenticationToken token = AuthenticationToken.parse(value); + assertNotEquals(0L, token.getMaxInactives()); + assertNotEquals(0L, token.getExpires()); + assertFalse(token.isExpired()); + } else { + //make sure that no auth cookie is dropped. + //For unauthorized response, auth cookie is dropped with empty value + assertTrue(!cookieMap.containsKey(AuthenticatedURL.AUTH_COOKIE)); + } + } + + private static void verifyUnauthorized(AuthenticationFilter filter, + HttpServletRequest request, + HttpServletResponse response, + FilterChain chain) throws + IOException, + ServletException { + //For unauthorized response, a cookie is dropped with empty string as value + final Map cookieMap = new HashMap(); + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + String cookieHeader = (String) invocation.getArguments()[1]; + parseCookieMap(cookieHeader, cookieMap); + return null; + } + }).when(response).addHeader(eq("Set-Cookie"), anyString()); + + filter.doFilter(request, response, chain); + + verify(response).sendError(eq(HttpServletResponse + .SC_UNAUTHORIZED), anyString()); + verify(chain, never()).doFilter( + any(ServletRequest.class), any(ServletResponse.class)); + + assertTrue(cookieMap.containsKey(AuthenticatedURL.AUTH_COOKIE)); + assertEquals("", cookieMap.get(AuthenticatedURL.AUTH_COOKIE)); + } + + @Test + public void testDoFilterAuthenticatedInvalidType() throws Exception { + String secret = "secret"; + AuthenticationFilter filter = new AuthenticationFilter(); + try { + FilterConfig config = mock(FilterConfig.class); + when(config.getInitParameter("management.operation.return")). + thenReturn("true"); + when(config.getInitParameter(AuthenticationFilter.AUTH_TYPE)).thenReturn( + DummyAuthenticationHandler.class.getName()); + when(config.getInitParameter(AuthenticationFilter.SIGNATURE_SECRET)).thenReturn( + secret); + when(config.getInitParameterNames()).thenReturn( + new Vector( + Arrays.asList(AuthenticationFilter.AUTH_TYPE, + AuthenticationFilter.SIGNATURE_SECRET, + "management.operation.return")).elements()); + getMockedServletContextWithStringSigner(config); + filter.init(config); + + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getRequestURL()).thenReturn(new StringBuffer("http://foo:8080/bar")); + + AuthenticationToken token = new AuthenticationToken("u", "p", "invalidtype"); + token.setExpires(System.currentTimeMillis() + TOKEN_VALIDITY_SEC); + SignerSecretProvider secretProvider = + StringSignerSecretProviderCreator.newStringSignerSecretProvider(); + Properties secretProviderProps = new Properties(); + secretProviderProps.setProperty( + AuthenticationFilter.SIGNATURE_SECRET, secret); + secretProvider.init(secretProviderProps, null, TOKEN_VALIDITY_SEC); + Signer signer = new Signer(secretProvider); + String tokenSigned = signer.sign(token.toString()); + + Cookie cookie = new Cookie(AuthenticatedURL.AUTH_COOKIE, tokenSigned); + when(request.getCookies()).thenReturn(new Cookie[]{cookie}); + + HttpServletResponse response = mock(HttpServletResponse.class); + when(response.containsHeader("WWW-Authenticate")).thenReturn(true); + FilterChain chain = mock(FilterChain.class); + + verifyUnauthorized(filter, request, response, chain); + } finally { + filter.destroy(); + } + } + + @Test + public void testManagementOperation() throws Exception { + AuthenticationFilter filter = new AuthenticationFilter(); + try { + FilterConfig config = mock(FilterConfig.class); + when(config.getInitParameter("management.operation.return")). + thenReturn("false"); + when(config.getInitParameter(AuthenticationFilter.AUTH_TYPE)). + thenReturn(DummyAuthenticationHandler.class.getName()); + when(config.getInitParameterNames()).thenReturn( + new Vector( + Arrays.asList(AuthenticationFilter.AUTH_TYPE, + "management.operation.return")).elements()); + getMockedServletContextWithStringSigner(config); + filter.init(config); + + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getRequestURL()). + thenReturn(new StringBuffer("http://foo:8080/bar")); + + HttpServletResponse response = mock(HttpServletResponse.class); + + FilterChain chain = mock(FilterChain.class); + + filter.doFilter(request, response, chain); + verify(response).setStatus(HttpServletResponse.SC_ACCEPTED); + verifyNoMoreInteractions(response); + + reset(request); + reset(response); + + AuthenticationToken token = new AuthenticationToken("u", "p", "t"); + token.setExpires(System.currentTimeMillis() + TOKEN_VALIDITY_SEC); + SignerSecretProvider secretProvider = + StringSignerSecretProviderCreator.newStringSignerSecretProvider(); + Properties secretProviderProps = new Properties(); + secretProviderProps.setProperty( + AuthenticationFilter.SIGNATURE_SECRET, "secret"); + secretProvider.init(secretProviderProps, null, TOKEN_VALIDITY_SEC); + Signer signer = new Signer(secretProvider); + String tokenSigned = signer.sign(token.toString()); + Cookie cookie = new Cookie(AuthenticatedURL.AUTH_COOKIE, tokenSigned); + when(request.getCookies()).thenReturn(new Cookie[]{cookie}); + when(request.getRequestURL()).thenReturn(new StringBuffer()); + + filter.doFilter(request, response, chain); + + verify(response).setStatus(HttpServletResponse.SC_ACCEPTED); + verifyNoMoreInteractions(response); + + } finally { + filter.destroy(); + } + } + +} diff --git a/hbase-auth-filters/src/test/java/org/apache/hadoop/hbase/security/authentication/util/StringSignerSecretProvider.java b/hbase-auth-filters/src/test/java/org/apache/hadoop/hbase/security/authentication/util/StringSignerSecretProvider.java new file mode 100644 index 000000000000..2f8b1fb4a3b7 --- /dev/null +++ b/hbase-auth-filters/src/test/java/org/apache/hadoop/hbase/security/authentication/util/StringSignerSecretProvider.java @@ -0,0 +1,53 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. See accompanying LICENSE file. + */ +package org.apache.hadoop.hbase.security.authentication.util; + +import java.nio.charset.StandardCharsets; +import java.util.Properties; +import javax.servlet.ServletContext; + +import org.apache.yetus.audience.InterfaceStability; +import org.apache.hadoop.hbase.security.authentication.server.AuthenticationFilter; +import org.apache.hadoop.hbase.security.authentication.util.SignerSecretProvider; + +/** + * A SignerSecretProvider that simply creates a secret based on a given String. + */ +@InterfaceStability.Unstable +class StringSignerSecretProvider extends SignerSecretProvider { + + private byte[] secret; + private byte[][] secrets; + + public StringSignerSecretProvider() {} + + @Override + public void init(Properties config, ServletContext servletContext, + long tokenValidity) throws Exception { + String signatureSecret = config.getProperty( + AuthenticationFilter.SIGNATURE_SECRET, null); + secret = signatureSecret.getBytes(StandardCharsets.UTF_8); + secrets = new byte[][]{secret}; + } + + @Override + public byte[] getCurrentSecret() { + return secret; + } + + @Override + public byte[][] getAllSecrets() { + return secrets; + } +} diff --git a/hbase-auth-filters/src/test/java/org/apache/hadoop/hbase/security/authentication/util/StringSignerSecretProviderCreator.java b/hbase-auth-filters/src/test/java/org/apache/hadoop/hbase/security/authentication/util/StringSignerSecretProviderCreator.java new file mode 100644 index 000000000000..e54b682d5bf3 --- /dev/null +++ b/hbase-auth-filters/src/test/java/org/apache/hadoop/hbase/security/authentication/util/StringSignerSecretProviderCreator.java @@ -0,0 +1,31 @@ +/** + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. See accompanying LICENSE file. + */ +package org.apache.hadoop.hbase.security.authentication.util; + +import org.apache.yetus.audience.InterfaceStability; + +/** + * Helper class for creating StringSignerSecretProviders in unit tests + */ +@InterfaceStability.Unstable +public class StringSignerSecretProviderCreator { + /** + * @return a new StringSignerSecretProvider + * @throws Exception + */ + public static StringSignerSecretProvider newStringSignerSecretProvider() + throws Exception { + return new StringSignerSecretProvider(); + } +} diff --git a/hbase-auth-filters/src/test/java/org/apache/hadoop/hbase/util/TestHttpExceptionUtils.java b/hbase-auth-filters/src/test/java/org/apache/hadoop/hbase/util/TestHttpExceptionUtils.java new file mode 100644 index 000000000000..edb62bbc96cf --- /dev/null +++ b/hbase-auth-filters/src/test/java/org/apache/hadoop/hbase/util/TestHttpExceptionUtils.java @@ -0,0 +1,54 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.hadoop.hbase.util; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Map; + +public class TestHttpExceptionUtils { + + @Test + public void testCreateServletException() throws IOException { + StringWriter writer = new StringWriter(); + PrintWriter printWriter = new PrintWriter(writer); + HttpServletResponse response = Mockito.mock(HttpServletResponse.class); + Mockito.when(response.getWriter()).thenReturn(printWriter); + int status = HttpServletResponse.SC_INTERNAL_SERVER_ERROR; + Exception ex = new IOException("Hello IOEX"); + HttpExceptionUtils.createServletExceptionResponse(response, status, ex); + Mockito.verify(response).setStatus(status); + Mockito.verify(response).setContentType(Mockito.eq("application/json")); + ObjectMapper mapper = new ObjectMapper(); + Map json = mapper.readValue(writer.toString(), Map.class); + json = (Map) json.get(HttpExceptionUtils.ERROR_JSON); + Assert.assertEquals(IOException.class.getName(), + json.get(HttpExceptionUtils.ERROR_CLASSNAME_JSON)); + Assert.assertEquals(IOException.class.getSimpleName(), + json.get(HttpExceptionUtils.ERROR_EXCEPTION_JSON)); + Assert.assertEquals("Hello IOEX", + json.get(HttpExceptionUtils.ERROR_MESSAGE_JSON)); + } +} diff --git a/hbase-http/pom.xml b/hbase-http/pom.xml index d64e6cd7fa84..0e3fb46d5e58 100644 --- a/hbase-http/pom.xml +++ b/hbase-http/pom.xml @@ -70,6 +70,10 @@ org.apache.hbase hbase-hadoop-compat + + org.apache.hbase + hbase-auth-filters + org.apache.hbase diff --git a/hbase-http/src/main/java/org/apache/hadoop/hbase/http/HttpServer.java b/hbase-http/src/main/java/org/apache/hadoop/hbase/http/HttpServer.java index fe2a9a48c210..e92726a82d8f 100644 --- a/hbase-http/src/main/java/org/apache/hadoop/hbase/http/HttpServer.java +++ b/hbase-http/src/main/java/org/apache/hadoop/hbase/http/HttpServer.java @@ -56,10 +56,10 @@ import org.apache.hadoop.hbase.http.log.LogLevel; import org.apache.hadoop.hbase.util.ReflectionUtils; import org.apache.hadoop.hbase.util.Threads; -import org.apache.hadoop.security.AuthenticationFilterInitializer; +import org.apache.hadoop.hbase.http.lib.AuthenticationFilterInitializer; import org.apache.hadoop.security.SecurityUtil; import org.apache.hadoop.security.UserGroupInformation; -import org.apache.hadoop.security.authentication.server.AuthenticationFilter; +import org.apache.hadoop.hbase.security.authentication.server.AuthenticationFilter; import org.apache.hadoop.security.authorize.AccessControlList; import org.apache.hadoop.security.authorize.ProxyUsers; import org.apache.hadoop.util.Shell; diff --git a/hbase-http/src/main/java/org/apache/hadoop/hbase/http/ProxyUserAuthenticationFilter.java b/hbase-http/src/main/java/org/apache/hadoop/hbase/http/ProxyUserAuthenticationFilter.java index 494a30c3e77e..da9773c7d40b 100644 --- a/hbase-http/src/main/java/org/apache/hadoop/hbase/http/ProxyUserAuthenticationFilter.java +++ b/hbase-http/src/main/java/org/apache/hadoop/hbase/http/ProxyUserAuthenticationFilter.java @@ -33,10 +33,10 @@ import javax.servlet.http.HttpServletResponse; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.security.UserGroupInformation; -import org.apache.hadoop.security.authentication.server.AuthenticationFilter; +import org.apache.hadoop.hbase.security.authentication.server.AuthenticationFilter; import org.apache.hadoop.security.authorize.AuthorizationException; import org.apache.hadoop.security.authorize.ProxyUsers; -import org.apache.hadoop.util.HttpExceptionUtils; +import org.apache.hadoop.hbase.util.HttpExceptionUtils; import org.apache.hadoop.util.StringUtils; import org.apache.yetus.audience.InterfaceAudience; import org.apache.yetus.audience.InterfaceStability; diff --git a/hbase-http/src/main/java/org/apache/hadoop/hbase/http/lib/AuthenticationFilterInitializer.java b/hbase-http/src/main/java/org/apache/hadoop/hbase/http/lib/AuthenticationFilterInitializer.java index f3a10d6a54e3..7ecb139426a9 100644 --- a/hbase-http/src/main/java/org/apache/hadoop/hbase/http/lib/AuthenticationFilterInitializer.java +++ b/hbase-http/src/main/java/org/apache/hadoop/hbase/http/lib/AuthenticationFilterInitializer.java @@ -26,8 +26,8 @@ import org.apache.hadoop.hbase.http.FilterInitializer; import org.apache.hadoop.hbase.http.HttpServer; import org.apache.hadoop.security.SecurityUtil; -import org.apache.hadoop.security.authentication.server.AuthenticationFilter; -import org.apache.hadoop.security.authentication.server.KerberosAuthenticationHandler; +import org.apache.hadoop.hbase.security.authentication.server.AuthenticationFilter; +import org.apache.hadoop.hbase.security.authentication.server.KerberosAuthenticationHandler; import org.apache.yetus.audience.InterfaceAudience; /** diff --git a/hbase-http/src/main/java/org/apache/hadoop/hbase/http/log/LogLevel.java b/hbase-http/src/main/java/org/apache/hadoop/hbase/http/log/LogLevel.java index 915f7e299183..c7ae7716ecfb 100644 --- a/hbase-http/src/main/java/org/apache/hadoop/hbase/http/log/LogLevel.java +++ b/hbase-http/src/main/java/org/apache/hadoop/hbase/http/log/LogLevel.java @@ -41,7 +41,7 @@ import org.apache.hadoop.security.authentication.client.AuthenticatedURL; import org.apache.hadoop.security.authentication.client.KerberosAuthenticator; import org.apache.hadoop.security.ssl.SSLFactory; -import org.apache.hadoop.util.ServletUtil; +import org.apache.hadoop.hbase.util.ServletUtil; import org.apache.hadoop.util.Tool; import org.apache.yetus.audience.InterfaceAudience; import org.slf4j.Logger; diff --git a/hbase-http/src/test/java/org/apache/hadoop/hbase/http/TestHttpCookieFlag.java b/hbase-http/src/test/java/org/apache/hadoop/hbase/http/TestHttpCookieFlag.java index 91935a97da62..ba62e87e97d4 100644 --- a/hbase-http/src/test/java/org/apache/hadoop/hbase/http/TestHttpCookieFlag.java +++ b/hbase-http/src/test/java/org/apache/hadoop/hbase/http/TestHttpCookieFlag.java @@ -39,7 +39,7 @@ import org.apache.hadoop.hbase.testclassification.MiscTests; import org.apache.hadoop.hbase.testclassification.SmallTests; import org.apache.hadoop.net.NetUtils; -import org.apache.hadoop.security.authentication.server.AuthenticationFilter; +import org.apache.hadoop.hbase.security.authentication.server.AuthenticationFilter; import org.apache.hadoop.security.ssl.KeyStoreTestUtil; import org.apache.hadoop.security.ssl.SSLFactory; import org.junit.AfterClass; diff --git a/hbase-http/src/test/java/org/apache/hadoop/hbase/http/lib/TestAuthenticationFilterInitializer.java b/hbase-http/src/test/java/org/apache/hadoop/hbase/http/lib/TestAuthenticationFilterInitializer.java index 68c48f282737..5fce528e5f39 100644 --- a/hbase-http/src/test/java/org/apache/hadoop/hbase/http/lib/TestAuthenticationFilterInitializer.java +++ b/hbase-http/src/test/java/org/apache/hadoop/hbase/http/lib/TestAuthenticationFilterInitializer.java @@ -27,7 +27,7 @@ import org.apache.hadoop.hbase.http.HttpServer; import org.apache.hadoop.hbase.testclassification.MiscTests; import org.apache.hadoop.hbase.testclassification.SmallTests; -import org.apache.hadoop.security.authentication.server.AuthenticationFilter; +import org.apache.hadoop.hbase.security.authentication.server.AuthenticationFilter; import org.junit.ClassRule; import org.junit.Test; import org.junit.experimental.categories.Category; diff --git a/hbase-mapreduce/pom.xml b/hbase-mapreduce/pom.xml index 9854ccf98330..c0d75fe0461e 100644 --- a/hbase-mapreduce/pom.xml +++ b/hbase-mapreduce/pom.xml @@ -309,6 +309,14 @@ javax.ws.rs-api test + + + javax.validation + validation-api + 2.0.1.Final + test + org.apache.hadoop diff --git a/hbase-rest/pom.xml b/hbase-rest/pom.xml index 4797de9ef89f..157a3be9cfd5 100644 --- a/hbase-rest/pom.xml +++ b/hbase-rest/pom.xml @@ -118,6 +118,10 @@ test-jar test + + org.apache.hbase + hbase-auth-filters + org.apache.hbase.thirdparty diff --git a/hbase-rest/src/main/java/org/apache/hadoop/hbase/rest/filter/AuthFilter.java b/hbase-rest/src/main/java/org/apache/hadoop/hbase/rest/filter/AuthFilter.java index c996e75f9376..506f544f8b2b 100644 --- a/hbase-rest/src/main/java/org/apache/hadoop/hbase/rest/filter/AuthFilter.java +++ b/hbase-rest/src/main/java/org/apache/hadoop/hbase/rest/filter/AuthFilter.java @@ -32,7 +32,7 @@ import org.apache.hadoop.hbase.util.DNS; import org.apache.hadoop.hbase.util.Strings; import org.apache.hadoop.security.SecurityUtil; -import org.apache.hadoop.security.authentication.server.AuthenticationFilter; +import org.apache.hadoop.hbase.security.authentication.server.AuthenticationFilter; import org.apache.yetus.audience.InterfaceAudience; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/pom.xml b/pom.xml index 13820e0b5296..9c426542905c 100644 --- a/pom.xml +++ b/pom.xml @@ -774,6 +774,7 @@ hbase-build-configuration + hbase-auth-filters hbase-replication hbase-balancer hbase-mapreduce @@ -1190,6 +1191,11 @@ ${project.version} test-jar + + org.apache.hbase + hbase-auth-filters + ${project.version} + org.apache.hbase hbase-balancer @@ -2643,14 +2649,15 @@ org.apache.hadoop.thirdparty.** - - true - 512 - Use ZooKeeper directly - - org.apache.curator.** - - + + + + + + + + + true 512 @@ -2933,6 +2940,12 @@ io.github.devacfr.maven.skins reflow-velocity-tools ${reflow-maven-skin.version} + + + log4j + log4j + +