extend database

This commit is contained in:
paul 2020-07-21 22:50:11 +02:00
parent 8e986c15b2
commit 6d25a0928f
25 changed files with 896 additions and 502 deletions

View file

@ -18,9 +18,9 @@ dev:
migrate: .env
$(GORUN) --tags=dev main.go migrate -u
.PHONY: migratedown
migratedown: .env
$(GORUN) --tags=dev main.go migrate --revision=0
.PHONY: drop
drop: .env
$(GORUN) --tags=dev main.go migrate --drop-all
.PHONY: generate
generate:

View file

@ -1 +1,6 @@
-- this file is only here so the database can track a completely empty state.
-- pgcrypto adds functions for generating UUIDs.
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
-- citext adds indexable case-insensitive text fields.
CREATE EXTENSION IF NOT EXISTS "citext";

View file

@ -1,4 +1,5 @@
DROP TABLE "reset";
DROP TABLE "confirmation";
DROP TABLE "email";
DROP TABLE "user";
DROP TABLE "person";
DROP TABLE "identity";

View file

@ -1,37 +1,51 @@
CREATE TABLE "user" (
-- An Identity is any object that can participate as an actor in the system.
-- It can have groups, permissions, own other objects etc.
CREATE TABLE "identity" (
"id" bigserial NOT NULL,
"login" citext NULL,
"passphrase" bytea NULL,
"totp_secret" text NULL,
"is_admin" boolean NOT NULL DEFAULT false,
"password" bytea NULL,
"created_at" timestamptz NOT NULL DEFAULT NOW(),
"is_disabled" boolean NOT NULL DEFAULT false,
"created_at" timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "identity_login_key" ON "identity" ("login");
-- A person is a human actor within the system, it is linked to exactly one
-- identity.
CREATE TABLE "person" (
"identity_id" bigint NOT NULL,
"display_name" text NULL,
"first_name" text NULL,
"last_name" text NULL,
"image_url" text NULL,
"zoneinfo" text NULL,
"locale" text NULL,
FOREIGN KEY ("identity_id")
REFERENCES "identity" ("id")
ON DELETE CASCADE
ON UPDATE RESTRICT,
PRIMARY KEY ("identity_id")
);
-- Email is an email address for an identity (most likely for a person),
-- that may be verified. Zero or one email address assigned to the identity
-- may be "primary", e.g. used for notifications or login.
CREATE TABLE "email" (
"address" text NOT NULL,
"user_id" bigint NOT NULL,
"created_at" timestamptz NOT NULL DEFAULT NOW(),
FOREIGN KEY ("user_id") REFERENCES "user" ("id") ON DELETE CASCADE,
"address" citext NOT NULL,
"identity_id" bigint NOT NULL,
"is_verified" boolean NOT NULL DEFAULT false,
"is_primary" boolean NOT NULL DEFAULT false,
"created_at" timestamptz NOT NULL DEFAULT now(),
FOREIGN KEY ("identity_id")
REFERENCES "identity" ("id")
ON DELETE CASCADE
ON UPDATE RESTRICT,
PRIMARY KEY ("address")
);
CREATE INDEX ON "email" ("user_id");
CREATE TABLE "confirmation" (
"email_address" text NOT NULL,
"user_id" bigint NOT NULL,
"selector" text NOT NULL,
"verifier" bytea NOT NULL, -- hashed
"expires_at" timestamptz NOT NULL DEFAULT NOW(),
FOREIGN KEY ("user_id") REFERENCES "user" ("id") ON DELETE CASCADE,
PRIMARY KEY ("selector")
);
CREATE INDEX ON "confirmation" ("user_id");
CREATE TABLE "reset" (
"user_id" bigint NOT NULL,
"selector" text NOT NULL,
"verifier" bytea NOT NULL, -- hashed
"expires_at" timestamptz NOT NULL DEFAULT NOW(),
FOREIGN KEY ("user_id") REFERENCES "user" ("id") ON DELETE CASCADE,
PRIMARY KEY ("selector")
);
CREATE UNIQUE INDEX ON "reset" ("user_id");
CREATE INDEX "email_is_verified_idx" ON "email" ("is_verified")
WHERE "is_verified" = true;
CREATE INDEX "email_identity_id_idx" ON "email" ("identity_id");
CREATE UNIQUE INDEX "email_is_primary_key" ON "email" ("identity_id", "is_primary")
WHERE "is_primary" = true;

View file

@ -0,0 +1,3 @@
DROP TABLE "email_confirmation";
DROP TABLE "password_reset";

View file

@ -0,0 +1,28 @@
-- Email Confirmation tracks all email confirmations that have been sent out.
CREATE TABLE "email_confirmation" (
"email_address" citext NOT NULL,
"selector" text NOT NULL,
"verifier" bytea NOT NULL,
"valid_until" timestamptz NOT NULL,
FOREIGN KEY ("email_address")
REFERENCES "email" ("address")
ON DELETE CASCADE
ON UPDATE RESTRICT,
PRIMARY KEY ("email_address")
);
CREATE UNIQUE INDEX "email_confirmation_selector_key"
ON "email_confirmation" ("selector");
-- Password reset keeps track of the password reset tokens.
CREATE TABLE "password_reset" (
"identity_id" bigserial NOT NULL,
"selector" text NOT NULL,
"verifier" bytea NOT NULL,
"valid_until" timestamptz NOT NULL,
FOREIGN KEY ("identity_id")
REFERENCES "person" ("identity_id")
ON DELETE CASCADE
ON UPDATE RESTRICT,
PRIMARY KEY ("identity_id")
);
CREATE UNIQUE INDEX "password_reset_selector_key" ON "password_reset" ("selector");

View file

@ -1,17 +0,0 @@
CREATE TABLE "external_auth" (
"id" bigserial NOT NULL,
"name" text NOT NULL,
"type" text NOT NULL,
"config" jsonb NOT NULL,
PRIMARY KEY ("id")
);
CREATE INDEX ON "external_auth" ("type");
CREATE TABLE "external_user" (
"external_auth_id" bigint NOT NULL,
"foreign_id" text NOT NULL,
"user_id" bigint NOT NULL,
FOREIGN KEY ("user_id") REFERENCES "user" ("id") ON DELETE CASCADE,
FOREIGN KEY ("external_auth_id") REFERENCES "external_auth" ("id") ON DELETE CASCADE,
PRIMARY KEY ("external_auth_id", "foreign_id")
);

View file

@ -0,0 +1,33 @@
CREATE TABLE "external_auth" (
"name" text NOT NULL,
"oidc_url" text NULL,
"auth_url" text NOT NULL,
"token_url" text NOT NULL,
"client_key" text NOT NULL,
"client_secret" text NOT NULL,
"created_at" timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY ("name")
);
CREATE UNIQUE INDEX "external_auth_name_key" ON "external_auth" ("name");
CREATE TABLE "external_user" (
"identity_id" bigint NOT NULL,
"external_auth_name" text NOT NULL,
"external_id" text NOT NULL,
"auth_token" text NULL,
"refresh_token" text NULL,
"identity_token" text NULL,
FOREIGN KEY ("identity_id")
REFERENCES "identity" ("id")
ON UPDATE RESTRICT
ON DELETE CASCADE,
FOREIGN KEY ("external_auth_name")
REFERENCES "external_auth" ("name")
ON UPDATE CASCADE
ON DELETE CASCADE,
PRIMARY KEY ("identity_id")
);
CREATE INDEX "external_user_external_auth_name_idx"
ON "external_user" ("external_auth_name");
CREATE UNIQUE INDEX "external_user_external_id_key"
ON "external_user" ("external_auth_name", "external_id");

View file

@ -8,86 +8,89 @@ import (
"time"
)
const createConfirmation = `-- name: CreateConfirmation :one
INSERT INTO "public"."confirmation" (
"email_address", "user_id", "selector", "verifier", "expires_at"
const createEmailConfirmation = `-- name: CreateEmailConfirmation :one
INSERT INTO "email_confirmation" (
"selector",
"verifier",
"valid_until",
"email_address"
) VALUES (
$1, $2, $3, $4, $5
) RETURNING email_address, user_id, selector, verifier, expires_at
$1,
$2,
$3,
$4
) RETURNING email_address, selector, verifier, valid_until
`
type CreateConfirmationParams struct {
EmailAddress string `json:"email_address"`
UserID int64 `json:"user_id"`
type CreateEmailConfirmationParams struct {
Selector string `json:"selector"`
Verifier []byte `json:"verifier"`
ExpiresAt time.Time `json:"expires_at"`
ValidUntil time.Time `json:"valid_until"`
EmailAddress string `json:"email_address"`
}
func (q *Queries) CreateConfirmation(ctx context.Context, arg CreateConfirmationParams) (Confirmation, error) {
row := q.db.QueryRowContext(ctx, createConfirmation,
arg.EmailAddress,
arg.UserID,
func (q *Queries) CreateEmailConfirmation(ctx context.Context, arg CreateEmailConfirmationParams) (EmailConfirmation, error) {
row := q.db.QueryRowContext(ctx, createEmailConfirmation,
arg.Selector,
arg.Verifier,
arg.ExpiresAt,
arg.ValidUntil,
arg.EmailAddress,
)
var i Confirmation
var i EmailConfirmation
err := row.Scan(
&i.EmailAddress,
&i.UserID,
&i.Selector,
&i.Verifier,
&i.ExpiresAt,
&i.ValidUntil,
)
return i, err
}
const destroyConfirmation = `-- name: DestroyConfirmation :exec
DELETE FROM "public"."confirmation" WHERE "selector" = $1
const destroyEmailConfirmation = `-- name: DestroyEmailConfirmation :exec
DELETE FROM "email_confirmation" WHERE "email_address" = $1
`
func (q *Queries) DestroyConfirmation(ctx context.Context, selector string) error {
_, err := q.db.ExecContext(ctx, destroyConfirmation, selector)
func (q *Queries) DestroyEmailConfirmation(ctx context.Context, emailAddress string) error {
_, err := q.db.ExecContext(ctx, destroyEmailConfirmation, emailAddress)
return err
}
const getConfirmationBySelector = `-- name: GetConfirmationBySelector :one
const getEmailConfirmationByAddress = `-- name: GetEmailConfirmationByAddress :one
SELECT
"email_address", "user_id", "selector", "verifier", "expires_at"
FROM "public"."confirmation"
email_address, selector, verifier, valid_until
FROM "email_confirmation"
WHERE "email_address" = $1
LIMIT 1
`
func (q *Queries) GetEmailConfirmationByAddress(ctx context.Context, emailAddress string) (EmailConfirmation, error) {
row := q.db.QueryRowContext(ctx, getEmailConfirmationByAddress, emailAddress)
var i EmailConfirmation
err := row.Scan(
&i.EmailAddress,
&i.Selector,
&i.Verifier,
&i.ValidUntil,
)
return i, err
}
const getEmailConfirmationBySelector = `-- name: GetEmailConfirmationBySelector :one
SELECT
email_address, selector, verifier, valid_until
FROM "email_confirmation"
WHERE "selector" = $1
LIMIT 1
`
func (q *Queries) GetConfirmationBySelector(ctx context.Context, selector string) (Confirmation, error) {
row := q.db.QueryRowContext(ctx, getConfirmationBySelector, selector)
var i Confirmation
func (q *Queries) GetEmailConfirmationBySelector(ctx context.Context, selector string) (EmailConfirmation, error) {
row := q.db.QueryRowContext(ctx, getEmailConfirmationBySelector, selector)
var i EmailConfirmation
err := row.Scan(
&i.EmailAddress,
&i.UserID,
&i.Selector,
&i.Verifier,
&i.ExpiresAt,
)
return i, err
}
const getConfirmationByUserID = `-- name: GetConfirmationByUserID :one
SELECT
"email_address", "user_id", "selector", "verifier", "expires_at"
FROM "public"."confirmation"
WHERE "user_id" = $1
`
func (q *Queries) GetConfirmationByUserID(ctx context.Context, userID int64) (Confirmation, error) {
row := q.db.QueryRowContext(ctx, getConfirmationByUserID, userID)
var i Confirmation
err := row.Scan(
&i.EmailAddress,
&i.UserID,
&i.Selector,
&i.Verifier,
&i.ExpiresAt,
&i.ValidUntil,
)
return i, err
}

View file

@ -5,27 +5,67 @@ package database
import (
"context"
"time"
)
const countEmailsByIdentityID = `-- name: CountEmailsByIdentityID :one
SELECT
COUNT(*)
FROM
"email"
WHERE
"identity_id" = $1
`
func (q *Queries) CountEmailsByIdentityID(ctx context.Context, identityID int64) (int64, error) {
row := q.db.QueryRowContext(ctx, countEmailsByIdentityID, identityID)
var count int64
err := row.Scan(&count)
return count, err
}
const countUnverifiedEmailsByIdentityID = `-- name: CountUnverifiedEmailsByIdentityID :one
SELECT
COUNT(*)
FROM
"email"
WHERE
"identity_id" = $1 AND
"is_verified" = FALSE
`
func (q *Queries) CountUnverifiedEmailsByIdentityID(ctx context.Context, identityID int64) (int64, error) {
row := q.db.QueryRowContext(ctx, countUnverifiedEmailsByIdentityID, identityID)
var count int64
err := row.Scan(&count)
return count, err
}
const createEmail = `-- name: CreateEmail :one
INSERT INTO "email" (
"address", "user_id", "created_at"
"address",
"identity_id",
"is_verified"
) VALUES (
$1, $2, $3
) RETURNING address, user_id, created_at
$1, $2, $3
) RETURNING address, identity_id, is_verified, is_primary, created_at
`
type CreateEmailParams struct {
Address string `json:"address"`
UserID int64 `json:"user_id"`
CreatedAt time.Time `json:"created_at"`
IdentityID int64 `json:"identity_id"`
IsVerified bool `json:"is_verified"`
}
func (q *Queries) CreateEmail(ctx context.Context, arg CreateEmailParams) (Email, error) {
row := q.db.QueryRowContext(ctx, createEmail, arg.Address, arg.UserID, arg.CreatedAt)
row := q.db.QueryRowContext(ctx, createEmail, arg.Address, arg.IdentityID, arg.IsVerified)
var i Email
err := row.Scan(&i.Address, &i.UserID, &i.CreatedAt)
err := row.Scan(
&i.Address,
&i.IdentityID,
&i.IsVerified,
&i.IsPrimary,
&i.CreatedAt,
)
return i, err
}
@ -40,47 +80,118 @@ func (q *Queries) DestroyEmail(ctx context.Context, address string) error {
const getEmailByAddress = `-- name: GetEmailByAddress :one
SELECT
"address", "user_id", "created_at"
FROM "public"."email"
WHERE "address" = $1
address, identity_id, is_verified, is_primary, created_at
FROM "email"
WHERE
"address" = $1
`
func (q *Queries) GetEmailByAddress(ctx context.Context, address string) (Email, error) {
row := q.db.QueryRowContext(ctx, getEmailByAddress, address)
var i Email
err := row.Scan(&i.Address, &i.UserID, &i.CreatedAt)
err := row.Scan(
&i.Address,
&i.IdentityID,
&i.IsVerified,
&i.IsPrimary,
&i.CreatedAt,
)
return i, err
}
const getEmailByUserID = `-- name: GetEmailByUserID :one
const getEmailByIdentityID = `-- name: GetEmailByIdentityID :many
SELECT
"address", "user_id", "created_at"
FROM "public"."email"
WHERE "user_id" = $1
address, identity_id, is_verified, is_primary, created_at
FROM "email"
WHERE
"identity_id" = $1
`
func (q *Queries) GetEmailByUserID(ctx context.Context, userID int64) (Email, error) {
row := q.db.QueryRowContext(ctx, getEmailByUserID, userID)
func (q *Queries) GetEmailByIdentityID(ctx context.Context, identityID int64) ([]Email, error) {
rows, err := q.db.QueryContext(ctx, getEmailByIdentityID, identityID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Email
for rows.Next() {
var i Email
err := row.Scan(&i.Address, &i.UserID, &i.CreatedAt)
if err := rows.Scan(
&i.Address,
&i.IdentityID,
&i.IsVerified,
&i.IsPrimary,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getPrimaryEmailByIdentityID = `-- name: GetPrimaryEmailByIdentityID :one
SELECT
address, identity_id, is_verified, is_primary, created_at
FROM "email"
WHERE
"identity_id" = $1 AND "is_primary"
`
func (q *Queries) GetPrimaryEmailByIdentityID(ctx context.Context, identityID int64) (Email, error) {
row := q.db.QueryRowContext(ctx, getPrimaryEmailByIdentityID, identityID)
var i Email
err := row.Scan(
&i.Address,
&i.IdentityID,
&i.IsVerified,
&i.IsPrimary,
&i.CreatedAt,
)
return i, err
}
const updateEmail = `-- name: UpdateEmail :exec
UPDATE "email" SET (
"user_id", "created_at"
) = (
$1, $2
) WHERE "address" = $3
const updateEmailPrimary = `-- name: UpdateEmailPrimary :exec
UPDATE "email"
SET
"is_primary" = NOT "is_primary"
WHERE
"identity_id" = $1 AND (
( "address" <> $2 AND "is_primary" = true ) OR
( "address" = $2 AND "is_primary" = false AND "is_verified" = true )
)
`
type UpdateEmailParams struct {
UserID int64 `json:"user_id"`
CreatedAt time.Time `json:"created_at"`
type UpdateEmailPrimaryParams struct {
IdentityID int64 `json:"identity_id"`
Address string `json:"address"`
}
func (q *Queries) UpdateEmail(ctx context.Context, arg UpdateEmailParams) error {
_, err := q.db.ExecContext(ctx, updateEmail, arg.UserID, arg.CreatedAt, arg.Address)
// UpdateEmailPrimary sets exactly one primary email for an identity.
// The mail address has to have been verified.
// this query basically combines these two queries into one atomic one:
// UPDATE "email" SET "is_primary" = false WHERE "identity_id" = $1;
// UPDATE "email" SET "is_primary" = true WHERE "identity_id" = $1 AND "address" = $2 AND "is_verified" = true;
func (q *Queries) UpdateEmailPrimary(ctx context.Context, arg UpdateEmailPrimaryParams) error {
_, err := q.db.ExecContext(ctx, updateEmailPrimary, arg.IdentityID, arg.Address)
return err
}
const updateEmailVerified = `-- name: UpdateEmailVerified :exec
UPDATE "email" SET (
"is_verified"
) = (
$2
) WHERE "address" = $1
`
func (q *Queries) UpdateEmailVerified(ctx context.Context, address string) error {
_, err := q.db.ExecContext(ctx, updateEmailVerified, address)
return err
}

View file

@ -0,0 +1,164 @@
// Code generated by sqlc. DO NOT EDIT.
// source: identity.sql
package database
import (
"context"
"database/sql"
"time"
)
const createIdentity = `-- name: CreateIdentity :one
INSERT INTO "identity" (
"login",
"passphrase",
"is_admin",
"is_disabled"
) VALUES (
$1,
$2,
$3,
$4
) RETURNING id, login, passphrase, totp_secret, is_admin, is_disabled, created_at
`
type CreateIdentityParams struct {
Login sql.NullString `json:"login"`
Passphrase []byte `json:"passphrase"`
IsAdmin bool `json:"is_admin"`
IsDisabled bool `json:"is_disabled"`
}
func (q *Queries) CreateIdentity(ctx context.Context, arg CreateIdentityParams) (Identity, error) {
row := q.db.QueryRowContext(ctx, createIdentity,
arg.Login,
arg.Passphrase,
arg.IsAdmin,
arg.IsDisabled,
)
var i Identity
err := row.Scan(
&i.ID,
&i.Login,
&i.Passphrase,
&i.TotpSecret,
&i.IsAdmin,
&i.IsDisabled,
&i.CreatedAt,
)
return i, err
}
const getIdentityByID = `-- name: GetIdentityByID :one
SELECT id, login, passphrase, totp_secret, is_admin, is_disabled, created_at FROM "identity"
WHERE "id" = $1
LIMIT 1
`
func (q *Queries) GetIdentityByID(ctx context.Context, id int64) (Identity, error) {
row := q.db.QueryRowContext(ctx, getIdentityByID, id)
var i Identity
err := row.Scan(
&i.ID,
&i.Login,
&i.Passphrase,
&i.TotpSecret,
&i.IsAdmin,
&i.IsDisabled,
&i.CreatedAt,
)
return i, err
}
const getIdentityByLogin = `-- name: GetIdentityByLogin :one
SELECT id, login, passphrase, totp_secret, is_admin, is_disabled, created_at FROM "identity"
WHERE "login" = $1
LIMIT 1
`
func (q *Queries) GetIdentityByLogin(ctx context.Context, login sql.NullString) (Identity, error) {
row := q.db.QueryRowContext(ctx, getIdentityByLogin, login)
var i Identity
err := row.Scan(
&i.ID,
&i.Login,
&i.Passphrase,
&i.TotpSecret,
&i.IsAdmin,
&i.IsDisabled,
&i.CreatedAt,
)
return i, err
}
const getIdentityByPrimaryEmail = `-- name: GetIdentityByPrimaryEmail :one
SELECT id, login, passphrase, totp_secret, is_admin, is_disabled, identity.created_at, address, identity_id, is_verified, is_primary, email.created_at FROM "identity"
INNER JOIN "email"
ON "email"."identity_id" = "identity"."id"
WHERE
"email"."address" = $1 AND
"email"."is_primary"
LIMIT 1
`
type GetIdentityByPrimaryEmailRow struct {
ID int64 `json:"id"`
Login sql.NullString `json:"login"`
Passphrase []byte `json:"passphrase"`
TotpSecret sql.NullString `json:"totp_secret"`
IsAdmin bool `json:"is_admin"`
IsDisabled bool `json:"is_disabled"`
CreatedAt time.Time `json:"created_at"`
Address string `json:"address"`
IdentityID int64 `json:"identity_id"`
IsVerified bool `json:"is_verified"`
IsPrimary bool `json:"is_primary"`
CreatedAt_2 time.Time `json:"created_at_2"`
}
func (q *Queries) GetIdentityByPrimaryEmail(ctx context.Context, address string) (GetIdentityByPrimaryEmailRow, error) {
row := q.db.QueryRowContext(ctx, getIdentityByPrimaryEmail, address)
var i GetIdentityByPrimaryEmailRow
err := row.Scan(
&i.ID,
&i.Login,
&i.Passphrase,
&i.TotpSecret,
&i.IsAdmin,
&i.IsDisabled,
&i.CreatedAt,
&i.Address,
&i.IdentityID,
&i.IsVerified,
&i.IsPrimary,
&i.CreatedAt_2,
)
return i, err
}
const updateIdentityLogin = `-- name: UpdateIdentityLogin :exec
UPDATE "identity" SET (
"login"
) = (
$2
) WHERE "id" = $1
`
func (q *Queries) UpdateIdentityLogin(ctx context.Context, id int64) error {
_, err := q.db.ExecContext(ctx, updateIdentityLogin, id)
return err
}
const updateIdentityPassphrase = `-- name: UpdateIdentityPassphrase :exec
UPDATE "identity" SET (
"passphrase"
) = (
$2
) WHERE "id" = $1
`
func (q *Queries) UpdateIdentityPassphrase(ctx context.Context, id int64) error {
_, err := q.db.ExecContext(ctx, updateIdentityPassphrase, id)
return err
}

View file

@ -54,6 +54,17 @@ const (
MigrateUp = -1
)
// GetCurrentMigration returns the currently active migration version.
// If no migration has been applied yet, it will return ErrNilVersion.
func GetCurrentMigration(db *sqlx.DB) (version uint, dirty bool, err error) {
migrator, err := getMigrator(db.DB, http.Dir("/invalid/path/unused"), "/unused")
if err != nil {
return 0, false, err
}
return migrator.Version()
}
// Migrate Migrates the schema of the supplied Database. Supported
// methods are:
// * database.MigrateUp Migrate to the latest version

View file

@ -3,47 +3,67 @@
package database
import (
"encoding/json"
"database/sql"
"time"
)
type Confirmation struct {
EmailAddress string `json:"email_address"`
UserID int64 `json:"user_id"`
Selector string `json:"selector"`
Verifier []byte `json:"verifier"`
ExpiresAt time.Time `json:"expires_at"`
}
type Email struct {
Address string `json:"address"`
UserID int64 `json:"user_id"`
IdentityID int64 `json:"identity_id"`
IsVerified bool `json:"is_verified"`
IsPrimary bool `json:"is_primary"`
CreatedAt time.Time `json:"created_at"`
}
type EmailConfirmation struct {
EmailAddress string `json:"email_address"`
Selector string `json:"selector"`
Verifier []byte `json:"verifier"`
ValidUntil time.Time `json:"valid_until"`
}
type ExternalAuth struct {
ID int64 `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Config json.RawMessage `json:"config"`
OidcUrl sql.NullString `json:"oidc_url"`
AuthUrl string `json:"auth_url"`
TokenUrl string `json:"token_url"`
ClientKey string `json:"client_key"`
ClientSecret string `json:"client_secret"`
CreatedAt time.Time `json:"created_at"`
}
type ExternalUser struct {
ExternalAuthID int64 `json:"external_auth_id"`
ForeignID string `json:"foreign_id"`
UserID int64 `json:"user_id"`
IdentityID int64 `json:"identity_id"`
ExternalAuthName string `json:"external_auth_name"`
ExternalID string `json:"external_id"`
AuthToken sql.NullString `json:"auth_token"`
RefreshToken sql.NullString `json:"refresh_token"`
IdentityToken sql.NullString `json:"identity_token"`
}
type Reset struct {
UserID int64 `json:"user_id"`
Selector string `json:"selector"`
Verifier []byte `json:"verifier"`
ExpiresAt time.Time `json:"expires_at"`
}
type User struct {
type Identity struct {
ID int64 `json:"id"`
Login sql.NullString `json:"login"`
Passphrase []byte `json:"passphrase"`
TotpSecret sql.NullString `json:"totp_secret"`
IsAdmin bool `json:"is_admin"`
Password []byte `json:"password"`
IsDisabled bool `json:"is_disabled"`
CreatedAt time.Time `json:"created_at"`
}
type PasswordReset struct {
IdentityID int64 `json:"identity_id"`
Selector string `json:"selector"`
Verifier []byte `json:"verifier"`
ValidUntil time.Time `json:"valid_until"`
}
type Person struct {
IdentityID int64 `json:"identity_id"`
DisplayName sql.NullString `json:"display_name"`
FirstName sql.NullString `json:"first_name"`
LastName sql.NullString `json:"last_name"`
ImageUrl sql.NullString `json:"image_url"`
Zoneinfo sql.NullString `json:"zoneinfo"`
Locale sql.NullString `json:"locale"`
}

View file

@ -4,25 +4,33 @@ package database
import (
"context"
"database/sql"
)
type Querier interface {
CreateConfirmation(ctx context.Context, arg CreateConfirmationParams) (Confirmation, error)
CountEmailsByIdentityID(ctx context.Context, identityID int64) (int64, error)
CountUnverifiedEmailsByIdentityID(ctx context.Context, identityID int64) (int64, error)
CreateEmail(ctx context.Context, arg CreateEmailParams) (Email, error)
CreateReset(ctx context.Context, arg CreateResetParams) (Reset, error)
CreateUser(ctx context.Context, arg CreateUserParams) (User, error)
DestroyConfirmation(ctx context.Context, selector string) error
CreateEmailConfirmation(ctx context.Context, arg CreateEmailConfirmationParams) (EmailConfirmation, error)
CreateIdentity(ctx context.Context, arg CreateIdentityParams) (Identity, error)
CreateReset(ctx context.Context, arg CreateResetParams) (PasswordReset, error)
DestroyEmail(ctx context.Context, address string) error
DestroyEmailConfirmation(ctx context.Context, emailAddress string) error
DestroyReset(ctx context.Context, selector string) error
GetConfirmationBySelector(ctx context.Context, selector string) (Confirmation, error)
GetConfirmationByUserID(ctx context.Context, userID int64) (Confirmation, error)
GetEmailByAddress(ctx context.Context, address string) (Email, error)
GetEmailByUserID(ctx context.Context, userID int64) (Email, error)
GetResetBySelector(ctx context.Context, selector string) (Reset, error)
GetResetByUserID(ctx context.Context, userID int64) (Reset, error)
GetUserByID(ctx context.Context, id int64) (User, error)
UpdateEmail(ctx context.Context, arg UpdateEmailParams) error
UpdateUser(ctx context.Context, arg UpdateUserParams) error
GetEmailByIdentityID(ctx context.Context, identityID int64) ([]Email, error)
GetEmailConfirmationByAddress(ctx context.Context, emailAddress string) (EmailConfirmation, error)
GetEmailConfirmationBySelector(ctx context.Context, selector string) (EmailConfirmation, error)
GetIdentityByID(ctx context.Context, id int64) (Identity, error)
GetIdentityByLogin(ctx context.Context, login sql.NullString) (Identity, error)
GetIdentityByPrimaryEmail(ctx context.Context, address string) (GetIdentityByPrimaryEmailRow, error)
GetPrimaryEmailByIdentityID(ctx context.Context, identityID int64) (Email, error)
GetResetByIdentityID(ctx context.Context, identityID int64) (PasswordReset, error)
GetResetBySelector(ctx context.Context, selector string) (PasswordReset, error)
UpdateEmailPrimary(ctx context.Context, arg UpdateEmailPrimaryParams) error
UpdateEmailVerified(ctx context.Context, address string) error
UpdateIdentityLogin(ctx context.Context, id int64) error
UpdateIdentityPassphrase(ctx context.Context, id int64) error
}
var _ Querier = (*Queries)(nil)

View file

@ -9,39 +9,45 @@ import (
)
const createReset = `-- name: CreateReset :one
INSERT INTO "reset" (
"user_id", "selector", "verifier", "expires_at"
INSERT INTO "password_reset" (
"identity_id",
"selector",
"verifier",
"valid_until"
) VALUES (
$1, $2, $3, $4
) RETURNING user_id, selector, verifier, expires_at
$1,
$2,
$3,
$4
) RETURNING identity_id, selector, verifier, valid_until
`
type CreateResetParams struct {
UserID int64 `json:"user_id"`
IdentityID int64 `json:"identity_id"`
Selector string `json:"selector"`
Verifier []byte `json:"verifier"`
ExpiresAt time.Time `json:"expires_at"`
ValidUntil time.Time `json:"valid_until"`
}
func (q *Queries) CreateReset(ctx context.Context, arg CreateResetParams) (Reset, error) {
func (q *Queries) CreateReset(ctx context.Context, arg CreateResetParams) (PasswordReset, error) {
row := q.db.QueryRowContext(ctx, createReset,
arg.UserID,
arg.IdentityID,
arg.Selector,
arg.Verifier,
arg.ExpiresAt,
arg.ValidUntil,
)
var i Reset
var i PasswordReset
err := row.Scan(
&i.UserID,
&i.IdentityID,
&i.Selector,
&i.Verifier,
&i.ExpiresAt,
&i.ValidUntil,
)
return i, err
}
const destroyReset = `-- name: DestroyReset :exec
DELETE FROM "reset" WHERE "selector" = $1
DELETE FROM "password_reset" WHERE "selector" = $1
`
func (q *Queries) DestroyReset(ctx context.Context, selector string) error {
@ -49,40 +55,40 @@ func (q *Queries) DestroyReset(ctx context.Context, selector string) error {
return err
}
const getResetByIdentityID = `-- name: GetResetByIdentityID :one
SELECT
identity_id, selector, verifier, valid_until
FROM "password_reset"
WHERE "identity_id" = $1
`
func (q *Queries) GetResetByIdentityID(ctx context.Context, identityID int64) (PasswordReset, error) {
row := q.db.QueryRowContext(ctx, getResetByIdentityID, identityID)
var i PasswordReset
err := row.Scan(
&i.IdentityID,
&i.Selector,
&i.Verifier,
&i.ValidUntil,
)
return i, err
}
const getResetBySelector = `-- name: GetResetBySelector :one
SELECT
"user_id", "selector", "verifier", "expires_at"
FROM "reset"
identity_id, selector, verifier, valid_until
FROM "password_reset"
WHERE "selector" = $1
`
func (q *Queries) GetResetBySelector(ctx context.Context, selector string) (Reset, error) {
func (q *Queries) GetResetBySelector(ctx context.Context, selector string) (PasswordReset, error) {
row := q.db.QueryRowContext(ctx, getResetBySelector, selector)
var i Reset
var i PasswordReset
err := row.Scan(
&i.UserID,
&i.IdentityID,
&i.Selector,
&i.Verifier,
&i.ExpiresAt,
)
return i, err
}
const getResetByUserID = `-- name: GetResetByUserID :one
SELECT
"user_id", "selector", "verifier", "expires_at"
FROM "reset"
WHERE "user_id" = $1
`
func (q *Queries) GetResetByUserID(ctx context.Context, userID int64) (Reset, error) {
row := q.db.QueryRowContext(ctx, getResetByUserID, userID)
var i Reset
err := row.Scan(
&i.UserID,
&i.Selector,
&i.Verifier,
&i.ExpiresAt,
&i.ValidUntil,
)
return i, err
}

View file

@ -1,21 +1,29 @@
-- name: CreateConfirmation :one
INSERT INTO "public"."confirmation" (
"email_address", "user_id", "selector", "verifier", "expires_at"
-- name: CreateEmailConfirmation :one
INSERT INTO "email_confirmation" (
"selector",
"verifier",
"valid_until",
"email_address"
) VALUES (
$1, $2, $3, $4, $5
$1,
$2,
$3,
$4
) RETURNING *;
-- name: DestroyConfirmation :exec
DELETE FROM "public"."confirmation" WHERE "selector" = $1;
-- name: GetConfirmationBySelector :one
-- name: GetEmailConfirmationBySelector :one
SELECT
"email_address", "user_id", "selector", "verifier", "expires_at"
FROM "public"."confirmation"
WHERE "selector" = $1;
*
FROM "email_confirmation"
WHERE "selector" = $1
LIMIT 1;
-- name: GetConfirmationByUserID :one
-- name: GetEmailConfirmationByAddress :one
SELECT
"email_address", "user_id", "selector", "verifier", "expires_at"
FROM "public"."confirmation"
WHERE "user_id" = $1;
*
FROM "email_confirmation"
WHERE "email_address" = $1
LIMIT 1;
-- name: DestroyEmailConfirmation :exec
DELETE FROM "email_confirmation" WHERE "email_address" = $1;

View file

@ -1,28 +1,71 @@
-- name: CreateEmail :one
INSERT INTO "email" (
"address", "user_id", "created_at"
"address",
"identity_id",
"is_verified"
) VALUES (
$1, $2, $3
$1, $2, $3
) RETURNING *;
-- name: UpdateEmail :exec
-- name: UpdateEmailVerified :exec
UPDATE "email" SET (
"user_id", "created_at"
"is_verified"
) = (
$1, $2
) WHERE "address" = $3;
$2
) WHERE "address" = $1;
-- name: DestroyEmail :exec
DELETE FROM "email" WHERE "address" = $1;
-- name: UpdateEmailPrimary :exec
-- UpdateEmailPrimary sets exactly one primary email for an identity.
-- The mail address has to have been verified.
-- this query basically combines these two queries into one atomic one:
-- UPDATE "email" SET "is_primary" = false WHERE "identity_id" = $1;
-- UPDATE "email" SET "is_primary" = true WHERE "identity_id" = $1 AND "address" = $2 AND "is_verified" = true;
UPDATE "email"
SET
"is_primary" = NOT "is_primary"
WHERE
"identity_id" = $1 AND (
( "address" <> $2 AND "is_primary" = true ) OR
( "address" = $2 AND "is_primary" = false AND "is_verified" = true )
);
-- name: GetEmailByAddress :one
SELECT
"address", "user_id", "created_at"
FROM "public"."email"
WHERE "address" = $1;
*
FROM "email"
WHERE
"address" = $1;
-- name: GetEmailByUserID :one
-- name: GetEmailByIdentityID :many
SELECT
"address", "user_id", "created_at"
FROM "public"."email"
WHERE "user_id" = $1;
*
FROM "email"
WHERE
"identity_id" = $1;
-- name: GetPrimaryEmailByIdentityID :one
SELECT
*
FROM "email"
WHERE
"identity_id" = $1 AND "is_primary";
-- name: CountEmailsByIdentityID :one
SELECT
COUNT(*)
FROM
"email"
WHERE
"identity_id" = $1;
-- name: CountUnverifiedEmailsByIdentityID :one
SELECT
COUNT(*)
FROM
"email"
WHERE
"identity_id" = $1 AND
"is_verified" = FALSE;
-- name: DestroyEmail :exec
DELETE FROM "email" WHERE "address" = $1;

View file

@ -0,0 +1,48 @@
-- name: CreateIdentity :one
INSERT INTO "identity" (
"login",
"passphrase",
"is_admin",
"is_disabled"
) VALUES (
$1,
$2,
$3,
$4
) RETURNING *;
-- name: GetIdentityByID :one
SELECT * FROM "identity"
WHERE "id" = $1
LIMIT 1;
-- name: GetIdentityByLogin :one
SELECT * FROM "identity"
WHERE "login" = $1
LIMIT 1;
-- name: GetIdentityByPrimaryEmail :one
SELECT * FROM "identity"
INNER JOIN "email"
ON "email"."identity_id" = "identity"."id"
WHERE
"email"."address" = $1 AND
"email"."is_primary"
LIMIT 1;
-- name: UpdateIdentityPassphrase :exec
UPDATE "identity" SET (
"passphrase"
) = (
$2
) WHERE "id" = $1;
-- name: UpdateIdentityLogin :exec
UPDATE "identity" SET (
"login"
) = (
$2
) WHERE "id" = $1;
-- Name: DestroyIdentity :exec
DELETE FROM "identity" WHERE "id" = $1;

View file

@ -0,0 +1,41 @@
-- Name: CreatePerson :one
INSERT INTO "person" (
"identity_id",
"display_name",
"first_name",
"last_name",
"image_url",
"zoneinfo",
"locale"
) VALUES (
$1,
$2,
$3,
$4,
$5,
$6,
$7
) RETURNING *;
-- Name: GetPersonByIdentityID :one
SELECT * FROM "person"
WHERE
"identity_id" = $1
LIMIT 1;
-- Name: UpdatePerson :exec
UPDATE "person" SET (
"display_name",
"first_name",
"last_name",
"image_url",
"zoneinfo",
"locale"
) = (
$2,
$3,
$4,
$5,
$6,
$7
) WHERE "identity_id" = $1;

View file

@ -1,21 +1,27 @@
-- name: CreateReset :one
INSERT INTO "reset" (
"user_id", "selector", "verifier", "expires_at"
INSERT INTO "password_reset" (
"identity_id",
"selector",
"verifier",
"valid_until"
) VALUES (
$1, $2, $3, $4
$1,
$2,
$3,
$4
) RETURNING *;
-- name: DestroyReset :exec
DELETE FROM "reset" WHERE "selector" = $1;
DELETE FROM "password_reset" WHERE "selector" = $1;
-- name: GetResetBySelector :one
SELECT
"user_id", "selector", "verifier", "expires_at"
FROM "reset"
*
FROM "password_reset"
WHERE "selector" = $1;
-- name: GetResetByUserID :one
-- name: GetResetByIdentityID :one
SELECT
"user_id", "selector", "verifier", "expires_at"
FROM "reset"
WHERE "user_id" = $1;
*
FROM "password_reset"
WHERE "identity_id" = $1;

View file

@ -1,19 +0,0 @@
-- name: GetUserByID :one
SELECT
"id", "is_admin", "password", "created_at"
FROM "user"
WHERE "id" = $1;
-- name: CreateUser :one
INSERT INTO "user" (
"is_admin", "password", "created_at"
) VALUES (
$1, $2, $3
) RETURNING *;
-- name: UpdateUser :exec
UPDATE "user" SET (
"is_admin", "password", "created_at"
) = (
$1, $2, $3
) WHERE "id" = $4;

View file

@ -1,58 +1,122 @@
-- This file is used for generating queries; If you change anything please
-- also add migrations in `assets/migrations`.
CREATE TABLE "user" (
-- pgcrypto adds functions for generating UUIDs.
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
-- citext adds indexable case-insensitive text fields.
CREATE EXTENSION IF NOT EXISTS "citext";
-- An Identity is any object that can participate as an actor in the system.
-- It can have groups, permissions, own other objects etc.
CREATE TABLE "identity" (
"id" bigserial NOT NULL,
"login" citext NULL,
"passphrase" bytea NULL,
"totp_secret" text NULL,
"is_admin" boolean NOT NULL DEFAULT false,
"password" bytea NULL,
"created_at" timestamptz NOT NULL DEFAULT NOW(),
"is_disabled" boolean NOT NULL DEFAULT false,
"created_at" timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "identity_login_key" ON "identity" ("login");
-- A person is a human actor within the system, it is linked to exactly one
-- identity.
CREATE TABLE "person" (
"identity_id" bigint NOT NULL,
"display_name" text NULL,
"first_name" text NULL,
"last_name" text NULL,
"image_url" text NULL,
"zoneinfo" text NULL,
"locale" text NULL,
FOREIGN KEY ("identity_id")
REFERENCES "identity" ("id")
ON DELETE CASCADE
ON UPDATE RESTRICT,
PRIMARY KEY ("identity_id")
);
-- Email is an email address for an identity (most likely for a person),
-- that may be verified. Zero or one email address assigned to the identity
-- may be "primary", e.g. used for notifications or login.
CREATE TABLE "email" (
"address" text NOT NULL,
"user_id" bigint NOT NULL,
"created_at" timestamptz NOT NULL DEFAULT NOW(),
FOREIGN KEY ("user_id") REFERENCES "user" ("id") ON DELETE CASCADE,
"address" citext NOT NULL,
"identity_id" bigint NOT NULL,
"is_verified" boolean NOT NULL DEFAULT false,
"is_primary" boolean NOT NULL DEFAULT false,
"created_at" timestamptz NOT NULL DEFAULT now(),
FOREIGN KEY ("identity_id")
REFERENCES "identity" ("id")
ON DELETE CASCADE
ON UPDATE RESTRICT,
PRIMARY KEY ("address")
);
CREATE INDEX ON "email" ("user_id");
CREATE INDEX "email_is_verified_idx" ON "email" ("is_verified")
WHERE "is_verified" = true;
CREATE INDEX "email_identity_id_idx" ON "email" ("identity_id");
CREATE UNIQUE INDEX "email_is_primary_key" ON "email" ("identity_id", "is_primary")
WHERE "is_primary" = true;
CREATE TABLE "confirmation" (
"email_address" text NOT NULL,
"user_id" bigint NOT NULL,
-- Email Confirmation tracks all email confirmations that have been sent out.
CREATE TABLE "email_confirmation" (
"email_address" citext NOT NULL,
"selector" text NOT NULL,
"verifier" bytea NOT NULL, -- hashed
"expires_at" timestamptz NOT NULL DEFAULT NOW(),
FOREIGN KEY ("user_id") REFERENCES "user" ("id") ON DELETE CASCADE,
PRIMARY KEY ("selector")
"verifier" bytea NOT NULL,
"valid_until" timestamptz NOT NULL,
FOREIGN KEY ("email_address")
REFERENCES "email" ("address")
ON DELETE CASCADE
ON UPDATE RESTRICT,
PRIMARY KEY ("email_address")
);
CREATE INDEX ON "confirmation" ("user_id");
CREATE UNIQUE INDEX "email_confirmation_selector_key"
ON "email_confirmation" ("selector");
CREATE TABLE "reset" (
"user_id" bigint NOT NULL,
-- Password reset keeps track of the password reset tokens.
CREATE TABLE "password_reset" (
"identity_id" bigserial NOT NULL,
"selector" text NOT NULL,
"verifier" bytea NOT NULL, -- hashed
"expires_at" timestamptz NOT NULL DEFAULT NOW(),
FOREIGN KEY ("user_id") REFERENCES "user" ("id") ON DELETE CASCADE,
PRIMARY KEY ("selector")
"verifier" bytea NOT NULL,
"valid_until" timestamptz NOT NULL,
FOREIGN KEY ("identity_id")
REFERENCES "person" ("identity_id")
ON DELETE CASCADE
ON UPDATE RESTRICT,
PRIMARY KEY ("identity_id")
);
CREATE UNIQUE INDEX ON "reset" ("user_id");
CREATE UNIQUE INDEX "password_reset_selector_key" ON "password_reset" ("selector");
CREATE TABLE "external_auth" (
"id" bigserial NOT NULL,
"name" text NOT NULL,
"type" text NOT NULL,
"config" jsonb NOT NULL,
PRIMARY KEY ("id")
"oidc_url" text NULL,
"auth_url" text NOT NULL,
"token_url" text NOT NULL,
"client_key" text NOT NULL,
"client_secret" text NOT NULL,
"created_at" timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY ("name")
);
CREATE INDEX ON "external_auth" ("type");
CREATE UNIQUE INDEX "external_auth_name_key" ON "external_auth" ("name");
CREATE TABLE "external_user" (
"external_auth_id" bigint NOT NULL,
"foreign_id" text NOT NULL,
"user_id" bigint NOT NULL,
FOREIGN KEY ("user_id") REFERENCES "user" ("id") ON DELETE CASCADE,
FOREIGN KEY ("external_auth_id") REFERENCES "external_auth" ("id") ON DELETE CASCADE,
PRIMARY KEY ("external_auth_id", "foreign_id")
"identity_id" bigint NOT NULL,
"external_auth_name" text NOT NULL,
"external_id" text NOT NULL,
"auth_token" text NULL,
"refresh_token" text NULL,
"identity_token" text NULL,
FOREIGN KEY ("identity_id")
REFERENCES "identity" ("id")
ON UPDATE RESTRICT
ON DELETE CASCADE,
FOREIGN KEY ("external_auth_name")
REFERENCES "external_auth" ("name")
ON UPDATE CASCADE
ON DELETE CASCADE,
PRIMARY KEY ("identity_id")
);
CREATE INDEX "external_user_external_auth_name_idx"
ON "external_user" ("external_auth_name");
CREATE UNIQUE INDEX "external_user_external_id_key"
ON "external_user" ("external_auth_name", "external_id");

View file

@ -1,79 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// source: user.sql
package database
import (
"context"
"time"
)
const createUser = `-- name: CreateUser :one
INSERT INTO "user" (
"is_admin", "password", "created_at"
) VALUES (
$1, $2, $3
) RETURNING id, is_admin, password, created_at
`
type CreateUserParams struct {
IsAdmin bool `json:"is_admin"`
Password []byte `json:"password"`
CreatedAt time.Time `json:"created_at"`
}
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) {
row := q.db.QueryRowContext(ctx, createUser, arg.IsAdmin, arg.Password, arg.CreatedAt)
var i User
err := row.Scan(
&i.ID,
&i.IsAdmin,
&i.Password,
&i.CreatedAt,
)
return i, err
}
const getUserByID = `-- name: GetUserByID :one
SELECT
"id", "is_admin", "password", "created_at"
FROM "user"
WHERE "id" = $1
`
func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) {
row := q.db.QueryRowContext(ctx, getUserByID, id)
var i User
err := row.Scan(
&i.ID,
&i.IsAdmin,
&i.Password,
&i.CreatedAt,
)
return i, err
}
const updateUser = `-- name: UpdateUser :exec
UPDATE "user" SET (
"is_admin", "password", "created_at"
) = (
$1, $2, $3
) WHERE "id" = $4
`
type UpdateUserParams struct {
IsAdmin bool `json:"is_admin"`
Password []byte `json:"password"`
CreatedAt time.Time `json:"created_at"`
ID int64 `json:"id"`
}
func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) error {
_, err := q.db.ExecContext(ctx, updateUser,
arg.IsAdmin,
arg.Password,
arg.CreatedAt,
arg.ID,
)
return err
}

View file

@ -1,108 +0,0 @@
package ldap
import (
"fmt"
"regexp"
ldap "github.com/go-ldap/ldap/v3"
"github.com/pkg/errors"
)
var (
// TimeLimitSeconds is the maximal time that LDAP will spend on a single
// request.
TimeLimitSeconds = 5
// SizeLimitEntries is the biggest number of results that is returned from a
// search request.
SizeLimitEntries = 100
// UserAttributes is the list of LDAP-Attributes that will be used for user
// accounts.
UserAttributes = []string{
"dn", // distinguished name, the unique "path" to a LDAP entry.
"cn", // common name, human readable e.g. "Max Powers".
"uid", // user identified, same as the username/login name.
"uidNumber", // unique user ID, integer.
"createTimestamp", // LDAP timestamp of when this entry was created.
"modifyTimestamp", // LDAP timestemp of when this entry was last modified.
}
)
type Server struct {
Host string
Port int
bindDN string
bindPW string
userBaseDN string
}
func (s *Server) newConn() (*ldap.Conn, error) {
lc, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", s.Host, s.Port))
if err != nil {
return nil, errors.Wrap(err, "Failed to dial LDAP")
}
err = lc.Bind(s.bindDN, s.bindPW)
if err != nil {
return nil, errors.Wrap(err, "Failed to bind service account to LDAP")
}
return lc, nil
}
// buildFilterForID builds an LDAP filter that searches for a user with a
// specific uidNumber.
func (s *Server) buildFilterForUserID(id int) string {
return fmt.Sprintf("(&(objectClass=inetOrgPerson)(uidNumber=%d))", id)
}
func (s *Server) buildFilterForEmail(email string) string {
reg := regexp.MustCompile("[^a-zA-Z0-9-+._@]+")
email = reg.ReplaceAllString(email, "")
return fmt.Sprintf("(&(objectClass=)())")
// Conn is an LDAP connection.
type Conn struct {
*ldap.Conn
}
type User struct {
Entry *ldap.Entry
}
// GetDisplayName implements User interface by returning the display name.
func (u User) GetDisplayName() string {
display := u.Entry.GetAttributeValue("displayName")
if display == "" {
display = u.Entry.GetAttributeValue("givenName")
}
if display == "" {
display = u.Entry.GetAttributeValue("cn")
}
if display == "" {
display = u.GetID()
}
return display
}
// GetID implements the User interface by returning the user ID.
func (u User) GetID() string {
id := u.Entry.GetAttributeValue("uid")
return id
}
func (lc *Conn) UserByID(ID string) (User, error) {
ldap.NewSearchRequest()
}
func (s *Server) UserByEmail(email string) (User, error) {
lc, err := s.newConn()
ldap.NewSearchRequest(
s.userBaseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases,
SizeLimitEntries, TimeLimitSeconds, false,
s.buildFilterForEmail(email), UserAttributes, nil)
}