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 <ironbound@hackerspace.pl>
changes/09/1509/2
q3k 2023-04-01 23:18:05 +00:00 committed by ar
parent 90cf314d1e
commit 0aa2910d00
6 changed files with 358 additions and 0 deletions

View File

@ -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",
)

View File

@ -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
```

View File

@ -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()
}

View File

@ -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,
},
},
},
}

View File

@ -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]),
},
},
},
}

View File

@ -0,0 +1,40 @@
-----BEGIN PGP MESSAGE-----
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
=SBGB
-----END PGP MESSAGE-----