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

Add OpenID authentication support using a KeyCloak server #720

Merged
merged 70 commits into from
May 2, 2024

Conversation

fmartingr
Copy link
Contributor

@fmartingr fmartingr commented Mar 27, 2024

Summary

  • Allow users to authenticate with Mattermost by using an OpenID provider
  • Adds provisioning for Keycloak server in in development mode with a postgresql database bundled in the same server
  • A default realm bundled into the load test with some default credentials.
    • An optional import file can be provided, if none is set, the default one is used.
    • Also a database dump can be provided in the form of an URI and the data will be imported into the SQL database.
  • An optional parameter to generate users in the Keycloak instance
  • Automatic configuration of the Mattermost instance by using the generated Keycloak server with the provided KeycloakClientID, KeycloakClientSecret and KeycloakRealmName (only needed when a custom import realm file is provided if the default values do not match).
  • Documentation

@fmartingr fmartingr self-assigned this Apr 4, 2024
@fmartingr fmartingr changed the title (WIP) Add OpenID authentication support using a KeyCloak server Add OpenID authentication support using a KeyCloak server Apr 4, 2024
@fmartingr fmartingr marked this pull request as ready for review April 4, 2024 12:23
Copy link
Member

@agarciamontoro agarciamontoro left a comment

Choose a reason for hiding this comment

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

This is brilliant, @fmartingr, thank you so much! I left some comments below, mostly nits but some other that do need discussing. I also have some higher-level questions:

  • Do we need to expose the settings for Keycloak's DB? I'd lean towards being more opinionated whenever possible so that we expose less settings. Given that we don't want to monitor/load-test this server, can we use a one-size-fits-all instance type? Maybe not only for the DB but also for the Keycloak server? Or do the specs differ a lot when using more users? In any case, I think we can at least hardcode the usage of mysql or postgres here.
  • Why do the docs say that production mode is not implemented? Is there anything missing? Also: do we really need the distinction? I guess development mode is just cheaper to deploy because of the lack of DB, right?
  • Could you add some docs on the logic around the prefix for the usernames? The overwriting of the users file path was a bit confusing for me at first.

docs/external_auth_providers.md Outdated Show resolved Hide resolved
deployment/terraform/assets/cluster.tf Show resolved Hide resolved
deployment/terraform/assets/cluster.tf Outdated Show resolved Hide resolved
deployment/terraform/assets/variables.tf Outdated Show resolved Hide resolved
deployment/config.go Show resolved Hide resolved
deployment/terraform/keycloak.go Outdated Show resolved Hide resolved
Comment on lines 172 to 175
// Check if users file exists. Prevents from creating users multiple times.
if _, err := os.Stat(usersTxtPath); err == nil || os.IsExist(err) {
return nil
}
Copy link
Member

Choose a reason for hiding this comment

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

Not sure if this is a dumb question, but: could it happen that the users file exists but that it doesn't contain the users we want? For example, we create a deployment with 10 users and then recreate it with 50.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I see that hapenning if that's an usual way of using the load test. Alternatively you could remove the file and the keycloak database to start from scratch.

Copy link
Member

Choose a reason for hiding this comment

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

I wouldn't discard that edge case, to be honest, so we should either:

  • Check the contents of the keycloak-users.txt file, not only that it exists
  • Clarify it in the docs
    Your call, I'm good with both :)

Copy link
Member

Choose a reason for hiding this comment

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

+1 to this. We should do a quick check to ensure that # of users in keycloak file is equal to # users in users.txt file. And if possible, have a way to add new users, or fail clearly in the logs. But importantly, there should never be a case of some users partially logging in via keycloak and some via email/pass.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed it to create the number of users setup in the configuration. If the number is higher it will create more users accordingly, using the number of lines in the users.txt file as the source. It will skip user creation if the number of lines is the same or higher than the expected number of users.

deployment/terraform/keycloak.go Outdated Show resolved Hide resolved
username := fmt.Sprintf("keycloak-auto-%06d", i) // this is the password too
email := username + "@test.mattermost.com"

_, err := sshc.RunCommand(fmt.Sprintf("%s/kcadm.sh create users -r %s -s username=%s -s enabled=true -s email=%s", keycloakBinPath, t.config.ExternalAuthProviderSettings.KeycloakRealmName, username, email))
Copy link
Member

Choose a reason for hiding this comment

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

Oh wow there is no way to create more than one user at a time? 😭

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We could call the API I guess, but there's no guarantee it would be faster.

deployment/config.go Outdated Show resolved Hide resolved
@agnivade
Copy link
Member

Is this ready to review now?

@fmartingr fmartingr removed the request for review from agnivade April 10, 2024 05:45
@fmartingr
Copy link
Contributor Author

Is this ready to review now?

