blob: e3ec2ee993245be99438ba0394a2404f3d7b20fb [file] [log] [blame]
Serge Bazanski0aa29102023-04-01 23:18:05 +00001package main
2
3import (
radexd318d7e2023-10-09 00:01:51 +02004 "context"
Serge Bazanski0aa29102023-04-01 23:18:05 +00005 "crypto/tls"
6 "flag"
7 "fmt"
8 "net/http"
9 "regexp"
10 "strings"
11 "sync"
12
Serge Bazanski97b5cd72023-07-28 17:14:50 +000013 ldap "github.com/go-ldap/ldap/v3"
Serge Bazanski0aa29102023-04-01 23:18:05 +000014 "github.com/golang/glog"
Serge Bazanski0aa29102023-04-01 23:18:05 +000015)
16
17type server struct {
18 mu sync.Mutex
19 ldap *ldap.Conn
20}
21
22var reURL = regexp.MustCompile(`^/([a-zA-Z0-9\-_]+)/([a-zA-Z0-9\-_]+)$`)
23
24func (s *server) handle(rw http.ResponseWriter, req *http.Request) {
25 if req.Method != "GET" {
26 rw.WriteHeader(http.StatusMethodNotAllowed)
27 fmt.Fprintf(rw, "method not allowed")
28 return
29 }
30
31 reqParts := reURL.FindStringSubmatch(req.URL.Path)
32 if len(reqParts) != 3 {
33 fmt.Fprintf(rw, "usage: GET /capability/user, eg. GET /staff/q3k")
34 return
35 }
36 c := reqParts[1]
37 u := reqParts[2]
38
39 res, err := s.capacify(c, u)
40 l := ""
41 r := ""
42 switch {
43 case err != nil:
44 l = fmt.Sprintf("%v", err)
45 r = "ERROR"
46 rw.WriteHeader(500)
47 case res:
48 l = "yes"
49 r = "YES"
50 rw.WriteHeader(200)
51 default:
52 l = "no"
53 r = "NO"
54 rw.WriteHeader(401)
55 }
56 glog.Infof("%s: GET /%s/%s: %s", req.RemoteAddr, c, u, l)
57 fmt.Fprintf(rw, "%s", r)
58}
59
60func (s *server) capacify(c, u string) (bool, error) {
61 switch c {
62 case "xmpp":
63 return s.checkLdap(u, "cn=xmpp-users,ou=Group,dc=hackerspace,dc=pl")
64 case "wiki_admin":
65 return s.checkLdap(u, "cn=admin,dc=wiki,dc=hackerspace,dc=pl")
66 case "twitter":
67 return s.checkLdap(u, "cn=twitter,ou=Group,dc=hackerspace,dc=pl")
68 case "lulzbot_access":
69 return s.checkLdap(u, "cn=lulzbot-access,ou=Group,dc=hackerspace,dc=pl")
70 case "staff":
71 return s.checkLdap(u, "cn=staff,ou=Group,dc=hackerspace,dc=pl")
72 case "kasownik_access":
73 return s.checkLdap(u, "cn=kasownik-access,ou=Group,dc=hackerspace,dc=pl")
74 case "starving":
75 return s.checkLdap(u, "cn=starving,ou=Group,dc=hackerspace,dc=pl")
76 case "fatty":
77 return s.checkLdap(u, "cn=fatty,ou=Group,dc=hackerspace,dc=pl")
78 case "member":
79 // Where we're going we don't need applicatives.
80 res, err := s.capacify("fatty", u)
81 if err != nil {
82 return false, err
83 }
84 if res {
85 return true, nil
86 }
87 return s.capacify("starving", u)
88 default:
89 return false, nil
90 }
91}
92
93func (s *server) getLdap() (*ldap.Conn, error) {
94 s.mu.Lock()
95 defer s.mu.Unlock()
96 if s.ldap == nil {
97 lconn, err := connectLdap()
98 if err != nil {
99 return nil, err
100 }
101 s.ldap = lconn
102 }
103 return s.ldap, nil
104}
105
106func (s *server) closeLdap() {
107 s.mu.Lock()
108 defer s.mu.Unlock()
109 if s.ldap != nil {
110 s.ldap.Close()
111 s.ldap = nil
112 }
113}
114
115func (s *server) checkLdap(u, dn string) (bool, error) {
116 lconn, err := s.getLdap()
117 if err != nil {
118 return false, err
119 }
120
121 if strings.ContainsAny(u, `\#+<>,;"=`) {
122 return false, nil
123 }
124 filter := fmt.Sprintf("(uniqueMember=uid=%s,ou=People,dc=hackerspace,dc=pl)", u)
125 search := ldap.NewSearchRequest(
126 dn,
127 ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
128 filter, []string{"dn", "cn"}, nil,
129 )
130 sr, err := lconn.Search(search)
131 if err != nil {
132 s.closeLdap()
133 return false, fmt.Errorf("search failed: %w", err)
134 }
135
136 for _, entry := range sr.Entries {
137 if entry.DN == dn {
138 return true, nil
139 }
140 }
141
142 return false, nil
143}
144
145func init() {
146 flag.Set("logtostderr", "true")
147}
148
149var (
150 flagLDAPServer string
151 flagLDAPBindDN string
152 flagLDAPBindPW string
153 flagListen string
154)
155
156func connectLdap() (*ldap.Conn, error) {
157 tlsConfig := &tls.Config{}
158 lconn, err := ldap.DialTLS("tcp", flagLDAPServer, tlsConfig)
159 if err != nil {
160 return nil, fmt.Errorf("ldap.DialTLS: %v", err)
161 }
162
163 if err := lconn.Bind(flagLDAPBindDN, flagLDAPBindPW); err != nil {
164 lconn.Close()
165 return nil, fmt.Errorf("ldap.Bind: %v", err)
166 }
167 return lconn, nil
168}
169
170func main() {
171 flag.StringVar(&flagListen, "api_listen", ":2137", "Address to listen on for API requests")
172 flag.StringVar(&flagLDAPServer, "ldap_server", "ldap.hackerspace.pl:636", "LDAP server address")
173 flag.StringVar(&flagLDAPBindDN, "ldap_bind_dn", "cn=capacifier,ou=Services,dc=hackerspace,dc=pl", "LDAP bind DN")
174 flag.StringVar(&flagLDAPBindPW, "ldap_bind_pw", "", "LDAP bind password")
175 flag.Parse()
176
177 if flagLDAPBindPW == "" {
178 glog.Exitf("-ldap_bind_pw must be set")
179 }
180
radexd318d7e2023-10-09 00:01:51 +0200181 // TODO(q3k): use sigint-interruptible context
182 ctx := context.Background()
Serge Bazanski0aa29102023-04-01 23:18:05 +0000183
184 s := &server{}
185 mux := http.NewServeMux()
186 mux.HandleFunc("/", s.handle)
187
188 go func() {
189 glog.Infof("API Listening on %s", flagListen)
190 if err := http.ListenAndServe(flagListen, mux); err != nil {
191 glog.Exitf("API Listen failed: %v", err)
192 }
193 }()
194
radexd318d7e2023-10-09 00:01:51 +0200195 <-ctx.Done()
Serge Bazanski0aa29102023-04-01 23:18:05 +0000196}