Skip to content

Commit

Permalink
WG529 Auth styling and flags improvements (#1624)
Browse files Browse the repository at this point in the history
* WG529 Styling User Settings

* WG529 Replace background svg

* WG529 sign_in route should redirect to apps page if auth flag is not switched on

* WG529 display user (uneditable)

* Add auth options

* WG529 display username input

* WG529 sign in error does not persist when user navigates away

* WG529 Styling Welcome screen

* Add username

* WG529 Update username field

* Read OIDC config from secret

* WG529 New flags for user and oidc flows

* Use lowercase keys

Co-authored-by: Yiannis <yiannis@weave.works>
  • Loading branch information
AlinaGoaga and yiannistri committed Mar 11, 2022
1 parent 931b52e commit f7098fb
Show file tree
Hide file tree
Showing 15 changed files with 1,552 additions and 187 deletions.
4 changes: 0 additions & 4 deletions cmd/gitops/cmderrors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,5 @@ import "errors"
var (
ErrNoWGEEndpoint = errors.New("the Weave GitOps Enterprise HTTP API endpoint flag (--endpoint) has not been set")
ErrNoURL = errors.New("the URL flag (--url) has not been set")
ErrNoIssuerURL = errors.New("the OIDC issuer URL flag (--oidc-issuer-url) has not been set")
ErrNoClientID = errors.New("the OIDC client ID flag (--oidc-client-id) has not been set")
ErrNoClientSecret = errors.New("the OIDC client secret flag (--oidc-client-secret) has not been set")
ErrNoRedirectURL = errors.New("the OIDC redirect URL flag (--oidc-redirect-url) has not been set")
ErrNoTLSCertOrKey = errors.New("both tls private key and cert must be specified")
)
96 changes: 48 additions & 48 deletions cmd/gitops/ui/run/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,16 @@ import (
"github.com/pkg/browser"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"go.uber.org/zap"

"github.com/weaveworks/weave-gitops/cmd/gitops/cmderrors"
"github.com/weaveworks/weave-gitops/pkg/helm/watcher"
"github.com/weaveworks/weave-gitops/pkg/helm/watcher/cache"
"github.com/weaveworks/weave-gitops/pkg/kube"
"github.com/weaveworks/weave-gitops/pkg/server"
"github.com/weaveworks/weave-gitops/pkg/server/auth"
"github.com/weaveworks/weave-gitops/pkg/server/tls"
"go.uber.org/zap"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
)

const (
Expand Down Expand Up @@ -69,10 +70,9 @@ var options Options
// NewCommand returns the `ui run` command
func NewCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "run [--log]",
Short: "Runs gitops ui",
PreRunE: preRunCmd,
RunE: runCmd,
Use: "run [--log]",
Short: "Runs gitops ui",
RunE: runCmd,
}

options = Options{}
Expand All @@ -93,44 +93,15 @@ func NewCommand() *cobra.Command {
cmd.Flags().StringVar(&options.TLSKey, "tls-private-key", "", "filename for the TLS key, in-memory generated if omitted")
cmd.Flags().BoolVar(&options.NoTLS, "no-tls", false, "do not attempt to read TLS certificates")

if server.AuthEnabled() {
cmd.Flags().StringVar(&options.OIDC.IssuerURL, "oidc-issuer-url", "", "The URL of the OpenID Connect issuer")
cmd.Flags().StringVar(&options.OIDC.ClientID, "oidc-client-id", "", "The client ID for the OpenID Connect client")
cmd.Flags().StringVar(&options.OIDC.ClientSecret, "oidc-client-secret", "", "The client secret to use with OpenID Connect issuer")
cmd.Flags().StringVar(&options.OIDC.RedirectURL, "oidc-redirect-url", "", "The OAuth2 redirect URL")
cmd.Flags().DurationVar(&options.OIDC.TokenDuration, "oidc-token-duration", time.Hour, "The duration of the ID token. It should be set in the format: number + time unit (s,m,h) e.g., 20m")
}
cmd.Flags().StringVar(&options.OIDC.IssuerURL, "oidc-issuer-url", "", "The URL of the OpenID Connect issuer")
cmd.Flags().StringVar(&options.OIDC.ClientID, "oidc-client-id", "", "The client ID for the OpenID Connect client")
cmd.Flags().StringVar(&options.OIDC.ClientSecret, "oidc-client-secret", "", "The client secret to use with OpenID Connect issuer")
cmd.Flags().StringVar(&options.OIDC.RedirectURL, "oidc-redirect-url", "", "The OAuth2 redirect URL")
cmd.Flags().DurationVar(&options.OIDC.TokenDuration, "oidc-token-duration", time.Hour, "The duration of the ID token. It should be set in the format: number + time unit (s,m,h) e.g., 20m")

return cmd
}

func preRunCmd(cmd *cobra.Command, args []string) error {
issuerURL := options.OIDC.IssuerURL
clientID := options.OIDC.ClientID
clientSecret := options.OIDC.ClientSecret
redirectURL := options.OIDC.RedirectURL

if issuerURL != "" || clientID != "" || clientSecret != "" || redirectURL != "" {
if issuerURL == "" {
return cmderrors.ErrNoIssuerURL
}

if clientID == "" {
return cmderrors.ErrNoClientID
}

if clientSecret == "" {
return cmderrors.ErrNoClientSecret
}

if redirectURL == "" {
return cmderrors.ErrNoRedirectURL
}
}

return nil
}

func runCmd(cmd *cobra.Command, args []string) error {
var log = logrus.New()

Expand Down Expand Up @@ -204,29 +175,58 @@ func runCmd(cmd *cobra.Command, args []string) error {
var authServer *auth.AuthServer

if server.AuthEnabled() {
_, err := url.Parse(options.OIDC.IssuerURL)
var OIDCConfig auth.OIDCConfig

// If OIDC auth secret is not found use CLI parameters
var secret corev1.Secret
if err := rawClient.Get(cmd.Context(), client.ObjectKey{
Namespace: auth.OIDCAuthSecretNamespace,
Name: auth.OIDCAuthSecretName,
}, &secret); err != nil {
appConfig.Logger.Error(err, "OIDC auth secret not found")

OIDCConfig.IssuerURL = options.OIDC.IssuerURL
OIDCConfig.ClientID = options.OIDC.ClientID
OIDCConfig.ClientSecret = options.OIDC.ClientSecret
OIDCConfig.RedirectURL = options.OIDC.RedirectURL
OIDCConfig.TokenDuration = options.OIDC.TokenDuration
} else {
OIDCConfig.IssuerURL = string(secret.Data["issuerURL"])
OIDCConfig.ClientID = string(secret.Data["clientID"])
OIDCConfig.ClientSecret = string(secret.Data["clientSecret"])
OIDCConfig.RedirectURL = string(secret.Data["redirectURL"])

tokenDuration, err := time.ParseDuration(string(secret.Data["tokenDuration"]))
if err != nil {
appConfig.Logger.Error(err, "Invalid token duration")
tokenDuration = time.Hour
}
OIDCConfig.TokenDuration = tokenDuration
}

_, err := url.Parse(OIDCConfig.IssuerURL)
if err != nil {
return fmt.Errorf("invalid issuer URL: %w", err)
}

_, err = url.Parse(options.OIDC.RedirectURL)
_, err = url.Parse(OIDCConfig.RedirectURL)
if err != nil {
return fmt.Errorf("invalid redirect URL: %w", err)
}

tsv, err := auth.NewHMACTokenSignerVerifier(options.OIDC.TokenDuration)
tsv, err := auth.NewHMACTokenSignerVerifier(OIDCConfig.TokenDuration)
if err != nil {
return fmt.Errorf("could not create HMAC token signer: %w", err)
}

srv, err := auth.NewAuthServer(cmd.Context(), appConfig.Logger, http.DefaultClient,
auth.AuthConfig{
OIDCConfig: auth.OIDCConfig{
IssuerURL: options.OIDC.IssuerURL,
ClientID: options.OIDC.ClientID,
ClientSecret: options.OIDC.ClientSecret,
RedirectURL: options.OIDC.RedirectURL,
TokenDuration: options.OIDC.TokenDuration,
IssuerURL: OIDCConfig.IssuerURL,
ClientID: OIDCConfig.ClientID,
ClientSecret: OIDCConfig.ClientSecret,
RedirectURL: OIDCConfig.RedirectURL,
TokenDuration: OIDCConfig.TokenDuration,
},
}, rawClient, tsv,
)
Expand Down
51 changes: 0 additions & 51 deletions cmd/gitops/ui/run/cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,65 +2,14 @@ package run_test

import (
"net/http"
"os"
"testing"

"github.com/go-resty/resty/v2"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/weaveworks/weave-gitops/cmd/gitops/cmderrors"
"github.com/weaveworks/weave-gitops/cmd/gitops/root"
"github.com/weaveworks/weave-gitops/cmd/gitops/ui/run"
)

func TestNoClientID(t *testing.T) {
os.Setenv("WEAVE_GITOPS_AUTH_ENABLED", "true")
defer os.Unsetenv("WEAVE_GITOPS_AUTH_ENABLED")

client := resty.New()
cmd := root.RootCmd(client)
cmd.SetArgs([]string{
"ui", "run",
"--oidc-issuer-url=http://weave.works",
})

err := cmd.Execute()
assert.ErrorIs(t, err, cmderrors.ErrNoClientID)
}

func TestNoClientSecret(t *testing.T) {
os.Setenv("WEAVE_GITOPS_AUTH_ENABLED", "true")
defer os.Unsetenv("WEAVE_GITOPS_AUTH_ENABLED")

client := resty.New()
cmd := root.RootCmd(client)
cmd.SetArgs([]string{
"ui", "run",
"--oidc-issuer-url=http://weave.works",
"--oidc-client-id=client-id",
})

err := cmd.Execute()
assert.ErrorIs(t, err, cmderrors.ErrNoClientSecret)
}

func TestNoRedirectURL(t *testing.T) {
os.Setenv("WEAVE_GITOPS_AUTH_ENABLED", "true")
defer os.Unsetenv("WEAVE_GITOPS_AUTH_ENABLED")

client := resty.New()
cmd := root.RootCmd(client)
cmd.SetArgs([]string{
"ui", "run",
"--oidc-issuer-url=http://weave.works",
"--oidc-client-id=client-id",
"--oidc-client-secret=client-secret",
})

err := cmd.Execute()
assert.ErrorIs(t, err, cmderrors.ErrNoRedirectURL)
}

func TestMissingTLSKeyOrCert(t *testing.T) {
log := logrus.New()
err := run.ListenAndServe(&http.Server{}, false, "foo", "", log)
Expand Down
2 changes: 1 addition & 1 deletion pkg/server/auth/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ func TestRateLimit(t *testing.T) {

hashedSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "admin-password-hash",
Name: "cluster-user-auth",
Namespace: "wego-system",
},
Data: map[string][]byte{
Expand Down
20 changes: 16 additions & 4 deletions pkg/server/auth/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@ import (
)

const (
LoginOIDC string = "oidc"
LoginUsername string = "username"
LoginOIDC string = "oidc"
LoginUsername string = "username"
ClusterUserAuthSecretNamespace string = "wego-system"
ClusterUserAuthSecretName string = "cluster-user-auth"
OIDCAuthSecretNamespace string = "wego-system"
OIDCAuthSecretName string = "oidc-auth"
)

// OIDCConfig is used to configure an AuthServer to interact with
Expand Down Expand Up @@ -49,6 +53,7 @@ type AuthServer struct {

// LoginRequest represents the data submitted by client when the auth flow (non-OIDC) is used.
type LoginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}

Expand Down Expand Up @@ -263,15 +268,22 @@ func (s *AuthServer) SignIn() http.HandlerFunc {
var hashedSecret corev1.Secret

if err := s.kubernetesClient.Get(r.Context(), ctrlclient.ObjectKey{
Namespace: "wego-system",
Name: "admin-password-hash",
Namespace: ClusterUserAuthSecretNamespace,
Name: ClusterUserAuthSecretName,
}, &hashedSecret); err != nil {
s.logger.Error(err, "Failed to query for the secret")
http.Error(rw, "Please ensure that a password has been set.", http.StatusBadRequest)

return
}

if loginRequest.Username != string(hashedSecret.Data["username"]) {
s.logger.Info("Wrong username")
rw.WriteHeader(http.StatusUnauthorized)

return
}

if err := bcrypt.CompareHashAndPassword(hashedSecret.Data["password"], []byte(loginRequest.Password)); err != nil {
s.logger.Error(err, "Failed to compare hash with password")
rw.WriteHeader(http.StatusUnauthorized)
Expand Down
56 changes: 54 additions & 2 deletions pkg/server/auth/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,58 @@ func TestSignInNoSecret(t *testing.T) {
}
}

func TestSignInWrongUsernameReturnsUnauthorized(t *testing.T) {
username := "admin"
password := "my-secret-password"

hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)

if err != nil {
t.Errorf("failed to generate a hash from password: %v", err)
}

hashedSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "cluster-user-auth",
Namespace: "wego-system",
},
Data: map[string][]byte{
"username": []byte(base64.StdEncoding.EncodeToString([]byte(username))),
"password": hashed,
},
}

fakeKubernetesClient := ctrlclientfake.NewClientBuilder().WithObjects(hashedSecret).Build()

tokenSignerVerifier, err := auth.NewHMACTokenSignerVerifier(5 * time.Minute)
if err != nil {
t.Errorf("failed to create HMAC signer: %v", err)
}

s, _ := makeAuthServer(t, fakeKubernetesClient, tokenSignerVerifier)

login := auth.LoginRequest{
Username: "wrong",
Password: "my-secret-password",
}

j, err := json.Marshal(login)
if err != nil {
t.Errorf("failed to marshal to JSON: %v", err)
}

reader := bytes.NewReader(j)

req := httptest.NewRequest(http.MethodPost, "https://example.com/signin", reader)
w := httptest.NewRecorder()
s.SignIn().ServeHTTP(w, req)

resp := w.Result()
if resp.StatusCode != http.StatusUnauthorized {
t.Errorf("expected status to be 401 but got %v instead", resp.StatusCode)
}
}

func TestSignInWrongPasswordReturnsUnauthorized(t *testing.T) {
password := "my-secret-password"

Expand All @@ -275,7 +327,7 @@ func TestSignInWrongPasswordReturnsUnauthorized(t *testing.T) {

hashedSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "admin-password-hash",
Name: "cluster-user-auth",
Namespace: "wego-system",
},
Data: map[string][]byte{
Expand Down Expand Up @@ -323,7 +375,7 @@ func TestSingInCorrectPassword(t *testing.T) {

hashedSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "admin-password-hash",
Name: "cluster-user-auth",
Namespace: "wego-system",
},
Data: map[string][]byte{
Expand Down
Loading

0 comments on commit f7098fb

Please sign in to comment.