I have to address Alejandro's comments and make some changes from the work in the LT environment. I will assign you again when that's done.

Co-authored-by: Alejandro García Montoro <[email protected]>
deployment/config.go Outdated Show resolved Hide resolved
@agnivade agnivade added 2: Dev Review Requires review by a core committer and removed Do Not Merge/ Awaiting Load-Test labels Apr 30, 2024
Copy link
Member

@agnivade agnivade left a comment

Choose a reason for hiding this comment

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

New commits look good to me. The only small thing is that let's change the default keycloak instance spec to c7i series. Thanks again for all the hard work!

Copy link
Contributor

@streamer45 streamer45 left a comment

Choose a reason for hiding this comment

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

Very quickly skimmed through this. Looks great overall. Left some comments.

Comment on lines +400 to +402
// Check if the user has a custom authentication type. Custom authentication types are
// specified by prepending the username with the authentication type followed by a colon.
// Example: "openid:[email protected] user1password"
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we add this information to docs/external_auth_providers.md?

Comment on lines +403 to +411
if usernameParts := strings.Split(username, ":"); len(usernameParts) > 1 {
authenticationType = usernameParts[0]
username = usernameParts[1]

// Fix the email as well
if emailParts := strings.Split(email, ":"); len(emailParts) > 1 {
email = emailParts[1]
}
}
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 could become a util function.

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 going to make a note of this but refrain from modifying too much logic until this is merged now that we have this working. I can address the rest of the comments in individual PRs later.

@@ -33,12 +33,19 @@ type UserEntity struct {
wsServerSeq int64
}

const (
AuthenticationTypeMattermost = "mattermost"
Copy link
Contributor

Choose a reason for hiding this comment

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

+1

Comment on lines +114 to +121
me, _, err := ue.client.GetMe(context.Background(), "")
if err != nil {
return fmt.Errorf("error while getting user: %w", err)
}

if err := ue.store.SetUser(me); err != nil {
return fmt.Errorf("error while setting user: %w", err)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to do this here? I'd expect this method to purely authenticate (e.g. set the token) and then deal with everything else in SignUp/Login.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There's a mix of logic here, is either I get the user and set it here, or I get the user by username later and set it in the other method. I've set a TODO to change the logic in a separate PR since I don't want to compromise or delay the test we're going to run today.

Comment on lines 166 to 174
appClients := make([]*ssh.Client, len(output.Instances))
for i, instance := range output.Instances {
client, err := extAgent.NewClient(instance.PublicIP)
if err != nil {
return fmt.Errorf("error in getting ssh connection %w", err)
}
defer client.Close()
appClients[i] = client
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Why are we initializing N clients if we just use the first?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch.

Comment on lines +34 to +37
sshc, err := extAgent.NewClient(t.output.KeycloakServer.PublicIP)
if err != nil {
return fmt.Errorf("error in getting ssh connection %w", err)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we close this client on returning?

Comment on lines 97 to 100
// Values for the keycloak.env file
keycloakEnvFileContents := []string{
// Enable health endpoints
"KC_HEALTH_ENABLED=true",
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe we could use a template file as we do with other plaintext assets.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, that's probably better with the current logic. At the beginning this was changuing depending on the deployer configuration that's why I followed this approach as it was simpler, but now that variables are fixed it makes more sense to have this on a static asset. Making a note for that.


// Check if users file exists and has the expected number of users. If the file has less users than expected,
// we will start creating users from the next number, otherwise we will skip user creation.
if _, err := os.Stat(usersTxtPath); err == nil || os.IsExist(err) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why would Stat return an error if the file exists?

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 think this was supposed to be os.IsNotExist in a previous apporach or something else with a different logic and I just inherited it here, this doesn't make any sense at the moment.

Copy link
Member

@agarciamontoro agarciamontoro left a comment

Choose a reason for hiding this comment

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

Again, amazing work, @fmartingr. Nothing else to add. Thank you!

@agnivade agnivade added 4: Reviews Complete All reviewers have approved the pull request and removed 2: Dev Review Requires review by a core committer labels May 2, 2024
@agnivade
Copy link
Member

agnivade commented May 2, 2024

Merge it at your convenience. 😄

@fmartingr
Copy link
Contributor Author

fmartingr commented May 2, 2024

Merge it at your convenience. 😄

@streamer45 I've put comments on heavy logic changes that you mentioned so I can address those on separate PRs so I can merge this for now since it's getting too big and not easy to review. Thank you for reviewing!

The only logic change was: 12b5487

@fmartingr fmartingr merged commit 0c4ddae into master May 2, 2024
1 check passed
@fmartingr fmartingr deleted the feat/openid-users branch May 2, 2024 10:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
4: Reviews Complete All reviewers have approved the pull request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants