Skip to content

Commit

Permalink
Allow configuring include/exclude path matching
Browse files Browse the repository at this point in the history
- New `trigger_rules` configuration option
- Only run the OIDC filter code when the request path matches a trigger
  rule
- We borrowed some of the matching code from Istio's Authentication
  Policy's C++ implementation

[Fixes #62]

Signed-off-by: Peter Chen <pchen@pivotal.io>
  • Loading branch information
cfryanr authored and Peter Chen committed Mar 3, 2020
1 parent c104127 commit 234fe39
Show file tree
Hide file tree
Showing 14 changed files with 410 additions and 30 deletions.
46 changes: 46 additions & 0 deletions config/config.proto
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,50 @@ message Config {
// for processing. The total number of running threads, including the main thread, will be N+1.
// Required.
uint32 threads = 5 [(validate.rules).uint32.gte = 1];

// List of trigger rules to decide if the Authservice should be used to authenticate the
// request. The Authservice authentication happens if any one of the rules matched.
// If the list is not empty and none of the rules matched, the request will be allowed
// to proceed without Authservice authentication.
// The format and semantics of `trigger_rules` are the same as the `triggerRules` setting
// on the Istio Authentication Policy
// (see https://istio.io/docs/reference/config/security/istio.authentication.v1alpha1).
// CAUTION: Be sure that your configured `OIDCConfig.callback` and `OIDCConfig.logout` paths
// each satisfies at least one of the trigger rules, or else the Authservice will not be able to
// intercept requests made to those paths to perform the appropriate login/logout behavior.
// Optional. Leave this empty to always trigger authentication for all paths.
repeated TriggerRule trigger_rules = 9;
}

// Trigger rule to match against a request. The trigger rule is satisfied if
// and only if both rules, excluded_paths and include_paths are satisfied.
message TriggerRule {
// List of paths to be excluded from the request. The rule is satisfied if
// request path does not match to any of the path in this list.
// Optional.
repeated StringMatch excluded_paths = 1;

// List of paths that the request must include. If the list is not empty, the
// rule is satisfied if request path matches at least one of the path in the list.
// If the list is empty, the rule is ignored, in other words the rule is always satisfied.
// Optional.
repeated StringMatch included_paths = 2;
}

// Describes how to match a given string. Match is case-sensitive.
message StringMatch {
oneof match_type {
// exact string match.
string exact = 1;

// prefix-based match.
string prefix = 2;

// suffix-based match.
string suffix = 3;

// ECMAscript style regex-based match as defined by [EDCA-262](http://en.cppreference.com/w/cpp/regex/ecmascript).
// Example: "^/pets/(.*?)?"
string regex = 4;
}
}
4 changes: 3 additions & 1 deletion config/oidc/config.proto
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ message OIDCConfig {
// Required.
string client_secret = 7 [(validate.rules).string.min_len = 1];

// Optional additional scopes passed to the OIDC Provider in the
// Additional scopes passed to the OIDC Provider in the
// [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest).
// The `openid` scope is always sent to the OIDC Provider, and does not need to be specified here.
// Required, but an empty array is allowed.
Expand Down Expand Up @@ -136,6 +136,7 @@ message OIDCConfig {
// that it was started, but can still timeout due to being idle.
// When both `max_absolute_session_timeout` and `max_session_idle_timeout` are zero, then sessions will never
// expire. These settings do not affect how quickly the OIDC tokens contained inside the user's session expire.
// Optional.
uint32 max_absolute_session_timeout = 13;

// The Authservice associates obtained OIDC tokens with a session ID in a session store.
Expand All @@ -147,6 +148,7 @@ message OIDCConfig {
// but can still consider timeout based on maximum absolute time since added.
// When both `max_absolute_session_timeout` and `max_session_idle_timeout` are zero, then sessions will never
// expire. These settings do not affect how quickly the OIDC tokens contained inside the user's session expire.
// Optional.
uint32 max_session_idle_timeout = 14;

// When specified, the Authservice will trust the specified Certificate Authority when performing HTTPS calls to
Expand Down
32 changes: 29 additions & 3 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ The top-level configuration object. For a simple example, see the [sample JSON i
| listen_port | The TCP port for the authservice to listen for incoming requests to process. Required. | int32 |
| log_level | The verbosity of logs generated by the authservice. Must be one of `trace`, `debug`, `info', 'error' or 'critical'. Required. | string |
| threads | The number of threads in the thread pool to use for processing. The main thread will be used for accepting connections, before sending them to the thread-pool for processing. The total number of running threads, including the main thread, will be N+1. Required. | uint32 |
| trigger_rules | List of trigger rules to decide if the Authservice should be used to authenticate the request. The Authservice authentication happens if any one of the rules matched. If the list is not empty and none of the rules matched, the request will be allowed to proceed without Authservice authentication. The format and semantics of `trigger_rules` are the same as the `triggerRules` setting on the Istio Authentication Policy (see https://istio.io/docs/reference/config/security/istio.authentication.v1alpha1). CAUTION: Be sure that your configured `OIDCConfig.callback` and `OIDCConfig.logout` paths each satisfies at least one of the trigger rules, or else the Authservice will not be able to intercept requests made to those paths to perform the appropriate login/logout behavior. Optional. Leave this empty to always trigger authentication for all paths. | (slice of) TriggerRule |



Expand Down Expand Up @@ -89,17 +90,31 @@ The configuration of an OpenID Connect filter that can be used to retrieve ident
| callback | This value will be used as the `redirect_uri` param of the authorization code grant [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). This URL must be one of the Redirection URI values for the Client pre-registered at the OIDC provider. Note: The Istio gateway's VirtualService must be prepared to ensure that this URL will get routed to the service so that the authservice can intercept the request and handle it (see [example](https://github.com/istio-ecosystem/authservice/blob/master/bookinfo-example/config/bookinfo-gateway.yaml)). Required. | common.Endpoint |
| client_id | The OIDC client ID assigned to the filter to be used in the [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). Required. | string |
| client_secret | The OIDC client secret assigned to the filter to be used in the [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). Required. | string |
| scopes | Optional additional scopes passed to the OIDC Provider in the [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). The `openid` scope is always sent to the OIDC Provider, and does not need to be specified here. Required, but an empty array is allowed. | (slice of) string |
| scopes | Additional scopes passed to the OIDC Provider in the [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). The `openid` scope is always sent to the OIDC Provider, and does not need to be specified here. Required, but an empty array is allowed. | (slice of) string |
| cookie_name_prefix | A unique identifier of the authservice's browser cookies. Can be any string. Only needed when multiple services in the same domain are each protected by their own authservice, in which case each service's authservice should have a unique value to avoid cookie name conflicts. Optional. | string |
| id_token | The configuration for adding ID Tokens as headers to requests forwarded to a service. Required. | TokenConfig |
| access_token | The configuration for adding Access Tokens as headers to requests forwarded to a service. Optional. | TokenConfig |
| logout | When specified, the authservice will destroy the authservice session when a request is made to the configured path. Optional. | LogoutConfig |
| max_absolute_session_timeout | The Authservice associates obtained OIDC tokens with a session ID in a session store. It also stores some temporary information during the login process into the session store, which will be removed when the user finishes the login. This configuration option sets the number of seconds since a user's session with the Authservice has started until that session should expire. When configured to `0`, which is the default value, the session will never timeout based on the time that it was started, but can still timeout due to being idle. When both `max_absolute_session_timeout` and `max_session_idle_timeout` are zero, then sessions will never expire. These settings do not affect how quickly the OIDC tokens contained inside the user's session expire. | uint32 |
| max_session_idle_timeout | The Authservice associates obtained OIDC tokens with a session ID in a session store. It also stores some temporary information during the login process into the session store, which will be removed when the user finishes the login. This configuration option sets the number of seconds since the most recent incoming request from that user until the user's session with the Authservice should expire. When configured to `0`, which is the default value, session expiration will not consider idle time, but can still consider timeout based on maximum absolute time since added. When both `max_absolute_session_timeout` and `max_session_idle_timeout` are zero, then sessions will never expire. These settings do not affect how quickly the OIDC tokens contained inside the user's session expire. | uint32 |
| max_absolute_session_timeout | The Authservice associates obtained OIDC tokens with a session ID in a session store. It also stores some temporary information during the login process into the session store, which will be removed when the user finishes the login. This configuration option sets the number of seconds since a user's session with the Authservice has started until that session should expire. When configured to `0`, which is the default value, the session will never timeout based on the time that it was started, but can still timeout due to being idle. When both `max_absolute_session_timeout` and `max_session_idle_timeout` are zero, then sessions will never expire. These settings do not affect how quickly the OIDC tokens contained inside the user's session expire. Optional. | uint32 |
| max_session_idle_timeout | The Authservice associates obtained OIDC tokens with a session ID in a session store. It also stores some temporary information during the login process into the session store, which will be removed when the user finishes the login. This configuration option sets the number of seconds since the most recent incoming request from that user until the user's session with the Authservice should expire. When configured to `0`, which is the default value, session expiration will not consider idle time, but can still consider timeout based on maximum absolute time since added. When both `max_absolute_session_timeout` and `max_session_idle_timeout` are zero, then sessions will never expire. These settings do not affect how quickly the OIDC tokens contained inside the user's session expire. Optional. | uint32 |
| trusted_certificate_authority | When specified, the Authservice will trust the specified Certificate Authority when performing HTTPS calls to the Token Endpoint of the OIDC Identity Provider. Optional. | string |



##### message `StringMatch` (config/config.proto)

Describes how to match a given string. Match is case-sensitive.

| Field | Description | Type |
| ----- | ----------- | ---- |
| match_type | | oneof |
| exact | exact string match. | string |
| prefix | prefix-based match. | string |
| suffix | suffix-based match. | string |
| regex | ECMAscript style regex-based match as defined by [EDCA-262](http://en.cppreference.com/w/cpp/regex/ecmascript). Example: "^/pets/(.*?)?" | string |



##### message `TokenConfig` (config/oidc/config.proto)

Defines how a token obtained through an OIDC flow is forwarded to services.
Expand All @@ -111,3 +126,14 @@ Defines how a token obtained through an OIDC flow is forwarded to services.



##### message `TriggerRule` (config/config.proto)

Trigger rule to match against a request. The trigger rule is satisfied if and only if both rules, excluded_paths and include_paths are satisfied.

| Field | Description | Type |
| ----- | ----------- | ---- |
| excluded_paths | List of paths to be excluded from the request. The rule is satisfied if request path does not match to any of the path in this list. Optional. | (slice of) StringMatch |
| included_paths | List of paths that the request must include. If the list is not empty, the rule is satisfied if request path matches at least one of the path in the list. If the list is empty, the rule is ignored, in other words the rule is always satisfied. Optional. | (slice of) StringMatch |



10 changes: 10 additions & 0 deletions src/common/utilities/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,13 @@ xx_library(
deps = [
],
)

xx_library(
name = "trigger_rules",
srcs = ["trigger_rules.cc"],
hdrs = ["trigger_rules.h"],
deps = [
"@com_github_abseil-cpp//absl/strings:strings",
"//config:config_cc",
],
)
79 changes: 79 additions & 0 deletions src/common/utilities/trigger_rules.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#include <regex>
#include "trigger_rules.h"
#include "absl/strings/match.h"

namespace authservice {
namespace common {
namespace utilities {
namespace trigger_rules {

// Note: these functions are heavily inspired by
// https://github.com/istio/proxy/blob/master/src/envoy/http/authn/authn_utils.cc

bool MatchString(absl::string_view str, const config::StringMatch& match) {
switch (match.match_type_case()) {
case config::StringMatch::kExact: {
return match.exact() == str;
}
case config::StringMatch::kPrefix: {
return absl::StartsWith(str, match.prefix());
}
case config::StringMatch::kSuffix: {
return absl::EndsWith(str, match.suffix());
}
case config::StringMatch::kRegex: {
return std::regex_match(std::string(str), std::regex(match.regex()));
}
default:
return false;
}
}

static bool matchRule(absl::string_view path, const config::TriggerRule& rule) {
for (const auto& excluded : rule.excluded_paths()) {
if (MatchString(path, excluded)) {
// The rule is not matched if any of excluded_paths matched.
return false;
}
}

if (rule.included_paths_size() > 0) {
for (const auto& included : rule.included_paths()) {
if (MatchString(path, included)) {
// The rule is matched if any of included_paths matched.
return true;
}
}

// The rule is not matched if included_paths is not empty and none of them
// matched.
return false;
}

// The rule is matched if none of excluded_paths matched and included_paths is
// empty.
return true;
}

bool TriggerRuleMatchesPath(
absl::string_view path,
const google::protobuf::RepeatedPtrField<config::TriggerRule> &trigger_rules_config) {

// If the path is empty which shouldn't happen for a HTTP request or if
// there are no trigger rules at all, then simply return true as if there're
// no per-path jwt support.
if (path == "" || trigger_rules_config.size() == 0) {
return true;
}
for (const auto& rule : trigger_rules_config) {
if (matchRule(path, rule)) {
return true;
}
}
return false;
}

} // namespace trigger_rules
} // namespace utilities
} // namespace common
} // namespace authservice
23 changes: 23 additions & 0 deletions src/common/utilities/trigger_rules.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#ifndef AUTHSERVICE_TRIGGER_RULES_H
#define AUTHSERVICE_TRIGGER_RULES_H

#include "config/config.pb.h"
#include "absl/strings/string_view.h"

namespace authservice {
namespace common {
namespace utilities {
namespace trigger_rules {

bool TriggerRuleMatchesPath(
absl::string_view path,
const google::protobuf::RepeatedPtrField<config::TriggerRule> &trigger_rules_config);

bool MatchString(absl::string_view str, const config::StringMatch& match);

} // namespace trigger_rules
} // namespace utilities
} // namespace common
} // namespace authservice

#endif //AUTHSERVICE_TRIGGER_RULES_H
2 changes: 2 additions & 0 deletions src/service/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ cc_library(
"//config:config_cc",
"//src/config",
"//src/filters:filter_chain",
"//src/common/http",
"//src/common/utilities:trigger_rules",
"@boost//:thread",
"@com_github_gabime_spdlog//:spdlog",
"@com_github_grpc_grpc//:grpc++",
Expand Down
24 changes: 21 additions & 3 deletions src/service/service_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@
#include <grpcpp/grpcpp.h>
#include <memory>
#include "spdlog/spdlog.h"
#include "src/common/http/http.h"
#include "src/common/utilities/trigger_rules.h"

namespace authservice {
namespace service {

AuthServiceImpl::AuthServiceImpl(const config::Config& config) {
AuthServiceImpl::AuthServiceImpl(const config::Config &config)
: trigger_rules_config_(config.trigger_rules()) {
for (const auto &chain_config : config.chains()) {
std::unique_ptr<filters::FilterChain> chain(new filters::FilterChainImpl(chain_config));
chains_.push_back(std::move(chain));
Expand All @@ -19,10 +22,22 @@ ::grpc::Status AuthServiceImpl::Check(
::envoy::service::auth::v2::CheckResponse *response) {
spdlog::trace("{}", __func__);
try {
auto request_path = common::http::http::DecodePath(request->attributes().request().http().path())[0];
if (!common::utilities::trigger_rules::TriggerRuleMatchesPath(request_path, trigger_rules_config_)) {
spdlog::debug(
"{}: no matching trigger rule, so allowing request to proceed without any authservice functionality {}://{}{} ",
__func__,
request->attributes().request().http().scheme(), request->attributes().request().http().host(),
request->attributes().request().http().path());
return ::grpc::Status::OK;
}

// Find a configured processing chain.
for (auto &chain : chains_) {
if (chain->Matches(request)) {
spdlog::debug("{}: processing request {}://{}{} with filter chain {}", __func__, request->attributes().request().http().scheme(), request->attributes().request().http().host(), request->attributes().request().http().path(), chain->Name());
spdlog::debug("{}: processing request {}://{}{} with filter chain {}", __func__,
request->attributes().request().http().scheme(), request->attributes().request().http().host(),
request->attributes().request().http().path(), chain->Name());
// Create a new instance of a processor.
auto processor = chain->New();
auto status = processor->Process(request, response);
Expand Down Expand Up @@ -50,8 +65,11 @@ ::grpc::Status AuthServiceImpl::Check(
}
}
}

// No matching filter chain found. Allow request to continue.
spdlog::debug("{}: no matching filter chain for request to {}://{}{} ", __func__, request->attributes().request().http().scheme(), request->attributes().request().http().host(), request->attributes().request().http().path());
spdlog::debug("{}: no matching filter chain for request to {}://{}{} ", __func__,
request->attributes().request().http().scheme(), request->attributes().request().http().host(),
request->attributes().request().http().path());
return ::grpc::Status::OK;
} catch (const std::exception &exception) {
spdlog::error("%s unexpected error: %s", __func__, exception.what());
Expand Down
Loading

0 comments on commit 234fe39

Please sign in to comment.