package engine import ( "encoding/json" "net/netip" "strings" "jarl/internal/iprange" "jarl/internal/model" ) // ---- blocking steps and client groups (docs/blocking.md) // stepRef resolves a step given by name or ID. ok is false when neither // is given. func stepRef(st *model.State, name *string, id *int64) (int64, bool, error) { switch { case name != nil: s, found := st.StepByName(*name) if !found { return 0, true, errf(KindBadRequest, "blocking step %q does not exist (steps: %s)", *name, stepNames(st)) } return s, true, nil case id != nil: if _, found := st.StepRank(*id); !found { return 0, true, errf(KindBadRequest, "blocking step %d does not exist", *id) } return *id, true, nil } return 0, false, nil } // stepNames lists the steps for messages: "off, normal, strict". func stepNames(st *model.State) string { names := []string{"off", "normal"} for _, s := range st.OrderedBlockSteps() { names = append(names, s.Name) } return strings.Join(names, ", ") } // NormalizeMember checks a client group member: an address, a prefix or // an ACL name (an ACL's own elements are checked by validation). It // returns the canonical form. func NormalizeMember(st *model.State, m string) (string, error) { m = strings.TrimSpace(m) if a, err := netip.ParseAddr(m); err == nil { if a.Zone() != "" { return "", errf(KindBadRequest, "%q: an address with a zone can't be matched", m) } return a.Unmap().String(), nil } if p, err := netip.ParsePrefix(m); err == nil { return p.Masked().String(), nil } if r, ok, err := iprange.Parse(m); ok { if err != nil { return "", errf(KindBadRequest, "%v", err) } return r.String(), nil } if a := st.ACLByName(m); a != nil { return a.Name, nil } return "", errf(KindBadRequest, "%q is not an address, a prefix (10.10.0.0/24), a range (10.10.0.20-10.10.0.49) or an ACL", m) } func applyBlockStepPut(st *model.State, op model.Op, c *applyCtx) error { if err := c.require(blockingAction()); err != nil { return err } var in BlockStepInput if err := decode(op.Data, &in); err != nil { return err } var s *model.BlockStep if op.Kind == OpBlockStepCreate { if op.ID <= 0 || st.BlockSteps[op.ID] != nil { return errf(KindConflict, "step ID %d is already in use", op.ID) } if in.Name == nil { return errf(KindBadRequest, "a blocking step needs a name") } pos := 0 for _, o := range st.BlockSteps { pos = max(pos, o.Position+1) } s = &model.BlockStep{ID: op.ID, Position: pos} } else { s = st.BlockSteps[op.ID] if s == nil { return errf(KindNotFound, "blocking step %d does not exist", op.ID) } if err := checkVersion(op, s.Version, "step "+s.Name); err != nil { return err } } if in.Name != nil { name := strings.ToLower(strings.TrimSpace(*in.Name)) if !blocklistNameRe.MatchString(name) || name == "off" || name == "normal" { return errf(KindBadRequest, "step name %q: use 1 to 40 lowercase letters, digits and hyphens, like strict (off and normal are built in)", *in.Name) } if id, found := st.StepByName(name); found && id != s.ID { return errf(KindConflict, "step %s already exists", name) } s.Name = name } if in.Position != nil { s.Position = *in.Position } if in.Comment != nil { s.Comment = *in.Comment } s.Touch(c.user(), c.now) st.BlockSteps[s.ID] = s return nil } func applyBlockStepDelete(st *model.State, op model.Op, c *applyCtx) error { if err := c.require(blockingAction()); err != nil { return err } s := st.BlockSteps[op.ID] if s == nil { return errf(KindNotFound, "blocking step %d does not exist", op.ID) } if err := checkVersion(op, s.Version, "step "+s.Name); err != nil { return err } if users := stepUsers(st, s.ID); len(users) > 0 { return errf(KindConflict, "step %s is used by %s; move them to another step first", s.Name, strings.Join(users, ", ")) } delete(st.BlockSteps, s.ID) return nil } // stepUsers lists the blocklists and client groups at a step. func stepUsers(st *model.State, id int64) []string { var out []string for _, b := range st.SortedBlocklists() { if b.StepID == id { out = append(out, "blocklist "+b.Name) } } for _, g := range st.SortedClientGroups() { if g.StepID == id { out = append(out, "client group "+g.Name) } } return out } func applyClientGroupPut(st *model.State, op model.Op, c *applyCtx) error { if err := c.require(blockingAction()); err != nil { return err } var in ClientGroupInput if err := decode(op.Data, &in); err != nil { return err } var g *model.ClientGroup if op.Kind == OpClientGroupCreate { if op.ID <= 0 || st.ClientGroups[op.ID] != nil { return errf(KindConflict, "client group ID %d is already in use", op.ID) } if in.Name == nil { return errf(KindBadRequest, "a client group needs a name") } g = &model.ClientGroup{ID: op.ID, Members: []string{}, StepID: model.StepNormal, ViewIDs: []int64{}} } else { g = st.ClientGroups[op.ID] if g == nil { return errf(KindNotFound, "client group %d does not exist", op.ID) } if err := checkVersion(op, g.Version, "client group "+g.Name); err != nil { return err } } if in.Name != nil { name := strings.ToLower(strings.TrimSpace(*in.Name)) if !blocklistNameRe.MatchString(name) { return errf(KindBadRequest, "client group name %q: use 1 to 40 lowercase letters, digits and hyphens, like kids", *in.Name) } if other := st.ClientGroupByName(name); other != nil && other.ID != g.ID { return errf(KindConflict, "client group %s already exists", name) } g.Name = name } if in.Members != nil { members := []string{} seen := map[string]bool{} for _, m := range *in.Members { if strings.TrimSpace(m) == "" { continue } n, err := NormalizeMember(st, m) if err != nil { return err } if !seen[n] { seen[n] = true members = append(members, n) } } g.Members = members } step, given, err := stepRef(st, in.Step, in.StepID) if err != nil { return err } if given { g.StepID = step } ids, err := viewIDs(st, in.Views, in.ViewIDs) if err != nil { return err } if ids != nil { g.ViewIDs = ids } if in.Comment != nil { g.Comment = *in.Comment } g.Touch(c.user(), c.now) st.ClientGroups[g.ID] = g return nil } func applyClientGroupDelete(st *model.State, op model.Op, c *applyCtx) error { if err := c.require(blockingAction()); err != nil { return err } g := st.ClientGroups[op.ID] if g == nil { return errf(KindNotFound, "client group %d does not exist", op.ID) } if err := checkVersion(op, g.Version, "client group "+g.Name); err != nil { return err } delete(st.ClientGroups, g.ID) return nil } // ---- expansion func (x *expander) blockStepCreate(req Request) ([]model.Op, error) { return x.simpleCreate(req, OpBlockStepCreate, func(data json.RawMessage) (string, error) { var in BlockStepInput if err := decode(data, &in); err != nil { return "", err } if in.Name == nil { return "", errf(KindBadRequest, "a blocking step needs a name") } return "add blocking step " + strings.ToLower(*in.Name), nil }) } func (x *expander) clientGroupCreate(req Request) ([]model.Op, error) { return x.simpleCreate(req, OpClientGroupCreate, func(data json.RawMessage) (string, error) { var in ClientGroupInput if err := decode(data, &in); err != nil { return "", err } if in.Name == nil { return "", errf(KindBadRequest, "a client group needs a name") } return "add client group " + strings.ToLower(*in.Name), nil }) }