coder/coderd/database/modelmethods.go

431 lines
12 KiB
Go

package database
import (
"sort"
"strconv"
"time"
"golang.org/x/exp/maps"
"golang.org/x/oauth2"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/rbac"
)
type WorkspaceStatus string
const (
WorkspaceStatusPending WorkspaceStatus = "pending"
WorkspaceStatusStarting WorkspaceStatus = "starting"
WorkspaceStatusRunning WorkspaceStatus = "running"
WorkspaceStatusStopping WorkspaceStatus = "stopping"
WorkspaceStatusStopped WorkspaceStatus = "stopped"
WorkspaceStatusFailed WorkspaceStatus = "failed"
WorkspaceStatusCanceling WorkspaceStatus = "canceling"
WorkspaceStatusCanceled WorkspaceStatus = "canceled"
WorkspaceStatusDeleting WorkspaceStatus = "deleting"
WorkspaceStatusDeleted WorkspaceStatus = "deleted"
)
func (s WorkspaceStatus) Valid() bool {
switch s {
case WorkspaceStatusPending, WorkspaceStatusStarting, WorkspaceStatusRunning,
WorkspaceStatusStopping, WorkspaceStatusStopped, WorkspaceStatusFailed,
WorkspaceStatusCanceling, WorkspaceStatusCanceled, WorkspaceStatusDeleting,
WorkspaceStatusDeleted:
return true
default:
return false
}
}
type WorkspaceAgentStatus string
// This is also in codersdk/workspaceagents.go and should be kept in sync.
const (
WorkspaceAgentStatusConnecting WorkspaceAgentStatus = "connecting"
WorkspaceAgentStatusConnected WorkspaceAgentStatus = "connected"
WorkspaceAgentStatusDisconnected WorkspaceAgentStatus = "disconnected"
WorkspaceAgentStatusTimeout WorkspaceAgentStatus = "timeout"
)
func (s WorkspaceAgentStatus) Valid() bool {
switch s {
case WorkspaceAgentStatusConnecting, WorkspaceAgentStatusConnected,
WorkspaceAgentStatusDisconnected, WorkspaceAgentStatusTimeout:
return true
default:
return false
}
}
type AuditableGroup struct {
Group
Members []GroupMember `json:"members"`
}
// Auditable returns an object that can be used in audit logs.
// Covers both group and group member changes.
func (g Group) Auditable(users []User) AuditableGroup {
members := make([]GroupMember, 0, len(users))
for _, u := range users {
members = append(members, GroupMember{
UserID: u.ID,
GroupID: g.ID,
})
}
// consistent ordering
sort.Slice(members, func(i, j int) bool {
return members[i].UserID.String() < members[j].UserID.String()
})
return AuditableGroup{
Group: g,
Members: members,
}
}
const EveryoneGroup = "Everyone"
func (s APIKeyScope) ToRBAC() rbac.ScopeName {
switch s {
case APIKeyScopeAll:
return rbac.ScopeAll
case APIKeyScopeApplicationConnect:
return rbac.ScopeApplicationConnect
default:
panic("developer error: unknown scope type " + string(s))
}
}
func (k APIKey) RBACObject() rbac.Object {
return rbac.ResourceAPIKey.WithIDString(k.ID).
WithOwner(k.UserID.String())
}
func (t Template) RBACObject() rbac.Object {
return rbac.ResourceTemplate.WithID(t.ID).
InOrg(t.OrganizationID).
WithACLUserList(t.UserACL).
WithGroupACL(t.GroupACL)
}
func (t GetFileTemplatesRow) RBACObject() rbac.Object {
return rbac.ResourceTemplate.WithID(t.TemplateID).
InOrg(t.TemplateOrganizationID).
WithACLUserList(t.UserACL).
WithGroupACL(t.GroupACL)
}
func (t Template) DeepCopy() Template {
cpy := t
cpy.UserACL = maps.Clone(t.UserACL)
cpy.GroupACL = maps.Clone(t.GroupACL)
return cpy
}
// AutostartAllowedDays returns the inverse of 'AutostartBlockDaysOfWeek'.
// It is more useful to have the days that are allowed to autostart from a UX
// POV. The database prefers the 0 value being 'all days allowed'.
func (t Template) AutostartAllowedDays() uint8 {
// Just flip the binary 0s to 1s and vice versa.
// There is an extra day with the 8th bit that needs to be zeroed.
return ^uint8(t.AutostartBlockDaysOfWeek) & 0b01111111
}
func (TemplateVersion) RBACObject(template Template) rbac.Object {
// Just use the parent template resource for controlling versions
return template.RBACObject()
}
// RBACObjectNoTemplate is for orphaned template versions.
func (v TemplateVersion) RBACObjectNoTemplate() rbac.Object {
return rbac.ResourceTemplate.InOrg(v.OrganizationID)
}
func (g Group) RBACObject() rbac.Object {
return rbac.ResourceGroup.WithID(g.ID).
InOrg(g.OrganizationID)
}
func (w GetWorkspaceByAgentIDRow) RBACObject() rbac.Object {
return w.Workspace.RBACObject()
}
func (w Workspace) RBACObject() rbac.Object {
return rbac.ResourceWorkspace.WithID(w.ID).
InOrg(w.OrganizationID).
WithOwner(w.OwnerID.String())
}
func (w Workspace) ExecutionRBAC() rbac.Object {
// If a workspace is locked it cannot be accessed.
if w.DormantAt.Valid {
return w.DormantRBAC()
}
return rbac.ResourceWorkspaceExecution.
WithID(w.ID).
InOrg(w.OrganizationID).
WithOwner(w.OwnerID.String())
}
func (w Workspace) ApplicationConnectRBAC() rbac.Object {
// If a workspace is locked it cannot be accessed.
if w.DormantAt.Valid {
return w.DormantRBAC()
}
return rbac.ResourceWorkspaceApplicationConnect.
WithID(w.ID).
InOrg(w.OrganizationID).
WithOwner(w.OwnerID.String())
}
func (w Workspace) WorkspaceBuildRBAC(transition WorkspaceTransition) rbac.Object {
// If a workspace is dormant it cannot be built.
// However we need to allow stopping a workspace by a caller once a workspace
// is locked (e.g. for autobuild). Additionally, if a user wants to delete
// a locked workspace, they shouldn't have to have it unlocked first.
if w.DormantAt.Valid && transition != WorkspaceTransitionStop &&
transition != WorkspaceTransitionDelete {
return w.DormantRBAC()
}
return rbac.ResourceWorkspaceBuild.
WithID(w.ID).
InOrg(w.OrganizationID).
WithOwner(w.OwnerID.String())
}
func (w Workspace) DormantRBAC() rbac.Object {
return rbac.ResourceWorkspaceDormant.
WithID(w.ID).
InOrg(w.OrganizationID).
WithOwner(w.OwnerID.String())
}
func (m OrganizationMember) RBACObject() rbac.Object {
return rbac.ResourceOrganizationMember.
WithID(m.UserID).
InOrg(m.OrganizationID).
WithOwner(m.UserID.String())
}
func (m GetOrganizationIDsByMemberIDsRow) RBACObject() rbac.Object {
// TODO: This feels incorrect as we are really returning a list of orgmembers.
// This return type should be refactored to return a list of orgmembers, not this
// special type.
return rbac.ResourceUserObject(m.UserID)
}
func (o Organization) RBACObject() rbac.Object {
return rbac.ResourceOrganization.
WithID(o.ID).
InOrg(o.ID)
}
func (p ProvisionerDaemon) RBACObject() rbac.Object {
return rbac.ResourceProvisionerDaemon.WithID(p.ID)
}
func (w WorkspaceProxy) RBACObject() rbac.Object {
return rbac.ResourceWorkspaceProxy.
WithID(w.ID)
}
func (w WorkspaceProxy) IsPrimary() bool {
return w.Name == "primary"
}
func (f File) RBACObject() rbac.Object {
return rbac.ResourceFile.
WithID(f.ID).
WithOwner(f.CreatedBy.String())
}
// RBACObject returns the RBAC object for the site wide user resource.
// If you are trying to get the RBAC object for the UserData, use
// u.UserDataRBACObject() instead.
func (u User) RBACObject() rbac.Object {
return rbac.ResourceUserObject(u.ID)
}
func (u User) UserDataRBACObject() rbac.Object {
return rbac.ResourceUserData.WithID(u.ID).WithOwner(u.ID.String())
}
func (u User) UserWorkspaceBuildParametersObject() rbac.Object {
return rbac.ResourceUserWorkspaceBuildParameters.WithID(u.ID).WithOwner(u.ID.String())
}
func (u GetUsersRow) RBACObject() rbac.Object {
return rbac.ResourceUserObject(u.ID)
}
func (u GitSSHKey) RBACObject() rbac.Object {
return rbac.ResourceUserData.WithID(u.UserID).WithOwner(u.UserID.String())
}
func (u ExternalAuthLink) RBACObject() rbac.Object {
// I assume UserData is ok?
return rbac.ResourceUserData.WithID(u.UserID).WithOwner(u.UserID.String())
}
func (u ExternalAuthLink) OAuthToken() *oauth2.Token {
return &oauth2.Token{
AccessToken: u.OAuthAccessToken,
RefreshToken: u.OAuthRefreshToken,
Expiry: u.OAuthExpiry,
}
}
func (u UserLink) RBACObject() rbac.Object {
// I assume UserData is ok?
return rbac.ResourceUserData.WithOwner(u.UserID.String()).WithID(u.UserID)
}
func (l License) RBACObject() rbac.Object {
return rbac.ResourceLicense.WithIDString(strconv.FormatInt(int64(l.ID), 10))
}
func (c OAuth2ProviderAppCode) RBACObject() rbac.Object {
return rbac.ResourceOAuth2ProviderAppCodeToken.WithOwner(c.UserID.String())
}
func (OAuth2ProviderAppSecret) RBACObject() rbac.Object {
return rbac.ResourceOAuth2ProviderAppSecret
}
func (OAuth2ProviderApp) RBACObject() rbac.Object {
return rbac.ResourceOAuth2ProviderApp
}
func (a GetOAuth2ProviderAppsByUserIDRow) RBACObject() rbac.Object {
return a.OAuth2ProviderApp.RBACObject()
}
type WorkspaceAgentConnectionStatus struct {
Status WorkspaceAgentStatus `json:"status"`
FirstConnectedAt *time.Time `json:"first_connected_at"`
LastConnectedAt *time.Time `json:"last_connected_at"`
DisconnectedAt *time.Time `json:"disconnected_at"`
}
func (a WorkspaceAgent) Status(inactiveTimeout time.Duration) WorkspaceAgentConnectionStatus {
connectionTimeout := time.Duration(a.ConnectionTimeoutSeconds) * time.Second
status := WorkspaceAgentConnectionStatus{
Status: WorkspaceAgentStatusDisconnected,
}
if a.FirstConnectedAt.Valid {
status.FirstConnectedAt = &a.FirstConnectedAt.Time
}
if a.LastConnectedAt.Valid {
status.LastConnectedAt = &a.LastConnectedAt.Time
}
if a.DisconnectedAt.Valid {
status.DisconnectedAt = &a.DisconnectedAt.Time
}
switch {
case !a.FirstConnectedAt.Valid:
switch {
case connectionTimeout > 0 && dbtime.Now().Sub(a.CreatedAt) > connectionTimeout:
// If the agent took too long to connect the first time,
// mark it as timed out.
status.Status = WorkspaceAgentStatusTimeout
default:
// If the agent never connected, it's waiting for the compute
// to start up.
status.Status = WorkspaceAgentStatusConnecting
}
// We check before instead of after because last connected at and
// disconnected at can be equal timestamps in tight-timed tests.
case !a.DisconnectedAt.Time.Before(a.LastConnectedAt.Time):
// If we've disconnected after our last connection, we know the
// agent is no longer connected.
status.Status = WorkspaceAgentStatusDisconnected
case dbtime.Now().Sub(a.LastConnectedAt.Time) > inactiveTimeout:
// The connection died without updating the last connected.
status.Status = WorkspaceAgentStatusDisconnected
// Client code needs an accurate disconnected at if the agent has been inactive.
status.DisconnectedAt = &a.LastConnectedAt.Time
case a.LastConnectedAt.Valid:
// The agent should be assumed connected if it's under inactivity timeouts
// and last connected at has been properly set.
status.Status = WorkspaceAgentStatusConnected
}
return status
}
func ConvertUserRows(rows []GetUsersRow) []User {
users := make([]User, len(rows))
for i, r := range rows {
users[i] = User{
ID: r.ID,
Email: r.Email,
Username: r.Username,
HashedPassword: r.HashedPassword,
CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt,
Status: r.Status,
RBACRoles: r.RBACRoles,
LoginType: r.LoginType,
AvatarURL: r.AvatarURL,
Deleted: r.Deleted,
LastSeenAt: r.LastSeenAt,
ThemePreference: r.ThemePreference,
}
}
return users
}
func ConvertWorkspaceRows(rows []GetWorkspacesRow) []Workspace {
workspaces := make([]Workspace, len(rows))
for i, r := range rows {
workspaces[i] = Workspace{
ID: r.ID,
CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt,
OwnerID: r.OwnerID,
OrganizationID: r.OrganizationID,
TemplateID: r.TemplateID,
Deleted: r.Deleted,
Name: r.Name,
AutostartSchedule: r.AutostartSchedule,
Ttl: r.Ttl,
LastUsedAt: r.LastUsedAt,
DormantAt: r.DormantAt,
DeletingAt: r.DeletingAt,
AutomaticUpdates: r.AutomaticUpdates,
Favorite: r.Favorite,
}
}
return workspaces
}
func (g Group) IsEveryone() bool {
return g.ID == g.OrganizationID
}
func (p ProvisionerJob) Finished() bool {
return p.CanceledAt.Valid || p.CompletedAt.Valid
}
func (p ProvisionerJob) FinishedAt() time.Time {
if p.CompletedAt.Valid {
return p.CompletedAt.Time
}
if p.CanceledAt.Valid {
return p.CanceledAt.Time
}
return time.Time{}
}