390 lines
14 KiB
Go
390 lines
14 KiB
Go
package handlers
|
|
|
|
import "database/sql"
|
|
import "errors"
|
|
import "net/http"
|
|
import "strings"
|
|
import "time"
|
|
|
|
import "codit/internal/middleware"
|
|
import "codit/internal/models"
|
|
import "codit/internal/util"
|
|
|
|
type sshPrincipalGrantTargetRequest struct {
|
|
TargetType string `json:"target_type"`
|
|
TargetID string `json:"target_id"`
|
|
}
|
|
|
|
type sshPrincipalGrantRequest struct {
|
|
Name string `json:"name"`
|
|
Principal string `json:"principal"`
|
|
Principals []string `json:"principals"`
|
|
ValidAfter int64 `json:"valid_after"`
|
|
ValidBefore int64 `json:"valid_before"`
|
|
MaxCertValidSeconds int64 `json:"max_cert_valid_seconds"`
|
|
MaxUses int64 `json:"max_uses"`
|
|
Disabled bool `json:"disabled"`
|
|
Reason string `json:"reason"`
|
|
Targets []sshPrincipalGrantTargetRequest `json:"targets"`
|
|
}
|
|
|
|
type sshPrincipalGrantTargetSummary struct {
|
|
GrantID string `json:"grant_id"`
|
|
TargetType string `json:"target_type"`
|
|
TargetID string `json:"target_id"`
|
|
TargetName string `json:"target_name"`
|
|
TargetActive bool `json:"target_active"`
|
|
CreatedAt int64 `json:"created_at"`
|
|
}
|
|
|
|
type sshPrincipalGrantSummary struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Principal string `json:"principal"`
|
|
Principals []string `json:"principals"`
|
|
ValidAfter int64 `json:"valid_after"`
|
|
ValidBefore int64 `json:"valid_before"`
|
|
MaxCertValidSeconds int64 `json:"max_cert_valid_seconds"`
|
|
MaxUses int64 `json:"max_uses"`
|
|
UsedCount int64 `json:"used_count"`
|
|
Disabled bool `json:"disabled"`
|
|
Reason string `json:"reason"`
|
|
CreatedByUserID string `json:"created_by_user_id"`
|
|
CreatedAt int64 `json:"created_at"`
|
|
UpdatedAt int64 `json:"updated_at"`
|
|
LastUsedAt int64 `json:"last_used_at"`
|
|
Targets []sshPrincipalGrantTargetSummary `json:"targets"`
|
|
}
|
|
|
|
func (api *API) ListSSHPrincipalGrants(w http.ResponseWriter, r *http.Request, _ map[string]string) {
|
|
var targetType string
|
|
var targetID string
|
|
var items []models.SSHPrincipalGrant
|
|
var out []sshPrincipalGrantSummary
|
|
var i int
|
|
var err error
|
|
if !api.requireAdmin(w, r) {
|
|
return
|
|
}
|
|
targetType = strings.TrimSpace(r.URL.Query().Get("target_type"))
|
|
targetID = strings.TrimSpace(r.URL.Query().Get("target_id"))
|
|
items, err = api.store(r).ListSSHPrincipalGrants(targetType, targetID)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
for i = 0; i < len(items); i++ {
|
|
out = append(out, buildSSHPrincipalGrantSummary(items[i]))
|
|
}
|
|
WriteJSON(w, http.StatusOK, out)
|
|
}
|
|
|
|
func (api *API) ListSSHPrincipalGrantsForSelf(w http.ResponseWriter, r *http.Request, _ map[string]string) {
|
|
var user models.User
|
|
var ok bool
|
|
var items []models.SSHPrincipalGrant
|
|
var out []sshPrincipalGrantSummary
|
|
var now int64
|
|
var i int
|
|
var err error
|
|
user, ok = middleware.UserFromContext(r.Context())
|
|
if !ok || user.Disabled {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
now = time.Now().UTC().Unix()
|
|
items, err = api.store(r).ListActiveSSHPrincipalGrantsForUser(user.ID, now)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
for i = 0; i < len(items); i++ {
|
|
out = append(out, buildSSHPrincipalGrantSummary(items[i]))
|
|
}
|
|
WriteJSON(w, http.StatusOK, out)
|
|
}
|
|
|
|
func (api *API) CreateSSHPrincipalGrant(w http.ResponseWriter, r *http.Request, _ map[string]string) {
|
|
var req sshPrincipalGrantRequest
|
|
var currentUser models.User
|
|
var hasCurrentUser bool
|
|
var name string
|
|
var principals []string
|
|
var maxCertValidSeconds int64
|
|
var maxUses int64
|
|
var targets []models.SSHPrincipalGrantTarget
|
|
var item models.SSHPrincipalGrant
|
|
var err error
|
|
if !api.requireAdmin(w, r) {
|
|
return
|
|
}
|
|
err = DecodeJSON(r, &req)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request"})
|
|
return
|
|
}
|
|
name, principals, err = normalizeSSHPrincipalGrantIdentity(req.Name, req.Principal, req.Principals)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
maxCertValidSeconds, err = normalizeSSHPrincipalGrantMaxCertValidSeconds(req.MaxCertValidSeconds)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
maxUses, err = normalizeSSHPrincipalGrantMaxUses(req.MaxUses)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
err = validateSSHPrincipalGrantWindow(req.ValidAfter, req.ValidBefore)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
targets, err = normalizeSSHPrincipalGrantTargets(req.Targets)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
currentUser, hasCurrentUser = middleware.UserFromContext(r.Context())
|
|
item = models.SSHPrincipalGrant{
|
|
Name: name,
|
|
Principals: principals,
|
|
ValidAfter: req.ValidAfter,
|
|
ValidBefore: req.ValidBefore,
|
|
MaxCertValidSeconds: maxCertValidSeconds,
|
|
MaxUses: maxUses,
|
|
Disabled: req.Disabled,
|
|
Reason: strings.TrimSpace(req.Reason),
|
|
Targets: targets,
|
|
}
|
|
if hasCurrentUser {
|
|
item.CreatedByUserID = currentUser.ID
|
|
}
|
|
item, err = api.store(r).CreateSSHPrincipalGrant(item)
|
|
if err != nil {
|
|
api.Logger.Write(logIDSSHCA, util.LOG_WARN, "grant create failed name=%s err=%v", name, err)
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
api.Logger.Write(logIDSSHCA, util.LOG_INFO, "grant create success id=%s name=%s principals=%d targets=%d", item.ID, item.Name, len(item.Principals), len(item.Targets))
|
|
WriteJSON(w, http.StatusCreated, buildSSHPrincipalGrantSummary(item))
|
|
}
|
|
|
|
func (api *API) UpdateSSHPrincipalGrant(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var req sshPrincipalGrantRequest
|
|
var existing models.SSHPrincipalGrant
|
|
var name string
|
|
var principals []string
|
|
var maxCertValidSeconds int64
|
|
var maxUses int64
|
|
var targets []models.SSHPrincipalGrantTarget
|
|
var item models.SSHPrincipalGrant
|
|
var err error
|
|
if !api.requireAdmin(w, r) {
|
|
return
|
|
}
|
|
err = DecodeJSON(r, &req)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request"})
|
|
return
|
|
}
|
|
existing, err = api.store(r).GetSSHPrincipalGrant(params["id"])
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "principal grant not found"})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
name, principals, err = normalizeSSHPrincipalGrantIdentity(req.Name, req.Principal, req.Principals)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
maxCertValidSeconds, err = normalizeSSHPrincipalGrantMaxCertValidSeconds(req.MaxCertValidSeconds)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
maxUses, err = normalizeSSHPrincipalGrantMaxUses(req.MaxUses)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
err = validateSSHPrincipalGrantWindow(req.ValidAfter, req.ValidBefore)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
targets, err = normalizeSSHPrincipalGrantTargets(req.Targets)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
item = existing
|
|
item.Name = name
|
|
item.Principals = principals
|
|
item.ValidAfter = req.ValidAfter
|
|
item.ValidBefore = req.ValidBefore
|
|
item.MaxCertValidSeconds = maxCertValidSeconds
|
|
item.MaxUses = maxUses
|
|
item.Disabled = req.Disabled
|
|
item.Reason = strings.TrimSpace(req.Reason)
|
|
item.Targets = targets
|
|
item, err = api.store(r).UpdateSSHPrincipalGrant(item)
|
|
if err != nil {
|
|
api.Logger.Write(logIDSSHCA, util.LOG_WARN, "grant update failed id=%s err=%v", params["id"], err)
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
api.Logger.Write(logIDSSHCA, util.LOG_INFO, "grant update success id=%s name=%s principals=%d targets=%d", item.ID, item.Name, len(item.Principals), len(item.Targets))
|
|
WriteJSON(w, http.StatusOK, buildSSHPrincipalGrantSummary(item))
|
|
}
|
|
|
|
func (api *API) DeleteSSHPrincipalGrant(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var err error
|
|
if !api.requireAdmin(w, r) {
|
|
return
|
|
}
|
|
err = api.store(r).DeleteSSHPrincipalGrant(params["id"])
|
|
if err != nil {
|
|
api.Logger.Write(logIDSSHCA, util.LOG_WARN, "grant delete failed id=%s err=%v", params["id"], err)
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
api.Logger.Write(logIDSSHCA, util.LOG_INFO, "grant delete success id=%s", params["id"])
|
|
WriteJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
|
|
}
|
|
|
|
func buildSSHPrincipalGrantSummary(item models.SSHPrincipalGrant) sshPrincipalGrantSummary {
|
|
var outTargets []sshPrincipalGrantTargetSummary
|
|
var i int
|
|
for i = 0; i < len(item.Targets); i++ {
|
|
outTargets = append(outTargets, sshPrincipalGrantTargetSummary{
|
|
GrantID: item.Targets[i].GrantID,
|
|
TargetType: item.Targets[i].TargetType,
|
|
TargetID: item.Targets[i].TargetID,
|
|
TargetName: item.Targets[i].TargetName,
|
|
TargetActive: item.Targets[i].TargetActive,
|
|
CreatedAt: item.Targets[i].CreatedAt,
|
|
})
|
|
}
|
|
return sshPrincipalGrantSummary{
|
|
ID: item.ID,
|
|
Name: item.Name,
|
|
Principal: item.Name,
|
|
Principals: item.Principals,
|
|
ValidAfter: item.ValidAfter,
|
|
ValidBefore: item.ValidBefore,
|
|
MaxCertValidSeconds: item.MaxCertValidSeconds,
|
|
MaxUses: item.MaxUses,
|
|
UsedCount: item.UsedCount,
|
|
Disabled: item.Disabled,
|
|
Reason: item.Reason,
|
|
CreatedByUserID: item.CreatedByUserID,
|
|
CreatedAt: item.CreatedAt,
|
|
UpdatedAt: item.UpdatedAt,
|
|
LastUsedAt: item.LastUsedAt,
|
|
Targets: outTargets,
|
|
}
|
|
}
|
|
|
|
func normalizeSSHPrincipalGrantIdentity(rawName string, rawPrincipal string, rawPrincipals []string) (string, []string, error) {
|
|
var name string
|
|
var principals []string
|
|
var i int
|
|
name = strings.TrimSpace(rawName)
|
|
if name == "" {
|
|
name = strings.TrimSpace(rawPrincipal)
|
|
}
|
|
principals = normalizeSSHPrincipals(rawPrincipals)
|
|
if len(principals) == 0 {
|
|
if strings.TrimSpace(rawPrincipal) != "" {
|
|
principals = []string{strings.TrimSpace(rawPrincipal)}
|
|
}
|
|
}
|
|
if name == "" {
|
|
return "", nil, errors.New("name is required")
|
|
}
|
|
if len(principals) == 0 {
|
|
return "", nil, errors.New("at least one principal is required")
|
|
}
|
|
if strings.ContainsAny(name, "\t\r\n") {
|
|
return "", nil, errors.New("name must not contain tabs or newlines")
|
|
}
|
|
for i = 0; i < len(principals); i++ {
|
|
if strings.ContainsAny(principals[i], " \t\r\n,") {
|
|
return "", nil, errors.New("principal names must not contain spaces or commas")
|
|
}
|
|
}
|
|
return name, principals, nil
|
|
}
|
|
|
|
func normalizeSSHPrincipalGrantMaxCertValidSeconds(raw int64) (int64, error) {
|
|
var value int64
|
|
value = raw
|
|
if value < 0 {
|
|
return 0, errors.New("max_cert_valid_seconds must be 0 or between 60 and 604800")
|
|
}
|
|
if value == 0 {
|
|
return 0, nil
|
|
}
|
|
if value < 60 || value > 604800 {
|
|
return 0, errors.New("max_cert_valid_seconds must be 0 or between 60 and 604800")
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
func normalizeSSHPrincipalGrantMaxUses(raw int64) (int64, error) {
|
|
var value int64
|
|
value = raw
|
|
if value < 0 {
|
|
return 0, errors.New("max_uses must be 0 or greater")
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
func validateSSHPrincipalGrantWindow(validAfter int64, validBefore int64) error {
|
|
if validAfter > 0 && validBefore > 0 && validAfter > validBefore {
|
|
return errors.New("valid_after must be less than or equal to valid_before")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func normalizeSSHPrincipalGrantTargets(raw []sshPrincipalGrantTargetRequest) ([]models.SSHPrincipalGrantTarget, error) {
|
|
var out []models.SSHPrincipalGrantTarget
|
|
var dedupe map[string]bool
|
|
var key string
|
|
var targetType string
|
|
var targetID string
|
|
var i int
|
|
dedupe = map[string]bool{}
|
|
for i = 0; i < len(raw); i++ {
|
|
targetType = strings.ToLower(strings.TrimSpace(raw[i].TargetType))
|
|
targetID = strings.TrimSpace(raw[i].TargetID)
|
|
if targetType != "user" && targetType != "group" {
|
|
return nil, errors.New("target_type must be user or group")
|
|
}
|
|
if targetID == "" {
|
|
return nil, errors.New("target_id is required")
|
|
}
|
|
key = targetType + ":" + targetID
|
|
if dedupe[key] {
|
|
continue
|
|
}
|
|
dedupe[key] = true
|
|
out = append(out, models.SSHPrincipalGrantTarget{
|
|
TargetType: targetType,
|
|
TargetID: targetID,
|
|
})
|
|
}
|
|
if len(out) == 0 {
|
|
return nil, errors.New("at least one target is required")
|
|
}
|
|
return out, nil
|
|
}
|