2023-01-17 13:46:22 +00:00
|
|
|
package pkg
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"log"
|
|
|
|
|
|
|
|
"github.com/jackc/pgx/v4"
|
|
|
|
"github.com/jackc/pgx/v4/pgxpool"
|
|
|
|
)
|
|
|
|
|
Implement login cookie, its verification, and logout
At first i thought that i would need to implement sessions, the ones
that keep small files onto the disk, to know which user is talking to
the server, but then i realized that, for now at least, i only need a
very large number, plus the email address, to be used as a lookup, and
that can be stored in the user table, in a separate schema.
Had to change login to avoid raising exceptions when login failed
because i now keep a record of login attemps, and functions are always
run in a single transaction, thus the exception would prevent me to
insert into login_attempt. Even if i use a separate procedure, i could
not keep the records.
I did not want to add a parameter to the logout function because i was
afraid that it could be called from separate users. I do not know
whether it is possible with the current approach, since the settings
variable is also set by the same applications; time will tell.
2023-01-17 19:48:50 +00:00
|
|
|
type Db struct {
|
Add a function to set request settings and the role
I did not like the idea that it was the Go server who should set values
such as request.user or set the role, because this is mostly something
that only the database wants for itself, such as when calling logout. I
am also planning to use these setings for row security with the user’s
id, that the Go application has no need for, but with the current
approach i would need to return it from check_cookie so that it can
return it back to the database when acquiring the connection.
I would have used the same function to set the settings and the role,
but security definer functions—obviously in retrospect—can not set the
role, because then could switch to any role of the user that defined the
function, not the roles they are member of. Thus, a new function.
I did not want to do that every time i needed the database connection
within the same request, because it would perform the same operations
each time—it is the same cookie, afterall—, so new connections are
request scoped and passed along in the context.
2023-01-19 12:07:32 +00:00
|
|
|
*pgxpool.Pool
|
Implement login cookie, its verification, and logout
At first i thought that i would need to implement sessions, the ones
that keep small files onto the disk, to know which user is talking to
the server, but then i realized that, for now at least, i only need a
very large number, plus the email address, to be used as a lookup, and
that can be stored in the user table, in a separate schema.
Had to change login to avoid raising exceptions when login failed
because i now keep a record of login attemps, and functions are always
run in a single transaction, thus the exception would prevent me to
insert into login_attempt. Even if i use a separate procedure, i could
not keep the records.
I did not want to add a parameter to the logout function because i was
afraid that it could be called from separate users. I do not know
whether it is possible with the current approach, since the settings
variable is also set by the same applications; time will tell.
2023-01-17 19:48:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func NewDatabase(ctx context.Context, connString string) (*Db, error) {
|
2023-01-17 13:46:22 +00:00
|
|
|
config, err := pgxpool.ParseConfig(connString)
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
config.AfterConnect = func(ctx context.Context, conn *pgx.Conn) error {
|
2023-02-20 10:42:21 +00:00
|
|
|
if _, err := conn.Exec(ctx, "SET search_path TO numerus, public"); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return registerPgTypes(ctx, conn)
|
2023-01-17 13:46:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
config.BeforeAcquire = func(ctx context.Context, conn *pgx.Conn) bool {
|
Add a function to set request settings and the role
I did not like the idea that it was the Go server who should set values
such as request.user or set the role, because this is mostly something
that only the database wants for itself, such as when calling logout. I
am also planning to use these setings for row security with the user’s
id, that the Go application has no need for, but with the current
approach i would need to return it from check_cookie so that it can
return it back to the database when acquiring the connection.
I would have used the same function to set the settings and the role,
but security definer functions—obviously in retrospect—can not set the
role, because then could switch to any role of the user that defined the
function, not the roles they are member of. Thus, a new function.
I did not want to do that every time i needed the database connection
within the same request, because it would perform the same operations
each time—it is the same cookie, afterall—, so new connections are
request scoped and passed along in the context.
2023-01-19 12:07:32 +00:00
|
|
|
cookie := ""
|
|
|
|
if value, ok := ctx.Value(ContextCookieKey).(string); ok {
|
|
|
|
cookie = value
|
2023-01-17 13:46:22 +00:00
|
|
|
}
|
Add a function to set request settings and the role
I did not like the idea that it was the Go server who should set values
such as request.user or set the role, because this is mostly something
that only the database wants for itself, such as when calling logout. I
am also planning to use these setings for row security with the user’s
id, that the Go application has no need for, but with the current
approach i would need to return it from check_cookie so that it can
return it back to the database when acquiring the connection.
I would have used the same function to set the settings and the role,
but security definer functions—obviously in retrospect—can not set the
role, because then could switch to any role of the user that defined the
function, not the roles they are member of. Thus, a new function.
I did not want to do that every time i needed the database connection
within the same request, because it would perform the same operations
each time—it is the same cookie, afterall—, so new connections are
request scoped and passed along in the context.
2023-01-19 12:07:32 +00:00
|
|
|
if _, err := conn.Exec(ctx, "select set_cookie($1)", cookie); err != nil {
|
2023-01-22 19:37:43 +00:00
|
|
|
log.Printf("ERROR - Failed to set role: %v", err)
|
|
|
|
return false
|
|
|
|
}
|
2023-01-17 13:46:22 +00:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
config.AfterRelease = func(conn *pgx.Conn) bool {
|
|
|
|
if _, err := conn.Exec(context.Background(), "RESET ROLE"); err != nil {
|
|
|
|
log.Printf("ERROR - Failed to reset role: %v", err)
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
Implement login cookie, its verification, and logout
At first i thought that i would need to implement sessions, the ones
that keep small files onto the disk, to know which user is talking to
the server, but then i realized that, for now at least, i only need a
very large number, plus the email address, to be used as a lookup, and
that can be stored in the user table, in a separate schema.
Had to change login to avoid raising exceptions when login failed
because i now keep a record of login attemps, and functions are always
run in a single transaction, thus the exception would prevent me to
insert into login_attempt. Even if i use a separate procedure, i could
not keep the records.
I did not want to add a parameter to the logout function because i was
afraid that it could be called from separate users. I do not know
whether it is possible with the current approach, since the settings
variable is also set by the same applications; time will tell.
2023-01-17 19:48:50 +00:00
|
|
|
pool, err := pgxpool.ConnectConfig(ctx, config)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return &Db{pool}, nil
|
|
|
|
}
|
|
|
|
|
2023-02-14 11:46:11 +00:00
|
|
|
func notFoundErrorOrPanic(err error) bool {
|
|
|
|
if err == pgx.ErrNoRows {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
Add a function to set request settings and the role
I did not like the idea that it was the Go server who should set values
such as request.user or set the role, because this is mostly something
that only the database wants for itself, such as when calling logout. I
am also planning to use these setings for row security with the user’s
id, that the Go application has no need for, but with the current
approach i would need to return it from check_cookie so that it can
return it back to the database when acquiring the connection.
I would have used the same function to set the settings and the role,
but security definer functions—obviously in retrospect—can not set the
role, because then could switch to any role of the user that defined the
function, not the roles they are member of. Thus, a new function.
I did not want to do that every time i needed the database connection
within the same request, because it would perform the same operations
each time—it is the same cookie, afterall—, so new connections are
request scoped and passed along in the context.
2023-01-19 12:07:32 +00:00
|
|
|
func (db *Db) Acquire(ctx context.Context) (*Conn, error) {
|
|
|
|
conn, err := db.Pool.Acquire(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return &Conn{conn}, nil
|
|
|
|
}
|
|
|
|
|
2023-01-22 19:37:43 +00:00
|
|
|
func (db *Db) MustAcquire(ctx context.Context) *Conn {
|
|
|
|
conn, err := db.Acquire(ctx)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
return conn
|
|
|
|
}
|
|
|
|
|
Add a function to set request settings and the role
I did not like the idea that it was the Go server who should set values
such as request.user or set the role, because this is mostly something
that only the database wants for itself, such as when calling logout. I
am also planning to use these setings for row security with the user’s
id, that the Go application has no need for, but with the current
approach i would need to return it from check_cookie so that it can
return it back to the database when acquiring the connection.
I would have used the same function to set the settings and the role,
but security definer functions—obviously in retrospect—can not set the
role, because then could switch to any role of the user that defined the
function, not the roles they are member of. Thus, a new function.
I did not want to do that every time i needed the database connection
within the same request, because it would perform the same operations
each time—it is the same cookie, afterall—, so new connections are
request scoped and passed along in the context.
2023-01-19 12:07:32 +00:00
|
|
|
type Conn struct {
|
|
|
|
*pgxpool.Conn
|
Implement login cookie, its verification, and logout
At first i thought that i would need to implement sessions, the ones
that keep small files onto the disk, to know which user is talking to
the server, but then i realized that, for now at least, i only need a
very large number, plus the email address, to be used as a lookup, and
that can be stored in the user table, in a separate schema.
Had to change login to avoid raising exceptions when login failed
because i now keep a record of login attemps, and functions are always
run in a single transaction, thus the exception would prevent me to
insert into login_attempt. Even if i use a separate procedure, i could
not keep the records.
I did not want to add a parameter to the logout function because i was
afraid that it could be called from separate users. I do not know
whether it is possible with the current approach, since the settings
variable is also set by the same applications; time will tell.
2023-01-17 19:48:50 +00:00
|
|
|
}
|
|
|
|
|
2023-02-08 12:47:36 +00:00
|
|
|
func (c *Conn) MustBegin(ctx context.Context) *Tx {
|
|
|
|
tx, err := c.Begin(ctx)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
return &Tx{tx}
|
|
|
|
}
|
|
|
|
|
2023-01-22 19:37:43 +00:00
|
|
|
func (c *Conn) MustGetText(ctx context.Context, def string, sql string, args ...interface{}) string {
|
Implement login cookie, its verification, and logout
At first i thought that i would need to implement sessions, the ones
that keep small files onto the disk, to know which user is talking to
the server, but then i realized that, for now at least, i only need a
very large number, plus the email address, to be used as a lookup, and
that can be stored in the user table, in a separate schema.
Had to change login to avoid raising exceptions when login failed
because i now keep a record of login attemps, and functions are always
run in a single transaction, thus the exception would prevent me to
insert into login_attempt. Even if i use a separate procedure, i could
not keep the records.
I did not want to add a parameter to the logout function because i was
afraid that it could be called from separate users. I do not know
whether it is possible with the current approach, since the settings
variable is also set by the same applications; time will tell.
2023-01-17 19:48:50 +00:00
|
|
|
var result string
|
2023-02-14 11:46:11 +00:00
|
|
|
if notFoundErrorOrPanic(c.Conn.QueryRow(ctx, sql, args...).Scan(&result)) {
|
|
|
|
return def
|
Implement login cookie, its verification, and logout
At first i thought that i would need to implement sessions, the ones
that keep small files onto the disk, to know which user is talking to
the server, but then i realized that, for now at least, i only need a
very large number, plus the email address, to be used as a lookup, and
that can be stored in the user table, in a separate schema.
Had to change login to avoid raising exceptions when login failed
because i now keep a record of login attemps, and functions are always
run in a single transaction, thus the exception would prevent me to
insert into login_attempt. Even if i use a separate procedure, i could
not keep the records.
I did not want to add a parameter to the logout function because i was
afraid that it could be called from separate users. I do not know
whether it is possible with the current approach, since the settings
variable is also set by the same applications; time will tell.
2023-01-17 19:48:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
2023-02-14 11:39:54 +00:00
|
|
|
func (c *Conn) MustGetBool(ctx context.Context, sql string, args ...interface{}) bool {
|
|
|
|
var result bool
|
|
|
|
if err := c.Conn.QueryRow(ctx, sql, args...).Scan(&result); err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
2023-01-22 19:37:43 +00:00
|
|
|
func (c *Conn) MustExec(ctx context.Context, sql string, args ...interface{}) {
|
Add a function to set request settings and the role
I did not like the idea that it was the Go server who should set values
such as request.user or set the role, because this is mostly something
that only the database wants for itself, such as when calling logout. I
am also planning to use these setings for row security with the user’s
id, that the Go application has no need for, but with the current
approach i would need to return it from check_cookie so that it can
return it back to the database when acquiring the connection.
I would have used the same function to set the settings and the role,
but security definer functions—obviously in retrospect—can not set the
role, because then could switch to any role of the user that defined the
function, not the roles they are member of. Thus, a new function.
I did not want to do that every time i needed the database connection
within the same request, because it would perform the same operations
each time—it is the same cookie, afterall—, so new connections are
request scoped and passed along in the context.
2023-01-19 12:07:32 +00:00
|
|
|
if _, err := c.Conn.Exec(ctx, sql, args...); err != nil {
|
Implement login cookie, its verification, and logout
At first i thought that i would need to implement sessions, the ones
that keep small files onto the disk, to know which user is talking to
the server, but then i realized that, for now at least, i only need a
very large number, plus the email address, to be used as a lookup, and
that can be stored in the user table, in a separate schema.
Had to change login to avoid raising exceptions when login failed
because i now keep a record of login attemps, and functions are always
run in a single transaction, thus the exception would prevent me to
insert into login_attempt. Even if i use a separate procedure, i could
not keep the records.
I did not want to add a parameter to the logout function because i was
afraid that it could be called from separate users. I do not know
whether it is possible with the current approach, since the settings
variable is also set by the same applications; time will tell.
2023-01-17 19:48:50 +00:00
|
|
|
panic(err)
|
|
|
|
}
|
2023-01-17 13:46:22 +00:00
|
|
|
}
|
2023-02-08 12:47:36 +00:00
|
|
|
|
2023-02-12 20:06:48 +00:00
|
|
|
func (c *Conn) MustQuery(ctx context.Context, sql string, args ...interface{}) pgx.Rows {
|
|
|
|
rows, err := c.Conn.Query(ctx, sql, args...)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
return rows
|
|
|
|
}
|
|
|
|
|
2023-02-08 12:47:36 +00:00
|
|
|
type Tx struct {
|
|
|
|
pgx.Tx
|
|
|
|
}
|
|
|
|
|
|
|
|
func (tx *Tx) MustCommit(ctx context.Context) {
|
|
|
|
if err := tx.Commit(ctx); err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (tx *Tx) MustRollback(ctx context.Context) {
|
|
|
|
if err := tx.Rollback(ctx); err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (tx *Tx) MustGetInteger(ctx context.Context, sql string, args ...interface{}) int {
|
|
|
|
var result int
|
|
|
|
if err := tx.QueryRow(ctx, sql, args...).Scan(&result); err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
|
|
|
func (tx *Tx) MustGetIntegerOrDefault(ctx context.Context, def int, sql string, args ...interface{}) int {
|
|
|
|
var result int
|
2023-02-14 11:46:11 +00:00
|
|
|
if notFoundErrorOrPanic(tx.QueryRow(ctx, sql, args...).Scan(&result)) {
|
|
|
|
return def
|
2023-02-08 12:47:36 +00:00
|
|
|
}
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
|
|
|
func (tx *Tx) MustCopyFrom(ctx context.Context, tableName string, columns []string, rows [][]interface{}) int64 {
|
|
|
|
copied, err := tx.CopyFrom(ctx, pgx.Identifier{tableName}, columns, pgx.CopyFromRows(rows))
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
return copied
|
|
|
|
}
|