Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

adapter: Prevent login to mz_system externally #14093

Merged
merged 14 commits into from
Aug 15, 2022

Conversation

jkosh44
Copy link
Contributor

@jkosh44 jkosh44 commented Aug 4, 2022

mz_system is the system user that is responsible for executing commands
that only an administrator should be doing. This commit prevents
logging into the mz_system user from an external port. You can only
login to the mz_system user via internal ports.

Works towards resolving: https://github.com/MaterializeInc/cloud/issues/3292

Motivation

This PR adds a known-desirable feature.

Checklist

  • This PR has adequate test coverage / QA involvement has been duly considered.

  • This PR evolves an existing $T ⇔ Proto$T mapping (possibly in a backwards-incompatible way) and therefore is tagged with a T-protobuf label.

  • This PR includes the following user-facing behavior changes:

    • You can no longer use the mz_system role with an external port.

@jkosh44 jkosh44 force-pushed the mz-system-login branch 11 times, most recently from 654ecc2 to 3dfd866 Compare August 10, 2022 23:21
@jkosh44 jkosh44 marked this pull request as ready for review August 10, 2022 23:45
that only an administrator should be doing. This commit prevents
logging into the mz_system user from an external port. You can only
login to the mz_system user via internal ports.

Works towards resolving: MaterializeInc/cloud#3292
src/adapter/src/client.rs Outdated Show resolved Hide resolved
@@ -152,6 +164,8 @@ impl ConnClient {
session: Session,
create_user_if_not_exists: bool,
) -> Result<(SessionClient, StartupResponse), AdapterError> {
self.validate_user(session.user())?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this go wherever the frontegg login stuff also happens? I guess not because it needs the client type. But I'm not thrilled that "can i login?" is now in multiple places.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not very familiar with the Frontegg code, but I agree with your point. I'll do some digging and see if I can integrate it with the Frontegg stuff.

@@ -95,7 +95,8 @@ pub mod builtin;
pub mod storage;

pub const SYSTEM_CONN_ID: ConnectionId = 0;
const SYSTEM_USER: &str = "mz_system";
pub const SYSTEM_USER: &str = "mz_system";
pub const HTTP_DEFAULT_USER: &str = "mz_http_default_user";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very slight preference for this to be mz_http_internal_user because default suggests that it's a fallback and is regularly used on the external http connection.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is actually only used on the external http connection as a fallback if there's no user authenticated. So maybe it should be mz_http_default_external_user?

src/adapter/src/client.rs Outdated Show resolved Hide resolved
src/environmentd/src/lib.rs Outdated Show resolved Hide resolved
@@ -330,7 +331,7 @@ pub async fn serve(config: Config) -> Result<Server, anyhow::Error> {
let http_server = http::Server::new(http::Config {
tls: http_tls,
frontegg: config.frontegg,
adapter_client,
adapter_client: external_adapter_client,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where does the internal http server get its internal adapter client?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit perplexing to me. The internal http port uses the http::InternalServer struct (initialized above all of this on line 204) which doesn't use an adapter client and has the following endpoints:

  • /metrics
  • /api/livez
  • /api/opentelemetry/config

The external http port uses the http::Server struct which uses an adapter client and has the following endpoints:

  • /
  • /api/internal/catalog (This one is really weird since it contains "internal" in it's path but is only accessible from the external port).
  • /api/sql
  • /memory
  • /hierarchical-memory
  • /prof/
  • /static/*path

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, good point! We should move that internal catalog API to the internal HTTP server.

Copy link
Member

@benesch benesch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks very much for whipping this one up, @jkosh44! A few quick thoughts now having looked at your PR:

  • Can we make mz_http_default_user not a built-in role? It's something that only is relevant when you're running locally without authentication, so it'd be nice to hide it. What if we called the user anonymous, and created it automatically iff frontegg_auth.is_none() && tls_mode.is_none()?
  • I'd love to leave authentication the sole responsibility of the environmentd crate, rather than putting some of the logic into the adapter crate! At present it's kinda nice that the adapter layer just blindly trusts the user that you tell it. Can we instead add a check to the external HTTP and SQL servers that rejects any user name that begins with mz_? That'll give us the flexibility to add future restricted-access system users, should they become necessary.

Comment on lines 310 to 316
// Create http default user for local usage.
let create_default_user = frontegg.is_none() && tls_mode.is_none() && user == HTTP_DEFAULT_USER;
// Add the authenticated user as an extension so downstream handlers can
// inspect it if necessary.
req.extensions_mut().insert(AuthedUser {
user,
create_if_not_exists: frontegg.is_some(),
create_if_not_exists: frontegg.is_some() || create_default_user,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@benesch Is this what you had in mind for creating the http user on demand?

Copy link
Contributor Author

@jkosh44 jkosh44 Aug 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it expected ever that frontegg.is_none() and the tls mode is TlsMode::Require? If so this won't work because TlsMode::Require and frontegg.is_none() defaults the user to HTTP_DEFAULT_USER, but then we won't create the default user because tls_mode.is_some().

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm asking because there are tests in auth.rs that explicitly test the HTTP API with TlsMode::Require and no frontegg and expect it to work.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After discussing with @benesch in person, this condition should be something like frontegg.is_some() || tls_mode != TlsMode::AssumeUser.

@jkosh44
Copy link
Contributor Author

jkosh44 commented Aug 12, 2022

@mjibson @benesch I moved the authentication logic out of the adapter crate and into the pgwire and environmentd crates, into the function that already handles authentication. I couldn't find a common place to include the logic that both pgwire and environmentd would have access to that wasn't the adapter crate. If you have any ideas then let me know.

48848ec

@@ -93,7 +93,8 @@ pub mod builtin;
pub mod storage;

pub const SYSTEM_CONN_ID: ConnectionId = 0;
const SYSTEM_USER: &str = "mz_system";
pub const SYSTEM_USER: &str = "mz_system";
pub const HTTP_DEFAULT_USER: &str = "anonymous_http_user";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should start with mz_ because that's guaranteed to be a protected username suffix.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is possible because the way this works is that anonymous_http_user doesn't exist unless someone tries to use the HTTP API with a username. Then we will create anonymous_http_user on the spot as a user Role. However we don't allow any user roles to be created with the prefix mz_ because it's a reserved prefix.

@@ -988,7 +989,7 @@ fn test_auth() -> Result<(), Box<dyn Error>> {
assert: Assert::Success,
},
TestCase::Http {
user: "mz_system",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs a test for logging in as mz_system but failing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added

@maddyblue
Copy link
Contributor

doesn't exist unless someone tries to use the HTTP API with a username

Oh, I missed this and the discussion about it during my review. I didn't realize it's local only. Ok, LGTM then.

@jkosh44 jkosh44 enabled auto-merge (squash) August 15, 2022 20:12
@jkosh44 jkosh44 merged commit 041f326 into MaterializeInc:main Aug 15, 2022
@jkosh44 jkosh44 deleted the mz-system-login branch August 15, 2022 20:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants