Monorepo for Tangled
tangled.org
1// an sqlite3 backed secret manager
2package secrets
3
4import (
5 "context"
6 "database/sql"
7 "fmt"
8 "time"
9
10 _ "github.com/mattn/go-sqlite3"
11)
12
13type SqliteManager struct {
14 db *sql.DB
15 tableName string
16}
17
18type SqliteManagerOpt func(*SqliteManager)
19
20func WithTableName(name string) SqliteManagerOpt {
21 return func(s *SqliteManager) {
22 s.tableName = name
23 }
24}
25
26func NewSQLiteManager(dbPath string, opts ...SqliteManagerOpt) (*SqliteManager, error) {
27 db, err := sql.Open("sqlite3", dbPath+"?_foreign_keys=1")
28 if err != nil {
29 return nil, fmt.Errorf("failed to open sqlite database: %w", err)
30 }
31
32 manager := &SqliteManager{
33 db: db,
34 tableName: "secrets",
35 }
36
37 for _, o := range opts {
38 o(manager)
39 }
40
41 if err := manager.init(); err != nil {
42 return nil, err
43 }
44
45 return manager, nil
46}
47
48// creates a table and sets up the schema, migrations if any can go here
49func (s *SqliteManager) init() error {
50 createTable :=
51 `create table if not exists ` + s.tableName + `(
52 id integer primary key autoincrement,
53 repo text not null,
54 key text not null,
55 value text not null,
56 created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
57 created_by text not null,
58
59 unique(repo, key)
60 );`
61 _, err := s.db.Exec(createTable)
62 return err
63}
64
65func (s *SqliteManager) AddSecret(ctx context.Context, secret UnlockedSecret) error {
66 query := fmt.Sprintf(`
67 insert or ignore into %s (repo, key, value, created_at, created_by)
68 values (?, ?, ?, ?, ?);
69 `, s.tableName)
70
71 createdAt := secret.CreatedAt
72 if createdAt.IsZero() {
73 createdAt = time.Now()
74 }
75 res, err := s.db.ExecContext(ctx, query, secret.Repo, secret.Key, secret.Value, createdAt.UTC().Format(time.RFC3339), secret.CreatedBy)
76 if err != nil {
77 return err
78 }
79
80 num, err := res.RowsAffected()
81 if err != nil {
82 return err
83 }
84
85 if num == 0 {
86 return ErrKeyAlreadyPresent
87 }
88
89 return nil
90}
91
92func (s *SqliteManager) RemoveSecret(ctx context.Context, secret Secret[any]) error {
93 query := fmt.Sprintf(`
94 delete from %s where repo = ? and key = ?;
95 `, s.tableName)
96
97 res, err := s.db.ExecContext(ctx, query, secret.Repo, secret.Key)
98 if err != nil {
99 return err
100 }
101
102 num, err := res.RowsAffected()
103 if err != nil {
104 return err
105 }
106
107 if num == 0 {
108 return ErrKeyNotFound
109 }
110
111 return nil
112}
113
114func (s *SqliteManager) GetSecretsLocked(ctx context.Context, didSlashRepo RepoIdentifier) ([]LockedSecret, error) {
115 query := fmt.Sprintf(`
116 select repo, key, created_at, created_by from %s where repo = ?;
117 `, s.tableName)
118
119 rows, err := s.db.QueryContext(ctx, query, didSlashRepo)
120 if err != nil {
121 return nil, err
122 }
123
124 var ls []LockedSecret
125 for rows.Next() {
126 var l LockedSecret
127 var createdAt string
128 if err = rows.Scan(&l.Repo, &l.Key, &createdAt, &l.CreatedBy); err != nil {
129 return nil, err
130 }
131
132 if t, err := time.Parse(time.RFC3339, createdAt); err == nil {
133 l.CreatedAt = t
134 }
135
136 ls = append(ls, l)
137 }
138
139 if err = rows.Err(); err != nil {
140 return nil, err
141 }
142
143 return ls, nil
144}
145
146func (s *SqliteManager) GetSecretsUnlocked(ctx context.Context, didSlashRepo RepoIdentifier) ([]UnlockedSecret, error) {
147 query := fmt.Sprintf(`
148 select repo, key, value, created_at, created_by from %s where repo = ?;
149 `, s.tableName)
150
151 rows, err := s.db.QueryContext(ctx, query, didSlashRepo)
152 if err != nil {
153 return nil, err
154 }
155
156 var ls []UnlockedSecret
157 for rows.Next() {
158 var l UnlockedSecret
159 var createdAt string
160 if err = rows.Scan(&l.Repo, &l.Key, &l.Value, &createdAt, &l.CreatedBy); err != nil {
161 return nil, err
162 }
163
164 if t, err := time.Parse(time.RFC3339, createdAt); err == nil {
165 l.CreatedAt = t
166 }
167
168 ls = append(ls, l)
169 }
170
171 if err = rows.Err(); err != nil {
172 return nil, err
173 }
174
175 return ls, nil
176}