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 }