2470 lines
84 KiB
Go
2470 lines
84 KiB
Go
package codit
|
|
|
|
//import "io"
|
|
import "context"
|
|
import "crypto/sha256"
|
|
import "crypto/tls"
|
|
import "crypto/x509"
|
|
import "database/sql"
|
|
import "encoding/hex"
|
|
import "encoding/pem"
|
|
import "errors"
|
|
import "fmt"
|
|
import "io"
|
|
import "log"
|
|
import "net"
|
|
import "net/http"
|
|
import "net/netip"
|
|
import "net/url"
|
|
import "os"
|
|
import "path/filepath"
|
|
import "sync"
|
|
import "strings"
|
|
import "time"
|
|
|
|
import "codit/internal/auth"
|
|
import codit_config "codit/config"
|
|
import "codit/internal/db"
|
|
import "codit/internal/docker"
|
|
import "codit/internal/git"
|
|
import "codit/internal/handlers"
|
|
import codit_http "codit/internal/http"
|
|
import "codit/internal/middleware"
|
|
import "codit/internal/models"
|
|
import "codit/internal/pkiutil"
|
|
import "codit/internal/rpm"
|
|
import "codit/internal/storage"
|
|
import "codit/internal/util"
|
|
|
|
import "golang.org/x/text/transform"
|
|
import _ "modernc.org/sqlite"
|
|
|
|
const logIDAPI string = "api"
|
|
|
|
type gitPathRewriteHandler struct {
|
|
next http.Handler
|
|
store *db.Store
|
|
}
|
|
|
|
type server_http_log_writer struct {
|
|
l Logger
|
|
id string
|
|
depth int
|
|
}
|
|
|
|
func (hlw *server_http_log_writer) Write(p []byte) (n int, err error) {
|
|
// the standard http.Server always requires *log.Logger
|
|
// use this iowriter to create a logger to pass it to the http server.
|
|
// since this is another log write wrapper, give adjustment value
|
|
hlw.l.WriteWithCallDepth(hlw.id, LOG_INFO, hlw.depth, string(p))
|
|
return len(p), nil
|
|
}
|
|
|
|
func (h *gitPathRewriteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
var path string
|
|
var parts []string
|
|
var slug string
|
|
var repoSegment string
|
|
var rest string
|
|
var repoName string
|
|
var orgPath string
|
|
var currentStore *db.Store
|
|
var project models.Project
|
|
var repo models.Repo
|
|
var projectStorageID int64
|
|
var repoStorageID int64
|
|
var err error
|
|
var newPath string
|
|
|
|
currentStore = requestStore(r, h.store)
|
|
path = strings.TrimPrefix(r.URL.Path, "/")
|
|
if path == "" {
|
|
h.next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
parts = strings.SplitN(path, "/", 3)
|
|
if len(parts) < 2 {
|
|
h.next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
slug = parts[0]
|
|
repoSegment = parts[1]
|
|
if len(parts) == 3 {
|
|
rest = "/" + parts[2]
|
|
} else {
|
|
rest = ""
|
|
}
|
|
orgPath = r.URL.Path
|
|
repoName = strings.TrimSuffix(repoSegment, ".git")
|
|
project, err = currentStore.GetProjectBySlug(slug)
|
|
if err != nil {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
repo, err = currentStore.GetRepoByProjectNameType(project.ID, repoName, "git")
|
|
if err != nil {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
projectStorageID, repoStorageID, err = currentStore.GetRepoStorageIDs(repo.ID)
|
|
if err != nil {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
newPath = "/" + storageIDSegment(projectStorageID) + "/" + storageIDSegment(repoStorageID) + ".git" + rest
|
|
r = git.WithRequestLogInfo(r, git.RequestLogInfo{
|
|
OrgPath: orgPath,
|
|
ProjectID: project.ID,
|
|
ProjectSlug: project.Slug,
|
|
RepoID: repo.ID,
|
|
RepoName: repo.Name,
|
|
})
|
|
r.URL.Path = newPath
|
|
h.next.ServeHTTP(w, r)
|
|
}
|
|
|
|
type gitIDPathRewriteHandler struct {
|
|
next http.Handler
|
|
store *db.Store
|
|
}
|
|
|
|
func (h *gitIDPathRewriteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
var path string
|
|
var parts []string
|
|
var rest string
|
|
var orgPath string
|
|
var currentStore *db.Store
|
|
var project models.Project
|
|
var repo models.Repo
|
|
var projectStorageID int64
|
|
var repoStorageID int64
|
|
var projectErr error
|
|
var err error
|
|
var newPath string
|
|
var repoID string
|
|
currentStore = requestStore(r, h.store)
|
|
path = strings.TrimPrefix(r.URL.Path, "/")
|
|
if path == "" {
|
|
h.next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
parts = strings.SplitN(path, "/", 3)
|
|
repoID = strings.TrimSuffix(parts[0], ".git")
|
|
orgPath = r.URL.Path
|
|
repo, err = currentStore.GetRepo(repoID)
|
|
if err != nil || repo.Type != "git" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
project, projectErr = currentStore.GetProject(repo.ProjectID)
|
|
projectStorageID, repoStorageID, err = currentStore.GetRepoStorageIDs(repo.ID)
|
|
if err != nil {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
if len(parts) == 3 {
|
|
rest = "/" + parts[1] + "/" + parts[2]
|
|
} else if len(parts) == 2 {
|
|
rest = "/" + parts[1]
|
|
} else {
|
|
rest = ""
|
|
}
|
|
newPath = "/" + storageIDSegment(projectStorageID) + "/" + storageIDSegment(repoStorageID) + ".git" + rest
|
|
if projectErr != nil {
|
|
project.Slug = ""
|
|
}
|
|
r = git.WithRequestLogInfo(r, git.RequestLogInfo{
|
|
OrgPath: orgPath,
|
|
ProjectID: repo.ProjectID,
|
|
ProjectSlug: project.Slug,
|
|
RepoID: repo.ID,
|
|
RepoName: repo.Name,
|
|
})
|
|
r.URL.Path = newPath
|
|
h.next.ServeHTTP(w, r)
|
|
}
|
|
|
|
type rpmPathRewriteHandler struct {
|
|
next http.Handler
|
|
store *db.Store
|
|
}
|
|
|
|
func (h *rpmPathRewriteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
var path string
|
|
var parts []string
|
|
var slug string
|
|
var repoName string
|
|
var rest string
|
|
var orgPath string
|
|
var currentStore *db.Store
|
|
var project models.Project
|
|
var repo models.Repo
|
|
var projectStorageID int64
|
|
var repoStorageID int64
|
|
var err error
|
|
var newPath string
|
|
currentStore = requestStore(r, h.store)
|
|
path = strings.TrimPrefix(r.URL.Path, "/")
|
|
if path == "" {
|
|
h.next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
parts = strings.SplitN(path, "/", 3)
|
|
if len(parts) < 2 {
|
|
h.next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
slug = parts[0]
|
|
repoName = parts[1]
|
|
if len(parts) == 3 {
|
|
rest = "/" + parts[2]
|
|
} else {
|
|
rest = ""
|
|
}
|
|
orgPath = r.URL.Path
|
|
project, err = currentStore.GetProjectBySlug(slug)
|
|
if err != nil {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
repo, err = currentStore.GetRepoByProjectNameType(project.ID, repoName, "rpm")
|
|
if err != nil {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
projectStorageID, repoStorageID, err = currentStore.GetRepoStorageIDs(repo.ID)
|
|
if err != nil {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
newPath = "/" + storageIDSegment(projectStorageID) + "/" + storageIDSegment(repoStorageID) + rest
|
|
r = rpm.WithRequestLogInfo(r, rpm.RequestLogInfo{
|
|
OrgPath: orgPath,
|
|
ProjectID: project.ID,
|
|
ProjectSlug: project.Slug,
|
|
RepoID: repo.ID,
|
|
RepoName: repo.Name,
|
|
})
|
|
r.URL.Path = newPath
|
|
h.next.ServeHTTP(w, r)
|
|
}
|
|
|
|
type rpmIDPathRewriteHandler struct {
|
|
next http.Handler
|
|
store *db.Store
|
|
}
|
|
|
|
func (h *rpmIDPathRewriteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
var path string
|
|
var parts []string
|
|
var rest string
|
|
var orgPath string
|
|
var currentStore *db.Store
|
|
var project models.Project
|
|
var repo models.Repo
|
|
var projectStorageID int64
|
|
var repoStorageID int64
|
|
var projectErr error
|
|
var err error
|
|
var newPath string
|
|
var repoID string
|
|
currentStore = requestStore(r, h.store)
|
|
path = strings.TrimPrefix(r.URL.Path, "/")
|
|
if path == "" {
|
|
h.next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
parts = strings.SplitN(path, "/", 3)
|
|
repoID = parts[0]
|
|
orgPath = r.URL.Path
|
|
repo, err = currentStore.GetRepo(repoID)
|
|
if err != nil || repo.Type != "rpm" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
project, projectErr = currentStore.GetProject(repo.ProjectID)
|
|
projectStorageID, repoStorageID, err = currentStore.GetRepoStorageIDs(repo.ID)
|
|
if err != nil {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
if len(parts) == 3 {
|
|
rest = "/" + parts[1] + "/" + parts[2]
|
|
} else if len(parts) == 2 {
|
|
rest = "/" + parts[1]
|
|
} else {
|
|
rest = ""
|
|
}
|
|
newPath = "/" + storageIDSegment(projectStorageID) + "/" + storageIDSegment(repoStorageID) + rest
|
|
if projectErr != nil {
|
|
project.Slug = ""
|
|
}
|
|
r = rpm.WithRequestLogInfo(r, rpm.RequestLogInfo{
|
|
OrgPath: orgPath,
|
|
ProjectID: repo.ProjectID,
|
|
ProjectSlug: project.Slug,
|
|
RepoID: repo.ID,
|
|
RepoName: repo.Name,
|
|
})
|
|
r.URL.Path = newPath
|
|
h.next.ServeHTTP(w, r)
|
|
}
|
|
|
|
func storageIDSegment(id int64) string {
|
|
return fmt.Sprintf("%016x", id)
|
|
}
|
|
|
|
type Server struct {
|
|
cfg *codit_config.Config
|
|
logger Logger
|
|
serverId string
|
|
siteName string
|
|
store *db.Store
|
|
|
|
docker_base_dir string
|
|
git_base_dir string
|
|
rpm_base_dir string
|
|
uploads_base_dir string
|
|
|
|
git_manager *git.RepoManager
|
|
rpm_meta *rpm.MetaManager
|
|
rpm_mirror *rpm.MirrorManager
|
|
upload_store *storage.FileStore
|
|
|
|
docker_server http.Handler
|
|
git_server http.Handler
|
|
rpm_server http.Handler
|
|
}
|
|
|
|
func NewServer(cfg *Config, logger Logger) (*Server, error) {
|
|
return NewServerWithId(cfg, logger, "codit")
|
|
}
|
|
|
|
func NewServerWithId(cfg *Config, logger Logger, identifier string) (*Server, error) {
|
|
if cfg == nil {
|
|
return nil, errors.New("config is required")
|
|
}
|
|
if logger == nil {
|
|
return nil, errors.New("logger is required")
|
|
}
|
|
|
|
return newServerWithInternalConfig(cfg, logger, identifier)
|
|
}
|
|
|
|
func auth_user(store *db.Store, username string, password string) (bool, error) {
|
|
var user models.User
|
|
var principal models.ServicePrincipal
|
|
var hash string
|
|
var err error
|
|
var key string
|
|
|
|
user, hash, err = store.GetUserByUsername(username)
|
|
if err != nil || hash == "" {
|
|
if password != "" {
|
|
key = password
|
|
user, err = store.GetUserByAPIKeyHash(util.HashToken(key))
|
|
if err == nil {
|
|
return true, nil
|
|
}
|
|
principal, err = store.GetPrincipalByAPIKeyHash(util.HashToken(key))
|
|
if err == nil {
|
|
return !principal.Disabled, nil
|
|
}
|
|
}
|
|
if password == "" && username != "" {
|
|
key = username
|
|
user, err = store.GetUserByAPIKeyHash(util.HashToken(key))
|
|
if err == nil {
|
|
return true, nil
|
|
}
|
|
principal, err = store.GetPrincipalByAPIKeyHash(util.HashToken(key))
|
|
if err == nil {
|
|
return !principal.Disabled, nil
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
err = auth.ComparePassword(hash, password)
|
|
if err != nil {
|
|
if password != "" {
|
|
key = password
|
|
user, err = store.GetUserByAPIKeyHash(util.HashToken(key))
|
|
if err == nil {
|
|
return true, nil
|
|
}
|
|
principal, err = store.GetPrincipalByAPIKeyHash(util.HashToken(key))
|
|
if err == nil {
|
|
return !principal.Disabled, nil
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
if user.Disabled {
|
|
return false, nil
|
|
}
|
|
return user.ID != "", nil
|
|
}
|
|
|
|
func newServerWithInternalConfig(cfg *codit_config.Config, logger Logger, identifier string) (*Server, error) {
|
|
var app Server
|
|
var dir string
|
|
var err error
|
|
|
|
cfg.DockerHTTPPrefix = codit_config.NormalizeHTTPPrefix(cfg.DockerHTTPPrefix, "/v2")
|
|
cfg.GitHTTPPrefix = codit_config.NormalizeHTTPPrefix(cfg.GitHTTPPrefix, "/git")
|
|
cfg.RPMHTTPPrefix = codit_config.NormalizeHTTPPrefix(cfg.RPMHTTPPrefix, "/rpm")
|
|
|
|
app.cfg = cfg
|
|
app.logger = logger
|
|
app.serverId = NormalizeServerId(identifier)
|
|
app.siteName = strings.TrimSpace(cfg.App.SiteName)
|
|
if app.siteName == "" {
|
|
// get the host from the public base url if set.
|
|
var tmp string
|
|
tmp = strings.TrimSpace(cfg.PublicBaseURL)
|
|
if tmp != "" {
|
|
var parsed *url.URL
|
|
parsed, err = url.Parse(tmp)
|
|
if err == nil && parsed != nil {
|
|
app.siteName = strings.TrimSpace(parsed.Host)
|
|
}
|
|
}
|
|
if app.siteName == "" { app.siteName = TitleFromServerId(app.serverId) }
|
|
}
|
|
|
|
app.docker_base_dir = filepath.Join(cfg.DataDir, "docker")
|
|
app.git_base_dir = filepath.Join(cfg.DataDir, "git")
|
|
app.rpm_base_dir = filepath.Join(cfg.DataDir, "rpm")
|
|
app.uploads_base_dir = filepath.Join(cfg.DataDir, "uploads")
|
|
|
|
err = os.MkdirAll(cfg.DataDir, 0o755)
|
|
if err != nil {
|
|
err = fmt.Errorf("data dir error - %v", err)
|
|
goto oops
|
|
}
|
|
|
|
app.store, err = db.Open(cfg.DBDriver, cfg.DBDSN)
|
|
if err != nil {
|
|
err = fmt.Errorf("db open error - %v", err)
|
|
goto oops
|
|
}
|
|
|
|
err = app.store.ApplyMigrationsFS(migrationFiles, "migrations")
|
|
if err != nil {
|
|
err = fmt.Errorf("migrations error - %v", err)
|
|
goto oops
|
|
}
|
|
|
|
err = app.store.EnsureDefaultTLSAuthPolicies()
|
|
if err != nil {
|
|
err = fmt.Errorf("tls auth policy init error - %v", err)
|
|
goto oops
|
|
}
|
|
|
|
err = app.store.EnsureTLSAuthPolicyBindings()
|
|
if err != nil {
|
|
err = fmt.Errorf("tls auth binding init error - %v", err)
|
|
goto oops
|
|
}
|
|
|
|
err = mergeTLSSettingsFromDB(cfg, app.store, EnvPrefixFromServerId(app.serverId))
|
|
if err != nil {
|
|
err = fmt.Errorf("tls settings load error - %v", err)
|
|
goto oops
|
|
}
|
|
|
|
err = bootstrapAdmin(app.store, EnvPrefixFromServerId(app.serverId))
|
|
if err != nil {
|
|
err = fmt.Errorf("bootstrap admin error - %v", err)
|
|
goto oops
|
|
}
|
|
|
|
app.git_manager = &git.RepoManager{BaseDir: app.git_base_dir}
|
|
app.rpm_meta = rpm.NewMetaManager()
|
|
app.rpm_mirror = rpm.NewMirrorManager(app.store, logger, app.rpm_meta)
|
|
app.upload_store = &storage.FileStore{BaseDir: app.uploads_base_dir}
|
|
|
|
for _, dir = range []string{app.docker_base_dir, app.git_base_dir, app.rpm_base_dir, app.uploads_base_dir} {
|
|
err = os.MkdirAll(dir, 0o755)
|
|
if err != nil {
|
|
err = fmt.Errorf("dir error (%s) - %v", dir, err)
|
|
goto oops
|
|
}
|
|
}
|
|
|
|
app.docker_server = docker.NewHTTPServer(app.store, app.docker_base_dir, nil, logger, cfg.DockerHTTPPrefix)
|
|
|
|
app.git_server, err = git.NewHTTPServer(app.git_base_dir, nil, logger)
|
|
if err != nil {
|
|
err = fmt.Errorf("git server error - %v", err)
|
|
goto oops
|
|
}
|
|
|
|
app.rpm_server = rpm.NewHTTPServer(app.rpm_base_dir, nil, logger)
|
|
|
|
return &app, nil
|
|
|
|
oops:
|
|
app.Close()
|
|
return nil, err
|
|
}
|
|
|
|
func (app *Server) Close() {
|
|
if app.store != nil {
|
|
app.store.Close()
|
|
}
|
|
}
|
|
|
|
func (app *Server) Serve() error {
|
|
return app.ServeContext(context.Background())
|
|
}
|
|
|
|
func (app *Server) ServeContext(ctx context.Context) error {
|
|
var api *handlers.API
|
|
var mux *http.ServeMux
|
|
var mainEndpoints []listenerEndpoint
|
|
var extraListenerManager *additionalListenerManager
|
|
var shutdownCtx context.Context
|
|
var cancel context.CancelFunc
|
|
var servicesStarted bool
|
|
var err error
|
|
|
|
api, mux, err = app.initHandlers()
|
|
if err != nil {
|
|
app.logger.Write("", LOG_ERROR, "failed to initialize handlers - %v", err)
|
|
return err
|
|
}
|
|
|
|
extraListenerManager = newAdditionalListenerManager(app.store, mux, app.logger)
|
|
api.OnTLSListenersChanged = extraListenerManager.NotifyReload
|
|
api.OnTLSListenerRuntimeStatus = extraListenerManager.ListenerEndpointCounts
|
|
|
|
app.rpm_mirror.Start()
|
|
servicesStarted = true
|
|
|
|
err = extraListenerManager.Start()
|
|
if err != nil {
|
|
app.logger.Write("", LOG_ERROR, "additional listener manager error - %v", err)
|
|
shutdownCtx, cancel = context.WithTimeout(context.Background(), 10*time.Second)
|
|
extraListenerManager.Stop(shutdownCtx)
|
|
app.rpm_mirror.Stop()
|
|
app.rpm_mirror.Wait()
|
|
cancel()
|
|
return err
|
|
}
|
|
|
|
mainEndpoints, err = buildListenerEndpoints("main", "main", "", util.TLSSettingsFromConfig(*app.cfg), app.store)
|
|
if err != nil {
|
|
app.logger.Write("", LOG_ERROR, "main listener config error - %v", err)
|
|
shutdownCtx, cancel = context.WithTimeout(context.Background(), 10*time.Second)
|
|
extraListenerManager.Stop(shutdownCtx)
|
|
app.rpm_mirror.Stop()
|
|
app.rpm_mirror.Wait()
|
|
cancel()
|
|
return err
|
|
}
|
|
if len(mainEndpoints) == 0 && extraListenerManager.TotalEndpointCount() == 0 {
|
|
err = errors.New("at least one main or additional listener is required")
|
|
app.logger.Write("", LOG_ERROR, "listener config error - %v", err)
|
|
shutdownCtx, cancel = context.WithTimeout(context.Background(), 10*time.Second)
|
|
extraListenerManager.Stop(shutdownCtx)
|
|
app.rpm_mirror.Stop()
|
|
app.rpm_mirror.Wait()
|
|
cancel()
|
|
return err
|
|
}
|
|
if len(mainEndpoints) == 0 {
|
|
<-ctx.Done()
|
|
shutdownCtx, cancel = context.WithTimeout(context.Background(), 10*time.Second)
|
|
extraListenerManager.Stop(shutdownCtx)
|
|
if servicesStarted && app.rpm_mirror != nil {
|
|
app.rpm_mirror.Stop()
|
|
app.rpm_mirror.Wait()
|
|
}
|
|
cancel()
|
|
return nil
|
|
}
|
|
err = serveListeners(ctx, mainEndpoints, mux, app.logger, app.serverId)
|
|
shutdownCtx, cancel = context.WithTimeout(context.Background(), 10*time.Second)
|
|
if extraListenerManager != nil {
|
|
extraListenerManager.Stop(shutdownCtx)
|
|
}
|
|
if servicesStarted && app.rpm_mirror != nil {
|
|
app.rpm_mirror.Stop()
|
|
app.rpm_mirror.Wait()
|
|
}
|
|
cancel()
|
|
if err != nil {
|
|
app.logger.Write("", LOG_ERROR, "server error - %v", err)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (app *Server) initHandlers() (*handlers.API, *http.ServeMux, error) {
|
|
var cfg *codit_config.Config = app.cfg
|
|
var logger Logger = app.logger
|
|
var store *db.Store = app.store
|
|
|
|
var api *handlers.API
|
|
var router *codit_http.Router
|
|
|
|
api = handlers.NewAPI(handlers.APIOptions{
|
|
ServerId: app.serverId,
|
|
SiteName: app.siteName,
|
|
Cfg: *cfg,
|
|
Store: app.store,
|
|
Repos: *app.git_manager,
|
|
RpmBase: app.rpm_base_dir,
|
|
RpmMeta: app.rpm_meta,
|
|
RpmMirror: app.rpm_mirror,
|
|
DockerBase: app.docker_base_dir,
|
|
Uploads: *app.upload_store,
|
|
Logger: logger,
|
|
})
|
|
|
|
router = codit_http.NewRouter()
|
|
|
|
router.Handle("GET", "/api/health", api.Health)
|
|
router.Handle("GET", "/api/app-info", api.AppInfo)
|
|
router.Handle("POST", "/api/login", api.Login)
|
|
router.Handle("POST", "/api/logout", api.Logout)
|
|
router.Handle("GET", "/api/auth/oidc/enabled", api.OIDCEnabled)
|
|
router.Handle("GET", "/api/auth/oidc/login", api.OIDCLogin)
|
|
router.Handle("GET", "/api/auth/oidc/callback", api.OIDCCallback)
|
|
router.Handle("GET", "/api/me", api.Me)
|
|
router.Handle("PATCH", "/api/me", api.UpdateMe)
|
|
router.Handle("GET", "/api/me/totp", api.GetMyTOTP)
|
|
router.Handle("POST", "/api/me/totp/setup", api.SetupMyTOTP)
|
|
router.Handle("POST", "/api/me/totp/enable", api.EnableMyTOTP)
|
|
router.Handle("POST", "/api/me/totp/verify", api.VerifyMyTOTP)
|
|
router.Handle("POST", "/api/me/totp/disable", api.DisableMyTOTP)
|
|
|
|
router.Handle("GET", "/api/users", api.ListUsers)
|
|
router.Handle("POST", "/api/users", api.CreateUser)
|
|
router.Handle("PATCH", "/api/users/:id", api.UpdateUser)
|
|
router.Handle("DELETE", "/api/users/:id", api.DeleteUser)
|
|
router.Handle("POST", "/api/users/:id/disable", api.DisableUser)
|
|
router.Handle("POST", "/api/users/:id/enable", api.EnableUser)
|
|
router.Handle("POST", "/api/users/:id/totp/reset", api.ResetUserTOTPAdmin)
|
|
router.Handle("GET", "/api/admin/user-groups", api.ListUserGroups)
|
|
router.Handle("POST", "/api/admin/user-groups", api.CreateUserGroup)
|
|
router.Handle("PATCH", "/api/admin/user-groups/:id", api.UpdateUserGroup)
|
|
router.Handle("DELETE", "/api/admin/user-groups/:id", api.DeleteUserGroup)
|
|
router.Handle("GET", "/api/admin/user-groups/:id/members", api.ListUserGroupMembers)
|
|
router.Handle("POST", "/api/admin/user-groups/:id/members", api.AddUserGroupMember)
|
|
router.Handle("DELETE", "/api/admin/user-groups/:id/members/:userId", api.RemoveUserGroupMember)
|
|
router.Handle("GET", "/api/admin/permissions", api.ListSubjectPermissions)
|
|
router.Handle("POST", "/api/admin/permissions", api.CreateSubjectPermissions)
|
|
router.Handle("DELETE", "/api/admin/permissions/:permission/:subjectType/:subjectID", api.DeleteSubjectPermission)
|
|
router.Handle("GET", "/api/admin/auth", api.GetAuthSettings)
|
|
router.Handle("PATCH", "/api/admin/auth", api.UpdateAuthSettings)
|
|
router.Handle("POST", "/api/admin/auth/test", api.TestLDAPSettings)
|
|
router.Handle("GET", "/api/admin/tls", api.GetTLSSettings)
|
|
router.Handle("PATCH", "/api/admin/tls", api.UpdateTLSSettings)
|
|
router.Handle("GET", "/api/admin/tls/auth-policies", api.ListTLSAuthPolicies)
|
|
router.Handle("POST", "/api/admin/tls/auth-policies", api.CreateTLSAuthPolicy)
|
|
router.Handle("PATCH", "/api/admin/tls/auth-policies/:id", api.UpdateTLSAuthPolicy)
|
|
router.Handle("DELETE", "/api/admin/tls/auth-policies/:id", api.DeleteTLSAuthPolicy)
|
|
router.Handle("GET", "/api/admin/tls/listeners", api.ListTLSListeners)
|
|
router.Handle("GET", "/api/admin/tls/listeners/runtime", api.GetTLSListenerRuntimeStatus)
|
|
router.Handle("POST", "/api/admin/tls/listeners", api.CreateTLSListener)
|
|
router.Handle("PATCH", "/api/admin/tls/listeners/:id", api.UpdateTLSListener)
|
|
router.Handle("DELETE", "/api/admin/tls/listeners/:id", api.DeleteTLSListener)
|
|
router.Handle("GET", "/api/admin/service-principals", api.ListServicePrincipals)
|
|
router.Handle("POST", "/api/admin/service-principals", api.CreateServicePrincipal)
|
|
router.Handle("PATCH", "/api/admin/service-principals/:id", api.UpdateServicePrincipal)
|
|
router.Handle("DELETE", "/api/admin/service-principals/:id", api.DeleteServicePrincipal)
|
|
router.Handle("GET", "/api/admin/service-principals/:id/api-keys", api.ListServicePrincipalAPIKeys)
|
|
router.Handle("POST", "/api/admin/service-principals/:id/api-keys", api.CreateServicePrincipalAPIKey)
|
|
router.Handle("DELETE", "/api/admin/service-principals/:id/api-keys/:keyId", api.DeleteServicePrincipalAPIKey)
|
|
router.Handle("POST", "/api/admin/service-principals/:id/api-keys/:keyId/disable", api.DisableServicePrincipalAPIKey)
|
|
router.Handle("POST", "/api/admin/service-principals/:id/api-keys/:keyId/enable", api.EnableServicePrincipalAPIKey)
|
|
router.Handle("GET", "/api/admin/service-principals/:id/roles", api.ListPrincipalProjectRoles)
|
|
router.Handle("POST", "/api/admin/service-principals/:id/roles", api.UpsertPrincipalProjectRole)
|
|
router.Handle("DELETE", "/api/admin/service-principals/:id/roles/:projectId", api.DeletePrincipalProjectRole)
|
|
router.Handle("GET", "/api/admin/cert-principal-bindings", api.ListCertPrincipalBindings)
|
|
router.Handle("POST", "/api/admin/cert-principal-bindings", api.UpsertCertPrincipalBinding)
|
|
router.Handle("DELETE", "/api/admin/cert-principal-bindings/:fingerprint", api.DeleteCertPrincipalBinding)
|
|
router.Handle("GET", "/api/admin/auth/ldap", api.GetAuthSettings)
|
|
router.Handle("PATCH", "/api/admin/auth/ldap", api.UpdateAuthSettings)
|
|
router.Handle("POST", "/api/admin/auth/ldap/test", api.TestLDAPSettings)
|
|
router.Handle("GET", "/api/admin/pki/cas", api.ListPKICAs)
|
|
router.Handle("GET", "/api/admin/pki/cas/:id", api.GetPKICA)
|
|
router.Handle("PATCH", "/api/admin/pki/cas/:id", api.UpdatePKICA)
|
|
router.Handle("GET", "/api/admin/pki/cas/:id/bundle", api.DownloadPKICABundle)
|
|
router.Handle("POST", "/api/admin/pki/cas/root", api.CreatePKIRootCA)
|
|
router.Handle("POST", "/api/admin/pki/cas/intermediate", api.CreatePKIIntermediateCA)
|
|
router.Handle("GET", "/api/admin/pki/cas/:id/crl", api.GetPKICRL)
|
|
router.Handle("DELETE", "/api/admin/pki/cas/:id", api.DeletePKICA)
|
|
router.Handle("GET", "/api/admin/pki/certs", api.ListPKICerts)
|
|
router.Handle("GET", "/api/admin/pki/certs/:id", api.GetPKICert)
|
|
router.Handle("GET", "/api/admin/pki/certs/:id/inspect", api.GetPKICertInspect)
|
|
router.Handle("POST", "/api/admin/pki/certs/:id/renew-acme", api.RenewPKICertWithACME)
|
|
router.Handle("GET", "/api/admin/pki/certs/:id/bundle", api.DownloadPKICertBundle)
|
|
router.Handle("POST", "/api/admin/pki/certs", api.IssuePKICert)
|
|
router.Handle("POST", "/api/admin/pki/certs/import", api.ImportPKICert)
|
|
router.Handle("POST", "/api/admin/pki/certs/:id/revoke", api.RevokePKICert)
|
|
router.Handle("DELETE", "/api/admin/pki/certs/:id", api.DeletePKICert)
|
|
router.Handle("GET", "/api/admin/pki/client/profiles", api.ListPKIClientProfiles)
|
|
router.Handle("POST", "/api/admin/pki/client/profiles", api.CreatePKIClientProfile)
|
|
router.Handle("PATCH", "/api/admin/pki/client/profiles/:id", api.UpdatePKIClientProfile)
|
|
router.Handle("DELETE", "/api/admin/pki/client/profiles/:id", api.DeletePKIClientProfile)
|
|
router.Handle("GET", "/api/admin/pki/acme/profiles", api.ListACMEProfiles)
|
|
router.Handle("POST", "/api/admin/pki/acme/profiles", api.CreateACMEProfile)
|
|
router.Handle("PATCH", "/api/admin/pki/acme/profiles/:id", api.UpdateACMEProfile)
|
|
router.Handle("DELETE", "/api/admin/pki/acme/profiles/:id", api.DeleteACMEProfile)
|
|
router.Handle("GET", "/api/admin/pki/acme/orders", api.ListACMEOrders)
|
|
router.Handle("POST", "/api/admin/pki/acme/orders", api.CreateACMEOrder)
|
|
router.Handle("POST", "/api/admin/pki/acme/orders/:id/finalize", api.FinalizeACMEOrder)
|
|
router.Handle("DELETE", "/api/admin/pki/acme/orders/:id", api.DeleteACMEOrder)
|
|
router.Handle("GET", "/api/admin/rpm/mirrors", api.ListAdminRPMMirrors)
|
|
router.Handle("GET", "/api/admin/ssh/user-cas", api.ListSSHUserCAs)
|
|
router.Handle("POST", "/api/admin/ssh/user-cas", api.CreateSSHUserCA)
|
|
router.Handle("GET", "/api/admin/ssh/user-cas/:id", api.GetSSHUserCA)
|
|
router.Handle("PATCH", "/api/admin/ssh/user-cas/:id", api.UpdateSSHUserCA)
|
|
router.Handle("DELETE", "/api/admin/ssh/user-cas/:id", api.DeleteSSHUserCA)
|
|
router.Handle("GET", "/api/admin/ssh/user-cas/:id/public-key", api.DownloadSSHUserCAPublicKey)
|
|
router.Handle("GET", "/api/admin/ssh/user-cas/:id/private-key", api.DownloadSSHUserCAPrivateKey)
|
|
router.Handle("POST", "/api/admin/ssh/user-cas/:id/sign", api.SignSSHUserKey)
|
|
router.Handle("GET", "/api/admin/ssh/user-cas/:id/issuances", api.ListSSHUserCAIssuances)
|
|
router.Handle("GET", "/api/admin/ssh/issuances", api.ListSSHUserCAIssuancesAll)
|
|
router.Handle("GET", "/api/admin/ssh/principal-grants", api.ListSSHPrincipalGrants)
|
|
router.Handle("GET", "/api/admin/ssh/principal-grants/:id", api.GetSSHPrincipalGrant)
|
|
router.Handle("POST", "/api/admin/ssh/principal-grants", api.CreateSSHPrincipalGrant)
|
|
router.Handle("PATCH", "/api/admin/ssh/principal-grants/:id", api.UpdateSSHPrincipalGrant)
|
|
router.Handle("DELETE", "/api/admin/ssh/principal-grants/:id", api.DeleteSSHPrincipalGrant)
|
|
router.Handle("GET", "/api/admin/ssh/servers", api.ListSSHServersAdmin)
|
|
router.Handle("POST", "/api/admin/ssh/servers", api.CreateSSHServerAdmin)
|
|
router.Handle("GET", "/api/admin/ssh/servers/:id", api.GetSSHServerAdmin)
|
|
router.Handle("PATCH", "/api/admin/ssh/servers/:id", api.UpdateSSHServerAdmin)
|
|
router.Handle("DELETE", "/api/admin/ssh/servers/:id", api.DeleteSSHServerAdmin)
|
|
router.Handle("GET", "/api/admin/ssh/servers/:id/host-keys", api.ListSSHServerHostKeysAdmin)
|
|
router.Handle("POST", "/api/admin/ssh/servers/:id/host-keys", api.CreateSSHServerHostKeyAdmin)
|
|
router.Handle("POST", "/api/admin/ssh/servers/:id/host-keys/discover", api.DiscoverSSHServerHostKeyAdmin)
|
|
router.Handle("DELETE", "/api/admin/ssh/servers/:id/host-keys/:hostKeyId", api.DeleteSSHServerHostKeyAdmin)
|
|
router.Handle("GET", "/api/admin/ssh/server-groups", api.ListSSHServerGroupsAdmin)
|
|
router.Handle("POST", "/api/admin/ssh/server-groups", api.CreateSSHServerGroupAdmin)
|
|
router.Handle("PATCH", "/api/admin/ssh/server-groups/:id", api.UpdateSSHServerGroupAdmin)
|
|
router.Handle("DELETE", "/api/admin/ssh/server-groups/:id", api.DeleteSSHServerGroupAdmin)
|
|
router.Handle("GET", "/api/admin/ssh/server-groups/:id/servers", api.ListSSHServerGroupMembersAdmin)
|
|
router.Handle("POST", "/api/admin/ssh/server-groups/:id/servers", api.AddSSHServerGroupMemberAdmin)
|
|
router.Handle("DELETE", "/api/admin/ssh/server-groups/:id/servers/:serverId", api.DeleteSSHServerGroupMemberAdmin)
|
|
router.Handle("GET", "/api/admin/ssh/credentials", api.ListSSHCredentialsAdmin)
|
|
router.Handle("POST", "/api/admin/ssh/credentials", api.CreateSSHCredentialAdmin)
|
|
router.Handle("PATCH", "/api/admin/ssh/credentials/:id", api.UpdateSSHCredentialAdmin)
|
|
router.Handle("DELETE", "/api/admin/ssh/credentials/:id", api.DeleteSSHCredentialAdmin)
|
|
router.Handle("GET", "/api/admin/ssh/access-profiles", api.ListSSHAccessProfilesAdmin)
|
|
router.Handle("POST", "/api/admin/ssh/access-profiles", api.CreateSSHAccessProfileAdmin)
|
|
router.Handle("GET", "/api/admin/ssh/access-profiles/:id", api.GetSSHAccessProfileAdmin)
|
|
router.Handle("PATCH", "/api/admin/ssh/access-profiles/:id", api.UpdateSSHAccessProfileAdmin)
|
|
router.Handle("DELETE", "/api/admin/ssh/access-profiles/:id", api.DeleteSSHAccessProfileAdmin)
|
|
router.Handle("GET", "/api/admin/ssh/sessions", api.ListSSHSessionsAdmin)
|
|
router.Handle("GET", "/api/admin/ssh/sessions/:id/transcript", api.GetSSHSessionTranscriptAdmin)
|
|
router.Handle("GET", "/api/ssh/user-cas", api.ListSSHUserCAsForSelf)
|
|
router.Handle("GET", "/api/ssh/user-cas/:id", api.GetSSHUserCAForSelf)
|
|
router.Handle("GET", "/api/ssh/user-cas/:id/public-key", api.DownloadSSHUserCAPublicKeyForSelf)
|
|
router.Handle("GET", "/api/ssh/principal-grants", api.ListSSHPrincipalGrantsForSelf)
|
|
router.Handle("GET", "/api/ssh/servers", api.ListSSHServersForSelf)
|
|
router.Handle("GET", "/api/ssh/servers/:id", api.GetSSHServerForSelf)
|
|
router.Handle("POST", "/api/ssh/servers", api.CreateSSHServerForSelf)
|
|
router.Handle("PATCH", "/api/ssh/servers/:id", api.UpdateSSHServerForSelf)
|
|
router.Handle("DELETE", "/api/ssh/servers/:id", api.DeleteSSHServerForSelf)
|
|
router.Handle("GET", "/api/ssh/servers/:id/host-keys", api.ListSSHServerHostKeysForSelf)
|
|
router.Handle("POST", "/api/ssh/servers/:id/host-keys", api.CreateSSHServerHostKeyForSelf)
|
|
router.Handle("POST", "/api/ssh/servers/:id/host-keys/discover", api.DiscoverSSHServerHostKeyForSelf)
|
|
router.Handle("DELETE", "/api/ssh/servers/:id/host-keys/:hostKeyId", api.DeleteSSHServerHostKeyForSelf)
|
|
router.Handle("GET", "/api/ssh/credentials", api.ListSSHCredentialsForSelf)
|
|
router.Handle("POST", "/api/ssh/credentials", api.CreateSSHCredentialForSelf)
|
|
router.Handle("PATCH", "/api/ssh/credentials/:id", api.UpdateSSHCredentialForSelf)
|
|
router.Handle("DELETE", "/api/ssh/credentials/:id", api.DeleteSSHCredentialForSelf)
|
|
router.Handle("GET", "/api/ssh/access-profiles", api.ListSSHAccessProfilesForSelf)
|
|
router.Handle("POST", "/api/ssh/access-profiles", api.CreateSSHAccessProfileForSelf)
|
|
router.Handle("GET", "/api/ssh/access-profiles/:id", api.GetSSHAccessProfileForSelf)
|
|
router.Handle("GET", "/api/ssh/access-profiles/:id/credential", api.GetSSHAccessProfileCredentialForSelf)
|
|
router.Handle("GET", "/api/ssh/access-profiles/:id/servers", api.ListSSHAccessProfileServersForSelf)
|
|
router.Handle("PATCH", "/api/ssh/access-profiles/:id", api.UpdateSSHAccessProfileForSelf)
|
|
router.Handle("DELETE", "/api/ssh/access-profiles/:id", api.DeleteSSHAccessProfileForSelf)
|
|
router.Handle("POST", "/api/ssh/access-profiles/:id/connect", api.CreateSSHSessionForSelf)
|
|
router.Handle("GET", "/api/ssh/sessions", api.ListSSHSessionsForSelf)
|
|
router.Handle("GET", "/api/ssh/stream", api.StreamSSHWorkspaceForSelf)
|
|
router.Handle("GET", "/api/ssh/sessions/:id", api.GetSSHSessionForSelf)
|
|
router.Handle("GET", "/api/ssh/sessions/:id/transcript", api.GetSSHSessionTranscriptForSelf)
|
|
router.Handle("POST", "/api/ssh/sessions/:id/disconnect", api.DisconnectSSHSessionForSelf)
|
|
router.Handle("POST", "/api/ssh/sessions/:id/files/upload", api.UploadSSHSessionFilesForSelf)
|
|
router.Handle("POST", "/api/ssh/sessions/:id/files/download", api.DownloadSSHSessionFilesForSelf)
|
|
router.Handle("POST", "/api/ssh/sessions/:id/files/copy-to", api.CopySSHSessionFilesForSelf)
|
|
router.Handle("POST", "/api/ssh/cert/inspect", api.InspectSSHCertificate)
|
|
router.Handle("POST", "/api/ssh/user-cas/:id/sign", api.SignSSHUserKeyForSelf)
|
|
router.Handle("GET", "/api/ssh/issuances", api.ListSSHUserCAIssuancesForSelf)
|
|
router.Handle("GET", "/api/pki/cas/:id/crl", api.GetPKICRLForSelf)
|
|
router.Handle("GET", "/api/pki/cas/:id", api.GetPKICAForSelf)
|
|
router.Handle("GET", "/api/pki/client/profiles", api.ListPKIClientProfilesForSelf)
|
|
router.Handle("GET", "/api/pki/client/issuances", api.ListPKIClientIssuancesForSelf)
|
|
router.Handle("POST", "/api/pki/client/certs", api.IssuePKIClientCertForSelf)
|
|
router.Handle("GET", "/api/pki/client/certs/:id", api.GetPKIClientCertForSelf)
|
|
router.Handle("GET", "/api/pki/client/certs/:id/inspect", api.GetPKIClientCertInspectForSelf)
|
|
router.Handle("GET", "/api/pki/client/certs/:id/bundle", api.DownloadPKIClientCertBundleForSelf)
|
|
router.Handle("POST", "/api/pki/client/certs/:id/revoke", api.RevokePKIClientCertForSelf)
|
|
|
|
router.Handle("GET", "/api/projects", api.ListProjects)
|
|
router.Handle("POST", "/api/projects", api.CreateProject)
|
|
router.Handle("GET", "/api/projects/:id", api.GetProject)
|
|
router.Handle("PATCH", "/api/projects/:id", api.UpdateProject)
|
|
router.Handle("DELETE", "/api/projects/:id", api.DeleteProject)
|
|
|
|
router.Handle("GET", "/api/projects/:projectId/members", api.ListProjectMembers)
|
|
router.Handle("GET", "/api/projects/:projectId/member-candidates", api.ListProjectMemberCandidates)
|
|
router.Handle("GET", "/api/projects/:projectId/group-candidates", api.ListProjectGroupCandidates)
|
|
router.Handle("POST", "/api/projects/:projectId/members", api.AddProjectMember)
|
|
router.Handle("PATCH", "/api/projects/:projectId/members", api.UpdateProjectMember)
|
|
router.Handle("DELETE", "/api/projects/:projectId/members/:userId", api.RemoveProjectMember)
|
|
router.Handle("GET", "/api/projects/:projectId/group-roles", api.ListProjectGroupRoles)
|
|
router.Handle("POST", "/api/projects/:projectId/group-roles", api.UpsertProjectGroupRole)
|
|
router.Handle("DELETE", "/api/projects/:projectId/group-roles/:groupId", api.RemoveProjectGroupRole)
|
|
|
|
router.Handle("GET", "/api/projects/:projectId/repos", api.ListRepos)
|
|
router.Handle("POST", "/api/projects/:projectId/repos", api.CreateRepo)
|
|
router.Handle("GET", "/api/repos/types", api.RepoTypes)
|
|
router.Handle("GET", "/api/repos", api.ListAllRepos)
|
|
router.Handle("GET", "/api/projects/:projectId/foreign-repos/available", api.ListAvailableRepos)
|
|
router.Handle("POST", "/api/projects/:projectId/foreign-repos", api.AttachForeignRepo)
|
|
router.Handle("DELETE", "/api/projects/:projectId/foreign-repos/:repoId", api.DetachForeignRepo)
|
|
router.Handle("GET", "/api/repos/:id", api.GetRepo)
|
|
router.Handle("PATCH", "/api/repos/:id", api.UpdateRepo)
|
|
router.Handle("DELETE", "/api/repos/:id", api.DeleteRepo)
|
|
router.Handle("GET", "/api/repos/:id/branches", api.RepoBranches)
|
|
router.Handle("GET", "/api/repos/:id/branches/info", api.RepoBranchesInfo)
|
|
router.Handle("PUT", "/api/repos/:id/branches/default", api.RepoSetDefaultBranch)
|
|
router.Handle("POST", "/api/repos/:id/branches/rename", api.RepoRenameBranch)
|
|
router.Handle("POST", "/api/repos/:id/branches/delete", api.RepoDeleteBranch)
|
|
router.Handle("POST", "/api/repos/:id/branches/create", api.RepoCreateBranch)
|
|
router.Handle("GET", "/api/repos/:id/commits", api.RepoCommits)
|
|
router.Handle("GET", "/api/repos/:id/tree", api.RepoTree)
|
|
router.Handle("GET", "/api/repos/:id/blob", api.RepoBlob)
|
|
router.Handle("GET", "/api/repos/:id/blob/raw", api.RepoBlobRaw)
|
|
router.Handle("GET", "/api/repos/:id/history", api.RepoFileHistory)
|
|
router.Handle("GET", "/api/repos/:id/diff", api.RepoFileDiff)
|
|
router.Handle("GET", "/api/repos/:id/commit", api.RepoCommitDetail)
|
|
router.Handle("GET", "/api/repos/:id/commit/diff", api.RepoCommitDiff)
|
|
router.Handle("GET", "/api/repos/:id/compare", api.RepoCompare)
|
|
router.Handle("GET", "/api/repos/:id/stats", api.RepoStats)
|
|
router.Handle("GET", "/api/repos/:id/rpm/packages", api.RepoRPMPackages)
|
|
router.Handle("GET", "/api/repos/:id/rpm/package", api.RepoRPMPackage)
|
|
router.Handle("POST", "/api/repos/:id/rpm/subdirs", api.RepoRPMCreateSubdir)
|
|
router.Handle("GET", "/api/repos/:id/rpm/subdir", api.RepoRPMGetSubdir)
|
|
router.Handle("POST", "/api/repos/:id/rpm/subdir/update", api.RepoRPMRenameSubdir)
|
|
router.Handle("POST", "/api/repos/:id/rpm/subdir/rename", api.RepoRPMRenameSubdir)
|
|
router.Handle("POST", "/api/repos/:id/rpm/subdir/sync", api.RepoRPMSyncSubdir)
|
|
router.Handle("POST", "/api/repos/:id/rpm/subdir/suspend", api.RepoRPMSuspendSubdir)
|
|
router.Handle("POST", "/api/repos/:id/rpm/subdir/resume", api.RepoRPMResumeSubdir)
|
|
router.Handle("POST", "/api/repos/:id/rpm/subdir/rebuild-metadata", api.RepoRPMRebuildSubdirMetadata)
|
|
router.Handle("POST", "/api/repos/:id/rpm/subdir/cancel", api.RepoRPMCancelSubdirSync)
|
|
router.Handle("GET", "/api/repos/:id/rpm/subdir/runs", api.RepoRPMMirrorRuns)
|
|
router.Handle("DELETE", "/api/repos/:id/rpm/subdir/runs", api.RepoRPMClearMirrorRuns)
|
|
router.Handle("DELETE", "/api/repos/:id/rpm/subdir", api.RepoRPMDeleteSubdir)
|
|
router.Handle("DELETE", "/api/repos/:id/rpm/file", api.RepoRPMDeleteFile)
|
|
router.Handle("GET", "/api/repos/:id/rpm/file", api.RepoRPMFile)
|
|
router.Handle("GET", "/api/repos/:id/rpm/tree", api.RepoRPMTree)
|
|
router.Handle("POST", "/api/repos/:id/rpm/upload", api.RepoRPMUpload)
|
|
router.Handle("GET", "/api/repos/:id/docker/images", api.RepoDockerImages)
|
|
router.Handle("GET", "/api/repos/:id/docker/tags", api.RepoDockerTags)
|
|
router.Handle("GET", "/api/repos/:id/docker/manifest", api.RepoDockerManifest)
|
|
router.Handle("DELETE", "/api/repos/:id/docker/tag", api.RepoDockerDeleteTag)
|
|
router.Handle("DELETE", "/api/repos/:id/docker/image", api.RepoDockerDeleteImage)
|
|
router.Handle("POST", "/api/repos/:id/docker/tag/rename", api.RepoDockerRenameTag)
|
|
router.Handle("POST", "/api/repos/:id/docker/image/rename", api.RepoDockerRenameImage)
|
|
router.Handle("GET", "/api/me/keys", api.ListAPIKeys)
|
|
router.Handle("POST", "/api/me/keys", api.CreateAPIKey)
|
|
router.Handle("DELETE", "/api/me/keys/:id", api.DeleteAPIKey)
|
|
router.Handle("POST", "/api/me/keys/:id/disable", api.DisableAPIKey)
|
|
router.Handle("POST", "/api/me/keys/:id/enable", api.EnableAPIKey)
|
|
router.Handle("GET", "/api/admin/api-keys", api.ListAdminAPIKeys)
|
|
router.Handle("DELETE", "/api/admin/api-keys/:id", api.DeleteAdminAPIKey)
|
|
router.Handle("POST", "/api/admin/api-keys/:id/disable", api.DisableAdminAPIKey)
|
|
router.Handle("POST", "/api/admin/api-keys/:id/enable", api.EnableAdminAPIKey)
|
|
|
|
router.Handle("GET", "/api/projects/:projectId/issues", api.ListIssues)
|
|
router.Handle("POST", "/api/projects/:projectId/issues", api.CreateIssue)
|
|
router.Handle("PATCH", "/api/issues/:id", api.UpdateIssue)
|
|
router.Handle("POST", "/api/issues/:id/comments", api.AddIssueComment)
|
|
|
|
router.Handle("GET", "/api/projects/:projectId/wiki/pages", api.ListWikiPages)
|
|
router.Handle("POST", "/api/projects/:projectId/wiki/pages", api.CreateWikiPage)
|
|
router.Handle("PATCH", "/api/wiki/pages/:id", api.UpdateWikiPage)
|
|
|
|
router.Handle("POST", "/api/projects/:projectId/uploads", api.UploadFile)
|
|
router.Handle("GET", "/api/projects/:projectId/uploads", api.ListUploads)
|
|
router.Handle("GET", "/api/uploads/:id", api.DownloadFile)
|
|
|
|
|
|
// ---------------------------------------------------------------
|
|
var mux *http.ServeMux
|
|
var apiPublicHandler http.Handler
|
|
var apiHandler http.Handler
|
|
var gitHandler http.Handler
|
|
var gitIDHandler http.Handler
|
|
var rpmHandler http.Handler
|
|
var rpmIDHandler http.Handler
|
|
var dockerHandler http.Handler
|
|
var gitRewrite http.Handler
|
|
var gitIDRewrite http.Handler
|
|
var rpmRewrite http.Handler
|
|
var rpmIDRewrite http.Handler
|
|
|
|
mux = http.NewServeMux()
|
|
|
|
gitRewrite = &gitPathRewriteHandler{next: app.git_server, store: store}
|
|
gitHandler = withServiceAuth(serviceGit, cfg.GitHTTPPrefix, http.StripPrefix(cfg.GitHTTPPrefix, gitRewrite), auth_user, store, logger)
|
|
gitHandler = middleware.WithStoreTransactionUnbuffered(store, gitHandler)
|
|
gitHandler = middleware.WithRequestStore(store, gitHandler)
|
|
mux.Handle(cfg.GitHTTPPrefix+"/", gitHandler)
|
|
|
|
gitIDRewrite = &gitIDPathRewriteHandler{next: app.git_server, store: store}
|
|
gitIDHandler = withServiceAuth(serviceGit, cfg.GitHTTPPrefix+"-id", http.StripPrefix(cfg.GitHTTPPrefix+"-id", gitIDRewrite), auth_user, store, logger)
|
|
gitIDHandler = middleware.WithStoreTransactionUnbuffered(store, gitIDHandler)
|
|
gitIDHandler = middleware.WithRequestStore(store, gitIDHandler)
|
|
mux.Handle(cfg.GitHTTPPrefix+"-id/", gitIDHandler)
|
|
|
|
rpmRewrite = &rpmPathRewriteHandler{next: app.rpm_server, store: store}
|
|
rpmHandler = withServiceAuth(serviceRPM, cfg.RPMHTTPPrefix, http.StripPrefix(cfg.RPMHTTPPrefix, rpmRewrite), auth_user, store, logger)
|
|
rpmHandler = middleware.WithStoreTransactionUnbuffered(store, rpmHandler)
|
|
rpmHandler = middleware.WithRequestStore(store, rpmHandler)
|
|
mux.Handle(cfg.RPMHTTPPrefix+"/", rpmHandler)
|
|
|
|
rpmIDRewrite = &rpmIDPathRewriteHandler{next: app.rpm_server, store: store}
|
|
rpmIDHandler = withServiceAuth(serviceRPM, cfg.RPMHTTPPrefix+"-id", http.StripPrefix(cfg.RPMHTTPPrefix+"-id", rpmIDRewrite), auth_user, store, logger)
|
|
rpmIDHandler = middleware.WithStoreTransactionUnbuffered(store, rpmIDHandler)
|
|
rpmIDHandler = middleware.WithRequestStore(store, rpmIDHandler)
|
|
mux.Handle(cfg.RPMHTTPPrefix+"-id/", rpmIDHandler)
|
|
|
|
dockerHandler = withServiceAuth(serviceV2, cfg.DockerHTTPPrefix, app.docker_server, auth_user, store, logger)
|
|
dockerHandler = middleware.WithStoreTransactionUnbuffered(store, dockerHandler)
|
|
dockerHandler = middleware.WithRequestStore(store, dockerHandler)
|
|
mux.Handle(cfg.DockerHTTPPrefix, dockerHandler)
|
|
mux.Handle(cfg.DockerHTTPPrefix+"/", dockerHandler)
|
|
|
|
mux.HandleFunc("/pki/crl/", api.ServePKICRL)
|
|
|
|
// apiHandler = middleware.WithRequestStore(store,
|
|
// middleware.AccessLog(logger,
|
|
// middleware.WithStoreTransaction(store,
|
|
// middleware.WithUser(store,
|
|
// withAPIAuth(router, auth_user, store, logger)))))
|
|
apiHandler = withAPIAuth(router, auth_user, store, logger)
|
|
apiHandler = middleware.RequireInteractiveTOTP(apiHandler)
|
|
apiHandler = middleware.WithUserCookie(store, sessionCookieNameFromServerId(app.serverId), apiHandler)
|
|
apiHandler = middleware.WithStoreTransaction(store, apiHandler)
|
|
apiHandler = middleware.AccessLog(logger, apiHandler)
|
|
apiHandler = middleware.WithRequestStore(store, apiHandler)
|
|
apiHandler = middleware.WithCors(apiHandler)
|
|
mux.Handle("/api/", apiHandler)
|
|
mux.Handle("/api/health", middleware.AccessLog(logger, router))
|
|
|
|
// apiPublicHandler = middleware.WithRequestStore(store,
|
|
// middleware.AccessLog(logger,
|
|
// middleware.WithStoreTransaction(store,
|
|
// middleware.WithUser(store, router))))
|
|
apiPublicHandler = middleware.WithUserCookie(store, sessionCookieNameFromServerId(app.serverId), router)
|
|
apiPublicHandler = middleware.WithStoreTransaction(store, apiPublicHandler)
|
|
apiPublicHandler = middleware.AccessLog(logger, apiPublicHandler)
|
|
apiPublicHandler = middleware.WithRequestStore(store, apiPublicHandler)
|
|
apiPublicHandler = middleware.WithCors(apiPublicHandler)
|
|
mux.Handle("/api/login", apiPublicHandler)
|
|
mux.Handle("/api/logout", apiPublicHandler)
|
|
mux.Handle("/api/auth/oidc/enabled", apiPublicHandler)
|
|
mux.Handle("/api/auth/oidc/login", apiPublicHandler)
|
|
mux.Handle("/api/auth/oidc/callback", apiPublicHandler)
|
|
mux.Handle("/api/app-info", apiPublicHandler)
|
|
|
|
mux.Handle("/", middleware.WithUserCookie(store, sessionCookieNameFromServerId(app.serverId), spaHandler(cfg.FrontendDir, logger, app.serverId, TitleFromServerId(app.serverId), app.siteName)))
|
|
|
|
return api, mux, nil
|
|
}
|
|
|
|
|
|
|
|
type listenerEndpoint struct {
|
|
Name string
|
|
Key string
|
|
Addr string
|
|
IsHTTPS bool
|
|
TLSConfig *tls.Config
|
|
ListenerKind string
|
|
ListenerID string
|
|
}
|
|
|
|
type ctxKey string
|
|
|
|
const listenerIdentityKey ctxKey = "listener_identity"
|
|
|
|
type serviceKind string
|
|
|
|
const (
|
|
serviceAPI serviceKind = "api"
|
|
serviceGit serviceKind = "git"
|
|
serviceRPM serviceKind = "rpm"
|
|
serviceV2 serviceKind = "v2" // docker registry
|
|
)
|
|
|
|
type opKind string
|
|
|
|
const (
|
|
opRead opKind = "read"
|
|
opWrite opKind = "write"
|
|
)
|
|
|
|
type authOutcome string
|
|
|
|
const (
|
|
authRequireAuth authOutcome = "require_auth"
|
|
authAllow authOutcome = "allow"
|
|
authRequireCert authOutcome = "require_cert"
|
|
authCertOrAuth authOutcome = "require_cert_or_auth"
|
|
authDeny authOutcome = "deny"
|
|
)
|
|
|
|
type listenerAuthPolicy struct {
|
|
ReadMode string
|
|
WriteMode string
|
|
RequirePrincipalForWrite bool
|
|
CertAllowlist map[string]bool
|
|
ClientCertRequiredPermissions []string
|
|
ClientCertPermissionMatch string
|
|
ClientCertRequiredScope string
|
|
}
|
|
|
|
type listenerIdentity struct {
|
|
Kind string
|
|
ID string
|
|
}
|
|
|
|
type runningListener struct {
|
|
Endpoint listenerEndpoint
|
|
Server *http.Server
|
|
}
|
|
|
|
type additionalListenerManager struct {
|
|
Store *db.Store
|
|
Handler http.Handler
|
|
Logger Logger
|
|
mu sync.Mutex
|
|
Running map[string]runningListener
|
|
Reload chan struct{}
|
|
StopCh chan struct{}
|
|
StopOnce sync.Once
|
|
wg sync.WaitGroup
|
|
}
|
|
|
|
func newAdditionalListenerManager(store *db.Store, handler http.Handler, logger Logger) *additionalListenerManager {
|
|
var manager *additionalListenerManager
|
|
manager = &additionalListenerManager{
|
|
Store: store,
|
|
Handler: handler,
|
|
Logger: logger,
|
|
Running: make(map[string]runningListener),
|
|
Reload: make(chan struct{}, 1),
|
|
StopCh: make(chan struct{}),
|
|
}
|
|
return manager
|
|
}
|
|
|
|
func (m *additionalListenerManager) NotifyReload() {
|
|
select {
|
|
case m.Reload <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
|
|
func (m *additionalListenerManager) ListenerEndpointCounts() map[string]int {
|
|
var out map[string]int
|
|
var key string
|
|
var parts []string
|
|
var listenerID string
|
|
out = make(map[string]int)
|
|
m.mu.Lock()
|
|
for key = range m.Running {
|
|
parts = strings.SplitN(key, ":", 5)
|
|
if len(parts) < 3 {
|
|
continue
|
|
}
|
|
listenerID = strings.TrimSpace(parts[1])
|
|
if listenerID == "" {
|
|
continue
|
|
}
|
|
out[listenerID] = out[listenerID] + 1
|
|
}
|
|
m.mu.Unlock()
|
|
return out
|
|
}
|
|
|
|
func (m *additionalListenerManager) TotalEndpointCount() int {
|
|
var count int
|
|
|
|
if m == nil {
|
|
return 0
|
|
}
|
|
m.mu.Lock()
|
|
count = len(m.Running)
|
|
m.mu.Unlock()
|
|
return count
|
|
}
|
|
|
|
func (m *additionalListenerManager) Start() error {
|
|
var err error
|
|
var i int
|
|
for i = 0; i < 30; i++ {
|
|
err = m.reconcile()
|
|
if err == nil {
|
|
break
|
|
}
|
|
if !isSQLiteBusyError(err) {
|
|
return err
|
|
}
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
m.wg.Add(1)
|
|
go func() {
|
|
var reconcileErr error
|
|
defer m.wg.Done()
|
|
for {
|
|
select {
|
|
case <-m.StopCh:
|
|
return
|
|
case <-m.Reload:
|
|
reconcileErr = m.reconcile()
|
|
if reconcileErr != nil {
|
|
m.Logger.Write("", LOG_ERROR, "additional listener reconcile error - %v", reconcileErr)
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
return nil
|
|
}
|
|
|
|
func (m *additionalListenerManager) Stop(ctx context.Context) {
|
|
var done chan struct{}
|
|
var key string
|
|
var running runningListener
|
|
var shutdownCtx context.Context
|
|
var cancel context.CancelFunc
|
|
|
|
if m == nil {
|
|
return
|
|
}
|
|
m.StopOnce.Do(func() {
|
|
close(m.StopCh)
|
|
})
|
|
|
|
m.mu.Lock()
|
|
for key, running = range m.Running {
|
|
shutdownCtx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
|
|
_ = running.Server.Shutdown(shutdownCtx)
|
|
cancel()
|
|
delete(m.Running, key)
|
|
}
|
|
m.mu.Unlock()
|
|
|
|
done = make(chan struct{})
|
|
go func() {
|
|
m.wg.Wait()
|
|
close(done)
|
|
}()
|
|
select {
|
|
case <-done:
|
|
case <-ctx.Done():
|
|
}
|
|
}
|
|
|
|
func isSQLiteBusyError(err error) bool {
|
|
var msg string
|
|
if err == nil {
|
|
return false
|
|
}
|
|
msg = strings.ToLower(err.Error())
|
|
return strings.Contains(msg, "database is locked") || strings.Contains(msg, "sqlite_busy")
|
|
}
|
|
|
|
func (m *additionalListenerManager) reconcile() error {
|
|
var listeners []models.TLSListener
|
|
var desired map[string]listenerEndpoint
|
|
var err error
|
|
var i int
|
|
var key string
|
|
var configToken string
|
|
var endpoint listenerEndpoint
|
|
var running runningListener
|
|
var shutdownCtx context.Context
|
|
var cancel context.CancelFunc
|
|
var more []listenerEndpoint
|
|
var j int
|
|
var settings models.TLSSettings
|
|
|
|
listeners, err = m.Store.ListTLSListeners()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
desired = make(map[string]listenerEndpoint)
|
|
for i = 0; i < len(listeners); i++ {
|
|
if !listeners[i].Enabled {
|
|
continue
|
|
}
|
|
|
|
settings = models.TLSSettings{
|
|
HTTPAddrs: listeners[i].HTTPAddrs,
|
|
HTTPSAddrs: listeners[i].HTTPSAddrs,
|
|
TLSServerCertSource: listeners[i].TLSServerCertSource,
|
|
TLSCertFile: listeners[i].TLSCertFile,
|
|
TLSKeyFile: listeners[i].TLSKeyFile,
|
|
TLSPKIServerCertID: listeners[i].TLSPKIServerCertID,
|
|
TLSClientAuth: listeners[i].TLSClientAuth,
|
|
TLSClientCAFile: listeners[i].TLSClientCAFile,
|
|
TLSPKIClientCAID: listeners[i].TLSPKIClientCAID,
|
|
TLSMinVersion: listeners[i].TLSMinVersion,
|
|
}
|
|
|
|
more, err = buildListenerEndpoints(listeners[i].Name, "extra", listeners[i].ID, settings, m.Store)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for j = 0; j < len(more); j++ {
|
|
configToken = listenerEndpointConfigToken(settings, more[j].IsHTTPS)
|
|
key = "extra:" + listeners[i].ID + ":" + configToken + ":" + endpointScheme(more[j]) + ":" + more[j].Addr
|
|
more[j].Key = key
|
|
desired[key] = more[j]
|
|
}
|
|
}
|
|
|
|
m.mu.Lock()
|
|
|
|
// among the running listeners, stop all disabled/invalid listeners.
|
|
for key, running = range m.Running {
|
|
var exists bool
|
|
_, exists = desired[key]
|
|
if exists {
|
|
continue
|
|
} // found in configuration
|
|
shutdownCtx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
|
|
_ = running.Server.Shutdown(shutdownCtx)
|
|
cancel()
|
|
delete(m.Running, key)
|
|
m.Logger.Write("", LOG_INFO, "additional listener stopped name=%s addr=%s", running.Endpoint.Name, running.Endpoint.Addr)
|
|
}
|
|
|
|
// start the listeners if they are not already running
|
|
for key, endpoint = range desired {
|
|
var exists bool
|
|
_, exists = m.Running[key]
|
|
if exists {
|
|
continue
|
|
} // already running
|
|
running = m.startEndpoint(endpoint)
|
|
m.Running[key] = running
|
|
}
|
|
m.mu.Unlock()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *additionalListenerManager) startEndpoint(endpoint listenerEndpoint) runningListener {
|
|
var server *http.Server
|
|
var running runningListener
|
|
server = &http.Server{
|
|
Addr: endpoint.Addr,
|
|
Handler: m.Handler,
|
|
ConnContext: connContextWithListenerIdentity(endpoint.ListenerKind, endpoint.ListenerID),
|
|
ErrorLog: log.New(&server_http_log_writer{l: m.Logger, id: endpoint.Name, depth: +2}, "", 0),
|
|
}
|
|
if endpoint.IsHTTPS {
|
|
server.TLSConfig = endpoint.TLSConfig
|
|
}
|
|
running = runningListener{
|
|
Endpoint: endpoint,
|
|
Server: server,
|
|
}
|
|
if endpoint.IsHTTPS {
|
|
m.Logger.Write(endpoint.Name, LOG_INFO, "additional listener started https://%s", endpoint.Addr)
|
|
} else {
|
|
m.Logger.Write(endpoint.Name, LOG_INFO, "additional listener started %s", endpoint.Addr)
|
|
}
|
|
m.wg.Add(1)
|
|
go func(ep listenerEndpoint, srv *http.Server) {
|
|
var err error
|
|
var listener net.Listener
|
|
var network string
|
|
defer m.wg.Done()
|
|
network = tcpAddrStrClass(ep.Addr)
|
|
listener, err = net.Listen(network, ep.Addr)
|
|
if err != nil {
|
|
m.Logger.Write(ep.Name, LOG_ERROR, "additional listener failed network=%s addr=%s err=%v", network, ep.Addr, err)
|
|
return
|
|
}
|
|
if ep.IsHTTPS {
|
|
err = srv.ServeTLS(listener, "", "")
|
|
} else {
|
|
err = srv.Serve(listener)
|
|
}
|
|
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
m.Logger.Write(ep.Name, LOG_ERROR, "additional listener failed network=%s addr=%s err=%v", network, ep.Addr, err)
|
|
}
|
|
}(endpoint, server)
|
|
return running
|
|
}
|
|
|
|
func endpointScheme(endpoint listenerEndpoint) string {
|
|
if endpoint.IsHTTPS {
|
|
return "https"
|
|
}
|
|
return "http"
|
|
}
|
|
|
|
func listenerEndpointConfigToken(settings models.TLSSettings, isHTTPS bool) string {
|
|
var values []string
|
|
var sum [32]byte
|
|
|
|
if !isHTTPS {
|
|
return "http"
|
|
}
|
|
values = append(values, strings.TrimSpace(settings.TLSServerCertSource))
|
|
values = append(values, strings.TrimSpace(settings.TLSCertFile))
|
|
values = append(values, strings.TrimSpace(settings.TLSKeyFile))
|
|
values = append(values, strings.TrimSpace(settings.TLSPKIServerCertID))
|
|
values = append(values, strings.TrimSpace(settings.TLSClientAuth))
|
|
values = append(values, strings.TrimSpace(settings.TLSClientCAFile))
|
|
values = append(values, strings.TrimSpace(settings.TLSPKIClientCAID))
|
|
values = append(values, strings.TrimSpace(settings.TLSMinVersion))
|
|
sum = sha256.Sum256([]byte(strings.Join(values, "\n")))
|
|
return hex.EncodeToString(sum[:8])
|
|
}
|
|
|
|
func connContextWithListenerIdentity(kind string, id string) func(ctx context.Context, conn net.Conn) context.Context {
|
|
return func(ctx context.Context, _ net.Conn) context.Context {
|
|
return context.WithValue(ctx, listenerIdentityKey, listenerIdentity{Kind: kind, ID: id})
|
|
}
|
|
}
|
|
|
|
func listenerIdentityFromRequest(r *http.Request) listenerIdentity {
|
|
var identity listenerIdentity
|
|
var value any
|
|
var ok bool
|
|
|
|
identity = listenerIdentity{Kind: "main", ID: ""}
|
|
if r == nil {
|
|
return identity
|
|
}
|
|
value = r.Context().Value(listenerIdentityKey)
|
|
identity, ok = value.(listenerIdentity)
|
|
if !ok {
|
|
return identity
|
|
}
|
|
if strings.TrimSpace(identity.Kind) == "" {
|
|
identity.Kind = "main"
|
|
}
|
|
return identity
|
|
}
|
|
|
|
func listenerPolicyFromRequest(r *http.Request, service serviceKind, store *db.Store) (listenerAuthPolicy, error) {
|
|
var identity listenerIdentity
|
|
var item models.TLSAuthPolicy
|
|
var policy listenerAuthPolicy
|
|
var err error
|
|
|
|
identity = listenerIdentityFromRequest(r)
|
|
item, err = store.ResolveTLSAuthPolicy(identity.Kind, identity.ID, string(service))
|
|
if err != nil {
|
|
return defaultListenerPolicy(), err
|
|
}
|
|
policy, err = compileListenerAuthPolicy(item, store)
|
|
if err != nil {
|
|
return defaultListenerPolicy(), err
|
|
}
|
|
return policy, nil
|
|
}
|
|
|
|
func evaluateAuthOutcome(policy listenerAuthPolicy, operation opKind) authOutcome {
|
|
var mode string
|
|
|
|
mode = policy.ReadMode
|
|
if operation == opWrite {
|
|
mode = policy.WriteMode
|
|
}
|
|
switch strings.ToLower(strings.TrimSpace(mode)) {
|
|
case "public":
|
|
return authAllow
|
|
case "cert":
|
|
return authRequireCert
|
|
case "cert_or_auth":
|
|
return authCertOrAuth
|
|
case "deny":
|
|
return authDeny
|
|
default:
|
|
return authRequireAuth
|
|
}
|
|
}
|
|
|
|
func requestClientCertFingerprint(r *http.Request) string {
|
|
var cert *x509.Certificate
|
|
var hash [32]byte
|
|
if r == nil || r.TLS == nil || len(r.TLS.PeerCertificates) == 0 {
|
|
return ""
|
|
}
|
|
cert = r.TLS.PeerCertificates[0]
|
|
hash = sha256.Sum256(cert.Raw)
|
|
return strings.ToLower(hex.EncodeToString(hash[:]))
|
|
}
|
|
|
|
func isClientCertAllowed(fp string, policy listenerAuthPolicy) bool {
|
|
if len(policy.CertAllowlist) == 0 { return true }
|
|
return policy.CertAllowlist[fp]
|
|
}
|
|
|
|
func requestStore(r *http.Request, store *db.Store) *db.Store {
|
|
var requestStore *db.Store
|
|
var ok bool
|
|
|
|
if r != nil {
|
|
requestStore, ok = middleware.StoreFromContext(r.Context())
|
|
if ok && requestStore != nil { return requestStore }
|
|
}
|
|
return store
|
|
}
|
|
|
|
func authenticateByBasic(r *http.Request, auth func(store *db.Store, username string, password string) (bool, error), store *db.Store) bool {
|
|
var username string
|
|
var password string
|
|
var ok bool
|
|
var allowed bool
|
|
var err error
|
|
|
|
if auth == nil {
|
|
return false
|
|
}
|
|
username, password, ok = r.BasicAuth()
|
|
|
|
if !ok {
|
|
return false
|
|
}
|
|
allowed, err = auth(store, username, password)
|
|
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return allowed
|
|
}
|
|
|
|
func withServiceAuth(service serviceKind, servicePrefix string, next http.Handler, auth func(store *db.Store, username string, password string) (bool, error), store *db.Store, logger Logger) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
var policy listenerAuthPolicy
|
|
var operation opKind
|
|
var outcome authOutcome
|
|
var currentStore *db.Store
|
|
var ok bool
|
|
var certReason string
|
|
var mtlsInfo middleware.ClientCertLogInfo
|
|
var principal models.ServicePrincipal
|
|
var principalOK bool
|
|
var err error
|
|
currentStore = requestStore(r, store)
|
|
policy, err = listenerPolicyFromRequest(r, service, currentStore)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
operation = classifyServiceOperation(service, r)
|
|
outcome = evaluateAuthOutcome(policy, operation)
|
|
if outcome == authDeny {
|
|
middleware.RememberAuthReason(r.Context(), "policy_denied")
|
|
mtlsInfo = middleware.ClientCertLogFields(r)
|
|
logger.Write(string(service), LOG_DEBUG, "auth denied policy=%s method=%s path=%s remote=%s mtls_cn=%q mtls_issuer_cn=%q mtls_serial=%q mtls_user_id=%q mtls_username=%q mtls_profile_id=%q mtls_permissions=%q mtls_scope=%q reason=%s", listenerPolicyLogLabel(policy), r.Method, r.URL.Path, r.RemoteAddr, mtlsInfo.SubjectCN, mtlsInfo.IssuerCN, mtlsInfo.Serial, mtlsInfo.UserID, mtlsInfo.Username, mtlsInfo.ProfileID, mtlsInfo.Permissions, mtlsInfo.Scope, "policy_denied")
|
|
w.WriteHeader(http.StatusForbidden)
|
|
return
|
|
}
|
|
if outcome == authAllow {
|
|
next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
if outcome == authRequireCert {
|
|
ok, principal, principalOK, certReason, err = authorizeByClientCert(service, servicePrefix, r, policy, operation, currentStore)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if ok {
|
|
if principalOK {
|
|
r = middleware.WithPrincipal(r, principal)
|
|
}
|
|
next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
middleware.RememberAuthReason(r.Context(), certReason)
|
|
mtlsInfo = middleware.ClientCertLogFields(r)
|
|
logger.Write(string(service), LOG_DEBUG, "auth denied policy=%s method=%s path=%s remote=%s mtls_cn=%q mtls_issuer_cn=%q mtls_serial=%q mtls_user_id=%q mtls_username=%q mtls_profile_id=%q mtls_permissions=%q mtls_scope=%q reason=%s", listenerPolicyLogLabel(policy), r.Method, r.URL.Path, r.RemoteAddr, mtlsInfo.SubjectCN, mtlsInfo.IssuerCN, mtlsInfo.Serial, mtlsInfo.UserID, mtlsInfo.Username, mtlsInfo.ProfileID, mtlsInfo.Permissions, mtlsInfo.Scope, certReason)
|
|
w.WriteHeader(http.StatusForbidden)
|
|
return
|
|
}
|
|
if outcome == authCertOrAuth {
|
|
ok, principal, principalOK, certReason, err = authorizeByClientCert(service, servicePrefix, r, policy, operation, currentStore)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if ok {
|
|
if principalOK {
|
|
r = middleware.WithPrincipal(r, principal)
|
|
}
|
|
next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
}
|
|
ok = authenticateByBasic(r, auth, currentStore)
|
|
if ok {
|
|
next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
if service == serviceGit {
|
|
w.Header().Set("WWW-Authenticate", `Basic realm="git"`)
|
|
} else if service == serviceRPM {
|
|
w.Header().Set("WWW-Authenticate", `Basic realm="rpm"`)
|
|
} else if service == serviceV2 {
|
|
w.Header().Set("WWW-Authenticate", `Basic realm="docker"`)
|
|
}
|
|
if strings.TrimSpace(certReason) == "" {
|
|
certReason = "auth_failed"
|
|
}
|
|
middleware.RememberAuthReason(r.Context(), certReason)
|
|
mtlsInfo = middleware.ClientCertLogFields(r)
|
|
logger.Write(string(service), LOG_DEBUG, "auth denied policy=%s method=%s path=%s remote=%s mtls_cn=%q mtls_issuer_cn=%q mtls_serial=%q mtls_user_id=%q mtls_username=%q mtls_profile_id=%q mtls_permissions=%q mtls_scope=%q reason=%s", listenerPolicyLogLabel(policy), r.Method, r.URL.Path, r.RemoteAddr, mtlsInfo.SubjectCN, mtlsInfo.IssuerCN, mtlsInfo.Serial, mtlsInfo.UserID, mtlsInfo.Username, mtlsInfo.ProfileID, mtlsInfo.Permissions, mtlsInfo.Scope, certReason)
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
})
|
|
}
|
|
|
|
func withAPIAuth(next http.Handler, auth func(store *db.Store, username string, password string) (bool, error), store *db.Store, logger Logger) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
var policy listenerAuthPolicy
|
|
var operation opKind
|
|
var outcome authOutcome
|
|
var currentStore *db.Store
|
|
var certReason string
|
|
var mtlsInfo middleware.ClientCertLogInfo
|
|
var user models.User
|
|
var hasUser bool
|
|
var hasPrincipal bool
|
|
var ok bool
|
|
var principal models.ServicePrincipal
|
|
var principalOK bool
|
|
var err error
|
|
|
|
currentStore = requestStore(r, store)
|
|
policy, err = listenerPolicyFromRequest(r, serviceAPI, currentStore)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
operation = classifyServiceOperation(serviceAPI, r)
|
|
outcome = evaluateAuthOutcome(policy, operation)
|
|
user, hasUser = middleware.UserFromContext(r.Context())
|
|
if user.Disabled {
|
|
hasUser = false
|
|
}
|
|
principal, hasPrincipal = middleware.PrincipalFromContext(r.Context())
|
|
if principal.Disabled {
|
|
hasPrincipal = false
|
|
}
|
|
|
|
if outcome == authDeny {
|
|
middleware.RememberAuthReason(r.Context(), "policy_denied")
|
|
mtlsInfo = middleware.ClientCertLogFields(r)
|
|
logger.Write(logIDAPI, LOG_DEBUG, "auth denied policy=%s method=%s path=%s remote=%s mtls_cn=%q mtls_issuer_cn=%q mtls_serial=%q mtls_user_id=%q mtls_username=%q mtls_profile_id=%q mtls_permissions=%q mtls_scope=%q reason=%s", listenerPolicyLogLabel(policy), r.Method, r.URL.Path, r.RemoteAddr, mtlsInfo.SubjectCN, mtlsInfo.IssuerCN, mtlsInfo.Serial, mtlsInfo.UserID, mtlsInfo.Username, mtlsInfo.ProfileID, mtlsInfo.Permissions, mtlsInfo.Scope, "policy_denied")
|
|
w.WriteHeader(http.StatusForbidden)
|
|
return
|
|
}
|
|
if outcome == authAllow {
|
|
next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
if outcome == authRequireCert {
|
|
ok, principal, principalOK, certReason, err = authorizeByClientCert(serviceAPI, "", r, policy, operation, currentStore)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if ok {
|
|
if principalOK {
|
|
r = middleware.WithPrincipal(r, principal)
|
|
}
|
|
next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
middleware.RememberAuthReason(r.Context(), certReason)
|
|
mtlsInfo = middleware.ClientCertLogFields(r)
|
|
logger.Write(logIDAPI, LOG_DEBUG, "auth denied policy=%s method=%s path=%s remote=%s mtls_cn=%q mtls_issuer_cn=%q mtls_serial=%q mtls_user_id=%q mtls_username=%q mtls_profile_id=%q mtls_permissions=%q mtls_scope=%q reason=%s", listenerPolicyLogLabel(policy), r.Method, r.URL.Path, r.RemoteAddr, mtlsInfo.SubjectCN, mtlsInfo.IssuerCN, mtlsInfo.Serial, mtlsInfo.UserID, mtlsInfo.Username, mtlsInfo.ProfileID, mtlsInfo.Permissions, mtlsInfo.Scope, certReason)
|
|
w.WriteHeader(http.StatusForbidden)
|
|
return
|
|
}
|
|
if outcome == authCertOrAuth {
|
|
ok, principal, principalOK, certReason, err = authorizeByClientCert(serviceAPI, "", r, policy, operation, currentStore)
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if ok || hasUser || hasPrincipal {
|
|
if principalOK {
|
|
r = middleware.WithPrincipal(r, principal)
|
|
}
|
|
next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
if authenticateByBasic(r, auth, currentStore) {
|
|
next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
if strings.TrimSpace(certReason) == "" {
|
|
certReason = "auth_failed"
|
|
}
|
|
middleware.RememberAuthReason(r.Context(), certReason)
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
if hasUser || hasPrincipal || authenticateByBasic(r, auth, currentStore) {
|
|
next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
if strings.TrimSpace(certReason) == "" {
|
|
certReason = "auth_failed"
|
|
}
|
|
middleware.RememberAuthReason(r.Context(), certReason)
|
|
mtlsInfo = middleware.ClientCertLogFields(r)
|
|
logger.Write(logIDAPI, LOG_DEBUG, "auth denied policy=%s method=%s path=%s remote=%s mtls_cn=%q mtls_issuer_cn=%q mtls_serial=%q mtls_user_id=%q mtls_username=%q mtls_profile_id=%q mtls_permissions=%q mtls_scope=%q reason=%s", listenerPolicyLogLabel(policy), r.Method, r.URL.Path, r.RemoteAddr, mtlsInfo.SubjectCN, mtlsInfo.IssuerCN, mtlsInfo.Serial, mtlsInfo.UserID, mtlsInfo.Username, mtlsInfo.ProfileID, mtlsInfo.Permissions, mtlsInfo.Scope, certReason)
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
})
|
|
}
|
|
|
|
func classifyServiceOperation(service serviceKind, r *http.Request) opKind {
|
|
var serviceName string
|
|
serviceName = ""
|
|
if r != nil {
|
|
serviceName = r.URL.Query().Get("service")
|
|
}
|
|
if service == serviceGit {
|
|
if strings.Contains(r.URL.Path, "git-receive-pack") || serviceName == "git-receive-pack" {
|
|
return opWrite
|
|
}
|
|
return opRead
|
|
}
|
|
if service == serviceAPI {
|
|
if r.Method == http.MethodGet || r.Method == http.MethodHead || r.Method == http.MethodOptions {
|
|
return opRead
|
|
}
|
|
return opWrite
|
|
}
|
|
if service == serviceRPM {
|
|
if r.Method == http.MethodGet || r.Method == http.MethodHead || r.Method == http.MethodOptions {
|
|
return opRead
|
|
}
|
|
return opWrite
|
|
}
|
|
if service == serviceV2 {
|
|
if r.Method == http.MethodGet || r.Method == http.MethodHead {
|
|
return opRead
|
|
}
|
|
if r.Method == http.MethodPost || r.Method == http.MethodPut || r.Method == http.MethodPatch || r.Method == http.MethodDelete {
|
|
return opWrite
|
|
}
|
|
}
|
|
return opRead
|
|
}
|
|
|
|
func authorizeByClientCert(service serviceKind, servicePrefix string, r *http.Request, policy listenerAuthPolicy, operation opKind, store *db.Store) (bool, models.ServicePrincipal, bool, string, error) {
|
|
var fp string
|
|
var cert *x509.Certificate
|
|
var info pkiutil.ClientAuthorizationInfo
|
|
var hasInfo bool
|
|
var principal models.ServicePrincipal
|
|
var bound bool
|
|
var allowed bool
|
|
var reason string
|
|
var err error
|
|
|
|
fp = requestClientCertFingerprint(r)
|
|
if fp == "" {
|
|
return false, principal, false, "missing_client_cert", nil
|
|
}
|
|
|
|
if !isClientCertAllowed(fp, policy) {
|
|
return false, principal, false, "client_cert_not_allowed", nil
|
|
}
|
|
cert = requestClientCertificate(r)
|
|
info, hasInfo, err = pkiutil.ParseClientAuthorizationFromCertificate(cert)
|
|
if err != nil {
|
|
return false, principal, false, "client_cert_extension_error", err
|
|
}
|
|
allowed, reason, err = isClientCertAuthorizationAllowed(info, hasInfo, policy)
|
|
if err != nil {
|
|
return false, principal, false, reason, err
|
|
}
|
|
if !allowed {
|
|
return false, principal, false, reason, nil
|
|
}
|
|
if operation == opRead {
|
|
return true, principal, false, "", nil
|
|
}
|
|
if !policy.RequirePrincipalForWrite {
|
|
return true, principal, false, "", nil
|
|
}
|
|
|
|
principal, bound, err = store.GetPrincipalByCertFingerprint(fp)
|
|
if err != nil || !bound {
|
|
if err != nil {
|
|
return false, principal, false, "principal_binding_lookup_failed", err
|
|
}
|
|
return false, principal, false, "client_cert_principal_not_bound", nil
|
|
}
|
|
if service == serviceAPI {
|
|
return true, principal, true, "", nil
|
|
}
|
|
|
|
allowed, err = principalCanWriteService(service, servicePrefix, principal, r, store)
|
|
if err != nil || !allowed {
|
|
if err != nil {
|
|
return false, principal, false, "principal_write_check_failed", err
|
|
}
|
|
return false, principal, false, "principal_write_denied", nil
|
|
}
|
|
|
|
return true, principal, true, "", nil
|
|
}
|
|
|
|
func requestClientCertificate(r *http.Request) *x509.Certificate {
|
|
if r == nil || r.TLS == nil || len(r.TLS.PeerCertificates) == 0 {
|
|
return nil
|
|
}
|
|
return r.TLS.PeerCertificates[0]
|
|
}
|
|
|
|
func isClientCertAuthorizationAllowed(info pkiutil.ClientAuthorizationInfo, hasInfo bool, policy listenerAuthPolicy) (bool, string, error) {
|
|
var needsExtension bool
|
|
|
|
needsExtension = len(policy.ClientCertRequiredPermissions) > 0 || strings.TrimSpace(policy.ClientCertRequiredScope) != ""
|
|
if !needsExtension {
|
|
return true, "", nil
|
|
}
|
|
if !hasInfo {
|
|
return false, "client_cert_extension_missing", nil
|
|
}
|
|
if !clientCertPermissionsAllowed(info.Permissions, policy.ClientCertRequiredPermissions, policy.ClientCertPermissionMatch) {
|
|
return false, "client_cert_permission_mismatch", nil
|
|
}
|
|
if strings.TrimSpace(policy.ClientCertRequiredScope) != "" && strings.TrimSpace(info.Scope) != strings.TrimSpace(policy.ClientCertRequiredScope) {
|
|
return false, "client_cert_scope_mismatch", nil
|
|
}
|
|
return true, "", nil
|
|
}
|
|
|
|
func clientCertPermissionsAllowed(actual []string, required []string, matchMode string) bool {
|
|
var normalized map[string]bool
|
|
var i int
|
|
var item string
|
|
|
|
if len(required) == 0 {
|
|
return true
|
|
}
|
|
normalized = make(map[string]bool)
|
|
for i = 0; i < len(actual); i++ {
|
|
item = strings.ToLower(strings.TrimSpace(actual[i]))
|
|
if item == "" {
|
|
continue
|
|
}
|
|
normalized[item] = true
|
|
}
|
|
if strings.TrimSpace(matchMode) == "any" {
|
|
for i = 0; i < len(required); i++ {
|
|
item = strings.ToLower(strings.TrimSpace(required[i]))
|
|
if item == "" {
|
|
continue
|
|
}
|
|
if normalized[item] {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
for i = 0; i < len(required); i++ {
|
|
item = strings.ToLower(strings.TrimSpace(required[i]))
|
|
if item == "" {
|
|
continue
|
|
}
|
|
if !normalized[item] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func principalCanWriteService(service serviceKind, servicePrefix string, principal models.ServicePrincipal, r *http.Request, store *db.Store) (bool, error) {
|
|
var projectID string
|
|
var role string
|
|
var err error
|
|
if principal.IsAdmin {
|
|
return true, nil
|
|
}
|
|
projectID, err = resolveProjectIDForServiceWrite(service, servicePrefix, r, store)
|
|
if err != nil || strings.TrimSpace(projectID) == "" {
|
|
return false, nil
|
|
}
|
|
role, err = store.GetPrincipalProjectRole(principal.ID, projectID)
|
|
if err != nil {
|
|
return false, nil
|
|
}
|
|
if !roleAllowsWrite(role) {
|
|
return false, nil
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func roleAllowsWrite(role string) bool {
|
|
var r string
|
|
r = strings.ToLower(strings.TrimSpace(role))
|
|
return r == "writer" || r == "admin"
|
|
}
|
|
|
|
func resolveProjectIDForServiceWrite(service serviceKind, servicePrefix string, r *http.Request, store *db.Store) (string, error) {
|
|
var path string
|
|
var parts []string
|
|
var first string
|
|
var second string
|
|
var repo models.Repo
|
|
var project models.Project
|
|
var err error
|
|
path = strings.Trim(strings.TrimSpace(r.URL.Path), "/")
|
|
servicePrefix = strings.Trim(codit_config.NormalizeHTTPPrefix(servicePrefix, ""), "/")
|
|
if servicePrefix != "" && strings.HasPrefix(path, servicePrefix + "/") {
|
|
path = strings.TrimPrefix(path, servicePrefix + "/")
|
|
}
|
|
if service == serviceV2 {
|
|
parts = strings.Split(path, "/")
|
|
if len(parts) < 2 {
|
|
return "", errors.New("invalid v2 path")
|
|
}
|
|
project, err = store.GetProjectBySlug(parts[0])
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
repo, err = store.GetRepoByProjectNameType(project.ID, parts[1], "docker")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return repo.ProjectID, nil
|
|
}
|
|
if service != serviceGit && service != serviceRPM {
|
|
return "", errors.New("unsupported service")
|
|
}
|
|
parts = strings.Split(path, "/")
|
|
if len(parts) < 1 {
|
|
return "", errors.New("invalid path")
|
|
}
|
|
first = strings.TrimSuffix(parts[0], ".git")
|
|
repo, err = store.GetRepo(first)
|
|
if err == nil {
|
|
return repo.ProjectID, nil
|
|
}
|
|
if len(parts) < 2 {
|
|
return "", errors.New("invalid path")
|
|
}
|
|
second = strings.TrimSuffix(parts[1], ".git")
|
|
project, err = store.GetProjectBySlug(parts[0])
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
repo, err = store.GetRepoByProjectNameType(project.ID, second, serviceRepoType(service, parts[1]))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return repo.ProjectID, nil
|
|
}
|
|
|
|
func serviceRepoType(service serviceKind, segment string) string {
|
|
if service == serviceGit || strings.HasSuffix(segment, ".git") {
|
|
return "git"
|
|
}
|
|
return "rpm"
|
|
}
|
|
|
|
func defaultListenerPolicy() listenerAuthPolicy {
|
|
var policy listenerAuthPolicy
|
|
policy = listenerAuthPolicy{
|
|
ReadMode: "auth",
|
|
WriteMode: "auth",
|
|
RequirePrincipalForWrite: true,
|
|
CertAllowlist: make(map[string]bool),
|
|
ClientCertPermissionMatch: "all",
|
|
ClientCertRequiredPermissions: []string{},
|
|
}
|
|
return policy
|
|
}
|
|
|
|
func compileListenerAuthPolicy(item models.TLSAuthPolicy, store *db.Store) (listenerAuthPolicy, error) {
|
|
var policy listenerAuthPolicy
|
|
var i int
|
|
var fp string
|
|
var cert models.PKICert
|
|
var err error
|
|
|
|
policy = defaultListenerPolicy()
|
|
policy.ReadMode = strings.ToLower(strings.TrimSpace(item.ReadMode))
|
|
if policy.ReadMode == "" {
|
|
policy.ReadMode = "auth"
|
|
}
|
|
policy.WriteMode = strings.ToLower(strings.TrimSpace(item.WriteMode))
|
|
if policy.WriteMode == "" {
|
|
policy.WriteMode = "auth"
|
|
}
|
|
policy.RequirePrincipalForWrite = item.RequirePrincipalForWrite
|
|
policy.ClientCertRequiredPermissions = append(policy.ClientCertRequiredPermissions, item.RequiredPermissions...)
|
|
policy.ClientCertPermissionMatch = strings.TrimSpace(item.PermissionMatch)
|
|
if policy.ClientCertPermissionMatch == "" {
|
|
policy.ClientCertPermissionMatch = "all"
|
|
}
|
|
policy.ClientCertRequiredScope = strings.TrimSpace(item.RequiredScope)
|
|
for i = 0; i < len(item.AllowedCertFingerprints); i++ {
|
|
fp = strings.ToLower(strings.TrimSpace(item.AllowedCertFingerprints[i]))
|
|
if fp == "" {
|
|
continue
|
|
}
|
|
policy.CertAllowlist[fp] = true
|
|
}
|
|
for i = 0; i < len(item.AllowedPKIClientCertIDs); i++ {
|
|
cert, err = store.GetPKICert(strings.TrimSpace(item.AllowedPKIClientCertIDs[i]))
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
continue
|
|
}
|
|
return policy, err
|
|
}
|
|
fp, err = certFingerprintFromPEM(cert.CertPEM)
|
|
if err != nil {
|
|
return policy, err
|
|
}
|
|
if fp == "" {
|
|
continue
|
|
}
|
|
policy.CertAllowlist[fp] = fp != ""
|
|
}
|
|
return policy, nil
|
|
}
|
|
|
|
func certFingerprintFromPEM(certPEM string) (string, error) {
|
|
var block *pem.Block
|
|
var cert *x509.Certificate
|
|
var err error
|
|
var hash [32]byte
|
|
|
|
block, _ = pem.Decode([]byte(certPEM))
|
|
if block == nil {
|
|
return "", errors.New("failed to decode certificate pem")
|
|
}
|
|
cert, err = x509.ParseCertificate(block.Bytes)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
hash = sha256.Sum256(cert.Raw)
|
|
return strings.ToLower(hex.EncodeToString(hash[:])), nil
|
|
}
|
|
|
|
func listenerPolicyLogLabel(policy listenerAuthPolicy) string {
|
|
return policy.ReadMode + "/" + policy.WriteMode
|
|
}
|
|
|
|
func sessionCookieNameFromServerId(value string) string {
|
|
return strings.ReplaceAll(NormalizeServerId(value), "-", "_") + "_session"
|
|
}
|
|
|
|
func oidcStateCookieNameFromServerId(value string) string {
|
|
return strings.ReplaceAll(NormalizeServerId(value), "-", "_") + "_oidc_state"
|
|
}
|
|
|
|
func NormalizeServerId(value string) string {
|
|
return codit_config.NormalizeServerId(value)
|
|
}
|
|
|
|
func EnvPrefixFromServerId(value string) string {
|
|
return codit_config.EnvPrefixFromServerId(value)
|
|
}
|
|
|
|
func TitleFromServerId(value string) string {
|
|
return codit_config.TitleFromServerId(value)
|
|
}
|
|
|
|
func tcpAddrStrClass(addr string) string {
|
|
var ap netip.AddrPort
|
|
var err error
|
|
|
|
if len(addr) > 0 {
|
|
ap, err = netip.ParseAddrPort(addr)
|
|
if err == nil {
|
|
if ap.Addr().Is6() {
|
|
return "tcp6"
|
|
}
|
|
if ap.Addr().Is4() || ap.Addr().Is4In6() {
|
|
return "tcp4"
|
|
}
|
|
}
|
|
}
|
|
return "tcp"
|
|
}
|
|
|
|
func serveListeners(ctx context.Context, endpoints []listenerEndpoint, handler http.Handler, logger Logger, serverId string) error {
|
|
var listenCtx context.Context
|
|
var cancel context.CancelFunc
|
|
var wg sync.WaitGroup
|
|
var errs chan error
|
|
var servers []*http.Server
|
|
var serversMu sync.Mutex
|
|
var i int
|
|
var ep listenerEndpoint
|
|
var listenErr error
|
|
var shutdown func()
|
|
serverId = NormalizeServerId(serverId)
|
|
if len(endpoints) == 0 {
|
|
return http.ErrServerClosed
|
|
}
|
|
listenCtx, cancel = context.WithCancel(ctx)
|
|
defer cancel()
|
|
errs = make(chan error, len(endpoints))
|
|
shutdown = func() {
|
|
var copied []*http.Server
|
|
var server *http.Server
|
|
var shutdownCtx context.Context
|
|
var shutdownCancel context.CancelFunc
|
|
|
|
serversMu.Lock()
|
|
copied = append(copied, servers...)
|
|
serversMu.Unlock()
|
|
for _, server = range copied {
|
|
shutdownCtx, shutdownCancel = context.WithTimeout(context.Background(), 5*time.Second)
|
|
_ = server.Shutdown(shutdownCtx)
|
|
shutdownCancel()
|
|
}
|
|
}
|
|
for i = 0; i < len(endpoints); i++ {
|
|
ep = endpoints[i]
|
|
if ep.IsHTTPS {
|
|
logger.Write("", LOG_INFO, "%s server listener=%s https://%s", serverId, ep.Name, ep.Addr)
|
|
} else {
|
|
logger.Write("", LOG_INFO, "%s server listener=%s %s", serverId, ep.Name, ep.Addr)
|
|
}
|
|
wg.Add(1)
|
|
go func(endpoint listenerEndpoint) {
|
|
var err error
|
|
var server *http.Server
|
|
var l *log.Logger
|
|
var listener net.Listener
|
|
var network string
|
|
defer wg.Done()
|
|
|
|
l = log.New(&server_http_log_writer{l: logger, id: "", depth: +2}, "", 0)
|
|
network = tcpAddrStrClass(endpoint.Addr)
|
|
if endpoint.IsHTTPS {
|
|
server = &http.Server{
|
|
Addr: endpoint.Addr,
|
|
Handler: handler,
|
|
TLSConfig: endpoint.TLSConfig,
|
|
ErrorLog: l,
|
|
ConnContext: connContextWithListenerIdentity(endpoint.ListenerKind, endpoint.ListenerID),
|
|
}
|
|
} else {
|
|
server = &http.Server{
|
|
Addr: endpoint.Addr,
|
|
Handler: handler,
|
|
ErrorLog: l,
|
|
ConnContext: connContextWithListenerIdentity(endpoint.ListenerKind, endpoint.ListenerID),
|
|
}
|
|
}
|
|
serversMu.Lock()
|
|
servers = append(servers, server)
|
|
serversMu.Unlock()
|
|
listener, err = net.Listen(network, endpoint.Addr)
|
|
if err != nil {
|
|
errs <- fmt.Errorf("listen %s %s: %w", network, endpoint.Addr, err)
|
|
return
|
|
}
|
|
if endpoint.IsHTTPS {
|
|
err = server.ServeTLS(listener, "", "")
|
|
} else {
|
|
err = server.Serve(listener)
|
|
}
|
|
errs <- err
|
|
}(ep)
|
|
}
|
|
select {
|
|
case <-listenCtx.Done():
|
|
shutdown()
|
|
wg.Wait()
|
|
return nil
|
|
case listenErr = <-errs:
|
|
cancel()
|
|
shutdown()
|
|
wg.Wait()
|
|
if errors.Is(listenErr, http.ErrServerClosed) {
|
|
return nil
|
|
}
|
|
}
|
|
return listenErr
|
|
}
|
|
|
|
func buildListenerEndpoints(name string, listenerKind string, listenerID string, settings models.TLSSettings, store *db.Store) ([]listenerEndpoint, error) {
|
|
var out []listenerEndpoint
|
|
var tlsConfig *tls.Config
|
|
var i int
|
|
var err error
|
|
settings.HTTPAddrs = codit_config.NormalizeHTTPAddrs(settings.HTTPAddrs)
|
|
settings.HTTPSAddrs = codit_config.NormalizeHTTPAddrs(settings.HTTPSAddrs)
|
|
if len(settings.HTTPAddrs) == 0 && len(settings.HTTPSAddrs) == 0 {
|
|
return out, nil
|
|
}
|
|
if len(settings.HTTPSAddrs) > 0 {
|
|
tlsConfig, err = buildServerTLSConfig(settings, store)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
for i = 0; i < len(settings.HTTPAddrs); i++ {
|
|
out = append(out, listenerEndpoint{Name: name, Addr: settings.HTTPAddrs[i], IsHTTPS: false, TLSConfig: nil, ListenerKind: listenerKind, ListenerID: listenerID})
|
|
}
|
|
for i = 0; i < len(settings.HTTPSAddrs); i++ {
|
|
out = append(out, listenerEndpoint{Name: name, Addr: settings.HTTPSAddrs[i], IsHTTPS: true, TLSConfig: tlsConfig, ListenerKind: listenerKind, ListenerID: listenerID})
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func buildServerTLSConfig(settings models.TLSSettings, store *db.Store) (*tls.Config, error) {
|
|
var cert tls.Certificate
|
|
var caPool *x509.CertPool
|
|
var clientAuth tls.ClientAuthType
|
|
var minVersion uint16
|
|
var err error
|
|
cert, err = loadServerCertificate(settings, store)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
clientAuth = parseTLSClientAuth(settings.TLSClientAuth)
|
|
caPool, err = loadClientCAPool(settings, store)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if (clientAuth == tls.RequireAndVerifyClientCert || clientAuth == tls.VerifyClientCertIfGiven) && caPool == nil {
|
|
return nil, errors.New("client auth is enabled but no client CA configured")
|
|
}
|
|
minVersion = parseTLSMinVersion(settings.TLSMinVersion)
|
|
return &tls.Config{
|
|
MinVersion: minVersion,
|
|
Certificates: []tls.Certificate{cert},
|
|
ClientAuth: clientAuth,
|
|
ClientCAs: caPool,
|
|
}, nil
|
|
}
|
|
|
|
func loadServerCertificate(settings models.TLSSettings, store *db.Store) (tls.Certificate, error) {
|
|
var cert tls.Certificate
|
|
var certData []byte
|
|
var keyData []byte
|
|
var pkiCert models.PKICert
|
|
var err error
|
|
var source string
|
|
var certFile string
|
|
var keyFile string
|
|
|
|
source = strings.ToLower(strings.TrimSpace(settings.TLSServerCertSource))
|
|
if source == "" {
|
|
source = "pki"
|
|
}
|
|
switch source {
|
|
case "files":
|
|
certFile = strings.TrimSpace(settings.TLSCertFile)
|
|
keyFile = strings.TrimSpace(settings.TLSKeyFile)
|
|
if certFile == "" || keyFile == "" {
|
|
return cert, errors.New("ctl.tls.cert-file and ctl.tls.key-file are required when ctl.tls.server-cert-source is files")
|
|
}
|
|
cert, err = tls.LoadX509KeyPair(certFile, keyFile)
|
|
if err != nil {
|
|
return cert, err
|
|
}
|
|
return cert, nil
|
|
case "pki":
|
|
if strings.TrimSpace(settings.TLSPKIServerCertID) == "" {
|
|
return cert, errors.New("ctl.tls.pki-server-cert-id is required when ctl.tls.server-cert-source is pki")
|
|
}
|
|
default:
|
|
return cert, errors.New("tls_server_cert_source must be files or pki")
|
|
}
|
|
pkiCert, err = store.GetPKICert(strings.TrimSpace(settings.TLSPKIServerCertID))
|
|
if err != nil {
|
|
return cert, err
|
|
}
|
|
certData = []byte(pkiCert.CertPEM)
|
|
keyData = []byte(pkiCert.KeyPEM)
|
|
cert, err = tls.X509KeyPair(certData, keyData)
|
|
if err != nil {
|
|
return cert, err
|
|
}
|
|
return cert, nil
|
|
}
|
|
|
|
func loadClientCAPool(settings models.TLSSettings, store *db.Store) (*x509.CertPool, error) {
|
|
var pool *x509.CertPool
|
|
var pkiCA models.PKICA
|
|
var ok bool
|
|
var err error
|
|
pool = nil
|
|
if strings.TrimSpace(settings.TLSPKIClientCAID) != "" {
|
|
pkiCA, err = store.GetPKICA(strings.TrimSpace(settings.TLSPKIClientCAID))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if pool == nil {
|
|
pool = x509.NewCertPool()
|
|
}
|
|
ok = pool.AppendCertsFromPEM([]byte(pkiCA.CertPEM))
|
|
if !ok {
|
|
return nil, errors.New("failed to parse tls_pki_client_ca_id certificate")
|
|
}
|
|
}
|
|
return pool, nil
|
|
}
|
|
|
|
func parseTLSClientAuth(value string) tls.ClientAuthType {
|
|
var v string
|
|
v = strings.ToLower(strings.TrimSpace(value))
|
|
switch v {
|
|
case "request":
|
|
return tls.RequestClientCert
|
|
case "require":
|
|
return tls.RequireAnyClientCert
|
|
case "verify_if_given":
|
|
return tls.VerifyClientCertIfGiven
|
|
case "require_and_verify":
|
|
return tls.RequireAndVerifyClientCert
|
|
default:
|
|
return tls.NoClientCert
|
|
}
|
|
}
|
|
|
|
func parseTLSMinVersion(value string) uint16 {
|
|
var v string
|
|
v = strings.ToLower(strings.TrimSpace(value))
|
|
switch v {
|
|
case "1.0", "tls1.0":
|
|
return tls.VersionTLS10
|
|
case "1.1", "tls1.1":
|
|
return tls.VersionTLS11
|
|
case "1.3", "tls1.3":
|
|
return tls.VersionTLS13
|
|
default:
|
|
return tls.VersionTLS12
|
|
}
|
|
}
|
|
|
|
func mergeTLSSettingsFromDB(cfg *codit_config.Config, store *db.Store, envPrefix string) error {
|
|
var settings models.TLSSettings
|
|
var envAddrs string
|
|
var envTLSEnabled string
|
|
var envServerSource string
|
|
var envCertFile string
|
|
var envKeyFile string
|
|
var envPKIServerCertID string
|
|
var envClientAuth string
|
|
var envClientCAFile string
|
|
var envPKIClientCAID string
|
|
var envTLSMinVersion string
|
|
var hasConfiguredAddrs bool
|
|
var err error
|
|
|
|
hasConfiguredAddrs = len(codit_config.NormalizeHTTPAddrs(cfg.CTL.Service.Addrs)) > 0
|
|
settings, err = store.GetTLSSettings()
|
|
if err != nil { return err }
|
|
|
|
envAddrs = strings.TrimSpace(os.Getenv(envPrefix + "CTL_SERVICE_ADDRESSES"))
|
|
envTLSEnabled = strings.TrimSpace(os.Getenv(envPrefix + "CTL_TLS_ENABLED"))
|
|
envServerSource = strings.TrimSpace(os.Getenv(envPrefix + "CTL_TLS_SERVER_CERT_SOURCE"))
|
|
envCertFile = strings.TrimSpace(os.Getenv(envPrefix + "CTL_TLS_CERT_FILE"))
|
|
envKeyFile = strings.TrimSpace(os.Getenv(envPrefix + "CTL_TLS_KEY_FILE"))
|
|
envPKIServerCertID = strings.TrimSpace(os.Getenv(envPrefix + "CTL_TLS_PKI_SERVER_CERT_ID"))
|
|
envClientAuth = strings.TrimSpace(os.Getenv(envPrefix + "CTL_TLS_CLIENT_AUTH"))
|
|
envClientCAFile = strings.TrimSpace(os.Getenv(envPrefix + "CTL_TLS_CLIENT_CA_FILE"))
|
|
envPKIClientCAID = strings.TrimSpace(os.Getenv(envPrefix + "CTL_TLS_PKI_CLIENT_CA_ID"))
|
|
envTLSMinVersion = strings.TrimSpace(os.Getenv(envPrefix + "CTL_TLS_MIN_VERSION"))
|
|
if len(settings.HTTPSAddrs) > 0 && envAddrs == "" && !hasConfiguredAddrs {
|
|
cfg.CTL.Service.Addrs = settings.HTTPSAddrs
|
|
} else if len(settings.HTTPAddrs) > 0 && envAddrs == "" && !hasConfiguredAddrs {
|
|
cfg.CTL.Service.Addrs = settings.HTTPAddrs
|
|
}
|
|
if envTLSEnabled == "" && !hasConfiguredAddrs {
|
|
cfg.CTL.TLS.Enabled = len(settings.HTTPSAddrs) > 0
|
|
}
|
|
if strings.TrimSpace(settings.TLSServerCertSource) != "" && envServerSource == "" {
|
|
cfg.CTL.TLS.ServerCertSource = settings.TLSServerCertSource
|
|
}
|
|
if strings.TrimSpace(settings.TLSCertFile) != "" && envCertFile == "" {
|
|
cfg.CTL.TLS.CertFile = settings.TLSCertFile
|
|
}
|
|
if strings.TrimSpace(settings.TLSKeyFile) != "" && envKeyFile == "" {
|
|
cfg.CTL.TLS.KeyFile = settings.TLSKeyFile
|
|
}
|
|
if strings.TrimSpace(settings.TLSPKIServerCertID) != "" && envPKIServerCertID == "" {
|
|
cfg.CTL.TLS.PKIServerCertID = settings.TLSPKIServerCertID
|
|
}
|
|
if strings.TrimSpace(settings.TLSClientAuth) != "" && envClientAuth == "" {
|
|
cfg.CTL.TLS.ClientAuth = settings.TLSClientAuth
|
|
}
|
|
if strings.TrimSpace(settings.TLSClientCAFile) != "" && envClientCAFile == "" {
|
|
cfg.CTL.TLS.ClientCAFile = settings.TLSClientCAFile
|
|
}
|
|
if strings.TrimSpace(settings.TLSPKIClientCAID) != "" && envPKIClientCAID == "" {
|
|
cfg.CTL.TLS.PKIClientCAID = settings.TLSPKIClientCAID
|
|
}
|
|
if strings.TrimSpace(settings.TLSMinVersion) != "" && envTLSMinVersion == "" {
|
|
cfg.CTL.TLS.MinVersion = settings.TLSMinVersion
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func serveIndexFile(w http.ResponseWriter, r *http.Request, path string, logger Logger, serverId string, serverTitle string, siteName string) {
|
|
var f *os.File
|
|
var err error
|
|
|
|
f, err = os.Open(path)
|
|
if err != nil {
|
|
http.Error(w, "file not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
defer f.Close()
|
|
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
|
|
// the content may get modified. the actual content-length after transformation may
|
|
// not match the length of the original response.
|
|
// remove the Content-Length header to drive the http library to switch to
|
|
// the Chunked encoding.
|
|
w.Header().Del("Content-Length")
|
|
|
|
_, err = io.Copy(w,
|
|
transform.NewReader(f, transform.Chain(
|
|
NewStringTransformer("%SERVER_ID%", serverId),
|
|
NewStringTransformer("%SERVER_TITLE%", serverTitle),
|
|
NewStringTransformer("%SITE_NAME%", siteName))))
|
|
if err != nil {
|
|
if logger != nil {
|
|
logger.Write("", LOG_WARN, "stream response failed path=%s remote=%s err=%v", path, r.RemoteAddr, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func spaHandler(root string, logger Logger, serverId string, serverTitle string, siteName string) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
var path string
|
|
var reqPath string
|
|
var cleaned string
|
|
var info os.FileInfo
|
|
var indexPath string
|
|
var err error
|
|
|
|
reqPath = strings.TrimPrefix(r.URL.Path, "/")
|
|
cleaned = filepath.Clean(reqPath)
|
|
if cleaned == "." { cleaned = "" }
|
|
if strings.HasPrefix(cleaned, "..") {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
indexPath = filepath.Join(root, "index.html")
|
|
path = filepath.Join(root, cleaned)
|
|
info, err = os.Stat(path)
|
|
if err == nil && !info.IsDir() {
|
|
if path == indexPath {
|
|
serveIndexFile(w, r, indexPath, logger, serverId, serverTitle, siteName)
|
|
} else {
|
|
http.ServeFile(w, r, path)
|
|
}
|
|
return
|
|
}
|
|
if filepath.Ext(cleaned) != "" {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
_, err = os.Stat(indexPath)
|
|
if err == nil {
|
|
serveIndexFile(w, r, indexPath, logger, serverId, serverTitle, siteName)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}
|
|
}
|
|
|
|
func bootstrapAdmin(store *db.Store, envPrefix string) error {
|
|
var txStore *db.Store
|
|
var bootstrap string
|
|
var parts []string
|
|
var username string
|
|
var password string
|
|
var hash string
|
|
var err error
|
|
|
|
bootstrap = os.Getenv(envPrefix + "BOOTSTRAP_ADMIN")
|
|
if bootstrap == "" {
|
|
return nil
|
|
}
|
|
|
|
parts = strings.SplitN(bootstrap, ":", 2)
|
|
if len(parts) != 2 {
|
|
return nil
|
|
}
|
|
|
|
username = strings.TrimSpace(parts[0])
|
|
password = strings.TrimSpace(parts[1])
|
|
if username == "" || password == "" {
|
|
return nil
|
|
}
|
|
|
|
txStore, err = store.BeginStore(context.Background())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, _, err = txStore.GetUserByUsername(username)
|
|
if err == nil {
|
|
_ = txStore.Rollback()
|
|
return nil
|
|
}
|
|
if err != sql.ErrNoRows {
|
|
_ = txStore.Rollback()
|
|
return err
|
|
}
|
|
|
|
hash, err = auth.HashPassword(password)
|
|
if err != nil {
|
|
_ = txStore.Rollback()
|
|
return err
|
|
}
|
|
|
|
_, err = txStore.CreateUser(models.User{
|
|
Username: username,
|
|
DisplayName: username,
|
|
Email: username + "@local",
|
|
IsAdmin: true,
|
|
AuthSource: "db",
|
|
}, hash)
|
|
if err != nil {
|
|
_ = txStore.Rollback()
|
|
return err
|
|
}
|
|
err = txStore.Commit()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|