Files
coder/coderd/database/types.go
T
2026-05-18 22:32:05 +01:00

407 lines
10 KiB
Go

package database
import (
"database/sql/driver"
"encoding/json"
"fmt"
"net"
"strings"
"time"
"github.com/google/uuid"
"github.com/lib/pq"
"github.com/sqlc-dev/pqtype"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/rbac/policy"
)
// AuditOAuthConvertState is never stored in the database. It is stored in a cookie
// clientside as a JWT. This type is provided for audit logging purposes.
type AuditOAuthConvertState struct {
CreatedAt time.Time `db:"created_at" json:"created_at"`
// The time at which the state string expires, a merge request times out if the user does not perform it quick enough.
ExpiresAt time.Time `db:"expires_at" json:"expires_at"`
FromLoginType LoginType `db:"from_login_type" json:"from_login_type"`
// The login type the user is converting to. Should be github or oidc.
ToLoginType LoginType `db:"to_login_type" json:"to_login_type"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
}
type HealthSettings struct {
ID uuid.UUID `db:"id" json:"id"`
DismissedHealthchecks []string `db:"dismissed_healthchecks" json:"dismissed_healthchecks"`
}
type NotificationsSettings struct {
ID uuid.UUID `db:"id" json:"id"`
NotifierPaused bool `db:"notifier_paused" json:"notifier_paused"`
}
type PrebuildsSettings struct {
ID uuid.UUID `db:"id" json:"id"`
ReconciliationPaused bool `db:"reconciliation_paused" json:"reconciliation_paused"`
}
type Actions []policy.Action
func (a *Actions) Scan(src interface{}) error {
switch v := src.(type) {
case string:
return json.Unmarshal([]byte(v), &a)
case []byte:
return json.Unmarshal(v, &a)
}
return xerrors.Errorf("unexpected type %T", src)
}
func (a *Actions) Value() (driver.Value, error) {
return json.Marshal(a)
}
// TemplateACL is a map of ids to permissions.
type TemplateACL map[string][]policy.Action
func (t *TemplateACL) Scan(src interface{}) error {
switch v := src.(type) {
case string:
return json.Unmarshal([]byte(v), &t)
case []byte:
return json.Unmarshal(v, &t)
case json.RawMessage:
return json.Unmarshal(v, &t)
}
return xerrors.Errorf("unexpected type %T", src)
}
func (t TemplateACL) Value() (driver.Value, error) {
return json.Marshal(t)
}
type ChatACL map[string]ChatACLEntry
func (c *ChatACL) Scan(src interface{}) error {
switch v := src.(type) {
case string:
return json.Unmarshal([]byte(v), &c)
case []byte:
return json.Unmarshal(v, &c)
case json.RawMessage:
return json.Unmarshal(v, &c)
}
return xerrors.Errorf("unexpected type %T", src)
}
//nolint:revive
func (c ChatACL) RBACACL() map[string][]policy.Action {
rbacACL := make(map[string][]policy.Action, len(c))
for id, entry := range c {
rbacACL[id] = entry.Permissions
}
return rbacACL
}
func (c ChatACL) Value() (driver.Value, error) {
if c == nil {
return json.Marshal(ChatACL{})
}
return json.Marshal(c)
}
type ChatACLEntry struct {
Permissions []policy.Action `json:"permissions"`
}
type WorkspaceACL map[string]WorkspaceACLEntry
func (t *WorkspaceACL) Scan(src interface{}) error {
switch v := src.(type) {
case string:
return json.Unmarshal([]byte(v), &t)
case []byte:
return json.Unmarshal(v, &t)
case json.RawMessage:
return json.Unmarshal(v, &t)
}
return xerrors.Errorf("unexpected type %T", src)
}
//nolint:revive
func (w WorkspaceACL) RBACACL() map[string][]policy.Action {
// Convert WorkspaceACL to a map of string to []policy.Action.
// This is used for RBAC checks.
rbacACL := make(map[string][]policy.Action, len(w))
for id, entry := range w {
rbacACL[id] = entry.Permissions
}
return rbacACL
}
func (t WorkspaceACL) Value() (driver.Value, error) {
return json.Marshal(t)
}
type WorkspaceACLEntry struct {
Permissions []policy.Action `json:"permissions"`
}
// WorkspaceACLDisplayInfo supplements workspace ACLs with the actors'
// display info. Key is string rather than uuid.UUID as this aligns
// with how RBAC represents actor IDs.
type WorkspaceACLDisplayInfo map[string]struct {
Name string `json:"name"`
AvatarURL string `json:"avatar_url"`
}
// WorkspaceACLDisplayInfo is only used to read from the DB.
func (w *WorkspaceACLDisplayInfo) Scan(src interface{}) error {
switch v := src.(type) {
case string:
return json.Unmarshal([]byte(v), w)
case []byte:
return json.Unmarshal(v, w)
case json.RawMessage:
return json.Unmarshal(v, w)
}
return xerrors.Errorf("unexpected type %T", src)
}
type ExternalAuthProvider struct {
ID string `json:"id"`
Optional bool `json:"optional,omitempty"`
}
type StringMap map[string]string
func (m *StringMap) Scan(src interface{}) error {
if src == nil {
return nil
}
switch src := src.(type) {
case []byte:
err := json.Unmarshal(src, m)
if err != nil {
return err
}
default:
return xerrors.Errorf("unsupported Scan, storing driver.Value type %T into type %T", src, m)
}
return nil
}
func (m StringMap) Value() (driver.Value, error) {
return json.Marshal(m)
}
type StringMapOfInt map[string]int64
func (m *StringMapOfInt) Scan(src interface{}) error {
if src == nil {
return nil
}
switch src := src.(type) {
case []byte:
err := json.Unmarshal(src, m)
if err != nil {
return err
}
default:
return xerrors.Errorf("unsupported Scan, storing driver.Value type %T into type %T", src, m)
}
return nil
}
func (m StringMapOfInt) Value() (driver.Value, error) {
return json.Marshal(m)
}
type CustomRolePermissions []CustomRolePermission
func (s *APIKeyScopes) Scan(src any) error {
var arr []string
if err := pq.Array(&arr).Scan(src); err != nil {
return err
}
out := make(APIKeyScopes, len(arr))
for i, v := range arr {
out[i] = APIKeyScope(v)
}
*s = out
return nil
}
func (s APIKeyScopes) Value() (driver.Value, error) {
arr := make([]string, len(s))
for i, v := range s {
arr[i] = string(v)
}
return pq.Array(arr).Value()
}
func (a *CustomRolePermissions) Scan(src interface{}) error {
switch v := src.(type) {
case string:
return json.Unmarshal([]byte(v), &a)
case []byte:
return json.Unmarshal(v, &a)
}
return xerrors.Errorf("unexpected type %T", src)
}
func (a CustomRolePermissions) Value() (driver.Value, error) {
return json.Marshal(a)
}
type CustomRolePermission struct {
Negate bool `json:"negate"`
ResourceType string `json:"resource_type"`
Action policy.Action `json:"action"`
}
func (a CustomRolePermission) String() string {
str := a.ResourceType + "." + string(a.Action)
if a.Negate {
return "-" + str
}
return str
}
// NameOrganizationPair is used as a lookup tuple for custom role rows.
type NameOrganizationPair struct {
Name string `db:"name" json:"name"`
// OrganizationID if unset will assume a null column value
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
}
func (*NameOrganizationPair) Scan(_ interface{}) error {
return xerrors.Errorf("this should never happen, type 'NameOrganizationPair' should only be used as a parameter")
}
// Value returns the tuple **literal**
// To get the literal value to return, you can use the expression syntax in a psql
// shell.
//
// SELECT ('customrole'::text,'ece79dac-926e-44ca-9790-2ff7c5eb6e0c'::uuid);
// To see 'null' option. Using the nil uuid as null to avoid empty string literals for null.
// SELECT ('customrole',00000000-0000-0000-0000-000000000000);
//
// This value is usually used as an array, NameOrganizationPair[]. You can see
// what that literal is as well, with proper quoting.
//
// SELECT ARRAY[('customrole'::text,'ece79dac-926e-44ca-9790-2ff7c5eb6e0c'::uuid)];
func (a NameOrganizationPair) Value() (driver.Value, error) {
return fmt.Sprintf(`(%s,%s)`, a.Name, a.OrganizationID.String()), nil
}
// AgentIDNamePair is used as a result tuple for workspace and agent rows.
type AgentIDNamePair struct {
ID uuid.UUID `db:"id" json:"id"`
Name string `db:"name" json:"name"`
}
func (p *AgentIDNamePair) Scan(src interface{}) error {
var v string
switch a := src.(type) {
case []byte:
v = string(a)
case string:
v = a
default:
return xerrors.Errorf("unexpected type %T", src)
}
parts := strings.Split(strings.Trim(v, "()"), ",")
if len(parts) != 2 {
return xerrors.New("invalid format for AgentIDNamePair")
}
id, err := uuid.Parse(strings.TrimSpace(parts[0]))
if err != nil {
return err
}
p.ID, p.Name = id, strings.TrimSpace(parts[1])
return nil
}
func (p AgentIDNamePair) Value() (driver.Value, error) {
return fmt.Sprintf(`(%s,%s)`, p.ID.String(), p.Name), nil
}
// UserLinkClaims is the returned IDP claims for a given user link.
// These claims are fetched at login time. These are the claims that were
// used for IDP sync.
type UserLinkClaims struct {
IDTokenClaims map[string]interface{} `json:"id_token_claims"`
UserInfoClaims map[string]interface{} `json:"user_info_claims"`
// MergeClaims are computed in Golang. It is the result of merging
// the IDTokenClaims and UserInfoClaims. UserInfoClaims take precedence.
MergedClaims map[string]interface{} `json:"merged_claims"`
}
func (a *UserLinkClaims) Scan(src interface{}) error {
switch v := src.(type) {
case string:
return json.Unmarshal([]byte(v), &a)
case []byte:
return json.Unmarshal(v, &a)
}
return xerrors.Errorf("unexpected type %T", src)
}
func (a UserLinkClaims) Value() (driver.Value, error) {
return json.Marshal(a)
}
func ParseIP(ipStr string) pqtype.Inet {
ip := net.ParseIP(ipStr)
ipNet := net.IPNet{}
if ip != nil {
ipNet = net.IPNet{
IP: ip,
Mask: net.CIDRMask(len(ip)*8, len(ip)*8),
}
}
return pqtype.Inet{
IPNet: ipNet,
Valid: ip != nil,
}
}
// AllowList is a typed wrapper around a list of AllowListTarget entries.
// It implements sql.Scanner and driver.Valuer so it can be stored in and
// loaded from a Postgres text[] column that stores each entry in the
// canonical form "type:id".
type AllowList []rbac.AllowListElement
// Scan implements sql.Scanner. It supports inputs that pq.Array can decode
// into []string, and then converts each element to an AllowListTarget.
func (a *AllowList) Scan(src any) error {
var raw []string
if err := pq.Array(&raw).Scan(src); err != nil {
return err
}
out := make([]rbac.AllowListElement, len(raw))
for i, s := range raw {
e, err := rbac.ParseAllowListEntry(s)
if err != nil {
return err
}
out[i] = e
}
*a = out
return nil
}
// Value implements driver.Valuer by converting the list to []string using the
// canonical "type:id" form and delegating to pq.Array for encoding.
func (a AllowList) Value() (driver.Value, error) {
raw := make([]string, len(a))
for i, t := range a {
raw[i] = t.String()
}
return pq.Array(raw).Value()
}