package db import "database/sql" import "encoding/json" import "errors" import "sort" import "strings" import "time" import "codit/internal/models" import "codit/internal/util" func (s *Store) ListSSHServers() ([]models.SSHServer, error) { var rows *sql.Rows var err error var items []models.SSHServer var item models.SSHServer var tagsJSON string rows, err = s.Query(`SELECT public_id, name, host, port, description, tags_json, enabled, created_by_kind, created_by_subject_id, created_by_subject_name, created_at, updated_at FROM ssh_servers ORDER BY name, host, port`) if err != nil { return nil, err } defer rows.Close() for rows.Next() { err = rows.Scan(&item.ID, &item.Name, &item.Host, &item.Port, &item.Description, &tagsJSON, &item.Enabled, &item.CreatedByKind, &item.CreatedBySubjectID, &item.CreatedBySubjectName, &item.CreatedAt, &item.UpdatedAt) if err != nil { return nil, err } item.Tags, err = decodeStringList(tagsJSON) if err != nil { return nil, err } items = append(items, item) } err = rows.Err() if err != nil { return nil, err } return items, nil } func (s *Store) GetSSHServer(id string) (models.SSHServer, error) { var row *sql.Row var item models.SSHServer var tagsJSON string var err error row = s.QueryRow(`SELECT public_id, name, host, port, description, tags_json, enabled, created_by_kind, created_by_subject_id, created_by_subject_name, created_at, updated_at FROM ssh_servers WHERE public_id = ?`, strings.TrimSpace(id)) err = row.Scan(&item.ID, &item.Name, &item.Host, &item.Port, &item.Description, &tagsJSON, &item.Enabled, &item.CreatedByKind, &item.CreatedBySubjectID, &item.CreatedBySubjectName, &item.CreatedAt, &item.UpdatedAt) if err != nil { return item, err } item.Tags, err = decodeStringList(tagsJSON) if err != nil { return item, err } return item, nil } func (s *Store) ListSSHServersForUser(userID string) ([]models.SSHServer, error) { var rows *sql.Rows var err error var items []models.SSHServer var item models.SSHServer var tagsJSON string var trimmedUserID string trimmedUserID = strings.TrimSpace(userID) rows, err = s.Query(`SELECT public_id, name, host, port, description, tags_json, enabled, created_by_kind, created_by_subject_id, created_by_subject_name, created_at, updated_at FROM ssh_servers WHERE created_by_kind = 'user' AND created_by_subject_id = ? ORDER BY name, host, port`, trimmedUserID) if err != nil { return nil, err } defer rows.Close() for rows.Next() { err = rows.Scan(&item.ID, &item.Name, &item.Host, &item.Port, &item.Description, &tagsJSON, &item.Enabled, &item.CreatedByKind, &item.CreatedBySubjectID, &item.CreatedBySubjectName, &item.CreatedAt, &item.UpdatedAt) if err != nil { return nil, err } item.Tags, err = decodeStringList(tagsJSON) if err != nil { return nil, err } item.Editable = item.CreatedByKind == "user" && item.CreatedBySubjectID == trimmedUserID items = append(items, item) } err = rows.Err() if err != nil { return nil, err } return items, nil } func (s *Store) GetSSHServerForUser(userID string, id string) (models.SSHServer, error) { var item models.SSHServer var items []models.SSHServer var trimmedID string var i int var err error trimmedID = strings.TrimSpace(id) items, err = s.ListSSHServersForUser(userID) if err != nil { return item, err } for i = 0; i < len(items); i++ { if items[i].ID == trimmedID { return items[i], nil } } return item, sql.ErrNoRows } func (s *Store) GetOwnedSSHServerForUser(userID string, id string) (models.SSHServer, error) { var item models.SSHServer var trimmedUserID string var err error trimmedUserID = strings.TrimSpace(userID) item, err = s.GetSSHServer(strings.TrimSpace(id)) if err != nil { return item, err } item.Editable = item.CreatedByKind == "user" && item.CreatedBySubjectID == trimmedUserID if !item.Editable { return item, sql.ErrNoRows } return item, nil } func (s *Store) CreateSSHServer(item models.SSHServer) (models.SSHServer, error) { var err error var tagsJSON string var now int64 if strings.TrimSpace(item.ID) == "" { item.ID, err = util.NewID() if err != nil { return item, err } } if item.Port <= 0 { item.Port = 22 } item.Tags = normalizeStringList(item.Tags) tagsJSON, err = encodeStringList(item.Tags) if err != nil { return item, err } now = time.Now().UTC().Unix() item.CreatedAt = now item.UpdatedAt = now _, err = s.Exec(`INSERT INTO ssh_servers ( public_id, name, host, port, description, tags_json, enabled, created_by_kind, created_by_subject_id, created_by_subject_name, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, item.ID, strings.TrimSpace(item.Name), strings.TrimSpace(item.Host), item.Port, strings.TrimSpace(item.Description), tagsJSON, item.Enabled, strings.TrimSpace(item.CreatedByKind), strings.TrimSpace(item.CreatedBySubjectID), strings.TrimSpace(item.CreatedBySubjectName), item.CreatedAt, item.UpdatedAt, ) if err != nil { return item, err } return item, nil } func (s *Store) UpdateSSHServer(item models.SSHServer) (models.SSHServer, error) { var err error var tagsJSON string if item.Port <= 0 { item.Port = 22 } item.Tags = normalizeStringList(item.Tags) tagsJSON, err = encodeStringList(item.Tags) if err != nil { return item, err } item.UpdatedAt = time.Now().UTC().Unix() _, err = s.Exec(`UPDATE ssh_servers SET name = ?, host = ?, port = ?, description = ?, tags_json = ?, enabled = ?, updated_at = ? WHERE public_id = ?`, strings.TrimSpace(item.Name), strings.TrimSpace(item.Host), item.Port, strings.TrimSpace(item.Description), tagsJSON, item.Enabled, item.UpdatedAt, strings.TrimSpace(item.ID), ) if err != nil { return item, err } item, err = s.GetSSHServer(item.ID) if err != nil { return item, err } return item, nil } func (s *Store) DeleteSSHServer(id string) error { var err error _, err = s.Exec(`DELETE FROM ssh_servers WHERE public_id = ?`, strings.TrimSpace(id)) return err } func (s *Store) ListSSHAccessProfiles() ([]models.SSHAccessProfile, error) { var rows *sql.Rows var err error var items []models.SSHAccessProfile var item models.SSHAccessProfile var principalsJSON string var grantIDsJSON string var serverTagsJSON string rows, err = s.Query(`SELECT p.public_id, p.server_public_id, p.name, p.description, p.remote_username, p.auth_method, p.owner_scope, p.owner_user_public_id, p.allow_user_edit, p.enabled, COALESCE(p.secret_public_id, ''), p.auth_public_key, p.auth_public_key_fingerprint, COALESCE(p.ssh_user_ca_public_id, ''), p.ssh_principal_mode, p.ssh_principals_json, p.ssh_principal_grant_ids_json, p.default_valid_seconds, p.max_valid_seconds, p.created_by_kind, p.created_by_subject_id, p.created_by_subject_name, p.created_at, p.updated_at, s.public_id, s.name, s.host, s.port, s.description, s.tags_json, s.enabled, s.created_by_kind, s.created_by_subject_id, s.created_by_subject_name, s.created_at, s.updated_at FROM ssh_access_profiles p JOIN ssh_servers s ON s.public_id = p.server_public_id ORDER BY s.name, p.name`) if err != nil { return nil, err } defer rows.Close() for rows.Next() { err = rows.Scan( &item.ID, &item.ServerID, &item.Name, &item.Description, &item.RemoteUsername, &item.AuthMethod, &item.OwnerScope, &item.OwnerUserID, &item.AllowUserEdit, &item.Enabled, &item.SecretID, &item.AuthPublicKey, &item.AuthPublicKeyFingerprint, &item.SSHUserCAID, &item.SSHPrincipalMode, &principalsJSON, &grantIDsJSON, &item.DefaultValidSeconds, &item.MaxValidSeconds, &item.CreatedByKind, &item.CreatedBySubjectID, &item.CreatedBySubjectName, &item.CreatedAt, &item.UpdatedAt, &item.Server.ID, &item.Server.Name, &item.Server.Host, &item.Server.Port, &item.Server.Description, &serverTagsJSON, &item.Server.Enabled, &item.Server.CreatedByKind, &item.Server.CreatedBySubjectID, &item.Server.CreatedBySubjectName, &item.Server.CreatedAt, &item.Server.UpdatedAt, ) if err != nil { return nil, err } item.SSHPrincipals, err = decodeStringList(principalsJSON) if err != nil { return nil, err } item.SSHPrincipalGrantIDs, err = decodeStringList(grantIDsJSON) if err != nil { return nil, err } item.Server.Tags, err = decodeStringList(serverTagsJSON) if err != nil { return nil, err } item.Targets, err = s.listSSHAccessProfileTargets(item.ID) if err != nil { return nil, err } items = append(items, item) } err = rows.Err() if err != nil { return nil, err } return items, nil } func (s *Store) GetSSHAccessProfile(id string) (models.SSHAccessProfile, error) { var row *sql.Row var item models.SSHAccessProfile var err error var principalsJSON string var grantIDsJSON string var serverTagsJSON string row = s.QueryRow(`SELECT p.public_id, p.server_public_id, p.name, p.description, p.remote_username, p.auth_method, p.owner_scope, p.owner_user_public_id, p.allow_user_edit, p.enabled, COALESCE(p.secret_public_id, ''), p.auth_public_key, p.auth_public_key_fingerprint, COALESCE(p.ssh_user_ca_public_id, ''), p.ssh_principal_mode, p.ssh_principals_json, p.ssh_principal_grant_ids_json, p.default_valid_seconds, p.max_valid_seconds, p.created_by_kind, p.created_by_subject_id, p.created_by_subject_name, p.created_at, p.updated_at, s.public_id, s.name, s.host, s.port, s.description, s.tags_json, s.enabled, s.created_by_kind, s.created_by_subject_id, s.created_by_subject_name, s.created_at, s.updated_at FROM ssh_access_profiles p JOIN ssh_servers s ON s.public_id = p.server_public_id WHERE p.public_id = ?`, strings.TrimSpace(id)) err = row.Scan( &item.ID, &item.ServerID, &item.Name, &item.Description, &item.RemoteUsername, &item.AuthMethod, &item.OwnerScope, &item.OwnerUserID, &item.AllowUserEdit, &item.Enabled, &item.SecretID, &item.AuthPublicKey, &item.AuthPublicKeyFingerprint, &item.SSHUserCAID, &item.SSHPrincipalMode, &principalsJSON, &grantIDsJSON, &item.DefaultValidSeconds, &item.MaxValidSeconds, &item.CreatedByKind, &item.CreatedBySubjectID, &item.CreatedBySubjectName, &item.CreatedAt, &item.UpdatedAt, &item.Server.ID, &item.Server.Name, &item.Server.Host, &item.Server.Port, &item.Server.Description, &serverTagsJSON, &item.Server.Enabled, &item.Server.CreatedByKind, &item.Server.CreatedBySubjectID, &item.Server.CreatedBySubjectName, &item.Server.CreatedAt, &item.Server.UpdatedAt, ) if err != nil { return item, err } item.SSHPrincipals, err = decodeStringList(principalsJSON) if err != nil { return item, err } item.SSHPrincipalGrantIDs, err = decodeStringList(grantIDsJSON) if err != nil { return item, err } item.Server.Tags, err = decodeStringList(serverTagsJSON) if err != nil { return item, err } item.Targets, err = s.listSSHAccessProfileTargets(item.ID) if err != nil { return item, err } return item, nil } func (s *Store) CreateSSHAccessProfile(item models.SSHAccessProfile) (models.SSHAccessProfile, error) { var tx *sql.Tx var owned bool var err error var now int64 var principalsJSON string var grantIDsJSON string var i int var target models.SSHAccessProfileTarget var secret models.SSHSecret if strings.TrimSpace(item.Name) == "" { return item, errors.New("name is required") } if strings.TrimSpace(item.ServerID) == "" { return item, errors.New("server_id is required") } if strings.TrimSpace(item.RemoteUsername) == "" { return item, errors.New("remote_username is required") } if strings.TrimSpace(item.AuthMethod) == "" { return item, errors.New("auth_method is required") } if strings.TrimSpace(item.ID) == "" { item.ID, err = util.NewID() if err != nil { return item, err } } if item.DefaultValidSeconds <= 0 { item.DefaultValidSeconds = 3600 } if item.MaxValidSeconds <= 0 { item.MaxValidSeconds = item.DefaultValidSeconds } item.SSHPrincipals = normalizeStringList(item.SSHPrincipals) item.SSHPrincipalGrantIDs = normalizeStringList(item.SSHPrincipalGrantIDs) principalsJSON, err = encodeStringList(item.SSHPrincipals) if err != nil { return item, err } grantIDsJSON, err = encodeStringList(item.SSHPrincipalGrantIDs) if err != nil { return item, err } now = time.Now().UTC().Unix() item.CreatedAt = now item.UpdatedAt = now tx, owned, err = s.begin() if err != nil { return item, err } if strings.TrimSpace(item.SecretPayload) != "" { secret = models.SSHSecret{ ID: item.SecretID, Kind: "private_key", Payload: item.SecretPayload, CreatedByKind: item.CreatedByKind, CreatedBySubjectID: item.CreatedBySubjectID, CreatedBySubjectName: item.CreatedBySubjectName, } secret, err = createSSHSecretTx(tx, secret) if err != nil { rollbackIfOwned(tx, owned) return item, err } item.SecretID = secret.ID } _, err = tx.Exec(`INSERT INTO ssh_access_profiles ( public_id, server_public_id, name, description, remote_username, auth_method, owner_scope, owner_user_public_id, allow_user_edit, enabled, secret_public_id, auth_public_key, auth_public_key_fingerprint, ssh_user_ca_public_id, ssh_principal_mode, ssh_principals_json, ssh_principal_grant_ids_json, default_valid_seconds, max_valid_seconds, created_by_kind, created_by_subject_id, created_by_subject_name, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, item.ID, strings.TrimSpace(item.ServerID), strings.TrimSpace(item.Name), strings.TrimSpace(item.Description), strings.TrimSpace(item.RemoteUsername), strings.TrimSpace(item.AuthMethod), strings.TrimSpace(item.OwnerScope), strings.TrimSpace(item.OwnerUserID), item.AllowUserEdit, item.Enabled, emptyStringToNil(item.SecretID), strings.TrimSpace(item.AuthPublicKey), strings.TrimSpace(item.AuthPublicKeyFingerprint), emptyStringToNil(item.SSHUserCAID), strings.TrimSpace(item.SSHPrincipalMode), principalsJSON, grantIDsJSON, item.DefaultValidSeconds, item.MaxValidSeconds, strings.TrimSpace(item.CreatedByKind), strings.TrimSpace(item.CreatedBySubjectID), strings.TrimSpace(item.CreatedBySubjectName), item.CreatedAt, item.UpdatedAt, ) if err != nil { rollbackIfOwned(tx, owned) return item, err } for i = 0; i < len(item.Targets); i++ { target = item.Targets[i] err = insertSSHAccessProfileTargetTx(tx, item.ID, target.TargetType, target.TargetID, now) if err != nil { rollbackIfOwned(tx, owned) return item, err } } err = commitIfOwned(tx, owned) if err != nil { return item, err } item, err = s.GetSSHAccessProfile(item.ID) if err != nil { return item, err } return item, nil } func (s *Store) UpdateSSHAccessProfile(item models.SSHAccessProfile) (models.SSHAccessProfile, error) { var tx *sql.Tx var owned bool var err error var principalsJSON string var grantIDsJSON string var i int var target models.SSHAccessProfileTarget var secret models.SSHSecret if strings.TrimSpace(item.Name) == "" { return item, errors.New("name is required") } if strings.TrimSpace(item.ServerID) == "" { return item, errors.New("server_id is required") } if strings.TrimSpace(item.RemoteUsername) == "" { return item, errors.New("remote_username is required") } if strings.TrimSpace(item.AuthMethod) == "" { return item, errors.New("auth_method is required") } if item.DefaultValidSeconds <= 0 { item.DefaultValidSeconds = 3600 } if item.MaxValidSeconds <= 0 { item.MaxValidSeconds = item.DefaultValidSeconds } item.SSHPrincipals = normalizeStringList(item.SSHPrincipals) item.SSHPrincipalGrantIDs = normalizeStringList(item.SSHPrincipalGrantIDs) principalsJSON, err = encodeStringList(item.SSHPrincipals) if err != nil { return item, err } grantIDsJSON, err = encodeStringList(item.SSHPrincipalGrantIDs) if err != nil { return item, err } item.UpdatedAt = time.Now().UTC().Unix() tx, owned, err = s.begin() if err != nil { return item, err } if strings.TrimSpace(item.SecretPayload) != "" { secret = models.SSHSecret{ ID: item.SecretID, Kind: "private_key", Payload: item.SecretPayload, CreatedByKind: item.CreatedByKind, CreatedBySubjectID: item.CreatedBySubjectID, CreatedBySubjectName: item.CreatedBySubjectName, } if strings.TrimSpace(item.SecretID) == "" { secret, err = createSSHSecretTx(tx, secret) if err != nil { rollbackIfOwned(tx, owned) return item, err } } else { secret, err = updateSSHSecretTx(tx, secret) if err != nil { rollbackIfOwned(tx, owned) return item, err } } item.SecretID = secret.ID } _, err = tx.Exec(`UPDATE ssh_access_profiles SET server_public_id = ?, name = ?, description = ?, remote_username = ?, auth_method = ?, owner_scope = ?, owner_user_public_id = ?, allow_user_edit = ?, enabled = ?, secret_public_id = ?, auth_public_key = ?, auth_public_key_fingerprint = ?, ssh_user_ca_public_id = ?, ssh_principal_mode = ?, ssh_principals_json = ?, ssh_principal_grant_ids_json = ?, default_valid_seconds = ?, max_valid_seconds = ?, updated_at = ? WHERE public_id = ?`, strings.TrimSpace(item.ServerID), strings.TrimSpace(item.Name), strings.TrimSpace(item.Description), strings.TrimSpace(item.RemoteUsername), strings.TrimSpace(item.AuthMethod), strings.TrimSpace(item.OwnerScope), strings.TrimSpace(item.OwnerUserID), item.AllowUserEdit, item.Enabled, emptyStringToNil(item.SecretID), strings.TrimSpace(item.AuthPublicKey), strings.TrimSpace(item.AuthPublicKeyFingerprint), emptyStringToNil(item.SSHUserCAID), strings.TrimSpace(item.SSHPrincipalMode), principalsJSON, grantIDsJSON, item.DefaultValidSeconds, item.MaxValidSeconds, item.UpdatedAt, strings.TrimSpace(item.ID), ) if err != nil { rollbackIfOwned(tx, owned) return item, err } _, err = tx.Exec(`DELETE FROM ssh_access_profile_targets WHERE profile_public_id = ?`, strings.TrimSpace(item.ID)) if err != nil { rollbackIfOwned(tx, owned) return item, err } for i = 0; i < len(item.Targets); i++ { target = item.Targets[i] err = insertSSHAccessProfileTargetTx(tx, item.ID, target.TargetType, target.TargetID, item.UpdatedAt) if err != nil { rollbackIfOwned(tx, owned) return item, err } } err = commitIfOwned(tx, owned) if err != nil { return item, err } item, err = s.GetSSHAccessProfile(item.ID) if err != nil { return item, err } return item, nil } func (s *Store) DeleteSSHAccessProfile(id string) error { var tx *sql.Tx var owned bool var row *sql.Row var secretID sql.NullString var err error tx, owned, err = s.begin() if err != nil { return err } row = tx.QueryRow(`SELECT secret_public_id FROM ssh_access_profiles WHERE public_id = ?`, strings.TrimSpace(id)) err = row.Scan(&secretID) if err != nil { rollbackIfOwned(tx, owned) return err } _, err = tx.Exec(`DELETE FROM ssh_access_profiles WHERE public_id = ?`, strings.TrimSpace(id)) if err != nil { rollbackIfOwned(tx, owned) return err } if secretID.Valid && strings.TrimSpace(secretID.String) != "" { _, err = tx.Exec(`DELETE FROM ssh_secrets WHERE public_id = ?`, strings.TrimSpace(secretID.String)) if err != nil { rollbackIfOwned(tx, owned) return err } } err = commitIfOwned(tx, owned) if err != nil { return err } return nil } func (s *Store) ListSSHAccessProfilesForUser(userID string) ([]models.SSHAccessProfile, error) { var rows *sql.Rows var err error var items []models.SSHAccessProfile var item models.SSHAccessProfile var principalsJSON string var grantIDsJSON string var serverTagsJSON string var trimmedUserID string trimmedUserID = strings.TrimSpace(userID) rows, err = s.Query(`SELECT DISTINCT p.public_id, p.server_public_id, p.name, p.description, p.remote_username, p.auth_method, p.owner_scope, p.owner_user_public_id, p.allow_user_edit, p.enabled, COALESCE(p.secret_public_id, ''), p.auth_public_key, p.auth_public_key_fingerprint, COALESCE(p.ssh_user_ca_public_id, ''), p.ssh_principal_mode, p.ssh_principals_json, p.ssh_principal_grant_ids_json, p.default_valid_seconds, p.max_valid_seconds, p.created_by_kind, p.created_by_subject_id, p.created_by_subject_name, p.created_at, p.updated_at, s.public_id, s.name, s.host, s.port, s.description, s.tags_json, s.enabled, s.created_by_kind, s.created_by_subject_id, s.created_by_subject_name, s.created_at, s.updated_at FROM ssh_access_profiles p JOIN ssh_servers s ON s.public_id = p.server_public_id LEFT JOIN ssh_access_profile_targets t ON t.profile_public_id = p.public_id LEFT JOIN user_groups ug ON t.target_type = 'group' AND ug.public_id = t.target_public_id LEFT JOIN user_group_members gm ON t.target_type = 'group' AND gm.group_id = ug.id LEFT JOIN users gu ON gm.user_id = gu.id WHERE p.enabled = 1 AND s.enabled = 1 AND ( (p.owner_scope = 'user' AND p.owner_user_public_id = ?) OR ( p.owner_scope = 'admin_shared' AND ( (t.target_type = 'user' AND t.target_public_id = ?) OR (t.target_type = 'group' AND ug.disabled = 0 AND gu.public_id = ?) ) ) ) ORDER BY s.name, p.name`, trimmedUserID, trimmedUserID, trimmedUserID) if err != nil { return nil, err } defer rows.Close() for rows.Next() { err = rows.Scan( &item.ID, &item.ServerID, &item.Name, &item.Description, &item.RemoteUsername, &item.AuthMethod, &item.OwnerScope, &item.OwnerUserID, &item.AllowUserEdit, &item.Enabled, &item.SecretID, &item.AuthPublicKey, &item.AuthPublicKeyFingerprint, &item.SSHUserCAID, &item.SSHPrincipalMode, &principalsJSON, &grantIDsJSON, &item.DefaultValidSeconds, &item.MaxValidSeconds, &item.CreatedByKind, &item.CreatedBySubjectID, &item.CreatedBySubjectName, &item.CreatedAt, &item.UpdatedAt, &item.Server.ID, &item.Server.Name, &item.Server.Host, &item.Server.Port, &item.Server.Description, &serverTagsJSON, &item.Server.Enabled, &item.Server.CreatedByKind, &item.Server.CreatedBySubjectID, &item.Server.CreatedBySubjectName, &item.Server.CreatedAt, &item.Server.UpdatedAt, ) if err != nil { return nil, err } item.SSHPrincipals, err = decodeStringList(principalsJSON) if err != nil { return nil, err } item.SSHPrincipalGrantIDs, err = decodeStringList(grantIDsJSON) if err != nil { return nil, err } item.Server.Tags, err = decodeStringList(serverTagsJSON) if err != nil { return nil, err } item.Targets, err = s.listSSHAccessProfileTargets(item.ID) if err != nil { return nil, err } item.Editable = item.OwnerScope == "user" && item.OwnerUserID == trimmedUserID items = append(items, item) } err = rows.Err() if err != nil { return nil, err } return items, nil } func (s *Store) GetSSHAccessProfileForUser(userID string, id string) (models.SSHAccessProfile, error) { var items []models.SSHAccessProfile var item models.SSHAccessProfile var i int var err error items, err = s.ListSSHAccessProfilesForUser(userID) if err != nil { return item, err } for i = 0; i < len(items); i++ { if items[i].ID == strings.TrimSpace(id) { return items[i], nil } } return item, sql.ErrNoRows } func (s *Store) listSSHAccessProfileTargets(profileID string) ([]models.SSHAccessProfileTarget, error) { var rows *sql.Rows var err error var items []models.SSHAccessProfileTarget var item models.SSHAccessProfileTarget rows, err = s.Query(`SELECT p.public_id, t.target_type, t.target_public_id, CASE WHEN t.target_type = 'user' THEN COALESCE(CASE WHEN u.display_name != '' THEN u.display_name || ' (' || u.username || ')' ELSE u.username END, t.target_public_id) ELSE COALESCE(g.name, t.target_public_id) END, CASE WHEN t.target_type = 'user' THEN CASE WHEN u.public_id IS NOT NULL AND u.disabled = 0 THEN 1 ELSE 0 END ELSE CASE WHEN g.public_id IS NOT NULL AND g.disabled = 0 THEN 1 ELSE 0 END END, t.created_at FROM ssh_access_profile_targets t JOIN ssh_access_profiles p ON p.public_id = t.profile_public_id LEFT JOIN users u ON t.target_type = 'user' AND u.public_id = t.target_public_id LEFT JOIN user_groups g ON t.target_type = 'group' AND g.public_id = t.target_public_id WHERE p.public_id = ? ORDER BY t.target_type, t.target_public_id`, strings.TrimSpace(profileID)) if err != nil { return nil, err } defer rows.Close() for rows.Next() { err = rows.Scan(&item.ProfileID, &item.TargetType, &item.TargetID, &item.TargetName, &item.TargetActive, &item.CreatedAt) if err != nil { return nil, err } items = append(items, item) } err = rows.Err() if err != nil { return nil, err } return items, nil } func (s *Store) GetSSHSecret(id string) (models.SSHSecret, error) { var row *sql.Row var item models.SSHSecret var err error row = s.QueryRow(`SELECT public_id, kind, payload, metadata_json, created_by_kind, created_by_subject_id, created_by_subject_name, created_at, updated_at FROM ssh_secrets WHERE public_id = ?`, strings.TrimSpace(id)) err = row.Scan(&item.ID, &item.Kind, &item.Payload, &item.MetadataJSON, &item.CreatedByKind, &item.CreatedBySubjectID, &item.CreatedBySubjectName, &item.CreatedAt, &item.UpdatedAt) if err != nil { return item, err } return item, nil } func (s *Store) ListSSHServerHostKeys(serverID string) ([]models.SSHServerHostKey, error) { var rows *sql.Rows var items []models.SSHServerHostKey var item models.SSHServerHostKey var err error rows, err = s.Query(`SELECT public_id, server_public_id, algorithm, public_key, fingerprint, created_at FROM ssh_server_host_keys WHERE server_public_id = ? ORDER BY created_at, fingerprint`, strings.TrimSpace(serverID)) if err != nil { return nil, err } defer rows.Close() for rows.Next() { err = rows.Scan(&item.ID, &item.ServerID, &item.Algorithm, &item.PublicKey, &item.Fingerprint, &item.CreatedAt) if err != nil { return nil, err } items = append(items, item) } err = rows.Err() if err != nil { return nil, err } return items, nil } func (s *Store) CreateSSHServerHostKey(item models.SSHServerHostKey) (models.SSHServerHostKey, error) { var err error var now int64 if strings.TrimSpace(item.ID) == "" { item.ID, err = util.NewID() if err != nil { return item, err } } now = time.Now().UTC().Unix() item.CreatedAt = now _, err = s.Exec(`INSERT INTO ssh_server_host_keys (public_id, server_public_id, algorithm, public_key, fingerprint, created_at) VALUES (?, ?, ?, ?, ?, ?)`, item.ID, strings.TrimSpace(item.ServerID), strings.TrimSpace(item.Algorithm), strings.TrimSpace(item.PublicKey), strings.TrimSpace(item.Fingerprint), item.CreatedAt, ) if err != nil { return item, err } return item, nil } func (s *Store) DeleteSSHServerHostKey(serverID string, hostKeyID string) error { var err error _, err = s.Exec(`DELETE FROM ssh_server_host_keys WHERE server_public_id = ? AND public_id = ?`, strings.TrimSpace(serverID), strings.TrimSpace(hostKeyID)) return err } func (s *Store) CreateSSHSession(item models.SSHSession) (models.SSHSession, error) { var err error var now int64 if strings.TrimSpace(item.ID) == "" { item.ID, err = util.NewID() if err != nil { return item, err } } now = time.Now().UTC().Unix() if item.Status == "" { item.Status = "pending" } if item.RequestedTerm == "" { item.RequestedTerm = "xterm-256color" } if item.RequestedCols <= 0 { item.RequestedCols = 80 } if item.RequestedRows <= 0 { item.RequestedRows = 24 } item.StartedAt = now _, err = s.Exec(`INSERT INTO ssh_sessions ( public_id, profile_public_id, server_public_id, user_public_id, username, remote_username, auth_method, host, port, status, host_key_fingerprint, requested_term, requested_cols, requested_rows, started_at, connected_at, ended_at, remote_addr, user_agent, error ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, item.ID, strings.TrimSpace(item.ProfileID), strings.TrimSpace(item.ServerID), strings.TrimSpace(item.UserID), strings.TrimSpace(item.Username), strings.TrimSpace(item.RemoteUsername), strings.TrimSpace(item.AuthMethod), strings.TrimSpace(item.Host), item.Port, strings.TrimSpace(item.Status), strings.TrimSpace(item.HostKeyFingerprint), strings.TrimSpace(item.RequestedTerm), item.RequestedCols, item.RequestedRows, item.StartedAt, item.ConnectedAt, item.EndedAt, strings.TrimSpace(item.RemoteAddr), strings.TrimSpace(item.UserAgent), strings.TrimSpace(item.Error), ) if err != nil { return item, err } return item, nil } func (s *Store) GetSSHSession(id string) (models.SSHSession, error) { var row *sql.Row var item models.SSHSession var err error row = s.QueryRow(`SELECT public_id, profile_public_id, server_public_id, user_public_id, username, remote_username, auth_method, host, port, status, host_key_fingerprint, requested_term, requested_cols, requested_rows, started_at, connected_at, ended_at, remote_addr, user_agent, error FROM ssh_sessions WHERE public_id = ?`, strings.TrimSpace(id)) err = row.Scan(&item.ID, &item.ProfileID, &item.ServerID, &item.UserID, &item.Username, &item.RemoteUsername, &item.AuthMethod, &item.Host, &item.Port, &item.Status, &item.HostKeyFingerprint, &item.RequestedTerm, &item.RequestedCols, &item.RequestedRows, &item.StartedAt, &item.ConnectedAt, &item.EndedAt, &item.RemoteAddr, &item.UserAgent, &item.Error) if err != nil { return item, err } return item, nil } func (s *Store) ListSSHSessionsForUser(userID string, limit int) ([]models.SSHSession, error) { var rows *sql.Rows var items []models.SSHSession var item models.SSHSession var err error if limit <= 0 { limit = 50 } if limit > 500 { limit = 500 } rows, err = s.Query(`SELECT public_id, profile_public_id, server_public_id, user_public_id, username, remote_username, auth_method, host, port, status, host_key_fingerprint, requested_term, requested_cols, requested_rows, started_at, connected_at, ended_at, remote_addr, user_agent, error FROM ssh_sessions WHERE user_public_id = ? ORDER BY started_at DESC LIMIT ?`, strings.TrimSpace(userID), limit) if err != nil { return nil, err } defer rows.Close() for rows.Next() { err = rows.Scan(&item.ID, &item.ProfileID, &item.ServerID, &item.UserID, &item.Username, &item.RemoteUsername, &item.AuthMethod, &item.Host, &item.Port, &item.Status, &item.HostKeyFingerprint, &item.RequestedTerm, &item.RequestedCols, &item.RequestedRows, &item.StartedAt, &item.ConnectedAt, &item.EndedAt, &item.RemoteAddr, &item.UserAgent, &item.Error) if err != nil { return nil, err } items = append(items, item) } err = rows.Err() if err != nil { return nil, err } return items, nil } func (s *Store) UpdateSSHSessionStatus(id string, status string, hostKeyFingerprint string, connectedAt int64, endedAt int64, errorText string) error { var err error _, err = s.Exec(`UPDATE ssh_sessions SET status = ?, host_key_fingerprint = ?, connected_at = ?, ended_at = ?, error = ? WHERE public_id = ?`, strings.TrimSpace(status), strings.TrimSpace(hostKeyFingerprint), connectedAt, endedAt, strings.TrimSpace(errorText), strings.TrimSpace(id), ) return err } func createSSHSecretTx(tx *sql.Tx, item models.SSHSecret) (models.SSHSecret, error) { var err error var now int64 if strings.TrimSpace(item.ID) == "" { item.ID, err = util.NewID() if err != nil { return item, err } } now = time.Now().UTC().Unix() item.CreatedAt = now item.UpdatedAt = now _, err = tx.Exec(`INSERT INTO ssh_secrets ( public_id, kind, payload, metadata_json, created_by_kind, created_by_subject_id, created_by_subject_name, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, item.ID, strings.TrimSpace(item.Kind), item.Payload, normalizeJSONText(item.MetadataJSON, "{}"), strings.TrimSpace(item.CreatedByKind), strings.TrimSpace(item.CreatedBySubjectID), strings.TrimSpace(item.CreatedBySubjectName), item.CreatedAt, item.UpdatedAt, ) if err != nil { return item, err } return item, nil } func updateSSHSecretTx(tx *sql.Tx, item models.SSHSecret) (models.SSHSecret, error) { var err error item.UpdatedAt = time.Now().UTC().Unix() _, err = tx.Exec(`UPDATE ssh_secrets SET kind = ?, payload = ?, metadata_json = ?, updated_at = ? WHERE public_id = ?`, strings.TrimSpace(item.Kind), item.Payload, normalizeJSONText(item.MetadataJSON, "{}"), item.UpdatedAt, strings.TrimSpace(item.ID), ) if err != nil { return item, err } return item, nil } func insertSSHAccessProfileTargetTx(tx *sql.Tx, profileID string, targetType string, targetID string, createdAt int64) error { var err error var id string targetType = strings.ToLower(strings.TrimSpace(targetType)) targetID = strings.TrimSpace(targetID) if targetType != "user" && targetType != "group" { return errors.New("target_type must be user or group") } if targetID == "" { return errors.New("target_id is required") } id, err = util.NewID() if err != nil { return err } _, err = tx.Exec(`INSERT INTO ssh_access_profile_targets ( public_id, profile_public_id, target_type, target_public_id, created_at ) VALUES (?, ?, ?, ?, ?)`, id, strings.TrimSpace(profileID), targetType, targetID, createdAt, ) return err } func normalizeStringList(items []string) []string { var out []string var seen map[string]bool var value string var i int seen = map[string]bool{} for i = 0; i < len(items); i++ { value = strings.TrimSpace(items[i]) if value == "" { continue } if seen[value] { continue } seen[value] = true out = append(out, value) } return out } func encodeStringList(items []string) (string, error) { var raw []byte var err error raw, err = json.Marshal(items) if err != nil { return "", err } return string(raw), nil } func decodeStringList(raw string) ([]string, error) { var out []string var err error raw = strings.TrimSpace(raw) if raw == "" { return []string{}, nil } err = json.Unmarshal([]byte(raw), &out) if err != nil { return nil, err } return normalizeStringList(out), nil } func normalizeJSONText(raw string, fallback string) string { var value string value = strings.TrimSpace(raw) if value == "" { return fallback } return value } func emptyStringToNil(raw string) any { var value string value = strings.TrimSpace(raw) if value == "" { return nil } return value } func NormalizeSSHServerTags(items []string) []string { var out []string out = normalizeStringList(items) sort.Strings(out) return out }