[Harness] Adding JWT/PAT/SAT Support, Harness Clients, Inline User/ServiceAccount Creation, harness Build flag, ... (#22)

This change adds the initial stepping stones for harness integration:
- Authentication: JWT/PAT/SAT support
- Authorization: ACL integration (acl currently denies requests as gitness hasn't been integrated yet)
- Remote Clients for Token, User, ServiceAccount, ACL
- User Integration: Syncs harness users during authentication if unknown
- SA integration: syncs harness service accounts during authentication if unknown
- Initial harness API: THIS WILL BE CHANGED IN THE FUTURE!
- single harness subpackage (all marked with harness build flag)
- harness & standalone wire + make build commands
This commit is contained in:
Johannes Batzill 2022-09-30 16:22:12 -07:00 committed by GitHub
parent 5baf42d5ca
commit 4668e94027
67 changed files with 903 additions and 770 deletions

18
.harness.env Normal file
View File

@ -0,0 +1,18 @@
GITNESS_TRACE=true
HARNESS_JWT_IDENTITY="gitness"
HARNESS_JWT_SECRET="IC04LYMBf1lDP5oeY4hupxd4HJhLmN6azUku3xEbeE3SUx5G3ZYzhbiwVtK4i7AmqyU9OZkwB4v8E9qM"
HARNESS_JWT_VALIDINMIN=1440
HARNESS_JWT_BEARER_IDENTITY="Bearer"
HARNESS_JWT_BEARER_SECRET="dOkdsVqdRPPRJG31XU0qY4MPqmBBMk0PTAGIKM6O7TGqhjyxScIdJe80mwh5Yb5zF3KxYBHw6B3Lfzlq"
HARNESS_JWT_IDENTITY_SERVICE_IDENTITY="IdentityService"
HARNESS_JWT_IDENTITY_SERVICE_SECRET="HVSKUYqD4e5Rxu12hFDdCJKGM64sxgEynvdDhaOHaTHhwwn0K4Ttr0uoOxSsEVYNrUU"
HARNESS_JWT_MANAGER_IDENTITY="Manager"
HARNESS_JWT_MANAGER_SECRET="dOkdsVqdRPPRJG31XU0qY4MPqmBBMk0PTAGIKM6O7TGqhjyxScIdJe80mwh5Yb5zF3KxYBHw6B3Lfzlq"
HARNESS_JWT_NGMANAGER_IDENTITY="NextGenManager"
HARNESS_JWT_NGMANAGER_SECRET="IC04LYMBf1lDP5oeY4hupxd4HJhLmN6azUku3xEbeE3SUx5G3ZYzhbiwVtK4i7AmqyU9OZkwB4v8E9qM"
HARNESS_CLIENTS_ACL_SECURE=false
HARNESS_CLIENTS_ACL_BASEURL="http://localhost:9006/api"
HARNESS_CLIENTS_MANAGER_SECURE=false
HARNESS_CLIENTS_MANAGER_BASEURL="http://localhost:3457/api"
HARNESS_CLIENTS_NGMANAGER_SECURE=false
HARNESS_CLIENTS_NGMANAGER_BASEURL="http://localhost:7457"

View File

@ -0,0 +1 @@
GITNESS_TRACE=true

View File

@ -35,13 +35,19 @@ tools: $(tools) ## Install tools required for the build
mocks: $(mocks) mocks: $(mocks)
@echo "Generating Test Mocks" @echo "Generating Test Mocks"
generate: $(mocks) cli/server/wire_gen.go mocks/mock_client.go wire: cli/server/harness.wire_gen.go cli/server/standalone.wire_gen.go
generate: $(mocks) wire mocks/mock_client.go
@echo "Generating Code" @echo "Generating Code"
build: generate ## Build the gitness service binary build: generate ## Build the gitness service binary
@echo "Building Gitness Server" @echo "Building Gitness Server"
go build -ldflags="-X github.com/harness/gitness/version.GitCommit=${GIT_COMMIT} -X github.com/harness/gitness/version.Version.Major=${GITNESS_VERSION}" -o ./gitness . go build -ldflags="-X github.com/harness/gitness/version.GitCommit=${GIT_COMMIT} -X github.com/harness/gitness/version.Version.Major=${GITNESS_VERSION}" -o ./gitness .
harness-build: generate ## Build the gitness service binary for harness embedded mode
@echo "Building Gitness Server for Harness"
go build -tags=harness -ldflags="-X github.com/harness/gitness/version.GitCommit=${GIT_COMMIT} -X github.com/harness/gitness/version.Version.Major=${GITNESS_VERSION}" -o ./gitness .
test: generate ## Run the go tests test: generate ## Run the go tests
@echo "Running tests" @echo "Running tests"
go test -v -coverprofile=coverage.out ./internal/... go test -v -coverprofile=coverage.out ./internal/...
@ -114,9 +120,19 @@ lint: tools generate # lint the golang code
# Some code generation can be slow, so we only run it if # Some code generation can be slow, so we only run it if
# the source file has changed. # the source file has changed.
########################################### ###########################################
cli/server/wire_gen.go: cli/server/wire.go ## Update the wire dependency injection if wire.go has changed. cli/server/harness.wire_gen.go: cli/server/harness.wire.go ## Update the wire dependency injection if harness.wire.go has changed.
@echo "Updating wire_gen.go" @echo "Updating harness.wire_gen.go"
go generate ./cli/server/wire_gen.go @go run github.com/google/wire/cmd/wire gen -tags=harness -output_file_prefix="harness." github.com/harness/gitness/cli/server
@perl -ni -e 'print unless /go:generate/' cli/server/harness.wire_gen.go
@perl -i -pe's/\+build !wireinject/\+build !wireinject,harness/g' cli/server/harness.wire_gen.go
@perl -i -pe's/go:build !wireinject/go:build !wireinject && harness/g' cli/server/harness.wire_gen.go
cli/server/standalone.wire_gen.go: cli/server/standalone.wire.go ## Update the wire dependency injection if standalone.wire.go has changed.
@echo "Updating standalone.wire_gen.go"
@go run github.com/google/wire/cmd/wire gen -tags= -output_file_prefix="standalone." github.com/harness/gitness/cli/server
@perl -ni -e 'print unless /go:generate/' cli/server/standalone.wire_gen.go
@perl -i -pe's/\+build !wireinject/\+build !wireinject,!harness/g' cli/server/standalone.wire_gen.go
@perl -i -pe's/go:build !wireinject/go:build !wireinject && !harness/g' cli/server/standalone.wire_gen.go
mocks/mock_client.go: internal/store/store.go client/client.go mocks/mock_client.go: internal/store/store.go client/client.go
go generate mocks/mock.go go generate mocks/mock.go

View File

@ -19,11 +19,11 @@ type registerCommand struct {
} }
func (c *registerCommand) run(*kingpin.ParseContext) error { func (c *registerCommand) run(*kingpin.ParseContext) error {
username, password := util.Credentials() username, name, email, password := util.Registration()
httpClient := client.New(c.server) httpClient := client.New(c.server)
ctx, cancel := context.WithTimeout(context.Background(), time.Minute) ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel() defer cancel()
ts, err := httpClient.Register(ctx, username, password) ts, err := httpClient.Register(ctx, username, name, email, password)
if err != nil { if err != nil {
return err return err
} }

View File

@ -18,6 +18,8 @@ import (
) )
const userTmpl = ` const userTmpl = `
uid: {{ .UID }}
name: {{ .Name }}
email: {{ .Email }} email: {{ .Email }}
admin: {{ .Admin }} admin: {{ .Admin }}
` `

View File

@ -5,30 +5,14 @@
package server package server
import ( import (
"os"
"github.com/harness/gitness/types" "github.com/harness/gitness/types"
"github.com/kelseyhightower/envconfig" "github.com/kelseyhightower/envconfig"
) )
// legacy environment variables. the key is the legacy
// variable name, and the value is the new variable name.
var legacy = map[string]string{
// none defined
}
// load returns the system configuration from the // load returns the system configuration from the
// host environment. // host environment.
func load() (*types.Config, error) { func load() (*types.Config, error) {
// loop through legacy environment variable and, if set
// rewrite to the new variable name.
for k, v := range legacy {
if s, ok := os.LookupEnv(k); ok {
os.Setenv(v, s)
}
}
config := new(types.Config) config := new(types.Config)
// read the configuration from the environment and // read the configuration from the environment and
// populate the configuration structure. // populate the configuration structure.

View File

@ -0,0 +1,41 @@
// Copyright 2021 Harness Inc. All rights reserved.
// Use of this source code is governed by the Polyform Free Trial License
// that can be found in the LICENSE.md file for this repository.
//go:build wireinject && harness
// +build wireinject,harness
package server
import (
"github.com/harness/gitness/harness"
"github.com/harness/gitness/harness/auth/authn"
"github.com/harness/gitness/harness/auth/authz"
"github.com/harness/gitness/harness/client"
"github.com/harness/gitness/harness/router/translator"
"github.com/harness/gitness/internal/cron"
"github.com/harness/gitness/internal/router"
"github.com/harness/gitness/internal/server"
"github.com/harness/gitness/internal/store/database"
"github.com/harness/gitness/internal/store/memory"
"github.com/harness/gitness/types"
"github.com/google/wire"
)
func initSystem(config *types.Config) (*system, error) {
wire.Build(
newSystem,
database.WireSet,
memory.WireSet,
router.WireSet,
server.WireSet,
cron.WireSet,
harness.LoadConfig,
authn.WireSet,
authz.WireSet,
client.WireSet,
translator.WireSet,
)
return &system{}, nil
}

View File

@ -0,0 +1,73 @@
// Code generated by Wire. DO NOT EDIT.
//go:build !wireinject && harness
// +build !wireinject,harness
package server
import (
"github.com/harness/gitness/harness"
"github.com/harness/gitness/harness/auth/authn"
"github.com/harness/gitness/harness/auth/authz"
"github.com/harness/gitness/harness/client"
"github.com/harness/gitness/harness/router/translator"
"github.com/harness/gitness/internal/cron"
"github.com/harness/gitness/internal/router"
"github.com/harness/gitness/internal/server"
"github.com/harness/gitness/internal/store/database"
"github.com/harness/gitness/internal/store/memory"
"github.com/harness/gitness/types"
)
// Injectors from harness.wire.go:
func initSystem(config *types.Config) (*system, error) {
requestTranslator := translator.ProvideRequestTranslator()
systemStore := memory.New(config)
db, err := database.ProvideDatabase(config)
if err != nil {
return nil, err
}
userStore := database.ProvideUserStore(db)
spaceStore := database.ProvideSpaceStore(db)
repoStore := database.ProvideRepoStore(db)
tokenStore := database.ProvideTokenStore(db)
serviceAccountStore := database.ProvideServiceAccountStore(db)
harnessConfig, err := harness.LoadConfig()
if err != nil {
return nil, err
}
serviceJWTProvider, err := client.ProvideServiceJWTProvider(harnessConfig)
if err != nil {
return nil, err
}
tokenClient, err := client.ProvideTokenClient(serviceJWTProvider, harnessConfig)
if err != nil {
return nil, err
}
userClient, err := client.ProvideUserClient(serviceJWTProvider, harnessConfig)
if err != nil {
return nil, err
}
serviceAccountClient, err := client.ProvideServiceAccountClient(serviceJWTProvider, harnessConfig)
if err != nil {
return nil, err
}
authenticator, err := authn.ProvideAuthenticator(userStore, tokenClient, userClient, harnessConfig, serviceAccountClient, serviceAccountStore)
if err != nil {
return nil, err
}
aclClient, err := client.ProvideACLClient(serviceJWTProvider, harnessConfig)
if err != nil {
return nil, err
}
authorizer := authz.ProvideAuthorizer(aclClient)
handler, err := router.ProvideHTTPHandler(requestTranslator, systemStore, userStore, spaceStore, repoStore, tokenStore, serviceAccountStore, authenticator, authorizer)
if err != nil {
return nil, err
}
serverServer := server.ProvideServer(config, handler)
nightly := cron.NewNightly()
serverSystem := newSystem(serverServer, nightly)
return serverSystem, nil
}

View File

@ -2,8 +2,8 @@
// Use of this source code is governed by the Polyform Free Trial License // Use of this source code is governed by the Polyform Free Trial License
// that can be found in the LICENSE.md file for this repository. // that can be found in the LICENSE.md file for this repository.
//go:build wireinject //go:build wireinject && !harness
// +build wireinject // +build wireinject,!harness
package server package server
@ -14,6 +14,7 @@ import (
"github.com/harness/gitness/internal/auth/authz" "github.com/harness/gitness/internal/auth/authz"
"github.com/harness/gitness/internal/cron" "github.com/harness/gitness/internal/cron"
"github.com/harness/gitness/internal/router" "github.com/harness/gitness/internal/router"
"github.com/harness/gitness/internal/router/translator"
"github.com/harness/gitness/internal/server" "github.com/harness/gitness/internal/server"
"github.com/harness/gitness/internal/store/database" "github.com/harness/gitness/internal/store/database"
"github.com/harness/gitness/internal/store/memory" "github.com/harness/gitness/internal/store/memory"
@ -24,14 +25,15 @@ import (
func initSystem(ctx context.Context, config *types.Config) (*system, error) { func initSystem(ctx context.Context, config *types.Config) (*system, error) {
wire.Build( wire.Build(
newSystem,
database.WireSet, database.WireSet,
memory.WireSet, memory.WireSet,
router.WireSet, router.WireSet,
server.WireSet, server.WireSet,
cron.WireSet, cron.WireSet,
newSystem,
authn.WireSet, authn.WireSet,
authz.WireSet, authz.WireSet,
translator.WireSet,
) )
return &system{}, nil return &system{}, nil
} }

View File

@ -1,8 +1,7 @@
// Code generated by Wire. DO NOT EDIT. // Code generated by Wire. DO NOT EDIT.
//go:generate go run github.com/google/wire/cmd/wire //go:build !wireinject && !harness
//go:build !wireinject // +build !wireinject,!harness
// +build !wireinject
package server package server
@ -13,15 +12,17 @@ import (
"github.com/harness/gitness/internal/auth/authz" "github.com/harness/gitness/internal/auth/authz"
"github.com/harness/gitness/internal/cron" "github.com/harness/gitness/internal/cron"
"github.com/harness/gitness/internal/router" "github.com/harness/gitness/internal/router"
"github.com/harness/gitness/internal/router/translator"
"github.com/harness/gitness/internal/server" "github.com/harness/gitness/internal/server"
"github.com/harness/gitness/internal/store/database" "github.com/harness/gitness/internal/store/database"
"github.com/harness/gitness/internal/store/memory" "github.com/harness/gitness/internal/store/memory"
"github.com/harness/gitness/types" "github.com/harness/gitness/types"
) )
// Injectors from wire.go: // Injectors from standalone.wire.go:
func initSystem(ctx context.Context, config *types.Config) (*system, error) { func initSystem(ctx context.Context, config *types.Config) (*system, error) {
requestTranslator := translator.ProvideRequestTranslator()
systemStore := memory.New(config) systemStore := memory.New(config)
db, err := database.ProvideDatabase(ctx, config) db, err := database.ProvideDatabase(ctx, config)
if err != nil { if err != nil {
@ -32,9 +33,9 @@ func initSystem(ctx context.Context, config *types.Config) (*system, error) {
repoStore := database.ProvideRepoStore(db) repoStore := database.ProvideRepoStore(db)
tokenStore := database.ProvideTokenStore(db) tokenStore := database.ProvideTokenStore(db)
serviceAccountStore := database.ProvideServiceAccountStore(db) serviceAccountStore := database.ProvideServiceAccountStore(db)
authenticator := authn.NewTokenAuthenticator(userStore, serviceAccountStore, tokenStore) authenticator := authn.ProvideAuthenticator(userStore, serviceAccountStore, tokenStore)
authorizer := authz.NewUnsafeAuthorizer() authorizer := authz.ProvideAuthorizer()
handler, err := router.New(systemStore, userStore, spaceStore, repoStore, tokenStore, serviceAccountStore, authenticator, authorizer) handler, err := router.ProvideHTTPHandler(requestTranslator, systemStore, userStore, spaceStore, repoStore, tokenStore, serviceAccountStore, authenticator, authorizer)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -101,6 +101,11 @@ func Config() (string, error) {
) )
} }
// Registration returns the username, name, email and password from stdin.
func Registration() (string, string, string, string) {
return Username(), Name(), Email(), Password()
}
// Credentials returns the username and password from stdin. // Credentials returns the username and password from stdin.
func Credentials() (string, string) { func Credentials() (string, string) {
return Username(), Password() return Username(), Password()
@ -116,6 +121,26 @@ func Username() string {
return strings.TrimSpace(username) return strings.TrimSpace(username)
} }
// Name returns the name from stdin.
func Name() string {
reader := bufio.NewReader(os.Stdin)
fmt.Print("Enter Name: ")
name, _ := reader.ReadString('\n')
return strings.TrimSpace(name)
}
// Email returns the email from stdin.
func Email() string {
reader := bufio.NewReader(os.Stdin)
fmt.Print("Enter Email: ")
email, _ := reader.ReadString('\n')
return strings.TrimSpace(email)
}
// Password returns the password from stdin. // Password returns the password from stdin.
func Password() string { func Password() string {
fmt.Print("Enter Password: ") fmt.Print("Enter Password: ")

View File

@ -69,9 +69,12 @@ func (c *HTTPClient) Login(ctx context.Context, username, password string) (*typ
} }
// Register registers a new user and returns a JWT token. // Register registers a new user and returns a JWT token.
func (c *HTTPClient) Register(ctx context.Context, username, password string) (*types.TokenResponse, error) { func (c *HTTPClient) Register(ctx context.Context,
username, name, email, password string) (*types.TokenResponse, error) {
form := &url.Values{} form := &url.Values{}
form.Add("username", username) form.Add("username", username)
form.Add("name", name)
form.Add("email", email)
form.Add("password", password) form.Add("password", password)
out := new(types.TokenResponse) out := new(types.TokenResponse)
uri := fmt.Sprintf("%s/api/v1/register", c.base) uri := fmt.Sprintf("%s/api/v1/register", c.base)

View File

@ -16,7 +16,7 @@ type Client interface {
Login(ctx context.Context, username, password string) (*types.TokenResponse, error) Login(ctx context.Context, username, password string) (*types.TokenResponse, error)
// Register registers a new user and returns a JWT token. // Register registers a new user and returns a JWT token.
Register(ctx context.Context, username, password string) (*types.TokenResponse, error) Register(ctx context.Context, username, name, email, password string) (*types.TokenResponse, error)
// Self returns the currently authenticated user. // Self returns the currently authenticated user.
Self(ctx context.Context) (*types.User, error) Self(ctx context.Context) (*types.User, error)

View File

@ -5,6 +5,7 @@
package account package account
import ( import (
"errors"
"net/http" "net/http"
"github.com/harness/gitness/internal/api/render" "github.com/harness/gitness/internal/api/render"
@ -25,11 +26,14 @@ func HandleLogin(userStore store.UserStore, system store.SystemStore, tokenStore
username := r.FormValue("username") username := r.FormValue("username")
password := r.FormValue("password") password := r.FormValue("password")
user, err := userStore.FindEmail(ctx, username) user, err := userStore.FindUID(ctx, username)
if errors.Is(err, store.ErrResourceNotFound) {
user, err = userStore.FindEmail(ctx, username)
}
if err != nil { if err != nil {
log.Debug().Err(err). log.Debug().Err(err).
Str("user", username). Msgf("cannot find user with '%s'", username)
Msg("cannot find user")
// always give not found error as extra security measurement. // always give not found error as extra security measurement.
render.NotFound(w) render.NotFound(w)
@ -42,7 +46,7 @@ func HandleLogin(userStore store.UserStore, system store.SystemStore, tokenStore
) )
if err != nil { if err != nil {
log.Debug().Err(err). log.Debug().Err(err).
Str("user", username). Str("user_uid", user.UID).
Msg("invalid password") Msg("invalid password")
render.NotFound(w) render.NotFound(w)
@ -52,7 +56,7 @@ func HandleLogin(userStore store.UserStore, system store.SystemStore, tokenStore
token, jwtToken, err := token.CreateUserSession(ctx, tokenStore, user, "login") token, jwtToken, err := token.CreateUserSession(ctx, tokenStore, user, "login")
if err != nil { if err != nil {
log.Err(err). log.Err(err).
Str("user", username). Str("user_uid", user.UID).
Msg("failed to generate token") Msg("failed to generate token")
render.InternalError(w) render.InternalError(w)

View File

@ -26,23 +26,25 @@ func HandleRegister(userStore store.UserStore, system store.SystemStore, tokenSt
ctx := r.Context() ctx := r.Context()
log := hlog.FromRequest(r) log := hlog.FromRequest(r)
username := r.FormValue("username") uid := r.FormValue("username")
name := r.FormValue("name")
email := r.FormValue("email")
password := r.FormValue("password") password := r.FormValue("password")
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil { if err != nil {
log.Err(err). log.Err(err).
Str("email", username). Str("uid", uid).
Msg("Failed to hash password") Msg("Failed to hash password")
render.InternalError(w) render.InternalError(w)
return return
} }
// TODO: allow to provide email and name separately ...
user := &types.User{ user := &types.User{
Name: username, UID: uid,
Email: username, Name: name,
Email: email,
Password: string(hash), Password: string(hash),
Salt: uniuri.NewLen(uniuri.UUIDLen), Salt: uniuri.NewLen(uniuri.UUIDLen),
Created: time.Now().UnixMilli(), Created: time.Now().UnixMilli(),
@ -51,7 +53,7 @@ func HandleRegister(userStore store.UserStore, system store.SystemStore, tokenSt
if err = check.User(user); err != nil { if err = check.User(user); err != nil {
log.Debug().Err(err). log.Debug().Err(err).
Str("email", username). Str("uid", uid).
Msg("invalid user input") Msg("invalid user input")
render.UserfiedErrorOrInternal(w, err) render.UserfiedErrorOrInternal(w, err)
@ -60,7 +62,7 @@ func HandleRegister(userStore store.UserStore, system store.SystemStore, tokenSt
if err = userStore.Create(ctx, user); err != nil { if err = userStore.Create(ctx, user); err != nil {
log.Err(err). log.Err(err).
Str("email", username). Str("uid", uid).
Msg("Failed to create user") Msg("Failed to create user")
render.InternalError(w) render.InternalError(w)
@ -74,8 +76,7 @@ func HandleRegister(userStore store.UserStore, system store.SystemStore, tokenSt
user.Admin = true user.Admin = true
if err = userStore.Update(ctx, user); err != nil { if err = userStore.Update(ctx, user); err != nil {
log.Err(err). log.Err(err).
Str("email", username). Str("user_uid", user.UID).
Int64("user_id", user.ID).
Msg("Failed to enable admin user") Msg("Failed to enable admin user")
render.InternalError(w) render.InternalError(w)
@ -86,7 +87,7 @@ func HandleRegister(userStore store.UserStore, system store.SystemStore, tokenSt
token, jwtToken, err := token.CreateUserSession(ctx, tokenStore, user, "register") token, jwtToken, err := token.CreateUserSession(ctx, tokenStore, user, "register")
if err != nil { if err != nil {
log.Err(err). log.Err(err).
Str("user", username). Str("user", uid).
Msg("failed to generate token") Msg("failed to generate token")
render.InternalError(w) render.InternalError(w)

View File

@ -13,6 +13,7 @@ type CreatePathRequest struct {
// CreateServiceAccountRequest used for service account creation apis. // CreateServiceAccountRequest used for service account creation apis.
type CreateServiceAccountRequest struct { type CreateServiceAccountRequest struct {
UID string `json:"uid"`
Name string `json:"name"` Name string `json:"name"`
ParentType enum.ParentResourceType `json:"parentType"` ParentType enum.ParentResourceType `json:"parentType"`
ParentID int64 `json:"parentId"` ParentID int64 `json:"parentId"`

View File

@ -36,6 +36,7 @@ func HandleCreate(guard *guard.Guard, saStore store.ServiceAccountStore) http.Ha
} }
sa := &types.ServiceAccount{ sa := &types.ServiceAccount{
UID: in.UID,
Name: in.Name, Name: in.Name,
Salt: uniuri.NewLen(uniuri.UUIDLen), Salt: uniuri.NewLen(uniuri.UUIDLen),
Created: time.Now().UnixMilli(), Created: time.Now().UnixMilli(),

View File

@ -20,7 +20,9 @@ import (
) )
type userCreateInput struct { type userCreateInput struct {
Username string `json:"email"` UID string `json:"uid"`
Name string `json:"name"`
Email string `json:"email"`
Password string `json:"password"` Password string `json:"password"`
Admin bool `json:"admin"` Admin bool `json:"admin"`
} }
@ -42,7 +44,7 @@ func HandleCreate(userStore store.UserStore) http.HandlerFunc {
hash, err := bcrypt.GenerateFromPassword([]byte(in.Password), bcrypt.DefaultCost) hash, err := bcrypt.GenerateFromPassword([]byte(in.Password), bcrypt.DefaultCost)
if err != nil { if err != nil {
log.Err(err). log.Err(err).
Str("email", in.Username). Str("uid", in.UID).
Msg("Failed to hash password") Msg("Failed to hash password")
render.InternalError(w) render.InternalError(w)
@ -50,7 +52,9 @@ func HandleCreate(userStore store.UserStore) http.HandlerFunc {
} }
user := &types.User{ user := &types.User{
Email: in.Username, UID: in.UID,
Name: in.Name,
Email: in.Email,
Admin: in.Admin, Admin: in.Admin,
Password: string(hash), Password: string(hash),
Salt: uniuri.NewLen(uniuri.UUIDLen), Salt: uniuri.NewLen(uniuri.UUIDLen),
@ -59,7 +63,7 @@ func HandleCreate(userStore store.UserStore) http.HandlerFunc {
} }
if err = check.User(user); err != nil { if err = check.User(user); err != nil {
log.Debug().Err(err). log.Debug().Err(err).
Str("email", user.Email). Str("uid", user.UID).
Msg("invalid user input") Msg("invalid user input")
render.UserfiedErrorOrInternal(w, err) render.UserfiedErrorOrInternal(w, err)
@ -69,7 +73,7 @@ func HandleCreate(userStore store.UserStore) http.HandlerFunc {
err = userStore.Create(ctx, user) err = userStore.Create(ctx, user)
if err != nil { if err != nil {
log.Err(err). log.Err(err).
Str("email", user.Email). Str("uid", user.UID).
Msg("failed to create user") Msg("failed to create user")
render.UserfiedErrorOrInternal(w, err) render.UserfiedErrorOrInternal(w, err)

View File

@ -30,11 +30,10 @@ func HandleDelete(userStore store.UserStore, tokenStore store.TokenStore) http.H
return return
} }
err = userStore.Delete(ctx, user) err = userStore.Delete(ctx, user.ID)
if err != nil { if err != nil {
log.Error().Err(err). log.Error().Err(err).
Int64("user_id", user.ID). Str("user_uid", user.UID).
Str("user_email", user.Email).
Msg("failed to delete user") Msg("failed to delete user")
render.UserfiedErrorOrInternal(w, err) render.UserfiedErrorOrInternal(w, err)

View File

@ -43,8 +43,7 @@ func HandleUpdate(userStore store.UserStore) http.HandlerFunc {
hash, err := hashPassword([]byte(ptr.ToString(in.Password)), bcrypt.DefaultCost) hash, err := hashPassword([]byte(ptr.ToString(in.Password)), bcrypt.DefaultCost)
if err != nil { if err != nil {
log.Err(err). log.Err(err).
Int64("user_id", user.ID). Str("user_uid", user.UID).
Str("user_email", user.Email).
Msg("Failed to hash password") Msg("Failed to hash password")
render.InternalError(w) render.InternalError(w)
@ -67,8 +66,7 @@ func HandleUpdate(userStore store.UserStore) http.HandlerFunc {
hash, err := bcrypt.GenerateFromPassword([]byte(ptr.ToString(in.Password)), bcrypt.DefaultCost) hash, err := bcrypt.GenerateFromPassword([]byte(ptr.ToString(in.Password)), bcrypt.DefaultCost)
if err != nil { if err != nil {
log.Err(err). log.Err(err).
Int64("user_id", user.ID). Str("user_uid", user.UID).
Str("user_email", user.Email).
Msg("Failed to hash password") Msg("Failed to hash password")
render.InternalError(w) render.InternalError(w)
@ -78,8 +76,7 @@ func HandleUpdate(userStore store.UserStore) http.HandlerFunc {
} }
if err := check.User(user); err != nil { if err := check.User(user); err != nil {
log.Debug().Err(err). log.Debug().Err(err).
Int64("user_id", user.ID). Str("user_uid", user.UID).
Str("user_email", user.Email).
Msg("invalid user input") Msg("invalid user input")
render.UserfiedErrorOrInternal(w, err) render.UserfiedErrorOrInternal(w, err)
@ -91,8 +88,7 @@ func HandleUpdate(userStore store.UserStore) http.HandlerFunc {
err := userStore.Update(ctx, user) err := userStore.Update(ctx, user)
if err != nil { if err != nil {
log.Err(err). log.Err(err).
Int64("user_id", user.ID). Str("user_uid", user.UID).
Str("user_email", user.Email).
Msg("Failed to update the usser") Msg("Failed to update the usser")
render.UserfiedErrorOrInternal(w, err) render.UserfiedErrorOrInternal(w, err)

View File

@ -18,8 +18,6 @@ func HlogHandler() func(http.Handler) http.Handler {
return hlog.AccessHandler( return hlog.AccessHandler(
func(r *http.Request, status, size int, duration time.Duration) { func(r *http.Request, status, size int, duration time.Duration) {
hlog.FromRequest(r).Info(). hlog.FromRequest(r).Info().
Str("method", r.Method).
Stringer("url", r.URL).
Int("status_code", status). Int("status_code", status).
Int("response_size_bytes", size). Int("response_size_bytes", size).
Dur("elapsed_ms", duration). Dur("elapsed_ms", duration).

View File

@ -51,7 +51,7 @@ func Attempt(authenticator authn.Authenticator) func(http.Handler) http.Handler
// Update the logging context and inject principal in context // Update the logging context and inject principal in context
log.UpdateContext(func(c zerolog.Context) zerolog.Context { log.UpdateContext(func(c zerolog.Context) zerolog.Context {
return c. return c.
Int64("principal_id", session.Principal.ID). Str("principal_uid", session.Principal.UID).
Str("principal_type", string(session.Principal.Type)). Str("principal_type", string(session.Principal.Type)).
Bool("principal_admin", session.Principal.Admin) Bool("principal_admin", session.Principal.Admin)
}) })

View File

@ -2,39 +2,42 @@ package encode
import ( import (
"net/http" "net/http"
"net/url"
"strings" "strings"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/hlog"
"github.com/harness/gitness/internal/request"
"github.com/harness/gitness/types" "github.com/harness/gitness/types"
) )
// GitPathBefore wraps an http.HandlerFunc in a layer that encodes Paths coming as part of the GIT api const (
// (e.g. "space1/repo.git") before executing the provided http.HandlerFunc EncodedPathSeparator = "%252F"
)
// GitPath encodes Paths coming as part of the GIT api (e.g. "space1/repo.git")
// The first prefix that matches the URL.Path will be used during encoding. // The first prefix that matches the URL.Path will be used during encoding.
func GitPathBefore(h http.HandlerFunc) http.HandlerFunc { func GitPath(r *http.Request) error {
return func(w http.ResponseWriter, r *http.Request) { _, err := pathTerminatedWithMarker(r, "", ".git", false)
r, _ = pathTerminatedWithMarker(r, "", ".git", false) return err
h.ServeHTTP(w, r)
}
} }
// TerminatedPathBefore wraps an http.HandlerFunc in a layer that encodes a terminated path (e.g. "/space1/space2/+") // TerminatedPath wraps an http.HandlerFunc in a layer that encodes a terminated path (e.g. "/space1/space2/+")
// before executing the provided http.HandlerFunc. The first prefix that matches the URL.Path will // before executing the provided http.HandlerFunc. The first prefix that matches the URL.Path will
// be used during encoding. // be used during encoding (prefix is ignored during encoding).
func TerminatedPathBefore(prefixes []string, h http.HandlerFunc) http.HandlerFunc { func TerminatedPath(prefixes []string, r *http.Request) error {
return func(w http.ResponseWriter, r *http.Request) {
for _, p := range prefixes { for _, p := range prefixes {
// IMPORTANT: define changed separately to avoid overshadowing r changed, err := pathTerminatedWithMarker(r, p, "/+", false)
var changed bool if err != nil {
if r, changed = pathTerminatedWithMarker(r, p, "/+", false); changed { return err
}
// first prefix that leads to success we can stop
if changed {
break break
} }
} }
h.ServeHTTP(w, r) return nil
}
} }
// pathTerminatedWithMarker function encodes a path followed by a custom marker and returns a request with an // pathTerminatedWithMarker function encodes a path followed by a custom marker and returns a request with an
@ -46,49 +49,41 @@ func TerminatedPathBefore(prefixes []string, h http.HandlerFunc) http.HandlerFun
// Prefix: "" Path: "/space1/space2/+" => "/space1%2Fspace2" // Prefix: "" Path: "/space1/space2/+" => "/space1%2Fspace2"
// Prefix: "" Path: "/space1/space2.git" => "/space1%2Fspace2" // Prefix: "" Path: "/space1/space2.git" => "/space1%2Fspace2"
// Prefix: "/spaces" Path: "/spaces/space1/space2/+/authToken" => "/spaces/space1%2Fspace2/authToken". // Prefix: "/spaces" Path: "/spaces/space1/space2/+/authToken" => "/spaces/space1%2Fspace2/authToken".
func pathTerminatedWithMarker(r *http.Request, prefix string, marker string, keepMarker bool) (*http.Request, bool) { func pathTerminatedWithMarker(r *http.Request, prefix string, marker string, keepMarker bool) (bool, error) {
// In case path doesn't start with prefix - nothing to encode // In case path doesn't start with prefix - nothing to encode
if len(r.URL.Path) < len(prefix) || r.URL.Path[0:len(prefix)] != prefix { if len(r.URL.Path) < len(prefix) || r.URL.Path[0:len(prefix)] != prefix {
return r, false return false, nil
} }
originalSubPath := r.URL.Path[len(prefix):] originalSubPath := r.URL.Path[len(prefix):]
path, suffix, found := strings.Cut(originalSubPath, marker) path, _, found := strings.Cut(originalSubPath, marker)
// If we don't find a marker - nothing to encode // If we don't find a marker - nothing to encode
if !found { if !found {
return r, false return false, nil
} }
// if marker was found - convert to escaped version (skip first character in case path starts with '/') // if marker was found - convert to escaped version (skip first character in case path starts with '/').
escapedPath := path[0:1] + strings.ReplaceAll(path[1:], types.PathSeparator, "%2F") // Since replacePrefix unescapes the strings, we have to double escape.
escapedPath := path[0:1] + strings.ReplaceAll(path[1:], types.PathSeparator, EncodedPathSeparator)
if keepMarker { if keepMarker {
escapedPath += marker escapedPath += marker
} }
updatedSubPath := escapedPath + suffix
// TODO: Proper Logging prefixWithPath := prefix + path + marker
log.Debug().Msgf( prefixWithEscapedPath := prefix + escapedPath
"[Encode] prefix: '%s', marker: '%s', original: '%s', updated: '%s'.\n",
hlog.FromRequest(r).Trace().Msgf(
"[Encode] prefix: '%s', marker: '%s', original: '%s', escaped: '%s'.\n",
prefix, prefix,
marker, marker,
originalSubPath, prefixWithPath,
updatedSubPath) prefixWithEscapedPath)
/* err := request.ReplacePrefix(r, prefixWithPath, prefixWithEscapedPath)
* Return shallow clone with updated URL, similar to http.StripPrefix or earlier version of request.WithContext if err != nil {
* https://cs.opensource.google/go/go/+/refs/tags/go1.19:src/net/http/server.go;l=2138 return false, err
* https://cs.opensource.google/go/go/+/refs/tags/go1.18:src/net/http/request.go;l=355 }
*
* http.StripPrefix initially changed the path only, but that was updated because of official recommendations:
* https://github.com/golang/go/issues/18952
*/
r2 := new(http.Request)
*r2 = *r
r2.URL = new(url.URL)
*r2.URL = *r.URL
r2.URL.Path = prefix + updatedSubPath
r2.URL.RawPath = ""
return r2, true return true, nil
} }

View File

@ -25,17 +25,17 @@ func ServiceAccount(saStore store.ServiceAccountStore) func(http.Handler) http.H
ctx := r.Context() ctx := r.Context()
log := hlog.FromRequest(r) log := hlog.FromRequest(r)
id, err := request.GetServiceAccountID(r) uid, err := request.GetServiceAccountUID(r)
if err != nil { if err != nil {
log.Info().Err(err).Msgf("Receieved no or invalid service account id") log.Info().Err(err).Msgf("Receieved no or invalid service account uid")
render.BadRequest(w) render.BadRequest(w)
return return
} }
sa, err := saStore.Find(ctx, id) sa, err := saStore.FindUID(ctx, uid)
if err != nil { if err != nil {
log.Warn().Err(err).Msgf("Failed to get service account with id '%d'.", id) log.Warn().Err(err).Msgf("Failed to get service account with uid '%s'.", uid)
render.UserfiedErrorOrInternal(w, err) render.UserfiedErrorOrInternal(w, err)
return return
@ -43,7 +43,7 @@ func ServiceAccount(saStore store.ServiceAccountStore) func(http.Handler) http.H
// Update the logging context and inject repo in context // Update the logging context and inject repo in context
log.UpdateContext(func(c zerolog.Context) zerolog.Context { log.UpdateContext(func(c zerolog.Context) zerolog.Context {
return c.Int64("sa_id", sa.ID).Str("sa_name", sa.Name) return c.Str("sa_uid", sa.UID)
}) })
next.ServeHTTP(w, r.WithContext( next.ServeHTTP(w, r.WithContext(

View File

@ -47,7 +47,6 @@ func Space(spaceStore store.SpaceStore) func(http.Handler) http.Handler {
if err != nil { if err != nil {
log.Debug().Err(err).Msgf("Failed to get space using ref '%s'.", ref) log.Debug().Err(err).Msgf("Failed to get space using ref '%s'.", ref)
render.UserfiedErrorOrInternal(w, err) render.UserfiedErrorOrInternal(w, err)
return return
} }

View File

@ -25,7 +25,7 @@ func User(userStore store.UserStore) func(http.Handler) http.Handler {
ctx := r.Context() ctx := r.Context()
log := hlog.FromRequest(r) log := hlog.FromRequest(r)
id, err := request.GetUserID(r) uid, err := request.GetUserUID(r)
if err != nil { if err != nil {
log.Info().Err(err).Msgf("Receieved no or invalid user id") log.Info().Err(err).Msgf("Receieved no or invalid user id")
@ -33,9 +33,9 @@ func User(userStore store.UserStore) func(http.Handler) http.Handler {
return return
} }
user, err := userStore.Find(ctx, id) user, err := userStore.FindUID(ctx, uid)
if err != nil { if err != nil {
log.Info().Err(err).Msgf("Failed to get user with id '%d'.", id) log.Info().Err(err).Msgf("Failed to get user with uid '%s'.", uid)
render.UserfiedErrorOrInternal(w, err) render.UserfiedErrorOrInternal(w, err)
return return
@ -43,7 +43,7 @@ func User(userStore store.UserStore) func(http.Handler) http.Handler {
// Update the logging context and inject repo in context // Update the logging context and inject repo in context
log.UpdateContext(func(c zerolog.Context) zerolog.Context { log.UpdateContext(func(c zerolog.Context) zerolog.Context {
return c.Int64("user_id", user.ID).Str("user_name", user.Name) return c.Str("user_uid", user.UID)
}) })
next.ServeHTTP(w, r.WithContext( next.ServeHTTP(w, r.WithContext(

View File

@ -5,14 +5,14 @@ import (
) )
const ( const (
UserIDParamName = "userId" UserUIDParamName = "userUID"
ServiceAccountIDParamName = "saId" ServiceAccountUIDParamName = "saUID"
) )
func GetUserID(r *http.Request) (int64, error) { func GetUserUID(r *http.Request) (string, error) {
return ParseAsInt64(r, UserIDParamName) return ParamOrError(r, UserUIDParamName)
} }
func GetServiceAccountID(r *http.Request) (int64, error) { func GetServiceAccountUID(r *http.Request) (string, error) {
return ParseAsInt64(r, ServiceAccountIDParamName) return ParamOrError(r, ServiceAccountUIDParamName)
} }

View File

@ -14,11 +14,22 @@ import (
"github.com/harness/gitness/types/enum" "github.com/harness/gitness/types/enum"
) )
// ParamOrError tries to retrieve the parameter from the request and
// returns the parameter if it exists and is not empty, otherwise returns an error.
func ParamOrError(r *http.Request, paramName string) (string, error) {
value := chi.URLParam(r, paramName)
if value == "" {
return "", fmt.Errorf("parameter '%s' not found in request", paramName)
}
return value, nil
}
// ParseAsInt64 tries to retrieve the parameter from the request and parse it to in64. // ParseAsInt64 tries to retrieve the parameter from the request and parse it to in64.
func ParseAsInt64(r *http.Request, paramName string) (int64, error) { func ParseAsInt64(r *http.Request, paramName string) (int64, error) {
rawID := chi.URLParam(r, paramName) rawID, err := ParamOrError(r, paramName)
if rawID == "" { if err != nil {
return 0, fmt.Errorf("parameter '%s' not found in request", paramName) return 0, err
} }
id, err := strconv.ParseInt(rawID, 10, 64) id, err := strconv.ParseInt(rawID, 10, 64)

View File

@ -1,28 +0,0 @@
// Copyright 2022 Harness Inc. All rights reserved.
// Use of this source code is governed by the Polyform Free Trial License
// that can be found in the LICENSE.md file for this repository.
package harness
import (
"net/http"
"github.com/harness/gitness/internal/auth"
"github.com/harness/gitness/internal/auth/authn"
"github.com/harness/gitness/types"
)
var _ authn.Authenticator = (*Authenticator)(nil)
// Authenticator that validates access token provided by harness SAAS.
type Authenticator struct {
// some config to validate jwt
}
func NewAuthenticator() (authn.Authenticator, error) {
return &Authenticator{}, nil
}
func (a *Authenticator) Authenticate(r *http.Request) (*auth.Session, error) {
return &auth.Session{Principal: types.Principal{}, Metadata: &auth.EmptyMetadata{}}, nil
}

View File

@ -35,7 +35,7 @@ type TokenAuthenticator struct {
func NewTokenAuthenticator( func NewTokenAuthenticator(
userStore store.UserStore, userStore store.UserStore,
saStore store.ServiceAccountStore, saStore store.ServiceAccountStore,
tokenStore store.TokenStore) Authenticator { tokenStore store.TokenStore) *TokenAuthenticator {
return &TokenAuthenticator{ return &TokenAuthenticator{
userStore: userStore, userStore: userStore,
saStore: saStore, saStore: saStore,

View File

@ -6,9 +6,15 @@ package authn
import ( import (
"github.com/google/wire" "github.com/google/wire"
"github.com/harness/gitness/internal/store"
) )
// WireSet provides a wire set for this package. // WireSet provides a wire set for this package.
var WireSet = wire.NewSet( var WireSet = wire.NewSet(
NewTokenAuthenticator, ProvideAuthenticator,
) )
func ProvideAuthenticator(userStore store.UserStore, saStore store.ServiceAccountStore,
tokenStore store.TokenStore) Authenticator {
return NewTokenAuthenticator(userStore, saStore, tokenStore)
}

View File

@ -1,243 +0,0 @@
// Copyright 2022 Harness Inc. All rights reserved.
// Use of this source code is governed by the Polyform Free Trial License
// that can be found in the LICENSE.md file for this repository.
package harness
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"github.com/harness/gitness/internal/auth"
"github.com/harness/gitness/internal/auth/authz"
"github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum"
)
var _ authz.Authorizer = (*Authorizer)(nil)
type Authorizer struct {
client *http.Client
aclEndpoint string
authToken string
}
func NewAuthorizer(aclEndpoint, authToken string) (authz.Authorizer, error) {
// build http client - could be injected, too
tr := &http.Transport{
// TODO: expose InsecureSkipVerify in config
TLSClientConfig: &tls.Config{
//nolint:gosec // accept any host cert
InsecureSkipVerify: true,
},
}
client := &http.Client{Transport: tr}
return &Authorizer{
client: client,
aclEndpoint: aclEndpoint,
authToken: authToken,
}, nil
}
func (a *Authorizer) Check(ctx context.Context, session *auth.Session,
scope *types.Scope, resource *types.Resource, permission enum.Permission) (bool, error) {
return a.CheckAll(ctx, session, types.PermissionCheck{
Scope: *scope,
Resource: *resource,
Permission: permission,
})
}
func (a *Authorizer) CheckAll(ctx context.Context, session *auth.Session,
permissionChecks ...types.PermissionCheck) (bool, error) {
if len(permissionChecks) == 0 {
return false, authz.ErrNoPermissionCheckProvided
}
// TODO: Ensure that we also handle HarnessMetadata!
requestDto, err := createACLRequest(&session.Principal, permissionChecks)
if err != nil {
return false, err
}
byt, err := json.Marshal(requestDto)
if err != nil {
return false, err
}
// TODO: accountId might be different!
url := a.aclEndpoint + "?routingId=" + requestDto.Permissions[0].ResourceScope.AccountIdentifier
httpRequest, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(byt))
if err != nil {
return false, err
}
httpRequest.Header = http.Header{
"Content-Type": []string{"application/json"},
"Authorization": []string{"Bearer " + a.authToken},
}
response, err := a.client.Do(httpRequest)
if response != nil {
defer response.Body.Close()
}
if err != nil {
return false, err
}
if response.StatusCode != http.StatusOK {
return false, fmt.Errorf("got unexpected status code '%d' - assume unauthorized", response.StatusCode)
}
bodyByte, err := io.ReadAll(response.Body)
if err != nil {
return false, err
}
var responseDto aclResponse
err = json.Unmarshal(bodyByte, &responseDto)
if err != nil {
return false, err
}
return checkACLResponse(permissionChecks, responseDto)
}
func createACLRequest(principal *types.Principal,
permissionChecks []types.PermissionCheck) (*aclRequest, error) {
// Generate ACL req
req := aclRequest{
Permissions: []aclPermission{},
Principal: aclPrincipal{
PrincipalIdentifier: principal.ExternalID,
},
}
// map principaltype
actualPrincipalType, err := mapPrincipalType(principal.Type)
if err != nil {
return nil, err
}
req.Principal.PrincipalType = actualPrincipalType
// map all permissionchecks to ACL permission checks
for _, c := range permissionChecks {
mappedPermission := mapPermission(c.Permission)
var mappedResourceScope *aclResourceScope
mappedResourceScope, err = mapScope(c.Scope)
if err != nil {
return nil, err
}
req.Permissions = append(req.Permissions, aclPermission{
Permission: mappedPermission,
ResourceScope: *mappedResourceScope,
ResourceType: string(c.Resource.Type),
ResourceIdentifier: c.Resource.Name,
})
}
return &req, nil
}
func checkACLResponse(permissionChecks []types.PermissionCheck, responseDto aclResponse) (bool, error) {
/*
* We are assuming two things:
* - All permission checks were made for the same principal.
* - Permissions inherit down the hierarchy (Account -> Organization -> Project -> Repository)
* - No two checks are for the same permission - is similar to ff implementation:
* https://github.com/wings-software/ff-server/blob/master/pkg/rbac/client.go#L88
*
* Based on that, if there's any permitted result for a permission check the permission is allowed.
* Now we just have to ensure that all permissions are allowed
*
* TODO: Use resource name + scope for verifying results.
*/
for _, check := range permissionChecks {
permissionPermitted := false
for _, ace := range responseDto.Data.AccessControlList {
if string(check.Permission) == ace.Permission && ace.Permitted {
permissionPermitted = true
break
}
}
if !permissionPermitted {
return false, fmt.Errorf("permission '%s' is not permitted according to ACL (correlationId: '%s')",
check.Permission,
responseDto.CorrelationID)
}
}
return true, nil
}
func mapScope(scope types.Scope) (*aclResourceScope, error) {
/*
* ASSUMPTION:
* Harness embeded structure is mapped to the following scm space:
* {Account}/{Organization}/{Project}
*
* We can use that assumption to translate back from scope.spacePath to harness scope.
* However, this only works as long as resources exist within spaces only.
* For controlling access to any child resources of a repository, harness doesn't have a matching
* structure out of the box (e.g. branches, ...)
*
* IMPORTANT:
* For now harness embedded doesn't support scope.Repository (has to be configured on space level ...)
*
* TODO: Handle scope.Repository in harness embedded mode
*/
const (
accIndex = 0
orgIndex = 1
projectIndex = 2
scopes = 3
)
harnessIdentifiers := strings.Split(scope.SpacePath, "/")
if len(harnessIdentifiers) > scopes {
return nil, fmt.Errorf("unable to convert '%s' to harness resource scope "+
"(expected {Account}/{Organization}/{Project} or a sub scope)", scope.SpacePath)
}
aclScope := &aclResourceScope{}
if len(harnessIdentifiers) > accIndex {
aclScope.AccountIdentifier = harnessIdentifiers[accIndex]
}
if len(harnessIdentifiers) > orgIndex {
aclScope.OrgIdentifier = harnessIdentifiers[orgIndex]
}
if len(harnessIdentifiers) > projectIndex {
aclScope.ProjectIdentifier = harnessIdentifiers[projectIndex]
}
return aclScope, nil
}
func mapPermission(permission enum.Permission) string {
// harness has multiple modules - add scm prefix
return "scm_" + string(permission)
}
func mapPrincipalType(principalType enum.PrincipalType) (string, error) {
switch principalType {
case enum.PrincipalTypeUser:
return "USER", nil
case enum.PrincipalTypeServiceAccount:
return "SERVICE_ACCOUNT", nil
case enum.PrincipalTypeService:
return "SERVICE", nil
default:
return "", fmt.Errorf("unknown principaltype '%s'", principalType)
}
}

View File

@ -1,52 +0,0 @@
// Copyright 2022 Harness Inc. All rights reserved.
// Use of this source code is governed by the Polyform Free Trial License
// that can be found in the LICENSE.md file for this repository.
package harness
/*
* Classes required for harness ACL.
* For now keep it here, as it shouldn't even be part of the code base in the first place
* (should be in its own harness wide client library).
*/
type aclRequest struct {
Principal aclPrincipal `json:"principal"`
Permissions []aclPermission `json:"permissions"`
}
type aclResponse struct {
Status string `json:"status"`
CorrelationID string `json:"correlationId"`
Data aclResponseData `json:"data"`
}
type aclResponseData struct {
Principal aclPrincipal `json:"principal"`
AccessControlList []aclControlElement `json:"accessControlList"`
}
type aclControlElement struct {
Permission string `json:"permission"`
ResourceScope aclResourceScope `json:"resourceScope,omitempty"`
ResourceType string `json:"resourceType"`
ResourceIdentifier string `json:"resourceIdentifier"`
Permitted bool `json:"permitted"`
}
type aclResourceScope struct {
AccountIdentifier string `json:"accountIdentifier"`
OrgIdentifier string `json:"orgIdentifier,omitempty"`
ProjectIdentifier string `json:"projectIdentifier,omitempty"`
}
type aclPermission struct {
ResourceScope aclResourceScope `json:"resourceScope,omitempty"`
ResourceType string `json:"resourceType"`
ResourceIdentifier string `json:"resourceIdentifier"`
Permission string `json:"permission"`
}
type aclPrincipal struct {
PrincipalIdentifier string `json:"principalIdentifier"`
PrincipalType string `json:"principalType"`
}

View File

@ -21,7 +21,7 @@ var _ Authorizer = (*UnsafeAuthorizer)(nil)
*/ */
type UnsafeAuthorizer struct{} type UnsafeAuthorizer struct{}
func NewUnsafeAuthorizer() Authorizer { func NewUnsafeAuthorizer() *UnsafeAuthorizer {
return &UnsafeAuthorizer{} return &UnsafeAuthorizer{}
} }

View File

@ -10,5 +10,9 @@ import (
// WireSet provides a wire set for this package. // WireSet provides a wire set for this package.
var WireSet = wire.NewSet( var WireSet = wire.NewSet(
NewUnsafeAuthorizer, ProvideAuthorizer,
) )
func ProvideAuthorizer() Authorizer {
return NewUnsafeAuthorizer()
}

View File

@ -1,18 +0,0 @@
// Copyright 2022 Harness Inc. All rights reserved.
// Use of this source code is governed by the Polyform Free Trial License
// that can be found in the LICENSE.md file for this repository.
package harness
import "github.com/harness/gitness/types/enum"
// Metadata is used for all harness embedded auths (apart from ssh).
type Metadata struct {
ExecutingPrincipalType enum.PrincipalType
ExecutingPrincipalID int64
}
// RequiresEnforcement returns true if the metadata contains authz related info.
func (m *Metadata) RequiresEnforcement() bool {
return true
}

View File

@ -0,0 +1,63 @@
// Copyright 2021 Harness Inc. All rights reserved.
// Use of this source code is governed by the Polyform Free Trial License
// that can be found in the LICENSE.md file for this repository.
// Package router provides http handlers for serving the
// web applicationa and API endpoints.
package request
import (
"fmt"
"net/http"
"net/url"
)
// ReplacePrefix replaces the path of the request.
// IMPORTANT:
// - both prefix are unescaped for path, and used as is for RawPath!
// - only called by top level handler!!
func ReplacePrefix(r *http.Request, oldPrefix string, newPrefix string) error {
/*
* According to official documentation, we can change anything in the request but the body:
* https://pkg.go.dev/net/http#Handler
*
* ASSUMPTION:
* This is called by a top level handler (no router or middleware above it)
* Therefore, we don't have to worry about getting any routing metadata out of sync.
*
* This is different to returning a shallow clone with updated URL, which is what
* http.StripPrefix or earlier versions of request.WithContext are doing:
* https://cs.opensource.google/go/go/+/refs/tags/go1.19:src/net/http/server.go;l=2138
* https://cs.opensource.google/go/go/+/refs/tags/go1.18:src/net/http/request.go;l=355
*
* http.StripPrefix initially changed the path only, but that was updated because of official recommendations:
* https://github.com/golang/go/issues/18952
*/
unOldPrefix, err := url.PathUnescape(oldPrefix)
if err != nil {
return fmt.Errorf("failed to unescape old prefix '%s'", oldPrefix)
}
unNewPrefix, err := url.PathUnescape(newPrefix)
if err != nil {
return fmt.Errorf("failed to unescape new prefix '%s'", newPrefix)
}
unl := len(unOldPrefix)
if len(r.URL.Path) < unl || r.URL.Path[0:unl] != unOldPrefix {
return fmt.Errorf("path '%s' doesn't contain prefix '%s'", r.URL.Path, unOldPrefix)
}
// only change RawPath if it exists
if r.URL.RawPath != "" {
l := len(oldPrefix)
if len(r.URL.RawPath) < l || r.URL.RawPath[0:l] != oldPrefix {
return fmt.Errorf("raw path '%s' doesn't contain prefix '%s'", r.URL.RawPath, oldPrefix)
}
r.URL.RawPath = newPrefix + r.URL.RawPath[l:]
}
r.URL.Path = unNewPrefix + r.URL.Path[unl:]
return nil
}

View File

@ -14,7 +14,6 @@ import (
handleruser "github.com/harness/gitness/internal/api/handler/user" handleruser "github.com/harness/gitness/internal/api/handler/user"
"github.com/harness/gitness/internal/api/middleware/accesslog" "github.com/harness/gitness/internal/api/middleware/accesslog"
middlewareauthn "github.com/harness/gitness/internal/api/middleware/authn" middlewareauthn "github.com/harness/gitness/internal/api/middleware/authn"
"github.com/harness/gitness/internal/api/middleware/encode"
"github.com/harness/gitness/internal/api/middleware/resolve" "github.com/harness/gitness/internal/api/middleware/resolve"
"github.com/harness/gitness/internal/api/request" "github.com/harness/gitness/internal/api/request"
@ -27,15 +26,12 @@ import (
"github.com/go-chi/chi/middleware" "github.com/go-chi/chi/middleware"
"github.com/go-chi/cors" "github.com/go-chi/cors"
"github.com/rs/zerolog/hlog" "github.com/rs/zerolog/hlog"
"github.com/rs/zerolog/log"
) )
/* /*
* Mounts the Rest API Router under mountPath (path has to end with ). * newAPIHandler returns a new http handler for handling API calls.
* The handler is wrapped within a layer that handles encoding terminated Paths.
*/ */
func newAPIHandler( func newAPIHandler(
mountPath string,
systemStore store.SystemStore, systemStore store.SystemStore,
userStore store.UserStore, userStore store.UserStore,
spaceStore store.SpaceStore, spaceStore store.SpaceStore,
@ -50,14 +46,13 @@ func newAPIHandler(
// Use go-chi router for inner routing (restricted to mountPath!) // Use go-chi router for inner routing (restricted to mountPath!)
r := chi.NewRouter() r := chi.NewRouter()
r.Route(mountPath, func(r chi.Router) {
// Apply common api middleware // Apply common api middleware
r.Use(middleware.NoCache) r.Use(middleware.NoCache)
r.Use(middleware.Recoverer) r.Use(middleware.Recoverer)
// configure logging middleware. // configure logging middleware.
r.Use(hlog.NewHandler(log.Logger)) r.Use(hlog.URLHandler("url"))
r.Use(hlog.URLHandler("path"))
r.Use(hlog.MethodHandler("method")) r.Use(hlog.MethodHandler("method"))
r.Use(hlog.RequestIDHandler("request", "Request-Id")) r.Use(hlog.RequestIDHandler("request", "Request-Id"))
r.Use(accesslog.HlogHandler()) r.Use(accesslog.HlogHandler())
@ -71,15 +66,8 @@ func newAPIHandler(
r.Route("/v1", func(r chi.Router) { r.Route("/v1", func(r chi.Router) {
setupRoutesV1(r, systemStore, userStore, spaceStore, repoStore, tokenStore, saStore, authenticator, g) setupRoutesV1(r, systemStore, userStore, spaceStore, repoStore, tokenStore, saStore, authenticator, g)
}) })
})
// Generate list of all path prefixes that expect terminated Paths return r
terminatedPathPrefixes := []string{
mountPath + "/v1/spaces",
mountPath + "/v1/repos",
}
return encode.TerminatedPathBefore(terminatedPathPrefixes, r.ServeHTTP)
} }
func corsHandler(config *types.Config) func(http.Handler) http.Handler { func corsHandler(config *types.Config) func(http.Handler) http.Handler {
@ -214,7 +202,7 @@ func setupServiceAccounts(r chi.Router, saStore store.ServiceAccountStore, token
// create takes parent information via body // create takes parent information via body
r.Post("/", handlerserviceaccount.HandleCreate(guard, saStore)) r.Post("/", handlerserviceaccount.HandleCreate(guard, saStore))
r.Route(fmt.Sprintf("/{%s}", request.ServiceAccountIDParamName), func(r chi.Router) { r.Route(fmt.Sprintf("/{%s}", request.ServiceAccountUIDParamName), func(r chi.Router) {
// resolves the service account and stores it in the context // resolves the service account and stores it in the context
r.Use(resolve.ServiceAccount(saStore)) r.Use(resolve.ServiceAccount(saStore))
@ -251,7 +239,7 @@ func setupAdmin(r chi.Router, userStore store.UserStore, guard *guard.Guard) {
_, _ = w.Write([]byte(fmt.Sprintf("Create user '%s'", chi.URLParam(r, "rref")))) _, _ = w.Write([]byte(fmt.Sprintf("Create user '%s'", chi.URLParam(r, "rref"))))
}) })
r.Route(fmt.Sprintf("/{%s}", request.UserIDParamName), func(r chi.Router) { r.Route(fmt.Sprintf("/{%s}", request.UserUIDParamName), func(r chi.Router) {
// resolves the user and stores it in the context // resolves the user and stores it in the context
resolve.User(userStore) resolve.User(userStore)

View File

@ -7,7 +7,6 @@ import (
"github.com/harness/gitness/internal/api/guard" "github.com/harness/gitness/internal/api/guard"
"github.com/harness/gitness/internal/api/middleware/accesslog" "github.com/harness/gitness/internal/api/middleware/accesslog"
middleware_authn "github.com/harness/gitness/internal/api/middleware/authn" middleware_authn "github.com/harness/gitness/internal/api/middleware/authn"
"github.com/harness/gitness/internal/api/middleware/encode"
"github.com/harness/gitness/internal/api/middleware/resolve" "github.com/harness/gitness/internal/api/middleware/resolve"
"github.com/harness/gitness/internal/api/request" "github.com/harness/gitness/internal/api/request"
"github.com/harness/gitness/internal/auth/authn" "github.com/harness/gitness/internal/auth/authn"
@ -18,15 +17,12 @@ import (
"github.com/go-chi/chi" "github.com/go-chi/chi"
"github.com/go-chi/chi/middleware" "github.com/go-chi/chi/middleware"
"github.com/rs/zerolog/hlog" "github.com/rs/zerolog/hlog"
"github.com/rs/zerolog/log"
) )
/* /*
* Mounts the GIT Router under mountPath. * newGitHandler returns a new http handler for handling GIT calls.
* The handler is wrapped within a layer that handles encoding Paths.
*/ */
func newGitHandler( func newGitHandler(
mountPath string,
_ store.SystemStore, _ store.SystemStore,
_ store.UserStore, _ store.UserStore,
spaceStore store.SpaceStore, spaceStore store.SpaceStore,
@ -34,16 +30,15 @@ func newGitHandler(
authenticator authn.Authenticator, authenticator authn.Authenticator,
authorizer authz.Authorizer) http.Handler { authorizer authz.Authorizer) http.Handler {
guard := guard.New(authorizer, spaceStore, repoStore) guard := guard.New(authorizer, spaceStore, repoStore)
// Use go-chi router for inner routing (restricted to mountPath!) // Use go-chi router for inner routing (restricted to mountPath!)
r := chi.NewRouter() r := chi.NewRouter()
r.Route(mountPath, func(r chi.Router) {
// Apply common api middleware // Apply common api middleware
r.Use(middleware.NoCache) r.Use(middleware.NoCache)
r.Use(middleware.Recoverer) r.Use(middleware.Recoverer)
// configure logging middleware. // configure logging middleware.
r.Use(hlog.NewHandler(log.Logger)) r.Use(hlog.URLHandler("url"))
r.Use(hlog.URLHandler("path"))
r.Use(hlog.MethodHandler("method")) r.Use(hlog.MethodHandler("method"))
r.Use(hlog.RequestIDHandler("request", "Request-Id")) r.Use(hlog.RequestIDHandler("request", "Request-Id"))
r.Use(accesslog.HlogHandler()) r.Use(accesslog.HlogHandler())
@ -80,9 +75,8 @@ func newGitHandler(
r.Get("/objects/pack/pack-{file:[0-9a-f]{40}}.idx", stubGitHandler) r.Get("/objects/pack/pack-{file:[0-9a-f]{40}}.idx", stubGitHandler)
}) })
}) })
})
return encode.GitPathBefore(r.ServeHTTP) return r
} }
func stubGitHandler(w http.ResponseWriter, r *http.Request) { func stubGitHandler(w http.ResponseWriter, r *http.Request) {

View File

@ -10,25 +10,33 @@ import (
"net/http" "net/http"
"strings" "strings"
"github.com/harness/gitness/internal/api/render"
"github.com/harness/gitness/internal/auth/authn" "github.com/harness/gitness/internal/auth/authn"
"github.com/harness/gitness/internal/auth/authz" "github.com/harness/gitness/internal/auth/authz"
"github.com/harness/gitness/internal/request"
"github.com/harness/gitness/internal/router/translator"
"github.com/harness/gitness/internal/store" "github.com/harness/gitness/internal/store"
"github.com/rs/zerolog"
"github.com/rs/zerolog/hlog"
"github.com/rs/zerolog/log"
) )
const ( const (
restMount = "/api" APIMount = "/api"
gitUserAgentPrefix = "git/" gitUserAgentPrefix = "git/"
) )
type Router struct { type Router struct {
translator translator.RequestTranslator
api http.Handler api http.Handler
git http.Handler git http.Handler
web http.Handler web http.Handler
} }
// New returns a new http.Handler that routes traffic // NewRouter returns a new http.Handler that routes traffic
// to the appropriate http.Handlers. // to the appropriate http.Handlers.
func New( func NewRouter(
translator translator.RequestTranslator,
systemStore store.SystemStore, systemStore store.SystemStore,
userStore store.UserStore, userStore store.UserStore,
spaceStore store.SpaceStore, spaceStore store.SpaceStore,
@ -37,13 +45,14 @@ func New(
saStore store.ServiceAccountStore, saStore store.ServiceAccountStore,
authenticator authn.Authenticator, authenticator authn.Authenticator,
authorizer authz.Authorizer, authorizer authz.Authorizer,
) (http.Handler, error) { ) (*Router, error) {
api := newAPIHandler(restMount, systemStore, userStore, spaceStore, repoStore, tokenStore, saStore, api := newAPIHandler(systemStore, userStore, spaceStore, repoStore, tokenStore, saStore,
authenticator, authorizer) authenticator, authorizer)
git := newGitHandler("/", systemStore, userStore, spaceStore, repoStore, authenticator, authorizer) git := newGitHandler(systemStore, userStore, spaceStore, repoStore, authenticator, authorizer)
web := newWebHandler("/", systemStore) web := newWebHandler(systemStore)
return &Router{ return &Router{
translator: translator,
api: api, api: api,
git: git, git: git,
web: web, web: web,
@ -51,6 +60,23 @@ func New(
} }
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
var err error
// setup logger for request
log := log.Logger.With().Logger()
req = req.WithContext(log.WithContext(req.Context()))
log.UpdateContext(func(c zerolog.Context) zerolog.Context {
return c.
Str("original_url", req.URL.String())
})
// Initial translation of the request before any routing.
req, err = r.translator.TranslatePreRouting(req)
if err != nil {
log.Err(err).Msgf("Failed pre-routing translation of request.")
render.InternalError(w)
return
}
/* /*
* 1. GIT * 1. GIT
* *
@ -60,6 +86,18 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
*/ */
ua := req.Header.Get("user-agent") ua := req.Header.Get("user-agent")
if strings.HasPrefix(ua, gitUserAgentPrefix) { if strings.HasPrefix(ua, gitUserAgentPrefix) {
log.UpdateContext(func(c zerolog.Context) zerolog.Context {
return c.Str("handler", "git")
})
// Translate git request
req, err = r.translator.TranslateGit(req)
if err != nil {
hlog.FromRequest(req).Err(err).Msgf("Failed GIT translation of request.")
render.InternalError(w)
return
}
r.git.ServeHTTP(w, req) r.git.ServeHTTP(w, req)
return return
} }
@ -68,10 +106,28 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
* 2. REST API * 2. REST API
* *
* All Rest API calls start with "/api/", and thus can be uniquely identified. * All Rest API calls start with "/api/", and thus can be uniquely identified.
* Note: This assumes that we are blocking "api" as a space name!
*/ */
p := req.URL.Path p := req.URL.Path
if strings.HasPrefix(p, restMount) { if strings.HasPrefix(p, APIMount) {
log.UpdateContext(func(c zerolog.Context) zerolog.Context {
return c.Str("handler", "api")
})
// remove matched prefix to simplify API handlers
if err = stripPrefix(APIMount, req); err != nil {
hlog.FromRequest(req).Err(err).Msgf("Failed striping of prefix for api request.")
render.InternalError(w)
return
}
// Translate API request
req, err = r.translator.TranslateAPI(req)
if err != nil {
hlog.FromRequest(req).Err(err).Msgf("Failed API translation of request.")
render.InternalError(w)
return
}
r.api.ServeHTTP(w, req) r.api.ServeHTTP(w, req)
return return
} }
@ -81,5 +137,21 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
* *
* Everything else will be routed to web (or return 404) * Everything else will be routed to web (or return 404)
*/ */
log.UpdateContext(func(c zerolog.Context) zerolog.Context {
return c.Str("handler", "web")
})
req, err = r.translator.TranslateWeb(req)
if err != nil {
hlog.FromRequest(req).Err(err).Msgf("Failed Web translation of request.")
render.InternalError(w)
return
}
r.web.ServeHTTP(w, req) r.web.ServeHTTP(w, req)
} }
// stripPrefix removes the prefix from the request path (expected to be there).
func stripPrefix(prefix string, r *http.Request) error {
return request.ReplacePrefix(r, r.URL.Path[:len(prefix)], "")
}

View File

@ -0,0 +1,25 @@
// Copyright 2021 Harness Inc. All rights reserved.
// Use of this source code is governed by the Polyform Free Trial License
// that can be found in the LICENSE.md file for this repository.
package translator
import (
"net/http"
)
// RequestTranslator is responsible to translate an incomming request
// before it's getting routed and handled.
type RequestTranslator interface {
// TranslatePreRouting is called before any routing decisions are made.
TranslatePreRouting(*http.Request) (*http.Request, error)
// TranslateGit is called for a git related request.
TranslateGit(*http.Request) (*http.Request, error)
// TranslateAPI is called for an API related request.
TranslateAPI(*http.Request) (*http.Request, error)
// TranslateWeb is called for an web related request.
TranslateWeb(*http.Request) (*http.Request, error)
}

View File

@ -0,0 +1,49 @@
// Copyright 2021 Harness Inc. All rights reserved.
// Use of this source code is governed by the Polyform Free Trial License
// that can be found in the LICENSE.md file for this repository.
package translator
import (
"net/http"
"github.com/harness/gitness/internal/api/middleware/encode"
)
var (
terminatedPathPrefixesAPI = []string{"/v1/spaces/", "/v1/repos/"}
)
var _ RequestTranslator = (*TerminatedPathTranslator)(nil)
// TerminatedPathTranslator translates encoded paths.
// For example:
// - /space1/space2/+ -> /space1%2Fspace2
// - /space1/rep1.git -> /space1%2Frepo1
//
// Note: paths are terminated after initial routing.
type TerminatedPathTranslator struct{}
func NewTerminatedPathTranslator() *TerminatedPathTranslator {
return &TerminatedPathTranslator{}
}
// TranslatePreRouting is called before any routing decisions are made.
func (t *TerminatedPathTranslator) TranslatePreRouting(r *http.Request) (*http.Request, error) {
return r, nil
}
// TranslateGit is called for a git related request.
func (t *TerminatedPathTranslator) TranslateGit(r *http.Request) (*http.Request, error) {
return r, encode.GitPath(r)
}
// TranslateAPI is called for an API related request.
func (t *TerminatedPathTranslator) TranslateAPI(r *http.Request) (*http.Request, error) {
return r, encode.TerminatedPath(terminatedPathPrefixesAPI, r)
}
// TranslateWeb is called for an web related request.
func (t *TerminatedPathTranslator) TranslateWeb(r *http.Request) (*http.Request, error) {
return r, nil
}

View File

@ -0,0 +1,18 @@
// Copyright 2021 Harness Inc. All rights reserved.
// Use of this source code is governed by the Polyform Free Trial License
// that can be found in the LICENSE.md file for this repository.
package translator
import (
"github.com/google/wire"
)
// WireSet provides a wire set for this package.
var WireSet = wire.NewSet(
ProvideRequestTranslator,
)
func ProvideRequestTranslator() RequestTranslator {
return NewTerminatedPathTranslator()
}

View File

@ -4,7 +4,6 @@ import (
"context" "context"
"net/http" "net/http"
"github.com/harness/gitness/internal/api/middleware/encode"
"github.com/harness/gitness/internal/store" "github.com/harness/gitness/internal/store"
"github.com/harness/gitness/web" "github.com/harness/gitness/web"
"github.com/swaggest/swgui/v3emb" "github.com/swaggest/swgui/v3emb"
@ -14,18 +13,13 @@ import (
) )
/* /*
* Mounts the WEB Router under mountPath. * newWebHandler returns a new http handler for handling WEB calls.
* The handler is wrapped within a layer that handles encoding Paths.
*/ */
func newWebHandler( func newWebHandler(systemStore store.SystemStore) http.Handler {
mountPath string,
systemStore store.SystemStore) http.Handler {
//
config := systemStore.Config(context.Background()) config := systemStore.Config(context.Background())
// Use go-chi router for inner routing (restricted to mountPath!) // Use go-chi router for inner routing (restricted to mountPath!)
r := chi.NewRouter() r := chi.NewRouter()
r.Route(mountPath, func(r chi.Router) {
// create middleware to enforce security best practices for // create middleware to enforce security best practices for
// the user interface. note that theis middleware is only used // the user interface. note that theis middleware is only used
// when serving the user interface (not found handler, below). // when serving the user interface (not found handler, below).
@ -59,8 +53,6 @@ func newWebHandler(
r.With(sec.Handler).NotFound( r.With(sec.Handler).NotFound(
web.Handler(), web.Handler(),
) )
})
// web doesn't have any prefixes for terminated paths return r
return encode.TerminatedPathBefore([]string{""}, r.ServeHTTP)
} }

View File

@ -5,8 +5,29 @@
package router package router
import ( import (
"net/http"
"github.com/google/wire" "github.com/google/wire"
"github.com/harness/gitness/internal/auth/authn"
"github.com/harness/gitness/internal/auth/authz"
"github.com/harness/gitness/internal/router/translator"
"github.com/harness/gitness/internal/store"
) )
// WireSet provides a wire set for this package. // WireSet provides a wire set for this package.
var WireSet = wire.NewSet(New) var WireSet = wire.NewSet(ProvideHTTPHandler)
func ProvideHTTPHandler(
translator translator.RequestTranslator,
systemStore store.SystemStore,
userStore store.UserStore,
spaceStore store.SpaceStore,
repoStore store.RepoStore,
tokenStore store.TokenStore,
saStore store.ServiceAccountStore,
authenticator authn.Authenticator,
authorizer authz.Authorizer,
) (http.Handler, error) {
return NewRouter(translator, systemStore, userStore, spaceStore,
repoStore, tokenStore, saStore, authenticator, authorizer)
}

View File

@ -1,9 +1,9 @@
CREATE TABLE IF NOT EXISTS principals ( CREATE TABLE IF NOT EXISTS principals (
principal_id SERIAL PRIMARY KEY principal_id SERIAL PRIMARY KEY
,principal_uid TEXT
,principal_type TEXT ,principal_type TEXT
,principal_name TEXT ,principal_name TEXT
,principal_admin BOOLEAN ,principal_admin BOOLEAN
,principal_externalId TEXT
,principal_blocked BOOLEAN ,principal_blocked BOOLEAN
,principal_salt TEXT ,principal_salt TEXT
,principal_created INTEGER ,principal_created INTEGER
@ -15,6 +15,7 @@ principal_id SERIAL PRIMARY KEY
,principal_sa_parentType TEXT ,principal_sa_parentType TEXT
,principal_sa_parentId INTEGER ,principal_sa_parentId INTEGER
,UNIQUE(principal_uid)
,UNIQUE(principal_salt) ,UNIQUE(principal_salt)
,UNIQUE(principal_user_email) ,UNIQUE(principal_user_email)
); );

View File

@ -1,9 +1,9 @@
CREATE TABLE IF NOT EXISTS principals ( CREATE TABLE IF NOT EXISTS principals (
principal_id INTEGER PRIMARY KEY AUTOINCREMENT principal_id INTEGER PRIMARY KEY AUTOINCREMENT
,principal_uid TEXT
,principal_type TEXT ,principal_type TEXT
,principal_name TEXT ,principal_name TEXT
,principal_admin BOOLEAN ,principal_admin BOOLEAN
,principal_externalId TEXT
,principal_blocked BOOLEAN ,principal_blocked BOOLEAN
,principal_salt TEXT ,principal_salt TEXT
,principal_created INTEGER ,principal_created INTEGER
@ -15,6 +15,7 @@ principal_id INTEGER PRIMARY KEY AUTOINCREMENT
,principal_sa_parentType TEXT ,principal_sa_parentType TEXT
,principal_sa_parentId INTEGER ,principal_sa_parentId INTEGER
,UNIQUE(principal_uid)
,UNIQUE(principal_salt) ,UNIQUE(principal_salt)
,UNIQUE(principal_user_email COLLATE NOCASE) ,UNIQUE(principal_user_email COLLATE NOCASE)
); );

View File

@ -37,6 +37,15 @@ func (s *ServiceAccountStore) Find(ctx context.Context, id int64) (*types.Servic
return dst, nil return dst, nil
} }
// FindUID finds the service account by uid.
func (s *ServiceAccountStore) FindUID(ctx context.Context, uid string) (*types.ServiceAccount, error) {
dst := new(types.ServiceAccount)
if err := s.db.GetContext(ctx, dst, serviceAccountSelectUID, uid); err != nil {
return nil, processSQLErrorf(err, "Select by uid query failed")
}
return dst, nil
}
// Create saves the service account. // Create saves the service account.
func (s *ServiceAccountStore) Create(ctx context.Context, sa *types.ServiceAccount) error { func (s *ServiceAccountStore) Create(ctx context.Context, sa *types.ServiceAccount) error {
query, arg, err := s.db.BindNamed(serviceAccountInsert, sa) query, arg, err := s.db.BindNamed(serviceAccountInsert, sa)
@ -113,8 +122,8 @@ WHERE principal_type = "serviceaccount" and principal_sa_parentType = $1 and pri
const serviceAccountBase = ` const serviceAccountBase = `
SELECT SELECT
principal_id principal_id
,principal_uid
,principal_name ,principal_name
,principal_externalId
,principal_blocked ,principal_blocked
,principal_salt ,principal_salt
,principal_created ,principal_created
@ -133,6 +142,10 @@ const serviceAccountSelectID = serviceAccountBase + `
WHERE principal_type = "serviceaccount" AND principal_id = $1 WHERE principal_type = "serviceaccount" AND principal_id = $1
` `
const serviceAccountSelectUID = serviceAccountBase + `
WHERE principal_type = "serviceaccount" AND principal_uid = $1
`
const serviceAccountDelete = ` const serviceAccountDelete = `
DELETE FROM principals DELETE FROM principals
WHERE principal_type = "serviceaccount" AND principal_id = $1 WHERE principal_type = "serviceaccount" AND principal_id = $1
@ -141,9 +154,9 @@ WHERE principal_type = "serviceaccount" AND principal_id = $1
const serviceAccountInsert = ` const serviceAccountInsert = `
INSERT INTO principals ( INSERT INTO principals (
principal_type principal_type
,principal_uid
,principal_name ,principal_name
,principal_admin ,principal_admin
,principal_externalId
,principal_blocked ,principal_blocked
,principal_salt ,principal_salt
,principal_created ,principal_created
@ -152,9 +165,9 @@ principal_type
,principal_sa_parentId ,principal_sa_parentId
) values ( ) values (
"serviceaccount" "serviceaccount"
,:principal_uid
,:principal_name ,:principal_name
,false ,false
,:principal_externalId
,:principal_blocked ,:principal_blocked
,:principal_salt ,:principal_salt
,:principal_created ,:principal_created
@ -168,7 +181,6 @@ const serviceAccountUpdate = `
UPDATE principals UPDATE principals
SET SET
principal_name = :principal_name principal_name = :principal_name
,:principal_externalId = :principal_externalId
,:principal_blocked = :principal_blocked ,:principal_blocked = :principal_blocked
,:principal_salt = :principal_salt ,:principal_salt = :principal_salt
,:principal_updated = :principal_updated ,:principal_updated = :principal_updated

View File

@ -34,6 +34,13 @@ func (s *ServiceAccountStoreSync) Find(ctx context.Context, id int64) (*types.Se
return s.base.Find(ctx, id) return s.base.Find(ctx, id)
} }
// FindUID finds the service account by uid.
func (s *ServiceAccountStoreSync) FindUID(ctx context.Context, uid string) (*types.ServiceAccount, error) {
mutex.RLock()
defer mutex.RUnlock()
return s.base.FindUID(ctx, uid)
}
// Create saves the service account. // Create saves the service account.
func (s *ServiceAccountStoreSync) Create(ctx context.Context, sa *types.ServiceAccount) error { func (s *ServiceAccountStoreSync) Create(ctx context.Context, sa *types.ServiceAccount) error {
mutex.RLock() mutex.RLock()

View File

@ -1,6 +1,7 @@
[ [
{ {
"id": 1, "id": 1,
"uid": "jane21",
"email": "jane@example.com", "email": "jane@example.com",
"name": "jane", "name": "jane",
"company": "acme", "company": "acme",
@ -12,6 +13,7 @@
}, },
{ {
"id": 2, "id": 2,
"uid": "john21",
"email": "john@example.com", "email": "john@example.com",
"name": "john", "name": "john",
"company": "acme", "company": "acme",

View File

@ -7,7 +7,6 @@ package database
import ( import (
"context" "context"
"database/sql" "database/sql"
"strconv"
"github.com/harness/gitness/internal/store" "github.com/harness/gitness/internal/store"
"github.com/harness/gitness/types" "github.com/harness/gitness/types"
@ -39,6 +38,15 @@ func (s *UserStore) Find(ctx context.Context, id int64) (*types.User, error) {
return dst, nil return dst, nil
} }
// FindUID finds the user by uid.
func (s *UserStore) FindUID(ctx context.Context, uid string) (*types.User, error) {
dst := new(types.User)
if err := s.db.GetContext(ctx, dst, userSelectUID, uid); err != nil {
return nil, processSQLErrorf(err, "Select by uid query failed")
}
return dst, nil
}
// FindEmail finds the user by email. // FindEmail finds the user by email.
func (s *UserStore) FindEmail(ctx context.Context, email string) (*types.User, error) { func (s *UserStore) FindEmail(ctx context.Context, email string) (*types.User, error) {
dst := new(types.User) dst := new(types.User)
@ -48,15 +56,6 @@ func (s *UserStore) FindEmail(ctx context.Context, email string) (*types.User, e
return dst, nil return dst, nil
} }
// FindKey finds the user unique key (email or id).
func (s *UserStore) FindKey(ctx context.Context, key string) (*types.User, error) {
id, err := strconv.ParseInt(key, 10, 64)
if err == nil {
return s.Find(ctx, id)
}
return s.FindEmail(ctx, key)
}
// List returns a list of users. // List returns a list of users.
func (s *UserStore) List(ctx context.Context, opts *types.UserFilter) ([]*types.User, error) { func (s *UserStore) List(ctx context.Context, opts *types.UserFilter) ([]*types.User, error) {
dst := []*types.User{} dst := []*types.User{}
@ -88,8 +87,8 @@ func (s *UserStore) List(ctx context.Context, opts *types.UserFilter) ([]*types.
stmt = stmt.OrderBy("principal_updated " + opts.Order.String()) stmt = stmt.OrderBy("principal_updated " + opts.Order.String())
case enum.UserAttrEmail: case enum.UserAttrEmail:
stmt = stmt.OrderBy("principal_user_email " + opts.Order.String()) stmt = stmt.OrderBy("principal_user_email " + opts.Order.String())
case enum.UserAttrID: case enum.UserAttrUID:
stmt = stmt.OrderBy("principal_id " + opts.Order.String()) stmt = stmt.OrderBy("principal_uid " + opts.Order.String())
case enum.UserAttrAdmin: case enum.UserAttrAdmin:
stmt = stmt.OrderBy("principal_admin " + opts.Order.String()) stmt = stmt.OrderBy("principal_admin " + opts.Order.String())
} }
@ -135,7 +134,7 @@ func (s *UserStore) Update(ctx context.Context, user *types.User) error {
} }
// Delete deletes the user. // Delete deletes the user.
func (s *UserStore) Delete(ctx context.Context, user *types.User) error { func (s *UserStore) Delete(ctx context.Context, id int64) error {
tx, err := s.db.BeginTx(ctx, nil) tx, err := s.db.BeginTx(ctx, nil)
if err != nil { if err != nil {
return processSQLErrorf(err, "Failed to start a new transaction") return processSQLErrorf(err, "Failed to start a new transaction")
@ -144,7 +143,7 @@ func (s *UserStore) Delete(ctx context.Context, user *types.User) error {
_ = tx.Rollback() _ = tx.Rollback()
}(tx) }(tx)
// delete the user // delete the user
if _, err = tx.ExecContext(ctx, userDelete, user.ID); err != nil { if _, err = tx.ExecContext(ctx, userDelete, id); err != nil {
return processSQLErrorf(err, "The delete query failed") return processSQLErrorf(err, "The delete query failed")
} }
return tx.Commit() return tx.Commit()
@ -169,9 +168,9 @@ WHERE principal_type = "user"
const userBase = ` const userBase = `
SELECT SELECT
principal_id principal_id
,principal_uid
,principal_name ,principal_name
,principal_admin ,principal_admin
,principal_externalId
,principal_blocked ,principal_blocked
,principal_salt ,principal_salt
,principal_created ,principal_created
@ -191,6 +190,10 @@ const userSelectID = userBase + `
WHERE principal_type = "user" AND principal_id = $1 WHERE principal_type = "user" AND principal_id = $1
` `
const userSelectUID = userBase + `
WHERE principal_type = "user" AND principal_uid = $1
`
const userSelectEmail = userBase + ` const userSelectEmail = userBase + `
WHERE principal_type = "user" AND principal_user_email = $1 WHERE principal_type = "user" AND principal_user_email = $1
` `
@ -203,9 +206,9 @@ WHERE principal_type = "user" AND principal_id = $1
const userInsert = ` const userInsert = `
INSERT INTO principals ( INSERT INTO principals (
principal_type principal_type
,principal_uid
,principal_name ,principal_name
,principal_admin ,principal_admin
,principal_externalId
,principal_blocked ,principal_blocked
,principal_salt ,principal_salt
,principal_created ,principal_created
@ -214,9 +217,9 @@ principal_type
,principal_user_password ,principal_user_password
) values ( ) values (
"user" "user"
,:principal_uid
,:principal_name ,:principal_name
,:principal_admin ,:principal_admin
,:principal_externalId
,:principal_blocked ,:principal_blocked
,:principal_salt ,:principal_salt
,:principal_created ,:principal_created
@ -231,7 +234,6 @@ UPDATE principals
SET SET
principal_name = :principal_name principal_name = :principal_name
,principal_admin = :principal_admin ,principal_admin = :principal_admin
,principal_externalId = :principal_externalId
,principal_blocked = :principal_blocked ,principal_blocked = :principal_blocked
,principal_salt = :principal_salt ,principal_salt = :principal_salt
,principal_updated = :principal_updated ,principal_updated = :principal_updated

View File

@ -31,6 +31,13 @@ func (s *UserStoreSync) Find(ctx context.Context, id int64) (*types.User, error)
return s.base.Find(ctx, id) return s.base.Find(ctx, id)
} }
// FindUID finds the user by uid.
func (s *UserStoreSync) FindUID(ctx context.Context, uid string) (*types.User, error) {
mutex.RLock()
defer mutex.RUnlock()
return s.base.FindUID(ctx, uid)
}
// FindEmail finds the user by email. // FindEmail finds the user by email.
func (s *UserStoreSync) FindEmail(ctx context.Context, email string) (*types.User, error) { func (s *UserStoreSync) FindEmail(ctx context.Context, email string) (*types.User, error) {
mutex.RLock() mutex.RLock()
@ -38,13 +45,6 @@ func (s *UserStoreSync) FindEmail(ctx context.Context, email string) (*types.Use
return s.base.FindEmail(ctx, email) return s.base.FindEmail(ctx, email)
} }
// FindKey finds the user unique key (email or id).
func (s *UserStoreSync) FindKey(ctx context.Context, key string) (*types.User, error) {
mutex.RLock()
defer mutex.RUnlock()
return s.base.FindKey(ctx, key)
}
// List returns a list of users. // List returns a list of users.
func (s *UserStoreSync) List(ctx context.Context, opts *types.UserFilter) ([]*types.User, error) { func (s *UserStoreSync) List(ctx context.Context, opts *types.UserFilter) ([]*types.User, error) {
mutex.RLock() mutex.RLock()
@ -67,10 +67,10 @@ func (s *UserStoreSync) Update(ctx context.Context, user *types.User) error {
} }
// Delete deletes the user. // Delete deletes the user.
func (s *UserStoreSync) Delete(ctx context.Context, user *types.User) error { func (s *UserStoreSync) Delete(ctx context.Context, id int64) error {
mutex.Lock() mutex.Lock()
defer mutex.Unlock() defer mutex.Unlock()
return s.base.Delete(ctx, user) return s.base.Delete(ctx, id)
} }
// Count returns a count of users. // Count returns a count of users.

View File

@ -141,6 +141,18 @@ func testUserFind(store store.UserStore) func(t *testing.T) {
} }
}) })
t.Run("uid", func(t *testing.T) {
got, err := store.FindUID(ctx, "jane21")
if err != nil {
t.Error(err)
return
}
if diff := cmp.Diff(got, want, userIgnore); len(diff) != 0 {
t.Errorf(diff)
return
}
})
t.Run("email", func(t *testing.T) { t.Run("email", func(t *testing.T) {
got, err := store.FindEmail(ctx, want.Email) got, err := store.FindEmail(ctx, want.Email)
if err != nil { if err != nil {
@ -164,30 +176,6 @@ func testUserFind(store store.UserStore) func(t *testing.T) {
return return
} }
}) })
t.Run("key/id", func(t *testing.T) {
got, err := store.FindKey(ctx, "1")
if err != nil {
t.Error(err)
return
}
if diff := cmp.Diff(got, want, userIgnore); len(diff) != 0 {
t.Errorf(diff)
return
}
})
t.Run("key/email", func(t *testing.T) {
got, err := store.FindKey(ctx, want.Email)
if err != nil {
t.Error(err)
return
}
if diff := cmp.Diff(got, want, userIgnore); len(diff) != 0 {
t.Errorf(diff)
return
}
})
} }
} }
@ -247,12 +235,12 @@ func testUserUpdate(store store.UserStore) func(t *testing.T) {
func testUserDelete(s store.UserStore) func(t *testing.T) { func testUserDelete(s store.UserStore) func(t *testing.T) {
return func(t *testing.T) { return func(t *testing.T) {
ctx := context.Background() ctx := context.Background()
v, err := s.Find(ctx, 1) _, err := s.Find(ctx, 1)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
return return
} }
if err = s.Delete(ctx, v); err != nil { if err = s.Delete(ctx, 1); err != nil {
t.Error(err) t.Error(err)
return return
} }

View File

@ -40,7 +40,7 @@ func offset(page, size int) int {
// Logs the error and message, returns either the original error or a store equivalent if possible. // Logs the error and message, returns either the original error or a store equivalent if possible.
func processSQLErrorf(err error, format string, args ...interface{}) error { func processSQLErrorf(err error, format string, args ...interface{}) error {
// always log DB error (print formated message) // always log DB error (print formated message)
log.Warn().Msgf("%s %s", fmt.Sprintf(format, args...), err) log.Debug().Msgf("%s %s", fmt.Sprintf(format, args...), err)
// If it's a known error, return converted error instead. // If it's a known error, return converted error instead.
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {

View File

@ -18,12 +18,12 @@ type (
// Find finds the user by id. // Find finds the user by id.
Find(ctx context.Context, id int64) (*types.User, error) Find(ctx context.Context, id int64) (*types.User, error)
// FindUID finds the user by uid.
FindUID(ctx context.Context, uid string) (*types.User, error)
// FindEmail finds the user by email. // FindEmail finds the user by email.
FindEmail(ctx context.Context, email string) (*types.User, error) FindEmail(ctx context.Context, email string) (*types.User, error)
// FindKey finds the user by unique key (email or id).
FindKey(ctx context.Context, key string) (*types.User, error)
// Create saves the user details. // Create saves the user details.
Create(ctx context.Context, user *types.User) error Create(ctx context.Context, user *types.User) error
@ -31,7 +31,7 @@ type (
Update(ctx context.Context, user *types.User) error Update(ctx context.Context, user *types.User) error
// Delete deletes the user. // Delete deletes the user.
Delete(ctx context.Context, user *types.User) error Delete(ctx context.Context, id int64) error
// List returns a list of users. // List returns a list of users.
List(ctx context.Context, params *types.UserFilter) ([]*types.User, error) List(ctx context.Context, params *types.UserFilter) ([]*types.User, error)
@ -45,6 +45,9 @@ type (
// Find finds the service account by id. // Find finds the service account by id.
Find(ctx context.Context, id int64) (*types.ServiceAccount, error) Find(ctx context.Context, id int64) (*types.ServiceAccount, error)
// FindUID finds the service account by uid.
FindUID(ctx context.Context, uid string) (*types.ServiceAccount, error)
// Create saves the service account. // Create saves the service account.
Create(ctx context.Context, sa *types.ServiceAccount) error Create(ctx context.Context, sa *types.ServiceAccount) error

View File

@ -51,18 +51,18 @@ func (mr *MockClientMockRecorder) Login(arg0, arg1, arg2 interface{}) *gomock.Ca
} }
// Register mocks base method. // Register mocks base method.
func (m *MockClient) Register(arg0 context.Context, arg1, arg2 string) (*types.TokenResponse, error) { func (m *MockClient) Register(arg0 context.Context, arg1, arg2, arg3, arg4 string) (*types.TokenResponse, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Register", arg0, arg1, arg2) ret := m.ctrl.Call(m, "Register", arg0, arg1, arg2, arg3, arg4)
ret0, _ := ret[0].(*types.TokenResponse) ret0, _ := ret[0].(*types.TokenResponse)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
// Register indicates an expected call of Register. // Register indicates an expected call of Register.
func (mr *MockClientMockRecorder) Register(arg0, arg1, arg2 interface{}) *gomock.Call { func (mr *MockClientMockRecorder) Register(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Register", reflect.TypeOf((*MockClient)(nil).Register), arg0, arg1, arg2) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Register", reflect.TypeOf((*MockClient)(nil).Register), arg0, arg1, arg2, arg3, arg4)
} }
// Self mocks base method. // Self mocks base method.

View File

@ -102,7 +102,7 @@ func (mr *MockUserStoreMockRecorder) Create(arg0, arg1 interface{}) *gomock.Call
} }
// Delete mocks base method. // Delete mocks base method.
func (m *MockUserStore) Delete(arg0 context.Context, arg1 *types.User) error { func (m *MockUserStore) Delete(arg0 context.Context, arg1 int64) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", arg0, arg1) ret := m.ctrl.Call(m, "Delete", arg0, arg1)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
@ -145,19 +145,19 @@ func (mr *MockUserStoreMockRecorder) FindEmail(arg0, arg1 interface{}) *gomock.C
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindEmail", reflect.TypeOf((*MockUserStore)(nil).FindEmail), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindEmail", reflect.TypeOf((*MockUserStore)(nil).FindEmail), arg0, arg1)
} }
// FindKey mocks base method. // FindUID mocks base method.
func (m *MockUserStore) FindKey(arg0 context.Context, arg1 string) (*types.User, error) { func (m *MockUserStore) FindUID(arg0 context.Context, arg1 string) (*types.User, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FindKey", arg0, arg1) ret := m.ctrl.Call(m, "FindUID", arg0, arg1)
ret0, _ := ret[0].(*types.User) ret0, _ := ret[0].(*types.User)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
// FindKey indicates an expected call of FindKey. // FindUID indicates an expected call of FindUID.
func (mr *MockUserStoreMockRecorder) FindKey(arg0, arg1 interface{}) *gomock.Call { func (mr *MockUserStoreMockRecorder) FindUID(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindKey", reflect.TypeOf((*MockUserStore)(nil).FindKey), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindUID", reflect.TypeOf((*MockUserStore)(nil).FindUID), arg0, arg1)
} }
// List mocks base method. // List mocks base method.

View File

@ -17,20 +17,32 @@ const (
minNameLength = 1 minNameLength = 1
maxNameLength = 256 maxNameLength = 256
nameRegex = "^[a-zA-Z][a-zA-Z0-9\\-\\_ ]*$" nameRegex = "^[a-zA-Z][a-zA-Z0-9\\-\\_ ]*$"
minUIDLength = 2
maxUIDLength = 64
uidRegex = "^[a-z][a-z0-9\\-\\_]*$"
) )
var ( var (
ErrPathNameLength = &ValidationError{ ErrPathNameLength = &ValidationError{
fmt.Sprintf("Path name has to be between %d and %d in length.", minPathNameLength, maxPathNameLength), fmt.Sprintf("Path name has to be between %d and %d in length.", minPathNameLength, maxPathNameLength),
} }
ErrPathNameRegex = &ValidationError{"Path name has start with a letter and only contain the following [a-z0-9-_]."} ErrPathNameRegex = &ValidationError{"Path name has to start with a letter and only contain the following [a-z0-9-_]."}
ErrNameLength = &ValidationError{ ErrNameLength = &ValidationError{
fmt.Sprintf("Name has to be between %d and %d in length.", fmt.Sprintf("Name has to be between %d and %d in length.",
minNameLength, maxNameLength), minNameLength, maxNameLength),
} }
ErrNameRegex = &ValidationError{ ErrNameRegex = &ValidationError{
"Name has start with a letter and only contain the following [a-zA-Z0-9-_ ].", "Name has to start with a letter and only contain the following [a-zA-Z0-9-_ ].",
}
ErrUIDLength = &ValidationError{
fmt.Sprintf("UID has to be between %d and %d in length.",
minUIDLength, maxUIDLength),
}
ErrUIDRegex = &ValidationError{
"UID has to start with a letter and only contain the following [a-z0-9-_].",
} }
) )
@ -61,3 +73,17 @@ func Name(name string) error {
return nil return nil
} }
// UID checks the provided uid and returns an error in it isn't valid.
func UID(uid string) error {
l := len(uid)
if l < minUIDLength || l > maxUIDLength {
return ErrUIDLength
}
if ok, _ := regexp.Match(uidRegex, []byte(uid)); !ok {
return ErrUIDRegex
}
return nil
}

View File

@ -17,6 +17,11 @@ var (
// ServiceAccount returns true if the ServiceAccount if valid. // ServiceAccount returns true if the ServiceAccount if valid.
func ServiceAccount(sa *types.ServiceAccount) error { func ServiceAccount(sa *types.ServiceAccount) error {
// validate UID
if err := UID(sa.UID); err != nil {
return err
}
// verify name // verify name
if err := Name(sa.Name); err != nil { if err := Name(sa.Name); err != nil {
return err return err

View File

@ -25,6 +25,11 @@ var (
// User returns true if the User if valid. // User returns true if the User if valid.
func User(user *types.User) error { func User(user *types.User) error {
// validate UID
if err := UID(user.UID); err != nil {
return err
}
// validate name // validate name
if err := Name(user.Name); err != nil { if err := Name(user.Name); err != nil {
return err return err

View File

@ -8,60 +8,60 @@ import "time"
// Config stores the system configuration. // Config stores the system configuration.
type Config struct { type Config struct {
Debug bool `envconfig:"APP_DEBUG"` Debug bool `envconfig:"GITNESS_DEBUG"`
Trace bool `envconfig:"APP_TRACE"` Trace bool `envconfig:"GITNESS_TRACE"`
// Server defines the server configuration parameters. // Server defines the server configuration parameters.
Server struct { Server struct {
Bind string `envconfig:"APP_HTTP_BIND" default:":3000"` Bind string `envconfig:"GITNESS_HTTP_BIND" default:":3000"`
Proto string `envconfig:"APP_HTTP_PROTO"` Proto string `envconfig:"GITNESS_HTTP_PROTO"`
Host string `envconfig:"APP_HTTP_HOST"` Host string `envconfig:"GITNESS_HTTP_HOST"`
// Acme defines Acme configuration parameters. // Acme defines Acme configuration parameters.
Acme struct { Acme struct {
Enabled bool `envconfig:"APP_ACME_ENABLED"` Enabled bool `envconfig:"GITNESS_ACME_ENABLED"`
Endpont string `envconfig:"APP_ACME_ENDPOINT"` Endpont string `envconfig:"GITNESS_ACME_ENDPOINT"`
Email bool `envconfig:"APP_ACME_EMAIL"` Email bool `envconfig:"GITNESS_ACME_EMAIL"`
} }
} }
// Database defines the database configuration parameters. // Database defines the database configuration parameters.
Database struct { Database struct {
Driver string `envconfig:"APP_DATABASE_DRIVER" default:"sqlite3"` Driver string `envconfig:"GITNESS_DATABASE_DRIVER" default:"sqlite3"`
Datasource string `envconfig:"APP_DATABASE_DATASOURCE" default:"database.sqlite3"` Datasource string `envconfig:"GITNESS_DATABASE_DATASOURCE" default:"database.sqlite3"`
} }
// Token defines token configuration parameters. // Token defines token configuration parameters.
Token struct { Token struct {
Expire time.Duration `envconfig:"APP_TOKEN_EXPIRE" default:"720h"` Expire time.Duration `envconfig:"GITNESS_TOKEN_EXPIRE" default:"720h"`
} }
// Cors defines http cors parameters // Cors defines http cors parameters
Cors struct { Cors struct {
AllowedOrigins []string `envconfig:"APP_CORS_ALLOWED_ORIGINS" default:"*"` AllowedOrigins []string `envconfig:"GITNESS_CORS_ALLOWED_ORIGINS" default:"*"`
AllowedMethods []string `envconfig:"APP_CORS_ALLOWED_METHODS" default:"GET,POST,PATCH,PUT,DELETE,OPTIONS"` AllowedMethods []string `envconfig:"GITNESS_CORS_ALLOWED_METHODS" default:"GET,POST,PATCH,PUT,DELETE,OPTIONS"`
AllowedHeaders []string `envconfig:"APP_CORS_ALLOWED_HEADERS" default:"Origin,Accept,Accept-Language,Authorization,Content-Type,Content-Language,X-Requested-With,X-Request-Id"` //nolint:lll // struct tags can't be multiline AllowedHeaders []string `envconfig:"GITNESS_CORS_ALLOWED_HEADERS" default:"Origin,Accept,Accept-Language,Authorization,Content-Type,Content-Language,X-Requested-With,X-Request-Id"` //nolint:lll // struct tags can't be multiline
ExposedHeaders []string `envconfig:"APP_CORS_EXPOSED_HEADERS" default:"Link"` ExposedHeaders []string `envconfig:"GITNESS_CORS_EXPOSED_HEADERS" default:"Link"`
AllowCredentials bool `envconfig:"APP_CORS_ALLOW_CREDENTIALS" default:"true"` AllowCredentials bool `envconfig:"GITNESS_CORS_ALLOW_CREDENTIALS" default:"true"`
MaxAge int `envconfig:"APP_CORS_MAX_AGE" default:"300"` MaxAge int `envconfig:"GITNESS_CORS_MAX_AGE" default:"300"`
} }
// Secure defines http security parameters. // Secure defines http security parameters.
Secure struct { Secure struct {
AllowedHosts []string `envconfig:"APP_HTTP_ALLOWED_HOSTS"` AllowedHosts []string `envconfig:"GITNESS_HTTP_ALLOWED_HOSTS"`
HostsProxyHeaders []string `envconfig:"APP_HTTP_PROXY_HEADERS"` HostsProxyHeaders []string `envconfig:"GITNESS_HTTP_PROXY_HEADERS"`
SSLRedirect bool `envconfig:"APP_HTTP_SSL_REDIRECT"` SSLRedirect bool `envconfig:"GITNESS_HTTP_SSL_REDIRECT"`
SSLTemporaryRedirect bool `envconfig:"APP_HTTP_SSL_TEMPORARY_REDIRECT"` SSLTemporaryRedirect bool `envconfig:"GITNESS_HTTP_SSL_TEMPORARY_REDIRECT"`
SSLHost string `envconfig:"APP_HTTP_SSL_HOST"` SSLHost string `envconfig:"GITNESS_HTTP_SSL_HOST"`
SSLProxyHeaders map[string]string `envconfig:"APP_HTTP_SSL_PROXY_HEADERS"` SSLProxyHeaders map[string]string `envconfig:"GITNESS_HTTP_SSL_PROXY_HEADERS"`
STSSeconds int64 `envconfig:"APP_HTTP_STS_SECONDS"` STSSeconds int64 `envconfig:"GITNESS_HTTP_STS_SECONDS"`
STSIncludeSubdomains bool `envconfig:"APP_HTTP_STS_INCLUDE_SUBDOMAINS"` STSIncludeSubdomains bool `envconfig:"GITNESS_HTTP_STS_INCLUDE_SUBDOMAINS"`
STSPreload bool `envconfig:"APP_HTTP_STS_PRELOAD"` STSPreload bool `envconfig:"GITNESS_HTTP_STS_PRELOAD"`
ForceSTSHeader bool `envconfig:"APP_HTTP_STS_FORCE_HEADER"` ForceSTSHeader bool `envconfig:"GITNESS_HTTP_STS_FORCE_HEADER"`
BrowserXSSFilter bool `envconfig:"APP_HTTP_BROWSER_XSS_FILTER" default:"true"` BrowserXSSFilter bool `envconfig:"GITNESS_HTTP_BROWSER_XSS_FILTER" default:"true"`
FrameDeny bool `envconfig:"APP_HTTP_FRAME_DENY" default:"true"` FrameDeny bool `envconfig:"GITNESS_HTTP_FRAME_DENY" default:"true"`
ContentTypeNosniff bool `envconfig:"APP_HTTP_CONTENT_TYPE_NO_SNIFF"` ContentTypeNosniff bool `envconfig:"GITNESS_HTTP_CONTENT_TYPE_NO_SNIFF"`
ContentSecurityPolicy string `envconfig:"APP_HTTP_CONTENT_SECURITY_POLICY"` ContentSecurityPolicy string `envconfig:"GITNESS_HTTP_CONTENT_SECURITY_POLICY"`
ReferrerPolicy string `envconfig:"APP_HTTP_REFERRER_POLICY"` ReferrerPolicy string `envconfig:"GITNESS_HTTP_REFERRER_POLICY"`
} }
} }

View File

@ -13,7 +13,7 @@ type UserAttr int
// Order enumeration. // Order enumeration.
const ( const (
UserAttrNone UserAttr = iota UserAttrNone UserAttr = iota
UserAttrID UserAttrUID
UserAttrName UserAttrName
UserAttrEmail UserAttrEmail
UserAttrAdmin UserAttrAdmin
@ -26,7 +26,7 @@ const (
func ParseUserAttr(s string) UserAttr { func ParseUserAttr(s string) UserAttr {
switch strings.ToLower(s) { switch strings.ToLower(s) {
case "id": case "id":
return UserAttrID return UserAttrUID
case "name": case "name":
return UserAttrName return UserAttrName
case "email": case "email":

View File

@ -11,7 +11,7 @@ func TestParseUserAttr(t *testing.T) {
text string text string
want UserAttr want UserAttr
}{ }{
{"id", UserAttrID}, {"id", UserAttrUID},
{"name", UserAttrName}, {"name", UserAttrName},
{"email", UserAttrEmail}, {"email", UserAttrEmail},
{"created", UserAttrCreated}, {"created", UserAttrCreated},

View File

@ -10,13 +10,14 @@ import "github.com/harness/gitness/types/enum"
type ( type (
// Represents the identity of an acting entity (User, ServiceAccount, Service). // Represents the identity of an acting entity (User, ServiceAccount, Service).
Principal struct { Principal struct {
ID int64 `db:"principal_id" json:"id"` // ID is the internal identifier of a principal (primary key)
ID int64 `db:"principal_id" json:"-"`
UID string `db:"principal_uid" json:"uid"`
Type enum.PrincipalType `db:"principal_type" json:"type"` Type enum.PrincipalType `db:"principal_type" json:"type"`
Name string `db:"principal_name" json:"name"` Name string `db:"principal_name" json:"name"`
Admin bool `db:"principal_admin" json:"admin"` Admin bool `db:"principal_admin" json:"admin"`
// Should be part of principal or not? // Should be part of principal or not?
ExternalID string `db:"principal_externalId" json:"externalId"`
Blocked bool `db:"principal_blocked" json:"blocked"` Blocked bool `db:"principal_blocked" json:"blocked"`
Salt string `db:"principal_salt" json:"-"` Salt string `db:"principal_salt" json:"-"`
@ -29,10 +30,10 @@ type (
func PrincipalFromUser(user *User) *Principal { func PrincipalFromUser(user *User) *Principal {
return &Principal{ return &Principal{
ID: user.ID, ID: user.ID,
UID: user.UID,
Type: enum.PrincipalTypeUser, Type: enum.PrincipalTypeUser,
Name: user.Name, Name: user.Name,
Admin: user.Admin, Admin: user.Admin,
ExternalID: user.ExternalID,
Blocked: user.Blocked, Blocked: user.Blocked,
Salt: user.Salt, Salt: user.Salt,
Created: user.Created, Created: user.Created,
@ -43,13 +44,27 @@ func PrincipalFromUser(user *User) *Principal {
func PrincipalFromServiceAccount(sa *ServiceAccount) *Principal { func PrincipalFromServiceAccount(sa *ServiceAccount) *Principal {
return &Principal{ return &Principal{
ID: sa.ID, ID: sa.ID,
UID: sa.UID,
Type: enum.PrincipalTypeServiceAccount, Type: enum.PrincipalTypeServiceAccount,
Name: sa.Name, Name: sa.Name,
Admin: false, Admin: false,
ExternalID: sa.ExternalID,
Blocked: sa.Blocked, Blocked: sa.Blocked,
Salt: sa.Salt, Salt: sa.Salt,
Created: sa.Created, Created: sa.Created,
Updated: sa.Updated, Updated: sa.Updated,
} }
} }
func PrincipalFromService(s *Service) *Principal {
return &Principal{
ID: s.ID,
UID: s.UID,
Type: enum.PrincipalTypeService,
Name: s.Name,
Admin: true,
Blocked: s.Blocked,
Salt: s.Salt,
Created: s.Created,
Updated: s.Updated,
}
}

View File

@ -10,11 +10,11 @@ import "github.com/harness/gitness/types/enum"
type ( type (
// Service is a principal representing a different internal service that runs alongside gitness. // Service is a principal representing a different internal service that runs alongside gitness.
Service struct { Service struct {
// Fields from Principal (without admin) // Fields from Principal (without admin, as it's always admin for now)
ID int64 `db:"principal_id" json:"id"` ID int64 `db:"principal_id" json:"-"`
UID string `db:"principal_uid" json:"uid"`
Name string `db:"principal_name" json:"name"` Name string `db:"principal_name" json:"name"`
Admin bool `db:"principal_admin" json:"admin"` Admin bool `db:"principal_admin" json:"admin"`
ExternalID string `db:"principal_externalId" json:"externalId"`
Blocked bool `db:"principal_blocked" json:"blocked"` Blocked bool `db:"principal_blocked" json:"blocked"`
Salt string `db:"principal_salt" json:"-"` Salt string `db:"principal_salt" json:"-"`
Created int64 `db:"principal_created" json:"created"` Created int64 `db:"principal_created" json:"created"`

View File

@ -10,10 +10,10 @@ import "github.com/harness/gitness/types/enum"
type ( type (
// ServiceAccount is a principal representing a service account. // ServiceAccount is a principal representing a service account.
ServiceAccount struct { ServiceAccount struct {
// Fields from Principal (without admin) // Fields from Principal (without admin, as it's never an admin)
ID int64 `db:"principal_id" json:"id"` ID int64 `db:"principal_id" json:"-"`
UID string `db:"principal_uid" json:"uid"`
Name string `db:"principal_name" json:"name"` Name string `db:"principal_name" json:"name"`
ExternalID string `db:"principal_externalId" json:"externalId"`
Blocked bool `db:"principal_blocked" json:"blocked"` Blocked bool `db:"principal_blocked" json:"blocked"`
Salt string `db:"principal_salt" json:"-"` Salt string `db:"principal_salt" json:"-"`
Created int64 `db:"principal_created" json:"created"` Created int64 `db:"principal_created" json:"created"`

View File

@ -13,10 +13,10 @@ type (
// User is a principal representing an end user. // User is a principal representing an end user.
User struct { User struct {
// Fields from Principal // Fields from Principal
ID int64 `db:"principal_id" json:"id"` ID int64 `db:"principal_id" json:"-"`
UID string `db:"principal_uid" json:"uid"`
Name string `db:"principal_name" json:"name"` Name string `db:"principal_name" json:"name"`
Admin bool `db:"principal_admin" json:"admin"` Admin bool `db:"principal_admin" json:"admin"`
ExternalID string `db:"principal_externalId" json:"externalId"`
Blocked bool `db:"principal_blocked" json:"blocked"` Blocked bool `db:"principal_blocked" json:"blocked"`
Salt string `db:"principal_salt" json:"-"` Salt string `db:"principal_salt" json:"-"`
Created int64 `db:"principal_created" json:"created"` Created int64 `db:"principal_created" json:"created"`