Skip to content

Commit

Permalink
Merge pull request #360 from keel-hq/feature/basic_auth
Browse files Browse the repository at this point in the history
Feature/basic auth
  • Loading branch information
rusenask authored Apr 16, 2019
2 parents 5500238 + cf6f442 commit 8f297c7
Show file tree
Hide file tree
Showing 7 changed files with 305 additions and 49 deletions.
2 changes: 1 addition & 1 deletion bot/slack/slack.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func (b *Bot) Configure(approvalsRespCh chan *bot.ApprovalResponse, botMessagesC

b.approvalsChannel = "general"
if channel := os.Getenv(constants.EnvSlackApprovalsChannel); channel != "" {
b.approvalsChannel = channel
b.approvalsChannel = strings.TrimPrefix(channel, "#")
}

b.slackClient = client
Expand Down
2 changes: 2 additions & 0 deletions cmd/keel/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,8 @@ func setupTriggers(ctx context.Context, providers provider.Providers, approvalsM
Port: types.KeelDefaultPort,
Providers: providers,
ApprovalManager: approvalsManager,
Username: os.Getenv(constants.EnvBasicAuthUser),
Password: os.Getenv(constants.EnvBasicAuthPassword),
})

go func() {
Expand Down
155 changes: 154 additions & 1 deletion tests/acceptance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@ import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"testing"
"time"

"github.com/keel-hq/keel/constants"

"github.com/keel-hq/keel/types"

apps_v1 "k8s.io/api/apps/v1"
"k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"

log "github.com/sirupsen/logrus"
Expand Down Expand Up @@ -405,3 +408,153 @@ func TestApprovals(t *testing.T) {
}
})
}

func TestApprovalsWithAuthentication(t *testing.T) {

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

username := "foobar"
password := "barfood"

// go startKeel(ctx)
keel := &KeelCmd{
env: []string{
fmt.Sprintf("%s=%s", constants.EnvBasicAuthUser, username),
fmt.Sprintf("%s=%s", constants.EnvBasicAuthPassword, password),
},
}
go func() {
err := keel.Start(ctx)
if err != nil {
log.WithFields(log.Fields{
"error": err,
}).Error("failed to start Keel process")
}
}()

defer func() {
err := keel.Stop()
if err != nil {
log.WithFields(log.Fields{
"error": err,
}).Error("failed to stop Keel process")
}
}()

_, kcs := getKubernetesClient()

t.Run("CreateDeploymentWithApprovals", func(t *testing.T) {

testNamespace := createNamespaceForTest()
defer deleteTestNamespace(testNamespace)

dep := &apps_v1.Deployment{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{
Name: "dep-1-auth-test",
Namespace: testNamespace,
Labels: map[string]string{
types.KeelPolicyLabel: "all",
types.KeelMinimumApprovalsLabel: "1",
types.KeelApprovalDeadlineLabel: "5",
},
Annotations: map[string]string{},
},
apps_v1.DeploymentSpec{
Selector: &meta_v1.LabelSelector{
MatchLabels: map[string]string{
"app": "wd-1",
},
},
Template: v1.PodTemplateSpec{
ObjectMeta: meta_v1.ObjectMeta{
Labels: map[string]string{
"app": "wd-1",
"release": "1",
},
},
Spec: v1.PodSpec{
Containers: []v1.Container{
v1.Container{
Name: "wd-1",
Image: "karolisr/webhook-demo:0.0.14",
},
},
},
},
},
apps_v1.DeploymentStatus{},
}

_, err := kcs.AppsV1().Deployments(testNamespace).Create(dep)
if err != nil {
t.Fatalf("failed to create deployment: %s", err)
}
// giving some time to get started
// TODO: replace with a readiness check function to wait for 1/1 READY
time.Sleep(2 * time.Second)

// sending webhook
client := http.DefaultClient
buf := bytes.NewBufferString(dockerHub0150Webhook)
req, err := http.NewRequest("POST", "http://localhost:9300/v1/webhooks/dockerhub", buf)
if err != nil {
t.Fatalf("failed to create req: %s", err)
}
resp, err := client.Do(req)
if err != nil {
t.Errorf("failed to make a webhook request to keel: %s", err)
}

if resp.StatusCode != 200 {
t.Errorf("unexpected webhook response from keel: %d", resp.StatusCode)
}

time.Sleep(2 * time.Second)

reqNoAuth, err := http.NewRequest("GET", "http://localhost:9300/v1/approvals", nil)
if err != nil {
t.Fatalf("failed to create req: %s", err)
}
respNoAuth, err := client.Do(reqNoAuth)
if err != nil {
t.Logf("failed to make req: %s", err)
}
defer respNoAuth.Body.Close()
if respNoAuth.StatusCode != http.StatusUnauthorized {
t.Errorf("expected 401, got: %d", respNoAuth.StatusCode)
}

// doing it again with authentication
reqAuth, err := http.NewRequest("GET", "http://localhost:9300/v1/approvals", nil)
if err != nil {
t.Fatalf("failed to create req: %s", err)
}
reqAuth.SetBasicAuth(username, password)
resp, err = client.Do(reqAuth)
if err != nil {
t.Errorf("failed to make req: %s", err)
}

var approvals []*types.Approval
dec := json.NewDecoder(resp.Body)
defer resp.Body.Close()
err = dec.Decode(&approvals)
if err != nil {
t.Fatalf("failed to decode approvals resp: %s", err)
}

if len(approvals) == 0 {
t.Errorf("no approvals found")
}

ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()

err = waitFor(ctx, kcs, testNamespace, dep.ObjectMeta.Name, "karolisr/webhook-demo:0.0.14")
if err != nil {
t.Errorf("update failed: %s", err)
}
})
}
6 changes: 5 additions & 1 deletion tests/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
"strings"
"time"

