package store import ( "context" "fmt" "strings" "atlas9.dev/c/core" "atlas9.dev/c/core/dbi" "atlas9.dev/c/core/iam" "atlas9.dev/c/demo/lib" ) type SqliteGroupStore struct { db dbi.DBI guard lib.Guard } var _ iam.GroupStore = (*SqliteGroupStore)(nil) func NewSqliteGroupStore(db dbi.DBI, guard lib.Guard) *SqliteGroupStore { return &SqliteGroupStore{db: db, guard: guard} } func (s *SqliteGroupStore) Save(ctx context.Context, g *iam.Group) error { if err := s.guard.Check(ctx, iam.CapGroupsSave, g.Tenant, g.Path); err != nil { return err } _, err := s.db.Exec(ctx, ` INSERT INTO groups (tenant, path, name) VALUES ($1, $2, $3) ON CONFLICT (tenant, path) DO UPDATE SET name = $3 `, g.Tenant, g.Path, g.Name) return err } func (s *SqliteGroupStore) Get(ctx context.Context, tenant core.ID, path core.Path, out *iam.Group) error { if err := s.guard.Check(ctx, iam.CapGroupsGet, tenant, path); err != nil { return err } err := dbi.Get(ctx, s.db, out, ` SELECT tenant, path, name FROM groups WHERE tenant = $1 AND path = $2 `, tenant, path) return err } func (s *SqliteGroupStore) Delete(ctx context.Context, tenant core.ID, path core.Path) error { if err := s.guard.Check(ctx, iam.CapGroupsDelete, tenant, path); err != nil { return err } _, err := s.db.Exec(ctx, `DELETE FROM groups WHERE tenant = $1 AND path = $2`, tenant, path) return err } func (s *SqliteGroupStore) List(ctx context.Context, tenant core.ID, page core.PageReq, out *core.Page[iam.Group]) error { if err := s.guard.Check(ctx, iam.CapGroupsList, tenant, ""); err != nil { return err } return dbi.Paginate(ctx, s.db, page, out, func(g iam.Group) string { return string(g.Path) }, `SELECT tenant, path, name FROM groups WHERE tenant = $1 AND path > $cursor ORDER BY path LIMIT $limit`, tenant) } func (s *SqliteGroupStore) AddMember(ctx context.Context, tenant core.ID, group core.Path, userID core.ID) error { if err := s.guard.Check(ctx, iam.CapGroupsAddMember, tenant, group); err != nil { return err } _, err := s.db.Exec(ctx, ` INSERT INTO group_members (tenant, group_path, user_id) VALUES ($1, $2, $3) ON CONFLICT (tenant, group_path, user_id) DO NOTHING `, tenant, group, userID) if err != nil && strings.Contains(err.Error(), "FOREIGN KEY constraint failed") { return fmt.Errorf("user is not a member of this tenant") } return err } func (s *SqliteGroupStore) RemoveMember(ctx context.Context, tenant core.ID, group core.Path, userID core.ID) error { if err := s.guard.Check(ctx, iam.CapGroupsRemoveMember, tenant, group); err != nil { return err } _, err := s.db.Exec(ctx, ` DELETE FROM group_members WHERE tenant = $1 AND group_path = $2 AND user_id = $3 `, tenant, group, userID) return err } func (s *SqliteGroupStore) ListMembers(ctx context.Context, tenant core.ID, group core.Path, page core.PageReq, out *core.Page[core.ID]) error { if err := s.guard.Check(ctx, iam.CapGroupsListMembers, tenant, group); err != nil { return err } return dbi.Paginate(ctx, s.db, page, out, func(id core.ID) string { return id.String() }, `SELECT user_id FROM group_members WHERE tenant = $1 AND group_path = $2 AND user_id > $cursor ORDER BY user_id LIMIT $limit`, tenant, group) } // TODO similar to tenant members, you can list your own groups func (s *SqliteGroupStore) ListMemberGroups(ctx context.Context, userID core.ID, page core.PageReq, out *core.Page[iam.Group]) error { if err := s.guard.System(ctx, iam.CapGroupsListMemberGroups); err != nil { return err } limit := page.Limit if limit <= 0 { limit = 100 } rows, err := s.db.Query(ctx, ` SELECT g.tenant, g.path, g.name FROM groups g JOIN group_members gm ON gm.tenant = g.tenant AND gm.group_path = g.path WHERE gm.user_id = $1 AND g.path > $2 ORDER BY g.path LIMIT $3 `, userID, page.Cursor, limit) if err != nil { return err } defer rows.Close() for rows.Next() { var g iam.Group if err := rows.Scan(&g.Tenant, &g.Path, &g.Name); err != nil { return err } out.Items = append(out.Items, g) } if err := rows.Err(); err != nil { return err } if len(out.Items) == limit { out.Cursor = string(out.Items[limit-1].Path) } return nil }