[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)
@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"
build: generate ## Build the gitness service binary
@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 .
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
@echo "Running tests"
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
# the source file has changed.
###########################################
cli/server/wire_gen.go: cli/server/wire.go ## Update the wire dependency injection if wire.go has changed.
@echo "Updating wire_gen.go"
go generate ./cli/server/wire_gen.go
cli/server/harness.wire_gen.go: cli/server/harness.wire.go ## Update the wire dependency injection if harness.wire.go has changed.
@echo "Updating harness.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
go generate mocks/mock.go

View File

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

View File

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

View File

@ -5,30 +5,14 @@
package server
import (
"os"
"github.com/harness/gitness/types"
"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
// host environment.
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)
// read the configuration from the environment and
// 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
// that can be found in the LICENSE.md file for this repository.
//go:build wireinject
// +build wireinject
//go:build wireinject && !harness
// +build wireinject,!harness
package server
@ -14,6 +14,7 @@ import (
"github.com/harness/gitness/internal/auth/authz"
"github.com/harness/gitness/internal/cron"
"github.com/harness/gitness/internal/router"
"github.com/harness/gitness/internal/router/translator"
"github.com/harness/gitness/internal/server"
"github.com/harness/gitness/internal/store/database"
"github.com/harness/gitness/internal/store/memory"
@ -24,14 +25,15 @@ import (
func initSystem(ctx context.Context, config *types.Config) (*system, error) {
wire.Build(
newSystem,
database.WireSet,
memory.WireSet,
router.WireSet,
server.WireSet,
cron.WireSet,
newSystem,
authn.WireSet,
authz.WireSet,
translator.WireSet,
)
return &system{}, nil
}

View File

@ -1,8 +1,7 @@
// Code generated by Wire. DO NOT EDIT.
//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject
//go:build !wireinject && !harness
// +build !wireinject,!harness
package server
@ -13,15 +12,17 @@ import (
"github.com/harness/gitness/internal/auth/authz"
"github.com/harness/gitness/internal/cron"
"github.com/harness/gitness/internal/router"
"github.com/harness/gitness/internal/router/translator"
"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 wire.go:
// Injectors from standalone.wire.go:
func initSystem(ctx context.Context, config *types.Config) (*system, error) {
requestTranslator := translator.ProvideRequestTranslator()
systemStore := memory.New(config)
db, err := database.ProvideDatabase(ctx, config)
if err != nil {
@ -32,9 +33,9 @@ func initSystem(ctx context.Context, config *types.Config) (*system, error) {
repoStore := database.ProvideRepoStore(db)
tokenStore := database.ProvideTokenStore(db)
serviceAccountStore := database.ProvideServiceAccountStore(db)
authenticator := authn.NewTokenAuthenticator(userStore, serviceAccountStore, tokenStore)
authorizer := authz.NewUnsafeAuthorizer()
handler, err := router.New(systemStore, userStore, spaceStore, repoStore, tokenStore, serviceAccountStore, authenticator, authorizer)
authenticator := authn.ProvideAuthenticator(userStore, serviceAccountStore, tokenStore)
authorizer := authz.ProvideAuthorizer()
handler, err := router.ProvideHTTPHandler(requestTranslator, systemStore, userStore, spaceStore, repoStore, tokenStore, serviceAccountStore, authenticator, authorizer)
if err != nil {
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.
func Credentials() (string, string) {
return Username(), Password()
@ -116,6 +121,26 @@ func Username() string {
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.
func Password() string {
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.
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.Add("username", username)
form.Add("name", name)
form.Add("email", email)
form.Add("password", password)
out := new(types.TokenResponse)
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)
// 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(ctx context.Context) (*types.User, error)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -30,11 +30,10 @@ func HandleDelete(userStore store.UserStore, tokenStore store.TokenStore) http.H
return
}
err = userStore.Delete(ctx, user)
err = userStore.Delete(ctx, user.ID)
if err != nil {
log.Error().Err(err).
Int64("user_id", user.ID).
Str("user_email", user.Email).
Str("user_uid", user.UID).
Msg("failed to delete user")
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)
if err != nil {
log.Err(err).
Int64("user_id", user.ID).
Str("user_email", user.Email).
Str("user_uid", user.UID).
Msg("Failed to hash password")
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)
if err != nil {
log.Err(err).
Int64("user_id", user.ID).
Str("user_email", user.Email).
Str("user_uid", user.UID).
Msg("Failed to hash password")
render.InternalError(w)
@ -78,8 +76,7 @@ func HandleUpdate(userStore store.UserStore) http.HandlerFunc {
}
if err := check.User(user); err != nil {
log.Debug().Err(err).
Int64("user_id", user.ID).
Str("user_email", user.Email).
Str("user_uid", user.UID).
Msg("invalid user input")
render.UserfiedErrorOrInternal(w, err)
@ -91,8 +88,7 @@ func HandleUpdate(userStore store.UserStore) http.HandlerFunc {
err := userStore.Update(ctx, user)
if err != nil {
log.Err(err).
Int64("user_id", user.ID).
Str("user_email", user.Email).
Str("user_uid", user.UID).
Msg("Failed to update the usser")
render.UserfiedErrorOrInternal(w, err)

View File

@ -18,8 +18,6 @@ func HlogHandler() func(http.Handler) http.Handler {
return hlog.AccessHandler(
func(r *http.Request, status, size int, duration time.Duration) {
hlog.FromRequest(r).Info().
Str("method", r.Method).
Stringer("url", r.URL).
Int("status_code", status).
Int("response_size_bytes", size).
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
log.UpdateContext(func(c zerolog.Context) zerolog.Context {
return c.
Int64("principal_id", session.Principal.ID).
Str("principal_uid", session.Principal.UID).
Str("principal_type", string(session.Principal.Type)).
Bool("principal_admin", session.Principal.Admin)
})

View File

@ -2,39 +2,42 @@ package encode
import (
"net/http"
"net/url"
"strings"
"github.com/rs/zerolog/log"
"github.com/rs/zerolog/hlog"
"github.com/harness/gitness/internal/request"
"github.com/harness/gitness/types"
)
// GitPathBefore wraps an http.HandlerFunc in a layer that encodes Paths coming as part of the GIT api
// (e.g. "space1/repo.git") before executing the provided http.HandlerFunc
const (
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.
func GitPathBefore(h http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
r, _ = pathTerminatedWithMarker(r, "", ".git", false)
h.ServeHTTP(w, r)
}
func GitPath(r *http.Request) error {
_, err := pathTerminatedWithMarker(r, "", ".git", false)
return err
}
// 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
// be used during encoding.
func TerminatedPathBefore(prefixes []string, h http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
for _, p := range prefixes {
// IMPORTANT: define changed separately to avoid overshadowing r
var changed bool
if r, changed = pathTerminatedWithMarker(r, p, "/+", false); changed {
break
}
// be used during encoding (prefix is ignored during encoding).
func TerminatedPath(prefixes []string, r *http.Request) error {
for _, p := range prefixes {
changed, err := pathTerminatedWithMarker(r, p, "/+", false)
if err != nil {
return err
}
h.ServeHTTP(w, r)
// first prefix that leads to success we can stop
if changed {
break
}
}
return nil
}
// 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.git" => "/space1%2Fspace2"
// 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
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):]
path, suffix, found := strings.Cut(originalSubPath, marker)
path, _, found := strings.Cut(originalSubPath, marker)
// If we don't find a marker - nothing to encode
if !found {
return r, false
return false, nil
}
// 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")
// if marker was found - convert to escaped version (skip first character in case path starts with '/').
// Since replacePrefix unescapes the strings, we have to double escape.
escapedPath := path[0:1] + strings.ReplaceAll(path[1:], types.PathSeparator, EncodedPathSeparator)
if keepMarker {
escapedPath += marker
}
updatedSubPath := escapedPath + suffix
// TODO: Proper Logging
log.Debug().Msgf(
"[Encode] prefix: '%s', marker: '%s', original: '%s', updated: '%s'.\n",
prefixWithPath := prefix + path + marker
prefixWithEscapedPath := prefix + escapedPath
hlog.FromRequest(r).Trace().Msgf(
"[Encode] prefix: '%s', marker: '%s', original: '%s', escaped: '%s'.\n",
prefix,
marker,
originalSubPath,
updatedSubPath)
prefixWithPath,
prefixWithEscapedPath)
/*
* Return shallow clone with updated URL, similar to http.StripPrefix or earlier version of request.WithContext
* 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
*/
r2 := new(http.Request)
*r2 = *r
r2.URL = new(url.URL)
*r2.URL = *r.URL
r2.URL.Path = prefix + updatedSubPath
r2.URL.RawPath = ""
err := request.ReplacePrefix(r, prefixWithPath, prefixWithEscapedPath)
if err != nil {
return false, err
}
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()
log := hlog.FromRequest(r)
id, err := request.GetServiceAccountID(r)
uid, err := request.GetServiceAccountUID(r)
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)
return
}
sa, err := saStore.Find(ctx, id)
sa, err := saStore.FindUID(ctx, uid)
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)
return
@ -43,7 +43,7 @@ func ServiceAccount(saStore store.ServiceAccountStore) func(http.Handler) http.H
// Update the logging context and inject repo in 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(

View File

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

View File

@ -25,7 +25,7 @@ func User(userStore store.UserStore) func(http.Handler) http.Handler {
ctx := r.Context()
log := hlog.FromRequest(r)
id, err := request.GetUserID(r)
uid, err := request.GetUserUID(r)
if err != nil {
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
}
user, err := userStore.Find(ctx, id)
user, err := userStore.FindUID(ctx, uid)
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)
return
@ -43,7 +43,7 @@ func User(userStore store.UserStore) func(http.Handler) http.Handler {
// Update the logging context and inject repo in 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(

View File

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

View File

@ -14,11 +14,22 @@ import (
"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.
func ParseAsInt64(r *http.Request, paramName string) (int64, error) {
rawID := chi.URLParam(r, paramName)
if rawID == "" {
return 0, fmt.Errorf("parameter '%s' not found in request", paramName)
rawID, err := ParamOrError(r, paramName)
if err != nil {
return 0, err
}
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(
userStore store.UserStore,
saStore store.ServiceAccountStore,
tokenStore store.TokenStore) Authenticator {
tokenStore store.TokenStore) *TokenAuthenticator {
return &TokenAuthenticator{
userStore: userStore,
saStore: saStore,

View File

@ -6,9 +6,15 @@ package authn
import (
"github.com/google/wire"
"github.com/harness/gitness/internal/store"
)
// WireSet provides a wire set for this package.
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{}
func NewUnsafeAuthorizer() Authorizer {
func NewUnsafeAuthorizer() *UnsafeAuthorizer {
return &UnsafeAuthorizer{}
}

View File

@ -10,5 +10,9 @@ import (
// WireSet provides a wire set for this package.
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"
"github.com/harness/gitness/internal/api/middleware/accesslog"
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/request"
@ -27,15 +26,12 @@ import (
"github.com/go-chi/chi/middleware"
"github.com/go-chi/cors"
"github.com/rs/zerolog/hlog"
"github.com/rs/zerolog/log"
)
/*
* Mounts the Rest API Router under mountPath (path has to end with ).
* The handler is wrapped within a layer that handles encoding terminated Paths.
* newAPIHandler returns a new http handler for handling API calls.
*/
func newAPIHandler(
mountPath string,
systemStore store.SystemStore,
userStore store.UserStore,
spaceStore store.SpaceStore,
@ -50,36 +46,28 @@ func newAPIHandler(
// Use go-chi router for inner routing (restricted to mountPath!)
r := chi.NewRouter()
r.Route(mountPath, func(r chi.Router) {
// Apply common api middleware
r.Use(middleware.NoCache)
r.Use(middleware.Recoverer)
// configure logging middleware.
r.Use(hlog.NewHandler(log.Logger))
r.Use(hlog.URLHandler("path"))
r.Use(hlog.MethodHandler("method"))
r.Use(hlog.RequestIDHandler("request", "Request-Id"))
r.Use(accesslog.HlogHandler())
// Apply common api middleware
r.Use(middleware.NoCache)
r.Use(middleware.Recoverer)
// configure cors middleware
r.Use(corsHandler(config))
// configure logging middleware.
r.Use(hlog.URLHandler("url"))
r.Use(hlog.MethodHandler("method"))
r.Use(hlog.RequestIDHandler("request", "Request-Id"))
r.Use(accesslog.HlogHandler())
// for now always attempt auth - enforced per operation
r.Use(middlewareauthn.Attempt(authenticator))
// configure cors middleware
r.Use(corsHandler(config))
r.Route("/v1", func(r chi.Router) {
setupRoutesV1(r, systemStore, userStore, spaceStore, repoStore, tokenStore, saStore, authenticator, g)
})
// for now always attempt auth - enforced per operation
r.Use(middlewareauthn.Attempt(authenticator))
r.Route("/v1", func(r chi.Router) {
setupRoutesV1(r, systemStore, userStore, spaceStore, repoStore, tokenStore, saStore, authenticator, g)
})
// Generate list of all path prefixes that expect terminated Paths
terminatedPathPrefixes := []string{
mountPath + "/v1/spaces",
mountPath + "/v1/repos",
}
return encode.TerminatedPathBefore(terminatedPathPrefixes, r.ServeHTTP)
return r
}
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
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
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"))))
})
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
resolve.User(userStore)

View File

@ -7,7 +7,6 @@ import (
"github.com/harness/gitness/internal/api/guard"
"github.com/harness/gitness/internal/api/middleware/accesslog"
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/request"
"github.com/harness/gitness/internal/auth/authn"
@ -18,15 +17,12 @@ import (
"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
"github.com/rs/zerolog/hlog"
"github.com/rs/zerolog/log"
)
/*
* Mounts the GIT Router under mountPath.
* The handler is wrapped within a layer that handles encoding Paths.
* newGitHandler returns a new http handler for handling GIT calls.
*/
func newGitHandler(
mountPath string,
_ store.SystemStore,
_ store.UserStore,
spaceStore store.SpaceStore,
@ -34,55 +30,53 @@ func newGitHandler(
authenticator authn.Authenticator,
authorizer authz.Authorizer) http.Handler {
guard := guard.New(authorizer, spaceStore, repoStore)
// Use go-chi router for inner routing (restricted to mountPath!)
r := chi.NewRouter()
r.Route(mountPath, func(r chi.Router) {
// Apply common api middleware
r.Use(middleware.NoCache)
r.Use(middleware.Recoverer)
// Apply common api middleware
r.Use(middleware.NoCache)
r.Use(middleware.Recoverer)
// configure logging middleware.
r.Use(hlog.NewHandler(log.Logger))
r.Use(hlog.URLHandler("path"))
r.Use(hlog.MethodHandler("method"))
r.Use(hlog.RequestIDHandler("request", "Request-Id"))
r.Use(accesslog.HlogHandler())
// configure logging middleware.
r.Use(hlog.URLHandler("url"))
r.Use(hlog.MethodHandler("method"))
r.Use(hlog.RequestIDHandler("request", "Request-Id"))
r.Use(accesslog.HlogHandler())
// for now always attempt auth - enforced per operation
r.Use(middleware_authn.Attempt(authenticator))
// for now always attempt auth - enforced per operation
r.Use(middleware_authn.Attempt(authenticator))
r.Route(fmt.Sprintf("/{%s}", request.RepoRefParamName), func(r chi.Router) {
// resolves the repo and stores in the context
r.Use(resolve.Repo(repoStore))
r.Route(fmt.Sprintf("/{%s}", request.RepoRefParamName), func(r chi.Router) {
// resolves the repo and stores in the context
r.Use(resolve.Repo(repoStore))
// Write operations (need auth)
r.Group(func(r chi.Router) {
// TODO: specific permission for pushing code?
r.Use(guard.ForRepo(enum.PermissionRepoEdit, false))
// Write operations (need auth)
r.Group(func(r chi.Router) {
// TODO: specific permission for pushing code?
r.Use(guard.ForRepo(enum.PermissionRepoEdit, false))
r.Handle("/git-upload-pack", http.HandlerFunc(stubGitHandler))
})
r.Handle("/git-upload-pack", http.HandlerFunc(stubGitHandler))
})
// Read operations (only need of it not public)
r.Group(func(r chi.Router) {
// middlewares
r.Use(guard.ForRepo(enum.PermissionRepoView, true))
// handlers
r.Post("/git-receive-pack", stubGitHandler)
r.Get("/info/refs", stubGitHandler)
r.Get("/HEAD", stubGitHandler)
r.Get("/objects/info/alternates", stubGitHandler)
r.Get("/objects/info/http-alternates", stubGitHandler)
r.Get("/objects/info/packs", stubGitHandler)
r.Get("/objects/info/{file:[^/]*}", stubGitHandler)
r.Get("/objects/{head:[0-9a-f]{2}}/{hash:[0-9a-f]{38}}", stubGitHandler)
r.Get("/objects/pack/pack-{file:[0-9a-f]{40}}.pack", stubGitHandler)
r.Get("/objects/pack/pack-{file:[0-9a-f]{40}}.idx", stubGitHandler)
})
// Read operations (only need of it not public)
r.Group(func(r chi.Router) {
// middlewares
r.Use(guard.ForRepo(enum.PermissionRepoView, true))
// handlers
r.Post("/git-receive-pack", stubGitHandler)
r.Get("/info/refs", stubGitHandler)
r.Get("/HEAD", stubGitHandler)
r.Get("/objects/info/alternates", stubGitHandler)
r.Get("/objects/info/http-alternates", stubGitHandler)
r.Get("/objects/info/packs", stubGitHandler)
r.Get("/objects/info/{file:[^/]*}", stubGitHandler)
r.Get("/objects/{head:[0-9a-f]{2}}/{hash:[0-9a-f]{38}}", stubGitHandler)
r.Get("/objects/pack/pack-{file:[0-9a-f]{40}}.pack", 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) {

View File

@ -10,25 +10,33 @@ import (
"net/http"
"strings"
"github.com/harness/gitness/internal/api/render"
"github.com/harness/gitness/internal/auth/authn"
"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/rs/zerolog"
"github.com/rs/zerolog/hlog"
"github.com/rs/zerolog/log"
)
const (
restMount = "/api"
APIMount = "/api"
gitUserAgentPrefix = "git/"
)
type Router struct {
api http.Handler
git http.Handler
web http.Handler
translator translator.RequestTranslator
api http.Handler
git 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.
func New(
func NewRouter(
translator translator.RequestTranslator,
systemStore store.SystemStore,
userStore store.UserStore,
spaceStore store.SpaceStore,
@ -37,20 +45,38 @@ func New(
saStore store.ServiceAccountStore,
authenticator authn.Authenticator,
authorizer authz.Authorizer,
) (http.Handler, error) {
api := newAPIHandler(restMount, systemStore, userStore, spaceStore, repoStore, tokenStore, saStore,
) (*Router, error) {
api := newAPIHandler(systemStore, userStore, spaceStore, repoStore, tokenStore, saStore,
authenticator, authorizer)
git := newGitHandler("/", systemStore, userStore, spaceStore, repoStore, authenticator, authorizer)
web := newWebHandler("/", systemStore)
git := newGitHandler(systemStore, userStore, spaceStore, repoStore, authenticator, authorizer)
web := newWebHandler(systemStore)
return &Router{
api: api,
git: git,
web: web,
translator: translator,
api: api,
git: git,
web: web,
}, nil
}
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
*
@ -60,6 +86,18 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
*/
ua := req.Header.Get("user-agent")
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)
return
}
@ -68,10 +106,28 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
* 2. REST API
*
* 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
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)
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)
*/
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)
}
// 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"
"net/http"
"github.com/harness/gitness/internal/api/middleware/encode"
"github.com/harness/gitness/internal/store"
"github.com/harness/gitness/web"
"github.com/swaggest/swgui/v3emb"
@ -14,53 +13,46 @@ import (
)
/*
* Mounts the WEB Router under mountPath.
* The handler is wrapped within a layer that handles encoding Paths.
* newWebHandler returns a new http handler for handling WEB calls.
*/
func newWebHandler(
mountPath string,
systemStore store.SystemStore) http.Handler {
//
func newWebHandler(systemStore store.SystemStore) http.Handler {
config := systemStore.Config(context.Background())
// Use go-chi router for inner routing (restricted to mountPath!)
r := chi.NewRouter()
r.Route(mountPath, func(r chi.Router) {
// create middleware to enforce security best practices for
// the user interface. note that theis middleware is only used
// when serving the user interface (not found handler, below).
sec := secure.New(
secure.Options{
AllowedHosts: config.Secure.AllowedHosts,
HostsProxyHeaders: config.Secure.HostsProxyHeaders,
SSLRedirect: config.Secure.SSLRedirect,
SSLTemporaryRedirect: config.Secure.SSLTemporaryRedirect,
SSLHost: config.Secure.SSLHost,
SSLProxyHeaders: config.Secure.SSLProxyHeaders,
STSSeconds: config.Secure.STSSeconds,
STSIncludeSubdomains: config.Secure.STSIncludeSubdomains,
STSPreload: config.Secure.STSPreload,
ForceSTSHeader: config.Secure.ForceSTSHeader,
FrameDeny: config.Secure.FrameDeny,
ContentTypeNosniff: config.Secure.ContentTypeNosniff,
BrowserXssFilter: config.Secure.BrowserXSSFilter,
ContentSecurityPolicy: config.Secure.ContentSecurityPolicy,
ReferrerPolicy: config.Secure.ReferrerPolicy,
},
)
// create middleware to enforce security best practices for
// the user interface. note that theis middleware is only used
// when serving the user interface (not found handler, below).
sec := secure.New(
secure.Options{
AllowedHosts: config.Secure.AllowedHosts,
HostsProxyHeaders: config.Secure.HostsProxyHeaders,
SSLRedirect: config.Secure.SSLRedirect,
SSLTemporaryRedirect: config.Secure.SSLTemporaryRedirect,
SSLHost: config.Secure.SSLHost,
SSLProxyHeaders: config.Secure.SSLProxyHeaders,
STSSeconds: config.Secure.STSSeconds,
STSIncludeSubdomains: config.Secure.STSIncludeSubdomains,
STSPreload: config.Secure.STSPreload,
ForceSTSHeader: config.Secure.ForceSTSHeader,
FrameDeny: config.Secure.FrameDeny,
ContentTypeNosniff: config.Secure.ContentTypeNosniff,
BrowserXssFilter: config.Secure.BrowserXSSFilter,
ContentSecurityPolicy: config.Secure.ContentSecurityPolicy,
ReferrerPolicy: config.Secure.ReferrerPolicy,
},
)
// openapi playground endpoints
swagger := v3emb.NewHandler("API Definition", "/api/v1/swagger.yaml", "/swagger")
r.With(sec.Handler).Handle("/swagger", swagger)
r.With(sec.Handler).Handle("/swagger/*", swagger)
// openapi playground endpoints
swagger := v3emb.NewHandler("API Definition", "/api/v1/swagger.yaml", "/swagger")
r.With(sec.Handler).Handle("/swagger", swagger)
r.With(sec.Handler).Handle("/swagger/*", swagger)
// serve all other routes from the embedded filesystem,
// which in turn serves the user interface.
r.With(sec.Handler).NotFound(
web.Handler(),
)
})
// serve all other routes from the embedded filesystem,
// which in turn serves the user interface.
r.With(sec.Handler).NotFound(
web.Handler(),
)
// web doesn't have any prefixes for terminated paths
return encode.TerminatedPathBefore([]string{""}, r.ServeHTTP)
return r
}

View File

@ -5,8 +5,29 @@
package router
import (
"net/http"
"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.
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 (
principal_id SERIAL PRIMARY KEY
,principal_uid TEXT
,principal_type TEXT
,principal_name TEXT
,principal_admin BOOLEAN
,principal_externalId TEXT
,principal_blocked BOOLEAN
,principal_salt TEXT
,principal_created INTEGER
@ -15,6 +15,7 @@ principal_id SERIAL PRIMARY KEY
,principal_sa_parentType TEXT
,principal_sa_parentId INTEGER
,UNIQUE(principal_uid)
,UNIQUE(principal_salt)
,UNIQUE(principal_user_email)
);

View File

@ -1,9 +1,9 @@
CREATE TABLE IF NOT EXISTS principals (
principal_id INTEGER PRIMARY KEY AUTOINCREMENT
,principal_uid TEXT
,principal_type TEXT
,principal_name TEXT
,principal_admin BOOLEAN
,principal_externalId TEXT
,principal_blocked BOOLEAN
,principal_salt TEXT
,principal_created INTEGER
@ -15,6 +15,7 @@ principal_id INTEGER PRIMARY KEY AUTOINCREMENT
,principal_sa_parentType TEXT
,principal_sa_parentId INTEGER
,UNIQUE(principal_uid)
,UNIQUE(principal_salt)
,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
}
// 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.
func (s *ServiceAccountStore) Create(ctx context.Context, sa *types.ServiceAccount) error {
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 = `
SELECT
principal_id
,principal_uid
,principal_name
,principal_externalId
,principal_blocked
,principal_salt
,principal_created
@ -133,6 +142,10 @@ const serviceAccountSelectID = serviceAccountBase + `
WHERE principal_type = "serviceaccount" AND principal_id = $1
`
const serviceAccountSelectUID = serviceAccountBase + `
WHERE principal_type = "serviceaccount" AND principal_uid = $1
`
const serviceAccountDelete = `
DELETE FROM principals
WHERE principal_type = "serviceaccount" AND principal_id = $1
@ -141,9 +154,9 @@ WHERE principal_type = "serviceaccount" AND principal_id = $1
const serviceAccountInsert = `
INSERT INTO principals (
principal_type
,principal_uid
,principal_name
,principal_admin
,principal_externalId
,principal_blocked
,principal_salt
,principal_created
@ -152,9 +165,9 @@ principal_type
,principal_sa_parentId
) values (
"serviceaccount"
,:principal_uid
,:principal_name
,false
,:principal_externalId
,:principal_blocked
,:principal_salt
,:principal_created
@ -168,7 +181,6 @@ const serviceAccountUpdate = `
UPDATE principals
SET
principal_name = :principal_name
,:principal_externalId = :principal_externalId
,:principal_blocked = :principal_blocked
,:principal_salt = :principal_salt
,: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)
}
// 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.
func (s *ServiceAccountStoreSync) Create(ctx context.Context, sa *types.ServiceAccount) error {
mutex.RLock()

View File

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

View File

@ -7,7 +7,6 @@ package database
import (
"context"
"database/sql"
"strconv"
"github.com/harness/gitness/internal/store"
"github.com/harness/gitness/types"
@ -39,6 +38,15 @@ func (s *UserStore) Find(ctx context.Context, id int64) (*types.User, error) {
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.
func (s *UserStore) FindEmail(ctx context.Context, email string) (*types.User, error) {
dst := new(types.User)
@ -48,15 +56,6 @@ func (s *UserStore) FindEmail(ctx context.Context, email string) (*types.User, e
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.
func (s *UserStore) List(ctx context.Context, opts *types.UserFilter) ([]*types.User, error) {
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())
case enum.UserAttrEmail:
stmt = stmt.OrderBy("principal_user_email " + opts.Order.String())
case enum.UserAttrID:
stmt = stmt.OrderBy("principal_id " + opts.Order.String())
case enum.UserAttrUID:
stmt = stmt.OrderBy("principal_uid " + opts.Order.String())
case enum.UserAttrAdmin:
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.
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)
if err != nil {
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)
// 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 tx.Commit()
@ -169,9 +168,9 @@ WHERE principal_type = "user"
const userBase = `
SELECT
principal_id
,principal_uid
,principal_name
,principal_admin
,principal_externalId
,principal_blocked
,principal_salt
,principal_created
@ -191,6 +190,10 @@ const userSelectID = userBase + `
WHERE principal_type = "user" AND principal_id = $1
`
const userSelectUID = userBase + `
WHERE principal_type = "user" AND principal_uid = $1
`
const userSelectEmail = userBase + `
WHERE principal_type = "user" AND principal_user_email = $1
`
@ -203,9 +206,9 @@ WHERE principal_type = "user" AND principal_id = $1
const userInsert = `
INSERT INTO principals (
principal_type
,principal_uid
,principal_name
,principal_admin
,principal_externalId
,principal_blocked
,principal_salt
,principal_created
@ -214,9 +217,9 @@ principal_type
,principal_user_password
) values (
"user"
,:principal_uid
,:principal_name
,:principal_admin
,:principal_externalId
,:principal_blocked
,:principal_salt
,:principal_created
@ -231,7 +234,6 @@ UPDATE principals
SET
principal_name = :principal_name
,principal_admin = :principal_admin
,principal_externalId = :principal_externalId
,principal_blocked = :principal_blocked
,principal_salt = :principal_salt
,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)
}
// 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.
func (s *UserStoreSync) FindEmail(ctx context.Context, email string) (*types.User, error) {
mutex.RLock()
@ -38,13 +45,6 @@ func (s *UserStoreSync) FindEmail(ctx context.Context, email string) (*types.Use
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.
func (s *UserStoreSync) List(ctx context.Context, opts *types.UserFilter) ([]*types.User, error) {
mutex.RLock()
@ -67,10 +67,10 @@ func (s *UserStoreSync) Update(ctx context.Context, user *types.User) error {
}
// 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()
defer mutex.Unlock()
return s.base.Delete(ctx, user)
return s.base.Delete(ctx, id)
}
// 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) {
got, err := store.FindEmail(ctx, want.Email)
if err != nil {
@ -164,30 +176,6 @@ func testUserFind(store store.UserStore) func(t *testing.T) {
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) {
return func(t *testing.T) {
ctx := context.Background()
v, err := s.Find(ctx, 1)
_, err := s.Find(ctx, 1)
if err != nil {
t.Error(err)
return
}
if err = s.Delete(ctx, v); err != nil {
if err = s.Delete(ctx, 1); err != nil {
t.Error(err)
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.
func processSQLErrorf(err error, format string, args ...interface{}) error {
// 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 errors.Is(err, sql.ErrNoRows) {

View File

@ -18,12 +18,12 @@ type (
// Find finds the user by id.
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(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(ctx context.Context, user *types.User) error
@ -31,7 +31,7 @@ type (
Update(ctx context.Context, user *types.User) error
// 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(ctx context.Context, params *types.UserFilter) ([]*types.User, error)
@ -45,6 +45,9 @@ type (
// Find finds the service account by id.
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(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.
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()
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)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// 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()
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.

View File

@ -102,7 +102,7 @@ func (mr *MockUserStoreMockRecorder) Create(arg0, arg1 interface{}) *gomock.Call
}
// 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()
ret := m.ctrl.Call(m, "Delete", arg0, arg1)
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)
}
// FindKey mocks base method.
func (m *MockUserStore) FindKey(arg0 context.Context, arg1 string) (*types.User, error) {
// FindUID mocks base method.
func (m *MockUserStore) FindUID(arg0 context.Context, arg1 string) (*types.User, error) {
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)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// FindKey indicates an expected call of FindKey.
func (mr *MockUserStoreMockRecorder) FindKey(arg0, arg1 interface{}) *gomock.Call {
// FindUID indicates an expected call of FindUID.
func (mr *MockUserStoreMockRecorder) FindUID(arg0, arg1 interface{}) *gomock.Call {
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.

View File

@ -17,20 +17,32 @@ const (
minNameLength = 1
maxNameLength = 256
nameRegex = "^[a-zA-Z][a-zA-Z0-9\\-\\_ ]*$"
minUIDLength = 2
maxUIDLength = 64
uidRegex = "^[a-z][a-z0-9\\-\\_]*$"
)
var (
ErrPathNameLength = &ValidationError{
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{
fmt.Sprintf("Name has to be between %d and %d in length.",
minNameLength, maxNameLength),
}
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
}
// 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.
func ServiceAccount(sa *types.ServiceAccount) error {
// validate UID
if err := UID(sa.UID); err != nil {
return err
}
// verify name
if err := Name(sa.Name); err != nil {
return err

View File

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

View File

@ -8,60 +8,60 @@ import "time"
// Config stores the system configuration.
type Config struct {
Debug bool `envconfig:"APP_DEBUG"`
Trace bool `envconfig:"APP_TRACE"`
Debug bool `envconfig:"GITNESS_DEBUG"`
Trace bool `envconfig:"GITNESS_TRACE"`
// Server defines the server configuration parameters.
Server struct {
Bind string `envconfig:"APP_HTTP_BIND" default:":3000"`
Proto string `envconfig:"APP_HTTP_PROTO"`
Host string `envconfig:"APP_HTTP_HOST"`
Bind string `envconfig:"GITNESS_HTTP_BIND" default:":3000"`
Proto string `envconfig:"GITNESS_HTTP_PROTO"`
Host string `envconfig:"GITNESS_HTTP_HOST"`
// Acme defines Acme configuration parameters.
Acme struct {
Enabled bool `envconfig:"APP_ACME_ENABLED"`
Endpont string `envconfig:"APP_ACME_ENDPOINT"`
Email bool `envconfig:"APP_ACME_EMAIL"`
Enabled bool `envconfig:"GITNESS_ACME_ENABLED"`
Endpont string `envconfig:"GITNESS_ACME_ENDPOINT"`
Email bool `envconfig:"GITNESS_ACME_EMAIL"`
}
}
// Database defines the database configuration parameters.
Database struct {
Driver string `envconfig:"APP_DATABASE_DRIVER" default:"sqlite3"`
Datasource string `envconfig:"APP_DATABASE_DATASOURCE" default:"database.sqlite3"`
Driver string `envconfig:"GITNESS_DATABASE_DRIVER" default:"sqlite3"`
Datasource string `envconfig:"GITNESS_DATABASE_DATASOURCE" default:"database.sqlite3"`
}
// Token defines token configuration parameters.
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 struct {
AllowedOrigins []string `envconfig:"APP_CORS_ALLOWED_ORIGINS" default:"*"`
AllowedMethods []string `envconfig:"APP_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
ExposedHeaders []string `envconfig:"APP_CORS_EXPOSED_HEADERS" default:"Link"`
AllowCredentials bool `envconfig:"APP_CORS_ALLOW_CREDENTIALS" default:"true"`
MaxAge int `envconfig:"APP_CORS_MAX_AGE" default:"300"`
AllowedOrigins []string `envconfig:"GITNESS_CORS_ALLOWED_ORIGINS" default:"*"`
AllowedMethods []string `envconfig:"GITNESS_CORS_ALLOWED_METHODS" default:"GET,POST,PATCH,PUT,DELETE,OPTIONS"`
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:"GITNESS_CORS_EXPOSED_HEADERS" default:"Link"`
AllowCredentials bool `envconfig:"GITNESS_CORS_ALLOW_CREDENTIALS" default:"true"`
MaxAge int `envconfig:"GITNESS_CORS_MAX_AGE" default:"300"`
}
// Secure defines http security parameters.
Secure struct {
AllowedHosts []string `envconfig:"APP_HTTP_ALLOWED_HOSTS"`
HostsProxyHeaders []string `envconfig:"APP_HTTP_PROXY_HEADERS"`
SSLRedirect bool `envconfig:"APP_HTTP_SSL_REDIRECT"`
SSLTemporaryRedirect bool `envconfig:"APP_HTTP_SSL_TEMPORARY_REDIRECT"`
SSLHost string `envconfig:"APP_HTTP_SSL_HOST"`
SSLProxyHeaders map[string]string `envconfig:"APP_HTTP_SSL_PROXY_HEADERS"`
STSSeconds int64 `envconfig:"APP_HTTP_STS_SECONDS"`
STSIncludeSubdomains bool `envconfig:"APP_HTTP_STS_INCLUDE_SUBDOMAINS"`
STSPreload bool `envconfig:"APP_HTTP_STS_PRELOAD"`
ForceSTSHeader bool `envconfig:"APP_HTTP_STS_FORCE_HEADER"`
BrowserXSSFilter bool `envconfig:"APP_HTTP_BROWSER_XSS_FILTER" default:"true"`
FrameDeny bool `envconfig:"APP_HTTP_FRAME_DENY" default:"true"`
ContentTypeNosniff bool `envconfig:"APP_HTTP_CONTENT_TYPE_NO_SNIFF"`
ContentSecurityPolicy string `envconfig:"APP_HTTP_CONTENT_SECURITY_POLICY"`
ReferrerPolicy string `envconfig:"APP_HTTP_REFERRER_POLICY"`
AllowedHosts []string `envconfig:"GITNESS_HTTP_ALLOWED_HOSTS"`
HostsProxyHeaders []string `envconfig:"GITNESS_HTTP_PROXY_HEADERS"`
SSLRedirect bool `envconfig:"GITNESS_HTTP_SSL_REDIRECT"`
SSLTemporaryRedirect bool `envconfig:"GITNESS_HTTP_SSL_TEMPORARY_REDIRECT"`
SSLHost string `envconfig:"GITNESS_HTTP_SSL_HOST"`
SSLProxyHeaders map[string]string `envconfig:"GITNESS_HTTP_SSL_PROXY_HEADERS"`
STSSeconds int64 `envconfig:"GITNESS_HTTP_STS_SECONDS"`
STSIncludeSubdomains bool `envconfig:"GITNESS_HTTP_STS_INCLUDE_SUBDOMAINS"`
STSPreload bool `envconfig:"GITNESS_HTTP_STS_PRELOAD"`
ForceSTSHeader bool `envconfig:"GITNESS_HTTP_STS_FORCE_HEADER"`
BrowserXSSFilter bool `envconfig:"GITNESS_HTTP_BROWSER_XSS_FILTER" default:"true"`
FrameDeny bool `envconfig:"GITNESS_HTTP_FRAME_DENY" default:"true"`
ContentTypeNosniff bool `envconfig:"GITNESS_HTTP_CONTENT_TYPE_NO_SNIFF"`
ContentSecurityPolicy string `envconfig:"GITNESS_HTTP_CONTENT_SECURITY_POLICY"`
ReferrerPolicy string `envconfig:"GITNESS_HTTP_REFERRER_POLICY"`
}
}

View File

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

View File

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

View File

@ -10,15 +10,16 @@ import "github.com/harness/gitness/types/enum"
type (
// Represents the identity of an acting entity (User, ServiceAccount, Service).
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"`
Name string `db:"principal_name" json:"name"`
Admin bool `db:"principal_admin" json:"admin"`
// Should be part of principal or not?
ExternalID string `db:"principal_externalId" json:"externalId"`
Blocked bool `db:"principal_blocked" json:"blocked"`
Salt string `db:"principal_salt" json:"-"`
Blocked bool `db:"principal_blocked" json:"blocked"`
Salt string `db:"principal_salt" json:"-"`
// Other info
Created int64 `db:"principal_created" json:"created"`
@ -28,28 +29,42 @@ type (
func PrincipalFromUser(user *User) *Principal {
return &Principal{
ID: user.ID,
Type: enum.PrincipalTypeUser,
Name: user.Name,
Admin: user.Admin,
ExternalID: user.ExternalID,
Blocked: user.Blocked,
Salt: user.Salt,
Created: user.Created,
Updated: user.Updated,
ID: user.ID,
UID: user.UID,
Type: enum.PrincipalTypeUser,
Name: user.Name,
Admin: user.Admin,
Blocked: user.Blocked,
Salt: user.Salt,
Created: user.Created,
Updated: user.Updated,
}
}
func PrincipalFromServiceAccount(sa *ServiceAccount) *Principal {
return &Principal{
ID: sa.ID,
Type: enum.PrincipalTypeServiceAccount,
Name: sa.Name,
Admin: false,
ExternalID: sa.ExternalID,
Blocked: sa.Blocked,
Salt: sa.Salt,
Created: sa.Created,
Updated: sa.Updated,
ID: sa.ID,
UID: sa.UID,
Type: enum.PrincipalTypeServiceAccount,
Name: sa.Name,
Admin: false,
Blocked: sa.Blocked,
Salt: sa.Salt,
Created: sa.Created,
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,15 +10,15 @@ import "github.com/harness/gitness/types/enum"
type (
// Service is a principal representing a different internal service that runs alongside gitness.
Service struct {
// Fields from Principal (without admin)
ID int64 `db:"principal_id" json:"id"`
Name string `db:"principal_name" json:"name"`
Admin bool `db:"principal_admin" json:"admin"`
ExternalID string `db:"principal_externalId" json:"externalId"`
Blocked bool `db:"principal_blocked" json:"blocked"`
Salt string `db:"principal_salt" json:"-"`
Created int64 `db:"principal_created" json:"created"`
Updated int64 `db:"principal_updated" json:"updated"`
// Fields from Principal (without admin, as it's always admin for now)
ID int64 `db:"principal_id" json:"-"`
UID string `db:"principal_uid" json:"uid"`
Name string `db:"principal_name" json:"name"`
Admin bool `db:"principal_admin" json:"admin"`
Blocked bool `db:"principal_blocked" json:"blocked"`
Salt string `db:"principal_salt" json:"-"`
Created int64 `db:"principal_created" json:"created"`
Updated int64 `db:"principal_updated" json:"updated"`
}
// ServiceAccountInput store details used to

View File

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

View File

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