Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
f45b594
testing db on demand
cowsed Jul 30, 2026
4b57155
graphql for groups init
cowsed Aug 10, 2026
bee6014
Merge branch 'cowsed/group_testing' into cowsed/groups_graphql
cowsed Aug 10, 2026
96558de
more graphql and db tests
cowsed Aug 10, 2026
e95c5ea
finished round one of resolvers
cowsed Aug 10, 2026
d0c264d
changes to group repo interface for resolvers
cowsed Aug 10, 2026
6c5ecff
moved test stuff around
cowsed Aug 10, 2026
ea5c686
more resolver testing
cowsed Aug 10, 2026
4b0675d
yet more group resolver testing
cowsed Aug 10, 2026
ef61311
actually doing graphql things
cowsed Aug 10, 2026
040da44
Merge branch 'main' into cowsed/groups_graphql
cowsed Aug 18, 2026
01ac06b
Update group resolver tests
cowsed Aug 18, 2026
f12b99b
Tests both unit and using DB
cowsed Aug 18, 2026
09689d2
start adding test makerspace
cowsed Aug 18, 2026
89cc49d
Checks to prevent infinite cycles, group schema tidbits, extracting c…
cowsed Aug 18, 2026
aa920ad
a groups are real?!?!?!?
cowsed Aug 18, 2026
4694195
integrating anonymous groups
cowsed Aug 19, 2026
adb2389
more agroup gqls and tests
cowsed Aug 19, 2026
30d9797
more testing of agroups and their queries and mutations as well as ch…
cowsed Aug 19, 2026
df859a8
More protections for graphql endpoints
cowsed Aug 19, 2026
cc90a60
Stricter permissions and enforcing that only root group can hve null …
cowsed Aug 19, 2026
68f71b9
allow gqlgen to format
cowsed Aug 19, 2026
36190cd
AuditLogs for new resolvers
cowsed Aug 19, 2026
b469cda
makerspace directives for permission checks
cowsed Aug 20, 2026
cb53ea1
honing in on actual finctionality
cowsed Aug 21, 2026
1d5a9e5
Users can see their own groups
cowsed Sep 1, 2026
eda2524
check id field when checking user groups
cowsed Sep 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/make-backend/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ func startHttp(db *sql.DB, store *database.Store, logger *logging.Logger, port i

mux := http.NewServeMux()

protectedQueryHandler := sessionManager.LoadAndSave(auth.RequiredAuthMiddleware(srv, sessionManager))
protectedQueryHandler := sessionManager.LoadAndSave(auth.OptionalAuthMiddleware(srv, sessionManager))

mux.Handle("/playground", playground.Handler("GraphQL playground", "/query"))
mux.Handle("/query", protectedQueryHandler)
Expand Down
54 changes: 53 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ require (
github.com/joho/godotenv v1.5.1
github.com/lib/pq v1.12.3
github.com/pressly/goose/v3 v3.27.2
github.com/testcontainers/testcontainers-go v0.43.0
github.com/testcontainers/testcontainers-go/modules/postgres v0.43.0
github.com/vektah/gqlparser/v2 v2.5.35
google.golang.org/grpc v1.80.0
google.golang.org/protobuf v1.36.11
Expand All @@ -26,6 +28,56 @@ require (
google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529 // indirect
)

require (
dario.cat/mergo v1.0.2 // indirect
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/cpuguy83/dockercfg v0.3.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/go-connections v0.7.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/ebitengine/purego v0.10.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/klauspost/compress v1.18.5 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/magiconair/properties v1.8.10 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/go-archive v0.2.0 // indirect
github.com/moby/moby/api v1.55.0 // indirect
github.com/moby/moby/client v0.5.0 // indirect
github.com/moby/patternmatcher v0.6.1 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
github.com/moby/sys/user v0.4.0 // indirect
github.com/moby/sys/userns v0.1.0 // indirect
github.com/moby/term v0.5.2 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/shirou/gopsutil/v4 v4.26.5 // indirect
github.com/sirupsen/logrus v1.9.4 // indirect
github.com/stretchr/testify v1.11.1 // indirect
github.com/tklauser/go-sysconf v0.3.16 // indirect
github.com/tklauser/numcpus v0.11.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect
go.opentelemetry.io/otel v1.43.0 // indirect
go.opentelemetry.io/otel/metric v1.43.0 // indirect
go.opentelemetry.io/otel/trace v1.43.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

require (
github.com/agnivade/levenshtein v1.2.1 // indirect
github.com/alexedwards/scs/postgresstore v0.0.0-20251002162104-209de6e426de
Expand All @@ -52,7 +104,7 @@ require (
golang.org/x/crypto v0.53.0
golang.org/x/mod v0.37.0 // indirect
golang.org/x/sync v0.21.0 // indirect
golang.org/x/text v0.38.0 // indirect
golang.org/x/text v0.38.0
golang.org/x/tools v0.46.0 // indirect
)

Expand Down
118 changes: 114 additions & 4 deletions go.sum

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions internal/auth/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,26 @@ package auth

import (
"context"
"errors"
"net/http"

"github.com/alexedwards/scs/v2"
)

type UserContextKey struct{}

var ErrNotAuthenticated error = errors.New("not authenticated")

func UserIDFromContext(ctx context.Context) *int {
userVal := ctx.Value(UserContextKey{})
if userVal == nil {
return nil
}
userID := userVal.(int)
return &userID

}

func OptionalAuthMiddleware(next http.Handler, sessionManager *scs.SessionManager) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

Expand Down
105 changes: 105 additions & 0 deletions internal/database/group_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package database

import (
"log/slog"
"testing"
)

func TestUserGroupVisibilityGood(t *testing.T) {
if testing.Short() {
t.Skip("Skipping db test in short mode.")
}
_, store, data := VerifyTestDb(slog.Default())
user := data.Users[0]
group := data.BeatlesMusicians

// brian can see musicians
visible, err := store.Groups.IsGroupVisibleToUser(t.Context(), user.Id, group.Id)
if err != nil {
t.Fatalf("Failed to check group visibility of %+v to see %+v: %v", user, group, err)
}
if visible != true {
t.Fatalf("%v should be able to see %v but couldn't", user, group)
}
}
func TestUserGroupVisibilityBad(t *testing.T) {
if testing.Short() {
t.Skip("Skipping db test in short mode.")
}
_, store, data := VerifyTestDb(slog.Default())
user := data.Users[1]
group := data.BeatlesManagers

// musicians can't see up (unless explicitly allowed to)
visible, err := store.Groups.IsGroupVisibleToUser(t.Context(), user.Id, group.Id)
if err != nil {
t.Fatalf("Failed to check group visibility of %+v to see %+v: %v", user, group, err)
}
if visible != false {
t.Fatalf("%v should NOT be able to see %v but could", user, group)
}
}

func TestAddedUsers(t *testing.T) {
if testing.Short() {
t.Skip("Skipping db test in short mode.")
}
_, store, _ := VerifyTestDb(slog.Default())
for _, user := range localContext.Users {
user, err := store.Users.GetUserByEmail(t.Context(), user.Email)
if err != nil {
t.Fatalf("Failed to find user with email '%s': %v", user.Email, err)
}
}
}

func TestGroupMembership(t *testing.T) {
if testing.Short() {
t.Skip("Skipping db test in short mode.")
}
_, store, _ := VerifyTestDb(slog.Default())
for _, email := range []string{
"john@beatles.com",
"paul@beatles.com",
"george@beatles.com",
"ringo@beatles.com",
} {
user, err := store.Users.GetUserByEmail(t.Context(), email)
if err != nil {
t.Fatalf("couldn't find user with email '%s': %v", email, err)
}
inGroup, _, err := store.Groups.IsUserInGroup(t.Context(), user.Id, localContext.BeatlesMusicians.Id)
if err != nil {
t.Fatalf("couldn't check user in group for email '%s': %v", email, err)
}
if !inGroup {
t.Fatalf("user with email '%s' should be in group '%s' but wasn't", email, localContext.BeatlesMusicians.Name)
}
}
}

func TestSubgroupMembership(t *testing.T) {
if testing.Short() {
t.Skip("Skipping db test in short mode.")
}
_, store, _ := VerifyTestDb(slog.Default())
for _, email := range []string{
"john@beatles.com",
"paul@beatles.com",
"george@beatles.com",
"ringo@beatles.com",
"brian@beatles.com",
} {
user, err := store.Users.GetUserByEmail(t.Context(), email)
if err != nil {
t.Fatalf("couldn't find user with email '%s': %v", email, err)
}
inGroup, _, err := store.Groups.IsUserInGroup(t.Context(), user.Id, localContext.Brits.Id)
if err != nil {
t.Fatalf("couldn't check user in group for email '%s': %v", email, err)
}
if !inGroup {
t.Fatalf("user with email '%s' should be in group '%s' via subgroup but wasn't", email, localContext.BeatlesMusicians.Name)
}
}
}
64 changes: 35 additions & 29 deletions internal/database/migrations/20260703201718_initial_schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,7 @@ CREATE TABLE users (
setup_complete BOOLEAN NOT NULL DEFAULT FALSE,
archived BOOLEAN NOT NULL DEFAULT FALSE,
notes TEXT NOT NULL DEFAULT '',
admin BOOLEAN NOT NULL DEFAULT FALSE,
force_archive BOOLEAN,
card_tag TEXT NOT NULL DEFAULT ''
force_archive BOOLEAN
);

CREATE TABLE holds (
Expand All @@ -44,15 +42,17 @@ CREATE TABLE images (

CREATE TABLE groups (
id SERIAL PRIMARY KEY,
name TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
manager_id INT REFERENCES groups(id),
description TEXT NOT NULL DEFAULT '',
UNIQUE (name, manager_id)
);
INSERT INTO groups (name, description)
VALUES ('admin', 'The root of all groups');

INSERT INTO groups (id, name, description)
VALUES (0, 'admin', 'The root of all groups');

alter table groups
add constraint nonnull_manager_except_for_root
check (id = 0 or manager_id is not null);

CREATE TABLE group_direct_membership(
group_id INT NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
Expand All @@ -62,8 +62,8 @@ CREATE TABLE group_direct_membership(
);

CREATE TABLE group_direct_subgroups (
group_id INT REFERENCES groups(id),
subgroup_id INT REFERENCES groups(id),
group_id INT REFERENCES groups(id) ON DELETE CASCADE,
subgroup_id INT REFERENCES groups(id) ON DELETE CASCADE,
view_permission INT NOT NULL DEFAULT 0 CHECK (view_permission in (0, 1, 2)),
PRIMARY KEY (group_id, subgroup_id)
);
Expand Down Expand Up @@ -142,14 +142,18 @@ create view group_management as (
group by manager_group_id , group_id
);


create view group_membership as (
select group_id, user_id, gdm.view_permission from group_direct_membership gdm
union
select distinct gs.supergroup_id, gdm.user_id, gs.view_permission
from group_subgroups gs
left join group_direct_membership gdm
on gdm.group_id = gs.subgroup_id
where user_id is not null
select group_id, user_id, MAX(view_permission) as view_permission from (
select group_id, user_id, gdm.view_permission as view_permission from group_direct_membership gdm
union
select distinct gs.supergroup_id, gdm.user_id, gs.view_permission
from group_subgroups gs
left join group_direct_membership gdm
on gdm.group_id = gs.subgroup_id
where user_id is not null
) as subquery
group by group_id, user_id
);


Expand All @@ -164,6 +168,13 @@ CREATE TABLE anonymous_group_subgroups(
);


create view anonymous_group_membership as (
select distinct ags.anonymous_id as agroup_id , gm.user_id as user_id
from anonymous_group_subgroups ags
left join group_membership gm
on gm.group_id = ags.group_id
);


CREATE TABLE makerspaces (
id SERIAL PRIMARY KEY,
Expand All @@ -177,8 +188,8 @@ CREATE TABLE makerspaces (
timezone TEXT NOT NULL DEFAULT 'America/New_York',
-- agroup of users who can manage this space
management_agroup_id INT NOT NULL REFERENCES anonymous_groups(id) ON DELETE CASCADE,
-- agroup of users who can site-set equipment state
can_change_equipment_state_agroup_id INT NOT NULL REFERENCES anonymous_groups(id) ON DELETE CASCADE
-- agroup of users who can site-set equipment state and other such actions
staff_agroup_id INT NOT NULL REFERENCES anonymous_groups(id) ON DELETE CASCADE
);

CREATE TABLE restrictions (
Expand Down Expand Up @@ -222,17 +233,6 @@ CREATE TABLE announcements (
makerspace_id INT REFERENCES makerspaces(id) ON DELETE CASCADE
);

CREATE TABLE managers (
user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
makerspace_id INT NOT NULL REFERENCES makerspaces(id) ON DELETE CASCADE,
PRIMARY KEY (user_id, makerspace_id)
);

CREATE TABLE staff (
user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
makerspace_id INT NOT NULL REFERENCES makerspaces(id) ON DELETE CASCADE,
PRIMARY KEY (user_id, makerspace_id)
);

CREATE TABLE equipment (
id SERIAL PRIMARY KEY,
Expand Down Expand Up @@ -395,6 +395,12 @@ DROP TABLE IF EXISTS groups;
DROP TABLE IF EXISTS group_direct_membership;
DROP TABLE IF EXISTS group_direct_subgroups;

DROP VIEW IF EXISTS group_noncombination_management;
DROP VIEW IF EXISTS group_subgroups;
DROP VIEW IF EXISTS group_management;
DROP VIEW IF EXISTS group_membership;
DROP VIEW IF EXISTS anonymous_group_membership;


DROP TABLE IF EXISTS anonymous_groups;
DROP TABLE IF EXISTS anonymous_group_subgroups;
Expand Down
Loading
Loading