Serge Bazanski | 0aa2910 | 2023-04-01 23:18:05 +0000 | [diff] [blame] | 1 | package main |
| 2 | |
| 3 | import ( |
radex | d318d7e | 2023-10-09 00:01:51 +0200 | [diff] [blame] | 4 | "context" |
Serge Bazanski | 0aa2910 | 2023-04-01 23:18:05 +0000 | [diff] [blame] | 5 | "crypto/tls" |
| 6 | "flag" |
| 7 | "fmt" |
| 8 | "net/http" |
| 9 | "regexp" |
| 10 | "strings" |
| 11 | "sync" |
| 12 | |
Serge Bazanski | 97b5cd7 | 2023-07-28 17:14:50 +0000 | [diff] [blame] | 13 | ldap "github.com/go-ldap/ldap/v3" |
Serge Bazanski | 0aa2910 | 2023-04-01 23:18:05 +0000 | [diff] [blame] | 14 | "github.com/golang/glog" |
Serge Bazanski | 0aa2910 | 2023-04-01 23:18:05 +0000 | [diff] [blame] | 15 | ) |
| 16 | |
| 17 | type server struct { |
| 18 | mu sync.Mutex |
| 19 | ldap *ldap.Conn |
| 20 | } |
| 21 | |
| 22 | var reURL = regexp.MustCompile(`^/([a-zA-Z0-9\-_]+)/([a-zA-Z0-9\-_]+)$`) |
| 23 | |
| 24 | func (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 | |
| 60 | func (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 | |
| 93 | func (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 | |
| 106 | func (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 | |
| 115 | func (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 | |
| 145 | func init() { |
| 146 | flag.Set("logtostderr", "true") |
| 147 | } |
| 148 | |
| 149 | var ( |
| 150 | flagLDAPServer string |
| 151 | flagLDAPBindDN string |
| 152 | flagLDAPBindPW string |
| 153 | flagListen string |
| 154 | ) |
| 155 | |
| 156 | func 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 | |
| 170 | func 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 | |
radex | d318d7e | 2023-10-09 00:01:51 +0200 | [diff] [blame] | 181 | // TODO(q3k): use sigint-interruptible context |
| 182 | ctx := context.Background() |
Serge Bazanski | 0aa2910 | 2023-04-01 23:18:05 +0000 | [diff] [blame] | 183 | |
| 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 | |
radex | d318d7e | 2023-10-09 00:01:51 +0200 | [diff] [blame] | 195 | <-ctx.Done() |
Serge Bazanski | 0aa2910 | 2023-04-01 23:18:05 +0000 | [diff] [blame] | 196 | } |