Files
codit/backend/internal/db/pki_client_profiles.go
hyung-hwan 5c77d4101f updated some cert apis to be more consistent
updated the self-service cert to support the server auth optionally
2026-04-03 00:32:48 +09:00

707 lines
16 KiB
Go

package db
import "database/sql"
import "errors"
import "strings"
import "time"
import "codit/internal/models"
import "codit/internal/util"
func (s *Store) ListPKIClientProfiles(targetType string, targetID string) ([]models.PKIClientProfile, error) {
var rows *sql.Rows
var err error
var items []models.PKIClientProfile
var item models.PKIClientProfile
targetType = strings.TrimSpace(targetType)
targetID = strings.TrimSpace(targetID)
if targetType != "" && targetID != "" {
rows, err = s.Query(`SELECT DISTINCT
p.public_id,
p.name,
ca.public_id,
ca.name,
p.subject_organization,
p.san_uri_prefix,
p.allow_server_auth,
p.authz_permissions,
p.authz_scope,
p.default_valid_seconds,
p.max_valid_seconds,
p.enabled,
COALESCE(u.public_id, ''),
COALESCE(u.username, ''),
p.created_at,
p.updated_at
FROM pki_client_profiles p
JOIN pki_cas ca ON ca.id = p.ca_id
LEFT JOIN users u ON u.id = p.created_by_user_id
JOIN pki_client_profile_targets t ON t.profile_id = p.id
WHERE t.target_type = ? AND t.target_public_id = ?
ORDER BY p.name`, targetType, targetID)
} else {
rows, err = s.Query(`SELECT
p.public_id,
p.name,
ca.public_id,
ca.name,
p.subject_organization,
p.san_uri_prefix,
p.allow_server_auth,
p.authz_permissions,
p.authz_scope,
p.default_valid_seconds,
p.max_valid_seconds,
p.enabled,
COALESCE(u.public_id, ''),
COALESCE(u.username, ''),
p.created_at,
p.updated_at
FROM pki_client_profiles p
JOIN pki_cas ca ON ca.id = p.ca_id
LEFT JOIN users u ON u.id = p.created_by_user_id
ORDER BY p.name`)
}
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
err = rows.Scan(
&item.ID,
&item.Name,
&item.CAID,
&item.CAName,
&item.SubjectOrganization,
&item.SANURIPrefix,
&item.AllowServerAuth,
&item.AuthzPermissions,
&item.AuthzScope,
&item.DefaultValidSeconds,
&item.MaxValidSeconds,
&item.Enabled,
&item.CreatedByUserID,
&item.CreatedByUsername,
&item.CreatedAt,
&item.UpdatedAt,
)
if err != nil {
return nil, err
}
item.Targets, err = s.listPKIClientProfileTargets(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) ListActivePKIClientProfilesForUser(userID string) ([]models.PKIClientProfile, error) {
var rows *sql.Rows
var err error
var items []models.PKIClientProfile
var item models.PKIClientProfile
rows, err = s.Query(`SELECT DISTINCT
p.public_id,
p.name,
ca.public_id,
ca.name,
p.subject_organization,
p.san_uri_prefix,
p.allow_server_auth,
p.authz_permissions,
p.authz_scope,
p.default_valid_seconds,
p.max_valid_seconds,
p.enabled,
COALESCE(u.public_id, ''),
COALESCE(u.username, ''),
p.created_at,
p.updated_at
FROM pki_client_profiles p
JOIN pki_cas ca ON ca.id = p.ca_id
LEFT JOIN users u ON u.id = p.created_by_user_id
JOIN pki_client_profile_targets t ON t.profile_id = p.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 ca.status = 'active'
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 p.name`,
strings.TrimSpace(userID),
strings.TrimSpace(userID),
)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
err = rows.Scan(
&item.ID,
&item.Name,
&item.CAID,
&item.CAName,
&item.SubjectOrganization,
&item.SANURIPrefix,
&item.AllowServerAuth,
&item.AuthzPermissions,
&item.AuthzScope,
&item.DefaultValidSeconds,
&item.MaxValidSeconds,
&item.Enabled,
&item.CreatedByUserID,
&item.CreatedByUsername,
&item.CreatedAt,
&item.UpdatedAt,
)
if err != nil {
return nil, err
}
item.Targets, err = s.listPKIClientProfileTargets(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) GetPKIClientProfile(id string) (models.PKIClientProfile, error) {
var row *sql.Row
var item models.PKIClientProfile
var err error
row = s.QueryRow(`SELECT
p.public_id,
p.name,
ca.public_id,
ca.name,
p.subject_organization,
p.san_uri_prefix,
p.allow_server_auth,
p.authz_permissions,
p.authz_scope,
p.default_valid_seconds,
p.max_valid_seconds,
p.enabled,
COALESCE(u.public_id, ''),
COALESCE(u.username, ''),
p.created_at,
p.updated_at
FROM pki_client_profiles p
JOIN pki_cas ca ON ca.id = p.ca_id
LEFT JOIN users u ON u.id = p.created_by_user_id
WHERE p.public_id = ?`, strings.TrimSpace(id))
err = row.Scan(
&item.ID,
&item.Name,
&item.CAID,
&item.CAName,
&item.SubjectOrganization,
&item.SANURIPrefix,
&item.AllowServerAuth,
&item.AuthzPermissions,
&item.AuthzScope,
&item.DefaultValidSeconds,
&item.MaxValidSeconds,
&item.Enabled,
&item.CreatedByUserID,
&item.CreatedByUsername,
&item.CreatedAt,
&item.UpdatedAt,
)
if err != nil {
return item, err
}
item.Targets, err = s.listPKIClientProfileTargets(item.ID)
if err != nil {
return item, err
}
return item, nil
}
func (s *Store) GetActivePKIClientProfileForUser(profileID string, userID string) (models.PKIClientProfile, error) {
var row *sql.Row
var item models.PKIClientProfile
var err error
row = s.QueryRow(`SELECT DISTINCT
p.public_id,
p.name,
ca.public_id,
ca.name,
p.subject_organization,
p.san_uri_prefix,
p.allow_server_auth,
p.authz_permissions,
p.authz_scope,
p.default_valid_seconds,
p.max_valid_seconds,
p.enabled,
COALESCE(u.public_id, ''),
COALESCE(u.username, ''),
p.created_at,
p.updated_at
FROM pki_client_profiles p
JOIN pki_cas ca ON ca.id = p.ca_id
LEFT JOIN users u ON u.id = p.created_by_user_id
JOIN pki_client_profile_targets t ON t.profile_id = p.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.public_id = ?
AND p.enabled = 1
AND ca.status = 'active'
AND (
(t.target_type = 'user' AND t.target_public_id = ?)
OR
(t.target_type = 'group' AND ug.disabled = 0 AND gu.public_id = ?)
)`,
strings.TrimSpace(profileID),
strings.TrimSpace(userID),
strings.TrimSpace(userID),
)
err = row.Scan(
&item.ID,
&item.Name,
&item.CAID,
&item.CAName,
&item.SubjectOrganization,
&item.SANURIPrefix,
&item.AllowServerAuth,
&item.AuthzPermissions,
&item.AuthzScope,
&item.DefaultValidSeconds,
&item.MaxValidSeconds,
&item.Enabled,
&item.CreatedByUserID,
&item.CreatedByUsername,
&item.CreatedAt,
&item.UpdatedAt,
)
if err != nil {
return item, err
}
item.Targets, err = s.listPKIClientProfileTargets(item.ID)
if err != nil {
return item, err
}
return item, nil
}
func (s *Store) CreatePKIClientProfile(item models.PKIClientProfile) (models.PKIClientProfile, error) {
var tx *sql.Tx
var owned bool
var err error
var now int64
var i int
var target models.PKIClientProfileTarget
if strings.TrimSpace(item.Name) == "" {
return item, errors.New("name is required")
}
if strings.TrimSpace(item.CAID) == "" {
return item, errors.New("ca_id is required")
}
if len(item.Targets) == 0 {
return item, errors.New("at least one target is required")
}
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
tx, owned, err = s.begin()
if err != nil {
return item, err
}
_, err = tx.Exec(`INSERT INTO pki_client_profiles (
public_id,
name,
ca_id,
subject_organization,
san_uri_prefix,
allow_server_auth,
authz_permissions,
authz_scope,
default_valid_seconds,
max_valid_seconds,
enabled,
created_by_user_id,
created_at,
updated_at
) VALUES (
?,
?,
(SELECT id FROM pki_cas WHERE public_id = ?),
?,
?,
?,
?,
?,
?,
?,
?,
CASE WHEN ? = '' THEN NULL ELSE (SELECT id FROM users WHERE public_id = ?) END,
?,
?
)`,
strings.TrimSpace(item.ID),
strings.TrimSpace(item.Name),
strings.TrimSpace(item.CAID),
strings.TrimSpace(item.SubjectOrganization),
strings.TrimSpace(item.SANURIPrefix),
item.AllowServerAuth,
strings.TrimSpace(item.AuthzPermissions),
strings.TrimSpace(item.AuthzScope),
item.DefaultValidSeconds,
item.MaxValidSeconds,
item.Enabled,
strings.TrimSpace(item.CreatedByUserID),
strings.TrimSpace(item.CreatedByUserID),
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 = insertPKIClientProfileTargetTx(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.GetPKIClientProfile(item.ID)
if err != nil {
return item, err
}
return item, nil
}
func (s *Store) UpdatePKIClientProfile(item models.PKIClientProfile) (models.PKIClientProfile, error) {
var tx *sql.Tx
var owned bool
var err error
var now int64
var i int
var target models.PKIClientProfileTarget
if strings.TrimSpace(item.ID) == "" {
return item, errors.New("id is required")
}
if strings.TrimSpace(item.Name) == "" {
return item, errors.New("name is required")
}
if strings.TrimSpace(item.CAID) == "" {
return item, errors.New("ca_id is required")
}
if len(item.Targets) == 0 {
return item, errors.New("at least one target is required")
}
now = time.Now().UTC().Unix()
item.UpdatedAt = now
tx, owned, err = s.begin()
if err != nil {
return item, err
}
_, err = tx.Exec(`UPDATE pki_client_profiles
SET
name = ?,
ca_id = (SELECT id FROM pki_cas WHERE public_id = ?),
subject_organization = ?,
san_uri_prefix = ?,
allow_server_auth = ?,
authz_permissions = ?,
authz_scope = ?,
default_valid_seconds = ?,
max_valid_seconds = ?,
enabled = ?,
updated_at = ?
WHERE public_id = ?`,
strings.TrimSpace(item.Name),
strings.TrimSpace(item.CAID),
strings.TrimSpace(item.SubjectOrganization),
strings.TrimSpace(item.SANURIPrefix),
item.AllowServerAuth,
strings.TrimSpace(item.AuthzPermissions),
strings.TrimSpace(item.AuthzScope),
item.DefaultValidSeconds,
item.MaxValidSeconds,
item.Enabled,
item.UpdatedAt,
strings.TrimSpace(item.ID),
)
if err != nil {
rollbackIfOwned(tx, owned)
return item, err
}
_, err = tx.Exec(`DELETE FROM pki_client_profile_targets
WHERE profile_id = (SELECT id FROM pki_client_profiles WHERE 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 = insertPKIClientProfileTargetTx(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.GetPKIClientProfile(item.ID)
if err != nil {
return item, err
}
return item, nil
}
func (s *Store) DeletePKIClientProfile(id string) error {
var err error
_, err = s.Exec(`DELETE FROM pki_client_profiles WHERE public_id = ?`, strings.TrimSpace(id))
return err
}
func (s *Store) ListPKIClientIssuancesForUser(userID string) ([]models.PKIClientIssuance, error) {
var rows *sql.Rows
var err error
var items []models.PKIClientIssuance
var item models.PKIClientIssuance
rows, err = s.Query(`SELECT
i.public_id,
i.cert_public_id,
i.user_public_id,
i.username,
i.profile_public_id,
i.profile_name,
i.serial_hex,
i.common_name,
i.san_uri,
i.authz_permissions,
i.authz_scope,
i.not_before,
i.not_after,
COALESCE(c.status, 'deleted'),
COALESCE(c.revoked_at, 0),
COALESCE(c.revocation_reason, ''),
i.created_at
FROM pki_client_issuances i
LEFT JOIN pki_certs c ON c.public_id = i.cert_public_id
WHERE i.user_public_id = ?
ORDER BY i.created_at DESC`, strings.TrimSpace(userID))
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
err = rows.Scan(
&item.ID,
&item.CertID,
&item.UserID,
&item.Username,
&item.ProfileID,
&item.ProfileName,
&item.SerialHex,
&item.CommonName,
&item.SANURI,
&item.AuthzPermissions,
&item.AuthzScope,
&item.NotBefore,
&item.NotAfter,
&item.Status,
&item.RevokedAt,
&item.RevocationReason,
&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) CreatePKIClientIssuance(item models.PKIClientIssuance) (models.PKIClientIssuance, error) {
var err error
if strings.TrimSpace(item.ID) == "" {
item.ID, err = util.NewID()
if err != nil {
return item, err
}
}
if item.CreatedAt == 0 {
item.CreatedAt = time.Now().UTC().Unix()
}
_, err = s.Exec(`INSERT INTO pki_client_issuances (
public_id,
cert_public_id,
user_public_id,
username,
profile_public_id,
profile_name,
serial_hex,
common_name,
san_uri,
authz_permissions,
authz_scope,
not_before,
not_after,
created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
strings.TrimSpace(item.ID),
strings.TrimSpace(item.CertID),
strings.TrimSpace(item.UserID),
strings.TrimSpace(item.Username),
strings.TrimSpace(item.ProfileID),
strings.TrimSpace(item.ProfileName),
strings.TrimSpace(item.SerialHex),
strings.TrimSpace(item.CommonName),
strings.TrimSpace(item.SANURI),
strings.TrimSpace(item.AuthzPermissions),
strings.TrimSpace(item.AuthzScope),
item.NotBefore,
item.NotAfter,
item.CreatedAt,
)
if err != nil {
return item, err
}
return item, nil
}
func (s *Store) UserOwnsPKIClientCert(userID string, certID string) (bool, error) {
var row *sql.Row
var count int
var err error
row = s.QueryRow(`SELECT COUNT(*) FROM pki_client_issuances
WHERE user_public_id = ? AND cert_public_id = ?`,
strings.TrimSpace(userID),
strings.TrimSpace(certID),
)
err = row.Scan(&count)
if err != nil {
return false, err
}
return count > 0, nil
}
func (s *Store) listPKIClientProfileTargets(profileID string) ([]models.PKIClientProfileTarget, error) {
var rows *sql.Rows
var err error
var items []models.PKIClientProfileTarget
var item models.PKIClientProfileTarget
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 pki_client_profile_targets t
JOIN pki_client_profiles p ON p.id = t.profile_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 insertPKIClientProfileTargetTx(tx *sql.Tx, profileID string, targetType string, targetID string, createdAt int64) error {
var err error
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")
}
_, err = tx.Exec(`INSERT INTO pki_client_profile_targets (
profile_id,
target_type,
target_public_id,
created_at
) VALUES (
(SELECT id FROM pki_client_profiles WHERE public_id = ?),
?,
?,
?
)`,
strings.TrimSpace(profileID),
targetType,
targetID,
createdAt,
)
return err
}