feat: add template RBAC/groups (#4235)

This commit is contained in:
Jon Ayers
2022-10-10 15:37:06 -05:00
committed by GitHub
parent 2687e3db49
commit 3120c94c22
122 changed files with 8088 additions and 1062 deletions

View File

@ -1,62 +0,0 @@
package database
import (
"context"
"fmt"
"github.com/lib/pq"
"golang.org/x/xerrors"
"github.com/coder/coder/coderd/rbac"
)
type customQuerier interface {
GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspacesParams, authorizedFilter rbac.AuthorizeFilter) ([]Workspace, error)
}
// GetAuthorizedWorkspaces returns all workspaces that the user is authorized to access.
// This code is copied from `GetWorkspaces` and adds the authorized filter WHERE
// clause.
func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspacesParams, authorizedFilter rbac.AuthorizeFilter) ([]Workspace, error) {
// The name comment is for metric tracking
query := fmt.Sprintf("-- name: GetAuthorizedWorkspaces :many\n%s AND %s", getWorkspaces, authorizedFilter.SQLString(rbac.DefaultConfig()))
rows, err := q.db.QueryContext(ctx, query,
arg.Deleted,
arg.OwnerID,
arg.OwnerUsername,
arg.TemplateName,
pq.Array(arg.TemplateIds),
arg.Name,
)
if err != nil {
return nil, xerrors.Errorf("get authorized workspaces: %w", err)
}
defer rows.Close()
var items []Workspace
for rows.Next() {
var i Workspace
if err := rows.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.OwnerID,
&i.OrganizationID,
&i.TemplateID,
&i.Deleted,
&i.Name,
&i.AutostartSchedule,
&i.Ttl,
&i.LastUsedAt,
); 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
}

View File

