Compare commits
6 Commits
2b7297d169
...
debbbb2a86
| Author | SHA1 | Date | |
|---|---|---|---|
| debbbb2a86 | |||
| 374c696b5c | |||
| 9a2b250b10 | |||
| fadf8488f3 | |||
| 02cbd5dda3 | |||
| bc8277e647 |
@@ -0,0 +1,229 @@
|
||||
package db
|
||||
|
||||
import "database/sql"
|
||||
import "errors"
|
||||
import "strings"
|
||||
import "time"
|
||||
|
||||
import "codit/internal/models"
|
||||
import "codit/internal/util"
|
||||
|
||||
const AuthProviderTypeDB = "db"
|
||||
const AuthProviderTypeLDAP = "ldap"
|
||||
const AuthProviderTypeOIDC = "oidc"
|
||||
|
||||
const BuiltinDBProviderPublicID = "db"
|
||||
|
||||
func scanAuthProvider(row interface {
|
||||
Scan(dest ...any) error
|
||||
}, item *models.AuthProvider) error {
|
||||
var defaultUserGroupID sql.NullString
|
||||
var err error
|
||||
err = row.Scan(
|
||||
&item.ID,
|
||||
&item.Name,
|
||||
&item.Type,
|
||||
&item.Enabled,
|
||||
&item.LDAPUrl,
|
||||
&item.LDAPBindDN,
|
||||
&item.LDAPBindPassword,
|
||||
&item.LDAPUserBaseDN,
|
||||
&item.LDAPUserFilter,
|
||||
&item.LDAPTLSInsecureSkipVerify,
|
||||
&item.OIDCClientID,
|
||||
&item.OIDCClientSecret,
|
||||
&item.OIDCAuthorizeURL,
|
||||
&item.OIDCTokenURL,
|
||||
&item.OIDCUserInfoURL,
|
||||
&item.OIDCRedirectURL,
|
||||
&item.OIDCScopes,
|
||||
&item.OIDCTLSInsecureSkipVerify,
|
||||
&defaultUserGroupID,
|
||||
&item.UserCount,
|
||||
&item.CreatedAt,
|
||||
&item.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
item.DefaultUserGroupID = defaultUserGroupID.String
|
||||
return nil
|
||||
}
|
||||
|
||||
const authProviderSelectCols = `
|
||||
p.public_id, p.name, p.type, p.enabled,
|
||||
p.ldap_url, p.ldap_bind_dn, p.ldap_bind_password, p.ldap_user_base_dn, p.ldap_user_filter,
|
||||
p.ldap_tls_insecure_skip_verify,
|
||||
p.oidc_client_id, p.oidc_client_secret, p.oidc_authorize_url, p.oidc_token_url,
|
||||
p.oidc_userinfo_url, p.oidc_redirect_url, p.oidc_scopes, p.oidc_tls_insecure_skip_verify,
|
||||
COALESCE(ug.public_id, ''),
|
||||
(SELECT COUNT(*) FROM users WHERE auth_provider_id = p.id),
|
||||
p.created_at, p.updated_at`
|
||||
|
||||
const authProviderFromClause = `
|
||||
FROM auth_providers p
|
||||
LEFT JOIN user_groups ug ON ug.id = p.default_user_group_id`
|
||||
|
||||
func (s *Store) ListAuthProviders() ([]models.AuthProvider, error) {
|
||||
var rows *sql.Rows
|
||||
var items []models.AuthProvider
|
||||
var item models.AuthProvider
|
||||
var err error
|
||||
|
||||
rows, err = s.Query(`SELECT` + authProviderSelectCols + authProviderFromClause + ` ORDER BY p.name`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
err = scanAuthProvider(rows, &item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
return items, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Store) ListEnabledAuthProviders() ([]models.AuthProvider, error) {
|
||||
var rows *sql.Rows
|
||||
var items []models.AuthProvider
|
||||
var item models.AuthProvider
|
||||
var err error
|
||||
|
||||
rows, err = s.Query(`SELECT` + authProviderSelectCols + authProviderFromClause + ` WHERE p.enabled = 1 ORDER BY p.created_at`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
err = scanAuthProvider(rows, &item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
return items, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Store) GetAuthProvider(id string) (models.AuthProvider, error) {
|
||||
var row *sql.Row
|
||||
var item models.AuthProvider
|
||||
var err error
|
||||
|
||||
row = s.QueryRow(`SELECT`+authProviderSelectCols+authProviderFromClause+` WHERE p.public_id = ?`, strings.TrimSpace(id))
|
||||
err = scanAuthProvider(row, &item)
|
||||
return item, err
|
||||
}
|
||||
|
||||
func (s *Store) CountAuthProviderUsers(id string) (int, error) {
|
||||
var row *sql.Row
|
||||
var count int
|
||||
var err error
|
||||
|
||||
row = s.QueryRow(`SELECT COUNT(*) FROM users WHERE auth_provider_id = (SELECT id FROM auth_providers WHERE public_id = ?)`, strings.TrimSpace(id))
|
||||
err = row.Scan(&count)
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (s *Store) CreateAuthProvider(item models.AuthProvider) (models.AuthProvider, error) {
|
||||
var err error
|
||||
var now int64
|
||||
|
||||
now = time.Now().Unix()
|
||||
item.CreatedAt = now
|
||||
item.UpdatedAt = now
|
||||
|
||||
if strings.TrimSpace(item.ID) == "" {
|
||||
item.ID, err = util.NewID()
|
||||
if err != nil {
|
||||
return item, err
|
||||
}
|
||||
}
|
||||
if item.Type == AuthProviderTypeLDAP && strings.TrimSpace(item.LDAPUserFilter) == "" {
|
||||
item.LDAPUserFilter = "(uid={username})"
|
||||
}
|
||||
if item.Type == AuthProviderTypeOIDC && strings.TrimSpace(item.OIDCScopes) == "" {
|
||||
item.OIDCScopes = "openid profile email"
|
||||
}
|
||||
|
||||
_, err = s.Exec(`INSERT INTO auth_providers (
|
||||
public_id, name, type, enabled,
|
||||
ldap_url, ldap_bind_dn, ldap_bind_password, ldap_user_base_dn, ldap_user_filter,
|
||||
ldap_tls_insecure_skip_verify,
|
||||
oidc_client_id, oidc_client_secret, oidc_authorize_url, oidc_token_url,
|
||||
oidc_userinfo_url, oidc_redirect_url, oidc_scopes, oidc_tls_insecure_skip_verify,
|
||||
default_user_group_id,
|
||||
created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CASE WHEN ? = '' THEN NULL ELSE (SELECT id FROM user_groups WHERE public_id = ?) END, ?, ?)`,
|
||||
item.ID, item.Name, item.Type, item.Enabled,
|
||||
item.LDAPUrl, item.LDAPBindDN, item.LDAPBindPassword, item.LDAPUserBaseDN, item.LDAPUserFilter,
|
||||
item.LDAPTLSInsecureSkipVerify,
|
||||
item.OIDCClientID, item.OIDCClientSecret, item.OIDCAuthorizeURL, item.OIDCTokenURL,
|
||||
item.OIDCUserInfoURL, item.OIDCRedirectURL, item.OIDCScopes, item.OIDCTLSInsecureSkipVerify,
|
||||
item.DefaultUserGroupID, item.DefaultUserGroupID,
|
||||
item.CreatedAt, item.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return item, err
|
||||
}
|
||||
return s.GetAuthProvider(item.ID)
|
||||
}
|
||||
|
||||
func (s *Store) UpdateAuthProvider(item models.AuthProvider) (models.AuthProvider, error) {
|
||||
var err error
|
||||
var now int64
|
||||
|
||||
now = time.Now().Unix()
|
||||
item.UpdatedAt = now
|
||||
|
||||
if item.Type == AuthProviderTypeLDAP && strings.TrimSpace(item.LDAPUserFilter) == "" {
|
||||
item.LDAPUserFilter = "(uid={username})"
|
||||
}
|
||||
if item.Type == AuthProviderTypeOIDC && strings.TrimSpace(item.OIDCScopes) == "" {
|
||||
item.OIDCScopes = "openid profile email"
|
||||
}
|
||||
|
||||
_, err = s.Exec(`UPDATE auth_providers SET
|
||||
name = ?, enabled = ?,
|
||||
ldap_url = ?, ldap_bind_dn = ?, ldap_bind_password = ?, ldap_user_base_dn = ?, ldap_user_filter = ?,
|
||||
ldap_tls_insecure_skip_verify = ?,
|
||||
oidc_client_id = ?, oidc_client_secret = ?, oidc_authorize_url = ?, oidc_token_url = ?,
|
||||
oidc_userinfo_url = ?, oidc_redirect_url = ?, oidc_scopes = ?, oidc_tls_insecure_skip_verify = ?,
|
||||
default_user_group_id = CASE WHEN ? = '' THEN NULL ELSE (SELECT id FROM user_groups WHERE public_id = ?) END,
|
||||
updated_at = ?
|
||||
WHERE public_id = ?`,
|
||||
item.Name, item.Enabled,
|
||||
item.LDAPUrl, item.LDAPBindDN, item.LDAPBindPassword, item.LDAPUserBaseDN, item.LDAPUserFilter,
|
||||
item.LDAPTLSInsecureSkipVerify,
|
||||
item.OIDCClientID, item.OIDCClientSecret, item.OIDCAuthorizeURL, item.OIDCTokenURL,
|
||||
item.OIDCUserInfoURL, item.OIDCRedirectURL, item.OIDCScopes, item.OIDCTLSInsecureSkipVerify,
|
||||
item.DefaultUserGroupID, item.DefaultUserGroupID,
|
||||
item.UpdatedAt,
|
||||
strings.TrimSpace(item.ID),
|
||||
)
|
||||
if err != nil {
|
||||
return item, err
|
||||
}
|
||||
return s.GetAuthProvider(item.ID)
|
||||
}
|
||||
|
||||
func (s *Store) DeleteAuthProvider(id string, force bool) error {
|
||||
var count int
|
||||
var err error
|
||||
|
||||
if strings.TrimSpace(id) == BuiltinDBProviderPublicID {
|
||||
return errors.New("builtin provider cannot be deleted")
|
||||
}
|
||||
if !force {
|
||||
count, err = s.CountAuthProviderUsers(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
return errors.New("provider has associated users")
|
||||
}
|
||||
}
|
||||
_, err = s.Exec(`DELETE FROM auth_providers WHERE public_id = ?`, strings.TrimSpace(id))
|
||||
return err
|
||||
}
|
||||
+21
-11
@@ -238,20 +238,26 @@ func (s *Store) ListPKICerts(caID string) ([]models.PKICert, error) {
|
||||
var items []models.PKICert
|
||||
var item models.PKICert
|
||||
if caID == "" {
|
||||
rows, err = s.Query(`SELECT c.public_id, COALESCE(ca.public_id, ''), COALESCE(ca.name, ''), c.created_by_kind, c.created_by_subject_id, c.created_by_subject_name, c.issuance_source, c.serial_hex, c.common_name, c.san_dns, c.san_ips, c.is_ca, c.cert_pem, c.key_pem, c.not_before, c.not_after, c.status, c.revoked_at, c.revocation_reason, c.created_at
|
||||
rows, err = s.Query(`SELECT c.public_id, COALESCE(ca.public_id, ''), COALESCE(ca.name, ''), c.created_by_kind, COALESCE(cbu.public_id, cbp.public_id, ''), c.created_by_subject_name, c.issuance_source, c.serial_hex, c.common_name, c.san_dns, c.san_ips, c.is_ca, c.cert_pem, c.key_pem, c.not_before, c.not_after, c.status, c.revoked_at, c.revocation_reason, c.created_at
|
||||
FROM pki_certs c
|
||||
LEFT JOIN pki_cas ca ON ca.id = c.ca_id
|
||||
LEFT JOIN users cbu ON cbu.id = c.created_by_user_id
|
||||
LEFT JOIN service_principals cbp ON cbp.id = c.created_by_principal_id
|
||||
ORDER BY c.created_at DESC`)
|
||||
} else if caID == "standalone" {
|
||||
rows, err = s.Query(`SELECT c.public_id, COALESCE(ca.public_id, ''), COALESCE(ca.name, ''), c.created_by_kind, c.created_by_subject_id, c.created_by_subject_name, c.issuance_source, c.serial_hex, c.common_name, c.san_dns, c.san_ips, c.is_ca, c.cert_pem, c.key_pem, c.not_before, c.not_after, c.status, c.revoked_at, c.revocation_reason, c.created_at
|
||||
rows, err = s.Query(`SELECT c.public_id, COALESCE(ca.public_id, ''), COALESCE(ca.name, ''), c.created_by_kind, COALESCE(cbu.public_id, cbp.public_id, ''), c.created_by_subject_name, c.issuance_source, c.serial_hex, c.common_name, c.san_dns, c.san_ips, c.is_ca, c.cert_pem, c.key_pem, c.not_before, c.not_after, c.status, c.revoked_at, c.revocation_reason, c.created_at
|
||||
FROM pki_certs c
|
||||
LEFT JOIN pki_cas ca ON ca.id = c.ca_id
|
||||
LEFT JOIN users cbu ON cbu.id = c.created_by_user_id
|
||||
LEFT JOIN service_principals cbp ON cbp.id = c.created_by_principal_id
|
||||
WHERE c.ca_id IS NULL
|
||||
ORDER BY c.created_at DESC`)
|
||||
} else {
|
||||
rows, err = s.Query(`SELECT c.public_id, COALESCE(ca.public_id, ''), COALESCE(ca.name, ''), c.created_by_kind, c.created_by_subject_id, c.created_by_subject_name, c.issuance_source, c.serial_hex, c.common_name, c.san_dns, c.san_ips, c.is_ca, c.cert_pem, c.key_pem, c.not_before, c.not_after, c.status, c.revoked_at, c.revocation_reason, c.created_at
|
||||
rows, err = s.Query(`SELECT c.public_id, COALESCE(ca.public_id, ''), COALESCE(ca.name, ''), c.created_by_kind, COALESCE(cbu.public_id, cbp.public_id, ''), c.created_by_subject_name, c.issuance_source, c.serial_hex, c.common_name, c.san_dns, c.san_ips, c.is_ca, c.cert_pem, c.key_pem, c.not_before, c.not_after, c.status, c.revoked_at, c.revocation_reason, c.created_at
|
||||
FROM pki_certs c
|
||||
LEFT JOIN pki_cas ca ON ca.id = c.ca_id
|
||||
LEFT JOIN users cbu ON cbu.id = c.created_by_user_id
|
||||
LEFT JOIN service_principals cbp ON cbp.id = c.created_by_principal_id
|
||||
WHERE c.ca_id = (SELECT id FROM pki_cas WHERE public_id = ?)
|
||||
ORDER BY c.created_at DESC`, caID)
|
||||
}
|
||||
@@ -277,9 +283,11 @@ func (s *Store) GetPKICert(id string) (models.PKICert, error) {
|
||||
var row *sql.Row
|
||||
var item models.PKICert
|
||||
var err error
|
||||
row = s.QueryRow(`SELECT c.public_id, COALESCE(ca.public_id, ''), COALESCE(ca.name, ''), c.created_by_kind, c.created_by_subject_id, c.created_by_subject_name, c.issuance_source, c.serial_hex, c.common_name, c.san_dns, c.san_ips, c.is_ca, c.cert_pem, c.key_pem, c.not_before, c.not_after, c.status, c.revoked_at, c.revocation_reason, c.created_at
|
||||
row = s.QueryRow(`SELECT c.public_id, COALESCE(ca.public_id, ''), COALESCE(ca.name, ''), c.created_by_kind, COALESCE(cbu.public_id, cbp.public_id, ''), c.created_by_subject_name, c.issuance_source, c.serial_hex, c.common_name, c.san_dns, c.san_ips, c.is_ca, c.cert_pem, c.key_pem, c.not_before, c.not_after, c.status, c.revoked_at, c.revocation_reason, c.created_at
|
||||
FROM pki_certs c
|
||||
LEFT JOIN pki_cas ca ON ca.id = c.ca_id
|
||||
LEFT JOIN users cbu ON cbu.id = c.created_by_user_id
|
||||
LEFT JOIN service_principals cbp ON cbp.id = c.created_by_principal_id
|
||||
WHERE c.public_id = ?`, id)
|
||||
err = row.Scan(&item.ID, &item.CAID, &item.CAName, &item.CreatedByKind, &item.CreatedBySubjectID, &item.CreatedBySubjectName, &item.IssuanceSource, &item.SerialHex, &item.CommonName, &item.SANDNS, &item.SANIPs, &item.IsCA, &item.CertPEM, &item.KeyPEM, &item.NotBefore, &item.NotAfter, &item.Status, &item.RevokedAt, &item.RevocationReason, &item.CreatedAt)
|
||||
if err != nil {
|
||||
@@ -305,9 +313,9 @@ func (s *Store) CreatePKICert(item models.PKICert) (models.PKICert, error) {
|
||||
now = time.Now().UTC().Unix()
|
||||
item.CreatedAt = now
|
||||
item.CAID = strings.TrimSpace(item.CAID)
|
||||
_, err = s.Exec(`INSERT INTO pki_certs (public_id, ca_id, created_by_kind, created_by_subject_id, created_by_subject_name, issuance_source, serial_hex, common_name, san_dns, san_ips, is_ca, cert_pem, key_pem, not_before, not_after, status, revoked_at, revocation_reason, created_at)
|
||||
VALUES (?, CASE WHEN ? = '' THEN NULL ELSE (SELECT id FROM pki_cas WHERE public_id = ?) END, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
item.ID, item.CAID, item.CAID, item.CreatedByKind, item.CreatedBySubjectID, item.CreatedBySubjectName, item.IssuanceSource, item.SerialHex, item.CommonName, item.SANDNS, item.SANIPs, item.IsCA, item.CertPEM, item.KeyPEM, item.NotBefore, item.NotAfter, item.Status, item.RevokedAt, item.RevocationReason, item.CreatedAt)
|
||||
_, err = s.Exec(`INSERT INTO pki_certs (public_id, ca_id, created_by_kind, created_by_user_id, created_by_principal_id, created_by_subject_name, issuance_source, serial_hex, common_name, san_dns, san_ips, is_ca, cert_pem, key_pem, not_before, not_after, status, revoked_at, revocation_reason, created_at)
|
||||
VALUES (?, CASE WHEN ? = '' THEN NULL ELSE (SELECT id FROM pki_cas WHERE public_id = ?) END, ?, (SELECT id FROM users WHERE public_id = ? AND ? = 'user'), (SELECT id FROM service_principals WHERE public_id = ? AND ? = 'service_principal'), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
item.ID, item.CAID, item.CAID, item.CreatedByKind, item.CreatedBySubjectID, item.CreatedByKind, item.CreatedBySubjectID, item.CreatedByKind, item.CreatedBySubjectName, item.IssuanceSource, item.SerialHex, item.CommonName, item.SANDNS, item.SANIPs, item.IsCA, item.CertPEM, item.KeyPEM, item.NotBefore, item.NotAfter, item.Status, item.RevokedAt, item.RevocationReason, item.CreatedAt)
|
||||
if err != nil {
|
||||
return item, err
|
||||
}
|
||||
@@ -333,7 +341,8 @@ func (s *Store) ReplacePKICert(item models.PKICert) (models.PKICert, error) {
|
||||
SET
|
||||
ca_id = CASE WHEN ? = '' THEN NULL ELSE (SELECT id FROM pki_cas WHERE public_id = ?) END,
|
||||
created_by_kind = ?,
|
||||
created_by_subject_id = ?,
|
||||
created_by_user_id = (SELECT id FROM users WHERE public_id = ? AND ? = 'user'),
|
||||
created_by_principal_id = (SELECT id FROM service_principals WHERE public_id = ? AND ? = 'service_principal'),
|
||||
created_by_subject_name = ?,
|
||||
issuance_source = ?,
|
||||
serial_hex = ?,
|
||||
@@ -351,7 +360,8 @@ func (s *Store) ReplacePKICert(item models.PKICert) (models.PKICert, error) {
|
||||
WHERE public_id = ?`,
|
||||
item.CAID, item.CAID,
|
||||
item.CreatedByKind,
|
||||
item.CreatedBySubjectID,
|
||||
item.CreatedBySubjectID, item.CreatedByKind,
|
||||
item.CreatedBySubjectID, item.CreatedByKind,
|
||||
item.CreatedBySubjectName,
|
||||
item.IssuanceSource,
|
||||
item.SerialHex,
|
||||
@@ -387,7 +397,7 @@ func (s *Store) CountTLSServerCertReferences(certID string) (int, int, error) {
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
row = s.QueryRow(`SELECT COUNT(*) FROM tls_listeners WHERE tls_pki_server_cert_id = ?`, certID)
|
||||
row = s.QueryRow(`SELECT COUNT(*) FROM tls_listeners WHERE tls_pki_server_cert_id = (SELECT id FROM pki_certs WHERE public_id = ?)`, certID)
|
||||
err = row.Scan(&listenerCount)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
@@ -405,7 +415,7 @@ func (s *Store) CountTLSClientCAReferences(caID string) (int, int, error) {
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
row = s.QueryRow(`SELECT COUNT(*) FROM tls_listeners WHERE tls_pki_client_ca_id = ?`, caID)
|
||||
row = s.QueryRow(`SELECT COUNT(*) FROM tls_listeners WHERE tls_pki_client_ca_id = (SELECT id FROM pki_cas WHERE public_id = ?)`, caID)
|
||||
err = row.Scan(&listenerCount)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
|
||||
@@ -45,9 +45,11 @@ func (s *Store) ListSSHServers() ([]models.SSHServer, error) {
|
||||
var item models.SSHServer
|
||||
var tagsJSON string
|
||||
|
||||
rows, err = s.Query(`SELECT srv.public_id, srv.name, srv.host, srv.port, srv.description, srv.tags_json, srv.enabled, srv.host_key_policy, srv.owner_scope, COALESCE(ou.public_id, ''), srv.created_by_kind, srv.created_by_subject_id, srv.created_by_subject_name, srv.created_at, srv.updated_at
|
||||
rows, err = s.Query(`SELECT srv.public_id, srv.name, srv.host, srv.port, srv.description, srv.tags_json, srv.enabled, srv.host_key_policy, srv.owner_scope, COALESCE(ou.public_id, ''), srv.created_by_kind, COALESCE(srv_cbu.public_id, srv_cbp.public_id, ''), srv.created_by_subject_name, srv.created_at, srv.updated_at
|
||||
FROM ssh_servers srv
|
||||
LEFT JOIN users ou ON ou.id = srv.owner_user_id
|
||||
LEFT JOIN users srv_cbu ON srv_cbu.id = srv.created_by_user_id
|
||||
LEFT JOIN service_principals srv_cbp ON srv_cbp.id = srv.created_by_principal_id
|
||||
ORDER BY srv.name, srv.host, srv.port`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -78,9 +80,11 @@ func (s *Store) GetSSHServer(id string) (models.SSHServer, error) {
|
||||
var tagsJSON string
|
||||
var err error
|
||||
|
||||
row = s.QueryRow(`SELECT srv.public_id, srv.name, srv.host, srv.port, srv.description, srv.tags_json, srv.enabled, srv.host_key_policy, srv.owner_scope, COALESCE(ou.public_id, ''), srv.created_by_kind, srv.created_by_subject_id, srv.created_by_subject_name, srv.created_at, srv.updated_at
|
||||
row = s.QueryRow(`SELECT srv.public_id, srv.name, srv.host, srv.port, srv.description, srv.tags_json, srv.enabled, srv.host_key_policy, srv.owner_scope, COALESCE(ou.public_id, ''), srv.created_by_kind, COALESCE(srv_cbu.public_id, srv_cbp.public_id, ''), srv.created_by_subject_name, srv.created_at, srv.updated_at
|
||||
FROM ssh_servers srv
|
||||
LEFT JOIN users ou ON ou.id = srv.owner_user_id
|
||||
LEFT JOIN users srv_cbu ON srv_cbu.id = srv.created_by_user_id
|
||||
LEFT JOIN service_principals srv_cbp ON srv_cbp.id = srv.created_by_principal_id
|
||||
WHERE srv.public_id = ?`, strings.TrimSpace(id))
|
||||
err = row.Scan(&item.ID, &item.Name, &item.Host, &item.Port, &item.Description, &tagsJSON, &item.Enabled, &item.HostKeyPolicy, &item.OwnerScope, &item.OwnerUserID, &item.CreatedByKind, &item.CreatedBySubjectID, &item.CreatedBySubjectName, &item.CreatedAt, &item.UpdatedAt)
|
||||
if err != nil { return item, err }
|
||||
@@ -99,11 +103,13 @@ func (s *Store) ListSSHServersForUser(userID string) ([]models.SSHServer, error)
|
||||
var trimmedUserID string
|
||||
|
||||
trimmedUserID = strings.TrimSpace(userID)
|
||||
rows, err = s.Query(`SELECT srv.public_id, srv.name, srv.host, srv.port, srv.description, srv.tags_json, srv.enabled, srv.host_key_policy, srv.owner_scope, COALESCE(ou.public_id, ''), srv.created_by_kind, srv.created_by_subject_id, srv.created_by_subject_name, srv.created_at, srv.updated_at
|
||||
rows, err = s.Query(`SELECT srv.public_id, srv.name, srv.host, srv.port, srv.description, srv.tags_json, srv.enabled, srv.host_key_policy, srv.owner_scope, COALESCE(ou.public_id, ''), srv.created_by_kind, COALESCE(srv_cbu.public_id, srv_cbp.public_id, ''), srv.created_by_subject_name, srv.created_at, srv.updated_at
|
||||
FROM ssh_servers srv
|
||||
LEFT JOIN users ou ON ou.id = srv.owner_user_id
|
||||
WHERE srv.owner_scope = 'user' AND ou.public_id = ?
|
||||
ORDER BY srv.name, srv.host, srv.port`, trimmedUserID)
|
||||
LEFT JOIN users srv_cbu ON srv_cbu.id = srv.created_by_user_id
|
||||
LEFT JOIN service_principals srv_cbp ON srv_cbp.id = srv.created_by_principal_id
|
||||
WHERE srv.owner_scope = ? AND ou.public_id = ?
|
||||
ORDER BY srv.name, srv.host, srv.port`, SSHOwnerScopeUser, trimmedUserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -203,11 +209,12 @@ func (s *Store) CreateSSHServer(item models.SSHServer) (models.SSHServer, error)
|
||||
owner_scope,
|
||||
owner_user_id,
|
||||
created_by_kind,
|
||||
created_by_subject_id,
|
||||
created_by_user_id,
|
||||
created_by_principal_id,
|
||||
created_by_subject_name,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, CASE WHEN ? = '' THEN NULL ELSE (SELECT id FROM users WHERE public_id = ?) END, ?, ?, ?, ?, ?)`,
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, CASE WHEN ? = '' THEN NULL ELSE (SELECT id FROM users WHERE public_id = ?) END, ?, (SELECT id FROM users WHERE public_id = ? AND ? = 'user'), (SELECT id FROM service_principals WHERE public_id = ? AND ? = 'service_principal'), ?, ?, ?)`,
|
||||
item.ID,
|
||||
strings.TrimSpace(item.Name),
|
||||
strings.TrimSpace(item.Host),
|
||||
@@ -221,6 +228,9 @@ func (s *Store) CreateSSHServer(item models.SSHServer) (models.SSHServer, error)
|
||||
strings.TrimSpace(item.OwnerUserID),
|
||||
strings.TrimSpace(item.CreatedByKind),
|
||||
strings.TrimSpace(item.CreatedBySubjectID),
|
||||
strings.TrimSpace(item.CreatedByKind),
|
||||
strings.TrimSpace(item.CreatedBySubjectID),
|
||||
strings.TrimSpace(item.CreatedByKind),
|
||||
strings.TrimSpace(item.CreatedBySubjectName),
|
||||
item.CreatedAt,
|
||||
item.UpdatedAt,
|
||||
@@ -305,7 +315,7 @@ func (s *Store) ListSSHAccessProfiles() ([]models.SSHAccessProfile, error) {
|
||||
COALESCE(g.description, ''),
|
||||
COALESCE(g.enabled, 0),
|
||||
COALESCE(g.created_by_kind, ''),
|
||||
COALESCE(g.created_by_subject_id, ''),
|
||||
COALESCE(g_cbu.public_id, g_cbp.public_id, ''),
|
||||
COALESCE(g.created_by_subject_name, ''),
|
||||
COALESCE(g.created_at, 0),
|
||||
COALESCE(g.updated_at, 0),
|
||||
@@ -331,7 +341,7 @@ func (s *Store) ListSSHAccessProfiles() ([]models.SSHAccessProfile, error) {
|
||||
p.valid_after,
|
||||
p.valid_before,
|
||||
p.created_by_kind,
|
||||
p.created_by_subject_id,
|
||||
COALESCE(p_cbu.public_id, p_cbp.public_id, ''),
|
||||
p.created_by_subject_name,
|
||||
p.created_at,
|
||||
p.updated_at,
|
||||
@@ -346,15 +356,21 @@ func (s *Store) ListSSHAccessProfiles() ([]models.SSHAccessProfile, error) {
|
||||
s.owner_scope,
|
||||
COALESCE(sou.public_id, ''),
|
||||
s.created_by_kind,
|
||||
s.created_by_subject_id,
|
||||
COALESCE(s_cbu.public_id, s_cbp.public_id, ''),
|
||||
s.created_by_subject_name,
|
||||
s.created_at,
|
||||
s.updated_at
|
||||
FROM ssh_access_profiles p
|
||||
JOIN ssh_servers s ON s.id = p.server_id
|
||||
LEFT JOIN users sou ON sou.id = s.owner_user_id
|
||||
LEFT JOIN users s_cbu ON s_cbu.id = s.created_by_user_id
|
||||
LEFT JOIN service_principals s_cbp ON s_cbp.id = s.created_by_principal_id
|
||||
LEFT JOIN ssh_server_groups g ON g.id = p.server_group_id
|
||||
LEFT JOIN users g_cbu ON g_cbu.id = g.created_by_user_id
|
||||
LEFT JOIN service_principals g_cbp ON g_cbp.id = g.created_by_principal_id
|
||||
LEFT JOIN users pou ON pou.id = p.owner_user_id
|
||||
LEFT JOIN users p_cbu ON p_cbu.id = p.created_by_user_id
|
||||
LEFT JOIN service_principals p_cbp ON p_cbp.id = p.created_by_principal_id
|
||||
LEFT JOIN ssh_secrets sec ON sec.id = p.secret_id
|
||||
LEFT JOIN ssh_credentials c ON c.id = p.ssh_credential_id
|
||||
LEFT JOIN ssh_user_cas ca ON ca.id = p.ssh_user_ca_id
|
||||
@@ -458,7 +474,7 @@ func (s *Store) GetSSHAccessProfile(id string) (models.SSHAccessProfile, error)
|
||||
COALESCE(g.description, ''),
|
||||
COALESCE(g.enabled, 0),
|
||||
COALESCE(g.created_by_kind, ''),
|
||||
COALESCE(g.created_by_subject_id, ''),
|
||||
COALESCE(g_cbu.public_id, g_cbp.public_id, ''),
|
||||
COALESCE(g.created_by_subject_name, ''),
|
||||
COALESCE(g.created_at, 0),
|
||||
COALESCE(g.updated_at, 0),
|
||||
@@ -484,7 +500,7 @@ func (s *Store) GetSSHAccessProfile(id string) (models.SSHAccessProfile, error)
|
||||
p.valid_after,
|
||||
p.valid_before,
|
||||
p.created_by_kind,
|
||||
p.created_by_subject_id,
|
||||
COALESCE(p_cbu.public_id, p_cbp.public_id, ''),
|
||||
p.created_by_subject_name,
|
||||
p.created_at,
|
||||
p.updated_at,
|
||||
@@ -499,15 +515,21 @@ func (s *Store) GetSSHAccessProfile(id string) (models.SSHAccessProfile, error)
|
||||
s.owner_scope,
|
||||
COALESCE(sou.public_id, ''),
|
||||
s.created_by_kind,
|
||||
s.created_by_subject_id,
|
||||
COALESCE(s_cbu.public_id, s_cbp.public_id, ''),
|
||||
s.created_by_subject_name,
|
||||
s.created_at,
|
||||
s.updated_at
|
||||
FROM ssh_access_profiles p
|
||||
JOIN ssh_servers s ON s.id = p.server_id
|
||||
LEFT JOIN users sou ON sou.id = s.owner_user_id
|
||||
LEFT JOIN users s_cbu ON s_cbu.id = s.created_by_user_id
|
||||
LEFT JOIN service_principals s_cbp ON s_cbp.id = s.created_by_principal_id
|
||||
LEFT JOIN ssh_server_groups g ON g.id = p.server_group_id
|
||||
LEFT JOIN users g_cbu ON g_cbu.id = g.created_by_user_id
|
||||
LEFT JOIN service_principals g_cbp ON g_cbp.id = g.created_by_principal_id
|
||||
LEFT JOIN users pou ON pou.id = p.owner_user_id
|
||||
LEFT JOIN users p_cbu ON p_cbu.id = p.created_by_user_id
|
||||
LEFT JOIN service_principals p_cbp ON p_cbp.id = p.created_by_principal_id
|
||||
LEFT JOIN ssh_secrets sec ON sec.id = p.secret_id
|
||||
LEFT JOIN ssh_credentials c ON c.id = p.ssh_credential_id
|
||||
LEFT JOIN ssh_user_cas ca ON ca.id = p.ssh_user_ca_id
|
||||
@@ -688,11 +710,12 @@ func (s *Store) CreateSSHAccessProfile(item models.SSHAccessProfile) (models.SSH
|
||||
valid_after,
|
||||
valid_before,
|
||||
created_by_kind,
|
||||
created_by_subject_id,
|
||||
created_by_user_id,
|
||||
created_by_principal_id,
|
||||
created_by_subject_name,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (?, (SELECT id FROM ssh_servers WHERE public_id = ?), ?, CASE WHEN ? = '' THEN NULL ELSE (SELECT id FROM ssh_server_groups WHERE public_id = ?) END, ?, ?, ?, ?, ?, ?, CASE WHEN ? = '' THEN NULL ELSE (SELECT id FROM users WHERE public_id = ?) END, ?, ?, CASE WHEN ? = '' THEN NULL ELSE (SELECT id FROM ssh_secrets WHERE public_id = ?) END, CASE WHEN ? = '' THEN NULL ELSE (SELECT id FROM ssh_credentials WHERE public_id = ?) END, ?, ?, CASE WHEN ? = '' THEN NULL ELSE (SELECT id FROM ssh_user_cas WHERE public_id = ?) END, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
) VALUES (?, (SELECT id FROM ssh_servers WHERE public_id = ?), ?, CASE WHEN ? = '' THEN NULL ELSE (SELECT id FROM ssh_server_groups WHERE public_id = ?) END, ?, ?, ?, ?, ?, ?, CASE WHEN ? = '' THEN NULL ELSE (SELECT id FROM users WHERE public_id = ?) END, ?, ?, CASE WHEN ? = '' THEN NULL ELSE (SELECT id FROM ssh_secrets WHERE public_id = ?) END, CASE WHEN ? = '' THEN NULL ELSE (SELECT id FROM ssh_credentials WHERE public_id = ?) END, ?, ?, CASE WHEN ? = '' THEN NULL ELSE (SELECT id FROM ssh_user_cas WHERE public_id = ?) END, ?, ?, ?, ?, ?, (SELECT id FROM users WHERE public_id = ? AND ? = 'user'), (SELECT id FROM service_principals WHERE public_id = ? AND ? = 'service_principal'), ?, ?, ?)`,
|
||||
item.ID,
|
||||
strings.TrimSpace(item.ServerID),
|
||||
strings.TrimSpace(item.ServerTargetType),
|
||||
@@ -723,6 +746,9 @@ func (s *Store) CreateSSHAccessProfile(item models.SSHAccessProfile) (models.SSH
|
||||
item.ValidBefore,
|
||||
strings.TrimSpace(item.CreatedByKind),
|
||||
strings.TrimSpace(item.CreatedBySubjectID),
|
||||
strings.TrimSpace(item.CreatedByKind),
|
||||
strings.TrimSpace(item.CreatedBySubjectID),
|
||||
strings.TrimSpace(item.CreatedByKind),
|
||||
strings.TrimSpace(item.CreatedBySubjectName),
|
||||
item.CreatedAt,
|
||||
item.UpdatedAt,
|
||||
@@ -945,7 +971,7 @@ func (s *Store) ListSSHAccessProfilesForUser(userID string) ([]models.SSHAccessP
|
||||
COALESCE(g.description, ''),
|
||||
COALESCE(g.enabled, 0),
|
||||
COALESCE(g.created_by_kind, ''),
|
||||
COALESCE(g.created_by_subject_id, ''),
|
||||
COALESCE(g_cbu.public_id, g_cbp.public_id, ''),
|
||||
COALESCE(g.created_by_subject_name, ''),
|
||||
COALESCE(g.created_at, 0),
|
||||
COALESCE(g.updated_at, 0),
|
||||
@@ -971,7 +997,7 @@ func (s *Store) ListSSHAccessProfilesForUser(userID string) ([]models.SSHAccessP
|
||||
p.valid_after,
|
||||
p.valid_before,
|
||||
p.created_by_kind,
|
||||
p.created_by_subject_id,
|
||||
COALESCE(p_cbu.public_id, p_cbp.public_id, ''),
|
||||
p.created_by_subject_name,
|
||||
p.created_at,
|
||||
p.updated_at,
|
||||
@@ -986,15 +1012,21 @@ func (s *Store) ListSSHAccessProfilesForUser(userID string) ([]models.SSHAccessP
|
||||
s.owner_scope,
|
||||
COALESCE(sou.public_id, ''),
|
||||
s.created_by_kind,
|
||||
s.created_by_subject_id,
|
||||
COALESCE(s_cbu.public_id, s_cbp.public_id, ''),
|
||||
s.created_by_subject_name,
|
||||
s.created_at,
|
||||
s.updated_at
|
||||
FROM ssh_access_profiles p
|
||||
JOIN ssh_servers s ON s.id = p.server_id
|
||||
LEFT JOIN users sou ON sou.id = s.owner_user_id
|
||||
LEFT JOIN users s_cbu ON s_cbu.id = s.created_by_user_id
|
||||
LEFT JOIN service_principals s_cbp ON s_cbp.id = s.created_by_principal_id
|
||||
LEFT JOIN ssh_server_groups g ON g.id = p.server_group_id
|
||||
LEFT JOIN users g_cbu ON g_cbu.id = g.created_by_user_id
|
||||
LEFT JOIN service_principals g_cbp ON g_cbp.id = g.created_by_principal_id
|
||||
LEFT JOIN users pou ON pou.id = p.owner_user_id
|
||||
LEFT JOIN users p_cbu ON p_cbu.id = p.created_by_user_id
|
||||
LEFT JOIN service_principals p_cbp ON p_cbp.id = p.created_by_principal_id
|
||||
LEFT JOIN ssh_secrets sec ON sec.id = p.secret_id
|
||||
LEFT JOIN ssh_credentials c ON c.id = p.ssh_credential_id
|
||||
LEFT JOIN ssh_user_cas ca ON ca.id = p.ssh_user_ca_id
|
||||
@@ -1004,10 +1036,10 @@ func (s *Store) ListSSHAccessProfilesForUser(userID string) ([]models.SSHAccessP
|
||||
LEFT JOIN users gu ON gm.user_id = gu.id
|
||||
LEFT JOIN users tu ON t.target_type = 'user' AND tu.id = t.target_id
|
||||
WHERE (
|
||||
(p.owner_scope = 'user' AND pou.public_id = ?)
|
||||
(p.owner_scope = ? AND pou.public_id = ?)
|
||||
OR
|
||||
(
|
||||
p.owner_scope = 'admin_shared'
|
||||
p.owner_scope = ?
|
||||
AND p.enabled = 1
|
||||
AND s.enabled = 1
|
||||
AND (p.valid_after = 0 OR p.valid_after <= strftime('%s','now'))
|
||||
@@ -1019,7 +1051,7 @@ func (s *Store) ListSSHAccessProfilesForUser(userID string) ([]models.SSHAccessP
|
||||
)
|
||||
)
|
||||
)
|
||||
ORDER BY s.name, p.name`, trimmedUserID, trimmedUserID, trimmedUserID)
|
||||
ORDER BY s.name, p.name`, SSHOwnerScopeUser, trimmedUserID, SSHOwnerScopeAdminShared, trimmedUserID, trimmedUserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1130,7 +1162,7 @@ func (s *Store) GetSSHAccessProfileForUser(userID string, id string) (models.SSH
|
||||
COALESCE(g.description, ''),
|
||||
COALESCE(g.enabled, 0),
|
||||
COALESCE(g.created_by_kind, ''),
|
||||
COALESCE(g.created_by_subject_id, ''),
|
||||
COALESCE(g_cbu.public_id, g_cbp.public_id, ''),
|
||||
COALESCE(g.created_by_subject_name, ''),
|
||||
COALESCE(g.created_at, 0),
|
||||
COALESCE(g.updated_at, 0),
|
||||
@@ -1156,7 +1188,7 @@ func (s *Store) GetSSHAccessProfileForUser(userID string, id string) (models.SSH
|
||||
p.valid_after,
|
||||
p.valid_before,
|
||||
p.created_by_kind,
|
||||
p.created_by_subject_id,
|
||||
COALESCE(p_cbu.public_id, p_cbp.public_id, ''),
|
||||
p.created_by_subject_name,
|
||||
p.created_at,
|
||||
p.updated_at,
|
||||
@@ -1171,15 +1203,21 @@ func (s *Store) GetSSHAccessProfileForUser(userID string, id string) (models.SSH
|
||||
s.owner_scope,
|
||||
COALESCE(sou.public_id, ''),
|
||||
s.created_by_kind,
|
||||
s.created_by_subject_id,
|
||||
COALESCE(s_cbu.public_id, s_cbp.public_id, ''),
|
||||
s.created_by_subject_name,
|
||||
s.created_at,
|
||||
s.updated_at
|
||||
FROM ssh_access_profiles p
|
||||
JOIN ssh_servers s ON s.id = p.server_id
|
||||
LEFT JOIN users sou ON sou.id = s.owner_user_id
|
||||
LEFT JOIN users s_cbu ON s_cbu.id = s.created_by_user_id
|
||||
LEFT JOIN service_principals s_cbp ON s_cbp.id = s.created_by_principal_id
|
||||
LEFT JOIN ssh_server_groups g ON g.id = p.server_group_id
|
||||
LEFT JOIN users g_cbu ON g_cbu.id = g.created_by_user_id
|
||||
LEFT JOIN service_principals g_cbp ON g_cbp.id = g.created_by_principal_id
|
||||
LEFT JOIN users pou ON pou.id = p.owner_user_id
|
||||
LEFT JOIN users p_cbu ON p_cbu.id = p.created_by_user_id
|
||||
LEFT JOIN service_principals p_cbp ON p_cbp.id = p.created_by_principal_id
|
||||
LEFT JOIN ssh_secrets sec ON sec.id = p.secret_id
|
||||
LEFT JOIN ssh_credentials c ON c.id = p.ssh_credential_id
|
||||
LEFT JOIN ssh_user_cas ca ON ca.id = p.ssh_user_ca_id
|
||||
@@ -1190,10 +1228,10 @@ func (s *Store) GetSSHAccessProfileForUser(userID string, id string) (models.SSH
|
||||
LEFT JOIN users tu ON t.target_type = 'user' AND tu.id = t.target_id
|
||||
WHERE p.public_id = ?
|
||||
AND (
|
||||
(p.owner_scope = 'user' AND pou.public_id = ?)
|
||||
(p.owner_scope = ? AND pou.public_id = ?)
|
||||
OR
|
||||
(
|
||||
p.owner_scope = 'admin_shared'
|
||||
p.owner_scope = ?
|
||||
AND p.enabled = 1
|
||||
AND s.enabled = 1
|
||||
AND (p.valid_after = 0 OR p.valid_after <= strftime('%s','now'))
|
||||
@@ -1205,7 +1243,7 @@ func (s *Store) GetSSHAccessProfileForUser(userID string, id string) (models.SSH
|
||||
)
|
||||
)
|
||||
)
|
||||
LIMIT 1`, trimmedID, trimmedUserID, trimmedUserID, trimmedUserID)
|
||||
LIMIT 1`, trimmedID, SSHOwnerScopeUser, trimmedUserID, SSHOwnerScopeAdminShared, trimmedUserID, trimmedUserID)
|
||||
err = row.Scan(
|
||||
&item.ID,
|
||||
&item.ServerID,
|
||||
@@ -1369,7 +1407,11 @@ func (s *Store) GetSSHSecret(id string) (models.SSHSecret, error) {
|
||||
var item models.SSHSecret
|
||||
var err error
|
||||
|
||||
row = s.QueryRow(`SELECT public_id, kind, payload, password, 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))
|
||||
row = s.QueryRow(`SELECT sec.public_id, sec.kind, sec.payload, sec.password, sec.metadata_json, sec.created_by_kind, COALESCE(sec_cbu.public_id, sec_cbp.public_id, ''), sec.created_by_subject_name, sec.created_at, sec.updated_at
|
||||
FROM ssh_secrets sec
|
||||
LEFT JOIN users sec_cbu ON sec_cbu.id = sec.created_by_user_id
|
||||
LEFT JOIN service_principals sec_cbp ON sec_cbp.id = sec.created_by_principal_id
|
||||
WHERE sec.public_id = ?`, strings.TrimSpace(id))
|
||||
err = row.Scan(&item.ID, &item.Kind, &item.Payload, &item.Password, &item.MetadataJSON, &item.CreatedByKind, &item.CreatedBySubjectID, &item.CreatedBySubjectName, &item.CreatedAt, &item.UpdatedAt)
|
||||
if err != nil {
|
||||
return item, err
|
||||
@@ -1988,11 +2030,12 @@ func createSSHSecretTx(tx *sql.Tx, item models.SSHSecret) (models.SSHSecret, err
|
||||
password,
|
||||
metadata_json,
|
||||
created_by_kind,
|
||||
created_by_subject_id,
|
||||
created_by_user_id,
|
||||
created_by_principal_id,
|
||||
created_by_subject_name,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
) VALUES (?, ?, ?, ?, ?, ?, (SELECT id FROM users WHERE public_id = ? AND ? = 'user'), (SELECT id FROM service_principals WHERE public_id = ? AND ? = 'service_principal'), ?, ?, ?)`,
|
||||
item.ID,
|
||||
strings.TrimSpace(item.Kind),
|
||||
item.Payload,
|
||||
@@ -2000,6 +2043,9 @@ func createSSHSecretTx(tx *sql.Tx, item models.SSHSecret) (models.SSHSecret, err
|
||||
normalizeJSONText(item.MetadataJSON, "{}"),
|
||||
strings.TrimSpace(item.CreatedByKind),
|
||||
strings.TrimSpace(item.CreatedBySubjectID),
|
||||
strings.TrimSpace(item.CreatedByKind),
|
||||
strings.TrimSpace(item.CreatedBySubjectID),
|
||||
strings.TrimSpace(item.CreatedByKind),
|
||||
strings.TrimSpace(item.CreatedBySubjectName),
|
||||
item.CreatedAt,
|
||||
item.UpdatedAt,
|
||||
|
||||
@@ -31,12 +31,14 @@ func (s *Store) ListSSHCredentials() ([]models.SSHCredential, error) {
|
||||
var item models.SSHCredential
|
||||
var err error
|
||||
|
||||
rows, err = s.Query(`SELECT c.public_id, c.name, c.description, c.type, COALESCE(sec.public_id, ''), c.public_key, c.fingerprint, c.enabled, c.owner_scope, COALESCE(u.public_id, ''), c.created_by_kind, c.created_by_subject_id, c.created_by_subject_name, c.created_at, c.updated_at
|
||||
rows, err = s.Query(`SELECT c.public_id, c.name, c.description, c.type, COALESCE(sec.public_id, ''), c.public_key, c.fingerprint, c.enabled, c.owner_scope, COALESCE(u.public_id, ''), c.created_by_kind, COALESCE(cbu.public_id, cbp.public_id, ''), c.created_by_subject_name, c.created_at, c.updated_at
|
||||
FROM ssh_credentials c
|
||||
LEFT JOIN ssh_secrets sec ON sec.id = c.secret_id
|
||||
LEFT JOIN users u ON u.id = c.owner_user_id
|
||||
WHERE owner_scope = 'admin'
|
||||
ORDER BY name`)
|
||||
LEFT JOIN users cbu ON cbu.id = c.created_by_user_id
|
||||
LEFT JOIN service_principals cbp ON cbp.id = c.created_by_principal_id
|
||||
WHERE c.owner_scope = ?
|
||||
ORDER BY c.name`, SSHOwnerScopeAdmin)
|
||||
if err != nil { return nil, err }
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
@@ -55,12 +57,14 @@ func (s *Store) ListSSHCredentialsForUser(userID string) ([]models.SSHCredential
|
||||
var item models.SSHCredential
|
||||
var err error
|
||||
|
||||
rows, err = s.Query(`SELECT c.public_id, c.name, c.description, c.type, COALESCE(sec.public_id, ''), c.public_key, c.fingerprint, c.enabled, c.owner_scope, COALESCE(u.public_id, ''), c.created_by_kind, c.created_by_subject_id, c.created_by_subject_name, c.created_at, c.updated_at
|
||||
rows, err = s.Query(`SELECT c.public_id, c.name, c.description, c.type, COALESCE(sec.public_id, ''), c.public_key, c.fingerprint, c.enabled, c.owner_scope, COALESCE(u.public_id, ''), c.created_by_kind, COALESCE(cbu.public_id, cbp.public_id, ''), c.created_by_subject_name, c.created_at, c.updated_at
|
||||
FROM ssh_credentials c
|
||||
LEFT JOIN ssh_secrets sec ON sec.id = c.secret_id
|
||||
LEFT JOIN users u ON u.id = c.owner_user_id
|
||||
WHERE c.owner_scope = 'user' AND u.public_id = ?
|
||||
ORDER BY c.name`, strings.TrimSpace(userID))
|
||||
LEFT JOIN users cbu ON cbu.id = c.created_by_user_id
|
||||
LEFT JOIN service_principals cbp ON cbp.id = c.created_by_principal_id
|
||||
WHERE c.owner_scope = ? AND u.public_id = ?
|
||||
ORDER BY c.name`, SSHOwnerScopeUser, strings.TrimSpace(userID))
|
||||
if err != nil { return nil, err }
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
@@ -78,10 +82,12 @@ func (s *Store) GetSSHCredential(id string) (models.SSHCredential, error) {
|
||||
var item models.SSHCredential
|
||||
var err error
|
||||
|
||||
row = s.QueryRow(`SELECT c.public_id, c.name, c.description, c.type, COALESCE(sec.public_id, ''), c.public_key, c.fingerprint, c.enabled, c.owner_scope, COALESCE(u.public_id, ''), c.created_by_kind, c.created_by_subject_id, c.created_by_subject_name, c.created_at, c.updated_at
|
||||
row = s.QueryRow(`SELECT c.public_id, c.name, c.description, c.type, COALESCE(sec.public_id, ''), c.public_key, c.fingerprint, c.enabled, c.owner_scope, COALESCE(u.public_id, ''), c.created_by_kind, COALESCE(cbu.public_id, cbp.public_id, ''), c.created_by_subject_name, c.created_at, c.updated_at
|
||||
FROM ssh_credentials c
|
||||
LEFT JOIN ssh_secrets sec ON sec.id = c.secret_id
|
||||
LEFT JOIN users u ON u.id = c.owner_user_id
|
||||
LEFT JOIN users cbu ON cbu.id = c.created_by_user_id
|
||||
LEFT JOIN service_principals cbp ON cbp.id = c.created_by_principal_id
|
||||
WHERE c.public_id = ?`, strings.TrimSpace(id))
|
||||
err = scanSSHCredential(row, &item)
|
||||
return item, err
|
||||
@@ -92,11 +98,13 @@ func (s *Store) GetSSHCredentialForUser(userID string, id string) (models.SSHCre
|
||||
var item models.SSHCredential
|
||||
var err error
|
||||
|
||||
row = s.QueryRow(`SELECT c.public_id, c.name, c.description, c.type, COALESCE(sec.public_id, ''), c.public_key, c.fingerprint, c.enabled, c.owner_scope, COALESCE(u.public_id, ''), c.created_by_kind, c.created_by_subject_id, c.created_by_subject_name, c.created_at, c.updated_at
|
||||
row = s.QueryRow(`SELECT c.public_id, c.name, c.description, c.type, COALESCE(sec.public_id, ''), c.public_key, c.fingerprint, c.enabled, c.owner_scope, COALESCE(u.public_id, ''), c.created_by_kind, COALESCE(cbu.public_id, cbp.public_id, ''), c.created_by_subject_name, c.created_at, c.updated_at
|
||||
FROM ssh_credentials c
|
||||
LEFT JOIN ssh_secrets sec ON sec.id = c.secret_id
|
||||
LEFT JOIN users u ON u.id = c.owner_user_id
|
||||
WHERE c.public_id = ? AND c.owner_scope = 'user' AND u.public_id = ?`, strings.TrimSpace(id), strings.TrimSpace(userID))
|
||||
LEFT JOIN users cbu ON cbu.id = c.created_by_user_id
|
||||
LEFT JOIN service_principals cbp ON cbp.id = c.created_by_principal_id
|
||||
WHERE c.public_id = ? AND c.owner_scope = ? AND u.public_id = ?`, strings.TrimSpace(id), SSHOwnerScopeUser, strings.TrimSpace(userID))
|
||||
err = scanSSHCredential(row, &item)
|
||||
return item, err
|
||||
}
|
||||
@@ -131,8 +139,8 @@ func (s *Store) CreateSSHCredential(item models.SSHCredential, secret models.SSH
|
||||
item.CreatedAt = now
|
||||
item.UpdatedAt = now
|
||||
item.SecretID = secret.ID
|
||||
_, err = tx.Exec(`INSERT INTO ssh_credentials (public_id, name, description, type, secret_id, public_key, fingerprint, enabled, owner_scope, owner_user_id, created_by_kind, created_by_subject_id, created_by_subject_name, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, (SELECT id FROM ssh_secrets WHERE public_id = ?), ?, ?, ?, ?, (SELECT id FROM users WHERE public_id = ?), ?, ?, ?, ?, ?)`,
|
||||
_, err = tx.Exec(`INSERT INTO ssh_credentials (public_id, name, description, type, secret_id, public_key, fingerprint, enabled, owner_scope, owner_user_id, created_by_kind, created_by_user_id, created_by_principal_id, created_by_subject_name, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, (SELECT id FROM ssh_secrets WHERE public_id = ?), ?, ?, ?, ?, (SELECT id FROM users WHERE public_id = ?), ?, (SELECT id FROM users WHERE public_id = ? AND ? = 'user'), (SELECT id FROM service_principals WHERE public_id = ? AND ? = 'service_principal'), ?, ?, ?)`,
|
||||
item.ID,
|
||||
strings.TrimSpace(item.Name),
|
||||
strings.TrimSpace(item.Description),
|
||||
@@ -144,7 +152,8 @@ func (s *Store) CreateSSHCredential(item models.SSHCredential, secret models.SSH
|
||||
strings.TrimSpace(item.OwnerScope),
|
||||
strings.TrimSpace(item.OwnerUserID),
|
||||
strings.TrimSpace(item.CreatedByKind),
|
||||
strings.TrimSpace(item.CreatedBySubjectID),
|
||||
strings.TrimSpace(item.CreatedBySubjectID), strings.TrimSpace(item.CreatedByKind),
|
||||
strings.TrimSpace(item.CreatedBySubjectID), strings.TrimSpace(item.CreatedByKind),
|
||||
strings.TrimSpace(item.CreatedBySubjectName),
|
||||
item.CreatedAt,
|
||||
item.UpdatedAt,
|
||||
|
||||
@@ -47,7 +47,11 @@ func (s *Store) ListSSHServerGroups() ([]models.SSHServerGroup, error) {
|
||||
var item models.SSHServerGroup
|
||||
var err error
|
||||
|
||||
rows, err = s.Query(`SELECT public_id, name, description, enabled, tags_json, created_by_kind, created_by_subject_id, created_by_subject_name, created_at, updated_at FROM ssh_server_groups ORDER BY name`)
|
||||
rows, err = s.Query(`SELECT g.public_id, g.name, g.description, g.enabled, g.tags_json, g.created_by_kind, COALESCE(cbu.public_id, cbp.public_id, ''), g.created_by_subject_name, g.created_at, g.updated_at
|
||||
FROM ssh_server_groups g
|
||||
LEFT JOIN users cbu ON cbu.id = g.created_by_user_id
|
||||
LEFT JOIN service_principals cbp ON cbp.id = g.created_by_principal_id
|
||||
ORDER BY g.name`)
|
||||
if err != nil { return nil, err }
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
@@ -67,7 +71,11 @@ func (s *Store) GetSSHServerGroup(id string) (models.SSHServerGroup, error) {
|
||||
var item models.SSHServerGroup
|
||||
var err error
|
||||
|
||||
row = s.QueryRow(`SELECT public_id, name, description, enabled, tags_json, created_by_kind, created_by_subject_id, created_by_subject_name, created_at, updated_at FROM ssh_server_groups WHERE public_id = ?`, strings.TrimSpace(id))
|
||||
row = s.QueryRow(`SELECT g.public_id, g.name, g.description, g.enabled, g.tags_json, g.created_by_kind, COALESCE(cbu.public_id, cbp.public_id, ''), g.created_by_subject_name, g.created_at, g.updated_at
|
||||
FROM ssh_server_groups g
|
||||
LEFT JOIN users cbu ON cbu.id = g.created_by_user_id
|
||||
LEFT JOIN service_principals cbp ON cbp.id = g.created_by_principal_id
|
||||
WHERE g.public_id = ?`, strings.TrimSpace(id))
|
||||
err = scanSSHServerGroup(row, &item)
|
||||
if err != nil { return item, err }
|
||||
item.ServerIDs, err = s.listSSHServerGroupMemberIDs(item.ID)
|
||||
@@ -82,11 +90,13 @@ func (s *Store) ListSSHServersForGroup(groupID string) ([]models.SSHServer, erro
|
||||
var tagsJSON string
|
||||
var err error
|
||||
|
||||
rows, err = s.Query(`SELECT s.public_id, s.name, s.host, s.port, s.description, s.tags_json, s.enabled, s.host_key_policy, s.owner_scope, COALESCE(u.public_id, ''), s.created_by_kind, s.created_by_subject_id, s.created_by_subject_name, s.created_at, s.updated_at
|
||||
rows, err = s.Query(`SELECT s.public_id, s.name, s.host, s.port, s.description, s.tags_json, s.enabled, s.host_key_policy, s.owner_scope, COALESCE(u.public_id, ''), s.created_by_kind, COALESCE(sbu.public_id, sbp.public_id, ''), s.created_by_subject_name, s.created_at, s.updated_at
|
||||
FROM ssh_server_group_members gm
|
||||
JOIN ssh_servers s ON s.id = gm.server_id
|
||||
JOIN ssh_server_groups g ON g.id = gm.group_id
|
||||
LEFT JOIN users u ON u.id = s.owner_user_id
|
||||
LEFT JOIN users sbu ON sbu.id = s.created_by_user_id
|
||||
LEFT JOIN service_principals sbp ON sbp.id = s.created_by_principal_id
|
||||
WHERE g.public_id = ?
|
||||
ORDER BY s.name`, strings.TrimSpace(groupID))
|
||||
if err != nil { return nil, err }
|
||||
@@ -123,7 +133,7 @@ func (s *Store) CreateSSHServerGroup(item models.SSHServerGroup) (models.SSHServ
|
||||
item.UpdatedAt = now
|
||||
tx, owned, err = s.begin()
|
||||
if err != nil { return item, err }
|
||||
_, err = tx.Exec(`INSERT INTO ssh_server_groups (public_id, name, description, enabled, tags_json, 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.Description), item.Enabled, tagsJSON, strings.TrimSpace(item.CreatedByKind), strings.TrimSpace(item.CreatedBySubjectID), strings.TrimSpace(item.CreatedBySubjectName), item.CreatedAt, item.UpdatedAt)
|
||||
_, err = tx.Exec(`INSERT INTO ssh_server_groups (public_id, name, description, enabled, tags_json, created_by_kind, created_by_user_id, created_by_principal_id, created_by_subject_name, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, (SELECT id FROM users WHERE public_id = ? AND ? = 'user'), (SELECT id FROM service_principals WHERE public_id = ? AND ? = 'service_principal'), ?, ?, ?)`, item.ID, strings.TrimSpace(item.Name), strings.TrimSpace(item.Description), item.Enabled, tagsJSON, strings.TrimSpace(item.CreatedByKind), strings.TrimSpace(item.CreatedBySubjectID), strings.TrimSpace(item.CreatedByKind), strings.TrimSpace(item.CreatedBySubjectID), strings.TrimSpace(item.CreatedByKind), strings.TrimSpace(item.CreatedBySubjectName), item.CreatedAt, item.UpdatedAt)
|
||||
if err != nil { rollbackIfOwned(tx, owned); return item, err }
|
||||
err = commitIfOwned(tx, owned)
|
||||
if err != nil { return item, err }
|
||||
|
||||
+34
-250
@@ -24,13 +24,10 @@ func (s *Store) CreateUser(user models.User, passwordHash string) (models.User,
|
||||
nowUnix = now.Unix()
|
||||
user.CreatedAt = nowUnix
|
||||
user.UpdatedAt = nowUnix
|
||||
if user.AuthSource == "" {
|
||||
user.AuthSource = "db"
|
||||
}
|
||||
_, err = s.Exec(`
|
||||
INSERT INTO users (public_id, username, display_name, email, password_hash, is_admin, disabled, auth_source, totp_required, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`, user.ID, user.Username, user.DisplayName, user.Email, passwordHash, user.IsAdmin, user.Disabled, user.AuthSource, user.TOTPRequired, now, now)
|
||||
INSERT INTO users (public_id, username, display_name, email, password_hash, is_admin, disabled, auth_provider_id, totp_required, external_subject, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, CASE WHEN ? = '' THEN NULL ELSE (SELECT id FROM auth_providers WHERE public_id = ?) END, ?, ?, ?, ?)
|
||||
`, user.ID, user.Username, user.DisplayName, user.Email, passwordHash, user.IsAdmin, user.Disabled, user.AuthProviderID, user.AuthProviderID, user.TOTPRequired, user.ExternalSubject, now, now)
|
||||
return user, err
|
||||
}
|
||||
|
||||
@@ -84,8 +81,8 @@ func (s *Store) GetUserByID(id string) (models.User, error) {
|
||||
var created time.Time
|
||||
var updated time.Time
|
||||
var err error
|
||||
row = s.QueryRow(`SELECT u.public_id, u.username, u.display_name, u.email, u.is_admin, u.disabled, u.auth_source, COALESCE(t.enabled, 0), u.totp_required, u.created_at, u.updated_at FROM users u LEFT JOIN user_totp t ON t.user_id = u.id WHERE u.public_id = ?`, id)
|
||||
err = row.Scan(&user.ID, &user.Username, &user.DisplayName, &user.Email, &user.IsAdmin, &user.Disabled, &user.AuthSource, &user.TOTPEnabled, &user.TOTPRequired, &created, &updated)
|
||||
row = s.QueryRow(`SELECT u.public_id, u.username, u.display_name, u.email, u.is_admin, u.disabled, COALESCE(ap.public_id, ''), COALESCE(t.enabled, 0), u.totp_required, u.created_at, u.updated_at FROM users u LEFT JOIN auth_providers ap ON ap.id = u.auth_provider_id LEFT JOIN user_totp t ON t.user_id = u.id WHERE u.public_id = ?`, id)
|
||||
err = row.Scan(&user.ID, &user.Username, &user.DisplayName, &user.Email, &user.IsAdmin, &user.Disabled, &user.AuthProviderID, &user.TOTPEnabled, &user.TOTPRequired, &created, &updated)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
@@ -101,8 +98,8 @@ func (s *Store) GetUserByUsername(username string) (models.User, string, error)
|
||||
var err error
|
||||
var created time.Time
|
||||
var updated time.Time
|
||||
row = s.QueryRow(`SELECT u.public_id, u.username, u.display_name, u.email, u.is_admin, u.disabled, u.auth_source, u.password_hash, COALESCE(t.enabled, 0), u.totp_required, u.created_at, u.updated_at FROM users u LEFT JOIN user_totp t ON t.user_id = u.id WHERE u.username = ?`, username)
|
||||
err = row.Scan(&user.ID, &user.Username, &user.DisplayName, &user.Email, &user.IsAdmin, &user.Disabled, &user.AuthSource, &passwordHash, &user.TOTPEnabled, &user.TOTPRequired, &created, &updated)
|
||||
row = s.QueryRow(`SELECT u.public_id, u.username, u.display_name, u.email, u.is_admin, u.disabled, COALESCE(ap.public_id, ''), u.password_hash, COALESCE(t.enabled, 0), u.totp_required, u.created_at, u.updated_at FROM users u LEFT JOIN auth_providers ap ON ap.id = u.auth_provider_id LEFT JOIN user_totp t ON t.user_id = u.id WHERE u.username = ?`, username)
|
||||
err = row.Scan(&user.ID, &user.Username, &user.DisplayName, &user.Email, &user.IsAdmin, &user.Disabled, &user.AuthProviderID, &passwordHash, &user.TOTPEnabled, &user.TOTPRequired, &created, &updated)
|
||||
if err != nil {
|
||||
return user, passwordHash.String, err
|
||||
}
|
||||
@@ -118,13 +115,13 @@ func (s *Store) ListUsers() ([]models.User, error) {
|
||||
var u models.User
|
||||
var created time.Time
|
||||
var updated time.Time
|
||||
rows, err = s.Query(`SELECT u.public_id, u.username, u.display_name, u.email, u.is_admin, u.disabled, u.auth_source, COALESCE(t.enabled, 0), u.totp_required, u.created_at, u.updated_at FROM users u LEFT JOIN user_totp t ON t.user_id = u.id ORDER BY u.username`)
|
||||
rows, err = s.Query(`SELECT u.public_id, u.username, u.display_name, u.email, u.is_admin, u.disabled, COALESCE(ap.public_id, ''), COALESCE(t.enabled, 0), u.totp_required, u.created_at, u.updated_at FROM users u LEFT JOIN auth_providers ap ON ap.id = u.auth_provider_id LEFT JOIN user_totp t ON t.user_id = u.id ORDER BY u.username`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
err = rows.Scan(&u.ID, &u.Username, &u.DisplayName, &u.Email, &u.IsAdmin, &u.Disabled, &u.AuthSource, &u.TOTPEnabled, &u.TOTPRequired, &created, &updated)
|
||||
err = rows.Scan(&u.ID, &u.Username, &u.DisplayName, &u.Email, &u.IsAdmin, &u.Disabled, &u.AuthProviderID, &u.TOTPEnabled, &u.TOTPRequired, &created, &updated)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -167,243 +164,28 @@ func (s *Store) DeleteUser(id string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) GetAuthSettings() (models.AuthSettings, error) {
|
||||
var settings models.AuthSettings
|
||||
var rows *sql.Rows
|
||||
func (s *Store) GetUserByProviderAndSub(providerPublicID string, sub string) (models.User, error) {
|
||||
var user models.User
|
||||
var row *sql.Row
|
||||
var created time.Time
|
||||
var updated time.Time
|
||||
var err error
|
||||
var key string
|
||||
var value string
|
||||
rows, err = s.Query(`SELECT key, value FROM app_settings WHERE key IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
"auth.mode",
|
||||
"auth.oidc.enabled",
|
||||
"auth.ldap.url",
|
||||
"auth.ldap.bind_dn",
|
||||
"auth.ldap.bind_password",
|
||||
"auth.ldap.user_base_dn",
|
||||
"auth.ldap.user_filter",
|
||||
"auth.ldap.tls_insecure_skip_verify",
|
||||
"auth.oidc.client_id",
|
||||
"auth.oidc.client_secret",
|
||||
"auth.oidc.authorize_url",
|
||||
"auth.oidc.token_url",
|
||||
"auth.oidc.userinfo_url",
|
||||
"auth.oidc.redirect_url",
|
||||
"auth.oidc.scopes",
|
||||
"auth.oidc.tls_insecure_skip_verify",
|
||||
"auth.ldap.default_user_group_id",
|
||||
"auth.oidc.default_user_group_id")
|
||||
row = s.QueryRow(`
|
||||
SELECT u.public_id, u.username, u.display_name, u.email, u.is_admin, u.disabled,
|
||||
COALESCE(ap.public_id, ''), u.external_subject, COALESCE(t.enabled, 0), u.totp_required, u.created_at, u.updated_at
|
||||
FROM users u
|
||||
LEFT JOIN auth_providers ap ON ap.id = u.auth_provider_id
|
||||
LEFT JOIN user_totp t ON t.user_id = u.id
|
||||
WHERE ap.public_id = ? AND u.external_subject = ?
|
||||
`, strings.TrimSpace(providerPublicID), strings.TrimSpace(sub))
|
||||
err = row.Scan(&user.ID, &user.Username, &user.DisplayName, &user.Email, &user.IsAdmin, &user.Disabled,
|
||||
&user.AuthProviderID, &user.ExternalSubject, &user.TOTPEnabled, &user.TOTPRequired, &created, &updated)
|
||||
if err != nil {
|
||||
return settings, err
|
||||
return user, err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
err = rows.Scan(&key, &value)
|
||||
if err != nil {
|
||||
return settings, err
|
||||
}
|
||||
switch key {
|
||||
case "auth.mode":
|
||||
settings.AuthMode = value
|
||||
case "auth.oidc.enabled":
|
||||
settings.OIDCEnabled = value == "1"
|
||||
case "auth.ldap.url":
|
||||
settings.LDAPURL = value
|
||||
case "auth.ldap.bind_dn":
|
||||
settings.LDAPBindDN = value
|
||||
case "auth.ldap.bind_password":
|
||||
settings.LDAPBindPassword = value
|
||||
case "auth.ldap.user_base_dn":
|
||||
settings.LDAPUserBaseDN = value
|
||||
case "auth.ldap.user_filter":
|
||||
settings.LDAPUserFilter = value
|
||||
case "auth.ldap.tls_insecure_skip_verify":
|
||||
settings.LDAPTLSInsecureSkipVerify = value == "1"
|
||||
case "auth.oidc.client_id":
|
||||
settings.OIDCClientID = value
|
||||
case "auth.oidc.client_secret":
|
||||
settings.OIDCClientSecret = value
|
||||
case "auth.oidc.authorize_url":
|
||||
settings.OIDCAuthorizeURL = value
|
||||
case "auth.oidc.token_url":
|
||||
settings.OIDCTokenURL = value
|
||||
case "auth.oidc.userinfo_url":
|
||||
settings.OIDCUserInfoURL = value
|
||||
case "auth.oidc.redirect_url":
|
||||
settings.OIDCRedirectURL = value
|
||||
case "auth.oidc.scopes":
|
||||
settings.OIDCScopes = value
|
||||
case "auth.oidc.tls_insecure_skip_verify":
|
||||
settings.OIDCTLSInsecureSkipVerify = value == "1"
|
||||
case "auth.ldap.default_user_group_id":
|
||||
settings.LDAPDefaultUserGroupID = value
|
||||
case "auth.oidc.default_user_group_id":
|
||||
settings.OIDCDefaultUserGroupID = value
|
||||
}
|
||||
}
|
||||
err = rows.Err()
|
||||
if err != nil {
|
||||
return settings, err
|
||||
}
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
func (s *Store) SetAuthSettings(settings models.AuthSettings) error {
|
||||
var tx *sql.Tx
|
||||
var owned bool
|
||||
var err error
|
||||
var now int64
|
||||
var tlsInsecure string
|
||||
tx, owned, err = s.begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
now = time.Now().UTC().Unix()
|
||||
_, err = tx.Exec(`INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?)
|
||||
ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`,
|
||||
"auth.mode", settings.AuthMode, now)
|
||||
if err != nil {
|
||||
rollbackIfOwned(tx, owned)
|
||||
return err
|
||||
}
|
||||
if settings.OIDCEnabled {
|
||||
tlsInsecure = "1"
|
||||
} else {
|
||||
tlsInsecure = "0"
|
||||
}
|
||||
_, err = tx.Exec(`INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?)
|
||||
ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`,
|
||||
"auth.oidc.enabled", tlsInsecure, now)
|
||||
if err != nil {
|
||||
rollbackIfOwned(tx, owned)
|
||||
return err
|
||||
}
|
||||
_, err = tx.Exec(`INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?)
|
||||
ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`,
|
||||
"auth.ldap.url", settings.LDAPURL, now)
|
||||
if err != nil {
|
||||
rollbackIfOwned(tx, owned)
|
||||
return err
|
||||
}
|
||||
_, err = tx.Exec(`INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?)
|
||||
ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`,
|
||||
"auth.ldap.bind_dn", settings.LDAPBindDN, now)
|
||||
if err != nil {
|
||||
rollbackIfOwned(tx, owned)
|
||||
return err
|
||||
}
|
||||
_, err = tx.Exec(`INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?)
|
||||
ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`,
|
||||
"auth.ldap.bind_password", settings.LDAPBindPassword, now)
|
||||
if err != nil {
|
||||
rollbackIfOwned(tx, owned)
|
||||
return err
|
||||
}
|
||||
_, err = tx.Exec(`INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?)
|
||||
ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`,
|
||||
"auth.ldap.user_base_dn", settings.LDAPUserBaseDN, now)
|
||||
if err != nil {
|
||||
rollbackIfOwned(tx, owned)
|
||||
return err
|
||||
}
|
||||
_, err = tx.Exec(`INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?)
|
||||
ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`,
|
||||
"auth.ldap.user_filter", settings.LDAPUserFilter, now)
|
||||
if err != nil {
|
||||
rollbackIfOwned(tx, owned)
|
||||
return err
|
||||
}
|
||||
if settings.LDAPTLSInsecureSkipVerify {
|
||||
tlsInsecure = "1"
|
||||
} else {
|
||||
tlsInsecure = "0"
|
||||
}
|
||||
_, err = tx.Exec(`INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?)
|
||||
ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`,
|
||||
"auth.ldap.tls_insecure_skip_verify", tlsInsecure, now)
|
||||
if err != nil {
|
||||
rollbackIfOwned(tx, owned)
|
||||
return err
|
||||
}
|
||||
_, err = tx.Exec(`INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?)
|
||||
ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`,
|
||||
"auth.oidc.client_id", settings.OIDCClientID, now)
|
||||
if err != nil {
|
||||
rollbackIfOwned(tx, owned)
|
||||
return err
|
||||
}
|
||||
_, err = tx.Exec(`INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?)
|
||||
ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`,
|
||||
"auth.oidc.client_secret", settings.OIDCClientSecret, now)
|
||||
if err != nil {
|
||||
rollbackIfOwned(tx, owned)
|
||||
return err
|
||||
}
|
||||
_, err = tx.Exec(`INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?)
|
||||
ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`,
|
||||
"auth.oidc.authorize_url", settings.OIDCAuthorizeURL, now)
|
||||
if err != nil {
|
||||
rollbackIfOwned(tx, owned)
|
||||
return err
|
||||
}
|
||||
_, err = tx.Exec(`INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?)
|
||||
ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`,
|
||||
"auth.oidc.token_url", settings.OIDCTokenURL, now)
|
||||
if err != nil {
|
||||
rollbackIfOwned(tx, owned)
|
||||
return err
|
||||
}
|
||||
_, err = tx.Exec(`INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?)
|
||||
ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`,
|
||||
"auth.oidc.userinfo_url", settings.OIDCUserInfoURL, now)
|
||||
if err != nil {
|
||||
rollbackIfOwned(tx, owned)
|
||||
return err
|
||||
}
|
||||
_, err = tx.Exec(`INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?)
|
||||
ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`,
|
||||
"auth.oidc.redirect_url", settings.OIDCRedirectURL, now)
|
||||
if err != nil {
|
||||
rollbackIfOwned(tx, owned)
|
||||
return err
|
||||
}
|
||||
_, err = tx.Exec(`INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?)
|
||||
ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`,
|
||||
"auth.oidc.scopes", settings.OIDCScopes, now)
|
||||
if err != nil {
|
||||
rollbackIfOwned(tx, owned)
|
||||
return err
|
||||
}
|
||||
if settings.OIDCTLSInsecureSkipVerify {
|
||||
tlsInsecure = "1"
|
||||
} else {
|
||||
tlsInsecure = "0"
|
||||
}
|
||||
_, err = tx.Exec(`INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?)
|
||||
ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`,
|
||||
"auth.oidc.tls_insecure_skip_verify", tlsInsecure, now)
|
||||
if err != nil {
|
||||
rollbackIfOwned(tx, owned)
|
||||
return err
|
||||
}
|
||||
_, err = tx.Exec(`INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?)
|
||||
ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`,
|
||||
"auth.ldap.default_user_group_id", settings.LDAPDefaultUserGroupID, now)
|
||||
if err != nil {
|
||||
rollbackIfOwned(tx, owned)
|
||||
return err
|
||||
}
|
||||
_, err = tx.Exec(`INSERT INTO app_settings (key, value, updated_at) VALUES (?, ?, ?)
|
||||
ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`,
|
||||
"auth.oidc.default_user_group_id", settings.OIDCDefaultUserGroupID, now)
|
||||
if err != nil {
|
||||
rollbackIfOwned(tx, owned)
|
||||
return err
|
||||
}
|
||||
err = commitIfOwned(tx, owned)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
user.CreatedAt = created.Unix()
|
||||
user.UpdatedAt = updated.Unix()
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetTLSSettings() (models.TLSSettings, error) {
|
||||
@@ -836,16 +618,17 @@ func (s *Store) GetUserByAPIKeyHash(tokenHash string) (models.User, error) {
|
||||
now = time.Now().UTC()
|
||||
currentUnix = now.Unix()
|
||||
row = tx.QueryRow(`
|
||||
SELECT u.public_id, u.username, u.display_name, u.email, u.is_admin, u.disabled, u.auth_source, u.created_at, u.updated_at, k.id
|
||||
SELECT u.public_id, u.username, u.display_name, u.email, u.is_admin, u.disabled, COALESCE(ap.public_id, ''), u.created_at, u.updated_at, k.id
|
||||
FROM api_keys k
|
||||
JOIN users u ON u.id = k.user_id
|
||||
LEFT JOIN auth_providers ap ON ap.id = u.auth_provider_id
|
||||
WHERE k.token_hash = ?
|
||||
AND u.disabled = 0
|
||||
AND k.disabled = 0
|
||||
AND (k.expires_at = 0 OR k.expires_at > ?)
|
||||
LIMIT 1
|
||||
`, tokenHash, currentUnix)
|
||||
err = row.Scan(&user.ID, &user.Username, &user.DisplayName, &user.Email, &user.IsAdmin, &user.Disabled, &user.AuthSource, &created, &updated, &keyID)
|
||||
err = row.Scan(&user.ID, &user.Username, &user.DisplayName, &user.Email, &user.IsAdmin, &user.Disabled, &user.AuthProviderID, &created, &updated, &keyID)
|
||||
if err != nil {
|
||||
rollbackIfOwned(tx, owned)
|
||||
return user, err
|
||||
@@ -901,7 +684,7 @@ func (s *Store) GetSessionUser(token string) (models.User, time.Time, bool, erro
|
||||
var created time.Time
|
||||
var updated time.Time
|
||||
row = s.QueryRow(`
|
||||
SELECT u.public_id, u.username, u.display_name, u.email, u.is_admin, u.disabled, u.auth_source, COALESCE(t.enabled, 0), u.totp_required,
|
||||
SELECT u.public_id, u.username, u.display_name, u.email, u.is_admin, u.disabled, COALESCE(ap.public_id, ''), COALESCE(t.enabled, 0), u.totp_required,
|
||||
(u.totp_required OR EXISTS (
|
||||
SELECT 1 FROM user_group_members m JOIN user_groups g ON g.id = m.group_id
|
||||
WHERE m.user_id = u.id AND g.disabled = 0 AND g.totp_required = 1
|
||||
@@ -912,9 +695,10 @@ func (s *Store) GetSessionUser(token string) (models.User, time.Time, bool, erro
|
||||
s.totp_verified, u.created_at, u.updated_at, s.expires_at
|
||||
FROM sessions s JOIN users u ON u.id = s.user_id
|
||||
LEFT JOIN user_totp t ON t.user_id = u.id
|
||||
LEFT JOIN auth_providers ap ON ap.id = u.auth_provider_id
|
||||
WHERE s.token = ? AND u.disabled = 0
|
||||
`, token)
|
||||
err = row.Scan(&user.ID, &user.Username, &user.DisplayName, &user.Email, &user.IsAdmin, &user.Disabled, &user.AuthSource, &user.TOTPEnabled, &user.TOTPRequired, &user.TOTPEffectiveRequired, &totpVerified, &created, &updated, &expires)
|
||||
err = row.Scan(&user.ID, &user.Username, &user.DisplayName, &user.Email, &user.IsAdmin, &user.Disabled, &user.AuthProviderID, &user.TOTPEnabled, &user.TOTPRequired, &user.TOTPEffectiveRequired, &totpVerified, &created, &updated, &expires)
|
||||
if err != nil {
|
||||
return user, time.Time{}, false, err
|
||||
}
|
||||
|
||||
@@ -171,7 +171,7 @@ func (s *Store) ListTLSAuthPolicies() ([]models.TLSAuthPolicy, error) {
|
||||
var allowedFingerprints string
|
||||
var requiredPermissions string
|
||||
|
||||
rows, err = s.Query(`SELECT public_id, name, description, read_mode, write_mode, require_principal_for_write, allowed_pki_client_cert_ids, allowed_cert_fingerprints, required_permissions, permission_match, required_scope, created_at, updated_at FROM tls_auth_policies ORDER BY name`)
|
||||
rows, err = s.Query(`SELECT p.public_id, p.name, p.description, p.read_mode, p.write_mode, p.require_principal_for_write, COALESCE((SELECT GROUP_CONCAT(c.public_id, ',') FROM tls_auth_policy_allowed_pki_certs apc JOIN pki_certs c ON c.id = apc.cert_id WHERE apc.policy_id = p.id), '') AS allowed_pki_client_cert_ids, p.allowed_cert_fingerprints, p.required_permissions, p.permission_match, p.required_scope, p.created_at, p.updated_at FROM tls_auth_policies p ORDER BY p.name`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -201,7 +201,7 @@ func (s *Store) GetTLSAuthPolicy(id string) (models.TLSAuthPolicy, error) {
|
||||
var allowedFingerprints string
|
||||
var requiredPermissions string
|
||||
|
||||
row = s.QueryRow(`SELECT public_id, name, description, read_mode, write_mode, require_principal_for_write, allowed_pki_client_cert_ids, allowed_cert_fingerprints, required_permissions, permission_match, required_scope, created_at, updated_at FROM tls_auth_policies WHERE public_id = ?`, id)
|
||||
row = s.QueryRow(`SELECT p.public_id, p.name, p.description, p.read_mode, p.write_mode, p.require_principal_for_write, COALESCE((SELECT GROUP_CONCAT(c.public_id, ',') FROM tls_auth_policy_allowed_pki_certs apc JOIN pki_certs c ON c.id = apc.cert_id WHERE apc.policy_id = p.id), '') AS allowed_pki_client_cert_ids, p.allowed_cert_fingerprints, p.required_permissions, p.permission_match, p.required_scope, p.created_at, p.updated_at FROM tls_auth_policies p WHERE p.public_id = ?`, id)
|
||||
err = row.Scan(&item.ID, &item.Name, &item.Description, &item.ReadMode, &item.WriteMode, &item.RequirePrincipalForWrite, &allowedPKICertIDs, &allowedFingerprints, &requiredPermissions, &item.PermissionMatch, &item.RequiredScope, &item.CreatedAt, &item.UpdatedAt)
|
||||
if err != nil {
|
||||
return item, err
|
||||
@@ -216,6 +216,10 @@ func (s *Store) CreateTLSAuthPolicy(item models.TLSAuthPolicy) (models.TLSAuthPo
|
||||
var id string
|
||||
var now int64
|
||||
var err error
|
||||
var result sql.Result
|
||||
var internalID int64
|
||||
var i int
|
||||
var certPublicID string
|
||||
|
||||
if item.ID == "" {
|
||||
id, err = util.NewID()
|
||||
@@ -227,23 +231,55 @@ func (s *Store) CreateTLSAuthPolicy(item models.TLSAuthPolicy) (models.TLSAuthPo
|
||||
now = time.Now().UTC().Unix()
|
||||
item.CreatedAt = now
|
||||
item.UpdatedAt = now
|
||||
_, err = s.Exec(`INSERT INTO tls_auth_policies (public_id, name, description, read_mode, write_mode, require_principal_for_write, allowed_pki_client_cert_ids, allowed_cert_fingerprints, required_permissions, permission_match, required_scope, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
item.ID, item.Name, item.Description, item.ReadMode, item.WriteMode, item.RequirePrincipalForWrite, strings.Join(item.AllowedPKIClientCertIDs, ","), strings.Join(item.AllowedCertFingerprints, ","), strings.Join(item.RequiredPermissions, ","), item.PermissionMatch, item.RequiredScope, item.CreatedAt, item.UpdatedAt)
|
||||
result, err = s.Exec(`INSERT INTO tls_auth_policies (public_id, name, description, read_mode, write_mode, require_principal_for_write, allowed_cert_fingerprints, required_permissions, permission_match, required_scope, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
item.ID, item.Name, item.Description, item.ReadMode, item.WriteMode, item.RequirePrincipalForWrite, strings.Join(item.AllowedCertFingerprints, ","), strings.Join(item.RequiredPermissions, ","), item.PermissionMatch, item.RequiredScope, item.CreatedAt, item.UpdatedAt)
|
||||
if err != nil {
|
||||
return item, err
|
||||
}
|
||||
internalID, err = result.LastInsertId()
|
||||
if err != nil {
|
||||
return item, err
|
||||
}
|
||||
for i = 0; i < len(item.AllowedPKIClientCertIDs); i++ {
|
||||
certPublicID = item.AllowedPKIClientCertIDs[i]
|
||||
_, err = s.Exec(`INSERT OR IGNORE INTO tls_auth_policy_allowed_pki_certs (policy_id, cert_id) SELECT ?, id FROM pki_certs WHERE public_id = ?`, internalID, certPublicID)
|
||||
if err != nil {
|
||||
return item, err
|
||||
}
|
||||
}
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (s *Store) UpdateTLSAuthPolicy(item models.TLSAuthPolicy) error {
|
||||
var now int64
|
||||
var err error
|
||||
var row *sql.Row
|
||||
var internalID int64
|
||||
var i int
|
||||
|
||||
row = s.QueryRow(`SELECT id FROM tls_auth_policies WHERE public_id = ?`, item.ID)
|
||||
err = row.Scan(&internalID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
now = time.Now().UTC().Unix()
|
||||
item.UpdatedAt = now
|
||||
_, err = s.Exec(`UPDATE tls_auth_policies SET name = ?, description = ?, read_mode = ?, write_mode = ?, require_principal_for_write = ?, allowed_pki_client_cert_ids = ?, allowed_cert_fingerprints = ?, required_permissions = ?, permission_match = ?, required_scope = ?, updated_at = ? WHERE public_id = ?`,
|
||||
item.Name, item.Description, item.ReadMode, item.WriteMode, item.RequirePrincipalForWrite, strings.Join(item.AllowedPKIClientCertIDs, ","), strings.Join(item.AllowedCertFingerprints, ","), strings.Join(item.RequiredPermissions, ","), item.PermissionMatch, item.RequiredScope, item.UpdatedAt, item.ID)
|
||||
return err
|
||||
_, err = s.Exec(`UPDATE tls_auth_policies SET name = ?, description = ?, read_mode = ?, write_mode = ?, require_principal_for_write = ?, allowed_cert_fingerprints = ?, required_permissions = ?, permission_match = ?, required_scope = ?, updated_at = ? WHERE public_id = ?`,
|
||||
item.Name, item.Description, item.ReadMode, item.WriteMode, item.RequirePrincipalForWrite, strings.Join(item.AllowedCertFingerprints, ","), strings.Join(item.RequiredPermissions, ","), item.PermissionMatch, item.RequiredScope, item.UpdatedAt, item.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = s.Exec(`DELETE FROM tls_auth_policy_allowed_pki_certs WHERE policy_id = ?`, internalID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i = 0; i < len(item.AllowedPKIClientCertIDs); i++ {
|
||||
_, err = s.Exec(`INSERT OR IGNORE INTO tls_auth_policy_allowed_pki_certs (policy_id, cert_id) SELECT ?, id FROM pki_certs WHERE public_id = ?`, internalID, item.AllowedPKIClientCertIDs[i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) DeleteTLSAuthPolicy(id string) error {
|
||||
|
||||
@@ -15,7 +15,7 @@ func (s *Store) ListTLSListeners() ([]models.TLSListener, error) {
|
||||
var httpAddrs string
|
||||
var httpsAddrs string
|
||||
var endpointPoliciesJSON string
|
||||
rows, err = s.Query(`SELECT l.public_id, l.name, l.enabled, l.http_addrs, l.https_addrs, l.endpoint_policies_json, l.tls_server_cert_source, l.tls_cert_file, l.tls_key_file, l.tls_pki_server_cert_id, l.tls_client_auth, l.tls_client_ca_file, l.tls_pki_client_ca_id, l.tls_min_version, l.created_at, l.updated_at FROM tls_listeners l ORDER BY l.name`)
|
||||
rows, err = s.Query(`SELECT l.public_id, l.name, l.enabled, l.http_addrs, l.https_addrs, l.endpoint_policies_json, l.tls_server_cert_source, l.tls_cert_file, l.tls_key_file, COALESCE(sc.public_id, ''), l.tls_client_auth, l.tls_client_ca_file, COALESCE(cca.public_id, ''), l.tls_min_version, l.created_at, l.updated_at FROM tls_listeners l LEFT JOIN pki_certs sc ON sc.id = l.tls_pki_server_cert_id LEFT JOIN pki_cas cca ON cca.id = l.tls_pki_client_ca_id ORDER BY l.name`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -44,7 +44,7 @@ func (s *Store) GetTLSListener(id string) (models.TLSListener, error) {
|
||||
var httpsAddrs string
|
||||
var endpointPoliciesJSON string
|
||||
var err error
|
||||
row = s.QueryRow(`SELECT l.public_id, l.name, l.enabled, l.http_addrs, l.https_addrs, l.endpoint_policies_json, l.tls_server_cert_source, l.tls_cert_file, l.tls_key_file, l.tls_pki_server_cert_id, l.tls_client_auth, l.tls_client_ca_file, l.tls_pki_client_ca_id, l.tls_min_version, l.created_at, l.updated_at FROM tls_listeners l WHERE l.public_id = ?`, id)
|
||||
row = s.QueryRow(`SELECT l.public_id, l.name, l.enabled, l.http_addrs, l.https_addrs, l.endpoint_policies_json, l.tls_server_cert_source, l.tls_cert_file, l.tls_key_file, COALESCE(sc.public_id, ''), l.tls_client_auth, l.tls_client_ca_file, COALESCE(cca.public_id, ''), l.tls_min_version, l.created_at, l.updated_at FROM tls_listeners l LEFT JOIN pki_certs sc ON sc.id = l.tls_pki_server_cert_id LEFT JOIN pki_cas cca ON cca.id = l.tls_pki_client_ca_id WHERE l.public_id = ?`, id)
|
||||
err = row.Scan(&item.ID, &item.Name, &item.Enabled, &httpAddrs, &httpsAddrs, &endpointPoliciesJSON, &item.TLSServerCertSource, &item.TLSCertFile, &item.TLSKeyFile, &item.TLSPKIServerCertID, &item.TLSClientAuth, &item.TLSClientCAFile, &item.TLSPKIClientCAID, &item.TLSMinVersion, &item.CreatedAt, &item.UpdatedAt)
|
||||
if err != nil {
|
||||
return item, err
|
||||
@@ -74,7 +74,7 @@ func (s *Store) CreateTLSListener(item models.TLSListener) (models.TLSListener,
|
||||
if err != nil {
|
||||
return item, err
|
||||
}
|
||||
_, err = s.Exec(`INSERT INTO tls_listeners (public_id, name, enabled, http_addrs, https_addrs, endpoint_policies_json, tls_server_cert_source, tls_cert_file, tls_key_file, tls_pki_server_cert_id, tls_client_auth, tls_client_ca_file, tls_pki_client_ca_id, tls_min_version, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
_, err = s.Exec(`INSERT INTO tls_listeners (public_id, name, enabled, http_addrs, https_addrs, endpoint_policies_json, tls_server_cert_source, tls_cert_file, tls_key_file, tls_pki_server_cert_id, tls_client_auth, tls_client_ca_file, tls_pki_client_ca_id, tls_min_version, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, (SELECT id FROM pki_certs WHERE public_id = ?), ?, ?, (SELECT id FROM pki_cas WHERE public_id = ?), ?, ?, ?)`,
|
||||
item.ID, item.Name, item.Enabled, strings.Join(item.HTTPAddrs, ","), strings.Join(item.HTTPSAddrs, ","), endpointPoliciesJSON, item.TLSServerCertSource, item.TLSCertFile, item.TLSKeyFile, item.TLSPKIServerCertID, item.TLSClientAuth, item.TLSClientCAFile, item.TLSPKIClientCAID, item.TLSMinVersion, item.CreatedAt, item.UpdatedAt)
|
||||
if err != nil {
|
||||
return item, err
|
||||
@@ -92,7 +92,7 @@ func (s *Store) UpdateTLSListener(item models.TLSListener) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = s.Exec(`UPDATE tls_listeners SET name = ?, enabled = ?, http_addrs = ?, https_addrs = ?, endpoint_policies_json = ?, tls_server_cert_source = ?, tls_cert_file = ?, tls_key_file = ?, tls_pki_server_cert_id = ?, tls_client_auth = ?, tls_client_ca_file = ?, tls_pki_client_ca_id = ?, tls_min_version = ?, updated_at = ? WHERE public_id = ?`,
|
||||
_, err = s.Exec(`UPDATE tls_listeners SET name = ?, enabled = ?, http_addrs = ?, https_addrs = ?, endpoint_policies_json = ?, tls_server_cert_source = ?, tls_cert_file = ?, tls_key_file = ?, tls_pki_server_cert_id = (SELECT id FROM pki_certs WHERE public_id = ?), tls_client_auth = ?, tls_client_ca_file = ?, tls_pki_client_ca_id = (SELECT id FROM pki_cas WHERE public_id = ?), tls_min_version = ?, updated_at = ? WHERE public_id = ?`,
|
||||
item.Name, item.Enabled, strings.Join(item.HTTPAddrs, ","), strings.Join(item.HTTPSAddrs, ","), endpointPoliciesJSON, item.TLSServerCertSource, item.TLSCertFile, item.TLSKeyFile, item.TLSPKIServerCertID, item.TLSClientAuth, item.TLSClientCAFile, item.TLSPKIClientCAID, item.TLSMinVersion, item.UpdatedAt, item.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -161,27 +161,7 @@ type updateMeRequest struct {
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type updateAuthSettingsRequest struct {
|
||||
AuthMode string `json:"auth_mode"`
|
||||
OIDCEnabled bool `json:"oidc_enabled"`
|
||||
LDAPURL string `json:"ldap_url"`
|
||||
LDAPBindDN string `json:"ldap_bind_dn"`
|
||||
LDAPBindPassword string `json:"ldap_bind_password"`
|
||||
LDAPUserBaseDN string `json:"ldap_user_base_dn"`
|
||||
LDAPUserFilter string `json:"ldap_user_filter"`
|
||||
LDAPTLSInsecureSkipVerify bool `json:"ldap_tls_insecure_skip_verify"`
|
||||
OIDCClientID string `json:"oidc_client_id"`
|
||||
OIDCClientSecret string `json:"oidc_client_secret"`
|
||||
OIDCAuthorizeURL string `json:"oidc_authorize_url"`
|
||||
OIDCTokenURL string `json:"oidc_token_url"`
|
||||
OIDCUserInfoURL string `json:"oidc_userinfo_url"`
|
||||
OIDCRedirectURL string `json:"oidc_redirect_url"`
|
||||
OIDCScopes string `json:"oidc_scopes"`
|
||||
OIDCTLSInsecureSkipVerify bool `json:"oidc_tls_insecure_skip_verify"`
|
||||
}
|
||||
|
||||
type testLDAPSettingsRequest struct {
|
||||
AuthMode string `json:"auth_mode"`
|
||||
LDAPURL string `json:"ldap_url"`
|
||||
LDAPBindDN string `json:"ldap_bind_dn"`
|
||||
LDAPBindPassword string `json:"ldap_bind_password"`
|
||||
@@ -439,11 +419,11 @@ type projectGroupRoleRequest struct {
|
||||
}
|
||||
|
||||
type userGroupRequest struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Disabled bool `json:"disabled"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Disabled bool `json:"disabled"`
|
||||
TOTPRequired bool `json:"totp_required"`
|
||||
Scope string `json:"scope"`
|
||||
Scope string `json:"scope"`
|
||||
}
|
||||
|
||||
type userGroupMemberRequest struct {
|
||||
@@ -458,7 +438,14 @@ func (api *API) Login(w http.ResponseWriter, r *http.Request, _ map[string]strin
|
||||
var ldapUser auth.LDAPUser
|
||||
var newUser models.User
|
||||
var created models.User
|
||||
var authCfg config.Config
|
||||
var dbProvider models.AuthProvider
|
||||
var providers []models.AuthProvider
|
||||
var p models.AuthProvider
|
||||
var cfg config.Config
|
||||
|
||||
// this usually handles login by builtin mechanism.
|
||||
// an external mechanism is handled by a relevant callback.
|
||||
// for example, OIDCCallbackWithProvider for oidc
|
||||
err = DecodeJSON(r, &req)
|
||||
if err != nil {
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "invalid json")
|
||||
@@ -469,48 +456,80 @@ func (api *API) Login(w http.ResponseWriter, r *http.Request, _ map[string]strin
|
||||
return
|
||||
}
|
||||
|
||||
// we need to find the user in the database
|
||||
user, storedHash, err = api.store(r).GetUserByUsername(req.Username)
|
||||
if err == nil && user.Disabled {
|
||||
WriteJSONWithErrorReason(w, r, http.StatusForbidden, "user disabled")
|
||||
return
|
||||
}
|
||||
if err == nil && storedHash != "" && auth.ComparePassword(storedHash, req.Password) == nil {
|
||||
|
||||
// if db authentication method is enabled, compare the password against the password loaded from the database
|
||||
// if the stored password is not blank and the given password is equal to it, it calls finishLogin()
|
||||
// to issue a session or require an otp code depending on configuration.
|
||||
dbProvider, _ = api.store(r).GetAuthProvider(db.BuiltinDBProviderPublicID)
|
||||
if dbProvider.Enabled && err == nil && user.AuthProviderID == db.BuiltinDBProviderPublicID && storedHash != "" && auth.ComparePassword(storedHash, req.Password) == nil {
|
||||
api.finishLogin(w, r, user, req.OTPCode)
|
||||
return
|
||||
}
|
||||
|
||||
authCfg, _ = api.effectiveAuthConfig(r.Context())
|
||||
if authCfg.AuthMode == "ldap" || authCfg.AuthMode == "hybrid" {
|
||||
api.Logger.Write(logIDAuth, codit_logger.LOG_INFO, "ldap login attempt username=%s mode=%s", req.Username, authCfg.AuthMode)
|
||||
ldapUser, err = auth.LDAPAuthenticateContext(r.Context(), authCfg, req.Username, req.Password)
|
||||
if err == nil {
|
||||
api.Logger.Write(logIDAuth, codit_logger.LOG_INFO, "ldap login success username=%s", req.Username)
|
||||
if user.ID != "" && user.Disabled {
|
||||
WriteJSONWithErrorReason(w, r, http.StatusForbidden, "user disabled")
|
||||
// try ldap authentication
|
||||
providers, err = api.store(r).ListEnabledAuthProviders() // gets the enabled providers only
|
||||
if err != nil {
|
||||
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, fmt.Sprintf("failed to list enabled auth providers - %s", err.Error()))
|
||||
return
|
||||
}
|
||||
for _, p = range providers {
|
||||
if p.Type != db.AuthProviderTypeLDAP { continue }
|
||||
|
||||
api.Logger.Write(logIDAuth, codit_logger.LOG_INFO, "ldap login attempt username=%s provider=%s", req.Username, p.Name)
|
||||
cfg = api.Cfg
|
||||
cfg.LDAPURL = p.LDAPUrl
|
||||
cfg.LDAPBindDN = p.LDAPBindDN
|
||||
cfg.LDAPBindPassword = p.LDAPBindPassword
|
||||
cfg.LDAPUserBaseDN = p.LDAPUserBaseDN
|
||||
cfg.LDAPUserFilter = p.LDAPUserFilter
|
||||
cfg.LDAPTLSInsecureSkipVerify = p.LDAPTLSInsecureSkipVerify
|
||||
ldapUser, err = auth.LDAPAuthenticateContext(r.Context(), cfg, req.Username, req.Password)
|
||||
if err != nil {
|
||||
api.Logger.Write(logIDAuth, codit_logger.LOG_WARN, "ldap login failed username=%s provider=%s err=%v", req.Username, p.Name, err)
|
||||
continue
|
||||
}
|
||||
api.Logger.Write(logIDAuth, codit_logger.LOG_INFO, "ldap login success username=%s provider=%s", req.Username, p.Name)
|
||||
if user.ID != "" {
|
||||
if user.AuthProviderID != p.ID {
|
||||
// the user entry exists in the database.
|
||||
// check if the user entry has been created of this ldap server.
|
||||
api.Logger.Write(logIDAuth, codit_logger.LOG_WARN, "ldap login rejected username=%s provider=%s reason=provider_mismatch user_provider=%s", req.Username, p.Name, user.AuthProviderID)
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
// user not found. create a new user from this ldap auth source.
|
||||
// in this case, the user info from the first successful ldap server wins.
|
||||
// it can be confusing to users if there are duplicate user IDs across
|
||||
// multiple enabled ldap servers and thier passwords are not the same
|
||||
// on those ldap servres.
|
||||
newUser = models.User{
|
||||
Username: ldapUser.Username,
|
||||
DisplayName: ldapUser.DisplayName,
|
||||
Email: ldapUser.Email,
|
||||
AuthProviderID: p.ID,
|
||||
}
|
||||
|
||||
if newUser.Email == "" { newUser.Email = ldapUser.Username + "@ldap" }
|
||||
|
||||
created, err = api.store(r).CreateUser(newUser, "")
|
||||
if err == nil {
|
||||
user = created
|
||||
if p.DefaultUserGroupID != "" {
|
||||
_ = api.store(r).AddUserGroupMember(p.DefaultUserGroupID, user.ID)
|
||||
}
|
||||
} else {
|
||||
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if user.ID == "" {
|
||||
newUser = models.User{
|
||||
Username: ldapUser.Username,
|
||||
DisplayName: ldapUser.DisplayName,
|
||||
Email: ldapUser.Email,
|
||||
AuthSource: "ldap",
|
||||
}
|
||||
if newUser.Email == "" {
|
||||
newUser.Email = ldapUser.Username + "@local"
|
||||
}
|
||||
created, err = api.store(r).CreateUser(newUser, "")
|
||||
if err == nil {
|
||||
user = created
|
||||
if authCfg.LDAPDefaultUserGroupID != "" {
|
||||
_ = api.store(r).AddUserGroupMember(authCfg.LDAPDefaultUserGroupID, user.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
api.finishLogin(w, r, user, req.OTPCode)
|
||||
return
|
||||
}
|
||||
api.Logger.Write(logIDAuth, codit_logger.LOG_WARN, "ldap login failed username=%s err=%v", req.Username, err)
|
||||
api.finishLogin(w, r, user, req.OTPCode)
|
||||
return
|
||||
}
|
||||
|
||||
WriteJSONWithErrorReason(w, r, http.StatusUnauthorized, "invalid credentials")
|
||||
@@ -521,10 +540,10 @@ func (api *API) issueSession(w http.ResponseWriter, r *http.Request, user models
|
||||
var err error
|
||||
var expires time.Time
|
||||
var cookie *http.Cookie
|
||||
|
||||
token, err = auth.NewSessionToken()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err != nil { return }
|
||||
|
||||
expires = auth.SessionExpiry(api.Cfg)
|
||||
_ = api.store(r).DeleteExpiredSessions(time.Now().UTC())
|
||||
_ = api.store(r).CreateSession(user.ID, token, expires, totpVerified)
|
||||
@@ -611,7 +630,7 @@ func (api *API) UpdateMe(w http.ResponseWriter, r *http.Request, _ map[string]st
|
||||
user.Email = req.Email
|
||||
}
|
||||
newPassword = strings.TrimSpace(req.Password) != ""
|
||||
if newPassword && user.AuthSource != "db" {
|
||||
if newPassword && user.AuthProviderID != "" {
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "password update not supported for non-db users")
|
||||
return
|
||||
}
|
||||
@@ -674,12 +693,12 @@ func (api *API) CreateUser(w http.ResponseWriter, r *http.Request, _ map[string]
|
||||
return
|
||||
}
|
||||
user = models.User{
|
||||
Username: req.Username,
|
||||
DisplayName: req.DisplayName,
|
||||
Email: req.Email,
|
||||
IsAdmin: req.IsAdmin,
|
||||
TOTPRequired: req.TOTPRequired,
|
||||
AuthSource: "db",
|
||||
Username: req.Username,
|
||||
DisplayName: req.DisplayName,
|
||||
Email: req.Email,
|
||||
IsAdmin: req.IsAdmin,
|
||||
TOTPRequired: req.TOTPRequired,
|
||||
AuthProviderID: db.BuiltinDBProviderPublicID,
|
||||
}
|
||||
created, err = api.store(r).CreateUser(user, hash)
|
||||
if err != nil {
|
||||
@@ -922,87 +941,11 @@ func (api *API) RemoveUserGroupMember(w http.ResponseWriter, r *http.Request, pa
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (api *API) GetAuthSettings(w http.ResponseWriter, r *http.Request, _ map[string]string) {
|
||||
var settings models.AuthSettings
|
||||
var err error
|
||||
var user models.User
|
||||
var ok bool
|
||||
if !api.requireAdmin(w, r) {
|
||||
return
|
||||
}
|
||||
user, ok = middleware.UserFromContext(r.Context())
|
||||
if ok {
|
||||
api.Logger.Write(logIDAuth, codit_logger.LOG_INFO, "auth settings fetch requested by user=%s", user.Username)
|
||||
}
|
||||
settings, err = api.getMergedAuthSettings(r.Context())
|
||||
if err != nil {
|
||||
api.Logger.Write(logIDAuth, codit_logger.LOG_ERROR, "auth settings fetch failed err=%v", err)
|
||||
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
api.Logger.Write(logIDAuth, codit_logger.LOG_INFO, "auth settings fetch success mode=%s ldap_url=%s oidc_authorize_url=%s", settings.AuthMode, settings.LDAPURL, settings.OIDCAuthorizeURL)
|
||||
WriteJSON(w, http.StatusOK, settings)
|
||||
}
|
||||
|
||||
func (api *API) UpdateAuthSettings(w http.ResponseWriter, r *http.Request, _ map[string]string) {
|
||||
var req updateAuthSettingsRequest
|
||||
var err error
|
||||
var settings models.AuthSettings
|
||||
var user models.User
|
||||
var ok bool
|
||||
if !api.requireAdmin(w, r) {
|
||||
return
|
||||
}
|
||||
user, ok = middleware.UserFromContext(r.Context())
|
||||
err = DecodeJSON(r, &req)
|
||||
if err != nil {
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "invalid json")
|
||||
return
|
||||
}
|
||||
settings = models.AuthSettings{
|
||||
AuthMode: normalizeAuthMode(req.AuthMode),
|
||||
OIDCEnabled: req.OIDCEnabled,
|
||||
LDAPURL: strings.TrimSpace(req.LDAPURL),
|
||||
LDAPBindDN: strings.TrimSpace(req.LDAPBindDN),
|
||||
LDAPBindPassword: req.LDAPBindPassword,
|
||||
LDAPUserBaseDN: strings.TrimSpace(req.LDAPUserBaseDN),
|
||||
LDAPUserFilter: strings.TrimSpace(req.LDAPUserFilter),
|
||||
LDAPTLSInsecureSkipVerify: req.LDAPTLSInsecureSkipVerify,
|
||||
OIDCClientID: strings.TrimSpace(req.OIDCClientID),
|
||||
OIDCClientSecret: req.OIDCClientSecret,
|
||||
OIDCAuthorizeURL: strings.TrimSpace(req.OIDCAuthorizeURL),
|
||||
OIDCTokenURL: strings.TrimSpace(req.OIDCTokenURL),
|
||||
OIDCUserInfoURL: strings.TrimSpace(req.OIDCUserInfoURL),
|
||||
OIDCRedirectURL: strings.TrimSpace(req.OIDCRedirectURL),
|
||||
OIDCScopes: strings.TrimSpace(req.OIDCScopes),
|
||||
OIDCTLSInsecureSkipVerify: req.OIDCTLSInsecureSkipVerify,
|
||||
}
|
||||
if settings.LDAPUserFilter == "" {
|
||||
settings.LDAPUserFilter = "(uid={username})"
|
||||
}
|
||||
if settings.OIDCScopes == "" {
|
||||
settings.OIDCScopes = "openid profile email"
|
||||
}
|
||||
if ok {
|
||||
api.Logger.Write(logIDAuth, codit_logger.LOG_INFO, "auth settings update requested by user=%s mode=%s oidc_enabled=%t ldap_url=%s oidc_authorize_url=%s",
|
||||
user.Username, settings.AuthMode, settings.OIDCEnabled, settings.LDAPURL, settings.OIDCAuthorizeURL)
|
||||
}
|
||||
err = api.store(r).SetAuthSettings(settings)
|
||||
if err != nil {
|
||||
api.Logger.Write(logIDAuth, codit_logger.LOG_ERROR, "auth settings update failed err=%v", err)
|
||||
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
api.Logger.Write(logIDAuth, codit_logger.LOG_INFO, "auth settings update success mode=%s oidc_enabled=%t ldap_url=%s oidc_authorize_url=%s", settings.AuthMode, settings.OIDCEnabled, settings.LDAPURL, settings.OIDCAuthorizeURL)
|
||||
WriteJSON(w, http.StatusOK, settings)
|
||||
}
|
||||
|
||||
func (api *API) TestLDAPSettings(w http.ResponseWriter, r *http.Request, _ map[string]string) {
|
||||
var req testLDAPSettingsRequest
|
||||
var err error
|
||||
var merged models.AuthSettings
|
||||
var cfg config.Config
|
||||
var user auth.LDAPUser
|
||||
var ldapUser auth.LDAPUser
|
||||
if !api.requireAdmin(w, r) {
|
||||
return
|
||||
}
|
||||
@@ -1011,42 +954,18 @@ func (api *API) TestLDAPSettings(w http.ResponseWriter, r *http.Request, _ map[s
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "invalid json")
|
||||
return
|
||||
}
|
||||
merged, err = api.getMergedAuthSettings(r.Context())
|
||||
if err != nil {
|
||||
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(req.AuthMode) != "" {
|
||||
merged.AuthMode = normalizeAuthMode(req.AuthMode)
|
||||
}
|
||||
if strings.TrimSpace(req.LDAPURL) != "" {
|
||||
merged.LDAPURL = strings.TrimSpace(req.LDAPURL)
|
||||
}
|
||||
if strings.TrimSpace(req.LDAPBindDN) != "" {
|
||||
merged.LDAPBindDN = strings.TrimSpace(req.LDAPBindDN)
|
||||
}
|
||||
if req.LDAPBindPassword != "" {
|
||||
merged.LDAPBindPassword = req.LDAPBindPassword
|
||||
}
|
||||
if strings.TrimSpace(req.LDAPUserBaseDN) != "" {
|
||||
merged.LDAPUserBaseDN = strings.TrimSpace(req.LDAPUserBaseDN)
|
||||
}
|
||||
if strings.TrimSpace(req.LDAPUserFilter) != "" {
|
||||
merged.LDAPUserFilter = strings.TrimSpace(req.LDAPUserFilter)
|
||||
cfg = api.Cfg
|
||||
cfg.LDAPURL = strings.TrimSpace(req.LDAPURL)
|
||||
cfg.LDAPBindDN = strings.TrimSpace(req.LDAPBindDN)
|
||||
cfg.LDAPBindPassword = req.LDAPBindPassword
|
||||
cfg.LDAPUserBaseDN = strings.TrimSpace(req.LDAPUserBaseDN)
|
||||
cfg.LDAPUserFilter = strings.TrimSpace(req.LDAPUserFilter)
|
||||
if cfg.LDAPUserFilter == "" {
|
||||
cfg.LDAPUserFilter = "(uid={username})"
|
||||
}
|
||||
if req.LDAPTLSInsecureSkipVerify != nil {
|
||||
merged.LDAPTLSInsecureSkipVerify = *req.LDAPTLSInsecureSkipVerify
|
||||
cfg.LDAPTLSInsecureSkipVerify = *req.LDAPTLSInsecureSkipVerify
|
||||
}
|
||||
if merged.LDAPUserFilter == "" {
|
||||
merged.LDAPUserFilter = "(uid={username})"
|
||||
}
|
||||
cfg = api.Cfg
|
||||
cfg.AuthMode = merged.AuthMode
|
||||
cfg.LDAPURL = merged.LDAPURL
|
||||
cfg.LDAPBindDN = merged.LDAPBindDN
|
||||
cfg.LDAPBindPassword = merged.LDAPBindPassword
|
||||
cfg.LDAPUserBaseDN = merged.LDAPUserBaseDN
|
||||
cfg.LDAPUserFilter = merged.LDAPUserFilter
|
||||
err = auth.LDAPTestConnectionContext(r.Context(), cfg)
|
||||
if err != nil {
|
||||
api.Logger.Write(logIDAuth, codit_logger.LOG_WARN, "ldap test connection failed url=%s err=%v", cfg.LDAPURL, err)
|
||||
@@ -1055,14 +974,14 @@ func (api *API) TestLDAPSettings(w http.ResponseWriter, r *http.Request, _ map[s
|
||||
}
|
||||
api.Logger.Write(logIDAuth, codit_logger.LOG_INFO, "ldap test connection success url=%s", cfg.LDAPURL)
|
||||
if strings.TrimSpace(req.Username) != "" && strings.TrimSpace(req.Password) != "" {
|
||||
user, err = auth.LDAPAuthenticateContext(r.Context(), cfg, strings.TrimSpace(req.Username), req.Password)
|
||||
ldapUser, err = auth.LDAPAuthenticateContext(r.Context(), cfg, strings.TrimSpace(req.Username), req.Password)
|
||||
if err != nil {
|
||||
api.Logger.Write(logIDAuth, codit_logger.LOG_WARN, "ldap test bind failed username=%s err=%v", strings.TrimSpace(req.Username), err)
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
api.Logger.Write(logIDAuth, codit_logger.LOG_INFO, "ldap test bind success username=%s", user.Username)
|
||||
WriteJSON(w, http.StatusOK, map[string]string{"status": "ok", "user": user.Username})
|
||||
api.Logger.Write(logIDAuth, codit_logger.LOG_INFO, "ldap test bind success username=%s", ldapUser.Username)
|
||||
WriteJSON(w, http.StatusOK, map[string]string{"status": "ok", "user": ldapUser.Username})
|
||||
return
|
||||
}
|
||||
WriteJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
||||
@@ -5532,123 +5451,6 @@ func normalizeProjectHomePage(value string) string {
|
||||
return "info"
|
||||
}
|
||||
|
||||
func normalizeAuthMode(value string) string {
|
||||
var v string
|
||||
v = strings.ToLower(strings.TrimSpace(value))
|
||||
if v == "ldap" || v == "hybrid" || v == "db" {
|
||||
return v
|
||||
}
|
||||
return "db"
|
||||
}
|
||||
|
||||
func (api *API) getMergedAuthSettings(ctx context.Context) (models.AuthSettings, error) {
|
||||
var settings models.AuthSettings
|
||||
var saved models.AuthSettings
|
||||
var err error
|
||||
settings = models.AuthSettings{
|
||||
AuthMode: normalizeAuthMode(api.Cfg.AuthMode),
|
||||
LDAPURL: api.Cfg.LDAPURL,
|
||||
LDAPBindDN: api.Cfg.LDAPBindDN,
|
||||
LDAPBindPassword: api.Cfg.LDAPBindPassword,
|
||||
LDAPUserBaseDN: api.Cfg.LDAPUserBaseDN,
|
||||
LDAPUserFilter: api.Cfg.LDAPUserFilter,
|
||||
LDAPTLSInsecureSkipVerify: api.Cfg.LDAPTLSInsecureSkipVerify,
|
||||
OIDCClientID: api.Cfg.OIDCClientID,
|
||||
OIDCClientSecret: api.Cfg.OIDCClientSecret,
|
||||
OIDCAuthorizeURL: api.Cfg.OIDCAuthorizeURL,
|
||||
OIDCTokenURL: api.Cfg.OIDCTokenURL,
|
||||
OIDCUserInfoURL: api.Cfg.OIDCUserInfoURL,
|
||||
OIDCRedirectURL: api.Cfg.OIDCRedirectURL,
|
||||
OIDCScopes: api.Cfg.OIDCScopes,
|
||||
OIDCEnabled: api.Cfg.OIDCEnabled,
|
||||
OIDCTLSInsecureSkipVerify: api.Cfg.OIDCTLSInsecureSkipVerify,
|
||||
}
|
||||
saved, err = api.storeContext(ctx).GetAuthSettings()
|
||||
if err != nil {
|
||||
return settings, err
|
||||
}
|
||||
if strings.TrimSpace(saved.AuthMode) != "" {
|
||||
settings.AuthMode = normalizeAuthMode(saved.AuthMode)
|
||||
}
|
||||
settings.OIDCEnabled = saved.OIDCEnabled
|
||||
if strings.ToLower(strings.TrimSpace(saved.AuthMode)) == "oidc" {
|
||||
settings.OIDCEnabled = true
|
||||
}
|
||||
if strings.TrimSpace(saved.LDAPURL) != "" {
|
||||
settings.LDAPURL = saved.LDAPURL
|
||||
}
|
||||
if strings.TrimSpace(saved.LDAPBindDN) != "" {
|
||||
settings.LDAPBindDN = saved.LDAPBindDN
|
||||
}
|
||||
if saved.LDAPBindPassword != "" {
|
||||
settings.LDAPBindPassword = saved.LDAPBindPassword
|
||||
}
|
||||
if strings.TrimSpace(saved.LDAPUserBaseDN) != "" {
|
||||
settings.LDAPUserBaseDN = saved.LDAPUserBaseDN
|
||||
}
|
||||
if strings.TrimSpace(saved.LDAPUserFilter) != "" {
|
||||
settings.LDAPUserFilter = saved.LDAPUserFilter
|
||||
}
|
||||
if settings.LDAPUserFilter == "" {
|
||||
settings.LDAPUserFilter = "(uid={username})"
|
||||
}
|
||||
if strings.TrimSpace(saved.OIDCClientID) != "" {
|
||||
settings.OIDCClientID = saved.OIDCClientID
|
||||
}
|
||||
if strings.TrimSpace(saved.OIDCClientSecret) != "" {
|
||||
settings.OIDCClientSecret = saved.OIDCClientSecret
|
||||
}
|
||||
if strings.TrimSpace(saved.OIDCAuthorizeURL) != "" {
|
||||
settings.OIDCAuthorizeURL = saved.OIDCAuthorizeURL
|
||||
}
|
||||
if strings.TrimSpace(saved.OIDCTokenURL) != "" {
|
||||
settings.OIDCTokenURL = saved.OIDCTokenURL
|
||||
}
|
||||
if strings.TrimSpace(saved.OIDCUserInfoURL) != "" {
|
||||
settings.OIDCUserInfoURL = saved.OIDCUserInfoURL
|
||||
}
|
||||
if strings.TrimSpace(saved.OIDCRedirectURL) != "" {
|
||||
settings.OIDCRedirectURL = saved.OIDCRedirectURL
|
||||
}
|
||||
if strings.TrimSpace(saved.OIDCScopes) != "" {
|
||||
settings.OIDCScopes = saved.OIDCScopes
|
||||
}
|
||||
settings.OIDCTLSInsecureSkipVerify = saved.OIDCTLSInsecureSkipVerify
|
||||
if strings.TrimSpace(settings.OIDCScopes) == "" {
|
||||
settings.OIDCScopes = "openid profile email"
|
||||
}
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
func (api *API) effectiveAuthConfig(ctx context.Context) (config.Config, error) {
|
||||
var cfg config.Config
|
||||
var settings models.AuthSettings
|
||||
var err error
|
||||
cfg = api.Cfg
|
||||
settings, err = api.getMergedAuthSettings(ctx)
|
||||
if err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
cfg.AuthMode = settings.AuthMode
|
||||
cfg.LDAPURL = settings.LDAPURL
|
||||
cfg.LDAPBindDN = settings.LDAPBindDN
|
||||
cfg.LDAPBindPassword = settings.LDAPBindPassword
|
||||
cfg.LDAPUserBaseDN = settings.LDAPUserBaseDN
|
||||
cfg.LDAPUserFilter = settings.LDAPUserFilter
|
||||
cfg.LDAPTLSInsecureSkipVerify = settings.LDAPTLSInsecureSkipVerify
|
||||
cfg.OIDCClientID = settings.OIDCClientID
|
||||
cfg.OIDCClientSecret = settings.OIDCClientSecret
|
||||
cfg.OIDCAuthorizeURL = settings.OIDCAuthorizeURL
|
||||
cfg.OIDCTokenURL = settings.OIDCTokenURL
|
||||
cfg.OIDCUserInfoURL = settings.OIDCUserInfoURL
|
||||
cfg.OIDCRedirectURL = settings.OIDCRedirectURL
|
||||
cfg.OIDCScopes = settings.OIDCScopes
|
||||
cfg.OIDCEnabled = settings.OIDCEnabled
|
||||
cfg.OIDCTLSInsecureSkipVerify = settings.OIDCTLSInsecureSkipVerify
|
||||
cfg.LDAPDefaultUserGroupID = settings.LDAPDefaultUserGroupID
|
||||
cfg.OIDCDefaultUserGroupID = settings.OIDCDefaultUserGroupID
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (api *API) getMergedTLSSettings(ctx context.Context) (models.TLSSettings, error) {
|
||||
var settings models.TLSSettings
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
package handlers
|
||||
|
||||
import "net/http"
|
||||
import "strings"
|
||||
|
||||
import "codit/internal/db"
|
||||
import "codit/internal/auth"
|
||||
import "codit/config"
|
||||
import "codit/internal/models"
|
||||
|
||||
func (api *API) ListAuthProviders(w http.ResponseWriter, r *http.Request, _ map[string]string) {
|
||||
var providers []models.AuthProvider
|
||||
var err error
|
||||
if !api.requireAdmin(w, r) {
|
||||
return
|
||||
}
|
||||
providers, err = api.store(r).ListAuthProviders()
|
||||
if err != nil {
|
||||
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
WriteJSON(w, http.StatusOK, providers)
|
||||
}
|
||||
|
||||
func (api *API) GetAuthProvider(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
||||
var provider models.AuthProvider
|
||||
var err error
|
||||
if !api.requireAdmin(w, r) {
|
||||
return
|
||||
}
|
||||
provider, err = api.store(r).GetAuthProvider(params["id"])
|
||||
if err != nil {
|
||||
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "auth provider not found")
|
||||
return
|
||||
}
|
||||
WriteJSON(w, http.StatusOK, provider)
|
||||
}
|
||||
|
||||
func (api *API) CreateAuthProvider(w http.ResponseWriter, r *http.Request, _ map[string]string) {
|
||||
var req models.AuthProvider
|
||||
var created models.AuthProvider
|
||||
var err error
|
||||
if !api.requireAdmin(w, r) {
|
||||
return
|
||||
}
|
||||
err = DecodeJSON(r, &req)
|
||||
if err != nil {
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "invalid json")
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(req.Name) == "" {
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "name is required")
|
||||
return
|
||||
}
|
||||
if req.Type != db.AuthProviderTypeLDAP && req.Type != db.AuthProviderTypeOIDC {
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "type must be ldap or oidc")
|
||||
return
|
||||
}
|
||||
created, err = api.store(r).CreateAuthProvider(req)
|
||||
if err != nil {
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
WriteJSON(w, http.StatusCreated, created)
|
||||
}
|
||||
|
||||
func (api *API) UpdateAuthProvider(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
||||
var req models.AuthProvider
|
||||
var updated models.AuthProvider
|
||||
var existing models.AuthProvider
|
||||
var err error
|
||||
if !api.requireAdmin(w, r) {
|
||||
return
|
||||
}
|
||||
existing, err = api.store(r).GetAuthProvider(params["id"])
|
||||
if err != nil {
|
||||
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "auth provider not found")
|
||||
return
|
||||
}
|
||||
err = DecodeJSON(r, &req)
|
||||
if err != nil {
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "invalid json")
|
||||
return
|
||||
}
|
||||
req.ID = existing.ID
|
||||
req.Type = existing.Type
|
||||
if existing.ID == db.BuiltinDBProviderPublicID {
|
||||
existing.Enabled = req.Enabled
|
||||
if strings.TrimSpace(req.Name) != "" {
|
||||
existing.Name = strings.TrimSpace(req.Name)
|
||||
}
|
||||
updated, err = api.store(r).UpdateAuthProvider(existing)
|
||||
} else {
|
||||
if strings.TrimSpace(req.Name) == "" {
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "name is required")
|
||||
return
|
||||
}
|
||||
updated, err = api.store(r).UpdateAuthProvider(req)
|
||||
}
|
||||
if err != nil {
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
WriteJSON(w, http.StatusOK, updated)
|
||||
}
|
||||
|
||||
func (api *API) DeleteAuthProvider(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
||||
var err error
|
||||
var force bool
|
||||
var count int
|
||||
if !api.requireAdmin(w, r) {
|
||||
return
|
||||
}
|
||||
if params["id"] == db.BuiltinDBProviderPublicID {
|
||||
WriteJSONWithErrorReason(w, r, http.StatusForbidden, "builtin provider cannot be deleted")
|
||||
return
|
||||
}
|
||||
force = strings.EqualFold(r.URL.Query().Get("force"), "true")
|
||||
if !force {
|
||||
count, err = api.store(r).CountAuthProviderUsers(params["id"])
|
||||
if err != nil {
|
||||
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if count > 0 {
|
||||
WriteJSON(w, http.StatusConflict, map[string]any{
|
||||
"error": "provider has associated users",
|
||||
"user_count": count,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
err = api.store(r).DeleteAuthProvider(params["id"], force)
|
||||
if err != nil {
|
||||
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
WriteJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
func (api *API) TestAuthProvider(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
||||
var provider models.AuthProvider
|
||||
var req testLDAPSettingsRequest
|
||||
var cfg config.Config
|
||||
var ldapUser auth.LDAPUser
|
||||
var err error
|
||||
if !api.requireAdmin(w, r) {
|
||||
return
|
||||
}
|
||||
provider, err = api.store(r).GetAuthProvider(params["id"])
|
||||
if err != nil {
|
||||
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "auth provider not found")
|
||||
return
|
||||
}
|
||||
if provider.Type != db.AuthProviderTypeLDAP {
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "test is only supported for ldap providers")
|
||||
return
|
||||
}
|
||||
err = DecodeJSON(r, &req)
|
||||
if err != nil {
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "invalid json")
|
||||
return
|
||||
}
|
||||
cfg = api.Cfg
|
||||
cfg.LDAPURL = provider.LDAPUrl
|
||||
cfg.LDAPBindDN = provider.LDAPBindDN
|
||||
cfg.LDAPBindPassword = provider.LDAPBindPassword
|
||||
cfg.LDAPUserBaseDN = provider.LDAPUserBaseDN
|
||||
cfg.LDAPUserFilter = provider.LDAPUserFilter
|
||||
if cfg.LDAPUserFilter == "" {
|
||||
cfg.LDAPUserFilter = "(uid={username})"
|
||||
}
|
||||
cfg.LDAPTLSInsecureSkipVerify = provider.LDAPTLSInsecureSkipVerify
|
||||
if strings.TrimSpace(req.LDAPURL) != "" {
|
||||
cfg.LDAPURL = strings.TrimSpace(req.LDAPURL)
|
||||
}
|
||||
if strings.TrimSpace(req.LDAPBindDN) != "" {
|
||||
cfg.LDAPBindDN = strings.TrimSpace(req.LDAPBindDN)
|
||||
}
|
||||
if req.LDAPBindPassword != "" {
|
||||
cfg.LDAPBindPassword = req.LDAPBindPassword
|
||||
}
|
||||
if strings.TrimSpace(req.LDAPUserBaseDN) != "" {
|
||||
cfg.LDAPUserBaseDN = strings.TrimSpace(req.LDAPUserBaseDN)
|
||||
}
|
||||
if strings.TrimSpace(req.LDAPUserFilter) != "" {
|
||||
cfg.LDAPUserFilter = strings.TrimSpace(req.LDAPUserFilter)
|
||||
}
|
||||
if req.LDAPTLSInsecureSkipVerify != nil {
|
||||
cfg.LDAPTLSInsecureSkipVerify = *req.LDAPTLSInsecureSkipVerify
|
||||
}
|
||||
err = auth.LDAPTestConnectionContext(r.Context(), cfg)
|
||||
if err != nil {
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(req.Username) != "" && strings.TrimSpace(req.Password) != "" {
|
||||
ldapUser, err = auth.LDAPAuthenticateContext(r.Context(), cfg, strings.TrimSpace(req.Username), req.Password)
|
||||
if err != nil {
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
WriteJSON(w, http.StatusOK, map[string]string{"status": "ok", "user": ldapUser.Username})
|
||||
return
|
||||
}
|
||||
WriteJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
||||
}
|
||||
+113
-122
@@ -3,7 +3,7 @@ package handlers
|
||||
import codit_logger "codit/logger"
|
||||
import "context"
|
||||
import "crypto/rand"
|
||||
import "crypto/sha1"
|
||||
import "crypto/sha256"
|
||||
import "crypto/tls"
|
||||
import "database/sql"
|
||||
import "encoding/base64"
|
||||
@@ -17,7 +17,7 @@ import "net/url"
|
||||
import "strings"
|
||||
import "time"
|
||||
|
||||
import "codit/config"
|
||||
import "codit/internal/db"
|
||||
import "codit/internal/models"
|
||||
|
||||
type oidcTokenResponse struct {
|
||||
@@ -37,44 +37,46 @@ type oidcErrorResponse struct {
|
||||
ErrorDescription string `json:"error_description"`
|
||||
}
|
||||
|
||||
func (api *API) OIDCEnabled(w http.ResponseWriter, r *http.Request, _ map[string]string) {
|
||||
var settings models.AuthSettings
|
||||
func (api *API) ListPublicAuthProviders(w http.ResponseWriter, r *http.Request, _ map[string]string) {
|
||||
var providers []models.AuthProvider
|
||||
var err error
|
||||
var configured bool
|
||||
settings, err = api.getMergedAuthSettings(r.Context())
|
||||
var result []map[string]string
|
||||
var p models.AuthProvider
|
||||
providers, err = api.store(r).ListEnabledAuthProviders()
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusOK, map[string]any{"enabled": false, "configured": false})
|
||||
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
configured = api.oidcConfiguredFromSettings(settings)
|
||||
WriteJSON(w, http.StatusOK, map[string]any{
|
||||
"enabled": settings.OIDCEnabled,
|
||||
"configured": configured,
|
||||
})
|
||||
result = make([]map[string]string, 0, len(providers))
|
||||
for _, p = range providers {
|
||||
if p.Type == db.AuthProviderTypeDB {
|
||||
continue
|
||||
}
|
||||
result = append(result, map[string]string{
|
||||
"id": p.ID,
|
||||
"name": p.Name,
|
||||
"type": p.Type,
|
||||
})
|
||||
}
|
||||
WriteJSON(w, http.StatusOK, result)
|
||||
}
|
||||
|
||||
func (api *API) OIDCLogin(w http.ResponseWriter, r *http.Request, _ map[string]string) {
|
||||
var settings models.AuthSettings
|
||||
var cfg config.Config
|
||||
func (api *API) OIDCLoginWithProvider(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
||||
var provider models.AuthProvider
|
||||
var state string
|
||||
var err error
|
||||
var authURL string
|
||||
settings, err = api.getMergedAuthSettings(r.Context())
|
||||
if err != nil {
|
||||
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, "failed to load auth settings")
|
||||
provider, err = api.store(r).GetAuthProvider(params["id"])
|
||||
if err != nil || provider.Type != db.AuthProviderTypeOIDC {
|
||||
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "oidc provider not found")
|
||||
return
|
||||
}
|
||||
cfg, err = api.effectiveAuthConfig(r.Context())
|
||||
if err != nil {
|
||||
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, "failed to load auth settings")
|
||||
if !provider.Enabled {
|
||||
WriteJSONWithErrorReason(w, r, http.StatusServiceUnavailable, "oidc provider is disabled")
|
||||
return
|
||||
}
|
||||
if !settings.OIDCEnabled {
|
||||
WriteJSONWithErrorReason(w, r, http.StatusServiceUnavailable, "oidc login is disabled")
|
||||
return
|
||||
}
|
||||
if !api.oidcConfiguredFromSettings(settings) {
|
||||
WriteJSONWithErrorReason(w, r, http.StatusServiceUnavailable, "oidc is not configured")
|
||||
if !oidcProviderConfigured(provider) {
|
||||
WriteJSONWithErrorReason(w, r, http.StatusServiceUnavailable, "oidc provider is not configured")
|
||||
return
|
||||
}
|
||||
state, err = newOIDCState()
|
||||
@@ -83,21 +85,20 @@ func (api *API) OIDCLogin(w http.ResponseWriter, r *http.Request, _ map[string]s
|
||||
return
|
||||
}
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: api.oidcStateCookieName(),
|
||||
Name: oidcStateCookieNameForProvider(api, provider.ID),
|
||||
Value: state,
|
||||
HttpOnly: true,
|
||||
Path: "/api/auth/oidc",
|
||||
Expires: time.Now().UTC().Add(10 * time.Minute),
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
authURL = api.buildOIDCAuthorizeURL(cfg, state)
|
||||
api.Logger.Write(logIDAuth, codit_logger.LOG_INFO, "oidc login redirect authorize_url=%s", cfg.OIDCAuthorizeURL)
|
||||
authURL = buildOIDCAuthorizeURLFromProvider(provider, state)
|
||||
api.Logger.Write(logIDAuth, codit_logger.LOG_INFO, "oidc login redirect provider=%s authorize_url=%s", provider.Name, provider.OIDCAuthorizeURL)
|
||||
http.Redirect(w, r, authURL, http.StatusFound)
|
||||
}
|
||||
|
||||
func (api *API) OIDCCallback(w http.ResponseWriter, r *http.Request, _ map[string]string) {
|
||||
var settings models.AuthSettings
|
||||
var cfg config.Config
|
||||
func (api *API) OIDCCallbackWithProvider(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
||||
var provider models.AuthProvider
|
||||
var state string
|
||||
var code string
|
||||
var cookie *http.Cookie
|
||||
@@ -107,28 +108,23 @@ func (api *API) OIDCCallback(w http.ResponseWriter, r *http.Request, _ map[strin
|
||||
var user models.User
|
||||
var required bool
|
||||
var sessionVerified bool
|
||||
settings, err = api.getMergedAuthSettings(r.Context())
|
||||
if err != nil {
|
||||
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, "failed to load auth settings")
|
||||
provider, err = api.store(r).GetAuthProvider(params["id"])
|
||||
if err != nil || provider.Type != db.AuthProviderTypeOIDC {
|
||||
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "oidc provider not found")
|
||||
return
|
||||
}
|
||||
cfg, err = api.effectiveAuthConfig(r.Context())
|
||||
if err != nil {
|
||||
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, "failed to load auth settings")
|
||||
if !provider.Enabled {
|
||||
WriteJSONWithErrorReason(w, r, http.StatusServiceUnavailable, "oidc provider is disabled")
|
||||
return
|
||||
}
|
||||
if !settings.OIDCEnabled {
|
||||
WriteJSONWithErrorReason(w, r, http.StatusServiceUnavailable, "oidc login is disabled")
|
||||
return
|
||||
}
|
||||
if !api.oidcConfiguredFromSettings(settings) {
|
||||
WriteJSONWithErrorReason(w, r, http.StatusServiceUnavailable, "oidc is not configured")
|
||||
if !oidcProviderConfigured(provider) {
|
||||
WriteJSONWithErrorReason(w, r, http.StatusServiceUnavailable, "oidc provider is not configured")
|
||||
return
|
||||
}
|
||||
state = strings.TrimSpace(r.URL.Query().Get("state"))
|
||||
code = strings.TrimSpace(r.URL.Query().Get("code"))
|
||||
cookie, err = r.Cookie(api.oidcStateCookieName())
|
||||
api.clearOIDCStateCookie(w)
|
||||
cookie, err = r.Cookie(oidcStateCookieNameForProvider(api, provider.ID))
|
||||
clearOIDCStateCookieForProvider(w, api, provider.ID)
|
||||
if err != nil || cookie.Value == "" || state == "" || state != cookie.Value {
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "invalid oidc state")
|
||||
return
|
||||
@@ -137,21 +133,21 @@ func (api *API) OIDCCallback(w http.ResponseWriter, r *http.Request, _ map[strin
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "missing authorization code")
|
||||
return
|
||||
}
|
||||
token, err = api.oidcExchangeCode(r.Context(), cfg, code)
|
||||
token, err = api.oidcExchangeCodeFromProvider(r.Context(), provider, code)
|
||||
if err != nil {
|
||||
api.Logger.Write(logIDAuth, codit_logger.LOG_WARN, "oidc token exchange failed err=%v", err)
|
||||
api.Logger.Write(logIDAuth, codit_logger.LOG_WARN, "oidc token exchange failed provider=%s err=%v", provider.Name, err)
|
||||
WriteJSONWithErrorReason(w, r, http.StatusUnauthorized, err.Error())
|
||||
return
|
||||
}
|
||||
claims, err = api.oidcResolveClaims(r.Context(), cfg, token)
|
||||
claims, err = api.oidcResolveClaimsFromProvider(r.Context(), provider, token)
|
||||
if err != nil {
|
||||
api.Logger.Write(logIDAuth, codit_logger.LOG_WARN, "oidc claims fetch failed err=%v", err)
|
||||
api.Logger.Write(logIDAuth, codit_logger.LOG_WARN, "oidc claims fetch failed provider=%s err=%v", provider.Name, err)
|
||||
WriteJSONWithErrorReason(w, r, http.StatusUnauthorized, "oidc claims fetch failed")
|
||||
return
|
||||
}
|
||||
user, err = api.oidcGetOrCreateUser(r.Context(), claims)
|
||||
user, err = api.oidcGetOrCreateUser(r.Context(), claims, provider)
|
||||
if err != nil {
|
||||
api.Logger.Write(logIDAuth, codit_logger.LOG_WARN, "oidc user mapping failed err=%v", err)
|
||||
api.Logger.Write(logIDAuth, codit_logger.LOG_WARN, "oidc user mapping failed provider=%s err=%v", provider.Name, err)
|
||||
WriteJSONWithErrorReason(w, r, http.StatusUnauthorized, "oidc user mapping failed")
|
||||
return
|
||||
}
|
||||
@@ -169,59 +165,74 @@ func (api *API) OIDCCallback(w http.ResponseWriter, r *http.Request, _ map[strin
|
||||
user.TOTPSetupRequired = required && !user.TOTPEnabled
|
||||
sessionVerified = !required
|
||||
api.issueSession(w, r, user, sessionVerified)
|
||||
api.Logger.Write(logIDAuth, codit_logger.LOG_INFO, "oidc login success username=%s", user.Username)
|
||||
api.Logger.Write(logIDAuth, codit_logger.LOG_INFO, "oidc login success username=%s provider=%s", user.Username, provider.Name)
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
}
|
||||
|
||||
func (api *API) oidcConfiguredFromSettings(settings models.AuthSettings) bool {
|
||||
if strings.TrimSpace(settings.OIDCClientID) == "" {
|
||||
func oidcProviderConfigured(p models.AuthProvider) bool {
|
||||
if strings.TrimSpace(p.OIDCClientID) == "" {
|
||||
return false
|
||||
}
|
||||
if strings.TrimSpace(settings.OIDCClientSecret) == "" {
|
||||
if strings.TrimSpace(p.OIDCClientSecret) == "" {
|
||||
return false
|
||||
}
|
||||
if strings.TrimSpace(settings.OIDCAuthorizeURL) == "" {
|
||||
if strings.TrimSpace(p.OIDCAuthorizeURL) == "" {
|
||||
return false
|
||||
}
|
||||
if strings.TrimSpace(settings.OIDCTokenURL) == "" {
|
||||
if strings.TrimSpace(p.OIDCTokenURL) == "" {
|
||||
return false
|
||||
}
|
||||
if strings.TrimSpace(settings.OIDCRedirectURL) == "" {
|
||||
if strings.TrimSpace(p.OIDCRedirectURL) == "" {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (api *API) buildOIDCAuthorizeURL(cfg config.Config, state string) string {
|
||||
func oidcStateCookieNameForProvider(api *API, providerID string) string {
|
||||
return api.oidcStateCookieName() + "_" + providerID
|
||||
}
|
||||
|
||||
func clearOIDCStateCookieForProvider(w http.ResponseWriter, api *API, providerID string) {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: oidcStateCookieNameForProvider(api, providerID),
|
||||
Value: "",
|
||||
Path: "/api/auth/oidc",
|
||||
Expires: time.Unix(0, 0),
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
}
|
||||
|
||||
func buildOIDCAuthorizeURLFromProvider(p models.AuthProvider, state string) string {
|
||||
var values url.Values
|
||||
var scopes string
|
||||
var endpoint string
|
||||
values = url.Values{}
|
||||
values.Set("response_type", "code")
|
||||
values.Set("client_id", cfg.OIDCClientID)
|
||||
values.Set("redirect_uri", cfg.OIDCRedirectURL)
|
||||
scopes = strings.TrimSpace(cfg.OIDCScopes)
|
||||
values.Set("client_id", p.OIDCClientID)
|
||||
values.Set("redirect_uri", p.OIDCRedirectURL)
|
||||
scopes = strings.TrimSpace(p.OIDCScopes)
|
||||
if scopes == "" {
|
||||
scopes = "openid profile email"
|
||||
}
|
||||
values.Set("scope", scopes)
|
||||
values.Set("state", state)
|
||||
endpoint = cfg.OIDCAuthorizeURL
|
||||
endpoint = p.OIDCAuthorizeURL
|
||||
if strings.Contains(endpoint, "?") {
|
||||
return endpoint + "&" + values.Encode()
|
||||
}
|
||||
return endpoint + "?" + values.Encode()
|
||||
}
|
||||
|
||||
func (api *API) oidcHTTPClient(cfg config.Config) *http.Client {
|
||||
func oidcHTTPClientFromProvider(p models.AuthProvider) *http.Client {
|
||||
var transport *http.Transport
|
||||
transport = &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: cfg.OIDCTLSInsecureSkipVerify},
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: p.OIDCTLSInsecureSkipVerify},
|
||||
}
|
||||
return &http.Client{Transport: transport, Timeout: 15 * time.Second}
|
||||
}
|
||||
|
||||
func (api *API) oidcExchangeCode(ctx context.Context, cfg config.Config, code string) (oidcTokenResponse, error) {
|
||||
func (api *API) oidcExchangeCodeFromProvider(ctx context.Context, p models.AuthProvider, code string) (oidcTokenResponse, error) {
|
||||
var client *http.Client
|
||||
var form url.Values
|
||||
var req *http.Request
|
||||
@@ -229,14 +240,14 @@ func (api *API) oidcExchangeCode(ctx context.Context, cfg config.Config, code st
|
||||
var body []byte
|
||||
var token oidcTokenResponse
|
||||
var err error
|
||||
client = api.oidcHTTPClient(cfg)
|
||||
client = oidcHTTPClientFromProvider(p)
|
||||
form = url.Values{}
|
||||
form.Set("grant_type", "authorization_code")
|
||||
form.Set("code", code)
|
||||
form.Set("redirect_uri", cfg.OIDCRedirectURL)
|
||||
form.Set("client_id", cfg.OIDCClientID)
|
||||
form.Set("client_secret", cfg.OIDCClientSecret)
|
||||
req, err = http.NewRequestWithContext(ctx, http.MethodPost, cfg.OIDCTokenURL, strings.NewReader(form.Encode()))
|
||||
form.Set("redirect_uri", p.OIDCRedirectURL)
|
||||
form.Set("client_id", p.OIDCClientID)
|
||||
form.Set("client_secret", p.OIDCClientSecret)
|
||||
req, err = http.NewRequestWithContext(ctx, http.MethodPost, p.OIDCTokenURL, strings.NewReader(form.Encode()))
|
||||
if err != nil {
|
||||
return token, err
|
||||
}
|
||||
@@ -287,11 +298,11 @@ func oidcErrorDetail(body []byte) string {
|
||||
return text
|
||||
}
|
||||
|
||||
func (api *API) oidcResolveClaims(ctx context.Context, cfg config.Config, token oidcTokenResponse) (oidcUserClaims, error) {
|
||||
func (api *API) oidcResolveClaimsFromProvider(ctx context.Context, p models.AuthProvider, token oidcTokenResponse) (oidcUserClaims, error) {
|
||||
var claims oidcUserClaims
|
||||
var err error
|
||||
if strings.TrimSpace(cfg.OIDCUserInfoURL) != "" && strings.TrimSpace(token.AccessToken) != "" {
|
||||
claims, err = api.oidcUserInfo(ctx, cfg, token.AccessToken)
|
||||
if strings.TrimSpace(p.OIDCUserInfoURL) != "" && strings.TrimSpace(token.AccessToken) != "" {
|
||||
claims, err = api.oidcUserInfoFromProvider(ctx, p, token.AccessToken)
|
||||
if err == nil {
|
||||
return claims, nil
|
||||
}
|
||||
@@ -306,15 +317,15 @@ func (api *API) oidcResolveClaims(ctx context.Context, cfg config.Config, token
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
func (api *API) oidcUserInfo(ctx context.Context, cfg config.Config, accessToken string) (oidcUserClaims, error) {
|
||||
func (api *API) oidcUserInfoFromProvider(ctx context.Context, p models.AuthProvider, accessToken string) (oidcUserClaims, error) {
|
||||
var client *http.Client
|
||||
var req *http.Request
|
||||
var res *http.Response
|
||||
var body []byte
|
||||
var claims oidcUserClaims
|
||||
var err error
|
||||
client = api.oidcHTTPClient(cfg)
|
||||
req, err = http.NewRequestWithContext(ctx, http.MethodGet, cfg.OIDCUserInfoURL, nil)
|
||||
client = oidcHTTPClientFromProvider(p)
|
||||
req, err = http.NewRequestWithContext(ctx, http.MethodGet, p.OIDCUserInfoURL, nil)
|
||||
if err != nil {
|
||||
return claims, err
|
||||
}
|
||||
@@ -342,52 +353,42 @@ func (api *API) oidcUserInfo(ctx context.Context, cfg config.Config, accessToken
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
func (api *API) oidcGetOrCreateUser(ctx context.Context, claims oidcUserClaims) (models.User, error) {
|
||||
func (api *API) oidcGetOrCreateUser(ctx context.Context, claims oidcUserClaims, provider models.AuthProvider) (models.User, error) {
|
||||
var username string
|
||||
var displayName string
|
||||
var email string
|
||||
var user models.User
|
||||
var hash string
|
||||
var err error
|
||||
var created models.User
|
||||
var authCfg models.AuthSettings
|
||||
|
||||
username = oidcUsernameFromSub(claims.Sub)
|
||||
user, hash, err = api.storeContext(ctx).GetUserByUsername(username)
|
||||
_ = hash
|
||||
user, err = api.storeContext(ctx).GetUserByProviderAndSub(provider.ID, claims.Sub)
|
||||
if err == nil {
|
||||
if user.Disabled {
|
||||
return user, errors.New("user disabled")
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
if !errors.Is(err, sql.ErrNoRows) {
|
||||
return user, err
|
||||
}
|
||||
if !errors.Is(err, sql.ErrNoRows) { return user, err }
|
||||
|
||||
username = oidcUsernameFromProviderAndSub(provider.ID, claims.Sub)
|
||||
displayName = strings.TrimSpace(claims.Name)
|
||||
if displayName == "" {
|
||||
displayName = strings.TrimSpace(claims.PreferredUsername)
|
||||
}
|
||||
if displayName == "" {
|
||||
displayName = username
|
||||
}
|
||||
if displayName == "" { displayName = strings.TrimSpace(claims.PreferredUsername) }
|
||||
if displayName == "" { displayName = username }
|
||||
email = strings.TrimSpace(claims.Email)
|
||||
if email == "" {
|
||||
email = username + "@oidc.local"
|
||||
}
|
||||
if email == "" { email = username + "@oidc.local" }
|
||||
|
||||
user = models.User{
|
||||
Username: username,
|
||||
DisplayName: displayName,
|
||||
Email: email,
|
||||
AuthSource: "oidc",
|
||||
Username: username,
|
||||
DisplayName: displayName,
|
||||
Email: email,
|
||||
AuthProviderID: provider.ID,
|
||||
ExternalSubject: strings.TrimSpace(claims.Sub),
|
||||
}
|
||||
created, err = api.storeContext(ctx).CreateUser(user, "")
|
||||
if err != nil {
|
||||
return created, err
|
||||
}
|
||||
authCfg, err = api.storeContext(ctx).GetAuthSettings()
|
||||
if err == nil && authCfg.OIDCDefaultUserGroupID != "" {
|
||||
_ = api.storeContext(ctx).AddUserGroupMember(authCfg.OIDCDefaultUserGroupID, created.ID)
|
||||
if err != nil { return created, err }
|
||||
|
||||
if provider.DefaultUserGroupID != "" {
|
||||
_ = api.storeContext(ctx).AddUserGroupMember(provider.DefaultUserGroupID, created.ID)
|
||||
}
|
||||
return created, nil
|
||||
}
|
||||
@@ -415,10 +416,11 @@ func oidcClaimsFromIDToken(idToken string) (oidcUserClaims, error) {
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
func oidcUsernameFromSub(sub string) string {
|
||||
var sum [20]byte
|
||||
sum = sha1.Sum([]byte(strings.TrimSpace(sub)))
|
||||
return "oidc-" + hex.EncodeToString(sum[:6])
|
||||
// Username is a display artifact only; OIDC identity is looked up by (auth_provider_id, external_subject).
|
||||
func oidcUsernameFromProviderAndSub(providerID string, sub string) string {
|
||||
var sum [32]byte
|
||||
sum = sha256.Sum256([]byte(providerID + ":" + strings.TrimSpace(sub)))
|
||||
return "oidc-" + hex.EncodeToString(sum[:8])
|
||||
}
|
||||
|
||||
func newOIDCState() (string, error) {
|
||||
@@ -431,14 +433,3 @@ func newOIDCState() (string, error) {
|
||||
}
|
||||
return hex.EncodeToString(buf), nil
|
||||
}
|
||||
|
||||
func (api *API) clearOIDCStateCookie(w http.ResponseWriter) {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: api.oidcStateCookieName(),
|
||||
Value: "",
|
||||
Path: "/api/auth/oidc",
|
||||
Expires: time.Unix(0, 0),
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -466,7 +466,9 @@ func (api *API) CreateSSHServerGroupAdmin(w http.ResponseWriter, r *http.Request
|
||||
var ok bool
|
||||
var item models.SSHServerGroup
|
||||
var err error
|
||||
|
||||
if !api.requireAdmin(w, r) { return }
|
||||
|
||||
err = DecodeJSON(r, &req)
|
||||
if err != nil {
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "invalid json")
|
||||
@@ -477,6 +479,7 @@ func (api *API) CreateSSHServerGroupAdmin(w http.ResponseWriter, r *http.Request
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
item = models.SSHServerGroup{Name: strings.TrimSpace(req.Name), Description: strings.TrimSpace(req.Description), Enabled: req.Enabled, Tags: req.Tags, CreatedByKind: "user", CreatedBySubjectID: user.ID, CreatedBySubjectName: user.Username}
|
||||
item, err = api.store(r).CreateSSHServerGroup(item)
|
||||
if err != nil {
|
||||
@@ -1783,12 +1786,20 @@ func (api *API) CreateSSHSessionForSelf(w http.ResponseWriter, r *http.Request,
|
||||
}
|
||||
middleware.RegisterAfterCommit(r.Context(), func() {
|
||||
if api.SSHPromptedAuthStore != nil {
|
||||
// arrange to store auth info after successful commit.
|
||||
// the info is to be used by runSSHSessionStream().
|
||||
// it is to preserve the password and otp code passsed
|
||||
// by the api POST /api/ssh/access-profiles/:id/connect.
|
||||
// the function that establishes the ssh connection takes
|
||||
// the info from the in-memory auth store.
|
||||
api.SSHPromptedAuthStore.Put(item.ID, SSHPromptedAuthInput{
|
||||
Password: req.Password,
|
||||
OTPCode: req.OTPCode,
|
||||
})
|
||||
}
|
||||
if api.SSHPreparedSessionStore != nil {
|
||||
// arrange to store session info after successful commit.
|
||||
// the info is to be used by runSSHSessionStream().
|
||||
api.SSHPreparedSessionStore.Put(item.ID, prepared)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -19,7 +19,7 @@ type meResponse struct {
|
||||
Email string `json:"email"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
Disabled bool `json:"disabled"`
|
||||
AuthSource string `json:"auth_source"`
|
||||
AuthProviderID string `json:"auth_provider_id"`
|
||||
TOTPEnabled bool `json:"totp_enabled"`
|
||||
TOTPRequired bool `json:"totp_required"`
|
||||
TOTPEffectiveRequired bool `json:"totp_effective_required"`
|
||||
@@ -179,7 +179,7 @@ func buildMeResponse(user models.User, permissions []string) meResponse {
|
||||
Email: user.Email,
|
||||
IsAdmin: user.IsAdmin,
|
||||
Disabled: user.Disabled,
|
||||
AuthSource: user.AuthSource,
|
||||
AuthProviderID: user.AuthProviderID,
|
||||
TOTPEnabled: user.TOTPEnabled,
|
||||
TOTPRequired: user.TOTPRequired,
|
||||
TOTPEffectiveRequired: user.TOTPEffectiveRequired,
|
||||
|
||||
@@ -1,19 +1,45 @@
|
||||
package models
|
||||
|
||||
type User struct {
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Email string `json:"email"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
Disabled bool `json:"disabled"`
|
||||
AuthSource string `json:"auth_source"`
|
||||
TOTPEnabled bool `json:"totp_enabled"`
|
||||
TOTPRequired bool `json:"totp_required"`
|
||||
TOTPEffectiveRequired bool `json:"totp_effective_required"`
|
||||
TOTPSetupRequired bool `json:"totp_setup_required"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Email string `json:"email"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
Disabled bool `json:"disabled"`
|
||||
AuthProviderID string `json:"auth_provider_id"`
|
||||
ExternalSubject string `json:"-"`
|
||||
TOTPEnabled bool `json:"totp_enabled"`
|
||||
TOTPRequired bool `json:"totp_required"`
|
||||
TOTPEffectiveRequired bool `json:"totp_effective_required"`
|
||||
TOTPSetupRequired bool `json:"totp_setup_required"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
}
|
||||
|
||||
type AuthProvider struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Enabled bool `json:"enabled"`
|
||||
LDAPUrl string `json:"ldap_url"`
|
||||
LDAPBindDN string `json:"ldap_bind_dn"`
|
||||
LDAPBindPassword string `json:"ldap_bind_password"`
|
||||
LDAPUserBaseDN string `json:"ldap_user_base_dn"`
|
||||
LDAPUserFilter string `json:"ldap_user_filter"`
|
||||
LDAPTLSInsecureSkipVerify bool `json:"ldap_tls_insecure_skip_verify"`
|
||||
OIDCClientID string `json:"oidc_client_id"`
|
||||
OIDCClientSecret string `json:"oidc_client_secret"`
|
||||
OIDCAuthorizeURL string `json:"oidc_authorize_url"`
|
||||
OIDCTokenURL string `json:"oidc_token_url"`
|
||||
OIDCUserInfoURL string `json:"oidc_userinfo_url"`
|
||||
OIDCRedirectURL string `json:"oidc_redirect_url"`
|
||||
OIDCScopes string `json:"oidc_scopes"`
|
||||
OIDCTLSInsecureSkipVerify bool `json:"oidc_tls_insecure_skip_verify"`
|
||||
DefaultUserGroupID string `json:"default_user_group_id"`
|
||||
UserCount int `json:"user_count,omitempty"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
}
|
||||
|
||||
type Project struct {
|
||||
@@ -247,27 +273,6 @@ type PrincipalAPIKey struct {
|
||||
Disabled bool `json:"disabled"`
|
||||
}
|
||||
|
||||
type AuthSettings struct {
|
||||
AuthMode string `json:"auth_mode"`
|
||||
OIDCEnabled bool `json:"oidc_enabled"`
|
||||
LDAPURL string `json:"ldap_url"`
|
||||
LDAPBindDN string `json:"ldap_bind_dn"`
|
||||
LDAPBindPassword string `json:"ldap_bind_password"`
|
||||
LDAPUserBaseDN string `json:"ldap_user_base_dn"`
|
||||
LDAPUserFilter string `json:"ldap_user_filter"`
|
||||
LDAPTLSInsecureSkipVerify bool `json:"ldap_tls_insecure_skip_verify"`
|
||||
OIDCClientID string `json:"oidc_client_id"`
|
||||
OIDCClientSecret string `json:"oidc_client_secret"`
|
||||
OIDCAuthorizeURL string `json:"oidc_authorize_url"`
|
||||
OIDCTokenURL string `json:"oidc_token_url"`
|
||||
OIDCUserInfoURL string `json:"oidc_userinfo_url"`
|
||||
OIDCRedirectURL string `json:"oidc_redirect_url"`
|
||||
OIDCScopes string `json:"oidc_scopes"`
|
||||
OIDCTLSInsecureSkipVerify bool `json:"oidc_tls_insecure_skip_verify"`
|
||||
LDAPDefaultUserGroupID string `json:"ldap_default_user_group_id"`
|
||||
OIDCDefaultUserGroupID string `json:"oidc_default_user_group_id"`
|
||||
}
|
||||
|
||||
type TLSSettings struct {
|
||||
HTTPAddrs []string `json:"http_addrs"`
|
||||
HTTPSAddrs []string `json:"https_addrs"`
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
-- Fix 1: tls_listeners - change TEXT PKI ID columns to INTEGER FKs
|
||||
|
||||
ALTER TABLE tls_listeners ADD COLUMN tls_pki_server_cert_id_new INTEGER REFERENCES pki_certs(id) ON DELETE SET NULL;
|
||||
UPDATE tls_listeners SET tls_pki_server_cert_id_new = (SELECT id FROM pki_certs WHERE public_id = tls_pki_server_cert_id) WHERE tls_pki_server_cert_id != '';
|
||||
ALTER TABLE tls_listeners DROP COLUMN tls_pki_server_cert_id;
|
||||
ALTER TABLE tls_listeners RENAME COLUMN tls_pki_server_cert_id_new TO tls_pki_server_cert_id;
|
||||
|
||||
ALTER TABLE tls_listeners ADD COLUMN tls_pki_client_ca_id_new INTEGER REFERENCES pki_cas(id) ON DELETE SET NULL;
|
||||
UPDATE tls_listeners SET tls_pki_client_ca_id_new = (SELECT id FROM pki_cas WHERE public_id = tls_pki_client_ca_id) WHERE tls_pki_client_ca_id != '';
|
||||
ALTER TABLE tls_listeners DROP COLUMN tls_pki_client_ca_id;
|
||||
ALTER TABLE tls_listeners RENAME COLUMN tls_pki_client_ca_id_new TO tls_pki_client_ca_id;
|
||||
|
||||
-- Fix 2: tls_auth_policies.allowed_pki_client_cert_ids - migrate CSV TEXT to a proper junction table
|
||||
|
||||
CREATE TABLE tls_auth_policy_allowed_pki_certs (
|
||||
policy_id INTEGER NOT NULL,
|
||||
cert_id INTEGER NOT NULL,
|
||||
PRIMARY KEY (policy_id, cert_id),
|
||||
FOREIGN KEY (policy_id) REFERENCES tls_auth_policies(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (cert_id) REFERENCES pki_certs(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
WITH RECURSIVE
|
||||
policy_csvs(policy_id, csv) AS (
|
||||
SELECT id, allowed_pki_client_cert_ids || ','
|
||||
FROM tls_auth_policies
|
||||
WHERE allowed_pki_client_cert_ids != ''
|
||||
),
|
||||
tokens(policy_id, token, rest) AS (
|
||||
SELECT policy_id,
|
||||
substr(csv, 1, instr(csv, ',') - 1),
|
||||
substr(csv, instr(csv, ',') + 1)
|
||||
FROM policy_csvs
|
||||
UNION ALL
|
||||
SELECT policy_id,
|
||||
substr(rest, 1, instr(rest, ',') - 1),
|
||||
substr(rest, instr(rest, ',') + 1)
|
||||
FROM tokens
|
||||
WHERE rest != ''
|
||||
)
|
||||
INSERT OR IGNORE INTO tls_auth_policy_allowed_pki_certs (policy_id, cert_id)
|
||||
SELECT t.policy_id, pc.id
|
||||
FROM tokens t
|
||||
JOIN pki_certs pc ON pc.public_id = t.token
|
||||
WHERE t.token != '';
|
||||
|
||||
ALTER TABLE tls_auth_policies DROP COLUMN allowed_pki_client_cert_ids;
|
||||
|
||||
-- Fix 3: created_by_subject_id TEXT -> created_by_user_id + created_by_principal_id INTEGER FKs
|
||||
-- Pattern: add two nullable FK columns, populate from existing TEXT value, drop old TEXT column.
|
||||
|
||||
ALTER TABLE ssh_servers ADD COLUMN created_by_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL;
|
||||
ALTER TABLE ssh_servers ADD COLUMN created_by_principal_id INTEGER REFERENCES service_principals(id) ON DELETE SET NULL;
|
||||
UPDATE ssh_servers SET created_by_user_id = (SELECT id FROM users WHERE public_id = created_by_subject_id) WHERE created_by_kind = 'user';
|
||||
UPDATE ssh_servers SET created_by_principal_id = (SELECT id FROM service_principals WHERE public_id = created_by_subject_id) WHERE created_by_kind = 'service_principal';
|
||||
ALTER TABLE ssh_servers DROP COLUMN created_by_subject_id;
|
||||
|
||||
ALTER TABLE ssh_server_groups ADD COLUMN created_by_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL;
|
||||
ALTER TABLE ssh_server_groups ADD COLUMN created_by_principal_id INTEGER REFERENCES service_principals(id) ON DELETE SET NULL;
|
||||
UPDATE ssh_server_groups SET created_by_user_id = (SELECT id FROM users WHERE public_id = created_by_subject_id) WHERE created_by_kind = 'user';
|
||||
UPDATE ssh_server_groups SET created_by_principal_id = (SELECT id FROM service_principals WHERE public_id = created_by_subject_id) WHERE created_by_kind = 'service_principal';
|
||||
ALTER TABLE ssh_server_groups DROP COLUMN created_by_subject_id;
|
||||
|
||||
ALTER TABLE ssh_credentials ADD COLUMN created_by_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL;
|
||||
ALTER TABLE ssh_credentials ADD COLUMN created_by_principal_id INTEGER REFERENCES service_principals(id) ON DELETE SET NULL;
|
||||
UPDATE ssh_credentials SET created_by_user_id = (SELECT id FROM users WHERE public_id = created_by_subject_id) WHERE created_by_kind = 'user';
|
||||
UPDATE ssh_credentials SET created_by_principal_id = (SELECT id FROM service_principals WHERE public_id = created_by_subject_id) WHERE created_by_kind = 'service_principal';
|
||||
ALTER TABLE ssh_credentials DROP COLUMN created_by_subject_id;
|
||||
|
||||
ALTER TABLE ssh_secrets ADD COLUMN created_by_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL;
|
||||
ALTER TABLE ssh_secrets ADD COLUMN created_by_principal_id INTEGER REFERENCES service_principals(id) ON DELETE SET NULL;
|
||||
UPDATE ssh_secrets SET created_by_user_id = (SELECT id FROM users WHERE public_id = created_by_subject_id) WHERE created_by_kind = 'user';
|
||||
UPDATE ssh_secrets SET created_by_principal_id = (SELECT id FROM service_principals WHERE public_id = created_by_subject_id) WHERE created_by_kind = 'service_principal';
|
||||
ALTER TABLE ssh_secrets DROP COLUMN created_by_subject_id;
|
||||
|
||||
ALTER TABLE ssh_access_profiles ADD COLUMN created_by_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL;
|
||||
ALTER TABLE ssh_access_profiles ADD COLUMN created_by_principal_id INTEGER REFERENCES service_principals(id) ON DELETE SET NULL;
|
||||
UPDATE ssh_access_profiles SET created_by_user_id = (SELECT id FROM users WHERE public_id = created_by_subject_id) WHERE created_by_kind = 'user';
|
||||
UPDATE ssh_access_profiles SET created_by_principal_id = (SELECT id FROM service_principals WHERE public_id = created_by_subject_id) WHERE created_by_kind = 'service_principal';
|
||||
ALTER TABLE ssh_access_profiles DROP COLUMN created_by_subject_id;
|
||||
|
||||
ALTER TABLE pki_certs ADD COLUMN created_by_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL;
|
||||
ALTER TABLE pki_certs ADD COLUMN created_by_principal_id INTEGER REFERENCES service_principals(id) ON DELETE SET NULL;
|
||||
UPDATE pki_certs SET created_by_user_id = (SELECT id FROM users WHERE public_id = created_by_subject_id) WHERE created_by_kind = 'user';
|
||||
UPDATE pki_certs SET created_by_principal_id = (SELECT id FROM service_principals WHERE public_id = created_by_subject_id) WHERE created_by_kind = 'service_principal';
|
||||
ALTER TABLE pki_certs DROP COLUMN created_by_subject_id;
|
||||
@@ -0,0 +1,92 @@
|
||||
-- Create auth_providers table
|
||||
CREATE TABLE IF NOT EXISTS auth_providers (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
public_id TEXT NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
type TEXT NOT NULL,
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
ldap_url TEXT NOT NULL DEFAULT '',
|
||||
ldap_bind_dn TEXT NOT NULL DEFAULT '',
|
||||
ldap_bind_password TEXT NOT NULL DEFAULT '',
|
||||
ldap_user_base_dn TEXT NOT NULL DEFAULT '',
|
||||
ldap_user_filter TEXT NOT NULL DEFAULT '(uid={username})',
|
||||
ldap_tls_insecure_skip_verify INTEGER NOT NULL DEFAULT 0,
|
||||
oidc_client_id TEXT NOT NULL DEFAULT '',
|
||||
oidc_client_secret TEXT NOT NULL DEFAULT '',
|
||||
oidc_authorize_url TEXT NOT NULL DEFAULT '',
|
||||
oidc_token_url TEXT NOT NULL DEFAULT '',
|
||||
oidc_userinfo_url TEXT NOT NULL DEFAULT '',
|
||||
oidc_redirect_url TEXT NOT NULL DEFAULT '',
|
||||
oidc_scopes TEXT NOT NULL DEFAULT 'openid profile email',
|
||||
oidc_tls_insecure_skip_verify INTEGER NOT NULL DEFAULT 0,
|
||||
default_user_group_id INTEGER REFERENCES user_groups(id) ON DELETE SET NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
-- Migrate existing LDAP config from app_settings into auth_providers
|
||||
INSERT INTO auth_providers (
|
||||
public_id, name, type, enabled,
|
||||
ldap_url, ldap_bind_dn, ldap_bind_password, ldap_user_base_dn, ldap_user_filter,
|
||||
ldap_tls_insecure_skip_verify, default_user_group_id, created_at, updated_at
|
||||
)
|
||||
SELECT
|
||||
'ldap-default', 'Default LDAP', 'ldap', 1,
|
||||
COALESCE((SELECT value FROM app_settings WHERE key = 'auth.ldap.url'), ''),
|
||||
COALESCE((SELECT value FROM app_settings WHERE key = 'auth.ldap.bind_dn'), ''),
|
||||
COALESCE((SELECT value FROM app_settings WHERE key = 'auth.ldap.bind_password'), ''),
|
||||
COALESCE((SELECT value FROM app_settings WHERE key = 'auth.ldap.user_base_dn'), ''),
|
||||
COALESCE(NULLIF((SELECT value FROM app_settings WHERE key = 'auth.ldap.user_filter'), ''), '(uid={username})'),
|
||||
COALESCE((SELECT CASE WHEN value = '1' THEN 1 ELSE 0 END FROM app_settings WHERE key = 'auth.ldap.tls_insecure_skip_verify'), 0),
|
||||
(SELECT ug.id FROM user_groups ug WHERE ug.public_id = (SELECT value FROM app_settings WHERE key = 'auth.ldap.default_user_group_id') LIMIT 1),
|
||||
strftime('%s', 'now'), strftime('%s', 'now')
|
||||
WHERE EXISTS (SELECT 1 FROM app_settings WHERE key = 'auth.ldap.url' AND value != '');
|
||||
|
||||
-- Migrate existing OIDC config from app_settings into auth_providers
|
||||
INSERT INTO auth_providers (
|
||||
public_id, name, type, enabled,
|
||||
oidc_client_id, oidc_client_secret, oidc_authorize_url, oidc_token_url, oidc_userinfo_url,
|
||||
oidc_redirect_url, oidc_scopes, oidc_tls_insecure_skip_verify, default_user_group_id,
|
||||
created_at, updated_at
|
||||
)
|
||||
SELECT
|
||||
'oidc-default', 'Default OIDC', 'oidc',
|
||||
COALESCE((SELECT CASE WHEN value = '1' THEN 1 ELSE 0 END FROM app_settings WHERE key = 'auth.oidc.enabled'), 0),
|
||||
COALESCE((SELECT value FROM app_settings WHERE key = 'auth.oidc.client_id'), ''),
|
||||
COALESCE((SELECT value FROM app_settings WHERE key = 'auth.oidc.client_secret'), ''),
|
||||
COALESCE((SELECT value FROM app_settings WHERE key = 'auth.oidc.authorize_url'), ''),
|
||||
COALESCE((SELECT value FROM app_settings WHERE key = 'auth.oidc.token_url'), ''),
|
||||
COALESCE((SELECT value FROM app_settings WHERE key = 'auth.oidc.userinfo_url'), ''),
|
||||
COALESCE((SELECT value FROM app_settings WHERE key = 'auth.oidc.redirect_url'), ''),
|
||||
COALESCE(NULLIF((SELECT value FROM app_settings WHERE key = 'auth.oidc.scopes'), ''), 'openid profile email'),
|
||||
COALESCE((SELECT CASE WHEN value = '1' THEN 1 ELSE 0 END FROM app_settings WHERE key = 'auth.oidc.tls_insecure_skip_verify'), 0),
|
||||
(SELECT ug.id FROM user_groups ug WHERE ug.public_id = (SELECT value FROM app_settings WHERE key = 'auth.oidc.default_user_group_id') LIMIT 1),
|
||||
strftime('%s', 'now'), strftime('%s', 'now')
|
||||
WHERE EXISTS (SELECT 1 FROM app_settings WHERE key = 'auth.oidc.client_id' AND value != '');
|
||||
|
||||
-- Add auth.db.enabled setting derived from old auth.mode
|
||||
INSERT INTO app_settings (key, value, updated_at) VALUES ('auth.db.enabled', '1', strftime('%s', 'now'));
|
||||
UPDATE app_settings SET value = '0' WHERE key = 'auth.db.enabled'
|
||||
AND EXISTS (SELECT 1 FROM app_settings WHERE key = 'auth.mode' AND value = 'ldap');
|
||||
|
||||
-- If mode was 'db', disable any newly created LDAP providers
|
||||
UPDATE auth_providers SET enabled = 0 WHERE type = 'ldap'
|
||||
AND EXISTS (SELECT 1 FROM app_settings WHERE key = 'auth.mode' AND value = 'db');
|
||||
|
||||
-- Add auth_provider_id to users and populate from auth_source
|
||||
ALTER TABLE users ADD COLUMN auth_provider_id INTEGER REFERENCES auth_providers(id) ON DELETE SET NULL;
|
||||
UPDATE users SET auth_provider_id = (SELECT id FROM auth_providers WHERE type = 'ldap' LIMIT 1) WHERE auth_source = 'ldap';
|
||||
UPDATE users SET auth_provider_id = (SELECT id FROM auth_providers WHERE type = 'oidc' LIMIT 1) WHERE auth_source = 'oidc';
|
||||
ALTER TABLE users DROP COLUMN auth_source;
|
||||
|
||||
-- Remove migrated auth settings from app_settings
|
||||
DELETE FROM app_settings WHERE key IN (
|
||||
'auth.mode',
|
||||
'auth.ldap.url', 'auth.ldap.bind_dn', 'auth.ldap.bind_password',
|
||||
'auth.ldap.user_base_dn', 'auth.ldap.user_filter', 'auth.ldap.tls_insecure_skip_verify',
|
||||
'auth.ldap.default_user_group_id',
|
||||
'auth.oidc.enabled', 'auth.oidc.client_id', 'auth.oidc.client_secret',
|
||||
'auth.oidc.authorize_url', 'auth.oidc.token_url', 'auth.oidc.userinfo_url',
|
||||
'auth.oidc.redirect_url', 'auth.oidc.scopes', 'auth.oidc.tls_insecure_skip_verify',
|
||||
'auth.oidc.default_user_group_id'
|
||||
);
|
||||
@@ -0,0 +1,23 @@
|
||||
-- Insert the builtin DB auth provider at reserved id=0.
|
||||
-- enabled is seeded from the existing auth.db.enabled setting (default true).
|
||||
INSERT INTO auth_providers (id, public_id, name, type, enabled, created_at, updated_at)
|
||||
VALUES (
|
||||
0, 'db', 'Database', 'db',
|
||||
COALESCE((SELECT CASE WHEN value = '1' THEN 1 ELSE 0 END FROM app_settings WHERE key = 'auth.db.enabled'), 1),
|
||||
strftime('%s', 'now'), strftime('%s', 'now')
|
||||
);
|
||||
|
||||
-- Backfill existing DB users: NULL auth_provider_id → 0 (builtin).
|
||||
UPDATE users SET auth_provider_id = 0 WHERE auth_provider_id IS NULL;
|
||||
|
||||
-- Add external_subject column for stable OIDC identity anchoring.
|
||||
ALTER TABLE users ADD COLUMN external_subject TEXT NOT NULL DEFAULT '';
|
||||
|
||||
-- Remove the now-redundant app_settings row.
|
||||
DELETE FROM app_settings WHERE key = 'auth.db.enabled';
|
||||
|
||||
-- Enforce uniqueness of (provider, subject) for OIDC users.
|
||||
-- The partial condition excludes DB users (external_subject='') and unlinked rows (auth_provider_id IS NULL).
|
||||
CREATE UNIQUE INDEX users_auth_provider_subject_uq
|
||||
ON users(auth_provider_id, external_subject)
|
||||
WHERE auth_provider_id IS NOT NULL AND external_subject != '';
|
||||
+18
-21
@@ -629,9 +629,9 @@ func (app *Server) initHandlers() (*handlers.API, *http.ServeMux, error) {
|
||||
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/auth/providers", api.ListPublicAuthProviders)
|
||||
router.Handle("GET", "/api/auth/oidc/:id/login", api.OIDCLoginWithProvider)
|
||||
router.Handle("GET", "/api/auth/oidc/:id/callback", api.OIDCCallbackWithProvider)
|
||||
router.Handle("GET", "/api/me", api.Me)
|
||||
router.Handle("PATCH", "/api/me", api.UpdateMe)
|
||||
router.Handle("GET", "/api/me/totp", api.GetMyTOTP)
|
||||
@@ -657,9 +657,12 @@ func (app *Server) initHandlers() (*handlers.API, *http.ServeMux, error) {
|
||||
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/auth-providers", api.ListAuthProviders)
|
||||
router.Handle("POST", "/api/admin/auth-providers", api.CreateAuthProvider)
|
||||
router.Handle("GET", "/api/admin/auth-providers/:id", api.GetAuthProvider)
|
||||
router.Handle("PUT", "/api/admin/auth-providers/:id", api.UpdateAuthProvider)
|
||||
router.Handle("DELETE", "/api/admin/auth-providers/:id", api.DeleteAuthProvider)
|
||||
router.Handle("POST", "/api/admin/auth-providers/:id/test", api.TestAuthProvider)
|
||||
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)
|
||||
@@ -686,9 +689,7 @@ func (app *Server) initHandlers() (*handlers.API, *http.ServeMux, error) {
|
||||
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)
|
||||
@@ -967,10 +968,7 @@ func (app *Server) initHandlers() (*handlers.API, *http.ServeMux, error) {
|
||||
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 bypasses the authentication layer. WithAPIAuth is not called.
|
||||
apiPublicHandler = middleware.WithUserCookie(store, sessionCookieNameFromServerId(app.serverId), router)
|
||||
apiPublicHandler = middleware.WithStoreTransaction(store, apiPublicHandler)
|
||||
apiPublicHandler = middleware.AccessLog(logger, apiPublicHandler)
|
||||
@@ -978,9 +976,8 @@ func (app *Server) initHandlers() (*handlers.API, *http.ServeMux, error) {
|
||||
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/auth/providers", apiPublicHandler)
|
||||
mux.Handle("/api/auth/oidc/", apiPublicHandler) // for /api/auth/oidc/:id/login and /api/auth/oidc/:id/callback without authentication
|
||||
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)))
|
||||
@@ -2457,11 +2454,11 @@ func bootstrapAdmin(store *db.Store, envPrefix string) error {
|
||||
}
|
||||
|
||||
_, err = txStore.CreateUser(models.User{
|
||||
Username: username,
|
||||
DisplayName: username,
|
||||
Email: username + "@local",
|
||||
IsAdmin: true,
|
||||
AuthSource: "db",
|
||||
Username: username,
|
||||
DisplayName: username,
|
||||
Email: username + "@local",
|
||||
IsAdmin: true,
|
||||
AuthProviderID: db.BuiltinDBProviderPublicID,
|
||||
}, hash)
|
||||
if err != nil {
|
||||
_ = txStore.Rollback()
|
||||
|
||||
+27
-19
@@ -4,7 +4,7 @@ export interface User {
|
||||
display_name: string
|
||||
email: string
|
||||
is_admin: boolean
|
||||
auth_source?: string
|
||||
auth_provider_id?: string
|
||||
totp_enabled?: boolean
|
||||
totp_required?: boolean
|
||||
totp_effective_required?: boolean
|
||||
@@ -434,9 +434,11 @@ export interface PrincipalProjectRole {
|
||||
created_at: number
|
||||
}
|
||||
|
||||
export interface AuthSettings {
|
||||
auth_mode: 'db' | 'ldap' | 'hybrid'
|
||||
oidc_enabled: boolean
|
||||
export interface AuthProvider {
|
||||
id: string
|
||||
name: string
|
||||
type: 'ldap' | 'oidc' | 'db'
|
||||
enabled: boolean
|
||||
ldap_url: string
|
||||
ldap_bind_dn: string
|
||||
ldap_bind_password: string
|
||||
@@ -451,8 +453,16 @@ export interface AuthSettings {
|
||||
oidc_redirect_url: string
|
||||
oidc_scopes: string
|
||||
oidc_tls_insecure_skip_verify: boolean
|
||||
ldap_default_user_group_id: string
|
||||
oidc_default_user_group_id: string
|
||||
default_user_group_id: string
|
||||
user_count?: number
|
||||
created_at: number
|
||||
updated_at: number
|
||||
}
|
||||
|
||||
export type PublicAuthProvider = {
|
||||
id: string
|
||||
name: string
|
||||
type: 'ldap' | 'oidc'
|
||||
}
|
||||
|
||||
export interface TLSSettings {
|
||||
@@ -509,10 +519,6 @@ export interface TLSListener {
|
||||
updated_at: number
|
||||
}
|
||||
|
||||
export interface OIDCStatus {
|
||||
enabled: boolean
|
||||
configured?: boolean
|
||||
}
|
||||
|
||||
export interface PKICA {
|
||||
id: string
|
||||
@@ -1348,7 +1354,7 @@ async function requestText(path: string, options: RequestInit = {}): Promise<str
|
||||
}
|
||||
|
||||
export const api = {
|
||||
oidcStatus: () => request<OIDCStatus>('/api/auth/oidc/enabled'),
|
||||
listPublicAuthProviders: () => request<PublicAuthProvider[]>('/api/auth/providers'),
|
||||
login: (username: string, password: string, otpCode?: string) =>
|
||||
request<User>('/api/login', {
|
||||
method: 'POST',
|
||||
@@ -1418,14 +1424,16 @@ export const api = {
|
||||
request<CertPrincipalBinding>('/api/admin/cert-principal-bindings', { method: 'POST', body: JSON.stringify(payload) }),
|
||||
deleteCertPrincipalBinding: (fingerprint: string) =>
|
||||
request<void>(`/api/admin/cert-principal-bindings/${encodeURIComponent(fingerprint)}`, { method: 'DELETE' }),
|
||||
getAuthSettings: () => request<AuthSettings>('/api/admin/auth'),
|
||||
updateAuthSettings: (payload: AuthSettings) =>
|
||||
request<AuthSettings>('/api/admin/auth', { method: 'PATCH', body: JSON.stringify(payload) }),
|
||||
testAuthSettings: (
|
||||
payload: Partial<AuthSettings> & { username?: string; password?: string },
|
||||
signal?: AbortSignal
|
||||
) =>
|
||||
request<{ status: string; user?: string }>('/api/admin/auth/test', {
|
||||
listAuthProviders: () => request<AuthProvider[]>('/api/admin/auth-providers'),
|
||||
getAuthProvider: (id: string) => request<AuthProvider>(`/api/admin/auth-providers/${id}`),
|
||||
createAuthProvider: (payload: Omit<AuthProvider, 'id' | 'user_count' | 'created_at' | 'updated_at'>) =>
|
||||
request<AuthProvider>('/api/admin/auth-providers', { method: 'POST', body: JSON.stringify(payload) }),
|
||||
updateAuthProvider: (id: string, payload: Omit<AuthProvider, 'id' | 'user_count' | 'created_at' | 'updated_at'>) =>
|
||||
request<AuthProvider>(`/api/admin/auth-providers/${id}`, { method: 'PUT', body: JSON.stringify(payload) }),
|
||||
deleteAuthProvider: (id: string, force?: boolean) =>
|
||||
request<void>(`/api/admin/auth-providers/${id}${force ? '?force=true' : ''}`, { method: 'DELETE' }),
|
||||
testAuthProvider: (id: string, payload: { username?: string; password?: string }, signal?: AbortSignal) =>
|
||||
request<{ status: string; user?: string }>(`/api/admin/auth-providers/${id}/test`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
signal
|
||||
|
||||
@@ -111,8 +111,7 @@ export const routes: RouteObject[] = [
|
||||
{ path: 'admin/principals', element: <AdminServicePrincipalsPage /> },
|
||||
{ path: 'admin/auth', element: <AdminSiteAuthPage /> },
|
||||
{ path: 'admin/tls/auth-policies', element: <AdminTLSAuthPoliciesPage /> },
|
||||
{ path: 'admin/tls', element: <AdminTLSSettingsPage /> },
|
||||
{ path: 'admin/auth/ldap', element: <AdminSiteAuthPage /> }
|
||||
{ path: 'admin/tls', element: <AdminTLSSettingsPage /> }
|
||||
]
|
||||
},
|
||||
{ path: '*', element: <NotFoundPage /> }
|
||||
|
||||
@@ -30,7 +30,7 @@ export default function UserDetailsDialog(props: UserDetailsDialogProps) {
|
||||
<TextField label="Email" value={item?.email || '-'} InputProps={{ readOnly: true }} />
|
||||
<TextField label="Admin" value={yesNo(item?.is_admin)} InputProps={{ readOnly: true }} />
|
||||
<TextField label="Disabled" value={yesNo(item?.disabled)} InputProps={{ readOnly: true }} />
|
||||
<TextField label="Auth Source" value={item?.auth_source || 'db'} InputProps={{ readOnly: true }} />
|
||||
<TextField label="Auth Provider" value={item?.auth_provider_id || 'db'} InputProps={{ readOnly: true }} />
|
||||
<TextField label="TOTP Enabled" value={yesNo(item?.totp_enabled)} InputProps={{ readOnly: true }} />
|
||||
<TextField label="TOTP Required" value={yesNo(item?.totp_required)} InputProps={{ readOnly: true }} />
|
||||
<TextField label="Effective TOTP Required" value={yesNo(item?.totp_effective_required)} InputProps={{ readOnly: true }} />
|
||||
|
||||
@@ -179,7 +179,7 @@ export default function AccountPage() {
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(event) => setPassword(event.target.value)}
|
||||
helperText={user?.auth_source === 'db' ? '' : 'Password updates are available for db users only.'}
|
||||
helperText={user?.auth_provider_id === 'db' ? '' : 'Password updates are available for db users only.'}
|
||||
/>
|
||||
<TextField
|
||||
label="Confirm New Password"
|
||||
@@ -250,7 +250,7 @@ export default function AccountPage() {
|
||||
type="password"
|
||||
value={totpPassword}
|
||||
onChange={(event) => setTOTPPassword(event.target.value)}
|
||||
helperText={user?.auth_source === 'db' ? 'Required for db users.' : 'Non-db users can leave this empty.'}
|
||||
helperText={user?.auth_provider_id === 'db' ? 'Required for db users.' : 'Non-db users can leave this empty.'}
|
||||
/>
|
||||
<TextField
|
||||
label="Authenticator Code"
|
||||
|
||||
@@ -226,7 +226,7 @@ export default function AdminApiKeysPage() {
|
||||
<CompactListItemText
|
||||
primary={
|
||||
<Typography component="div">
|
||||
{key.name}({key.prefix})
|
||||
{key.name} ({key.prefix})
|
||||
{key.disabled? (<> <Chip size="small" color='error' label='Disabled'/></>): null}
|
||||
</Typography>
|
||||
}
|
||||
|
||||
@@ -325,7 +325,7 @@ export default function AdminPKIClientProfilesPage() {
|
||||
<CompactListItemText
|
||||
primary={
|
||||
<Typography component="div">
|
||||
{item.name}({item.id})
|
||||
{item.name} ({item.id})
|
||||
{item.enabled? null: (<> <Chip size="small" color='error' label='Disabled'/></>)}
|
||||
</Typography>
|
||||
}
|
||||
|
||||
@@ -422,7 +422,7 @@ export default function AdminSSHAccessProfilesPage() {
|
||||
<SectionCard
|
||||
title={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, minWidth: 0, flexWrap: 'wrap' }}>
|
||||
<Typography variant="h6">SSH Access Profiles</Typography>
|
||||
<Typography variant="h6">SSH Access Profiles ({filteredItems.length})</Typography>
|
||||
<TextField
|
||||
size="small"
|
||||
label="Search"
|
||||
|
||||
@@ -12,7 +12,7 @@ import List from '@mui/material/List'
|
||||
import ListItem from '@mui/material/ListItem'
|
||||
import TextField from '@mui/material/TextField'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import ConfirmDeleteDialog from '../components/ConfirmDeleteDialog'
|
||||
import FormDialogContent from '../components/FormDialogContent'
|
||||
import HeaderActionButton from '../components/HeaderActionButton'
|
||||
@@ -56,6 +56,22 @@ export default function AdminSSHCredentialsPage() {
|
||||
const [viewItem, setViewItem] = useState<SSHCredential | null>(null)
|
||||
const [deleteItem, setDeleteItem] = useState<SSHCredential | null>(null)
|
||||
const [deleteConfirm, setDeleteConfirm] = useState<string>('')
|
||||
const [search, setSearch] = useState<string>('')
|
||||
|
||||
const filteredItems = useMemo(() => {
|
||||
const normalized: string = search.trim().toLowerCase()
|
||||
|
||||
if (!normalized) {
|
||||
return items
|
||||
}
|
||||
return items.filter((item: SSHCredential) => {
|
||||
return item.id.toLowerCase().includes(normalized)
|
||||
|| item.name.toLowerCase().includes(normalized)
|
||||
|| (item.description || '').toLowerCase().includes(normalized)
|
||||
|| (item.fingerprint || '').toLowerCase().includes(normalized)
|
||||
|| (item.type || '').toLowerCase().includes(normalized)
|
||||
})
|
||||
}, [items, search])
|
||||
|
||||
const load = async () => {
|
||||
let list: SSHCredential[]
|
||||
@@ -135,7 +151,19 @@ export default function AdminSSHCredentialsPage() {
|
||||
<Box>
|
||||
<Typography variant="h5" sx={{ mb: 2 }}>Admin: SSH Credentials</Typography>
|
||||
<SectionCard
|
||||
title="Imported Private Keys"
|
||||
title={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, minWidth: 0, flexWrap: 'wrap' }}>
|
||||
<Typography variant="h6">Imported Private Keys ({filteredItems.length})</Typography>
|
||||
<TextField
|
||||
size="small"
|
||||
label="Search"
|
||||
placeholder="name, fingerprint, or type"
|
||||
value={search}
|
||||
onChange={(event) => setSearch(event.target.value)}
|
||||
sx={{ minWidth: 240, maxWidth: 320 }}
|
||||
/>
|
||||
</Box>
|
||||
}
|
||||
actions={
|
||||
<HeaderActionButton variant="outlined" startIcon={<AddIcon />} onClick={openCreate}>
|
||||
Import Key
|
||||
@@ -144,12 +172,12 @@ export default function AdminSSHCredentialsPage() {
|
||||
>
|
||||
<PageAlert message={error} onClose={() => setError(null)} sx={{ mb: 1 }} />
|
||||
<List>
|
||||
{items.map((item) => (
|
||||
{filteredItems.map((item: SSHCredential) => (
|
||||
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
|
||||
<CompactListItemText
|
||||
primary={
|
||||
<Typography component="div">
|
||||
{item.name}({item.id})
|
||||
{item.name} ({item.id})
|
||||
{item.enabled? null: (<> <Chip size="small" color='error' label='Disabled'/></>)}
|
||||
</Typography>
|
||||
}
|
||||
@@ -174,6 +202,9 @@ export default function AdminSSHCredentialsPage() {
|
||||
</ListRowActions>
|
||||
</ListItem>
|
||||
))}
|
||||
{filteredItems.length === 0 ? (
|
||||
<Typography variant="body2" color="text.secondary">{items.length ? 'No matching SSH credentials.' : 'No SSH credentials found.'}</Typography>
|
||||
) : null}
|
||||
</List>
|
||||
</SectionCard>
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
TextField,
|
||||
Typography
|
||||
} from '@mui/material'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import ConfirmDeleteDialog from '../components/ConfirmDeleteDialog'
|
||||
import HeaderActionButton from '../components/HeaderActionButton'
|
||||
import { ListRowActionButton, ListRowActions } from '../components/ListRowActions'
|
||||
@@ -22,10 +22,16 @@ import SectionCard from '../components/SectionCard'
|
||||
import SSHPrincipalGrantDetailsDialog from '../components/SSHPrincipalGrantDetailsDialog'
|
||||
import SSHPrincipalGrantFormDialog, { SSHPrincipalGrantFormState } from '../components/SSHPrincipalGrantFormDialog'
|
||||
import { api, SSHPrincipalGrant, SSHPrincipalGrantUpsertPayload, User, UserGroup } from '../api'
|
||||
import Autocomplete from '../components/Autocomplete'
|
||||
import SelectField from '../components/SelectField'
|
||||
import CompactListItemText from '../components/CompactListItemText'
|
||||
import PageAlert from '../components/PageAlert'
|
||||
|
||||
type TargetOption = {
|
||||
id: string
|
||||
label: string
|
||||
}
|
||||
|
||||
function fmt(value: number): string {
|
||||
if (!value || value <= 0) {
|
||||
return '-'
|
||||
@@ -79,6 +85,7 @@ export default function AdminSSHPrincipalGrantsPage() {
|
||||
|
||||
const [filterTargetType, setFilterTargetType] = useState<'user' | 'group' | ''>('')
|
||||
const [filterTargetID, setFilterTargetID] = useState('')
|
||||
const [search, setSearch] = useState('')
|
||||
|
||||
const [viewItem, setViewItem] = useState<SSHPrincipalGrant | null>(null)
|
||||
const [editOpen, setEditOpen] = useState(false)
|
||||
@@ -232,23 +239,47 @@ export default function AdminSSHPrincipalGrantsPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const userOptions = users.map((item) => ({
|
||||
const userOptions: TargetOption[] = users.map((item: User) => ({
|
||||
id: item.id,
|
||||
label: item.display_name ? `${item.display_name} (${item.username})` : item.username
|
||||
}))
|
||||
|
||||
const groupOptions = groups.map((item) => ({
|
||||
const groupOptions: TargetOption[] = groups.map((item: UserGroup) => ({
|
||||
id: item.id,
|
||||
label: `${item.name}${item.disabled ? ' (disabled)' : ''}`
|
||||
}))
|
||||
|
||||
const filteredItems = useMemo(() => {
|
||||
const normalized: string = search.trim().toLowerCase()
|
||||
|
||||
if (!normalized) {
|
||||
return items
|
||||
}
|
||||
return items.filter((item: SSHPrincipalGrant) => {
|
||||
return item.id.toLowerCase().includes(normalized)
|
||||
|| (item.name || '').toLowerCase().includes(normalized)
|
||||
|| (item.principal || '').toLowerCase().includes(normalized)
|
||||
|| (item.principals || []).join(' ').toLowerCase().includes(normalized)
|
||||
|| (item.reason || '').toLowerCase().includes(normalized)
|
||||
|| (item.targets || []).map((target) => `${target.target_type} ${target.target_id} ${target.target_name || ''}`).join(' ').toLowerCase().includes(normalized)
|
||||
})
|
||||
}, [items, search])
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="h5" sx={{ mb: 2 }}>Admin: SSH Principal Grants</Typography>
|
||||
<SectionCard
|
||||
title={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap', minWidth: 0 }}>
|
||||
<Typography variant="h6">SSH Principal Grants</Typography>
|
||||
<Typography variant="h6">SSH Principal Grants ({filteredItems.length})</Typography>
|
||||
<TextField
|
||||
size="small"
|
||||
label="Search"
|
||||
placeholder="grant, principal, target, or reason"
|
||||
value={search}
|
||||
onChange={(event) => setSearch(event.target.value)}
|
||||
sx={{ minWidth: 240, maxWidth: 320 }}
|
||||
/>
|
||||
<SelectField
|
||||
select
|
||||
size="small"
|
||||
@@ -265,34 +296,30 @@ export default function AdminSSHPrincipalGrantsPage() {
|
||||
<MenuItem value="group">Group</MenuItem>
|
||||
</SelectField>
|
||||
{filterTargetType === 'user' ? (
|
||||
<SelectField
|
||||
select
|
||||
<Autocomplete<TargetOption, false, false, false>
|
||||
size="small"
|
||||
label="User"
|
||||
value={filterTargetID}
|
||||
onChange={(event) => setFilterTargetID(event.target.value)}
|
||||
options={userOptions}
|
||||
value={userOptions.find((item: TargetOption) => item.id === filterTargetID) || null}
|
||||
onChange={(_event, value: TargetOption | null) => setFilterTargetID(value?.id || '')}
|
||||
getOptionLabel={(item: TargetOption) => item.label}
|
||||
isOptionEqualToValue={(option: TargetOption, value: TargetOption) => option.id === value.id}
|
||||
noOptionsText="No users"
|
||||
renderInput={(params) => <TextField {...params} label="User" placeholder="(all users)" />}
|
||||
sx={{ minWidth: 240 }}
|
||||
>
|
||||
<MenuItem value="">(all users)</MenuItem>
|
||||
{users.map((item) => (
|
||||
<MenuItem key={item.id} value={item.id}>{item.display_name ? `${item.display_name} (${item.username})` : item.username}</MenuItem>
|
||||
))}
|
||||
</SelectField>
|
||||
/>
|
||||
) : null}
|
||||
{filterTargetType === 'group' ? (
|
||||
<SelectField
|
||||
select
|
||||
<Autocomplete<TargetOption, false, false, false>
|
||||
size="small"
|
||||
label="Group"
|
||||
value={filterTargetID}
|
||||
onChange={(event) => setFilterTargetID(event.target.value)}
|
||||
options={groupOptions}
|
||||
value={groupOptions.find((item: TargetOption) => item.id === filterTargetID) || null}
|
||||
onChange={(_event, value: TargetOption | null) => setFilterTargetID(value?.id || '')}
|
||||
getOptionLabel={(item: TargetOption) => item.label}
|
||||
isOptionEqualToValue={(option: TargetOption, value: TargetOption) => option.id === value.id}
|
||||
noOptionsText="No groups"
|
||||
renderInput={(params) => <TextField {...params} label="Group" placeholder="(all groups)" />}
|
||||
sx={{ minWidth: 220 }}
|
||||
>
|
||||
<MenuItem value="">(all groups)</MenuItem>
|
||||
{groups.map((item) => (
|
||||
<MenuItem key={item.id} value={item.id}>{item.name}</MenuItem>
|
||||
))}
|
||||
</SelectField>
|
||||
/>
|
||||
) : null}
|
||||
</Box>
|
||||
}
|
||||
@@ -304,10 +331,10 @@ export default function AdminSSHPrincipalGrantsPage() {
|
||||
>
|
||||
<PageAlert message={error} onClose={() => setError(null)} sx={{ mb: 1 }} />
|
||||
{loading ? <Typography variant="body2" color="text.secondary">Loading principal grants...</Typography> : null}
|
||||
{!loading && items.length === 0 ? <Typography variant="body2" color="text.secondary">No principal grants configured.</Typography> : null}
|
||||
{!loading && filteredItems.length === 0 ? <Typography variant="body2" color="text.secondary">{items.length ? 'No matching principal grants.' : 'No principal grants configured.'}</Typography> : null}
|
||||
{!loading ? (
|
||||
<List dense>
|
||||
{items.map((item) => (
|
||||
{filteredItems.map((item: SSHPrincipalGrant) => (
|
||||
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, width: '100%', minWidth: 0 }}>
|
||||
<CompactListItemText
|
||||
|
||||
@@ -178,35 +178,42 @@ export default function AdminSSHSignHistoryPage() {
|
||||
<Box>
|
||||
<Typography variant="h5" sx={{ mb: 2 }}>Admin: SSH Signing History</Typography>
|
||||
<PageAlert message={error} onClose={() => setError(null)} sx={{ mb: 1 }} />
|
||||
|
||||
<SectionCard title="SSH Signing History">
|
||||
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<Autocomplete<SSHUserCA, false, false, false>
|
||||
size="small"
|
||||
options={cas}
|
||||
value={cas.find((item: SSHUserCA) => item.id === caID) || null}
|
||||
onChange={(_event, value: SSHUserCA | null) => {
|
||||
const nextCAID: string = value?.id || ''
|
||||
setCAID(nextCAID)
|
||||
load(nextCAID).catch(() => {})
|
||||
}}
|
||||
getOptionLabel={(item: SSHUserCA) => `${item.name} (${item.id})`}
|
||||
isOptionEqualToValue={(option: SSHUserCA, value: SSHUserCA) => option.id === value.id}
|
||||
noOptionsText="No SSH CAs"
|
||||
renderInput={(params) => <TextField {...params} label="CA" placeholder="(all)" />}
|
||||
sx={{ minWidth: 400 }}
|
||||
/>
|
||||
<TextField
|
||||
size="small"
|
||||
label="Limit"
|
||||
value={limitText}
|
||||
onChange={(event) => setLimitText(event.target.value)}
|
||||
sx={{ width: 120 }}
|
||||
/>
|
||||
<Button variant="outlined" onClick={() => load().catch(() => {})} disabled={loading}>
|
||||
{loading ? 'Loading...' : 'Refresh'}
|
||||
</Button>
|
||||
</Box>
|
||||
<SectionCard
|
||||
title={
|
||||
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<Typography variant="h6">SSH Signing History</Typography>
|
||||
<Autocomplete<SSHUserCA, false, false, false>
|
||||
size="small"
|
||||
options={cas}
|
||||
value={cas.find((item: SSHUserCA) => item.id === caID) || null}
|
||||
onChange={(_event, value: SSHUserCA | null) => {
|
||||
const nextCAID: string = value?.id || ''
|
||||
setCAID(nextCAID)
|
||||
load(nextCAID).catch(() => {})
|
||||
}}
|
||||
getOptionLabel={(item: SSHUserCA) => `${item.name} (${item.id})`}
|
||||
isOptionEqualToValue={(option: SSHUserCA, value: SSHUserCA) => option.id === value.id}
|
||||
noOptionsText="No SSH CAs"
|
||||
renderInput={(params) => <TextField {...params} label="CA" placeholder="(all)" />}
|
||||
sx={{ minWidth: 400 }}
|
||||
/>
|
||||
</Box>
|
||||
}
|
||||
actions={
|
||||
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<TextField
|
||||
size="small"
|
||||
label="Limit"
|
||||
value={limitText}
|
||||
onChange={(event) => setLimitText(event.target.value)}
|
||||
sx={{ width: 120 }}
|
||||
/>
|
||||
<Button variant="outlined" onClick={() => load().catch(() => {})} disabled={loading}>
|
||||
{loading ? 'Loading...' : 'Refresh'}
|
||||
</Button>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
{!loading && items.length === 0 ? <Typography variant="body2" color="text.secondary">No signing history found.</Typography> : null}
|
||||
<List>
|
||||
{items.map((item: SSHUserCAIssuance) => (
|
||||
|
||||
@@ -355,7 +355,7 @@ export default function AdminSSHUserCAPage() {
|
||||
<CompactListItemText
|
||||
primary={
|
||||
<Typography component="div">
|
||||
{item.name}({item.id})
|
||||
{item.name} ({item.id})
|
||||
{item.enabled? null: (<> <Chip size="small" color='error' label='Disabled'/></>)}
|
||||
</Typography>
|
||||
}
|
||||
|
||||
@@ -792,17 +792,20 @@ export default function AdminServicePrincipalsPage() {
|
||||
<ListItem key={item.fingerprint} divider sx={{ alignItems: 'flex-start' }}>
|
||||
<CompactListItemText
|
||||
primary={
|
||||
pkiCertByFingerprint(item.fingerprint) ? (
|
||||
<Link
|
||||
underline="hover"
|
||||
onClick={() => void openCertView(pkiCertByFingerprint(item.fingerprint)?.id || '')}
|
||||
sx={{ cursor: 'default' }}
|
||||
>
|
||||
{pkiCertByFingerprint(item.fingerprint)?.common_name || pkiCertByFingerprint(item.fingerprint)?.serial_hex} ({pkiCertByFingerprint(item.fingerprint)?.id})
|
||||
</Link>
|
||||
) : (
|
||||
<Typography>{item.fingerprint}</Typography>
|
||||
)
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, minWidth: 0, flexWrap: 'wrap' }}>
|
||||
{pkiCertByFingerprint(item.fingerprint) ? (
|
||||
<Link
|
||||
underline="hover"
|
||||
onClick={() => void openCertView(pkiCertByFingerprint(item.fingerprint)?.id || '')}
|
||||
sx={{ cursor: 'default' }}
|
||||
>
|
||||
{pkiCertByFingerprint(item.fingerprint)?.common_name || pkiCertByFingerprint(item.fingerprint)?.serial_hex} ({pkiCertByFingerprint(item.fingerprint)?.id})
|
||||
</Link>
|
||||
) : (
|
||||
<Typography>{item.fingerprint}</Typography>
|
||||
)}
|
||||
{item.enabled ? null : <Chip size="small" color="error" label="Disabled" />}
|
||||
</Box>
|
||||
}
|
||||
secondary={
|
||||
<Box sx={{ display: 'grid' }}>
|
||||
|
||||
@@ -1,17 +1,56 @@
|
||||
import { Box, Button, Checkbox, FormControlLabel, MenuItem, TextField, Typography } from '@mui/material'
|
||||
import Alert from '@mui/material/Alert'
|
||||
import Box from '@mui/material/Box'
|
||||
import Button from '@mui/material/Button'
|
||||
import Checkbox from '@mui/material/Checkbox'
|
||||
import Chip from '@mui/material/Chip'
|
||||
import Dialog from '../components/ModalDialog'
|
||||
import DialogActions from '@mui/material/DialogActions'
|
||||
import DialogTitle from '@mui/material/DialogTitle'
|
||||
import FormControlLabel from '@mui/material/FormControlLabel'
|
||||
import List from '@mui/material/List'
|
||||
import ListItem from '@mui/material/ListItem'
|
||||
import MenuItem from '@mui/material/MenuItem'
|
||||
import TextField from '@mui/material/TextField'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'
|
||||
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import PageAlert from '../components/PageAlert'
|
||||
import { api, AuthSettings, UserGroup } from '../api'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import CompactListItemText from '../components/CompactListItemText'
|
||||
import ConfirmDeleteDialog from '../components/ConfirmDeleteDialog'
|
||||
import FormDialogContent from '../components/FormDialogContent'
|
||||
import { ListRowActionButton, ListRowActions } from '../components/ListRowActions'
|
||||
import SectionCard from '../components/SectionCard'
|
||||
import SelectField from '../components/SelectField'
|
||||
import PageAlert from '../components/PageAlert'
|
||||
import Autocomplete from '../components/Autocomplete'
|
||||
import { api, AuthProvider, UserGroup } from '../api'
|
||||
|
||||
export default function AdminSiteAuthPage() {
|
||||
const [settings, setSettings] = useState<AuthSettings>({
|
||||
auth_mode: 'db',
|
||||
oidc_enabled: false,
|
||||
type ProviderFormState = {
|
||||
name: string
|
||||
type: 'ldap' | 'oidc' | 'db'
|
||||
enabled: boolean
|
||||
ldap_url: string
|
||||
ldap_bind_dn: string
|
||||
ldap_bind_password: string
|
||||
ldap_user_base_dn: string
|
||||
ldap_user_filter: string
|
||||
ldap_tls_insecure_skip_verify: boolean
|
||||
oidc_client_id: string
|
||||
oidc_client_secret: string
|
||||
oidc_authorize_url: string
|
||||
oidc_token_url: string
|
||||
oidc_userinfo_url: string
|
||||
oidc_redirect_url: string
|
||||
oidc_scopes: string
|
||||
oidc_tls_insecure_skip_verify: boolean
|
||||
default_user_group_id: string
|
||||
}
|
||||
|
||||
function emptyProviderForm(): ProviderFormState {
|
||||
return {
|
||||
name: '',
|
||||
type: 'ldap',
|
||||
enabled: true,
|
||||
ldap_url: '',
|
||||
ldap_bind_dn: '',
|
||||
ldap_bind_password: '',
|
||||
@@ -26,71 +65,164 @@ export default function AdminSiteAuthPage() {
|
||||
oidc_redirect_url: '',
|
||||
oidc_scopes: 'openid profile email',
|
||||
oidc_tls_insecure_skip_verify: false,
|
||||
ldap_default_user_group_id: '',
|
||||
oidc_default_user_group_id: ''
|
||||
})
|
||||
default_user_group_id: ''
|
||||
}
|
||||
}
|
||||
|
||||
export default function AdminSiteAuthPage() {
|
||||
// Providers list
|
||||
const [providers, setProviders] = useState<AuthProvider[]>([])
|
||||
const [userGroups, setUserGroups] = useState<UserGroup[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [providersLoading, setProvidersLoading] = useState(false)
|
||||
const [providersError, setProvidersError] = useState<string | null>(null)
|
||||
const [providerSearch, setProviderSearch] = useState('')
|
||||
|
||||
// Provider dialog
|
||||
const [dialogOpen, setDialogOpen] = useState(false)
|
||||
const [editingProvider, setEditingProvider] = useState<AuthProvider | null>(null)
|
||||
const [form, setForm] = useState<ProviderFormState>(emptyProviderForm())
|
||||
const [dialogError, setDialogError] = useState<string | null>(null)
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
||||
// Delete dialog
|
||||
const [deleteItem, setDeleteItem] = useState<AuthProvider | null>(null)
|
||||
const [deleteConfirm, setDeleteConfirm] = useState('')
|
||||
const [forceDelete, setForceDelete] = useState(false)
|
||||
const [deleteError, setDeleteError] = useState<string | null>(null)
|
||||
const [deleting, setDeleting] = useState(false)
|
||||
|
||||
// LDAP test
|
||||
const [testing, setTesting] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [saved, setSaved] = useState(false)
|
||||
const [testError, setTestError] = useState<string | null>(null)
|
||||
const [testResult, setTestResult] = useState<string | null>(null)
|
||||
const [testUsername, setTestUsername] = useState('')
|
||||
const [testPassword, setTestPassword] = useState('')
|
||||
const testControllerRef = useRef<AbortController | null>(null)
|
||||
|
||||
const filteredProviders = useMemo(() => {
|
||||
const needle = providerSearch.trim().toLowerCase()
|
||||
if (!needle) return providers
|
||||
return providers.filter((p) =>
|
||||
[p.name, p.type, p.id].join(' ').toLowerCase().includes(needle)
|
||||
)
|
||||
}, [providers, providerSearch])
|
||||
|
||||
const loadProviders = async () => {
|
||||
setProvidersLoading(true)
|
||||
try {
|
||||
const [list, groups] = await Promise.all([api.listAuthProviders(), api.listUserGroups()])
|
||||
setProviders(Array.isArray(list) ? list : [])
|
||||
setUserGroups(Array.isArray(groups) ? groups.filter((g) => g.scope === 'explicit') : [])
|
||||
} catch (err) {
|
||||
setProvidersError(err instanceof Error ? err.message : 'Failed to load auth providers')
|
||||
} finally {
|
||||
setProvidersLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
Promise.all([api.getAuthSettings(), api.listUserGroups()])
|
||||
.then(([authData, groups]) => {
|
||||
setSettings(authData)
|
||||
setUserGroups(groups.filter((g) => g.scope === 'explicit'))
|
||||
})
|
||||
.catch((err) => {
|
||||
const message = err instanceof Error ? err.message : 'Failed to load authentication settings'
|
||||
setError(message)
|
||||
})
|
||||
.finally(() => setLoading(false))
|
||||
loadProviders()
|
||||
}, [])
|
||||
|
||||
const handleSave = async () => {
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (testControllerRef.current) testControllerRef.current.abort()
|
||||
}
|
||||
}, [])
|
||||
|
||||
const openCreate = () => {
|
||||
setEditingProvider(null)
|
||||
setForm(emptyProviderForm())
|
||||
setDialogError(null)
|
||||
setTestError(null)
|
||||
setTestResult(null)
|
||||
setTestUsername('')
|
||||
setTestPassword('')
|
||||
setDialogOpen(true)
|
||||
}
|
||||
|
||||
const openEdit = (item: AuthProvider) => {
|
||||
setEditingProvider(item)
|
||||
setForm({
|
||||
name: item.name,
|
||||
type: item.type,
|
||||
enabled: item.enabled,
|
||||
ldap_url: item.ldap_url,
|
||||
ldap_bind_dn: item.ldap_bind_dn,
|
||||
ldap_bind_password: item.ldap_bind_password,
|
||||
ldap_user_base_dn: item.ldap_user_base_dn,
|
||||
ldap_user_filter: item.ldap_user_filter || '(uid={username})',
|
||||
ldap_tls_insecure_skip_verify: item.ldap_tls_insecure_skip_verify,
|
||||
oidc_client_id: item.oidc_client_id,
|
||||
oidc_client_secret: item.oidc_client_secret,
|
||||
oidc_authorize_url: item.oidc_authorize_url,
|
||||
oidc_token_url: item.oidc_token_url,
|
||||
oidc_userinfo_url: item.oidc_userinfo_url,
|
||||
oidc_redirect_url: item.oidc_redirect_url,
|
||||
oidc_scopes: item.oidc_scopes || 'openid profile email',
|
||||
oidc_tls_insecure_skip_verify: item.oidc_tls_insecure_skip_verify,
|
||||
default_user_group_id: item.default_user_group_id
|
||||
})
|
||||
setDialogError(null)
|
||||
setTestError(null)
|
||||
setTestResult(null)
|
||||
setTestUsername('')
|
||||
setTestPassword('')
|
||||
setDialogOpen(true)
|
||||
}
|
||||
|
||||
const handleSaveProvider = async () => {
|
||||
if (!form.name.trim()) {
|
||||
setDialogError('Provider name is required')
|
||||
return
|
||||
}
|
||||
setSaving(true)
|
||||
setError(null)
|
||||
setSaved(false)
|
||||
setDialogError(null)
|
||||
try {
|
||||
const updated = await api.updateAuthSettings(settings)
|
||||
setSettings(updated)
|
||||
setSaved(true)
|
||||
if (editingProvider) {
|
||||
await api.updateAuthProvider(editingProvider.id, form)
|
||||
} else {
|
||||
await api.createAuthProvider(form)
|
||||
}
|
||||
setDialogOpen(false)
|
||||
await loadProviders()
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Failed to save authentication settings'
|
||||
setError(message)
|
||||
setDialogError(err instanceof Error ? err.message : 'Failed to save auth provider')
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!deleteItem) return
|
||||
setDeleting(true)
|
||||
setDeleteError(null)
|
||||
try {
|
||||
await api.deleteAuthProvider(deleteItem.id, forceDelete)
|
||||
setDeleteItem(null)
|
||||
await loadProviders()
|
||||
} catch (err) {
|
||||
setDeleteError(err instanceof Error ? err.message : 'Failed to delete auth provider')
|
||||
} finally {
|
||||
setDeleting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleTest = async () => {
|
||||
let controller: AbortController
|
||||
if (!editingProvider) return
|
||||
if (testing) {
|
||||
if (testControllerRef.current) {
|
||||
testControllerRef.current.abort()
|
||||
}
|
||||
if (testControllerRef.current) testControllerRef.current.abort()
|
||||
return
|
||||
}
|
||||
controller = new AbortController()
|
||||
const controller = new AbortController()
|
||||
testControllerRef.current = controller
|
||||
setTesting(true)
|
||||
setTestError(null)
|
||||
setTestResult(null)
|
||||
try {
|
||||
const result = await api.testAuthSettings(
|
||||
{
|
||||
...settings,
|
||||
username: testUsername.trim() || undefined,
|
||||
password: testPassword || undefined
|
||||
},
|
||||
const result = await api.testAuthProvider(
|
||||
editingProvider.id,
|
||||
{ username: testUsername.trim() || undefined, password: testPassword || undefined },
|
||||
controller.signal
|
||||
)
|
||||
setTestResult(result.user ? `Connection ok. User test ok: ${result.user}` : 'Connection ok.')
|
||||
@@ -99,180 +231,323 @@ export default function AdminSiteAuthPage() {
|
||||
setTestResult('LDAP test canceled.')
|
||||
return
|
||||
}
|
||||
const message = err instanceof Error ? err.message : 'LDAP test failed'
|
||||
setTestError(message)
|
||||
setTestError(err instanceof Error ? err.message : 'LDAP test failed')
|
||||
} finally {
|
||||
setTesting(false)
|
||||
testControllerRef.current = null
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (testControllerRef.current) {
|
||||
testControllerRef.current.abort()
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const testSubtitle = testError ? (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, color: 'error.main' }}>
|
||||
<ErrorOutlineIcon fontSize="small" />
|
||||
<Typography variant="body2" color="inherit">
|
||||
{testError}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : testResult ? (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, color: 'success.main' }}>
|
||||
<CheckCircleOutlineIcon fontSize="small" />
|
||||
<Typography variant="body2" color="inherit">
|
||||
{testResult}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : 'Optional user bind'
|
||||
const isLDAP = form.type === 'ldap'
|
||||
const isOIDC = form.type === 'oidc'
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'grid', gap: 1, minWidth: 0 }}>
|
||||
<Typography variant="h5">
|
||||
Admin: Site Authentication
|
||||
</Typography>
|
||||
{loading ? (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Loading...
|
||||
</Typography>
|
||||
) : null}
|
||||
<PageAlert message={error} onClose={() => setError(null)} />
|
||||
<PageAlert severity="success" message={saved ? 'Saved.' : null} onClose={() => setSaved(false)} />
|
||||
<Box sx={{ display: 'grid', gap: 1, width: '100%', minWidth: 0 }}>
|
||||
<SectionCard
|
||||
title="General"
|
||||
actions={
|
||||
<Button variant="contained" onClick={handleSave} disabled={saving || loading}>
|
||||
{saving ? 'Saving...' : 'Save'}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<SelectField
|
||||
select
|
||||
label="Auth Mode"
|
||||
value={settings.auth_mode}
|
||||
onChange={(event) => setSettings((prev) => ({ ...prev, auth_mode: event.target.value as 'db' | 'ldap' | 'hybrid' }))}
|
||||
>
|
||||
<MenuItem value="db">db</MenuItem>
|
||||
<MenuItem value="ldap">ldap</MenuItem>
|
||||
<MenuItem value="hybrid">hybrid</MenuItem>
|
||||
</SelectField>
|
||||
</SectionCard>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'grid',
|
||||
gap: 1,
|
||||
width: '100%',
|
||||
minWidth: 0,
|
||||
gridTemplateColumns: { xs: '1fr', lg: 'minmax(0, 1fr) minmax(0, 1fr)' },
|
||||
alignItems: 'start'
|
||||
}}
|
||||
>
|
||||
<SectionCard title="OIDC">
|
||||
<Typography variant="h5" sx={{ mb: 1 }}>Admin: Site Authentication</Typography>
|
||||
|
||||
{/* Auth providers card */}
|
||||
<SectionCard
|
||||
collapsible
|
||||
title={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, minWidth: 0, flexWrap: 'wrap' }}>
|
||||
<Typography variant="h6">Providers ({filteredProviders.length})</Typography>
|
||||
<TextField
|
||||
size="small"
|
||||
label="Search"
|
||||
value={providerSearch}
|
||||
onChange={(event) => setProviderSearch(event.target.value)}
|
||||
sx={{ minWidth: 240 }}
|
||||
/>
|
||||
</Box>
|
||||
}
|
||||
actions={
|
||||
<Button variant="outlined" onClick={openCreate}>
|
||||
New Provider
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<PageAlert message={providersError} onClose={() => setProvidersError(null)} />
|
||||
{providersLoading ? (
|
||||
<Typography variant="body2" color="text.secondary">Loading...</Typography>
|
||||
) : null}
|
||||
{filteredProviders.length === 0 && !providersLoading ? (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
No auth providers configured.
|
||||
</Typography>
|
||||
) : null}
|
||||
<List>
|
||||
{filteredProviders.map((item) => (
|
||||
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
|
||||
<CompactListItemText
|
||||
primary={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}>
|
||||
<Typography sx={{ wordBreak: 'break-word' }}>{item.name}</Typography>
|
||||
<Chip label={item.type.toUpperCase()} size="small" variant="outlined" />
|
||||
{item.enabled ? (
|
||||
<Chip label="enabled" size="small" color="success" />
|
||||
) : (
|
||||
<Chip label="disabled" size="small" />
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
secondary={
|
||||
<Box sx={{ display: 'grid', gap: 0.5 }}>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{(item.user_count ?? 0) > 0 ? `${item.user_count} user${item.user_count === 1 ? '' : 's'} · ` : ''}{item.id}
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
disableTypography
|
||||
/>
|
||||
<ListRowActions>
|
||||
<ListRowActionButton onClick={() => openEdit(item)}>Edit</ListRowActionButton>
|
||||
<ListRowActionButton color="error" disabled={item.id === 'db'} onClick={() => {
|
||||
setDeleteError(null)
|
||||
setForceDelete(false)
|
||||
setDeleteConfirm('')
|
||||
setDeleteItem(item)
|
||||
}}>
|
||||
Delete
|
||||
</ListRowActionButton>
|
||||
</ListRowActions>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</SectionCard>
|
||||
|
||||
{/* Create / Edit dialog */}
|
||||
<Dialog open={dialogOpen} onClose={() => setDialogOpen(false)} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>
|
||||
<Box sx={{ display: 'grid', gap: 1 }}>
|
||||
<Typography variant="h6">
|
||||
{editingProvider ? `Edit Provider: ${editingProvider.name}` : 'New Provider'}
|
||||
</Typography>
|
||||
<PageAlert message={dialogError} onClose={() => setDialogError(null)} />
|
||||
</Box>
|
||||
</DialogTitle>
|
||||
<FormDialogContent>
|
||||
<TextField
|
||||
label="Name"
|
||||
value={form.name}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, name: e.target.value }))}
|
||||
/>
|
||||
{!editingProvider ? (
|
||||
<SelectField
|
||||
select
|
||||
label="Type"
|
||||
value={form.type}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, type: e.target.value as 'ldap' | 'oidc' }))}
|
||||
>
|
||||
<MenuItem value="ldap">LDAP</MenuItem>
|
||||
<MenuItem value="oidc">OIDC</MenuItem>
|
||||
</SelectField>
|
||||
) : null}
|
||||
<FormControlLabel
|
||||
control={<Checkbox checked={settings.oidc_enabled} onChange={(event) => setSettings((prev) => ({ ...prev, oidc_enabled: event.target.checked }))} />}
|
||||
label="Enable OIDC login"
|
||||
/>
|
||||
<TextField label="Client ID" value={settings.oidc_client_id} onChange={(event) => setSettings((prev) => ({ ...prev, oidc_client_id: event.target.value }))} />
|
||||
<TextField
|
||||
label="Client Secret"
|
||||
type="password"
|
||||
value={settings.oidc_client_secret}
|
||||
onChange={(event) => setSettings((prev) => ({ ...prev, oidc_client_secret: event.target.value }))}
|
||||
/>
|
||||
<TextField
|
||||
label="Authorize URL"
|
||||
value={settings.oidc_authorize_url}
|
||||
onChange={(event) => setSettings((prev) => ({ ...prev, oidc_authorize_url: event.target.value }))}
|
||||
/>
|
||||
<TextField
|
||||
label="Token URL"
|
||||
value={settings.oidc_token_url}
|
||||
onChange={(event) => setSettings((prev) => ({ ...prev, oidc_token_url: event.target.value }))}
|
||||
/>
|
||||
<TextField
|
||||
label="UserInfo URL (optional)"
|
||||
value={settings.oidc_userinfo_url}
|
||||
onChange={(event) => setSettings((prev) => ({ ...prev, oidc_userinfo_url: event.target.value }))}
|
||||
/>
|
||||
<TextField
|
||||
label="Redirect URL"
|
||||
value={settings.oidc_redirect_url}
|
||||
onChange={(event) => setSettings((prev) => ({ ...prev, oidc_redirect_url: event.target.value }))}
|
||||
/>
|
||||
<TextField
|
||||
label="Scopes"
|
||||
value={settings.oidc_scopes}
|
||||
onChange={(event) => setSettings((prev) => ({ ...prev, oidc_scopes: event.target.value }))}
|
||||
helperText="Example: openid profile email"
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={<Checkbox checked={settings.oidc_tls_insecure_skip_verify} onChange={(event) => setSettings((prev) => ({ ...prev, oidc_tls_insecure_skip_verify: event.target.checked }))} />}
|
||||
label="OIDC TLS insecure skip verify (testing/self-signed only)"
|
||||
control={
|
||||
<Checkbox
|
||||
checked={form.enabled}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, enabled: e.target.checked }))}
|
||||
/>
|
||||
}
|
||||
label="Enabled"
|
||||
/>
|
||||
{editingProvider?.type !== 'db' ? (
|
||||
<>
|
||||
<Autocomplete<UserGroup, false, false, false>
|
||||
options={userGroups}
|
||||
value={userGroups.find((g) => g.id === settings.oidc_default_user_group_id) || null}
|
||||
onChange={(_event, value) => setSettings((prev) => ({ ...prev, oidc_default_user_group_id: value?.id || '' }))}
|
||||
value={userGroups.find((g) => g.id === form.default_user_group_id) ?? null}
|
||||
onChange={(_event, value) => setForm((prev) => ({ ...prev, default_user_group_id: value?.id ?? '' }))}
|
||||
getOptionLabel={(g) => g.name}
|
||||
isOptionEqualToValue={(option, value) => option.id === value.id}
|
||||
noOptionsText="No user groups"
|
||||
renderInput={(params) => <TextField {...params} label="Default User Group for new OIDC users" helperText="Automatically added to this group on first login." />}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
label="Default User Group"
|
||||
helperText="New users from this provider are added to this group automatically."
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</SectionCard>
|
||||
<SectionCard title="LDAP">
|
||||
<TextField label="LDAP URL" value={settings.ldap_url} onChange={(event) => setSettings((prev) => ({ ...prev, ldap_url: event.target.value }))} />
|
||||
<TextField label="Bind DN" value={settings.ldap_bind_dn} onChange={(event) => setSettings((prev) => ({ ...prev, ldap_bind_dn: event.target.value }))} />
|
||||
<TextField
|
||||
label="Bind Password"
|
||||
type="password"
|
||||
value={settings.ldap_bind_password}
|
||||
onChange={(event) => setSettings((prev) => ({ ...prev, ldap_bind_password: event.target.value }))}
|
||||
/>
|
||||
<TextField
|
||||
label="User Base DN"
|
||||
value={settings.ldap_user_base_dn}
|
||||
onChange={(event) => setSettings((prev) => ({ ...prev, ldap_user_base_dn: event.target.value }))}
|
||||
/>
|
||||
<TextField
|
||||
label="User Filter"
|
||||
value={settings.ldap_user_filter}
|
||||
onChange={(event) => setSettings((prev) => ({ ...prev, ldap_user_filter: event.target.value }))}
|
||||
helperText="Use {username} placeholder."
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={<Checkbox checked={settings.ldap_tls_insecure_skip_verify} onChange={(event) => setSettings((prev) => ({ ...prev, ldap_tls_insecure_skip_verify: event.target.checked }))} />}
|
||||
label="TLS insecure skip verify (testing/self-signed only)"
|
||||
/>
|
||||
<Autocomplete<UserGroup, false, false, false>
|
||||
options={userGroups}
|
||||
value={userGroups.find((g) => g.id === settings.ldap_default_user_group_id) || null}
|
||||
onChange={(_event, value) => setSettings((prev) => ({ ...prev, ldap_default_user_group_id: value?.id || '' }))}
|
||||
getOptionLabel={(g) => g.name}
|
||||
isOptionEqualToValue={(option, value) => option.id === value.id}
|
||||
noOptionsText="No user groups"
|
||||
renderInput={(params) => <TextField {...params} label="Default User Group for new LDAP users" helperText="Automatically added to this group on first login." />}
|
||||
/>
|
||||
</SectionCard>
|
||||
</Box>
|
||||
<SectionCard
|
||||
title="Test LDAP"
|
||||
subtitle={testSubtitle}
|
||||
actions={
|
||||
<Button variant="outlined" onClick={handleTest} color={testing ? 'warning' : 'primary'} disabled={loading}>
|
||||
{testing ? 'Cancel Test' : 'Test Connection'}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<TextField label="Test Username" value={testUsername} onChange={(event) => setTestUsername(event.target.value)} />
|
||||
<TextField label="Test Password" type="password" value={testPassword} onChange={(event) => setTestPassword(event.target.value)} />
|
||||
</SectionCard>
|
||||
</Box>
|
||||
{isLDAP ? (
|
||||
<>
|
||||
<TextField
|
||||
label="LDAP URL"
|
||||
value={form.ldap_url}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, ldap_url: e.target.value }))}
|
||||
helperText="Example: ldap://ldap.example.com:389"
|
||||
/>
|
||||
<TextField
|
||||
label="Bind DN (optional)"
|
||||
value={form.ldap_bind_dn}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, ldap_bind_dn: e.target.value }))}
|
||||
/>
|
||||
<TextField
|
||||
label="Bind Password"
|
||||
type="password"
|
||||
value={form.ldap_bind_password}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, ldap_bind_password: e.target.value }))}
|
||||
/>
|
||||
<TextField
|
||||
label="User Base DN"
|
||||
value={form.ldap_user_base_dn}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, ldap_user_base_dn: e.target.value }))}
|
||||
/>
|
||||
<TextField
|
||||
label="User Filter"
|
||||
value={form.ldap_user_filter}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, ldap_user_filter: e.target.value }))}
|
||||
helperText="Use {username} as placeholder. Example: (uid={username})"
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={form.ldap_tls_insecure_skip_verify}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, ldap_tls_insecure_skip_verify: e.target.checked }))}
|
||||
/>
|
||||
}
|
||||
label="TLS insecure skip verify (testing/self-signed only)"
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
{isOIDC ? (
|
||||
<>
|
||||
<TextField
|
||||
label="Client ID"
|
||||
value={form.oidc_client_id}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, oidc_client_id: e.target.value }))}
|
||||
/>
|
||||
<TextField
|
||||
label="Client Secret"
|
||||
type="password"
|
||||
value={form.oidc_client_secret}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, oidc_client_secret: e.target.value }))}
|
||||
/>
|
||||
<TextField
|
||||
label="Authorize URL"
|
||||
value={form.oidc_authorize_url}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, oidc_authorize_url: e.target.value }))}
|
||||
/>
|
||||
<TextField
|
||||
label="Token URL"
|
||||
value={form.oidc_token_url}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, oidc_token_url: e.target.value }))}
|
||||
/>
|
||||
<TextField
|
||||
label="UserInfo URL (optional)"
|
||||
value={form.oidc_userinfo_url}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, oidc_userinfo_url: e.target.value }))}
|
||||
/>
|
||||
<TextField
|
||||
label="Redirect URL"
|
||||
value={form.oidc_redirect_url}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, oidc_redirect_url: e.target.value }))}
|
||||
helperText={
|
||||
editingProvider
|
||||
? `Callback URL: /api/auth/oidc/${editingProvider.id}/callback`
|
||||
: 'You can set this after creating the provider once you have its ID.'
|
||||
}
|
||||
/>
|
||||
<TextField
|
||||
label="Scopes"
|
||||
value={form.oidc_scopes}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, oidc_scopes: e.target.value }))}
|
||||
helperText="Space-separated. Example: openid profile email"
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={form.oidc_tls_insecure_skip_verify}
|
||||
onChange={(e) => setForm((prev) => ({ ...prev, oidc_tls_insecure_skip_verify: e.target.checked }))}
|
||||
/>
|
||||
}
|
||||
label="OIDC TLS insecure skip verify (testing/self-signed only)"
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
{editingProvider && editingProvider.type === 'ldap' ? (
|
||||
<Box sx={{ borderTop: 1, borderColor: 'divider', pt: 1.5, mt: 0.5, display: 'grid', gap: 1 }}>
|
||||
<Typography variant="subtitle2">Test LDAP Connection</Typography>
|
||||
{testError ? (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, color: 'error.main' }}>
|
||||
<ErrorOutlineIcon fontSize="small" />
|
||||
<Typography variant="body2" color="inherit">{testError}</Typography>
|
||||
</Box>
|
||||
) : testResult ? (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, color: 'success.main' }}>
|
||||
<CheckCircleOutlineIcon fontSize="small" />
|
||||
<Typography variant="body2" color="inherit">{testResult}</Typography>
|
||||
</Box>
|
||||
) : null}
|
||||
<TextField
|
||||
label="Test Username (optional)"
|
||||
size="small"
|
||||
value={testUsername}
|
||||
onChange={(e) => setTestUsername(e.target.value)}
|
||||
/>
|
||||
<TextField
|
||||
label="Test Password (optional)"
|
||||
type="password"
|
||||
size="small"
|
||||
value={testPassword}
|
||||
onChange={(e) => setTestPassword(e.target.value)}
|
||||
/>
|
||||
<Box>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onClick={handleTest}
|
||||
color={testing ? 'warning' : 'primary'}
|
||||
>
|
||||
{testing ? 'Cancel Test' : 'Test Connection'}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
) : null}
|
||||
</>) : null}
|
||||
</FormDialogContent>
|
||||
<DialogActions>
|
||||
<Button variant="contained" onClick={handleSaveProvider} disabled={saving}>
|
||||
{saving ? 'Saving...' : editingProvider ? 'Save' : 'Create'}
|
||||
</Button>
|
||||
<Button onClick={() => setDialogOpen(false)} disabled={saving}>Cancel</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
{/* Delete dialog */}
|
||||
<ConfirmDeleteDialog
|
||||
open={!!deleteItem}
|
||||
title="Delete Auth Provider"
|
||||
prompt="Type the provider name to confirm deletion."
|
||||
expectedText={deleteItem?.name}
|
||||
confirmLabel="Provider name"
|
||||
confirmValue={deleteConfirm}
|
||||
onConfirmValueChange={setDeleteConfirm}
|
||||
error={deleteError}
|
||||
busy={deleting}
|
||||
confirmDisabled={(deleteItem?.user_count ?? 0) > 0 && !forceDelete}
|
||||
onConfirm={handleDelete}
|
||||
onClose={() => { if (!deleting) { setDeleteItem(null); setDeleteConfirm('') } }}
|
||||
>
|
||||
{(deleteItem?.user_count ?? 0) > 0 ? (
|
||||
<>
|
||||
<Alert severity="warning">
|
||||
This provider has <strong>{deleteItem!.user_count}</strong> associated{' '}
|
||||
{deleteItem!.user_count === 1 ? 'user' : 'users'}. Deleting it will unlink those users
|
||||
from this provider.
|
||||
</Alert>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={forceDelete}
|
||||
onChange={(e) => setForceDelete(e.target.checked)}
|
||||
color="warning"
|
||||
/>
|
||||
}
|
||||
label={`Delete even though ${deleteItem!.user_count} ${deleteItem!.user_count === 1 ? 'user' : 'users'} will be unlinked`}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</ConfirmDeleteDialog>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -186,7 +186,7 @@ export default function AdminTLSAuthPoliciesPage() {
|
||||
<SectionCard
|
||||
title={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, minWidth: 0, flexWrap: 'wrap' }}>
|
||||
<Typography variant="h6">Policies</Typography>
|
||||
<Typography variant="h6">Policies ({filteredItems.length})</Typography>
|
||||
<TextField
|
||||
size="small"
|
||||
label="Search"
|
||||
|
||||
@@ -61,6 +61,7 @@ export default function AdminTLSSettingsPage() {
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [listeners, setListeners] = useState<TLSListener[]>([])
|
||||
const [listenersLoading, setListenersLoading] = useState(false)
|
||||
const [listenerSearch, setListenerSearch] = useState('')
|
||||
const [dialogOpen, setDialogOpen] = useState(false)
|
||||
const [editingMain, setEditingMain] = useState(false)
|
||||
const [editingID, setEditingID] = useState<string | null>(null)
|
||||
@@ -189,6 +190,51 @@ export default function AdminTLSSettingsPage() {
|
||||
.join(' · ')
|
||||
}
|
||||
|
||||
const listenerMatchesSearch = (item: ListenerForm & { id?: string }, value: string) => {
|
||||
const normalized: string = value.trim().toLowerCase()
|
||||
|
||||
if (!normalized) {
|
||||
return true
|
||||
}
|
||||
return [
|
||||
item.id || '',
|
||||
item.name,
|
||||
(item.http_addrs || []).join(' '),
|
||||
(item.https_addrs || []).join(' '),
|
||||
summarizeEndpointPolicies(item.endpoint_policies || []),
|
||||
item.tls_server_cert_source,
|
||||
item.tls_pki_server_cert_id,
|
||||
item.tls_client_auth,
|
||||
item.tls_pki_client_ca_id,
|
||||
item.tls_min_version
|
||||
]
|
||||
.join(' ')
|
||||
.toLowerCase()
|
||||
.includes(normalized)
|
||||
}
|
||||
|
||||
const mainListenerMatches: boolean = listenerMatchesSearch({
|
||||
name: 'Main Listener',
|
||||
enabled: true,
|
||||
http_addrs: settings.http_addrs || [],
|
||||
https_addrs: settings.https_addrs || [],
|
||||
endpoint_policies: settings.endpoint_policies || [],
|
||||
tls_server_cert_source: settings.tls_server_cert_source || 'pki',
|
||||
tls_cert_file: settings.tls_cert_file || '',
|
||||
tls_key_file: settings.tls_key_file || '',
|
||||
tls_pki_server_cert_id: settings.tls_pki_server_cert_id || '',
|
||||
tls_client_auth: settings.tls_client_auth || 'none',
|
||||
tls_client_ca_file: settings.tls_client_ca_file || '',
|
||||
tls_pki_client_ca_id: settings.tls_pki_client_ca_id || '',
|
||||
tls_min_version: settings.tls_min_version || ''
|
||||
}, listenerSearch)
|
||||
|
||||
const filteredListeners = useMemo(() => {
|
||||
return (listeners || []).filter((item: TLSListener) => listenerMatchesSearch(item, listenerSearch))
|
||||
}, [authPolicies, listenerSearch, listeners])
|
||||
|
||||
const listenerCount: number = filteredListeners.length + (mainListenerMatches ? 1 : 0)
|
||||
|
||||
const openCreateDialog = () => {
|
||||
setEditingMain(false)
|
||||
setEditingID(null)
|
||||
@@ -389,7 +435,19 @@ export default function AdminTLSSettingsPage() {
|
||||
Admin: Site TLS
|
||||
</Typography>
|
||||
<SectionCard
|
||||
title="Listeners"
|
||||
title={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, minWidth: 0, flexWrap: 'wrap' }}>
|
||||
<Typography variant="h6">Listeners ({listenerCount})</Typography>
|
||||
<TextField
|
||||
size="small"
|
||||
label="Search"
|
||||
placeholder="name, address, policy, or TLS setting"
|
||||
value={listenerSearch}
|
||||
onChange={(event) => setListenerSearch(event.target.value)}
|
||||
sx={{ minWidth: 240, maxWidth: 320 }}
|
||||
/>
|
||||
</Box>
|
||||
}
|
||||
actions={
|
||||
<Button variant="outlined" onClick={openCreateDialog}>
|
||||
New Listener
|
||||
@@ -403,6 +461,7 @@ export default function AdminTLSSettingsPage() {
|
||||
</Typography>
|
||||
) : null}
|
||||
<List>
|
||||
{mainListenerMatches ? (
|
||||
<ListItem divider sx={{ alignItems: 'flex-start' }}>
|
||||
<CompactListItemText
|
||||
primary={
|
||||
@@ -435,7 +494,8 @@ export default function AdminTLSSettingsPage() {
|
||||
<ListRowActionButton onClick={openMainEditDialog}>Edit</ListRowActionButton>
|
||||
</ListRowActions>
|
||||
</ListItem>
|
||||
{!listenersLoading && (listeners || []).length === 0 ? (
|
||||
) : null}
|
||||
{!listenersLoading && !listenerSearch.trim() && (listeners || []).length === 0 ? (
|
||||
<ListItem>
|
||||
<CompactListItemText
|
||||
primary={
|
||||
@@ -446,7 +506,18 @@ export default function AdminTLSSettingsPage() {
|
||||
/>
|
||||
</ListItem>
|
||||
) : null}
|
||||
{(listeners || []).map((item) => (
|
||||
{!listenersLoading && listenerSearch.trim() && listenerCount === 0 ? (
|
||||
<ListItem>
|
||||
<CompactListItemText
|
||||
primary={
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
No matching listeners.
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
) : null}
|
||||
{filteredListeners.map((item: TLSListener) => (
|
||||
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
|
||||
<CompactListItemText
|
||||
primary={
|
||||
|
||||
@@ -65,7 +65,7 @@ export default function AdminUsersPage() {
|
||||
|| item.username.toLowerCase().includes(normalized)
|
||||
|| (item.display_name || '').toLowerCase().includes(normalized)
|
||||
|| (item.email || '').toLowerCase().includes(normalized)
|
||||
|| (item.auth_source || '').toLowerCase().includes(normalized)
|
||||
|| (item.auth_provider_id || '').toLowerCase().includes(normalized)
|
||||
})
|
||||
}, [users, userSearch])
|
||||
|
||||
@@ -299,13 +299,13 @@ export default function AdminUsersPage() {
|
||||
<CompactListItemText
|
||||
primary={
|
||||
<Typography component="div">
|
||||
{u.display_name || u.username}({u.username})
|
||||
{u.display_name || u.username} ({u.username})
|
||||
{u.disabled? (<> <Chip size="small" color='error' label='Disabled'/></>): null}
|
||||
</Typography>
|
||||
}
|
||||
secondary={
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{u.is_admin ? 'admin' : 'user'} · source: {u.auth_source || 'db'}{u.totp_required ? ' · TOTP required' : ''}{u.totp_enabled ? ' · TOTP enabled' : ''}{hasDirectProjectCreate(u.id) ? ' · direct project.create' : ''}
|
||||
{u.is_admin ? 'admin' : 'user'} · source: {u.auth_provider_id || 'db'}{u.totp_required ? ' · TOTP required' : ''}{u.totp_enabled ? ' · TOTP enabled' : ''}{hasDirectProjectCreate(u.id) ? ' · direct project.create' : ''}
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -226,7 +226,7 @@ export default function ApiKeysPage() {
|
||||
<CompactListItemText
|
||||
primary={
|
||||
<Typography component="div">
|
||||
{key.name}({key.prefix})
|
||||
{key.name} ({key.prefix})
|
||||
{key.disabled? (<> <Chip size="small" color='error' label='Disabled'/></>): null}
|
||||
</Typography>
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { Box, Button, Paper, TextField, Typography } from '@mui/material'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { api } from '../api'
|
||||
import { api, PublicAuthProvider } from '../api'
|
||||
import { useBrand } from '../branding'
|
||||
|
||||
export default function LoginPage() {
|
||||
const brand = useBrand()
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [oidcEnabled, setOIDCEnabled] = useState(false)
|
||||
const [oidcConfigured, setOIDCConfigured] = useState(true)
|
||||
const [oidcProviders, setOIDCProviders] = useState<PublicAuthProvider[]>([])
|
||||
const [username, setUsername] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [otpCode, setOTPCode] = useState('')
|
||||
@@ -17,14 +16,12 @@ export default function LoginPage() {
|
||||
|
||||
useEffect(() => {
|
||||
api
|
||||
.oidcStatus()
|
||||
.then((res) => {
|
||||
setOIDCEnabled(Boolean(res.enabled))
|
||||
setOIDCConfigured(res.configured !== false)
|
||||
.listPublicAuthProviders()
|
||||
.then((providers) => {
|
||||
setOIDCProviders(providers.filter((p) => p.type === 'oidc'))
|
||||
})
|
||||
.catch(() => {
|
||||
setOIDCEnabled(false)
|
||||
setOIDCConfigured(false)
|
||||
setOIDCProviders([])
|
||||
})
|
||||
}, [])
|
||||
|
||||
@@ -74,27 +71,20 @@ export default function LoginPage() {
|
||||
<Button type="submit" variant="contained" fullWidth sx={{ mt: 2 }}>
|
||||
{otpRequired ? 'Verify' : 'Login'}
|
||||
</Button>
|
||||
{oidcEnabled ? (
|
||||
<>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
sx={{ mt: 1 }}
|
||||
onClick={() => {
|
||||
window.location.assign('/api/auth/oidc/login')
|
||||
}}
|
||||
disabled={!oidcConfigured}
|
||||
>
|
||||
Login with OIDC
|
||||
</Button>
|
||||
{!oidcConfigured ? (
|
||||
<Typography color="warning.main" variant="body2" sx={{ mt: 1 }}>
|
||||
OIDC is selected but not fully configured.
|
||||
</Typography>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
{oidcProviders.map((p) => (
|
||||
<Button
|
||||
key={p.id}
|
||||
type="button"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
sx={{ mt: 1 }}
|
||||
onClick={() => {
|
||||
window.location.assign(`/api/auth/oidc/${p.id}/login`)
|
||||
}}
|
||||
>
|
||||
Login with {p.name}
|
||||
</Button>
|
||||
))}
|
||||
</Box>
|
||||
</Paper>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user