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