From 0aa2910d0027b749ca905e883fd1f2b544c8957b Mon Sep 17 00:00:00 2001 From: Serge Bazanski Date: Sat, 1 Apr 2023 23:18:05 +0000 Subject: [PATCH] hswaw/capacifier: rewrite it in go This reimplements capacifier, one of the earliest just-some-flask-code-on-boston-packets services, in Go. It's a minimum reimplementation, as this service is generally deprecated - but some stuff still depends on it. So we do away with capacifier v0's bespoke rule language and just hardcode everything. It's not like any of these rules ever changed, anyway. This is not yet deployed. Change-Id: Id65ef92784a524c32ae5223cd5460736ac683116 Reviewed-on: https://gerrit.hackerspace.pl/c/hscloud/+/1509 Reviewed-by: ironbound --- hswaw/capacifier/BUILD.bazel | 45 ++++ hswaw/capacifier/README.md | 23 ++ hswaw/capacifier/capacifier.go | 203 ++++++++++++++++++ hswaw/kube/capacifier.libsonnet | 41 ++++ hswaw/kube/hswaw.jsonnet | 6 + .../secrets/cipher/prod-capacifier-password | 40 ++++ 6 files changed, 358 insertions(+) create mode 100644 hswaw/capacifier/BUILD.bazel create mode 100644 hswaw/capacifier/README.md create mode 100644 hswaw/capacifier/capacifier.go create mode 100644 hswaw/kube/capacifier.libsonnet create mode 100644 hswaw/kube/secrets/cipher/prod-capacifier-password diff --git a/hswaw/capacifier/BUILD.bazel b/hswaw/capacifier/BUILD.bazel new file mode 100644 index 00000000..752064d5 --- /dev/null +++ b/hswaw/capacifier/BUILD.bazel @@ -0,0 +1,45 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") +load("@io_bazel_rules_docker//container:container.bzl", "container_image", "container_layer", "container_push") + +go_library( + name = "go_default_library", + srcs = ["capacifier.go"], + importpath = "code.hackerspace.pl/hscloud/hswaw/capacifier", + visibility = ["//visibility:private"], + deps = [ + "//go/mirko:go_default_library", + "@com_github_golang_glog//:go_default_library", + "@in_gopkg_ldap_v3//:go_default_library", + ], +) + +go_binary( + name = "capacifier", + embed = [":go_default_library"], + visibility = ["//visibility:public"], +) + +container_layer( + name = "layer_bin", + directory = "/hswaw/capacifier/", + files = [ + ":capacifier", + ], +) + +container_image( + name = "runtime", + base = "@prodimage-bionic//image", + layers = [ + ":layer_bin", + ], +) + +container_push( + name = "push", + format = "Docker", + image = ":runtime", + registry = "registry.k0.hswaw.net", + repository = "q3k/capacifier", + tag = "1680390588", +) diff --git a/hswaw/capacifier/README.md b/hswaw/capacifier/README.md new file mode 100644 index 00000000..f2b77418 --- /dev/null +++ b/hswaw/capacifier/README.md @@ -0,0 +1,23 @@ +capacifier +=== + +rewrite-in-go of code.haclerspace.pl/tomek/capacifier. + +This is one of the oldest API services at the Warsaw hackerspace, and exists +solely to provide a generic 'is X a member of Y' functionality. It's generally +deprecated (instead OIDC should be used as much as possible), but it's so +entrenched into our infra that it's difficult to fully kill. + +While the previous implementation had a whole bespoke rule expression language, +this implementation is stupidly simple, with all rules hardcoded. + +Running +--- + +Get the password for the capacifier service account from prod. + +Then: + +``` + bazel run //hswaw/capacifier -- --ldap_bind_pw xxx +``` diff --git a/hswaw/capacifier/capacifier.go b/hswaw/capacifier/capacifier.go new file mode 100644 index 00000000..34c62a1b --- /dev/null +++ b/hswaw/capacifier/capacifier.go @@ -0,0 +1,203 @@ +package main + +import ( + "crypto/tls" + "flag" + "fmt" + "net/http" + "regexp" + "strings" + "sync" + + "github.com/golang/glog" + ldap "gopkg.in/ldap.v3" + + "code.hackerspace.pl/hscloud/go/mirko" +) + +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") + } + + m := mirko.New() + if err := m.Listen(); err != nil { + glog.Exitf("Listen(): %v", err) + } + + 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) + } + }() + + if err := m.Serve(); err != nil { + glog.Exitf("Serve(): %v", err) + } + + <-m.Done() +} diff --git a/hswaw/kube/capacifier.libsonnet b/hswaw/kube/capacifier.libsonnet new file mode 100644 index 00000000..343209f9 --- /dev/null +++ b/hswaw/kube/capacifier.libsonnet @@ -0,0 +1,41 @@ +local mirko = import "../../kube/mirko.libsonnet"; +local kube = import "../../kube/kube.libsonnet"; + +{ + cfg:: { + ldapBindPassword: error "ldapBindPassword must be set!", + image: "registry.k0.hswaw.net/q3k/capacifier:1680390588", + fqdn: "capacifier.hackerspace.pl", + }, + + component(cfg, env):: mirko.Component(env, "capacifier") { + local capacifier = self, + cfg+: { + image: cfg.image, + container: capacifier.GoContainer("main", "/hswaw/capacifier/capacifier") { + env_: { + BIND_PW: kube.SecretKeyRef(capacifier.secret, "bindPW"), + }, + command+: [ + "-listen", "0.0.0.0:5000", + "-ldap_bind_pw", "$(BIND_PW)", + ], + }, + ports+: { + publicHTTP: { + api: { + port: 5000, + dns: cfg.fqdn, + } + }, + }, + }, + + secret: kube.Secret("capacifier") { + metadata+: capacifier.metadata, + data_: { + bindPW: cfg.ldapBindPassword, + }, + }, + }, +} diff --git a/hswaw/kube/hswaw.jsonnet b/hswaw/kube/hswaw.jsonnet index 9a1bec7b..7918043e 100644 --- a/hswaw/kube/hswaw.jsonnet +++ b/hswaw/kube/hswaw.jsonnet @@ -8,6 +8,7 @@ local frab = import "frab.libsonnet"; local pretalx = import "pretalx.libsonnet"; local cebulacamp = import "cebulacamp.libsonnet"; local site = import "site.libsonnet"; +local capacifier = import "capacifier.libsonnet"; { hswaw(name):: mirko.Environment(name) { @@ -22,6 +23,7 @@ local site = import "site.libsonnet"; pretalx: pretalx.cfg, cebulacamp: cebulacamp.cfg, site: site.cfg, + capacifier: capacifier.cfg, }, components: { @@ -33,6 +35,7 @@ local site = import "site.libsonnet"; pretalx: pretalx.component(cfg.pretalx, env), cebulacamp: cebulacamp.component(cfg.cebulacamp, env), site: site.component(cfg.site, env), + capacifier: capacifier.component(cfg.capacifier, env), }, }, @@ -75,6 +78,9 @@ local site = import "site.libsonnet"; site+: { webFQDN: "new.hackerspace.pl", }, + capacifier+: { + ldapBindPassword: std.base64(std.split(importstr "secrets/plain/prod-capacifier-password", "\n")[0]), + }, }, }, } diff --git a/hswaw/kube/secrets/cipher/prod-capacifier-password b/hswaw/kube/secrets/cipher/prod-capacifier-password new file mode 100644 index 00000000..ec666cdc --- /dev/null +++ b/hswaw/kube/secrets/cipher/prod-capacifier-password @@ -0,0 +1,40 @@ +-----BEGIN PGP MESSAGE----- + +hQEMAzhuiT4RC8VbAQgAtAcnJCFOzbsIu0Hm+DDe0BYn/NhfCNE9ZETdnq/wbJNG +cAIolbeNumz45A+4UuEDOHlUUkEolwMi8WPxiNpVJoJCvcfT0Lx600SF63QBPJgK +andl5nSS4C3ZwA7YO9XE7tv63Qji6Icqj69nmephNjlEqeVSm4SYr/3khUP/59ZH +ruRW2PFwHVmF7SVVSS/rCRZjSqCxaVQp1x/ySxWgODO2fcwBNaRRj6Ouf2B+nBwc +5uxsk5ckhoVJagCLnBilwqrBZG9BVoMi2C1apkzflVfHmFgbDKPuVfVzS4+SXgJp +v+unEuKq5bvtOrsfsFIY5S8x8uMwm6+S8pTA/Fo29oUBDANcG2tp6fXqvgEIAMDC +SedxyuWqUkOKWa6sZ7+J9mWkAsiwUNMvaOjrGo79Jp3RUGzmV0tw6bG2j7qJF4xQ +R82erSY/9WFiJIXMnoQHlCXl9hi1HOimpgfjFWILMKUIDq02V7ON6AZTUe/vydIF +/msOxRVwNh5q+xK6uSKLaAvvaarB6R2Z4JXCtjqw6h5MTeIVjgJ2bGN/AZ1POlCC +lSJyJMsotwY18G/tHg+M1tlS/byOWs6I14TMPiHxC4la+VZG4uoSs9mu5nz+V5Hx +Zo8yzOwb5kPSudzovHIgtkIX7z0onDbevaF5EiCFhgI37ORPhHRwsrO0r9H+npa1 +NMdssQXgoZkibXrA4p+FAgwDodoT8VqRl4UBD/9dJhUkcIN8RuU6kbyB4rXnpTOZ +ZzYyG0GDPNMuQ25XiujCOq7fNJZCnwsrbfGFxkEJ55Vj80BOKz2m3JFUlDRxeWVz +w+NqnCCv4ONqINBkuIoW/TbCnbjI7W0fP5hx1LWHWjNt1DyFbgHZPIdle/caSsMg +Uvh4az1veQ6wRzE23tStVL6Xv74gabbwwwb8/7V7tLvD+0kfRni4N3m8PHhqYfs8 +u3YL1XfoNmLxSVoAEzQCSmP8s+rQS+2yljy4PLepRjsTSW5rZetcAOO43VLPtwKK +OAUGxgGZmC1BZBamVdWr3EeNaQk+82r3ZZ3o7EV443/jcvDtX6SF9CVaGnd+DqWT +1MU7ngDL5h2OKsSbf6t2YCq5MrlZs98hPISSRMyHLy9qeXe/L+ODoGvRW84d/oKO +0mLTuMgpm8xfMnMt82QEdBRyWYwoWILxwyORp67MRPRXHygJgSpuycYAuZyvHXj7 +HIeVzqT++07FMc7Nl3l78LYmyDZAu+3KXgvfr2dqKhVCu6UHjqVscy6DXbkJR544 +vowknhu7g211QxQfKP+l/WoczhOv7/9Ea0F6nK7vKFgdfiaEvgIHKzgnmEYwO+fY +allOsTW3vINvVF0O3qFgtysFbXFdBFrInf7Gj31PFwjHiMFalwFUZUXS5LIgVscz +uehKjlrbhj/+h8vmLIUCDAPiA8lOXOuz7wEP/1Lw/502tcfpN4HNN4WF1nlPVegP +xlseMxCwfkzePLZ0H/J7PPch3XiN3eYV3qhQNzTzT7DP9O/HBc//U0HfbUBGmmha +Hy6Nfgp+9rsmr5zCGYyyijz+qarngbBiEanNkY8IKCE+jQJ3/fPqeLaupyGmg7zf +l8ycaMelocxhpy5iFT0o38EsUYgkDZw0NevcThEdSlybvJOid8TCuFcecChyJb/L +4ouNzINsLPAcPYVVzvUzsBmYvRe6A/wLLCXElV6lubKA9lOfF4nDP3GMRV6BKOmA +AbLmbTT/W8vnVxwmw2iHkxUgaSLfAX1IBxJZzy+Adb8wREO4ABEGLHrRb5WrR4hU +FOK/KCPJbNUPXXa4WlRQ274GFbZ5UK2NzhVYPMekLgIFpvvwC93SfWp4KSAY23eO +K/uZBuI9UzhArj6kn4ECmaz1QyMVlr33xIgjhGcmKr99nKmOeBTGFsX41wE++6kg +3e+BQcMw08W6xh0Tvb3cIQQN+8szwZB1yv5/oLeNgHIJTipqZC0tAvdyJbN4kyK8 +FGJ0WBJMu9kUaMllcqBwftF6gV4K3kBF2spaLRABWJpjKsD76zgkATttgUvda+Jv +9iVj5cgF0B5iHfAhlCXlWWn+SVVwlbuXyn1PwsQD6g5Iwnhl6ramIYMtW5R/Qt5q +RMOBHCTYWc3cn3G30nMBZovPi/ZK6Vw8F6xLk1tH8MImz0vS3HyCORJWSJkE53kS +wLfZSw/zNyiRMhV8+v9LZimHMfvL+5J8R65D50ZKZAW0+7ACRyR33rsB5PrWap0N +EM/Ku6x6cAh3OOvoW+ha+OgcUgZS/jV2kn5Mfvr7jMMd +=SBGB +-----END PGP MESSAGE-----