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

Feature/basic auth #360

Merged
merged 7 commits into from
Apr 16, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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