-
Notifications
You must be signed in to change notification settings - Fork 351
/
expander.go
177 lines (154 loc) · 4.32 KB
/
expander.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
package templater
import (
"github.com/treeverse/lakefs/pkg/auth"
auth_model "github.com/treeverse/lakefs/pkg/auth/model"
"github.com/treeverse/lakefs/pkg/config"
"context"
"errors"
"fmt"
"io"
"io/fs"
"net/http"
"path"
"strings"
"text/template"
)
const (
// Prefix inside templates FS to access actual contents.
embeddedContentPrefix = "content"
)
var (
ErrNotFound = errors.New("template not found")
ErrPathTraversalBlocked = errors.New("path traversal blocked")
)
type AuthService interface {
auth.Authorizer
auth.CredentialsCreator
}
type Phase string
const (
PhasePrepare Phase = "prepare"
PhaseExpand Phase = "expand"
)
type ControlledParams struct {
Ctx context.Context
Phase Phase
Auth AuthService
// Headers are the HTTP response headers. They may only be modified
// during PhasePrepare.
Header http.Header
// User is the user expanding the template.
User *auth_model.User
// Store is a place for funcs to keep stuff around, mostly between
// phases. Each func should use its name as its index.
Store map[string]interface{}
}
type UncontrolledData struct {
// UserName is the name of the executing user.
Username string
// Query is the (parsed) querystring of the HTTP access.
Query map[string]string
}
// Params parametrizes a single template expansion.
type Params struct {
// Controlled is the data visible to functions to control expansion.
// It is _not_ directly visible to templates for expansion.
Controlled *ControlledParams
// Data is directly visible to templates for expansion, with no
// authorization required.
Data *UncontrolledData
}
// Expander is a template that may be expanded as requested by users.
type Expander interface {
// Expand serves the template into w using the parameters specified
// in params. If during expansion a template function fails,
// returns an error without writing anything to w. (However if
// expansion fails for other reasons, Expand may write to w!)
Expand(w io.Writer, params *Params) error
}
type expander struct {
template *template.Template
cfg *config.Config
auth AuthService
}
// MakeExpander creates an expander for the text of tmpl.
func MakeExpander(name, tmpl string, cfg *config.Config, auth AuthService) (Expander, error) {
t := template.New(name).Funcs(templateFuncs).Option("missingkey=error")
t, err := t.Parse(tmpl)
if err != nil {
return nil, err
}
return &expander{
template: t,
cfg: cfg,
auth: auth,
}, nil
}
func (e *expander) Expand(w io.Writer, params *Params) error {
// Expand with no output: verify that no template functions will
// fail.
params.Controlled.Phase = PhasePrepare
if err := e.expandTo(io.Discard, params); err != nil {
return fmt.Errorf("prepare: %w", err)
}
params.Controlled.Phase = PhaseExpand
if err := e.expandTo(w, params); err != nil {
return fmt.Errorf("execute: %w", err)
}
return nil
}
func (e *expander) expandTo(w io.Writer, params *Params) error {
clone, err := e.template.Clone()
if err != nil {
return err
}
wrappedFuncs := WrapFuncMapWithData(templateFuncs, params.Controlled)
clone.Funcs(wrappedFuncs)
return clone.Execute(w, params.Data)
}
// ExpanderMap reads and caches Expanders from a fs.FS. Currently, it
// provides no uncaching as it is only used with a prebuilt FS.
type ExpanderMap struct {
fs fs.FS
cfg *config.Config
auth AuthService
expanders map[string]Expander
}
func NewExpanderMap(fs fs.FS, cfg *config.Config, auth AuthService) *ExpanderMap {
return &ExpanderMap{
fs: fs,
cfg: cfg,
auth: auth,
expanders: make(map[string]Expander, 0),
}
}
func (em *ExpanderMap) Get(ctx context.Context, username, name string) (Expander, error) {
if e, ok := em.expanders[name]; ok {
// Fast-path through the cache
if e == nil {
// Negative cache
return nil, ErrNotFound
}
return e, nil
}
// Compute path
p := path.Join(embeddedContentPrefix, name)
if !strings.HasPrefix(p, embeddedContentPrefix+"/") {
// Path traversal, fail
return nil, fmt.Errorf("%s: %w", name, ErrPathTraversalBlocked)
}
tmpl, err := fs.ReadFile(em.fs, p)
if errors.Is(err, fs.ErrNotExist) {
return nil, ErrNotFound
}
if err != nil {
return nil, err
}
e, err := MakeExpander(name, string(tmpl), em.cfg, em.auth)
if err != nil {
// Store negative cache result
e = nil
}
em.expanders[name] = e
return e, err
}