Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ require (
github.com/golang/protobuf v1.4.0
github.com/gomodule/redigo v2.0.0+incompatible
github.com/google/go-cmp v0.4.0
github.com/google/go-github/v29 v29.0.3
github.com/google/uuid v1.1.1
github.com/hashicorp/golang-lru v0.5.1
github.com/prometheus/client_golang v1.6.0
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,10 @@ github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-github/v29 v29.0.3 h1:IktKCTwU//aFHnpA+2SLIi7Oo9uhAzgsdZNbcAqhgdc=
github.com/google/go-github/v29 v29.0.3/go.mod h1:CHKiKKPHJ0REzfwc14QMklvtHwCveD0PxlMjLlzAM5E=
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g=
Expand Down
1 change: 1 addition & 0 deletions pkg/app/api/authhandler/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "go_default_library",
srcs = [
"callback_handler.go",
"handler.go",
"login_handler.go",
],
Expand Down
104 changes: 104 additions & 0 deletions pkg/app/api/authhandler/callback_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright 2020 The PipeCD Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package authhandler

import (
"context"
"crypto/subtle"
"encoding/hex"
"fmt"
"net/http"
"time"

"go.uber.org/zap"
"golang.org/x/net/xsrftoken"

"github.com/pipe-cd/pipe/pkg/jwt"
"github.com/pipe-cd/pipe/pkg/model"
)

func (h *Handler) handleCallback(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")

err := checkState(r, h.stateKey)
if err != nil {
handleError(w, r, rootPath, "unauthorized access", h.logger, err)
return
}

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

proj, err := h.getProject(ctx, r)
if err != nil {
handleError(w, r, rootPath, "wrong project", h.logger, err)
return
}

user, err := proj.Sso.GenerateUserInfo(ctx, r.FormValue(authCodeFormKey))
if err != nil {
handleError(w, r, rootPath, "failed to generate the user information", h.logger, err)
return
}
claims := jwt.NewClaims(
user.Username,
user.AvatarURL,
defaultTokenTTL,
model.Role{
ProjectId: proj.Id,
ProjectRole: user.Role,
},
)
signedToken, err := h.signer.Sign(claims)
if err != nil {
handleError(w, r, rootPath, "internal error", h.logger, err)
return
}
http.SetCookie(w, makeTokenCookie(signedToken))
http.SetCookie(w, makeExpiredStateCookie())

h.logger.Info("user logged in",
zap.String("user", user.Username),
zap.String("project-id", proj.Id),
zap.String("project-role", user.Role.String()),
)

http.Redirect(w, r, rootPath, http.StatusFound)
}

func checkState(r *http.Request, key string) error {
state := r.FormValue(stateFormKey)
rawStateToken, err := hex.DecodeString(state)
if err != nil {
return err
}

stateToken := string(rawStateToken)
if !xsrftoken.Valid(stateToken, key, "", "") {
return fmt.Errorf("invalid state")
}

c, err := r.Cookie(stateCookieKey)
if err != nil {
return err
}

secretState := c.Value
if state == "" || subtle.ConstantTimeCompare([]byte(state), []byte(secretState)) != 1 {
return fmt.Errorf("wrong state")
}

return nil
}
9 changes: 6 additions & 3 deletions pkg/app/api/authhandler/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ const (
projectFormKey = "project"
usernameFormKey = "username"
passwordFormKey = "password"
authCodeFormKey = "code"
stateFormKey = "state"

stateCookieKey = "state"
errorCookieKey = "error"
Expand All @@ -59,7 +61,7 @@ type projectGetter interface {
type Handler struct {
signer jwt.Signer
apiURL string
stateSeed string
stateKey string
projectGetter projectGetter
logger *zap.Logger
}
Expand All @@ -68,14 +70,14 @@ type Handler struct {
func NewHandler(
signer jwt.Signer,
apiURL string,
stateSeed string,
stateKey string,
projectGetter projectGetter,
logger *zap.Logger,
) *Handler {
return &Handler{
signer: signer,
apiURL: apiURL,
stateSeed: stateSeed,
stateKey: stateKey,
projectGetter: projectGetter,
logger: logger,
}
Expand All @@ -85,6 +87,7 @@ func NewHandler(
func (h *Handler) Register(reg func(string, func(http.ResponseWriter, *http.Request))) {
reg(loginPath, h.handleLogin)
reg(staticLoginPath, h.handleStaticLogin)
reg(callbackPath, h.handleCallback)
reg(logoutPath, h.handleLogout)
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/app/api/authhandler/login_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func (h *Handler) handleLogin(w http.ResponseWriter, r *http.Request) {
return
}

stateToken := xsrftoken.Generate(h.stateSeed, "", "")
stateToken := xsrftoken.Generate(h.stateKey, "", "")
state := hex.EncodeToString([]byte(stateToken))
authURL, err := proj.Sso.GenerateAuthCodeURL(proj.Id, h.apiURL, callbackPath, state)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion pkg/app/api/cmd/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ func (s *server) run(ctx context.Context, t cli.Telemetry) error {
Handler: mux,
}
handlers := []httpHandler{
authhandler.NewHandler(signer, cfg.APIURL, cfg.StateSeed, datastore.NewProjectStore(ds), t.Logger),
authhandler.NewHandler(signer, cfg.APIURL, cfg.StateKey, datastore.NewProjectStore(ds), t.Logger),
}
for _, h := range handlers {
h.Register(mux.HandleFunc)
Expand Down
4 changes: 2 additions & 2 deletions pkg/config/control_plane.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ type ControlPlaneSpec struct {
Projects []ControlPlaneProject `json:"projects"`
// The address to the API of PipeCD control plane.
APIURL string `json:"apiUrl"`
// The seed to generate oauth state paramater.
StateSeed string `json:"stateSeed"`
// The key to generate oauth state paramater.
StateKey string `json:"stateKey"`
}

func (s *ControlPlaneSpec) Validate() error {
Expand Down
1 change: 1 addition & 0 deletions pkg/model/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ go_library(
importpath = "github.com/pipe-cd/pipe/pkg/model",
visibility = ["//visibility:public"],
deps = [
"@com_github_google_go_github_v29//github:go_default_library",
"@org_golang_google_protobuf//proto:go_default_library",
"@org_golang_x_crypto//bcrypt:go_default_library",
"@org_golang_x_oauth2//:go_default_library",
Expand Down
101 changes: 101 additions & 0 deletions pkg/model/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@
package model

import (
"context"
"crypto/subtle"
"fmt"
"net/url"
"strings"

"github.com/google/go-github/v29/github"
"golang.org/x/crypto/bcrypt"
"golang.org/x/oauth2"
)
Expand All @@ -28,6 +30,13 @@ var (
githubScopes = []string{"read:org"}
)

// UserInfo is the login user information.
type UserInfo struct {
AvatarURL string
Username string
Role Role_ProjectRole
}
Comment thread
gkuga marked this conversation as resolved.
Outdated

// Auth confirms username and password.
func (p *ProjectStaticUser) Auth(username, password string) error {
if username == "" {
Expand Down Expand Up @@ -77,3 +86,95 @@ func (p *ProjectSingleSignOn_GitHub) GenerateAuthCodeURL(project, apiURL, callba

return authURL, nil
}

// GenerateUserInfo generates a login user information.
func (p *ProjectSingleSignOn) GenerateUserInfo(ctx context.Context, code string) (*UserInfo, error) {
switch p.Provider {
case ProjectSingleSignOnProvider_GITHUB:
if p.Github == nil {
return nil, fmt.Errorf("missing GitHub oauth in the SSO configuration")
}
return p.Github.GenerateUserInfo(ctx, code)
default:
return nil, fmt.Errorf("not implemented")
}
}

// GenerateUserInfo generates a login user information.
func (p *ProjectSingleSignOn_GitHub) GenerateUserInfo(ctx context.Context, code string) (*UserInfo, error) {
Comment thread
gkuga marked this conversation as resolved.
Outdated
bu, err := url.Parse(p.BaseUrl)
Comment thread
gkuga marked this conversation as resolved.
Outdated
if err != nil {
return nil, err
}
uu, err := url.Parse(p.UploadUrl)
if err != nil {
return nil, err
}

cfg := oauth2.Config{
ClientID: p.ClientId,
ClientSecret: p.ClientSecret,
Endpoint: oauth2.Endpoint{TokenURL: fmt.Sprintf("%s://%s%s", bu.Scheme, bu.Host, "/login/oauth/access_token")},
}
token, err := cfg.Exchange(ctx, code)
if err != nil {
return nil, err
}

cli := github.NewClient(cfg.Client(ctx, token))
if !strings.HasSuffix(bu.Path, "/") {
Comment thread
gkuga marked this conversation as resolved.
Outdated
bu.Path += "/"
}
cli.BaseURL = bu
if !strings.HasSuffix(uu.Path, "/") {
bu.Path += "/"
}
cli.UploadURL = uu

user, _, err := cli.Users.Get(ctx, "")
if err != nil {
return nil, err
}
teams, _, err := cli.Teams.ListUserTeams(ctx, nil)
if err != nil {
return nil, err
}
role, err := p.decideRole(user.GetLogin(), teams)
if err != nil {
return nil, err
}

return &UserInfo{
Username: user.GetLogin(),
AvatarURL: user.GetAvatarURL(),
Role: *role,
}, nil
Comment thread
gkuga marked this conversation as resolved.
Outdated
}

func (p *ProjectSingleSignOn_GitHub) decideRole(user string, teams []*github.Team) (*Role_ProjectRole, error) {
Comment thread
gkuga marked this conversation as resolved.
Outdated
var viewer, editor bool
for _, team := range teams {
slug := team.GetSlug()
if p.Org != team.Organization.GetLogin() || slug == "" {
continue
}
switch slug {
case p.AdminTeam:
r := Role_ADMIN
return &r, nil
case p.EditorTeam:
editor = true
case p.ViewerTeam:
viewer = true
}
}
if editor {
r := Role_EDITOR
return &r, nil
}
if viewer {
r := Role_VIEWER
return &r, nil
}
return nil, fmt.Errorf("user (%s) not found in any of the %d project teams", user, len(teams))
}
10 changes: 6 additions & 4 deletions pkg/model/project.proto
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,16 @@ enum ProjectSingleSignOnProvider {

message ProjectSingleSignOn {
ProjectSingleSignOnProvider provider = 1 [(validate.rules).enum.defined_only = true];
string admin_team = 2 [(validate.rules).string.min_len = 1];
string editor_team = 3 [(validate.rules).string.min_len = 1];
string viewer_team = 4 [(validate.rules).string.min_len = 1];

message GitHub {
string client_id = 1 [(validate.rules).string.min_len = 1];
string client_secret = 2 [(validate.rules).string.min_len = 1];
string base_url = 3;
string base_url = 3 [(validate.rules).string.min_len = 1];
string upload_url = 4 [(validate.rules).string.min_len = 1];
string org = 5 [(validate.rules).string.min_len = 1];
string admin_team = 6 [(validate.rules).string.min_len = 1];
string editor_team = 7 [(validate.rules).string.min_len = 1];
string viewer_team = 8 [(validate.rules).string.min_len = 1];
}

message Google {
Expand Down