Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bamboohr: get employees #2

Merged
merged 2 commits into from
Apr 14, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
get employees
  • Loading branch information
wasaga committed Apr 14, 2022
commit a88220f299579ff75b3d1d72cc15116f76721a9a
28 changes: 28 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.2.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
exclude: "(docs/.*|integration/tpl/files/.*)"
- id: check-yaml
exclude: "examples/.*"
- id: check-added-large-files
- repo: local
hooks:
- id: go-mod-tidy
name: go-mod-tidy
language: system
entry: make
args: ["tidy"]
types: ["go"]
pass_filenames: false
- repo: local
hooks:
- id: lint
name: lint
language: system
entry: make
args: ["lint"]
types: ["go"]
pass_filenames: false
7 changes: 7 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.PHONY: tidy
tidy:
cd bamboohr && make tidy

.PHONY: lint
lint:
cd bamboohr && make lint
1 change: 1 addition & 0 deletions bamboohr/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
bin/bamboohr
58 changes: 58 additions & 0 deletions bamboohr/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
PREFIX?=$(shell pwd)
NAME := bamboohr
PKG := github.com/pomerium/datasource/bamboohr


BUILDDIR := ${PREFIX}/dist
BINDIR := ${PREFIX}/bin

GITCOMMIT := $(shell git rev-parse --short HEAD)
BUILDMETA:=
GITUNTRACKEDCHANGES := $(shell git status --porcelain --untracked-files=no)
ifneq ($(GITUNTRACKEDCHANGES),)
BUILDMETA := dirty
endif

CTIMEVAR=-X $(PKG)/version.GitCommit=$(GITCOMMIT) \
-X $(PKG)/internal/.BuildMeta=$(BUILDMETA) \
-X $(PKG)/internal/version.ProjectName=$(NAME) \
-X $(PKG)/internal/version.ProjectURL=$(PKG)

GO ?= "go"
GO_LDFLAGS=-ldflags "-s -w $(CTIMEVAR)"
GOOSARCHES = linux/amd64 darwin/amd64 windows/amd64

.PHONY: all
all: clean lint test build

.PHONY: test
test: ## test everything
go test ./...

.PHONY: lint
lint: ## run go mod tidy
go run github.com/golangci/golangci-lint/cmd/golangci-lint --timeout=120s run ./...

.PHONY: tidy
tidy: ## run go mod tidy
go mod tidy -compat=1.17

.PHONY: clean
clean: ## Cleanup any build binaries or packages.
@echo "==> $@"
$(RM) -r $(BINDIR)
$(RM) -r $(BUILDDIR)

.PHONY: build
build: ## Build everything.
@echo "==> $@"
@CGO_ENABLED=0 GO111MODULE=on go build -tags "$(BUILDTAGS)" ${GO_LDFLAGS} -o $(BINDIR)/$(NAME) ./cmd/

.PHONY: snapshot
snapshot: ## Create release snapshot
APPARITOR_GITHUB_TOKEN=foo goreleaser release --snapshot --rm-dist


.PHONY: help
help:
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
37 changes: 37 additions & 0 deletions bamboohr/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Overview

This is a reference Pomerium data provider that returns employee data from BambooHR.
It has to be configured to connect to your BambooHR account, and would provide Pomerium
with subset of information that may be used to configure access policies.

# Use Cases

## Limit Access By Employee Properties

There may be business rules that require that employees access to certain resources
may be limited:

- `status` to deactivate access if an employee is terminated
- `department`, `division`, `jobTitle`
- `country`, `state`, `city`, `location`, `nationality`

# Configuration

For security reasons, all requests to BambooHR are runtime parameters
and may not be customized.

## Restricted fields

Fields `national_id, sin, ssn, nin` are restricted and may not be exported for security reasons.

# API

A `Token` HTTP header must be provided in each request, matching `api-token` runtime parameter.

## `GET /employees`

Returns list of current employees, with fields provided, as JSON array.

# `GET /employees/out`

Returns list of employees that are currently out.
Empty file added bamboohr/bin/.gitkeep
Empty file.
40 changes: 40 additions & 0 deletions bamboohr/cmd/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package main

import (
"context"
"os"
"os/signal"
"syscall"
"time"

"github.com/pomerium/datasource/bamboohr/internal"
"github.com/rs/zerolog"
)

func main() {
log := makeLogger()
log.Info().Str("version", internal.FullVersion()).Msg("starting")
cmd := serveCommand(log)
if err := cmd.ExecuteContext(signalContext(log)); err != nil {
log.Fatal().Err(err).Msg("exit")
}
}

func makeLogger() zerolog.Logger {
return zerolog.New(zerolog.NewConsoleWriter())
}

func signalContext(log zerolog.Logger) context.Context {
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
ctx, cancel := context.WithCancel(context.Background())
go func() {
sig := <-sigs
log.Error().Str("signal", sig.String()).Msg("caught signal, quitting...")
cancel()
time.Sleep(time.Second * 2)
log.Error().Msg("did not shut down gracefully, exit")
os.Exit(1)
}()
return ctx
}
152 changes: 152 additions & 0 deletions bamboohr/cmd/serve.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package main

