blob: a4a2efc40761449988495e39a8497aa72d9f4f25 [file] [log] [blame]
Giorgi Lekveishvili0ba5e402024-03-20 15:56:30 +04001package main
2
3import (
4 "bytes"
5 "context"
6 "crypto/tls"
Davit Tabidze5f00a392024-08-13 18:37:02 +04007 "embed"
Giorgi Lekveishvili0ba5e402024-03-20 15:56:30 +04008 "encoding/json"
9 "flag"
10 "fmt"
Davit Tabidze5f00a392024-08-13 18:37:02 +040011 "html/template"
Giorgi Lekveishvili0ba5e402024-03-20 15:56:30 +040012 "io"
13 "log"
14 "net/http"
15 "net/http/cookiejar"
16 "net/url"
gio4fde4a12024-10-13 12:19:30 +040017 "regexp"
Davit Tabidze5f00a392024-08-13 18:37:02 +040018 "slices"
Giorgi Lekveishvili0ba5e402024-03-20 15:56:30 +040019 "strings"
20)
21
22var port = flag.Int("port", 3000, "Port to listen on")
23var whoAmIAddr = flag.String("whoami-addr", "", "Kratos whoami endpoint address")
24var loginAddr = flag.String("login-addr", "", "Login page address")
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +040025var membershipAddr = flag.String("membership-addr", "", "Group membership API endpoint")
Davit Tabidze5f00a392024-08-13 18:37:02 +040026var membershipPublicAddr = flag.String("membership-public-addr", "", "Public address of membership service")
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +040027var groups = flag.String("groups", "", "Comma separated list of groups. User must be part of at least one of them. If empty group membership will not be checked.")
Giorgi Lekveishvili0ba5e402024-03-20 15:56:30 +040028var upstream = flag.String("upstream", "", "Upstream service address")
gio4fde4a12024-10-13 12:19:30 +040029var noAuthPathPatterns = flag.String("no-auth-path-patterns", "", "Path regex patterns to disable authentication for")
Giorgi Lekveishvili0ba5e402024-03-20 15:56:30 +040030
Davit Tabidze5f00a392024-08-13 18:37:02 +040031//go:embed unauthorized.html
32var unauthorizedHTML embed.FS
33
34//go:embed static/*
35var f embed.FS
36
gio4fde4a12024-10-13 12:19:30 +040037var noAuthPathRegexps []*regexp.Regexp
gio134be722025-07-20 19:01:17 +040038var allowedGroups []Group
gio4fde4a12024-10-13 12:19:30 +040039
40func initPathPatterns() error {
41 for _, p := range strings.Split(*noAuthPathPatterns, ",") {
42 t := strings.TrimSpace(p)
43 if len(t) == 0 {
44 continue
45 }
46 exp, err := regexp.Compile(t)
47 if err != nil {
48 return err
49 }
50 noAuthPathRegexps = append(noAuthPathRegexps, exp)
51 }
52 return nil
53}
54
gio134be722025-07-20 19:01:17 +040055type getGroupReq struct {
56 GroupId string `json:"groupId"`
57}
58
59type getGroupResp struct {
60 Self Group `json:"self"`
61}
62
63func initAllowedGroups() error {
64 for _, groupId := range strings.Split(*groups, ",") {
65 gid := strings.TrimSpace(groupId)
66 if len(gid) == 0 {
67 continue
68 }
69 var buf bytes.Buffer
70 if err := json.NewEncoder(&buf).Encode(getGroupReq{gid}); err != nil {
71 return err
72 }
73 resp, err := http.Post(fmt.Sprintf("%s/api/get-group", *membershipAddr), "application/json", &buf)
74 if err != nil {
75 return err
76 }
77 var info getGroupResp
78 if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
79 return err
80 }
81 fmt.Println(info.Self)
82 allowedGroups = append(allowedGroups, info.Self)
83 }
84 return nil
85}
86
Davit Tabidze5f00a392024-08-13 18:37:02 +040087type cachingHandler struct {
88 h http.Handler
89}
90
91func (h cachingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
92 w.Header().Set("Cache-Control", "max-age=604800")
93 h.h.ServeHTTP(w, r)
94}
95
Giorgi Lekveishvili0ba5e402024-03-20 15:56:30 +040096type user struct {
97 Identity struct {
giodd213152024-09-27 11:26:59 +020098 Id string `json:"id"`
Giorgi Lekveishvili0ba5e402024-03-20 15:56:30 +040099 Traits struct {
100 Username string `json:"username"`
101 } `json:"traits"`
102 } `json:"identity"`
103}
104
105type authError struct {
106 Error struct {
107 Status string `json:"status"`
108 } `json:"error"`
109}
110
111func getAddr(r *http.Request) (*url.URL, error) {
112 return url.Parse(fmt.Sprintf(
113 "%s://%s%s",
114 r.Header["X-Forwarded-Scheme"][0],
115 r.Header["X-Forwarded-Host"][0],
116 r.URL.RequestURI()))
117}
118
Davit Tabidze5f00a392024-08-13 18:37:02 +0400119var funcMap = template.FuncMap{
120 "IsLast": func(index int, slice []string) bool {
121 return index == len(slice)-1
122 },
123}
124
125type UnauthorizedPageData struct {
126 MembershipPublicAddr string
gio134be722025-07-20 19:01:17 +0400127 Groups []Group
Davit Tabidze5f00a392024-08-13 18:37:02 +0400128}
129
130func renderUnauthorizedPage(w http.ResponseWriter, groups []string) {
131 tmpl, err := template.New("unauthorized.html").Funcs(funcMap).ParseFS(unauthorizedHTML, "unauthorized.html")
132 if err != nil {
133 http.Error(w, "Failed to load template", http.StatusInternalServerError)
134 return
135 }
136 data := UnauthorizedPageData{
137 MembershipPublicAddr: *membershipPublicAddr,
gio134be722025-07-20 19:01:17 +0400138 Groups: allowedGroups,
Davit Tabidze5f00a392024-08-13 18:37:02 +0400139 }
140 w.Header().Set("Content-Type", "text/html")
141 w.WriteHeader(http.StatusUnauthorized)
142 if err := tmpl.Execute(w, data); err != nil {
143 http.Error(w, "Failed render template", http.StatusInternalServerError)
144 }
145}
146
Giorgi Lekveishvili0ba5e402024-03-20 15:56:30 +0400147func handle(w http.ResponseWriter, r *http.Request) {
gio9870cc02024-10-13 09:20:11 +0400148 user, err := queryWhoAmI(r.Cookies())
149 if err != nil {
150 http.Error(w, err.Error(), http.StatusInternalServerError)
151 return
152 }
gioc81a8472024-09-24 13:06:19 +0200153 reqAuth := true
gio4fde4a12024-10-13 12:19:30 +0400154 for _, p := range noAuthPathRegexps {
155 if p.MatchString(r.URL.Path) {
gioc81a8472024-09-24 13:06:19 +0200156 reqAuth = false
157 break
Giorgi Lekveishvili0ba5e402024-03-20 15:56:30 +0400158 }
gioc81a8472024-09-24 13:06:19 +0200159 }
gioc81a8472024-09-24 13:06:19 +0200160 if reqAuth {
gioc81a8472024-09-24 13:06:19 +0200161 if user == nil {
162 if r.Method != http.MethodGet {
163 http.Error(w, "Unauthorized", http.StatusUnauthorized)
164 return
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400165 }
gioc81a8472024-09-24 13:06:19 +0200166 curr, err := getAddr(r)
167 if err != nil {
168 http.Error(w, err.Error(), http.StatusInternalServerError)
169 return
170 }
171 addr := fmt.Sprintf("%s?return_to=%s", *loginAddr, curr.String())
172 http.Redirect(w, r, addr, http.StatusSeeOther)
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400173 return
174 }
gioc81a8472024-09-24 13:06:19 +0200175 if *groups != "" {
176 hasPermission := false
gio134be722025-07-20 19:01:17 +0400177 tg, err := getGroupsUserCanActAs(user.Identity.Id)
gioc81a8472024-09-24 13:06:19 +0200178 if err != nil {
179 http.Error(w, err.Error(), http.StatusInternalServerError)
180 return
181 }
gio134be722025-07-20 19:01:17 +0400182 for _, i := range allowedGroups {
183 if slices.Contains(tg, i) {
gioc81a8472024-09-24 13:06:19 +0200184 hasPermission = true
185 break
186 }
187 }
188 if !hasPermission {
189 groupList := strings.Split(*groups, ",")
190 renderUnauthorizedPage(w, groupList)
191 return
192 }
193 }
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400194 }
Giorgi Lekveishvili0ba5e402024-03-20 15:56:30 +0400195 rc := r.Clone(context.Background())
gioc8faeac2024-10-09 15:26:16 +0400196 if user != nil {
197 rc.Header.Add("X-Forwarded-User", user.Identity.Traits.Username)
198 rc.Header.Add("X-Forwarded-UserId", user.Identity.Id)
199 } else {
200 delete(rc.Header, "X-Forwarded-User")
201 delete(rc.Header, "X-Forwarded-UserId")
202 }
Giorgi Lekveishvili0ba5e402024-03-20 15:56:30 +0400203 ru, err := url.Parse(fmt.Sprintf("http://%s%s", *upstream, r.URL.RequestURI()))
204 if err != nil {
205 http.Error(w, err.Error(), http.StatusInternalServerError)
206 return
207 }
208 rc.URL = ru
209 rc.RequestURI = ""
210 client := &http.Client{
211 Transport: &http.Transport{
212 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
213 },
214 CheckRedirect: func(req *http.Request, via []*http.Request) error {
215 return http.ErrUseLastResponse
216 },
217 }
218 resp, err := client.Do(rc)
219 if err != nil {
220 http.Error(w, err.Error(), http.StatusInternalServerError)
221 return
222 }
223 for name, values := range resp.Header {
224 for _, value := range values {
225 w.Header().Add(name, value)
226 }
227 }
228 w.WriteHeader(resp.StatusCode)
229 if _, err := io.Copy(w, resp.Body); err != nil {
230 http.Error(w, err.Error(), http.StatusInternalServerError)
231 return
232 }
233}
234
235func queryWhoAmI(cookies []*http.Cookie) (*user, error) {
236 jar, err := cookiejar.New(nil)
237 if err != nil {
238 return nil, err
239 }
240 client := &http.Client{
241 Jar: jar,
242 Transport: &http.Transport{
243 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
244 },
245 }
246 addr, err := url.Parse(*whoAmIAddr)
247 if err != nil {
248 return nil, err
249 }
250 client.Jar.SetCookies(addr, cookies)
251 resp, err := client.Get(*whoAmIAddr)
252 if err != nil {
253 return nil, err
254 }
Giorgi Lekveishvili0ba5e402024-03-20 15:56:30 +0400255 if resp.StatusCode == http.StatusOK {
256 u := &user{}
gio134be722025-07-20 19:01:17 +0400257 if err := json.NewDecoder(resp.Body).Decode(u); err != nil {
Giorgi Lekveishvili0ba5e402024-03-20 15:56:30 +0400258 return nil, err
259 }
260 return u, nil
261 }
262 e := &authError{}
gio134be722025-07-20 19:01:17 +0400263 if err := json.NewDecoder(resp.Body).Decode(e); err != nil {
Giorgi Lekveishvili0ba5e402024-03-20 15:56:30 +0400264 return nil, err
265 }
266 if e.Error.Status == "Unauthorized" {
267 return nil, nil
268 }
gio134be722025-07-20 19:01:17 +0400269 return nil, fmt.Errorf("Unknown error")
270}
271
272type Group struct {
273 Id string `json:"id"`
274 Title string `json:"title"`
275 Description string `json:"description"`
Giorgi Lekveishvili0ba5e402024-03-20 15:56:30 +0400276}
277
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400278type MembershipInfo struct {
gio134be722025-07-20 19:01:17 +0400279 CanActAs []Group `json:"canActAs"`
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400280}
281
gio134be722025-07-20 19:01:17 +0400282func getGroupsUserCanActAs(user string) ([]Group, error) {
283 resp, err := http.Get(fmt.Sprintf("%s/api/user/%s", *membershipAddr, user))
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400284 if err != nil {
285 return nil, err
286 }
287 var info MembershipInfo
288 if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
289 return nil, err
290 }
gio134be722025-07-20 19:01:17 +0400291 return info.CanActAs, nil
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400292}
293
Giorgi Lekveishvili0ba5e402024-03-20 15:56:30 +0400294func main() {
295 flag.Parse()
Davit Tabidze5f00a392024-08-13 18:37:02 +0400296 if *groups != "" && (*membershipAddr == "" || *membershipPublicAddr == "") {
297 log.Fatal("membership-addr and membership-public-addr flags are required when groups are provided")
Giorgi Lekveishvilia09fad72024-03-21 15:24:35 +0400298 }
gio4fde4a12024-10-13 12:19:30 +0400299 if err := initPathPatterns(); err != nil {
300 log.Fatal(err)
301 }
gio134be722025-07-20 19:01:17 +0400302 if err := initAllowedGroups(); err != nil {
303 log.Fatal(err)
304 }
gioc81a8472024-09-24 13:06:19 +0200305 http.Handle("/.auth/static/", http.StripPrefix("/.auth", cachingHandler{http.FileServer(http.FS(f))}))
Giorgi Lekveishvili0ba5e402024-03-20 15:56:30 +0400306 http.HandleFunc("/", handle)
307 fmt.Printf("Starting HTTP server on port: %d\n", *port)
308 log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil))
309}