| package main |
| |
| import ( |
| "context" |
| "crypto/tls" |
| "flag" |
| "fmt" |
| "net/http" |
| "regexp" |
| "strings" |
| "sync" |
| |
| ldap "github.com/go-ldap/ldap/v3" |
| "github.com/golang/glog" |
| ) |
| |
| type server struct { |
| mu sync.Mutex |
| ldap *ldap.Conn |
| } |
| |
| var reURL = regexp.MustCompile(`^/([a-zA-Z0-9\-_]+)/([a-zA-Z0-9\-_]+)$`) |
| |
| func (s *server) handle(rw http.ResponseWriter, req *http.Request) { |
| if req.Method != "GET" { |
| rw.WriteHeader(http.StatusMethodNotAllowed) |
| fmt.Fprintf(rw, "method not allowed") |
| return |
| } |
| |
| reqParts := reURL.FindStringSubmatch(req.URL.Path) |
| if len(reqParts) != 3 { |
| fmt.Fprintf(rw, "usage: GET /capability/user, eg. GET /staff/q3k") |
| return |
| } |
| c := reqParts[1] |
| u := reqParts[2] |
| |
| res, err := s.capacify(c, u) |
| l := "" |
| r := "" |
| switch { |
| case err != nil: |
| l = fmt.Sprintf("%v", err) |
| r = "ERROR" |
| rw.WriteHeader(500) |
| case res: |
| l = "yes" |
| r = "YES" |
| rw.WriteHeader(200) |
| default: |
| l = "no" |
| r = "NO" |
| rw.WriteHeader(401) |
| } |
| glog.Infof("%s: GET /%s/%s: %s", req.RemoteAddr, c, u, l) |
| fmt.Fprintf(rw, "%s", r) |
| } |
| |
| func (s *server) capacify(c, u string) (bool, error) { |
| switch c { |
| case "xmpp": |
| return s.checkLdap(u, "cn=xmpp-users,ou=Group,dc=hackerspace,dc=pl") |
| case "wiki_admin": |
| return s.checkLdap(u, "cn=admin,dc=wiki,dc=hackerspace,dc=pl") |
| case "twitter": |
| return s.checkLdap(u, "cn=twitter,ou=Group,dc=hackerspace,dc=pl") |
| case "lulzbot_access": |
| return s.checkLdap(u, "cn=lulzbot-access,ou=Group,dc=hackerspace,dc=pl") |
| case "staff": |
| return s.checkLdap(u, "cn=staff,ou=Group,dc=hackerspace,dc=pl") |
| case "kasownik_access": |
| return s.checkLdap(u, "cn=kasownik-access,ou=Group,dc=hackerspace,dc=pl") |
| case "starving": |
| return s.checkLdap(u, "cn=starving,ou=Group,dc=hackerspace,dc=pl") |
| case "fatty": |
| return s.checkLdap(u, "cn=fatty,ou=Group,dc=hackerspace,dc=pl") |
| case "member": |
| // Where we're going we don't need applicatives. |
| res, err := s.capacify("fatty", u) |
| if err != nil { |
| return false, err |
| } |
| if res { |
| return true, nil |
| } |
| return s.capacify("starving", u) |
| default: |
| return false, nil |
| } |
| } |
| |
| func (s *server) getLdap() (*ldap.Conn, error) { |
| s.mu.Lock() |
| defer s.mu.Unlock() |
| if s.ldap == nil { |
| lconn, err := connectLdap() |
| if err != nil { |
| return nil, err |
| } |
| s.ldap = lconn |
| } |
| return s.ldap, nil |
| } |
| |
| func (s *server) closeLdap() { |
| s.mu.Lock() |
| defer s.mu.Unlock() |
| if s.ldap != nil { |
| s.ldap.Close() |
| s.ldap = nil |
| } |
| } |
| |
| func (s *server) checkLdap(u, dn string) (bool, error) { |
| lconn, err := s.getLdap() |
| if err != nil { |
| return false, err |
| } |
| |
| if strings.ContainsAny(u, `\#+<>,;"=`) { |
| return false, nil |
| } |
| filter := fmt.Sprintf("(uniqueMember=uid=%s,ou=People,dc=hackerspace,dc=pl)", u) |
| search := ldap.NewSearchRequest( |
| dn, |
| ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, |
| filter, []string{"dn", "cn"}, nil, |
| ) |
| sr, err := lconn.Search(search) |
| if err != nil { |
| s.closeLdap() |
| return false, fmt.Errorf("search failed: %w", err) |
| } |
| |
| for _, entry := range sr.Entries { |
| if entry.DN == dn { |
| return true, nil |
| } |
| } |
| |
| return false, nil |
| } |
| |
| func init() { |
| flag.Set("logtostderr", "true") |
| } |
| |
| var ( |
| flagLDAPServer string |
| flagLDAPBindDN string |
| flagLDAPBindPW string |
| flagListen string |
| ) |
| |
| func connectLdap() (*ldap.Conn, error) { |
| tlsConfig := &tls.Config{} |
| lconn, err := ldap.DialTLS("tcp", flagLDAPServer, tlsConfig) |
| if err != nil { |
| return nil, fmt.Errorf("ldap.DialTLS: %v", err) |
| } |
| |
| if err := lconn.Bind(flagLDAPBindDN, flagLDAPBindPW); err != nil { |
| lconn.Close() |
| return nil, fmt.Errorf("ldap.Bind: %v", err) |
| } |
| return lconn, nil |
| } |
| |
| func main() { |
| flag.StringVar(&flagListen, "api_listen", ":2137", "Address to listen on for API requests") |
| flag.StringVar(&flagLDAPServer, "ldap_server", "ldap.hackerspace.pl:636", "LDAP server address") |
| flag.StringVar(&flagLDAPBindDN, "ldap_bind_dn", "cn=capacifier,ou=Services,dc=hackerspace,dc=pl", "LDAP bind DN") |
| flag.StringVar(&flagLDAPBindPW, "ldap_bind_pw", "", "LDAP bind password") |
| flag.Parse() |
| |
| if flagLDAPBindPW == "" { |
| glog.Exitf("-ldap_bind_pw must be set") |
| } |
| |
| // TODO(q3k): use sigint-interruptible context |
| ctx := context.Background() |
| |
| s := &server{} |
| mux := http.NewServeMux() |
| mux.HandleFunc("/", s.handle) |
| |
| go func() { |
| glog.Infof("API Listening on %s", flagListen) |
| if err := http.ListenAndServe(flagListen, mux); err != nil { |
| glog.Exitf("API Listen failed: %v", err) |
| } |
| }() |
| |
| <-ctx.Done() |
| } |