"k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
Expand Down Expand Up @@ -75,6 +75,8 @@ func deleteTestNamespace(namespace string) error {

type KeelCmd struct {
cmd *exec.Cmd

env []string
}

func (kc *KeelCmd) Start(ctx context.Context) error {
Expand All @@ -87,6 +89,8 @@ func (kc *KeelCmd) Start(ctx context.Context) error {
c.Env = []string{
"DEBUG=true",
}
c.Env = append(c.Env, kc.env...)

c.Stdout = os.Stdout
c.Stderr = os.Stderr

Expand Down
36 changes: 0 additions & 36 deletions trigger/http/approvals_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ import (
"encoding/json"
"fmt"
"net/http"
"os"

"github.com/keel-hq/keel/constants"
"github.com/keel-hq/keel/cache"
"github.com/keel-hq/keel/types"
)
Expand All @@ -26,23 +24,6 @@ const (

func (s *TriggerServer) approvalsHandler(resp http.ResponseWriter, req *http.Request) {

// basic auth
if os.Getenv(constants.EnvBasicAuthUser) != "" && os.Getenv(constants.EnvBasicAuthPassword) != "" {

resp.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)

username, password, authOK := req.BasicAuth()
if authOK == false {
http.Error(resp, "Not authorized", 401)
return
}

if username != os.Getenv(constants.EnvBasicAuthUser) || password != os.Getenv(constants.EnvBasicAuthPassword) {
http.Error(resp, "Not authorized", 401)
return
}
}

// unknown lists all
approvals, err := s.approvalsManager.List()
if err != nil {
Expand All @@ -67,23 +48,6 @@ func (s *TriggerServer) approvalsHandler(resp http.ResponseWriter, req *http.Req

func (s *TriggerServer) approvalApproveHandler(resp http.ResponseWriter, req *http.Request) {

// basic auth
if os.Getenv(constants.EnvBasicAuthUser) != "" && os.Getenv(constants.EnvBasicAuthPassword) != "" {

resp.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)

username, password, authOK := req.BasicAuth()
if authOK == false {
http.Error(resp, "Not authorized", 401)
return
}

if username != os.Getenv(constants.EnvBasicAuthUser) || password != os.Getenv(constants.EnvBasicAuthPassword) {
http.Error(resp, "Not authorized", 401)
return
}
}

var ar approveRequest
dec := json.NewDecoder(req.Body)
defer req.Body.Close()
Expand Down
105 changes: 105 additions & 0 deletions trigger/http/approvals_endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -352,3 +352,108 @@ func TestReject(t *testing.T) {
}

}

func TestAuthListApprovalsA(t *testing.T) {

fp := &fakeProvider{}
mem := memory.NewMemoryCache()
am := approvals.New(mem)
providers := provider.New([]provider.Provider{fp}, am)
srv := NewTriggerServer(&Opts{
Providers: providers,
ApprovalManager: am,
Username: "user-1",
Password: "secret",
})
srv.registerRoutes(srv.router)

err := am.Create(&types.Approval{
Identifier: "123",
VotesRequired: 5,
NewVersion: "2.0.0",
CurrentVersion: "1.0.0",
})

if err != nil {
t.Fatalf("failed to create approval: %s", err)
}

// listing
req, err := http.NewRequest("GET", "/v1/approvals", nil)
if err != nil {
t.Fatalf("failed to create req: %s", err)
}

rec := httptest.NewRecorder()

srv.router.ServeHTTP(rec, req)
if rec.Code != 401 {
t.Errorf("expected 401 status code, got: %d", rec.Code)

t.Log(rec.Body.String())
}
}

func TestAuthListApprovalsB(t *testing.T) {

fp := &fakeProvider{}
mem := memory.NewMemoryCache()
am := approvals.New(mem)
providers := provider.New([]provider.Provider{fp}, am)
srv := NewTriggerServer(&Opts{
Providers: providers,
ApprovalManager: am,
Username: "user-1",
Password: "secret",
})
srv.registerRoutes(srv.router)

err := am.Create(&types.Approval{
Identifier: "123",
VotesRequired: 5,
NewVersion: "2.0.0",
CurrentVersion: "1.0.0",
})

if err != nil {
t.Fatalf("failed to create approval: %s", err)
}

// listing
req, err := http.NewRequest("GET", "/v1/approvals", nil)
if err != nil {
t.Fatalf("failed to create req: %s", err)
}

req.SetBasicAuth("user-1", "secret")

rec := httptest.NewRecorder()

srv.router.ServeHTTP(rec, req)
if rec.Code != 200 {
t.Errorf("expected 200 status code, got: %d", rec.Code)

t.Log(rec.Body.String())
}

var approvals []*types.Approval

err = json.Unmarshal(rec.Body.Bytes(), &approvals)
if err != nil {
t.Fatalf("failed to unmarshal response into approvals: %s", err)
}

if len(approvals) != 1 {
t.Fatalf("expected to find 1 approval but found: %d", len(approvals))
}

if approvals[0].VotesRequired != 5 {
t.Errorf("unexpected votes required")
}
if approvals[0].NewVersion != "2.0.0" {
t.Errorf("unexpected new version: %s", approvals[0].NewVersion)
}
if approvals[0].CurrentVersion != "1.0.0" {
t.Errorf("unexpected current version: %s", approvals[0].CurrentVersion)
}
}
Loading

0 comments on commit 8f297c7

Please sign in to comment.