import (
"fmt"
"net"
"net/http"
"os"
"strings"

"github.com/go-playground/validator/v10"
"github.com/rs/zerolog"
"github.com/spf13/cobra"

"github.com/pomerium/datasource/bamboohr/internal"
)

type serveCmd struct {
BambooAPIKey string `validate:"required"`
BambooSubdomain string `validate:"required"`
BambooEmployeeFields []string `validate:"required"`
BambooEmployeeFieldRemap []string `validate:"required"`
Address string `validate:"required,tcp_addr"`
AccessToken string `validate:"required"`
cobra.Command `validate:"-"`
zerolog.Logger `validate:"-"`
}

var (
// DefaultBambooEmployeeFields
DefaultBambooEmployeeFields = []string{"email"}
// RestrictedBambooEmployeeFields prohibit certain sensitive fields from being exposed
RestrictedBambooEmployeeFields = []string{"national_id", "sin", "ssn", "nin", "id"}
// DefaultBambooEmployeeFieldsRemap
DefaultBambooEmployeeFieldsRemap = []string{"email=id"}
)

func serveCommand(log zerolog.Logger) *cobra.Command {
cmd := &serveCmd{
Command: cobra.Command{
Use: "serve",
Short: "run BambooHR connector",
},
Logger: log,
}
cmd.RunE = cmd.exec

cmd.setupFlags()
return &cmd.Command
}

func (cmd *serveCmd) setupFlags() {
flags := cmd.Flags()
flags.StringVar(&cmd.BambooSubdomain, "bamboohr-subdomain", "", "subdomain")
flags.StringSliceVar(&cmd.BambooEmployeeFields, "bamboohr-employee-fields", DefaultBambooEmployeeFields, "employee fields")
flags.StringSliceVar(&cmd.BambooEmployeeFieldRemap, "bamboohr-employee-field-remap", DefaultBambooEmployeeFieldsRemap, "list of key=newKey to rename keys")
flags.StringVar(&cmd.BambooAPIKey, "bamboohr-api-key", "", "api key, see https://documentation.bamboohr.com/docs#section-authentication")
flags.StringVar(&cmd.AccessToken, "access-token", "", "all requests must contain Token header matching this token")
}

func (cmd *serveCmd) exec(c *cobra.Command, _ []string) error {
if err := validator.New().Struct(cmd); err != nil {
return err
}

if err := checkFieldsNotInList(cmd.BambooEmployeeFields, RestrictedBambooEmployeeFields); err != nil {
return fmt.Errorf("field check: %w", err)
}
srv, err := cmd.newServer()
if err != nil {
return fmt.Errorf("prep server: %w", err)
}

l, err := net.Listen("tcp", cmd.Address)
if err != nil {
return fmt.Errorf("listen %s: %w", cmd.Address, err)
}

log := zerolog.New(os.Stdout)
log.Info().Msg("ready")
return http.Serve(l, srv)
}

func (cmd *serveCmd) newServer() (http.Handler, error) {
auth := internal.Auth{
APIKey: cmd.BambooAPIKey,
Subdomain: cmd.BambooSubdomain,
}

remap, err := keyRemap(cmd.BambooEmployeeFieldRemap)
if err != nil {
return nil, fmt.Errorf("field remap: %w", err)
}

if err := checkFieldsInList(cmd.BambooEmployeeFields, remap); err != nil {
return nil, fmt.Errorf("remap fields ")
}
emplReq := internal.EmployeeRequest{
Auth: auth,
CurrentOnly: true,
Fields: cmd.BambooEmployeeFields,
Remap: remap,
}
return internal.NewServer(emplReq, cmd.AccessToken, cmd.Logger), nil
}

func keyRemap(m []string) (map[string]string, error) {
dst := make(map[string]string, len(m))
for _, r := range m {
pair := strings.Split(r, "=")
if len(pair) != 2 {
return nil, fmt.Errorf("%s: expect key=newKey format", r)
}
key, newKey := pair[0], pair[1]
if key == "" || newKey == "" {
return nil, fmt.Errorf("%s: expect key=newKey format", r)
}
if _, there := dst[newKey]; there {
return nil, fmt.Errorf("%s: key %s was already used", r, newKey)
}
dst[key] = newKey
}
return dst, nil
}

func checkFieldsInList(fields []string, remap map[string]string) error {
fm := make(map[string]struct{}, len(fields))
for _, fld := range fields {
fm[fld] = struct{}{}
}

for fld := range remap {
if _, there := fm[fld]; !there {
return fmt.Errorf("field %s in field remap must be present in the field list", fld)
}
}
return nil
}

func checkFieldsNotInList(fields []string, denyList []string) error {
denySet := make(map[string]struct{}, len(denyList))
for _, key := range denyList {
denySet[key] = struct{}{}
}

for _, key := range fields {
if _, there := denySet[key]; there {
return fmt.Errorf("%s is restricted", key)
}
}

return nil
}
Loading