diff --git a/internal/cmd/docker/keycloak.go b/internal/cmd/docker/keycloak.go new file mode 100644 index 0000000000000000000000000000000000000000..7cb079ca38fed369a82b7994f34b816e67bc4570 --- /dev/null +++ b/internal/cmd/docker/keycloak.go @@ -0,0 +1,333 @@ +package docker + +import ( + _ "embed" + + "github.com/spf13/cobra" + "gitlab.wikimedia.org/repos/releng/cli/internal/cli" + mwdd "gitlab.wikimedia.org/repos/releng/cli/internal/mwdd" +) + +//go:embed long/mwdd_keycloak.md +var mwddKeycloakLong string + +func NewKeycloakCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "keycloak", + Short: "Keycloak service", + Long: cli.RenderMarkdown(mwddKeycloakLong), + Aliases: []string{"kc"}, + } + cmd.AddCommand(mwdd.NewServiceCreateCmd("keycloak")) + cmd.AddCommand(mwdd.NewServiceDestroyCmd("keycloak")) + cmd.AddCommand(mwdd.NewServiceStopCmd("keycloak")) + cmd.AddCommand(mwdd.NewServiceStartCmd("keycloak")) + cmd.AddCommand(mwdd.NewServiceExecCmd("keycloak", "keycloak")) + cmd.AddCommand(NewKeycloakAddCmd()) + cmd.AddCommand(NewKeycloakDeleteCmd()) + cmd.AddCommand(NewKeycloakListCmd()) + cmd.AddCommand(NewKeycloakGetCmd()) + return cmd +} + +func NewKeycloakAddCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "add", + Short: "Add a keycloak realm, client, or user", + } + cmd.AddCommand(NewKeycloakAddRealmCmd()) + cmd.AddCommand(NewKeycloakAddClientCmd()) + cmd.AddCommand(NewKeycloakAddUserCmd()) + return cmd +} + +func NewKeycloakAddRealmCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "realm ", + Short: "Add a keycloak realm", + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + mwdd.DefaultForUser().EnsureReady() + KeycloakLogin() + mwdd.DefaultForUser().Exec("keycloak", []string{ + "/opt/keycloak/bin/kcadm.sh", + "create", + "realms", + "--set", "enabled=true", + "--set", "realm=" + args[0], + }, "root") + }, + } + return cmd +} + +func NewKeycloakAddClientCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "client ", + Short: "Add a keycloak client to a realm", + Args: cobra.MinimumNArgs(2), + Run: func(cmd *cobra.Command, args []string) { + mwdd.DefaultForUser().EnsureReady() + KeycloakLogin() + mwdd.DefaultForUser().Exec("keycloak", []string{ + "/mwdd/create_client.sh", + args[0], + args[1], + }, "root") + }, + } + return cmd +} + +func NewKeycloakAddUserCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "user ", + Short: "Add a keycloak user to a realm with a temporary password", + Args: cobra.MinimumNArgs(3), + Run: func(cmd *cobra.Command, args []string) { + mwdd.DefaultForUser().EnsureReady() + KeycloakLogin() + mwdd.DefaultForUser().Exec("keycloak", []string{ + "/mwdd/create_user.sh", + args[0], + args[1], + args[2], + }, "root") + }, + } + return cmd +} + +func NewKeycloakDeleteCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "delete", + Short: "Delete keycloak realm, client, or user", + } + cmd.AddCommand(NewKeycloakDeleteRealmCmd()) + cmd.AddCommand(NewKeycloakDeleteClientCmd()) + cmd.AddCommand(NewKeycloakDeleteUserCmd()) + return cmd +} + +func NewKeycloakDeleteRealmCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "realm ", + Short: "Delete keycloak realm", + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + mwdd.DefaultForUser().EnsureReady() + KeycloakLogin() + mwdd.DefaultForUser().Exec("keycloak", []string{ + "/opt/keycloak/bin/kcadm.sh", + "delete", + "realms/" + args[0], + }, "root") + }, + } + return cmd +} + +func NewKeycloakDeleteClientCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "client ", + Short: "Delete keycloak client in a realm", + Args: cobra.MinimumNArgs(2), + Run: func(cmd *cobra.Command, args []string) { + mwdd.DefaultForUser().EnsureReady() + KeycloakLogin() + mwdd.DefaultForUser().Exec("keycloak", []string{ + "/mwdd/delete_client.sh", + args[0], + args[1], + }, "root") + }, + } + return cmd +} + +func NewKeycloakDeleteUserCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "user ", + Short: "Delete keycloak user in a realm", + Args: cobra.MinimumNArgs(2), + Run: func(cmd *cobra.Command, args []string) { + mwdd.DefaultForUser().EnsureReady() + KeycloakLogin() + mwdd.DefaultForUser().Exec("keycloak", []string{ + "/mwdd/delete_user.sh", + args[0], + args[1], + }, "root") + }, + } + return cmd +} + +func NewKeycloakListCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "List keycloak realms, clients, or users", + } + cmd.AddCommand(NewKeycloakListRealmsCmd()) + cmd.AddCommand(NewKeycloakListClientsCmd()) + cmd.AddCommand(NewKeycloakListUsersCmd()) + return cmd +} + +func NewKeycloakListRealmsCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "realms", + Short: "List keycloak realms", + Run: func(cmd *cobra.Command, args []string) { + mwdd.DefaultForUser().EnsureReady() + KeycloakLogin() + mwdd.DefaultForUser().Exec("keycloak", []string{ + "/opt/keycloak/bin/kcadm.sh", + "get", + "realms", + "--fields", "realm", + "--format", "csv", + "--noquotes", + }, "root") + }, + } + return cmd +} + +func NewKeycloakListClientsCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "clients ", + Short: "List keycloak clients in a realm", + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + mwdd.DefaultForUser().EnsureReady() + KeycloakLogin() + mwdd.DefaultForUser().Exec("keycloak", []string{ + "/opt/keycloak/bin/kcadm.sh", + "get", + "clients", + "--target-realm", args[0], + "--fields", "clientId", + "--format", "csv", + "--noquotes", + }, "root") + }, + } + return cmd +} + +func NewKeycloakListUsersCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "users ", + Short: "List keycloak users in a realm", + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + mwdd.DefaultForUser().EnsureReady() + KeycloakLogin() + mwdd.DefaultForUser().Exec("keycloak", []string{ + "/opt/keycloak/bin/kcadm.sh", + "get", + "users", + "--target-realm", args[0], + "--fields", "username", + "--format", "csv", + "--noquotes", + }, "root") + }, + } + return cmd +} + +func NewKeycloakGetCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "get", + Short: "Get metadata for keycloak realm, client, or user", + } + cmd.AddCommand(NewKeycloakGetRealmCmd()) + cmd.AddCommand(NewKeycloakGetClientCmd()) + cmd.AddCommand(NewKeycloakGetClientSecretCmd()) + cmd.AddCommand(NewKeycloakGetUserCmd()) + return cmd +} + +func NewKeycloakGetRealmCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "realm ", + Short: "Get metadata for keycloak realm", + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + mwdd.DefaultForUser().EnsureReady() + KeycloakLogin() + mwdd.DefaultForUser().Exec("keycloak", []string{ + "/opt/keycloak/bin/kcadm.sh", + "get", + "realms/" + args[0], + }, "root") + }, + } + return cmd +} + +func NewKeycloakGetClientCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "client ", + Short: "Get metadata for keycloak client in a realm", + Args: cobra.MinimumNArgs(2), + Run: func(cmd *cobra.Command, args []string) { + mwdd.DefaultForUser().EnsureReady() + KeycloakLogin() + mwdd.DefaultForUser().Exec("keycloak", []string{ + "/opt/keycloak/bin/kcadm.sh", + "get", + "clients", + "--query", "clientId=" + args[0], + "--target-realm", args[1], + }, "root") + }, + } + return cmd +} + +func NewKeycloakGetClientSecretCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "clientsecret ", + Short: "Get client secret for keycloak client in a realm", + Args: cobra.MinimumNArgs(2), + Run: func(cmd *cobra.Command, args []string) { + mwdd.DefaultForUser().EnsureReady() + KeycloakLogin() + mwdd.DefaultForUser().Exec("keycloak", []string{ + "/mwdd/get_client_secret.sh", + args[0], + args[1], + }, "root") + }, + } + return cmd +} + +func NewKeycloakGetUserCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "user ", + Short: "Get metadata for keycloak user in a realm", + Args: cobra.MinimumNArgs(2), + Run: func(cmd *cobra.Command, args []string) { + mwdd.DefaultForUser().EnsureReady() + KeycloakLogin() + mwdd.DefaultForUser().Exec("keycloak", []string{ + "/opt/keycloak/bin/kcadm.sh", + "get", + "users", + "--query", "username=" + args[0], + "--target-realm", args[1], + }, "root") + }, + } + return cmd +} + +func KeycloakLogin() { + mwdd.DefaultForUser().ExecNoOutput("keycloak", []string{ + "/mwdd/login.sh", + }, "root") +} diff --git a/internal/cmd/docker/long/mwdd_keycloak.md b/internal/cmd/docker/long/mwdd_keycloak.md new file mode 100644 index 0000000000000000000000000000000000000000..a1e3aa46f5a4d0efd4d4b2c30dd4ff96a94a12a2 --- /dev/null +++ b/internal/cmd/docker/long/mwdd_keycloak.md @@ -0,0 +1,65 @@ +# Keycloak service + +[Keycloak](https://www.keycloak.org/) is an open source identity manager (IdM) that can be used to +provide single-sign on. It supports OpenID Connect and SAML. + +They keycloak service allows you to add, delete, list, and get metadata for keycloak +realms, clients, and users. + +## Setting up MediaWiki with OpenID Connect + +You will need to create a realm, a client, and at least one user as follows: + +```bash +mw docker keycloak create +mw docker keycloak add realm +mw docker keycloak add client +mw docker keycloak add user +``` + +where <realmname> is the name you choose for your realm, <clientname> is the name +you choose for your client, <username> is the name you choose for your user, and +<temporarypassword> is a temporary password that you will be asked to change at your +first login. + +Then, you will need to get the client secret that was assigned to your client: + +```bash +mw docker keycloak get clientsecret +``` + +Using the client secret returned as <clientsecret> below, add the following to your +LocalSettings.php: + +```php +wfLoadExtension('PluggableAuth'); +wfLoadExtension('OpenIDConnect'); +$wgPluggableAuth_Config = [ + "Keycloak" => [ + 'plugin' => 'OpenIDConnect', + 'data' => [ + 'providerURL' => 'http://keycloak.mwdd.localhost:8080/realms/', + 'clientID' => '', + 'clientsecret' => '' + ] + ] +]; +``` + +## More Control + +If you need finer-grained control of the keycloak service, you can +use the exec command: + +```bash +mw docker keycloak exec -- bash +``` + +to get a command line and then use the ```/opt/keycloak/bin/kcadm.sh``` commands shown in +[the Keycloak Admin CLI guide](https://www.keycloak.org/docs/latest/server_admin/#admin-cli). + +## See Also + +- [Keycloak](https://www.keycloak.org/docs/latest/server_admin/) +- [PluggableAuth](https://www.mediawiki.org/wiki/Extension:PluggableAuth) +- [OpenID Connect](https://www.mediawiki.org/wiki/Extension:OpenID_Connect) \ No newline at end of file diff --git a/internal/cmd/docker/root.go b/internal/cmd/docker/root.go index 8870bf8822a336992b39e7707a58bfd4863c5850..4449dd2cf8675778620e6da2f6170c54cc99694e 100644 --- a/internal/cmd/docker/root.go +++ b/internal/cmd/docker/root.go @@ -139,7 +139,7 @@ func NewCmd() *cobra.Command { cmd.AddCommand(mwdd.NewServiceCmd("phpmyadmin", "", []string{"ppma"})) cmd.AddCommand(mwdd.NewServiceCmd("postgres", "", []string{})) - cmd.AddCommand(mwdd.NewServiceCmd("keycloak", "", []string{})) + cmd.AddCommand(NewKeycloakCmd()) cmd.AddCommand(NewShellboxCmd()) diff --git a/internal/mwdd/files/embed/files.txt b/internal/mwdd/files/embed/files.txt index 2aacb448b2c2aa67c25222052fcfaf41e6ca70f8..17e87864af98ce7e0e282d561152b8bf760863ac 100644 --- a/internal/mwdd/files/embed/files.txt +++ b/internal/mwdd/files/embed/files.txt @@ -6,6 +6,12 @@ ./files.txt ./graphite.yml ./keycloak.yml +./keycloak/create_client.sh +./keycloak/create_user.sh +./keycloak/delete_client.sh +./keycloak/delete_user.sh +./keycloak/get_client_secret.sh +./keycloak/login.sh ./mailhog.yml ./mediawiki-fresh.yml ./mediawiki-quibble.yml diff --git a/internal/mwdd/files/embed/keycloak.yml b/internal/mwdd/files/embed/keycloak.yml index 7bf47124556dc79584676ebe73b08d2280969ff7..0d4c66ca0301af3b59d9c62bb15a6d7be2418e8e 100644 --- a/internal/mwdd/files/embed/keycloak.yml +++ b/internal/mwdd/files/embed/keycloak.yml @@ -1,10 +1,12 @@ version: '3.7' services: - keycloak.mwdd.localhost: + keycloak: image: "${KEYCLOAK_IMAGE:-quay.io/keycloak/keycloak:18.0.0}" restart: unless-stopped entrypoint: /opt/keycloak/bin/kc.sh start-dev + volumes: + - ./keycloak:/mwdd:ro environment: - KEYCLOAK_ADMIN=admin - KEYCLOAK_ADMIN_PASSWORD=admin diff --git a/internal/mwdd/files/embed/keycloak/create_client.sh b/internal/mwdd/files/embed/keycloak/create_client.sh new file mode 100644 index 0000000000000000000000000000000000000000..eb1cd4a079cee1d0d561d7141270f43a1dec6d5f --- /dev/null +++ b/internal/mwdd/files/embed/keycloak/create_client.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +CID=$(/opt/keycloak/bin/kcadm.sh create clients --target-realm $2 --set clientId=$1 --set 'redirectUris=["http://*"]' --id) +/opt/keycloak/bin/kcadm.sh create clients/${CID}/client-secret --target-realm $2 \ No newline at end of file diff --git a/internal/mwdd/files/embed/keycloak/create_user.sh b/internal/mwdd/files/embed/keycloak/create_user.sh new file mode 100644 index 0000000000000000000000000000000000000000..025c94068b8f726e8076d33d26176a6696c1d4e7 --- /dev/null +++ b/internal/mwdd/files/embed/keycloak/create_user.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +/opt/keycloak/bin/kcadm.sh create users --target-realm $3 --set username=$1 --set enabled=true +/opt/keycloak/bin/kcadm.sh set-password --target-realm $3 --username $1 --new-password $2 --temporary diff --git a/internal/mwdd/files/embed/keycloak/delete_client.sh b/internal/mwdd/files/embed/keycloak/delete_client.sh new file mode 100644 index 0000000000000000000000000000000000000000..bd6ed5c180c591e72b025fd73419f19ed3d0a8e8 --- /dev/null +++ b/internal/mwdd/files/embed/keycloak/delete_client.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +CID=$(/opt/keycloak/bin/kcadm.sh get clients --target-realm $2 --fields id -q clientId=$1 --format csv --noquotes) +/opt/keycloak/bin/kcadm.sh delete clients/${CID} --target-realm $2 \ No newline at end of file diff --git a/internal/mwdd/files/embed/keycloak/delete_user.sh b/internal/mwdd/files/embed/keycloak/delete_user.sh new file mode 100644 index 0000000000000000000000000000000000000000..eb2a252543ac2dd596831cc70706e9d33631c667 --- /dev/null +++ b/internal/mwdd/files/embed/keycloak/delete_user.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +USERID=$(/opt/keycloak/bin/kcadm.sh get users --target-realm $2 --fields id -q username=$1 --format csv --noquotes) +/opt/keycloak/bin/kcadm.sh delete users/${USERID} --target-realm $2 \ No newline at end of file diff --git a/internal/mwdd/files/embed/keycloak/get_client_secret.sh b/internal/mwdd/files/embed/keycloak/get_client_secret.sh new file mode 100644 index 0000000000000000000000000000000000000000..1fd1e301087da475438e6096ba76158f6e25b066 --- /dev/null +++ b/internal/mwdd/files/embed/keycloak/get_client_secret.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +CID=$(/opt/keycloak/bin/kcadm.sh get clients --target-realm $2 --fields id -q clientId=$1 --format csv --noquotes) +/opt/keycloak/bin/kcadm.sh get clients/${CID}/client-secret --target-realm $2 --fields value --format csv --noquotes \ No newline at end of file diff --git a/internal/mwdd/files/embed/keycloak/login.sh b/internal/mwdd/files/embed/keycloak/login.sh new file mode 100644 index 0000000000000000000000000000000000000000..6ccea035887ad68c255baaed505bb7113ee59404 --- /dev/null +++ b/internal/mwdd/files/embed/keycloak/login.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +/opt/keycloak/bin/kcadm.sh config credentials --server http://localhost:8080 --realm master --user ${KEYCLOAK_ADMIN} --password ${KEYCLOAK_ADMIN_PASSWORD}