@ -12,23 +12,30 @@ import (
"github.com/lib/pq"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
"golang.org/x/xerrors"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/rbac"
"github.com/coder/coder/coderd/util/slice"
)
var errDuplicateKey = &pq.Error{
Code: "23505",
Message: "duplicate key value violates unique constraint",
}
// New returns an in-memory fake of the database.
func New() database.Store {
return &fakeQuerier{
mutex: &sync.RWMutex{},
data: &data{
apiKeys: make([]database.APIKey, 0),
agentStats: make([]database.AgentStat, 0),
organizationMembers: make([]database.OrganizationMember, 0),
organizations: make([]database.Organization, 0),
users: make([]database.User, 0),
apiKeys: make([]database.APIKey, 0),
agentStats: make([]database.AgentStat, 0),
organizationMembers: make([]database.OrganizationMember, 0),
organizations: make([]database.Organization, 0),
users: make([]database.User, 0),
groups: make([]database.Group, 0),
groupMembers: make([]database.GroupMember, 0),
auditLogs: make([]database.AuditLog, 0),
files: make([]database.File, 0),
gitSSHKey: make([]database.GitSSHKey, 0),
@ -84,6 +91,8 @@ type data struct {
auditLogs []database.AuditLog
files []database.File
gitSSHKey []database.GitSSHKey
groups []database.Group
groupMembers []database.GroupMember
parameterSchemas []database.ParameterSchema
parameterValues []database.ParameterValue
provisionerDaemons []database.ProvisionerDaemon
@ -518,6 +527,13 @@ func (q *fakeQuerier) GetAuthorizationUserRoles(_ context.Context, userID uuid.U
}
}
var groups []string
for _, member := range q.groupMembers {
if member.UserID == userID {
groups = append(groups, member.GroupID.String())
}
}
if user == nil {
return database.GetAuthorizationUserRolesRow{}, sql.ErrNoRows
}
@ -527,6 +543,7 @@ func (q *fakeQuerier) GetAuthorizationUserRoles(_ context.Context, userID uuid.U
Username: user.Username,
Status: user.Status,
Roles: roles,
Groups: groups,
}, nil
}
@ -1269,6 +1286,116 @@ func (q *fakeQuerier) GetTemplates(_ context.Context) ([]database.Template, erro
return templates, nil
}
func (q *fakeQuerier) UpdateTemplateUserACLByID(_ context.Context, id uuid.UUID, acl database.TemplateACL) error {
q.mutex.RLock()
defer q.mutex.RUnlock()
for i, t := range q.templates {
if t.ID == id {
t = t.SetUserACL(acl)
q.templates[i] = t
return nil
}
}
return sql.ErrNoRows
}
func (q *fakeQuerier) UpdateTemplateGroupACLByID(_ context.Context, id uuid.UUID, acl database.TemplateACL) error {
q.mutex.RLock()
defer q.mutex.RUnlock()
for i, t := range q.templates {
if t.ID == id {
t = t.SetGroupACL(acl)
q.templates[i] = t
return nil
}
}
return sql.ErrNoRows
}
func (q *fakeQuerier) GetTemplateUserRoles(_ context.Context, id uuid.UUID) ([]database.TemplateUser, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
var template database.Template
for _, t := range q.templates {
if t.ID == id {
template = t
break
}
}
if template.ID == uuid.Nil {
return nil, sql.ErrNoRows
}
acl := template.UserACL()
users := make([]database.TemplateUser, 0, len(acl))
for k, v := range acl {
user, err := q.GetUserByID(context.Background(), uuid.MustParse(k))
if err != nil && xerrors.Is(err, sql.ErrNoRows) {
return nil, xerrors.Errorf("get user by ID: %w", err)
}
// We don't delete users from the map if they
// get deleted so just skip.
if xerrors.Is(err, sql.ErrNoRows) {
continue
}
if user.Deleted || user.Status == database.UserStatusSuspended {
continue
}
users = append(users, database.TemplateUser{
User: user,
Actions: v,
})
}
return users, nil
}
func (q *fakeQuerier) GetTemplateGroupRoles(_ context.Context, id uuid.UUID) ([]database.TemplateGroup, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
var template database.Template
for _, t := range q.templates {
if t.ID == id {
template = t
break
}
}
if template.ID == uuid.Nil {
return nil, sql.ErrNoRows
}
acl := template.GroupACL()
groups := make([]database.TemplateGroup, 0, len(acl))
for k, v := range acl {
group, err := q.GetGroupByID(context.Background(), uuid.MustParse(k))
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
return nil, xerrors.Errorf("get group by ID: %w", err)
}
// We don't delete groups from the map if they
// get deleted so just skip.
if xerrors.Is(err, sql.ErrNoRows) {
continue
}
groups = append(groups, database.TemplateGroup{
Group: group,
Actions: v,
})
}
return groups, nil
}
func (q *fakeQuerier) GetOrganizationMemberByUserID(_ context.Context, arg database.GetOrganizationMemberByUserIDParams) (database.OrganizationMember, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
@ -1749,6 +1876,10 @@ func (q *fakeQuerier) InsertTemplate(_ context.Context, arg database.InsertTempl
MinAutostartInterval: arg.MinAutostartInterval,
CreatedBy: arg.CreatedBy,
}
template = template.SetUserACL(database.TemplateACL{})
template = template.SetGroupACL(database.TemplateACL{
arg.OrganizationID.String(): []rbac.Action{rbac.ActionRead},
})
q.templates = append(q.templates, template)
return template, nil
}
@ -2299,7 +2430,7 @@ func (q *fakeQuerier) UpdateWorkspace(_ context.Context, arg database.UpdateWork
continue
}
if other.Name == arg.Name {
return database.Workspace{}, &pq.Error{Code: "23505", Message: "duplicate key value violates unique constraint"}
return database.Workspace{}, errDuplicateKey
}
}
@ -2437,6 +2568,52 @@ func (q *fakeQuerier) UpdateGitSSHKey(_ context.Context, arg database.UpdateGitS
return sql.ErrNoRows
}
func (q *fakeQuerier) InsertGroupMember(_ context.Context, arg database.InsertGroupMemberParams) error {
q.mutex.Lock()
defer q.mutex.Unlock()
for _, member := range q.groupMembers {
if member.GroupID == arg.GroupID &&
member.UserID == arg.UserID {
return errDuplicateKey
}
}
//nolint:gosimple
q.groupMembers = append(q.groupMembers, database.GroupMember{
GroupID: arg.GroupID,
UserID: arg.UserID,
})
return nil
}
func (q *fakeQuerier) DeleteGroupMember(_ context.Context, userID uuid.UUID) error {
q.mutex.Lock()
defer q.mutex.Unlock()
for i, member := range q.groupMembers {
if member.UserID == userID {
q.groupMembers = append(q.groupMembers[:i], q.groupMembers[i+1:]...)
}
}
return nil
}
func (q *fakeQuerier) UpdateGroupByID(_ context.Context, arg database.UpdateGroupByIDParams) (database.Group, error) {
q.mutex.Lock()
defer q.mutex.Unlock()
for i, group := range q.groups {
if group.ID == arg.ID {
group.Name = arg.Name
q.groups[i] = group
return group, nil
}
}
return database.Group{}, sql.ErrNoRows
}
func (q *fakeQuerier) DeleteGitSSHKey(_ context.Context, userID uuid.UUID) error {
q.mutex.Lock()
defer q.mutex.Unlock()
@ -2714,3 +2891,137 @@ func (q *fakeQuerier) UpdateUserLink(_ context.Context, params database.UpdateUs
return database.UserLink{}, sql.ErrNoRows
}
func (q *fakeQuerier) GetGroupByID(_ context.Context, id uuid.UUID) (database.Group, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
for _, group := range q.groups {
if group.ID == id {
return group, nil
}
}
return database.Group{}, sql.ErrNoRows
}
func (q *fakeQuerier) GetGroupByOrgAndName(_ context.Context, arg database.GetGroupByOrgAndNameParams) (database.Group, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
for _, group := range q.groups {
if group.OrganizationID == arg.OrganizationID &&
group.Name == arg.Name {
return group, nil
}
}
return database.Group{}, sql.ErrNoRows
}
func (q *fakeQuerier) InsertAllUsersGroup(ctx context.Context, orgID uuid.UUID) (database.Group, error) {
return q.InsertGroup(ctx, database.InsertGroupParams{
ID: orgID,
Name: database.AllUsersGroup,
OrganizationID: orgID,
})
}
func (q *fakeQuerier) InsertGroup(_ context.Context, arg database.InsertGroupParams) (database.Group, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
for _, group := range q.groups {
if group.OrganizationID.String() == arg.OrganizationID.String() &&
group.Name == arg.Name {
return database.Group{}, errDuplicateKey
}
}
//nolint:gosimple
group := database.Group{
ID: arg.ID,
Name: arg.Name,
OrganizationID: arg.OrganizationID,
}
q.groups = append(q.groups, group)
return group, nil
}
func (*fakeQuerier) GetUserGroups(_ context.Context, _ uuid.UUID) ([]database.Group, error) {
panic("not implemented")
}
func (q *fakeQuerier) GetGroupMembers(_ context.Context, groupID uuid.UUID) ([]database.User, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
var members []database.GroupMember
for _, member := range q.groupMembers {
if member.GroupID == groupID {
members = append(members, member)
}
}
users := make([]database.User, 0, len(members))
for _, member := range members {
for _, user := range q.users {
if user.ID == member.UserID && user.Status == database.UserStatusActive && !user.Deleted {
users = append(users, user)
break
}
}
}
return users, nil
}
func (q *fakeQuerier) GetGroupsByOrganizationID(_ context.Context, organizationID uuid.UUID) ([]database.Group, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
var groups []database.Group
for _, group := range q.groups {
// Omit the allUsers group.
if group.OrganizationID == organizationID && group.ID != organizationID {
groups = append(groups, group)
}
}
return groups, nil
}
func (q *fakeQuerier) GetAllOrganizationMembers(_ context.Context, organizationID uuid.UUID) ([]database.User, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
var users []database.User
for _, member := range q.organizationMembers {
if member.OrganizationID == organizationID {
for _, user := range q.users {
if user.ID == member.UserID {
users = append(users, user)
}
}
}
}
return users, nil
}
func (q *fakeQuerier) DeleteGroupByID(_ context.Context, id uuid.UUID) error {
q.mutex.Lock()
defer q.mutex.Unlock()
for i, group := range q.groups {
if group.ID == id {
q.groups = append(q.groups[:i], q.groups[i+1:]...)
return nil
}
}
return sql.ErrNoRows
}

View File

@ -13,6 +13,7 @@ import (
"database/sql"
"errors"
"github.com/jmoiron/sqlx"
"golang.org/x/xerrors"
)
@ -32,24 +33,34 @@ type DBTX interface {
PrepareContext(context.Context, string) (*sql.Stmt, error)
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
SelectContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error
GetContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error
}
// New creates a new database store using a SQL database connection.
func New(sdb *sql.DB) Store {
dbx := sqlx.NewDb(sdb, "postgres")
return &sqlQuerier{
db: sdb,
sdb: sdb,
db: dbx,
sdb: dbx,
}
}
// queries encompasses both are sqlc generated
// queries and our custom queries.
type querier interface {
sqlcQuerier
customQuerier
}
type sqlQuerier struct {
sdb *sql.DB
sdb *sqlx.DB
db DBTX
}
// InTx performs database operations inside a transaction.
func (q *sqlQuerier) InTx(function func(Store) error) error {
if _, ok := q.db.(*sql.Tx); ok {
if _, ok := q.db.(*sqlx.Tx); ok {
// If the current inner "db" is already a transaction, we just reuse it.
// We do not need to handle commit/rollback as the outer tx will handle
// that.
@ -60,7 +71,7 @@ func (q *sqlQuerier) InTx(function func(Store) error) error {
return nil
}
transaction, err := q.sdb.Begin()
transaction, err := q.sdb.BeginTxx(context.Background(), nil)
if err != nil {
return xerrors.Errorf("begin transaction: %w", err)
}

View File

@ -0,0 +1,40 @@
package dbtestutil
import (
"context"
"database/sql"
"os"
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/database/databasefake"
"github.com/coder/coder/coderd/database/postgres"
)
func NewDB(t *testing.T) (database.Store, database.Pubsub) {
t.Helper()
db := databasefake.New()
pubsub := database.NewPubsubInMemory()
if os.Getenv("DB") != "" {
connectionURL, closePg, err := postgres.Open()
require.NoError(t, err)
t.Cleanup(closePg)
sqlDB, err := sql.Open("postgres", connectionURL)
require.NoError(t, err)
t.Cleanup(func() {
_ = sqlDB.Close()
})
db = database.New(sqlDB)
pubsub, err = database.NewPubsub(context.Background(), sqlDB, connectionURL)
require.NoError(t, err)
t.Cleanup(func() {
_ = pubsub.Close()
})
}
return db, pubsub
}

View File

@ -0,0 +1,26 @@
package database
import (
"database/sql/driver"
"encoding/json"
"golang.org/x/xerrors"
"github.com/coder/coder/coderd/rbac"
)
type Actions []rbac.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)
}

View File

@ -162,6 +162,17 @@ CREATE TABLE gitsshkeys (
public_key text NOT NULL
);
CREATE TABLE group_members (
user_id uuid NOT NULL,
group_id uuid NOT NULL
);
CREATE TABLE groups (
id uuid NOT NULL,
name text NOT NULL,
organization_id uuid NOT NULL
);
CREATE TABLE licenses (
id integer NOT NULL,
uploaded_at timestamp with time zone NOT NULL,
@ -295,7 +306,9 @@ CREATE TABLE templates (
max_ttl bigint DEFAULT '604800000000000'::bigint NOT NULL,
min_autostart_interval bigint DEFAULT '3600000000000'::bigint NOT NULL,
created_by uuid NOT NULL,
icon character varying(256) DEFAULT ''::character varying NOT NULL
icon character varying(256) DEFAULT ''::character varying NOT NULL,
user_acl jsonb DEFAULT '{}'::jsonb NOT NULL,
group_acl jsonb DEFAULT '{}'::jsonb NOT NULL
);
CREATE TABLE user_links (
@ -424,6 +437,15 @@ ALTER TABLE ONLY files
ALTER TABLE ONLY gitsshkeys
ADD CONSTRAINT gitsshkeys_pkey PRIMARY KEY (user_id);
ALTER TABLE ONLY group_members
ADD CONSTRAINT group_members_user_id_group_id_key UNIQUE (user_id, group_id);
ALTER TABLE ONLY groups
ADD CONSTRAINT groups_name_organization_id_key UNIQUE (name, organization_id);
ALTER TABLE ONLY groups
ADD CONSTRAINT groups_pkey PRIMARY KEY (id);
ALTER TABLE ONLY licenses
ADD CONSTRAINT licenses_jwt_key UNIQUE (jwt);
@ -545,6 +567,15 @@ ALTER TABLE ONLY api_keys
ALTER TABLE ONLY gitsshkeys
ADD CONSTRAINT gitsshkeys_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id);
ALTER TABLE ONLY group_members
ADD CONSTRAINT group_members_group_id_fkey FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE;
ALTER TABLE ONLY group_members
ADD CONSTRAINT group_members_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ALTER TABLE ONLY groups
ADD CONSTRAINT groups_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE;
ALTER TABLE ONLY organization_members
ADD CONSTRAINT organization_members_organization_id_uuid_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE;

View File

@ -42,7 +42,7 @@ SCRIPT_DIR=$(dirname "${BASH_SOURCE[0]}")
rm -f queries/*.go
# Fix struct/interface names.
gofmt -w -r 'Querier -> querier' -- *.go
gofmt -w -r 'Querier -> sqlcQuerier' -- *.go
gofmt -w -r 'Queries -> sqlQuerier' -- *.go
# Ensure correct imports exist. Modules must all be downloaded so we get correct

View File

@ -0,0 +1,8 @@
BEGIN;
DROP TABLE group_members;
DROP TABLE groups;
ALTER TABLE templates DROP COLUMN group_acl;
ALTER TABLE templates DROP COLUMN user_acl;
COMMIT;

View File

@ -0,0 +1,48 @@
BEGIN;
ALTER TABLE templates ADD COLUMN user_acl jsonb NOT NULL default '{}';
ALTER TABLE templates ADD COLUMN group_acl jsonb NOT NULL default '{}';
CREATE TABLE groups (
id uuid NOT NULL,
name text NOT NULL,
organization_id uuid NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
PRIMARY KEY(id),
UNIQUE(name, organization_id)
);
CREATE TABLE group_members (
user_id uuid NOT NULL,
group_id uuid NOT NULL,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY(group_id) REFERENCES groups(id) ON DELETE CASCADE,
UNIQUE(user_id, group_id)
);
-- Insert a group for every organization (which should just be 1).
INSERT INTO groups (
id,
name,
organization_id
) SELECT
id, 'Everyone' as name, id
FROM
organizations;
-- Insert allUsers groups into every existing template to avoid breaking
-- existing deployments.
UPDATE
templates
SET
group_acl = (
SELECT
json_build_object(
organizations.id, array_to_json('{"read"}'::text[])
)
FROM
organizations
WHERE
templates.organization_id = organizations.id
);
COMMIT;

View File

@ -1,9 +1,65 @@
package database
import (
"encoding/json"
"fmt"
"github.com/coder/coder/coderd/rbac"
)
const AllUsersGroup = "Everyone"
// TemplateACL is a map of user_ids to permissions.
type TemplateACL map[string][]rbac.Action
func (t Template) UserACL() TemplateACL {
var acl TemplateACL
if len(t.userACL) == 0 {
return acl
}
err := json.Unmarshal(t.userACL, &acl)
if err != nil {
panic(fmt.Sprintf("failed to unmarshal template.userACL: %v", err.Error()))
}
return acl
}
func (t Template) GroupACL() TemplateACL {
var acl TemplateACL
if len(t.groupACL) == 0 {
return acl
}
err := json.Unmarshal(t.groupACL, &acl)
if err != nil {
panic(fmt.Sprintf("failed to unmarshal template.userACL: %v", err.Error()))
}
return acl
}
func (t Template) SetGroupACL(acl TemplateACL) Template {
raw, err := json.Marshal(acl)
if err != nil {
panic(fmt.Sprintf("marshal user acl: %v", err))
}
t.groupACL = raw
return t
}
func (t Template) SetUserACL(acl TemplateACL) Template {
raw, err := json.Marshal(acl)
if err != nil {
panic(fmt.Sprintf("marshal user acl: %v", err))
}
t.userACL = raw
return t
}
func (s APIKeyScope) ToRBAC() rbac.Scope {
switch s {
case APIKeyScopeAll:
@ -16,12 +72,19 @@ func (s APIKeyScope) ToRBAC() rbac.Scope {
}
func (t Template) RBACObject() rbac.Object {
return rbac.ResourceTemplate.InOrg(t.OrganizationID)
obj := rbac.ResourceTemplate
return obj.InOrg(t.OrganizationID).
WithACLUserList(t.UserACL()).
WithGroupACL(t.GroupACL())
}
func (t TemplateVersion) RBACObject() rbac.Object {
func (TemplateVersion) RBACObject(template Template) rbac.Object {
// Just use the parent template resource for controlling versions
return rbac.ResourceTemplate.InOrg(t.OrganizationID)
return template.RBACObject()
}
func (g Group) RBACObject() rbac.Object {
return rbac.ResourceGroup.InOrg(g.OrganizationID)
}
func (w Workspace) RBACObject() rbac.Object {

View File

@ -0,0 +1,208 @@
package database
import (
"context"
"encoding/json"
"fmt"
"github.com/lib/pq"
"github.com/coder/coder/coderd/rbac"
"github.com/google/uuid"
"golang.org/x/xerrors"
)
// customQuerier encompasses all non-generated queries.
// It provides a flexible way to write queries for cases
// where sqlc proves inadequate.
type customQuerier interface {
templateQuerier
workspaceQuerier
}
type templateQuerier interface {
UpdateTemplateUserACLByID(ctx context.Context, id uuid.UUID, acl TemplateACL) error
UpdateTemplateGroupACLByID(ctx context.Context, id uuid.UUID, acl TemplateACL) error
GetTemplateGroupRoles(ctx context.Context, id uuid.UUID) ([]TemplateGroup, error)
GetTemplateUserRoles(ctx context.Context, id uuid.UUID) ([]TemplateUser, error)
}
type TemplateUser struct {
User
Actions Actions `db:"actions"`
}
func (q *sqlQuerier) UpdateTemplateUserACLByID(ctx context.Context, id uuid.UUID, acl TemplateACL) error {
raw, err := json.Marshal(acl)
if err != nil {
return xerrors.Errorf("marshal user acl: %w", err)
}
const query = `
UPDATE
templates
SET
user_acl = $2
WHERE
id = $1`
_, err = q.db.ExecContext(ctx, query, id.String(), raw)
if err != nil {
return xerrors.Errorf("update user acl: %w", err)
}
return nil
}
func (q *sqlQuerier) GetTemplateUserRoles(ctx context.Context, id uuid.UUID) ([]TemplateUser, error) {
const query = `
SELECT
perms.value as actions, users.*
FROM
users
JOIN
(
SELECT
*
FROM
jsonb_each_text(
(
SELECT
templates.user_acl
FROM
templates
WHERE
id = $1
)
)
) AS perms
ON
users.id::text = perms.key
WHERE
users.deleted = false
AND
users.status = 'active';
`
var tus []TemplateUser
err := q.db.SelectContext(ctx, &tus, query, id.String())
if err != nil {
return nil, xerrors.Errorf("select user actions: %w", err)
}
return tus, nil
}
type TemplateGroup struct {
Group
Actions Actions `db:"actions"`
}
func (q *sqlQuerier) UpdateTemplateGroupACLByID(ctx context.Context, id uuid.UUID, acl TemplateACL) error {
raw, err := json.Marshal(acl)
if err != nil {
return xerrors.Errorf("marshal user acl: %w", err)
}
const query = `
UPDATE
templates
SET
group_acl = $2
WHERE
id = $1`
_, err = q.db.ExecContext(ctx, query, id.String(), raw)
if err != nil {
return xerrors.Errorf("update user acl: %w", err)
}
return nil
}
func (q *sqlQuerier) GetTemplateGroupRoles(ctx context.Context, id uuid.UUID) ([]TemplateGroup, error) {
const query = `
SELECT
perms.value as actions, groups.*
FROM
groups
JOIN
(
SELECT
*
FROM
jsonb_each_text(
(
SELECT
templates.group_acl
FROM
templates
WHERE
id = $1
)
)
) AS perms
ON
groups.id::text = perms.key;
`
var tgs []TemplateGroup
err := q.db.SelectContext(ctx, &tgs, query, id.String())
if err != nil {
return nil, xerrors.Errorf("select group roles: %w", err)
}
return tgs, nil
}
type workspaceQuerier interface {
GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspacesParams, authorizedFilter rbac.AuthorizeFilter) ([]Workspace, error)
}
// GetAuthorizedWorkspaces returns all workspaces that the user is authorized to access.
// This code is copied from `GetWorkspaces` and adds the authorized filter WHERE
// clause.
func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspacesParams, authorizedFilter rbac.AuthorizeFilter) ([]Workspace, error) {
// The name comment is for metric tracking
query := fmt.Sprintf("-- name: GetAuthorizedWorkspaces :many\n%s AND %s", getWorkspaces, authorizedFilter.SQLString(rbac.NoACLConfig()))
rows, err := q.db.QueryContext(ctx, query,
arg.Deleted,
arg.OwnerID,
arg.OwnerUsername,
arg.TemplateName,
pq.Array(arg.TemplateIds),
arg.Name,
)
if err != nil {
return nil, xerrors.Errorf("get authorized workspaces: %w", err)
}
defer rows.Close()
var items []Workspace
for rows.Next() {
var i Workspace
if err := rows.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.OwnerID,
&i.OrganizationID,
&i.TemplateID,
&i.Deleted,
&i.Name,
&i.AutostartSchedule,
&i.Ttl,
&i.LastUsedAt,
); 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
}

View File

@ -11,6 +11,7 @@ import (
"time"
"github.com/google/uuid"
"github.com/lib/pq"
"github.com/tabbed/pqtype"
)
@ -413,6 +414,17 @@ type GitSSHKey struct {
PublicKey string `db:"public_key" json:"public_key"`
}
type Group struct {
ID uuid.UUID `db:"id" json:"id"`
Name string `db:"name" json:"name"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
}
type GroupMember struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
GroupID uuid.UUID `db:"group_id" json:"group_id"`
}
type License struct {
ID int32 `db:"id" json:"id"`
UploadedAt time.Time `db:"uploaded_at" json:"uploaded_at"`
@ -524,6 +536,8 @@ type Template struct {
MinAutostartInterval int64 `db:"min_autostart_interval" json:"min_autostart_interval"`
CreatedBy uuid.UUID `db:"created_by" json:"created_by"`
Icon string `db:"icon" json:"icon"`
userACL json.RawMessage `db:"user_acl" json:"user_acl"`
groupACL json.RawMessage `db:"group_acl" json:"group_acl"`
}
type TemplateVersion struct {
@ -546,7 +560,7 @@ type User struct {
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
Status UserStatus `db:"status" json:"status"`
RBACRoles []string `db:"rbac_roles" json:"rbac_roles"`
RBACRoles pq.StringArray `db:"rbac_roles" json:"rbac_roles"`
LoginType LoginType `db:"login_type" json:"login_type"`
AvatarURL sql.NullString `db:"avatar_url" json:"avatar_url"`
Deleted bool `db:"deleted" json:"deleted"`

View File

@ -11,7 +11,7 @@ import (
"github.com/google/uuid"
)
type querier interface {
type sqlcQuerier interface {
// Acquires the lock for a single job that isn't started, completed,
// canceled, and that matches an array of provisioner types.
//
@ -21,6 +21,8 @@ type querier interface {
AcquireProvisionerJob(ctx context.Context, arg AcquireProvisionerJobParams) (ProvisionerJob, error)
DeleteAPIKeyByID(ctx context.Context, id string) error
DeleteGitSSHKey(ctx context.Context, userID uuid.UUID) error
DeleteGroupByID(ctx context.Context, id uuid.UUID) error
DeleteGroupMember(ctx context.Context, userID uuid.UUID) error
DeleteLicense(ctx context.Context, id int32) (int32, error)
DeleteOldAgentStats(ctx context.Context) error
DeleteParameterValueByID(ctx context.Context, id uuid.UUID) error
@ -28,6 +30,7 @@ type querier interface {
GetAPIKeysByLoginType(ctx context.Context, loginType LoginType) ([]APIKey, error)
GetAPIKeysLastUsedAfter(ctx context.Context, lastUsed time.Time) ([]APIKey, error)
GetActiveUserCount(ctx context.Context) (int64, error)
GetAllOrganizationMembers(ctx context.Context, organizationID uuid.UUID) ([]User, error)
GetAuditLogCount(ctx context.Context, arg GetAuditLogCountParams) (int64, error)
// GetAuditLogsBefore retrieves `row_limit` number of audit logs before the provided
// ID.
@ -38,6 +41,10 @@ type querier interface {
GetDeploymentID(ctx context.Context) (string, error)
GetFileByHash(ctx context.Context, hash string) (File, error)
GetGitSSHKey(ctx context.Context, userID uuid.UUID) (GitSSHKey, error)
GetGroupByID(ctx context.Context, id uuid.UUID) (Group, error)
GetGroupByOrgAndName(ctx context.Context, arg GetGroupByOrgAndNameParams) (Group, error)
GetGroupMembers(ctx context.Context, groupID uuid.UUID) ([]User, error)
GetGroupsByOrganizationID(ctx context.Context, organizationID uuid.UUID) ([]Group, error)
GetLatestAgentStat(ctx context.Context, agentID uuid.UUID) (AgentStat, error)
GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (WorkspaceBuild, error)
GetLatestWorkspaceBuilds(ctx context.Context) ([]WorkspaceBuild, error)
@ -73,6 +80,7 @@ type querier interface {
GetUserByEmailOrUsername(ctx context.Context, arg GetUserByEmailOrUsernameParams) (User, error)
GetUserByID(ctx context.Context, id uuid.UUID) (User, error)
GetUserCount(ctx context.Context) (int64, error)
GetUserGroups(ctx context.Context, userID uuid.UUID) ([]Group, error)
GetUserLinkByLinkedID(ctx context.Context, linkedID string) (UserLink, error)
GetUserLinkByUserIDLoginType(ctx context.Context, arg GetUserLinkByUserIDLoginTypeParams) (UserLink, error)
GetUsers(ctx context.Context, arg GetUsersParams) ([]User, error)
@ -108,10 +116,16 @@ type querier interface {
GetWorkspaces(ctx context.Context, arg GetWorkspacesParams) ([]Workspace, error)
InsertAPIKey(ctx context.Context, arg InsertAPIKeyParams) (APIKey, error)
InsertAgentStat(ctx context.Context, arg InsertAgentStatParams) (AgentStat, error)
// We use the organization_id as the id
// for simplicity since all users is
// every member of the org.
InsertAllUsersGroup(ctx context.Context, organizationID uuid.UUID) (Group, error)
InsertAuditLog(ctx context.Context, arg InsertAuditLogParams) (AuditLog, error)
InsertDeploymentID(ctx context.Context, value string) error
InsertFile(ctx context.Context, arg InsertFileParams) (File, error)
InsertGitSSHKey(ctx context.Context, arg InsertGitSSHKeyParams) (GitSSHKey, error)
InsertGroup(ctx context.Context, arg InsertGroupParams) (Group, error)
InsertGroupMember(ctx context.Context, arg InsertGroupMemberParams) error
InsertLicense(ctx context.Context, arg InsertLicenseParams) (License, error)
InsertOrganization(ctx context.Context, arg InsertOrganizationParams) (Organization, error)
InsertOrganizationMember(ctx context.Context, arg InsertOrganizationMemberParams) (OrganizationMember, error)
@ -134,6 +148,7 @@ type querier interface {
ParameterValues(ctx context.Context, arg ParameterValuesParams) ([]ParameterValue, error)
UpdateAPIKeyByID(ctx context.Context, arg UpdateAPIKeyByIDParams) error
UpdateGitSSHKey(ctx context.Context, arg UpdateGitSSHKeyParams) error
UpdateGroupByID(ctx context.Context, arg UpdateGroupByIDParams) (Group, error)
UpdateMemberRoles(ctx context.Context, arg UpdateMemberRolesParams) (OrganizationMember, error)
UpdateProvisionerDaemonByID(ctx context.Context, arg UpdateProvisionerDaemonByIDParams) error
UpdateProvisionerJobByID(ctx context.Context, arg UpdateProvisionerJobByIDParams) error
@ -163,4 +178,4 @@ type querier interface {
UpdateWorkspaceTTL(ctx context.Context, arg UpdateWorkspaceTTLParams) error
}
var _ querier = (*sqlQuerier)(nil)
var _ sqlcQuerier = (*sqlQuerier)(nil)

View File

@ -807,6 +807,328 @@ func (q *sqlQuerier) UpdateGitSSHKey(ctx context.Context, arg UpdateGitSSHKeyPar
return err
}
const deleteGroupByID = `-- name: DeleteGroupByID :exec
DELETE FROM
groups
WHERE
id = $1
`
func (q *sqlQuerier) DeleteGroupByID(ctx context.Context, id uuid.UUID) error {
_, err := q.db.ExecContext(ctx, deleteGroupByID, id)
return err
}
const deleteGroupMember = `-- name: DeleteGroupMember :exec
DELETE FROM
group_members
WHERE
user_id = $1
`
func (q *sqlQuerier) DeleteGroupMember(ctx context.Context, userID uuid.UUID) error {
_, err := q.db.ExecContext(ctx, deleteGroupMember, userID)
return err
}
const getAllOrganizationMembers = `-- name: GetAllOrganizationMembers :many
SELECT
users.id, users.email, users.username, users.hashed_password, users.created_at, users.updated_at, users.status, users.rbac_roles, users.login_type, users.avatar_url, users.deleted, users.last_seen_at
FROM
users
JOIN
organization_members
ON
users.id = organization_members.user_id
WHERE
organization_members.organization_id = $1
`
func (q *sqlQuerier) GetAllOrganizationMembers(ctx context.Context, organizationID uuid.UUID) ([]User, error) {
rows, err := q.db.QueryContext(ctx, getAllOrganizationMembers, organizationID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []User
for rows.Next() {
var i User
if err := rows.Scan(
&i.ID,
&i.Email,
&i.Username,
&i.HashedPassword,
&i.CreatedAt,
&i.UpdatedAt,
&i.Status,
&i.RBACRoles,
&i.LoginType,
&i.AvatarURL,
&i.Deleted,
&i.LastSeenAt,
); 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 getGroupByID = `-- name: GetGroupByID :one
SELECT
id, name, organization_id
FROM
groups
WHERE
id = $1
LIMIT
1
`
func (q *sqlQuerier) GetGroupByID(ctx context.Context, id uuid.UUID) (Group, error) {
row := q.db.QueryRowContext(ctx, getGroupByID, id)
var i Group
err := row.Scan(&i.ID, &i.Name, &i.OrganizationID)
return i, err
}
const getGroupByOrgAndName = `-- name: GetGroupByOrgAndName :one
SELECT
id, name, organization_id
FROM
groups
WHERE
organization_id = $1
AND
name = $2
LIMIT
1
`
type GetGroupByOrgAndNameParams struct {
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
Name string `db:"name" json:"name"`
}
func (q *sqlQuerier) GetGroupByOrgAndName(ctx context.Context, arg GetGroupByOrgAndNameParams) (Group, error) {
row := q.db.QueryRowContext(ctx, getGroupByOrgAndName, arg.OrganizationID, arg.Name)
var i Group
err := row.Scan(&i.ID, &i.Name, &i.OrganizationID)
return i, err
}
const getGroupMembers = `-- name: GetGroupMembers :many
SELECT
users.id, users.email, users.username, users.hashed_password, users.created_at, users.updated_at, users.status, users.rbac_roles, users.login_type, users.avatar_url, users.deleted, users.last_seen_at
FROM
users
JOIN
group_members
ON
users.id = group_members.user_id
WHERE
group_members.group_id = $1
AND
users.status = 'active'
AND
users.deleted = 'false'
`
func (q *sqlQuerier) GetGroupMembers(ctx context.Context, groupID uuid.UUID) ([]User, error) {
rows, err := q.db.QueryContext(ctx, getGroupMembers, groupID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []User
for rows.Next() {
var i User
if err := rows.Scan(
&i.ID,
&i.Email,
&i.Username,
&i.HashedPassword,
&i.CreatedAt,
&i.UpdatedAt,
&i.Status,
&i.RBACRoles,
&i.LoginType,
&i.AvatarURL,
&i.Deleted,
&i.LastSeenAt,
); 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 getGroupsByOrganizationID = `-- name: GetGroupsByOrganizationID :many
SELECT
id, name, organization_id
FROM
groups
WHERE
organization_id = $1
AND
id != $1
`
func (q *sqlQuerier) GetGroupsByOrganizationID(ctx context.Context, organizationID uuid.UUID) ([]Group, error) {
rows, err := q.db.QueryContext(ctx, getGroupsByOrganizationID, organizationID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Group
for rows.Next() {
var i Group
if err := rows.Scan(&i.ID, &i.Name, &i.OrganizationID); 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 getUserGroups = `-- name: GetUserGroups :many
SELECT
groups.id, groups.name, groups.organization_id
FROM
groups
JOIN
group_members
ON
groups.id = group_members.group_id
WHERE
group_members.user_id = $1
`
func (q *sqlQuerier) GetUserGroups(ctx context.Context, userID uuid.UUID) ([]Group, error) {
rows, err := q.db.QueryContext(ctx, getUserGroups, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Group
for rows.Next() {
var i Group
if err := rows.Scan(&i.ID, &i.Name, &i.OrganizationID); 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 insertAllUsersGroup = `-- name: InsertAllUsersGroup :one
INSERT INTO groups (
id,
name,
organization_id
)
VALUES
( $1, 'Everyone', $1) RETURNING id, name, organization_id
`
// We use the organization_id as the id
// for simplicity since all users is
// every member of the org.
func (q *sqlQuerier) InsertAllUsersGroup(ctx context.Context, organizationID uuid.UUID) (Group, error) {
row := q.db.QueryRowContext(ctx, insertAllUsersGroup, organizationID)
var i Group
err := row.Scan(&i.ID, &i.Name, &i.OrganizationID)
return i, err
}
const insertGroup = `-- name: InsertGroup :one
INSERT INTO groups (
id,
name,
organization_id
)
VALUES
( $1, $2, $3) RETURNING id, name, organization_id
`
type InsertGroupParams struct {
ID uuid.UUID `db:"id" json:"id"`
Name string `db:"name" json:"name"`
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
}
func (q *sqlQuerier) InsertGroup(ctx context.Context, arg InsertGroupParams) (Group, error) {
row := q.db.QueryRowContext(ctx, insertGroup, arg.ID, arg.Name, arg.OrganizationID)
var i Group
err := row.Scan(&i.ID, &i.Name, &i.OrganizationID)
return i, err
}
const insertGroupMember = `-- name: InsertGroupMember :exec
INSERT INTO group_members (
user_id,
group_id
)
VALUES ( $1, $2)
`
type InsertGroupMemberParams struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
GroupID uuid.UUID `db:"group_id" json:"group_id"`
}
func (q *sqlQuerier) InsertGroupMember(ctx context.Context, arg InsertGroupMemberParams) error {
_, err := q.db.ExecContext(ctx, insertGroupMember, arg.UserID, arg.GroupID)
return err
}
const updateGroupByID = `-- name: UpdateGroupByID :one
UPDATE
groups
SET
name = $1
WHERE
id = $2
RETURNING id, name, organization_id
`
type UpdateGroupByIDParams struct {
Name string `db:"name" json:"name"`
ID uuid.UUID `db:"id" json:"id"`
}
func (q *sqlQuerier) UpdateGroupByID(ctx context.Context, arg UpdateGroupByIDParams) (Group, error) {
row := q.db.QueryRowContext(ctx, updateGroupByID, arg.Name, arg.ID)
var i Group
err := row.Scan(&i.ID, &i.Name, &i.OrganizationID)
return i, err
}
const deleteLicense = `-- name: DeleteLicense :one
DELETE
FROM licenses
@ -2231,7 +2553,7 @@ func (q *sqlQuerier) InsertDeploymentID(ctx context.Context, value string) error
const getTemplateByID = `-- name: GetTemplateByID :one
SELECT
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by, icon
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by, icon, user_acl, group_acl
FROM
templates
WHERE
@ -2257,13 +2579,15 @@ func (q *sqlQuerier) GetTemplateByID(ctx context.Context, id uuid.UUID) (Templat
&i.MinAutostartInterval,
&i.CreatedBy,
&i.Icon,
&i.userACL,
&i.groupACL,
)
return i, err
}
const getTemplateByOrganizationAndName = `-- name: GetTemplateByOrganizationAndName :one
SELECT
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by, icon
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by, icon, user_acl, group_acl
FROM
templates
WHERE
@ -2297,12 +2621,14 @@ func (q *sqlQuerier) GetTemplateByOrganizationAndName(ctx context.Context, arg G
&i.MinAutostartInterval,
&i.CreatedBy,
&i.Icon,
&i.userACL,
&i.groupACL,
)
return i, err
}
const getTemplates = `-- name: GetTemplates :many
SELECT id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by, icon FROM templates
SELECT id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by, icon, user_acl, group_acl FROM templates
ORDER BY (name, id) ASC
`
@ -2329,6 +2655,8 @@ func (q *sqlQuerier) GetTemplates(ctx context.Context) ([]Template, error) {
&i.MinAutostartInterval,
&i.CreatedBy,
&i.Icon,
&i.userACL,
&i.groupACL,
); err != nil {
return nil, err
}
@ -2345,7 +2673,7 @@ func (q *sqlQuerier) GetTemplates(ctx context.Context) ([]Template, error) {
const getTemplatesWithFilter = `-- name: GetTemplatesWithFilter :many
SELECT
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by, icon
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by, icon, user_acl, group_acl
FROM
templates
WHERE
@ -2407,6 +2735,8 @@ func (q *sqlQuerier) GetTemplatesWithFilter(ctx context.Context, arg GetTemplate
&i.MinAutostartInterval,
&i.CreatedBy,
&i.Icon,
&i.userACL,
&i.groupACL,
); err != nil {
return nil, err
}
@ -2438,7 +2768,7 @@ INSERT INTO
icon
)
VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by, icon
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by, icon, user_acl, group_acl
`
type InsertTemplateParams struct {
@ -2486,6 +2816,8 @@ func (q *sqlQuerier) InsertTemplate(ctx context.Context, arg InsertTemplateParam
&i.MinAutostartInterval,
&i.CreatedBy,
&i.Icon,
&i.userACL,
&i.groupACL,
)
return i, err
}
@ -2545,7 +2877,7 @@ SET
WHERE
id = $1
RETURNING
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by, icon
id, created_at, updated_at, organization_id, deleted, name, provisioner, active_version_id, description, max_ttl, min_autostart_interval, created_by, icon, user_acl, group_acl
`
type UpdateTemplateMetaByIDParams struct {
@ -2583,6 +2915,8 @@ func (q *sqlQuerier) UpdateTemplateMetaByID(ctx context.Context, arg UpdateTempl
&i.MinAutostartInterval,
&i.CreatedBy,
&i.Icon,
&i.userACL,
&i.groupACL,
)
return i, err
}
@ -3071,16 +3405,36 @@ SELECT
-- status is used to enforce 'suspended' users, as all roles are ignored
-- when suspended.
id, username, status,
-- All user roles, including their org roles.
array_cat(
-- All users are members
array_append(users.rbac_roles, 'member'),
-- All org_members get the org-member role for their orgs
array_append(organization_members.roles, 'organization-member:'||organization_members.organization_id::text)) :: text[]
AS roles
array_append(users.rbac_roles, 'member'),
(
SELECT
array_agg(org_roles)
FROM
organization_members,
-- All org_members get the org-member role for their orgs
unnest(
array_append(roles, 'organization-member:' || organization_members.organization_id::text)
) AS org_roles
WHERE
user_id = users.id
)
) :: text[] AS roles,
-- All groups the user is in.
(
SELECT
array_agg(
group_members.group_id :: text
)
FROM
group_members
WHERE
user_id = users.id
) :: text[] AS groups
FROM
users
LEFT JOIN organization_members
ON id = user_id
WHERE
id = $1
`
@ -3090,6 +3444,7 @@ type GetAuthorizationUserRolesRow struct {
Username string `db:"username" json:"username"`
Status UserStatus `db:"status" json:"status"`
Roles []string `db:"roles" json:"roles"`
Groups []string `db:"groups" json:"groups"`
}
// This function returns roles for authorization purposes. Implied member roles
@ -3102,6 +3457,7 @@ func (q *sqlQuerier) GetAuthorizationUserRoles(ctx context.Context, userID uuid.
&i.Username,
&i.Status,
pq.Array(&i.Roles),
pq.Array(&i.Groups),
)
return i, err
}
@ -3135,7 +3491,7 @@ func (q *sqlQuerier) GetUserByEmailOrUsername(ctx context.Context, arg GetUserBy
&i.CreatedAt,
&i.UpdatedAt,
&i.Status,
pq.Array(&i.RBACRoles),
&i.RBACRoles,
&i.LoginType,
&i.AvatarURL,
&i.Deleted,
@ -3166,7 +3522,7 @@ func (q *sqlQuerier) GetUserByID(ctx context.Context, id uuid.UUID) (User, error
&i.CreatedAt,
&i.UpdatedAt,
&i.Status,
pq.Array(&i.RBACRoles),
&i.RBACRoles,
&i.LoginType,
&i.AvatarURL,
&i.Deleted,
@ -3285,7 +3641,7 @@ func (q *sqlQuerier) GetUsers(ctx context.Context, arg GetUsersParams) ([]User,
&i.CreatedAt,
&i.UpdatedAt,
&i.Status,
pq.Array(&i.RBACRoles),
&i.RBACRoles,
&i.LoginType,
&i.AvatarURL,
&i.Deleted,
@ -3328,7 +3684,7 @@ func (q *sqlQuerier) GetUsersByIDs(ctx context.Context, ids []uuid.UUID) ([]User
&i.CreatedAt,
&i.UpdatedAt,
&i.Status,
pq.Array(&i.RBACRoles),
&i.RBACRoles,
&i.LoginType,
&i.AvatarURL,
&i.Deleted,
@ -3364,14 +3720,14 @@ VALUES
`
type InsertUserParams struct {
ID uuid.UUID `db:"id" json:"id"`
Email string `db:"email" json:"email"`
Username string `db:"username" json:"username"`
HashedPassword []byte `db:"hashed_password" json:"hashed_password"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
RBACRoles []string `db:"rbac_roles" json:"rbac_roles"`
LoginType LoginType `db:"login_type" json:"login_type"`
ID uuid.UUID `db:"id" json:"id"`
Email string `db:"email" json:"email"`
Username string `db:"username" json:"username"`
HashedPassword []byte `db:"hashed_password" json:"hashed_password"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
RBACRoles pq.StringArray `db:"rbac_roles" json:"rbac_roles"`
LoginType LoginType `db:"login_type" json:"login_type"`
}
func (q *sqlQuerier) InsertUser(ctx context.Context, arg InsertUserParams) (User, error) {
@ -3382,7 +3738,7 @@ func (q *sqlQuerier) InsertUser(ctx context.Context, arg InsertUserParams) (User
arg.HashedPassword,
arg.CreatedAt,
arg.UpdatedAt,
pq.Array(arg.RBACRoles),
arg.RBACRoles,
arg.LoginType,
)
var i User
@ -3394,7 +3750,7 @@ func (q *sqlQuerier) InsertUser(ctx context.Context, arg InsertUserParams) (User
&i.CreatedAt,
&i.UpdatedAt,
&i.Status,
pq.Array(&i.RBACRoles),
&i.RBACRoles,
&i.LoginType,
&i.AvatarURL,
&i.Deleted,
@ -3468,7 +3824,7 @@ func (q *sqlQuerier) UpdateUserLastSeenAt(ctx context.Context, arg UpdateUserLas
&i.CreatedAt,
&i.UpdatedAt,
&i.Status,
pq.Array(&i.RBACRoles),
&i.RBACRoles,
&i.LoginType,
&i.AvatarURL,
&i.Deleted,
@ -3514,7 +3870,7 @@ func (q *sqlQuerier) UpdateUserProfile(ctx context.Context, arg UpdateUserProfil
&i.CreatedAt,
&i.UpdatedAt,
&i.Status,
pq.Array(&i.RBACRoles),
&i.RBACRoles,
&i.LoginType,
&i.AvatarURL,
&i.Deleted,
@ -3550,7 +3906,7 @@ func (q *sqlQuerier) UpdateUserRoles(ctx context.Context, arg UpdateUserRolesPar
&i.CreatedAt,
&i.UpdatedAt,
&i.Status,
pq.Array(&i.RBACRoles),
&i.RBACRoles,
&i.LoginType,
&i.AvatarURL,
&i.Deleted,
@ -3586,7 +3942,7 @@ func (q *sqlQuerier) UpdateUserStatus(ctx context.Context, arg UpdateUserStatusP
&i.CreatedAt,
&i.UpdatedAt,
&i.Status,
pq.Array(&i.RBACRoles),
&i.RBACRoles,
&i.LoginType,
&i.AvatarURL,
&i.Deleted,

View File

@ -0,0 +1,122 @@
-- name: GetGroupByID :one
SELECT
*
FROM
groups
WHERE
id = $1
LIMIT
1;
-- name: GetGroupByOrgAndName :one
SELECT
*
FROM
groups
WHERE
organization_id = $1
AND
name = $2
LIMIT
1;
-- name: GetUserGroups :many
SELECT
groups.*
FROM
groups
JOIN
group_members
ON
groups.id = group_members.group_id
WHERE
group_members.user_id = $1;
-- name: GetGroupMembers :many
SELECT
users.*
FROM
users
JOIN
group_members
ON
users.id = group_members.user_id
WHERE
group_members.group_id = $1
AND
users.status = 'active'
AND
users.deleted = 'false';
-- name: GetAllOrganizationMembers :many
SELECT
users.*
FROM
users
JOIN
organization_members
ON
users.id = organization_members.user_id
WHERE
organization_members.organization_id = $1;
-- name: GetGroupsByOrganizationID :many
SELECT
*
FROM
groups
WHERE
organization_id = $1
AND
id != $1;
-- name: InsertGroup :one
INSERT INTO groups (
id,
name,
organization_id
)
VALUES
( $1, $2, $3) RETURNING *;
-- We use the organization_id as the id
-- for simplicity since all users is
-- every member of the org.
-- name: InsertAllUsersGroup :one
INSERT INTO groups (
id,
name,
organization_id
)
VALUES
( sqlc.arg(organization_id), 'Everyone', sqlc.arg(organization_id)) RETURNING *;
-- name: UpdateGroupByID :one
UPDATE
groups
SET
name = $1
WHERE
id = $2
RETURNING *;
-- name: InsertGroupMember :exec
INSERT INTO group_members (
user_id,
group_id
)
VALUES ( $1, $2);
-- name: DeleteGroupMember :exec
DELETE FROM
group_members
WHERE
user_id = $1;
-- name: DeleteGroupByID :exec
DELETE FROM
groups
WHERE
id = $1;

View File

@ -178,15 +178,35 @@ SELECT
-- status is used to enforce 'suspended' users, as all roles are ignored
-- when suspended.
id, username, status,
-- All user roles, including their org roles.
array_cat(
-- All users are members
array_append(users.rbac_roles, 'member'),
-- All org_members get the org-member role for their orgs
array_append(organization_members.roles, 'organization-member:'||organization_members.organization_id::text)) :: text[]
AS roles
array_append(users.rbac_roles, 'member'),
(
SELECT
array_agg(org_roles)
FROM
organization_members,
-- All org_members get the org-member role for their orgs
unnest(
array_append(roles, 'organization-member:' || organization_members.organization_id::text)
) AS org_roles
WHERE
user_id = users.id
)
) :: text[] AS roles,
-- All groups the user is in.
(
SELECT
array_agg(
group_members.group_id :: text
)
FROM
group_members
WHERE
user_id = users.id
) :: text[] AS groups
FROM
users
LEFT JOIN organization_members
ON id = user_id
WHERE
id = @user_id;

View File

@ -16,6 +16,10 @@ packages:
# deleted after generation.
output_db_file_name: db_tmp.go
overrides:
- column: "users.rbac_roles"
go_type: "github.com/lib/pq.StringArray"
rename:
api_key: APIKey
api_key_scope: APIKeyScope
@ -35,3 +39,5 @@ rename:
ip_addresses: IPAddresses
ids: IDs
jwt: JWT
user_acl: userACL
group_acl: groupACL

View File

@ -6,6 +6,8 @@ type UniqueConstraint string
// UniqueConstraint enums.
const (
UniqueGroupMembersUserIDGroupIDKey UniqueConstraint = "group_members_user_id_group_id_key" // ALTER TABLE ONLY group_members ADD CONSTRAINT group_members_user_id_group_id_key UNIQUE (user_id, group_id);
UniqueGroupsNameOrganizationIDKey UniqueConstraint = "groups_name_organization_id_key" // ALTER TABLE ONLY groups ADD CONSTRAINT groups_name_organization_id_key UNIQUE (name, organization_id);
UniqueLicensesJWTKey UniqueConstraint = "licenses_jwt_key" // ALTER TABLE ONLY licenses ADD CONSTRAINT licenses_jwt_key UNIQUE (jwt);
UniqueParameterSchemasJobIDNameKey UniqueConstraint = "parameter_schemas_job_id_name_key" // ALTER TABLE ONLY parameter_schemas ADD CONSTRAINT parameter_schemas_job_id_name_key UNIQUE (job_id, name);
UniqueParameterValuesScopeIDNameKey UniqueConstraint = "parameter_values_scope_id_name_key" // ALTER TABLE ONLY parameter_values ADD CONSTRAINT parameter_values_scope_id_name_key UNIQUE (scope_id, name);