pico
created pr with
96.1
added 96.2
1: bd8d606 = 1: bd8d606 feat: access logs
-: ------- > 2: d247cbd feat: find access logs by pubkey
cmds
checkout latest patchset:
ssh pr.pico.sh print 96 | git am -3checkout any patchset in a patch request:
ssh pr.pico.sh print 96.[rev] | git am -3add changes to patch request:
git format-patch main --stdout | ssh pr.pico.sh pr add 96
Patchset
96.1
feat: access logs
Eric Bower
2025-12-17T20:15:23ZThis adds a new table to pico to track access logs. This helps pico admins understand what pubkeys are access their services and what their identity is in the case of certified public keys.
Semantic diff summary
10 added,
6 modified,
1 signature changed,
0 removed
across 9 analyzed files
(2 files skipped: unsupported file type)
+2
-1
Makefile
#
| ... | ... | @@ -142,10 +142,11 @@ migrate: | |
| 142 | 142 | $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20250320_add_tunnel_id_to_tuns_event_logs_table.sql | |
| 143 | 143 | $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20250410_add_index_analytics_visits_host_list.sql | |
| 144 | 144 | $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20250418_add_project_post_idx_analytics.sql | |
| 145 | + | $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20251217_add_access_logs_table.sql | |
| 145 | 146 | .PHONY: migrate | |
| 146 | 147 | ||
| 147 | 148 | latest: | |
| 148 | - | $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20250418_add_project_post_idx_analytics.sql | |
| 149 | + | $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20251217_add_access_logs_table.sql | |
| 149 | 150 | .PHONY: latest | |
| 150 | 151 | ||
| 151 | 152 | psql: |
+12
-2
pkg/apps/auth/api.go
#
| ... | ... | @@ -262,14 +262,14 @@ func keyHandler(apiConfig *shared.ApiConfig) http.HandlerFunc { | |
| 262 | 262 | return | |
| 263 | 263 | } | |
| 264 | 264 | ||
| 265 | - | pubkey, err := shared.PubkeyCertVerify(key, space) | |
| 265 | + | authed, err := shared.PubkeyCertVerify(key, space) | |
| 266 | 266 | if err != nil { | |
| 267 | 267 | log.Error("pubkey cert verify", "err", err) | |
| 268 | 268 | http.Error(w, err.Error(), http.StatusBadRequest) | |
| 269 | 269 | return | |
| 270 | 270 | } | |
| 271 | 271 | ||
| 272 | - | user, err := apiConfig.Dbpool.FindUserForKey(data.Username, pubkey) | |
| 272 | + | user, err := apiConfig.Dbpool.FindUserForKey(data.Username, authed.Pubkey) | |
| 273 | 273 | if err != nil { | |
| 274 | 274 | log.Error("find user for key", "err", err) | |
| 275 | 275 | w.WriteHeader(http.StatusUnauthorized) |
| ... | ... | @@ -282,6 +282,16 @@ func keyHandler(apiConfig *shared.ApiConfig) http.HandlerFunc { | |
| 282 | 282 | return | |
| 283 | 283 | } | |
| 284 | 284 | ||
| 285 | + | err = apiConfig.Dbpool.InsertAccessLog(&db.AccessLog{ | |
| 286 | + | UserID: user.ID, | |
| 287 | + | Service: space, | |
| 288 | + | Identity: authed.Identity, | |
| 289 | + | Pubkey: authed.OrigPubkey, | |
| 290 | + | }) | |
| 291 | + | if err != nil { | |
| 292 | + | log.Error("cannot insert access log", "err", err) | |
| 293 | + | } | |
| 294 | + | ||
| 285 | 295 | if !apiConfig.HasPrivilegedAccess(shared.GetApiToken(r)) { | |
| 286 | 296 | w.WriteHeader(http.StatusOK) | |
| 287 | 297 | return |
+1
-0
pkg/apps/pgs/db/db.go
#
| ... | ... | @@ -9,6 +9,7 @@ type PgsDB interface { | |
| 9 | 9 | FindUsers() ([]*db.User, error) | |
| 10 | 10 | ||
| 11 | 11 | FindFeature(userID string, name string) (*db.FeatureFlag, error) | |
| 12 | + | InsertAccessLog(*db.AccessLog) error | |
| 12 | 13 | ||
| 13 | 14 | InsertProject(userID, name, projectDir string) (string, error) | |
| 14 | 15 | UpdateProject(userID, name string) error |
+4
-0
pkg/apps/pgs/db/memory.go
#
| ... | ... | @@ -191,3 +191,7 @@ func (me *MemoryDB) UpdateProjectAcl(userID, name string, acl db.ProjectAcl) err | |
| 191 | 191 | func (me *MemoryDB) RegisterAdmin(username, pubkey, pubkeyName string) error { | |
| 192 | 192 | return errNotImpl | |
| 193 | 193 | } | |
| 194 | + | ||
| 195 | + | func (me *MemoryDB) InsertAccessLog(*db.AccessLog) error { | |
| 196 | + | return errNotImpl | |
| 197 | + | } |
+11
-0
pkg/apps/pgs/db/postgres.go
#
| ... | ... | @@ -82,6 +82,17 @@ func (me *PgsPsqlDB) FindFeature(userID, name string) (*db.FeatureFlag, error) { | |
| 82 | 82 | return &ff, err | |
| 83 | 83 | } | |
| 84 | 84 | ||
| 85 | + | func (me *PgsPsqlDB) InsertAccessLog(log *db.AccessLog) error { | |
| 86 | + | _, err := me.Db.Exec( | |
| 87 | + | `INSERT INTO access_logs (user_id, service, pubkey, identity) VALUES ($1, $2, $3, $4);`, | |
| 88 | + | log.UserID, | |
| 89 | + | log.Service, | |
| 90 | + | log.Pubkey, | |
| 91 | + | log.Identity, | |
| 92 | + | ) | |
| 93 | + | return err | |
| 94 | + | } | |
| 95 | + | ||
| 85 | 96 | func (me *PgsPsqlDB) InsertProject(userID, name, projectDir string) (string, error) { | |
| 86 | 97 | if !utils.IsValidSubdomain(name) { | |
| 87 | 98 | return "", fmt.Errorf("'%s' is not a valid project name, must match /^[a-z0-9-]+$/", name) |
+13
-0
pkg/db/db.go
#
| ... | ... | @@ -205,6 +205,15 @@ type AnalyticsVisits struct { | |
| 205 | 205 | ContentType string `json:"content_type"` | |
| 206 | 206 | } | |
| 207 | 207 | ||
| 208 | + | type AccessLog struct { | |
| 209 | + | ID string `json:"id"` | |
| 210 | + | UserID string `json:"user_id"` | |
| 211 | + | Service string `json:"service"` | |
| 212 | + | Pubkey string `json:"pubkey"` | |
| 213 | + | Identity string `json:"identity"` | |
| 214 | + | CreatedAt *time.Time `json:"created_at"` | |
| 215 | + | } | |
| 216 | + | ||
| 208 | 217 | type Pager struct { | |
| 209 | 218 | Num int | |
| 210 | 219 | Page int |
| ... | ... | @@ -454,5 +463,9 @@ type DB interface { | |
| 454 | 463 | FindTunsEventLogs(userID string) ([]*TunsEventLog, error) | |
| 455 | 464 | FindTunsEventLogsByAddr(userID, addr string) ([]*TunsEventLog, error) | |
| 456 | 465 | ||
| 466 | + | InsertAccessLog(log *AccessLog) error | |
| 467 | + | FindAccessLogs(userID string, fromDate *time.Time) ([]*AccessLog, error) | |
| 468 | + | FindPubkeysInAccessLogs(userID string) ([]string, error) | |
| 469 | + | ||
| 457 | 470 | Close() error | |
| 458 | 471 | } |
+62
-0
pkg/db/postgres/storage.go
#
| ... | ... | @@ -1940,3 +1940,65 @@ func (me *PsqlDB) FindUserStats(userID string) (*db.UserStats, error) { | |
| 1940 | 1940 | stats.Pages = *pgs | |
| 1941 | 1941 | return &stats, err | |
| 1942 | 1942 | } | |
| 1943 | + | ||
| 1944 | + | func (me *PsqlDB) FindAccessLogs(userID string, fromDate *time.Time) ([]*db.AccessLog, error) { | |
| 1945 | + | logs := []*db.AccessLog{} | |
| 1946 | + | rs, err := me.Db.Query( | |
| 1947 | + | `SELECT id, user_id, service, pubkey, identity, created_at FROM access_logs WHERE user_id=$1 AND created_at >= $2 ORDER BY created_at DESC`, userID, fromDate) | |
| 1948 | + | if err != nil { | |
| 1949 | + | return nil, err | |
| 1950 | + | } | |
| 1951 | + | ||
| 1952 | + | for rs.Next() { | |
| 1953 | + | log := db.AccessLog{} | |
| 1954 | + | err := rs.Scan( | |
| 1955 | + | &log.ID, &log.UserID, &log.Service, &log.Pubkey, &log.Identity, &log.CreatedAt, | |
| 1956 | + | ) | |
| 1957 | + | if err != nil { | |
| 1958 | + | return nil, err | |
| 1959 | + | } | |
| 1960 | + | logs = append(logs, &log) | |
| 1961 | + | } | |
| 1962 | + | ||
| 1963 | + | if rs.Err() != nil { | |
| 1964 | + | return nil, rs.Err() | |
| 1965 | + | } | |
| 1966 | + | ||
| 1967 | + | return logs, nil | |
| 1968 | + | } | |
| 1969 | + | ||
| 1970 | + | func (me *PsqlDB) FindPubkeysInAccessLogs(userID string) ([]string, error) { | |
| 1971 | + | pubkeys := []string{} | |
| 1972 | + | rs, err := me.Db.Query( | |
| 1973 | + | `SELECT DISTINCT(pubkey) FROM access_logs WHERE user_id=$1`, userID, | |
| 1974 | + | ) | |
| 1975 | + | if err != nil { | |
| 1976 | + | return nil, err | |
| 1977 | + | } | |
| 1978 | + | ||
| 1979 | + | for rs.Next() { | |
| 1980 | + | pubkey := "" | |
| 1981 | + | err := rs.Scan(&pubkey) | |
| 1982 | + | if err != nil { | |
| 1983 | + | return nil, err | |
| 1984 | + | } | |
| 1985 | + | pubkeys = append(pubkeys, pubkey) | |
| 1986 | + | } | |
| 1987 | + | ||
| 1988 | + | if rs.Err() != nil { | |
| 1989 | + | return nil, rs.Err() | |
| 1990 | + | } | |
| 1991 | + | ||
| 1992 | + | return pubkeys, nil | |
| 1993 | + | } | |
| 1994 | + | ||
| 1995 | + | func (me *PsqlDB) InsertAccessLog(log *db.AccessLog) error { | |
| 1996 | + | _, err := me.Db.Exec( | |
| 1997 | + | `INSERT INTO access_logs (user_id, service, pubkey, identity) VALUES ($1, $2, $3, $4);`, | |
| 1998 | + | log.UserID, | |
| 1999 | + | log.Service, | |
| 2000 | + | log.Pubkey, | |
| 2001 | + | log.Identity, | |
| 2002 | + | ) | |
| 2003 | + | return err | |
| 2004 | + | } |
+12
-0
pkg/db/stub/stub.go
#
| ... | ... | @@ -288,3 +288,15 @@ func (me *StubDB) VisitUrlNotFound(opts *db.SummaryOpts) ([]*db.VisitUrl, error) | |
| 288 | 288 | func (me *StubDB) FindUsersWithPost(space string) ([]*db.User, error) { | |
| 289 | 289 | return nil, errNotImpl | |
| 290 | 290 | } | |
| 291 | + | ||
| 292 | + | func (me *StubDB) FindAccessLogs(userID string, fromDate *time.Time) ([]*db.AccessLog, error) { | |
| 293 | + | return nil, errNotImpl | |
| 294 | + | } | |
| 295 | + | ||
| 296 | + | func (me *StubDB) FindPubkeysInAccessLogs(userID string) ([]string, error) { | |
| 297 | + | return []string{}, errNotImpl | |
| 298 | + | } | |
| 299 | + | ||
| 300 | + | func (me *StubDB) InsertAccessLog(log *db.AccessLog) error { | |
| 301 | + | return errNotImpl | |
| 302 | + | } |
+3
-0
pkg/pssh/logger.go
#
| ... | ... | @@ -51,11 +51,14 @@ func LogMiddleware(getLogger GetLoggerInterface, database FindUserInterface) SSH | |
| 51 | 51 | } | |
| 52 | 52 | ||
| 53 | 53 | if found { | |
| 54 | + | // identity provided by ssh-cert | |
| 55 | + | identity := s.Permissions().Extensions["identity"] | |
| 54 | 56 | if err == nil && user != nil { | |
| 55 | 57 | logger = logger.With( | |
| 56 | 58 | "user", user.Name, | |
| 57 | 59 | "userId", user.ID, | |
| 58 | 60 | "ip", s.RemoteAddr().String(), | |
| 61 | + | "identity", identity, | |
| 59 | 62 | ) | |
| 60 | 63 | ||
| 61 | 64 | SetUser(s, user) |
+15
-0
sql/migrations/20251217_add_access_logs_table.sql
#
| ... | ... | @@ -0,0 +1,15 @@ | |
| 1 | + | CREATE TABLE IF NOT EXISTS access_logs ( | |
| 2 | + | id uuid NOT NULL DEFAULT uuid_generate_v4(), | |
| 3 | + | user_id uuid NOT NULL, | |
| 4 | + | service character varying(255) NOT NULL, | |
| 5 | + | pubkey text NOT NULL DEFAULT '', | |
| 6 | + | identity text NOT NULL DEFAULT '', | |
| 7 | + | data jsonb NOT NULL DEFAULT '{}'::jsonb, | |
| 8 | + | created_at timestamp without time zone NOT NULL DEFAULT NOW(), | |
| 9 | + | CONSTRAINT access_logs_pkey PRIMARY KEY (id), | |
| 10 | + | CONSTRAINT fk_access_logs_app_users | |
| 11 | + | FOREIGN KEY(user_id) | |
| 12 | + | REFERENCES app_users(id) | |
| 13 | + | ON DELETE CASCADE | |
| 14 | + | ON UPDATE CASCADE | |
| 15 | + | ); |